Skip to content
This repository was archived by the owner on Jan 1, 2026. It is now read-only.

Commit ca9e9a8

Browse files
authored
Include error info from multiple locations (#23)
1 parent 7418a69 commit ca9e9a8

File tree

2 files changed

+132
-49
lines changed

2 files changed

+132
-49
lines changed

cppcheck_junit.py

Lines changed: 66 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import argparse
66
import collections
7+
from dataclasses import dataclass
78
from datetime import datetime
89
import os
910
from socket import gethostname
@@ -14,26 +15,38 @@
1415
from exitstatus import ExitStatus
1516

1617

18+
@dataclass
19+
class CppcheckLocation:
20+
"""
21+
file: Error location file.
22+
line: Error location line.
23+
column: Error location column.
24+
info: Error location info.
25+
"""
26+
27+
file: str
28+
line: int
29+
column: int
30+
info: str
31+
32+
33+
@dataclass
1734
class CppcheckError:
18-
def __init__(
19-
self, file: str, line: int, message: str, severity: str, error_id: str, verbose: str
20-
) -> None:
21-
"""Constructor.
22-
23-
Args:
24-
file: File error originated on.
25-
line: Line error originated on.
26-
message: Error message.
27-
severity: Severity of the error.
28-
error_id: Unique identifier for the error.
29-
verbose: Verbose error message.
30-
"""
31-
self.file = file
32-
self.line = line
33-
self.message = message
34-
self.severity = severity
35-
self.error_id = error_id
36-
self.verbose = verbose
35+
"""
36+
file: File error originated on.
37+
locations: Error locations.
38+
message: Error message.
39+
severity: Severity of the error.
40+
error_id: Unique identifier for the error.
41+
verbose: Verbose error message.
42+
"""
43+
44+
file: str
45+
locations: List[CppcheckLocation]
46+
message: str
47+
severity: str
48+
error_id: str
49+
verbose: str
3750

3851

3952
def parse_arguments() -> argparse.Namespace:
@@ -80,21 +93,27 @@ def parse_cppcheck(file_name: str) -> Dict[str, List[CppcheckError]]:
8093

8194
errors = collections.defaultdict(list)
8295
for error_element in error_root:
83-
location_element: ElementTree.Element = error_element.find("location")
84-
if location_element is not None:
85-
file = location_element.get("file")
86-
line = int(location_element.get("line"))
87-
else:
88-
file = ""
89-
line = 0
96+
file = error_element.get("file0", "")
97+
locations = []
98+
for location in error_element.findall("location"):
99+
if not file:
100+
file = location.get("file", "")
101+
locations.append(
102+
CppcheckLocation(
103+
location.get("file"),
104+
int(location.get("line", 0)),
105+
int(location.get("column", 0)),
106+
location.get("info", ""),
107+
)
108+
)
90109

91110
error = CppcheckError(
92111
file=file,
93-
line=line,
94-
message=error_element.get("msg"),
95-
severity=error_element.get("severity"),
96-
error_id=error_element.get("id"),
97-
verbose=error_element.get("verbose"),
112+
locations=locations,
113+
message=error_element.get("msg", ""),
114+
severity=error_element.get("severity", ""),
115+
error_id=error_element.get("id", ""),
116+
verbose=error_element.get("verbose", ""),
98117
)
99118
errors[error.file].append(error)
100119

@@ -128,14 +147,26 @@ def generate_test_suite(errors: Dict[str, List[CppcheckError]]) -> ElementTree.E
128147
time=str(1),
129148
)
130149
for error in errors:
131-
ElementTree.SubElement(
150+
error_element = ElementTree.SubElement(
132151
test_case,
133152
"error",
134-
type="",
153+
type=error.severity,
135154
file=os.path.relpath(error.file) if error.file else "",
136-
line=str(error.line),
137-
message=f"{error.line}: ({error.severity}) {error.message}",
155+
message=f"{error.message}",
138156
)
157+
if len(error.locations) == 0:
158+
error_element.text = error.verbose
159+
elif len(error.locations) == 1 and error.locations[0].info == "":
160+
location = error.locations[0]
161+
file = os.path.relpath(location.file) if location.file else ""
162+
error_element.text = f"{file}:{location.line}:{location.column}: {error.verbose}"
163+
else:
164+
error_element.text = error.verbose
165+
for location in error.locations:
166+
file = os.path.relpath(location.file) if location.file else ""
167+
error_element.text += (
168+
f"\n{file}:{location.line}:{location.column}: {location.info}"
169+
)
139170

140171
return ElementTree.ElementTree(test_suite)
141172

test.py

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from cppcheck_junit import (
1212
CppcheckError,
13+
CppcheckLocation,
1314
generate_single_success_test_suite,
1415
generate_test_suite,
1516
parse_arguments,
@@ -27,13 +28,13 @@ def test_bad(self) -> None:
2728
errors = parse_cppcheck("tests/cppcheck-out-bad.xml")
2829

2930
self.assertEqual(errors[file1][0].file, file1)
30-
self.assertEqual(errors[file1][0].line, 4)
31+
self.assertEqual(errors[file1][0].locations[0].line, 4)
3132
self.assertEqual(
3233
errors[file1][0].message, "Variable 'a' is assigned a value that is never used."
3334
)
3435

3536
self.assertEqual(errors[file1][1].file, file1)
36-
self.assertEqual(errors[file1][1].line, 4)
37+
self.assertEqual(errors[file1][1].locations[0].line, 4)
3738
self.assertEqual(
3839
errors[file1][1].message, "Array 'a[10]' accessed at index 10, which is out of bounds."
3940
)
@@ -45,7 +46,7 @@ def test_no_location_element(self) -> None:
4546
self.assertEqual(len(errors), 1)
4647
error = errors[file][0]
4748
self.assertEqual(error.file, file)
48-
self.assertEqual(error.line, 0)
49+
self.assertEqual(error.locations, [])
4950
self.assertEqual(
5051
error.message,
5152
"Too many #ifdef configurations - cppcheck only checks 12 configurations. "
@@ -61,7 +62,7 @@ def test_missing_include_no_location_element(self) -> None:
6162
self.assertEqual(len(errors), 1)
6263
error = errors[file][0]
6364
self.assertEqual(error.file, file)
64-
self.assertEqual(error.line, 0)
65+
self.assertEqual(error.locations, [])
6566
self.assertEqual(
6667
error.message,
6768
"Cppcheck cannot find all the include files (use --check-config for details)",
@@ -78,25 +79,25 @@ def test_all(self) -> None:
7879
errors = parse_cppcheck("tests/cppcheck-out-all.xml")
7980

8081
self.assertEqual(errors[file1][0].file, file1)
81-
self.assertEqual(errors[file1][0].line, 4)
82+
self.assertEqual(errors[file1][0].locations[0].line, 4)
8283
self.assertEqual(
8384
errors[file1][0].message, "Variable 'a' is assigned a value that is never used."
8485
)
8586

8687
self.assertEqual(errors[file1][1].file, file1)
87-
self.assertEqual(errors[file1][1].line, 4)
88+
self.assertEqual(errors[file1][1].locations[0].line, 4)
8889
self.assertEqual(
8990
errors[file1][1].message, "Array 'a[10]' accessed at index 10, which is out of bounds."
9091
)
9192

9293
self.assertEqual(errors[file2][0].file, file2)
93-
self.assertEqual(errors[file2][0].line, 4)
94+
self.assertEqual(errors[file2][0].locations[0].line, 4)
9495
self.assertEqual(
9596
errors[file2][0].message, "Variable 'a' is assigned a value that is never used."
9697
)
9798

9899
self.assertEqual(errors[file2][1].file, file2)
99-
self.assertEqual(errors[file2][1].line, 4)
100+
self.assertEqual(errors[file2][1].locations[0].line, 4)
100101
self.assertEqual(
101102
errors[file2][1].message, "Array 'a[10]' accessed at index 10, which is out of bounds."
102103
)
@@ -136,7 +137,7 @@ def test_single(self) -> None:
136137
"file_name": [
137138
CppcheckError(
138139
"file_name",
139-
4,
140+
[CppcheckLocation("file_name", 4, 0, "")],
140141
"error message",
141142
"severity",
142143
"error_id",
@@ -161,8 +162,9 @@ def test_single(self) -> None:
161162

162163
error_element = testcase_element.find("error")
163164
self.assertEqual(error_element.get("file"), "file_name")
164-
self.assertEqual(error_element.get("line"), str(4))
165-
self.assertEqual(error_element.get("message"), "4: (severity) error message")
165+
self.assertEqual(error_element.get("type"), "severity")
166+
self.assertEqual(error_element.get("message"), "error message")
167+
self.assertEqual(error_element.text, "file_name:4:0: verbose error message")
166168
# Check that error element is compliant with the spec
167169
for required_attribute in self.junit_error_attributes:
168170
self.assertTrue(required_attribute in error_element.attrib.keys())
@@ -172,7 +174,7 @@ def test_missing_file(self) -> None:
172174
"": [
173175
CppcheckError(
174176
file="",
175-
line=0,
177+
locations=[],
176178
message="Too many #ifdef configurations - cppcheck only checks "
177179
"12 configurations. Use --force to check all "
178180
"configurations. For more details, use "
@@ -206,14 +208,64 @@ def test_missing_file(self) -> None:
206208

207209
error_element = testcase_element.find("error")
208210
self.assertEqual(error_element.get("file"), "")
209-
self.assertEqual(error_element.get("line"), str(0))
210211
self.assertEqual(
211212
error_element.get("message"),
212-
"0: (information) Too many #ifdef configurations - cppcheck only checks "
213+
"Too many #ifdef configurations - cppcheck only checks "
213214
"12 configurations. Use --force to check all "
214215
"configurations. For more details, use "
215216
"--enable=information.",
216217
)
218+
self.assertEqual(
219+
error_element.text,
220+
"The checking of the file will be interrupted because "
221+
"there are too many #ifdef configurations. Checking of "
222+
"all #ifdef configurations can be forced by --force "
223+
"command line option or from GUI preferences. However "
224+
"that may increase the checking time. For more details, "
225+
"use --enable=information.",
226+
)
227+
# Check that error element is compliant with the spec
228+
for required_attribute in self.junit_error_attributes:
229+
self.assertTrue(required_attribute in error_element.attrib.keys())
230+
231+
def test_multiple(self) -> None:
232+
errors = {
233+
"file_name": [
234+
CppcheckError(
235+
"file_name",
236+
[
237+
CppcheckLocation("file_name", 4, 0, "info"),
238+
CppcheckLocation("file_name", 5, 0, "info"),
239+
],
240+
"error message",
241+
"severity",
242+
"error_id",
243+
"verbose error message",
244+
)
245+
]
246+
}
247+
tree = generate_test_suite(errors)
248+
testsuite_element = tree.getroot()
249+
self.assertEqual(testsuite_element.get("errors"), str(1))
250+
self.assertEqual(testsuite_element.get("failures"), str(0))
251+
self.assertEqual(testsuite_element.get("tests"), str(1))
252+
# Check that testsuite element is compliant with the spec
253+
for required_attribute in self.junit_testsuite_attributes:
254+
self.assertTrue(required_attribute in testsuite_element.attrib.keys())
255+
256+
testcase_element = testsuite_element.find("testcase")
257+
self.assertEqual(testcase_element.get("name"), "file_name")
258+
# Check that test_case is compliant with the spec
259+
for required_attribute in self.junit_testcase_attributes:
260+
self.assertTrue(required_attribute in testcase_element.attrib.keys())
261+
262+
error_element = testcase_element.find("error")
263+
self.assertEqual(error_element.get("file"), "file_name")
264+
self.assertEqual(error_element.get("type"), "severity")
265+
self.assertEqual(error_element.get("message"), "error message")
266+
self.assertEqual(
267+
error_element.text, "verbose error message\nfile_name:4:0: info\nfile_name:5:0: info"
268+
)
217269
# Check that error element is compliant with the spec
218270
for required_attribute in self.junit_error_attributes:
219271
self.assertTrue(required_attribute in error_element.attrib.keys())

0 commit comments

Comments
 (0)