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

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 

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

118 

119 return test_ok, test_tags, source_file, source_line 

120 

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) 

132 

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" 

140 

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

142 c_files_rel, file_list = self._collect_input_files(options) 

143 

144 items = [] 

145 

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) 

161 

162 test_source = self._resolve_test_source( 

163 c_files_rel, source_file, source_line) 

164 

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

166 status = self._resolve_test_status(test_executed, test_ok) 

167 

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

176 

177 items.append(item) 

178 

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

187 

188 return 0 

189 

190 

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

192 return GtestTool().run(args)