Skip to content

Commit 94cdc77

Browse files
committed
light/valgrind: Process and create reports from valgrind outputs
Signed-off-by: Andras Mitzki <andras.mitzki@axoflow.com>
1 parent 12eef1d commit 94cdc77

File tree

2 files changed

+196
-0
lines changed

2 files changed

+196
-0
lines changed

.github/workflows/devshell_light_valgrind.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ jobs:
9090
working-directory: ./build
9191
run: |
9292
make light-valgrind-check
93+
python3 scripts/process_valgrind_output.py
94+
cp definitely_lost_blocks.txt reports/
95+
cp errors_in_context.txt reports/
9396
9497
- name: "Prepare artifact: light-reports"
9598
id: prepare-light-reports
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
#!/usr/bin/env python
2+
#############################################################################
3+
# Copyright (c) 2025 Axoflow
4+
# Copyright (c) 2025 Andras Mitzki <andras.mitzki@axoflow.com>
5+
#
6+
# This program is free software: you can redistribute it and/or modify it
7+
# under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
#
19+
# As an additional exemption you are allowed to compile & link against the
20+
# OpenSSL libraries as published by the OpenSSL project. See the file
21+
# COPYING for details.
22+
#
23+
#############################################################################
24+
import os
25+
import re
26+
from typing import Iterable
27+
from typing import List
28+
29+
# precompiled patterns
30+
definitely_pat = re.compile(r'definitely lost in', re.IGNORECASE)
31+
generic_pat = re.compile(r'bytes in ')
32+
errors_in_context_pat = re.compile(r'errors in context', re.IGNORECASE)
33+
generic_errors_in_context_pat = re.compile(r'== $', re.IGNORECASE)
34+
_pid_re = re.compile(r'==\d+==\s*')
35+
_exclude_re = re.compile(r'blocks are definitely lost in loss record')
36+
37+
38+
def find_all_valgrind_logs(root_dir: str) -> List[str]:
39+
"""Return list of files under root_dir whose names end with '_valgrind_output'."""
40+
valgrind_logs = []
41+
for dirpath, _, filenames in os.walk(root_dir):
42+
for filename in filenames:
43+
if filename.endswith("_valgrind_output"):
44+
valgrind_logs.append(os.path.join(dirpath, filename))
45+
return valgrind_logs
46+
47+
48+
def file_contains_definitely_lost(file_path: str) -> bool:
49+
"""Quick scan to decide whether a file has 'definitely lost' (avoids loading large files unnecessarily)."""
50+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
51+
for line in f:
52+
if 'definitely lost' in line:
53+
return True
54+
return False
55+
56+
57+
def file_contains_errors_in_context(file_path: str) -> bool:
58+
"""Quick scan to decide whether a file has 'errors in context'."""
59+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
60+
for line in f:
61+
if 'errors in context' in line:
62+
return True
63+
return False
64+
65+
66+
def extract_definitely_lost_blocks_from_lines(lines: Iterable[str]) -> List[List[str]]:
67+
"""
68+
Parse lines and return a list of 'definitely lost' blocks.
69+
"""
70+
blocks: List[List[str]] = []
71+
current: List[str] | None = None
72+
73+
for line in lines:
74+
if definitely_pat.search(line):
75+
if current:
76+
blocks.append(current)
77+
current = [line.strip()]
78+
continue
79+
80+
if generic_pat.search(line):
81+
if current:
82+
blocks.append(current)
83+
current = None
84+
continue
85+
86+
if current is not None:
87+
current.append(line.strip())
88+
89+
if current:
90+
blocks.append(current)
91+
92+
return blocks
93+
94+
95+
def extract_errors_in_context_blocks_from_lines(lines: Iterable[str]) -> List[List[str]]:
96+
"""
97+
Parse lines and return a list of 'errors in context' blocks.
98+
"""
99+
blocks: List[List[str]] = []
100+
current: List[str] | None = None
101+
102+
for line in lines:
103+
if errors_in_context_pat.search(line):
104+
if current:
105+
blocks.append(current)
106+
current = [line.strip()]
107+
continue
108+
109+
if generic_errors_in_context_pat.search(line):
110+
if current:
111+
blocks.append(current)
112+
current = None
113+
continue
114+
115+
if current is not None:
116+
current.append(line.strip())
117+
118+
if current:
119+
blocks.append(current)
120+
121+
return blocks
122+
123+
124+
def extract_definitely_lost_blocks_from_file(file_path: str) -> List[List[str]]:
125+
"""Open file and extract blocks if it contains 'definitely lost'."""
126+
if not file_contains_definitely_lost(file_path):
127+
return []
128+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
129+
return extract_definitely_lost_blocks_from_lines(f)
130+
131+
132+
def extract_errors_in_context_blocks_from_file(file_path: str) -> List[List[str]]:
133+
"""Extract blocks containing 'errors in context' from a file."""
134+
if not file_contains_errors_in_context(file_path):
135+
return []
136+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
137+
return extract_errors_in_context_blocks_from_lines(f)
138+
139+
140+
def _normalize_line(line: str) -> str:
141+
"""Remove Valgrind PID markers like '==606373== ' and trim."""
142+
return _pid_re.sub('', line).strip()
143+
144+
145+
def dedupe_blocks(blocks: List[List[str]]) -> List[List[str]]:
146+
"""
147+
Deduplicate blocks based on their content, ignoring metadata lines.
148+
Returns a list of unique blocks.
149+
"""
150+
151+
content_map = {}
152+
for block in blocks:
153+
meta = _normalize_line(block[0])
154+
block_content = tuple(_normalize_line(line) for line in block[1:] if not line.endswith("=="))
155+
if block_content not in content_map:
156+
content_map.update({block_content: []})
157+
content_map[block_content].append(meta)
158+
159+
return content_map
160+
161+
162+
def write_file(file_path: str, blocks: List[List[str]]) -> None:
163+
"""Write blocks to a file."""
164+
with open(file_path, 'w', encoding='utf-8') as f:
165+
block_counter = 0
166+
for block_content, metas in blocks.items():
167+
block_counter += 1
168+
all_bytes = []
169+
for meta in metas:
170+
all_bytes.append(int(meta.split(' ')[0].strip().replace(',', '')))
171+
172+
f.write(f"\nBlock {block_counter}, number of occurrences in all tests: {len(metas)}, leaked bytes:\n {list(reversed(sorted(all_bytes)))}\n")
173+
for line in block_content:
174+
f.write(f"{line}\n")
175+
176+
177+
def main(root_dir: str) -> None:
178+
files = find_all_valgrind_logs(root_dir)
179+
assert files, f"No valgrind log files found in {root_dir}"
180+
all_definitely_lost_blocks: List[List[str]] = []
181+
all_errors_in_context_blocks: List[List[str]] = []
182+
for file in files:
183+
all_definitely_lost_blocks.extend(extract_definitely_lost_blocks_from_file(file))
184+
all_errors_in_context_blocks.extend(extract_errors_in_context_blocks_from_file(file))
185+
unique_definitely_lost_blocks = dedupe_blocks(all_definitely_lost_blocks)
186+
write_file("definitely_lost_blocks.txt", unique_definitely_lost_blocks)
187+
188+
unique_errors_in_context_blocks = dedupe_blocks(all_errors_in_context_blocks)
189+
write_file("errors_in_context_blocks.txt", unique_errors_in_context_blocks)
190+
191+
192+
if __name__ == "__main__":
193+
main("./reports/")

0 commit comments

Comments
 (0)