kopia lustrzana https://github.com/micropython/micropython-lib
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>pull/537/head
rodzic
cb88a6a554
commit
796a5986cd
|
@ -0,0 +1,7 @@
|
|||
metadata(version="0.1.0")
|
||||
|
||||
require("argparse")
|
||||
require("fnmatch")
|
||||
require("unittest")
|
||||
|
||||
module("unittest_discover.py")
|
|
@ -0,0 +1,4 @@
|
|||
# Module that is used in both test_isolated_1.py and test_isolated_2.py.
|
||||
# The module should be clean reloaded for each.
|
||||
|
||||
state = None
|
|
@ -0,0 +1,8 @@
|
|||
import unittest
|
||||
import isolated
|
||||
|
||||
|
||||
class TestUnittestIsolated1(unittest.TestCase):
|
||||
def test_NotChangedByOtherTest(self):
|
||||
self.assertIsNone(isolated.state)
|
||||
isolated.state = True
|
|
@ -0,0 +1,8 @@
|
|||
import unittest
|
||||
import isolated
|
||||
|
||||
|
||||
class TestUnittestIsolated2(unittest.TestCase):
|
||||
def test_NotChangedByOtherTest(self):
|
||||
self.assertIsNone(isolated.state)
|
||||
isolated.state = True
|
|
@ -0,0 +1,140 @@
|
|||
# Extension for "unittest" that adds the ability to run via "micropython -m unittest".
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from fnmatch import fnmatch
|
||||
from micropython import const
|
||||
|
||||
from unittest import TestRunner, TestResult, TestSuite
|
||||
|
||||
|
||||
# Run a single test in a clean environment.
|
||||
def _run_test_module(runner: TestRunner, module_name: str, *extra_paths: list[str]):
|
||||
module_snapshot = {k: v for k, v in sys.modules.items()}
|
||||
path_snapshot = sys.path[:]
|
||||
try:
|
||||
for path in reversed(extra_paths):
|
||||
if path:
|
||||
sys.path.insert(0, path)
|
||||
|
||||
module = __import__(module_name)
|
||||
suite = TestSuite(module_name)
|
||||
suite._load_module(module)
|
||||
return runner.run(suite)
|
||||
finally:
|
||||
sys.path[:] = path_snapshot
|
||||
sys.modules.clear()
|
||||
sys.modules.update(module_snapshot)
|
||||
|
||||
|
||||
_DIR_TYPE = const(0x4000)
|
||||
|
||||
|
||||
def _run_all_in_dir(runner: TestRunner, path: str, pattern: str, top: str):
|
||||
result = TestResult()
|
||||
for fname, ftype, *_ in os.ilistdir(path):
|
||||
if fname in ("..", "."):
|
||||
continue
|
||||
if ftype == _DIR_TYPE:
|
||||
result += _run_all_in_dir(
|
||||
runner=runner,
|
||||
path="/".join((path, fname)),
|
||||
pattern=pattern,
|
||||
top=top,
|
||||
)
|
||||
if fnmatch(fname, pattern):
|
||||
module_name = fname.rsplit(".", 1)[0]
|
||||
result += _run_test_module(runner, module_name, path, top)
|
||||
return result
|
||||
|
||||
|
||||
# Implements discovery inspired by https://docs.python.org/3/library/unittest.html#test-discovery
|
||||
def _discover(runner: TestRunner):
|
||||
parser = argparse.ArgumentParser()
|
||||
# parser.add_argument(
|
||||
# "-v",
|
||||
# "--verbose",
|
||||
# action="store_true",
|
||||
# help="Verbose output",
|
||||
# )
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--start-directory",
|
||||
dest="start",
|
||||
default=".",
|
||||
help="Directory to start discovery",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--pattern ",
|
||||
dest="pattern",
|
||||
default="test*.py",
|
||||
help="Pattern to match test files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--top-level-directory",
|
||||
dest="top",
|
||||
help="Top level directory of project (defaults to start directory)",
|
||||
)
|
||||
args = parser.parse_args(args=sys.argv[2:])
|
||||
|
||||
path = args.start
|
||||
top = args.top or path
|
||||
|
||||
return _run_all_in_dir(
|
||||
runner=runner,
|
||||
path=path,
|
||||
pattern=args.pattern,
|
||||
top=top,
|
||||
)
|
||||
|
||||
|
||||
# TODO: Use os.path for path handling.
|
||||
PATH_SEP = getattr(os, "sep", "/")
|
||||
|
||||
|
||||
# foo/bar/x.y.z --> foo/bar, x.y
|
||||
def _dirname_filename_no_ext(path):
|
||||
# Workaround: The Windows port currently reports "/" for os.sep
|
||||
# (and MicroPython doesn't have os.altsep). So for now just
|
||||
# always work with os.sep (i.e. "/").
|
||||
path = path.replace("\\", PATH_SEP)
|
||||
|
||||
split = path.rsplit(PATH_SEP, 1)
|
||||
if len(split) == 1:
|
||||
dirname, filename = "", split[0]
|
||||
else:
|
||||
dirname, filename = split
|
||||
return dirname, filename.rsplit(".", 1)[0]
|
||||
|
||||
|
||||
# This is called from unittest when __name__ == "__main__".
|
||||
def discover_main():
|
||||
failures = 0
|
||||
runner = TestRunner()
|
||||
|
||||
if len(sys.argv) == 1 or (
|
||||
len(sys.argv) >= 2
|
||||
and _dirname_filename_no_ext(sys.argv[0])[1] == "unittest"
|
||||
and sys.argv[1] == "discover"
|
||||
):
|
||||
# No args, or `python -m unittest discover ...`.
|
||||
result = _discover(runner)
|
||||
failures += result.failuresNum or result.errorsNum
|
||||
else:
|
||||
for test_spec in sys.argv[1:]:
|
||||
try:
|
||||
os.stat(test_spec)
|
||||
# File exists, strip extension and import with its parent directory in sys.path.
|
||||
dirname, module_name = _dirname_filename_no_ext(test_spec)
|
||||
result = _run_test_module(runner, module_name, dirname)
|
||||
except OSError:
|
||||
# Not a file, treat as named module to import.
|
||||
result = _run_test_module(runner, test_spec)
|
||||
|
||||
failures += result.failuresNum or result.errorsNum
|
||||
|
||||
# Terminate with non zero return code in case of failures.
|
||||
sys.exit(failures)
|
|
@ -0,0 +1,3 @@
|
|||
metadata(version="0.10.0")
|
||||
|
||||
module("unittest.py")
|
|
@ -1,5 +1,4 @@
|
|||
import unittest
|
||||
from test_unittest_isolated import global_context
|
||||
|
||||
|
||||
class TestUnittestAssertions(unittest.TestCase):
|
||||
|
@ -143,11 +142,6 @@ class TestUnittestAssertions(unittest.TestCase):
|
|||
else:
|
||||
self.fail("Unexpected success was not detected")
|
||||
|
||||
def test_NotChangedByOtherTest(self):
|
||||
global global_context
|
||||
assert global_context is None
|
||||
global_context = True
|
||||
|
||||
def test_subtest_even(self):
|
||||
"""
|
||||
Test that numbers between 0 and 5 are all even.
|
||||
|
@ -157,24 +151,5 @@ class TestUnittestAssertions(unittest.TestCase):
|
|||
self.assertEqual(i % 2, 0)
|
||||
|
||||
|
||||
class TestUnittestSetup(unittest.TestCase):
|
||||
class_setup_var = 0
|
||||
|
||||
def setUpClass(self):
|
||||
TestUnittestSetup.class_setup_var += 1
|
||||
|
||||
def tearDownClass(self):
|
||||
# Not sure how to actually test this, but we can check (in the test case below)
|
||||
# that it hasn't been run already at least.
|
||||
TestUnittestSetup.class_setup_var = -1
|
||||
|
||||
def testSetUpTearDownClass_1(self):
|
||||
assert TestUnittestSetup.class_setup_var == 1, TestUnittestSetup.class_setup_var
|
||||
|
||||
def testSetUpTearDownClass_2(self):
|
||||
# Test this twice, as if setUpClass() gets run like setUp() it would be run twice
|
||||
assert TestUnittestSetup.class_setup_var == 1, TestUnittestSetup.class_setup_var
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
|
@ -0,0 +1,29 @@
|
|||
import unittest
|
||||
|
||||
|
||||
class TestWithRunTest(unittest.TestCase):
|
||||
run = False
|
||||
|
||||
def runTest(self):
|
||||
TestWithRunTest.run = True
|
||||
|
||||
def testRunTest(self):
|
||||
self.fail()
|
||||
|
||||
@staticmethod
|
||||
def tearDownClass():
|
||||
if not TestWithRunTest.run:
|
||||
raise ValueError()
|
||||
|
||||
|
||||
def test_func():
|
||||
pass
|
||||
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_foo():
|
||||
raise ValueError()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
|
@ -0,0 +1,28 @@
|
|||
import unittest
|
||||
|
||||
|
||||
class TestUnittestSetup(unittest.TestCase):
|
||||
class_setup_var = 0
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
assert cls is TestUnittestSetup
|
||||
TestUnittestSetup.class_setup_var += 1
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
assert cls is TestUnittestSetup
|
||||
# Not sure how to actually test this, but we can check (in the test case below)
|
||||
# that it hasn't been run already at least.
|
||||
TestUnittestSetup.class_setup_var = -1
|
||||
|
||||
def testSetUpTearDownClass_1(self):
|
||||
assert TestUnittestSetup.class_setup_var == 1, TestUnittestSetup.class_setup_var
|
||||
|
||||
def testSetUpTearDownClass_2(self):
|
||||
# Test this twice, as if setUpClass() gets run like setUp() it would be run twice
|
||||
assert TestUnittestSetup.class_setup_var == 1, TestUnittestSetup.class_setup_var
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
|
@ -1,22 +1,13 @@
|
|||
import io
|
||||
import os
|
||||
import sys
|
||||
import uos
|
||||
|
||||
try:
|
||||
import io
|
||||
import traceback
|
||||
except ImportError:
|
||||
import uio as io
|
||||
|
||||
traceback = None
|
||||
|
||||
|
||||
def _snapshot_modules():
|
||||
return {k: v for k, v in sys.modules.items()}
|
||||
|
||||
|
||||
__modules__ = _snapshot_modules()
|
||||
|
||||
|
||||
class SkipTest(Exception):
|
||||
pass
|
||||
|
||||
|
@ -61,7 +52,7 @@ class SubtestContext:
|
|||
detail = ", ".join(f"{k}={v}" for k, v in self.params.items())
|
||||
test_details += (f" ({detail})",)
|
||||
|
||||
handle_test_exception(test_details, __test_result__, exc_info, False)
|
||||
_handle_test_exception(test_details, __test_result__, exc_info, False)
|
||||
# Suppress the exception as we've captured it above
|
||||
return True
|
||||
|
||||
|
@ -258,9 +249,17 @@ class TestSuite:
|
|||
|
||||
def run(self, result):
|
||||
for c in self._tests:
|
||||
run_suite(c, result, self.name)
|
||||
_run_suite(c, result, self.name)
|
||||
return result
|
||||
|
||||
def _load_module(self, mod):
|
||||
for tn in dir(mod):
|
||||
c = getattr(mod, tn)
|
||||
if isinstance(c, object) and isinstance(c, type) and issubclass(c, TestCase):
|
||||
self.addTest(c)
|
||||
elif tn.startswith("test") and callable(c):
|
||||
self.addTest(c)
|
||||
|
||||
|
||||
class TestRunner:
|
||||
def run(self, suite: TestSuite):
|
||||
|
@ -331,7 +330,7 @@ class TestResult:
|
|||
return self
|
||||
|
||||
|
||||
def capture_exc(exc, traceback):
|
||||
def _capture_exc(exc, traceback):
|
||||
buf = io.StringIO()
|
||||
if hasattr(sys, "print_exception"):
|
||||
sys.print_exception(exc, buf)
|
||||
|
@ -340,12 +339,12 @@ def capture_exc(exc, traceback):
|
|||
return buf.getvalue()
|
||||
|
||||
|
||||
def handle_test_exception(
|
||||
def _handle_test_exception(
|
||||
current_test: tuple, test_result: TestResult, exc_info: tuple, verbose=True
|
||||
):
|
||||
exc = exc_info[1]
|
||||
traceback = exc_info[2]
|
||||
ex_str = capture_exc(exc, traceback)
|
||||
ex_str = _capture_exc(exc, traceback)
|
||||
if isinstance(exc, AssertionError):
|
||||
test_result.failuresNum += 1
|
||||
test_result.failures.append((current_test, ex_str))
|
||||
|
@ -359,7 +358,7 @@ def handle_test_exception(
|
|||
test_result._newFailures += 1
|
||||
|
||||
|
||||
def run_suite(c, test_result: TestResult, suite_name=""):
|
||||
def _run_suite(c, test_result: TestResult, suite_name=""):
|
||||
if isinstance(c, TestSuite):
|
||||
c.run(test_result)
|
||||
return
|
||||
|
@ -388,9 +387,7 @@ def run_suite(c, test_result: TestResult, suite_name=""):
|
|||
try:
|
||||
test_result._newFailures = 0
|
||||
test_result.testsRun += 1
|
||||
test_globals = dict(**globals())
|
||||
test_globals["test_function"] = test_function
|
||||
exec("test_function()", test_globals, test_globals)
|
||||
test_function()
|
||||
# No exception occurred, test passed
|
||||
if test_result._newFailures:
|
||||
print(" FAIL")
|
||||
|
@ -402,7 +399,7 @@ def run_suite(c, test_result: TestResult, suite_name=""):
|
|||
test_result.skippedNum += 1
|
||||
test_result.skipped.append((name, c, reason))
|
||||
except Exception as ex:
|
||||
handle_test_exception(
|
||||
_handle_test_exception(
|
||||
current_test=(name, c), test_result=test_result, exc_info=sys.exc_info()
|
||||
)
|
||||
# Uncomment to investigate failure in detail
|
||||
|
@ -417,102 +414,59 @@ def run_suite(c, test_result: TestResult, suite_name=""):
|
|||
pass
|
||||
|
||||
set_up_class()
|
||||
try:
|
||||
if hasattr(o, "runTest"):
|
||||
name = str(o)
|
||||
run_one(o.runTest)
|
||||
return
|
||||
|
||||
if hasattr(o, "runTest"):
|
||||
name = str(o)
|
||||
run_one(o.runTest)
|
||||
return
|
||||
for name in dir(o):
|
||||
if name.startswith("test"):
|
||||
m = getattr(o, name)
|
||||
if not callable(m):
|
||||
continue
|
||||
run_one(m)
|
||||
|
||||
for name in dir(o):
|
||||
if name.startswith("test"):
|
||||
m = getattr(o, name)
|
||||
if not callable(m):
|
||||
continue
|
||||
run_one(m)
|
||||
|
||||
if callable(o):
|
||||
name = o.__name__
|
||||
run_one(o)
|
||||
|
||||
tear_down_class()
|
||||
if callable(o):
|
||||
name = o.__name__
|
||||
run_one(o)
|
||||
finally:
|
||||
tear_down_class()
|
||||
|
||||
return exceptions
|
||||
|
||||
|
||||
def _test_cases(mod):
|
||||
for tn in dir(mod):
|
||||
c = getattr(mod, tn)
|
||||
if isinstance(c, object) and isinstance(c, type) and issubclass(c, TestCase):
|
||||
yield c
|
||||
elif tn.startswith("test_") and callable(c):
|
||||
yield c
|
||||
|
||||
|
||||
def run_module(runner, module, path, top):
|
||||
if not module:
|
||||
raise ValueError("Empty module name")
|
||||
|
||||
# Reset the python environment before running test
|
||||
sys.modules.clear()
|
||||
sys.modules.update(__modules__)
|
||||
|
||||
sys_path_initial = sys.path[:]
|
||||
# Add script dir and top dir to import path
|
||||
sys.path.insert(0, str(path))
|
||||
if top:
|
||||
sys.path.insert(1, top)
|
||||
try:
|
||||
suite = TestSuite(module)
|
||||
m = __import__(module) if isinstance(module, str) else module
|
||||
for c in _test_cases(m):
|
||||
suite.addTest(c)
|
||||
result = runner.run(suite)
|
||||
return result
|
||||
|
||||
finally:
|
||||
sys.path[:] = sys_path_initial
|
||||
|
||||
|
||||
def discover(runner: TestRunner):
|
||||
from unittest_discover import discover
|
||||
|
||||
global __modules__
|
||||
__modules__ = _snapshot_modules()
|
||||
return discover(runner=runner)
|
||||
|
||||
|
||||
# This supports either:
|
||||
#
|
||||
# >>> import mytest
|
||||
# >>> unitttest.main(mytest)
|
||||
#
|
||||
# >>> unittest.main("mytest")
|
||||
#
|
||||
# Or, a script that ends with:
|
||||
# if __name__ == "__main__":
|
||||
# unittest.main()
|
||||
# e.g. run via `mpremote run mytest.py`
|
||||
def main(module="__main__", testRunner=None):
|
||||
if testRunner:
|
||||
if isinstance(testRunner, type):
|
||||
runner = testRunner()
|
||||
else:
|
||||
runner = testRunner
|
||||
else:
|
||||
runner = TestRunner()
|
||||
if testRunner is None:
|
||||
testRunner = TestRunner()
|
||||
elif isinstance(testRunner, type):
|
||||
testRunner = testRunner()
|
||||
|
||||
if len(sys.argv) <= 1:
|
||||
result = discover(runner)
|
||||
elif sys.argv[0].split(".")[0] == "unittest" and sys.argv[1] == "discover":
|
||||
result = discover(runner)
|
||||
else:
|
||||
for test_spec in sys.argv[1:]:
|
||||
try:
|
||||
uos.stat(test_spec)
|
||||
# test_spec is a local file, run it directly
|
||||
if "/" in test_spec:
|
||||
path, fname = test_spec.rsplit("/", 1)
|
||||
else:
|
||||
path, fname = ".", test_spec
|
||||
modname = fname.rsplit(".", 1)[0]
|
||||
result = run_module(runner, modname, path, None)
|
||||
|
||||
except OSError:
|
||||
# Not a file, treat as import name
|
||||
result = run_module(runner, test_spec, ".", None)
|
||||
|
||||
# Terminate with non zero return code in case of failures
|
||||
sys.exit(result.failuresNum or result.errorsNum)
|
||||
if isinstance(module, str):
|
||||
module = __import__(module)
|
||||
suite = TestSuite(module.__name__)
|
||||
suite._load_module(module)
|
||||
return testRunner.run(suite)
|
||||
|
||||
|
||||
# Support `micropython -m unittest` (only useful if unitest-discover is
|
||||
# installed).
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
try:
|
||||
# If unitest-discover is installed, use the main() provided there.
|
||||
from unittest_discover import discover_main
|
||||
|
||||
discover_main()
|
||||
except ImportError:
|
||||
pass
|
|
@ -1,6 +0,0 @@
|
|||
metadata(version="0.9.0")
|
||||
|
||||
require("argparse")
|
||||
require("fnmatch")
|
||||
|
||||
module("unittest.py")
|
|
@ -1,15 +0,0 @@
|
|||
import unittest
|
||||
|
||||
|
||||
global_context = None
|
||||
|
||||
|
||||
class TestUnittestIsolated(unittest.TestCase):
|
||||
def test_NotChangedByOtherTest(self):
|
||||
global global_context
|
||||
assert global_context is None
|
||||
global_context = True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
|
@ -1,70 +0,0 @@
|
|||
import argparse
|
||||
import sys
|
||||
import uos
|
||||
from fnmatch import fnmatch
|
||||
|
||||
from unittest import TestRunner, TestResult, run_module
|
||||
|
||||
|
||||
def discover(runner: TestRunner):
|
||||
"""
|
||||
Implements discover function inspired by https://docs.python.org/3/library/unittest.html#test-discovery
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
# parser.add_argument(
|
||||
# "-v",
|
||||
# "--verbose",
|
||||
# action="store_true",
|
||||
# help="Verbose output",
|
||||
# )
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--start-directory",
|
||||
dest="start",
|
||||
default=".",
|
||||
help="Directory to start discovery",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--pattern ",
|
||||
dest="pattern",
|
||||
default="test*.py",
|
||||
help="Pattern to match test files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--top-level-directory",
|
||||
dest="top",
|
||||
help="Top level directory of project (defaults to start directory)",
|
||||
)
|
||||
args = parser.parse_args(args=sys.argv[2:])
|
||||
|
||||
path = args.start
|
||||
top = args.top or path
|
||||
|
||||
return run_all_in_dir(
|
||||
runner=runner,
|
||||
path=path,
|
||||
pattern=args.pattern,
|
||||
top=top,
|
||||
)
|
||||
|
||||
|
||||
def run_all_in_dir(runner: TestRunner, path: str, pattern: str, top: str):
|
||||
DIR_TYPE = 0x4000
|
||||
|
||||
result = TestResult()
|
||||
for fname, type, *_ in uos.ilistdir(path):
|
||||
if fname in ("..", "."):
|
||||
continue
|
||||
if type == DIR_TYPE:
|
||||
result += run_all_in_dir(
|
||||
runner=runner,
|
||||
path="/".join((path, fname)),
|
||||
pattern=pattern,
|
||||
top=top,
|
||||
)
|
||||
if fnmatch(fname, pattern):
|
||||
modname = fname[: fname.rfind(".")]
|
||||
result += run_module(runner, modname, path, top)
|
||||
return result
|
Ładowanie…
Reference in New Issue