Currently, kunit.py has many subcommands and options, making it difficult
to remember them without checking the help message.

Add --list-cmds and --list-opts to kunit.py to get available commands and
options, use those outputs in kunit-completion.sh to show completion.

This implementation is similar to perf and tools/perf/perf-completion.sh.

Example output:
  $ source tools/testing/kunit/kunit-completion.sh
  $ ./tools/testing/kunit/kunit.py [TAB][TAB]
  build   config  exec    parse   run
  $ ./tools/testing/kunit/kunit.py run --k[TAB][TAB]
  --kconfig_add  --kernel_args  --kunitconfig

Signed-off-by: Ryota Sakamoto <[email protected]>
---
 Documentation/dev-tools/kunit/run_wrapper.rst |  9 ++++++++
 tools/testing/kunit/kunit-completion.sh       | 33 +++++++++++++++++++++++++++
 tools/testing/kunit/kunit.py                  | 30 ++++++++++++++++++++++++
 tools/testing/kunit/kunit_tool_test.py        | 21 +++++++++++++++++
 4 files changed, 93 insertions(+)

diff --git a/Documentation/dev-tools/kunit/run_wrapper.rst 
b/Documentation/dev-tools/kunit/run_wrapper.rst
index 
6697c71ee8ca020b8ac7e91b46e29ab082d9dea0..3c0b585dcfffbd3929d0eef1ab9376fa4f380872
 100644
--- a/Documentation/dev-tools/kunit/run_wrapper.rst
+++ b/Documentation/dev-tools/kunit/run_wrapper.rst
@@ -335,3 +335,12 @@ command line arguments:
 
 - ``--list_tests_attr``: If set, lists all tests that will be run and all of 
their
   attributes.
+
+Command-line completion
+==============================
+
+The kunit_tool comes with a bash completion script:
+
+.. code-block:: bash
+
+       source tools/testing/kunit/kunit-completion.sh
diff --git a/tools/testing/kunit/kunit-completion.sh 
b/tools/testing/kunit/kunit-completion.sh
new file mode 100644
index 
0000000000000000000000000000000000000000..3b9b68e3bc384c026f10f74b8a1df2129cb2cd50
--- /dev/null
+++ b/tools/testing/kunit/kunit-completion.sh
@@ -0,0 +1,33 @@
+# SPDX-License-Identifier: GPL-2.0
+# bash completion support for KUnit
+
+_kunit_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+
+_kunit()
+{
+       local cur prev words cword
+       _init_completion || return
+
+       local script="${_kunit_dir}/kunit.py"
+
+       if [[ $cword -eq 1 && "$cur" != -* ]]; then
+               local cmds=$(${script} --list-cmds 2>/dev/null)
+               COMPREPLY=($(compgen -W "${cmds}" -- "$cur"))
+               return 0
+       fi
+
+       if [[ "$cur" == -* ]]; then
+               if [[ -n "${words[1]}" && "${words[1]}" != -* ]]; then
+                       local opts=$(${script} ${words[1]} --list-opts 
2>/dev/null)
+                       COMPREPLY=($(compgen -W "${opts}" -- "$cur"))
+                       return 0
+               else
+                       local opts=$(${script} --list-opts 2>/dev/null)
+                       COMPREPLY=($(compgen -W "${opts}" -- "$cur"))
+                       return 0
+               fi
+       fi
+}
+
+complete -o default -F _kunit kunit.py
+complete -o default -F _kunit kunit
diff --git a/tools/testing/kunit/kunit.py b/tools/testing/kunit/kunit.py
index 
cd99c1956331dbbfb06cf4ddf130db3dcf2a7c31..a5aee1eb88e65fa2387b2623642d2ee9a66db600
 100755
--- a/tools/testing/kunit/kunit.py
+++ b/tools/testing/kunit/kunit.py
@@ -323,6 +323,17 @@ def get_default_jobs() -> int:
                return ncpu
        raise RuntimeError("os.cpu_count() returned None")
 
+def add_completion_opts(parser: argparse.ArgumentParser) -> None:
+       parser.add_argument('--list-opts',
+                           help=argparse.SUPPRESS,
+                           action='store_true')
+
+def add_root_opts(parser: argparse.ArgumentParser) -> None:
+       parser.add_argument('--list-cmds',
+                           help=argparse.SUPPRESS,
+                           action='store_true')
+       add_completion_opts(parser)
+
 def add_common_opts(parser: argparse.ArgumentParser) -> None:
        parser.add_argument('--build_dir',
                            help='As in the make command, it specifies the 
build '
@@ -374,6 +385,8 @@ def add_common_opts(parser: argparse.ArgumentParser) -> 
None:
                            help='Additional QEMU arguments, e.g. "-smp 8"',
                            action='append', metavar='')
 
+       add_completion_opts(parser)
+
 def add_build_opts(parser: argparse.ArgumentParser) -> None:
        parser.add_argument('--jobs',
                            help='As in the make command, "Specifies  the 
number of '
@@ -569,6 +582,7 @@ subcommand_handlers_map = {
 def main(argv: Sequence[str]) -> None:
        parser = argparse.ArgumentParser(
                        description='Helps writing and running KUnit tests.')
+       add_root_opts(parser)
        subparser = parser.add_subparsers(dest='subcommand')
 
        # The 'run' command will config, build, exec, and parse in one go.
@@ -603,12 +617,28 @@ def main(argv: Sequence[str]) -> None:
        parse_parser.add_argument('file',
                                  help='Specifies the file to read results 
from.',
                                  type=str, nargs='?', metavar='input_file')
+       add_completion_opts(parse_parser)
 
        cli_args = parser.parse_args(massage_argv(argv))
 
        if get_kernel_root_path():
                os.chdir(get_kernel_root_path())
 
+       if cli_args.list_cmds:
+               print(" ".join(subparser.choices.keys()))
+               return
+
+       if cli_args.list_opts:
+               target_parser = subparser.choices.get(cli_args.subcommand)
+               if not target_parser:
+                       target_parser = parser
+
+               # Accessing private attribute _option_string_actions to get
+               # the list of options. This is not a public API, but argparse
+               # does not provide a way to inspect options programmatically.
+               print(' '.join(target_parser._option_string_actions.keys()))
+               return
+
        subcomand_handler = subcommand_handlers_map.get(cli_args.subcommand, 
None)
 
        if subcomand_handler is None:
diff --git a/tools/testing/kunit/kunit_tool_test.py 
b/tools/testing/kunit/kunit_tool_test.py
index 
bbba921e0eacb18663abfcabb2bccf330d8666f5..a7f09a6c97a473ff85e087d17c2f5faf7755b994
 100755
--- a/tools/testing/kunit/kunit_tool_test.py
+++ b/tools/testing/kunit/kunit_tool_test.py
@@ -11,11 +11,13 @@ from unittest import mock
 
 import tempfile, shutil # Handling test_tmpdir
 
+import io
 import itertools
 import json
 import os
 import signal
 import subprocess
+import sys
 from typing import Iterable
 
 import kunit_config
@@ -855,5 +857,24 @@ class KUnitMainTest(unittest.TestCase):
                        mock.call(args=None, build_dir='.kunit', 
filter_glob='suite2.test1', filter='', filter_action=None, timeout=300),
                ])
 
+       @mock.patch.object(sys, 'stdout', new_callable=io.StringIO)
+       def test_list_cmds(self, mock_stdout):
+               kunit.main(['--list-cmds'])
+               output = mock_stdout.getvalue()
+               output_cmds = sorted(output.split())
+               expected_cmds = sorted(['build', 'config', 'exec', 'parse', 
'run'])
+               self.assertEqual(output_cmds, expected_cmds)
+
+       @mock.patch.object(sys, 'stdout', new_callable=io.StringIO)
+       def test_run_list_opts(self, mock_stdout):
+               kunit.main(['run', '--list-opts'])
+               output = mock_stdout.getvalue()
+               output_cmds = set(output.split())
+               self.assertIn('--help', output_cmds)
+               self.assertIn('--kunitconfig', output_cmds)
+               self.assertIn('--jobs', output_cmds)
+               self.assertIn('--kernel_args', output_cmds)
+               self.assertIn('--raw_output', output_cmds)
+
 if __name__ == '__main__':
        unittest.main()

---
base-commit: b71e635feefc852405b14620a7fc58c4c80c0f73
change-id: 20260114-kunit-completion-265889f59c52

Best regards,
-- 
Ryota Sakamoto <[email protected]>


Reply via email to