Mercurial > repos > bimib > cobraxy
comparison COBRAxy/test/testing.py @ 547:73f2f7e2be17 draft
Uploaded
| author | francesco_lapi | 
|---|---|
| date | Tue, 28 Oct 2025 10:44:07 +0000 | 
| parents | |
| children | 
   comparison
  equal
  deleted
  inserted
  replaced
| 546:01147e83f43c | 547:73f2f7e2be17 | 
|---|---|
| 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() | 
