Skip to content

Commit 19c2685

Browse files
committed
tests: add a better test for maintainers file coverage
Add a test which checks if new files added need a MAINTAINERS entry. Signed-off-by: Jakub Kicinski <[email protected]>
1 parent c700a1c commit 19c2685

File tree

6 files changed

+207
-53
lines changed

6 files changed

+207
-53
lines changed

tests/patch/cc_maintainers/test.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22
#
33
# Copyright (c) 2020 Facebook
44

5-
from typing import Tuple
5+
"""
6+
Test if relevant maintainers were CCed
7+
"""
8+
69
import datetime
710
import email
811
import email.utils
9-
import subprocess
10-
import tempfile
12+
import json
1113
import os
1214
import re
13-
import json
14-
""" Test if relevant maintainers were CCed """
15+
import subprocess
16+
import tempfile
17+
from typing import Tuple
1518

1619
emailpat = re.compile(r'([^ <"]*@[^ >"]*)')
1720

@@ -39,6 +42,9 @@
3942
local_map = ["Vladimir Oltean <[email protected]> <[email protected]>",
4043
"Alexander Duyck <[email protected]> <[email protected]>"]
4144

45+
#
46+
# Maintainer auto-staleness checking
47+
#
4248

4349
class StalenessEntry:
4450
def __init__(self, e, since_months):
@@ -132,14 +138,18 @@ def get_stale(sender_from, missing, out):
132138
ret.add(e)
133139
return ret
134140

141+
#
142+
# Main
143+
#
135144

136145
def cc_maintainers(tree, thing, result_dir) -> Tuple[int, str, str]:
146+
""" Main test entry point """
137147
out = []
138148
raw_gm = []
139149
patch = thing
140150

141151
if patch.series and patch.series.cover_pull:
142-
return 0, f"Pull request co-post, skipping", ""
152+
return 0, "Pull request co-post, skipping", ""
143153

144154
msg = email.message_from_string(patch.raw_patch)
145155
addrs = msg.get_all('to', [])

tests/patch/checkpatch/checkpatch.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ MACRO_ARG_REUSE,\
99
ALLOC_SIZEOF_STRUCT,\
1010
NO_AUTHOR_SIGN_OFF,\
1111
GIT_COMMIT_ID,\
12-
CAMELCASE
12+
CAMELCASE,\
13+
FILE_PATH_CHANGES
1314

1415
tmpfile=$(mktemp)
1516

tests/patch/maintainers/info.json

Lines changed: 0 additions & 4 deletions
This file was deleted.

tests/patch/maintainers/maintainers.sh

Lines changed: 0 additions & 42 deletions
This file was deleted.

tests/series/maintainers/info.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"pymod": "test",
3+
"pyfunc": "maintainers"
4+
}

tests/series/maintainers/test.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
3+
""" Test if the MAINTAINERS file needs an update """
4+
5+
import os
6+
import subprocess
7+
from typing import Tuple
8+
9+
#
10+
# Checking for needed new MAINTAINERS entries
11+
#
12+
13+
new_file_ignore_pfx = [ 'Documentation/', 'tools/testing/']
14+
15+
def extract_files(series):
16+
"""Extract paths of new files being added by the series."""
17+
18+
new_files = set()
19+
mod_files = set()
20+
lines = []
21+
for patch in series.patches:
22+
lines += patch.raw_patch.split("\n")
23+
24+
# Walk lines, skip last since it doesn't have next
25+
for i, line in enumerate(lines[:-1]):
26+
next_line = lines[i + 1]
27+
28+
if not next_line.startswith("+++ b/"):
29+
continue
30+
file_path = next_line[6:]
31+
32+
# .startswith() can take a while array of alternatives
33+
if file_path.startswith(tuple(new_file_ignore_pfx)):
34+
continue
35+
36+
if line == "--- /dev/null":
37+
new_files.add(file_path)
38+
else:
39+
mod_files.add(file_path)
40+
41+
# We're testing a series, same file may appear multiple times
42+
mod_files -= new_files
43+
return list(new_files), list(mod_files)
44+
45+
46+
def count_files_for_maintainer_entry(tree, maintainer_entry):
47+
"""Count how many files are covered by a specific maintainer entry."""
48+
patterns = []
49+
50+
# Extract file patterns from the maintainer entry
51+
for line in maintainer_entry.split("\n"):
52+
if line.startswith("F:"):
53+
pattern = line[2:].strip()
54+
patterns.append(pattern)
55+
if not patterns:
56+
return 0
57+
58+
# Count files matching these patterns
59+
total_files = 0
60+
for pattern in patterns:
61+
if pattern[-1] == '/':
62+
where = pattern
63+
what = '*'
64+
elif '/' in pattern:
65+
where = os.path.dirname(pattern)
66+
what = os.path.basename(pattern)
67+
else:
68+
where = "."
69+
what = pattern
70+
cmd = ["find", where, "-name", what, "-type", "f"]
71+
result = subprocess.run(cmd, cwd=tree.path, capture_output=True,
72+
text=True, check=False)
73+
if result.returncode == 0:
74+
total_files += result.stdout.count("\n")
75+
76+
return total_files
77+
78+
79+
def get_maintainer_entry_for_file(tree, file_path):
80+
"""Get the full MAINTAINERS entry for a specific file."""
81+
82+
cmd = ["./scripts/get_maintainer.pl", "--sections", file_path]
83+
result = subprocess.run(cmd, cwd=tree.path, capture_output=True, text=True,
84+
check=False)
85+
86+
if result.returncode == 0:
87+
return result.stdout
88+
return ""
89+
90+
91+
def check_maintainer_coverage(tree, new_files, out):
92+
"""Check if new files should have an MAINTAINERS entry."""
93+
has_miss = False
94+
has_fail = False
95+
has_warn = False
96+
warnings = []
97+
98+
# Ideal entry size is <50. But if someone is adding a Kconfig file,
99+
# chances are they should be a maintainer.
100+
pass_target = 50
101+
if 'Kconfig' in new_files:
102+
pass_target = 3
103+
104+
for file_path in new_files:
105+
out.append("\nChecking coverage for a new file: " + file_path)
106+
107+
maintainer_info = get_maintainer_entry_for_file(tree, file_path)
108+
109+
# This should not happen, Linus catches all
110+
if not maintainer_info.strip():
111+
warnings.append(f"Failed to fetch MAINTAINERS for {file_path}")
112+
has_warn = True
113+
continue
114+
115+
# Parse the maintainer sections
116+
sections = []
117+
current_section = []
118+
119+
prev = ""
120+
for line in maintainer_info.split("\n"):
121+
if len(line) > 1 and line[1] == ':':
122+
if not current_section:
123+
current_section = [prev]
124+
current_section.append(line)
125+
elif len(line) < 2:
126+
if current_section:
127+
sections.append("\n".join(current_section))
128+
current_section = []
129+
prev = line
130+
131+
if current_section:
132+
sections.append("\n".join(current_section))
133+
134+
# Check each maintainer section
135+
min_cnt = 999999
136+
for section in sections:
137+
name = section.split("\n")[0]
138+
# Count files for this maintainer entry
139+
file_count = count_files_for_maintainer_entry(tree, section)
140+
out.append(f" Section {name} covers ~{file_count} files")
141+
142+
if 0 < file_count < pass_target:
143+
out.append("PASS")
144+
break
145+
min_cnt = min(min_cnt, file_count)
146+
else:
147+
# Intel and nVidia drivers have 400+ files, just warn for these
148+
# sort of sizes. More files than 500 means we fell down to subsystem
149+
# level of entries.
150+
out.append(f" MIN {min_cnt}")
151+
has_miss = True
152+
if min_cnt < 500:
153+
has_warn = True
154+
else:
155+
has_fail = True
156+
157+
if has_miss:
158+
warnings.append("Expecting a new MAINTAINERS entry")
159+
else:
160+
warnings.append("MAINTAINERS coverage looks sufficient")
161+
162+
ret = 0
163+
if has_fail:
164+
ret = 1
165+
elif has_warn:
166+
ret = 250
167+
168+
return ret, "; ".join(warnings)
169+
170+
171+
def maintainers(tree, series, _result_dir) -> Tuple[int, str, str]:
172+
""" Main function / entry point """
173+
174+
# Check for new files in the series
175+
new_files, mod_files = extract_files(series)
176+
177+
ret = 0
178+
log = ["New files:"] + new_files + ["", "Modified files:"] + mod_files
179+
180+
if not new_files:
181+
desc = "No new files, skip"
182+
else:
183+
ret, desc = check_maintainer_coverage(tree, new_files, log)
184+
185+
return ret, desc, "\n".join(log)

0 commit comments

Comments
 (0)