From dcdac1f552d2684751ff0f75d20030b12e97407f Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Fri, 18 Mar 2022 12:41:10 +1100 Subject: [PATCH 01/38] fnmatch: Add ure compatibility. Removes dependency on re-pcre which is only available on unix port. --- python-stdlib/fnmatch/fnmatch.py | 18 +++++++++++++++++- python-stdlib/fnmatch/metadata.txt | 2 +- python-stdlib/fnmatch/setup.py | 2 +- python-stdlib/fnmatch/test_fnmatch.py | 7 ++++--- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/python-stdlib/fnmatch/fnmatch.py b/python-stdlib/fnmatch/fnmatch.py index 93b5d5214..f573d75c6 100644 --- a/python-stdlib/fnmatch/fnmatch.py +++ b/python-stdlib/fnmatch/fnmatch.py @@ -17,6 +17,8 @@ __all__ = ["filter", "fnmatch", "fnmatchcase", "translate"] +COMPAT = re.__name__ == "ure" + def fnmatch(name, pat): """Test whether FILENAME matches PATTERN. @@ -46,6 +48,11 @@ def _compile_pattern(pat): res = bytes(res_str, "ISO-8859-1") else: res = translate(pat) + if COMPAT: + if res.startswith("(?ms)"): + res = res[5:] + if res.endswith("\\Z"): + res = res[:-2] + "$" return re.compile(res).match @@ -104,6 +111,15 @@ def translate(pat): stuff = "\\" + stuff res = "%s[%s]" % (res, stuff) else: - res = res + re.escape(c) + try: + res = res + re.escape(c) + except AttributeError: + # Using ure rather than re-pcre + res = res + re_escape(c) # Original patterns is undefined, see http://bugs.python.org/issue21464 return "(?ms)" + res + "\Z" + + +def re_escape(pattern): + # Replacement minimal re.escape for ure compatibility + return re.sub(r"([\^\$\.\|\?\*\+\(\)\[\\])", r"\\\1", pattern) diff --git a/python-stdlib/fnmatch/metadata.txt b/python-stdlib/fnmatch/metadata.txt index faa832140..5eb297d46 100644 --- a/python-stdlib/fnmatch/metadata.txt +++ b/python-stdlib/fnmatch/metadata.txt @@ -1,4 +1,4 @@ srctype = cpython type = module version = 0.5.2 -depends = os, os.path, re-pcre +depends = os, os.path diff --git a/python-stdlib/fnmatch/setup.py b/python-stdlib/fnmatch/setup.py index 415804c01..5a9ac6323 100644 --- a/python-stdlib/fnmatch/setup.py +++ b/python-stdlib/fnmatch/setup.py @@ -21,5 +21,5 @@ license="Python", cmdclass={"sdist": sdist_upip.sdist}, py_modules=["fnmatch"], - install_requires=["micropython-os", "micropython-os.path", "micropython-re-pcre"], + install_requires=["micropython-os", "micropython-os.path"], ) diff --git a/python-stdlib/fnmatch/test_fnmatch.py b/python-stdlib/fnmatch/test_fnmatch.py index 1ddf8a607..4eaeec63b 100644 --- a/python-stdlib/fnmatch/test_fnmatch.py +++ b/python-stdlib/fnmatch/test_fnmatch.py @@ -10,7 +10,8 @@ class FnmatchTestCase(unittest.TestCase): def check_match(self, filename, pattern, should_match=1, fn=fnmatch): if should_match: self.assertTrue( - fn(filename, pattern), "expected %r to match pattern %r" % (filename, pattern) + fn(filename, pattern), + "expected %r to match pattern %r" % (filename, pattern), ) else: self.assertTrue( @@ -80,9 +81,9 @@ def test_filter(self): self.assertEqual(filter(["a", "b"], "a"), ["a"]) -def test_main(): +def main(): support.run_unittest(FnmatchTestCase, TranslateTestCase, FilterTestCase) if __name__ == "__main__": - test_main() + main() From 7259f0fd6f8759646d044eee2a40a6456734aca6 Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Tue, 12 Apr 2022 05:49:06 +1000 Subject: [PATCH 02/38] fnmatch: Remove dependency on os.path. --- python-stdlib/fnmatch/fnmatch.py | 24 +++++++++++++++++------- python-stdlib/fnmatch/metadata.txt | 1 - python-stdlib/fnmatch/setup.py | 1 - 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/python-stdlib/fnmatch/fnmatch.py b/python-stdlib/fnmatch/fnmatch.py index f573d75c6..71009afa2 100644 --- a/python-stdlib/fnmatch/fnmatch.py +++ b/python-stdlib/fnmatch/fnmatch.py @@ -9,11 +9,21 @@ The function translate(PATTERN) returns a regular expression corresponding to PATTERN. (It does not compile it.) """ -import os -import os.path import re -# import functools +try: + from os.path import normcase +except ImportError: + + def normcase(s): + """ + From os.path.normcase + Normalize the case of a pathname. On Windows, convert all characters + in the pathname to lowercase, and also convert forward slashes to + backward slashes. On other operating systems, return the path unchanged. + """ + return s + __all__ = ["filter", "fnmatch", "fnmatchcase", "translate"] @@ -35,8 +45,8 @@ def fnmatch(name, pat): if the operating system requires it. If you don't want this, use fnmatchcase(FILENAME, PATTERN). """ - name = os.path.normcase(name) - pat = os.path.normcase(pat) + name = normcase(name) + pat = normcase(pat) return fnmatchcase(name, pat) @@ -59,10 +69,10 @@ def _compile_pattern(pat): def filter(names, pat): """Return the subset of the list NAMES that match PAT.""" result = [] - pat = os.path.normcase(pat) + pat = normcase(pat) match = _compile_pattern(pat) for name in names: - if match(os.path.normcase(name)): + if match(normcase(name)): result.append(name) return result diff --git a/python-stdlib/fnmatch/metadata.txt b/python-stdlib/fnmatch/metadata.txt index 5eb297d46..10b54b832 100644 --- a/python-stdlib/fnmatch/metadata.txt +++ b/python-stdlib/fnmatch/metadata.txt @@ -1,4 +1,3 @@ srctype = cpython type = module version = 0.5.2 -depends = os, os.path diff --git a/python-stdlib/fnmatch/setup.py b/python-stdlib/fnmatch/setup.py index 5a9ac6323..7ca64ddf8 100644 --- a/python-stdlib/fnmatch/setup.py +++ b/python-stdlib/fnmatch/setup.py @@ -21,5 +21,4 @@ license="Python", cmdclass={"sdist": sdist_upip.sdist}, py_modules=["fnmatch"], - install_requires=["micropython-os", "micropython-os.path"], ) From d64557a2114d4c366242e929af15dd61b6ff7f4b Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Fri, 18 Mar 2022 12:42:00 +1100 Subject: [PATCH 03/38] fnmatch: Release 0.6.0. --- python-stdlib/fnmatch/metadata.txt | 2 +- python-stdlib/fnmatch/setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python-stdlib/fnmatch/metadata.txt b/python-stdlib/fnmatch/metadata.txt index 10b54b832..27b8c03f3 100644 --- a/python-stdlib/fnmatch/metadata.txt +++ b/python-stdlib/fnmatch/metadata.txt @@ -1,3 +1,3 @@ srctype = cpython type = module -version = 0.5.2 +version = 0.6.0 diff --git a/python-stdlib/fnmatch/setup.py b/python-stdlib/fnmatch/setup.py index 7ca64ddf8..06331a9cb 100644 --- a/python-stdlib/fnmatch/setup.py +++ b/python-stdlib/fnmatch/setup.py @@ -10,7 +10,7 @@ setup( name="micropython-fnmatch", - version="0.5.2", + version="0.6.0", description="CPython fnmatch module ported to MicroPython", long_description="This is a module ported from CPython standard library to be compatible with\nMicroPython interpreter. Usually, this means applying small patches for\nfeatures not supported (yet, or at all) in MicroPython. Sometimes, heavier\nchanges are required. Note that CPython modules are written with availability\nof vast resources in mind, and may not work for MicroPython ports with\nlimited heap. If you are affected by such a case, please help reimplement\nthe module from scratch.", url="https://github.com/micropython/micropython-lib", From a9cd99ce2d2af4d236cfe86a722f55af2bcd4528 Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Thu, 1 Nov 2018 16:25:38 +1100 Subject: [PATCH 04/38] unittest: Allow passing module name or instance into unittest.main() --- python-stdlib/unittest/unittest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index 2b00fbddb..3aed90c72 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -219,7 +219,7 @@ def test_cases(m): if isinstance(c, object) and isinstance(c, type) and issubclass(c, TestCase): yield c - m = __import__(module) + m = __import__(module) if isinstance(module, str) else module suite = TestSuite() for c in test_cases(m): suite.addTest(c) From 7d4d02edfc689811ac9fb120a188498686bfc56d Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Wed, 31 Oct 2018 11:49:45 +1100 Subject: [PATCH 05/38] unittest: Log failure tracebacks at test end. Store traceback details for each test failure and log to console at the end of the test, like CPython version of the module does. --- python-stdlib/unittest/unittest.py | 31 ++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index 3aed90c72..55468b548 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -1,5 +1,13 @@ import sys +try: + import io + import traceback +except ImportError: + import uio as io + + traceback = None + class SkipTest(Exception): pass @@ -160,7 +168,7 @@ class TestRunner: def run(self, suite): res = TestResult() for c in suite.tests: - run_class(c, res) + res.exceptions.extend(run_class(c, res)) print("Ran %d tests\n" % res.testsRun) if res.failuresNum > 0 or res.errorsNum > 0: @@ -180,16 +188,27 @@ def __init__(self): self.failuresNum = 0 self.skippedNum = 0 self.testsRun = 0 + self.exceptions = [] def wasSuccessful(self): return self.errorsNum == 0 and self.failuresNum == 0 +def capture_exc(e): + buf = io.StringIO() + if hasattr(sys, "print_exception"): + sys.print_exception(e, buf) + elif traceback is not None: + traceback.print_exception(None, e, sys.exc_info()[2], file=buf) + return buf.getvalue() + + # TODO: Uncompliant def run_class(c, test_result): o = c() set_up = getattr(o, "setUp", lambda: None) tear_down = getattr(o, "tearDown", lambda: None) + exceptions = [] for name in dir(o): if name.startswith("test"): print("%s (%s) ..." % (name, c.__qualname__), end="") @@ -202,14 +221,14 @@ def run_class(c, test_result): except SkipTest as e: print(" skipped:", e.args[0]) test_result.skippedNum += 1 - except: + except Exception as ex: + exceptions.append(capture_exc(ex)) print(" FAIL") test_result.failuresNum += 1 - # Uncomment to investigate failure in detail - # raise continue finally: tear_down() + return exceptions def main(module="__main__"): @@ -225,5 +244,9 @@ def test_cases(m): suite.addTest(c) runner = TestRunner() result = runner.run(suite) + if result.exceptions: + sep = "\n----------------------------------------------------------------------\n" + print(sep) + print(sep.join(result.exceptions)) # Terminate with non zero return code in case of failures sys.exit(result.failuresNum > 0) From 663a3d6c54a97f4f11288889b59b23cbe8e4d8cd Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Wed, 19 Dec 2018 00:36:08 +0300 Subject: [PATCH 06/38] unittest: test_unittest.py: Fix typo in method name. --- python-stdlib/unittest/test_unittest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-stdlib/unittest/test_unittest.py b/python-stdlib/unittest/test_unittest.py index 4651cf852..78c3dc9e8 100644 --- a/python-stdlib/unittest/test_unittest.py +++ b/python-stdlib/unittest/test_unittest.py @@ -37,7 +37,7 @@ def test_AlmostEqual(self): with self.assertRaises(AssertionError): self.assertNotAlmostEqual(float("inf"), float("inf")) - def test_AmostEqualWithDelta(self): + def test_AlmostEqualWithDelta(self): self.assertAlmostEqual(1.1, 1.0, delta=0.5) self.assertAlmostEqual(1.0, 1.1, delta=0.5) self.assertNotAlmostEqual(1.1, 1.0, delta=0.05) From 669d343feb80f17eb932f5f576e6ebfb79cd3295 Mon Sep 17 00:00:00 2001 From: sss Date: Wed, 31 Oct 2018 15:59:56 +1100 Subject: [PATCH 07/38] unittest: Allow to catch AssertionError with assertRaises(). Without this change, current implementaiton produces a false positive result for AssertionError type. Example of falsely passing test code: def test(a, b): assert a > 10 assert b > 10 self.assertRaises(AssertionError, test, 20, 20) --- python-stdlib/unittest/unittest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index 55468b548..f6772ddbe 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -126,12 +126,13 @@ def assertRaises(self, exc, func=None, *args, **kwargs): try: func(*args, **kwargs) - assert False, "%r not raised" % exc except Exception as e: if isinstance(e, exc): return raise + assert False, "%r not raised" % exc + def skip(msg): def _decor(fun): From fca89f65c7d2e8780cdc7b6be7c8548eb1f704c1 Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Sat, 26 Jan 2019 02:42:30 +0300 Subject: [PATCH 08/38] unittest: test_unittest: Add test for .assertRaises(AssertionError). Make sure that not raising AssertionError from tested function is properly caught. --- python-stdlib/unittest/test_unittest.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/python-stdlib/unittest/test_unittest.py b/python-stdlib/unittest/test_unittest.py index 78c3dc9e8..645363cee 100644 --- a/python-stdlib/unittest/test_unittest.py +++ b/python-stdlib/unittest/test_unittest.py @@ -111,6 +111,21 @@ def testRaises(self): def testSkip(self): self.assertFail("this should be skipped") + def testAssert(self): + + e1 = None + try: + + def func_under_test(a): + assert a > 10 + + self.assertRaises(AssertionError, func_under_test, 20) + except AssertionError as e: + e1 = e + + if not e1 or "not raised" not in e1.args[0]: + self.fail("Expected to catch lack of AssertionError from assert in func_under_test") + if __name__ == "__main__": unittest.main() From 965e25ce89d9dfd3ca9e7b51cc34cd3ef265574e Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Sun, 3 Mar 2019 23:40:09 +0300 Subject: [PATCH 09/38] unittest: test_unittest: Typo fix. --- python-stdlib/unittest/test_unittest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-stdlib/unittest/test_unittest.py b/python-stdlib/unittest/test_unittest.py index 645363cee..8188def9e 100644 --- a/python-stdlib/unittest/test_unittest.py +++ b/python-stdlib/unittest/test_unittest.py @@ -109,7 +109,7 @@ def testRaises(self): @unittest.skip("test of skipping") def testSkip(self): - self.assertFail("this should be skipped") + self.fail("this should be skipped") def testAssert(self): From a57b575020b026bc339cdb0abab89da47c0e81ea Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Sat, 27 Jul 2019 07:14:45 +0300 Subject: [PATCH 10/38] unittest: AssertRaisesContext: Store exception value as self.exception. For tests to check. This feature is used by CPython stdlib tests. --- python-stdlib/unittest/unittest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index f6772ddbe..061a0b0b4 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -21,6 +21,7 @@ def __enter__(self): return self def __exit__(self, exc_type, exc_value, tb): + self.exception = exc_value if exc_type is None: assert False, "%r not raised" % self.expected if issubclass(exc_type, self.expected): From dedfe2dcd4627546c52f4620393324f7ab57a55a Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Tue, 13 Aug 2019 08:29:50 +0300 Subject: [PATCH 11/38] unittest: Add assertLessEqual, assertGreaterEqual methods. As used by CPython testsuite. --- python-stdlib/unittest/unittest.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index 061a0b0b4..a22880d5a 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -43,6 +43,16 @@ def assertNotEqual(self, x, y, msg=""): msg = "%r not expected to be equal %r" % (x, y) assert x != y, msg + def assertLessEqual(self, x, y, msg=None): + if msg is None: + msg = "%r is expected to be <= %r" % (x, y) + assert x <= y, msg + + def assertGreaterEqual(self, x, y, msg=None): + if msg is None: + msg = "%r is expected to be >= %r" % (x, y) + assert x >= y, msg + def assertAlmostEqual(self, x, y, places=None, msg="", delta=None): if x == y: return From dc788f4e5003bef2bf1dcc87d16691f7c82a10c4 Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Sat, 14 Dec 2019 22:17:02 +0300 Subject: [PATCH 12/38] unittest: Reinstate useful debugger helper. Signed-off-by: Paul Sokolovsky --- python-stdlib/unittest/unittest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index a22880d5a..a3e058b1b 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -237,6 +237,8 @@ def run_class(c, test_result): exceptions.append(capture_exc(ex)) print(" FAIL") test_result.failuresNum += 1 + # Uncomment to investigate failure in detail + # raise continue finally: tear_down() From c72ec5c0296165b7430f8562826e18e75274998b Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Sun, 15 Dec 2019 15:05:18 +0300 Subject: [PATCH 13/38] unittest: TestSuite: Add undescore to internal field, self._tests. To avoid possible name clashes. Signed-off-by: Paul Sokolovsky --- python-stdlib/unittest/unittest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index a3e058b1b..e0c463fff 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -170,16 +170,16 @@ def skipUnless(cond, msg): class TestSuite: def __init__(self): - self.tests = [] + self._tests = [] def addTest(self, cls): - self.tests.append(cls) + self._tests.append(cls) class TestRunner: def run(self, suite): res = TestResult() - for c in suite.tests: + for c in suite._tests: res.exceptions.extend(run_class(c, res)) print("Ran %d tests\n" % res.testsRun) From d747b21fc690191b63242948b0695eafa12a9bf5 Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Sun, 15 Dec 2019 16:13:47 +0300 Subject: [PATCH 14/38] unittest: Only treat callable fields as test methods. Signed-off-by: Paul Sokolovsky --- python-stdlib/unittest/unittest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index e0c463fff..066ad3613 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -223,8 +223,10 @@ def run_class(c, test_result): exceptions = [] for name in dir(o): if name.startswith("test"): - print("%s (%s) ..." % (name, c.__qualname__), end="") m = getattr(o, name) + if not callable(m): + continue + print("%s (%s) ..." % (name, c.__qualname__), end="") set_up() try: test_result.testsRun += 1 From f09d2ec608f8238c7c88c04575c0bb3d87334ef8 Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Wed, 26 Feb 2020 15:03:46 +0200 Subject: [PATCH 15/38] unittest: Support both test classes and class instances. And for clarity, rename runner function run_class() -> run_suite(). Signed-off-by: Paul Sokolovsky --- python-stdlib/unittest/unittest.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index 066ad3613..55d002f42 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -180,7 +180,7 @@ class TestRunner: def run(self, suite): res = TestResult() for c in suite._tests: - res.exceptions.extend(run_class(c, res)) + res.exceptions.extend(run_suite(c, res)) print("Ran %d tests\n" % res.testsRun) if res.failuresNum > 0 or res.errorsNum > 0: @@ -216,8 +216,11 @@ def capture_exc(e): # TODO: Uncompliant -def run_class(c, test_result): - o = c() +def run_suite(c, test_result): + if isinstance(c, type): + o = c() + else: + o = c set_up = getattr(o, "setUp", lambda: None) tear_down = getattr(o, "tearDown", lambda: None) exceptions = [] From 5d3a44cb1c80ced042be88e1a4c0dd4bbbfe9c27 Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Thu, 27 Feb 2020 17:17:48 +0200 Subject: [PATCH 16/38] unittest: TestCase: Add (dummy) __init__. Mostly to workaround inherited MicroPython's issues with inheritance. Signed-off-by: Paul Sokolovsky --- python-stdlib/unittest/unittest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index 55d002f42..fb6c61254 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -30,6 +30,9 @@ def __exit__(self, exc_type, exc_value, tb): class TestCase: + def __init__(self): + pass + def fail(self, msg=""): assert False, msg From 7d777740275c270223fd88d261a47557834f1cbc Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Sat, 8 Aug 2020 11:42:24 +0300 Subject: [PATCH 17/38] unittest: Add TestCase.skipTest() method. Signed-off-by: Paul Sokolovsky --- python-stdlib/unittest/unittest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index fb6c61254..70da2ce16 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -33,6 +33,9 @@ class TestCase: def __init__(self): pass + def skipTest(self, reason): + raise SkipTest(reason) + def fail(self, msg=""): assert False, msg From 555f28ce6d83f2193149b22a2da7551243e6396f Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Sun, 9 Aug 2020 11:58:19 +0300 Subject: [PATCH 18/38] unittest: Add dummy TestCase.subTest() context manager. Just runs "subtests" in the scope of the main TestCase. Signed-off-by: Paul Sokolovsky --- python-stdlib/unittest/unittest.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index 70da2ce16..deb678b9d 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -29,10 +29,21 @@ def __exit__(self, exc_type, exc_value, tb): return False +class NullContext: + def __enter__(self): + pass + + def __exit__(self, a, b, c): + pass + + class TestCase: def __init__(self): pass + def subTest(self, msg=None, **params): + return NullContext() + def skipTest(self, reason): raise SkipTest(reason) From 04dce89790881d3bc1a4659a4020e7899759be13 Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Mon, 10 Aug 2020 20:54:49 +0300 Subject: [PATCH 19/38] unittest: Add dummy TestCase.assertWarns() context manager. Signed-off-by: Paul Sokolovsky --- python-stdlib/unittest/unittest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index deb678b9d..5911117c2 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -161,6 +161,9 @@ def assertRaises(self, exc, func=None, *args, **kwargs): assert False, "%r not raised" % exc + def assertWarns(self, warn): + return NullContext() + def skip(msg): def _decor(fun): From 2c0b508e4dcf929d96e58e62567d11ff6661e0b2 Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Sun, 15 Nov 2020 11:52:23 +0300 Subject: [PATCH 20/38] unittest: TestSuite: Add run() method. For CPython compatibility. Signed-off-by: Paul Sokolovsky --- python-stdlib/unittest/unittest.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index 5911117c2..4e428204d 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -195,12 +195,16 @@ def __init__(self): def addTest(self, cls): self._tests.append(cls) + def run(self, result): + for c in self._tests: + result.exceptions.extend(run_suite(c, result)) + return result + class TestRunner: def run(self, suite): res = TestResult() - for c in suite._tests: - res.exceptions.extend(run_suite(c, res)) + suite.run(res) print("Ran %d tests\n" % res.testsRun) if res.failuresNum > 0 or res.errorsNum > 0: From 1b46612f941ce96800af57d82e59d16750943284 Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Thu, 13 May 2021 01:30:32 +0300 Subject: [PATCH 21/38] unittest: Implement basic addCleanup()/doCleanup(). Signed-off-by: Paul Sokolovsky --- python-stdlib/unittest/unittest.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index 4e428204d..8c092a31d 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -41,6 +41,17 @@ class TestCase: def __init__(self): pass + def addCleanup(self, func, *args, **kwargs): + if not hasattr(self, "_cleanups"): + self._cleanups = [] + self._cleanups.append((func, args, kwargs)) + + def doCleanups(self): + if hasattr(self, "_cleanups"): + while self._cleanups: + func, args, kwargs = self._cleanups.pop() + func(*args, **kwargs) + def subTest(self, msg=None, **params): return NullContext() @@ -271,6 +282,7 @@ def run_suite(c, test_result): continue finally: tear_down() + o.doCleanups() return exceptions From e582666f5da9670abeba5c84cfcabeff9099c0d1 Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Fri, 14 May 2021 17:15:24 +0300 Subject: [PATCH 22/38] unittest: Properly handle failures vs errors. Also, rework result printing to be more compatible with CPython. Signed-off-by: Paul Sokolovsky --- python-stdlib/unittest/unittest.py | 46 +++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index 8c092a31d..ac1ff5fc3 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -208,7 +208,7 @@ def addTest(self, cls): def run(self, result): for c in self._tests: - result.exceptions.extend(run_suite(c, result)) + run_suite(c, result) return result @@ -217,6 +217,8 @@ def run(self, suite): res = TestResult() suite.run(res) + res.printErrors() + print("----------------------------------------------------------------------") print("Ran %d tests\n" % res.testsRun) if res.failuresNum > 0 or res.errorsNum > 0: print("FAILED (failures=%d, errors=%d)" % (res.failuresNum, res.errorsNum)) @@ -235,11 +237,33 @@ def __init__(self): self.failuresNum = 0 self.skippedNum = 0 self.testsRun = 0 - self.exceptions = [] + self.errors = [] + self.failures = [] def wasSuccessful(self): return self.errorsNum == 0 and self.failuresNum == 0 + def printErrors(self): + print() + self.printErrorList(self.errors) + self.printErrorList(self.failures) + + def printErrorList(self, lst): + sep = "----------------------------------------------------------------------" + for c, e in lst: + print("======================================================================") + print(c) + print(sep) + print(e) + + def __repr__(self): + # Format is compatible with CPython. + return "" % ( + self.testsRun, + self.errorsNum, + self.failuresNum, + ) + def capture_exc(e): buf = io.StringIO() @@ -274,9 +298,15 @@ def run_suite(c, test_result): print(" skipped:", e.args[0]) test_result.skippedNum += 1 except Exception as ex: - exceptions.append(capture_exc(ex)) - print(" FAIL") - test_result.failuresNum += 1 + ex_str = capture_exc(ex) + if isinstance(ex, AssertionError): + test_result.failuresNum += 1 + test_result.failures.append(((name, c), ex_str)) + print(" FAIL") + else: + test_result.errorsNum += 1 + test_result.errors.append(((name, c), ex_str)) + print(" ERROR") # Uncomment to investigate failure in detail # raise continue @@ -299,9 +329,5 @@ def test_cases(m): suite.addTest(c) runner = TestRunner() result = runner.run(suite) - if result.exceptions: - sep = "\n----------------------------------------------------------------------\n" - print(sep) - print(sep.join(result.exceptions)) # Terminate with non zero return code in case of failures - sys.exit(result.failuresNum > 0) + sys.exit(result.failuresNum or result.errorsNum) From 8e82f3d80b436656a37d3feb1edcfc416b1a6674 Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Sat, 15 May 2021 10:35:25 +0300 Subject: [PATCH 23/38] unittest: Support recursive TestSuite's. Signed-off-by: Paul Sokolovsky --- python-stdlib/unittest/unittest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index ac1ff5fc3..dcdc5a1c1 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -276,6 +276,10 @@ def capture_exc(e): # TODO: Uncompliant def run_suite(c, test_result): + if isinstance(c, TestSuite): + c.run(test_result) + return + if isinstance(c, type): o = c() else: From 377ebbfe56076584f697af731c4c9e4259f6cefc Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Mon, 19 Jul 2021 17:28:35 +0300 Subject: [PATCH 24/38] unittest: Add expectedFailure decorator. Signed-off-by: Paul Sokolovsky --- python-stdlib/unittest/unittest.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index dcdc5a1c1..e6793809c 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -199,6 +199,18 @@ def skipUnless(cond, msg): return skip(msg) +def expectedFailure(test): + def test_exp_fail(*args, **kwargs): + try: + test(*args, **kwargs) + except: + pass + else: + assert False, "unexpected success" + + return test_exp_fail + + class TestSuite: def __init__(self): self._tests = [] From 01fcd42042162e62a208fe88c79b981f7d5dd6a2 Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Tue, 20 Jul 2021 22:06:52 +0300 Subject: [PATCH 25/38] unittest: test_unittest: Add tests for expectedFailure decorator. Signed-off-by: Paul Sokolovsky --- python-stdlib/unittest/test_unittest.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/python-stdlib/unittest/test_unittest.py b/python-stdlib/unittest/test_unittest.py index 8188def9e..7d7e4ca27 100644 --- a/python-stdlib/unittest/test_unittest.py +++ b/python-stdlib/unittest/test_unittest.py @@ -126,6 +126,22 @@ def func_under_test(a): if not e1 or "not raised" not in e1.args[0]: self.fail("Expected to catch lack of AssertionError from assert in func_under_test") + @unittest.expectedFailure + def testExpectedFailure(self): + self.assertEqual(1, 0) + + def testExpectedFailureNot(self): + @unittest.expectedFailure + def testInner(): + self.assertEqual(1, 1) + + try: + testInner() + except: + pass + else: + self.fail("Unexpected success was not detected") + if __name__ == "__main__": unittest.main() From 5a53a75ec102fa3510488fca5311f6ffbc400398 Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Tue, 12 Oct 2021 08:57:23 +0300 Subject: [PATCH 26/38] unittest: Print no. of skipped tests in a way compatible with CPython. Perhaps, modern CPython (3.8). Signed-off-by: Paul Sokolovsky --- python-stdlib/unittest/unittest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index e6793809c..b5e2090a0 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -237,7 +237,7 @@ def run(self, suite): else: msg = "OK" if res.skippedNum > 0: - msg += " (%d skipped)" % res.skippedNum + msg += " (skipped=%d)" % res.skippedNum print(msg) return res From ac282d861e5163412883ad7aac8d18dd3cd6e8dd Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Wed, 13 Oct 2021 22:22:20 +0300 Subject: [PATCH 27/38] unittest: Add TextTestRunner as alias for TestRunner. For CPython compatibility. Signed-off-by: Paul Sokolovsky --- python-stdlib/unittest/unittest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index b5e2090a0..2fbe97583 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -243,6 +243,9 @@ def run(self, suite): return res +TextTestRunner = TestRunner + + class TestResult: def __init__(self): self.errorsNum = 0 From f92833b0157ae284b09e1b3c6d355516f8e66209 Mon Sep 17 00:00:00 2001 From: Paul Sokolovsky Date: Sat, 23 Oct 2021 18:09:32 +0300 Subject: [PATCH 28/38] unittest: Support TestCase subclasses with own runTest() method. E.g. for doctest. Signed-off-by: Paul Sokolovsky --- python-stdlib/unittest/unittest.py | 58 +++++++++++++++++------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index 2fbe97583..02a94d04d 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -302,36 +302,44 @@ def run_suite(c, test_result): set_up = getattr(o, "setUp", lambda: None) tear_down = getattr(o, "tearDown", lambda: None) exceptions = [] + + def run_one(m): + print("%s (%s) ..." % (name, c.__qualname__), end="") + set_up() + try: + test_result.testsRun += 1 + m() + print(" ok") + except SkipTest as e: + print(" skipped:", e.args[0]) + test_result.skippedNum += 1 + except Exception as ex: + ex_str = capture_exc(ex) + if isinstance(ex, AssertionError): + test_result.failuresNum += 1 + test_result.failures.append(((name, c), ex_str)) + print(" FAIL") + else: + test_result.errorsNum += 1 + test_result.errors.append(((name, c), ex_str)) + print(" ERROR") + # Uncomment to investigate failure in detail + # raise + finally: + tear_down() + o.doCleanups() + + 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 - print("%s (%s) ..." % (name, c.__qualname__), end="") - set_up() - try: - test_result.testsRun += 1 - m() - print(" ok") - except SkipTest as e: - print(" skipped:", e.args[0]) - test_result.skippedNum += 1 - except Exception as ex: - ex_str = capture_exc(ex) - if isinstance(ex, AssertionError): - test_result.failuresNum += 1 - test_result.failures.append(((name, c), ex_str)) - print(" FAIL") - else: - test_result.errorsNum += 1 - test_result.errors.append(((name, c), ex_str)) - print(" ERROR") - # Uncomment to investigate failure in detail - # raise - continue - finally: - tear_down() - o.doCleanups() + run_one(m) return exceptions From c7eb3de858b91578da0faaaca7e75bd29aa25831 Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Fri, 18 Mar 2022 14:56:24 +1100 Subject: [PATCH 29/38] unittest: Print module name on result lines. Matches cpython format. --- python-stdlib/unittest/unittest.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index 02a94d04d..1211035bd 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -212,15 +212,16 @@ def test_exp_fail(*args, **kwargs): class TestSuite: - def __init__(self): + def __init__(self, name=""): self._tests = [] + self.name = name def addTest(self, cls): self._tests.append(cls) def run(self, result): for c in self._tests: - run_suite(c, result) + run_suite(c, result, self.name) return result @@ -290,7 +291,7 @@ def capture_exc(e): # TODO: Uncompliant -def run_suite(c, test_result): +def run_suite(c, test_result, suite_name=""): if isinstance(c, TestSuite): c.run(test_result) return @@ -302,9 +303,13 @@ def run_suite(c, test_result): set_up = getattr(o, "setUp", lambda: None) tear_down = getattr(o, "tearDown", lambda: None) exceptions = [] + try: + suite_name += "." + c.__qualname__ + except AttributeError: + pass def run_one(m): - print("%s (%s) ..." % (name, c.__qualname__), end="") + print("%s (%s) ..." % (name, suite_name), end="") set_up() try: test_result.testsRun += 1 @@ -351,7 +356,7 @@ def test_cases(m): yield c m = __import__(module) if isinstance(module, str) else module - suite = TestSuite() + suite = TestSuite(m.__name__) for c in test_cases(m): suite.addTest(c) runner = TestRunner() From 9d9ca3d59bad61538c9e11ebca70e98367dac909 Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Fri, 18 Mar 2022 14:58:20 +1100 Subject: [PATCH 30/38] unittest: Run test_* functions as well as TestCase classes. --- python-stdlib/unittest/unittest.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index 1211035bd..c59446b0c 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -332,7 +332,10 @@ def run_one(m): # raise finally: tear_down() - o.doCleanups() + try: + o.doCleanups() + except AttributeError: + pass if hasattr(o, "runTest"): name = str(o) @@ -340,11 +343,16 @@ def run_one(m): return for name in dir(o): - if name.startswith("test"): + 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) + return exceptions @@ -354,6 +362,8 @@ def test_cases(m): c = getattr(m, tn) if isinstance(c, object) and isinstance(c, type) and issubclass(c, TestCase): yield c + elif tn.startswith("test_") and callable(c): + yield c m = __import__(module) if isinstance(module, str) else module suite = TestSuite(m.__name__) From a7b2f6311788e1d658641e130327d7027818dc82 Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Fri, 18 Mar 2022 17:32:25 +1100 Subject: [PATCH 31/38] unittest: Add discover function. --- python-stdlib/unittest/metadata.txt | 1 + python-stdlib/unittest/setup.py | 1 + python-stdlib/unittest/unittest.py | 84 +++++++++++++++++---- python-stdlib/unittest/unittest_discover.py | 70 +++++++++++++++++ 4 files changed, 141 insertions(+), 15 deletions(-) create mode 100644 python-stdlib/unittest/unittest_discover.py diff --git a/python-stdlib/unittest/metadata.txt b/python-stdlib/unittest/metadata.txt index f3c23ccee..8c0c0ff56 100644 --- a/python-stdlib/unittest/metadata.txt +++ b/python-stdlib/unittest/metadata.txt @@ -1,3 +1,4 @@ srctype = micropython-lib type = module version = 0.3.2 +depends = argparse, fnmatch diff --git a/python-stdlib/unittest/setup.py b/python-stdlib/unittest/setup.py index 74b985e81..6549e44bf 100644 --- a/python-stdlib/unittest/setup.py +++ b/python-stdlib/unittest/setup.py @@ -21,4 +21,5 @@ license="MIT", cmdclass={"sdist": sdist_upip.sdist}, py_modules=["unittest"], + install_requires=["micropython-argparse", "micropython-fnmatch"], ) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index c59446b0c..5a87f601b 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -1,4 +1,5 @@ import sys +import uos try: import io @@ -280,6 +281,15 @@ def __repr__(self): self.failuresNum, ) + def __add__(self, other): + self.errorsNum += other.errorsNum + self.failuresNum += other.failuresNum + self.skippedNum += other.skippedNum + self.testsRun += other.testsRun + self.errors.extend(other.errors) + self.failures.extend(other.failures) + return self + def capture_exc(e): buf = io.StringIO() @@ -290,7 +300,6 @@ def capture_exc(e): return buf.getvalue() -# TODO: Uncompliant def run_suite(c, test_result, suite_name=""): if isinstance(c, TestSuite): c.run(test_result) @@ -343,7 +352,7 @@ def run_one(m): return for name in dir(o): - if name.startswith("test_"): + if name.startswith("test"): m = getattr(o, name) if not callable(m): continue @@ -356,20 +365,65 @@ def run_one(m): 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): + 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 + + return discover(runner=runner) + + def main(module="__main__"): - def test_cases(m): - for tn in dir(m): - c = getattr(m, tn) - if isinstance(c, object) and isinstance(c, type) and issubclass(c, TestCase): - yield c - elif tn.startswith("test_") and callable(c): - yield c - - m = __import__(module) if isinstance(module, str) else module - suite = TestSuite(m.__name__) - for c in test_cases(m): - suite.addTest(c) runner = TestRunner() - result = runner.run(suite) + + 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 __name__ == "__main__": + main() diff --git a/python-stdlib/unittest/unittest_discover.py b/python-stdlib/unittest/unittest_discover.py new file mode 100644 index 000000000..7c5abd1f8 --- /dev/null +++ b/python-stdlib/unittest/unittest_discover.py @@ -0,0 +1,70 @@ +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 From cb8d108ac1d032733c737fa233874f38d3c2b800 Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Tue, 3 May 2022 16:10:12 +1000 Subject: [PATCH 32/38] unittest: Add test for environment isolation. --- python-stdlib/unittest/test_unittest.py | 6 ++++++ python-stdlib/unittest/test_unittest_isolated.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 python-stdlib/unittest/test_unittest_isolated.py diff --git a/python-stdlib/unittest/test_unittest.py b/python-stdlib/unittest/test_unittest.py index 7d7e4ca27..690fb40df 100644 --- a/python-stdlib/unittest/test_unittest.py +++ b/python-stdlib/unittest/test_unittest.py @@ -1,4 +1,5 @@ import unittest +from test_unittest_isolated import global_context class TestUnittestAssertions(unittest.TestCase): @@ -142,6 +143,11 @@ def testInner(): else: self.fail("Unexpected success was not detected") + def test_NotChangedByOtherTest(self): + global global_context + assert global_context is None + global_context = True + if __name__ == "__main__": unittest.main() diff --git a/python-stdlib/unittest/test_unittest_isolated.py b/python-stdlib/unittest/test_unittest_isolated.py new file mode 100644 index 000000000..a828f9a3b --- /dev/null +++ b/python-stdlib/unittest/test_unittest_isolated.py @@ -0,0 +1,15 @@ +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() From 9f6f21150622b25e665cc5d9533839a5cc092121 Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Tue, 12 Apr 2022 05:41:15 +1000 Subject: [PATCH 33/38] unittest: Reset python env between tests. --- python-stdlib/unittest/unittest.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index 5a87f601b..dd4bb607d 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -10,6 +10,13 @@ traceback = None +def _snapshot_modules(): + return {k: v for k, v in sys.modules.items()} + + +__modules__ = _snapshot_modules() + + class SkipTest(Exception): pass @@ -375,6 +382,13 @@ def _test_cases(mod): 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)) @@ -395,6 +409,8 @@ def run_module(runner, module, path, top): def discover(runner: TestRunner): from unittest_discover import discover + global __modules__ + __modules__ = _snapshot_modules() return discover(runner=runner) From 9b6315a2ba8c9274f09bec7bf2aeb2a4d2732f98 Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Tue, 3 May 2022 17:07:48 +1000 Subject: [PATCH 34/38] unittest: Add exception capturing for subTest. --- python-stdlib/unittest/test_unittest.py | 8 +++ python-stdlib/unittest/unittest.py | 75 +++++++++++++++++++------ 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/python-stdlib/unittest/test_unittest.py b/python-stdlib/unittest/test_unittest.py index 690fb40df..1b02056db 100644 --- a/python-stdlib/unittest/test_unittest.py +++ b/python-stdlib/unittest/test_unittest.py @@ -148,6 +148,14 @@ def test_NotChangedByOtherTest(self): assert global_context is None global_context = True + def test_subtest_even(self): + """ + Test that numbers between 0 and 5 are all even. + """ + for i in range(0, 10, 2): + with self.subTest("Should only pass for even numbers", i=i): + self.assertEqual(i % 2, 0) + if __name__ == "__main__": unittest.main() diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index dd4bb607d..8d20acd7d 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -37,11 +37,35 @@ def __exit__(self, exc_type, exc_value, tb): return False +# These are used to provide required context to things like subTest +__current_test__ = None +__test_result__ = None + + +class SubtestContext: + def __enter__(self): + pass + + def __exit__(self, *exc_info): + if exc_info[0] is not None: + # Exception raised + global __test_result__, __current_test__ + handle_test_exception( + __current_test__, + __test_result__, + exc_info + ) + # Suppress the exception as we've captured it above + return True + + + + class NullContext: def __enter__(self): pass - def __exit__(self, a, b, c): + def __exit__(self, exc_type, exc_value, traceback): pass @@ -61,7 +85,7 @@ def doCleanups(self): func(*args, **kwargs) def subTest(self, msg=None, **params): - return NullContext() + return SubtestContext(msg=msg, params=params) def skipTest(self, reason): raise SkipTest(reason) @@ -298,15 +322,29 @@ def __add__(self, other): return self -def capture_exc(e): +def capture_exc(exc, traceback): buf = io.StringIO() if hasattr(sys, "print_exception"): - sys.print_exception(e, buf) + sys.print_exception(exc, buf) elif traceback is not None: - traceback.print_exception(None, e, sys.exc_info()[2], file=buf) + traceback.print_exception(None, exc, traceback, file=buf) return buf.getvalue() +def handle_test_exception(current_test: tuple, test_result: TestResult, exc_info: tuple): + exc = exc_info[1] + traceback = exc_info[2] + ex_str = capture_exc(exc, traceback) + if isinstance(exc, AssertionError): + test_result.failuresNum += 1 + test_result.failures.append((current_test, ex_str)) + print(" FAIL") + else: + test_result.errorsNum += 1 + test_result.errors.append((current_test, ex_str)) + print(" ERROR") + + def run_suite(c, test_result, suite_name=""): if isinstance(c, TestSuite): c.run(test_result) @@ -324,29 +362,34 @@ def run_suite(c, test_result, suite_name=""): except AttributeError: pass - def run_one(m): + def run_one(test_function): + global __test_result__, __current_test__ print("%s (%s) ..." % (name, suite_name), end="") set_up() + __test_result__ = test_result + test_container = f"({suite_name})" + __current_test__ = (name, test_container) try: test_result.testsRun += 1 - m() + test_globals = dict(**globals()) + test_globals["test_function"] = test_function + exec("test_function()", test_globals, test_globals) + # No exception occurred, test passed print(" ok") except SkipTest as e: print(" skipped:", e.args[0]) test_result.skippedNum += 1 except Exception as ex: - ex_str = capture_exc(ex) - if isinstance(ex, AssertionError): - test_result.failuresNum += 1 - test_result.failures.append(((name, c), ex_str)) - print(" FAIL") - else: - test_result.errorsNum += 1 - test_result.errors.append(((name, c), ex_str)) - print(" ERROR") + handle_test_exception( + current_test=(name, c), + test_result=test_result, + exc_info=sys.exc_info() + ) # Uncomment to investigate failure in detail # raise finally: + __test_result__ = None + __current_test__ = None tear_down() try: o.doCleanups() From ddeb9a7da2355bea917af99dbda6dc1d9926ba42 Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Tue, 3 May 2022 17:25:25 +1000 Subject: [PATCH 35/38] unittest: Improve failure text consistency with cpython. --- python-stdlib/unittest/unittest.py | 46 +++++++++++++++++++----------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index 8d20acd7d..0fcb32937 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -43,6 +43,10 @@ def __exit__(self, exc_type, exc_value, tb): class SubtestContext: + def __init__(self, msg=None, params=None): + self.msg = msg + self.params = params + def __enter__(self): pass @@ -50,17 +54,18 @@ def __exit__(self, *exc_info): if exc_info[0] is not None: # Exception raised global __test_result__, __current_test__ - handle_test_exception( - __current_test__, - __test_result__, - exc_info - ) + test_details = __current_test__ + if self.msg: + test_details += (f" [{self.msg}]",) + if self.params: + 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) # Suppress the exception as we've captured it above return True - - class NullContext: def __enter__(self): pass @@ -287,6 +292,7 @@ def __init__(self): self.testsRun = 0 self.errors = [] self.failures = [] + self.newFailures = 0 def wasSuccessful(self): return self.errorsNum == 0 and self.failuresNum == 0 @@ -299,8 +305,9 @@ def printErrors(self): def printErrorList(self, lst): sep = "----------------------------------------------------------------------" for c, e in lst: + detail = " ".join((str(i) for i in c)) print("======================================================================") - print(c) + print(f"FAIL: {detail}") print(sep) print(e) @@ -331,18 +338,23 @@ def capture_exc(exc, traceback): return buf.getvalue() -def handle_test_exception(current_test: tuple, test_result: TestResult, exc_info: tuple): +def handle_test_exception( + current_test: tuple, test_result: TestResult, exc_info: tuple, verbose=True +): exc = exc_info[1] - traceback = exc_info[2] + traceback = exc_info[2] ex_str = capture_exc(exc, traceback) if isinstance(exc, AssertionError): test_result.failuresNum += 1 test_result.failures.append((current_test, ex_str)) - print(" FAIL") + if verbose: + print(" FAIL") else: test_result.errorsNum += 1 test_result.errors.append((current_test, ex_str)) - print(" ERROR") + if verbose: + print(" ERROR") + test_result.newFailures += 1 def run_suite(c, test_result, suite_name=""): @@ -370,20 +382,22 @@ def run_one(test_function): test_container = f"({suite_name})" __current_test__ = (name, test_container) 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) # No exception occurred, test passed - print(" ok") + if test_result.newFailures: + print(" FAIL") + else: + print(" ok") except SkipTest as e: print(" skipped:", e.args[0]) test_result.skippedNum += 1 except Exception as ex: handle_test_exception( - current_test=(name, c), - test_result=test_result, - exc_info=sys.exc_info() + current_test=(name, c), test_result=test_result, exc_info=sys.exc_info() ) # Uncomment to investigate failure in detail # raise From 2d61dbdb93fa941c54b3990c2ee046074d2fce07 Mon Sep 17 00:00:00 2001 From: Steve Li Date: Tue, 5 Apr 2022 10:05:30 +1000 Subject: [PATCH 36/38] unittest: Add setUpClass and tearDownClass handling. Supports setUp and tearDown functionality at Class level. --- python-stdlib/unittest/test_unittest.py | 19 +++++++++++++++++++ python-stdlib/unittest/unittest.py | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/python-stdlib/unittest/test_unittest.py b/python-stdlib/unittest/test_unittest.py index 1b02056db..8e108995a 100644 --- a/python-stdlib/unittest/test_unittest.py +++ b/python-stdlib/unittest/test_unittest.py @@ -157,5 +157,24 @@ def test_subtest_even(self): 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() diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index 0fcb32937..cfb274a6c 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -366,6 +366,8 @@ def run_suite(c, test_result, suite_name=""): o = c() else: o = c + set_up_class = getattr(o, "setUpClass", lambda: None) + tear_down_class = getattr(o, "tearDownClass", lambda: None) set_up = getattr(o, "setUp", lambda: None) tear_down = getattr(o, "tearDown", lambda: None) exceptions = [] @@ -410,6 +412,8 @@ def run_one(test_function): except AttributeError: pass + set_up_class() + if hasattr(o, "runTest"): name = str(o) run_one(o.runTest) @@ -426,6 +430,8 @@ def run_one(test_function): name = o.__name__ run_one(o) + tear_down_class() + return exceptions From 959115d3a9e203b9cb780746798ce05d44d0b250 Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Thu, 5 May 2022 21:17:48 +1000 Subject: [PATCH 37/38] unittest: Add support for specifying custom TestRunner. --- python-stdlib/unittest/unittest.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/python-stdlib/unittest/unittest.py b/python-stdlib/unittest/unittest.py index cfb274a6c..b61686981 100644 --- a/python-stdlib/unittest/unittest.py +++ b/python-stdlib/unittest/unittest.py @@ -263,7 +263,7 @@ def run(self, result): class TestRunner: - def run(self, suite): + def run(self, suite: TestSuite): res = TestResult() suite.run(res) @@ -292,7 +292,8 @@ def __init__(self): self.testsRun = 0 self.errors = [] self.failures = [] - self.newFailures = 0 + self.skipped = [] + self._newFailures = 0 def wasSuccessful(self): return self.errorsNum == 0 and self.failuresNum == 0 @@ -326,6 +327,7 @@ def __add__(self, other): self.testsRun += other.testsRun self.errors.extend(other.errors) self.failures.extend(other.failures) + self.skipped.extend(other.skipped) return self @@ -354,10 +356,10 @@ def handle_test_exception( test_result.errors.append((current_test, ex_str)) if verbose: print(" ERROR") - test_result.newFailures += 1 + test_result._newFailures += 1 -def run_suite(c, test_result, suite_name=""): +def run_suite(c, test_result: TestResult, suite_name=""): if isinstance(c, TestSuite): c.run(test_result) return @@ -384,19 +386,21 @@ def run_one(test_function): test_container = f"({suite_name})" __current_test__ = (name, test_container) try: - test_result.newFailures = 0 + 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) # No exception occurred, test passed - if test_result.newFailures: + if test_result._newFailures: print(" FAIL") else: print(" ok") except SkipTest as e: - print(" skipped:", e.args[0]) + reason = e.args[0] + print(" skipped:", reason) test_result.skippedNum += 1 + test_result.skipped.append((name, c, reason)) except Exception as ex: handle_test_exception( current_test=(name, c), test_result=test_result, exc_info=sys.exc_info() @@ -477,8 +481,14 @@ def discover(runner: TestRunner): return discover(runner=runner) -def main(module="__main__"): - runner = TestRunner() +def main(module="__main__", testRunner=None): + if testRunner: + if isinstance(testRunner, type): + runner = testRunner() + else: + runner = testRunner + else: + runner = TestRunner() if len(sys.argv) <= 1: result = discover(runner) From db4c739863e49fc874bdaae8aa8c316c7ed4276a Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Sun, 24 Oct 2021 09:17:03 +0300 Subject: [PATCH 38/38] unittest: Version 0.9.0 --- python-stdlib/unittest/metadata.txt | 2 +- python-stdlib/unittest/setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python-stdlib/unittest/metadata.txt b/python-stdlib/unittest/metadata.txt index 8c0c0ff56..ba9a983fc 100644 --- a/python-stdlib/unittest/metadata.txt +++ b/python-stdlib/unittest/metadata.txt @@ -1,4 +1,4 @@ srctype = micropython-lib type = module -version = 0.3.2 +version = 0.9.0 depends = argparse, fnmatch diff --git a/python-stdlib/unittest/setup.py b/python-stdlib/unittest/setup.py index 6549e44bf..d1604f813 100644 --- a/python-stdlib/unittest/setup.py +++ b/python-stdlib/unittest/setup.py @@ -10,7 +10,7 @@ setup( name="micropython-unittest", - version="0.3.2", + version="0.9.0", description="unittest module for MicroPython", long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.", url="https://github.com/micropython/micropython-lib",