Coverage for gpkit\tests\helpers.py: 0%
76 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-05 22:28 -0500
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-05 22:28 -0500
1"""Convenience classes and functions for unit testing"""
2import unittest
3import sys
4import os
5import importlib
8def generate_example_tests(path, testclasses, solvers=None, newtest_fn=None):
9 """
10 Mutate TestCase class so it behaves as described in TestExamples docstring
12 Arguments
13 ---------
14 path : str
15 directory containing example modules to test
16 testclass : class
17 class that inherits from `unittest.TestCase`
18 newtest_fn : function
19 function that returns new tests. defaults to import_test_and_log_output
20 solvers : iterable
21 solvers to run for; or only for default if solvers is None
22 """
23 import_dict = {}
24 if newtest_fn is None:
25 newtest_fn = new_test
26 tests = []
27 for testclass in testclasses:
28 if os.path.isdir(path):
29 sys.path.insert(0, path)
30 for fn in dir(testclass):
31 if fn[:5] == "test_":
32 name = fn[5:]
33 old_test = getattr(testclass, fn)
34 setattr(testclass, name, old_test) # move to a non-test fn
35 delattr(testclass, fn) # delete the old old_test
36 for solver in solvers:
37 new_name = f"test_{name}_{solver}"
38 new_fn = newtest_fn(name, solver, import_dict, path)
39 setattr(testclass, new_name, new_fn)
40 tests.append(testclass)
41 return tests
44def new_test(name, solver, import_dict, path, testfn=None):
45 """logged_example_testcase with a NewDefaultSolver"""
46 if testfn is None:
47 testfn = logged_example_testcase
49 def test(self):
50 # pylint: disable=missing-docstring
51 # No docstring because it'd be uselessly the same for each example
53 import gpkit # pylint: disable=import-outside-toplevel
54 with NewDefaultSolver(solver):
55 testfn(name, import_dict, path)(self)
57 # clear modelnums to ensure deterministic script-like output!
58 gpkit.globals.NamedVariables.reset_modelnumbers()
60 # check all global state is falsy
61 for globname, global_thing in [
62 ("model numbers", gpkit.globals.NamedVariables.modelnums),
63 ("lineage", gpkit.NamedVariables.lineage),
64 ("signomials enabled", gpkit.SignomialsEnabled),
65 ("vectorization", gpkit.Vectorize.vectorization),
66 ("namedvars", gpkit.NamedVariables.namedvars)]:
67 if global_thing: # pragma: no cover
68 raise ValueError(
69 f"global attribute {globname} should have been falsy after"
70 " the test, but was instead {global_thing}")
71 return test
74def logged_example_testcase(name, imported, path):
75 """Returns a method for attaching to a unittest.TestCase that imports
76 or reloads module 'name' and stores in imported[name].
77 Runs top-level code, which is typically a docs example, in the process.
79 Returns a method.
80 """
81 def test(self):
82 # pylint: disable=missing-docstring
83 # No docstring because it'd be uselessly the same for each example
84 filepath = ("".join([path, os.sep, f"{name}_output.txt"])
85 if name not in imported else None)
86 with StdoutCaptured(logfilepath=filepath):
87 if name in imported:
88 importlib.reload(imported[name])
89 else:
90 imported[name] = importlib.import_module(name)
91 getattr(self, name)(imported[name])
92 return test
95def run_tests(tests, xmloutput=None, verbosity=2):
96 """Default way to run tests, to be used in __main__.
98 Arguments
99 ---------
100 tests: iterable of unittest.TestCase
101 xmloutput: string or None
102 if not None, generate xml output for continuous integration,
103 with name given by the input string
104 verbosity: int
105 verbosity level for unittest.TextTestRunner
106 """
107 suite = unittest.TestSuite()
108 loader = unittest.TestLoader()
109 for t in tests:
110 suite.addTests(loader.loadTestsFromTestCase(t))
111 if xmloutput:
112 import xmlrunner # pylint: disable=import-error,import-outside-toplevel
113 xmlrunner.XMLTestRunner(output=xmloutput).run(suite)
114 else: # pragma: no cover
115 unittest.TextTestRunner(verbosity=verbosity).run(suite)
118class NullFile:
119 "A fake file interface that does nothing"
120 def write(self, string):
121 "Do not write, do not pass go."
123 def close(self):
124 "Having not written, cease."
127class NewDefaultSolver:
128 "Creates an environment with a different default solver"
129 def __init__(self, solver):
130 self.solver = solver
131 self.prev_default_solver = None
133 def __enter__(self):
134 "Change default solver."
135 import gpkit # pylint: disable=import-outside-toplevel
136 self.prev_default_solver = gpkit.settings["default_solver"]
137 gpkit.settings["default_solver"] = self.solver
139 def __exit__(self, *args):
140 "Reset default solver."
141 import gpkit # pylint: disable=import-outside-toplevel
142 gpkit.settings["default_solver"] = self.prev_default_solver
145class StdoutCaptured:
146 "Puts everything that would have printed to stdout in a log file instead"
147 def __init__(self, logfilepath=None):
148 self.logfilepath = logfilepath
149 self.original_stdout = None
150 self.original_unit_printing = None
152 def __enter__(self):
153 "Capture stdout"
154 self.original_stdout = sys.stdout
155 sys.stdout = (open(self.logfilepath, mode="w", encoding="UTF-8")
156 if self.logfilepath else NullFile())
158 def __exit__(self, *args):
159 "Return stdout"
160 sys.stdout.close()
161 sys.stdout = self.original_stdout