Coverage for lobster/tools/gtest/gtest.py: 0%
127 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-15 06:41 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-15 06:41 +0000
1#!/usr/bin/env python3
2#
3# lobster_gtest - Extract GoogleTest tracing tags for LOBSTER
4# Copyright (C) 2022-2026 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU Affero General Public License as
8# published by the Free Software Foundation, either version 3 of the
9# License, or (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14# Affero General Public License for more details.
15#
16# You should have received a copy of the GNU Affero General Public
17# License along with this program. If not, see
18# <https://www.gnu.org/licenses/>.
20from argparse import Namespace
21import sys
22import os
23from typing import Optional, Sequence
24import xml.etree.ElementTree as ET
26from lobster.common.items import Tracing_Tag, Activity
27from lobster.common.location import Void_Reference, File_Reference
28from lobster.common.io import lobster_write, ensure_output_directory
29from lobster.common.meta_data_tool_base import MetaDataToolBase
32class GtestTool(MetaDataToolBase):
33 def __init__(self):
34 super().__init__(
35 name = "gtest",
36 description = "Extract tracing tags from GoogleTest XML output",
37 official = True,
38 )
39 self._argument_parser.add_argument(
40 "files",
41 nargs="+",
42 metavar="FILE|DIR",
43 )
44 self._argument_parser.add_argument("--out", default=None)
46 @staticmethod
47 def _is_xml_file(filename: str) -> bool:
48 return os.path.splitext(filename)[1] == ".xml"
50 @staticmethod
51 def _is_c_file(filename: str) -> bool:
52 return os.path.splitext(filename)[1] in (".cpp", ".cc", ".c")
54 @classmethod
55 def _collect_directory_files(
56 cls,
57 item: str,
58 c_files_rel,
59 file_list,
60 ) -> None:
61 for path, _, files in os.walk(item, followlinks=True):
62 for filename in files:
63 full_path = os.path.join(path, filename)
64 if not os.path.isfile(full_path):
65 continue
66 if cls._is_xml_file(filename):
67 file_list.append(full_path)
68 continue
69 if not cls._is_c_file(filename):
70 continue
71 fullname = os.path.relpath(os.path.realpath(full_path))
72 if ".cache" in str(fullname):
73 continue
74 if filename not in c_files_rel:
75 c_files_rel[filename] = set()
76 c_files_rel[filename].add(fullname)
78 def _collect_input_files(self, options: Namespace):
79 c_files_rel = {}
80 file_list = []
81 for item in options.files:
82 if os.path.isfile(item):
83 file_list.append(item)
84 continue
85 if os.path.isdir(item):
86 self._collect_directory_files(item, c_files_rel, file_list)
87 continue
88 self._argument_parser.error(f"{item} is not a file or directory")
90 file_list = {os.path.realpath(os.path.abspath(f)) for f in file_list}
91 return c_files_rel, file_list
93 @staticmethod
94 def _parse_testcase_properties(testcase):
95 test_ok = True
96 test_tags = []
97 source_file = testcase.attrib.get("file", None)
98 source_line = int(testcase.attrib["line"]) \
99 if "line" in testcase.attrib \
100 else None
101 extra = {}
103 for props in testcase:
104 if props.tag == "failure":
105 test_ok = False
106 continue
107 if props.tag != "properties":
108 continue
109 for prop in props:
110 assert prop.tag == "property"
111 name = prop.attrib["name"]
112 value = prop.attrib["value"]
113 if name == "lobster-tracing":
114 test_tags += [
115 x.strip()
116 for x in value.split(",")]
117 elif name == "lobster-tracing-file":
118 source_file = value
119 elif name == "lobster-tracing-line":
120 source_line = int(value)
121 else:
122 extra[name] = value
124 return test_ok, test_tags, source_file, source_line, extra
126 @staticmethod
127 def _resolve_test_source(c_files_rel, source_file, source_line):
128 if source_file in c_files_rel and len(c_files_rel[source_file]) == 1:
129 return File_Reference(
130 filename = list(c_files_rel[source_file])[0],
131 line = source_line)
132 if source_file is None:
133 return Void_Reference()
134 return File_Reference(
135 filename = source_file,
136 line = source_line)
138 @staticmethod
139 def _resolve_test_status(test_executed: bool, test_ok: bool) -> str:
140 if not test_executed:
141 return "not run"
142 if test_ok:
143 return "ok"
144 return "fail"
146 def _run_impl(self, options: Namespace) -> int:
147 c_files_rel, file_list = self._collect_input_files(options)
149 items = []
151 for filename in file_list:
152 tree = ET.parse(filename)
153 root = tree.getroot()
154 if root.tag != "testsuites":
155 continue
156 for suite in root:
157 assert suite.tag == "testsuite"
158 suite_name = suite.attrib["name"]
159 for testcase in suite:
160 if testcase.tag != "testcase":
161 continue
162 test_name = testcase.attrib["name"]
163 test_executed = testcase.attrib["status"] == "run"
164 test_ok, test_tags, source_file, source_line, extra = \
165 self._parse_testcase_properties(testcase)
167 test_source = self._resolve_test_source(
168 c_files_rel, source_file, source_line)
170 uid = f"{suite_name}:{test_name}"
171 status = self._resolve_test_status(test_executed, test_ok)
173 parts = [f":{k.capitalize()}: {v}"
174 for k, v in extra.items()]
175 text = "\n".join(parts) if parts else None
177 tag = Tracing_Tag("gtest", uid)
178 item = Activity(tag = tag,
179 location = test_source,
180 framework = "GoogleTest",
181 kind = "test",
182 text = text,
183 status = status)
184 for ref in test_tags:
185 item.add_tracing_target(Tracing_Tag("req", ref))
187 items.append(item)
189 if options.out:
190 ensure_output_directory(options.out)
191 with open(options.out, "w", encoding="UTF-8") as fd:
192 lobster_write(fd, Activity, "lobster_gtest", items)
193 print(f"Written output for {len(items)} items to {options.out}")
194 else:
195 lobster_write(sys.stdout, Activity, "lobster_gtest", items)
196 print()
198 return 0
201def main(args: Optional[Sequence[str]] = None) -> int:
202 return GtestTool().run(args)