Coverage for tests/test_normalFormExpression.py: 27%

70 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-16 02:09 -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 

25from typing import List, Optional, Union 

26 

27import astropy.time 

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

29 Node, 

30 NormalForm, 

31 NormalFormExpression, 

32 ParserYacc, 

33 TreeVisitor, 

34) 

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

36 

37 

38class BooleanEvaluationTreeVisitor(TreeVisitor[bool]): 

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

40 boolean values for identifiers. 

41 """ 

42 

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

44 self.values = kwargs 

45 

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

47 self.form = form 

48 

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

50 # Docstring inherited from TreeVisitor.visitNumericLiteral 

51 raise NotImplementedError() 

52 

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

54 # Docstring inherited from TreeVisitor.visitStringLiteral 

55 raise NotImplementedError() 

56 

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

58 # Docstring inherited from TreeVisitor.visitTimeLiteral 

59 raise NotImplementedError() 

60 

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

62 # Docstring inherited from TreeVisitor.visitIdentifier 

63 return self.values[name] 

64 

65 def visitUnaryOp( 

66 self, 

67 operator: str, 

68 operand: bool, 

69 node: Node, 

70 ) -> bool: 

71 # Docstring inherited from TreeVisitor.visitUnaryOp 

72 if operator == "NOT": 

73 return not operand 

74 raise NotImplementedError() 

75 

76 def visitBinaryOp( 

77 self, 

78 operator: str, 

79 lhs: bool, 

80 rhs: bool, 

81 node: Node, 

82 ) -> bool: 

83 # Docstring inherited from TreeVisitor.visitBinaryOp 

84 if operator == "AND": 

85 return lhs and rhs 

86 if operator == "OR": 

87 return lhs or rhs 

88 raise NotImplementedError() 

89 

90 def visitIsIn( 

91 self, 

92 lhs: bool, 

93 values: List[bool], 

94 not_in: bool, 

95 node: Node, 

96 ) -> bool: 

97 # Docstring inherited from TreeVisitor.visitIsIn 

98 raise NotImplementedError() 

99 

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

101 # Docstring inherited from TreeVisitor.visitParens 

102 return expression 

103 

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

105 # Docstring inherited from TreeVisitor.visitRangeLiteral 

106 raise NotImplementedError() 

107 

108 

109class NormalFormExpressionTestCase(unittest.TestCase): 

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

111 

112 def check( 

113 self, 

114 expression: str, 

115 conjunctive: Union[str, bool] = False, 

116 disjunctive: Union[str, bool] = False, 

117 ) -> None: 

118 """Compare the results of transforming an expression to normal form 

119 against given expected values. 

120 

121 Parameters 

122 ---------- 

123 expression : `str` 

124 Expression to parse and transform. 

125 conjunctive : `str` or `bool` 

126 The expected string form of the expression after transformation 

127 to conjunctive normal form, or `True` to indicate that 

128 ``expression`` should already be in that form. `False` to skip 

129 tests on this form. 

130 disjunctive : `str` or `bool` 

131 Same as ``conjunctive``, but for disjunctive normal form. 

132 """ 

133 with self.subTest(expression): 

134 parser = ParserYacc() 

135 originalTree = parser.parse(expression) 

136 wrapper = originalTree.visit(TransformationVisitor()) 

137 trees = {form: NormalFormExpression.fromTree(originalTree, form).toTree() for form in NormalForm} 

138 if conjunctive is True: # expected to be conjunctive already 

139 self.assertTrue(wrapper.satisfies(NormalForm.CONJUNCTIVE), msg=str(wrapper)) 

140 elif conjunctive: # str to expect after normalization to conjunctive 

141 self.assertEqual(str(trees[NormalForm.CONJUNCTIVE]), conjunctive) 

142 if disjunctive is True: # expected to be disjunctive already 

143 self.assertTrue(wrapper.satisfies(NormalForm.DISJUNCTIVE), msg=str(wrapper)) 

144 elif disjunctive: # str to expect after normalization to disjunctive 

145 self.assertEqual(str(trees[NormalForm.DISJUNCTIVE]), disjunctive) 

146 for i in range(10): 

147 values = {k: bool(random.randint(0, 1)) for k in "ABCDEF"} 

148 visitor = BooleanEvaluationTreeVisitor(**values) 

149 expected = originalTree.visit(visitor) 

150 for name, tree in trees.items(): 

151 self.assertEqual(expected, tree.visit(visitor), msg=name) 

152 

153 def testNormalize(self): 

154 self.check("A AND B", conjunctive=True, disjunctive=True) 

155 self.check("A OR B", conjunctive=True, disjunctive=True) 

156 self.check("NOT (A OR B)", conjunctive="NOT A AND NOT B", disjunctive="NOT A AND NOT B") 

157 self.check("NOT (A AND B)", conjunctive="NOT A OR NOT B", disjunctive="NOT A OR NOT B") 

158 self.check( 

159 "NOT (A AND (NOT B OR C))", 

160 conjunctive="(NOT A OR B) AND (NOT A OR NOT C)", 

161 disjunctive=True, 

162 ) 

163 self.check( 

164 "NOT (A OR (B AND NOT C))", 

165 conjunctive=True, 

166 disjunctive="(NOT A AND NOT B) OR (NOT A AND C)", 

167 ) 

168 self.check( 

169 "A AND (B OR C OR D)", 

170 conjunctive=True, 

171 disjunctive="(A AND B) OR (A AND C) OR (A AND D)", 

172 ) 

173 self.check( 

174 "A OR (B AND C AND D)", 

175 disjunctive=True, 

176 conjunctive="(A OR B) AND (A OR C) AND (A OR D)", 

177 ) 

178 self.check( 

179 "A AND (B OR NOT C) AND (NOT D OR E OR F)", 

180 conjunctive=True, 

181 disjunctive=( 

182 "(A AND B AND NOT D) OR (A AND B AND E) OR (A AND B AND F) " 

183 "OR (A AND NOT C AND NOT D) OR (A AND NOT C AND E) OR (A AND NOT C AND F)" 

184 ), 

185 ) 

186 self.check( 

187 "A OR (B AND NOT C) OR (NOT D AND E AND F)", 

188 conjunctive=( 

189 "(A OR B OR NOT D) AND (A OR B OR E) AND (A OR B OR F) " 

190 "AND (A OR NOT C OR NOT D) AND (A OR NOT C OR E) AND (A OR NOT C OR F)" 

191 ), 

192 disjunctive=True, 

193 ) 

194 self.check( 

195 "(A OR (B AND NOT (C OR (NOT D AND E)))) OR F", 

196 conjunctive=("(A OR B OR F) AND (A OR NOT C OR F) AND (A OR D OR NOT E OR F)"), 

197 disjunctive="A OR (B AND NOT C AND D) OR (B AND NOT C AND NOT E) OR F", 

198 ) 

199 

200 

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

202 unittest.main()