Mercurial > repos > bimib > cobraxy
comparison COBRAxy/testing.py @ 4:41f35c2f0c7b draft
Uploaded
| author | luca_milaz |
|---|---|
| date | Wed, 18 Sep 2024 10:59:10 +0000 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 3:1f3ac6fd9867 | 4:41f35c2f0c7b |
|---|---|
| 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() |
