diff --git a/README b/README index dc133dfd..db82db4d 100644 --- a/README +++ b/README @@ -1,6 +1,6 @@ ------------------------------------------------- - README file for the LTP GCOV extension (LCOV) - -- Last changes: 2024-12-25 +- Last changes: 2025-10-01 ------------------------------------------------- Description @@ -405,12 +405,15 @@ LCOV features and capabilities fall into 7 major categories: The user can supply callbacks which are used to: - i) interface with the revision control system + i) interface with the revision control system - to determine source + code differences between versions and to find the author + and date of the most recent edits. Sample scripts: - Perforce: see 'p4diff', 'p4annotate.pm', 'p4annotate' - Git: see 'gitdiff', 'gitblame.pm', 'gitblame' - ii) verify that source code versions are compatible, and - Sample scripts: see 'get_signature', 'P4version.pm', 'getp4version', + ii) verify that source code versions are compatible - e.g., + before aggregating coverage data or displaying source code. + Sample scripts: 'get_signature', 'P4version.pm', 'getp4version', 'gitversion', 'gitversion.pm', and 'batchGitVersion.pm' iii) enforce a desired code coverage criteria Sample script: criteria.pm/criteria @@ -423,11 +426,15 @@ LCOV features and capabilities fall into 7 major categories: Sample script: select.pm vi) keep track of environment and other settings - to aid infrastructure debugging in more complicated use cases. + Sample script: context.pm vii) compress the 'function detail' table to improve readability by shortening long C++ template and function names. + Sample script: simplify.pm The callback may be any desired script or executable - but there may be performance advantages if it is written as a Perl module. + See the "Additional considerations" and "Callbacks and parallel + execution" discussion in the genhtml man page. See the genhtml/lcov/geninfo man pages for details. diff --git a/lib/lcovutil.pm b/lib/lcovutil.pm index a3f6c74a..83295fe4 100644 --- a/lib/lcovutil.pm +++ b/lib/lcovutil.pm @@ -62,6 +62,7 @@ our @EXPORT_OK = qw($tool_name $tool_dir $lcov_version $lcov_url $VERSION @exclude_file_patterns @include_file_patterns %excluded_files @omit_line_patterns @exclude_function_patterns $case_insensitive munge_file_patterns warn_file_patterns transform_pattern + warn_pattern_list parse_cov_filters summarize_cov_filters disable_cov_filters reenable_cov_filters is_filter_enabled filterStringsAndComments simplifyCode balancedParens @@ -219,6 +220,15 @@ our $opt_no_external; our @build_directory; our @configured_callbacks; +# list of callbacks which support save/restore +our @callback_save_restore; +# list of callbacks which support 'finalize' +our @callback_finalize; +# list of callbacks which implement 'start' - which gets called when +# child process starts +our @callback_start_list; +# the callback data which is saved from child process/restored from child process +our @callback_state; # optional callback to keep track of whatever user decides is important our @contextCallback; @@ -988,6 +998,26 @@ sub configure_callback #$package->import(qw(new)); # the first value in @_ is the script name $$cb = $class->new(@args); + if (exists($ENV{LCOV_FORCE_PARALLEL}) || + (defined($lcovutil::maxParallelism) && + 1 != $lcovutil::maxParallelism) + ) { + # don't set up for parallel processing if we aren't going to fork + if ($$cb->can('save')) { + if ($$cb->can('restore')) { + push(@callback_save_restore, [$class, $$cb]); + push(@callback_start_list, [$class, $$cb]) + if ($$cb->can('start')); + } else { + lcovutil::ignorable_error($lcovutil::ERROR_PACKAGE, + "$class implements 'save' but not 'restore'."); + return; + } + } + } + # implement 'finalize', regardless of parallel/not parallel + push(@callback_finalize, [$class, $$cb]) + if ($$cb->can('finalize')); }; if ($@ || !defined($$cb)) { @@ -1745,23 +1775,40 @@ sub munge_file_patterns @suppress_function_patterns = map({ $_->[0] } @exclude_function_patterns); } +sub warn_pattern_list +{ + my ($type, $patterns) = @_; + foreach my $pat (@$patterns) { + my $count = $pat->[-1]; + if (0 == $count) { + my $str = $pat->[-2]; + lcovutil::ignorable_error($ERROR_UNUSED, + "'$type' pattern '$str' is unused."); + } + } +} + sub warn_file_patterns { + # a bit of a hack...we need a place to call the 'finalize' methods + # (if any are registered) - and this method is called very late in + # the game, by lcov/genhtml/geninfo - so is a workable location + for (my $i = 0; $i <= $#lcovutil::callback_finalize; ++$i) { + my ($class, $cb) = @{$lcovutil::callback_finalize[$i]}; + eval { $cb->finalize(); }; + if ($@) { + lcovutil::ignorable_error($lcovutil::ERROR_CALLBACK, + "\"$class->finalize()\" failed: $@"); + } + } + foreach my $p (['include', \@include_file_patterns], ['exclude', \@exclude_file_patterns], ['substitute', \@file_subst_patterns], ['omit-lines', \@omit_line_patterns], ['exclude-functions', \@exclude_function_patterns], ) { - my ($type, $patterns) = @$p; - foreach my $pat (@$patterns) { - my $count = $pat->[scalar(@$pat) - 1]; - if (0 == $count) { - my $str = $pat->[scalar(@$pat) - 2]; - lcovutil::ignorable_error($ERROR_UNUSED, - "'$type' pattern '$str' is unused."); - } - } + warn_pattern_list(@$p); } } @@ -2081,14 +2128,24 @@ sub initial_state } } + for (my $i = 0; $i <= $#lcovutil::callback_start_list; ++$i) { + my ($class, $cb) = @{$lcovutil::callback_start_list[$i]}; + eval { $cb->start(); }; + if ($@) { + lcovutil::ignorable_error($lcovutil::ERROR_CALLBACK, + "\"$class->start()\" failed: $@"); + } + } + return Storable::dclone([\@message_count, \%versionCache, \%resolveCache]); } sub compute_update { my $state = shift; - my @new_count; my ($initialCount, $initialVersionCache, $initialResolveCache) = @$state; + + my @new_count; my $id = 0; foreach my $count (@message_count) { my $v = $count - $initialCount->[$id++]; @@ -2104,7 +2161,20 @@ sub compute_update $resolveUpdate{$f} = $v unless exists($initialResolveCache->{$f}); } - my @rtn = (\@new_count, + my @cbData; + for (my $i = 0; $i <= $#lcovutil::callback_save_restore; ++$i) { + my ($class, $cb) = @{$lcovutil::callback_save_restore[$i]}; + eval { + my $data = $cb->save(); + push(@cbData, $data); + }; + if ($@) { + lcovutil::ignorable_error($lcovutil::ERROR_CALLBACK, + "\"$class->save(...)\" failed: $@"); + } + } + my @rtn = (\@cbData, + \@new_count, \%versionUpdate, \%resolveUpdate, \%message_types, @@ -2131,6 +2201,15 @@ sub compute_update sub update_state { + my $callbackData = shift; + for (my $i = 0; $i <= $#$callbackData; ++$i) { + my ($class, $cb) = @{$lcovutil::callback_save_restore[$i]}; + eval { $cb->restore($callbackData->[$i]); }; + if ($@) { + lcovutil::ignorable_error($lcovutil::ERROR_CALLBACK, + "\"$class->restore(...)\" failed: $@"); + } + } my $updateCount = shift; my $id = 0; foreach my $count (@$updateCount) { diff --git a/man/genhtml.1 b/man/genhtml.1 index 42a8cff6..739983e9 100644 --- a/man/genhtml.1 +++ b/man/genhtml.1 @@ -631,7 +631,7 @@ and does not modify information in the coveage DB. .IP 2. 3 The option may be specified as a single .I split_char -separated string which is divied into words (see +separated string which is divided into words (see .B man lcovrc(5) ), or as a list of arguments. The resulting command line is passed @@ -778,6 +778,75 @@ The callback will occur in the child process (possibly simultaneously with other As a result: if your callback needs to pass data back to the parent, you will need to arrange a communication mechanism to do so. .br +.SS Callbacks and parallel execution + +Because callbacks may need to record data - +.I e.g., +for error reporting or action summaries in the presence of parallel execution - +.B genhtml (and +.B lcov and +.B geninfo +) can call certain optional callback methods: + +.IP \- 3 +.I $callback->start() +.br +is called when the child process begins execution. This method can be +used to capture initial state - +.I e.g., +to set the count of events in this child to zero. +This method is optional. +.PP + +.IP \- 3 +.I my $data = $callback->save() +.br +is called when processing is complete, just before the child process exits. +The scalar +.I $data +returned by your +.I $callback->save() +method +.I $callback->restore() +method when the child process is reaped. +.PP + +.IP \- 3 +.I $callback->restore($data) +.br +is called in the parent process when the child is reaped. +.I $data +is the data that was returned when your +.I $callback->save() +method was called in the child. (Serialization/deserialization has happened under the covers.) +.PP + +.IP \- 3 +.I $callback->finalize() +.br +is called in the parent process when all calculalations are complete +and the parent setting up to report final results. +This method is optional. +.br +Note that, unlike the other callback methods described in this section, +.I finalize() +is called in both parallel and serial execution contexts. +.PP + +Note that your callback must implement +.I $callback->restore() +if it implements +.I $callback->save(). +.I $callback->start() +and +.I $callback->finalize() +are optional: if they are implemented, then they will be called. + +These methods are available only for callbacks implemented a perl modules. +If you callback is implemented as an executable script (say) - then you +are free to implement parent/child data passing however you prefer. + + .SS Additional considerations If the diff --git a/scripts/simplify.pm b/scripts/simplify.pm index 3108cea5..db9d8207 100644 --- a/scripts/simplify.pm +++ b/scripts/simplify.pm @@ -92,8 +92,9 @@ EOF # verify that the patterns are valid... lcovutil::verify_regexp_patterns($script, \@patterns); + my @munged = map({ [$_, 0]; } @patterns); - return bless \@patterns, $class; + return bless \@munged, $class; } sub simplify @@ -101,14 +102,53 @@ sub simplify my ($self, $name) = @_; foreach my $p (@$self) { + my $orig = $name; # sadly, no support for pre-compiled patterns - eval "\$name =~ $p ;"; # apply pattern that user provided... + eval "\$name =~ $p->[0] ;"; # apply pattern that user provided... # $@ should never match: we already checked pattern validity during # initialization - above. Still: belt and braces. die("invalid 'simplify' regexp '$p->[0]': $@") if ($@); + ++$p->[1] + if ($name ne $orig); } return $name; } +sub start +{ + my $self = shift; + foreach my $p (@$self) { + $p->[1] = 0; + } +} + +sub save +{ + my $self = shift; + my @data; + foreach my $p (@$self) { + push(@data, $p->[1]); + } + return \@data; +} + +sub restore +{ + my ($self, $data) = @_; + die("unexpected restore: (" . + join(' ', @$self) . ") <- [" . + join(' ', @$data) . "]\n") + unless $#$self == $#$data; + for (my $i = 0; $i <= $#$self; ++$i) { + $self->[$i]->[-1] += $data->[$i]; + } +} + +sub finalize +{ + my $self = shift; + lcovutil::warn_pattern_list("simplify", $self); +} + 1; diff --git a/tests/Makefile b/tests/Makefile index 173cc040..a3405949 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -27,6 +27,7 @@ report: --version-script $(VERSION_SCRIPT) \ --exclude genError.pm --exclude filter.pl \ --exclude brokenCallback.pm --exclude MsgContext.pm \ + --exclude missingRestore.pm --exclude parallelFail.pm \ --omit-lines 'ERROR_INTERNAL' --omit-lines '\bdie\b' \ --ignore unsupported,unused,inconsistent \ --filter region ; \ diff --git a/tests/gendiffcov/errs/genError.pm b/tests/gendiffcov/errs/genError.pm index fa176691..86170f2c 100644 --- a/tests/gendiffcov/errs/genError.pm +++ b/tests/gendiffcov/errs/genError.pm @@ -13,6 +13,9 @@ sub new sub select { + if (grep(/select/, @$self)) { + return 1; + } die("die in select"); } @@ -29,26 +32,46 @@ sub extract_version sub compare_version { + my $self = shift; + if (grep(/compare/, @$self)) { + return 1; + } die("die in compare_version"); } sub annotate { + my $self = shift; + if (grep(/annotate/, @$self)) { + return 'abc'; + } die("die in annotate"); } sub resolve { + my ($self, $data) = @_; + if (grep(/resolve/, @$self)) { + return $data; + } die("die in resolve"); } sub check_criteria { + my $self = shift; + if (grep(/criteria/, @$self)) { + return 0; + } die("die in check_criteria"); } sub simplify { + my ($self, $data) = @_; + if (grep(/simplify/, @$self)) { + return $data; + } die("die in simplify"); } diff --git a/tests/gendiffcov/errs/missingRestore.pm b/tests/gendiffcov/errs/missingRestore.pm new file mode 100644 index 00000000..d383ee60 --- /dev/null +++ b/tests/gendiffcov/errs/missingRestore.pm @@ -0,0 +1,30 @@ +#!/usr/bin/env perl + +# test 'missing restore callback' message + +package missingRestore; + +sub new +{ + my $class = shift; + my $self = [@_]; # don't die if callback name is in list... + return bless $self, $class; +} + +sub simplify +{ + my ($self, $data) = @_; + return $data; +} + +sub start +{ + die("die in simplify"); +} + +sub save +{ + die("die in save"); +} + +1; diff --git a/tests/gendiffcov/errs/msgtest.sh b/tests/gendiffcov/errs/msgtest.sh index a0a294dc..f2b44b38 100755 --- a/tests/gendiffcov/errs/msgtest.sh +++ b/tests/gendiffcov/errs/msgtest.sh @@ -4,7 +4,7 @@ set +x source ../../common.tst rm -f test.cpp *.gcno *.gcda a.out *.info *.log *.json diff.txt loop*.rc markers.err* readThis.rc testing.rc -rm -rf select criteria annotate empty unused_src scriptErr scriptFixed epoch inconsistent highlight etc mycache cacheFail expect subset context labels sortTables +rm -rf select criteria annotate empty unused_src scriptErr scriptFixed epoch inconsistent highlight etc mycache cacheFail expect subset context labels sortTables simplify_* simplify missingRestore clean_cover @@ -674,6 +674,56 @@ for callback in select annotate criteria simplify ; do fi done +# check callback fails in save/restore/start/finalize callbacks +SKIP_ARG='' +for cb in start save restore finalize ; do + echo genhtml $DIFCOV_OPTS initial.info -o simplify_$cb --simplify-script ./parallelFail.pm$SKIP_ARG --parallel + LCOV_FORCE_PARALLEL=1 $COVER $GENHTML_TOOL $DIFFCOV_OPTS initial.info -o simplify_$cb --simplify-script ./parallelFail.pm$SKIP_ARG --parallel 2>&1 | tee simplify_${cb}_err.log + if [ 0 == ${PIPESTATUS[0]} ] ; then + echo "ERROR: genhtml simplify '$cb' passed by accident" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi + fi + SKIP_ARG="$SKIP_ARG,$cb" + grep -E "parallelFail->${cb}.* failed" simplify_${cb}_err.log + if [ 0 != $? ] ; then + echo "ERROR: $cb message" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi + fi +done + +for ignore in '' '--ignore package' ; do + echo genhtml $DIFCOV_OPTS initial.info -o missingRestore --simplify-script ./missingRestore.pm --parallel $ignore + LCOV_FORCE_PARALLEL=1 $COVER $GENHTML_TOOL $DIFFCOV_OPTS initial.info -o missingRestore --simplify-script ./missingRestore.pm --parallel $ignore 2>&1 | tee missingRestore.log + status=${PIPESTATUS[0]} + if [ '' == "$ignore" ] ; then + if [ 0 == $status ] ; then + echo "ERROR: genhtml missing restore passed by accident" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi + fi + else + if [ 0 != $status ] ; then + echo "ERROR: genhtml ignore missing restore failed" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi + fi + fi + grep "implements 'save' but not 'restore'" missingRestore.log + if [ 0 != $? ] ; then + echo "ERROR: missingRestore message" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi + fi +done + + echo genhtml $DIFCOV_OPTS initial.info -o unused_src --source-dir ../.. $COVER $GENHTML_TOOL $DIFFCOV_OPTS initial.info -o unused_src --source-dir ../.. 2>&1 | tee src_err.log if [ 0 == ${PIPESTATUS[0]} ] ; then diff --git a/tests/gendiffcov/errs/parallelFail.pm b/tests/gendiffcov/errs/parallelFail.pm new file mode 100644 index 00000000..b712d52c --- /dev/null +++ b/tests/gendiffcov/errs/parallelFail.pm @@ -0,0 +1,56 @@ +#!/usr/bin/env perl + +# die() when callback is called - to enable error message testing + +package parallelFail; + +sub new +{ + my $class = shift; + my $self = [@_]; # don't die if callback name is in list... + return bless $self, $class; +} + +sub simplify +{ + my ($self, $data) = @_; + return $data; +} + +sub start +{ + my $self = shift; + if (grep(/start/, @$self)) { + return; + } + die("die in simplify"); +} + +sub save +{ + my $self = shift; + if (grep(/save/, @$self)) { + return 'abc'; + } + die("die in save"); +} + +sub restore +{ + my $self = shift; + if (grep(/restore/, @$self)) { + return; + } + die("die in save"); +} + +sub finalize +{ + my $self = shift; + if (grep(/finalize/, @$self)) { + return; + } + die("die in finalize"); +} + +1; diff --git a/tests/lcov/demangle/demangle.sh b/tests/lcov/demangle/demangle.sh index cba6f045..fdf82bd2 100755 --- a/tests/lcov/demangle/demangle.sh +++ b/tests/lcov/demangle/demangle.sh @@ -88,6 +88,32 @@ for callback in './simplify.pl' "${SIMPLIFY_SCRIPT},--sep,;,--re,s/Animal::Anima fi done +# test unused regexp in simplify callback +for PAR in '' '--parallel' ; do + $COVER $GENHTML_TOOL --branch $PARLLEL $PROFILE -o simplify demangle.info --flat --simplify "${SIMPLIFY_SCRIPT},--sep,;,--re,s/Animal::Animal/subst1/;s/Cat::Cat/subst2/;s/subst2/subst3/;s/foo/bar/" $PAR 2>&1 | tee simplifyErr.log + if [ ${PIPESTATUS[0]} == 0 ] ; then + echo "genhtml --simplify unused regexp didn't fail" + exit 1 + fi + grep "'simplify' pattern 's/foo/bar/' is unused" simplifyErr.log + if [ $? != 0 ] ; then + echo "didn't find expected unused error" + exit 1 + fi + + $COVER $GENHTML_TOOL --branch $PARLLEL $PROFILE -o simplify demangle.info --flat --simplify "${SIMPLIFY_SCRIPT},--sep,;,--re,s/Animal::Animal/subst1/;s/Cat::Cat/subst2/;s/subst2/subst3/;s/foo/bar/" $PAR --ignore unused 2>&1 | tee simplifyWarn.log + if [ ${PIPESTATUS[0]} != 0 ] ; then + echo "genhtml --simplify unused regexp warn didn't pass" + exit 1 + fi + grep "'simplify' pattern 's/foo/bar/' is unused" simplifyWarn.log + if [ $? != 0 ] ; then + echo "didn't find expected unused error" + exit 1 + fi +done + + $COVER $LCOV_TOOL $LCOV_OPTS --capture --filter branch --directory . -o vanilla.info $COVER $LCOV_TOOL $LCOV_OPTS --list vanilla.info