summaryrefslogtreecommitdiffstats
path: root/lib/Test/Lintian/Prepare.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Test/Lintian/Prepare.pm')
-rw-r--r--lib/Test/Lintian/Prepare.pm551
1 files changed, 551 insertions, 0 deletions
diff --git a/lib/Test/Lintian/Prepare.pm b/lib/Test/Lintian/Prepare.pm
new file mode 100644
index 0000000..8914fcc
--- /dev/null
+++ b/lib/Test/Lintian/Prepare.pm
@@ -0,0 +1,551 @@
+# Copyright (C) 2018-2020 Felix Lechner
+# Copyright (C) 2019 Chris Lamb <lamby@debian.org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, you can find it on the World Wide
+# Web at https://www.gnu.org/copyleft/gpl.html, or write to the Free
+# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
+# MA 02110-1301, USA
+
+package Test::Lintian::Prepare;
+
+=head1 NAME
+
+Test::Lintian::Prepare -- routines to prepare the work directories
+
+=head1 SYNOPSIS
+
+ use Test::Lintian::Prepare qw(prepare);
+
+=head1 DESCRIPTION
+
+The routines in this module prepare the work directories in which the
+tests are run. To do so, they use the specifications in the test set.
+
+=cut
+
+use v5.20;
+use warnings;
+use utf8;
+
+use Exporter qw(import);
+
+BEGIN {
+ our @EXPORT_OK = qw(
+ prepare
+ filleval
+ );
+}
+
+use Carp;
+use Const::Fast;
+use Cwd qw(getcwd);
+use File::Copy;
+use File::Find::Rule;
+use File::Path qw(make_path remove_tree);
+use File::stat;
+use List::Util qw(max);
+use Path::Tiny;
+use Text::Template;
+use Unicode::UTF8 qw(encode_utf8);
+
+use Lintian::Deb822::Section;
+
+use Test::Lintian::ConfigFile qw(read_config write_config);
+use Test::Lintian::Helper qw(rfc822date copy_dir_contents);
+use Test::Lintian::Templates
+ qw(copy_skeleton_template_sets remove_surplus_templates fill_skeleton_templates);
+
+const my $EMPTY => q{};
+const my $SPACE => q{ };
+const my $SLASH => q{/};
+const my $COMMA => q{,};
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item prepare(SPEC_PATH, SOURCE_PATH, TEST_SET, REBUILD)
+
+Populates a work directory SOURCE_PATH with data from the test located
+in SPEC_PATH. The optional parameter REBUILD forces a rebuild if true.
+
+=cut
+
+sub prepare {
+ my ($specpath, $sourcepath, $testset, $force_rebuild)= @_;
+
+ say encode_utf8('------- Preparation starts here -------');
+ say encode_utf8("Work directory is $sourcepath.");
+
+ # for template fill, earliest date without timewarp warning
+ my $data_epoch = $ENV{'POLICY_EPOCH'}//time;
+
+ # read defaults
+ my $defaultspath = "$testset/defaults";
+
+ # read default file names
+ my $defaultfilespath = "$defaultspath/files";
+ die encode_utf8("Cannot find $defaultfilespath")
+ unless -e $defaultfilespath;
+
+ # read file and adjust data age threshold
+ my $files = read_config($defaultfilespath);
+ # $data_epoch= max($data_epoch, stat($defaultfilespath)->mtime);
+
+ # read test data
+ my $descpath = $specpath . $SLASH . $files->unfolded_value('Fill-Values');
+ my $desc = read_config($descpath);
+ # $data_epoch= max($data_epoch, stat($descpath)->mtime);
+
+ # read test defaults
+ my $descdefaultspath
+ = $defaultspath . $SLASH . $files->unfolded_value('Fill-Values');
+ my $defaults = read_config($descdefaultspath);
+ # $data_epoch= max($data_epoch, stat($descdefaultspath)->mtime);
+
+ # start with a shallow copy of defaults
+ my $testcase = Lintian::Deb822::Section->new;
+ $testcase->store($_, $defaults->value($_)) for $defaults->names;
+
+ die encode_utf8("Name missing for $specpath")
+ unless $desc->declares('Testname');
+
+ die encode_utf8('Outdated test specification (./debian/debian exists).')
+ if -e "$specpath/debian/debian";
+
+ if (-d $sourcepath) {
+
+ # check for old build artifacts
+ my $buildstamp = "$sourcepath/build-stamp";
+ say encode_utf8('Found old build artifact.') if -e $buildstamp;
+
+ # check for old debian/debian directory
+ my $olddebiandir = "$sourcepath/debian/debian";
+ say encode_utf8('Found old debian/debian directory.')
+ if -e $olddebiandir;
+
+ # check for rebuild demand
+ say encode_utf8('Forcing rebuild.') if $force_rebuild;
+
+ # delete work directory
+ if($force_rebuild || -e $buildstamp || -e $olddebiandir) {
+ say encode_utf8("Removing work directory $sourcepath.");
+ remove_tree($sourcepath);
+ }
+ }
+
+ # create work directory
+ unless (-d $sourcepath) {
+ say encode_utf8("Creating directory $sourcepath.");
+ make_path($sourcepath);
+ }
+
+ # delete old test scripts
+ my @oldrunners = File::Find::Rule->file->name('*.t')->in($sourcepath);
+ if (@oldrunners) {
+ unlink(@oldrunners)
+ or die encode_utf8("Cannot unlink @oldrunners");
+ }
+
+ my $skeletonname = $desc->unfolded_value('Skeleton');
+ if (length $skeletonname) {
+
+ # load skeleton
+ my $skeletonpath = "$testset/skeletons/$skeletonname";
+ my $skeleton = read_config($skeletonpath);
+
+ $testcase->store($_, $skeleton->value($_)) for $skeleton->names;
+ }
+
+ # populate working directory with specified template sets
+ copy_skeleton_template_sets($testcase->value('Template-Sets'),
+ $sourcepath, $testset)
+ if $testcase->declares('Template-Sets');
+
+ # delete templates for which we have originals
+ remove_surplus_templates($specpath, $sourcepath);
+
+ # copy test specification to working directory
+ my $offset = path($specpath)->relative($testset)->stringify;
+ say encode_utf8(
+ "Copy test specification $offset from $testset to $sourcepath.");
+ copy_dir_contents($specpath, $sourcepath);
+
+ my $valuefolder = $testcase->unfolded_value('Fill-Values-Folder');
+ if (length $valuefolder) {
+
+ # load all the values in the fill values folder
+ my $valuepath = "$sourcepath/$valuefolder";
+ my @filepaths
+ = File::Find::Rule->file->name('*.values')->in($valuepath);
+
+ for my $filepath (sort @filepaths) {
+ my $fill_values = read_config($filepath);
+
+ $testcase->store($_, $fill_values->value($_))
+ for $fill_values->names;
+ }
+ }
+
+ # add individual settings after skeleton
+ $testcase->store($_, $desc->value($_)) for $desc->names;
+
+ # record path to specification
+ $testcase->store('Spec-Path', $specpath);
+
+ # record path to specification
+ $testcase->store('Source-Path', $sourcepath);
+
+ # add other helpful info to testcase
+ $testcase->store('Source', $testcase->unfolded_value('Testname'))
+ unless $testcase->declares('Source');
+
+ # record our effective data age as date, unless given
+ $testcase->store('Date', rfc822date($data_epoch))
+ unless $testcase->declares('Date');
+
+ warn encode_utf8('Cannot override Architecture: in test '
+ . $testcase->unfolded_value('Testname'))
+ if $testcase->declares('Architecture');
+
+ die encode_utf8('DEB_HOST_ARCH is not set.')
+ unless defined $ENV{'DEB_HOST_ARCH'};
+ $testcase->store('Host-Architecture', $ENV{'DEB_HOST_ARCH'});
+
+ die encode_utf8('Could not get POLICY_VERSION.')
+ unless defined $ENV{'POLICY_VERSION'};
+ $testcase->store('Standards-Version', $ENV{'POLICY_VERSION'})
+ unless $testcase->declares('Standards-Version');
+
+ die encode_utf8('Could not get DEFAULT_DEBHELPER_COMPAT.')
+ unless defined $ENV{'DEFAULT_DEBHELPER_COMPAT'};
+ $testcase->store('Dh-Compat-Level', $ENV{'DEFAULT_DEBHELPER_COMPAT'})
+ unless $testcase->declares('Dh-Compat-Level');
+
+ # add additional version components
+ if ($testcase->declares('Version')) {
+
+ # add upstream version
+ my $upstream_version = $testcase->unfolded_value('Version');
+ $upstream_version =~ s/-[^-]+$//;
+ $upstream_version =~ s/(-|^)(\d+):/$1/;
+ $testcase->store('Upstream-Version', $upstream_version);
+
+ # version without epoch
+ my $no_epoch = $testcase->unfolded_value('Version');
+ $no_epoch =~ s/^\d+://;
+ $testcase->store('No-Epoch', $no_epoch);
+
+ unless ($testcase->declares('Prev-Version')) {
+ my $prev_version = '0.0.1';
+ $prev_version .= '-1'
+ unless $testcase->unfolded_value('Type') eq 'native';
+
+ $testcase->store('Prev-Version', $prev_version);
+ }
+ }
+
+ # calculate build dependencies
+ warn encode_utf8('Cannot override Build-Depends:')
+ if $testcase->declares('Build-Depends');
+ combine_fields($testcase, 'Build-Depends', $COMMA . $SPACE,
+ 'Default-Build-Depends', 'Extra-Build-Depends');
+
+ # calculate build conflicts
+ warn encode_utf8('Cannot override Build-Conflicts:')
+ if $testcase->declares('Build-Conflicts');
+ combine_fields($testcase, 'Build-Conflicts', $COMMA . $SPACE,
+ 'Default-Build-Conflicts', 'Extra-Build-Conflicts');
+
+ # fill testcase with itself; do it twice to make sure all is done
+ my $hashref = deb822_section_to_hash($testcase);
+ $hashref = fill_hash_from_hash($hashref);
+ $hashref = fill_hash_from_hash($hashref);
+ write_hash_to_deb822_section($hashref, $testcase);
+
+ say encode_utf8($EMPTY);
+
+ # fill remaining templates
+ fill_skeleton_templates($testcase->value('Fill-Targets'),
+ $hashref, $data_epoch, $sourcepath, $testset)
+ if $testcase->declares('Fill-Targets');
+
+ # write the dynamic file names
+ my $runfiles = path($sourcepath)->child('files');
+ write_config($files, $runfiles->stringify);
+
+ # set mtime for dynamic file names
+ $runfiles->touch($data_epoch);
+
+ # write the dynamic test case file
+ my $rundesc
+ = path($sourcepath)->child($files->unfolded_value('Fill-Values'));
+ write_config($testcase, $rundesc->stringify);
+
+ # set mtime for dynamic test data
+ $rundesc->touch($data_epoch);
+
+ say encode_utf8($EMPTY);
+
+ # announce data age
+ say encode_utf8('Data epoch is : '. rfc822date($data_epoch));
+
+ return;
+}
+
+=item filleval(SPEC_PATH, EVAL_PATH, TEST_SET, REBUILD)
+
+Populates a evaluation directory EVAL_PATH with data from the test located
+in SPEC_PATH. The optional parameter REBUILD forces a rebuild if true.
+
+=cut
+
+sub filleval {
+ my ($specpath, $evalpath, $testset, $force_rebuild)= @_;
+
+ say encode_utf8('------- Filling evaluation starts here -------');
+ say encode_utf8("Evaluation directory is $evalpath.");
+
+ # read defaults
+ my $defaultspath = "$testset/defaults";
+
+ # read default file names
+ my $defaultfilespath = "$defaultspath/files";
+ die encode_utf8("Cannot find $defaultfilespath")
+ unless -e $defaultfilespath;
+
+ # read file with default file names
+ my $files = read_config($defaultfilespath);
+
+ # read test data
+ my $descpath
+ = $specpath . $SLASH . $files->unfolded_value('Test-Specification');
+ my $desc = read_config($descpath);
+
+ # read test defaults
+ my $descdefaultspath
+ = $defaultspath . $SLASH . $files->unfolded_value('Test-Specification');
+ my $defaults = read_config($descdefaultspath);
+
+ # start with a shallow copy of defaults
+ my $testcase = Lintian::Deb822::Section->new;
+ $testcase->store($_, $defaults->value($_)) for $defaults->names;
+
+ die encode_utf8("Name missing for $specpath")
+ unless $desc->declares('Testname');
+
+ # delete old test scripts
+ my @oldrunners = File::Find::Rule->file->name('*.t')->in($evalpath);
+ if (@oldrunners) {
+ unlink(@oldrunners)
+ or die encode_utf8("Cannot unlink @oldrunners");
+ }
+
+ $testcase->store('Skeleton', $desc->value('Skeleton'))
+ unless $testcase->declares('Skeleton');
+
+ my $skeletonname = $testcase->unfolded_value('Skeleton');
+ if (length $skeletonname) {
+
+ # load skeleton
+ my $skeletonpath = "$testset/skeletons/$skeletonname";
+ my $skeleton = read_config($skeletonpath);
+
+ $testcase->store($_, $skeleton->value($_)) for $skeleton->names;
+ }
+
+ # add individual settings after skeleton
+ $testcase->store($_, $desc->value($_)) for $desc->names;
+
+ # populate working directory with specified template sets
+ copy_skeleton_template_sets($testcase->value('Template-Sets'),
+ $evalpath, $testset)
+ if $testcase->declares('Template-Sets');
+
+ # delete templates for which we have originals
+ remove_surplus_templates($specpath, $evalpath);
+
+ # copy test specification to working directory
+ my $offset = path($specpath)->relative($testset)->stringify;
+ say encode_utf8(
+ "Copy test specification $offset from $testset to $evalpath.");
+ copy_dir_contents($specpath, $evalpath);
+
+ my $valuefolder = $testcase->unfolded_value('Fill-Values-Folder');
+ if (length $valuefolder) {
+
+ # load all the values in the fill values folder
+ my $valuepath = "$evalpath/$valuefolder";
+ my @filepaths
+ = File::Find::Rule->file->name('*.values')->in($valuepath);
+
+ for my $filepath (sort @filepaths) {
+ my $fill_values = read_config($filepath);
+
+ $testcase->store($_, $fill_values->value($_))
+ for $fill_values->names;
+ }
+ }
+
+ # add individual settings after skeleton
+ $testcase->store($_, $desc->value($_)) for $desc->names;
+
+ # fill testcase with itself; do it twice to make sure all is done
+ my $hashref = deb822_section_to_hash($testcase);
+ $hashref = fill_hash_from_hash($hashref);
+ $hashref = fill_hash_from_hash($hashref);
+ write_hash_to_deb822_section($hashref, $testcase);
+
+ say encode_utf8($EMPTY);
+
+ # fill remaining templates
+ fill_skeleton_templates($testcase->value('Fill-Targets'),
+ $hashref, time, $evalpath, $testset)
+ if $testcase->declares('Fill-Targets');
+
+ # write the dynamic file names
+ my $runfiles = path($evalpath)->child('files');
+ write_config($files, $runfiles->stringify);
+
+ # write the dynamic test case file
+ my $rundesc
+ = path($evalpath)->child($files->unfolded_value('Test-Specification'));
+ write_config($testcase, $rundesc->stringify);
+
+ say encode_utf8($EMPTY);
+
+ return;
+}
+
+=item combine_fields
+
+=cut
+
+sub combine_fields {
+ my ($testcase, $destination, $delimiter, @sources) = @_;
+
+ return
+ unless length $destination;
+
+ # we are combining these contents
+ my @contents;
+ for my $source (@sources) {
+ push(@contents, $testcase->value($source))
+ if length $source;
+ $testcase->drop($source);
+ }
+
+ # combine
+ for my $content (@contents) {
+ $testcase->store(
+ $destination,
+ join($delimiter,
+ grep { length }($testcase->value($destination),$content))
+ );
+ }
+
+ # delete the combined entry if it is empty
+ $testcase->drop($destination)
+ unless length $testcase->value($destination);
+
+ return;
+}
+
+=item deb822_section_to_hash
+
+=cut
+
+sub deb822_section_to_hash {
+ my ($section) = @_;
+
+ my %hash;
+ for my $name ($section->names) {
+
+ my $transformed = lc $name;
+ $transformed =~ s/-/_/g;
+
+ $hash{$transformed} = $section->value($name);
+ }
+
+ return \%hash;
+}
+
+=item write_hash_to_deb822_section
+
+=cut
+
+sub write_hash_to_deb822_section {
+ my ($hashref, $section) = @_;
+
+ for my $name ($section->names) {
+
+ my $transformed = lc $name;
+ $transformed =~ s/-/_/g;
+
+ $section->store($name, $hashref->{$transformed});
+ }
+
+ return;
+}
+
+=item fill_hash_from_hash
+
+=cut
+
+sub fill_hash_from_hash {
+ my ($hashref, $delimiters) = @_;
+
+ $delimiters //= ['[%', '%]'];
+
+ my %origin = %{$hashref};
+ my %destination;
+
+ # fill hash with itself
+ for my $key (keys %origin) {
+
+ my $template = $origin{$key} // $EMPTY;
+ my $filler= Text::Template->new(TYPE => 'STRING', SOURCE => $template);
+ croak encode_utf8(
+ "Cannot read template $template: $Text::Template::ERROR")
+ unless $filler;
+
+ my $generated
+ = $filler->fill_in(HASH => \%origin, DELIMITERS => $delimiters);
+ croak encode_utf8("Could not create string from template $template")
+ unless defined $generated;
+ $destination{$key} = $generated;
+ }
+
+ return \%destination;
+}
+
+=back
+
+=head1 AUTHOR
+
+Originally written by Felix Lechner <felix.lechner@lease-up.com> for Lintian.
+
+=head1 SEE ALSO
+
+lintian(1)
+
+=cut
+
+1;
+
+# Local Variables:
+# indent-tabs-mode: nil
+# cperl-indent-level: 4
+# End:
+# vim: syntax=perl sw=4 sts=4 sr et