Skip to content

Commit ff6be2f

Browse files
committed
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 <[email protected]>
1 parent 108eb52 commit ff6be2f

2 files changed

Lines changed: 134 additions & 19 deletions

File tree

tests/meson.build

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
# SPDX-License-Identifier: GPL-2.0-or-later
2+
#
3+
# This file is part of nvme.
4+
# Copyright (c) 2026 SUSE LLC
5+
#
6+
# Authors: Daniel Wagner <[email protected]>
27

38
infra = [
49
'config.json',
510
'nvme_test.py',
611
'nvme_test_io.py',
712
'nvme_test_logger.py',
813
'nvme_simple_template_test.py',
14+
'tap_runner.py',
915
]
1016

1117
tests = [
@@ -31,29 +37,26 @@ tests = [
3137
'nvme_ctrl_reset_test.py',
3238
]
3339

34-
runtests = find_program('nose2', required : false)
35-
36-
if runtests.found()
37-
foreach file : infra + tests
38-
configure_file(input: file, output: file, copy: true)
39-
endforeach
40-
41-
foreach t : tests
42-
t_name = t.split('.')[0]
43-
test(
44-
'nvme-cli - @0@'.format(t_name),
45-
runtests,
46-
args: ['--verbose', '--start-dir', meson.current_build_dir(), t_name],
47-
env: ['PATH=' + meson.project_build_root() + ':/usr/bin:/usr/sbin'],
48-
timeout: 500,
49-
)
50-
endforeach
51-
endif
52-
5340
python_module = import('python')
5441

5542
python = python_module.find_installation('python3')
5643

44+
foreach t : tests
45+
t_name = t.split('.')[0]
46+
test(
47+
'nvme-cli - @0@'.format(t_name),
48+
python,
49+
args: [
50+
meson.current_source_dir() / 'tap_runner.py',
51+
'--start-dir', meson.current_source_dir(),
52+
t_name,
53+
],
54+
env: ['PATH=' + meson.project_build_root() + ':/usr/bin:/usr/sbin'],
55+
timeout: 500,
56+
protocol: 'tap',
57+
)
58+
endforeach
59+
5760
mypy = find_program(
5861
'mypy',
5962
required : false,

tests/tap_runner.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/usr/bin/env python3
2+
# SPDX-License-Identifier: GPL-2.0-or-later
3+
#
4+
# This file is part of nvme.
5+
# Copyright (c) 2026 SUSE LLC
6+
#
7+
# Authors: Daniel Wagner <[email protected]>
8+
"""
9+
TAP (Test Anything Protocol) version 13 runner for nvme-cli Python tests.
10+
11+
Wraps Python's unittest framework and emits TAP output so that meson can
12+
parse individual subtest results when protocol: 'tap' is set in meson.build.
13+
"""
14+
15+
import argparse
16+
import importlib
17+
import sys
18+
import traceback
19+
import unittest
20+
21+
22+
class TAPTestResult(unittest.TestResult):
23+
"""Collect unittest results and render them as TAP version 13."""
24+
25+
def __init__(self) -> None:
26+
super().__init__()
27+
self._test_count = 0
28+
self._lines: list[str] = []
29+
30+
def _description(self, test: unittest.TestCase) -> str:
31+
return '{} ({})'.format(test._testMethodName, type(test).__name__)
32+
33+
def addSuccess(self, test: unittest.TestCase) -> None:
34+
super().addSuccess(test)
35+
self._test_count += 1
36+
self._lines.append('ok {} - {}\n'.format(
37+
self._test_count, self._description(test)))
38+
39+
def addError(self, test: unittest.TestCase, err: object) -> None:
40+
super().addError(test, err)
41+
self._test_count += 1
42+
self._lines.append('not ok {} - {}\n'.format(
43+
self._test_count, self._description(test)))
44+
for line in traceback.format_exception(*err): # type: ignore[misc]
45+
for subline in line.splitlines():
46+
self._lines.append('# {}\n'.format(subline))
47+
48+
def addFailure(self, test: unittest.TestCase, err: object) -> None:
49+
super().addFailure(test, err)
50+
self._test_count += 1
51+
self._lines.append('not ok {} - {}\n'.format(
52+
self._test_count, self._description(test)))
53+
for line in traceback.format_exception(*err): # type: ignore[misc]
54+
for subline in line.splitlines():
55+
self._lines.append('# {}\n'.format(subline))
56+
57+
def addSkip(self, test: unittest.TestCase, reason: str) -> None:
58+
super().addSkip(test, reason)
59+
self._test_count += 1
60+
self._lines.append('ok {} - {} # SKIP {}\n'.format(
61+
self._test_count, self._description(test), reason))
62+
63+
def addExpectedFailure(self, test: unittest.TestCase, err: object) -> None:
64+
super().addExpectedFailure(test, err)
65+
self._test_count += 1
66+
self._lines.append('ok {} - {} # TODO expected failure\n'.format(
67+
self._test_count, self._description(test)))
68+
69+
def addUnexpectedSuccess(self, test: unittest.TestCase) -> None:
70+
super().addUnexpectedSuccess(test)
71+
self._test_count += 1
72+
self._lines.append('not ok {} - {} # TODO unexpected success\n'.format(
73+
self._test_count, self._description(test)))
74+
75+
def print_tap(self, stream: object = sys.stdout) -> None:
76+
stream.write('TAP version 13\n') # type: ignore[union-attr]
77+
stream.write('1..{}\n'.format(self._test_count)) # type: ignore[union-attr]
78+
for line in self._lines:
79+
stream.write(line) # type: ignore[union-attr]
80+
stream.flush() # type: ignore[union-attr]
81+
82+
83+
def run_tests(test_module_name: str, start_dir: str | None = None) -> bool:
84+
if start_dir:
85+
sys.path.insert(0, start_dir)
86+
87+
module = importlib.import_module(test_module_name)
88+
89+
loader = unittest.TestLoader()
90+
suite = loader.loadTestsFromModule(module)
91+
92+
result = TAPTestResult()
93+
suite.run(result)
94+
result.print_tap()
95+
return result.wasSuccessful()
96+
97+
98+
def main() -> None:
99+
parser = argparse.ArgumentParser(
100+
description='TAP test runner for nvme-cli tests')
101+
parser.add_argument('test_module', help='Test module name to run')
102+
parser.add_argument('--start-dir',
103+
help='Directory to prepend to sys.path for imports',
104+
default=None)
105+
args = parser.parse_args()
106+
107+
success = run_tests(args.test_module, args.start_dir)
108+
sys.exit(0 if success else 1)
109+
110+
111+
if __name__ == '__main__':
112+
main()

0 commit comments

Comments
 (0)