Coverage for tests/test_normalFormExpression.py: 24%

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

70 statements  

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 

24from typing import List, Optional, Union 

25import unittest 

26 

27import astropy.time 

28 

29from lsst.daf.butler.registry.queries.expressions import ( 

30 Node, 

31 NormalForm, 

32 NormalFormExpression, 

33 ParserYacc, 

34 TreeVisitor, 

35) 

36 

37from lsst.daf.butler.registry.queries.expressions.normalForm import TransformationVisitor 

38 

39 

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 

46 

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

48 self.form = form 

49 

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

51 # Docstring inherited from TreeVisitor.visitNumericLiteral 

52 raise NotImplementedError() 

53 

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

55 # Docstring inherited from TreeVisitor.visitStringLiteral 

56 raise NotImplementedError() 

57 

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

59 # Docstring inherited from TreeVisitor.visitTimeLiteral 

60 raise NotImplementedError() 

61 

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

63 # Docstring inherited from TreeVisitor.visitIdentifier 

64 return self.values[name] 

65 

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

76 

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

90 

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

100 

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

102 # Docstring inherited from TreeVisitor.visitParens 

103 return expression 

104 

105 def visitRangeLiteral(self, start: int, stop: int, stride: Optional[int], node: Node) -> bool: 

106 # Docstring inherited from TreeVisitor.visitRangeLiteral 

107 raise NotImplementedError() 

108 

109 

110class NormalFormExpressionTestCase(unittest.TestCase): 

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

112 """ 

113 

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. 

122 

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) 

154 

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 ) 

201 

202 

203if __name__ == "__main__": 203 ↛ 204line 203 didn't jump to line 204, because the condition on line 203 was never true

204 unittest.main()