From 1bdb63e34e648b4f6baa44391d79e7b7de05bec4 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Mon, 26 Jan 2026 15:12:01 +0100 Subject: [PATCH] Add source of unittest package --- internal_filesystem/lib/README.md | 4 +- internal_filesystem/lib/unittest/__init__.mpy | Bin 5853 -> 0 bytes internal_filesystem/lib/unittest/__init__.py | 464 ++++++++++++++++++ 3 files changed, 466 insertions(+), 2 deletions(-) delete mode 100644 internal_filesystem/lib/unittest/__init__.mpy create mode 100644 internal_filesystem/lib/unittest/__init__.py diff --git a/internal_filesystem/lib/README.md b/internal_filesystem/lib/README.md index ed312a17..25d05903 100644 --- a/internal_filesystem/lib/README.md +++ b/internal_filesystem/lib/README.md @@ -1,8 +1,7 @@ This /lib folder contains: + - mip.install('github:jonnor/micropython-zipfile') - mip.install("aiohttp") # easy websockets -- mip.install("collections") # used by aiohttp -- mip.install("unittest") - https://github.com/micropython/micropython-lib/blob/master/micropython/aiorepl/aiorepl.py version 0.2.2 # for asyncio REPL, allowing await expressions @@ -10,3 +9,4 @@ This /lib folder contains: - https://github.com/micropython/micropython-lib/blob/master/python-stdlib/binascii/binascii.py version 2.4.1 # for base64.py - https://github.com/micropython/micropython-lib/blob/master/python-stdlib/logging/logging.py version 0.6.2 # for About app - https://github.com/micropython/micropython-lib/blob/master/python-stdlib/shutil/shutil.py version 0.0.5 # for rmtree() +- https://github.com/micropython/micropython-lib/blob/master/python-stdlib/unittest/unittest/__init__.py version 0.10.4 # for testing (also on-device) diff --git a/internal_filesystem/lib/unittest/__init__.mpy b/internal_filesystem/lib/unittest/__init__.mpy deleted file mode 100644 index ce71760c6ae6314e0db15e51d9c1631170cda7bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5853 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 f3pMg?Fkw6(#Jxe-(1h{OCK2NeW{i=LayJ= y, msg + + def assertAlmostEqual(self, x, y, places=None, msg="", delta=None): + if x == y: + return + if delta is not None and places is not None: + raise TypeError("specify delta or places not both") + + if delta is not None: + if abs(x - y) <= delta: + return + if not msg: + msg = "%r != %r within %r delta" % (x, y, delta) + else: + if places is None: + places = 7 + if round(abs(y - x), places) == 0: + return + if not msg: + msg = "%r != %r within %r places" % (x, y, places) + + assert False, msg + + def assertNotAlmostEqual(self, x, y, places=None, msg="", delta=None): + if delta is not None and places is not None: + raise TypeError("specify delta or places not both") + + if delta is not None: + if not (x == y) and abs(x - y) > delta: + return + if not msg: + msg = "%r == %r within %r delta" % (x, y, delta) + else: + if places is None: + places = 7 + if not (x == y) and round(abs(y - x), places) != 0: + return + if not msg: + msg = "%r == %r within %r places" % (x, y, places) + + assert False, msg + + def assertIs(self, x, y, msg=""): + if not msg: + msg = "%r is not %r" % (x, y) + assert x is y, msg + + def assertIsNot(self, x, y, msg=""): + if not msg: + msg = "%r is %r" % (x, y) + assert x is not y, msg + + def assertIsNone(self, x, msg=""): + if not msg: + msg = "%r is not None" % x + assert x is None, msg + + def assertIsNotNone(self, x, msg=""): + if not msg: + msg = "%r is None" % x + assert x is not None, msg + + def assertTrue(self, x, msg=""): + if not msg: + msg = "Expected %r to be True" % x + assert x, msg + + def assertFalse(self, x, msg=""): + if not msg: + msg = "Expected %r to be False" % x + assert not x, msg + + def assertIn(self, x, y, msg=""): + if not msg: + msg = "Expected %r to be in %r" % (x, y) + assert x in y, msg + + def assertIsInstance(self, x, y, msg=""): + assert isinstance(x, y), msg + + def assertRaises(self, exc, func=None, *args, **kwargs): + if func is None: + return AssertRaisesContext(exc) + + try: + func(*args, **kwargs) + except Exception as e: + if isinstance(e, exc): + return + raise e + + assert False, "%r not raised" % exc + + def assertWarns(self, warn): + return NullContext() + + +def skip(msg): + def _decor(fun): + # We just replace original fun with _inner + def _inner(self): + raise SkipTest(msg) + + return _inner + + return _decor + + +def skipIf(cond, msg): + if not cond: + return lambda x: x + return skip(msg) + + +def skipUnless(cond, msg): + if cond: + return lambda x: x + 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, 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, 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): + 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)) + else: + msg = "OK" + if res.skippedNum > 0: + msg += " (skipped=%d)" % res.skippedNum + print(msg) + + return res + + +TextTestRunner = TestRunner + + +class TestResult: + def __init__(self): + self.errorsNum = 0 + self.failuresNum = 0 + self.skippedNum = 0 + self.testsRun = 0 + self.errors = [] + self.failures = [] + self.skipped = [] + self._newFailures = 0 + + def wasSuccessful(self): + return self.errorsNum == 0 and self.failuresNum == 0 + + def printErrors(self): + if self.errors or self.failures: + print() + self.printErrorList(self.errors) + self.printErrorList(self.failures) + + def printErrorList(self, lst): + sep = "----------------------------------------------------------------------" + for c, e in lst: + detail = " ".join((str(i) for i in c)) + print("======================================================================") + print(f"FAIL: {detail}") + print(sep) + print(e) + + def __repr__(self): + # Format is compatible with CPython. + return "" % ( + self.testsRun, + self.errorsNum, + 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) + self.skipped.extend(other.skipped) + return self + + +def _capture_exc(exc, exc_traceback): + buf = io.StringIO() + if hasattr(sys, "print_exception"): + sys.print_exception(exc, buf) + elif traceback is not None: + traceback.print_exception(None, exc, exc_traceback, file=buf) + return buf.getvalue() + + +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) + if isinstance(exc, SkipTest): + reason = exc.args[0] + test_result.skippedNum += 1 + test_result.skipped.append((current_test, reason)) + print(" skipped:", reason) + return + elif isinstance(exc, AssertionError): + test_result.failuresNum += 1 + test_result.failures.append((current_test, ex_str)) + if verbose: + print(" FAIL") + else: + test_result.errorsNum += 1 + test_result.errors.append((current_test, ex_str)) + if verbose: + print(" ERROR") + test_result._newFailures += 1 + + +def _run_suite(c, test_result: TestResult, suite_name=""): + if isinstance(c, TestSuite): + c.run(test_result) + return + + if isinstance(c, type): + 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 = [] + try: + suite_name += "." + c.__qualname__ + except AttributeError: + pass + + 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._newFailures = 0 + test_result.testsRun += 1 + test_function() + # No exception occurred, test passed + if test_result._newFailures: + print(" FAIL") + else: + print(" ok") + except Exception as ex: + _handle_test_exception( + current_test=(name, c), test_result=test_result, exc_info=(type(ex), ex, None) + ) + # Uncomment to investigate failure in detail + # raise ex + finally: + __test_result__ = None + __current_test__ = None + tear_down() + try: + o.doCleanups() + except AttributeError: + pass + + set_up_class() + try: + 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) + + if callable(o): + name = o.__name__ + run_one(o) + finally: + tear_down_class() + + return exceptions + + +# 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 is None: + testRunner = TestRunner() + elif isinstance(testRunner, type): + testRunner = testRunner() + + if isinstance(module, str): + module = __import__(module) + suite = TestSuite(module.__name__) + suite._load_module(module) + return testRunner.run(suite)