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