1
1
# vim: set syntax=python ts=4 :
2
2
#
3
- # Copyright (c) 2018-2022 Intel Corporation
3
+ # Copyright (c) 2018-2025 Intel Corporation
4
4
# SPDX-License-Identifier: Apache-2.0
5
5
6
6
import contextlib
@@ -31,6 +31,8 @@ def __init__(self):
31
31
self .gcov_tool = None
32
32
self .base_dir = None
33
33
self .output_formats = None
34
+ self .coverage_capture = True
35
+ self .coverage_report = True
34
36
35
37
@staticmethod
36
38
def factory (tool , jobs = None ):
@@ -109,7 +111,7 @@ def merge_hexdumps(self, hexdumps):
109
111
110
112
def create_gcda_files (self , extracted_coverage_info ):
111
113
gcda_created = True
112
- logger .debug ("Generating gcda files" )
114
+ logger .debug (f "Generating { len ( extracted_coverage_info ) } gcda files" )
113
115
for filename , hexdumps in extracted_coverage_info .items ():
114
116
# if kobject_hash is given for coverage gcovr fails
115
117
# hence skipping it problem only in gcovr v4.1
@@ -132,7 +134,7 @@ def create_gcda_files(self, extracted_coverage_info):
132
134
gcda_created = False
133
135
return gcda_created
134
136
135
- def generate (self , outdir ):
137
+ def capture_data (self , outdir ):
136
138
coverage_completed = True
137
139
for filename in glob .glob (f"{ outdir } /**/handler.log" , recursive = True ):
138
140
gcov_data = self .__class__ .retrieve_gcov_data (filename )
@@ -148,9 +150,15 @@ def generate(self, outdir):
148
150
else :
149
151
logger .error (f"Gcov data capture incomplete: { filename } " )
150
152
coverage_completed = False
153
+ return coverage_completed
151
154
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 = {}
152
160
with open (os .path .join (outdir , "coverage.log" ), "a" ) as coveragelog :
153
- ret = self ._generate (outdir , coveragelog )
161
+ ret , reports = self ._generate (outdir , coveragelog )
154
162
if ret == 0 :
155
163
report_log = {
156
164
"html" : "HTML report generated: {}" .format (
@@ -180,7 +188,7 @@ def generate(self, outdir):
180
188
else :
181
189
coverage_completed = False
182
190
logger .debug (f"All coverage data processed: { coverage_completed } " )
183
- return coverage_completed
191
+ return coverage_completed , reports
184
192
185
193
186
194
class Lcov (CoverageTool ):
@@ -274,6 +282,7 @@ def _generate(self, outdir, coveragelog):
274
282
"--output-file" , ztestfile ]
275
283
self .run_lcov (cmd , coveragelog )
276
284
285
+ files = []
277
286
if os .path .exists (ztestfile ) and os .path .getsize (ztestfile ) > 0 :
278
287
cmd = ["--remove" , ztestfile ,
279
288
os .path .join (self .base_dir , "tests/ztest/test/*" ),
@@ -289,12 +298,15 @@ def _generate(self, outdir, coveragelog):
289
298
self .run_lcov (cmd , coveragelog )
290
299
291
300
if 'html' not in self .output_formats .split (',' ):
292
- return 0
301
+ return 0 , {}
293
302
294
303
cmd = ["genhtml" , "--legend" , "--branch-coverage" ,
295
304
"--prefix" , self .base_dir ,
296
305
"-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 }
298
310
299
311
300
312
class Gcovr (CoverageTool ):
@@ -344,8 +356,9 @@ def _flatten_list(list):
344
356
return [a for b in list for a in b ]
345
357
346
358
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" )
349
362
350
363
excludes = Gcovr ._interleave_list ("-e" , self .ignores )
351
364
if len (self .ignore_branch_patterns ) > 0 :
@@ -358,24 +371,37 @@ def _generate(self, outdir, coveragelog):
358
371
mode_options = ["--merge-mode-functions=separate" ]
359
372
360
373
# 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 ,
362
375
"--gcov-ignore-parse-errors=negative_hits.warn_once_per_file" ,
363
376
"--gcov-executable" , self .gcov_tool ,
364
377
"-e" , "tests/*" ]
365
- cmd += excludes + mode_options + ["--json" , "-o" , coveragefile , outdir ]
378
+ cmd += excludes + mode_options + ["--json" , "-o" , coverage_file , outdir ]
366
379
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 ]
377
403
else :
378
- files = [coveragefile ]
404
+ files = [coverage_file ]
379
405
380
406
subdir = os .path .join (outdir , "coverage" )
381
407
os .makedirs (subdir , exist_ok = True )
@@ -396,21 +422,21 @@ def _generate(self, outdir, coveragelog):
396
422
[report_options [r ] for r in self .output_formats .split (',' )]
397
423
)
398
424
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 } " )
403
434
435
+ return ret , { 'report' : coverage_file , 'ztest' : ztest_file , 'summary' : coverage_summary }
404
436
405
437
406
- def run_coverage (testplan , options ):
407
- use_system_gcov = False
438
+ def choose_gcov_tool (options , is_system_gcov ):
408
439
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
414
440
if not options .gcov_tool :
415
441
zephyr_sdk_gcov_tool = os .path .join (
416
442
os .environ .get ("ZEPHYR_SDK_INSTALL_DIR" , default = "" ),
@@ -427,7 +453,7 @@ def run_coverage(testplan, options):
427
453
except OSError :
428
454
shutil .copy (llvm_cov , gcov_lnk )
429
455
gcov_tool = gcov_lnk
430
- elif use_system_gcov :
456
+ elif is_system_gcov :
431
457
gcov_tool = "gcov"
432
458
elif os .path .exists (zephyr_sdk_gcov_tool ):
433
459
gcov_tool = zephyr_sdk_gcov_tool
@@ -439,10 +465,19 @@ def run_coverage(testplan, options):
439
465
else :
440
466
gcov_tool = str (options .gcov_tool )
441
467
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 ):
444
472
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
446
481
coverage_tool .base_dir = os .path .abspath (options .coverage_basedir )
447
482
# Apply output format default
448
483
if options .coverage_formats is not None :
@@ -456,5 +491,32 @@ def run_coverage(testplan, options):
456
491
# Ignore branch coverage on __ASSERT* macros
457
492
# Covering the failing case is not desirable as it will immediately terminate the test.
458
493
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