# SPDX-License-Identifier: GPL-2.0-only
"""Userspace runner checks. No test loads a module or opens /dev/kmsg."""
import contextlib
import errno
import io
import json
import os
import re
from pathlib import Path
import tempfile
import unittest
from unittest.mock import Mock, patch
import run
PASS = [' # Subtest: scst-LIB', ' 1..1',
' ok 1 scst_lun_flat_encoding', 'ok 7 - scst-LIB']
FAIL = [line.replace('ok ', 'not ok ') if 'ok ' in line else line for line in PASS]
SKIP = [line + ' # SKIP unavailable' if 'ok ' in line else line for line in PASS]
VDISK_PASS = [line.replace('scst-LIB', 'scst-VDISK-capacity') for line in PASS]
OOPS = ['BUG: kernel NULL pointer dereference, address: 0000000000000000',
'#PF: supervisor write access in kernel mode', 'PGD 0 P4D 0',
'Oops: Oops: 0002 [#1] SMP NOPTI', 'CPU: 0 PID: 1 Comm: kunit_try_catch',
'Tainted: [D]=DIE, [N]=TEST', 'RIP: 0010:test_case+0x10/0x20 [scst]',
'Call Trace:', ' ', ' kunit_try_run_case+0x10/0x20 [kunit]', ' ',
'---[ end trace 00000000 ]---', 'RIP: 0010:test_case+0x10/0x20 [scst]']
def document(lines):
"""Wrap a suite body in the envelope and numbering supplied by debugfs."""
return "\n".join(['KTAP version 1', '1..1',
*[re.sub(r'^(not ok|ok) \d+ ', r'\1 1 ', line) for line in lines]]) + "\n"
def evidence(lines, suite='scst-LIB'):
return {'messages': lines, 'suite_results': {suite: document(lines)}}
def analyse(lines, expected):
return run.analyse({expected[0]: document(lines)}, expected, lines)
def report():
return {'modules': [{'name': 'scst', 'suites': ['scst-LIB']}], 'module_state': 'not attempted',
'commands': [], 'errors': [], 'expected_suites': ['scst-LIB']}
class ParseTests(unittest.TestCase):
def test_module_output_and_statuses(self):
for lines, status in ((PASS, 'PASS'), (FAIL, 'FAIL'), (SKIP, 'SKIP')):
with self.subTest(status=status):
parsed, _ = analyse(lines, ['scst-LIB'])
self.assertEqual(parsed['test_status'], status)
self.assertFalse(parsed['parse_errors'])
def test_kernel_output_does_not_supply_test_results(self):
parsed, _ = run.analyse({}, ['scst-LIB'], PASS)
self.assertEqual(parsed['test_status'], 'ERROR')
self.assertEqual(parsed['counts']['passed'], 0)
parsed, _ = run.analyse({'scst-LIB': document(PASS)}, ['scst-LIB'],
['SCST initializing', *FAIL, 'SCST unloaded'])
self.assertEqual(parsed['test_status'], 'PASS')
self.assertEqual(parsed['counts']['passed'], 1)
def test_parameterized_case(self):
lines = [PASS[0], ' 1..1', ' KTAP version 1',
' # Subtest: boundary', ' 1..2',
' ok 1 zero', ' not ok 2 maximum',
' not ok 1 boundary', 'not ok 2 - scst-LIB']
parsed, _ = analyse(lines, ['scst-LIB'])
self.assertEqual(parsed['test_status'], 'FAIL')
self.assertEqual(parsed['counts']['failed'], 1)
def test_skip_reasons_are_preserved(self):
parsed, details = analyse(SKIP, ['scst-LIB'])
suite = parsed['suites'][0]
self.assertEqual(suite['skip_reason'], 'unavailable')
self.assertEqual(suite['children'][0]['skip_reason'], 'unavailable')
self.assertIn('unavailable', details)
def test_parser_output_is_local_to_each_run(self):
console = io.StringIO()
with contextlib.redirect_stdout(console):
_, first = analyse(PASS, ['scst-LIB'])
_, second = analyse(VDISK_PASS, ['scst-VDISK-capacity'])
self.assertEqual(console.getvalue(), '')
self.assertIn('scst_lun_flat_encoding', first)
self.assertNotIn('scst-VDISK-capacity', first)
self.assertIn('scst-VDISK-capacity', second)
def test_bad_evidence_cannot_pass(self):
cases = [[], PASS[:-1], PASS[:2] + PASS[3:], PASS + PASS,
[line.replace('1..1', '1..2') for line in PASS],
[line.replace('ok 1 scst_', 'ok 2 scst_') for line in PASS],
[PASS[0], ' 1..0', PASS[-1]],
[line.replace('scst-LIB', 'scst-unexpected') for line in PASS]]
for lines in cases:
with self.subTest(lines=lines):
parsed, _ = analyse(lines, ['scst-LIB'])
self.assertEqual(parsed['test_status'], 'ERROR')
def test_missing_compiled_suite(self):
parsed, _ = analyse(PASS, ['scst-LIB', 'scst-extra'])
self.assertEqual(parsed['test_status'], 'ERROR')
def test_trailing_results_and_foreign_suite_cannot_pass(self):
for extra in ([PASS[-1]], [' not ok 2 stray'], [' 1..99'],
[' # Subtest: foreign-suite'],
[line.replace('scst-LIB', 'foreign-suite') for line in PASS]):
with self.subTest(extra=extra):
parsed, _ = analyse(PASS + extra, ['scst-LIB'])
self.assertEqual(parsed['test_status'], 'ERROR')
def test_parameter_name_does_not_become_a_suite(self):
logs = [PASS[0], ' 1..1', ' # Subtest: scst-parameter',
' 1..1', ' ok 1 value', ' ok 1 scst-parameter', PASS[-1]]
parsed, _ = analyse(logs, ['scst-LIB'])
self.assertEqual(parsed['test_status'], 'PASS')
self.assertEqual(len(parsed['suites']), 1)
def test_separate_debugfs_documents(self):
parsed, _ = run.analyse({'scst-LIB': document(PASS),
'scst-VDISK-capacity': document(VDISK_PASS)},
['scst-LIB', 'scst-VDISK-capacity'])
self.assertEqual(parsed['test_status'], 'PASS')
self.assertEqual(parsed['counts']['passed'], 2)
def test_native_envelope_and_suite_identity_are_validated(self):
good = document(PASS)
for text in ('', 'garbage', good.replace('KTAP version 1', 'KTAP version 99'),
good.replace('1..1', '1..2', 1), good.replace('1..1', '1..0', 1),
good.replace('ok 1 - scst-LIB', 'ok 2 - scst-LIB'),
good + good, good + 'ok 2 stray\n', good + 'trailing garbage\n',
document(suite_log('scst-OTHER'))):
with self.subTest(text=text):
parsed, _ = run.analyse({'scst-LIB': text}, ['scst-LIB'])
self.assertEqual(parsed['test_status'], 'ERROR')
def test_entire_suite_skip_without_cases(self):
text = 'KTAP version 1\n1..1\nok 1 scst-LIB # SKIP unavailable\n'
parsed, _ = run.analyse({'scst-LIB': text}, ['scst-LIB'])
self.assertEqual(parsed['test_status'], 'SKIP')
self.assertEqual(parsed['suites'][0]['skip_reason'], 'unavailable')
def test_missing_debugfs_body_identifies_the_incomplete_suite(self):
parsed, _ = run.analyse({'scst-LIB': 'KTAP version 1\n1..1\n'}, ['scst-LIB'])
self.assertEqual(parsed['test_status'], 'ERROR')
self.assertEqual(parsed['suites'][0]['name'], 'scst-LIB')
def test_diagnostic_groups_include_trace_without_consuming_results(self):
warning = ['WARNING: CPU: 0 PID: 1 at example', 'CPU: 0 PID: 1 Comm: insmod',
'Call Trace:', ' ', ' example+0x1/0x2', ' ',
'---[ end trace 00000000 ]---']
parsed, _ = analyse(warning + PASS + ['unrelated', 'BUG: another failure'], ['scst-LIB'])
self.assertEqual(parsed['diagnostics'], ['\n'.join(warning), 'BUG: another failure'])
self.assertEqual(parsed['counts']['passed'], 1)
def test_oops_header_and_trace_form_one_event(self):
logs = FAIL[:2] + OOPS + [' # assertion detail'] + FAIL[2:]
parsed, _ = analyse(logs, ['scst-LIB'])
self.assertEqual(parsed['diagnostics'], ['\n'.join(OOPS)])
self.assertEqual(parsed['counts']['failed'], 1)
self.assertEqual(run.kernel_diagnostics(OOPS + OOPS), ['\n'.join(OOPS)] * 2)
def test_independent_oops_warning_and_bare_trace_are_not_merged(self):
first = ['WARNING: unrelated', 'Call Trace:', ' warning_frame']
oops = OOPS[3:]
logs = first + oops + PASS + ['Call Trace:', ' bare_frame']
self.assertEqual(run.kernel_diagnostics(logs),
['\n'.join(first), '\n'.join(oops), 'Call Trace:\n bare_frame'])
def test_crash_console_is_brief_and_artifacts_keep_full_evidence(self):
logs = FAIL[:2] + OOPS + [' # assertion detail'] + FAIL[2:]
for verbose in (False, True):
with self.subTest(verbose=verbose), tempfile.TemporaryDirectory() as tmp:
result = report() | {**evidence(logs), 'module_state': 'retained',
'started_suites': ['scst-LIB'],
'expected_suites': ['scst-LIB', 'scst-VDISK-capacity']}
console = io.StringIO()
with contextlib.redirect_stdout(console):
self.assertEqual(run.finish(result, Path(tmp), verbose), 1)
output = console.getvalue()
self.assertIn('Kernel: FAIL - 1 diagnostic event', output)
self.assertIn('1/2 started, 1 not run', output)
self.assertEqual('NOT_RUN: scst-VDISK-capacity' in output, verbose)
self.assertIn(' [FAIL] scst_lun_flat_encoding', output)
self.assertIn('assertion detail', output)
self.assertEqual('Call Trace:' in output, verbose)
self.assertFalse(result['parse_errors'])
for artifact in ('messages.log', 'report.txt', 'result.json'):
content = (Path(tmp) / artifact).read_text()
self.assertIn('test_case+0x10/0x20', content)
self.assertIn('assertion detail', content)
self.assertEqual(result['not_run_suites'], ['scst-VDISK-capacity'])
def test_repeated_diagnostic_headlines_are_summarized(self):
with tempfile.TemporaryDirectory() as tmp:
result = report() | {**evidence(OOPS + OOPS + PASS), 'module_state': 'retained'}
console = io.StringIO()
with contextlib.redirect_stdout(console):
self.assertEqual(run.finish(result, Path(tmp)), 1)
self.assertIn('Kernel: FAIL - 2 diagnostic events', console.getvalue())
self.assertIn(' ' + OOPS[0] + ' (2 events)', console.getvalue())
self.assertNotIn('Call Trace:', console.getvalue())
def test_unstarted_selection_cannot_pass_without_a_diagnostic(self):
with tempfile.TemporaryDirectory() as tmp, contextlib.redirect_stdout(io.StringIO()):
result = report() | {**evidence(PASS), 'module_state': 'unloaded',
'started_suites': ['scst-LIB'],
'expected_suites': ['scst-LIB', 'scst-VDISK-capacity']}
self.assertEqual(run.finish(result, Path(tmp)), 2)
self.assertIn('Execution stopped', result['errors'][0])
self.assertEqual(result['not_run_suites'], ['scst-VDISK-capacity'])
def test_preflight_console_is_compact_and_full_report_keeps_selection(self):
with tempfile.TemporaryDirectory() as tmp:
names = [f'scst-suite-{i}' for i in range(15)]
result = report() | {'expected_suites': names,
'errors': ['Cannot verify preloaded KUnit: enable is unreadable.\n'
'Module left unchanged. Leave KUnit unloaded before retrying.']}
console = io.StringIO()
with contextlib.redirect_stdout(console):
self.assertEqual(run.finish(result, Path(tmp)), 2)
output = console.getvalue()
self.assertIn('Tests: NOT_RUN (15 suites not run)', output)
self.assertNotIn('NOT_RUN: scst-suite-', output)
self.assertNotIn('0 passed', output)
self.assertLess(len(output.splitlines()), 10)
saved = (Path(tmp) / 'report.txt').read_text()
self.assertTrue(all('NOT_RUN: ' + name in saved for name in names))
def test_color_is_terminal_only_and_reports_remain_plain(self):
for tty, disabled in ((True, False), (False, False), (True, True)):
with self.subTest(tty=tty, disabled=disabled), tempfile.TemporaryDirectory() as tmp:
console = io.StringIO()
environment = {'TERM': 'xterm'} | ({'NO_COLOR': ''} if disabled else {})
with contextlib.redirect_stdout(console), patch.object(console, 'isatty', return_value=tty), \
patch.dict(os.environ, environment, clear=True):
sample = run.color_console('PASS FAIL NOT_RUN')
run.finish(report() | {**evidence(PASS), 'module_state': 'unloaded'}, Path(tmp))
colored = tty and not disabled
for color in (31, 32, 33):
self.assertEqual(f'\033[{color}m' in sample, colored)
self.assertEqual('\033[' in console.getvalue(), colored)
for name in ('messages.log', 'report.txt', 'result.json'):
self.assertNotIn('\033[', (Path(tmp) / name).read_text())
def test_console_modes_preserve_full_report(self):
for verbose in (False, True):
with self.subTest(verbose=verbose), tempfile.TemporaryDirectory() as tmp:
result = report() | {**evidence(PASS), 'module_state': 'unloaded'}
console = io.StringIO()
with contextlib.redirect_stdout(console):
self.assertEqual(run.finish(result, Path(tmp), verbose), 0)
self.assertEqual('scst_lun_flat_encoding' in console.getvalue(), verbose)
self.assertIn('scst_lun_flat_encoding', (Path(tmp) / 'report.txt').read_text())
self.assertIn('scst-LIB', console.getvalue())
def test_failed_case_is_visible_in_default_console_and_json(self):
logs = FAIL[:2] + [' # assertion detail'] + FAIL[2:]
with tempfile.TemporaryDirectory() as tmp:
result = report() | {**evidence(logs), 'module_state': 'unloaded'}
console = io.StringIO()
with contextlib.redirect_stdout(console):
self.assertEqual(run.finish(result, Path(tmp)), 1)
self.assertIn(' [FAIL] scst_lun_flat_encoding', console.getvalue())
self.assertIn('assertion detail', console.getvalue())
data = json.loads((Path(tmp) / 'result.json').read_text())
self.assertIn('assertion detail', '\n'.join(data['suites'][0]['children'][0]['log']))
def test_parameter_failure_details_are_visible_with_and_without_a_plan(self):
for planned in (False, True):
for verbose in (False, True):
with self.subTest(planned=planned, verbose=verbose), tempfile.TemporaryDirectory() as tmp:
logs = [PASS[0], ' 1..1', ' KTAP version 1',
' # Subtest: boundary']
if planned:
logs.append(' 1..3')
logs += [' # first parameter assertion detail', ' not ok 1 first',
' # second parameter assertion detail', ' not ok 2 second',
' ok 3 third', ' not ok 1 boundary', 'not ok 1 scst-LIB']
result = report() | {**evidence(logs), 'module_state': 'unloaded'}
with contextlib.redirect_stdout(io.StringIO()) as console:
self.assertEqual(run.finish(result, Path(tmp), verbose), 1)
# Incremental output and final reporting must not duplicate diagnostics.
run.show_suites(result, result)
output = console.getvalue()
for parameter in ('first', 'second'):
detail = f'{parameter} parameter assertion detail'
self.assertEqual(output.count(detail), 1)
self.assertIn(f'[FAIL] boundary/{parameter}', output)
self.assertIn(detail, (Path(tmp) / 'report.txt').read_text())
self.assertIn(detail, (Path(tmp) / 'result.json').read_text())
case = result['suites'][0]['children'][0]['children'][
0 if parameter == 'first' else 1]
self.assertIn(detail, '\n'.join(case['log']))
heading = f' [FAIL] boundary/{parameter}\n'
section = output.split(heading, 1)[1].split('\n\n', 1)[0]
self.assertIn(detail, section)
parent = result['suites'][0]['children'][0]
self.assertNotIn('first parameter assertion detail', '\n'.join(parent['log']))
self.assertEqual('[PASS] boundary/third' in output, verbose)
self.assertEqual(result['counts']['failed'], 2)
self.assertEqual(result['counts']['passed'], 1)
self.assertFalse(result['errors'])
def test_progress_precedes_grouped_failures_and_keeps_assertion_indentation(self):
logs = [PASS[0], ' 1..1', ' # Subtest: boundary',
' # first assertion', ' Expected value == 1, but',
' value == 0', ' 1 == 1', ' not ok 1 first',
' # second assertion', ' not ok 2 second',
' # boundary: pass:0 fail:2 skip:0 total:2', ' not ok 1 boundary',
' # module: scst', ' # Totals: pass:0 fail:2 skip:0 total:2',
'not ok 1 scst-LIB']
with tempfile.TemporaryDirectory() as tmp:
result = report() | {**evidence(logs),
'suite_results': {'scst-LIB': document(logs),
'scst-VDISK-capacity': document(VDISK_PASS)}, 'module_state': 'unloaded',
'expected_suites': ['scst-LIB', 'scst-VDISK-capacity']}
parsed, _ = analyse(logs, ['scst-LIB'])
with contextlib.redirect_stdout(io.StringIO()) as console:
run.show_suites(parsed, result)
self.assertEqual(console.getvalue(), '[FAIL] scst-LIB\n')
self.assertEqual(run.finish(result, Path(tmp)), 1)
output = console.getvalue()
self.assertEqual(output.count('[FAIL] scst-LIB'), 1)
self.assertLess(output.index('[PASS] scst-VDISK-capacity'), output.index('Failures:'))
self.assertIn('Failures:\n\nscst-LIB\n [FAIL] boundary/first\n', output)
self.assertIn(' Expected value == 1, but\n value == 0\n 1 == 1', output)
self.assertIn('\n\n [FAIL] boundary/second\n', output)
self.assertNotIn('[FAIL] boundary\n', output)
self.assertNotIn('# Totals:', output)
self.assertNotIn('# module:', output)
self.assertNotIn('pass:0 fail:2', output)
self.assertIn('# Totals:', (Path(tmp) / 'report.txt').read_text())
def test_mixed_printk_and_assertions_keep_consistent_console_indentation(self):
for verbose in (False, True):
for indent in ('', ' ', ' '):
with self.subTest(verbose=verbose, indent=indent), tempfile.TemporaryDirectory() as tmp:
diagnostic = ['[42]: scst: ***ERROR***: injected backend error',
indent + '# boundary: EXPECTATION FAILED at suite.c:42',
indent + 'Expected actual == wanted, but',
indent + ' actual == 0 (0x0)',
indent + ' wanted == 1 (0x1)',
'custom assertion message', ' continuation of the message',
indent + '# boundary: ASSERTION FAILED at suite.c:43',
indent + 'Expected rc == 0, but',
indent + ' rc == -1 (0xffffffffffffffff)']
logs = [PASS[0], ' 1..1', ' # Subtest: boundary',
*diagnostic, ' not ok 1 first',
*diagnostic, ' not ok 2 second',
' not ok 1 boundary', 'not ok 1 scst-LIB']
result = report() | {**evidence(logs), 'module_state': 'unloaded'}
with contextlib.redirect_stdout(io.StringIO()) as console:
self.assertEqual(run.finish(result, Path(tmp), verbose), 1)
output = console.getvalue()
expected = (' # boundary: EXPECTATION FAILED at suite.c:42\n'
' Expected actual == wanted, but\n'
' actual == 0 (0x0)\n'
' wanted == 1 (0x1)\n'
' custom assertion message\n'
' continuation of the message\n'
' # boundary: ASSERTION FAILED at suite.c:43\n'
' Expected rc == 0, but\n'
' rc == -1 (0xffffffffffffffff)')
for parameter in ('first', 'second'):
section = output.split(f' [FAIL] boundary/{parameter}\n', 1)[1]
self.assertIn(expected, section.split('\n\n', 1)[0])
self.assertEqual(result['counts']['failed'], 2)
saved = json.loads((Path(tmp) / 'result.json').read_text())
cases = saved['suites'][0]['children'][0]['children']
self.assertEqual([case['log'] for case in cases], [diagnostic, diagnostic])
self.assertEqual((Path(tmp) / 'messages.log').read_text(), '\n'.join(logs) + '\n')
def test_passing_first_parameter_logs_do_not_leak_into_failed_parent(self):
logs = [PASS[0], ' 1..1', ' # Subtest: boundary',
' # expected negative-path message', ' ok 1 first',
' # actual failure', ' not ok 2 second',
' # parent teardown detail', ' not ok 1 boundary', 'not ok 1 scst-LIB']
for verbose in (False, True):
with self.subTest(verbose=verbose), tempfile.TemporaryDirectory() as tmp:
result = report() | {**evidence(logs), 'module_state': 'unloaded'}
with contextlib.redirect_stdout(io.StringIO()) as console:
self.assertEqual(run.finish(result, Path(tmp), verbose), 1)
output = console.getvalue()
self.assertEqual('expected negative-path message' in output, verbose)
self.assertIn('parent teardown detail', output)
parent = result['suites'][0]['children'][0]
self.assertEqual(parent['log'], [' # parent teardown detail'])
self.assertEqual(parent['children'][0]['log'], [' # expected negative-path message'])
self.assertEqual(parent['children'][1]['log'], [' # actual failure'])
self.assertIn('expected negative-path message', (Path(tmp) / 'report.txt').read_text())
def test_parameter_plan_validation_is_not_changed_by_log_association(self):
for plan in (' 1..3', ' 1..1'):
logs = [PASS[0], ' 1..1', ' # Subtest: boundary', plan,
' # first failure', ' not ok 1 first',
' ok 2 second', ' not ok 1 boundary', 'not ok 1 scst-LIB']
parsed, _ = analyse(logs, ['scst-LIB'])
self.assertEqual(parsed['test_status'], 'ERROR')
self.assertTrue(parsed['parse_errors'])
def test_module_metadata_defaults_and_full_reports(self):
metadata = {'version': '3.11.0-pre', 'description': 'SCSI target core',
'author': 'SCST contributors', 'license': 'GPL', 'git_commit': 'a' * 40,
'build_date': '2026-10-13 12:00:00 UTC', 'build_number': '42',
'kver': '7.2.9', 'arch_type': 'x86_64', 'srcversion': 'ABC123',
'vermagic': '7.2.9 SMP preempt mod_unload'}
for verbose in (False, True):
for failed in (False, True):
with self.subTest(verbose=verbose, failed=failed), tempfile.TemporaryDirectory() as tmp:
result = report() | {**evidence(FAIL if failed else PASS), 'module_state': 'unloaded',
'kernel': '7.2.9-test', 'architecture': 'x86_64'}
result['modules'][0].update(metadata)
console = io.StringIO()
with contextlib.redirect_stdout(console):
self.assertEqual(run.finish(result, Path(tmp), verbose), int(failed))
output = console.getvalue()
saved = (Path(tmp) / 'report.txt').read_text()
data = json.loads((Path(tmp) / 'result.json').read_text())
self.assertEqual(output.count('Module: scst\n'), int(verbose))
self.assertIn('\n\n' + ('Module: scst' if verbose else 'SCST: 3.11.0-pre'), output)
last_field = 'vermagic' if verbose else 'arch_type'
self.assertIn(metadata[last_field] + '\n\nRuntime kernel:', output)
self.assertIn('Runtime kernel: 7.2.9-test (x86_64)', output)
self.assertIn(('Module scst: unknown' if verbose else
'Runtime kernel: 7.2.9-test (x86_64)') + '\n\nTests:', output)
for key, value in metadata.items():
labels = {'version': 'SCST', 'git_commit': 'Commit',
'build_number': 'Build number', 'build_date': 'Build date',
'kver': 'Built for kernel', 'arch_type': 'Architecture'}
if verbose:
self.assertIn(f' {key}: {value}', output)
elif key in labels:
self.assertIn(f'{labels[key]}: {value}', output)
else:
self.assertNotIn(f'{key}:', output)
self.assertIn(f' {key}: {value}', saved)
self.assertEqual(data['modules'][0][key], value)
def test_each_module_keeps_its_own_metadata_and_missing_fields_are_optional(self):
for verbose in (False, True):
with self.subTest(verbose=verbose), tempfile.TemporaryDirectory() as tmp:
result = report() | {**evidence(PASS), 'module_state': 'unloaded',
'modules': [{'name': 'scst', 'version': '3.11.0-pre',
'git_commit': 'CORE'},
{'name': 'scst_vdisk', 'version': '3.11.0-pre',
'srcversion': 'HANDLER', 'git_commit': ''}]}
with contextlib.redirect_stdout(io.StringIO()) as console:
self.assertEqual(run.finish(result, Path(tmp), verbose), 0)
output = console.getvalue()
self.assertEqual(output.count('version: 3.11.0-pre'), 2 if verbose else 0)
self.assertEqual('SCST: 3.11.0-pre' in output, not verbose)
self.assertEqual('Module: scst_vdisk' in output, verbose)
self.assertEqual('srcversion: HANDLER' in output, verbose)
self.assertIn('git_commit: CORE' if verbose else 'Commit: CORE', output)
self.assertNotIn('build_date:', output)
if verbose:
self.assertIn('git_commit: CORE\n\nModule: scst_vdisk', output)
else:
self.assertIn('Commit: CORE\n\nTests:', output)
saved = (Path(tmp) / 'report.txt').read_text()
core, handler = saved.split('Module: scst_vdisk', 1)
self.assertIn('git_commit: CORE', core)
self.assertIn('srcversion: HANDLER', handler)
self.assertNotIn('git_commit:', handler)
data = json.loads((Path(tmp) / 'result.json').read_text())
self.assertEqual(data['modules'][1]['srcversion'], 'HANDLER')
def test_verbose_shows_optional_case_logs_without_changing_results(self):
logs = PASS[:2] + [' # scst_lun_flat_encoding: encoded LUN = 0x5234'] + PASS[2:]
for verbose in (False, True):
with self.subTest(verbose=verbose), tempfile.TemporaryDirectory() as tmp:
console = io.StringIO()
with contextlib.redirect_stdout(console):
run.finish(report() | {**evidence(logs), 'module_state': 'unloaded'}, Path(tmp), verbose)
self.assertEqual('encoded LUN' in console.getvalue(), verbose)
self.assertIn('encoded LUN', (Path(tmp) / 'report.txt').read_text())
self.assertIn('Tests: PASS', console.getvalue())
def test_kernel_diagnostics(self):
for message in ('WARNING: at scst', 'BUG: KASAN: use-after-free',
'watchdog: BUG: soft lockup', 'rcu: detected stalls',
'INFO: task insmod blocked for more than 120 seconds'):
with self.subTest(message=message):
parsed, _ = analyse(PASS + [message], ['scst-LIB'])
self.assertEqual(parsed['diagnostics'], [message])
def test_reports_and_exit_codes(self):
for lines, state, errors, expected in ((PASS, 'unloaded', [], 0),
(FAIL, 'unloaded', [], 1), (SKIP, 'unloaded', [], 0),
(PASS[:-1], 'retained', [], 2),
(PASS, 'loading', ['timeout'], 2),
(PASS + ['# SCST_KUNIT_RETAINED: worker'], 'retained', [], 2),
(PASS + ['WARNING: test'], 'retained', [], 1),
(PASS + ['BUG: unload'], 'unloaded', [], 1)):
with self.subTest(expected=expected), tempfile.TemporaryDirectory() as tmp:
result = report() | {**evidence(lines), 'module_state': state, 'errors': errors}
if lines[-1].startswith(('WARNING:', 'BUG:')):
result['suite_results'] = {'scst-LIB': document(PASS)}
with contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(run.finish(result, Path(tmp)), expected)
data = json.loads((Path(tmp) / 'result.json').read_text())
self.assertNotIn('messages', data)
self.assertTrue((Path(tmp) / 'report.txt').is_file())
def test_preflight_failure_reports_not_run_without_missing_results(self):
with tempfile.TemporaryDirectory() as tmp, contextlib.redirect_stdout(io.StringIO()):
result = report() | {'errors': ['KUnit preparation failed']}
self.assertEqual(run.finish(result, Path(tmp)), 2)
self.assertEqual(result['test_status'], 'NOT_RUN')
self.assertEqual(result['errors'], ['KUnit preparation failed'])
self.assertFalse(result['parse_errors'])
self.assertEqual(result['expected_suites'], ['scst-LIB'])
class DebugfsTests(unittest.TestCase):
def test_trigger_then_read_preserves_exact_document(self):
with tempfile.TemporaryDirectory() as tmp:
directory = Path(tmp)
(directory / 'ktap').mkdir()
suite = directory / 'scst-LIB'
suite.mkdir()
trigger = suite / 'run'
trigger.touch()
text = document(FAIL)
(suite / 'results').write_text(text)
capture = Mock()
capture.drain.return_value = False
result = report()
self.assertEqual(run.collect_suite(trigger, capture, directory, 10, result), 0)
self.assertEqual(trigger.read_text(), 'run\n')
self.assertEqual((directory / 'ktap/scst-LIB.ktap').read_text(), text)
self.assertEqual(result['suite_results'], {'scst-LIB': text})
self.assertEqual(len(result['commands']), 1)
def test_read_error_is_recorded_without_using_kernel_results(self):
with tempfile.TemporaryDirectory() as tmp:
directory = Path(tmp)
(directory / 'ktap').mkdir()
suite = directory / 'scst-LIB'
suite.mkdir()
trigger = suite / 'run'
trigger.touch()
capture = Mock(messages=PASS)
capture.drain.return_value = False
result = report()
self.assertNotEqual(run.collect_suite(trigger, capture, directory, 10, result), 0)
self.assertIn('FileNotFoundError', result['commands'][0]['output'])
self.assertNotIn('suite_results', result)
def test_result_read_shares_the_command_timeout(self):
child = Mock(pid=42)
child.poll.return_value = None
capture = Mock()
capture.drain.return_value = False
with tempfile.TemporaryDirectory() as tmp, \
patch.object(run.subprocess, 'Popen', return_value=child), \
patch.object(run.time, 'monotonic', side_effect=[0, 11]):
result = report()
with self.assertRaisesRegex(run.RunError, 'PID 42'):
run.collect_suite(Path(tmp) / 'scst-LIB/run', capture, Path(tmp), 10, result)
self.assertEqual(len(result['commands']), 1)
self.assertIn("with_name('results')", result['commands'][0]['argv'][2])
child.wait.assert_not_called()
def test_unexpected_execution_and_retained_work_are_rejected(self):
for lines in (PASS, ['KTAP version 1'], ['1..1'], ['ok 1 scst-LIB # SKIP reason']):
self.assertTrue(run.log_problems(lines))
for lines in (PASS + PASS, VDISK_PASS, ['ok 1 foreign'], ['# SCST_KUNIT_RETAINED: worker']):
self.assertTrue(run.log_problems(lines, 'scst-LIB'))
self.assertFalse(run.log_problems(PASS, 'scst-LIB'))
self.assertFalse(run.log_problems(['ordinary SCST message']))
class CaptureTests(unittest.TestCase):
def test_failed_command_keeps_bounded_stderr_without_prior_commands(self):
child = Mock(pid=42)
child.poll.return_value = 1
capture = Mock()
capture.drain.return_value = False
def start(argv, **kwargs):
kwargs['stdout'].write(b'x' * 5000 + b'\ninsmod: Unknown symbol dlm_new_lockspace\n')
return child
with tempfile.TemporaryDirectory() as tmp:
directory = Path(tmp)
(directory / 'commands.log').write_text('old command output\n')
result = report()
with patch.object(run.subprocess, 'Popen', side_effect=start):
self.assertEqual(run.command(['insmod', 'test.ko'], capture, directory, 10, result), 1)
error = str(run.command_error(result, 'insmod scst', 1))
self.assertIn('Unknown symbol dlm_new_lockspace', error)
self.assertNotIn('old command output', error)
self.assertLessEqual(len(result['commands'][0]['output']), 4096)
self.assertGreater((directory / 'commands.log').stat().st_size, 5000)
def capture(self):
capture = run.KernelLog.__new__(run.KernelLog)
capture.raw = io.BytesIO()
capture.sequence = None
capture.messages = []
capture.previous_kernel = False
capture.fd = 987
return capture
def test_sequence_and_raw_evidence(self):
capture = self.capture()
capture.accept(b'6,100,10,-;first\n')
capture.accept(b'6,101,11,-;second\n SUBSYSTEM=test\n')
self.assertEqual(capture.messages, ['first', 'second'])
with self.assertRaisesRegex(run.RunError, 'Lost kernel log records'):
capture.accept(b'6,103,12,-;after gap\n')
self.assertIn(b'after gap', capture.raw.getvalue())
def test_read_overrun(self):
with patch.object(run.os, 'read', side_effect=OSError(errno.EPIPE, 'overrun')):
with self.assertRaisesRegex(run.RunError, 'overrun'):
self.capture().drain()
def test_injected_results_are_raw_evidence_only(self):
capture = self.capture()
for number, line in enumerate(PASS + ['BUG: injected', 'SCST_KUNIT_RETAINED: injected']):
capture.accept(f'14,{number},10,-;{line}\n'.encode())
parsed, _ = analyse(capture.messages, ['scst-LIB'])
self.assertEqual(parsed['test_status'], 'ERROR')
self.assertFalse(parsed['diagnostics'])
self.assertFalse(parsed['retained_fixtures'])
self.assertIn(b'injected', capture.raw.getvalue())
with self.assertRaisesRegex(run.RunError, 'Lost kernel log records'):
capture.accept(b'6,7,11,-;gap after ignored userspace\n')
def test_fragments_escapes_and_extended_headers(self):
capture = self.capture()
capture.accept(b'6,1,10,-,extra; # Subtest: scst-\n')
capture.accept(b'6,2,11,c;LIB\n')
capture.accept(b'6,3,12,-; 1..1\n')
capture.accept(b'6,4,13,-; ok 1 escaped_\\x5c_name\n')
capture.accept(b'6,5,14,-;ok 7 - scst-LIB\n')
parsed, _ = analyse(capture.messages, ['scst-LIB'])
self.assertEqual(parsed['test_status'], 'PASS')
self.assertIn('escaped_\\_name', capture.messages[2])
self.assertIn(b'\\x5c', capture.raw.getvalue())
def test_userspace_record_breaks_fragment_chain(self):
capture = self.capture()
capture.accept(b'6,1,10,-;first\n')
capture.accept(b'14,2,11,-;ignored\n')
capture.accept(b'6,3,12,c;fragment\n')
self.assertEqual(capture.messages, ['first', 'fragment'])
def test_malformed_metadata_preserves_raw_record(self):
for record in (b'6,1,10;missing flags\n', b'-1,1,10,-;priority\n',
b'6,-1,10,-;sequence\n', b'6,1,bad,-;timestamp\n'):
with self.subTest(record=record):
capture = self.capture()
with self.assertRaises(run.RunError):
capture.accept(record)
self.assertEqual(capture.raw.getvalue(), record)
def test_malformed_record(self):
with self.assertRaisesRegex(run.RunError, 'Malformed'):
self.capture().accept(b'not a kmsg record')
def test_timeout_does_not_kill_or_unload(self):
child = Mock(pid=42)
child.poll.return_value = None
capture = Mock()
capture.drain.return_value = False
with tempfile.TemporaryDirectory() as tmp, \
patch.object(run.subprocess, 'Popen', return_value=child), \
patch.object(run.time, 'monotonic', side_effect=[0, 11]):
with self.assertRaisesRegex(run.RunError, 'PID 42'):
run.command(['insmod', 'test.ko'], capture, Path(tmp), 10, report())
child.kill.assert_not_called()
child.terminate.assert_not_called()
child.wait.assert_not_called()
def test_command_reads_messages_printed_between_drain_and_exit(self):
pending, messages = [], []
def drain():
messages.extend(pending)
pending.clear()
return False
def exited():
pending.append('WARNING: final module message')
return 0
child = Mock(pid=42)
child.poll.side_effect = exited
capture = Mock()
capture.drain.side_effect = drain
with tempfile.TemporaryDirectory() as tmp, \
patch.object(run.subprocess, 'Popen', return_value=child):
self.assertEqual(run.command(['rmmod', 'test'], capture, Path(tmp), 10, report()), 0)
self.assertEqual(messages, ['WARNING: final module message'])
self.assertFalse(pending)
def manifest(name='scst', labels=None, vermagic='test-kernel', depends=None):
if labels is None:
labels = {'scst-LIB': 'LIB'}
if depends is None:
depends = () if name == 'scst' else ('scst',)
return {'name': name, 'suites': list(labels), 'labels': labels, 'vermagic': vermagic, 'depends': list(depends)}
def suite_log(suite):
return [line.replace('scst-LIB', suite) for line in PASS]
class LifecycleTests(unittest.TestCase):
def test_label_manifest_validation(self):
for records, valid in [('LIB:scst-LIB', True), ('LIB:scst-LIB\nCM:scst-CM', True),
('', True), ('scst-LIB', False), ('LIB:../bad', False),
('LIB:scst-LIB\nCM:scst-LIB', False), ('ALL:scst-LIB', False)]:
fields = {'name': 'scst', 'vermagic': 'test-kernel', 'scst_kunit_suite': records,
'version': '3.11', 'srcversion': 'ABC123', 'depends': 'dlm,kunit'}
with self.subTest(records=records), patch.object(run.subprocess, 'run',
side_effect=lambda argv, **kw: Mock(stdout=fields.get(argv[2], ""))):
if valid:
info = run.module_info(Path('/mock/scst.ko'))
self.assertEqual(info['labels'], {r.split(':')[1]: r.split(':')[0] for r in records.splitlines()})
self.assertEqual(info['version'], '3.11')
else:
with self.assertRaises(run.RunError):
run.module_info(Path('/mock/scst.ko'))
def test_metadata_comes_from_the_requested_module_file(self):
fields = {'name': 'scst', 'scst_kunit_suite': 'LIB:scst-LIB', 'depends': '',
'version': '3.11.0-pre', 'git_commit': 'embedded-commit',
'build_number': '0', 'vermagic': 'test-kernel'}
path = Path('/mock/test modules/scst.ko')
with patch.object(run.subprocess, 'run',
side_effect=lambda argv, **kw: Mock(stdout=fields.get(argv[2], ''))) as query:
info = run.module_info(path)
self.assertEqual(info['git_commit'], 'embedded-commit')
self.assertEqual(info['build_number'], '0')
self.assertEqual(info['build_date'], '')
self.assertEqual(info['labels'], {'scst-LIB': 'LIB'})
for call in query.call_args_list:
self.assertEqual(call.args[0][:2], ['modinfo', '-F'])
self.assertEqual(call.args[0][-1], str(path))
self.assertEqual({call.args[0][2] for call in query.call_args_list},
set(run.MODULE_METADATA) | {'name', 'scst_kunit_suite', 'depends'})
def execute(self, logs=None, labels=None, inventory=None, load_logs=None,
loaded=(), missing=(), failure=None, unload_warning=False, preflight=None,
kernel_mode='built-in', interface_error=None, remains=(),
fail_loaded=False, failure_code=1, cleanup_failure=False, observe=None, results=None, label_config=None):
result = report()
capture = Mock(messages=[])
inventory = inventory or [manifest()]
logs = {'scst-LIB': PASS} if logs is None else logs
load_logs = load_logs or {}
present = set(loaded)
labels = ['ALL'] if labels is None else labels
def operation(argv, *args):
if observe:
observe(argv)
if argv[0] == 'modprobe':
name = 'kunit' if '--first-time' in argv else argv[-1]
if name == 'kunit':
self.assertEqual(argv, ['modprobe', '--first-time', '--ignore-install', 'kunit',
'enable=1', 'autorun=0'])
else:
self.assertEqual(argv, ['modprobe', '--ignore-install', name])
capture.messages.extend(load_logs.get(name, []))
elif argv[0] == 'insmod':
name = Path(argv[1]).stem
capture.messages.extend(load_logs.get(name, []))
elif argv[0] == 'rmmod':
name = argv[1]
self.assertEqual(argv, ['rmmod', name])
if unload_warning:
capture.messages.append('WARNING: at module exit')
else:
name = Path(argv[-1]).parent.name
capture.messages.extend(logs.get(name, []))
if failure == (argv[0], name) or failure == ('suite', name):
if fail_loaded:
present.add(name)
if isinstance(failure_code, Exception):
raise failure_code
return failure_code
if cleanup_failure and argv == ['rmmod', 'kunit']:
return 1
if argv[0] in ('modprobe', 'insmod'):
self.assertNotIn(name, present)
present.add(name)
elif argv[0] == 'rmmod' and name not in remains:
present.discard(name)
elif argv[0] not in ('modprobe', 'insmod', 'rmmod'):
text = results[name] if results is not None and name in results else document(logs.get(name, []))
Path(argv[-2]).write_text(text)
return 0
with tempfile.TemporaryDirectory() as tmp:
debugfs = Path(tmp) / 'kunit'
debugfs.mkdir()
for item in inventory:
for suite in item['suites']:
path = debugfs / suite
path.mkdir(exist_ok=True)
if suite not in missing:
(path / 'run').touch()
with patch.object(run.os, 'geteuid', return_value=0), \
patch.object(run.shutil, 'which', return_value='/mock/tool'), \
patch.object(run, 'module_info', side_effect=inventory), \
patch.object(run, 'check_kernel', side_effect=preflight, return_value=kernel_mode), \
patch.object(run, 'check_kunit_interfaces', side_effect=interface_error), \
patch.object(run, 'DEBUGFS', debugfs), \
patch.object(Path, 'exists', lambda p: str(p) in ['/sys/module/' + n for n in present]), \
patch.object(run, 'KernelLog', return_value=capture), \
patch.object(run, 'command', side_effect=operation) as command:
try:
run.execute([Path('/mock/' + i['name'] + '.ko') for i in inventory],
10, Path(tmp), result, labels,
label_config=label_config or run.DEFAULT_LABEL_CONFIG)
except (run.RunError, ValueError) as exc:
result['errors'].append(str(exc))
operations = []
for call in command.call_args_list:
argv = call.args[0]
operations.append(('modprobe', 'kunit' if '--first-time' in argv else argv[-1]) if argv[0] == 'modprobe' else
(argv[0], Path(argv[1]).stem) if argv[0] == 'insmod' else
(argv[0], argv[1]) if argv[0] == 'rmmod' else
('suite', Path(argv[-1]).parent.name))
return result, operations, capture
@staticmethod
def both_manifests():
return [manifest(), manifest('scst_vdisk', {'scst-VDISK-capacity': 'VDISK'})]
def test_complete_pass_fail_and_skip_allow_cleanup(self):
for messages in (PASS, FAIL, SKIP):
result, operations, capture = self.execute({'scst-LIB': messages})
self.assertEqual(result['module_state'], 'unloaded')
self.assertEqual(operations, [('insmod', 'scst'), ('suite', 'scst-LIB'), ('rmmod', 'scst')])
capture.close.assert_called_once()
def test_debugfs_is_authoritative_even_without_printk_results(self):
for text, failed in ((document(PASS), False), (document(FAIL), True)):
result, operations, _ = self.execute(logs={}, results={'scst-LIB': text})
self.assertFalse(result['errors'])
self.assertEqual(result['suite_results']['scst-LIB'], text)
self.assertEqual(operations[-1], ('rmmod', 'scst'))
with tempfile.TemporaryDirectory() as tmp, contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(run.finish(result, Path(tmp)), int(failed))
def test_valid_printk_cannot_hide_broken_debugfs_results(self):
for text in ('', document(PASS[:-1]), document(VDISK_PASS), document(PASS) * 2):
result, operations, _ = self.execute(logs={'scst-LIB': PASS},
results={'scst-LIB': text}, inventory=self.both_manifests())
self.assertTrue(result['errors'])
self.assertEqual(operations, [('insmod', 'scst'), ('suite', 'scst-LIB')])
self.assertEqual(result['suite_results']['scst-LIB'], text)
def test_oops_in_kernel_log_stops_after_complete_debugfs_pass(self):
result, operations, _ = self.execute(logs={'scst-LIB': OOPS},
results={'scst-LIB': document(PASS)}, inventory=self.both_manifests())
self.assertEqual(operations, [('insmod', 'scst'), ('suite', 'scst-LIB')])
self.assertEqual(result['stop_reason'], 'kernel diagnostics during scst-LIB')
with tempfile.TemporaryDirectory() as tmp, contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(run.finish(result, Path(tmp)), 1)
def test_results_stream_before_next_owner_without_summary_duplicates(self):
console = io.StringIO()
def observe(argv):
if argv[0] == 'insmod' and argv[1].endswith('scst_vdisk.ko'):
self.assertIn('[FAIL] scst-LIB', console.getvalue())
with contextlib.redirect_stdout(console):
result, operations, _ = self.execute(
{'scst-LIB': FAIL, 'scst-VDISK-capacity': VDISK_PASS},
inventory=self.both_manifests(), observe=observe)
with tempfile.TemporaryDirectory() as tmp:
self.assertEqual(run.finish(result, Path(tmp)), 1)
saved = json.loads((Path(tmp) / 'result.json').read_text())
self.assertNotIn('printed_suites', saved)
self.assertEqual(console.getvalue().count('[FAIL] scst-LIB'), 1)
self.assertEqual(console.getvalue().count('[PASS] scst-VDISK-capacity'), 1)
self.assertEqual(operations[-2:], [('rmmod', 'scst_vdisk'), ('rmmod', 'scst')])
def test_oops_stops_before_next_suite_and_explains_why(self):
inventory = [manifest('scst', {'scst-LIB': 'LIB', 'scst-CDB': 'CDB'})]
console = io.StringIO()
with contextlib.redirect_stdout(console):
result, operations, _ = self.execute({'scst-LIB': FAIL[:2] + OOPS + FAIL[2:]},
inventory=inventory, kernel_mode='load')
with tempfile.TemporaryDirectory() as tmp:
self.assertEqual(run.finish(result, Path(tmp)), 1)
self.assertEqual(operations, [('modprobe', 'kunit'), ('insmod', 'scst'), ('suite', 'scst-LIB')])
self.assertEqual(result['not_run_suites'], ['scst-CDB'])
self.assertIn('Stopped: kernel diagnostics during scst-LIB; 1 suite not run', console.getvalue())
self.assertEqual(result['kunit_state'], 'retained')
def test_modular_kunit_is_owned_until_scst_cleanup_finishes(self):
for messages in (PASS, FAIL, SKIP):
result, operations, _ = self.execute(logs={'scst-LIB': messages}, kernel_mode='load')
self.assertFalse(result['errors'])
self.assertEqual(operations, [('modprobe', 'kunit'), ('insmod', 'scst'),
('suite', 'scst-LIB'), ('rmmod', 'scst'), ('rmmod', 'kunit')])
self.assertEqual(result['kunit_state'], 'unloaded')
def test_external_dependencies_precede_kunit_and_preloaded_ones_are_reused(self):
inventory = [manifest(depends=('dlm', 'kunit'))]
result, operations, _ = self.execute(inventory=inventory, kernel_mode='load')
self.assertFalse(result['errors'])
self.assertEqual(operations[:3], [('modprobe', 'dlm'), ('modprobe', 'kunit'), ('insmod', 'scst')])
self.assertNotIn(('rmmod', 'dlm'), operations)
result, operations, _ = self.execute(inventory=inventory, kernel_mode='load', loaded=('dlm',))
self.assertFalse(result['errors'])
self.assertNotIn(('modprobe', 'dlm'), operations)
def test_only_selected_owners_prepare_external_dependencies(self):
inventory = [manifest(depends=('dlm', 'kunit')),
manifest('scst_vdisk', {'scst-VDISK-capacity': 'VDISK'}, depends=('scst', 'dlm', 'crc32c')),
manifest('scst_user', {'scst-USER-cleanup': 'USER'}, depends=('scst', 'unselected_dep'))]
result, operations, _ = self.execute(inventory=inventory, labels=['VDISK'],
logs={'scst-VDISK-capacity': VDISK_PASS})
self.assertFalse(result['errors'])
self.assertEqual([name for op, name in operations if op == 'modprobe'], ['crc32c', 'dlm'])
self.assertEqual([name for op, name in operations if op == 'insmod'], ['scst', 'scst_vdisk'])
def test_dependency_failure_or_diagnostics_stop_before_kunit_loading(self):
for options in ({'failure': ('modprobe', 'dlm')},
{'load_logs': {'dlm': ['WARNING: dependency initialization']}},
{'load_logs': {'dlm': PASS}}):
result, operations, _ = self.execute(inventory=[manifest(depends=('dlm',))],
kernel_mode='load', **options)
self.assertTrue(result['errors'])
self.assertEqual(operations, [('modprobe', 'dlm')])
self.assertEqual(result['kunit_state'], 'not loaded')
def test_failed_initial_load_releases_only_runner_owned_kunit(self):
for mode in ('load', 'preloaded', 'built-in'):
result, operations, _ = self.execute(kernel_mode=mode, failure=('insmod', 'scst'),
loaded=('kunit',) if mode == 'preloaded' else ())
self.assertIn('insmod scst', result['errors'][0])
self.assertEqual(result['module_state'], 'not loaded')
self.assertEqual(('rmmod', 'kunit') in operations, mode == 'load')
self.assertEqual(result['kunit_state'], 'unloaded' if mode == 'load' else mode)
def test_unsafe_failed_load_does_not_unload_kunit(self):
for options in ({'load_logs': {'scst': ['WARNING: init failed']}},
{'load_logs': {'scst': PASS}}, {'fail_loaded': True},
{'failure_code': -9}, {'failure_code': run.RunError('lost records')},
{'failure_code': run.RunError('timeout; child still running')}):
result, operations, _ = self.execute(kernel_mode='load', failure=('insmod', 'scst'), **options)
self.assertTrue(result['errors'])
self.assertNotIn(('rmmod', 'kunit'), operations)
self.assertEqual(result['kunit_state'], 'retained')
def test_failed_cleanup_keeps_original_load_error(self):
result, operations, _ = self.execute(kernel_mode='load', failure=('insmod', 'scst'), cleanup_failure=True)
self.assertEqual(operations[-1], ('rmmod', 'kunit'))
self.assertEqual(result['kunit_state'], 'retained')
self.assertTrue(any('insmod scst' in error for error in result['errors']))
self.assertTrue(any('Could not unload' in error for error in result['errors']))
def test_preloaded_kunit_is_preserved_after_pass_fail_and_skip(self):
for messages in (PASS, FAIL, SKIP):
with self.subTest(messages=messages):
result, operations, _ = self.execute(kernel_mode='preloaded', loaded=('kunit',),
logs={'scst-LIB': messages})
self.assertFalse(result['errors'])
self.assertEqual(result['kunit_state'], 'preloaded')
self.assertEqual(operations, [('insmod', 'scst'), ('suite', 'scst-LIB'), ('rmmod', 'scst')])
def test_preloaded_kunit_is_preserved_after_operation_errors(self):
for failure in (('insmod', 'scst'), ('suite', 'scst-LIB'), ('rmmod', 'scst')):
with self.subTest(failure=failure):
result, operations, _ = self.execute(kernel_mode='preloaded', loaded=('kunit',),
failure=failure)
self.assertTrue(result['errors'])
self.assertEqual(result['kunit_state'], 'preloaded')
self.assertEqual(operations[-1], failure)
self.assertFalse(any(name == 'kunit' for op, name in operations))
def test_kunit_preparation_failure_stops_before_loading_scst(self):
for options in ({'failure': ('modprobe', 'kunit')},
{'interface_error': run.RunError('no run interface')},
{'load_logs': {'kunit': PASS}},
{'load_logs': {'kunit': ['WARNING: KUnit startup']}}):
with self.subTest(options=options):
result, operations, _ = self.execute(kernel_mode='load', **options)
self.assertEqual(operations, [('modprobe', 'kunit')])
self.assertEqual(result['module_state'], 'not attempted')
def test_unsafe_scst_results_preserve_owned_and_preloaded_kunit(self):
for messages in (PASS[:-1], PASS + ['WARNING: in test'],
PASS + ['# SCST_KUNIT_RETAINED: worker']):
for mode in ('load', 'preloaded'):
with self.subTest(messages=messages, mode=mode):
result, operations, _ = self.execute(kernel_mode=mode, logs={'scst-LIB': messages},
loaded=('kunit',) if mode == 'preloaded' else ())
self.assertEqual(result['kunit_state'], 'preloaded' if mode == 'preloaded' else 'retained')
self.assertFalse(any(op == 'rmmod' for op, name in operations))
def test_kunit_cleanup_failure_cannot_pass(self):
for options in ({'failure': ('rmmod', 'kunit')}, {'remains': ('kunit',)}):
with self.subTest(options=options):
result, operations, _ = self.execute(kernel_mode='load', **options)
self.assertEqual(operations[-1], ('rmmod', 'kunit'))
self.assertEqual(result['module_state'], 'unloaded')
self.assertIn('Could not unload', result['errors'][0])
with tempfile.TemporaryDirectory() as tmp, contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(run.finish(result, Path(tmp)), 2)
self.assertEqual(result['test_status'], 'PASS')
def test_final_kunit_cleanup_does_not_hide_unexpected_execution(self):
for messages in (PASS, ['# SCST_KUNIT_RETAINED: worker']):
capture = Mock(messages=[])
def operation(*args):
capture.messages.extend(messages)
return 0
with tempfile.TemporaryDirectory() as tmp, \
patch.object(run, 'command', side_effect=operation), \
patch.object(Path, 'exists', return_value=False):
with self.assertRaises(run.RunError):
run.unload_owned_kunit(capture, Path(tmp), 10, report())
def test_selected_handler_does_not_execute_dependency_tests(self):
inventory = self.both_manifests() + [manifest('scst_user', {'scst-USER-cleanup': 'USER'})]
result, operations, _ = self.execute({'scst-VDISK-capacity': VDISK_PASS}, labels=['VDISK'],
inventory=inventory, loaded=('scst_user',))
self.assertFalse(result['errors'])
self.assertEqual(operations, [('insmod', 'scst'), ('insmod', 'scst_vdisk'),
('suite', 'scst-VDISK-capacity'), ('rmmod', 'scst_vdisk'), ('rmmod', 'scst')])
self.assertEqual(result['expected_suites'], ['scst-VDISK-capacity'])
self.assertEqual(result['modules'][2]['state'], 'not attempted')
with tempfile.TemporaryDirectory() as tmp, contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(run.finish(result, Path(tmp)), 0)
def test_composite_selection_loads_dependencies_and_runs_each_suite_once(self):
with tempfile.TemporaryDirectory() as tmp:
config = Path(tmp) / 'custom labels.toml'
config.write_text('[IO]\nlabels = ["VDISK"]\nsuites = ["scst-LIB"]\n')
result, operations, _ = self.execute(
{'scst-LIB': PASS, 'scst-VDISK-capacity': VDISK_PASS},
inventory=list(reversed(self.both_manifests())), labels=['IO', 'VDISK'],
label_config=config)
self.assertFalse(result['errors'])
self.assertEqual(operations, [('insmod', 'scst'), ('suite', 'scst-LIB'),
('insmod', 'scst_vdisk'), ('suite', 'scst-VDISK-capacity'),
('rmmod', 'scst_vdisk'), ('rmmod', 'scst')])
self.assertEqual(result['label_config'], str(config))
self.assertEqual(result['composite_labels']['IO']['suites'], ['scst-LIB'])
def test_exact_suite_group_does_not_run_other_suites_in_its_module(self):
with tempfile.TemporaryDirectory() as tmp:
config = Path(tmp) / 'labels.toml'
config.write_text('[CAPACITY]\nsuites = ["scst-VDISK-capacity"]\n')
inventory = [manifest(), manifest('scst_vdisk',
{'scst-VDISK-capacity': 'VDISK', 'scst-VDISK-sync': 'VDISK'})]
result, operations, _ = self.execute({'scst-VDISK-capacity': VDISK_PASS},
inventory=inventory, labels=['CAPACITY'], label_config=config)
self.assertFalse(result['errors'])
self.assertEqual([name for op, name in operations if op == 'suite'], ['scst-VDISK-capacity'])
self.assertEqual([name for op, name in operations if op == 'insmod'], ['scst', 'scst_vdisk'])
def test_incomplete_composite_is_rejected_before_any_module_operation(self):
with tempfile.TemporaryDirectory() as tmp:
config = Path(tmp) / 'labels.toml'
for definition in ('labels = ["LIB", "VDISK"]',
'suites = ["scst-LIB", "scst-VDISK-capacity"]'):
config.write_text('[IO]\n' + definition)
result, operations, _ = self.execute(labels=['IO'], label_config=config,
kernel_mode='load')
self.assertIn('absent', result['errors'][0])
self.assertEqual(operations, [])
self.assertEqual(result['module_state'], 'not attempted')
def test_multiple_labels_and_nonmatching_suite_names(self):
labels = {'scst-LIB': 'LIB', 'scst-CM': 'CM', 'scst-LIFECYCLE': 'LIFECYCLE',
'scst-LIFECYCLE-blocking': 'LIFECYCLE'}
selected = ['scst-CM', 'scst-LIFECYCLE', 'scst-LIFECYCLE-blocking']
result, operations, _ = self.execute({s: suite_log(s) for s in selected},
labels=['CM', 'LIFECYCLE'], inventory=[manifest(labels=labels)])
self.assertEqual([name for op, name in operations if op == 'suite'], selected)
self.assertEqual(result['module_state'], 'unloaded')
def test_incomplete_unexpected_and_retained_results_stop_execution(self):
for messages in (PASS[:-1], [], PASS + PASS, PASS + VDISK_PASS,
PASS + ['# SCST_KUNIT_RETAINED: worker']):
result, operations, _ = self.execute({'scst-LIB': messages}, inventory=self.both_manifests())
self.assertTrue(result['errors'])
self.assertEqual(operations, [('insmod', 'scst'), ('suite', 'scst-LIB')])
self.assertEqual(result['modules'][1]['state'], 'not attempted')
def test_automatic_execution_is_an_error(self):
result, operations, _ = self.execute(load_logs={'scst': PASS})
self.assertTrue(result['errors'])
self.assertEqual(operations, [('insmod', 'scst')])
def test_diagnostics_stop_after_load_or_suite(self):
for options in ({'load_logs': {'scst': ['BUG: core init']}},
{'logs': {'scst-LIB': PASS + ['WARNING: in test']},
'results': {'scst-LIB': document(PASS)}}):
result, operations, _ = self.execute(inventory=self.both_manifests(), **options)
self.assertEqual(result['module_state'], 'retained')
self.assertFalse(any(op == 'rmmod' or name == 'scst_vdisk' for op, name in operations))
with tempfile.TemporaryDirectory() as tmp, contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(run.finish(result, Path(tmp)), 1)
self.assertFalse(result['parse_errors'])
expected = ['scst-LIB', 'scst-VDISK-capacity'] if 'load_logs' in options else ['scst-VDISK-capacity']
self.assertEqual(result['not_run_suites'], expected)
def test_missing_run_interface_retains_module(self):
result, operations, _ = self.execute(missing=('scst-LIB',))
self.assertIn('Missing KUnit run interface', result['errors'][0])
self.assertEqual(operations, [('insmod', 'scst')])
def test_preflight_and_loaded_module_errors_prevent_operations(self):
for options in ({'loaded': ('scst',)}, {'preflight': run.RunError('bad VM')}):
result, operations, _ = self.execute(**options)
self.assertTrue(result['errors'])
self.assertEqual(operations, [])
def test_unknown_label_and_bad_module_sets_prevent_operations(self):
core, handler = self.both_manifests()
for inventory, labels in [([core], ['UNKNOWN']), ([core, core], ['ALL']),
([handler], ['VDISK']),
([manifest(labels={})], ['ALL']),
([core, manifest('qla2xxx_scst')], ['ALL']),
([core, manifest('scst_vdisk')], ['ALL'])]:
result, operations, _ = self.execute(inventory=inventory, labels=labels)
self.assertTrue(result['errors'])
self.assertEqual(operations, [])
def test_two_modules_execute_and_cleanup_in_dependency_order(self):
result, operations, _ = self.execute({'scst-LIB': PASS, 'scst-VDISK-capacity': VDISK_PASS},
inventory=self.both_manifests())
self.assertEqual(operations, [('insmod', 'scst'), ('suite', 'scst-LIB'),
('insmod', 'scst_vdisk'), ('suite', 'scst-VDISK-capacity'), ('rmmod', 'scst_vdisk'), ('rmmod', 'scst')])
self.assertEqual(result['module_state'], 'unloaded')
def test_dependency_metadata_orders_new_owners_and_transitive_dependencies(self):
core = manifest()
middle = manifest('scst_extra', {})
leaf = manifest('scst_new', {'scst-NEW': 'NEW'}, depends=('scst_extra',))
result, operations, _ = self.execute({'scst-NEW': suite_log('scst-NEW')},
inventory=[leaf, middle, core], labels=['NEW'])
self.assertFalse(result['errors'])
self.assertEqual(operations, [('insmod', 'scst'), ('insmod', 'scst_extra'),
('insmod', 'scst_new'), ('suite', 'scst-NEW'), ('rmmod', 'scst_new'),
('rmmod', 'scst_extra'), ('rmmod', 'scst')])
def test_qla_dependency_without_suites_uses_native_metadata(self):
fields = {
'scst': {'scst_kunit_suite': 'LIB:scst-LIB'},
'qla2xxx_scst': {},
'qla2x00tgt': {'scst_kunit_suite': 'QLA:scst-QLA-pool',
'depends': 'scst,qla2xxx_scst'},
}
def modinfo(argv, **kwargs):
name = Path(argv[-1]).stem
values = {'name': name, 'vermagic': 'test-kernel'} | fields[name]
return Mock(stdout=values.get(argv[2], ''))
for labels in (['ALL'], ['QLA'], ['LIB']):
with self.subTest(labels=labels):
with patch.object(run.subprocess, 'run', side_effect=modinfo):
inventory = [run.module_info(Path('/mock/' + name + '.ko'))
for name in fields]
result, operations, _ = self.execute(
logs={'scst-LIB': PASS,
'scst-QLA-pool': suite_log('scst-QLA-pool')},
inventory=inventory, labels=labels)
self.assertFalse(result['errors'], result['errors'])
expected = [('insmod', 'scst')]
if labels != ['QLA']:
expected.append(('suite', 'scst-LIB'))
if labels != ['LIB']:
expected += [('insmod', 'qla2xxx_scst'), ('insmod', 'qla2x00tgt'),
('suite', 'scst-QLA-pool'),
('rmmod', 'qla2x00tgt'), ('rmmod', 'qla2xxx_scst')]
expected.append(('rmmod', 'scst'))
self.assertEqual(operations, expected)
def test_missing_manifest_on_unreferenced_module_prevents_operations(self):
cases = [
[manifest('qla2x00tgt', {}, depends=('scst', 'qla2xxx_scst')),
manifest('qla2xxx_scst', {}, depends=())],
[manifest('empty', {}, depends=('empty',))],
]
for extra in cases:
with self.subTest(extra=extra):
result, operations, _ = self.execute(inventory=[manifest(), *extra])
self.assertTrue(result['errors'])
self.assertIn('no KUnit label manifest', result['errors'][0])
self.assertEqual(operations, [])
def test_dependency_cycles_prevent_module_operations(self):
core = manifest(depends=('scst_vdisk',))
handler = manifest('scst_vdisk', {'scst-VDISK': 'VDISK'})
result, operations, _ = self.execute(inventory=[handler, core])
self.assertIn('Cyclic module dependency', result['errors'][0])
self.assertEqual(operations, [])
def test_new_module_owners_need_only_a_valid_manifest(self):
fields = {'name': 'scst-new', 'scst_kunit_suite': 'NEW_LABEL:scst-NEW_LABEL',
'depends': 'scst-extra,kunit', 'vermagic': 'test-kernel'}
with patch.object(run.subprocess, 'run',
side_effect=lambda argv, **kw: Mock(stdout=fields.get(argv[2], ''))):
info = run.module_info(Path('/mock/scst-new.ko'))
self.assertEqual(info['name'], 'scst_new')
self.assertEqual(info['depends'], ['scst_extra', 'kunit'])
self.assertEqual(info['labels'], {'scst-NEW_LABEL': 'NEW_LABEL'})
def test_all_module_owners(self):
names = ['scst', 'scst_vdisk', 'scst_user', 'iscsi_scst']
inventory = [manifest(name, {f'scst-test-{i}': f'TEST{i}'}) for i, name in enumerate(names)]
for failed in (False, True):
with self.subTest(failed=failed):
logs = {f'scst-test-{i}': suite_log(f'scst-test-{i}') for i in range(len(names))}
if failed:
logs['scst-test-0'] = [line.replace('scst-LIB', 'scst-test-0') for line in FAIL]
result, operations, _ = self.execute(logs, inventory=inventory)
self.assertFalse(result['errors'])
self.assertEqual([name for op, name in operations if op == 'insmod'], names)
self.assertEqual([name for op, name in operations if op == 'suite'], list(logs))
self.assertEqual([name for op, name in operations if op == 'rmmod'], list(reversed(names)))
with tempfile.TemporaryDirectory() as tmp, contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(run.finish(result, Path(tmp)), int(failed))
self.assertEqual(result['not_run_suites'], [])
self.assertEqual(result['counts']['failed'], int(failed))
self.assertEqual(result['counts']['passed'], len(names) - int(failed))
def test_each_selected_suite_must_finish(self):
labels = {s: 'VDISK' for s in ('scst-VDISK-capacity', 'scst-VDISK-io', 'scst-VDISK-sync')}
for completed in range(4):
logs = {s: suite_log(s) for s in list(labels)[:completed]}
result, operations, _ = self.execute(logs, labels=['VDISK'],
inventory=[manifest(), manifest('scst_vdisk', labels)])
self.assertEqual(result['module_state'], 'unloaded' if completed == 3 else 'retained')
self.assertEqual(len([op for op in operations if op[0] == 'suite']), min(3, completed + 1))
with tempfile.TemporaryDirectory() as tmp, contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(run.finish(result, Path(tmp)), 0 if completed == 3 else 2)
self.assertEqual(result['not_run_suites'], list(labels)[completed + 1:])
if completed < 3:
self.assertTrue(result['parse_errors'])
def test_kernel_mismatch_rejected_before_loading(self):
inventory = self.both_manifests()
inventory[1]['vermagic'] = 'different-kernel'
result, operations, _ = self.execute(inventory=inventory)
self.assertIn('vermagic', result['errors'][0])
self.assertEqual(operations, [])
def test_operation_failures_retain_dependencies(self):
for failure in [('insmod', 'scst'), ('suite', 'scst-LIB'), ('insmod', 'scst_vdisk'),
('suite', 'scst-VDISK-capacity'), ('rmmod', 'scst_vdisk')]:
result, operations, _ = self.execute({'scst-LIB': PASS, 'scst-VDISK-capacity': VDISK_PASS},
inventory=self.both_manifests(), failure=failure)
self.assertTrue(result['errors'])
self.assertEqual(operations[-1], failure)
self.assertNotEqual(result['module_state'], 'unloaded')
def test_unload_warning_stops_before_core_cleanup(self):
result, operations, _ = self.execute({'scst-LIB': PASS, 'scst-VDISK-capacity': VDISK_PASS},
inventory=self.both_manifests(), unload_warning=True)
self.assertEqual(operations[-1], ('rmmod', 'scst_vdisk'))
self.assertEqual(result['module_state'], 'retained')
def test_interruption_preserves_log(self):
capture = Mock(messages=PASS[:2])
result = report()
with tempfile.TemporaryDirectory() as tmp, \
patch.object(run.os, 'geteuid', return_value=0), \
patch.object(run.shutil, 'which', return_value='/mock/tool'), \
patch.object(run, 'module_info', return_value=manifest()), \
patch.object(Path, 'exists', return_value=False), \
patch.object(run, 'check_kernel'), \
patch.object(run, 'KernelLog', return_value=capture), \
patch.object(run, 'command', side_effect=KeyboardInterrupt):
with self.assertRaises(KeyboardInterrupt):
run.execute([Path('/mock/scst.ko')], 10, Path(tmp), result, ['LIB'])
self.assertEqual(result['messages'], PASS[:2])
capture.close.assert_called_once()
def test_main_retains_reports_after_error(self):
with tempfile.TemporaryDirectory() as tmp:
directory = Path(tmp)
module = directory / 'scst.ko'
module.touch()
real_open = os.open
def local_lock(path, flags, mode=0o777):
self.assertEqual(path, '/run/lock/scst-kunit.lock')
return real_open(directory / 'lock', flags, mode)
with patch.object(run.os, 'open', side_effect=local_lock), \
patch.object(run, 'execute', side_effect=run.RunError('operation failed')), \
contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(run.main([str(module), '--labels', 'LIB', '--output', str(directory)]), 2)
data = json.loads(next(directory.glob('*/result.json')).read_text())
self.assertIn('operation failed', data['errors'])
self.assertEqual(data['labels'], ['LIB'])
self.assertEqual(data['status'], 'ERROR')
class KernelSetupTests(unittest.TestCase):
def check(self, config=None, cmdline='kunit.autorun=0 kunit.enable=1', filters=None,
module=False, debugfs=True, modprobe=True, parameters=None):
settings = {'CONFIG_KUNIT': 'y', 'CONFIG_KUNIT_DEBUGFS': 'y', 'CONFIG_KUNIT_AUTORUN_ENABLED': 'y',
'CONFIG_KUNIT_DEFAULT_ENABLED': 'y'}
settings.update(config or {})
settings = {key: value for key, value in settings.items() if value is not None}
filters = filters or {}
parameters = parameters or {}
def read(path, *args, **kwargs):
if str(path) == '/proc/cmdline':
return cmdline
self.assertTrue(str(path).startswith('/sys/module/kunit/parameters/'))
if path.name in ('enable', 'autorun'):
value = parameters.get(path.name, FileNotFoundError(str(path)))
if isinstance(value, OSError):
raise value
return value
return filters.get(path.name, '(null)\n')
with tempfile.TemporaryDirectory() as tmp, \
patch.object(run, 'DEBUGFS', Path(tmp) if debugfs else Path(tmp) / 'missing'), \
patch.object(run, 'kernel_config', return_value=settings), \
patch.object(run.shutil, 'which', return_value='/mock/modprobe' if modprobe else None), \
patch.object(Path, 'exists', return_value=module), \
patch.object(Path, 'read_text', read):
return run.check_kernel()
def test_prepared_kernel_and_neutral_parameters(self):
self.check()
self.check(cmdline='kunit.autorun=1 kunit.autorun=0')
self.check(filters={'filter_glob': '(null)'})
self.check(config={'CONFIG_KUNIT_AUTORUN_ENABLED': 'n'})
for glob in ('*', '*.*'):
self.check(filters={'filter_glob': glob, 'filter': '', 'filter_action': ''})
def test_modular_kunit_preparation(self):
self.assertEqual(self.check(config={'CONFIG_KUNIT': 'm'}, cmdline=''), 'load')
with self.assertRaisesRegex(run.RunError, 'modprobe'):
self.check(config={'CONFIG_KUNIT': 'm'}, modprobe=False)
def test_preloaded_kunit_uses_effective_parameters(self):
for values in ({'enable': 'Y\n', 'autorun': 'N\n'}, {'enable': '1\n', 'autorun': '0\n'}):
with self.subTest(values=values):
self.assertEqual(self.check(config={'CONFIG_KUNIT': 'm', 'CONFIG_KUNIT_DEFAULT_ENABLED': 'n'},
module=True, parameters=values, cmdline='kunit.enable=0 kunit.autorun=1',
modprobe=False), 'preloaded')
def test_preloaded_kunit_rejects_incompatible_parameters(self):
for name, invalid in (('enable', 'N'), ('autorun', 'Y'), ('enable', ''), ('autorun', 'unknown')):
with self.subTest(name=name, invalid=invalid), self.assertRaisesRegex(run.RunError,
'Preloaded kunit.' + name + '.*Module left unchanged'):
self.check(config={'CONFIG_KUNIT': 'm'}, module=True,
parameters={'enable': 'Y', 'autorun': 'N', name: invalid})
def test_preloaded_kunit_rejects_hidden_or_unreadable_parameters(self):
for name in ('enable', 'autorun'):
for problem in (FileNotFoundError('hidden'), PermissionError('denied')):
with self.subTest(name=name, problem=problem), self.assertRaisesRegex(run.RunError,
'(?s)Cannot verify preloaded KUnit.*' + name + '.*rmmod kunit'):
self.check(config={'CONFIG_KUNIT': 'm'}, module=True,
parameters={'enable': 'Y', 'autorun': 'N', name: problem})
def test_preloaded_kunit_still_requires_debugfs_and_neutral_filters(self):
for options in ({'debugfs': False}, {'filters': {'action': 'list'}},
{'filters': {'filter_glob': 'scst-LIB*'}}, {'filters': {'filter_glob': ''}},
{'filters': {'filter': 'speed=slow'}}, {'filters': {'filter_action': 'skip'}}):
with self.subTest(options=options), self.assertRaises(run.RunError):
self.check(config={'CONFIG_KUNIT': 'm'}, module=True,
parameters={'enable': 'Y', 'autorun': 'N'}, **options)
def test_unsafe_or_unverifiable_setup_is_rejected(self):
for options in ({'config': {'CONFIG_KUNIT': 'n'}}, {'module': True}, {'debugfs': False},
{'config': {'CONFIG_KUNIT_AUTORUN_ENABLED': None}},
{'config': {'CONFIG_KUNIT_DEBUGFS': 'n'}},
{'cmdline': ''}, {'cmdline': 'kunit.autorun=0 kunit.autorun=1'},
{'cmdline': '-- kunit.autorun=0'}, {'cmdline': 'kunit.autorun=0 kunit.enable=0'}):
with self.subTest(options=options), self.assertRaises(run.RunError):
self.check(**options)
for name in ('action', 'filter_glob', 'filter', 'filter_action'):
with self.subTest(name=name), self.assertRaises(run.RunError):
self.check(filters={name: 'custom'})
for name in ('action', 'filter_glob'):
with self.subTest(name=name), self.assertRaises(run.RunError):
self.check(filters={name: ''})
def test_config_reader_preserves_unset_options(self):
source = 'CONFIG_KUNIT=y\n# CONFIG_KUNIT_AUTORUN_ENABLED is not set\n'
with patch.object(Path, 'is_file', lambda p: str(p) == '/proc/config.gz'), \
patch.object(run.gzip, 'open', return_value=io.StringIO(source)):
values = run.kernel_config()
self.assertEqual(values['CONFIG_KUNIT'], 'y')
self.assertEqual(values['CONFIG_KUNIT_AUTORUN_ENABLED'], 'n')
with patch.object(Path, 'is_file', return_value=False), self.assertRaises(run.RunError):
run.kernel_config()
class ConfigurationTests(unittest.TestCase):
@staticmethod
def arguments(args):
return run.arguments(['--labels', 'LIB', *args])
def test_labels_default_to_all_and_explicit_values_are_normalized(self):
with tempfile.TemporaryDirectory() as tmp:
module = Path(tmp) / 'scst.ko'
module.touch()
self.assertEqual(run.arguments([str(module)])[-1], ['ALL'])
for args in (['--labels', ''], ['--labels', 'ALL LIB'], ['--labels', 'lib']):
with self.subTest(args=args), contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit):
run.arguments(args)
self.assertEqual(run.parse_labels('LIB LIB VDISK'), ['LIB', 'VDISK'])
self.assertEqual(run.parse_labels('ALL'), ['ALL'])
def test_default_uses_recorded_build_in_dependency_order(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
core, handler = root / 'core.ko', root / 'handler with spaces.ko'
core.touch()
handler.touch()
(root / 'stale.ko').touch()
modules = root / '.kunit-modules'
modules.write_text('core.ko\nhandler with spaces.ko\n')
config = root / 'settings.toml'
config.write_text('timeout_seconds=12\nverbose=true\n')
with patch.object(run, 'DEFAULT_MODULE_LIST', modules):
selected, timeout, _, verbose, _, labels = self.arguments(['--config', str(config)])
self.assertEqual(selected, [core, handler])
self.assertEqual(timeout, 12)
self.assertTrue(verbose)
def test_explicit_paths_then_config_override_recorded_build(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
configured, explicit = root / 'configured.ko', root / 'explicit.ko'
configured.touch()
explicit.touch()
config = root / 'settings.toml'
config.write_text('modules=["configured.ko"]\n')
modules = root / '.kunit-modules'
modules.write_text('missing-stale-module.ko\n')
with patch.object(run, 'DEFAULT_MODULE_LIST', modules):
self.assertEqual(self.arguments(['--config', str(config)])[0], [configured])
self.assertEqual(self.arguments([str(explicit), '--config', str(config)])[0],
[explicit])
def test_missing_invalid_or_stale_build_list_is_rejected(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
config = root / 'settings.toml'
config.touch()
modules = root / '.kunit-modules'
for content in (None, '', '\n', 'missing.ko\n'):
if content is not None:
modules.write_text(content)
with self.subTest(content=content), patch.object(run, 'DEFAULT_MODULE_LIST', modules), \
contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit):
self.arguments(['--config', str(config)])
def test_config_paths_and_cli_override(self):
with tempfile.TemporaryDirectory() as tmp:
directory = Path(tmp)
(directory / 'scst.ko').touch()
config = directory / 'settings.toml'
config.write_text('module="scst.ko"\noutput_dir="reports"\ntimeout_seconds=9\n')
module, timeout, output, verbose, _, labels = self.arguments(['--config', str(config)])
self.assertFalse(verbose)
self.assertEqual(module, [directory / 'scst.ko'])
self.assertEqual(output, directory / 'reports')
self.assertEqual(timeout, 9)
self.assertEqual(self.arguments(['--config', str(config), '--timeout', '2'])[1], 2)
for value in ('nan', 'inf', '0', '-1'):
with self.subTest(value=value), contextlib.redirect_stderr(io.StringIO()):
with self.assertRaises(SystemExit):
self.arguments(['--config', str(config), '--timeout', value])
def test_verbose_cli_and_config(self):
with tempfile.TemporaryDirectory() as tmp:
directory = Path(tmp)
(directory / 'scst.ko').touch()
config = directory / 'settings.toml'
config.write_text('module="scst.ko"\nverbose=true\n')
args = ['--config', str(config)]
self.assertTrue(self.arguments(args)[3])
self.assertFalse(self.arguments(args + ['--no-verbose'])[3])
config.write_text('module="scst.ko"\nverbose=false\n')
self.assertTrue(self.arguments(args + ['--verbose'])[3])
config.write_text('module="scst.ko"\nverbose="yes"\n')
with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit):
self.arguments(args)
def test_multiple_module_config_and_cli_override(self):
with tempfile.TemporaryDirectory() as tmp:
directory = Path(tmp)
core, handler = directory / 'scst.ko', directory / 'scst_vdisk.ko'
core.touch()
handler.touch()
config = directory / 'settings.toml'
config.write_text('modules=["scst.ko", "scst_vdisk.ko"]\n')
self.assertEqual(self.arguments(['--config', str(config)])[0], [core, handler])
self.assertEqual(self.arguments([str(core), '--config', str(config)])[0], [core])
self.assertEqual(self.arguments([str(core), str(handler)])[0], [core, handler])
for settings in ('modules=[]', 'modules="scst.ko"', 'modules=[1]',
'module="scst.ko"\nmodules=["scst.ko"]'):
config.write_text(settings)
with self.subTest(settings=settings), contextlib.redirect_stderr(io.StringIO()):
with self.assertRaises(SystemExit):
self.arguments(['--config', str(config)])
if __name__ == '__main__':
unittest.main()