forked from chromium/chromium
-
Notifications
You must be signed in to change notification settings - Fork 1
/
coverage.py
executable file
·1200 lines (959 loc) · 43.5 KB
/
coverage.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env vpython3
# Copyright 2017 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""This script helps to generate code coverage report.
It uses Clang Source-based Code Coverage -
https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
In order to generate code coverage report, you need to first add
"use_clang_coverage=true" and "is_component_build=false" GN flags to args.gn
file in your build output directory (e.g. out/coverage).
* Example usage:
gn gen out/coverage \\
--args="use_clang_coverage=true is_component_build=false\\
is_debug=false dcheck_always_on=true"
gclient runhooks
vpython3 tools/code_coverage/coverage.py crypto_unittests url_unittests \\
-b out/coverage -o out/report -c 'out/coverage/crypto_unittests' \\
-c 'out/coverage/url_unittests --gtest_filter=URLParser.PathURL' \\
-f url/ -f crypto/
The command above builds crypto_unittests and url_unittests targets and then
runs them with specified command line arguments. For url_unittests, it only
runs the test URLParser.PathURL. The coverage report is filtered to include
only files and sub-directories under url/ and crypto/ directories.
If you want to run tests that try to draw to the screen but don't have a
display connected, you can run tests in headless mode with xvfb.
* Sample flow for running a test target with xvfb (e.g. unit_tests):
vpython3 tools/code_coverage/coverage.py unit_tests -b out/coverage \\
-o out/report -c 'python testing/xvfb.py out/coverage/unit_tests'
If you are building a fuzz target, in addition to "use_clang_coverage=true"
and "is_component_build=false", you must have the following GN flags as well:
optimize_for_fuzzing=false
use_remoteexec=false
is_asan=false (ASAN & other sanitizers are incompatible with coverage)
use_libfuzzer=true
* Sample workflow for a fuzz target (e.g. pdfium_fuzzer):
vpython3 tools/code_coverage/coverage.py pdfium_fuzzer \\
-b out/coverage -o out/report \\
-c 'out/coverage/pdfium_fuzzer -runs=0 <corpus_dir>' \\
-f third_party/pdfium
where:
<corpus_dir> - directory containing samples files for this format.
To learn more about generating code coverage reports for fuzz targets, see
https://chromium.googlesource.com/chromium/src/+/main/testing/libfuzzer/efficient_fuzzer.md#Code-Coverage
* Sample workflow for running Blink web platform tests:
vpython3 tools/code_coverage/coverage.py blink_tests \\
-b out/coverage -o out/report -f third_party/blink -wt
-wt flag tells coverage script that it is a web test, and can also be
used to pass arguments to run_web_tests.py
vpython3 tools/code_coverage/coverage.py blink_wpt_tests \\
-b out/Release -o out/report
-wt external/wpt/webcodecs/per-frame-qp-encoding.https.any.js
For more options, please refer to tools/code_coverage/coverage.py -h.
For an overview of how code coverage works in Chromium, please refer to
https://chromium.googlesource.com/chromium/src/+/main/docs/testing/code_coverage.md
"""
from __future__ import print_function
import sys
import argparse
import glob
import json
import logging
import multiprocessing
import os
import platform
import re
import shlex
import shutil
import subprocess
from urllib.request import urlopen
sys.path.append(
os.path.join(
os.path.dirname(__file__), os.path.pardir, os.path.pardir,
'third_party'))
from collections import defaultdict
import coverage_utils
# Absolute path to the code coverage tools binary. These paths can be
# overwritten by user specified coverage tool paths.
# Absolute path to the root of the checkout.
SRC_ROOT_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)),
os.path.pardir, os.path.pardir)
LLVM_BIN_DIR = os.path.join(
os.path.join(SRC_ROOT_PATH, 'third_party', 'llvm-build', 'Release+Asserts'),
'bin')
LLVM_COV_PATH = os.path.join(LLVM_BIN_DIR, 'llvm-cov')
LLVM_PROFDATA_PATH = os.path.join(LLVM_BIN_DIR, 'llvm-profdata')
# Build directory, the value is parsed from command line arguments.
BUILD_DIR = None
# Output directory for generated artifacts, the value is parsed from command
# line arguemnts.
OUTPUT_DIR = None
# Name of the file extension for profraw data files.
PROFRAW_FILE_EXTENSION = 'profraw'
# Name of the final profdata file, and this file needs to be passed to
# "llvm-cov" command in order to call "llvm-cov show" to inspect the
# line-by-line coverage of specific files.
PROFDATA_FILE_NAME = os.extsep.join(['coverage', 'profdata'])
# Name of the file with summary information generated by llvm-cov export.
SUMMARY_FILE_NAME = os.extsep.join(['summary', 'json'])
# Name of the coverage file in lcov format generated by llvm-cov export.
LCOV_FILE_NAME = os.extsep.join(['coverage', 'lcov'])
# Build arg required for generating code coverage data.
CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
LOGS_DIR_NAME = 'logs'
# Used to extract a mapping between directories and components.
COMPONENT_MAPPING_URL = (
'https://storage.googleapis.com/chromium-owners/component_map.json')
# Caches the results returned by _GetBuildArgs, don't use this variable
# directly, call _GetBuildArgs instead.
_BUILD_ARGS = None
# Retry failed merges.
MERGE_RETRIES = 3
# Message to guide user to file a bug when everything else fails.
FILE_BUG_MESSAGE = (
'If it persists, please file a bug with the command you used, git revision '
'and args.gn config here: '
'https://bugs.chromium.org/p/chromium/issues/entry?'
'components=Infra%3ETest%3ECodeCoverage')
# String to replace with actual llvm profile path.
LLVM_PROFILE_FILE_PATH_SUBSTITUTION = '<llvm_profile_file_path>'
def _ConfigureLLVMCoverageTools(args):
"""Configures llvm coverage tools."""
if args.coverage_tools_dir:
llvm_bin_dir = coverage_utils.GetFullPath(args.coverage_tools_dir)
global LLVM_COV_PATH
global LLVM_PROFDATA_PATH
LLVM_COV_PATH = os.path.join(llvm_bin_dir, 'llvm-cov')
LLVM_PROFDATA_PATH = os.path.join(llvm_bin_dir, 'llvm-profdata')
else:
subprocess.check_call([
sys.executable, 'tools/clang/scripts/update.py', '--package',
'coverage_tools'
])
if coverage_utils.GetHostPlatform() == 'win':
LLVM_COV_PATH += '.exe'
LLVM_PROFDATA_PATH += '.exe'
coverage_tools_exist = (
os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
assert coverage_tools_exist, ('Cannot find coverage tools, please make sure '
'both \'%s\' and \'%s\' exist.') % (
LLVM_COV_PATH, LLVM_PROFDATA_PATH)
def _GetPathWithLLVMSymbolizerDir():
"""Add llvm-symbolizer directory to path for symbolized stacks."""
path = os.getenv('PATH')
dirs = path.split(os.pathsep)
if LLVM_BIN_DIR in dirs:
return path
return path + os.pathsep + LLVM_BIN_DIR
def _GetTargetOS():
"""Returns the target os specified in args.gn file.
Returns an empty string is target_os is not specified.
"""
build_args = _GetBuildArgs()
return build_args['target_os'] if 'target_os' in build_args else ''
def _IsAndroid():
"""Returns true if the target_os specified in args.gn file is android"""
return _GetTargetOS() == 'android'
def _IsIOS():
"""Returns true if the target_os specified in args.gn file is ios"""
return _GetTargetOS() == 'ios'
def _GeneratePerFileLineByLineCoverageInFormat(binary_paths, profdata_file_path,
filters, ignore_filename_regex,
output_format):
"""Generates per file line-by-line coverage in html or text using
'llvm-cov show'.
For a file with absolute path /a/b/x.cc, a html/txt report is generated as:
OUTPUT_DIR/coverage/a/b/x.cc.[html|txt]. For html format, an index html file
is also generated as: OUTPUT_DIR/index.html.
Args:
binary_paths: A list of paths to the instrumented binaries.
profdata_file_path: A path to the profdata file.
filters: A list of directories and files to get coverage for.
ignore_filename_regex: A regular expression for skipping source code files
with certain file paths.
output_format: The output format of generated report files.
"""
# llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
# [[-object BIN]] [SOURCES]
# NOTE: For object files, the first one is specified as a positional argument,
# and the rest are specified as keyword argument.
logging.debug('Generating per file line by line coverage reports using '
'"llvm-cov show" command.')
subprocess_cmd = [
LLVM_COV_PATH, 'show', '-format={}'.format(output_format),
'-compilation-dir={}'.format(BUILD_DIR),
'-output-dir={}'.format(OUTPUT_DIR),
'-instr-profile={}'.format(profdata_file_path), binary_paths[0]
]
subprocess_cmd.extend(
['-object=' + binary_path for binary_path in binary_paths[1:]])
_AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
if coverage_utils.GetHostPlatform() in ['linux', 'mac']:
subprocess_cmd.extend(['-Xdemangler', 'c++filt', '-Xdemangler', '-n'])
subprocess_cmd.extend(filters)
if ignore_filename_regex:
subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
subprocess.check_call(subprocess_cmd)
logging.debug('Finished running "llvm-cov show" command.')
def _GeneratePerFileLineByLineCoverageInLcov(binary_paths, profdata_file_path,
filters, ignore_filename_regex):
"""Generates per file line-by-line coverage using "llvm-cov export".
Args:
binary_paths: A list of paths to the instrumented binaries.
profdata_file_path: A path to the profdata file.
filters: A list of directories and files to get coverage for.
ignore_filename_regex: A regular expression for skipping source code files
with certain file paths.
"""
logging.debug('Generating per file line by line coverage reports using '
'"llvm-cov export" command.')
for path in binary_paths:
if not os.path.exists(path):
logging.error("Binary %s does not exist", path)
subprocess_cmd = [
LLVM_COV_PATH, 'export', '-format=lcov',
'-instr-profile=' + profdata_file_path, binary_paths[0]
]
subprocess_cmd.extend(
['-object=' + binary_path for binary_path in binary_paths[1:]])
_AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
subprocess_cmd.extend(filters)
if ignore_filename_regex:
subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
# Write output on the disk to be used by code coverage bot.
with open(_GetLcovFilePath(), 'w') as f:
subprocess.check_call(subprocess_cmd, stdout=f)
logging.debug('Finished running "llvm-cov export" command.')
def _GetLogsDirectoryPath():
"""Path to the logs directory."""
return os.path.join(
coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR), LOGS_DIR_NAME)
def _GetProfdataFilePath():
"""Path to the resulting .profdata file."""
return os.path.join(
coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR),
PROFDATA_FILE_NAME)
def _GetSummaryFilePath():
"""The JSON file that contains coverage summary written by llvm-cov export."""
return os.path.join(
coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR),
SUMMARY_FILE_NAME)
def _GetLcovFilePath():
"""The LCOV file that contains coverage data written by llvm-cov export."""
return os.path.join(
coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR),
LCOV_FILE_NAME)
def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
"""Builds and runs target to generate the coverage profile data.
Args:
targets: A list of targets to build with coverage instrumentation.
commands: A list of commands used to run the targets.
jobs_count: Number of jobs to run in parallel for building. If None, a
default value is derived based on CPUs availability.
Returns:
A relative path to the generated profdata file.
"""
_BuildTargets(targets, jobs_count)
target_profdata_file_paths = _GetTargetProfDataPathsByExecutingCommands(
targets, commands)
coverage_profdata_file_path = (
_CreateCoverageProfileDataFromTargetProfDataFiles(
target_profdata_file_paths))
for target_profdata_file_path in target_profdata_file_paths:
os.remove(target_profdata_file_path)
return coverage_profdata_file_path
def _BuildTargets(targets, jobs_count):
"""Builds target with Clang coverage instrumentation.
This function requires current working directory to be the root of checkout.
Args:
targets: A list of targets to build with coverage instrumentation.
jobs_count: Number of jobs to run in parallel for compilation. If None, a
default value is derived based on CPUs availability.
"""
logging.info('Building %s.', str(targets))
autoninja = 'autoninja'
if coverage_utils.GetHostPlatform() == 'win':
autoninja += '.bat'
subprocess_cmd = [autoninja, '-C', BUILD_DIR]
if jobs_count is not None:
subprocess_cmd.append('-j' + str(jobs_count))
subprocess_cmd.extend(targets)
subprocess.check_call(subprocess_cmd, shell=os.name == 'nt')
logging.debug('Finished building %s.', str(targets))
def _GetTargetProfDataPathsByExecutingCommands(targets, commands):
"""Runs commands and returns the relative paths to the profraw data files.
Args:
targets: A list of targets built with coverage instrumentation.
commands: A list of commands used to run the targets.
Returns:
A list of relative paths to the generated profraw data files.
"""
logging.debug('Executing the test commands.')
# Remove existing profraw data files.
report_root_dir = coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR)
for file_or_dir in os.listdir(report_root_dir):
if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
os.remove(os.path.join(report_root_dir, file_or_dir))
# Ensure that logs directory exists.
if not os.path.exists(_GetLogsDirectoryPath()):
os.makedirs(_GetLogsDirectoryPath())
profdata_file_paths = []
# Run all test targets to generate profraw data files.
for target, command in zip(targets, commands):
output_file_name = os.extsep.join([target + '_output', 'log'])
output_file_path = os.path.join(_GetLogsDirectoryPath(), output_file_name)
profdata_file_path = None
for _ in range(MERGE_RETRIES):
logging.info('Running command: "%s", the output is redirected to "%s".',
command, output_file_path)
if _IsIOSCommand(command):
# On iOS platform, due to lack of write permissions, profraw files are
# generated outside of the OUTPUT_DIR, and the exact paths are contained
# in the output of the command execution.
output = _ExecuteIOSCommand(command, output_file_path)
else:
# On other platforms, profraw files are generated inside the OUTPUT_DIR.
output = _ExecuteCommand(target, command, output_file_path)
profraw_file_paths = []
if _IsIOS():
profraw_file_paths = [_GetProfrawDataFileByParsingOutput(output)]
elif _IsAndroid():
android_coverage_dir = os.path.join(BUILD_DIR, 'coverage')
for r, _, files in os.walk(android_coverage_dir):
for f in files:
if f.endswith(PROFRAW_FILE_EXTENSION):
profraw_file_paths.append(os.path.join(r, f))
else:
for file_or_dir in os.listdir(report_root_dir):
if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
profraw_file_paths.append(
os.path.join(report_root_dir, file_or_dir))
assert profraw_file_paths, (
'Running target "%s" failed to generate any profraw data file, '
'please make sure the binary exists, is properly instrumented and '
'does not crash. %s' % (target, FILE_BUG_MESSAGE))
assert isinstance(profraw_file_paths, list), (
'Variable \'profraw_file_paths\' is expected to be of type \'list\', '
'but it is a %s. %s' % (type(profraw_file_paths), FILE_BUG_MESSAGE))
try:
profdata_file_path = _CreateTargetProfDataFileFromProfRawFiles(
target, profraw_file_paths)
break
except Exception:
logging.info('Retrying...')
finally:
# Remove profraw files now so that they are not used in next iteration.
for profraw_file_path in profraw_file_paths:
os.remove(profraw_file_path)
assert profdata_file_path, (
'Failed to merge target "%s" profraw files after %d retries. %s' %
(target, MERGE_RETRIES, FILE_BUG_MESSAGE))
profdata_file_paths.append(profdata_file_path)
logging.debug('Finished executing the test commands.')
return profdata_file_paths
def _GetEnvironmentVars(profraw_file_path):
"""Return environment vars for subprocess, given a profraw file path."""
env = os.environ.copy()
env.update({
'LLVM_PROFILE_FILE': profraw_file_path,
'PATH': _GetPathWithLLVMSymbolizerDir()
})
return env
def _SplitCommand(command):
"""Split a command string into parts in a platform-specific way."""
if coverage_utils.GetHostPlatform() == 'win':
return command.split()
split_command = shlex.split(command)
# Python's subprocess does not do glob expansion, so we expand it out here.
new_command = []
for item in split_command:
if '*' in item:
files = glob.glob(item)
for file in files:
new_command.append(file)
else:
new_command.append(item)
return new_command
def _ExecuteCommand(target, command, output_file_path):
"""Runs a single command and generates a profraw data file."""
# Per Clang "Source-based Code Coverage" doc:
#
# "%p" expands out to the process ID. It's not used by this scripts due to:
# 1) If a target program spawns too many processess, it may exhaust all disk
# space available. For example, unit_tests writes thousands of .profraw
# files each of size 1GB+.
# 2) If a target binary uses shared libraries, coverage profile data for them
# will be missing, resulting in incomplete coverage reports.
#
# "%Nm" expands out to the instrumented binary's signature. When this pattern
# is specified, the runtime creates a pool of N raw profiles which are used
# for on-line profile merging. The runtime takes care of selecting a raw
# profile from the pool, locking it, and updating it before the program exits.
# N must be between 1 and 9. The merge pool specifier can only occur once per
# filename pattern.
#
# "%1m" is used when tests run in single process, such as fuzz targets.
#
# For other cases, "%4m" is chosen as it creates some level of parallelism,
# but it's not too big to consume too much computing resource or disk space.
profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
expected_profraw_file_name = os.extsep.join(
[target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
expected_profraw_file_path = os.path.join(
coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR),
expected_profraw_file_name)
command = command.replace(LLVM_PROFILE_FILE_PATH_SUBSTITUTION,
expected_profraw_file_path)
try:
# Some fuzz targets or tests may write into stderr, redirect it as well.
with open(output_file_path, 'wb') as output_file_handle:
subprocess.check_call(_SplitCommand(command),
stdout=output_file_handle,
stderr=subprocess.STDOUT,
env=_GetEnvironmentVars(expected_profraw_file_path))
except subprocess.CalledProcessError as e:
logging.warning('Command: "%s" exited with non-zero return code.', command)
return open(output_file_path, 'rb').read()
def _IsFuzzerTarget(target):
"""Returns true if the target is a fuzzer target."""
build_args = _GetBuildArgs()
use_libfuzzer = ('use_libfuzzer' in build_args and
build_args['use_libfuzzer'] == 'true')
use_centipede = ('use_centipede' in build_args
and build_args['use_centipede'] == 'true')
return (use_libfuzzer or use_centipede) and target.endswith('_fuzzer')
def _ExecuteIOSCommand(command, output_file_path):
"""Runs a single iOS command and generates a profraw data file.
iOS application doesn't have write access to folders outside of the app, so
it's impossible to instruct the app to flush the profraw data file to the
desired location. The profraw data file will be generated somewhere within the
application's Documents folder, and the full path can be obtained by parsing
the output.
"""
assert _IsIOSCommand(command)
# After running tests, iossim generates a profraw data file, it won't be
# needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
# checkout.
iossim_profraw_file_path = os.path.join(
OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
command = command.replace(LLVM_PROFILE_FILE_PATH_SUBSTITUTION,
iossim_profraw_file_path)
try:
with open(output_file_path, 'wb') as output_file_handle:
subprocess.check_call(_SplitCommand(command),
stdout=output_file_handle,
stderr=subprocess.STDOUT,
env=_GetEnvironmentVars(iossim_profraw_file_path))
except subprocess.CalledProcessError as e:
# iossim emits non-zero return code even if tests run successfully, so
# ignore the return code.
pass
return open(output_file_path, 'rb').read()
def _GetProfrawDataFileByParsingOutput(output):
"""Returns the path to the profraw data file obtained by parsing the output.
The output of running the test target has no format, but it is guaranteed to
have a single line containing the path to the generated profraw data file.
NOTE: This should only be called when target os is iOS.
"""
assert _IsIOS()
output_by_lines = ''.join(output).splitlines()
profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
for line in output_by_lines:
result = profraw_file_pattern.match(line)
if result:
return result.group(1)
assert False, ('No profraw data file was generated, did you call '
'coverage_util::ConfigureCoverageReportPath() in test setup? '
'Please refer to base/test/test_support_ios.mm for example.')
def _CreateCoverageProfileDataFromTargetProfDataFiles(profdata_file_paths):
"""Returns a relative path to coverage profdata file by merging target
profdata files.
Args:
profdata_file_paths: A list of relative paths to the profdata data files
that are to be merged.
Returns:
A relative path to the merged coverage profdata file.
Raises:
CalledProcessError: An error occurred merging profdata files.
"""
logging.info('Creating the coverage profile data file.')
logging.debug('Merging target profraw files to create target profdata file.')
profdata_file_path = _GetProfdataFilePath()
try:
subprocess_cmd = [
LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
]
subprocess_cmd.extend(profdata_file_paths)
output = subprocess.check_output(subprocess_cmd)
logging.debug('Merge output: %s', output)
except subprocess.CalledProcessError as error:
logging.error(
'Failed to merge target profdata files to create coverage profdata. %s',
FILE_BUG_MESSAGE)
raise error
logging.debug('Finished merging target profdata files.')
logging.info('Code coverage profile data is created as: "%s".',
profdata_file_path)
return profdata_file_path
def _CreateTargetProfDataFileFromProfRawFiles(target, profraw_file_paths):
"""Returns a relative path to target profdata file by merging target
profraw files.
Args:
profraw_file_paths: A list of relative paths to the profdata data files
that are to be merged.
Returns:
A relative path to the merged coverage profdata file.
Raises:
CalledProcessError: An error occurred merging profdata files.
"""
logging.info('Creating target profile data file.')
logging.debug('Merging target profraw files to create target profdata file.')
profdata_file_path = os.path.join(OUTPUT_DIR, '%s.profdata' % target)
try:
subprocess_cmd = [
LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
]
subprocess_cmd.extend(profraw_file_paths)
output = subprocess.check_output(subprocess_cmd)
logging.debug('Merge output: %s', output)
except subprocess.CalledProcessError as error:
logging.error(
'Failed to merge target profraw files to create target profdata.')
raise error
logging.debug('Finished merging target profraw files.')
logging.info('Target "%s" profile data is created as: "%s".', target,
profdata_file_path)
return profdata_file_path
def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters,
ignore_filename_regex):
"""Generates per file coverage summary using "llvm-cov export" command."""
# llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
# [[-object BIN]] [SOURCES].
# NOTE: For object files, the first one is specified as a positional argument,
# and the rest are specified as keyword argument.
logging.debug('Generating per-file code coverage summary using "llvm-cov '
'export -summary-only" command.')
for path in binary_paths:
if not os.path.exists(path):
logging.error("Binary %s does not exist", path)
subprocess_cmd = [
LLVM_COV_PATH, 'export', '-summary-only',
'-compilation-dir={}'.format(BUILD_DIR),
'-instr-profile=' + profdata_file_path, binary_paths[0]
]
subprocess_cmd.extend(
['-object=' + binary_path for binary_path in binary_paths[1:]])
_AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
subprocess_cmd.extend(filters)
if ignore_filename_regex:
subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
export_output = subprocess.check_output(subprocess_cmd)
# Write output on the disk to be used by code coverage bot.
with open(_GetSummaryFilePath(), 'wb') as f:
f.write(export_output)
return export_output
def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
"""Appends -arch arguments to the command list if it's ios platform.
iOS binaries are universal binaries, and require specifying the architecture
to use, and one architecture needs to be specified for each binary.
"""
if _IsIOS():
cmd_list.extend(['-arch=x86_64'] * num_archs)
def _GetBinaryPath(command):
"""Returns a relative path to the binary to be run by the command.
Currently, following types of commands are supported (e.g. url_unittests):
1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
2. Use xvfb.
2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
for its usage.
3.1. "out/Coverage-iphonesimulator/iossim
<iossim_arguments> -c <app_arguments>
out/Coverage-iphonesimulator/url_unittests.app"
Args:
command: A command used to run a target.
Returns:
A relative path to the binary.
"""
xvfb_script_name = os.extsep.join(['xvfb', 'py'])
command_parts = _SplitCommand(command)
if os.path.basename(command_parts[0]) == 'python':
assert os.path.basename(command_parts[1]) == xvfb_script_name, (
'This tool doesn\'t understand the command: "%s".' % command)
return command_parts[2]
if os.path.basename(command_parts[0]) == xvfb_script_name:
return command_parts[1]
if _IsIOSCommand(command):
# For a given application bundle, the binary resides in the bundle and has
# the same name with the application without the .app extension.
app_path = command_parts[1].rstrip(os.path.sep)
app_name = os.path.splitext(os.path.basename(app_path))[0]
return os.path.join(app_path, app_name)
if coverage_utils.GetHostPlatform() == 'win' \
and not command_parts[0].endswith('.exe'):
return command_parts[0] + '.exe'
return command_parts[0]
def _IsIOSCommand(command):
"""Returns true if command is used to run tests on iOS platform."""
return os.path.basename(_SplitCommand(command)[0]) == 'iossim'
def _VerifyTargetExecutablesAreInBuildDirectory(commands):
"""Verifies that the target executables specified in the commands are inside
the given build directory."""
for command in commands:
binary_path = _GetBinaryPath(command)
binary_absolute_path = coverage_utils.GetFullPath(binary_path)
assert binary_absolute_path.startswith(BUILD_DIR + os.sep), (
'Target executable "%s" in command: "%s" is outside of '
'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
def _ValidateBuildingWithClangCoverage():
"""Asserts that targets are built with Clang coverage enabled."""
build_args = _GetBuildArgs()
if (CLANG_COVERAGE_BUILD_ARG not in build_args or
build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
assert False, ('\'{} = true\' is required in args.gn.'
).format(CLANG_COVERAGE_BUILD_ARG)
def _ValidateCurrentPlatformIsSupported():
"""Asserts that this script suports running on the current platform"""
target_os = _GetTargetOS()
if target_os:
current_platform = target_os
else:
current_platform = coverage_utils.GetHostPlatform()
supported_platforms = ['android', 'chromeos', 'ios', 'linux', 'mac', 'win']
assert current_platform in supported_platforms, ('Coverage is only'
'supported on %s' %
supported_platforms)
def _GetBuildArgs():
"""Parses args.gn file and returns results as a dictionary.
Returns:
A dictionary representing the build args.
"""
global _BUILD_ARGS
if _BUILD_ARGS is not None:
return _BUILD_ARGS
_BUILD_ARGS = {}
build_args_path = os.path.join(BUILD_DIR, 'args.gn')
assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
'missing args.gn file.' % BUILD_DIR)
with open(build_args_path) as build_args_file:
build_args_lines = build_args_file.readlines()
for build_arg_line in build_args_lines:
build_arg_without_comments = build_arg_line.split('#')[0]
key_value_pair = build_arg_without_comments.split('=')
if len(key_value_pair) != 2:
continue
key = key_value_pair[0].strip()
# Values are wrapped within a pair of double-quotes, so remove the leading
# and trailing double-quotes.
value = key_value_pair[1].strip().strip('"')
_BUILD_ARGS[key] = value
return _BUILD_ARGS
def _VerifyPathsAndReturnAbsolutes(paths):
"""Verifies that the paths specified in |paths| exist and returns absolute
versions.
Args:
paths: A list of files or directories.
"""
absolute_paths = []
for path in paths:
absolute_path = os.path.join(SRC_ROOT_PATH, path)
assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
absolute_paths.append(absolute_path)
return absolute_paths
def _GetBinaryPathsFromTargets(targets, build_dir):
"""Return binary paths from target names."""
# TODO(crbug.com/899974): Derive output binary from target build definitions
# rather than assuming that it is always the same name.
binary_paths = []
for target in targets:
binary_path = os.path.join(build_dir, target)
if coverage_utils.GetHostPlatform() == 'win':
binary_path += '.exe'
if os.path.exists(binary_path):
binary_paths.append(binary_path)
else:
logging.warning(
'Target binary "%s" not found in build directory, skipping.',
os.path.basename(binary_path))
return binary_paths
def _GetCommandForWebTests(targets, arguments):
"""Return command to run for blink web tests."""
assert len(targets) == 1, "Only one wpt target can be run"
target = targets[0]
expected_profraw_file_name = os.extsep.join(
[target, '%2m', PROFRAW_FILE_EXTENSION])
expected_profraw_file_path = os.path.join(
coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR),
expected_profraw_file_name)
cpu_count = multiprocessing.cpu_count()
if sys.platform == 'win32':
# TODO(crbug.com/1190269) - we can't use more than 56
# cores on Windows or Python3 may hang.
cpu_count = min(cpu_count, 56)
cpu_count = max(1, cpu_count // 2)
command_list = [
'third_party/blink/tools/run_web_tests.py',
'--additional-driver-flag=--no-sandbox',
'--additional-env-var=LLVM_PROFILE_FILE=%s' % expected_profraw_file_path,
'--child-processes=%d' % cpu_count, '--disable-breakpad',
'--no-show-results', '--skip-failing-tests',
'--target=%s' % os.path.basename(BUILD_DIR), '--timeout-ms=30000'
]
if arguments.strip():
command_list.append(arguments)
return ' '.join(command_list)
def _GetBinaryPathsForAndroid(targets):
"""Return binary paths used when running android tests."""
# TODO(crbug.com/899974): Implement approach that doesn't assume .so file is
# based on the target's name.
android_binaries = set()
for target in targets:
so_library_path = os.path.join(BUILD_DIR, 'lib.unstripped',
'lib%s__library.so' % target)
if os.path.exists(so_library_path):
android_binaries.add(so_library_path)
return list(android_binaries)
def _GetBinaryPathForWebTests():
"""Return binary path used to run blink web tests."""
host_platform = coverage_utils.GetHostPlatform()
if host_platform == 'win':
return os.path.join(BUILD_DIR, 'content_shell.exe')
elif host_platform == 'linux':
return os.path.join(BUILD_DIR, 'content_shell')
elif host_platform == 'mac':
return os.path.join(BUILD_DIR, 'Content Shell.app', 'Contents', 'MacOS',
'Content Shell')
else:
assert False, 'This platform is not supported for web tests.'
def _SetupOutputDir():
"""Setup output directory."""
if os.path.exists(OUTPUT_DIR):
shutil.rmtree(OUTPUT_DIR)
# Creates |OUTPUT_DIR| and its platform sub-directory.
os.makedirs(coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR))
def _SetMacXcodePath():
"""Set DEVELOPER_DIR to the path to hermetic Xcode.app on Mac OS X."""
if sys.platform != 'darwin':
return
xcode_path = os.path.join(SRC_ROOT_PATH, 'build', 'mac_files', 'Xcode.app')
if os.path.exists(xcode_path):
os.environ['DEVELOPER_DIR'] = xcode_path
def _ParseCommandArguments():
"""Adds and parses relevant arguments for tool comands.
Returns:
A dictionary representing the arguments.
"""
arg_parser = argparse.ArgumentParser()
arg_parser.usage = __doc__
arg_parser.add_argument(
'-b',
'--build-dir',
type=str,
required=True,
help='The build directory, the path needs to be relative to the root of '
'the checkout.')
arg_parser.add_argument(
'-o',
'--output-dir',
type=str,
required=True,
help='Output directory for generated artifacts.')
arg_parser.add_argument(
'-c',
'--command',
action='append',
required=False,
help='Commands used to run test targets, one test target needs one and '
'only one command, when specifying commands, one should assume the '
'current working directory is the root of the checkout. This option is '
'incompatible with -p/--profdata-file option.')
arg_parser.add_argument(
'-wt',
'--web-tests',
nargs='?',
type=str,
const=' ',
required=False,
help='Run blink web tests. Support passing arguments to run_web_tests.py')
arg_parser.add_argument(
'-p',
'--profdata-file',
type=str,
action='append',
required=False,
help=
'Path(s) to profdata file(s) to use for generating code coverage reports. '
'This can be useful if you generated the profdata file seperately in '
'your own test harness. This option is ignored if run command(s) are '
'already provided above using -c/--command option.')
arg_parser.add_argument(
'-f',
'--filters',
action='append',