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