33from conan .tools .build import can_run
44from conan .tools .env import VirtualRunEnv , VirtualBuildEnv
55from pathlib import Path
6+ import subprocess
67import shutil
78import yaml
89import os
910sep = os .path .sep
11+ _get_file_name = (lambda x : x .split (sep )[- 1 ])
1012
1113
1214def _clear_test_build ():
@@ -30,6 +32,7 @@ def _recursive_find(root: str, obj_files: list[str]):
3032
3133def _entry_lists () -> list [str ]:
3234 return ['#include <gtest/gtest.h>\n ' ,
35+ '\n ' ,
3336 '\n ' ,
3437 'int main(int argc, char **argv) {\n ' ,
3538 ' ::testing::InitGoogleTest(&argc, argv);\n ' ,
@@ -68,6 +71,7 @@ def generate(self):
6871 tc .variables ["LIB_NAME" ] = lib_name
6972 tc .variables ["CXX_DEPS" ] = self ._get_targets ()
7073 tc .variables ["TRIGGER_TESTS" ] = self .metadata .get ('trigger_tests' )
74+ tc .variables ['ENABLE_COVERAGE' ] = self .metadata .get ('activate_code_coverage' )
7175 tc .variables ["MAIN_LIB_TARGET" ] = [_a := self .metadata .get ('target' ),
7276 f'{ lib_name } ::{ lib_name } ' if _a == 'auto' else _a ][- 1 ]
7377 tc .generate ()
@@ -100,10 +104,11 @@ def layout(self):
100104
101105 def configure (self ):
102106 supported_compilers = {"gcc" , "msvc" , "clang" , "apple-clang" , } # no support for 'Visual Studio' in Conan1.0
103- if self .settings .compiler .__str__ () in supported_compilers :
107+ compiler = getattr (self .settings , 'compiler' )
108+ if compiler .__str__ () in supported_compilers :
104109 _build_std = self .metadata .get ("build_cppstd" )
105110 _build_std = "17" if _build_std not in {"17" , "20" , "23" } else _build_std # fallback
106- self . settings . compiler .cppstd = _build_std
111+ compiler .cppstd = _build_std
107112
108113 def test (self ):
109114
@@ -138,22 +143,105 @@ def test(self):
138143
139144 self ._remove_entries ()
140145
146+ if self .metadata .get ('activate_code_coverage' ):
147+ self ._code_coverage_auto ()
148+
149+ def _code_coverage_auto (self ):
150+ compiler = getattr (self .settings , 'compiler' ).__str__ ()
151+ if compiler == 'gcc' :
152+ self ._code_coverage_gcc ()
153+ elif compiler == 'clang' :
154+ self ._code_coverage_clang ()
155+ else :
156+ raise NotImplementedError (f'Compiler { compiler } is not supported.' )
157+
158+ def _code_coverage_clang (self ):
159+ raise NotImplementedError ('Clang is under implementation' )
160+
161+ def _code_coverage_gcc (self ):
162+
163+ # get conan build folder
164+ _name , _ver = [self .metadata .get (_ ) for _ in ['name' , 'version' ]]
165+ _tmp = subprocess .run (["conan" , "list" , f"{ _name } /{ _ver } :*" ], capture_output = True , text = True )
166+ _tmp_ref = [str (_ ).strip () for _ in _tmp .stdout .split ('\n ' )]
167+ _pkg_uid = _tmp_ref [[i for i , _ in enumerate (_tmp_ref ) if _ == 'packages' ][0 ] + 1 ]
168+ _tmp = subprocess .run (["conan" , "cache" , "path" , f"{ _name } /{ _ver } :{ _pkg_uid } " ],
169+ capture_output = True , text = True )
170+ _main_pkg_build_fd = sep .join (_tmp .stdout .split (sep )[:- 1 ] + ['b' , 'build' ])
171+
172+ # collect code coverage files to export/coverage/
173+ _gcda = [str (_ ) for _ in list (Path (_main_pkg_build_fd ).rglob ('*.gcda' ))]
174+ _gcno = [_ [:- 4 ] + 'gcno' for _ in _gcda ]
175+
176+ target_folder = self .recipe_folder + sep + 'test' + sep + 'export'
177+ coverage_folder = target_folder + sep + 'coverage'
178+ if not os .path .exists (target_folder ):
179+ os .mkdir (target_folder )
180+ else :
181+ if os .path .exists (coverage_folder ):
182+ shutil .rmtree (coverage_folder )
183+ os .mkdir (coverage_folder )
184+
185+ for v1 , v2 in zip (_gcda , _gcno ):
186+ shutil .copy2 (v1 , coverage_folder + sep + _get_file_name (v1 ))
187+ shutil .copy2 (v2 , coverage_folder + sep + _get_file_name (v2 ))
188+
189+ # auto html report generation
190+ cmd1 = ['lcov' , '--directory' , coverage_folder , '--capture' , '--output-file' ,
191+ os .path .join (coverage_folder , 'coverage_test.info' ), '--rc' , 'geninfo_auto_base=1' ]
192+ subprocess .run (cmd1 , check = True )
193+ cmd2 = ['lcov' , '--extract' , os .path .join (coverage_folder , 'coverage_test.info' ),
194+ f'*/.conan2/p/b/{ _name [:3 ]} *' , '--output-file' ,
195+ os .path .join (coverage_folder , 'coverage_test.filtered.info' )]
196+ subprocess .run (cmd2 , check = True ) # hard-coding: your package name len >= 3
197+ cmd3 = ['genhtml' , os .path .join (coverage_folder , 'coverage_test.filtered.info' ),
198+ '--output-directory' , os .path .join (coverage_folder , 'coverage_report' )]
199+ subprocess .run (cmd3 , check = True )
200+
201+ # remove intermediate files
202+ for _f in os .listdir (coverage_folder ):
203+ _full_name = coverage_folder + sep + _f
204+ if not os .path .isdir (_full_name ):
205+ os .remove (_full_name )
206+
141207 def _add_entries (self ):
142208 if self .metadata .get ('trigger_tests' ):
209+
143210 _f_stress = self .recipe_folder + sep + 'test' + sep + 'stress'
144- _f_unit = self .recipe_folder + sep + 'test' + sep + 'unit'
145211 if not os .path .exists (_m := _f_stress + sep + 'main.cpp' ):
146212 with open (_m , 'w' , encoding = 'utf-8' ) as f :
147213 f .write ('' .join (_entry_lists ()))
148- if not os .path .exists (_m := _f_unit + sep + 'main.cpp' ):
149- with open (_m , 'w' , encoding = 'utf-8' ) as f :
150- f .write ('' .join (_entry_lists ()))
214+
215+ _f_unit = self .recipe_folder + sep + 'test' + sep + 'unit'
216+ if self .metadata .get ('activate_code_coverage' ):
217+ _files = [str (_ ) for _ in list (Path (_f_unit ).rglob ('*.cpp' ))]
218+ _cache = [[_a := _ .split (sep ), (sep .join (_a [:- 1 ]), _a [- 1 ])][- 1 ] for _ in _files ]
219+ for _test_src , _test_ucov in zip (_files , _cache ):
220+ with open (_test_src , 'r' ) as f :
221+ _tmp = f .readlines ()
222+ _tmp .extend (_entry_lists ()[1 :])
223+ with open (_test_ucov [0 ] + sep + 'ucov_' + _test_ucov [1 ], 'w' , encoding = 'utf-8' ) as f :
224+ f .write ('' .join (_tmp ))
225+ else :
226+ if not os .path .exists (_m := _f_unit + sep + 'main.cpp' ):
227+ with open (_m , 'w' , encoding = 'utf-8' ) as f :
228+ f .write ('' .join (_entry_lists ()))
151229
152230 def _remove_entries (self ):
153- if os .path .exists (_f1 := self .recipe_folder + sep + 'test' + sep + 'stress' + sep + 'main.cpp' ):
154- os .remove (_f1 )
155- if os .path .exists (_f2 := self .recipe_folder + sep + 'test' + sep + 'unit' + sep + 'main.cpp' ):
156- os .remove (_f2 )
231+ if self .metadata .get ('trigger_tests' ):
232+
233+ if os .path .exists (_f1 := self .recipe_folder + sep + 'test' + sep + 'stress' + sep + 'main.cpp' ):
234+ os .remove (_f1 )
235+
236+ if self .metadata .get ('activate_code_coverage' ):
237+ _f_unit = self .recipe_folder + sep + 'test' + sep + 'unit'
238+ _files = [str (_ ) for _ in list (Path (_f_unit ).rglob ('*.cpp' ))]
239+ for _m in _files :
240+ if _m .split (sep )[- 1 ].startswith ('ucov_' ):
241+ os .remove (_m )
242+ else :
243+ if os .path .exists (_f3 := self .recipe_folder + sep + 'test' + sep + 'unit' + sep + 'main.cpp' ):
244+ os .remove (_f3 )
157245
158246
159247if __name__ == '__main__' :
0 commit comments