From: Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
To: Jonathan Corbet <corbet@lwn.net>,
Linux Doc Mailing List <linux-doc@vger.kernel.org>,
Mauro Carvalho Chehab <mchehab@kernel.org>
Cc: Mauro Carvalho Chehab <mchehab+huawei@kernel.org>,
bpf@vger.kernel.org, intel-wired-lan@lists.osuosl.org,
linux-kernel@vger.kernel.org, netdev@vger.kernel.org,
Peter Zijlstra <peterz@infradead.org>,
Randy Dunlap <rdunlap@infradead.org>,
Shuah Khan <skhan@linuxfoundation.org>,
Stephen Rothwell <sfr@canb.auug.org.au>
Subject: [Intel-wired-lan] [PATCH v2 21/25] tools: python: add helpers to run unit tests
Date: Wed, 28 Jan 2026 17:50:19 +0100 [thread overview]
Message-ID: <a797d1763ffa349ef4459ea339f9d4349415bd76.1769617841.git.mchehab+huawei@kernel.org> (raw)
In-Reply-To: <cover.1769617841.git.mchehab+huawei@kernel.org>
While python internal libraries have support for unit tests, its
output is not nice. Add a helper module to improve its output.
I wrote this module last year while testing some scripts I used
internally. The initial skeleton was generated with the help of
LLM tools, but it was higly modified to ensure that it will work
as I would expect.
Signed-off-by: Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
---
tools/lib/python/unittest_helper.py | 293 ++++++++++++++++++++++++++++
1 file changed, 293 insertions(+)
create mode 100755 tools/lib/python/unittest_helper.py
diff --git a/tools/lib/python/unittest_helper.py b/tools/lib/python/unittest_helper.py
new file mode 100755
index 000000000000..e438472fa704
--- /dev/null
+++ b/tools/lib/python/unittest_helper.py
@@ -0,0 +1,293 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# Copyright(c) 2025-2026: Mauro Carvalho Chehab <mchehab@kernel.org>.
+#
+# pylint: disable=C0103,R0912,R0914,E1101
+
+"""
+Helper class to better display unittest results.
+
+Those help functions provide a nice colored output summary of each
+executed test and, when a test fails, it shows the different in diff
+format when running in verbose mode, like::
+
+ $ tools/unittests/nested_match.py -v
+ ...
+ Traceback (most recent call last):
+ File "/new_devel/docs/tools/unittests/nested_match.py", line 69, in test_count_limit
+ self.assertEqual(replaced, "bar(a); bar(b); foo(c)")
+ ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ AssertionError: 'bar(a) foo(b); foo(c)' != 'bar(a); bar(b); foo(c)'
+ - bar(a) foo(b); foo(c)
+ ? ^^^^
+ + bar(a); bar(b); foo(c)
+ ? ^^^^^
+ ...
+
+It also allows filtering what tests will be executed via ``-k`` parameter.
+
+Typical usage is to do::
+
+ from unittest_helper import run_unittest
+ ...
+
+ if __name__ == "__main__":
+ run_unittest(__file__)
+
+If passing arguments is needed, on a more complex scenario, it can be
+used like on this example::
+
+ from unittest_helper import TestUnits, run_unittest
+ ...
+ env = {'sudo': ""}
+ ...
+ if __name__ == "__main__":
+ runner = TestUnits()
+ base_parser = runner.parse_args()
+ base_parser.add_argument('--sudo', action='store_true',
+ help='Enable tests requiring sudo privileges')
+
+ args = base_parser.parse_args()
+
+ # Update module-level flag
+ if args.sudo:
+ env['sudo'] = "1"
+
+ # Run tests with customized arguments
+ runner.run(__file__, parser=base_parser, args=args, env=env)
+"""
+
+import argparse
+import atexit
+import os
+import re
+import unittest
+import sys
+
+from unittest.mock import patch
+
+
+class Summary(unittest.TestResult):
+ """
+ Overrides unittest.TestResult class to provide a nice colored
+ summary. When in verbose mode, displays actual/expected difference in
+ unified diff format.
+ """
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ #: Dictionary to store organized test results
+ self.test_results = {}
+
+ #: max length of the test names
+ self.max_name_length = 0
+
+ def startTest(self, test):
+ super().startTest(test)
+ test_id = test.id()
+ parts = test_id.split(".")
+ # Extract module, class, and method names
+ if len(parts) >= 3:
+ module_name = parts[-3]
+ else:
+ module_name = ""
+ if len(parts) >= 2:
+ class_name = parts[-2]
+ else:
+ class_name = ""
+ method_name = parts[-1]
+ # Build the hierarchical structure
+ if module_name not in self.test_results:
+ self.test_results[module_name] = {}
+ if class_name not in self.test_results[module_name]:
+ self.test_results[module_name][class_name] = []
+ # Track maximum test name length for alignment
+ display_name = f"{method_name}:"
+
+ self.max_name_length = max(len(display_name), self.max_name_length)
+
+ def _record_test(self, test, status):
+ test_id = test.id()
+ parts = test_id.split(".")
+ if len(parts) >= 3:
+ module_name = parts[-3]
+ else:
+ module_name = ""
+ if len(parts) >= 2:
+ class_name = parts[-2]
+ else:
+ class_name = ""
+ method_name = parts[-1]
+ self.test_results[module_name][class_name].append((method_name, status))
+
+ def addSuccess(self, test):
+ super().addSuccess(test)
+ self._record_test(test, "OK")
+
+ def addFailure(self, test, err):
+ super().addFailure(test, err)
+ self._record_test(test, "FAIL")
+
+ def addError(self, test, err):
+ super().addError(test, err)
+ self._record_test(test, "ERROR")
+
+ def addSkip(self, test, reason):
+ super().addSkip(test, reason)
+ self._record_test(test, f"SKIP ({reason})")
+
+ def printResults(self):
+ """
+ Print results using colors if tty.
+ """
+ # Check for ANSI color support
+ use_color = sys.stdout.isatty()
+ COLORS = {
+ "OK": "\033[32m", # Green
+ "FAIL": "\033[31m", # Red
+ "SKIP": "\033[1;33m", # Yellow
+ "PARTIAL": "\033[33m", # Orange
+ "EXPECTED_FAIL": "\033[36m", # Cyan
+ "reset": "\033[0m", # Reset to default terminal color
+ }
+ if not use_color:
+ for c in COLORS:
+ COLORS[c] = ""
+
+ # Calculate maximum test name length
+ if not self.test_results:
+ return
+ try:
+ lengths = []
+ for module in self.test_results.values():
+ for tests in module.values():
+ for test_name, _ in tests:
+ lengths.append(len(test_name) + 1) # +1 for colon
+ max_length = max(lengths) + 2 # Additional padding
+ except ValueError:
+ sys.exit("Test list is empty")
+
+ # Print results
+ for module_name, classes in self.test_results.items():
+ print(f"{module_name}:")
+ for class_name, tests in classes.items():
+ print(f" {class_name}:")
+ for test_name, status in tests:
+ # Get base status without reason for SKIP
+ if status.startswith("SKIP"):
+ status_code = status.split()[0]
+ else:
+ status_code = status
+ color = COLORS.get(status_code, "")
+ print(
+ f" {test_name + ':':<{max_length}}{color}{status}{COLORS['reset']}"
+ )
+ print()
+
+ # Print summary
+ print(f"\nRan {self.testsRun} tests", end="")
+ if hasattr(self, "timeTaken"):
+ print(f" in {self.timeTaken:.3f}s", end="")
+ print()
+
+ if not self.wasSuccessful():
+ print(f"\n{COLORS['FAIL']}FAILED (", end="")
+ failures = getattr(self, "failures", [])
+ errors = getattr(self, "errors", [])
+ if failures:
+ print(f"failures={len(failures)}", end="")
+ if errors:
+ if failures:
+ print(", ", end="")
+ print(f"errors={len(errors)}", end="")
+ print(f"){COLORS['reset']}")
+
+
+def flatten_suite(suite):
+ """Flatten test suite hierarchy"""
+ tests = []
+ for item in suite:
+ if isinstance(item, unittest.TestSuite):
+ tests.extend(flatten_suite(item))
+ else:
+ tests.append(item)
+ return tests
+
+
+class TestUnits:
+ """
+ Helper class to set verbosity level
+ """
+ def parse_args(self):
+ """Returns a parser for command line arguments."""
+ parser = argparse.ArgumentParser(description="Test runner with regex filtering")
+ parser.add_argument("-v", "--verbose", action="count", default=1)
+ parser.add_argument("-f", "--failfast", action="store_true")
+ parser.add_argument("-k", "--keyword",
+ help="Regex pattern to filter test methods")
+ return parser
+
+ def run(self, caller_file, parser=None, args=None, env=None):
+ """Execute all tests from the unity test file"""
+ if not args:
+ if not parser:
+ parser = self.parse_args()
+ args = parser.parse_args()
+
+ if env:
+ patcher = patch.dict(os.environ, env)
+ patcher.start()
+ # ensure it gets stopped after
+ atexit.register(patcher.stop)
+
+ verbose = args.verbose
+
+ if verbose >= 2:
+ unittest.TextTestRunner(verbosity=verbose).run = lambda suite: suite
+
+ # Load ONLY tests from the calling file
+ loader = unittest.TestLoader()
+ suite = loader.discover(start_dir=os.path.dirname(caller_file),
+ pattern=os.path.basename(caller_file))
+
+ # Flatten the suite for environment injection
+ tests_to_inject = flatten_suite(suite)
+
+ # Filter tests by method name if -k specified
+ if args.keyword:
+ try:
+ pattern = re.compile(args.keyword)
+ filtered_suite = unittest.TestSuite()
+ for test in tests_to_inject: # Use the pre-flattened list
+ method_name = test.id().split(".")[-1]
+ if pattern.search(method_name):
+ filtered_suite.addTest(test)
+ suite = filtered_suite
+ except re.error as e:
+ sys.stderr.write(f"Invalid regex pattern: {e}\n")
+ sys.exit(1)
+ else:
+ # Maintain original suite structure if no keyword filtering
+ suite = unittest.TestSuite(tests_to_inject)
+
+ if verbose >= 2:
+ resultclass = None
+ else:
+ resultclass = Summary
+
+ runner = unittest.TextTestRunner(verbosity=args.verbose,
+ resultclass=resultclass,
+ failfast=args.failfast)
+ result = runner.run(suite)
+ if resultclass:
+ result.printResults()
+
+ sys.exit(not result.wasSuccessful())
+
+
+def run_unittest(fname):
+ """
+ Basic usage of TestUnits class.
+ Use it when there's no need to pass any extra argument to the tests.
+ """
+ TestUnits().run(fname)
--
2.52.0
next prev parent reply other threads:[~2026-01-28 16:50 UTC|newest]
Thread overview: 56+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-01-28 16:49 [Intel-wired-lan] [PATCH v2 00/25] kernel-doc: make it parse new functions and structs Mauro Carvalho Chehab
2026-01-28 16:49 ` [Intel-wired-lan] [PATCH v2 01/25] docs: kdoc_re: add support for groups() Mauro Carvalho Chehab
2026-01-28 17:44 ` Loktionov, Aleksandr
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 02/25] docs: kdoc_re: don't go past the end of a line Mauro Carvalho Chehab
2026-01-28 17:44 ` Loktionov, Aleksandr
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 03/25] docs: kdoc_parser: move var transformers to the beginning Mauro Carvalho Chehab
2026-01-28 17:44 ` Loktionov, Aleksandr
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 04/25] docs: kdoc_parser: don't mangle with function defines Mauro Carvalho Chehab
2026-01-28 17:45 ` Loktionov, Aleksandr
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 05/25] docs: kdoc_parser: add functions support for NestedMatch Mauro Carvalho Chehab
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 06/25] docs: kdoc_parser: use NestedMatch to handle __attribute__ on functions Mauro Carvalho Chehab
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 07/25] docs: kdoc_parser: fix variable regexes to work with size_t Mauro Carvalho Chehab
2026-01-28 17:45 ` Loktionov, Aleksandr
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 08/25] docs: kdoc_parser: fix the default_value logic for variables Mauro Carvalho Chehab
2026-01-28 17:45 ` Loktionov, Aleksandr
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 09/25] docs: kdoc_parser: add some debug for variable parsing Mauro Carvalho Chehab
2026-01-28 17:46 ` Loktionov, Aleksandr
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 10/25] docs: kdoc_parser: don't exclude defaults from prototype Mauro Carvalho Chehab
2026-01-28 17:46 ` Loktionov, Aleksandr
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 11/25] docs: kdoc_parser: fix parser to support multi-word types Mauro Carvalho Chehab
2026-01-28 17:47 ` Loktionov, Aleksandr
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 12/25] docs: kdoc_parser: ignore context analysis and lock attributes Mauro Carvalho Chehab
2026-01-28 17:47 ` Loktionov, Aleksandr
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 13/25] docs: kdoc_parser: add support for LIST_HEAD Mauro Carvalho Chehab
2026-01-28 17:47 ` Loktionov, Aleksandr
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 14/25] kdoc_parser: handle struct member macro VIRTIO_DECLARE_FEATURES(name) Mauro Carvalho Chehab
2026-01-28 17:47 ` Loktionov, Aleksandr
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 15/25] docs: kdoc_re: properly handle strings and escape chars on it Mauro Carvalho Chehab
2026-01-28 17:47 ` Loktionov, Aleksandr
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 16/25] docs: kdoc_re: better show KernRe() at documentation Mauro Carvalho Chehab
2026-01-28 17:48 ` Loktionov, Aleksandr
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 17/25] docs: kdoc_re: don't recompile NextMatch regex every time Mauro Carvalho Chehab
2026-01-28 17:48 ` Loktionov, Aleksandr
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 18/25] docs: kdoc_re: Change NestedMath args replacement to \0 Mauro Carvalho Chehab
2026-01-28 17:48 ` Loktionov, Aleksandr
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 19/25] docs: kdoc_re: make NextedMatch use KernRe Mauro Carvalho Chehab
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 20/25] tools: kdoc_re: add support on NestedMatch for argument replacement Mauro Carvalho Chehab
2026-01-28 17:49 ` Loktionov, Aleksandr
2026-01-28 16:50 ` Mauro Carvalho Chehab [this message]
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 22/25] unittests: add tests for NestedMatch class Mauro Carvalho Chehab
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 23/25] tools/lib/python/unittest_helper.py Mauro Carvalho Chehab
2026-01-28 17:17 ` Mauro Carvalho Chehab
2026-01-28 17:32 ` Loktionov, Aleksandr
2026-01-28 18:09 ` Jacob Keller
2026-01-28 21:02 ` Mauro Carvalho Chehab
2026-01-28 22:04 ` Jacob Keller
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 24/25] docs: kdoc_parser: better handle struct_group macros Mauro Carvalho Chehab
2026-01-28 17:49 ` Loktionov, Aleksandr
2026-01-28 16:50 ` [Intel-wired-lan] [PATCH v2 25/25] docs: kdoc_re: fix a parse bug on struct page_pool_params Mauro Carvalho Chehab
2026-01-28 17:49 ` Loktionov, Aleksandr
2026-01-28 17:27 ` [Intel-wired-lan] [PATCH v2 00/25] kernel-doc: make it parse new functions and structs Jonathan Corbet
2026-01-28 18:15 ` Jacob Keller
2026-01-28 22:00 ` Mauro Carvalho Chehab
2026-01-28 22:08 ` Jacob Keller
2026-01-29 8:14 ` Mauro Carvalho Chehab
2026-02-10 15:27 ` Mauro Carvalho Chehab
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=a797d1763ffa349ef4459ea339f9d4349415bd76.1769617841.git.mchehab+huawei@kernel.org \
--to=mchehab+huawei@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=corbet@lwn.net \
--cc=intel-wired-lan@lists.osuosl.org \
--cc=linux-doc@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=mchehab@kernel.org \
--cc=netdev@vger.kernel.org \
--cc=peterz@infradead.org \
--cc=rdunlap@infradead.org \
--cc=sfr@canb.auug.org.au \
--cc=skhan@linuxfoundation.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox