Coverage for tests/test_normalFormExpression.py: 25%

60 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-15 02:03 -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 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/>. 

27 

28 

29import random 

30import unittest 

31 

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 

39 

40 

41class BooleanEvaluationTreeVisitor(TreeVisitor[bool]): 

42 """A `TreeVisitor` implementation that evaluates boolean expressions given 

43 boolean values for identifiers. 

44 """ 

45 

46 def __init__(self, **kwargs: bool): 

47 self.values = kwargs 

48 

49 def init(self, form: NormalForm) -> None: 

50 self.form = form 

51 

52 def visitNumericLiteral(self, value: str, node: Node) -> bool: 

53 # Docstring inherited from TreeVisitor.visitNumericLiteral 

54 raise NotImplementedError() 

55 

56 def visitStringLiteral(self, value: str, node: Node) -> bool: 

57 # Docstring inherited from TreeVisitor.visitStringLiteral 

58 raise NotImplementedError() 

59 

60 def visitTimeLiteral(self, value: astropy.time.Time, node: Node) -> bool: 

61 # Docstring inherited from TreeVisitor.visitTimeLiteral 

62 raise NotImplementedError() 

63 

64 def visitIdentifier(self, name: str, node: Node) -> bool: 

65 # Docstring inherited from TreeVisitor.visitIdentifier 

66 return self.values[name] 

67 

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() 

78 

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() 

92 

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() 

102 

103 def visitParens(self, expression: bool, node: Node) -> bool: 

104 # Docstring inherited from TreeVisitor.visitParens 

105 return expression 

106 

107 def visitRangeLiteral(self, start: int, stop: int, stride: int | None, node: Node) -> bool: 

108 # Docstring inherited from TreeVisitor.visitRangeLiteral 

109 raise NotImplementedError() 

110 

111 

112class NormalFormExpressionTestCase(unittest.TestCase): 

113 """Tests for `NormalFormExpression` and its helper classes.""" 

114 

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. 

123 

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) 

155 

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 ) 

202 

203 

204if __name__ == "__main__": 

205 unittest.main()