Coverage for lobster/tools/gtest/gtest.py: 0%
121 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-04-20 08:24 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-04-20 08:24 +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
102 for props in testcase:
103 if props.tag == "failure":
104 test_ok = False
105 continue
106 if props.tag != "properties":
107 continue
108 for prop in props:
109 assert prop.tag == "property"
110 if prop.attrib["name"] == "lobster-tracing":
111 test_tags += [
112 x.strip()
113 for x in prop.attrib["value"].split(",")]
114 elif prop.attrib["name"] == "lobster-tracing-file":
115 source_file = prop.attrib["value"]
116 elif prop.attrib["name"] == "lobster-tracing-line":
117 source_line = int(prop.attrib["value"])
119 return test_ok, test_tags, source_file, source_line
121 @staticmethod
122 def _resolve_test_source(c_files_rel, source_file, source_line):
123 if source_file in c_files_rel and len(c_files_rel[source_file]) == 1:
124 return File_Reference(
125 filename = list(c_files_rel[source_file])[0],
126 line = source_line)
127 if source_file is None:
128 return Void_Reference()
129 return File_Reference(
130 filename = source_file,
131 line = source_line)
133 @staticmethod
134 def _resolve_test_status(test_executed: bool, test_ok: bool) -> str:
135 if not test_executed:
136 return "not run"
137 if test_ok:
138 return "ok"
139 return "fail"
141 def _run_impl(self, options: Namespace) -> int:
142 c_files_rel, file_list = self._collect_input_files(options)
144 items = []
146 for filename in file_list:
147 tree = ET.parse(filename)
148 root = tree.getroot()
149 if root.tag != "testsuites":
150 continue
151 for suite in root:
152 assert suite.tag == "testsuite"
153 suite_name = suite.attrib["name"]
154 for testcase in suite:
155 if testcase.tag != "testcase":
156 continue
157 test_name = testcase.attrib["name"]
158 test_executed = testcase.attrib["status"] == "run"
159 test_ok, test_tags, source_file, source_line = \
160 self._parse_testcase_properties(testcase)
162 test_source = self._resolve_test_source(
163 c_files_rel, source_file, source_line)
165 uid = f"{suite_name}:{test_name}"
166 status = self._resolve_test_status(test_executed, test_ok)
168 tag = Tracing_Tag("gtest", uid)
169 item = Activity(tag = tag,
170 location = test_source,
171 framework = "GoogleTest",
172 kind = "test",
173 status = status)
174 for ref in test_tags:
175 item.add_tracing_target(Tracing_Tag("req", ref))
177 items.append(item)
179 if options.out:
180 ensure_output_directory(options.out)
181 with open(options.out, "w", encoding="UTF-8") as fd:
182 lobster_write(fd, Activity, "lobster_gtest", items)
183 print(f"Written output for {len(items)} items to {options.out}")
184 else:
185 lobster_write(sys.stdout, Activity, "lobster_gtest", items)
186 print()
188 return 0
191def main(args: Optional[Sequence[str]] = None) -> int:
192 return GtestTool().run(args)