1+ #!/usr/bin/env python3
2+ """Generate LLVM coverage report for the project."""
3+
4+ import os
5+ import subprocess
6+ import sys
7+ import glob
8+ import argparse
9+
10+
11+ def run_command (cmd , cwd = None ):
12+ """Run a command and return its output."""
13+ print (f"Running: { ' ' .join (cmd )} " )
14+ result = subprocess .run (cmd , cwd = cwd , capture_output = True , text = True )
15+ if result .returncode != 0 :
16+ print (f"Error: { result .stderr } " )
17+ sys .exit (1 )
18+ return result .stdout
19+
20+
21+ def main ():
22+ parser = argparse .ArgumentParser (description = "Generate LLVM coverage report" )
23+ parser .add_argument ("--build-dir" , default = "build" , help = "Build directory" )
24+ parser .add_argument ("--output-dir" , default = "coverage" , help = "Output directory for coverage report" )
25+ parser .add_argument ("--llvm-version" , default = "20" , help = "LLVM version (default: 20)" )
26+ args = parser .parse_args ()
27+
28+ build_dir = os .path .abspath (args .build_dir )
29+ output_dir = os .path .abspath (args .output_dir )
30+
31+ # Try to find LLVM tools in various locations
32+ llvm_profdata = None
33+ llvm_cov = None
34+
35+ # List of possible LLVM tool names
36+ if args .llvm_version :
37+ profdata_names = [f"llvm-profdata-{ args .llvm_version } " , "llvm-profdata" ]
38+ cov_names = [f"llvm-cov-{ args .llvm_version } " , "llvm-cov" ]
39+ else :
40+ profdata_names = ["llvm-profdata" ]
41+ cov_names = ["llvm-cov" ]
42+
43+ # Try to find the tools
44+ for name in profdata_names :
45+ result = subprocess .run (["which" , name ], capture_output = True , text = True )
46+ if result .returncode == 0 :
47+ llvm_profdata = name
48+ break
49+
50+ for name in cov_names :
51+ result = subprocess .run (["which" , name ], capture_output = True , text = True )
52+ if result .returncode == 0 :
53+ llvm_cov = name
54+ break
55+
56+ if not llvm_profdata or not llvm_cov :
57+ print ("Error: Could not find llvm-profdata or llvm-cov in PATH" )
58+ print ("Make sure LLVM tools are installed and in your PATH" )
59+ sys .exit (1 )
60+
61+ if not os .path .exists (build_dir ):
62+ print (f"Error: Build directory { build_dir } does not exist" )
63+ sys .exit (1 )
64+
65+ # Create output directory
66+ os .makedirs (output_dir , exist_ok = True )
67+
68+ # Find all .profraw files
69+ profraw_files = glob .glob (os .path .join (build_dir , "**" , "*.profraw" ), recursive = True )
70+ if not profraw_files :
71+ print ("No .profraw files found. Make sure to run tests with LLVM_PROFILE_FILE set." )
72+ print ("Example: LLVM_PROFILE_FILE='coverage-%p-%m.profraw' ./your_test" )
73+ sys .exit (1 )
74+
75+ print (f"Found { len (profraw_files )} .profraw files" )
76+
77+ # Merge profiles
78+ profdata_file = os .path .join (output_dir , "coverage.profdata" )
79+ run_command ([llvm_profdata , "merge" , "-sparse" ] + profraw_files + ["-o" , profdata_file ])
80+ print (f"Created merged profile: { profdata_file } " )
81+
82+ # Find all executables in bin directory
83+ bin_dir = os .path .join (build_dir , "bin" )
84+ if not os .path .exists (bin_dir ):
85+ print (f"Error: Bin directory { bin_dir } does not exist" )
86+ sys .exit (1 )
87+
88+ executables = []
89+ for root , dirs , files in os .walk (bin_dir ):
90+ for file in files :
91+ filepath = os .path .join (root , file )
92+ if os .access (filepath , os .X_OK ) and not file .endswith ('.txt' ):
93+ executables .append (filepath )
94+
95+ if not executables :
96+ print ("No executables found in bin directory" )
97+ sys .exit (1 )
98+
99+ print (f"Found { len (executables )} executables" )
100+
101+ # Get the project root directory (parent of build dir)
102+ project_root = os .path .dirname (build_dir )
103+
104+ # Generate LCOV report
105+ lcov_file = os .path .join (output_dir , "coverage.lcov" )
106+ cmd = [llvm_cov , "export" ] + executables + [
107+ "--format=lcov" ,
108+ "--ignore-filename-regex=.*3rdparty/.*|/usr/.*|.*tests/.*|"
109+ ".*tasks/.*|.*modules/runners/.*|.*modules/util/include/perf_test_util.hpp|"
110+ ".*modules/util/include/func_test_util.hpp|.*modules/util/src/func_test_util.cpp" ,
111+ f"--instr-profile={ profdata_file } "
112+ ]
113+
114+ with open (lcov_file , "w" ) as f :
115+ result = subprocess .run (cmd , stdout = f , stderr = subprocess .PIPE , text = True )
116+ if result .returncode != 0 :
117+ print (f"Error generating LCOV report: { result .stderr } " )
118+ sys .exit (1 )
119+
120+ print (f"Generated LCOV report: { lcov_file } " )
121+
122+ # Post-process LCOV file to use relative paths
123+ with open (lcov_file , 'r' ) as f :
124+ lcov_content = f .read ()
125+
126+ # Replace absolute paths with relative paths
127+ lcov_content = lcov_content .replace (project_root + '/' , '' )
128+
129+ with open (lcov_file , 'w' ) as f :
130+ f .write (lcov_content )
131+
132+ print ("Post-processed LCOV file to use relative paths" )
133+
134+ # Generate HTML report
135+ html_dir = os .path .join (output_dir , "html" )
136+ cmd = [llvm_cov , "show" ] + executables + [
137+ "--format=html" ,
138+ f"--output-dir={ html_dir } " ,
139+ "--ignore-filename-regex=.*3rdparty/.*|/usr/.*|.*tests/.*|"
140+ ".*tasks/.*|.*modules/runners/.*|.*modules/util/include/perf_test_util.hpp|"
141+ ".*modules/util/include/func_test_util.hpp|.*modules/util/src/func_test_util.cpp" ,
142+ f"--instr-profile={ profdata_file } "
143+ ]
144+
145+ run_command (cmd )
146+ print (f"Generated HTML report: { html_dir } /index.html" )
147+
148+ # Generate summary
149+ cmd = [llvm_cov , "report" ] + executables + [
150+ f"--instr-profile={ profdata_file } " ,
151+ "--ignore-filename-regex=.*3rdparty/.*|/usr/.*|.*tasks/.*/tests/.*|.*modules/.*/tests/.*|"
152+ ".*tasks/common/runners/.*|.*modules/runners/.*|.*modules/util/include/perf_test_util.hpp|"
153+ ".*modules/util/include/func_test_util.hpp|.*modules/util/src/func_test_util.cpp"
154+ ]
155+
156+ summary = run_command (cmd )
157+ print ("\n Coverage Summary:" )
158+ print (summary )
159+
160+
161+ if __name__ == "__main__" :
162+ main ()
0 commit comments