VirtualBox

source: vbox/trunk/src/VBox/Devices/EFI/FirmwareNew/BaseTools/Plugin/HostBasedUnitTestRunner/HostBasedUnitTestRunner.py@ 108794

Last change on this file since 108794 was 108794, checked in by vboxsync, 2 weeks ago

Devices/EFI/FirmwareNew: Merge edk2-stable202502 from the vendor branch and make it build for the important platforms, bugref:4643

  • Property svn:eol-style set to native
File size: 12.8 KB
Line 
1# @file HostBasedUnitTestRunner.py
2# Plugin to located any host-based unit tests in the output directory and execute them.
3##
4# Copyright (c) Microsoft Corporation.
5# SPDX-License-Identifier: BSD-2-Clause-Patent
6#
7##
8import io
9import re
10import os
11import logging
12import glob
13import stat
14import xml.etree.ElementTree
15from edk2toolext.environment.plugintypes.uefi_build_plugin import IUefiBuildPlugin
16from edk2toolext import edk2_logging
17import edk2toollib.windows.locate_tools as locate_tools
18from edk2toolext.environment import shell_environment
19from edk2toollib.utility_functions import RunCmd
20from edk2toollib.utility_functions import GetHostInfo
21from textwrap import dedent
22
23
24class HostBasedUnitTestRunner(IUefiBuildPlugin):
25
26 def do_pre_build(self, thebuilder):
27 '''
28 Run Prebuild
29 '''
30
31 return 0
32
33 def do_post_build(self, thebuilder):
34 '''
35 After a build, will automatically locate and run all host-based unit tests. Logs any
36 failures with Warning severity and will return a count of the failures as the return code.
37
38 EXPECTS:
39 - Build Var 'CI_BUILD_TYPE' - If not set to 'host_unit_test', will not do anything.
40
41 UPDATES:
42 - Shell Var 'CMOCKA_XML_FILE'
43 '''
44 ci_type = thebuilder.env.GetValue('CI_BUILD_TYPE')
45 if ci_type != 'host_unit_test':
46 return 0
47
48 shell_env = shell_environment.GetEnvironment()
49 logging.log(edk2_logging.get_section_level(),
50 "Run Host based Unit Tests")
51 path = thebuilder.env.GetValue("BUILD_OUTPUT_BASE")
52
53 failure_count = 0
54
55 # Do not catch exceptions in gtest so they are handled by address sanitizer
56 shell_env.set_shell_var('GTEST_CATCH_EXCEPTIONS', '0')
57
58 # Disable address sanitizer memory leak detection
59 shell_env.set_shell_var('ASAN_OPTIONS', 'detect_leaks=0')
60
61 # Set up the reporting type for Cmocka.
62 shell_env.set_shell_var('CMOCKA_MESSAGE_OUTPUT', 'xml')
63
64 for arch in thebuilder.env.GetValue("TARGET_ARCH").split():
65 logging.log(edk2_logging.get_subsection_level(),
66 "Testing for architecture: " + arch)
67 cp = os.path.join(path, arch)
68
69 # If any old results XML files exist, clean them up.
70 for old_result in glob.iglob(os.path.join(cp, "*.result.xml")):
71 os.remove(old_result)
72
73 # Find and Run any Host Tests
74 if GetHostInfo().os.upper() == "LINUX":
75 testList = glob.glob(os.path.join(cp, "*Test*"))
76 for a in testList[:]:
77 p = os.path.join(cp, a)
78 # It must be a file
79 if not os.path.isfile(p):
80 testList.remove(a)
81 logging.debug(f"Remove directory file: {p}")
82 continue
83 # It must be executable
84 if os.stat(p).st_mode & (stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH) == 0:
85 testList.remove(a)
86 logging.debug(f"Remove non-executable file: {p}")
87 continue
88
89 logging.info(f"Test file found: {p}")
90
91 elif GetHostInfo().os.upper() == "WINDOWS":
92 testList = glob.glob(os.path.join(cp, "*Test*.exe"))
93 else:
94 raise NotImplementedError("Unsupported Operating System")
95
96 if not testList:
97 logging.warning(dedent("""
98 UnitTest Coverage:
99 No unit tests discovered. Test coverage will not be generated.
100
101 Prevent this message by:
102 1. Adding host-based unit tests to this package
103 2. Ensuring tests have the word "Test" in their name
104 3. Disabling HostUnitTestCompilerPlugin in the package CI YAML file
105 """).strip())
106 return 0
107
108 for test in testList:
109 # Configure output name if test uses cmocka.
110 shell_env.set_shell_var(
111 'CMOCKA_XML_FILE', test + ".CMOCKA.%g." + arch + ".result.xml")
112 # Configure output name if test uses gtest.
113 shell_env.set_shell_var(
114 'GTEST_OUTPUT', "xml:" + test + ".GTEST." + arch + ".result.xml")
115
116 # Run the test.
117 ret = RunCmd('"' + test + '"', "", workingdir=cp)
118 if ret != 0:
119 logging.error("UnitTest Execution Error: " +
120 os.path.basename(test))
121 failure_count += 1
122 else:
123 logging.info("UnitTest Completed: " +
124 os.path.basename(test))
125 file_match_pattern = test + ".*." + arch + ".result.xml"
126 xml_results_list = glob.glob(file_match_pattern)
127 for xml_result_file in xml_results_list:
128 root = xml.etree.ElementTree.parse(
129 xml_result_file).getroot()
130 for suite in root:
131 for case in suite:
132 for result in case:
133 if result.tag == 'failure':
134 logging.warning(
135 "%s Test Failed" % os.path.basename(test))
136 logging.warning(
137 " %s - %s" % (case.attrib['name'], result.text))
138 failure_count += 1
139
140 if thebuilder.env.GetValue("CODE_COVERAGE") != "FALSE":
141 if thebuilder.env.GetValue("TOOL_CHAIN_TAG") == "GCC5":
142 ret = self.gen_code_coverage_gcc(thebuilder)
143 if ret != 0:
144 failure_count += 1
145 elif thebuilder.env.GetValue("TOOL_CHAIN_TAG").startswith ("VS"):
146 ret = self.gen_code_coverage_msvc(thebuilder)
147 if ret != 0:
148 failure_count += 1
149 else:
150 logging.info("Skipping code coverage. Currently, support GCC and MSVC compiler.")
151
152 return failure_count
153
154 def get_lcov_version(self):
155 """Get lcov version number"""
156 lcov_ver = io.StringIO()
157 ret = RunCmd("lcov", "--version", outstream=lcov_ver)
158 if ret != 0:
159 return None
160 (major, _minor) = re.search(r"version (\d+)\.(\d+)", lcov_ver.getvalue()).groups()
161 return int(major)
162
163
164 def gen_code_coverage_gcc(self, thebuilder):
165 logging.info("Generating UnitTest code coverage")
166
167 buildOutputBase = thebuilder.env.GetValue("BUILD_OUTPUT_BASE")
168 workspace = thebuilder.env.GetValue("WORKSPACE")
169
170 lcov_version_major = self.get_lcov_version()
171 if not lcov_version_major:
172 logging.error("UnitTest Coverage: Failed to determine lcov version")
173 return 1
174 logging.info(f"Got lcov version {lcov_version_major}")
175
176 # Generate base code coverage for all source files
177 ret = RunCmd("lcov", f"--no-external --capture --initial --directory {buildOutputBase} --output-file {buildOutputBase}/cov-base.info --rc lcov_branch_coverage=1")
178 if ret != 0:
179 logging.error("UnitTest Coverage: Failed to build initial coverage data.")
180 return 1
181
182 # Coverage data for tested files only
183 # `--ignore-errors mismatch` needed to make lcov v2.0+/gcov work.
184 lcov_error_settings = "--ignore-errors mismatch" if lcov_version_major >= 2 else ""
185 ret = RunCmd("lcov", f"--capture --directory {buildOutputBase}/ --output-file {buildOutputBase}/coverage-test.info --rc lcov_branch_coverage=1 {lcov_error_settings}")
186 if ret != 0:
187 logging.error("UnitTest Coverage: Failed to build coverage data for tested files.")
188 return 1
189
190 # Aggregate all coverage data
191 ret = RunCmd("lcov", f"--add-tracefile {buildOutputBase}/cov-base.info --add-tracefile {buildOutputBase}/coverage-test.info --output-file {buildOutputBase}/total-coverage.info --rc lcov_branch_coverage=1")
192 if ret != 0:
193 logging.error("UnitTest Coverage: Failed to aggregate coverage data.")
194 return 1
195
196 # Generate coverage XML
197 ret = RunCmd("lcov_cobertura",f"{buildOutputBase}/total-coverage.info -o {buildOutputBase}/compare.xml")
198 if ret != 0:
199 logging.error("UnitTest Coverage: Failed to generate coverage XML.")
200 return 1
201
202 # Filter out auto-generated and test code
203 ret = RunCmd("lcov_cobertura",f"{buildOutputBase}/total-coverage.info --excludes ^.*UnitTest\|^.*MU\|^.*Mock\|^.*DEBUG -o {buildOutputBase}/coverage.xml")
204 if ret != 0:
205 logging.error("UnitTest Coverage: Failed generate filtered coverage XML.")
206 return 1
207
208 # Generate all coverage file
209 testCoverageList = glob.glob (f"{workspace}/Build/**/total-coverage.info", recursive=True)
210
211 coverageFile = ""
212 for testCoverage in testCoverageList:
213 coverageFile += " --add-tracefile " + testCoverage
214 ret = RunCmd("lcov", f"{coverageFile} --output-file {workspace}/Build/all-coverage.info --rc lcov_branch_coverage=1")
215 if ret != 0:
216 logging.error("UnitTest Coverage: Failed generate all coverage file.")
217 return 1
218
219 # Generate and XML file if requested.for all package
220 if os.path.isfile(f"{workspace}/Build/coverage.xml"):
221 os.remove(f"{workspace}/Build/coverage.xml")
222 ret = RunCmd("lcov_cobertura",f"{workspace}/Build/all-coverage.info --excludes ^.*UnitTest\|^.*MU\|^.*Mock\|^.*DEBUG -o {workspace}/Build/coverage.xml")
223
224 return 0
225
226
227 def gen_code_coverage_msvc(self, thebuilder):
228 logging.info("Generating UnitTest code coverage")
229
230
231 buildOutputBase = thebuilder.env.GetValue("BUILD_OUTPUT_BASE")
232 testList = glob.glob(os.path.join(buildOutputBase, "**","*Test*.exe"), recursive=True)
233 workspace = thebuilder.env.GetValue("WORKSPACE")
234 workspace = (workspace + os.sep) if workspace[-1] != os.sep else workspace
235 workspaceBuild = os.path.join(workspace, 'Build')
236 # Generate coverage file
237 coverageFile = ""
238 for testFile in testList:
239 ret = RunCmd("OpenCppCoverage", f"--source {workspace} --export_type binary:{testFile}.cov -- {testFile}")
240 if ret != 0:
241 logging.error("UnitTest Coverage: Failed to collect coverage data.")
242 return 1
243
244 coverageFile = f" --input_coverage={testFile}.cov"
245 totalCoverageFile = os.path.join(buildOutputBase, 'coverage.cov')
246 if os.path.isfile(totalCoverageFile):
247 coverageFile += f" --input_coverage={totalCoverageFile}"
248 ret = RunCmd(
249 "OpenCppCoverage",
250 f"--export_type binary:{totalCoverageFile} " +
251 f"--working_dir={workspaceBuild} " +
252 f"{coverageFile}"
253 )
254 if ret != 0:
255 logging.error("UnitTest Coverage: Failed to collect coverage data.")
256 return 1
257
258 # Generate and XML file if requested.by each package
259 ret = RunCmd(
260 "OpenCppCoverage",
261 f"--export_type cobertura:{os.path.join(buildOutputBase, 'coverage.xml')} " +
262 f"--working_dir={workspaceBuild} " +
263 f"--input_coverage={totalCoverageFile} "
264 )
265 if ret != 0:
266 logging.error("UnitTest Coverage: Failed to generate cobertura format xml in single package.")
267 return 1
268
269 # Generate total report XML file for all package
270 testCoverageList = glob.glob(os.path.join(workspace, "Build", "**", "*Test*.exe.cov"), recursive=True)
271 coverageFile = ""
272 totalCoverageFile = os.path.join(workspaceBuild, 'coverage.cov')
273 for testCoverage in testCoverageList:
274 coverageFile = f" --input_coverage={testCoverage}"
275 if os.path.isfile(totalCoverageFile):
276 coverageFile += f" --input_coverage={totalCoverageFile}"
277 ret = RunCmd(
278 "OpenCppCoverage",
279 f"--export_type binary:{totalCoverageFile} " +
280 f"--working_dir={workspaceBuild} " +
281 f"{coverageFile}"
282 )
283 if ret != 0:
284 logging.error("UnitTest Coverage: Failed to collect coverage data.")
285 return 1
286
287 ret = RunCmd(
288 "OpenCppCoverage",
289 f"--export_type cobertura:{os.path.join(workspaceBuild, 'coverage.xml')} " +
290 f"--working_dir={workspaceBuild} " +
291 f"--input_coverage={totalCoverageFile}"
292 )
293 if ret != 0:
294 logging.error("UnitTest Coverage: Failed to generate cobertura format xml.")
295 return 1
296
297 return 0
Note: See TracBrowser for help on using the repository browser.

© 2025 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette