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

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/>. 

19 

20from argparse import Namespace 

21import sys 

22import os 

23from typing import Optional, Sequence 

24import xml.etree.ElementTree as ET 

25 

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 

30 

31 

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) 

45 

46 @staticmethod 

47 def _is_xml_file(filename: str) -> bool: 

48 return os.path.splitext(filename)[1] == ".xml" 

49 

50 @staticmethod 

51 def _is_c_file(filename: str) -> bool: 

52 return os.path.splitext(filename)[1] in (".cpp", ".cc", ".c") 

53 

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) 

77 

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") 

89 

90 file_list = {os.path.realpath(os.path.abspath(f)) for f in file_list} 

91 return c_files_rel, file_list 

92 

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 = {} 

102 

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 

123 

124 return test_ok, test_tags, source_file, source_line, extra 

125 

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) 

137 

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" 

145 

146 def _run_impl(self, options: Namespace) -> int: 

147 c_files_rel, file_list = self._collect_input_files(options) 

148 

149 items = [] 

150 

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) 

166 

167 test_source = self._resolve_test_source( 

168 c_files_rel, source_file, source_line) 

169 

170 uid = f"{suite_name}:{test_name}" 

171 status = self._resolve_test_status(test_executed, test_ok) 

172 

173 parts = [f":{k.capitalize()}: {v}" 

174 for k, v in extra.items()] 

175 text = "\n".join(parts) if parts else None 

176 

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)) 

186 

187 items.append(item) 

188 

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() 

197 

198 return 0 

199 

200 

201def main(args: Optional[Sequence[str]] = None) -> int: 

202 return GtestTool().run(args)