#!/usr/bin/perl -w

use strict;
use warnings;
use Test::Repocop::Common;
use Test::Repocop::Options;
use Test::Repocop::SQL;
use Test::Repocop::RPMdb;
use Test::Repocop::TestDB;
use File::Path qw(make_path remove_tree);
use File::Basename;

use File::Temp qw/tempfile tempdir/;
use RPM::Header;
use DBI;

$verbose=1;
my ($repocop_use_fakeroot,$repocop_opt_skip_big_binary,$repocop_opt_skip_big_source);
my ($repocop_opt_ext_tmpdir,$repocop_opt_big_tmpdir,$repocop_opt_big_binary);
my $repocop_opt_collect=1;
my $repocop_opt_posttest=1;
my $repocop_opt_distrotest=0;
&Test::Repocop::Options::get_common_options(
	    "fakeroot!"  => \$repocop_use_fakeroot,
	    "collect!"  => \$repocop_opt_collect,
	    "posttest!"  => \$repocop_opt_posttest,
	    "distrotest!"  => \$repocop_opt_distrotest,
	    "big-tmpdir=s"  => \$repocop_opt_big_tmpdir,
	    "big-binary=s"  => \$repocop_opt_big_binary,
	    "tmpdir=s"  => \$repocop_opt_ext_tmpdir,
	    "skip-big-binary=s"  => \$repocop_opt_skip_big_binary,
	    "skip-big-source=s"  => \$repocop_opt_skip_big_source,
);
&__convert_megabytes('big-binary',\$repocop_opt_big_binary);
&__convert_megabytes('skip-big-binary',\$repocop_opt_skip_big_binary);
&__convert_megabytes('skip-big-source',\$repocop_opt_skip_big_source);

# create temporary dirs if defined
foreach ($repocop_opt_big_tmpdir, $repocop_opt_ext_tmpdir) {
    make_path($_) if $_ and not -d $_;
}

my $rundir=`pwd`;
chomp $rundir;

# global; we use it in exit_repocop
my $PKGTMPDIR;

my $tmpdir_template=$0.".XXXXXXXX";
$tmpdir_template=~s!^.*/!!;

$SIG{'INT'}  = \&sigexit_repocop;
$SIG{'QUIT'} = \&sigexit_repocop;
$SIG{'HUP'} = \&sigexit_repocop;

my @cpio_unpack_cmd=qw!cpio -idmu --quiet --force-local --no-absolute-filenames!;
if (-x '/usr/bin/pax') {
    &repocop_note2("Note: pax found. using pax.");
    @cpio_unpack_cmd=qw!pax -O -r!;
}
my $cpio_unpack_cmd=join(' ',@cpio_unpack_cmd);

&repocop_note("Note: using fakeroot (slower).") if $repocop_use_fakeroot;
die "cachedir $repocop_cachedir does is not a directory!\n" if (-e $repocop_cachedir && ! -d $repocop_cachedir);

# global; keeps options per test that can be configured in /options file
my %TestOptions;
my %TestFilePattern;
my %SourceTestPattern;
my @SourceTestPatterns;
my %DefaultOptions=qw/need_unpack 1/;

make_path($repocop_cachedir,$repocop_metadatadir,$repocop_test_dbdir);

# list of last packages that passed through tests
# we empty it there and create again at each run
my $repocop_lastrun_id_file="$repocop_metadatadir/last-run";
unlink $repocop_lastrun_id_file;
my @repocop_lastrun_ids;

$ENV{'REPOCOP_CACHEDIR'}=$repocop_cachedir;
my $testdb=Test::Repocop::TestDB->new();
$testdb->setup_environment();

foreach my $test (&Test::Repocop::Common::glob_tests('',@Repocop::arg::tests_dir)) {
    my $testname=basename($test);
    &Test::Repocop::Common::mkdir_test_environment($testname);
}

&load_test_options_files();

# sql constructor
foreach my $sqlconstructor (&Test::Repocop::Common::glob_tests('init.sql*', @Repocop::arg::tests_dir)) {
    my $testname=&Test::Repocop::Common::filepath2testname($sqlconstructor);
    my $repocop_test_db="$repocop_test_dbdir/$testname.db";
    &Test::Repocop::SQL::init_db($repocop_test_db,$sqlconstructor,$testname);
}

# collect test names and mtimes
my $source_collect_ref;
my $binary_collect_ref;
my %collect_test_name;
my %collect_test_mtime;

if ($repocop_opt_collect) {
    foreach my $testconstructor (&Test::Repocop::Common::glob_tests('init', @Repocop::arg::tests_dir)) {
	my $testname=&Test::Repocop::Common::filepath2testname($testconstructor);
	&Test::Repocop::Common::set_test_environment($testname);
	if (-x $testconstructor) {
	    system($testconstructor)==0 || die "error: $testname/init exited abnormally\n";
	}
    }

    &Test::Repocop::RPMdb::connect_rpm_db("$repocop_test_dbdir/rpm.db");

    $source_collect_ref = &calculate_collectors_info(\@Repocop::arg::srcscripts_dir);
    $binary_collect_ref = &calculate_collectors_info(\@Repocop::arg::pkgscripts_dir);

    foreach my $rpmarg (sort @Repocop::arg::pkglist) {
	if (-e $rpmarg) {
	    &process_rpm($rpmarg);
	} else {
	    warn "rpm file $rpmarg does not exist :(";
	}
    }
    &write_lastrun_ids_file($repocop_lastrun_id_file,\@repocop_lastrun_ids);

    &call_posttests('done');
}

if ($repocop_opt_posttest) {
    &call_posttests('posttest');
}
if ($repocop_opt_distrotest) {
    &call_posttests('distrotest');
}

&cleanup_on_repocop_exit();

#====================================================

sub calculate_collectors_info {
    my ($dirlist_ref)=@_;
    my @filelist;
    foreach my $test (&Test::Repocop::Common::glob_tests('test',@$dirlist_ref)) {
	my $testname = &Test::Repocop::Common::filepath2testname($test);
	if (! -x $test) {
	    &repocop_note("skipped $testname: not executable");
	} else {
	    push @filelist, $test;
	    $collect_test_mtime{$test} = -M "$test";
	    $collect_test_name{$test} = $testname;
	}
    }
    return \@filelist;
}

sub call_posttests {
    my ($methodtype) = @_;
    chdir $rundir;
    my @postmethods = &Test::Repocop::Common::glob_tests($methodtype, @Repocop::arg::tests_dir);
    my @postmethodnames;
    foreach my $postmethod (@postmethods) {
	my $testname=&Test::Repocop::Common::filepath2testname($postmethod);
	push @postmethodnames, $testname;
    }
    
    &repocop_note("preparing [$methodtype] methods");#@postmethodnames
    $testdb->remove_test(@postmethodnames);
    
    foreach my $postmethod (@postmethods) {
	my $testname=&Test::Repocop::Common::filepath2testname($postmethod);
	&Test::Repocop::Common::set_test_environment($testname);
	&repocop_note("trying [$methodtype] $testname");
	if (-x $postmethod) {
	    my $timeofstart=time();
	    system($postmethod)==0 || die "error: $testname/$methodtype exited abnormally\n";
	    my $elapsedtime = time()-$timeofstart;
	    if ($elapsedtime>10) {
		&repocop_note("Note: [$methodtype] $testname took $elapsedtime secs");
	    }
	}
    }
}

sub cleanup_on_repocop_exit {
    chdir $rundir;
    &del_PKGTMPDIR();
    &Test::Repocop::Common::cleanup_test_environments(
	map { basename $_} &Test::Repocop::Common::glob_tests('',@Repocop::arg::tests_dir)
    );
    &Test::Repocop::RPMdb::done_rpm_db();
}


sub sigexit_repocop {
    print "exiting repocop (signal)...\n" if $verbose;
    &cleanup_on_repocop_exit();
    exit 10 ;
}

sub del_PKGTMPDIR {
    if (defined $PKGTMPDIR and -d $PKGTMPDIR) {
	system 'chmod', '-Rf', 'u+rwX', $PKGTMPDIR;
	remove_tree($PKGTMPDIR);
    }

}

sub write_lastrun_ids_file {
    my ($filename, $lastrun_ids_ptr)=@_;
    open (AFILE, '>>', $filename) || die "can't open $filename: $!";
    foreach my $line (@$lastrun_ids_ptr) {
	print AFILE $line, "\n";
    }
    close AFILE;
}

sub process_rpm {
    my ($rpmarg)=@_;
    &repocop_note("processing $rpmarg");
    $rpmarg=$rundir.'/'.$rpmarg unless $rpmarg=~m!^/!;
    my $pkgname=basename($rpmarg);
    my $repocop_pkg_key=$pkgname;
    $repocop_pkg_key=~s/\.rpm$//;
    # mark the rpm as processed
    push @repocop_lastrun_ids,$repocop_pkg_key;

    $ENV{'REPOCOP_PKG'}=$rpmarg;
    $ENV{'REPOCOP_PKG_KEY'}=$repocop_pkg_key;

    my $lazyrhref = LazyRPMFileHeader->new($rpmarg);
    my $is_source= $repocop_pkg_key=~m/\.src$/; #$rhref->is_source();
    my $testfiles_ref=$is_source ? $source_collect_ref : $binary_collect_ref;
    my $is_set_environment_vars=0;
    my $is_pkg_unpacked=0;
    my $is_ls_created=0;
    my $is_tmpdir_created=0;
    my $lsfile;

    unless ($testdb->is_test_performed($repocop_pkg_key,'rpm')) {
	&repocop_note("trying rpm");
	&Test::Repocop::RPMdb::add_rpm_metadata($lazyrhref->get_rhref(),$repocop_pkg_key);
	$testdb->set_test_mtime($repocop_pkg_key,'rpm');
    }

    foreach my $testexecutable (@$testfiles_ref) {
	my $testname=$collect_test_name{$testexecutable};
	my $testmtime=$collect_test_mtime{$testexecutable};

	&Test::Repocop::Common::set_test_environment($testname);
	if ($testdb->is_test_performed($repocop_pkg_key,$testname)) {
	    if ($testdb->get_test_mtime($repocop_pkg_key,$testname) < $testmtime) {
	        # tests are already performed
		&repocop_note2("skipped $testname (cached)");
 		next;
	    } else {
		$testdb->clear_test_result_and_mtime($repocop_pkg_key,$testname);
		&repocop_note("cache for $testname is obsolete - run again");
	    }
	}

	unless ($is_set_environment_vars) {
	    &set_environment_vars($lazyrhref->get_rhref(), $is_source);
	    $is_set_environment_vars=1;
	}

	# unpack rpm only if required to speed up skipping
	if (!$is_pkg_unpacked && &get_test_option($testname,'need_unpack')) {
	    if (!$is_tmpdir_created) {
		&create_pkgtmpdir($rpmarg);
		$lsfile = $PKGTMPDIR.'/.ls';
		$is_tmpdir_created=1;
	    }
	    if ($TestFilePattern{$testname}) {
		unless ($is_ls_created) {
		    #system("rpmquery -lp '$rpmarg' > '$PKGTMPDIR/.ls'") && die "rpmquery failed on $rpmarg";
		    open my $fh, '>', $lsfile or die "Can't write to $lsfile: $!";
		    my $filenamesref=$lazyrhref->get_rhref()->filenames();
		    if ($filenamesref) {
			foreach my $rfile (@$filenamesref) {
			    print $fh $rfile, "\n";
			}
		    }
		    close $fh;
		    $is_ls_created=1;
		}
		if (system('pcregrep','-q','-f', $TestFilePattern{$testname}, $lsfile)!=0) {
		    # no need to unpack or call the collector;
		    $testdb->set_test_mtime($repocop_pkg_key,$testname);
		    next; # go to next test
		}
	    }
	    if (
		($is_source and $repocop_opt_skip_big_source and -s $rpmarg > $repocop_opt_skip_big_source) or
		(not $is_source and $repocop_opt_skip_big_binary and -s $rpmarg > $repocop_opt_skip_big_binary)
		) {
		&repocop_note("postponed $testname (too big rpm file)");
		next; # go to next test
	    }
	    unless (&unpack_rpm($rpmarg, $lazyrhref->get_rhref(), $is_source)) {
		&repocop_note("Note: PKGTMPDIR=$PKGTMPDIR.");
		die "unpacking $rpmarg failed\n";
	    }
	    $is_pkg_unpacked=1;
	}
	&repocop_note("trying $testname");
	if ($is_pkg_unpacked and $repocop_use_fakeroot and not $is_source) {
	    system qw!fakeroot -i ../.fakedata --!, $testexecutable;
	} else {
	    system $testexecutable;
	}
	$testdb->set_test_mtime($repocop_pkg_key,$testname);
    }
    chdir $rundir;
    &del_PKGTMPDIR();
}

sub set_environment_vars {
    my ($rhref, $is_source)=@_;
    foreach my $tag ('NAME','VERSION','RELEASE','EPOCH','ARCH') {
	my $tagval=$rhref->{$tag};
	$tagval='' unless defined $tagval;
	$ENV{'REPOCOP_PKG_'.$tag}=$tagval;
    }
    unless ($is_source) {
	my $srcshortname=$rhref->{'SOURCERPM'};
	$srcshortname=~s/\.src\.rpm$//;
	$ENV{'REPOCOP_PKG_SOURCEPKG'}=$srcshortname;
    }
}

sub create_pkgtmpdir {
    my ($rpmarg)=@_;
    my @tempdir_opt;
    if ($repocop_opt_big_tmpdir and $repocop_opt_big_binary and (-s $rpmarg >$repocop_opt_big_binary)) {
	push @tempdir_opt, DIR => $repocop_opt_big_tmpdir;
    } elsif ($repocop_opt_ext_tmpdir) {
	push @tempdir_opt, DIR => $repocop_opt_ext_tmpdir;
    } else {
	push @tempdir_opt, TMPDIR => 1;
    }
    
    # global; we use it in exit_repocop
    $PKGTMPDIR = tempdir( $tmpdir_template, @tempdir_opt, CLEANUP=>1);
    die "can't create temporary directory!" unless $PKGTMPDIR;
    my $chrootdir="$PKGTMPDIR/chroot";
    make_path($chrootdir);
    $ENV{'RPM_BUILD_ROOT'}=$chrootdir;
    $ENV{'REPOCOP_PKG_ROOT'}=$chrootdir;
}

sub unpack_rpm {
    my ($rpmarg, $rhref,$is_source)=@_;
    my $chrootdir="$PKGTMPDIR/chroot";
    die "can't change directory to $chrootdir" unless chdir $chrootdir;

    &repocop_note("unpacking...");
    if ($is_source) {
	my $specname;
	my $total_files=scalar(@{$rhref->{'FILEFLAGS'}});
	#SPEC files have flag 32
	for (my $i=0;$i<$total_files;$i++) {
	    $specname = $rhref->{'BASENAMES'}[$i] if (@{$rhref->{'FILEFLAGS'}}[$i] == 32);
	}
	my %unpack_add;
	if (@SourceTestPatterns) {
	    foreach my $srcfile (@{$rhref->{'BASENAMES'}}) {
		foreach my $pattern (@SourceTestPatterns) {
		    $unpack_add{$srcfile}=1 if $srcfile=~/$pattern/;
		}
	    }
	}
	#print STDERR "extra unpack: ".join(" ",keys(%unpack_add)),"\n" if %unpack_add;
	my @unpack_files=($specname, keys(%unpack_add));
	system("rpm2cpio '$rpmarg' | $cpio_unpack_cmd '".join("' '",@unpack_files)."'") && die "rpm2cpio failed on srpm $rpmarg";
	$ENV{'REPOCOP_PKG_SPECFILE'}="$chrootdir/$specname";
    } else {
	if ($repocop_use_fakeroot) {
	    if (system("rpm2cpio '$rpmarg' | fakeroot -s ../.fakedata -- $cpio_unpack_cmd")) {
		warn "rpm2cpio (via fakeroot) failed on $rpmarg";
		chdir $rundir;
		return 0;
	    }
	} else {
	    if (system("rpm2cpio '$rpmarg' | $cpio_unpack_cmd")) {
		warn "rpm2cpio failed on $rpmarg. Trying fakeroot...\n";
		chdir $rundir;
		&del_PKGTMPDIR();
		&create_pkgtmpdir($rpmarg);
		if (system("rpm2cpio '$rpmarg' | fakeroot -s ../.fakedata -- $cpio_unpack_cmd")) {
		    warn "rpm2cpio (via fakeroot) failed on $rpmarg";
		    chdir $rundir;
		    return 0;
		}
	    }
	}
    }
    return 1;
}

sub get_test_option {
    my ($testname, $optionname)=@_;
    return $TestOptions{$testname}{$optionname} if defined $TestOptions{$testname} and defined $TestOptions{$testname}{$optionname};
    return $DefaultOptions{$optionname};
}

sub load_test_options_files {
    local $_;
    foreach my $patternfile (&Test::Repocop::Common::glob_tests('filepattern',@Repocop::arg::pkgscripts_dir,@Repocop::arg::srcscripts_dir)) {
	my $testname=&Test::Repocop::Common::filepath2testname($patternfile);
	$TestFilePattern{$testname}=$patternfile;
    }

    foreach my $patternfile (&Test::Repocop::Common::glob_tests('filepattern',@Repocop::arg::srcscripts_dir)) {
	my $testname=&Test::Repocop::Common::filepath2testname($patternfile);
	my @patterns;
	open (PATFILE, '<', $patternfile) || die "can't open patterns file $patternfile: $!";
	while (<PATFILE>) {
	    chomp;
	    next if /^\s*(?:\#|$)/;
	    push @patterns,$_;
	}
	close(PATFILE);
	$SourceTestPattern{$testname}=\@patterns;
	push @SourceTestPatterns, @patterns;
    }

    foreach my $optionsfile (&Test::Repocop::Common::glob_tests('options',@Repocop::arg::srcscripts_dir,@Repocop::arg::pkgscripts_dir)) {
	my $testname=&Test::Repocop::Common::filepath2testname($optionsfile);
	my %options;
	open (OPTFILE, '<', $optionsfile) || die "can't open options file $optionsfile: $!";
	while (<OPTFILE>) {
	    next if /^\s*(?:\#|$)/;
	    if (/^\s*(\S+)\s*=\s*(\S+)\s*$/) {
		$options{$1}=$2;
	    } else {
		warn "unrecognized line $_ in options file $optionsfile\n";
	    }
	}
	close(OPTFILE);
	$TestOptions{$testname}=\%options;
    }
}

sub __convert_megabytes {
    my ($optname, $valptr)=@_;
    return unless $$valptr;
    die "$optname: invalid arg $$valptr: should be a number or a number[kb|mb|gb]" unless $$valptr=~/^\d+[KkMmGg]?[Ii]?[Bb]?$/;
    if ($$valptr=~s/([KkMmGg])?[Ii]?[Bb]?$//) {
	if ($1 eq 'K' or $1 eq 'k') {
	    $$valptr *= 1024;
	} elsif ($1 eq 'M' or $1 eq 'm') {
	    $$valptr *= 1048576;
	} elsif ($1 eq 'G' or $1 eq 'g') {
	    $$valptr *= 1073741824;
	}
    }
}

package LazyRPMFileHeader;

sub new {
    my ($class,$rpmfile)=@_;
    my $self=[$rpmfile,undef];
    return bless $self;
}

sub get_rhref {
    my $self=shift;
    return $self->[1] if $self->[1];
    $self->[1] = new RPM::Header $self->[0];
    return $self->[1];
}

__END__

=head1	NAME

repocop - run unit tests against the repo or a given set of RPM packages

=head1	SYNOPSIS

B<repocop-run>
[B<-f|--fakeroot> ]
[B<--no-fakeroot> ]
[B<--collect> ]
[B<--no-collect> ]
[B<--posttest> ]
[B<--no-posttest> ]
[B<--skip-big-binary> I<size>]
[B<--skip-big-source> I<size>]
[B<-h|--help>]
[B<-v|--verbose>]
[B<-q|--quiet>]
[B<-c|--cachedir> I<cachedir>]
[B<--et|--exclude-test> I<comma separated list of tests>]
[B<--it|--include-test> I<comma separated list of tests>]
[B<--ep|--exclude-packager> I<comma separated list of packager's nicks>]
[B<--ip|--include-packager> I<comma separated list of packager's nicks>]
[B<--pkgcollectors-dir> I<comma separated list of local collectors' dirs>]
[B<--srccollectors-dir> I<comma separated list of local collectors' dirs>]
[B<--pkgtests-dir> I<comma separated list of local tests' dirs>]
[B<--srctests-dir> I<comma separated list of local tests' dirs>]
[B<--ex|--except>] 
[B<-g|--given>] 
[B<-l|--last-run>] 
[B<--newer>] I<filename>
[B<-r|--report> <s[kip]|o[k]|w[arn]|f[ail]>]
[I<DIR>...] [I<FILE>...]

=head1	DESCRIPTION

B<repocop> executes a set of tests against each RPM package given on the
command line. Presize subset of tests can be selected using B<--include>
and B<--exclude> options.
Extra word splitting is performed on the I<comma separated list of tests>.
Each I<FILE> is treated as RPM package.  Each I<DIR> is listed with C<*.rpm>
pattern, and all RPM files found are processed.

=head1	OPTIONS

=over

=item	B<-f,--fakeroot>

Always use fakeroot for unpacking rpms.
(note: there are stability issues with some versions of fakeroot). 
It is slower and required only for tests that check ownership and so on.

=item	B<--no-fakeroot>

Do not use fakeroot for unpacking rpms (default).

=item	B<-f,--collect>

run package processing collectors and tests (default)

=item	B<--no-collect>

Skip package processing collectors and tests.

=item	B<-f,--posttest>

run post collectors and tests after package processing (default).

=item	B<--no-posttest>

Skip running post collectors and tests after package processing.

=item	B<-f,--distrotest>

run context dependent distro tests after package processing.

=item	B<--no-distrotest>

Skip running context dependent distro tests after package processing (default).

=item	B<--tmpdir> I<path>

Set default temporary dir to be at I<path>.

=item	B<--big-tmpdir> I<path>

Set the temporary dir for big files to be at I<path>.

=item	B<--big-binary> I<size>

If the binary rpm's size is bigger than I<size>, big-tmpdir is used instead of default temporary dir. 
kb, mb, gb, Kb,Mb, Gb, k, m, g suffixes are allowed.

=item	B<--skip-big-binary> I<size>

Skip unpacking binary rpms bigger than I<size>. kb, mb, gb, Kb,Mb, Gb, k, m, g suffixes are allowed.

=item	B<--skip-big-source> I<size>

Skip unpacking source rpms bigger than I<size>. kb, mb, gb, Kb,Mb, Gb, k, m, g suffixes are allowed.

=item	B<-c,--cachedir> I<dir>

Provides alternative location for cachedir. 
Repocop cachedir is a place where test results and 
packages metadata information are stored.

=item	B<--except>, B<--given>

Control processing of rpm arguments. 
B<--given> (default) means processing only given rpm arguments.
B<--except>  means processing all data except given rpm arguments.

=item	B<--et, --exclude-test> I<comma separated list of tests>

Report all processed tests exept the given excluded set.

=item	B<--it, --include-test> I<comma separated list of tests>

Report the given set of tests.

=item	B<--ep, --exclude-packager> I<comma separated list of tests>

=item	B<--it, --include-packager> I<comma separated list of tests>

Exclude/include packages according to Packager: tag.

=item [B<--pkgcollectors-dir> I<comma separated list of local collectors' dirs>]

=item [B<--srccollectors-dir> I<comma separated list of local collectors' dirs>]

=item [B<--pkgtests-dir> I<comma separated list of local tests' dirs>]

=item [B<--srctests-dir> I<comma separated list of local tests' dirs>]

Append user's local tests and collectors to repocop.

=item	B<-h, --help>

Display this help and exit.

=item	B<-v, --verbose>, B<-q, --quiet>

Verbosity level. Multiple -v increase the verbosity level, -q sets it to 0.

=item	B<-l, --last-run>

Use the set of packages processed at last run as an argument.

=item	B<--newer> I<filename>

Process packages newer then I<filename> only.
Note: this filtering does not apply to B<--last-run> option.

=item	B<--acl-file> I<file>

the argument is /path/to/Sisyphus/files/list/list.src.classic
This option is ALTLinux-specific. The file content is ACL db,
which is used to sort result by ALTLinux ACL.


=back

=head1	AUTHOR

Written by Igor Vlasenko <viy@altlinux.org>.

=head1	ACKNOWLEGEMENTS

To Alexey Torbin <at@altlinux.org>, whose qa-robot package
had a strong influence on repocop. 

=head1	COPYING

Copyright (c) 2008 Igor Vlasenko, ALT Linux Team.

This 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.

=cut
