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