mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
synced 2026-05-09 21:42:09 +02:00
07f6cb18c5
On quiet mode, only report errors. Signed-off-by: Mauro Carvalho Chehab <mchehab+huawei@kernel.org> Signed-off-by: Jonathan Corbet <corbet@lwn.net> Message-ID: <27556792ff70e6267ecd19c258149d380db8d423.1774551940.git.mchehab+huawei@kernel.org>
364 lines
11 KiB
Python
Executable File
364 lines
11 KiB
Python
Executable File
#!/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
|
|
|
|
"""
|
|
Provides helper functions and classes execute python unit tests.
|
|
|
|
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, verbose):
|
|
"""
|
|
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():
|
|
if verbose:
|
|
print(f"{module_name}:")
|
|
for class_name, tests in classes.items():
|
|
if verbose:
|
|
print(f" {class_name}:")
|
|
for test_name, status in tests:
|
|
if not verbose and status in [ "OK", "EXPECTED_FAIL" ]:
|
|
continue
|
|
|
|
# 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']}"
|
|
)
|
|
if verbose:
|
|
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.
|
|
|
|
This class discover test files, import its unittest classes and
|
|
executes the test on it.
|
|
"""
|
|
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("-q", "--quiet", action="store_true")
|
|
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=None, pattern=None,
|
|
suite=None, parser=None, args=None, env=None):
|
|
"""
|
|
Execute all tests from the unity test file.
|
|
|
|
It contains several optional parameters:
|
|
|
|
``caller_file``:
|
|
- name of the file that contains test.
|
|
|
|
typical usage is to place __file__ at the caller test, e.g.::
|
|
|
|
if __name__ == "__main__":
|
|
TestUnits().run(__file__)
|
|
|
|
``pattern``:
|
|
- optional pattern to match multiple file names. Defaults
|
|
to basename of ``caller_file``.
|
|
|
|
``suite``:
|
|
- an unittest suite initialized by the caller using
|
|
``unittest.TestLoader().discover()``.
|
|
|
|
``parser``:
|
|
- an argparse parser. If not defined, this helper will create
|
|
one.
|
|
|
|
``args``:
|
|
- an ``argparse.Namespace`` data filled by the caller.
|
|
|
|
``env``:
|
|
- environment variables that will be passed to the test suite
|
|
|
|
At least ``caller_file`` or ``suite`` must be used, otherwise a
|
|
``TypeError`` will be raised.
|
|
"""
|
|
if not args:
|
|
if not parser:
|
|
parser = self.parse_args()
|
|
args = parser.parse_args()
|
|
|
|
if not caller_file and not suite:
|
|
raise TypeError("Either caller_file or suite is needed at TestUnits")
|
|
|
|
if args.quiet:
|
|
verbose = 0
|
|
else:
|
|
verbose = args.verbose
|
|
|
|
if not env:
|
|
env = os.environ.copy()
|
|
|
|
env["VERBOSE"] = f"{verbose}"
|
|
|
|
patcher = patch.dict(os.environ, env)
|
|
patcher.start()
|
|
# ensure it gets stopped after
|
|
atexit.register(patcher.stop)
|
|
|
|
|
|
if verbose >= 2:
|
|
unittest.TextTestRunner(verbosity=verbose).run = lambda suite: suite
|
|
|
|
# Load ONLY tests from the calling file
|
|
if not suite:
|
|
if not pattern:
|
|
pattern = caller_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(verbose)
|
|
|
|
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
|
|
with. The recommended way is to place this at the end of each
|
|
unittest module::
|
|
|
|
if __name__ == "__main__":
|
|
run_unittest(__file__)
|
|
"""
|
|
TestUnits().run(fname)
|