Coverage for gpkit/tests/helpers.py: 100%

76 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-03 16:49 -0500

1"""Convenience classes and functions for unit testing""" 

2import unittest 

3import sys 

4import os 

5import importlib 

6 

7 

8def generate_example_tests(path, testclasses, solvers=None, newtest_fn=None): 

9 """ 

10 Mutate TestCase class so it behaves as described in TestExamples docstring 

11 

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 

42 

43 

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 

48 

49 def test(self): 

50 # pylint: disable=missing-docstring 

51 # No docstring because it'd be uselessly the same for each example 

52 

53 import gpkit 

54 with NewDefaultSolver(solver): 

55 testfn(name, import_dict, path)(self) 

56 

57 # clear modelnums to ensure deterministic script-like output! 

58 gpkit.globals.NamedVariables.reset_modelnumbers() 

59 

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 

72 

73 

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. 

78 

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

93 

94 

95def run_tests(tests, xmloutput=None, verbosity=2): 

96 """Default way to run tests, to be used in __main__. 

97 

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 

113 xmlrunner.XMLTestRunner(output=xmloutput).run(suite) 

114 else: # pragma: no cover 

115 unittest.TextTestRunner(verbosity=verbosity).run(suite) 

116 

117 

118class NullFile: 

119 "A fake file interface that does nothing" 

120 def write(self, string): 

121 "Do not write, do not pass go." 

122 

123 def close(self): 

124 "Having not written, cease." 

125 

126 

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 

132 

133 def __enter__(self): 

134 "Change default solver." 

135 import gpkit 

136 self.prev_default_solver = gpkit.settings["default_solver"] 

137 gpkit.settings["default_solver"] = self.solver 

138 

139 def __exit__(self, *args): 

140 "Reset default solver." 

141 import gpkit 

142 gpkit.settings["default_solver"] = self.prev_default_solver 

143 

144 

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 

151 

152 def __enter__(self): 

153 "Capture stdout" 

154 self.original_stdout = sys.stdout 

155 sys.stdout = (open(self.logfilepath, mode="w") 

156 if self.logfilepath else NullFile()) 

157 

158 def __exit__(self, *args): 

159 "Return stdout" 

160 sys.stdout.close() 

161 sys.stdout = self.original_stdout