Coverage for tests/test_normalFormExpression.py: 24%
70 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-24 23:50 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-24 23:50 -0700
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 program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
23import random
24import unittest
25from typing import List, Optional, Union
27import astropy.time
28from lsst.daf.butler.registry.queries.expressions import (
29 Node,
30 NormalForm,
31 NormalFormExpression,
32 ParserYacc,
33 TreeVisitor,
34)
35from lsst.daf.butler.registry.queries.expressions.normalForm import TransformationVisitor
38class BooleanEvaluationTreeVisitor(TreeVisitor[bool]):
39 """A `TreeVisitor` implementation that evaluates boolean expressions given
40 boolean values for identifiers.
41 """
43 def __init__(self, **kwargs: bool):
44 self.values = kwargs
46 def init(self, form: NormalForm) -> None:
47 self.form = form
49 def visitNumericLiteral(self, value: str, node: Node) -> bool:
50 # Docstring inherited from TreeVisitor.visitNumericLiteral
51 raise NotImplementedError()
53 def visitStringLiteral(self, value: str, node: Node) -> bool:
54 # Docstring inherited from TreeVisitor.visitStringLiteral
55 raise NotImplementedError()
57 def visitTimeLiteral(self, value: astropy.time.Time, node: Node) -> bool:
58 # Docstring inherited from TreeVisitor.visitTimeLiteral
59 raise NotImplementedError()
61 def visitIdentifier(self, name: str, node: Node) -> bool:
62 # Docstring inherited from TreeVisitor.visitIdentifier
63 return self.values[name]
65 def visitUnaryOp(
66 self,
67 operator: str,
68 operand: bool,
69 node: Node,
70 ) -> bool:
71 # Docstring inherited from TreeVisitor.visitUnaryOp
72 if operator == "NOT":
73 return not operand
74 raise NotImplementedError()
76 def visitBinaryOp(
77 self,
78 operator: str,
79 lhs: bool,
80 rhs: bool,
81 node: Node,
82 ) -> bool:
83 # Docstring inherited from TreeVisitor.visitBinaryOp
84 if operator == "AND":
85 return lhs and rhs
86 if operator == "OR":
87 return lhs or rhs
88 raise NotImplementedError()
90 def visitIsIn(
91 self,
92 lhs: bool,
93 values: List[bool],
94 not_in: bool,
95 node: Node,
96 ) -> bool:
97 # Docstring inherited from TreeVisitor.visitIsIn
98 raise NotImplementedError()
100 def visitParens(self, expression: bool, node: Node) -> bool:
101 # Docstring inherited from TreeVisitor.visitParens
102 return expression
104 def visitRangeLiteral(self, start: int, stop: int, stride: Optional[int], node: Node) -> bool:
105 # Docstring inherited from TreeVisitor.visitRangeLiteral
106 raise NotImplementedError()
109class NormalFormExpressionTestCase(unittest.TestCase):
110 """Tests for `NormalFormExpression` and its helper classes."""
112 def check(
113 self,
114 expression: str,
115 conjunctive: Union[str, bool] = False,
116 disjunctive: Union[str, bool] = False,
117 ) -> None:
118 """Compare the results of transforming an expression to normal form
119 against given expected values.
121 Parameters
122 ----------
123 expression : `str`
124 Expression to parse and transform.
125 conjunctive : `str` or `bool`
126 The expected string form of the expression after transformation
127 to conjunctive normal form, or `True` to indicate that
128 ``expression`` should already be in that form. `False` to skip
129 tests on this form.
130 disjunctive : `str` or `bool`
131 Same as ``conjunctive``, but for disjunctive normal form.
132 """
133 with self.subTest(expression):
134 parser = ParserYacc()
135 originalTree = parser.parse(expression)
136 wrapper = originalTree.visit(TransformationVisitor())
137 trees = {form: NormalFormExpression.fromTree(originalTree, form).toTree() for form in NormalForm}
138 if conjunctive is True: # expected to be conjunctive already
139 self.assertTrue(wrapper.satisfies(NormalForm.CONJUNCTIVE), msg=str(wrapper))
140 elif conjunctive: # str to expect after normalization to conjunctive
141 self.assertEqual(str(trees[NormalForm.CONJUNCTIVE]), conjunctive)
142 if disjunctive is True: # expected to be disjunctive already
143 self.assertTrue(wrapper.satisfies(NormalForm.DISJUNCTIVE), msg=str(wrapper))
144 elif disjunctive: # str to expect after normalization to disjunctive
145 self.assertEqual(str(trees[NormalForm.DISJUNCTIVE]), disjunctive)
146 for i in range(10):
147 values = {k: bool(random.randint(0, 1)) for k in "ABCDEF"}
148 visitor = BooleanEvaluationTreeVisitor(**values)
149 expected = originalTree.visit(visitor)
150 for name, tree in trees.items():
151 self.assertEqual(expected, tree.visit(visitor), msg=name)
153 def testNormalize(self):
154 self.check("A AND B", conjunctive=True, disjunctive=True)
155 self.check("A OR B", conjunctive=True, disjunctive=True)
156 self.check("NOT (A OR B)", conjunctive="NOT A AND NOT B", disjunctive="NOT A AND NOT B")
157 self.check("NOT (A AND B)", conjunctive="NOT A OR NOT B", disjunctive="NOT A OR NOT B")
158 self.check(
159 "NOT (A AND (NOT B OR C))",
160 conjunctive="(NOT A OR B) AND (NOT A OR NOT C)",
161 disjunctive=True,
162 )
163 self.check(
164 "NOT (A OR (B AND NOT C))",
165 conjunctive=True,
166 disjunctive="(NOT A AND NOT B) OR (NOT A AND C)",
167 )
168 self.check(
169 "A AND (B OR C OR D)",
170 conjunctive=True,
171 disjunctive="(A AND B) OR (A AND C) OR (A AND D)",
172 )
173 self.check(
174 "A OR (B AND C AND D)",
175 disjunctive=True,
176 conjunctive="(A OR B) AND (A OR C) AND (A OR D)",
177 )
178 self.check(
179 "A AND (B OR NOT C) AND (NOT D OR E OR F)",
180 conjunctive=True,
181 disjunctive=(
182 "(A AND B AND NOT D) OR (A AND B AND E) OR (A AND B AND F) "
183 "OR (A AND NOT C AND NOT D) OR (A AND NOT C AND E) OR (A AND NOT C AND F)"
184 ),
185 )
186 self.check(
187 "A OR (B AND NOT C) OR (NOT D AND E AND F)",
188 conjunctive=(
189 "(A OR B OR NOT D) AND (A OR B OR E) AND (A OR B OR F) "
190 "AND (A OR NOT C OR NOT D) AND (A OR NOT C OR E) AND (A OR NOT C OR F)"
191 ),
192 disjunctive=True,
193 )
194 self.check(
195 "(A OR (B AND NOT (C OR (NOT D AND E)))) OR F",
196 conjunctive=("(A OR B OR F) AND (A OR NOT C OR F) AND (A OR D OR NOT E OR F)"),
197 disjunctive="A OR (B AND NOT C AND D) OR (B AND NOT C AND NOT E) OR F",
198 )
201if __name__ == "__main__": 201 ↛ 202line 201 didn't jump to line 202, because the condition on line 201 was never true
202 unittest.main()