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