mirror of https://gitee.com/openkylin/lintian.git
973 lines
27 KiB
Perl
Executable File
973 lines
27 KiB
Perl
Executable File
#!/usr/bin/perl
|
|
|
|
# Copyright (C) 1998 Richard Braakman
|
|
# Copyright (C) 2008 Frank Lichtenheld
|
|
# Copyright (C) 2008, 2009 Russ Allbery
|
|
# Copyright (C) 2014 Niels Thykier
|
|
# Copyright (C) 2020 Felix Lechner
|
|
#
|
|
# 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.
|
|
|
|
# The harness for Lintian's test suite. For detailed information on
|
|
# the test suite layout and naming conventions, see t/tests/README.
|
|
# For more information about running tests, see
|
|
# doc/tutorial/Lintian/Tutorial/TestSuite.pod
|
|
#
|
|
|
|
use v5.20;
|
|
use warnings;
|
|
use utf8;
|
|
|
|
use Const::Fast;
|
|
use Cwd qw(realpath);
|
|
use File::Basename qw(dirname);
|
|
|
|
# neither Path::This nor lib::relative are in Debian
|
|
use constant THISFILE => realpath __FILE__;
|
|
use constant THISDIR => dirname realpath __FILE__;
|
|
|
|
# use Lintian modules that belong to this program
|
|
use lib THISDIR . '/../lib';
|
|
|
|
use Capture::Tiny qw(capture_merged);
|
|
use Cwd qw(getcwd);
|
|
use File::Copy;
|
|
use File::Find::Rule;
|
|
use File::Path qw(make_path);
|
|
use File::Spec::Functions qw(abs2rel rel2abs splitpath splitdir);
|
|
use File::stat;
|
|
use Getopt::Long;
|
|
use IPC::Run3;
|
|
use List::Compare;
|
|
use List::SomeUtils qw(any uniq);
|
|
use List::Util qw(max);
|
|
use IO::Interactive qw(is_interactive);
|
|
use IO::Prompt::Tiny qw(prompt);
|
|
use MCE::Loop;
|
|
use Path::Tiny;
|
|
use Syntax::Keyword::Try;
|
|
use TAP::Formatter::Console;
|
|
use TAP::Formatter::File;
|
|
use TAP::Harness;
|
|
use TAP::Parser::Aggregator;
|
|
use Term::ANSIColor;
|
|
use Time::Duration;
|
|
use Time::Moment;
|
|
use Time::Piece;
|
|
use Unicode::UTF8 qw(encode_utf8 decode_utf8);
|
|
|
|
use Lintian::IPC::Run3 qw(safe_qx);
|
|
|
|
use Test::Lintian::Build qw(build_subject);
|
|
use Test::Lintian::ConfigFile qw(read_config);
|
|
use Test::Lintian::Filter
|
|
qw(find_selected_scripts find_selected_lintian_testpaths);
|
|
use Test::Lintian::Helper
|
|
qw(rfc822date cache_dpkg_architecture_values get_latest_policy get_recommended_debhelper_version);
|
|
use Test::Lintian::Hooks qw(sed_hook sort_lines calibrate);
|
|
use Test::Lintian::Prepare qw(filleval prepare);
|
|
use Test::Lintian::Run qw(logged_runner);
|
|
use Test::ScriptAge qw(perl_modification_epoch our_modification_epoch);
|
|
|
|
const my $EMPTY => q{};
|
|
const my $SPACE => q{ };
|
|
const my $INDENT => $SPACE x 4;
|
|
const my $NEWLINE => qq{\n};
|
|
const my $SLASH => q{/};
|
|
const my $COMMA => q{,};
|
|
const my $COLON => q{:};
|
|
const my $ARROW => q{>>>};
|
|
const my $YES => q{yes};
|
|
const my $NO => q{no};
|
|
|
|
const my $WIDELY_READABLE => oct(22);
|
|
|
|
# display output immediately
|
|
STDOUT->autoflush;
|
|
|
|
# something changes the default handler, see Bug#974575
|
|
$SIG{WINCH} = 'DEFAULT';
|
|
|
|
# see https://stackoverflow.com/a/60761593
|
|
$SIG{CHLD} ||= 'DEFAULT';
|
|
$SIG{HUP} ||= 'DEFAULT';
|
|
|
|
my $processing_start = Time::Moment->from_string(gmtime->datetime . 'Z');
|
|
|
|
# whitelist the environment we permit to avoid things that mess up
|
|
# tests, like CFLAGS, DH_OPTIONS, DH_COMPAT, DEB_HOST_ARCH
|
|
my %PRESERVE_ENV = map { $_ => 1 } qw(
|
|
LINTIAN_TEST_INSTALLED
|
|
PATH
|
|
TMPDIR
|
|
);
|
|
|
|
my @disallowed = grep { !exists $PRESERVE_ENV{$_} } keys %ENV;
|
|
|
|
delete $ENV{$_} for @disallowed;
|
|
|
|
if (($ENV{LINTIAN_TEST_INSTALLED} // 'no') eq 'yes') {
|
|
|
|
$ENV{LINTIAN_UNDER_TEST} = realpath('/usr/bin/lintian')
|
|
// die encode_utf8('Lintian is not installed');
|
|
|
|
} else {
|
|
$ENV{LINTIAN_UNDER_TEST} = realpath(THISDIR . '/../bin/lintian');
|
|
}
|
|
|
|
$ENV{LINTIAN_BASE}= realpath(dirname(dirname($ENV{LINTIAN_UNDER_TEST})))
|
|
// die encode_utf8('Cannot resolve LINTIAN_BASE');
|
|
|
|
# options
|
|
my $coverage;
|
|
my $debug;
|
|
my $dump_logs = 1;
|
|
my $force_rebuild;
|
|
my $numjobs;
|
|
my $keep_going;
|
|
my $onlyrun;
|
|
my $outpath;
|
|
my $unattended;
|
|
my $verbose = 0;
|
|
|
|
Getopt::Long::Configure('bundling');
|
|
unless (
|
|
Getopt::Long::GetOptions(
|
|
'B|force-rebuild' => \$force_rebuild,
|
|
'c|coverage:s' => \$coverage,
|
|
'd|debug+' => \$debug,
|
|
'j|jobs:i' => \$numjobs,
|
|
'k|keep-going' => \$keep_going,
|
|
'L|dump-logs!' => \$dump_logs,
|
|
'o|onlyrun:s' => \$onlyrun,
|
|
'u|unattended' => \$unattended,
|
|
'v|verbose' => \$verbose,
|
|
'w|work-dir:s' => \$outpath,
|
|
'h|help' => sub {usage(); exit;},
|
|
)
|
|
) {
|
|
usage();
|
|
die;
|
|
}
|
|
|
|
# check number of arguments
|
|
die encode_utf8('Please use -h for usage information.')
|
|
if @ARGV > 1;
|
|
|
|
# get arguments
|
|
my ($testset) = @ARGV;
|
|
|
|
# default test set
|
|
$testset ||= 't';
|
|
|
|
# check test set directory
|
|
die encode_utf8("Cannot find testset directory $testset")
|
|
unless -d $testset;
|
|
|
|
# make sure testset is an absolute path
|
|
$testset = rel2abs($testset);
|
|
|
|
# calculate a default test work directory if none given
|
|
$outpath ||= dirname($testset) . '/debian/test-out';
|
|
|
|
# create test work directory unless it exists
|
|
make_path($outpath)
|
|
unless -e $outpath;
|
|
|
|
# make sure test work path is a directory
|
|
die encode_utf8("Test work directory $outpath is not a directory")
|
|
unless -d $outpath;
|
|
|
|
# make sure outpath is absolute
|
|
$outpath = rel2abs($outpath);
|
|
|
|
my $ACTIVE_JOBS = 0;
|
|
|
|
# get lintian modification date
|
|
my @lintianparts
|
|
= ('checks', 'commands', 'data','bin', 'profiles', 'vendors', 'lib/Lintian');
|
|
my @lintianfiles
|
|
= map { File::Find::Rule->file->in("$ENV{'LINTIAN_BASE'}/$_") }@lintianparts;
|
|
push(@lintianfiles, Cwd::realpath($ENV{'LINTIAN_UNDER_TEST'}));
|
|
$ENV{'LINTIAN_EPOCH'}
|
|
= max(map { -e ? path($_)->stat->mtime : time } @lintianfiles);
|
|
say encode_utf8('Lintian modified on '. rfc822date($ENV{'LINTIAN_EPOCH'}));
|
|
|
|
my $lintian_error;
|
|
my $bytes = capture_merged {
|
|
my @command = ($ENV{'LINTIAN_UNDER_TEST'}, '--version');
|
|
system(@command) == 0
|
|
or $lintian_error = "system @command failed: $?";
|
|
};
|
|
my $string = decode_utf8($bytes);
|
|
die encode_utf8($string . $lintian_error)
|
|
if length $lintian_error;
|
|
|
|
chomp $string;
|
|
my ($version) = $string =~ qr/^\S+\s+v(.+)$/;
|
|
die encode_utf8('Cannot get Lintian version') unless length $version;
|
|
say encode_utf8("Version under test is $version.");
|
|
|
|
say encode_utf8($EMPTY);
|
|
|
|
# set environment for coverage
|
|
if (defined $coverage) {
|
|
# Only collect coverage for stuff that D::NYTProf and
|
|
# Test::Pod::Coverage cannot do for us. This makes cover use less
|
|
# RAM in the other end.
|
|
my @criteria = qw(statement branch condition path subroutine);
|
|
my $args= '-MDevel::Cover=-silent,1,+ignore,^(.*/)?t/scripts/.+';
|
|
$args .= ',+ignore,/usr/bin/.*,+ignore,(.*/)?Dpkg';
|
|
$args .= ',-coverage,' . join(',-coverage,', @criteria);
|
|
$args .= $COMMA . $coverage if $coverage ne $EMPTY;
|
|
$ENV{'LINTIAN_COVERAGE'} = $args;
|
|
|
|
$ENV{'HARNESS_PERL_SWITCHES'} //= $EMPTY;
|
|
$ENV{'HARNESS_PERL_SWITCHES'} .= $SPACE . $args;
|
|
}
|
|
|
|
# Devel::Cover + one cover_db + multiple processes is a recipe
|
|
# for corruptions. Force $numjobs to 1 if we are running under
|
|
# coverage.
|
|
$numjobs = 1 if exists $ENV{'LINTIAN_COVERAGE'};
|
|
|
|
# tie verbosity to debug
|
|
$verbose = 1 + $debug if $debug;
|
|
|
|
# can be 0 without value ("-j") or undef if option was not specified at all
|
|
$numjobs ||= default_parallel();
|
|
say encode_utf8("Running up to $numjobs tests concurrently")
|
|
if $numjobs > 1 && $verbose >= 2;
|
|
|
|
$ENV{'DUMP_LOGS'} = $dump_logs//$NO ? $YES : $NO;
|
|
|
|
# Disable translation support in dpkg as it is a considerable
|
|
# unnecessary overhead.
|
|
$ENV{'DPKG_NLS'} = 0;
|
|
|
|
my $helperpath = "$testset/../private";
|
|
if (-d $helperpath) {
|
|
my $helpers = rel2abs($helperpath)
|
|
// die encode_utf8("Cannot resolve $helperpath: $!");
|
|
$ENV{'PATH'} = "$helpers:$ENV{'PATH'}";
|
|
}
|
|
|
|
# get architecture
|
|
cache_dpkg_architecture_values();
|
|
say encode_utf8("Host architecture is $ENV{'DEB_HOST_ARCH'}.");
|
|
|
|
# get latest policy version and date
|
|
($ENV{'POLICY_VERSION'}, $ENV{'POLICY_EPOCH'}) = get_latest_policy();
|
|
say encode_utf8("Latest policy version is $ENV{'POLICY_VERSION'} from "
|
|
. rfc822date($ENV{'POLICY_EPOCH'}));
|
|
|
|
# get current debhelper compat level; do not name DH_COMPAT; causes conflict
|
|
$ENV{'DEFAULT_DEBHELPER_COMPAT'} = get_recommended_debhelper_version();
|
|
say encode_utf8(
|
|
"Using compat level $ENV{'DEFAULT_DEBHELPER_COMPAT'} as a default for packages built with debhelper."
|
|
);
|
|
|
|
# get harness date, including templates, skeletons and whitelists
|
|
my @harnessparts
|
|
= ('bin', 't/defaults', 't/templates', 't/skeletons', 't/whitelists');
|
|
my @harnessfiles
|
|
= map { File::Find::Rule->file->in("$ENV{'LINTIAN_BASE'}/$_") }@harnessparts;
|
|
my $harness_files_epoch
|
|
= max(map { -e ? path($_)->stat->mtime : time } @harnessfiles);
|
|
$ENV{'HARNESS_EPOCH'}
|
|
= max(our_modification_epoch, perl_modification_epoch, $harness_files_epoch);
|
|
say encode_utf8('Harness modified on '. rfc822date($ENV{'HARNESS_EPOCH'}));
|
|
|
|
say encode_utf8($EMPTY);
|
|
|
|
# print environment
|
|
my @vars = sort keys %ENV;
|
|
say encode_utf8('Environment:') if @vars;
|
|
for my $var (@vars) { say encode_utf8($INDENT . "$var=$ENV{$var}") }
|
|
|
|
say encode_utf8($EMPTY);
|
|
|
|
my $status = 0;
|
|
|
|
my $formatter = TAP::Formatter::File->new(
|
|
{
|
|
errors => 1,
|
|
jobs => $numjobs,
|
|
}
|
|
);
|
|
$formatter = TAP::Formatter::Console->new(
|
|
{
|
|
errors => 1,
|
|
jobs => $numjobs,
|
|
color => 1,
|
|
}
|
|
) if is_interactive;
|
|
|
|
my $harness = TAP::Harness->new(
|
|
{
|
|
formatter => $formatter,
|
|
jobs => $numjobs,
|
|
lib => ["$ENV{'LINTIAN_BASE'}/lib"],
|
|
}
|
|
);
|
|
|
|
my $aggregator = TAP::Parser::Aggregator->new;
|
|
$aggregator->start;
|
|
|
|
my @runscripts;
|
|
my $allscripts_path = "$testset/scripts";
|
|
|
|
# add selected scripts
|
|
push(@runscripts, find_selected_scripts($allscripts_path, $onlyrun));
|
|
|
|
# always add internal harness tests
|
|
my @requiredscripts;
|
|
@requiredscripts
|
|
= sort File::Find::Rule->file()->name('*.t')->in("$allscripts_path/harness")
|
|
unless length $onlyrun;
|
|
push(@runscripts, @requiredscripts);
|
|
|
|
# remove any duplicates
|
|
@runscripts = uniq @runscripts;
|
|
|
|
# make all paths relative
|
|
@runscripts = map { abs2rel($_) } @runscripts;
|
|
|
|
say encode_utf8('Running selected and required Perl test scripts.');
|
|
say encode_utf8($EMPTY);
|
|
|
|
# run scripts through harness
|
|
$harness->aggregate_tests($aggregator, sort @runscripts);
|
|
|
|
if (@runscripts && !$aggregator->all_passed && !$keep_going) {
|
|
$aggregator->stop;
|
|
$formatter->summary($aggregator);
|
|
exit 1;
|
|
}
|
|
|
|
say encode_utf8($EMPTY);
|
|
|
|
my @testpaths = find_selected_lintian_testpaths($testset, $onlyrun);
|
|
|
|
my $recipe_root = "$testset/recipes";
|
|
|
|
# find test paths
|
|
my @recipes = map { path($_)->relative($recipe_root)->stringify }@testpaths;
|
|
|
|
# prepare output directories
|
|
say encode_utf8(
|
|
'Preparing the sources for '. scalar @recipes. ' test packages.')
|
|
if @recipes;
|
|
|
|
# for filled templates
|
|
my $source_root = "$outpath/package-sources";
|
|
|
|
# for built test packages
|
|
my $build_root = "$outpath/packages";
|
|
|
|
# find build specifications
|
|
my @all_recipes = map { path($_)->parent->stringify }
|
|
sort File::Find::Rule->relative->name('build-spec')->in($recipe_root);
|
|
|
|
my @source_paths
|
|
= map { path($_)->absolute($source_root)->stringify } @all_recipes;
|
|
my @build_paths
|
|
= map { path($_)->absolute($build_root)->stringify } @all_recipes;
|
|
|
|
# remove obsolete package sources
|
|
my @found_sources = map { path($_)->parent->absolute->stringify; }
|
|
File::Find::Rule->file->name('fill-values')->in($source_root);
|
|
my $sourcelc = List::Compare->new(\@found_sources, \@source_paths);
|
|
my @obsolete_sources = $sourcelc->get_Lonly;
|
|
path($_)->remove_tree for @obsolete_sources;
|
|
|
|
# remove obsolete built packages
|
|
my @found_builds = map { path($_)->parent->absolute->stringify; }
|
|
File::Find::Rule->file->name('source-files.sha1sums')->in($build_root);
|
|
my $packagelc= List::Compare->new(\@found_builds, \@build_paths);
|
|
my @obsolete_builds = $packagelc->get_Lonly;
|
|
path($_)->remove_tree for @obsolete_builds;
|
|
|
|
# remove empty directories
|
|
for my $folder (@obsolete_sources, @obsolete_builds) {
|
|
my $candidate = path($folder)->parent;
|
|
while ($candidate->exists && !$candidate->children) {
|
|
rmdir $candidate->stringify;
|
|
$candidate = $candidate->parent;
|
|
}
|
|
}
|
|
|
|
$ENV{PERL_PATH_TINY_NO_FLOCK} =1;
|
|
|
|
$SIG{INT} = sub { MCE::Loop->finish; die encode_utf8("Caught a sigint $!") };
|
|
my $mce_loop = MCE::Loop->init(
|
|
max_workers => $numjobs,
|
|
chunk_size => 1,
|
|
flush_stdout => 1,
|
|
flush_stderr => 1,
|
|
);
|
|
|
|
my %failedprep = mce_loop {
|
|
my ($mce, $chunk_ref, $chunk_id) = @_;
|
|
|
|
prepare_build($mce, $_);
|
|
}
|
|
@recipes;
|
|
|
|
if (%failedprep) {
|
|
say encode_utf8($EMPTY);
|
|
say encode_utf8('Failed preparation tasks:');
|
|
for my $recipe (sort keys %failedprep) {
|
|
say encode_utf8($EMPTY);
|
|
say encode_utf8($ARROW
|
|
. $SPACE
|
|
. path("$recipe_root/$recipe")->relative->stringify
|
|
. $COLON);
|
|
print encode_utf8($failedprep{$recipe});
|
|
}
|
|
|
|
MCE::Loop->finish;
|
|
exit 1;
|
|
|
|
} else {
|
|
say encode_utf8('Package sources are ready.');
|
|
}
|
|
|
|
say encode_utf8($EMPTY);
|
|
|
|
my %failedbuilds = mce_loop {
|
|
my ($mce, $chunk_ref, $chunk_id) = @_;
|
|
|
|
build_package($mce, $_, $chunk_id, scalar @recipes);
|
|
}
|
|
@recipes;
|
|
|
|
$SIG{INT} = 'DEFAULT';
|
|
MCE::Loop->finish;
|
|
|
|
if (%failedbuilds) {
|
|
say encode_utf8($EMPTY);
|
|
say encode_utf8('Failed build tasks:');
|
|
for my $recipe (sort keys %failedbuilds) {
|
|
say encode_utf8($EMPTY);
|
|
say encode_utf8($ARROW
|
|
. $SPACE
|
|
. path("$recipe_root/$recipe")->relative->stringify
|
|
. $COLON);
|
|
print encode_utf8($failedbuilds{$recipe});
|
|
}
|
|
|
|
exit 1;
|
|
} else {
|
|
say encode_utf8('All test packages are up to date.');
|
|
}
|
|
|
|
say encode_utf8($EMPTY);
|
|
|
|
my $build_end = Time::Moment->from_string(gmtime->datetime . 'Z');
|
|
my $build_duration = duration($processing_start->delta_seconds($build_end));
|
|
say encode_utf8("Building the test packages took $build_duration.");
|
|
|
|
say encode_utf8($EMPTY);
|
|
|
|
# for built test packages
|
|
my $buildroot = "$outpath/packages";
|
|
|
|
# for built test packages
|
|
my $evalroot = "$outpath/eval";
|
|
|
|
$SIG{INT} = sub { MCE::Loop->finish; die encode_utf8("Caught a sigint $!") };
|
|
|
|
mce_loop {
|
|
my ($mce, $chunk_ref, $chunk_id) = @_;
|
|
|
|
prepare_test($mce, $_);
|
|
}
|
|
sort @testpaths;
|
|
|
|
MCE::Loop->finish;
|
|
|
|
$SIG{INT} = 'DEFAULT';
|
|
|
|
# remap paths from testset to outpath to get work directories
|
|
my @workpaths
|
|
= map { rel2abs(abs2rel($_, "$testset/recipes"), "$outpath/eval") }
|
|
@testpaths;
|
|
|
|
# if ($platforms ne 'any') {
|
|
# my @wildcards = split(/$SPACE/, $platforms);
|
|
# my @matches= map {
|
|
# decode_utf8(qx{dpkg-architecture -a $ENV{'DEB_HOST_ARCH'} -i $_; echo -n \$?})
|
|
# } @wildcards;
|
|
# unless (any { $_ == 0 } @matches) {
|
|
# say encode_utf8('Architecture mismatch');
|
|
# return;
|
|
# }
|
|
# }
|
|
|
|
# make all paths relative to current directory
|
|
@workpaths = map { path($_)->relative } @workpaths;
|
|
|
|
# add the scripts in generated tests to be run
|
|
my @workscripts;
|
|
for my $path (@workpaths) {
|
|
|
|
my @runners = File::Find::Rule->file->name('*.t')->in($path);
|
|
|
|
die encode_utf8("No runner in $path")
|
|
unless scalar @runners;
|
|
die encode_utf8("More than one runner in $path")
|
|
if scalar @runners > 1;
|
|
|
|
push(@workscripts, @runners);
|
|
}
|
|
|
|
# run scripts through harness
|
|
$harness->aggregate_tests($aggregator, sort @workscripts);
|
|
|
|
$aggregator->stop;
|
|
$formatter->summary($aggregator);
|
|
|
|
say encode_utf8($EMPTY);
|
|
|
|
my $test_end = Time::Moment->from_string(gmtime->datetime . 'Z');
|
|
my $test_duration = duration($processing_start->delta_seconds($test_end));
|
|
say encode_utf8("The test suite ran for $test_duration.");
|
|
|
|
$status = 1
|
|
unless $aggregator->all_passed;
|
|
|
|
if (is_interactive && !$unattended) {
|
|
my @failed = $aggregator->failed;
|
|
say encode_utf8(
|
|
'Offering to re-calibrate the hints expected in tests that failed.')
|
|
if @failed;
|
|
|
|
my $accept_all;
|
|
|
|
for my $scriptpath (@failed) {
|
|
my $workpath = dirname($scriptpath);
|
|
|
|
my $descpath = "$workpath/desc";
|
|
my $testcase = read_config($descpath);
|
|
|
|
my $relative = abs2rel($workpath, $evalroot);
|
|
my $testpath = abs2rel(rel2abs($relative, "$testset/recipes"));
|
|
|
|
say encode_utf8($EMPTY);
|
|
say encode_utf8(
|
|
'Failed test: ' . colored($testpath, 'bold white on_blue'));
|
|
|
|
my $match_strategy = $testcase->unfolded_value('Match-Strategy');
|
|
|
|
if ($match_strategy eq 'hints') {
|
|
|
|
my $diffpath = "$workpath/hintdiff";
|
|
next
|
|
unless -r $diffpath;
|
|
|
|
my $diff = path($diffpath)->slurp_utf8;
|
|
print encode_utf8($diff);
|
|
|
|
} elsif ($match_strategy eq 'literal') {
|
|
|
|
my $actualpath = "$workpath/literal.actual.parsed";
|
|
next
|
|
unless -r $actualpath;
|
|
my @command
|
|
= ('diff', '-uN', "$testpath/eval/literal", $actualpath);
|
|
say encode_utf8(join($SPACE, @command));
|
|
system(@command);
|
|
|
|
} else {
|
|
say encode_utf8(
|
|
"Do not know how to fix tests using matching strategy $match_strategy."
|
|
);
|
|
next;
|
|
}
|
|
|
|
unless ($accept_all) {
|
|
|
|
my $decision_bytes = prompt(
|
|
encode_utf8(
|
|
'>>> Fix test (y), accept all (a), do not fix (n), quit (q/default)?'
|
|
)
|
|
);
|
|
my $decision = decode_utf8($decision_bytes);
|
|
|
|
last
|
|
if $decision eq 'q' || $decision eq $EMPTY;
|
|
|
|
next
|
|
unless $decision eq 'y' || $decision eq 'a';
|
|
|
|
$accept_all = 1
|
|
if $decision eq 'a';
|
|
}
|
|
|
|
if ($match_strategy eq 'hints') {
|
|
|
|
# create hints if needed; helps when writing new tests
|
|
my $hintspath = "$testpath/eval/hints";
|
|
path($hintspath)->touch
|
|
unless -e $hintspath;
|
|
|
|
my $diffpath = "$workpath/hintdiff";
|
|
next
|
|
unless -r $diffpath;
|
|
|
|
my @adjustargs = ($diffpath, $hintspath);
|
|
unshift(@adjustargs, '-i')
|
|
unless $accept_all;
|
|
|
|
die encode_utf8("Cannot run hintadjust for $testpath")
|
|
if system('hintadjust', @adjustargs);
|
|
|
|
# also copy the new hints to workpath; no need to rebuild
|
|
die encode_utf8("Cannot copy updated hints to $workpath")
|
|
if system('cp', $hintspath, "$workpath/hints");
|
|
|
|
} elsif ($match_strategy eq 'literal') {
|
|
|
|
my $actualpath = "$workpath/literal.actual.parsed";
|
|
next
|
|
unless -r $actualpath;
|
|
|
|
die encode_utf8(
|
|
"Cannot copy to accept literal output for $testpath")
|
|
if system('cp', $actualpath, "$testpath/eval/literal");
|
|
|
|
}
|
|
}
|
|
|
|
say encode_utf8($NEWLINE . 'Accepted all remaining hint changes.')
|
|
if $accept_all;
|
|
|
|
} else {
|
|
my @crashed = $aggregator->parse_errors;
|
|
|
|
say encode_utf8('Showing full logs for tests with parse errors.')
|
|
if @crashed;
|
|
|
|
for my $absolutepath (@crashed) {
|
|
|
|
my $scriptpath = abs2rel($absolutepath);
|
|
my $workpath = dirname($scriptpath);
|
|
my $logpath = "$workpath/log";
|
|
|
|
next
|
|
unless -e $logpath;
|
|
|
|
say encode_utf8($EMPTY);
|
|
say encode_utf8("Log for test $scriptpath:");
|
|
|
|
my $log = path($logpath)->slurp_utf8;
|
|
print encode_utf8($log);
|
|
}
|
|
}
|
|
|
|
# give a hint if not enough tests were run
|
|
unless (scalar @runscripts - scalar @requiredscripts + scalar @workscripts
|
|
|| $onlyrun eq 'minimal:') {
|
|
quick_hint($onlyrun);
|
|
exit 1;
|
|
}
|
|
|
|
say encode_utf8($EMPTY);
|
|
|
|
exit $status;
|
|
|
|
# program is done
|
|
|
|
sub prepare_build {
|
|
my ($mce, $recipe) = @_;
|
|
|
|
# label process
|
|
$0 = "Lintian prepare test: $recipe";
|
|
|
|
# destination
|
|
my $source_path = "$source_root/$recipe";
|
|
|
|
my $error;
|
|
|
|
# capture output
|
|
my $log_bytes =capture_merged {
|
|
|
|
try {
|
|
|
|
# remove destination
|
|
path($source_path)->remove_tree
|
|
if -e $source_path;
|
|
|
|
# prepare
|
|
prepare("$recipe_root/$recipe/build-spec",
|
|
$source_path, $testset, $force_rebuild);
|
|
|
|
} catch {
|
|
# catch any error
|
|
$error = $@;
|
|
}
|
|
};
|
|
|
|
my $log = decode_utf8($log_bytes);
|
|
|
|
# save log;
|
|
my $logfile = "$source_path.log";
|
|
path($logfile)->spew_utf8($log) if $log;
|
|
|
|
$mce->gather($recipe, $error)
|
|
if length $error;
|
|
|
|
return;
|
|
}
|
|
|
|
sub build_package {
|
|
my ($mce, $recipe, $position, $total) = @_;
|
|
|
|
# set a predictable locale
|
|
$ENV{'LC_ALL'} = 'C';
|
|
|
|
# many tests create files via debian/rules
|
|
umask $WIDELY_READABLE;
|
|
|
|
# get destination
|
|
my $source_path = "$source_root/$recipe";
|
|
my $build_path = "$build_root/$recipe";
|
|
|
|
my $savedir = getcwd;
|
|
chdir $source_path
|
|
or die encode_utf8("Cannot change to directory $source_path");
|
|
|
|
my $sha1sums_bytes;
|
|
run3('find . -type f -print0 | sort -z | xargs -0 sha1sum',
|
|
\undef, \$sha1sums_bytes);
|
|
|
|
chdir $savedir
|
|
or die encode_utf8("Cannot change to directory $savedir");
|
|
|
|
my $sha1sums = decode_utf8($sha1sums_bytes);
|
|
|
|
my $checksum_path = "$build_path/source-files.sha1sums";
|
|
if (-r $checksum_path) {
|
|
my $previous = path($checksum_path)->slurp_utf8;
|
|
|
|
# only rebuild if needed
|
|
# also need to look for build subject
|
|
return
|
|
if $sha1sums eq $previous;
|
|
}
|
|
|
|
$0 = "Lintian build test: $recipe [$position/$total]";
|
|
say encode_utf8('Building in '
|
|
. path($build_path)->relative->stringify
|
|
. " [$position/$total]");
|
|
|
|
path($build_path)->remove_tree
|
|
if -e $build_path;
|
|
path($build_path)->mkpath;
|
|
|
|
# read dynamic file names
|
|
my $runfiles = "$source_path/files";
|
|
my $files = read_config($runfiles);
|
|
|
|
my $error;
|
|
|
|
my $log_bytes = capture_merged {
|
|
|
|
try {
|
|
# call runner
|
|
build_subject($source_path, $build_path);
|
|
|
|
} catch {
|
|
# catch any error
|
|
$error = $@;
|
|
}
|
|
};
|
|
|
|
my $log = decode_utf8($log_bytes);
|
|
|
|
# delete old runner log
|
|
my $betterlogpath= $build_path . $SLASH . $files->unfolded_value('Log');
|
|
if (-e $betterlogpath) {
|
|
unlink $betterlogpath
|
|
or die encode_utf8("Cannot unlink $betterlogpath");
|
|
}
|
|
|
|
# move the early log for directory preparation to position of runner log
|
|
my $earlylogpath = "$source_path.log";
|
|
move($earlylogpath, $betterlogpath) if -e $earlylogpath;
|
|
|
|
# append runner log to population log
|
|
path($betterlogpath)->append_utf8($log) if length $log;
|
|
|
|
# add error if there was one
|
|
path($betterlogpath)->append_utf8($error) if length $error;
|
|
|
|
path($checksum_path)->spew_utf8($sha1sums)
|
|
unless length $error;
|
|
|
|
$mce->gather(path($build_path)->relative->stringify, $error . $log)
|
|
if length $error;
|
|
|
|
return;
|
|
}
|
|
|
|
sub prepare_test {
|
|
my ($mce, $specpath) = @_;
|
|
|
|
# label process
|
|
$0 = "Lintian prepare test: $specpath";
|
|
|
|
# calculate destination
|
|
my $relative = path($specpath)->relative("$testset/recipes");
|
|
my $buildpath = $relative->absolute($buildroot)->stringify;
|
|
my $evalpath = $relative->absolute($evalroot)->relative->stringify;
|
|
|
|
my $error;
|
|
|
|
# capture output
|
|
my $log_bytes = capture_merged {
|
|
|
|
try {
|
|
|
|
# remove destination
|
|
path($evalpath)->remove_tree
|
|
if -e $evalpath;
|
|
|
|
path($evalpath)->mkpath;
|
|
|
|
# prepare
|
|
filleval("$specpath/eval", $evalpath, $testset);
|
|
|
|
my $traversal = Cwd::realpath("$buildpath/subject");
|
|
|
|
if (length $traversal) {
|
|
die encode_utf8("Cannot link to subject in $buildpath")
|
|
if system("cd $evalpath; ln -s $traversal subject");
|
|
}
|
|
|
|
}catch {
|
|
# catch any error
|
|
$error = $@;
|
|
}
|
|
};
|
|
|
|
my $log = decode_utf8($log_bytes);
|
|
|
|
# save log;
|
|
my $logfile = "$evalpath/log";
|
|
path($logfile)->spew_utf8($log) if $log;
|
|
|
|
# print something if there was an error
|
|
die encode_utf8(
|
|
($log // $EMPTY) . "Preparation failed for $specpath: $error")
|
|
if $error;
|
|
|
|
return $specpath;
|
|
}
|
|
|
|
=item default_parallel
|
|
|
|
=cut
|
|
|
|
# Return the default number of parallelization to be used
|
|
sub default_parallel {
|
|
# check cpuinfo for the number of cores...
|
|
my $cpus = decode_utf8(safe_qx('nproc'));
|
|
if ($cpus =~ m/^\d+$/) {
|
|
# Running up to twice the number of cores usually gets the most out
|
|
# of the CPUs and disks but it might be too aggressive to be the
|
|
# default for -j. Only use <cores>+1 then.
|
|
return $cpus + 1;
|
|
}
|
|
|
|
# No decent number of jobs? Just use 2 as a default
|
|
return 2;
|
|
}
|
|
|
|
sub usage {
|
|
my $message =<<"END";
|
|
Usage: $0 [options] [-j [<jobs>]] <testset-directory>
|
|
|
|
--onlyrun Select only some tests for a quick check
|
|
--coverage Run Lintian under Devel::Cover (Warning: painfully slow)
|
|
-d Display additional debugging information
|
|
--dump-logs Print build log to STDOUT, if a build fails.
|
|
-j [<jobs>] Run up to <jobs> jobs in parallel.
|
|
If -j is passed without specifying <jobs>, the number
|
|
of jobs started is <nproc>+1.
|
|
-k Do not stop after one failed test
|
|
-v Be more verbose
|
|
--help, -h Print this help and exit
|
|
|
|
The option --onlyrun causes runtests to only run tests that match
|
|
the particular selection. This parameter can be a list of selectors:
|
|
what:<which>[,<what:...>]
|
|
|
|
* test:<testname>
|
|
- Run the named test. Please note that testnames may not be
|
|
unique, so it may run more than one test.
|
|
* script:(<script-name> || <dir-in-scripts-suite>)
|
|
- Run the named code quality script or all in the named directory.
|
|
E.g. "01-critic" will run all tests in "t/scripts/01-critic/".
|
|
* check:<check-name>
|
|
- Run all tests related to the given check.
|
|
* suite:<suite>
|
|
- Run all tests in the named suite.
|
|
* tag:<tag-name>
|
|
- Run any test that lists <tag-name> in "Test-For" or
|
|
"Test-Against".
|
|
|
|
Test artifacts are cached in --work-dir [default: debian/test-out] and
|
|
will generally be reused to save time. To recreate the test packages,
|
|
run 'private/build-test-packages'.
|
|
END
|
|
|
|
print encode_utf8($message);
|
|
|
|
return;
|
|
}
|
|
|
|
sub quick_hint {
|
|
my ($selection) = @_;
|
|
|
|
my $message =<<"END";
|
|
|
|
No tests were selected by your filter:
|
|
|
|
$selection
|
|
|
|
To select your tests, please use an appropriate argument with a
|
|
selector like:
|
|
|
|
'suite:', 'test:', 'check:', 'tag:', or 'script:'
|
|
|
|
You can also use 'minimal:', which runs only the tests that cannot
|
|
be turned off, such as the internal tests for the harness.
|
|
END
|
|
|
|
print encode_utf8($message);
|
|
|
|
return;
|
|
}
|
|
|
|
# Local Variables:
|
|
# indent-tabs-mode: nil
|
|
# cperl-indent-level: 4
|
|
# End:
|
|
# vim: syntax=perl sw=4 sts=4 sr et
|