Skip to content

Commit 469dcc9

Browse files
author
Ryan Roberts
committed
automated: kselftest: Rewrite parser to support nesting
The previous kselftests TAP parser has a few shortcomings that prevent it from working well with the mm kselftests output. mm kselftests are nested up to 3 levels deep, with the most important information, from the user's perspective, usually contained in the middle level. But the parser isn't nesting-aware, and instead flattens test results to include the first and last level of nesting and ignroed everything in the middle. This leads to undescriptive and confusing test names in kernelci UI. Additionally, conflicting test names extracted by the parser are not made unique, which leads to multiple distinct tests being collapsed into a single test within the kernelci UI, and its status is set to the last occurence of the test in the list. So you might have multiple instances that fail, but if the last one passes, it is shown as a single test that passes. This problem is compounded by the parser's inability to properly nest because the important middle level information is lost and this makes many more test names look identical, so even more get collapsed into one. Solve all of this by rewriting the parser to properly support recursive parsing. The tree of tests are then flattened into a test list in depth-first order, where the test name is built from the name of each level. Further, if duplicate test names exist, append a "_dup<N>" to the second instance onwards, where N is a unique number. This guarrantees that all test points output by TAP appear in kernelci UI. I've tested this against the output for arm64, ftrace, kvm, and sigstack kselftests (which don't have much nesting so work fine with the old parser): The outputs from both parsers are identical, except in a couple of instances where there are duplicate test name outputs and the new parser appends the "_dup<N>" suffix to make it unique. I've also tested this against the output from the mm kselftests: The output from the new parser is as expected, and much more useful than the old parser. The downside is that this implementation depends on the tap.py module (https://tappy.readthedocs.io). It is packaged for Debian and Ubuntu, so I've added that package as a dependency. But I couldn't find anything for Centos or Fedora, so this module (and its dependencies) will likely need to be installed from PyPI in these environments: $ pip3 install tap.py more-itertools pyyaml Signed-off-by: Ryan Roberts <ryan.roberts@arm.com>
1 parent 0a0c385 commit 469dcc9

File tree

2 files changed

+69
-22
lines changed

2 files changed

+69
-22
lines changed

automated/linux/kselftest/kselftest.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ install() {
135135
dist_name
136136
# shellcheck disable=SC2154
137137
case "${dist}" in
138-
debian|ubuntu) install_deps "sed perl wget xz-utils iproute2" "${SKIP_INSTALL}" ;;
138+
debian|ubuntu) install_deps "sed perl wget xz-utils iproute2 python3-tap" "${SKIP_INSTALL}" ;;
139139
centos|fedora) install_deps "sed perl wget xz iproute" "${SKIP_INSTALL}" ;;
140140
unknown) warn_msg "Unsupported distro: package install skipped" ;;
141141
esac
+68-21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python3
22
import sys
33
import re
4+
from tap import parser
45

56

67
def slugify(line):
@@ -11,25 +12,71 @@ def slugify(line):
1112
)
1213

1314

14-
tests = ""
15-
for line in sys.stdin:
16-
if "# selftests: " in line:
17-
tests = slugify(line.replace("\n", "").split("selftests:")[1])
18-
elif re.search(r"^.*?not ok \d{1,5} ", line):
19-
match = re.match(r"^.*?not ok [0-9]+ (.*?)$", line)
20-
ascii_test_line = slugify(re.sub("# .*$", "", match.group(1)))
21-
output = f"{tests}_{ascii_test_line} fail"
22-
if f"selftests_{tests}" in output:
23-
output = re.sub(r"^.*_selftests_", "", output)
24-
print(f"{output}")
25-
elif re.search(r"^.*?ok \d{1,5} ", line):
26-
match = re.match(r"^.*?ok [0-9]+ (.*?)$", line)
27-
if "# skip" in match.group(1).lower():
28-
ascii_test_line = slugify(re.sub("# skip", "", match.group(1).lower()))
29-
output = f"{tests}_{ascii_test_line} skip"
15+
def parse_nested_tap(string):
16+
results = []
17+
18+
def uncomment(line):
19+
# All of the input lines should be comments and begin with #, but let's
20+
# be cautious; don't do anything if the line doesn't begin with #.
21+
if len(line) > 0 and line[0] == "#":
22+
return line[1:].strip()
23+
return line
24+
25+
def make_name(name, directive, ok, skip):
26+
# Some of this is to maintain compatibility with the old parser.
27+
if name.startswith("selftests:"):
28+
name = name[10:]
29+
if ok and skip and directive.lower().startswith("skip"):
30+
directive = directive[4:]
3031
else:
31-
ascii_test_line = slugify(match.group(1))
32-
output = f"{tests}_{ascii_test_line} pass"
33-
if f"selftests_{tests}" in output:
34-
output = re.sub(r"^.*_selftests_", "", output)
35-
print(f"{output}")
32+
directive = ""
33+
name = f"{name} {directive}".strip()
34+
if name == "":
35+
name = "<unknown>"
36+
return slugify(name)
37+
38+
def make_result(ok, skip):
39+
return ("skip" if skip else "pass") if ok else "fail"
40+
41+
output = ""
42+
ps = parser.Parser()
43+
for l in ps.parse_text(string):
44+
if l.category == "test":
45+
results.append(
46+
{
47+
"name": make_name(l.description, l.directive.text, l.ok, l.skip),
48+
"result": make_result(l.ok, l.skip),
49+
"children": parse_nested_tap(output),
50+
}
51+
)
52+
output = ""
53+
elif l.category == "diagnostic":
54+
output += f"{uncomment(l.text)}\n"
55+
56+
return results
57+
58+
59+
def flatten_results(prefix, results):
60+
ret = []
61+
for r in results:
62+
test = f"{prefix}{r['name']}"
63+
children = flatten_results(f"{test}_", r["children"])
64+
ret += children + [{"name": test, "result": r["result"]}]
65+
return ret
66+
67+
68+
def make_names_unique(results):
69+
namecounts = {}
70+
for r in results:
71+
name = r["name"]
72+
namecounts[name] = namecounts.get(name, 0) + 1
73+
if namecounts[name] > 1:
74+
r["name"] += f"_dup{namecounts[name]}"
75+
76+
77+
if __name__ == "__main__":
78+
results = parse_nested_tap(sys.stdin.read())
79+
results = flatten_results("", results)
80+
make_names_unique(results)
81+
for r in results:
82+
print(f"{r['name']} {r['result']}")

0 commit comments

Comments
 (0)