From c7229f149aa4e13557bf80d00317bff957e90a9f Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Mon, 8 Dec 2025 15:09:59 +0100 Subject: [PATCH 1/7] tests: don't enforce NS management support for local testing The tests framework added supported and dependency on NS management for clean setups. Though this prevents to run part of the tests suites on devices which do no support NS management. Add feature detection and skip the setup/tear down code for the NS setup if the device doesn't support this. Signed-off-by: Daniel Wagner --- tests/nvme_test.py | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/tests/nvme_test.py b/tests/nvme_test.py index 0c66be88a8..b34d720243 100644 --- a/tests/nvme_test.py +++ b/tests/nvme_test.py @@ -43,7 +43,12 @@ def to_decimal(value): - Returns: - Decimal integer """ - return int(str(value), 0) + val = 0 + try: + val = int(str(value), 0) + except (TypeError, ValueError): + raise ValueError(f"Invalid value: {value!r}") + return val class TestNVMe(unittest.TestCase): @@ -77,14 +82,17 @@ def setUp(self): self.load_config() if self.do_validate_pci_device: self.validate_pci_device() - self.create_and_attach_default_ns() + self.ns_mgmt_supported = self.get_ns_mgmt_support() + if self.ns_mgmt_supported: + self.create_and_attach_default_ns() print(f"\nsetup: ctrl: {self.ctrl}, ns1: {self.ns1}, default_nsid: {self.default_nsid}, flbas: {self.flbas}\n") def tearDown(self): """ Post Section for TestNVMe. """ if self.clear_log_dir is True: shutil.rmtree(self.log_dir, ignore_errors=True) - self.create_and_attach_default_ns() + if self.ns_mgmt_supported: + self.create_and_attach_default_ns() print(f"\nteardown: ctrl: {self.ctrl}, ns1: {self.ns1}, default_nsid: {self.default_nsid}, flbas: {self.flbas}\n") @classmethod @@ -209,6 +217,30 @@ def get_ctrl_id(self): "ERROR : nvme list-ctrl could not find ctrl") return str(json_output['ctrl_list'][0]['ctrl_id']) + def get_ns_mgmt_support(self): + """ + Determine whether Namespace Management and Namespace Attachment + operations are supported by the controller. + + This method reads the Optional Admin Command Support (OACS) field + from the Identify Controller data structure and evaluates specific + bits that indicate support for: + - Namespace Management (bit 3) + - Namespace Attachment (bit 4) + + Both features must be supported for this function to return True. + + Returns: + bool: True if both Namespace Management and Namespace Attachment + are supported, False otherwise. + """ + oacs = to_decimal(self.get_id_ctrl_field_value("oacs")) + + ns_mgmt_supported = bool(oacs & (1 << 3)) + ns_attach_supported = bool(oacs & (1 << 4)) + + return ns_mgmt_supported and ns_attach_supported + def get_nsid_list(self): """ Wrapper for extracting the namespace list. - Args: From 108eb52276a1fb68482a13b029243ab382fbe364 Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Thu, 2 Apr 2026 09:56:22 +0200 Subject: [PATCH 2/7] tests: skip tests which rely on namespace management These tests will fail if the controller does not support namespace management, thus skip them. Signed-off-by: Daniel Wagner --- tests/nvme_attach_detach_ns_test.py | 4 ++++ tests/nvme_create_max_ns_test.py | 4 ++++ tests/nvme_format_test.py | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/tests/nvme_attach_detach_ns_test.py b/tests/nvme_attach_detach_ns_test.py index f23630087a..afb1eafe20 100644 --- a/tests/nvme_attach_detach_ns_test.py +++ b/tests/nvme_attach_detach_ns_test.py @@ -48,6 +48,10 @@ class TestNVMeAttachDetachNSCmd(TestNVMe): def setUp(self): """ Pre Section for TestNVMeAttachDetachNSCmd """ super().setUp() + + if not self.ns_mgmt_supported: + self.skipTest("Namespace Management / Attach not supported by controller") + self.dps = 0 self.flbas = 0 (ds, ms) = self.get_lba_format_size() diff --git a/tests/nvme_create_max_ns_test.py b/tests/nvme_create_max_ns_test.py index 41e7a8d3c8..d8620a78b1 100644 --- a/tests/nvme_create_max_ns_test.py +++ b/tests/nvme_create_max_ns_test.py @@ -48,6 +48,10 @@ class TestNVMeCreateMaxNS(TestNVMe): def setUp(self): """ Pre Section for TestNVMeAttachDetachNSCmd """ super().setUp() + + if not self.ns_mgmt_supported: + self.skipTest("Namespace Management / Attach not supported by controller") + self.dps = 0 self.flbas = 0 (ds, ms) = self.get_lba_format_size() diff --git a/tests/nvme_format_test.py b/tests/nvme_format_test.py index 68445ac843..7cdd72df94 100644 --- a/tests/nvme_format_test.py +++ b/tests/nvme_format_test.py @@ -62,6 +62,10 @@ class TestNVMeFormatCmd(TestNVMe): def setUp(self): """ Pre Section for TestNVMeFormatCmd """ super().setUp() + + if not self.ns_mgmt_supported: + self.skipTest("Namespace Management / Attach not supported by controller") + self.dps = 0 self.flbas = 0 # Assuming run_ns_io with 4KiB * 10 writes. From ff6be2f33892862cd2bef0bae4fe494cf1b78577 Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Thu, 2 Apr 2026 08:48:12 +0000 Subject: [PATCH 3/7] tests: add native TAP protocol support The nose2 framework is not really adding any value to the whole tests setup. It even makes it hard to pass on information to Meson's test framework. Thus replace it with our own simple wrapper which speaks TAP. This reduces the number of build/test dependencies and it's not necessary to copy the tests around anymore to get them working. Signed-off-by: Daniel Wagner --- tests/meson.build | 41 ++++++++-------- tests/tap_runner.py | 112 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 19 deletions(-) create mode 100644 tests/tap_runner.py diff --git a/tests/meson.build b/tests/meson.build index c9a5d0fad6..1b1a54b0ba 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1,4 +1,9 @@ # SPDX-License-Identifier: GPL-2.0-or-later +# +# This file is part of nvme. +# Copyright (c) 2026 SUSE LLC +# +# Authors: Daniel Wagner infra = [ 'config.json', @@ -6,6 +11,7 @@ infra = [ 'nvme_test_io.py', 'nvme_test_logger.py', 'nvme_simple_template_test.py', + 'tap_runner.py', ] tests = [ @@ -31,29 +37,26 @@ tests = [ 'nvme_ctrl_reset_test.py', ] -runtests = find_program('nose2', required : false) - -if runtests.found() - foreach file : infra + tests - configure_file(input: file, output: file, copy: true) - endforeach - - foreach t : tests - t_name = t.split('.')[0] - test( - 'nvme-cli - @0@'.format(t_name), - runtests, - args: ['--verbose', '--start-dir', meson.current_build_dir(), t_name], - env: ['PATH=' + meson.project_build_root() + ':/usr/bin:/usr/sbin'], - timeout: 500, - ) - endforeach -endif - python_module = import('python') python = python_module.find_installation('python3') +foreach t : tests + t_name = t.split('.')[0] + test( + 'nvme-cli - @0@'.format(t_name), + python, + args: [ + meson.current_source_dir() / 'tap_runner.py', + '--start-dir', meson.current_source_dir(), + t_name, + ], + env: ['PATH=' + meson.project_build_root() + ':/usr/bin:/usr/sbin'], + timeout: 500, + protocol: 'tap', + ) +endforeach + mypy = find_program( 'mypy', required : false, diff --git a/tests/tap_runner.py b/tests/tap_runner.py new file mode 100644 index 0000000000..d6a304da52 --- /dev/null +++ b/tests/tap_runner.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# +# This file is part of nvme. +# Copyright (c) 2026 SUSE LLC +# +# Authors: Daniel Wagner +""" +TAP (Test Anything Protocol) version 13 runner for nvme-cli Python tests. + +Wraps Python's unittest framework and emits TAP output so that meson can +parse individual subtest results when protocol: 'tap' is set in meson.build. +""" + +import argparse +import importlib +import sys +import traceback +import unittest + + +class TAPTestResult(unittest.TestResult): + """Collect unittest results and render them as TAP version 13.""" + + def __init__(self) -> None: + super().__init__() + self._test_count = 0 + self._lines: list[str] = [] + + def _description(self, test: unittest.TestCase) -> str: + return '{} ({})'.format(test._testMethodName, type(test).__name__) + + def addSuccess(self, test: unittest.TestCase) -> None: + super().addSuccess(test) + self._test_count += 1 + self._lines.append('ok {} - {}\n'.format( + self._test_count, self._description(test))) + + def addError(self, test: unittest.TestCase, err: object) -> None: + super().addError(test, err) + self._test_count += 1 + self._lines.append('not ok {} - {}\n'.format( + self._test_count, self._description(test))) + for line in traceback.format_exception(*err): # type: ignore[misc] + for subline in line.splitlines(): + self._lines.append('# {}\n'.format(subline)) + + def addFailure(self, test: unittest.TestCase, err: object) -> None: + super().addFailure(test, err) + self._test_count += 1 + self._lines.append('not ok {} - {}\n'.format( + self._test_count, self._description(test))) + for line in traceback.format_exception(*err): # type: ignore[misc] + for subline in line.splitlines(): + self._lines.append('# {}\n'.format(subline)) + + def addSkip(self, test: unittest.TestCase, reason: str) -> None: + super().addSkip(test, reason) + self._test_count += 1 + self._lines.append('ok {} - {} # SKIP {}\n'.format( + self._test_count, self._description(test), reason)) + + def addExpectedFailure(self, test: unittest.TestCase, err: object) -> None: + super().addExpectedFailure(test, err) + self._test_count += 1 + self._lines.append('ok {} - {} # TODO expected failure\n'.format( + self._test_count, self._description(test))) + + def addUnexpectedSuccess(self, test: unittest.TestCase) -> None: + super().addUnexpectedSuccess(test) + self._test_count += 1 + self._lines.append('not ok {} - {} # TODO unexpected success\n'.format( + self._test_count, self._description(test))) + + def print_tap(self, stream: object = sys.stdout) -> None: + stream.write('TAP version 13\n') # type: ignore[union-attr] + stream.write('1..{}\n'.format(self._test_count)) # type: ignore[union-attr] + for line in self._lines: + stream.write(line) # type: ignore[union-attr] + stream.flush() # type: ignore[union-attr] + + +def run_tests(test_module_name: str, start_dir: str | None = None) -> bool: + if start_dir: + sys.path.insert(0, start_dir) + + module = importlib.import_module(test_module_name) + + loader = unittest.TestLoader() + suite = loader.loadTestsFromModule(module) + + result = TAPTestResult() + suite.run(result) + result.print_tap() + return result.wasSuccessful() + + +def main() -> None: + parser = argparse.ArgumentParser( + description='TAP test runner for nvme-cli tests') + parser.add_argument('test_module', help='Test module name to run') + parser.add_argument('--start-dir', + help='Directory to prepend to sys.path for imports', + default=None) + args = parser.parse_args() + + success = run_tests(args.test_module, args.start_dir) + sys.exit(0 if success else 1) + + +if __name__ == '__main__': + main() From f2f19a9e6bde7acc17352407b872e87a2b09ac5a Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Thu, 2 Apr 2026 09:08:17 +0000 Subject: [PATCH 4/7] tests: ensure TAP output and test output separate The tests print diagnostic output which mingles with TAP. Thus redirect/buffer the output from the tests and sync the output with TAP outputs. As result, Meson's test framework understands the status report from the tests. Signed-off-by: Daniel Wagner --- tests/tap_runner.py | 51 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/tests/tap_runner.py b/tests/tap_runner.py index d6a304da52..e04ac9374a 100644 --- a/tests/tap_runner.py +++ b/tests/tap_runner.py @@ -14,11 +14,34 @@ import argparse import importlib +import io import sys import traceback import unittest +class DiagnosticCapture(io.TextIOBase): + """Capture writes and re-emit them as TAP diagnostic lines (# ...).""" + + def __init__(self, real_stdout: io.TextIOBase) -> None: + self._real = real_stdout + self._buf = '' + + def write(self, text: str) -> int: + self._buf += text + while '\n' in self._buf: + line, self._buf = self._buf.split('\n', 1) + self._real.write('# {}\n'.format(line)) + self._real.flush() + return len(text) + + def flush(self) -> None: + if self._buf: + self._real.write('# {}\n'.format(self._buf)) + self._buf = '' + self._real.flush() + + class TAPTestResult(unittest.TestResult): """Collect unittest results and render them as TAP version 13.""" @@ -72,12 +95,11 @@ def addUnexpectedSuccess(self, test: unittest.TestCase) -> None: self._lines.append('not ok {} - {} # TODO unexpected success\n'.format( self._test_count, self._description(test))) - def print_tap(self, stream: object = sys.stdout) -> None: - stream.write('TAP version 13\n') # type: ignore[union-attr] - stream.write('1..{}\n'.format(self._test_count)) # type: ignore[union-attr] + def print_tap(self, stream: io.TextIOBase) -> None: + stream.write('1..{}\n'.format(self._test_count)) for line in self._lines: - stream.write(line) # type: ignore[union-attr] - stream.flush() # type: ignore[union-attr] + stream.write(line) + stream.flush() def run_tests(test_module_name: str, start_dir: str | None = None) -> bool: @@ -89,9 +111,22 @@ def run_tests(test_module_name: str, start_dir: str | None = None) -> bool: loader = unittest.TestLoader() suite = loader.loadTestsFromModule(module) - result = TAPTestResult() - suite.run(result) - result.print_tap() + real_stdout = sys.stdout + # TAP version header must be the very first line on stdout. + real_stdout.write('TAP version 13\n') + real_stdout.flush() + + # Redirect stdout so any print() calls from setUp/tearDown/tests are + # re-emitted as TAP diagnostic lines and do not break the TAP stream. + sys.stdout = DiagnosticCapture(real_stdout) # type: ignore[assignment] + try: + result = TAPTestResult() + suite.run(result) + finally: + sys.stdout.flush() + sys.stdout = real_stdout + + result.print_tap(real_stdout) return result.wasSuccessful() From 60bd1b89f41bb1c44e5a25f1c8960142b2843969 Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Thu, 2 Apr 2026 11:26:17 +0200 Subject: [PATCH 5/7] tests: update documentation The nose2 framework has been replaced with a simple TAP wrapper. Update the documentation accordingly. Signed-off-by: Daniel Wagner --- tests/README | 67 +++++++--------------------------------------------- tests/TODO | 16 ------------- 2 files changed, 9 insertions(+), 74 deletions(-) delete mode 100644 tests/TODO diff --git a/tests/README b/tests/README index ad3a98d36e..1ca4bd6db3 100644 --- a/tests/README +++ b/tests/README @@ -16,59 +16,10 @@ nvmetests DO NOT RUN THEM IF YOU DO NOT KNOW WHAT YOU ARE DOING! You have been warned. - -1. Common Package Dependencies ------------------------------- - - 1. Python(>= 3.3) - 2. nose2 (Installation guide http://nose2.readthedocs.io/) - 3. flake8 (https://pypi.python.org/pypi/flake8) - 4. mypy (https://pypi.org/project/mypy/) - 5. autopep8 (https://pypi.org/project/autopep8/) - 6. isort (https://pypi.org/project/isort/) - - Python package management system pip can be used to install most of the - listed packages(https://pip.pypa.io/en/stable/installing/) :- - $ pip install nose2 flake8 mypy autopep8 isort - -2. Overview ------------ - - This framework follows simple class hierarchy. Each test file contains - one test. Each test is direct subclass or indirect subclass of TestNVMe - class which represents one testcase. To write a new testcase one can copy - existing template "nvme_simple_template_test.py" and start adding new - testcase specific functionality. For detailed information please look into - section 3. - - For more information about tests, class hierarchy and code please refer :- - - 1. Documentation :- html/ - 2. Class Index :- html/index.html - 3. Class Hierarchy :- html/class-tree.html - - For each testcase it will create log directory mentioned in - configuration file. This directory will be used for a temporary files - and storing execution logs of each testcases. Current implementation stores - stdout and stderr for each testcase under log directory, e.g. :- - - $ tree nvmetests/ - nvmetests/ - ├── TestNVMeAttachDetachNSCmd - │   ├── stderr.log - │   └── stdout.log - ├── TestNVMeFlushCmd - │   ├── stderr.log - │   └── stdout.log - └── TestNVMeFormatCmd - ├── stderr.log - └── stdout.log - . - . . -3. Walk-Through Example for writing a new testcase --------------------------------------------------- +Walk-Through Example for writing a new testcase +----------------------------------------------- 1. Copy simple test template file from current directory with appropriate name, replace "simple_template" with testcase name in new file name. Update config.json if necessary. @@ -90,11 +41,11 @@ nvmetests - Example "$ ninja -C .build format-python" will run autopep8 and isort on all the python files in the current directory. -4. Running testcases with framework ------------------------------------ - 1. Running single testcase (in the source tree) with nose2 :- - $ nose2 --verbose --start-dir tests nvme_writezeros_test - $ nose2 --verbose --start-dir tests nvme_read_write_test +Running testcases with framework +-------------------------------- + 1. Running single testcase :- + $ python3 tests/tap_runner.py --start-dir tests nvme_id_ctrl_test + $ meson test -C .build 'nvme-cli - nvme_id_ctrl_test' - 2. Running all the testcases (in the build root directory) with ninja :- - $ ninja test -C .build + 2. Running all the testcases (in the build root directory) :- + $ meson test -C .build diff --git a/tests/TODO b/tests/TODO deleted file mode 100644 index 265ba97751..0000000000 --- a/tests/TODO +++ /dev/null @@ -1,16 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later - -nvmetests TODO List -=================== - -Feature list (with priority):- ------------------------------- - 1. PRE and POST section improvements :- - a. Add functionality to load and unload driver. - b. Make sure default namespace is present, if not create one before - any test begins. Read the default namespace size from config file. - 2. Add system statistics collection in PRE and POST section of testcase. - 3. Create execution summary file under log directory at the end of each - run. - 4. Add tracing functionality to track overall and current progress of the - testcase. From 89931b806dd6505f677040b0904cfe99cd6fa5eb Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Thu, 2 Apr 2026 11:42:50 +0200 Subject: [PATCH 6/7] CI: update to nightly tests to use meson test framework The nose2 framework has been replaced with Meson's native one. Update the nightly test accordingly. Signed-off-by: Daniel Wagner --- .github/workflows/run-nightly-tests.yml | 25 ++----------------------- scripts/build.sh | 9 +++++++++ 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/.github/workflows/run-nightly-tests.yml b/.github/workflows/run-nightly-tests.yml index fe3567c91e..0c47a178e1 100644 --- a/.github/workflows/run-nightly-tests.yml +++ b/.github/workflows/run-nightly-tests.yml @@ -95,12 +95,11 @@ jobs: set -e set -x uname -a - PIPX_BIN_DIR=/usr/local/bin pipx install nose2 --force git clone https://github.com/${{ github.repository }} /nvme-cli git config --global --add safe.directory /nvme-cli cd /nvme-cli git checkout ${{ github.sha }} - scripts/build.sh -b release -c gcc + scripts/build.sh -b release -c gcc tests CONTROLLER=$(echo "${BDEV0}" | sed 's/n[0-9]*$//') cat > tests/config.json << EOJ { @@ -112,27 +111,7 @@ jobs: EOJ cat tests/config.json - nose2 --verbose --start-dir tests \ - nvme_attach_detach_ns_test \ - nvme_compare_test \ - nvme_copy_test \ - nvme_create_max_ns_test \ - nvme_ctrl_reset_test \ - nvme_dsm_test \ - nvme_error_log_test \ - nvme_flush_test \ - nvme_format_test \ - nvme_fw_log_test \ - nvme_get_features_test \ - nvme_get_lba_status_test \ - nvme_id_ctrl_test \ - nvme_id_ns_test \ - nvme_lba_status_log_test \ - nvme_read_write_test \ - nvme_smart_log_test \ - nvme_verify_test \ - nvme_writeuncor_test \ - nvme_writezeros_test + meson tests -C .build-ci EOF sudo chmod +x test.sh diff --git a/scripts/build.sh b/scripts/build.sh index 06f1cfcfaa..5b06f68c37 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -37,6 +37,7 @@ usage() { echo " static build a static binary" echo " minimal_static build a static binary without fabrics support" echo " libnvme build only libnvme" + echo " tests build for nightly build" echo "" echo "configs with muon:" echo " [default] minimal static build" @@ -241,6 +242,14 @@ config_meson_minimal_static() { "${BUILDDIR}" } +config_meson_tests() { + CC="${CC}" "${MESON}" setup \ + --werror \ + --buildtype="${BUILDTYPE}" \ + -Dnvme-tests=true \ + "${BUILDDIR}" +} + config_meson_libnvme() { CC="${CC}" "${MESON}" setup \ --werror \ From f905cc2e3943521097d045d0a4c30cb40ac4c211 Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Thu, 2 Apr 2026 11:44:10 +0200 Subject: [PATCH 7/7] build: honor the users input for libdbus option Don't enforce hard the dependency on libdbus. Signed-off-by: Daniel Wagner --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index cbae578d25..9ae1f25546 100644 --- a/meson.build +++ b/meson.build @@ -339,7 +339,7 @@ else # Check for libdbus availability. Optional, only required for MCTP dbus scan libdbus_dep = dependency( 'dbus-1', - required: true, + required: get_option('libdbus'), fallback: ['dbus', 'libdbus_dep'], default_options: [ 'default_library=static',