Coverage for tests/test_normalFormExpression.py: 25%
60 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-19 10:53 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-19 10:53 +0000
1# This file is part of daf_butler.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <https://www.gnu.org/licenses/>.
29import random
30import unittest
32import astropy.time
33from lsst.daf.butler.registry.queries.expressions.normalForm import (
34 NormalForm,
35 NormalFormExpression,
36 TransformationVisitor,
37)
38from lsst.daf.butler.registry.queries.expressions.parser import Node, ParserYacc, TreeVisitor
41class BooleanEvaluationTreeVisitor(TreeVisitor[bool]):
42 """A `TreeVisitor` implementation that evaluates boolean expressions given
43 boolean values for identifiers.
44 """
46 def __init__(self, **kwargs: bool):
47 self.values = kwargs
49 def init(self, form: NormalForm) -> None:
50 self.form = form
52 def visitNumericLiteral(self, value: str, node: Node) -> bool:
53 # Docstring inherited from TreeVisitor.visitNumericLiteral
54 raise NotImplementedError()
56 def visitStringLiteral(self, value: str, node: Node) -> bool:
57 # Docstring inherited from TreeVisitor.visitStringLiteral
58 raise NotImplementedError()
60 def visitTimeLiteral(self, value: astropy.time.Time, node: Node) -> bool:
61 # Docstring inherited from TreeVisitor.visitTimeLiteral
62 raise NotImplementedError()
64 def visitIdentifier(self, name: str, node: Node) -> bool:
65 # Docstring inherited from TreeVisitor.visitIdentifier
66 return self.values[name]
68 def visitUnaryOp(
69 self,
70 operator: str,
71 operand: bool,
72 node: Node,
73 ) -> bool:
74 # Docstring inherited from TreeVisitor.visitUnaryOp
75 if operator == "NOT":
76 return not operand
77 raise NotImplementedError()
79 def visitBinaryOp(
80 self,
81 operator: str,
82 lhs: bool,
83 rhs: bool,
84 node: Node,
85 ) -> bool:
86 # Docstring inherited from TreeVisitor.visitBinaryOp
87 if operator == "AND":
88 return lhs and rhs
89 if operator == "OR":
90 return lhs or rhs
91 raise NotImplementedError()
93 def visitIsIn(
94 self,
95 lhs: bool,
96 values: list[bool],
97 not_in: bool,
98 node: Node,
99 ) -> bool:
100 # Docstring inherited from TreeVisitor.visitIsIn
101 raise NotImplementedError()
103 def visitParens(self, expression: bool, node: Node) -> bool:
104 # Docstring inherited from TreeVisitor.visitParens
105 return expression
107 def visitRangeLiteral(self, start: int, stop: int, stride: int | None, node: Node) -> bool:
108 # Docstring inherited from TreeVisitor.visitRangeLiteral
109 raise NotImplementedError()
112class NormalFormExpressionTestCase(unittest.TestCase):
113 """Tests for `NormalFormExpression` and its helper classes."""
115 def check(
116 self,
117 expression: str,
118 conjunctive: str | bool = False,
119 disjunctive: str | bool = False,
120 ) -> None:
121 """Compare the results of transforming an expression to normal form
122 against given expected values.
124 Parameters
125 ----------
126 expression : `str`
127 Expression to parse and transform.
128 conjunctive : `str` or `bool`
129 The expected string form of the expression after transformation
130 to conjunctive normal form, or `True` to indicate that
131 ``expression`` should already be in that form. `False` to skip
132 tests on this form.
133 disjunctive : `str` or `bool`
134 Same as ``conjunctive``, but for disjunctive normal form.
135 """
136 with self.subTest(expression):
137 parser = ParserYacc()
138 originalTree = parser.parse(expression)
139 wrapper = originalTree.visit(TransformationVisitor())
140 trees = {form: NormalFormExpression.fromTree(originalTree, form).toTree() for form in NormalForm}
141 if conjunctive is True: # expected to be conjunctive already
142 self.assertTrue(wrapper.satisfies(NormalForm.CONJUNCTIVE), msg=str(wrapper))
143 elif conjunctive: # str to expect after normalization to conjunctive
144 self.assertEqual(str(trees[NormalForm.CONJUNCTIVE]), conjunctive)
145 if disjunctive is True: # expected to be disjunctive already
146 self.assertTrue(wrapper.satisfies(NormalForm.DISJUNCTIVE), msg=str(wrapper))
147 elif disjunctive: # str to expect after normalization to disjunctive
148 self.assertEqual(str(trees[NormalForm.DISJUNCTIVE]), disjunctive)
149 for _ in range(10):
150 values = {k: bool(random.randint(0, 1)) for k in "ABCDEF"}
151 visitor = BooleanEvaluationTreeVisitor(**values)
152 expected = originalTree.visit(visitor)
153 for name, tree in trees.items():
154 self.assertEqual(expected, tree.visit(visitor), msg=name)
156 def testNormalize(self):
157 self.check("A AND B", conjunctive=True, disjunctive=True)
158 self.check("A OR B", conjunctive=True, disjunctive=True)
159 self.check("NOT (A OR B)", conjunctive="NOT A AND NOT B", disjunctive="NOT A AND NOT B")
160 self.check("NOT (A AND B)", conjunctive="NOT A OR NOT B", disjunctive="NOT A OR NOT B")
161 self.check(
162 "NOT (A AND (NOT B OR C))",
163 conjunctive="(NOT A OR B) AND (NOT A OR NOT C)",
164 disjunctive=True,
165 )
166 self.check(
167 "NOT (A OR (B AND NOT C))",
168 conjunctive=True,
169 disjunctive="(NOT A AND NOT B) OR (NOT A AND C)",
170 )
171 self.check(
172 "A AND (B OR C OR D)",
173 conjunctive=True,
174 disjunctive="(A AND B) OR (A AND C) OR (A AND D)",
175 )
176 self.check(
177 "A OR (B AND C AND D)",
178 disjunctive=True,
179 conjunctive="(A OR B) AND (A OR C) AND (A OR D)",
180 )
181 self.check(
182 "A AND (B OR NOT C) AND (NOT D OR E OR F)",
183 conjunctive=True,
184 disjunctive=(
185 "(A AND B AND NOT D) OR (A AND B AND E) OR (A AND B AND F) "
186 "OR (A AND NOT C AND NOT D) OR (A AND NOT C AND E) OR (A AND NOT C AND F)"
187 ),
188 )
189 self.check(
190 "A OR (B AND NOT C) OR (NOT D AND E AND F)",
191 conjunctive=(
192 "(A OR B OR NOT D) AND (A OR B OR E) AND (A OR B OR F) "
193 "AND (A OR NOT C OR NOT D) AND (A OR NOT C OR E) AND (A OR NOT C OR F)"
194 ),
195 disjunctive=True,
196 )
197 self.check(
198 "(A OR (B AND NOT (C OR (NOT D AND E)))) OR F",
199 conjunctive="(A OR B OR F) AND (A OR NOT C OR F) AND (A OR D OR NOT E OR F)",
200 disjunctive="A OR (B AND NOT C AND D) OR (B AND NOT C AND NOT E) OR F",
201 )
204if __name__ == "__main__":
205 unittest.main()