Skip to content

Commit 796a598

Browse files
committed
unittest: Move back to python-stdlib.
In order to make this more suitable for non-unix ports, the discovery functionality is moved to a separate 'extension' module which can be optionally installed. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
1 parent cb88a6a commit 796a598

File tree

13 files changed

+290
-225
lines changed

13 files changed

+290
-225
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
metadata(version="0.1.0")
2+
3+
require("argparse")
4+
require("fnmatch")
5+
require("unittest")
6+
7+
module("unittest_discover.py")
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Module that is used in both test_isolated_1.py and test_isolated_2.py.
2+
# The module should be clean reloaded for each.
3+
4+
state = None
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import unittest
2+
import isolated
3+
4+
5+
class TestUnittestIsolated1(unittest.TestCase):
6+
def test_NotChangedByOtherTest(self):
7+
self.assertIsNone(isolated.state)
8+
isolated.state = True
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import unittest
2+
import isolated
3+
4+
5+
class TestUnittestIsolated2(unittest.TestCase):
6+
def test_NotChangedByOtherTest(self):
7+
self.assertIsNone(isolated.state)
8+
isolated.state = True
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Extension for "unittest" that adds the ability to run via "micropython -m unittest".
2+
3+
import argparse
4+
import os
5+
import sys
6+
from fnmatch import fnmatch
7+
from micropython import const
8+
9+
from unittest import TestRunner, TestResult, TestSuite
10+
11+
12+
# Run a single test in a clean environment.
13+
def _run_test_module(runner: TestRunner, module_name: str, *extra_paths: list[str]):
14+
module_snapshot = {k: v for k, v in sys.modules.items()}
15+
path_snapshot = sys.path[:]
16+
try:
17+
for path in reversed(extra_paths):
18+
if path:
19+
sys.path.insert(0, path)
20+
21+
module = __import__(module_name)
22+
suite = TestSuite(module_name)
23+
suite._load_module(module)
24+
return runner.run(suite)
25+
finally:
26+
sys.path[:] = path_snapshot
27+
sys.modules.clear()
28+
sys.modules.update(module_snapshot)
29+
30+
31+
_DIR_TYPE = const(0x4000)
32+
33+
34+
def _run_all_in_dir(runner: TestRunner, path: str, pattern: str, top: str):
35+
result = TestResult()
36+
for fname, ftype, *_ in os.ilistdir(path):
37+
if fname in ("..", "."):
38+
continue
39+
if ftype == _DIR_TYPE:
40+
result += _run_all_in_dir(
41+
runner=runner,
42+
path="/".join((path, fname)),
43+
pattern=pattern,
44+
top=top,
45+
)
46+
if fnmatch(fname, pattern):
47+
module_name = fname.rsplit(".", 1)[0]
48+
result += _run_test_module(runner, module_name, path, top)
49+
return result
50+
51+
52+
# Implements discovery inspired by https://docs.python.org/3/library/unittest.html#test-discovery
53+
def _discover(runner: TestRunner):
54+
parser = argparse.ArgumentParser()
55+
# parser.add_argument(
56+
# "-v",
57+
# "--verbose",
58+
# action="store_true",
59+
# help="Verbose output",
60+
# )
61+
parser.add_argument(
62+
"-s",
63+
"--start-directory",
64+
dest="start",
65+
default=".",
66+
help="Directory to start discovery",
67+
)
68+
parser.add_argument(
69+
"-p",
70+
"--pattern ",
71+
dest="pattern",
72+
default="test*.py",
73+
help="Pattern to match test files",
74+
)
75+
parser.add_argument(
76+
"-t",
77+
"--top-level-directory",
78+
dest="top",
79+
help="Top level directory of project (defaults to start directory)",
80+
)
81+
args = parser.parse_args(args=sys.argv[2:])
82+
83+
path = args.start
84+
top = args.top or path
85+
86+
return _run_all_in_dir(
87+
runner=runner,
88+
path=path,
89+
pattern=args.pattern,
90+
top=top,
91+
)
92+
93+
94+
# TODO: Use os.path for path handling.
95+
PATH_SEP = getattr(os, "sep", "/")
96+
97+
98+
# foo/bar/x.y.z --> foo/bar, x.y
99+
def _dirname_filename_no_ext(path):
100+
# Workaround: The Windows port currently reports "/" for os.sep
101+
# (and MicroPython doesn't have os.altsep). So for now just
102+
# always work with os.sep (i.e. "/").
103+
path = path.replace("\\", PATH_SEP)
104+
105+
split = path.rsplit(PATH_SEP, 1)
106+
if len(split) == 1:
107+
dirname, filename = "", split[0]
108+
else:
109+
dirname, filename = split
110+
return dirname, filename.rsplit(".", 1)[0]
111+
112+
113+
# This is called from unittest when __name__ == "__main__".
114+
def discover_main():
115+
failures = 0
116+
runner = TestRunner()
117+
118+
if len(sys.argv) == 1 or (
119+
len(sys.argv) >= 2
120+
and _dirname_filename_no_ext(sys.argv[0])[1] == "unittest"
121+
and sys.argv[1] == "discover"
122+
):
123+
# No args, or `python -m unittest discover ...`.
124+
result = _discover(runner)
125+
failures += result.failuresNum or result.errorsNum
126+
else:
127+
for test_spec in sys.argv[1:]:
128+
try:
129+
os.stat(test_spec)
130+
# File exists, strip extension and import with its parent directory in sys.path.
131+
dirname, module_name = _dirname_filename_no_ext(test_spec)
132+
result = _run_test_module(runner, module_name, dirname)
133+
except OSError:
134+
# Not a file, treat as named module to import.
135+
result = _run_test_module(runner, test_spec)
136+
137+
failures += result.failuresNum or result.errorsNum
138+
139+
# Terminate with non zero return code in case of failures.
140+
sys.exit(failures)

python-stdlib/unittest/manifest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
metadata(version="0.10.0")
2+
3+
module("unittest.py")

unix-ffi/unittest/test_unittest.py renamed to python-stdlib/unittest/tests/test_assertions.py

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import unittest
2-
from test_unittest_isolated import global_context
32

43

54
class TestUnittestAssertions(unittest.TestCase):
@@ -143,11 +142,6 @@ def testInner():
143142
else:
144143
self.fail("Unexpected success was not detected")
145144

146-
def test_NotChangedByOtherTest(self):
147-
global global_context
148-
assert global_context is None
149-
global_context = True
150-
151145
def test_subtest_even(self):
152146
"""
153147
Test that numbers between 0 and 5 are all even.
@@ -157,24 +151,5 @@ def test_subtest_even(self):
157151
self.assertEqual(i % 2, 0)
158152

159153

160-
class TestUnittestSetup(unittest.TestCase):
161-
class_setup_var = 0
162-
163-
def setUpClass(self):
164-
TestUnittestSetup.class_setup_var += 1
165-
166-
def tearDownClass(self):
167-
# Not sure how to actually test this, but we can check (in the test case below)
168-
# that it hasn't been run already at least.
169-
TestUnittestSetup.class_setup_var = -1
170-
171-
def testSetUpTearDownClass_1(self):
172-
assert TestUnittestSetup.class_setup_var == 1, TestUnittestSetup.class_setup_var
173-
174-
def testSetUpTearDownClass_2(self):
175-
# Test this twice, as if setUpClass() gets run like setUp() it would be run twice
176-
assert TestUnittestSetup.class_setup_var == 1, TestUnittestSetup.class_setup_var
177-
178-
179154
if __name__ == "__main__":
180155
unittest.main()
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import unittest
2+
3+
4+
class TestWithRunTest(unittest.TestCase):
5+
run = False
6+
7+
def runTest(self):
8+
TestWithRunTest.run = True
9+
10+
def testRunTest(self):
11+
self.fail()
12+
13+
@staticmethod
14+
def tearDownClass():
15+
if not TestWithRunTest.run:
16+
raise ValueError()
17+
18+
19+
def test_func():
20+
pass
21+
22+
23+
@unittest.expectedFailure
24+
def test_foo():
25+
raise ValueError()
26+
27+
28+
if __name__ == "__main__":
29+
unittest.main()
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import unittest
2+
3+
4+
class TestUnittestSetup(unittest.TestCase):
5+
class_setup_var = 0
6+
7+
@classmethod
8+
def setUpClass(cls):
9+
assert cls is TestUnittestSetup
10+
TestUnittestSetup.class_setup_var += 1
11+
12+
@classmethod
13+
def tearDownClass(cls):
14+
assert cls is TestUnittestSetup
15+
# Not sure how to actually test this, but we can check (in the test case below)
16+
# that it hasn't been run already at least.
17+
TestUnittestSetup.class_setup_var = -1
18+
19+
def testSetUpTearDownClass_1(self):
20+
assert TestUnittestSetup.class_setup_var == 1, TestUnittestSetup.class_setup_var
21+
22+
def testSetUpTearDownClass_2(self):
23+
# Test this twice, as if setUpClass() gets run like setUp() it would be run twice
24+
assert TestUnittestSetup.class_setup_var == 1, TestUnittestSetup.class_setup_var
25+
26+
27+
if __name__ == "__main__":
28+
unittest.main()

0 commit comments

Comments
 (0)