Coverage for tests/test_normalFormExpression.py: 24%

70 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-07 10: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/>. 

21 

22 

23import random 

24import unittest 

25from typing import List, Optional, Union 

26 

27import astropy.time 

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

29 NormalForm, 

30 NormalFormExpression, 

31 TransformationVisitor, 

32) 

33from lsst.daf.butler.registry.queries.expressions.parser import Node, ParserYacc, TreeVisitor 

34 

35 

36class BooleanEvaluationTreeVisitor(TreeVisitor[bool]): 

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

38 boolean values for identifiers. 

39 """ 

40 

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

42 self.values = kwargs 

43 

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

45 self.form = form 

46 

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

48 # Docstring inherited from TreeVisitor.visitNumericLiteral 

49 raise NotImplementedError() 

50 

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

52 # Docstring inherited from TreeVisitor.visitStringLiteral 

53 raise NotImplementedError() 

54 

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

56 # Docstring inherited from TreeVisitor.visitTimeLiteral 

57 raise NotImplementedError() 

58 

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

60 # Docstring inherited from TreeVisitor.visitIdentifier 

61 return self.values[name] 

62 

63 def visitUnaryOp( 

64 self, 

65 operator: str, 

66 operand: bool, 

67 node: Node, 

68 ) -> bool: 

69 # Docstring inherited from TreeVisitor.visitUnaryOp 

70 if operator == "NOT": 

71 return not operand 

72 raise NotImplementedError() 

73 

74 def visitBinaryOp( 

75 self, 

76 operator: str, 

77 lhs: bool, 

78 rhs: bool, 

79 node: Node, 

80 ) -> bool: 

81 # Docstring inherited from TreeVisitor.visitBinaryOp 

82 if operator == "AND": 

83 return lhs and rhs 

84 if operator == "OR": 

85 return lhs or rhs 

86 raise NotImplementedError() 

87 

88 def visitIsIn( 

89 self, 

90 lhs: bool, 

91 values: List[bool], 

92 not_in: bool, 

93 node: Node, 

94 ) -> bool: 

95 # Docstring inherited from TreeVisitor.visitIsIn 

96 raise NotImplementedError() 

97 

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

99 # Docstring inherited from TreeVisitor.visitParens 

100 return expression 

101 

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

103 # Docstring inherited from TreeVisitor.visitRangeLiteral 

104 raise NotImplementedError() 

105 

106 

107class NormalFormExpressionTestCase(unittest.TestCase): 

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

109 

110 def check( 

111 self, 

112 expression: str, 

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

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

115 ) -> None: 

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

117 against given expected values. 

118 

119 Parameters 

120 ---------- 

121 expression : `str` 

122 Expression to parse and transform. 

123 conjunctive : `str` or `bool` 

124 The expected string form of the expression after transformation 

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

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

127 tests on this form. 

128 disjunctive : `str` or `bool` 

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

130 """ 

131 with self.subTest(expression): 

132 parser = ParserYacc() 

133 originalTree = parser.parse(expression) 

134 wrapper = originalTree.visit(TransformationVisitor()) 

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

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

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

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

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

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

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

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

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

144 for i in range(10): 

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

146 visitor = BooleanEvaluationTreeVisitor(**values) 

147 expected = originalTree.visit(visitor) 

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

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

150 

151 def testNormalize(self): 

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

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

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

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

156 self.check( 

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

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

159 disjunctive=True, 

160 ) 

161 self.check( 

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

163 conjunctive=True, 

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

165 ) 

166 self.check( 

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

168 conjunctive=True, 

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

170 ) 

171 self.check( 

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

173 disjunctive=True, 

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

175 ) 

176 self.check( 

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

178 conjunctive=True, 

179 disjunctive=( 

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

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

182 ), 

183 ) 

184 self.check( 

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

186 conjunctive=( 

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

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

189 ), 

190 disjunctive=True, 

191 ) 

192 self.check( 

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

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

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

196 ) 

197 

198 

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

200 unittest.main()