mirror of
https://github.com/micropython/micropython.git
synced 2026-01-31 16:20:20 +01:00
Some checks failed
JavaScript code lint and formatting with Biome / eslint (push) Has been cancelled
Check code formatting / code-formatting (push) Has been cancelled
Check spelling with codespell / codespell (push) Has been cancelled
Build docs / build (push) Has been cancelled
Check examples / embedding (push) Has been cancelled
Package mpremote / build (push) Has been cancelled
.mpy file format and tools / test (push) Has been cancelled
Build ports metadata / build (push) Has been cancelled
alif port / build_alif (alif_ae3_build) (push) Has been cancelled
cc3200 port / build (push) Has been cancelled
esp32 port / build_idf (esp32_build_c2_c5_c6) (push) Has been cancelled
esp32 port / build_idf (esp32_build_cmod_spiram_s2) (push) Has been cancelled
esp32 port / build_idf (esp32_build_p4) (push) Has been cancelled
esp32 port / build_idf (esp32_build_s3_c3) (push) Has been cancelled
esp8266 port / build (push) Has been cancelled
mimxrt port / build (push) Has been cancelled
nrf port / build (push) Has been cancelled
powerpc port / build (push) Has been cancelled
qemu port / build_and_test_arm (bigendian) (push) Has been cancelled
qemu port / build_and_test_arm (sabrelite) (push) Has been cancelled
qemu port / build_and_test_arm (thumb_hardfp) (push) Has been cancelled
qemu port / build_and_test_arm (thumb_softfp) (push) Has been cancelled
qemu port / build_and_test_rv32 (push) Has been cancelled
qemu port / build_and_test_rv64 (push) Has been cancelled
renesas-ra port / build_renesas_ra_board (push) Has been cancelled
rp2 port / build (push) Has been cancelled
samd port / build (push) Has been cancelled
stm32 port / build_stm32 (stm32_misc_build) (push) Has been cancelled
stm32 port / build_stm32 (stm32_nucleo_build) (push) Has been cancelled
stm32 port / build_stm32 (stm32_pyb_build) (push) Has been cancelled
unix port / minimal (push) Has been cancelled
unix port / reproducible (push) Has been cancelled
unix port / standard (push) Has been cancelled
unix port / standard_v2 (push) Has been cancelled
unix port / coverage (push) Has been cancelled
unix port / coverage_32bit (push) Has been cancelled
unix port / nanbox (push) Has been cancelled
unix port / longlong (push) Has been cancelled
unix port / float (push) Has been cancelled
unix port / gil_enabled (push) Has been cancelled
unix port / stackless_clang (push) Has been cancelled
unix port / float_clang (push) Has been cancelled
unix port / settrace_stackless (push) Has been cancelled
unix port / repr_b (push) Has been cancelled
unix port / macos (push) Has been cancelled
unix port / qemu_mips (push) Has been cancelled
unix port / qemu_arm (push) Has been cancelled
unix port / qemu_riscv64 (push) Has been cancelled
unix port / sanitize_address (push) Has been cancelled
unix port / sanitize_undefined (push) Has been cancelled
webassembly port / build (push) Has been cancelled
windows port / build-vs (Debug, true, x64, dev, 2017, [15, 16)) (push) Has been cancelled
windows port / build-vs (Debug, true, x86, dev, 2017, [15, 16)) (push) Has been cancelled
windows port / build-vs (Debug, x64, dev, 2022, [17, 18)) (push) Has been cancelled
windows port / build-vs (Debug, x86, dev, 2022, [17, 18)) (push) Has been cancelled
windows port / build-vs (Release, true, x64, dev, 2017, [15, 16)) (push) Has been cancelled
windows port / build-vs (Release, true, x64, dev, 2019, [16, 17)) (push) Has been cancelled
windows port / build-vs (Release, true, x64, standard, 2017, [15, 16)) (push) Has been cancelled
windows port / build-vs (Release, true, x64, standard, 2019, [16, 17)) (push) Has been cancelled
windows port / build-vs (Release, true, x86, dev, 2017, [15, 16)) (push) Has been cancelled
windows port / build-vs (Release, true, x86, dev, 2019, [16, 17)) (push) Has been cancelled
windows port / build-vs (Release, true, x86, standard, 2017, [15, 16)) (push) Has been cancelled
windows port / build-vs (Release, true, x86, standard, 2019, [16, 17)) (push) Has been cancelled
windows port / build-vs (Release, x64, dev, 2022, [17, 18)) (push) Has been cancelled
windows port / build-vs (Release, x64, standard, 2022, [17, 18)) (push) Has been cancelled
windows port / build-vs (Release, x86, dev, 2022, [17, 18)) (push) Has been cancelled
windows port / build-vs (Release, x86, standard, 2022, [17, 18)) (push) Has been cancelled
windows port / build-mingw (i686, mingw32, dev) (push) Has been cancelled
windows port / build-mingw (i686, mingw32, standard) (push) Has been cancelled
windows port / build-mingw (x86_64, mingw64, dev) (push) Has been cancelled
windows port / build-mingw (x86_64, mingw64, standard) (push) Has been cancelled
windows port / cross-build-on-linux (push) Has been cancelled
zephyr port / build (push) Has been cancelled
Python code lint and formatting with ruff / ruff (push) Has been cancelled
The test runners have evolved over time and become more and more complex. In particular `tests/run-tests.py` is rather large now. The test runners also duplicate some functionality amongst themselves. As a start to improving this situation, this commit factors out the helper functions from `run-tests.py` into a new `test_utils.py` file, and uses that new module in all test runners. There should be no functional change here. Signed-off-by: Damien George <damien@micropython.org>
359 lines
12 KiB
Python
359 lines
12 KiB
Python
# This file is part of the MicroPython project, http://micropython.org/
|
|
# The MIT License (MIT)
|
|
# Copyright (c) 2019-2025 Damien P. George
|
|
|
|
import inspect
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
# See stackoverflow.com/questions/2632199: __file__ nor sys.argv[0]
|
|
# are guaranteed to always work, this one should though.
|
|
_BASEPATH = os.path.dirname(os.path.abspath(inspect.getsourcefile(lambda: None)))
|
|
|
|
|
|
def base_path(*p):
|
|
return os.path.abspath(os.path.join(_BASEPATH, *p)).replace("\\", "/")
|
|
|
|
|
|
sys.path.append(base_path("../tools"))
|
|
import pyboard
|
|
|
|
# File with the test results.
|
|
_RESULTS_FILE = "_results.json"
|
|
|
|
# Maximum time to run a single test, in seconds.
|
|
TEST_TIMEOUT = float(os.environ.get("MICROPY_TEST_TIMEOUT", 30))
|
|
|
|
# mpy-cross is only needed if --via-mpy command-line arg is passed
|
|
if os.name == "nt":
|
|
MPYCROSS = os.getenv("MICROPY_MPYCROSS", base_path("../mpy-cross/build/mpy-cross.exe"))
|
|
else:
|
|
MPYCROSS = os.getenv("MICROPY_MPYCROSS", base_path("../mpy-cross/build/mpy-cross"))
|
|
|
|
test_instance_description = """\
|
|
By default the tests are run against the unix port of MicroPython. To run it
|
|
against something else, use the -t option. See below for details.
|
|
"""
|
|
|
|
test_instance_epilog = """\
|
|
The -t option accepts the following for the test instance:
|
|
- unix - use the unix port of MicroPython, specified by the MICROPY_MICROPYTHON
|
|
environment variable (which defaults to the standard variant of either the unix
|
|
or windows ports, depending on the host platform)
|
|
- webassembly - use the webassembly port of MicroPython, specified by the
|
|
MICROPY_MICROPYTHON_MJS environment variable (which defaults to the standard
|
|
variant of the webassembly port)
|
|
- port:<device> - connect to and use the given serial port device
|
|
- a<n> - connect to and use /dev/ttyACM<n>
|
|
- u<n> - connect to and use /dev/ttyUSB<n>
|
|
- c<n> - connect to and use COM<n>
|
|
- exec:<command> - execute a command and attach to its stdin/stdout
|
|
- execpty:<command> - execute a command and attach to the printed /dev/pts/<n> device
|
|
- <a>.<b>.<c>.<d> - connect to the given IPv4 address
|
|
- anything else specifies a serial port
|
|
"""
|
|
|
|
test_directory_description = """\
|
|
Tests are discovered by scanning test directories for .py files or using the
|
|
specified test files. If test files nor directories are specified, the script
|
|
expects to be ran in the tests directory (where this file is located) and the
|
|
builtin tests suitable for the target platform are ran.
|
|
"""
|
|
|
|
# Code to allow a target MicroPython to import an .mpy from RAM
|
|
# Note: the module is named `__injected_test` but it needs to have `__name__` set to
|
|
# `__main__` so that the test sees itself as the main module, eg so unittest works.
|
|
_injected_import_hook_code = """\
|
|
import sys, os, io, vfs
|
|
class __File(io.IOBase):
|
|
def __init__(self):
|
|
module = sys.modules['__injected_test']
|
|
module.__name__ = '__main__'
|
|
sys.modules['__main__'] = module
|
|
self.off = 0
|
|
def ioctl(self, request, arg):
|
|
if request == 4: # MP_STREAM_CLOSE
|
|
return 0
|
|
return -1
|
|
def readinto(self, buf):
|
|
buf[:] = memoryview(__buf)[self.off:self.off + len(buf)]
|
|
self.off += len(buf)
|
|
return len(buf)
|
|
class __FS:
|
|
def mount(self, readonly, mkfs):
|
|
pass
|
|
def umount(self):
|
|
pass
|
|
def chdir(self, path):
|
|
pass
|
|
def getcwd(self):
|
|
return ""
|
|
def stat(self, path):
|
|
if path == '__injected_test.mpy':
|
|
return (0,0,0,0,0,0,0,0,0,0)
|
|
else:
|
|
raise OSError(2) # ENOENT
|
|
def open(self, path, mode):
|
|
self.stat(path)
|
|
return __File()
|
|
vfs.mount(__FS(), '/__vfstest')
|
|
os.chdir('/__vfstest')
|
|
{import_prologue}
|
|
__import__('__injected_test')
|
|
"""
|
|
|
|
|
|
class PyboardNodeRunner:
|
|
def __init__(self):
|
|
mjs = os.getenv("MICROPY_MICROPYTHON_MJS")
|
|
if mjs is None:
|
|
mjs = base_path("../ports/webassembly/build-standard/micropython.mjs")
|
|
else:
|
|
mjs = os.path.abspath(mjs)
|
|
self.micropython_mjs = mjs
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
def run_script_on_remote_target(self, args, test_file, is_special, requires_target_wiring):
|
|
cwd = os.path.dirname(test_file)
|
|
|
|
# Create system command list.
|
|
cmdlist = ["node"]
|
|
if test_file.endswith(".py"):
|
|
# Run a Python script indirectly via "node micropython.mjs <script.py>".
|
|
cmdlist.append(self.micropython_mjs)
|
|
if args.heapsize is not None:
|
|
cmdlist.extend(["-X", "heapsize=" + args.heapsize])
|
|
cmdlist.append(test_file)
|
|
else:
|
|
# Run a js/mjs script directly with Node, passing in the path to micropython.mjs.
|
|
cmdlist.append(test_file)
|
|
cmdlist.append(self.micropython_mjs)
|
|
|
|
# Run the script.
|
|
try:
|
|
had_crash = False
|
|
output_mupy = subprocess.check_output(
|
|
cmdlist, stderr=subprocess.STDOUT, timeout=TEST_TIMEOUT, cwd=cwd
|
|
)
|
|
except subprocess.CalledProcessError as er:
|
|
had_crash = True
|
|
output_mupy = er.output + b"CRASH"
|
|
except subprocess.TimeoutExpired as er:
|
|
had_crash = True
|
|
output_mupy = (er.output or b"") + b"TIMEOUT"
|
|
|
|
# Return the results.
|
|
return had_crash, output_mupy
|
|
|
|
|
|
def rm_f(fname):
|
|
if os.path.exists(fname):
|
|
os.remove(fname)
|
|
|
|
|
|
def normalize_newlines(data):
|
|
"""Normalize newline variations to \\n.
|
|
|
|
Only normalizes actual line endings, not literal \\r characters in strings.
|
|
Handles \\r\\r\\n and \\r\\n cases to ensure consistent comparison
|
|
across different platforms and terminals.
|
|
"""
|
|
if isinstance(data, bytes):
|
|
# Handle PTY double-newline issue first
|
|
data = data.replace(b"\r\r\n", b"\n")
|
|
# Then handle standard Windows line endings
|
|
data = data.replace(b"\r\n", b"\n")
|
|
# Don't convert standalone \r as it might be literal content
|
|
return data
|
|
|
|
|
|
def set_injected_prologue(prologue):
|
|
global _injected_import_hook_code
|
|
_injected_import_hook_code = _injected_import_hook_code.replace("{import_prologue}", prologue)
|
|
|
|
|
|
def get_results_filename(args):
|
|
return os.path.join(args.result_dir, _RESULTS_FILE)
|
|
|
|
|
|
def convert_device_shortcut_to_real_device(device):
|
|
if device.startswith("port:"):
|
|
return device.split(":", 1)[1]
|
|
elif device.startswith("a") and device[1:].isdigit():
|
|
return "/dev/ttyACM" + device[1:]
|
|
elif device.startswith("u") and device[1:].isdigit():
|
|
return "/dev/ttyUSB" + device[1:]
|
|
elif device.startswith("c") and device[1:].isdigit():
|
|
return "COM" + device[1:]
|
|
else:
|
|
return device
|
|
|
|
|
|
def get_test_instance(test_instance, baudrate, user, password):
|
|
if test_instance == "unix":
|
|
return None
|
|
elif test_instance == "webassembly":
|
|
return PyboardNodeRunner()
|
|
else:
|
|
# Assume it's a device path.
|
|
port = convert_device_shortcut_to_real_device(test_instance)
|
|
|
|
pyb = pyboard.Pyboard(port, baudrate, user, password)
|
|
pyboard.Pyboard.run_script_on_remote_target = run_script_on_remote_target
|
|
pyb.enter_raw_repl()
|
|
return pyb
|
|
|
|
|
|
def prepare_script_for_target(args, *, script_text=None, force_plain=False):
|
|
if force_plain or (not args.via_mpy and args.emit == "bytecode"):
|
|
# A plain test to run as-is, no processing needed.
|
|
pass
|
|
elif args.via_mpy:
|
|
tempname = tempfile.mktemp(dir="")
|
|
mpy_filename = tempname + ".mpy"
|
|
|
|
script_filename = tempname + ".py"
|
|
with open(script_filename, "wb") as f:
|
|
f.write(script_text)
|
|
|
|
try:
|
|
subprocess.check_output(
|
|
[MPYCROSS]
|
|
+ args.mpy_cross_flags.split()
|
|
+ ["-o", mpy_filename, "-X", "emit=" + args.emit, script_filename],
|
|
stderr=subprocess.STDOUT,
|
|
)
|
|
except subprocess.CalledProcessError as er:
|
|
return True, b"mpy-cross crash\n" + er.output
|
|
|
|
with open(mpy_filename, "rb") as f:
|
|
script_text = b"__buf=" + bytes(repr(f.read()), "ascii") + b"\n"
|
|
|
|
rm_f(mpy_filename)
|
|
rm_f(script_filename)
|
|
|
|
script_text += bytes(_injected_import_hook_code, "ascii")
|
|
else:
|
|
print("error: using emit={} must go via .mpy".format(args.emit))
|
|
sys.exit(1)
|
|
|
|
return False, script_text
|
|
|
|
|
|
def run_script_on_remote_target(pyb, args, test_file, is_special, requires_target_wiring):
|
|
with open(test_file, "rb") as f:
|
|
script = f.read()
|
|
|
|
# If the test is not a special test, prepend it with a print to indicate that it started.
|
|
# If the print does not execute this means that the test did not even start, eg it was
|
|
# too large for the target.
|
|
prepend_start_test = not is_special
|
|
if prepend_start_test:
|
|
if script.startswith(b"#"):
|
|
script = b"print('START TEST')" + script
|
|
else:
|
|
script = b"print('START TEST')\n" + script
|
|
|
|
had_crash, script = prepare_script_for_target(args, script_text=script, force_plain=is_special)
|
|
|
|
if had_crash:
|
|
return True, script
|
|
|
|
try:
|
|
had_crash = False
|
|
pyb.enter_raw_repl()
|
|
if requires_target_wiring and pyb.target_wiring_script:
|
|
pyb.exec_(
|
|
"import sys;sys.modules['target_wiring']=__build_class__(lambda:exec("
|
|
+ repr(pyb.target_wiring_script)
|
|
+ "),'target_wiring')"
|
|
)
|
|
output_mupy = pyb.exec_(script, timeout=TEST_TIMEOUT)
|
|
except pyboard.PyboardError as e:
|
|
had_crash = True
|
|
if not is_special and e.args[0] == "exception":
|
|
if prepend_start_test and e.args[1] == b"" and b"MemoryError" in e.args[2]:
|
|
output_mupy = b"SKIP-TOO-LARGE\n"
|
|
else:
|
|
output_mupy = e.args[1] + e.args[2] + b"CRASH"
|
|
else:
|
|
output_mupy = bytes(e.args[0], "ascii") + b"\nCRASH"
|
|
|
|
if prepend_start_test:
|
|
if output_mupy.startswith(b"START TEST\r\n"):
|
|
output_mupy = output_mupy.removeprefix(b"START TEST\r\n")
|
|
else:
|
|
had_crash = True
|
|
|
|
return had_crash, output_mupy
|
|
|
|
|
|
# Print a summary of the results and save them to a JSON file.
|
|
# Returns True if everything succeeded, False otherwise.
|
|
def create_test_report(args, test_results, testcase_count=None):
|
|
passed_tests = list(r for r in test_results if r[1] == "pass")
|
|
skipped_tests = list(r for r in test_results if r[1] == "skip" and r[2] != "too large")
|
|
skipped_tests_too_large = list(
|
|
r for r in test_results if r[1] == "skip" and r[2] == "too large"
|
|
)
|
|
failed_tests = list(r for r in test_results if r[1] == "fail")
|
|
|
|
num_tests_performed = len(passed_tests) + len(failed_tests)
|
|
|
|
testcase_count_info = ""
|
|
if testcase_count is not None:
|
|
testcase_count_info = " ({} individual testcases)".format(testcase_count)
|
|
print("{} tests performed{}".format(num_tests_performed, testcase_count_info))
|
|
|
|
print("{} tests passed".format(len(passed_tests)))
|
|
|
|
if len(skipped_tests) > 0:
|
|
print(
|
|
"{} tests skipped: {}".format(
|
|
len(skipped_tests), " ".join(test[0] for test in skipped_tests)
|
|
)
|
|
)
|
|
|
|
if len(skipped_tests_too_large) > 0:
|
|
print(
|
|
"{} tests skipped because they are too large: {}".format(
|
|
len(skipped_tests_too_large), " ".join(test[0] for test in skipped_tests_too_large)
|
|
)
|
|
)
|
|
|
|
if len(failed_tests) > 0:
|
|
print(
|
|
"{} tests failed: {}".format(
|
|
len(failed_tests), " ".join(test[0] for test in failed_tests)
|
|
)
|
|
)
|
|
|
|
# Serialize regex added by append_filter.
|
|
def to_json(obj):
|
|
if isinstance(obj, re.Pattern):
|
|
return obj.pattern
|
|
return obj
|
|
|
|
with open(get_results_filename(args), "w") as f:
|
|
json.dump(
|
|
{
|
|
# The arguments passed on the command-line.
|
|
"args": vars(args),
|
|
# A list of all results of the form [(test, result, reason), ...].
|
|
"results": list(test for test in test_results),
|
|
# A list of failed tests. This is deprecated, use the "results" above instead.
|
|
"failed_tests": [test[0] for test in failed_tests],
|
|
},
|
|
f,
|
|
default=to_json,
|
|
)
|
|
|
|
# Return True only if all tests succeeded.
|
|
return len(failed_tests) == 0
|