this repo has no description
1from enum import Enum
2from . import yaml
3import minizinc as mzn
4from minizinc.helpers import check_result
5import pathlib
6import re
7
8
9@yaml.mapping(u"!Test")
10class Test:
11 """
12 Represents a test case
13
14 Encoded using `!Test` in YAML.
15 """
16
17 def __init__(self, **kwargs):
18 self.name = yaml.Undefined
19 self.solvers = ["gecode", "cbc", "chuffed"]
20 self.check_against = []
21 self.expected = []
22 self.options = {}
23 self.extra_files = []
24 self.markers = []
25 self.type = "solve"
26
27 for key, value in kwargs.items():
28 setattr(self, key, value)
29
30 def run(self, mzn_file, solver, default_options={}):
31 """
32 Runs this test case given an mzn file path, a solver name and some default options.
33
34 Any options specified in this test case directly will override those provided in
35 default options.
36
37 Returns a tuple containing:
38 - the model produced
39 - the result of running the solver
40 - a list of expected results
41 - the actual obtained result
42
43 Pass the actual obtained result to passed() to determine if the test case passed.
44 """
45 options = {k: v for k, v in default_options.items()}
46 options.update(self.options)
47 file = pathlib.Path(mzn_file)
48 extra_files = [file.parent.joinpath(other) for other in self.extra_files]
49
50 try:
51 model = mzn.Model([file] + extra_files)
52 solver = mzn.Solver.lookup(solver)
53 instance = mzn.Instance(solver, model)
54 if self.type == "solve":
55 instance.output_type = Solution
56 result = instance.solve(**options)
57 obtained = Result.from_mzn(result)
58 elif self.type == "compile":
59 with instance.flat(**options) as (fzn, ozn, stats):
60 obtained = FlatZinc.from_mzn(fzn, file.parent)
61 result = obtained
62 elif self.type == "output-model":
63 with instance.flat(**options) as (fzn, ozn, stats):
64 obtained = OutputModel.from_mzn(ozn, file.parent)
65 result = obtained
66 else:
67 raise NotImplementedError("Unknown test case type")
68 except mzn.MiniZincError as error:
69 result = error
70 obtained = Error.from_mzn(result)
71
72 required = self.expected if isinstance(self.expected, list) else [self.expected]
73
74 return model, result, required, obtained
75
76 def passed(self, actual):
77 """
78 Returns whether the given result satisfies this test case
79 """
80 if isinstance(self.expected, list):
81 return any(exp.check(actual) for exp in self.expected)
82 return self.expected.check(actual)
83
84
85@yaml.mapping(u"!Result")
86class Result:
87 """
88 Represents a result generated by calling `solve()` on an instance.
89
90 Encoded using `!Result` in YAML.
91 """
92
93 def __init__(self, **kwargs):
94 self.status = yaml.Undefined
95 self.solution = yaml.Undefined
96
97 for k, v in kwargs.items():
98 setattr(self, k, v)
99
100 def check(self, actual):
101 """
102 Returns whether this result and the given result match.
103 """
104 if not isinstance(actual, mzn.Result):
105 return False
106
107 if self.status is not yaml.Undefined and str(actual.status) != self.status:
108 return False
109
110 if self.solution is not yaml.Undefined and self.solution is not None:
111 required = self.solution
112 obtained = actual.solution
113
114 if isinstance(required, list):
115 return len(required) == len(obtained) and all(
116 x.is_satisfied(y) for x, y in zip(required, obtained)
117 )
118 else:
119 return required.is_satisfied(obtained)
120
121 return True
122
123 @staticmethod
124 def from_mzn(result):
125 """
126 Constructs a Result from a minizinc Result type produced by running a solver
127 as opposed to deserializing from YAML.
128 """
129 instance = Result()
130 instance.status = str(result.status)
131 instance.solution = result.solution
132 return instance
133
134
135@yaml.sequence(u"!SolutionSet")
136class SolutionSet:
137 """
138 Represents a set of expected solutions that passes when
139 every obtained solution matches an entry in this set.
140
141 Represented with `!SolutionSet` in YAML
142 """
143
144 def __init__(self, *args):
145 self.items = args
146
147 def __iter__(self):
148 return iter(self.items)
149
150 def is_satisfied(self, other):
151 """
152 Returns whether or not every solution in `other` matches one in this solution set
153 """
154 return all(any(item.is_satisfied(o) for item in self.items) for o in other)
155
156
157@yaml.mapping(u"!Solution")
158class Solution:
159 """
160 A solution which is satisfied when every member of the this solution matches a member in the obtained solution.
161
162 That is, the obtained solution can contain extra items that are not compared.
163
164 Represented by `!Solution` in YAML.
165 """
166
167 def __init__(self, **kwargs):
168 for key, value in kwargs.items():
169 setattr(self, key, value)
170
171 def items(self):
172 return vars(self).items()
173
174 def is_satisfied(self, other):
175 """
176 Returns whether or not this solution is satisfied by an actual solution
177 """
178
179 def convertEnums(data):
180 # Convert enums to strings so that normal equality can be used
181 if isinstance(data, Enum):
182 return data.name
183 if isinstance(data, list):
184 return [convertEnums(d) for d in data]
185 if isinstance(data, set):
186 return set(convertEnums(d) for d in data)
187 return data
188
189 def in_other(k, v):
190 try:
191 return v == convertEnums(getattr(other, k))
192 except AttributeError:
193 return False
194
195 return all(in_other(k, v) for k, v in self.__dict__.items())
196
197
198@yaml.mapping(u"!Error")
199class Error:
200 """
201 A MiniZinc error with a type name and message.
202
203 Represented by `!Error` in YAML.
204 """
205
206 def __init__(self, **kwargs):
207 self.type = yaml.Undefined
208 self.message = yaml.Undefined
209 self.regex = yaml.Undefined
210
211 for key, value in kwargs.items():
212 setattr(self, key, value)
213
214 def check(self, actual):
215 """
216 Checks if this error matches an actual obtained result/error
217 """
218 if not isinstance(actual, mzn.MiniZincError):
219 return False
220
221 if self.type is not yaml.Undefined:
222 kind = type(actual)
223 classes = (kind,) + kind.__bases__
224 if not any(self.type == c.__name__ for c in classes):
225 return False
226
227 if self.message is not yaml.Undefined:
228 return self.message == str(actual)
229
230 if self.regex is not yaml.Undefined:
231 return re.match(self.regex, str(actual), flags=re.M | re.S) is not None
232
233 return True
234
235 @staticmethod
236 def from_mzn(error):
237 """
238 Creates an Error from a MiniZinc error (as opposed to from deserializing YAML)
239 """
240 instance = Error()
241 instance.type = type(error).__name__
242 instance.message = str(error)
243 return instance
244
245
246class CachedResult:
247 """
248 An object used to store a model and result from solving an instance
249
250 This is so that `CheckItem`s do not have to re-solve an instance when
251 verifying against another solver.
252 """
253
254 def __init__(self):
255 self.model = None
256 self.result = None
257 self.obtained = None
258
259 def test(self, solver):
260 """
261 Checks that the result stored in this object, when given as data for the
262 stored model with the given solver, is satisfied.
263 """
264 solver_instance = mzn.Solver.lookup(solver)
265 passed = check_result(
266 model=self.model, result=self.result, solver=solver_instance
267 )
268 return passed
269
270
271@yaml.scalar(u"!FlatZinc")
272class FlatZinc:
273 """
274 A FlatZinc result, encoded by !FlatZinc in YAML.
275 """
276
277 def __init__(self, path):
278 self.path = path
279 self.base = None
280 self.fzn = None
281
282 def check(self, actual):
283 if not isinstance(actual, FlatZinc):
284 return False
285
286 if self.fzn is None:
287 with open(actual.base.joinpath(self.path)) as f:
288 self.fzn = f.read()
289
290 return self.normalized() == actual.normalized()
291
292 def normalized(self):
293 """
294 Quick and dirty normalization of FlatZinc
295
296 Used to compare two `.fzn` files a bit more robustly than naive string comparison
297 """
298 fzn = self.fzn
299 var_nums = re.findall(r"X_INTRODUCED_(\d+)_", fzn)
300 for new, old in enumerate(sorted(set(int(x) for x in var_nums))):
301 # Compress the introduced variable numbers, removing any holes and starting from 0
302 needle = "X_INTRODUCED_{}_".format(old)
303 replacement = "X_INTRODUCED_{}_".format(new)
304 fzn = fzn.replace(needle, replacement)
305 return "\n".join(sorted(fzn.split("\n"))) # Sort the FlatZinc lines
306
307 def get_value(self):
308 if self.fzn is None:
309 return self.path
310 return self.normalized()
311
312 @staticmethod
313 def from_mzn(fzn, base):
314 """
315 Creates a `FlatZinc` object from a `File` returned by `flat()` in the minizinc interface.
316
317 Also takes the base path the mzn file was from, so that when loading the expected fzn file
318 it can be done relative to the mzn path.
319 """
320 with open(fzn.name) as file:
321 instance = FlatZinc(None)
322 instance.fzn = file.read()
323 instance.base = base
324 return instance
325
326
327@yaml.scalar(u"!OutputModel")
328class OutputModel:
329 """
330 An OZN result, encoded by !OutputModel in YAML.
331 """
332
333 def __init__(self, path):
334 self.path = path
335 self.base = None
336 self.ozn = None
337
338 def check(self, actual):
339 if not isinstance(actual, OutputModel):
340 return False
341
342 if self.ozn is None:
343 with open(actual.base.joinpath(self.path)) as f:
344 self.ozn = f.read()
345
346 return self.ozn == actual.ozn
347
348 def get_value(self):
349 if self.ozn is None:
350 return self.path
351 return self.ozn
352
353 @staticmethod
354 def from_mzn(ozn, base):
355 """
356 Creates a `OutputModel` object from a `File` returned by `flat()` in the minizinc interface.
357
358 Also takes the base path the mzn file was from, so that when loading the expected ozn file
359 it can be done relative to the mzn path.
360 """
361 with open(ozn.name) as file:
362 instance = OutputModel(None)
363 instance.ozn = file.read()
364 instance.base = base
365 return instance
366
367
368@yaml.mapping(u"!Suite")
369class Suite:
370 """
371 Represents a test suite configuration
372
373 Encoded using `!Suite` in YAML.
374 """
375
376 def __init__(self, **kwargs):
377 self.solvers = None
378 self.options = {}
379 self.strict = True
380 self.regex = r"."
381
382 for key, value in kwargs.items():
383 setattr(self, key, value)