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)