Coverage for tests/test_normalFormExpression.py: 24%
70 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-22 03:05 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-22 03:05 -0800
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.normalForm import (
29 NormalForm,
30 NormalFormExpression,
31 TransformationVisitor,
32)
33from lsst.daf.butler.registry.queries.expressions.parser import Node, ParserYacc, TreeVisitor
36class BooleanEvaluationTreeVisitor(TreeVisitor[bool]):
37 """A `TreeVisitor` implementation that evaluates boolean expressions given
38 boolean values for identifiers.
39 """
41 def __init__(self, **kwargs: bool):
42 self.values = kwargs
44 def init(self, form: NormalForm) -> None:
45 self.form = form
47 def visitNumericLiteral(self, value: str, node: Node) -> bool:
48 # Docstring inherited from TreeVisitor.visitNumericLiteral
49 raise NotImplementedError()
51 def visitStringLiteral(self, value: str, node: Node) -> bool:
52 # Docstring inherited from TreeVisitor.visitStringLiteral
53 raise NotImplementedError()
55 def visitTimeLiteral(self, value: astropy.time.Time, node: Node) -> bool:
56 # Docstring inherited from TreeVisitor.visitTimeLiteral
57 raise NotImplementedError()
59 def visitIdentifier(self, name: str, node: Node) -> bool:
60 # Docstring inherited from TreeVisitor.visitIdentifier
61 return self.values[name]
63 def visitUnaryOp(
64 self,
65 operator: str,
66 operand: bool,
67 node: Node,
68 ) -> bool:
69 # Docstring inherited from TreeVisitor.visitUnaryOp
70 if operator == "NOT":
71 return not operand
72 raise NotImplementedError()
74 def visitBinaryOp(
75 self,
76 operator: str,
77 lhs: bool,
78 rhs: bool,
79 node: Node,
80 ) -> bool:
81 # Docstring inherited from TreeVisitor.visitBinaryOp
82 if operator == "AND":
83 return lhs and rhs
84 if operator == "OR":
85 return lhs or rhs
86 raise NotImplementedError()
88 def visitIsIn(
89 self,
90 lhs: bool,
91 values: List[bool],
92 not_in: bool,
93 node: Node,
94 ) -> bool:
95 # Docstring inherited from TreeVisitor.visitIsIn
96 raise NotImplementedError()
98 def visitParens(self, expression: bool, node: Node) -> bool:
99 # Docstring inherited from TreeVisitor.visitParens
100 return expression
102 def visitRangeLiteral(self, start: int, stop: int, stride: Optional[int], node: Node) -> bool:
103 # Docstring inherited from TreeVisitor.visitRangeLiteral
104 raise NotImplementedError()
107class NormalFormExpressionTestCase(unittest.TestCase):
108 """Tests for `NormalFormExpression` and its helper classes."""
110 def check(
111 self,
112 expression: str,
113 conjunctive: Union[str, bool] = False,
114 disjunctive: Union[str, bool] = False,
115 ) -> None:
116 """Compare the results of transforming an expression to normal form
117 against given expected values.
119 Parameters
120 ----------
121 expression : `str`
122 Expression to parse and transform.
123 conjunctive : `str` or `bool`
124 The expected string form of the expression after transformation
125 to conjunctive normal form, or `True` to indicate that
126 ``expression`` should already be in that form. `False` to skip
127 tests on this form.
128 disjunctive : `str` or `bool`
129 Same as ``conjunctive``, but for disjunctive normal form.
130 """
131 with self.subTest(expression):
132 parser = ParserYacc()
133 originalTree = parser.parse(expression)
134 wrapper = originalTree.visit(TransformationVisitor())
135 trees = {form: NormalFormExpression.fromTree(originalTree, form).toTree() for form in NormalForm}
136 if conjunctive is True: # expected to be conjunctive already
137 self.assertTrue(wrapper.satisfies(NormalForm.CONJUNCTIVE), msg=str(wrapper))
138 elif conjunctive: # str to expect after normalization to conjunctive
139 self.assertEqual(str(trees[NormalForm.CONJUNCTIVE]), conjunctive)
140 if disjunctive is True: # expected to be disjunctive already
141 self.assertTrue(wrapper.satisfies(NormalForm.DISJUNCTIVE), msg=str(wrapper))
142 elif disjunctive: # str to expect after normalization to disjunctive
143 self.assertEqual(str(trees[NormalForm.DISJUNCTIVE]), disjunctive)
144 for i in range(10):
145 values = {k: bool(random.randint(0, 1)) for k in "ABCDEF"}
146 visitor = BooleanEvaluationTreeVisitor(**values)
147 expected = originalTree.visit(visitor)
148 for name, tree in trees.items():
149 self.assertEqual(expected, tree.visit(visitor), msg=name)
151 def testNormalize(self):
152 self.check("A AND B", conjunctive=True, disjunctive=True)
153 self.check("A OR B", conjunctive=True, disjunctive=True)
154 self.check("NOT (A OR B)", conjunctive="NOT A AND NOT B", disjunctive="NOT A AND NOT B")
155 self.check("NOT (A AND B)", conjunctive="NOT A OR NOT B", disjunctive="NOT A OR NOT B")
156 self.check(
157 "NOT (A AND (NOT B OR C))",
158 conjunctive="(NOT A OR B) AND (NOT A OR NOT C)",
159 disjunctive=True,
160 )
161 self.check(
162 "NOT (A OR (B AND NOT C))",
163 conjunctive=True,
164 disjunctive="(NOT A AND NOT B) OR (NOT A AND C)",
165 )
166 self.check(
167 "A AND (B OR C OR D)",
168 conjunctive=True,
169 disjunctive="(A AND B) OR (A AND C) OR (A AND D)",
170 )
171 self.check(
172 "A OR (B AND C AND D)",
173 disjunctive=True,
174 conjunctive="(A OR B) AND (A OR C) AND (A OR D)",
175 )
176 self.check(
177 "A AND (B OR NOT C) AND (NOT D OR E OR F)",
178 conjunctive=True,
179 disjunctive=(
180 "(A AND B AND NOT D) OR (A AND B AND E) OR (A AND B AND F) "
181 "OR (A AND NOT C AND NOT D) OR (A AND NOT C AND E) OR (A AND NOT C AND F)"
182 ),
183 )
184 self.check(
185 "A OR (B AND NOT C) OR (NOT D AND E AND F)",
186 conjunctive=(
187 "(A OR B OR NOT D) AND (A OR B OR E) AND (A OR B OR F) "
188 "AND (A OR NOT C OR NOT D) AND (A OR NOT C OR E) AND (A OR NOT C OR F)"
189 ),
190 disjunctive=True,
191 )
192 self.check(
193 "(A OR (B AND NOT (C OR (NOT D AND E)))) OR F",
194 conjunctive="(A OR B OR F) AND (A OR NOT C OR F) AND (A OR D OR NOT E OR F)",
195 disjunctive="A OR (B AND NOT C AND D) OR (B AND NOT C AND NOT E) OR F",
196 )
199if __name__ == "__main__": 199 ↛ 200line 199 didn't jump to line 200, because the condition on line 199 was never true
200 unittest.main()