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