Coverage for tests/test_normalFormExpression.py: 25%

60 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-12 10:56 -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/>. 

21 

22 

23import random 

24import unittest 

25 

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 

33 

34 

35class BooleanEvaluationTreeVisitor(TreeVisitor[bool]): 

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

37 boolean values for identifiers. 

38 """ 

39 

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

41 self.values = kwargs 

42 

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

44 self.form = form 

45 

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

47 # Docstring inherited from TreeVisitor.visitNumericLiteral 

48 raise NotImplementedError() 

49 

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

51 # Docstring inherited from TreeVisitor.visitStringLiteral 

52 raise NotImplementedError() 

53 

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

55 # Docstring inherited from TreeVisitor.visitTimeLiteral 

56 raise NotImplementedError() 

57 

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

59 # Docstring inherited from TreeVisitor.visitIdentifier 

60 return self.values[name] 

61 

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

72 

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

86 

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

96 

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

98 # Docstring inherited from TreeVisitor.visitParens 

99 return expression 

100 

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

102 # Docstring inherited from TreeVisitor.visitRangeLiteral 

103 raise NotImplementedError() 

104 

105 

106class NormalFormExpressionTestCase(unittest.TestCase): 

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

108 

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. 

117 

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

149 

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 ) 

196 

197 

198if __name__ == "__main__": 

199 unittest.main()