From 6dda802b0ed2ef9813027732b43aa342c8fd0d9b Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Sun, 15 Mar 2026 19:30:19 -0600 Subject: [PATCH 01/12] uman: Split run_tests() into smaller functions The run_tests() function handles command execution, progress tracking, result parsing, and output display all in one place. Extract run_ut() for running the command with progress tracking, and show_test_output() for parsing and displaying results. This makes run_tests() a short orchestrator and allows the pieces to be reused. Co-developed-by: Claude Opus 4.6 --- uman_pkg/cmdtest.py | 92 ++++++++++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 30 deletions(-) diff --git a/uman_pkg/cmdtest.py b/uman_pkg/cmdtest.py index dc3e838..a3d4c74 100644 --- a/uman_pkg/cmdtest.py +++ b/uman_pkg/cmdtest.py @@ -710,40 +710,25 @@ def finish(self): sys.stderr.flush() -def run_tests(sandbox, specs, args, col): # pylint: disable=R0914 - """Run sandbox tests +def run_ut(cmd, sandbox, specs): + """Run a ut command and capture the output + + Sets up the persistent-data directory and shows live progress on + stderr when available. Args: + cmd (list): Command and arguments to run sandbox (str): Path to sandbox executable - specs (list): List of (suite, pattern) tuples from parse_test_specs - args (argparse.Namespace): Arguments from cmdline - col (terminal.Color): Color object for output + specs (list): List of (suite, pattern) tuples Returns: - int: Exit code from tests + tuple: (result, elapsed) or (None, 0) on failure """ - # Ensure dm init data files exist if needed - if needs_dm_init(specs) and not ensure_dm_init_files(): - return 1 - - cmd = build_ut_cmd(sandbox, specs, full=args.flattree_too, - verbose=args.test_verbose, legacy=args.legacy, - manual=args.manual, - malloc_dump=args.malloc_dump) - - if args.dry_run: - tout.notice(shlex.join(cmd)) - return 0 - - tout.info(f"Running: {shlex.join(cmd)}") - - # Set up environment with persistent data directory build_dir = settings.get('build_dir', '/tmp/b') persist_dir = os.path.join(build_dir, 'sandbox', 'persistent-data') env = os.environ.copy() env['U_BOOT_PERSISTENT_DATA_DIR'] = persist_dir - # Show live progress if stderr is a terminal emit = has_emit_result() if sys.stderr.isatty(): total = count_tests(sandbox, specs) @@ -757,27 +742,38 @@ def run_tests(sandbox, specs, args, col): # pylint: disable=R0914 result = command.run_one(*cmd, capture=True, env=env, output_func=output_func) except command.CommandExc as exc: - # Tests may fail but still produce parseable output result = exc.result if result and isinstance(result.stdout, (bytes, bytearray)): result.to_output(False) if not result: tout.error(f'Command failed: {exc}') - return 1 + return None, 0 finally: if progress: progress.finish() - elapsed = time.time() - start_time + return result, time.time() - start_time + + +def show_test_output(result, args, col): + """Parse and display test results + + Args: + result (CommandResult): Output from running tests + args (argparse.Namespace): Arguments from cmdline + col (terminal.Color): Color object for output + Returns: + TestCounts, False, or None: Parsed result counts, False if no + results were found, None on error + """ # Detect old U-Boot that doesn't understand -E or -F flags if 'failed while parsing option: -E' in result.stdout: tout.error('U-Boot does not support -E flag; use -L for legacy mode') - return 1 + return None if 'failed while parsing option: -F' in result.stdout: tout.error('U-Boot does not support -F flag; use -f to run all tests') - return 1 + return None - # Parse results first to check for failures legacy = args.legacy or not has_emit_result() res = parse_results(result.stdout, show_results=args.results, col=col) if not res and legacy: @@ -789,7 +785,6 @@ def run_tests(sandbox, specs, args, col): # pylint: disable=R0914 # Print output in verbose mode, if there are failures, or no results if result.stdout and not args.results: if args.test_verbose or (res and res.failed) or not res: - # Skip U-Boot banner, show only test output in_tests = False for line in result.stdout.splitlines(): if not in_tests: @@ -797,6 +792,43 @@ def run_tests(sandbox, specs, args, col): # pylint: disable=R0914 in_tests = True if in_tests: print(line) + return res or False + + +def run_tests(sandbox, specs, args, col): + """Run sandbox tests + + Args: + sandbox (str): Path to sandbox executable + specs (list): List of (suite, pattern) tuples from parse_test_specs + args (argparse.Namespace): Arguments from cmdline + col (terminal.Color): Color object for output + + Returns: + int: Exit code from tests + """ + if needs_dm_init(specs) and not ensure_dm_init_files(): + return 1 + + cmd = build_ut_cmd(sandbox, specs, full=args.flattree_too, + verbose=args.test_verbose, legacy=args.legacy, + manual=args.manual, + malloc_dump=args.malloc_dump) + + if args.dry_run: + tout.notice(shlex.join(cmd)) + return 0 + + tout.info(f"Running: {shlex.join(cmd)}") + + result, elapsed = run_ut(cmd, sandbox, specs) + if not result: + return 1 + + res = show_test_output(result, args, col) + if res is None: + return 1 + if res: show_summary(res.passed, res.failed, res.skipped, elapsed) return result.return_code From ef3dbfc6d142d32001c6efea33b207b9c2a10717 Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Sun, 15 Mar 2026 19:44:09 -0600 Subject: [PATCH 02/12] uman: Extract helpers from do_test() to reduce complexity The do_test() function has many early returns and inline blocks for listing suites, listing tests, and reporting unmatched specs. Extract list_suites(), list_tests(), and report_unmatched() as separate functions, and consolidate the two validation checks into one error path. Co-developed-by: Claude Opus 4.6 --- uman_pkg/cmdtest.py | 112 +++++++++++++++++++++++++------------------- 1 file changed, 64 insertions(+), 48 deletions(-) diff --git a/uman_pkg/cmdtest.py b/uman_pkg/cmdtest.py index a3d4c74..574ea9f 100644 --- a/uman_pkg/cmdtest.py +++ b/uman_pkg/cmdtest.py @@ -851,7 +851,50 @@ def run_tests(sandbox, specs, args, col): return 1 -def do_test(args): # pylint: disable=R0912 +def report_unmatched(unmatched): + """Report unmatched test specs to stderr + + Args: + unmatched (list): List of (suite, pattern) tuples that did not match + """ + for suite, pattern in unmatched: + if suite and pattern: + tout.error(f'No tests found matching: {suite}.{pattern}') + elif suite: + tout.error(f'No tests found in suite: {suite}') + else: + tout.error(f'No tests found matching: {pattern}') + + +def list_suites(sandbox): + """List available test suites + + Args: + sandbox (str): Path to sandbox executable + """ + suites = get_suites_from_nm(sandbox) + tout.notice('Available test suites:') + for suite in suites: + print(f' {suite}') + + +def list_tests(sandbox, suite): + """List available tests, optionally filtered by suite + + Args: + sandbox (str): Path to sandbox executable + suite (str or None): Suite name to filter by, or None for all + """ + tests = get_tests_from_nm(sandbox, suite) + if suite: + tout.notice(f'Tests in suite "{suite}":') + else: + tout.notice('Available tests:') + for suite_name, test_name in tests: + print(f' {suite_name}.{test_name}') + + +def do_test(args): """Handle test command - run U-Boot sandbox tests Args: @@ -861,8 +904,8 @@ def do_test(args): # pylint: disable=R0912 int: Exit code """ board = args.board or 'sandbox' + ret = 0 - # Build if requested if args.build: if not build.build_board( board, args.dry_run, lto=args.lto, @@ -871,52 +914,25 @@ def do_test(args): # pylint: disable=R0912 jobs=args.jobs, trace=args.trace, trace_early=not args.no_trace_early, output_dir=args.output_dir): - return 1 + ret = 1 - sandbox = get_sandbox_path() - if not sandbox: + sandbox = None if ret else get_sandbox_path() + if not ret and not sandbox: tout.error(f'Sandbox not found. Build first with: uman build {board}') - return 1 - - # Handle list suites - if args.list_suites: - suites = get_suites_from_nm(sandbox) - tout.notice('Available test suites:') - for suite in suites: - print(f' {suite}') - return 0 - - # Handle list tests - if args.list_tests: - suite = args.tests[0] if args.tests else None - tests = get_tests_from_nm(sandbox, suite) - if suite: - tout.notice(f'Tests in suite "{suite}":') + ret = 1 + + if not ret and args.list_suites: + list_suites(sandbox) + elif not ret and args.list_tests: + list_tests(sandbox, args.tests[0] if args.tests else None) + elif not ret: + specs = parse_test_specs(args.tests) + specs, unmatched = resolve_specs(sandbox, specs) + if not unmatched: + unmatched = validate_specs(sandbox, specs) + if unmatched: + report_unmatched(unmatched) + ret = 1 else: - tout.notice('Available tests:') - for suite_name, test_name in tests: - print(f' {suite_name}.{test_name}') - return 0 - - # Parse test specs - specs = parse_test_specs(args.tests) - - # Resolve any specs that need suite lookup - specs, unmatched = resolve_specs(sandbox, specs) - if unmatched: - for suite, pattern in unmatched: - tout.error(f'No tests found matching: {pattern}') - return 1 - - # Validate that specs match actual tests - unmatched = validate_specs(sandbox, specs) - if unmatched: - for suite, pattern in unmatched: - if pattern: - tout.error(f'No tests found matching: {suite}.{pattern}') - else: - tout.error(f'No tests found in suite: {suite}') - return 1 - - # Run tests - return run_tests(sandbox, specs, args, args.col) + ret = run_tests(sandbox, specs, args, args.col) + return ret From e3cfc58d5e61f7bc98b17bf28844616dce48f617 Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Sun, 15 Mar 2026 19:54:17 -0600 Subject: [PATCH 03/12] uman: Reduce return statements in run_tests() The run_tests() function has many early returns for different error paths, making the flow hard to follow. Extract check_signal() for signal detection and restructure the result handling to use a single return at the end. Co-developed-by: Claude Opus 4.6 --- uman_pkg/cmdtest.py | 60 ++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/uman_pkg/cmdtest.py b/uman_pkg/cmdtest.py index 574ea9f..f7ff78a 100644 --- a/uman_pkg/cmdtest.py +++ b/uman_pkg/cmdtest.py @@ -795,6 +795,22 @@ def show_test_output(result, args, col): return res or False +def check_signal(return_code): + """Check if a return code indicates signal termination + + Args: + return_code (int): Process return code + + Returns: + int or None: Signal number, or None if not a signal + """ + if return_code < 0: + return -return_code + if return_code > 128: + return return_code - 128 + return None + + def run_tests(sandbox, specs, args, col): """Run sandbox tests @@ -820,35 +836,29 @@ def run_tests(sandbox, specs, args, col): return 0 tout.info(f"Running: {shlex.join(cmd)}") + ret = 1 result, elapsed = run_ut(cmd, sandbox, specs) - if not result: - return 1 - - res = show_test_output(result, args, col) - if res is None: - return 1 + if result: + res = show_test_output(result, args, col) + else: + res = None - if res: + if res is not None and res: show_summary(res.passed, res.failed, res.skipped, elapsed) - return result.return_code - - # Check for crash (signal termination) - ret = result.return_code - sig = None - if ret < 0: - sig = -ret - elif ret > 128: - sig = ret - 128 - if sig: - sig_names = {6: 'SIGABRT', 11: 'SIGSEGV', 15: 'SIGTERM'} - sig_name = sig_names.get(sig, f'signal {sig}') - os.system('tset') - tout.error(f'Test crashed ({sig_name})') - return ret - - tout.warning('No results detected (use -L for older U-Boot)') - return 1 + ret = result.return_code + elif res is not None: + sig = check_signal(result.return_code) + if sig: + sig_names = {6: 'SIGABRT', 11: 'SIGSEGV', 15: 'SIGTERM'} + os.system('tset') + tout.error(f'Test crashed ' + f'({sig_names.get(sig, f"signal {sig}")})') + ret = result.return_code + else: + tout.warning('No results detected (use -L for older U-Boot)') + + return ret def report_unmatched(unmatched): From 2348b052a2e8dd50dc01116aa89e3c5ab59ed75c Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Sun, 15 Mar 2026 05:28:44 -0600 Subject: [PATCH 04/12] uman: Install gdb-multiarch in containers Add gdb-multiarch to the default packages so that debugging with -G works inside containers without manual installation. Co-developed-by: Claude Opus 4.6 --- uman_pkg/cc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uman_pkg/cc.py b/uman_pkg/cc.py index 47a12b7..d6f414f 100644 --- a/uman_pkg/cc.py +++ b/uman_pkg/cc.py @@ -40,8 +40,8 @@ ALSA_PULSE_CONF = '/etc/alsa-pulse.conf' # Default packages to install in containers -DEFAULT_PACKAGES = ('build-essential gh glab libasound2-plugins' - ' libsox-fmt-pulse pylint sox xclip') +DEFAULT_PACKAGES = ('build-essential gdb-multiarch gh glab' + ' libasound2-plugins libsox-fmt-pulse pylint sox xclip') def get_log_path(name): From 563d6974312f265a7eabe679f1960d23b1d72644 Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Sun, 15 Mar 2026 07:15:57 -0600 Subject: [PATCH 05/12] uman: Ignore compiler warnings in buildman builds Buildman treats warnings as build failures by default, which causes builds to fail on harmless warnings in U-Boot source files. Add the -W flag to all buildman invocations so that warnings are still shown but do not cause a non-zero exit code. Co-developed-by: Claude Opus 4.6 --- uman_pkg/build.py | 25 +++++++++++++++++++------ uman_pkg/cmdpy.py | 5 ++--- uman_pkg/ftest.py | 4 ++-- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/uman_pkg/build.py b/uman_pkg/build.py index 7f87264..26e221f 100644 --- a/uman_pkg/build.py +++ b/uman_pkg/build.py @@ -174,6 +174,23 @@ def get_cmd(args, board, build_dir): return ['buildman'] + get_buildman_args(args, board, build_dir) +def base_bm_args(board, build_dir, lto=True): + """Build the common buildman arguments + + Args: + board (str): Board name to build + build_dir (str): Path to build directory + lto (bool): Enable LTO (default True) + + Returns: + list: Base arguments for buildman + """ + bm_args = ['-I', '-w', '-W', '--boards', board, '-o', build_dir] + if not lto: + bm_args.insert(0, '-L') + return bm_args + + def get_buildman_args(args, board, build_dir): """Build the buildman arguments @@ -188,9 +205,7 @@ def get_buildman_args(args, board, build_dir): if args.in_tree: bm_args = ['-i', '--boards', board] else: - bm_args = ['-I', '-w', '--boards', board, '-o', build_dir] - if not args.lto: - bm_args.insert(0, '-L') + bm_args = base_bm_args(board, build_dir, args.lto) if args.target: bm_args.extend(['--target', args.target]) if args.jobs: @@ -234,9 +249,7 @@ def build_board(board, dry_run=False, lto=False, adjust_cfg=None, tout.progress(f'Building {board}') - bm_args = ['-I', '-w', '--boards', board, '-o', build_dir] - if not lto: - bm_args.insert(0, '-L') + bm_args = base_bm_args(board, build_dir, lto) if force_reconfig: bm_args.append('-C') if jobs: diff --git a/uman_pkg/cmdpy.py b/uman_pkg/cmdpy.py index e09673c..feccc97 100644 --- a/uman_pkg/cmdpy.py +++ b/uman_pkg/cmdpy.py @@ -1154,9 +1154,8 @@ def do_pollute(args): base_dir = settings.get('build_dir', '/tmp/b') build_dir = f'{base_dir}/{args.board}-pollute' tout.notice(f'Building to {build_dir}...') - cmd = ['buildman', '-I', '-w', '--boards', args.board, '-o', build_dir] - if not args.lto: - cmd.insert(1, '-L') + cmd = ['buildman'] + build_mod.base_bm_args(args.board, build_dir, + args.lto) result = exec_cmd(cmd, args.dry_run, capture=False) if result and result.return_code != 0: tout.error('Build failed') diff --git a/uman_pkg/ftest.py b/uman_pkg/ftest.py index 9935044..a3f91fb 100644 --- a/uman_pkg/ftest.py +++ b/uman_pkg/ftest.py @@ -315,8 +315,8 @@ def test_get_dir_default(self): def test_get_cmd_basic(self): """Test basic build command generation (LTO disabled by default)""" args = cmdline.parse_args(['build', 'sandbox']) - self.assertEqual(['buildman', '-L', '-I', '-w', '--boards', 'sandbox', - '-o', '/tmp/b/sandbox'], + self.assertEqual(['buildman', '-L', '-I', '-w', '-W', '--boards', + 'sandbox', '-o', '/tmp/b/sandbox'], build.get_cmd(args, 'sandbox', '/tmp/b/sandbox')) def test_run_no_board(self): From 32e2f75f417e657fb78be210236dc4cb8c45a103 Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Sun, 15 Mar 2026 09:52:56 -0600 Subject: [PATCH 06/12] uman: Add --leak-check option to test command Pass -L to the ut command so that each test checks for memory leaks using mallinfo() before and after the test. Co-developed-by: Claude Opus 4.6 (1M context) --- README.rst | 1 + uman_pkg/cmdline.py | 22 ++++++++++++++++------ uman_pkg/cmdtest.py | 8 ++++++-- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 2567499..629281b 100644 --- a/README.rst +++ b/README.rst @@ -866,6 +866,7 @@ without going through pytest. This is faster for quick iteration on C code. - ``-j, --jobs JOBS``: Number of parallel jobs (use with -b) - ``-l, --list``: List available tests - ``-L, --lto``: Enable LTO when building (use with -b) +- ``--leak-check``: Check for memory leaks around each test using mallinfo() - ``--legacy``: Use legacy result parsing (for old U-Boot) - ``--malloc-dump FILE``: Write malloc heap dump on exit; ``%d`` in the filename is expanded to a sequence number diff --git a/uman_pkg/cmdline.py b/uman_pkg/cmdline.py index 19511a8..db43a12 100644 --- a/uman_pkg/cmdline.py +++ b/uman_pkg/cmdline.py @@ -177,6 +177,20 @@ def add_selftest_subparser(subparsers): return stest +def add_leak_opts(parser): + """Add leak-check and malloc-dump options to a parser + + Args: + parser: Argument parser to add options to + """ + parser.add_argument( + '-M', '--leak-check', action='store_true', dest='leak_check', + help='Check for memory leaks around each test') + parser.add_argument( + '--malloc-dump', metavar='FILE', dest='malloc_dump', + help='Write malloc dump to FILE on exit (use %%d for sequence number)') + + def add_test_opts(parser, board_help=None, board_default=None): """Add common test options to a parser @@ -204,9 +218,7 @@ def add_test_opts(parser, board_help=None, board_default=None): parser.add_argument( '-x', '--exitfirst', action='store_true', help='Stop on first test failure') - parser.add_argument( - '--malloc-dump', metavar='FILE', dest='malloc_dump', - help='Write malloc dump to FILE on exit (use %%d for sequence number)') + add_leak_opts(parser) def add_build_opts(parser): @@ -400,12 +412,10 @@ def add_test_subparser(subparsers): test.add_argument( '-s', '--suites', action='store_true', dest='list_suites', help='List available test suites') - test.add_argument( - '--malloc-dump', metavar='FILE', dest='malloc_dump', - help='Write malloc dump to FILE on exit (use %%d for sequence number)') test.add_argument( '-V', '--test-verbose', action='store_true', dest='test_verbose', help='Enable verbose test output') + add_leak_opts(test) add_build_opts(test) return test diff --git a/uman_pkg/cmdtest.py b/uman_pkg/cmdtest.py index f7ff78a..046603b 100644 --- a/uman_pkg/cmdtest.py +++ b/uman_pkg/cmdtest.py @@ -446,7 +446,7 @@ def has_emit_result(): def build_ut_cmd(sandbox, specs, full=False, verbose=False, legacy=False, - manual=False, malloc_dump=None): + manual=False, malloc_dump=None, leak_check=False): """Build the sandbox command line for running tests Args: @@ -457,6 +457,7 @@ def build_ut_cmd(sandbox, specs, full=False, verbose=False, legacy=False, legacy (bool): Legacy mode (don't use -E flag for older U-Boot) manual (bool): Force manual tests to run malloc_dump (str or None): File to write malloc dump to on exit + leak_check (bool): Check for memory leaks around each test Returns: list: Command and arguments @@ -481,6 +482,8 @@ def build_ut_cmd(sandbox, specs, full=False, verbose=False, legacy=False, flags += '-E ' if manual: flags += '-m ' + if leak_check: + flags += '-L ' cmds = [] for suite, pattern in specs: if pattern: @@ -829,7 +832,8 @@ def run_tests(sandbox, specs, args, col): cmd = build_ut_cmd(sandbox, specs, full=args.flattree_too, verbose=args.test_verbose, legacy=args.legacy, manual=args.manual, - malloc_dump=args.malloc_dump) + malloc_dump=args.malloc_dump, + leak_check=args.leak_check) if args.dry_run: tout.notice(shlex.join(cmd)) From b924c45a4481c0e2c7f2f0662ef94ab5d492d6ad Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Sun, 15 Mar 2026 10:18:08 -0600 Subject: [PATCH 07/12] uman: Search all suites when test spec is not a known suite When 'um t acpi' is used and 'acpi' is not a recognised suite name, there is no fallback to search for tests matching that name across all suites. Try the spec as a pattern across all suites, so that 'um t acpi' finds and runs all tests with 'acpi' in their name regardless of which suite they belong to. The pattern uses the full U-Boot test function name format ({suite}_test_{name}*) since the ut command does not support leading wildcards. Co-developed-by: Claude Opus 4.6 --- uman_pkg/cmdtest.py | 95 +++++++++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/uman_pkg/cmdtest.py b/uman_pkg/cmdtest.py index 046603b..dc93dbb 100644 --- a/uman_pkg/cmdtest.py +++ b/uman_pkg/cmdtest.py @@ -322,6 +322,50 @@ def parse_test_specs(tests): return [parse_one_test(t) for t in tests] +def resolve_one(suite, pattern, all_tests, known_suites): + """Resolve a single (suite, pattern) spec against known tests + + Args: + suite (str or None): Suite name, 'all', or None to search + pattern (str or None): Test pattern or None for whole suite + all_tests (list): List of (suite, test_name) from nm + known_suites (set): Set of known suite names + + Returns: + tuple: (resolved_list, matched) where resolved_list is a list of + (suite, pattern) tuples and matched is True if something matched + """ + if suite == 'all' or suite in known_suites: + return [(suite, pattern)], True + + if suite is not None: + # Suite doesn't exist - try to find full test name + if pattern: + full_name = f'{suite}_test_{pattern}' + else: + full_name = suite + for test_suite, test_name in all_tests: + if test_name == full_name: + return [(test_suite, full_name)], True + + # Try as a pattern across all suites + if pattern is None: + matches = set() + for test_suite, test_name in all_tests: + if fnmatch.fnmatch(test_name, f'*{suite}*'): + matches.add(test_suite) + if matches: + return [(s, f'{s}_test_{suite}*') + for s in sorted(matches)], True + return [], False + + # suite is None - search all suites for this pattern + for test_suite, test_name in all_tests: + if fnmatch.fnmatch(test_name, f'*{pattern}'): + return [(test_suite, pattern)], True + return [], False + + def resolve_specs(sandbox, specs): """Resolve specs with suite=None or invalid suite by looking up from nm @@ -334,48 +378,23 @@ def resolve_specs(sandbox, specs): """ resolved = [] unmatched = [] - all_tests = None # Lazy load - known_suites = None # Lazy load + all_tests = None + known_suites = None for suite, pattern in specs: - if suite is not None and suite != 'all': - # Check if suite exists + if suite not in (None, 'all'): if known_suites is None: - if all_tests is None: - all_tests = get_tests_from_nm(sandbox) - known_suites = {s for s, _ in all_tests} - - if suite in known_suites: - resolved.append((suite, pattern)) - else: - # Suite doesn't exist - try to find full test name - # Reconstruct the original test name - if pattern: - full_name = f'{suite}_test_{pattern}' - else: - full_name = suite - found = False - for test_suite, test_name in all_tests: - if test_name == full_name: - resolved.append((test_suite, full_name)) - found = True - break - if not found: - unmatched.append((suite, pattern)) - elif suite == 'all': - resolved.append((suite, pattern)) - else: - # Need to find suite(s) for this pattern - if all_tests is None: all_tests = get_tests_from_nm(sandbox) - found = False - for test_suite, test_name in all_tests: - if fnmatch.fnmatch(test_name, f'*{pattern}'): - resolved.append((test_suite, pattern)) - found = True - break # Only add first match - if not found: - unmatched.append((None, pattern)) + known_suites = {s for s, _ in all_tests} + elif suite is None and all_tests is None: + all_tests = get_tests_from_nm(sandbox) + known_suites = {s for s, _ in all_tests} + + found, matched = resolve_one(suite, pattern, + all_tests or [], known_suites or set()) + resolved.extend(found) + if not matched: + unmatched.append((suite, pattern)) return resolved, unmatched From 465468d7cd0dcaffd8683faa11b6f7036c13791f Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Sun, 15 Mar 2026 10:24:54 -0600 Subject: [PATCH 08/12] uman: Show leak count in test progress and summary When --leak-check is enabled, U-Boot outputs 'Leak: N allocs' lines for tests that leak memory. These are not currently tracked. Parse leak lines in both the live progress display and the result summary, showing the count alongside passed/failed/skipped when any leaks are detected. Co-developed-by: Claude Opus 4.6 --- uman_pkg/cmdtest.py | 30 +++++++++++++++++++++++------- uman_pkg/util.py | 14 +++++++++----- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/uman_pkg/cmdtest.py b/uman_pkg/cmdtest.py index dc93dbb..5dd3503 100644 --- a/uman_pkg/cmdtest.py +++ b/uman_pkg/cmdtest.py @@ -26,7 +26,9 @@ from uman_pkg.util import run_pytest, show_summary # Named tuple for test result counts -TestCounts = namedtuple('TestCounts', ['passed', 'failed', 'skipped']) +TestCounts = namedtuple('TestCounts', ['passed', 'failed', 'skipped', + 'leaked'], + defaults=[0]) # Patterns for parsing linker-list symbols from nm output # Format: _u_boot_list_2_ut__2_ @@ -42,6 +44,7 @@ RE_RESULT = re.compile(r'Result:\s*(PASS|FAIL|SKIP):?\s+(\S+)') RE_SUMMARY = re.compile(r'Tests run:\s*(\d+),.*failures:\s*(\d+)') RE_TEST_FAILED = re.compile(r"Test '.+' failed \d+ times") +RE_LEAK = re.compile(r'Leak:\s+(\d+)\s+alloc') # Unit test flags from include/test/test.h UTF_FLAT_TREE = 0x08 @@ -609,8 +612,12 @@ def parse_results(output, show_results=False, col=None): passed = 0 failed = 0 skipped = 0 + leaked = 0 for line in output.splitlines(): + if RE_LEAK.match(line): + leaked += 1 + continue result_match = RE_RESULT.match(line) if result_match: status, name = result_match.groups() @@ -625,7 +632,7 @@ def parse_results(output, show_results=False, col=None): if not passed and not failed and not skipped: return None - return TestCounts(passed, failed, skipped) + return TestCounts(passed, failed, skipped, leaked) def count_tests(sandbox, specs): @@ -667,6 +674,7 @@ def __init__(self, emit_result, total=0): self.passed = 0 self.failed = 0 self.skipped = 0 + self.leaked = 0 self.buf = '' self.pending = False # A Test: line seen, not yet resolved @@ -682,14 +690,21 @@ def _show(self): hdr = f'{done}/{self.total}:' else: hdr = f'{done}:' - sys.stderr.write( - f'\r {hdr} {grn}{self.passed} passed{rst}, ' - f'{red}{self.failed} failed{rst}, ' - f'{yel}{self.skipped} skipped{rst}') + mag = col.start(terminal.Color.MAGENTA) + parts = [f'{grn}{self.passed} passed{rst}', + f'{red}{self.failed} failed{rst}', + f'{yel}{self.skipped} skipped{rst}'] + if self.leaked: + parts.append(f'{mag}{self.leaked} leaked{rst}') + sys.stderr.write(f'\r {hdr} {", ".join(parts)}') sys.stderr.flush() def _process_line(self, line): """Process one complete line of output""" + if RE_LEAK.match(line): + self.leaked += 1 + self._show() + return if self.emit: match = RE_RESULT.match(line) if match: @@ -868,7 +883,8 @@ def run_tests(sandbox, specs, args, col): res = None if res is not None and res: - show_summary(res.passed, res.failed, res.skipped, elapsed) + show_summary(res.passed, res.failed, res.skipped, elapsed, + res.leaked) ret = result.return_code elif res is not None: sig = check_signal(result.return_code) diff --git a/uman_pkg/util.py b/uman_pkg/util.py index 6179b6e..8e15f54 100644 --- a/uman_pkg/util.py +++ b/uman_pkg/util.py @@ -220,7 +220,7 @@ def format_duration(seconds): return f'{minutes}m {secs:.1f}s' -def show_summary(passed, failed, skipped, elapsed): +def show_summary(passed, failed, skipped, elapsed, leaked=0): """Show a test results summary Args: @@ -228,13 +228,17 @@ def show_summary(passed, failed, skipped, elapsed): failed (int): Number of tests failed skipped (int): Number of tests skipped elapsed (float): Time taken in seconds + leaked (int): Number of tests that leaked memory """ col = terminal.Color() green = col.start(terminal.Color.GREEN) red = col.start(terminal.Color.RED) yellow = col.start(terminal.Color.YELLOW) reset = col.stop() - print(f'Results: {green}{passed} passed{reset}, ' - f'{red}{failed} failed{reset}, ' - f'{yellow}{skipped} skipped{reset} in ' - f'{format_duration(elapsed)}') + magenta = col.start(terminal.Color.MAGENTA) + parts = [f'{green}{passed} passed{reset}', + f'{red}{failed} failed{reset}', + f'{yellow}{skipped} skipped{reset}'] + if leaked: + parts.append(f'{magenta}{leaked} leaked{reset}') + print(f'Results: {", ".join(parts)} in {format_duration(elapsed)}') From 3130fe19467b40b69cdb9e1a951ba267a61754dd Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Sun, 15 Mar 2026 10:27:36 -0600 Subject: [PATCH 09/12] uman: Show total leaked bytes in test progress and summary The leak detail lines from U-Boot contain allocation sizes in hex. Parse these to show the total leaked bytes alongside the leak count, e.g. '12 leaked (0x3130 bytes)' in both the live progress display and the final summary. Co-developed-by: Claude Opus 4.6 --- uman_pkg/cmdtest.py | 26 ++++++++++++++++++++------ uman_pkg/util.py | 25 +++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/uman_pkg/cmdtest.py b/uman_pkg/cmdtest.py index 5dd3503..2115807 100644 --- a/uman_pkg/cmdtest.py +++ b/uman_pkg/cmdtest.py @@ -23,12 +23,12 @@ from u_boot_pylib import tout from uman_pkg import build, settings -from uman_pkg.util import run_pytest, show_summary +from uman_pkg.util import format_bytes, run_pytest, show_summary # Named tuple for test result counts TestCounts = namedtuple('TestCounts', ['passed', 'failed', 'skipped', - 'leaked'], - defaults=[0]) + 'leaked', 'leak_bytes'], + defaults=[0, 0]) # Patterns for parsing linker-list symbols from nm output # Format: _u_boot_list_2_ut__2_ @@ -45,6 +45,7 @@ RE_SUMMARY = re.compile(r'Tests run:\s*(\d+),.*failures:\s*(\d+)') RE_TEST_FAILED = re.compile(r"Test '.+' failed \d+ times") RE_LEAK = re.compile(r'Leak:\s+(\d+)\s+alloc') +RE_LEAK_DETAIL = re.compile(r'\s+[0-9a-f]+\s+([0-9a-f]+)\s+') # Unit test flags from include/test/test.h UTF_FLAT_TREE = 0x08 @@ -613,11 +614,16 @@ def parse_results(output, show_results=False, col=None): failed = 0 skipped = 0 leaked = 0 + leak_bytes = 0 for line in output.splitlines(): if RE_LEAK.match(line): leaked += 1 continue + detail = RE_LEAK_DETAIL.match(line) + if detail: + leak_bytes += int(detail.group(1), 16) + continue result_match = RE_RESULT.match(line) if result_match: status, name = result_match.groups() @@ -632,7 +638,7 @@ def parse_results(output, show_results=False, col=None): if not passed and not failed and not skipped: return None - return TestCounts(passed, failed, skipped, leaked) + return TestCounts(passed, failed, skipped, leaked, leak_bytes) def count_tests(sandbox, specs): @@ -675,6 +681,7 @@ def __init__(self, emit_result, total=0): self.failed = 0 self.skipped = 0 self.leaked = 0 + self.leak_bytes = 0 self.buf = '' self.pending = False # A Test: line seen, not yet resolved @@ -695,7 +702,10 @@ def _show(self): f'{red}{self.failed} failed{rst}', f'{yel}{self.skipped} skipped{rst}'] if self.leaked: - parts.append(f'{mag}{self.leaked} leaked{rst}') + leak_str = f'{self.leaked} leaked' + if self.leak_bytes: + leak_str += f' ({format_bytes(self.leak_bytes)})' + parts.append(f'{mag}{leak_str}{rst}') sys.stderr.write(f'\r {hdr} {", ".join(parts)}') sys.stderr.flush() @@ -705,6 +715,10 @@ def _process_line(self, line): self.leaked += 1 self._show() return + detail = RE_LEAK_DETAIL.match(line) + if detail: + self.leak_bytes += int(detail.group(1), 16) + return if self.emit: match = RE_RESULT.match(line) if match: @@ -884,7 +898,7 @@ def run_tests(sandbox, specs, args, col): if res is not None and res: show_summary(res.passed, res.failed, res.skipped, elapsed, - res.leaked) + res.leaked, res.leak_bytes) ret = result.return_code elif res is not None: sig = check_signal(result.return_code) diff --git a/uman_pkg/util.py b/uman_pkg/util.py index 8e15f54..2eca6d7 100644 --- a/uman_pkg/util.py +++ b/uman_pkg/util.py @@ -220,7 +220,24 @@ def format_duration(seconds): return f'{minutes}m {secs:.1f}s' -def show_summary(passed, failed, skipped, elapsed, leaked=0): +def format_bytes(nbytes): + """Format a byte count with K/M suffixes + + Args: + nbytes (int): Number of bytes + + Returns: + str: Formatted string (e.g. '1.2K', '3.5M', '512') + """ + if nbytes >= 1024 * 1024: + return f'{nbytes / 1024 / 1024:.1f}M' + if nbytes >= 1024: + return f'{nbytes / 1024:.1f}K' + return str(nbytes) + + +def show_summary(passed, failed, skipped, elapsed, leaked=0, + leak_bytes=0): """Show a test results summary Args: @@ -229,6 +246,7 @@ def show_summary(passed, failed, skipped, elapsed, leaked=0): skipped (int): Number of tests skipped elapsed (float): Time taken in seconds leaked (int): Number of tests that leaked memory + leak_bytes (int): Total bytes leaked """ col = terminal.Color() green = col.start(terminal.Color.GREEN) @@ -240,5 +258,8 @@ def show_summary(passed, failed, skipped, elapsed, leaked=0): f'{red}{failed} failed{reset}', f'{yellow}{skipped} skipped{reset}'] if leaked: - parts.append(f'{magenta}{leaked} leaked{reset}') + leak_str = f'{leaked} leaked' + if leak_bytes: + leak_str += f' ({format_bytes(leak_bytes)})' + parts.append(f'{magenta}{leak_str}{reset}') print(f'Results: {", ".join(parts)} in {format_duration(elapsed)}') From 9086a1f61eba773bd5ffc8dc0b6307ed39acafd3 Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Sun, 15 Mar 2026 10:37:54 -0600 Subject: [PATCH 10/12] uman: Add --show-leaks flag to show top leaking tests When leak checking is enabled, it is useful to see which tests leak the most memory. Track per-test leak bytes during result parsing and add --show-leaks N to display the top N leaking tests after the summary. Co-developed-by: Claude Opus 4.6 --- README.rst | 2 ++ uman_pkg/cmdline.py | 3 +++ uman_pkg/cmdpy.py | 2 ++ uman_pkg/cmdtest.py | 33 +++++++++++++++++++++++++++------ uman_pkg/util.py | 25 +++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 629281b..94c7399 100644 --- a/README.rst +++ b/README.rst @@ -716,6 +716,7 @@ hooks to PATH. - ``-T, --trace``: Enable function tracing; adds CONFIG_TRACE and CONFIG_TRACE_EARLY (use with -b) - ``--no-trace-early``: Disable TRACE_EARLY when using -T (use with -b) +- ``--leak-check``: Check for memory leaks around each test using mallinfo() - ``--malloc-dump FILE``: Write malloc heap dump on exit; ``%d`` in the filename is expanded to a sequence number - ``--no-timeout``: Disable test timeout @@ -867,6 +868,7 @@ without going through pytest. This is faster for quick iteration on C code. - ``-l, --list``: List available tests - ``-L, --lto``: Enable LTO when building (use with -b) - ``--leak-check``: Check for memory leaks around each test using mallinfo() +- ``--show-leaks N``: Show top N leaks by bytes (default: 10, 0 to disable) - ``--legacy``: Use legacy result parsing (for old U-Boot) - ``--malloc-dump FILE``: Write malloc heap dump on exit; ``%d`` in the filename is expanded to a sequence number diff --git a/uman_pkg/cmdline.py b/uman_pkg/cmdline.py index db43a12..01440f9 100644 --- a/uman_pkg/cmdline.py +++ b/uman_pkg/cmdline.py @@ -186,6 +186,9 @@ def add_leak_opts(parser): parser.add_argument( '-M', '--leak-check', action='store_true', dest='leak_check', help='Check for memory leaks around each test') + parser.add_argument( + '--show-leaks', type=int, default=10, metavar='N', dest='show_leaks', + help='Show top N leaks by bytes (default: 10, 0 to disable)') parser.add_argument( '--malloc-dump', metavar='FILE', dest='malloc_dump', help='Write malloc dump to FILE on exit (use %%d for sequence number)') diff --git a/uman_pkg/cmdpy.py b/uman_pkg/cmdpy.py index feccc97..65f4758 100644 --- a/uman_pkg/cmdpy.py +++ b/uman_pkg/cmdpy.py @@ -471,6 +471,8 @@ def build_pytest_cmd(args): cmd.append('--no-full') if args.malloc_dump: cmd.extend(['--malloc-dump', args.malloc_dump]) + if args.leak_check: + cmd.append('--leak-check') # Add extra pytest arguments (after --) if args.extra_args: diff --git a/uman_pkg/cmdtest.py b/uman_pkg/cmdtest.py index 2115807..bac2c02 100644 --- a/uman_pkg/cmdtest.py +++ b/uman_pkg/cmdtest.py @@ -23,12 +23,12 @@ from u_boot_pylib import tout from uman_pkg import build, settings -from uman_pkg.util import format_bytes, run_pytest, show_summary +from uman_pkg.util import format_bytes, run_pytest, show_leak_top, show_summary # Named tuple for test result counts TestCounts = namedtuple('TestCounts', ['passed', 'failed', 'skipped', - 'leaked', 'leak_bytes'], - defaults=[0, 0]) + 'leaked', 'leak_bytes', 'leak_top'], + defaults=[0, 0, None]) # Patterns for parsing linker-list symbols from nm output # Format: _u_boot_list_2_ut__2_ @@ -45,7 +45,7 @@ RE_SUMMARY = re.compile(r'Tests run:\s*(\d+),.*failures:\s*(\d+)') RE_TEST_FAILED = re.compile(r"Test '.+' failed \d+ times") RE_LEAK = re.compile(r'Leak:\s+(\d+)\s+alloc') -RE_LEAK_DETAIL = re.compile(r'\s+[0-9a-f]+\s+([0-9a-f]+)\s+') +RE_LEAK_DETAIL = re.compile(r'\s+[0-9a-f]+\s+([0-9a-f]+)\s+(.*)') # Unit test flags from include/test/test.h UTF_FLAT_TREE = 0x08 @@ -615,14 +615,27 @@ def parse_results(output, show_results=False, col=None): skipped = 0 leaked = 0 leak_bytes = 0 + cur_test = None + cur_leak_bytes = 0 + cur_details = [] + leak_top = [] for line in output.splitlines(): + test_match = RE_TEST_NAME.match(line) + if test_match: + cur_test = test_match.group(1) + cur_leak_bytes = 0 + cur_details = [] + continue if RE_LEAK.match(line): leaked += 1 continue detail = RE_LEAK_DETAIL.match(line) if detail: - leak_bytes += int(detail.group(1), 16) + nbytes = int(detail.group(1), 16) + leak_bytes += nbytes + cur_leak_bytes += nbytes + cur_details.append((nbytes, detail.group(2))) continue result_match = RE_RESULT.match(line) if result_match: @@ -635,10 +648,16 @@ def parse_results(output, show_results=False, col=None): skipped += 1 if show_results: show_result(status, name, col) + if cur_leak_bytes and cur_test: + leak_top.append((cur_leak_bytes, cur_test, cur_details)) + cur_leak_bytes = 0 + cur_details = [] if not passed and not failed and not skipped: return None - return TestCounts(passed, failed, skipped, leaked, leak_bytes) + leak_top.sort(reverse=True) + return TestCounts(passed, failed, skipped, leaked, leak_bytes, + leak_top) def count_tests(sandbox, specs): @@ -899,6 +918,8 @@ def run_tests(sandbox, specs, args, col): if res is not None and res: show_summary(res.passed, res.failed, res.skipped, elapsed, res.leaked, res.leak_bytes) + if res.leak_top and args.show_leaks: + show_leak_top(res.leak_top, args.show_leaks) ret = result.return_code elif res is not None: sig = check_signal(result.return_code) diff --git a/uman_pkg/util.py b/uman_pkg/util.py index 2eca6d7..5d087ac 100644 --- a/uman_pkg/util.py +++ b/uman_pkg/util.py @@ -263,3 +263,28 @@ def show_summary(passed, failed, skipped, elapsed, leaked=0, leak_str += f' ({format_bytes(leak_bytes)})' parts.append(f'{magenta}{leak_str}{reset}') print(f'Results: {", ".join(parts)} in {format_duration(elapsed)}') + + +def show_leak_top(leak_top, count): + """Show the top leaking tests with deduplicated backtraces + + Args: + leak_top (list): List of (bytes, name, details) tuples + count (int): Number of entries to show + """ + print('Top leaks:') + for nbytes, name, details in leak_top[:count]: + name = name.rstrip(':') + print(f' {format_bytes(nbytes):>7s} {name}') + by_trace = {} + for size, trace in details: + if trace in by_trace: + cnt, total = by_trace[trace] + by_trace[trace] = (cnt + 1, total + size) + else: + by_trace[trace] = (1, size) + for trace, (cnt, total) in by_trace.items(): + if cnt > 1: + print(f' {cnt}x {format_bytes(total)} {trace}') + else: + print(f' {format_bytes(total)} {trace}') From 4712a2e4e888f85c5190d255a33720498c14fb96 Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Sun, 15 Mar 2026 11:53:31 -0600 Subject: [PATCH 11/12] uman: Show leak summary in py subcommand from test-log.html The py subcommand passes --leak-check to test.py but does not show the leak summary afterwards. Parse test-log.html after pytest finishes to extract leak data and show the same summary and top-leaks display as the t subcommand. Extract add_leak_opts() and show_leak_top() as shared helpers to avoid duplication between the two subcommands. Co-developed-by: Claude Opus 4.6 --- uman_pkg/cmdpy.py | 36 ++++++++++++++++++++++++++++++------ uman_pkg/cmdtest.py | 11 ++++++++--- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/uman_pkg/cmdpy.py b/uman_pkg/cmdpy.py index 65f4758..7395166 100644 --- a/uman_pkg/cmdpy.py +++ b/uman_pkg/cmdpy.py @@ -27,8 +27,9 @@ from uman_pkg import build as build_mod from uman_pkg import settings -from uman_pkg.cmdtest import get_sandbox_path -from uman_pkg.util import exec_cmd, get_uboot_dir, show_summary +from uman_pkg.cmdtest import get_sandbox_path, parse_results +from uman_pkg.util import (exec_cmd, get_uboot_dir, show_leak_top, + show_summary) # Fallback hostname directory for test hooks (has standard QEMU board configs) HOOKS_FALLBACK = 'travis-ci' @@ -1365,7 +1366,9 @@ def do_pytest(args): # pylint: disable=too-many-return-statements,too-many-bran if args.gdb: return run_with_gdb(args) + start_time = time.time() result = exec_cmd(cmd, args.dry_run, env=env, capture=False) + elapsed = time.time() - start_time if result is None: # dry-run qemu_cmd = get_qemu_command(board, args) @@ -1378,8 +1381,29 @@ def do_pytest(args): # pylint: disable=too-many-return-statements,too-many-bran print(result.stderr, file=sys.stderr) if not args.quiet: tout.error('pytest failed') - return result.return_code + else: + if not args.quiet: + tout.notice('pytest passed') - if not args.quiet: - tout.notice('pytest passed') - return 0 + # Show leak summary from test log if leak-check was enabled + if args.leak_check: + if args.output_dir: + build_dir = args.output_dir + else: + base_dir = settings.get('build_dir', '/tmp/b') + build_dir = f'{base_dir}/{args.board}' + log_path = os.path.join(build_dir, 'test-log.html') + if os.path.exists(log_path): + import html + with open(log_path) as fh: + text = fh.read() + # Strip HTML tags first, then unescape entities + text = re.sub(r'<[^>]+>', '\n', text) + text = html.unescape(text) + res = parse_results(text) + if res and res.leaked: + show_summary(res.passed, res.failed, res.skipped, elapsed, + res.leaked, res.leak_bytes) + if res.leak_top and args.show_leaks: + show_leak_top(res.leak_top, args.show_leaks) + return result.return_code diff --git a/uman_pkg/cmdtest.py b/uman_pkg/cmdtest.py index bac2c02..f3ba2b2 100644 --- a/uman_pkg/cmdtest.py +++ b/uman_pkg/cmdtest.py @@ -620,9 +620,14 @@ def parse_results(output, show_results=False, col=None): cur_details = [] leak_top = [] + def flush_leak(): + if cur_leak_bytes and cur_test: + leak_top.append((cur_leak_bytes, cur_test, cur_details)) + for line in output.splitlines(): test_match = RE_TEST_NAME.match(line) if test_match: + flush_leak() cur_test = test_match.group(1) cur_leak_bytes = 0 cur_details = [] @@ -648,12 +653,12 @@ def parse_results(output, show_results=False, col=None): skipped += 1 if show_results: show_result(status, name, col) - if cur_leak_bytes and cur_test: - leak_top.append((cur_leak_bytes, cur_test, cur_details)) + flush_leak() cur_leak_bytes = 0 cur_details = [] + flush_leak() - if not passed and not failed and not skipped: + if not passed and not failed and not skipped and not leaked: return None leak_top.sort(reverse=True) return TestCounts(passed, failed, skipped, leaked, leak_bytes, From 291bda7a4ce0ef76cdd023d2897b234def9bd52f Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Sun, 15 Mar 2026 19:58:08 -0600 Subject: [PATCH 12/12] uman: Reset terminal on crash with parsed results When a test crashes (e.g. SIGSEGV), the sandbox process may leave the terminal in a bad state. The tset call to fix this only runs when no test results are parsed, but a crash can happen after some tests have already produced results. Move the signal check and terminal reset before the result display so it always runs when a signal is detected. Co-developed-by: Claude Opus 4.6 --- uman_pkg/cmdtest.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/uman_pkg/cmdtest.py b/uman_pkg/cmdtest.py index f3ba2b2..7adf9d5 100644 --- a/uman_pkg/cmdtest.py +++ b/uman_pkg/cmdtest.py @@ -920,17 +920,24 @@ def run_tests(sandbox, specs, args, col): else: res = None + # Reset terminal if killed by a signal (e.g. SIGSEGV) + sig = check_signal(result.return_code) if result else None + if sig: + os.system('tset') + if res is not None and res: show_summary(res.passed, res.failed, res.skipped, elapsed, res.leaked, res.leak_bytes) if res.leak_top and args.show_leaks: show_leak_top(res.leak_top, args.show_leaks) + if sig: + sig_names = {6: 'SIGABRT', 11: 'SIGSEGV', 15: 'SIGTERM'} + tout.error(f'Test crashed ' + f'({sig_names.get(sig, f"signal {sig}")})') ret = result.return_code elif res is not None: - sig = check_signal(result.return_code) if sig: sig_names = {6: 'SIGABRT', 11: 'SIGSEGV', 15: 'SIGTERM'} - os.system('tset') tout.error(f'Test crashed ' f'({sig_names.get(sig, f"signal {sig}")})') ret = result.return_code