| 
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() |