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