[Concept,05/24] pickman: Add test coverage support

Message ID 20251217022823.392557-6-sjg@u-boot.org
State New
Headers
Series pickman: Refine the feature set |

Commit Message

Simon Glass Dec. 17, 2025, 2:27 a.m. UTC
  From: Simon Glass <simon.glass@canonical.com>

Add support for running pickman tests with code coverage checking,
similar to binman. Use test_util.run_test_suites() for running tests
and test_util.run_test_coverage() for coverage checking.

Options added to the 'test' command:
  -P: Number of processes for parallel test execution
  -T: Run with coverage checking
  -v: Verbosity level (0-4)

The coverage check allows failures for modules that require external
services (agent.py, gitlab_api.py, control.py, database.py) since
these cannot easily achieve 100% coverage in unit tests.

Also update test_util.py to recognize 'pickman' as using the 'test'
subcommand like binman and patman.

Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
---

 tools/pickman/__main__.py       | 67 ++++++++++++++++++++++++++++++++-
 tools/u_boot_pylib/test_util.py |  8 ++--
 2 files changed, 70 insertions(+), 5 deletions(-)
  

Patch

diff --git a/tools/pickman/__main__.py b/tools/pickman/__main__.py
index 3d311264f06..2d2366c1b80 100755
--- a/tools/pickman/__main__.py
+++ b/tools/pickman/__main__.py
@@ -9,6 +9,7 @@ 
 import argparse
 import os
 import sys
+import unittest
 
 # Allow 'from pickman import xxx' to work via symlink
 our_path = os.path.dirname(os.path.realpath(__file__))
@@ -16,6 +17,8 @@  sys.path.insert(0, os.path.join(our_path, '..'))
 
 # pylint: disable=wrong-import-position,import-error
 from pickman import control
+from pickman import ftest
+from u_boot_pylib import test_util
 
 
 def parse_args(argv):
@@ -80,11 +83,65 @@  def parse_args(argv):
     poll_cmd.add_argument('-t', '--target', default='master',
                           help='Target branch for MR (default: master)')
 
-    subparsers.add_parser('test', help='Run tests')
+    test_cmd = subparsers.add_parser('test', help='Run tests')
+    test_cmd.add_argument('-P', '--processes', type=int,
+                          help='Number of processes to run tests (default: all)')
+    test_cmd.add_argument('-T', '--test-coverage', action='store_true',
+                          help='Run tests and check for 100%% coverage')
+    test_cmd.add_argument('-v', '--verbosity', type=int, default=1,
+                          help='Verbosity level (0-4, default: 1)')
+    test_cmd.add_argument('tests', nargs='*', help='Specific tests to run')
 
     return parser.parse_args(argv)
 
 
+def get_test_classes():
+    """Get all test classes from the ftest module.
+
+    Returns:
+        list: List of test class objects
+    """
+    return [getattr(ftest, name) for name in dir(ftest)
+            if name.startswith('Test') and
+            isinstance(getattr(ftest, name), type) and
+            issubclass(getattr(ftest, name), unittest.TestCase)]
+
+
+def run_tests(processes, verbosity, test_name):
+    """Run the pickman test suite.
+
+    Args:
+        processes (int): Number of processes for concurrent tests
+        verbosity (int): Verbosity level (0-4)
+        test_name (str): Specific test to run, or None for all
+
+    Returns:
+        int: 0 if tests passed, 1 otherwise
+    """
+    result = test_util.run_test_suites(
+        'pickman', False, verbosity, False, False, processes,
+        test_name, None, get_test_classes())
+
+    return 0 if result.wasSuccessful() else 1
+
+
+def run_test_coverage(args):
+    """Run tests with coverage checking.
+
+    Args:
+        args (list): Specific tests to run, or None for all
+    """
+    # agent.py and gitlab_api.py require external services (Claude, GitLab)
+    # so they can't achieve 100% coverage in unit tests
+    test_util.run_test_coverage(
+        'tools/pickman/pickman', None,
+        ['*test*', '*__main__.py', 'tools/u_boot_pylib/*'],
+        None, extra_args=None, args=args,
+        allow_failures=['tools/pickman/agent.py',
+                        'tools/pickman/gitlab_api.py',
+                        'tools/pickman/control.py'])
+
+
 def main(argv=None):
     """Main function to parse args and run commands.
 
@@ -92,6 +149,14 @@  def main(argv=None):
         argv (list): Command line arguments (None for sys.argv[1:])
     """
     args = parse_args(argv)
+
+    if args.cmd == 'test':
+        if args.test_coverage:
+            run_test_coverage(args.tests or None)
+            return 0
+        test_name = args.tests[0] if args.tests else None
+        return run_tests(args.processes, args.verbosity, test_name)
+
     return control.do_pickman(args)
 
 
diff --git a/tools/u_boot_pylib/test_util.py b/tools/u_boot_pylib/test_util.py
index 7bd12705557..b1c8740d883 100644
--- a/tools/u_boot_pylib/test_util.py
+++ b/tools/u_boot_pylib/test_util.py
@@ -57,7 +57,8 @@  def run_test_coverage(prog, filter_fname, exclude_list, build_dir,
     glob_list += exclude_list
     glob_list += ['*libfdt.py', '*/site-packages/*', '*/dist-packages/*']
     glob_list += ['*concurrencytest*']
-    test_cmd = 'test' if 'binman' in prog or 'patman' in prog else '-t'
+    use_test = 'binman' in prog or 'patman' in prog or 'pickman' in prog
+    test_cmd = 'test' if use_test else '-t'
     prefix = ''
     if build_dir:
         prefix = 'PYTHONPATH=$PYTHONPATH:%s/sandbox_spl/tools ' % build_dir
@@ -91,13 +92,12 @@  def run_test_coverage(prog, filter_fname, exclude_list, build_dir,
     print(coverage)
     if coverage != '100%':
         print(stdout)
-        print("To get a report in 'htmlcov/index.html', type: python3-coverage html")
+        print("To get a report in 'htmlcov/index.html', type: "
+              "python3-coverage html")
         print('Coverage error: %s, but should be 100%%' % coverage)
         ok = False
     if not ok:
         if allow_failures:
-            # for line in lines:
-                # print('.', line, re.match(r'^(tools/.*py) *\d+ *(\d+) *(\d+)%$', line))
             lines = [re.match(r'^(tools/.*py) *\d+ *(\d+) *\d+%$', line)
                      for line in stdout.splitlines()]
             bad = []