Skip to content

Commit 25416a4

Browse files
Copilotigaw
authored andcommitted
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 25416a4

2 files changed

Lines changed: 129 additions & 19 deletions

File tree

tests/meson.build

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ infra = [
66
'nvme_test_io.py',
77
'nvme_test_logger.py',
88
'nvme_simple_template_test.py',
9+
'tap_runner.py',
910
]
1011

1112
tests = [
@@ -31,29 +32,26 @@ tests = [
3132
'nvme_ctrl_reset_test.py',
3233
]
3334

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-
5335
python_module = import('python')
5436

5537
python = python_module.find_installation('python3')
5638

39+
foreach t : tests
40+
t_name = t.split('.')[0]
41+
test(
42+
'nvme-cli - @0@'.format(t_name),
43+
python,
44+
args: [
45+
meson.current_source_dir() / 'tap_runner.py',
46+
'--start-dir', meson.current_source_dir(),
47+
t_name,
48+
],
49+
env: ['PATH=' + meson.project_build_root() + ':/usr/bin:/usr/sbin'],
50+
timeout: 500,
51+
protocol: 'tap',
52+
)
53+
endforeach
54+
5755
mypy = find_program(
5856
'mypy',
5957
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)