Mercurial > repos > bimib > cobraxy
view COBRAxy/testing.py @ 178:824b1ecc29db draft
Uploaded
author | francesco_lapi |
---|---|
date | Wed, 20 Nov 2024 14:59:42 +0000 |
parents | 41f35c2f0c7b |
children |
line wrap: on
line source
# This is a general-purpose "testing utilities" module for the COBRAxy tool. # This code was written entirely by m.ferrari133@campus.unimib.it and then (hopefully) many # more people contributed by writing tests for this tool's modules, feel free to send an email for # any questions. # How the testing module works: # The testing module allows you to easily set up unit tests for functions in a module, obtaining # information on what each method returns, when and how it fails and so on. # How do I test a module? # - create a function at the very bottom, before the __main__ # - import the stuff you need # - create a UnitTester instance, follow the documentation # - fill it up with UnitTest instances, follow the documentation # - each UnitTest tests the function by passing specific parameters to it and by veryfing the correctness # of the output via a CheckingMode instance # - call testModule() on the UnitTester # TODO(s): # - This module was written before the utilities were introduced, it may want to use some of those functions. # - I never got around to writing a CheckingMode for methods you WANT to fail in certain scenarios, I # like the name "MustPanic". # - It's good practice to enforce boolean arguments of a function to be passed as kwargs and I did it a lot # in the code I wrote for these tool's modules, but the current implementation of UnitTest doesn't allow # you to pass kwargs to the functions you test. # - Implement integration tests as well, maybe! ## Imports: from typing import Dict, Callable, Type, List from enum import Enum, auto from collections.abc import Iterable ## Generic utilities: class TestResult: """ Represents the result of a test and contains all the relevant information about it. Loosely models two variants: - Ok: The test passed, no further information is saved besides the target's name. - Err: The test failed, an error message and further contextual details are also saved. This class does not ensure a static proof of the two states' behaviour, their meaning or mutual exclusivity outside of the :bool property "isPass", meant for outside reads. """ def __init__(self, isPass :bool, targetName :str, errMsg = "", details = "") -> None: """ (Private) Initializes an instance of TestResult. Args: isPass : distinction between TestResult.Ok (True) and TestResult.Err (False). targetName : the name of the target object / property / function / module being tested, not always set to a meaningful value at this stage. errMsg : concise error message explaining the test's failure. details : contextual details about the error. Returns: None : practically, a TestResult instance. """ self.isPass = isPass self.isFail = not isPass # Convenience above all self.targetName = targetName if isPass: return self.errMsg = errMsg self.details = details @classmethod def Ok(cls, targetName = "") -> "TestResult": """ Factory method for TestResult.Ok, where all we need to know is that our test passed. Args: targetName : the name of the target object / property / function / module being tested, not always set to a meaningful value at this stage. Returns: TestResult : a new Ok instance. """ return cls(True, targetName) @classmethod def Err(cls, errMsg :str, details :str, targetName = "") -> "TestResult": """ Factory method for TestResult.Err, where we store relevant error information. Args: errMsg : concise error message explaining the test's failure. details : contextual details about the error. targetName : the name of the target object / property / function / module being tested, not always set to a meaningful value at this stage. Returns: TestResult : a new Err instance. """ return cls(False, targetName, errMsg, details) def log(self, isCompact = True) -> str: """ Dumps all the available information in a :str, ready for logging. Args: isCompact : if True limits the amount of information displayed to the targetName. Returns: str : information about this test result. """ if isCompact: return f"{TestResult.__name__}::{'Ok' if self.isPass else 'Err'}(Unit test on {self.targetName})" logMsg = f"Unit test on {self.targetName} {'passed' if self.isPass else f'failed because {self.errMsg}'}" if self.details: logMsg += f", {self.details}" return logMsg def throw(self) -> None: #TODO: finer Exception typing would be desirable """ Logs the result information and panics. Raises: Exception : an error containing log information about the test result. Returns: None """ raise Exception(self.log()) class CheckingMode: """ (Private) Represents a way to check a value for correctness, in the context of "testing" it. """ def __init__(self) -> None: """ (Private) Implemented on child classes, initializes an instance of CheckingMode. Returns: None : practically, a CheckingMode instance. """ self.logMsg = "CheckingMode base class should not be used directly" def __checkPasses__(self, _) -> bool: """ (Private) Implemented on child classes, performs the actual correctness check on a received value. Returns: bool : True if the check passed, False if it failed. """ return True def check(self, value) -> TestResult: """ Converts the :bool evaluation of the value's correctness to a TestResult. Args: value : the value to check. Returns: TestResult : the result of the check. """ return TestResult.Ok() if self.__checkPasses__(value) else TestResult.Err(self.logMsg, f"got {value} instead") def __repr__(self) -> str: """ (Private) Implemented on child classes, formats :object as :str. """ return self.__class__.__name__ class ExactValue(CheckingMode): """ CheckingMode subclass variant to be used when the checked value needs to match another exactly. """ #I suggest solving the more complex equality checking edge cases with the "Satisfies" and "MatchingShape" variants. def __init__(self, value) -> None: self.value = value self.logMsg = f"value needed to match {value} exactly" def __checkPasses__(self, value) -> bool: return self.value == value def __repr__(self) -> str: return f"{super().__repr__()}({self.value})" class AcceptedValues(CheckingMode): """ CheckingMode subclass variant to be used when the checked value needs to appear in a list of accepted values. """ def __init__(self, *values) -> None: self.values = values self.logMsg = f"value needed to be one of these: {values}" def __checkPasses__(self, value) -> bool: return value in self.values def __repr__(self) -> str: return f"{super().__repr__()}{self.values}" class SatisfiesPredicate(CheckingMode): """ CheckingMode subclass variant to be used when the checked value needs to verify a given predicate, as in the predicate accepts it as input and returns True. """ def __init__(self, pred :Callable[..., bool], predName = "") -> None: self.pred = pred self.logMsg = f"value needed to verify a predicate{bool(predName) * f' called {predName}'}" def __checkPasses__(self, *params) -> bool: return self.pred(*params) def __repr__(self) -> str: return f"{super().__repr__()}(T) -> bool" class IsOfType(CheckingMode): """ CheckingMode subclass variant to be used when the checked value needs to be of a certain type. """ def __init__(self, type :Type) -> None: self.type = type self.logMsg = f"value needed to be of type {type.__name__}" def __checkPasses__(self, value :Type) -> bool: return isinstance(value, self.type) def __repr__(self) -> str: return f"{super().__repr__()}:{self.type.__name__}" class Exists(CheckingMode): """ CheckingMode subclass variant to be used when the checked value needs to exist (or not!). Mainly employed as a quick default check that always passes, it still upholds its contract when it comes to checking for existing properties in objects without much concern on what value they contain. """ def __init__(self, exists = True) -> None: self.exists = exists self.logMsg = f"value needed to {(not exists) * 'not '}exist" def __checkPasses__(self, _) -> bool: return self.exists def __repr__(self) -> str: return f"{super().__repr__() if self.exists else 'IsMissing'}" class MatchingShape(CheckingMode): """ CheckingMode subclass variant to be used when the checked value is an object that needs to have a certain shape, as in to posess properties with a given name and value. Each property is checked for existance and correctness with its own given CheckingMode. """ def __init__(self, props :Dict[str, CheckingMode], objName = "") -> None: """ (Private) Initializes an instance of MatchingShape. Args: props : :dict using property names as keys and checking modes for the property's value as values. objName : label for the object we're testing the shape of. Returns: None : practically, a MatchingShape instance. """ self.props = props self.objName = objName self.shapeRepr = " {\n" + "\n".join([f" {propName} : {prop}" for propName, prop in props.items()]) + "\n}" def check(self, obj :object) -> TestResult: objIsDict = isinstance(obj, dict) # Python forces us to distinguish between object properties and dict keys for propName, checkingMode in self.props.items(): # Checking if the property exists: if (not objIsDict and not hasattr(obj, propName)) or (objIsDict and propName not in obj): if not isinstance(checkingMode, Exists): return TestResult.Err( f"property \"{propName}\" doesn't exist on object {self.objName}", "", self.objName) if not checkingMode.exists: return TestResult.Ok(self.objName) # Either the property value is meant to be checked (checkingMode is anything but Exists) # or we want the property to not exist, all other cases are handled correctly ahead checkRes = checkingMode.check(obj[propName] if objIsDict else getattr(obj, propName)) if checkRes.isPass: continue checkRes.targetName = self.objName return TestResult.Err( f"property \"{propName}\" failed check {checkingMode} on shape {obj}", checkRes.log(isCompact = False), self.objName) return TestResult.Ok(self.objName) def __repr__(self) -> str: return super().__repr__() + self.shapeRepr class Many(CheckingMode): """ CheckingMode subclass variant to be used when the checked value is an Iterable we want to check item by item. """ def __init__(self, *values :CheckingMode) -> None: self.values = values self.shapeRepr = " [\n" + "\n".join([f" {value}" for value in values]) + "\n]" def check(self, coll :Iterable) -> TestResult: amt = len(coll) expectedAmt = len(self.values) # Length equality is forced: if amt != expectedAmt: return TestResult.Err( "items' quantities don't match", f"expected {expectedAmt} items, but got {amt}") # Items in the given collection value are paired in order with the corresponding checkingMode meant for each of them for item, checkingMode in zip(coll, self.values): checkRes = checkingMode.check(item) if checkRes.isFail: return TestResult.Err( f"item in list failed check {checkingMode}", checkRes.log(isCompact = False)) return TestResult.Ok() def __repr__(self) -> str: return super().__repr__() + self.shapeRepr class LogMode(Enum): """ Represents the level of detail of a logged message. Models 4 variants, in order of increasing detail: - Minimal : Logs the overall test result for the entire module. - Default : Also logs all single test fails, in compact mode. - Detailed : Logs all function test results, in compact mode. - Pedantic : Also logs all single test results in detailed mode. """ Minimal = auto() Default = auto() Detailed = auto() Pedantic = auto() def isMoreVerbose(self, requiredMode :"LogMode") -> bool: """ Compares the instance's level of detail with that of another. Args: requiredMode : the other instance. Returns: bool : True if the caller instance is a more detailed variant than the other. """ return self.value >= requiredMode.value ## Specific Unit Testing utilities: class UnitTest: """ Represents a unit test, the test of a single function's isolated correctness. """ def __init__(self, func :Callable, inputParams :list, expectedRes :CheckingMode) -> None: """ (Private) Initializes an instance of UnitTest. Args: func : the function to test. inputParams : list of parameters to pass as inputs to the function, in order. expectedRes : checkingMode to test the function's return value for correctness. Returns: None : practically, a UnitTest instance. """ self.func = func self.inputParams = inputParams self.expectedRes = expectedRes self.funcName = func.__name__ def test(self) -> TestResult: """ Tests the function. Returns: TestResult : the test's result. """ result = None try: result = self.func(*self.inputParams) except Exception as e: return TestResult.Err("the function panicked at runtime", e, self.funcName) checkRes = self.expectedRes.check(result) checkRes.targetName = self.funcName return checkRes class UnitTester: """ Manager class for unit testing an entire module, groups single UnitTests together and executes them in order on a per-function basis (tests about the same function are executed consecutively) giving back as much information as possible depending on the selected logMode. More customization options are available. """ def __init__(self, moduleName :str, logMode = LogMode.Default, stopOnFail = True, *funcTests :'UnitTest') -> None: """ (Private) initializes an instance of UnitTester. Args: moduleName : name of the tested module. logMode : level of detail applied to all messages logged during the test. stopOnFail : if True, the test stops entirely after one unit test fails. funcTests : the unit tests to perform on the module. Returns: None : practically, a UnitTester instance. """ self.logMode = logMode self.moduleName = moduleName self.stopOnFail = stopOnFail # This ensures the per-function order: self.funcTests :Dict[str, List[UnitTest]]= {} for test in funcTests: if test.funcName in self.funcTests: self.funcTests[test.funcName].append(test) else: self.funcTests[test.funcName] = [test] def logTestResult(self, testRes :TestResult) -> None: """ Prints the formatted result information of a unit test. Args: testRes : the result of the test. Returns: None """ if testRes.isPass: return self.log("Passed!", LogMode.Detailed, indent = 2) failMsg = "Failed! " # Doing it this way prevents .log computations when not needed if self.logMode.isMoreVerbose(LogMode.Detailed): # Given that Pedantic is the most verbose variant, there's no point in comparing with LogMode.isMoreVerbose failMsg += testRes.log(self.logMode is not LogMode.Pedantic) self.log(failMsg, indent = 2) def log(self, msg :str, minRequiredMode = LogMode.Default, indent = 0) -> None: """ Prints and formats a message only when the UnitTester instance is set to a level of detail at least equal to a minimum requirement, given as input. Args: msg : the message to print. minRequiredMode : minimum detail requirement. indent : formatting information, counter from 0 that adds 2 spaces each number up Returns: None """ if self.logMode.isMoreVerbose(minRequiredMode): print(" " * indent + msg) def testFunction(self, name :str) -> TestResult: """ Perform all unit tests relative to the same function, plus the surrounding logs and checks. Args: name : the name of the tested function. Returns : TestResult : the overall Ok result of all the tests passing or the first Err. This behaviour is unrelated to that of the overall testing procedure (stopOnFail), it always works like this for tests about the same function. """ self.log(f"Unit testing {name}...", indent = 1) allPassed = True for unitTest in self.funcTests[name]: testRes = unitTest.test() self.logTestResult(testRes) if testRes.isPass: continue allPassed = False if self.stopOnFail: break self.log("", LogMode.Detailed) # Provides one extra newline of space when needed, to better format the output if allPassed: return TestResult.Ok(name) if self.logMode is LogMode.Default: self.log("") return TestResult.Err(f"Unlogged err", "unit test failed", name) def testModule(self) -> None: """ Runs all the provided unit tests in order but on a per-function basis. Returns: None """ self.log(f"Unit testing module {self.moduleName}...", LogMode.Minimal) fails = 0 testStatusMsg = "complete" for funcName in self.funcTests.keys(): if self.testFunction(funcName).isPass: continue fails += 1 if self.stopOnFail: testStatusMsg = "interrupted" break self.log(f"Testing {testStatusMsg}: {fails} problem{'s' * (fails != 1)} found.\n", LogMode.Minimal) # ^^^ Manually applied an extra newline of space. ## Unit testing all the modules: def unit_cobraxy() -> None: import cobraxy as m import math import lxml.etree as ET import utils.general_utils as utils #m.ARGS = m.process_args() ids = ["react1", "react2", "react3", "react4", "react5"] metabMap = utils.Model.ENGRO2.getMap() class_pat = { "dataset1" :[ [2.3, 4, 7, 0, 0.01, math.nan, math.nan], [math.nan, math.nan, math.nan, math.nan, math.nan, math.nan, math.nan], [2.3, 4, 7, 0, 0.01, 5, 9], [math.nan, math.nan, 2.3, 4, 7, 0, 0.01], [2.3, 4, 7, math.nan, 2.3, 0, 0.01]], "dataset2" :[ [2.3, 4, 7, math.nan, 2.3, 0, 0.01], [2.3, 4, 7, 0, 0.01, math.nan, math.nan], [math.nan, math.nan, 2.3, 4, 7, 0, 0.01], [2.3, 4, 7, 0, 0.01, 5, 9], [math.nan, math.nan, math.nan, math.nan, math.nan, math.nan, math.nan]] } unitTester = UnitTester("cobraxy", LogMode.Pedantic, False, UnitTest(m.name_dataset, ["customName", 12], ExactValue("customName")), UnitTest(m.name_dataset, ["Dataset", 12], ExactValue("Dataset_12")), UnitTest(m.fold_change, [0.5, 0.5], ExactValue(0.0)), UnitTest(m.fold_change, [0, 0.35], ExactValue("-INF")), UnitTest(m.fold_change, [0.5, 0], ExactValue("INF")), UnitTest(m.fold_change, [0, 0], ExactValue(0)), UnitTest( m.Arrow(m.Arrow.MAX_W, m.ArrowColor.DownRegulated, isDashed = True).toStyleStr, [], ExactValue(";stroke:#0000FF;stroke-width:12;stroke-dasharray:5,5")), UnitTest(m.computeEnrichment, [metabMap, class_pat, ids], ExactValue(None)), UnitTest(m.computePValue, [class_pat["dataset1"][0], class_pat["dataset2"][0]], SatisfiesPredicate(math.isnan)), UnitTest(m.reactionIdIsDirectional, ["reactId"], ExactValue(m.ReactionDirection.Unknown)), UnitTest(m.reactionIdIsDirectional, ["reactId_F"], ExactValue(m.ReactionDirection.Direct)), UnitTest(m.reactionIdIsDirectional, ["reactId_B"], ExactValue(m.ReactionDirection.Inverse)), UnitTest(m.ArrowColor.fromFoldChangeSign, [-2], ExactValue(m.ArrowColor.DownRegulated)), UnitTest(m.ArrowColor.fromFoldChangeSign, [2], ExactValue(m.ArrowColor.UpRegulated)), UnitTest( m.Arrow(m.Arrow.MAX_W, m.ArrowColor.UpRegulated).styleReactionElements, [metabMap, "reactId"], ExactValue(None)), UnitTest(m.getArrowBodyElementId, ["reactId"], ExactValue("R_reactId")), UnitTest(m.getArrowBodyElementId, ["reactId_F"], ExactValue("R_reactId")), UnitTest( m.getArrowHeadElementId, ["reactId"], Many(ExactValue("F_reactId"), ExactValue("B_reactId"))), UnitTest( m.getArrowHeadElementId, ["reactId_F"], Many(ExactValue("F_reactId"), ExactValue(""))), UnitTest( m.getArrowHeadElementId, ["reactId_B"], Many(ExactValue("B_reactId"), ExactValue(""))), UnitTest( m.getElementById, ["reactId_F", metabMap], SatisfiesPredicate(lambda res : res.isErr and isinstance(res.value, utils.Result.ResultErr))), UnitTest( m.getElementById, ["F_tyr_L_t", metabMap], SatisfiesPredicate(lambda res : res.isOk and res.unwrap().get("id") == "F_tyr_L_t")), ).testModule() def unit_rps_generator() -> None: import rps_generator as rps import math import pandas as pd import utils.general_utils as utils dataset = pd.DataFrame({ "cell lines" : ["normal", "cancer"], "pyru_vate" : [5.3, 7.01], "glu,cose" : [8.2, 4.0], "unknown" : [3.0, 3.97], "()atp" : [7.05, 8.83], }) abundancesNormalRaw = { "pyru_vate" : 5.3, "glu,cose" : 8.2, "unknown" : 3.0, "()atp" : 7.05, } abundancesNormal = { "pyr" : 5.3, "glc__D" : 8.2, "atp" : 7.05, } # TODO: this currently doesn't work due to "the pickle extension problem", see FileFormat class for details. synsDict = utils.readPickle(utils.FilePath("synonyms", utils.FileFormat.PICKLE, prefix = "./local/pickle files")) reactionsDict = { "r1" : { "glc__D" : 1 }, "r2" : { "co2" : 2, "pyr" : 3, }, "r3" : { "atp" : 2, "glc__D" : 4, }, "r4" : { "atp" : 3, } } abundancesNormalEdited = { "pyr" : 5.3, "glc__D" : 8.2, "atp" : 7.05, "co2" : 1, } blackList = ["atp"] # No jokes allowed! missingInDataset = ["co2"] normalRpsShape = MatchingShape({ "r1" : ExactValue(8.2 ** 1), "r2" : ExactValue((1 ** 2) * (5.3 ** 3)), "r3" : ExactValue((8.2 ** 4) * (7.05 ** 2)), "r4" : SatisfiesPredicate(lambda n : math.isnan(n)) }, "rps dict") UnitTester("rps_generator", LogMode.Pedantic, False, UnitTest(rps.get_abund_data, [dataset, 0], MatchingShape({ "pyru_vate" : ExactValue(5.3), "glu,cose" : ExactValue(8.2), "unknown" : ExactValue(3.0), "()atp" : ExactValue(7.05), "name" : ExactValue("normal") }, "abundance series")), UnitTest(rps.get_abund_data, [dataset, 1], MatchingShape({ "pyru_vate" : ExactValue(7.01), "glu,cose" : ExactValue(4.0), "unknown" : ExactValue(3.97), "()atp" : ExactValue(8.83), "name" : ExactValue("cancer") }, "abundance series")), UnitTest(rps.get_abund_data, [dataset, -1], ExactValue(None)), UnitTest(rps.check_missing_metab, [reactionsDict, abundancesNormal.copy()], Many(MatchingShape({ "pyr" : ExactValue(5.3), "glc__D" : ExactValue(8.2), "atp" : ExactValue(7.05), "co2" : ExactValue(1) }, "updated abundances"), Many(ExactValue("co2")))), UnitTest(rps.clean_metabolite_name, ["4,4'-diphenylmethane diisocyanate"], ExactValue("44diphenylmethanediisocyanate")), UnitTest(rps.get_metabolite_id, ["tryptophan", synsDict], ExactValue("trp__L")), UnitTest(rps.calculate_rps, [reactionsDict, abundancesNormalEdited, blackList, missingInDataset], normalRpsShape), UnitTest(rps.rps_for_cell_lines, [dataset, reactionsDict, blackList, synsDict, "", True], Many(normalRpsShape, MatchingShape({ "r1" : ExactValue(4.0 ** 1), "r2" : ExactValue((1 ** 2) * (7.01 ** 3)), "r3" : ExactValue((4.0 ** 4) * (8.83 ** 2)), "r4" : SatisfiesPredicate(lambda n : math.isnan(n)) }, "rps dict"))), #UnitTest(rps.main, [], ExactValue(None)) # Complains about sys argvs ).testModule() def unit_custom_data_generator() -> None: import custom_data_generator as cdg UnitTester("custom data generator", LogMode.Pedantic, False, UnitTest(lambda :True, [], ExactValue(True)), # No tests can be done without a model at hand! ).testModule() def unit_utils() -> None: import utils.general_utils as utils import utils.rule_parsing as ruleUtils import utils.reaction_parsing as reactionUtils UnitTester("utils", LogMode.Pedantic, False, UnitTest(utils.CustomErr, ["myMsg", "more details"], MatchingShape({ "details" : ExactValue("more details"), "msg" : ExactValue("myMsg"), "id" : ExactValue(0) # this will fail if any custom errors happen anywhere else before! })), UnitTest(utils.CustomErr, ["myMsg", "more details", 42], MatchingShape({ "details" : ExactValue("more details"), "msg" : ExactValue("myMsg"), "id" : ExactValue(42) })), UnitTest(utils.Bool("someArg").check, ["TrUe"], ExactValue(True)), UnitTest(utils.Bool("someArg").check, ["FALse"], ExactValue(False)), UnitTest(utils.Bool("someArg").check, ["foo"], Exists(False)), # should panic! UnitTest(utils.Model.ENGRO2.getRules, ["."], IsOfType(dict)), UnitTest(utils.Model.Custom.getRules, [".", ""], Exists(False)), # expected panic # rule utilities tests: UnitTest(ruleUtils.parseRuleToNestedList, ["A"], Many(ExactValue("A"))), UnitTest(ruleUtils.parseRuleToNestedList, ["A or B"], Many(ExactValue("A"), ExactValue("B"))), UnitTest(ruleUtils.parseRuleToNestedList, ["A and B"], Many(ExactValue("A"), ExactValue("B"))), UnitTest(ruleUtils.parseRuleToNestedList, ["A foo B"], Exists(False)), # expected panic UnitTest(ruleUtils.parseRuleToNestedList, ["A)"], Exists(False)), # expected panic UnitTest( ruleUtils.parseRuleToNestedList, ["A or B"], MatchingShape({ "op" : ExactValue(ruleUtils.RuleOp.OR)})), UnitTest( ruleUtils.parseRuleToNestedList, ["A and B"], MatchingShape({ "op" : ExactValue(ruleUtils.RuleOp.AND)})), UnitTest( ruleUtils.parseRuleToNestedList, ["A or B and C"], MatchingShape({ "op" : ExactValue(ruleUtils.RuleOp.OR)})), UnitTest( ruleUtils.parseRuleToNestedList, ["A or B and C or (D and E)"], Many( ExactValue("A"), Many(ExactValue("B"), ExactValue("C")), Many(ExactValue("D"), ExactValue("E")) )), UnitTest(lambda s : ruleUtils.RuleOp(s), ["or"], ExactValue(ruleUtils.RuleOp.OR)), UnitTest(lambda s : ruleUtils.RuleOp(s), ["and"], ExactValue(ruleUtils.RuleOp.AND)), UnitTest(lambda s : ruleUtils.RuleOp(s), ["foo"], Exists(False)), # expected panic UnitTest(ruleUtils.RuleOp.isOperator, ["or"], ExactValue(True)), UnitTest(ruleUtils.RuleOp.isOperator, ["and"], ExactValue(True)), UnitTest(ruleUtils.RuleOp.isOperator, ["foo"], ExactValue(False)), # reaction utilities tests: UnitTest(reactionUtils.ReactionDir.fromReaction, ["atp <=> adp + pi"], ExactValue(reactionUtils.ReactionDir.REVERSIBLE)), UnitTest(reactionUtils.ReactionDir.fromReaction, ["atp --> adp + pi"], ExactValue(reactionUtils.ReactionDir.FORWARD)), UnitTest(reactionUtils.ReactionDir.fromReaction, ["atp <-- adp + pi"], ExactValue(reactionUtils.ReactionDir.BACKWARD)), UnitTest(reactionUtils.ReactionDir.fromReaction, ["atp ??? adp + pi"], Exists(False)), # should panic UnitTest( reactionUtils.create_reaction_dict, [{'shdgd': '2 pyruvate + 1 h2o <=> 1 h2o + 2 acetate', 'sgwrw': '2 co2 + 6 h2o --> 3 atp'}], MatchingShape({ "shdgd_B" : MatchingShape({ "acetate" : ExactValue(2), "h2o" : ExactValue(1), }), "shdgd_F" : MatchingShape({ "pyruvate" : ExactValue(2), "h2o" : ExactValue(1) }), "sgwrw" : MatchingShape({ "co2" : ExactValue(2), "h2o" : ExactValue(6), }) }, "reaction dict")), ).testModule() rule = "A and B or C or D and (E or F and G) or H" print(f"rule \"{rule}\" should comes out as: {ruleUtils.parseRuleToNestedList(rule)}") def unit_ras_generator() -> None: import ras_generator as ras import utils.rule_parsing as ruleUtils # Making an alias to mask the name of the inner function and separate the 2 tests: def opListAlias(op_list, dataset): ras.ARGS.none = False return ras.ras_op_list(op_list, dataset) ras.ARGS = ras.process_args() rule = ruleUtils.OpList(ruleUtils.RuleOp.AND) rule.extend(["foo", "bar", "baz"]) dataset = { "foo" : 5, "bar" : 2, "baz" : None } UnitTester("ras generator", LogMode.Pedantic, False, UnitTest(ras.ras_op_list, [rule, dataset], ExactValue(2)), UnitTest(opListAlias, [rule, dataset], ExactValue(None)), ).testModule() if __name__ == "__main__": unit_cobraxy() unit_custom_data_generator() unit_utils() unit_ras_generator()