Skip to content

Commit 8fc078f

Browse files
jacob-kellerkuba-moo
authored andcommitted
tests: kdoc: improve test comparison and output
The current algorithm for the kdoc test is a simple line number comparison of the output from ./scripts/kernel-doc. This is not guaranteed to be accurate, as a patch could fix some warnings while introducing others, resulting in the total number of lines not changing. In addition, it is difficult to quickly determine which warnings are new. Historically, a simple "diff" was used to compare the before and after results. This was difficult to parse because line numbers change due to code being added or moved. To fix this, use a set difference algorithm which compares based on data from the warning lines without including the line number. To do this, introduce a KdocWarning dataclass, which represents a warning based on its kind, file, line, and content. Extract these from the lines via a regular expression that looks for the expected pattern of output from ./scripts/kernel-doc. Using a @DataClass allows specifying which fields to compare against. In particular, the line number is not counted. Additionally, the original message as output from ./scripts/kernel-doc is preserved as its own (non-compared) field. Some warnings are spread over two lines, indicated by the first line ending in a semicolon. Handle this when iterating over the output of kernel-doc and converting to the new warning objects. Any output line which doesn't match the regular expression is converted so that its entire message becomes the contents. This ensures that such lines still get counted and still get compared in some form, rather than silently ignored. After obtaining the converted output from ./scripts/kernel-doc, convert the current_warnings and incumbent_warnings lists into sets. Calculate the set of new and removed warnings by iterating the original lists. For new_warnings, find the sequence of warnings which are in the current_warnings but not in the incumbent set. Do the same to calculate the set of removed warnings. This implementation is fast, as it can use the hash-based comparisons for efficient set lookup. Using the original list also preserves the order of the warnings as output by ./scripts/kernel-doc. Calculating both the new and removed counts provides useful data for users of the NIPA tests. It is much easier to see at a glance what warnings were added. Signed-off-by: Jacob Keller <jacob.e.keller@intel.com>
1 parent 3b57fb5 commit 8fc078f

File tree

1 file changed

+117
-3
lines changed

1 file changed

+117
-3
lines changed

tests/patch/kdoc/test.py

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
""" Test if kernel-doc generates new warnings """
44

5+
import collections
6+
import dataclasses
57
import os
8+
import re
69
import subprocess
710
from typing import List, Optional, Tuple
811

@@ -15,7 +18,84 @@ def get_git_head(tree) -> str:
1518

1619
return result.stdout.strip()
1720

18-
def run_kernel_doc(tree, commitish, files):
21+
@dataclasses.dataclass(frozen=True, eq=True, order=True, init=True)
22+
class KdocWarning:
23+
# The original warning message
24+
message : str = dataclasses.field(repr=False, compare=False)
25+
_ : dataclasses.KW_ONLY
26+
# Kind of warning line, determined during init
27+
kind : str = dataclasses.field(repr=True, compare=True)
28+
# The file path, or None if unable to determine
29+
file : Optional[str] = dataclasses.field(repr=True, compare=True)
30+
# The line, or None if unable to determine
31+
# Note: *not* part of comparison, or hash!
32+
line : Optional[int] = dataclasses.field(repr=True, compare=False)
33+
# The content of the warning (excluding kind, file, line)
34+
content : str = dataclasses.field(repr=True, compare=True)
35+
36+
@classmethod
37+
def from_text(self, line, extra=None):
38+
message = line
39+
40+
if extra:
41+
message += '\n' + extra
42+
43+
parser = re.compile(
44+
r"""
45+
^ # Start of string
46+
(?P<kind>warning|error): # Severity
47+
\s+ # Spacing
48+
(?P<file>[/a-z0-9_.-]*): # File path
49+
(?P<line>[0-9]+) # Line number
50+
\s* # Spacing
51+
(?P<content>.*) # Warning content
52+
$ # End of string
53+
""",
54+
re.VERBOSE | re.IGNORECASE)
55+
56+
m = parser.match(line)
57+
if m:
58+
kind = m['kind']
59+
file = m['file']
60+
line = int(m['line'])
61+
content = m['content']
62+
if extra:
63+
content += '\n' + extra
64+
else:
65+
kind = 'Unknown'
66+
file = None
67+
line = None
68+
content = message
69+
70+
return KdocWarning(message, kind=kind, file=file, line=line,
71+
content=content)
72+
73+
def __str__(self):
74+
return self.message
75+
76+
def parse_warnings(lines) -> List[KdocWarning]:
77+
skip = False
78+
length = len(lines)
79+
80+
warnings = []
81+
82+
# Walk through lines and convert to warning objects
83+
for i, line in enumerate(lines):
84+
if skip:
85+
skip = False
86+
continue
87+
88+
if line.endswith(':') and i + 1 < length:
89+
extra = lines[i + 1]
90+
skip = True
91+
else:
92+
extra = None
93+
94+
warnings.append(KdocWarning.from_text(line, extra))
95+
96+
return warnings
97+
98+
def run_kernel_doc(tree, commitish, files) -> List[KdocWarning]:
1999
""" Run ./scripts/kdoc on a given commit and capture its results. """
20100

21101
cmd = ["git", "checkout", "-q", commitish]
@@ -25,7 +105,9 @@ def run_kernel_doc(tree, commitish, files):
25105
result = subprocess.run(cmd, cwd=tree.path, text=True, check=False,
26106
stderr=subprocess.PIPE)
27107

28-
return result.stderr.strip().split('\n')
108+
lines = result.stderr.strip().split('\n')
109+
110+
return parse_warnings(lines)
29111

30112
def extract_files(patch):
31113
"""Extract paths added or modified by the patch."""
@@ -80,13 +162,45 @@ def kdoc(tree, patch, result_dir) -> Tuple[int, str, str]:
80162

81163
return ret, desc, "\n".join(log)
82164

165+
current_set = set(current_warnings)
166+
incumbent_set = set(incumbent_warnings)
167+
168+
# This construction preserves ordering vs using set difference
169+
new_warnings = [x for x in current_warnings if x not in incumbent_set]
170+
rm_warnings = [x for x in incumbent_warnings if x not in current_set]
171+
83172
incumbent_count = len(incumbent_warnings)
84173
current_count = len(current_warnings)
174+
new_count = len(new_warnings)
175+
rm_count = len(rm_warnings)
85176

86177
desc = f'Errors and warnings before: {incumbent_count} This patch: {current_count}'
178+
if new_count:
179+
desc += f' New: {new_count}'
180+
if rm_count:
181+
desc += f' Removed: {rm_count}'
87182
log += ["", desc]
88183

89-
if current_count > incumbent_count:
184+
if rm_count:
185+
log += ["", "Warnings removed:"]
186+
log.extend(map(str, rm_warnings))
187+
188+
file_breakdown = collections.Counter((x.file for x in rm_warnings))
189+
190+
log += ["Per-file breakdown:"]
191+
for f, count in file_breakdown.items():
192+
log += [f'{count:6} {f}']
193+
194+
if new_count:
90195
ret = 1
91196

197+
log += ["", "New warnings added:"]
198+
log.extend(map(str, new_warnings))
199+
200+
file_breakdown = collections.Counter((x.file for x in new_warnings))
201+
202+
log += ["Per-file breakdown:"]
203+
for f, count in file_breakdown.items():
204+
log += [f'{count:6} {f}']
205+
92206
return ret, desc, "\n".join(log)

0 commit comments

Comments
 (0)