11# vim: set syntax=python ts=4 :
22#
3- # Copyright (c) 2018-2022 Intel Corporation
3+ # Copyright (c) 2018-2025 Intel Corporation
44# SPDX-License-Identifier: Apache-2.0
55
66import contextlib
@@ -31,6 +31,8 @@ def __init__(self):
3131 self .gcov_tool = None
3232 self .base_dir = None
3333 self .output_formats = None
34+ self .coverage_capture = True
35+ self .coverage_report = True
3436
3537 @staticmethod
3638 def factory (tool , jobs = None ):
@@ -109,7 +111,7 @@ def merge_hexdumps(self, hexdumps):
109111
110112 def create_gcda_files (self , extracted_coverage_info ):
111113 gcda_created = True
112- logger .debug ("Generating gcda files" )
114+ logger .debug (f "Generating { len ( extracted_coverage_info ) } gcda files" )
113115 for filename , hexdumps in extracted_coverage_info .items ():
114116 # if kobject_hash is given for coverage gcovr fails
115117 # hence skipping it problem only in gcovr v4.1
@@ -132,7 +134,7 @@ def create_gcda_files(self, extracted_coverage_info):
132134 gcda_created = False
133135 return gcda_created
134136
135- def generate (self , outdir ):
137+ def capture_data (self , outdir ):
136138 coverage_completed = True
137139 for filename in glob .glob (f"{ outdir } /**/handler.log" , recursive = True ):
138140 gcov_data = self .__class__ .retrieve_gcov_data (filename )
@@ -148,9 +150,15 @@ def generate(self, outdir):
148150 else :
149151 logger .error (f"Gcov data capture incomplete: { filename } " )
150152 coverage_completed = False
153+ return coverage_completed
151154
155+ def generate (self , outdir ):
156+ coverage_completed = self .capture_data (outdir ) if self .coverage_capture else True
157+ if not coverage_completed or not self .coverage_report :
158+ return coverage_completed , {}
159+ reports = {}
152160 with open (os .path .join (outdir , "coverage.log" ), "a" ) as coveragelog :
153- ret = self ._generate (outdir , coveragelog )
161+ ret , reports = self ._generate (outdir , coveragelog )
154162 if ret == 0 :
155163 report_log = {
156164 "html" : "HTML report generated: {}" .format (
@@ -180,7 +188,7 @@ def generate(self, outdir):
180188 else :
181189 coverage_completed = False
182190 logger .debug (f"All coverage data processed: { coverage_completed } " )
183- return coverage_completed
191+ return coverage_completed , reports
184192
185193
186194class Lcov (CoverageTool ):
@@ -274,6 +282,7 @@ def _generate(self, outdir, coveragelog):
274282 "--output-file" , ztestfile ]
275283 self .run_lcov (cmd , coveragelog )
276284
285+ files = []
277286 if os .path .exists (ztestfile ) and os .path .getsize (ztestfile ) > 0 :
278287 cmd = ["--remove" , ztestfile ,
279288 os .path .join (self .base_dir , "tests/ztest/test/*" ),
@@ -289,12 +298,15 @@ def _generate(self, outdir, coveragelog):
289298 self .run_lcov (cmd , coveragelog )
290299
291300 if 'html' not in self .output_formats .split (',' ):
292- return 0
301+ return 0 , {}
293302
294303 cmd = ["genhtml" , "--legend" , "--branch-coverage" ,
295304 "--prefix" , self .base_dir ,
296305 "-output-directory" , os .path .join (outdir , "coverage" )] + files
297- return self .run_command (cmd , coveragelog )
306+ ret = self .run_command (cmd , coveragelog )
307+
308+ # TODO: Add LCOV summary coverage report.
309+ return ret , { 'report' : coveragefile , 'ztest' : ztestfile , 'summary' : None }
298310
299311
300312class Gcovr (CoverageTool ):
@@ -344,8 +356,9 @@ def _flatten_list(list):
344356 return [a for b in list for a in b ]
345357
346358 def _generate (self , outdir , coveragelog ):
347- coveragefile = os .path .join (outdir , "coverage.json" )
348- ztestfile = os .path .join (outdir , "ztest.json" )
359+ coverage_file = os .path .join (outdir , "coverage.json" )
360+ coverage_summary = os .path .join (outdir , "coverage_summary.json" )
361+ ztest_file = os .path .join (outdir , "ztest.json" )
349362
350363 excludes = Gcovr ._interleave_list ("-e" , self .ignores )
351364 if len (self .ignore_branch_patterns ) > 0 :
@@ -358,24 +371,37 @@ def _generate(self, outdir, coveragelog):
358371 mode_options = ["--merge-mode-functions=separate" ]
359372
360373 # We want to remove tests/* and tests/ztest/test/* but save tests/ztest
361- cmd = ["gcovr" , "-r" , self .base_dir ,
374+ cmd = ["gcovr" , "-v" , "- r" , self .base_dir ,
362375 "--gcov-ignore-parse-errors=negative_hits.warn_once_per_file" ,
363376 "--gcov-executable" , self .gcov_tool ,
364377 "-e" , "tests/*" ]
365- cmd += excludes + mode_options + ["--json" , "-o" , coveragefile , outdir ]
378+ cmd += excludes + mode_options + ["--json" , "-o" , coverage_file , outdir ]
366379 cmd_str = " " .join (cmd )
367- logger .debug (f"Running { cmd_str } ..." )
368- subprocess .call (cmd , stdout = coveragelog )
369-
370- subprocess .call (["gcovr" , "-r" , self .base_dir , "--gcov-executable" ,
371- self .gcov_tool , "-f" , "tests/ztest" , "-e" ,
372- "tests/ztest/test/*" , "--json" , "-o" , ztestfile ,
373- outdir ] + mode_options , stdout = coveragelog )
374-
375- if os .path .exists (ztestfile ) and os .path .getsize (ztestfile ) > 0 :
376- files = [coveragefile , ztestfile ]
380+ logger .debug (f"Running: { cmd_str } " )
381+ coveragelog .write (f"Running: { cmd_str } \n " )
382+ coveragelog .flush ()
383+ ret = subprocess .call (cmd , stdout = coveragelog , stderr = coveragelog )
384+ if ret :
385+ logger .error (f"GCOVR failed with { ret } " )
386+ return ret , {}
387+
388+ cmd = ["gcovr" , "-v" , "-r" , self .base_dir ] + mode_options
389+ cmd += ["--gcov-executable" , self .gcov_tool ,
390+ "-f" , "tests/ztest" , "-e" , "tests/ztest/test/*" ,
391+ "--json" , "-o" , ztest_file , outdir ]
392+ cmd_str = " " .join (cmd )
393+ logger .debug (f"Running: { cmd_str } " )
394+ coveragelog .write (f"Running: { cmd_str } \n " )
395+ coveragelog .flush ()
396+ ret = subprocess .call (cmd , stdout = coveragelog , stderr = coveragelog )
397+ if ret :
398+ logger .error (f"GCOVR ztest stage failed with { ret } " )
399+ return ret , {}
400+
401+ if os .path .exists (ztest_file ) and os .path .getsize (ztest_file ) > 0 :
402+ files = [coverage_file , ztest_file ]
377403 else :
378- files = [coveragefile ]
404+ files = [coverage_file ]
379405
380406 subdir = os .path .join (outdir , "coverage" )
381407 os .makedirs (subdir , exist_ok = True )
@@ -396,21 +422,21 @@ def _generate(self, outdir, coveragelog):
396422 [report_options [r ] for r in self .output_formats .split (',' )]
397423 )
398424
399- return subprocess .call (
400- ["gcovr" , "-r" , self .base_dir ] \
401- + mode_options + gcovr_options + tracefiles , stdout = coveragelog
402- )
425+ cmd = ["gcovr" , "-v" , "-r" , self .base_dir ] + mode_options + gcovr_options + tracefiles
426+ cmd += ["--json-summary-pretty" , "--json-summary" , coverage_summary ]
427+ cmd_str = " " .join (cmd )
428+ logger .debug (f"Running: { cmd_str } " )
429+ coveragelog .write (f"Running: { cmd_str } \n " )
430+ coveragelog .flush ()
431+ ret = subprocess .call (cmd , stdout = coveragelog , stderr = coveragelog )
432+ if ret :
433+ logger .error (f"GCOVR merge report stage failed with { ret } " )
403434
435+ return ret , { 'report' : coverage_file , 'ztest' : ztest_file , 'summary' : coverage_summary }
404436
405437
406- def run_coverage (testplan , options ):
407- use_system_gcov = False
438+ def choose_gcov_tool (options , is_system_gcov ):
408439 gcov_tool = None
409-
410- for plat in options .coverage_platform :
411- _plat = testplan .get_platform (plat )
412- if _plat and (_plat .type in {"native" , "unit" }):
413- use_system_gcov = True
414440 if not options .gcov_tool :
415441 zephyr_sdk_gcov_tool = os .path .join (
416442 os .environ .get ("ZEPHYR_SDK_INSTALL_DIR" , default = "" ),
@@ -427,7 +453,7 @@ def run_coverage(testplan, options):
427453 except OSError :
428454 shutil .copy (llvm_cov , gcov_lnk )
429455 gcov_tool = gcov_lnk
430- elif use_system_gcov :
456+ elif is_system_gcov :
431457 gcov_tool = "gcov"
432458 elif os .path .exists (zephyr_sdk_gcov_tool ):
433459 gcov_tool = zephyr_sdk_gcov_tool
@@ -439,10 +465,19 @@ def run_coverage(testplan, options):
439465 else :
440466 gcov_tool = str (options .gcov_tool )
441467
442- logger .info ("Generating coverage files..." )
443- logger .info (f"Using gcov tool: { gcov_tool } " )
468+ return gcov_tool
469+
470+
471+ def run_coverage_tool (options , outdir , is_system_gcov , coverage_capture , coverage_report ):
444472 coverage_tool = CoverageTool .factory (options .coverage_tool , jobs = options .jobs )
445- coverage_tool .gcov_tool = gcov_tool
473+ if not coverage_tool :
474+ return False , {}
475+
476+ coverage_tool .gcov_tool = str (choose_gcov_tool (options , is_system_gcov ))
477+ logger .debug (f"Using gcov tool: { coverage_tool .gcov_tool } " )
478+
479+ coverage_tool .coverage_capture = coverage_capture
480+ coverage_tool .coverage_report = coverage_report
446481 coverage_tool .base_dir = os .path .abspath (options .coverage_basedir )
447482 # Apply output format default
448483 if options .coverage_formats is not None :
@@ -456,5 +491,32 @@ def run_coverage(testplan, options):
456491 # Ignore branch coverage on __ASSERT* macros
457492 # Covering the failing case is not desirable as it will immediately terminate the test.
458493 coverage_tool .add_ignore_branch_pattern (r"^\s*__ASSERT(?:_EVAL|_NO_MSG|_POST_ACTION)?\(.*" )
459- coverage_completed = coverage_tool .generate (options .outdir )
460- return coverage_completed
494+ return coverage_tool .generate (outdir )
495+
496+
497+ def has_system_gcov (platform ):
498+ return platform and (platform .type in {"native" , "unit" })
499+
500+
501+ def run_coverage (options , testplan ):
502+ """ Summary code coverage over the full test plan's scope.
503+ """
504+ is_system_gcov = False
505+
506+ for plat in options .coverage_platform :
507+ if has_system_gcov (testplan .get_platform (plat )):
508+ is_system_gcov = True
509+ break
510+
511+ return run_coverage_tool (options , options .outdir , is_system_gcov ,
512+ coverage_capture = False ,
513+ coverage_report = True )
514+
515+
516+ def run_coverage_instance (options , instance ):
517+ """ Per-instance code coverage called by ProjectBuilder ('coverage' operation).
518+ """
519+ is_system_gcov = has_system_gcov (instance .platform )
520+ return run_coverage_tool (options , instance .build_dir , is_system_gcov ,
521+ coverage_capture = True ,
522+ coverage_report = options .coverage_per_instance )
0 commit comments