| 4 | 1 # This is a general-purpose "testing utilities" module for the COBRAxy tool. | 
|  | 2 # This code was written entirely by m.ferrari133@campus.unimib.it and then (hopefully) many | 
|  | 3 # more people contributed by writing tests for this tool's modules, feel free to send an email for | 
|  | 4 # any questions. | 
|  | 5 | 
|  | 6 # How the testing module works: | 
|  | 7 # The testing module allows you to easily set up unit tests for functions in a module, obtaining | 
|  | 8 # information on what each method returns, when and how it fails and so on. | 
|  | 9 | 
|  | 10 # How do I test a module? | 
|  | 11 # - create a function at the very bottom, before the __main__ | 
|  | 12 # - import the stuff you need | 
|  | 13 # - create a UnitTester instance, follow the documentation | 
|  | 14 # - fill it up with UnitTest instances, follow the documentation | 
|  | 15 # - each UnitTest tests the function by passing specific parameters to it and by veryfing the correctness | 
|  | 16 #   of the output via a CheckingMode instance | 
|  | 17 # - call testModule() on the UnitTester | 
|  | 18 | 
|  | 19 # TODO(s): | 
|  | 20 # - This module was written before the utilities were introduced, it may want to use some of those functions. | 
|  | 21 # - I never got around to writing a CheckingMode for methods you WANT to fail in certain scenarios, I | 
|  | 22 #   like the name "MustPanic". | 
|  | 23 # - It's good practice to enforce boolean arguments of a function to be passed as kwargs and I did it a lot | 
|  | 24 #   in the code I wrote for these tool's modules, but the current implementation of UnitTest doesn't allow | 
|  | 25 #   you to pass kwargs to the functions you test. | 
|  | 26 # - Implement integration tests as well, maybe! | 
|  | 27 | 
|  | 28 ## Imports: | 
|  | 29 from typing import Dict, Callable, Type, List | 
|  | 30 from enum import Enum, auto | 
|  | 31 from collections.abc import Iterable | 
|  | 32 | 
|  | 33 ## Generic utilities: | 
|  | 34 class TestResult: | 
|  | 35     """ | 
|  | 36     Represents the result of a test and contains all the relevant information about it. Loosely models two variants: | 
|  | 37     - Ok: The test passed, no further information is saved besides the target's name. | 
|  | 38     - Err: The test failed, an error message and further contextual details are also saved. | 
|  | 39 | 
|  | 40     This class does not ensure a static proof of the two states' behaviour, their meaning or mutual exclusivity outside | 
|  | 41     of the :bool property "isPass", meant for outside reads. | 
|  | 42     """ | 
|  | 43     def __init__(self, isPass :bool, targetName :str, errMsg = "", details = "") -> None: | 
|  | 44         """ | 
|  | 45         (Private) Initializes an instance of TestResult. | 
|  | 46 | 
|  | 47         Args: | 
|  | 48             isPass : distinction between TestResult.Ok (True) and TestResult.Err (False). | 
|  | 49             targetName : the name of the target object / property / function / module being tested, not always set | 
|  | 50             to a meaningful value at this stage. | 
|  | 51 | 
|  | 52             errMsg : concise error message explaining the test's failure. | 
|  | 53             details : contextual details about the error. | 
|  | 54 | 
|  | 55         Returns: | 
|  | 56             None : practically, a TestResult instance. | 
|  | 57         """ | 
|  | 58         self.isPass = isPass | 
|  | 59         self.isFail = not isPass # Convenience above all | 
|  | 60 | 
|  | 61         self.targetName = targetName | 
|  | 62         if isPass: return | 
|  | 63 | 
|  | 64         self.errMsg   = errMsg | 
|  | 65         self.details  = details | 
|  | 66 | 
|  | 67     @classmethod | 
|  | 68     def Ok(cls, targetName = "") -> "TestResult": | 
|  | 69         """ | 
|  | 70         Factory method for TestResult.Ok, where all we need to know is that our test passed. | 
|  | 71 | 
|  | 72         Args: | 
|  | 73             targetName : the name of the target object / property / function / module being tested, not always set | 
|  | 74             to a meaningful value at this stage. | 
|  | 75 | 
|  | 76         Returns: | 
|  | 77             TestResult : a new Ok instance. | 
|  | 78         """ | 
|  | 79         return cls(True, targetName) | 
|  | 80 | 
|  | 81     @classmethod | 
|  | 82     def Err(cls, errMsg :str, details :str, targetName = "") -> "TestResult": | 
|  | 83         """ | 
|  | 84         Factory method for TestResult.Err, where we store relevant error information. | 
|  | 85 | 
|  | 86         Args: | 
|  | 87             errMsg : concise error message explaining the test's failure. | 
|  | 88             details : contextual details about the error. | 
|  | 89             targetName : the name of the target object / property / function / module being tested, not always set | 
|  | 90             to a meaningful value at this stage. | 
|  | 91 | 
|  | 92         Returns: | 
|  | 93             TestResult : a new Err instance. | 
|  | 94         """ | 
|  | 95         return cls(False, targetName, errMsg, details) | 
|  | 96 | 
|  | 97     def log(self, isCompact = True) -> str: | 
|  | 98         """ | 
|  | 99         Dumps all the available information in a :str, ready for logging. | 
|  | 100 | 
|  | 101         Args: | 
|  | 102             isCompact : if True limits the amount of information displayed to the targetName. | 
|  | 103 | 
|  | 104         Returns: | 
|  | 105             str : information about this test result. | 
|  | 106 | 
|  | 107         """ | 
|  | 108         if isCompact: | 
|  | 109             return f"{TestResult.__name__}::{'Ok' if self.isPass else 'Err'}(Unit test on {self.targetName})" | 
|  | 110 | 
|  | 111         logMsg = f"Unit test on {self.targetName} {'passed' if self.isPass else f'failed because {self.errMsg}'}" | 
|  | 112         if self.details: logMsg += f", {self.details}" | 
|  | 113         return logMsg | 
|  | 114 | 
|  | 115     def throw(self) -> None: | 
|  | 116         #TODO: finer Exception typing would be desirable | 
|  | 117         """ | 
|  | 118         Logs the result information and panics. | 
|  | 119 | 
|  | 120         Raises: | 
|  | 121             Exception : an error containing log information about the test result. | 
|  | 122 | 
|  | 123         Returns: | 
|  | 124             None | 
|  | 125 | 
|  | 126         """ | 
|  | 127         raise Exception(self.log()) | 
|  | 128 | 
|  | 129 class CheckingMode: | 
|  | 130     """ | 
|  | 131     (Private) Represents a way to check a value for correctness, in the context of "testing" it. | 
|  | 132     """ | 
|  | 133 | 
|  | 134     def __init__(self) -> None: | 
|  | 135         """ | 
|  | 136         (Private) Implemented on child classes, initializes an instance of CheckingMode. | 
|  | 137 | 
|  | 138         Returns: | 
|  | 139             None : practically, a CheckingMode instance. | 
|  | 140         """ | 
|  | 141         self.logMsg = "CheckingMode base class should not be used directly" | 
|  | 142 | 
|  | 143     def __checkPasses__(self, _) -> bool: | 
|  | 144         """ | 
|  | 145         (Private) Implemented on child classes, performs the actual correctness check on a received value. | 
|  | 146 | 
|  | 147         Returns: | 
|  | 148             bool : True if the check passed, False if it failed. | 
|  | 149         """ | 
|  | 150         return True | 
|  | 151 | 
|  | 152     def check(self, value) -> TestResult: | 
|  | 153         """ | 
|  | 154         Converts the :bool evaluation of the value's correctness to a TestResult. | 
|  | 155 | 
|  | 156         Args: | 
|  | 157             value : the value to check. | 
|  | 158 | 
|  | 159         Returns: | 
|  | 160             TestResult : the result of the check. | 
|  | 161         """ | 
|  | 162         return TestResult.Ok() if self.__checkPasses__(value) else TestResult.Err(self.logMsg, f"got {value} instead") | 
|  | 163 | 
|  | 164     def __repr__(self) -> str: | 
|  | 165         """ | 
|  | 166         (Private) Implemented on child classes, formats :object as :str. | 
|  | 167         """ | 
|  | 168         return self.__class__.__name__ | 
|  | 169 | 
|  | 170 class ExactValue(CheckingMode): | 
|  | 171     """ | 
|  | 172     CheckingMode subclass variant to be used when the checked value needs to match another exactly. | 
|  | 173     """ | 
|  | 174 | 
|  | 175     #I suggest solving the more complex equality checking edge cases with the "Satisfies" and "MatchingShape" variants. | 
|  | 176     def __init__(self, value) -> None: | 
|  | 177         self.value  = value | 
|  | 178         self.logMsg = f"value needed to match {value} exactly" | 
|  | 179 | 
|  | 180     def __checkPasses__(self, value) -> bool: | 
|  | 181         return self.value == value | 
|  | 182 | 
|  | 183     def __repr__(self) -> str: | 
|  | 184         return f"{super().__repr__()}({self.value})" | 
|  | 185 | 
|  | 186 class AcceptedValues(CheckingMode): | 
|  | 187     """ | 
|  | 188     CheckingMode subclass variant to be used when the checked value needs to appear in a list of accepted values. | 
|  | 189     """ | 
|  | 190     def __init__(self, *values) -> None: | 
|  | 191         self.values = values | 
|  | 192         self.logMsg = f"value needed to be one of these: {values}" | 
|  | 193 | 
|  | 194     def __checkPasses__(self, value) -> bool: | 
|  | 195         return value in self.values | 
|  | 196 | 
|  | 197     def __repr__(self) -> str: | 
|  | 198         return f"{super().__repr__()}{self.values}" | 
|  | 199 | 
|  | 200 class SatisfiesPredicate(CheckingMode): | 
|  | 201     """ | 
|  | 202     CheckingMode subclass variant to be used when the checked value needs to verify a given predicate, as in | 
|  | 203     the predicate accepts it as input and returns True. | 
|  | 204     """ | 
|  | 205     def __init__(self, pred :Callable[..., bool], predName = "") -> None: | 
|  | 206         self.pred = pred | 
|  | 207         self.logMsg = f"value needed to verify a predicate{bool(predName) * f' called {predName}'}" | 
|  | 208 | 
|  | 209     def __checkPasses__(self, *params) -> bool: | 
|  | 210         return self.pred(*params) | 
|  | 211 | 
|  | 212     def __repr__(self) -> str: | 
|  | 213         return f"{super().__repr__()}(T) -> bool" | 
|  | 214 | 
|  | 215 class IsOfType(CheckingMode): | 
|  | 216     """ | 
|  | 217     CheckingMode subclass variant to be used when the checked value needs to be of a certain type. | 
|  | 218     """ | 
|  | 219     def __init__(self, type :Type) -> None: | 
|  | 220         self.type = type | 
|  | 221         self.logMsg = f"value needed to be of type {type.__name__}" | 
|  | 222 | 
|  | 223     def __checkPasses__(self, value :Type) -> bool: | 
|  | 224         return isinstance(value, self.type) | 
|  | 225 | 
|  | 226     def __repr__(self) -> str: | 
|  | 227         return f"{super().__repr__()}:{self.type.__name__}" | 
|  | 228 | 
|  | 229 class Exists(CheckingMode): | 
|  | 230     """ | 
|  | 231     CheckingMode subclass variant to be used when the checked value needs to exist (or not!). Mainly employed as a quick default | 
|  | 232     check that always passes, it still upholds its contract when it comes to checking for existing properties in objects | 
|  | 233     without much concern on what value they contain. | 
|  | 234     """ | 
|  | 235     def __init__(self, exists = True) -> None: | 
|  | 236         self.exists = exists | 
|  | 237         self.logMsg = f"value needed to {(not exists) * 'not '}exist" | 
|  | 238 | 
|  | 239     def __checkPasses__(self, _) -> bool: return self.exists | 
|  | 240 | 
|  | 241     def __repr__(self) -> str: | 
|  | 242         return f"{super().__repr__() if self.exists else 'IsMissing'}" | 
|  | 243 | 
|  | 244 class MatchingShape(CheckingMode): | 
|  | 245     """ | 
|  | 246     CheckingMode subclass variant to be used when the checked value is an object that needs to have a certain shape, | 
|  | 247     as in to posess properties with a given name and value. Each property is checked for existance and correctness with | 
|  | 248     its own given CheckingMode. | 
|  | 249     """ | 
|  | 250     def __init__(self, props :Dict[str, CheckingMode], objName = "") -> None: | 
|  | 251         """ | 
|  | 252         (Private) Initializes an instance of MatchingShape. | 
|  | 253 | 
|  | 254         Args: | 
|  | 255             props : :dict using property names as keys and checking modes for the property's value as values. | 
|  | 256             objName : label for the object we're testing the shape of. | 
|  | 257 | 
|  | 258         Returns: | 
|  | 259             None : practically, a MatchingShape instance. | 
|  | 260         """ | 
|  | 261         self.props   = props | 
|  | 262         self.objName = objName | 
|  | 263 | 
|  | 264         self.shapeRepr = " {\n" + "\n".join([f"  {propName} : {prop}" for propName, prop in props.items()]) + "\n}" | 
|  | 265 | 
|  | 266     def check(self, obj :object) -> TestResult: | 
|  | 267         objIsDict = isinstance(obj, dict) # Python forces us to distinguish between object properties and dict keys | 
|  | 268         for propName, checkingMode in self.props.items(): | 
|  | 269             # Checking if the property exists: | 
|  | 270             if (not objIsDict and not hasattr(obj, propName)) or (objIsDict and propName not in obj): | 
|  | 271                 if not isinstance(checkingMode, Exists): return TestResult.Err( | 
|  | 272                     f"property \"{propName}\" doesn't exist on object {self.objName}", "", self.objName) | 
|  | 273 | 
|  | 274                 if not checkingMode.exists: return TestResult.Ok(self.objName) | 
|  | 275                 # Either the property value is meant to be checked (checkingMode is anything but Exists) | 
|  | 276                 # or we want the property to not exist, all other cases are handled correctly ahead | 
|  | 277 | 
|  | 278             checkRes = checkingMode.check(obj[propName] if objIsDict else getattr(obj, propName)) | 
|  | 279             if checkRes.isPass: continue | 
|  | 280 | 
|  | 281             checkRes.targetName = self.objName | 
|  | 282             return TestResult.Err( | 
|  | 283                 f"property \"{propName}\" failed check {checkingMode} on shape {obj}", | 
|  | 284                 checkRes.log(isCompact = False), | 
|  | 285                 self.objName) | 
|  | 286 | 
|  | 287         return TestResult.Ok(self.objName) | 
|  | 288 | 
|  | 289     def __repr__(self) -> str: | 
|  | 290         return super().__repr__() + self.shapeRepr | 
|  | 291 | 
|  | 292 class Many(CheckingMode): | 
|  | 293     """ | 
|  | 294     CheckingMode subclass variant to be used when the checked value is an Iterable we want to check item by item. | 
|  | 295     """ | 
|  | 296     def __init__(self, *values :CheckingMode) -> None: | 
|  | 297         self.values = values | 
|  | 298         self.shapeRepr = " [\n" + "\n".join([f"  {value}" for value in values]) + "\n]" | 
|  | 299 | 
|  | 300     def check(self, coll :Iterable) -> TestResult: | 
|  | 301         amt         = len(coll) | 
|  | 302         expectedAmt = len(self.values) | 
|  | 303         # Length equality is forced: | 
|  | 304         if amt != expectedAmt: return TestResult.Err( | 
|  | 305             "items' quantities don't match", f"expected {expectedAmt} items, but got {amt}") | 
|  | 306 | 
|  | 307         # Items in the given collection value are paired in order with the corresponding checkingMode meant for each of them | 
|  | 308         for item, checkingMode in zip(coll, self.values): | 
|  | 309             checkRes = checkingMode.check(item) | 
|  | 310             if checkRes.isFail: return TestResult.Err( | 
|  | 311                 f"item in list failed check {checkingMode}", | 
|  | 312                 checkRes.log(isCompact = False)) | 
|  | 313 | 
|  | 314         return TestResult.Ok() | 
|  | 315 | 
|  | 316     def __repr__(self) -> str: | 
|  | 317         return super().__repr__() + self.shapeRepr | 
|  | 318 | 
|  | 319 class LogMode(Enum): | 
|  | 320     """ | 
|  | 321     Represents the level of detail of a logged message. Models 4 variants, in order of increasing detail: | 
|  | 322     - Minimal  : Logs the overall test result for the entire module. | 
|  | 323     - Default  : Also logs all single test fails, in compact mode. | 
|  | 324     - Detailed : Logs all function test results, in compact mode. | 
|  | 325     - Pedantic : Also logs all single test results in detailed mode. | 
|  | 326     """ | 
|  | 327     Minimal  = auto() | 
|  | 328     Default  = auto() | 
|  | 329     Detailed = auto() | 
|  | 330     Pedantic = auto() | 
|  | 331 | 
|  | 332     def isMoreVerbose(self, requiredMode :"LogMode") -> bool: | 
|  | 333         """ | 
|  | 334         Compares the instance's level of detail with that of another. | 
|  | 335 | 
|  | 336         Args: | 
|  | 337             requiredMode : the other instance. | 
|  | 338 | 
|  | 339         Returns: | 
|  | 340             bool : True if the caller instance is a more detailed variant than the other. | 
|  | 341         """ | 
|  | 342         return self.value >= requiredMode.value | 
|  | 343 | 
|  | 344 ## Specific Unit Testing utilities: | 
|  | 345 class UnitTest: | 
|  | 346     """ | 
|  | 347     Represents a unit test, the test of a single function's isolated correctness. | 
|  | 348     """ | 
|  | 349     def __init__(self, func :Callable, inputParams :list, expectedRes :CheckingMode) -> None: | 
|  | 350         """ | 
|  | 351         (Private) Initializes an instance of UnitTest. | 
|  | 352 | 
|  | 353         Args: | 
|  | 354             func : the function to test. | 
|  | 355             inputParams : list of parameters to pass as inputs to the function, in order. | 
|  | 356             expectedRes : checkingMode to test the function's return value for correctness. | 
|  | 357 | 
|  | 358         Returns: | 
|  | 359             None : practically, a UnitTest instance. | 
|  | 360         """ | 
|  | 361         self.func        = func | 
|  | 362         self.inputParams = inputParams | 
|  | 363         self.expectedRes = expectedRes | 
|  | 364 | 
|  | 365         self.funcName = func.__name__ | 
|  | 366 | 
|  | 367     def test(self) -> TestResult: | 
|  | 368         """ | 
|  | 369         Tests the function. | 
|  | 370 | 
|  | 371         Returns: | 
|  | 372             TestResult : the test's result. | 
|  | 373         """ | 
|  | 374         result = None | 
|  | 375         try: result = self.func(*self.inputParams) | 
|  | 376         except Exception as e: return TestResult.Err("the function panicked at runtime", e, self.funcName) | 
|  | 377 | 
|  | 378         checkRes = self.expectedRes.check(result) | 
|  | 379         checkRes.targetName = self.funcName | 
|  | 380         return checkRes | 
|  | 381 | 
|  | 382 class UnitTester: | 
|  | 383     """ | 
|  | 384     Manager class for unit testing an entire module, groups single UnitTests together and executes them in order on a | 
|  | 385     per-function basis (tests about the same function are executed consecutively) giving back as much information as | 
|  | 386     possible depending on the selected logMode. More customization options are available. | 
|  | 387     """ | 
|  | 388     def __init__(self, moduleName :str, logMode = LogMode.Default, stopOnFail = True, *funcTests :'UnitTest') -> None: | 
|  | 389         """ | 
|  | 390         (Private) initializes an instance of UnitTester. | 
|  | 391 | 
|  | 392         Args: | 
|  | 393             moduleName : name of the tested module. | 
|  | 394             logMode : level of detail applied to all messages logged during the test. | 
|  | 395             stopOnFail : if True, the test stops entirely after one unit test fails. | 
|  | 396             funcTests : the unit tests to perform on the module. | 
|  | 397 | 
|  | 398         Returns: | 
|  | 399             None : practically, a UnitTester instance. | 
|  | 400         """ | 
|  | 401         self.logMode    = logMode | 
|  | 402         self.moduleName = moduleName | 
|  | 403         self.stopOnFail = stopOnFail | 
|  | 404 | 
|  | 405         # This ensures the per-function order: | 
|  | 406         self.funcTests :Dict[str, List[UnitTest]]= {} | 
|  | 407         for test in funcTests: | 
|  | 408             if test.funcName in self.funcTests: self.funcTests[test.funcName].append(test) | 
|  | 409             else: self.funcTests[test.funcName] = [test] | 
|  | 410 | 
|  | 411     def logTestResult(self, testRes :TestResult) -> None: | 
|  | 412         """ | 
|  | 413         Prints the formatted result information of a unit test. | 
|  | 414 | 
|  | 415         Args: | 
|  | 416             testRes : the result of the test. | 
|  | 417 | 
|  | 418         Returns: | 
|  | 419             None | 
|  | 420         """ | 
|  | 421         if testRes.isPass: return self.log("Passed!", LogMode.Detailed, indent = 2) | 
|  | 422 | 
|  | 423         failMsg = "Failed! " | 
|  | 424         # Doing it this way prevents .log computations when not needed | 
|  | 425         if self.logMode.isMoreVerbose(LogMode.Detailed): | 
|  | 426             # Given that Pedantic is the most verbose variant, there's no point in comparing with LogMode.isMoreVerbose | 
|  | 427             failMsg += testRes.log(self.logMode is not LogMode.Pedantic) | 
|  | 428 | 
|  | 429         self.log(failMsg, indent = 2) | 
|  | 430 | 
|  | 431     def log(self, msg :str, minRequiredMode = LogMode.Default, indent = 0) -> None: | 
|  | 432         """ | 
|  | 433         Prints and formats a message only when the UnitTester instance is set to a level of detail at least equal | 
|  | 434         to a minimum requirement, given as input. | 
|  | 435 | 
|  | 436         Args: | 
|  | 437             msg : the message to print. | 
|  | 438             minRequiredMode : minimum detail requirement. | 
|  | 439             indent : formatting information, counter from 0 that adds 2 spaces each number up | 
|  | 440 | 
|  | 441         Returns: | 
|  | 442             None | 
|  | 443         """ | 
|  | 444         if self.logMode.isMoreVerbose(minRequiredMode): print("  " * indent + msg) | 
|  | 445 | 
|  | 446     def testFunction(self, name :str) -> TestResult: | 
|  | 447         """ | 
|  | 448         Perform all unit tests relative to the same function, plus the surrounding logs and checks. | 
|  | 449 | 
|  | 450         Args: | 
|  | 451             name : the name of the tested function. | 
|  | 452 | 
|  | 453         Returns : | 
|  | 454             TestResult : the overall Ok result of all the tests passing or the first Err. This behaviour is unrelated | 
|  | 455             to that of the overall testing procedure (stopOnFail), it always works like this for tests about the | 
|  | 456             same function. | 
|  | 457         """ | 
|  | 458         self.log(f"Unit testing {name}...", indent = 1) | 
|  | 459 | 
|  | 460         allPassed = True | 
|  | 461         for unitTest in self.funcTests[name]: | 
|  | 462             testRes = unitTest.test() | 
|  | 463             self.logTestResult(testRes) | 
|  | 464             if testRes.isPass: continue | 
|  | 465 | 
|  | 466             allPassed = False | 
|  | 467             if self.stopOnFail: break | 
|  | 468 | 
|  | 469         self.log("", LogMode.Detailed) # Provides one extra newline of space when needed, to better format the output | 
|  | 470         if allPassed: return TestResult.Ok(name) | 
|  | 471 | 
|  | 472         if self.logMode is LogMode.Default: self.log("") | 
|  | 473         return TestResult.Err(f"Unlogged err", "unit test failed", name) | 
|  | 474 | 
|  | 475     def testModule(self) -> None: | 
|  | 476         """ | 
|  | 477         Runs all the provided unit tests in order but on a per-function basis. | 
|  | 478 | 
|  | 479         Returns: | 
|  | 480             None | 
|  | 481         """ | 
|  | 482         self.log(f"Unit testing module {self.moduleName}...", LogMode.Minimal) | 
|  | 483 | 
|  | 484         fails = 0 | 
|  | 485         testStatusMsg = "complete" | 
|  | 486         for funcName in self.funcTests.keys(): | 
|  | 487             if self.testFunction(funcName).isPass: continue | 
|  | 488             fails += 1 | 
|  | 489 | 
|  | 490             if self.stopOnFail: | 
|  | 491                 testStatusMsg = "interrupted" | 
|  | 492                 break | 
|  | 493 | 
|  | 494         self.log(f"Testing {testStatusMsg}: {fails} problem{'s' * (fails != 1)} found.\n", LogMode.Minimal) | 
|  | 495         # ^^^ Manually applied an extra newline of space. | 
|  | 496 | 
|  | 497 ## Unit testing all the modules: | 
|  | 498 def unit_cobraxy() -> None: | 
|  | 499     import cobraxy as m | 
|  | 500     import math | 
|  | 501     import lxml.etree as ET | 
|  | 502     import utils.general_utils as utils | 
|  | 503 | 
|  | 504     #m.ARGS = m.process_args() | 
|  | 505 | 
|  | 506     ids = ["react1", "react2", "react3", "react4", "react5"] | 
|  | 507     metabMap = utils.Model.ENGRO2.getMap() | 
|  | 508     class_pat = { | 
|  | 509         "dataset1" :[ | 
|  | 510             [2.3, 4, 7, 0, 0.01, math.nan, math.nan], | 
|  | 511             [math.nan, math.nan, math.nan, math.nan, math.nan, math.nan, math.nan], | 
|  | 512             [2.3, 4, 7, 0, 0.01, 5, 9], | 
|  | 513             [math.nan, math.nan, 2.3, 4, 7, 0, 0.01], | 
|  | 514             [2.3, 4, 7, math.nan, 2.3, 0, 0.01]], | 
|  | 515 | 
|  | 516         "dataset2" :[ | 
|  | 517             [2.3, 4, 7, math.nan, 2.3, 0, 0.01], | 
|  | 518             [2.3, 4, 7, 0, 0.01, math.nan, math.nan], | 
|  | 519             [math.nan, math.nan, 2.3, 4, 7, 0, 0.01], | 
|  | 520             [2.3, 4, 7, 0, 0.01, 5, 9], | 
|  | 521             [math.nan, math.nan, math.nan, math.nan, math.nan, math.nan, math.nan]] | 
|  | 522     } | 
|  | 523 | 
|  | 524     unitTester = UnitTester("cobraxy", LogMode.Pedantic, False, | 
|  | 525         UnitTest(m.name_dataset, ["customName", 12], ExactValue("customName")), | 
|  | 526         UnitTest(m.name_dataset, ["Dataset", 12], ExactValue("Dataset_12")), | 
|  | 527 | 
|  | 528         UnitTest(m.fold_change, [0.5, 0.5], ExactValue(0.0)), | 
|  | 529         UnitTest(m.fold_change, [0, 0.35], ExactValue("-INF")), | 
|  | 530         UnitTest(m.fold_change, [0.5, 0], ExactValue("INF")), | 
|  | 531         UnitTest(m.fold_change, [0, 0], ExactValue(0)), | 
|  | 532 | 
|  | 533         UnitTest( | 
|  | 534             m.Arrow(m.Arrow.MAX_W, m.ArrowColor.DownRegulated, isDashed = True).toStyleStr, [], | 
|  | 535             ExactValue(";stroke:#0000FF;stroke-width:12;stroke-dasharray:5,5")), | 
|  | 536 | 
|  | 537         UnitTest(m.computeEnrichment, [metabMap, class_pat, ids], ExactValue(None)), | 
|  | 538 | 
|  | 539         UnitTest(m.computePValue, [class_pat["dataset1"][0], class_pat["dataset2"][0]], SatisfiesPredicate(math.isnan)), | 
|  | 540 | 
|  | 541         UnitTest(m.reactionIdIsDirectional, ["reactId"], ExactValue(m.ReactionDirection.Unknown)), | 
|  | 542         UnitTest(m.reactionIdIsDirectional, ["reactId_F"], ExactValue(m.ReactionDirection.Direct)), | 
|  | 543         UnitTest(m.reactionIdIsDirectional, ["reactId_B"], ExactValue(m.ReactionDirection.Inverse)), | 
|  | 544 | 
|  | 545         UnitTest(m.ArrowColor.fromFoldChangeSign, [-2], ExactValue(m.ArrowColor.DownRegulated)), | 
|  | 546         UnitTest(m.ArrowColor.fromFoldChangeSign, [2], ExactValue(m.ArrowColor.UpRegulated)), | 
|  | 547 | 
|  | 548         UnitTest( | 
|  | 549             m.Arrow(m.Arrow.MAX_W, m.ArrowColor.UpRegulated).styleReactionElements, | 
|  | 550             [metabMap, "reactId"], | 
|  | 551             ExactValue(None)), | 
|  | 552 | 
|  | 553         UnitTest(m.getArrowBodyElementId, ["reactId"], ExactValue("R_reactId")), | 
|  | 554         UnitTest(m.getArrowBodyElementId, ["reactId_F"], ExactValue("R_reactId")), | 
|  | 555 | 
|  | 556         UnitTest( | 
|  | 557             m.getArrowHeadElementId, ["reactId"], | 
|  | 558             Many(ExactValue("F_reactId"), ExactValue("B_reactId"))), | 
|  | 559 | 
|  | 560         UnitTest( | 
|  | 561             m.getArrowHeadElementId, ["reactId_F"], | 
|  | 562             Many(ExactValue("F_reactId"), ExactValue(""))), | 
|  | 563 | 
|  | 564         UnitTest( | 
|  | 565             m.getArrowHeadElementId, ["reactId_B"], | 
|  | 566             Many(ExactValue("B_reactId"), ExactValue(""))), | 
|  | 567 | 
|  | 568         UnitTest( | 
|  | 569             m.getElementById, ["reactId_F", metabMap], | 
|  | 570             SatisfiesPredicate(lambda res : res.isErr and isinstance(res.value, utils.Result.ResultErr))), | 
|  | 571 | 
|  | 572         UnitTest( | 
|  | 573             m.getElementById, ["F_tyr_L_t", metabMap], | 
|  | 574             SatisfiesPredicate(lambda res : res.isOk and res.unwrap().get("id") == "F_tyr_L_t")), | 
|  | 575     ).testModule() | 
|  | 576 | 
|  | 577 def unit_rps_generator() -> None: | 
|  | 578     import rps_generator as rps | 
|  | 579     import math | 
|  | 580     import pandas as pd | 
|  | 581     import utils.general_utils as utils | 
|  | 582     dataset = pd.DataFrame({ | 
|  | 583         "cell lines" : ["normal", "cancer"], | 
|  | 584         "pyru_vate"  : [5.3, 7.01], | 
|  | 585         "glu,cose"   : [8.2, 4.0], | 
|  | 586         "unknown"    : [3.0, 3.97], | 
|  | 587         "()atp"      : [7.05, 8.83], | 
|  | 588     }) | 
|  | 589 | 
|  | 590     abundancesNormalRaw = { | 
|  | 591         "pyru_vate" : 5.3, | 
|  | 592         "glu,cose"  : 8.2, | 
|  | 593         "unknown"   : 3.0, | 
|  | 594         "()atp"     : 7.05, | 
|  | 595     } | 
|  | 596 | 
|  | 597     abundancesNormal = { | 
|  | 598         "pyr"    : 5.3, | 
|  | 599         "glc__D" : 8.2, | 
|  | 600         "atp"    : 7.05, | 
|  | 601     } | 
|  | 602 | 
|  | 603     # TODO: this currently doesn't work due to "the pickle extension problem", see FileFormat class for details. | 
|  | 604     synsDict = utils.readPickle(utils.FilePath("synonyms", utils.FileFormat.PICKLE, prefix = "./local/pickle files")) | 
|  | 605 | 
|  | 606     reactionsDict = { | 
|  | 607         "r1" : { | 
|  | 608             "glc__D" : 1 | 
|  | 609         }, | 
|  | 610 | 
|  | 611         "r2" : { | 
|  | 612             "co2" : 2, | 
|  | 613             "pyr" : 3, | 
|  | 614         }, | 
|  | 615 | 
|  | 616         "r3" : { | 
|  | 617             "atp"    : 2, | 
|  | 618             "glc__D" : 4, | 
|  | 619         }, | 
|  | 620 | 
|  | 621         "r4" : { | 
|  | 622             "atp" : 3, | 
|  | 623         } | 
|  | 624     } | 
|  | 625 | 
|  | 626     abundancesNormalEdited = { | 
|  | 627         "pyr"    : 5.3, | 
|  | 628         "glc__D" : 8.2, | 
|  | 629         "atp"    : 7.05, | 
|  | 630         "co2"    : 1, | 
|  | 631     } | 
|  | 632 | 
|  | 633     blackList = ["atp"] # No jokes allowed! | 
|  | 634     missingInDataset = ["co2"] | 
|  | 635 | 
|  | 636     normalRpsShape = MatchingShape({ | 
|  | 637         "r1" : ExactValue(8.2 ** 1), | 
|  | 638         "r2" : ExactValue((1 ** 2) * (5.3 ** 3)), | 
|  | 639         "r3" : ExactValue((8.2 ** 4) * (7.05 ** 2)), | 
|  | 640         "r4" : SatisfiesPredicate(lambda n : math.isnan(n)) | 
|  | 641     }, "rps dict") | 
|  | 642 | 
|  | 643     UnitTester("rps_generator", LogMode.Pedantic, False, | 
|  | 644         UnitTest(rps.get_abund_data, [dataset, 0], MatchingShape({ | 
|  | 645             "pyru_vate" : ExactValue(5.3), | 
|  | 646             "glu,cose"  : ExactValue(8.2), | 
|  | 647             "unknown"   : ExactValue(3.0), | 
|  | 648             "()atp"     : ExactValue(7.05), | 
|  | 649             "name"      : ExactValue("normal") | 
|  | 650         }, "abundance series")), | 
|  | 651 | 
|  | 652         UnitTest(rps.get_abund_data, [dataset, 1], MatchingShape({ | 
|  | 653             "pyru_vate" : ExactValue(7.01), | 
|  | 654             "glu,cose"  : ExactValue(4.0), | 
|  | 655             "unknown"   : ExactValue(3.97), | 
|  | 656             "()atp"     : ExactValue(8.83), | 
|  | 657             "name"      : ExactValue("cancer") | 
|  | 658         }, "abundance series")), | 
|  | 659 | 
|  | 660         UnitTest(rps.get_abund_data, [dataset, -1], ExactValue(None)), | 
|  | 661 | 
|  | 662         UnitTest(rps.check_missing_metab, [reactionsDict, abundancesNormal.copy()], Many(MatchingShape({ | 
|  | 663             "pyr"    : ExactValue(5.3), | 
|  | 664             "glc__D" : ExactValue(8.2), | 
|  | 665             "atp"    : ExactValue(7.05), | 
|  | 666             "co2"    : ExactValue(1) | 
|  | 667         }, "updated abundances"), Many(ExactValue("co2")))), | 
|  | 668 | 
|  | 669         UnitTest(rps.clean_metabolite_name, ["4,4'-diphenylmethane diisocyanate"], ExactValue("44diphenylmethanediisocyanate")), | 
|  | 670 | 
|  | 671         UnitTest(rps.get_metabolite_id, ["tryptophan", synsDict], ExactValue("trp__L")), | 
|  | 672 | 
|  | 673         UnitTest(rps.calculate_rps, [reactionsDict, abundancesNormalEdited, blackList, missingInDataset], normalRpsShape), | 
|  | 674 | 
|  | 675         UnitTest(rps.rps_for_cell_lines, [dataset, reactionsDict, blackList, synsDict, "", True], Many(normalRpsShape, MatchingShape({ | 
|  | 676             "r1" : ExactValue(4.0 ** 1), | 
|  | 677             "r2" : ExactValue((1 ** 2) * (7.01 ** 3)), | 
|  | 678             "r3" : ExactValue((4.0 ** 4) * (8.83 ** 2)), | 
|  | 679             "r4" : SatisfiesPredicate(lambda n : math.isnan(n)) | 
|  | 680         }, "rps dict"))), | 
|  | 681 | 
|  | 682         #UnitTest(rps.main, [], ExactValue(None)) # Complains about sys argvs | 
|  | 683     ).testModule() | 
|  | 684 | 
|  | 685 def unit_custom_data_generator() -> None: | 
|  | 686     import custom_data_generator as cdg | 
|  | 687 | 
|  | 688     UnitTester("custom data generator", LogMode.Pedantic, False, | 
|  | 689         UnitTest(lambda :True, [], ExactValue(True)), # No tests can be done without a model at hand! | 
|  | 690     ).testModule() | 
|  | 691 | 
|  | 692 def unit_utils() -> None: | 
|  | 693     import utils.general_utils as utils | 
|  | 694     import utils.rule_parsing as ruleUtils | 
|  | 695     import utils.reaction_parsing as reactionUtils | 
|  | 696 | 
|  | 697     UnitTester("utils", LogMode.Pedantic, False, | 
|  | 698         UnitTest(utils.CustomErr, ["myMsg", "more details"], MatchingShape({ | 
|  | 699             "details" : ExactValue("more details"), | 
|  | 700             "msg"     : ExactValue("myMsg"), | 
|  | 701             "id"      : ExactValue(0) # this will fail if any custom errors happen anywhere else before! | 
|  | 702         })), | 
|  | 703 | 
|  | 704         UnitTest(utils.CustomErr, ["myMsg", "more details", 42], MatchingShape({ | 
|  | 705             "details" : ExactValue("more details"), | 
|  | 706             "msg"     : ExactValue("myMsg"), | 
|  | 707             "id"      : ExactValue(42) | 
|  | 708         })), | 
|  | 709 | 
|  | 710         UnitTest(utils.Bool("someArg").check, ["TrUe"],  ExactValue(True)), | 
|  | 711         UnitTest(utils.Bool("someArg").check, ["FALse"], ExactValue(False)), | 
|  | 712         UnitTest(utils.Bool("someArg").check, ["foo"],   Exists(False)), # should panic! | 
|  | 713 | 
|  | 714         UnitTest(utils.Model.ENGRO2.getRules, ["."], IsOfType(dict)), | 
|  | 715         UnitTest(utils.Model.Custom.getRules, [".", ""], Exists(False)), # expected panic | 
|  | 716 | 
|  | 717         # rule utilities tests: | 
|  | 718         UnitTest(ruleUtils.parseRuleToNestedList, ["A"], Many(ExactValue("A"))), | 
|  | 719         UnitTest(ruleUtils.parseRuleToNestedList, ["A or B"],  Many(ExactValue("A"), ExactValue("B"))), | 
|  | 720         UnitTest(ruleUtils.parseRuleToNestedList, ["A and B"], Many(ExactValue("A"), ExactValue("B"))), | 
|  | 721         UnitTest(ruleUtils.parseRuleToNestedList, ["A foo B"], Exists(False)), # expected panic | 
|  | 722         UnitTest(ruleUtils.parseRuleToNestedList, ["A)"], Exists(False)), # expected panic | 
|  | 723 | 
|  | 724         UnitTest( | 
|  | 725             ruleUtils.parseRuleToNestedList, ["A or B"], | 
|  | 726             MatchingShape({ "op" : ExactValue(ruleUtils.RuleOp.OR)})), | 
|  | 727 | 
|  | 728         UnitTest( | 
|  | 729             ruleUtils.parseRuleToNestedList, ["A and B"], | 
|  | 730             MatchingShape({ "op" : ExactValue(ruleUtils.RuleOp.AND)})), | 
|  | 731 | 
|  | 732         UnitTest( | 
|  | 733             ruleUtils.parseRuleToNestedList, ["A or B and C"], | 
|  | 734             MatchingShape({ "op" : ExactValue(ruleUtils.RuleOp.OR)})), | 
|  | 735 | 
|  | 736         UnitTest( | 
|  | 737             ruleUtils.parseRuleToNestedList, ["A or B and C or (D and E)"], | 
|  | 738             Many( | 
|  | 739                 ExactValue("A"), | 
|  | 740                 Many(ExactValue("B"), ExactValue("C")), | 
|  | 741                 Many(ExactValue("D"), ExactValue("E")) | 
|  | 742             )), | 
|  | 743 | 
|  | 744         UnitTest(lambda s : ruleUtils.RuleOp(s), ["or"],  ExactValue(ruleUtils.RuleOp.OR)), | 
|  | 745         UnitTest(lambda s : ruleUtils.RuleOp(s), ["and"], ExactValue(ruleUtils.RuleOp.AND)), | 
|  | 746         UnitTest(lambda s : ruleUtils.RuleOp(s), ["foo"], Exists(False)), # expected panic | 
|  | 747 | 
|  | 748         UnitTest(ruleUtils.RuleOp.isOperator, ["or"],  ExactValue(True)), | 
|  | 749         UnitTest(ruleUtils.RuleOp.isOperator, ["and"], ExactValue(True)), | 
|  | 750         UnitTest(ruleUtils.RuleOp.isOperator, ["foo"], ExactValue(False)), | 
|  | 751 | 
|  | 752         # reaction utilities tests: | 
|  | 753         UnitTest(reactionUtils.ReactionDir.fromReaction, ["atp <=> adp + pi"], ExactValue(reactionUtils.ReactionDir.REVERSIBLE)), | 
|  | 754         UnitTest(reactionUtils.ReactionDir.fromReaction, ["atp --> adp + pi"], ExactValue(reactionUtils.ReactionDir.FORWARD)), | 
|  | 755         UnitTest(reactionUtils.ReactionDir.fromReaction, ["atp <-- adp + pi"], ExactValue(reactionUtils.ReactionDir.BACKWARD)), | 
|  | 756         UnitTest(reactionUtils.ReactionDir.fromReaction, ["atp ??? adp + pi"], Exists(False)), # should panic | 
|  | 757 | 
|  | 758         UnitTest( | 
|  | 759             reactionUtils.create_reaction_dict, | 
|  | 760             [{'shdgd': '2 pyruvate + 1 h2o <=> 1 h2o + 2 acetate', 'sgwrw': '2 co2 + 6 h2o --> 3 atp'}], | 
|  | 761             MatchingShape({ | 
|  | 762                 "shdgd_B" : MatchingShape({ | 
|  | 763                     "acetate" : ExactValue(2), | 
|  | 764                     "h2o" : ExactValue(1), | 
|  | 765                 }), | 
|  | 766 | 
|  | 767                 "shdgd_F" : MatchingShape({ | 
|  | 768                     "pyruvate" : ExactValue(2), | 
|  | 769                     "h2o" : ExactValue(1) | 
|  | 770                 }), | 
|  | 771 | 
|  | 772                 "sgwrw" : MatchingShape({ | 
|  | 773                     "co2" : ExactValue(2), | 
|  | 774                     "h2o" : ExactValue(6), | 
|  | 775                 }) | 
|  | 776             }, "reaction dict")), | 
|  | 777     ).testModule() | 
|  | 778 | 
|  | 779     rule = "A and B or C or D and (E or F and G) or H" | 
|  | 780     print(f"rule \"{rule}\" should comes out as: {ruleUtils.parseRuleToNestedList(rule)}") | 
|  | 781 | 
|  | 782 def unit_ras_generator() -> None: | 
|  | 783     import ras_generator as ras | 
|  | 784     import utils.rule_parsing as ruleUtils | 
|  | 785 | 
|  | 786     # Making an alias to mask the name of the inner function and separate the 2 tests: | 
|  | 787     def opListAlias(op_list, dataset): | 
|  | 788         ras.ARGS.none = False | 
|  | 789         return ras.ras_op_list(op_list, dataset) | 
|  | 790 | 
|  | 791     ras.ARGS = ras.process_args() | 
|  | 792     rule = ruleUtils.OpList(ruleUtils.RuleOp.AND) | 
|  | 793     rule.extend(["foo", "bar", "baz"]) | 
|  | 794 | 
|  | 795     dataset = { "foo" : 5, "bar" : 2, "baz" : None } | 
|  | 796 | 
|  | 797     UnitTester("ras generator", LogMode.Pedantic, False, | 
|  | 798         UnitTest(ras.ras_op_list, [rule, dataset], ExactValue(2)), | 
|  | 799         UnitTest(opListAlias, [rule, dataset], ExactValue(None)), | 
|  | 800     ).testModule() | 
|  | 801 | 
|  | 802 if __name__ == "__main__": | 
|  | 803     unit_cobraxy() | 
|  | 804     unit_custom_data_generator() | 
|  | 805     unit_utils() | 
|  | 806     unit_ras_generator() |