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