Skip to content

Commit a20fc12

Browse files
committed
contest: hw: parse nested results and apply guess indicators
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
1 parent 262a041 commit a20fc12

9 files changed

Lines changed: 161 additions & 116 deletions

File tree

contest/hw/lib/deployer.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
import time
1313
from dataclasses import dataclass, field
1414

15-
from lib.nipa import has_crash, extract_crash
15+
from lib.nipa import (has_crash, extract_crash, guess_indicators,
16+
result_from_indicators, parse_nested_tests, namify)
1617

1718

1819
# Log file handle, set by set_log_file() before builds start.
@@ -416,17 +417,6 @@ def fetch_results(machine_ips, reservation_id, results_path):
416417
check=False)
417418

418419

419-
def _retcode_to_result(retcode, stdout):
420-
"""Map a test return code + stdout to pass/fail/skip."""
421-
if retcode == 4:
422-
return 'skip'
423-
if retcode != 0:
424-
return 'fail'
425-
if 'ok' not in stdout.lower():
426-
return 'skip'
427-
return 'pass'
428-
429-
430420
def parse_results(results_path, link):
431421
"""Parse fetched test output into a vmksft-p-style result list.
432422
@@ -464,28 +454,36 @@ def parse_results(results_path, link):
464454
with open(stdout_path, encoding='utf-8') as fp:
465455
stdout = fp.read()
466456

467-
# Determine result
468-
result = _retcode_to_result(retcode, stdout)
457+
# Determine result using indicators
458+
indicators = guess_indicators(stdout)
459+
result = result_from_indicators(retcode, indicators)
460+
461+
# Parse nested subtests
462+
nested = parse_nested_tests(stdout, namify)
469463

470464
# Determine retry result if present
471465
retry_result = None
466+
retry_nested = None
472467
if 'retry_retcode' in info:
473468
retry_stdout = ''
474469
retry_dir = os.path.join(output_dir, entry + '-retry')
475470
retry_stdout_path = os.path.join(retry_dir, 'stdout')
476471
if os.path.exists(retry_stdout_path):
477472
with open(retry_stdout_path, encoding='utf-8') as fp:
478473
retry_stdout = fp.read()
479-
retry_result = _retcode_to_result(info['retry_retcode'],
480-
retry_stdout)
474+
retry_indicators = guess_indicators(retry_stdout)
475+
retry_result = result_from_indicators(info['retry_retcode'],
476+
retry_indicators)
477+
if nested:
478+
retry_nested = list(nested)
479+
parse_nested_tests(retry_stdout, namify,
480+
prev_results=retry_nested)
481481

482-
safe_name = re.sub(r'[^0-9a-zA-Z]+', '-', prog)
483-
if safe_name and safe_name[-1] == '-':
484-
safe_name = safe_name[:-1]
482+
safe_name = namify(prog)
485483

486484
outcome = {
487485
'test': safe_name or entry,
488-
'group': f'selftests-{re.sub(r"[^0-9a-zA-Z]+", "-", target).rstrip("-")}',
486+
'group': f'selftests-{namify(target)}',
489487
'result': result,
490488
'link': link,
491489
}
@@ -495,6 +493,10 @@ def parse_results(results_path, link):
495493
outcome['retry'] = retry_result
496494
if info.get('crashes'):
497495
outcome['crashes'] = info['crashes']
496+
if retry_nested:
497+
outcome['results'] = retry_nested
498+
elif nested:
499+
outcome['results'] = nested
498500
cases.append(outcome)
499501

500502
# Check .attempted for crashed tests (attempted but no output)

contest/hw/lib/nipa.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
from contest.remote.lib.crash import has_crash # noqa: E402, F401
2020
from contest.remote.lib.crash import extract_crash # noqa: E402, F401
2121
from contest.remote.lib.crash import crash_finger_print # noqa: E402, F401
22+
from contest.remote.lib.results import guess_indicators # noqa: E402, F401
23+
from contest.remote.lib.results import result_from_indicators # noqa: E402, F401
24+
from contest.remote.lib.results import parse_nested_tests # noqa: E402, F401
2225
from contest.remote.lib.cbarg import CbArg # noqa: E402, F401
2326
from contest.remote.lib.fetcher import Fetcher # noqa: E402, F401
27+
from contest.remote.lib.fetcher import namify # noqa: E402, F401
2428
from core import NipaLifetime # noqa: E402, F401

contest/hw/lib/runner.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44

55
import json
66
import os
7-
import re
87
import subprocess
98
import time
109

11-
from lib.nipa import has_crash, extract_crash
10+
from lib.nipa import has_crash, extract_crash, namify
1211

1312

1413
def find_newest_unseen(tests_dir):
@@ -125,15 +124,6 @@ def _list_tests(test_dir):
125124
return tests
126125

127126

128-
def _namify(what):
129-
"""Convert test name to a safe identifier."""
130-
if not what:
131-
return "no-name"
132-
name = re.sub(r'[^0-9a-zA-Z]+', '-', what)
133-
if name and name[-1] == '-':
134-
name = name[:-1]
135-
return name
136-
137127

138128
def load_filters(test_dir):
139129
"""Load crash filters from filters.json in the test directory.
@@ -251,7 +241,7 @@ def run_tests(test_dir, results_dir):
251241

252242
for test_idx, (target, prog) in enumerate(tests):
253243
test_name = f"{target}:{prog}"
254-
safe_name = _namify(prog)
244+
safe_name = namify(prog)
255245
dir_name = f"{test_idx}-{safe_name}"
256246

257247
if test_name in previously_attempted:

contest/hw/tests/test_hw_worker.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
1212

1313
from lib.runner import (find_newest_unseen, mark_all_seen, load_attempted,
14-
mark_attempted, run_tests, DmesgReader, _namify)
14+
mark_attempted, run_tests, DmesgReader)
15+
from lib.nipa import namify
1516

1617

1718
class TestFindNewestUnseen(unittest.TestCase):
@@ -258,19 +259,19 @@ def test_load_attempted_empty_file(self):
258259

259260
class TestNamify(unittest.TestCase):
260261
def test_simple(self):
261-
self.assertEqual(_namify('test_name'), 'test-name')
262+
self.assertEqual(namify('test_name'), 'test-name')
262263

263264
def test_special_chars(self):
264-
self.assertEqual(_namify('test/name.sh'), 'test-name-sh')
265+
self.assertEqual(namify('test/name.sh'), 'test-name-sh')
265266

266267
def test_trailing_dash(self):
267-
self.assertEqual(_namify('test/'), 'test')
268+
self.assertEqual(namify('test/'), 'test')
268269

269270
def test_empty(self):
270-
self.assertEqual(_namify(''), 'no-name')
271+
self.assertEqual(namify(''), 'no-name')
271272

272273
def test_none(self):
273-
self.assertEqual(_namify(None), 'no-name')
274+
self.assertEqual(namify(None), 'no-name')
274275

275276

276277
class TestRunTests(unittest.TestCase):

contest/hw/tests/test_hwksft.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,12 @@ def test_parse_results(self):
263263
output_base = os.path.join(tmpdir, 'test-outputs')
264264

265265
for idx, (target, prog, rc, stdout_text) in enumerate([
266-
('net', 'test1.sh', 0, 'ok 1 test1\n'),
267-
('net', 'test2.sh', 1, 'not ok 1 test2\n'),
266+
('net', 'test1.sh', 0,
267+
'TAP version 13\n1..1\n'
268+
'ok 1 selftests: net: test1.sh\n'),
269+
('net', 'test2.sh', 1,
270+
'TAP version 13\n1..1\n'
271+
'not ok 1 selftests: net: test2.sh\n'),
268272
]):
269273
test_dir = os.path.join(output_base, f'{idx}-{prog.replace(".", "-")}')
270274
os.makedirs(test_dir)

contest/remote/lib/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from .fetcher import Fetcher, namify
44
from .loadavg import wait_loadavg
5-
from .vm import VM, new_vm, guess_indicators
5+
from .vm import VM, new_vm
66
from .cbarg import CbArg
77
from .crash import has_crash, extract_crash
8+
from .results import guess_indicators, result_from_indicators, parse_nested_tests

contest/remote/lib/results.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
3+
"""Shared result parsing helpers for kselftest output.
4+
5+
Functions in this module are used by both vmksft-p (VM testing) and
6+
hwksft (HW testing) to interpret test output.
7+
"""
8+
9+
import re
10+
11+
12+
def guess_indicators(output):
13+
"""Scan test output for pass/fail/skip indicator strings.
14+
15+
Returns a dict with 'pass', 'fail', 'skip' boolean values.
16+
"""
17+
return {
18+
"fail": output.find("[FAIL]") != -1 or output.find("[fail]") != -1 or
19+
output.find(" FAIL:") != -1 or
20+
output.find("\nnot ok 1 selftests: ") != -1 or
21+
output.find("\n# not ok 1") != -1,
22+
"skip": output.find("[SKIP]") != -1 or output.find("[skip]") != -1 or
23+
output.find(" # SKIP") != -1 or output.find("SKIP:") != -1,
24+
"pass": output.find("[OKAY]") != -1 or output.find("[PASS]") != -1 or
25+
output.find("[ OK ]") != -1 or output.find("[OK]") != -1 or
26+
output.find("[ ok ]") != -1 or output.find("[pass]") != -1 or
27+
output.find("PASSED all ") != -1 or
28+
output.find("\nok 1 selftests: ") != -1 or
29+
bool(re.search(
30+
r"# Totals: pass:[1-9]\d* fail:0 (xfail:0 )?(xpass:0 )?skip:0 error:0",
31+
output)),
32+
}
33+
34+
35+
def result_from_indicators(retcode, indicators):
36+
"""Determine test result from return code and output indicators."""
37+
result = 'pass'
38+
if indicators["skip"] or not indicators["pass"]:
39+
result = 'skip'
40+
if retcode == 4:
41+
result = 'skip'
42+
elif retcode:
43+
result = 'fail'
44+
if indicators["fail"]:
45+
result = 'fail'
46+
return result
47+
48+
49+
def parse_nested_tests(full_run, namify_fn, prev_results=None):
50+
"""Parse nested KTAP subtests from test output.
51+
52+
Args:
53+
full_run: full stdout of the test run
54+
namify_fn: function to sanitize test names
55+
prev_results: if not None, this is a retry run — merge results
56+
into prev_results instead of creating new entries
57+
58+
Returns a list of subtest dicts (empty if prev_results is used,
59+
since results are merged in-place).
60+
"""
61+
tests = []
62+
nested_tests = False
63+
64+
result_re = re.compile(
65+
r"(not )?ok (\d+)( -)? ([^#]*[^ ])( +# +)?([^ ].*)?$")
66+
time_re = re.compile(r"time=(\d+)ms")
67+
68+
for line in full_run.split('\n'):
69+
# nested subtests support: we parse the comments from 'TAP version'
70+
if nested_tests:
71+
if line.startswith("# "):
72+
line = line[2:]
73+
else:
74+
nested_tests = False
75+
elif line.startswith("# TAP version "):
76+
nested_tests = True
77+
continue
78+
79+
if not nested_tests:
80+
continue
81+
82+
if line.startswith("ok "):
83+
result = "pass"
84+
elif line.startswith("not ok "):
85+
result = "fail"
86+
else:
87+
continue
88+
89+
v = result_re.match(line).groups()
90+
r = {'test': namify_fn(v[3])}
91+
92+
if len(v) > 5 and v[4] and v[5]:
93+
if v[5].lower().startswith('skip'):
94+
result = "skip"
95+
96+
t = time_re.findall(v[5].lower())
97+
if t:
98+
r['time'] = round(int(t[-1]) / 1000.) # take the last one
99+
100+
r['result'] = result
101+
102+
if prev_results is not None:
103+
for entry in prev_results:
104+
if entry['test'] == r['test']:
105+
entry['retry'] = result
106+
break
107+
else:
108+
# the first run didn't validate this test: add it to the list
109+
r['result'] = 'skip'
110+
r['retry'] = result
111+
prev_results.append(r)
112+
else:
113+
tests.append(r)
114+
115+
# return an empty list when there are prev results: no replacement needed
116+
return tests

contest/remote/lib/vm.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import json
88
import os
99
import psutil
10-
import re
1110
import select
1211
import shutil
1312
import signal
@@ -469,18 +468,3 @@ def new_vm(results_path, vm_id, thr=None, vm=None, config=None, cwd=None):
469468
vm.dump_log(results_path + f'/vm-crashed-{thr_pfx}{vm_id}-{i}')
470469
vm.stop()
471470

472-
473-
def guess_indicators(output):
474-
return {
475-
"fail": output.find("[FAIL]") != -1 or output.find("[fail]") != -1 or \
476-
output.find(" FAIL:") != -1 or \
477-
output.find("\nnot ok 1 selftests: ") != -1 or \
478-
output.find("\n# not ok 1") != -1,
479-
"skip": output.find("[SKIP]") != -1 or output.find("[skip]") != -1 or \
480-
output.find(" # SKIP") != -1 or output.find("SKIP:") != -1,
481-
"pass": output.find("[OKAY]") != -1 or output.find("[PASS]") != -1 or \
482-
output.find("[ OK ]") != -1 or output.find("[OK]") != -1 or \
483-
output.find("[ ok ]") != -1 or output.find("[pass]") != -1 or \
484-
output.find("PASSED all ") != -1 or output.find("\nok 1 selftests: ") != -1 or \
485-
bool(re.search(r"# Totals: pass:[1-9]\d* fail:0 (xfail:0 )?(xpass:0 )?skip:0 error:0", output)),
486-
}

0 commit comments

Comments
 (0)