Skip to content

Commit 499ff56

Browse files
authored
feat: Add script for reporting maintenance chart pass/fail coverage (#1008)
1 parent b7ef525 commit 499ff56

1 file changed

Lines changed: 211 additions & 0 deletions

File tree

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
"""Maintenance Chart Checker
2+
3+
This script analyzes maintenance charts in reStructuredText (.rst) files. It recursively scans
4+
through directories looking for .rst files containing maintenance charts in the format:
5+
6+
**Maintenance chart**
7+
8+
+--------------+-------------------------------+----------------+--------------------------------+
9+
| Review Date | Working Group Reviewer | Release |Test situation |
10+
+--------------+-------------------------------+----------------+--------------------------------+
11+
| 2025-04-13 | reviewer | Release | Pass/Fail |
12+
+--------------+-------------------------------+----------------+--------------------------------+
13+
14+
The script generates a summary report showing:
15+
- Total number of files with maintenance charts in each directory
16+
- Number of empty maintenance charts
17+
- Number of charts with passing test situations
18+
- Number of charts with failing test situations
19+
20+
Usage:
21+
# Run from current directory (default depth=1)
22+
python maintenance_chart_checker.py
23+
24+
# Run from specific directory with custom depth
25+
python maintenance_chart_checker.py path/to/docs --depth 2
26+
27+
# Show full directory paths (depth=0)
28+
python maintenance_chart_checker.py --depth 0
29+
30+
# Show help
31+
python maintenance_chart_checker.py -h
32+
33+
Example output with depth=1:
34+
------------------------------------------------------------
35+
| Folder | Total | Empty | Passed | Failed |
36+
------------------------------------------------------------
37+
| educators | 12 | 3 | 5 | 4 |
38+
| faculty | 8 | 1 | 4 | 3 |
39+
------------------------------------------------------------
40+
41+
Example output with depth=2:
42+
------------------------------------------------------------
43+
| Folder | Total | Empty | Passed | Failed |
44+
------------------------------------------------------------
45+
| educators/how-tos | 8 | 2 | 4 | 2 |
46+
| educators/reference | 4 | 1 | 1 | 2 |
47+
| faculty/guides | 8 | 1 | 4 | 3 |
48+
------------------------------------------------------------
49+
"""
50+
51+
import os
52+
import argparse
53+
from typing import Dict, List, NamedTuple
54+
from collections import defaultdict
55+
56+
class MaintenanceStatus(NamedTuple):
57+
is_empty: bool
58+
has_fail: bool
59+
has_pass: bool
60+
file_path: str
61+
62+
def parse_maintenance_chart(content: str) -> MaintenanceStatus | None:
63+
"""Parse the maintenance chart in the content and return its status."""
64+
# Check if maintenance chart exists
65+
if "**Maintenance chart**" not in content:
66+
return None
67+
68+
# Split content into lines and find the maintenance chart section
69+
lines = content.split('\n')
70+
try:
71+
chart_start = next(i for i, line in enumerate(lines) if "**Maintenance chart**" in line)
72+
except StopIteration:
73+
return None
74+
75+
# Find both header separator lines
76+
try:
77+
separators = [i for i in range(chart_start, len(lines))
78+
if '+-' in lines[i] and '-+' in lines[i]]
79+
if len(separators) < 2: # Need both top and bottom separator
80+
return None
81+
first_data_idx = separators[1] + 1 # Take the line after the second separator
82+
except (StopIteration, IndexError):
83+
return None
84+
85+
# Get the first data row
86+
if first_data_idx >= len(lines):
87+
return None
88+
89+
first_data_row = lines[first_data_idx].strip()
90+
91+
# Check if the row is empty (just separators and spaces)
92+
is_empty = all(cell.strip() in ['|', '', ' '] for cell in first_data_row.split('|'))
93+
94+
# Look for pass/fail in test situation column (last column)
95+
columns = [col.strip().lower() for col in first_data_row.split('|') if col.strip()]
96+
if not columns: # Empty row
97+
return MaintenanceStatus(True, False, False, "")
98+
99+
# The test situation is in the last column
100+
test_situation = columns[-1] if len(columns) >= 4 else ""
101+
has_fail = 'fail' in test_situation.lower()
102+
has_pass = 'pass' in test_situation.lower()
103+
104+
return MaintenanceStatus(is_empty, has_fail, has_pass, "")
105+
106+
def get_folder_path(path: str, start_path: str, depth: int) -> str:
107+
"""Get the directory path at specified depth relative to start_path."""
108+
rel_path = os.path.relpath(path, start_path)
109+
if depth == 0:
110+
return rel_path
111+
parts = rel_path.split(os.sep)
112+
return os.sep.join(parts[:depth]) if depth < len(parts) else rel_path
113+
114+
def scan_directory(start_path: str, depth: int) -> Dict[str, List[MaintenanceStatus]]:
115+
"""Recursively scan directory for .rst files and analyze maintenance charts."""
116+
results = defaultdict(list)
117+
118+
for root, _, files in os.walk(start_path):
119+
rst_files = [f for f in files if f.endswith('.rst')]
120+
if not rst_files:
121+
continue
122+
123+
for file in rst_files:
124+
file_path = os.path.join(root, file)
125+
try:
126+
with open(file_path, 'r', encoding='utf-8') as f:
127+
content = f.read()
128+
status = parse_maintenance_chart(content)
129+
if status is not None: # Only include files with maintenance charts
130+
folder_path = get_folder_path(root, start_path, depth)
131+
results[folder_path].append(MaintenanceStatus(
132+
status.is_empty,
133+
status.has_fail,
134+
status.has_pass,
135+
file_path
136+
))
137+
except Exception as e:
138+
print(f"Error processing {file_path}: {e}")
139+
140+
return results
141+
142+
def print_report(results: Dict[str, List[MaintenanceStatus]]):
143+
"""Print a formatted report table of the maintenance chart analysis."""
144+
print("\nMaintenance Chart Analysis Report")
145+
print("================================\n")
146+
147+
# Print table header
148+
header = "| {:<30} | {:>8} | {:>8} | {:>8} | {:>8} |".format(
149+
"Folder", "Total", "Empty", "Passed", "Failed"
150+
)
151+
separator = "-" * len(header)
152+
153+
print(separator)
154+
print(header)
155+
print(separator)
156+
157+
# Print table rows
158+
for folder, statuses in sorted(results.items()):
159+
total = len(statuses)
160+
empty = sum(1 for s in statuses if s.is_empty)
161+
passed = sum(1 for s in statuses if s.has_pass)
162+
failed = sum(1 for s in statuses if s.has_fail)
163+
164+
row = "| {:<30} | {:>8} | {:>8} | {:>8} | {:>8} |".format(
165+
folder, total, empty, passed, failed
166+
)
167+
print(row)
168+
169+
print(separator)
170+
171+
def main():
172+
parser = argparse.ArgumentParser(
173+
description='Analyze maintenance charts in .rst files.',
174+
formatter_class=argparse.RawDescriptionHelpFormatter,
175+
epilog="""
176+
Examples:
177+
%(prog)s # Run from current directory (depth=1)
178+
%(prog)s path/to/docs # Run from specified directory
179+
%(prog)s --depth 2 # Group by two directory levels
180+
%(prog)s --depth 0 # Show full directory paths
181+
%(prog)s -h # Show this help message
182+
"""
183+
)
184+
185+
parser.add_argument(
186+
'start_dir',
187+
nargs='?',
188+
default='.',
189+
help='Directory to start scanning from (default: current directory)'
190+
)
191+
192+
parser.add_argument(
193+
'--depth', '-d',
194+
type=int,
195+
default=1,
196+
help='Number of directory levels to show in report (0 for full path, default: 1)'
197+
)
198+
199+
args = parser.parse_args()
200+
201+
# Convert to absolute path and verify directory exists
202+
start_path = os.path.abspath(args.start_dir)
203+
if not os.path.isdir(start_path):
204+
print(f"Error: Directory '{args.start_dir}' does not exist")
205+
return
206+
207+
results = scan_directory(start_path, args.depth)
208+
print_report(results)
209+
210+
if __name__ == "__main__":
211+
main()

0 commit comments

Comments
 (0)