Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 = "test_%s_%s" % (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
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("global attribute %s should have been"
69 " falsy after the test, but was instead %s"
70 % (globname, 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, "%s_output.txt" % name])
85 if name not in imported else None)
86 with StdoutCaptured(logfilepath=filepath):
87 imported[name] = importlib.import_module(name)
88 getattr(self, name)(imported[name])
89 return test
92def run_tests(tests, xmloutput=None, verbosity=2):
93 """Default way to run tests, to be used in __main__.
95 Arguments
96 ---------
97 tests: iterable of unittest.TestCase
98 xmloutput: string or None
99 if not None, generate xml output for continuous integration,
100 with name given by the input string
101 verbosity: int
102 verbosity level for unittest.TextTestRunner
103 """
104 suite = unittest.TestSuite()
105 loader = unittest.TestLoader()
106 for t in tests:
107 suite.addTests(loader.loadTestsFromTestCase(t))
108 if xmloutput:
109 import xmlrunner # pylint: disable=import-error
110 xmlrunner.XMLTestRunner(output=xmloutput).run(suite)
111 else: # pragma: no cover
112 unittest.TextTestRunner(verbosity=verbosity).run(suite)
115class NullFile:
116 "A fake file interface that does nothing"
117 def write(self, string):
118 "Do not write, do not pass go."
120 def close(self):
121 "Having not written, cease."
124class NewDefaultSolver:
125 "Creates an environment with a different default solver"
126 def __init__(self, solver):
127 self.solver = solver
128 self.prev_default_solver = None
130 def __enter__(self):
131 "Change default solver."
132 import gpkit
133 self.prev_default_solver = gpkit.settings["default_solver"]
134 gpkit.settings["default_solver"] = self.solver
136 def __exit__(self, *args):
137 "Reset default solver."
138 import gpkit
139 gpkit.settings["default_solver"] = self.prev_default_solver
142class StdoutCaptured:
143 "Puts everything that would have printed to stdout in a log file instead"
144 def __init__(self, logfilepath=None):
145 self.logfilepath = logfilepath
146 self.original_stdout = None
147 self.original_unit_printing = None
149 def __enter__(self):
150 "Capture stdout"
151 self.original_stdout = sys.stdout
152 sys.stdout = (open(self.logfilepath, mode="w")
153 if self.logfilepath else NullFile())
155 def __exit__(self, *args):
156 "Return stdout"
157 sys.stdout.close()
158 sys.stdout = self.original_stdout