From ce30de81cbb127ec17f49d3a6c67ea384486b88b Mon Sep 17 00:00:00 2001 From: Henry Cox Date: Thu, 24 Apr 2025 13:30:07 -0400 Subject: [PATCH] Add 'config_file = filename' RC file option - to enable recursive inclusion of config files. See discussion in PR #404 for a discussion of the feature and some potential use models. Signed-off-by: Henry Cox --- lcovrc | 8 ++ lib/lcovutil.pm | 133 ++++++++++++++----------------- man/genhtml.1 | 5 +- man/geninfo.1 | 8 +- man/lcov.1 | 5 +- man/lcovrc.5 | 54 ++++++++++++- tests/gendiffcov/errs/msgtest.sh | 26 +++++- 7 files changed, 161 insertions(+), 78 deletions(-) diff --git a/lcovrc b/lcovrc index fcab0103..c02dcaac 100644 --- a/lcovrc +++ b/lcovrc @@ -7,6 +7,14 @@ # Note that this example script does not include all configuration options # see 'man lcovrc(5) for a complete list and usage description. +# include some other config file +# e.g, user-specific options. Note the environment variable expansion +# config_file = $ENV{HOME}/.user_lcovrc +# or project specific - hard-coded from environment variable +# config_file = /path/to/myproject/.lcovrc +# or in the current run directory +# config_file = $ENV{PWD}/.lcovrc + # Specify an external style sheet file (same as --css-file option of genhtml) #genhtml_css_file = gcov.css diff --git a/lib/lcovutil.pm b/lib/lcovutil.pm index 97b8b3c9..e1e32c66 100644 --- a/lib/lcovutil.pm +++ b/lib/lcovutil.pm @@ -1009,57 +1009,6 @@ sub cleanup_callbacks } } -# -# apply_config(REF, ref) -# -# REF is a reference to a hash containing the following mapping: -# -# key_string => var_ref -# -# where KEY_STRING is a keyword and VAR_REF is a reference to an associated -# variable. If the global configuration hashes CONFIG or OPT_RC contain a value -# for keyword KEY_STRING, VAR_REF will be assigned the value for that keyword. -# -# Return 1 if we set something - -sub _set_config($$$) -{ - my ($ref, $key, $value) = @_; - my $r = $ref->{$key}; - my $t = ref($r); - if ('ARRAY' eq $t) { - info(2, " append $value to list $key\n"); - if ('ARRAY' eq ref($value)) { - push(@$r, @$value); - } else { - push(@$r, $value); - } - } else { - # opt is a scalar or not defined - # only way for $value to NOT be an array is if there is a bug in - # the caller such that a scalar ref was passed where a prior call - # had passed a list ref for the same RC option name - die("unexpected ARRAY for $key value") - if ('ARRAY' eq ref($value)); - $$r = $value; - info(2, " assign $$r to $key\n"); - } -} - -sub apply_config($$) -{ - my ($ref, $config) = @_; - my $set_value = 0; - foreach (keys(%{$ref})) { - # if sufficiently verbose, could mention that key is ignored - next unless exists($config->{$_}); - my $v = $config->{$_}; - $set_value = 1; - _set_config($ref, $_, $v); # write into options - } - return $set_value; -} - # use these list values from the RC file unless the option is # passed on the command line my (@rc_filter, @rc_ignore, @rc_exclude_patterns, @@ -1284,27 +1233,66 @@ sub warnDeprecated return $opt_used; } +sub _set_config($$$) +{ + # write an RC configuration value - array or scalar + my ($ref, $key, $value) = @_; + my $r = $ref->{$key}; + my $t = ref($r); + if ('ARRAY' eq $t) { + info(2, " append $value to list $key\n"); + if ('ARRAY' eq ref($value)) { + push(@$r, @$value); + } else { + push(@$r, $value); + } + } else { + # opt is a scalar or not defined + # only way for $value to NOT be an array is if there is a bug in + # the caller such that a scalar ref was passed where a prior call + # had passed a list ref for the same RC option name + die("unexpected ARRAY for $key value") + if ('ARRAY' eq ref($value)); + $$r = $value; + info(2, " assign $$r to $key\n"); + } +} + # -# read_config(filename) -# -# Read configuration file FILENAME and return a reference to a hash containing -# all valid key=value pairs found. +# read_config(filename, $optionsHash) # +# Read configuration file FILENAME and write supported key/values into +# RC options hash +# Return: 1 if some config value was set, 0 if not (used for error messaging) -sub read_config($) +sub read_config($$); # forward decl, to make perl happy about recursive call +my %included_config_files; +my @include_stack; + +sub read_config($$) { - my $filename = shift; - my %result; + my ($filename, $opts) = @_; my $key; my $value; local *HANDLE; + my $set_value = 0; info(1, "read_config: $filename\n"); + if (exists($included_config_files{abs_path($filename)})) { + lcovutil::ignorable_error($lcovutil::ERROR_USAGE, + 'config file inclusion loop detected: "' . + join('" -> "', @include_stack) . + '" -> "' . $filename . '"'); + return 0; + } + if (!open(HANDLE, "<", $filename)) { lcovutil::ignorable_error($lcovutil::ERROR_USAGE, "cannot read configuration file '$filename': $!"); - return undef; + return 0; # didn't set anything } + $included_config_files{abs_path($filename)} = 1; + push(@include_stack, $filename); VAR: while () { chomp; # Skip comments @@ -1337,15 +1325,15 @@ sub read_config($) } if (defined($key) && defined($value)) { info(2, " set: $key = $value\n"); - if (exists($result{$key})) { - if ('ARRAY' eq ref($result{$key})) { - push(@{$result{$key}}, $value); - } else { - $result{$key} = [$result{$key}, $value]; - } - } else { - $result{$key} = $value; + # special case: read included file + if ($key eq 'config_file') { + $set_value |= read_config($value, $opts); + next; } + # skip if application doesn't use this setting + next unless exists($opts->{$key}); + _set_config($opts, $key, $value); + $set_value = 1; } else { my $context = MessageContext::context(); push( @@ -1357,7 +1345,9 @@ sub read_config($) } } close(HANDLE) or die("unable to close $filename: $!\n"); - return \%result; + delete $included_config_files{abs_path($filename)}; + pop(@include_stack); + return $set_value; } # common utility used by genhtml, geninfo, lcov to clean up RC options, @@ -1387,17 +1377,14 @@ sub apply_rc_params($) if (0 != scalar(@opt_config_files)) { foreach my $f (@opt_config_files) { - my $cfg = read_config($f); - $set_value |= apply_config(\%rcHash, $cfg); + $set_value |= read_config($f, \%rcHash); } } else { foreach my $v (['HOME', '.lcovrc'], ['LCOV_HOME', 'etc', 'lcovrc']) { next unless exists($ENV{$v->[0]}); my $f = File::Spec->catfile($ENV{$v->[0]}, splice(@$v, 1)); if (-r $f) { - my $config = read_config($f); - # Copy configuration file and --rc values to variables - $set_value |= apply_config(\%rcHash, $config); + $set_value |= read_config($f, \%rcHash); last; } } diff --git a/man/genhtml.1 b/man/genhtml.1 index d6f2f51f..ff6a96ca 100644 --- a/man/genhtml.1 +++ b/man/genhtml.1 @@ -3231,7 +3231,10 @@ for more details. Specify a configuration file to use. See man .B lcovrc(5) -for details of the file format and options. +for details of the file format and options. Also see the +.I config_file +entry in the same man page for details on how to include one config file into +another. When this option is specified, neither the system\-wide configuration file /etc/lcovrc, nor the per\-user configuration file ~/.lcovrc is read. diff --git a/man/geninfo.1 b/man/geninfo.1 index 8a5ea629..f4306096 100644 --- a/man/geninfo.1 +++ b/man/geninfo.1 @@ -572,7 +572,13 @@ libtool, disable this option to prevent problems when capturing coverage data. .br .RS Specify a configuration file to use. -See the lcovrc man page for details of the file format and options. +See man +.B lcovrc(5) +for details of the file format and options. +Also see the +.I config_file +entry in the same man page for details on how to include one config file into +another. When this option is specified, neither the system\-wide configuration file /etc/lcovrc, nor the per\-user configuration file ~/.lcovrc is read. diff --git a/man/lcov.1 b/man/lcov.1 index 95449da8..208fbf15 100644 --- a/man/lcov.1 +++ b/man/lcov.1 @@ -671,7 +671,10 @@ libtool, disable this option to prevent problems when capturing coverage data. Specify a configuration file to use. See man .B lcovrc(5) -for details of the file format and options. +for details of the file format and options. Also see the +.I config_file +entry in the same man page for details on how to include one config file into +another. When this option is specified, neither the system\-wide configuration file /etc/lcovrc, nor the per\-user configuration file ~/.lcovrc is read. diff --git a/man/lcovrc.5 b/man/lcovrc.5 index 5b07a4b4..a83fb500 100644 --- a/man/lcovrc.5 +++ b/man/lcovrc.5 @@ -148,7 +148,21 @@ See the OPTIONS section below for details. .br # .br - +# include some other config file +.br +# e.g, user-specific options. Note the environment variable expansion +.br +# config_file = $ENV{HOME}/.user_lcovrc +.br +# or project specific - hard-coded from environment variable +.br +# config_file = /path/to/myproject/.lcovrc +.br +# or in the current run directory +.br +# config_file = $ENV{PWD}/.lcovrc +.br +.br # External style sheet file .br #genhtml_css_file = gcov.css @@ -727,6 +741,44 @@ maximum during parallel processing. .SH OPTIONS +.BR config_file " =" +.I filename +.IP + +Include another config file. + +Inclusion is equivalent to inserting the text from +.I filename +at this point in the current file. As a result, settings from the included +file are processed after earlier settings in the current file, but before later settings from the current file. +As a result: + +.BR "Scalar options" +set earlier in the current file are overridden by settings from the included file, and scalar options from the included file are overridden by later setting in the current file. + +.BR "Array options" +from earlier in the current file appear before setting from the included file, and array options from later in the current file appear after. + +Config file inclusion is recursive: an included config file may include another file - and so on. +Inclusion loops are not supported and will result in a +.I usage +error. + +The most common usecase for config file inclusion is so that a site-wide or project-wide options file can include a user-specific or module-specific options file - for example, as + +.RS 3 +.IP + ... +.br +config_file = $ENV{HOME}/.lcovrc_user +.br + ... +.PP +.RE + + +.PP + .BR genhtml_css_file " =" .I filename .IP diff --git a/tests/gendiffcov/errs/msgtest.sh b/tests/gendiffcov/errs/msgtest.sh index 757db3ca..7b64388f 100755 --- a/tests/gendiffcov/errs/msgtest.sh +++ b/tests/gendiffcov/errs/msgtest.sh @@ -3,7 +3,7 @@ set +x source ../../common.tst -rm -f test.cpp *.gcno *.gcda a.out *.info *.log *.json diff.txt +rm -f test.cpp *.gcno *.gcda a.out *.info *.log *.json diff.txt loop*.rc markers.err* rm -rf select criteria annotate empty unused_src scriptErr scriptFixed epoch inconsistent highlight etc mycache cacheFail expect subset context labels clean_cover @@ -203,6 +203,30 @@ if [ 0 != $? ] ; then fi fi +# loop in config file inclusion +echo "config_file = loop1.rc" > loop1.rc +echo lcov $LCOV_OPTS --summary initial.info --config-file loop1.rc --ignore usage +$COVER $LCOV_TOOL $LCOV_OPTS --summary initial.info --config-file loop1.rc --ignore usage 2>&1 | tee err_selfloop.log +grep "config file inclusion loop" err_selfloop.log +if [ 0 != $? ] ; then + echo "ERROR: missing config file message" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi +fi + +echo "config_file = loop3.rc" > loop2.rc +echo 'config_file = $ENV{PWD}/loop2.rc' > loop3.rc +echo lcov $LCOV_OPTS --summary initial.info --config-file loop2.rc --ignore usage +$COVER $LCOV_TOOL $LCOV_OPTS --summary initial.info --config-file loop2.rc --ignore usage 2>&1 | tee err_loop.log +grep "config file inclusion loop" err_loop.log +if [ 0 != $? ] ; then + echo "ERROR: missing config file message" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi +fi + echo lcov $LCOV_OPTS --capture -d . -o build.info --build-dir $LCOV_HOME $COVER $LCOV_TOOL $LCOV_OPTS --capture -d . -o build.info --build-dir $LCOV_HOME 2>&1 | tee build_dir_unused.log