#!/usr/bin/perl use strict; use warnings; use threads; use threads::shared; use Cwd; use Getopt::Long; use File::Basename; use File::Find; use File::Spec; use File::Copy::Recursive qw(fcopy rcopy dircopy); use Tie::File; use Cwd; use Cwd 'abs_path'; use Term::ANSIColor; use Data::Dumper; sub help { my $msg = << 'EOS'; The script run_test.pl is a test runner that work in conjunction with ps2autotests (https://github.com/unknownbrackets/ps2autotests) Mandatory Option --exe : the PCSX2 binary that you want to test --cfg : a path to the a default ini configuration of PCSX2 --suite : a path to ps2autotests test (root directory) Optional Option --cpu=1 : the number of parallel tests launched. Might create additional issue --timeout=20 : a global timeout for hang tests --show_diff : show debug information --test= : filter test based on their names --bad : only run blacklisted tests --regression : blacklist test that are known to be broken --option = : overload PCSX2 configuration option --debug_me : print script info --dry_run : don't launch PCSX2 PCSX2 option EnableEE=disabled : Use EE interpreter EnableIOP=disabled : Use IOP interpreter EnableVU0=disabled : Use VU0 interpreter EnableVU1=disabled : Use VU1 interpreter FPU.Roundmode=3 : EE FPU round mode VU.Roundmode=3 : VU round mode fpuExtraOverflow=enabled : Full EE FPU fpuFullMode=enabled : Full EE FPU EOS print $msg; exit } my $mt_timeout :shared; my ($o_suite, $o_help, $o_exe, $o_cfg, $o_max_cpu, $o_timeout, $o_show_diff, $o_debug_me, $o_test_name, $o_regression, $o_dry_run, %o_pcsx2_opt, $o_cygwin, $o_bad); # default value $o_bad = 0; $o_regression = 0; $o_cygwin = 0; $o_max_cpu = 8; $o_timeout = 30; $o_help = 0; $o_debug_me = 0; $o_dry_run = 0; $o_test_name = ".*"; $o_exe = File::Spec->catfile("bin", "PCSX2"); if (exists $ENV{"PS2_AUTOTESTS_ROOT"}) { $o_suite = $ENV{"PS2_AUTOTESTS_ROOT"}; } if (exists $ENV{"PS2_AUTOTESTS_CFG"}) { $o_cfg = $ENV{"PS2_AUTOTESTS_CFG"}; } my $status = Getopt::Long::GetOptions( 'bad' => \$o_bad, 'cfg=s' => \$o_cfg, 'cpu=i' => \$o_max_cpu, 'cygwin' => \$o_cygwin, 'debug_me' => \$o_debug_me, 'dry_run' => \$o_dry_run, 'exe=s' => \$o_exe, 'help' => \$o_help, 'option=s' => \%o_pcsx2_opt, 'regression' => \$o_regression, 'testname=s' => \$o_test_name, 'timeout=i' => \$o_timeout, 'show_diff' => \$o_show_diff, 'suite=s' => \$o_suite, ); # Auto detect cygwin mess if (-e "/cygdrive") { print "INFO: CYGWIN OS detected. Update path accordingly\n"; $o_cygwin = 1; } ##################################################### # Check option ##################################################### if (not $status or $o_help) { help(); } unless (defined $o_suite) { print "Error: require a test suite directory\n"; print "Note: you could use either use --suite or the env variable \$PS2_AUTOTESTS_ROOT\n"; help(); } # Default value if the dir exists $o_cfg = "bin/inis" if (not defined $o_cfg and -d "bin/inis"); unless (defined $o_cfg) { print "Error: require a default cfg directory\n"; print "Note: you could use either use --cfg or the env variable \$PS2_AUTOTESTS_CFG\n"; help(); } $o_exe = abs_path($o_exe); $o_cfg = abs_path($o_cfg); $o_suite = abs_path($o_suite); $mt_timeout = $o_timeout; unless (-d $o_suite) { print "Error: --suite option requires a directory\n"; help(); } unless (-x $o_exe) { print "Error: --exe option requires an executable\n"; help(); } unless (-d $o_cfg) { print "Error: --cfg option requires a directory\n"; help(); } my %blacklist; if ($o_regression or $o_bad) { # Blacklist bad test # FULL FPU rounding to avoid EE FPU test issu #$o_pcsx2_opt{"fpuExtraOverflow"} = "enabled"; #$o_pcsx2_opt{"fpuFullMode"} = "enabled"; # EE $blacklist{"branchdelay"} = 1; # EE fpu $blacklist{"arithmetic"} = 1; $blacklist{"branchdelay"} = 1; $blacklist{"compare"} = 1; $blacklist{"fcr"} = 1; $blacklist{"muldiv"} = 1; $blacklist{"sqrt"} = 1; # IOP $blacklist{"lsudelay"} = 1; # Kernel IOP $blacklist{"register"} = 1; $blacklist{"receive"} = 1; $blacklist{"stat"} = 1; $blacklist{"send"} = 1; # VU $blacklist{"triace"} = 1; } ##################################################### # Run ##################################################### # Round 1: Collect the tests my $cwd = getcwd(); my $g_test_db; print "INFO: search tests in $o_suite and run them in $o_max_cpu CPU)\n"; find({ wanted => \&add_test_cmd_for_elf, no_chdir => 1 }, $o_suite); print "\n"; chdir($cwd); # Just to be sure # Round 2: Run the tests (later in thread) foreach my $test (keys(%$g_test_db)) { # wait free CPU slot while( scalar(threads->list() >= $o_max_cpu) ) { if (close_joinnable_threads() == 0) { sleep(1); # test are often fast so 1s is more than enough $mt_timeout--; } kill_thread_if_timeout() } create_thread($test); } wait_all_threads(); # Round 3: Collect the results (not thread safe) collect_result(); # Pretty print print "\n\n Status | =========================== Test ================================\n"; foreach my $test (sort(keys(%$g_test_db))) { my $info = $g_test_db->{$test}; my $cfg = $info->{"CFG_DIR"}; my $out = $info->{"OUT"}; my $exp = $info->{"EXPECTED"}; if ($info->{"STATUS"} eq "OK") { print color('bold green'); print " OK | $test\n"; } else { if ($info->{"STATUS"} eq "T") { print color('bold blue'); print " Tout | $test\n"; } else { print color('bold red'); print " KO | $test\n"; } if ($o_show_diff) { print color('bold magenta'); print "-----------------------------------------------------------------------\n"; print color('reset'); # Easy copy/past to rerun the test in gdb. Yes lazy guy detected :p print "gdb -ex=r --args " . test_cmd($test, $cfg) . "\n"; print "vi -d $exp $out\n"; print "\n"; diff($exp, $out, 0); print color('bold magenta'); print "-----------------------------------------------------------------------\n"; print color('reset'); } } } print color('reset'); print "\n"; ##################################################### # Sub helper ##################################################### sub cyg_abs_path { my $p = shift; if ($o_cygwin) { $p =~ s/\/cygdrive\/(\w)/$1:/; } return $p; } sub collect_result { foreach my $test (keys(%$g_test_db)) { my $info = $g_test_db->{$test}; my $cfg = $info->{"CFG_DIR"}; my $out = $info->{"OUT"}; my $exp = $info->{"EXPECTED"}; extract_test_log(File::Spec->catfile($cfg, "emuLog.txt"), $out); $info->{"STATUS"} = diff($exp, $out, 1); # potentially not thread safe } } sub extract_test_log { my $in = shift; my $out = shift; return unless (-e $in); open(my $emulog, "<$in"); my @all_data = <$emulog>; open(my $short_log, ">$out") or die "Impossible to open $!"; my $dump = 0; foreach my $line (@all_data) { # Remove color $line =~ s/\e\[\d+(?>(;\d+)*)m//g; if ($line =~ /-- TEST BEGIN/) { $dump = 1; } if ($dump == 1) { chomp($line); $line =~ s/\r$//g; print $short_log "$line\n"; } if ($line =~ /-- TEST END/) { $dump = 0; last; } } } sub add_test_cmd_for_elf { my $file = $_; my $ext = "\\.(elf|irx)"; return 0 unless ($file =~ /$ext/); return 0 unless ($file =~ /$o_test_name/i); my($test, $dir_, $suffix) = fileparse($file, qw/.elf .irx/); return 0 if ($o_regression and exists $blacklist{$test}); return 0 if ($o_bad and not exists $blacklist{$test}); # Fast test #return 0 unless ($file =~ /branchdelay/); my $dir = $File::Find::dir; print "INFO: found test $test in $dir\n" if $o_debug_me or $o_dry_run; $g_test_db->{$file}->{"CFG_DIR"} = $file =~ s/$ext/_cfg/r; $g_test_db->{$file}->{"EXPECTED"} = $file =~ s/$ext/.expected/r; $g_test_db->{$file}->{"OUT"} = $file =~ s/$ext/.PCSX2.out/r; $g_test_db->{$file}->{"STATUS"} = "T"; return 1; } sub run_thread { my $test = shift; my $info = $g_test_db->{$test}; generate_cfg($info->{"CFG_DIR"}); run_elf($test, $info->{"CFG_DIR"}, $info->{"OUT"}); } sub generate_cfg { my $out_dir = shift; print "INFO: Copy dir $o_cfg to $out_dir\n" if $o_debug_me; local $File::Copy::Recursive::RMTrgDir = 2; dircopy($o_cfg, $out_dir) or die "Failed to copy directory: $!\n"; my %sed; # Enable logging for test $sed{".EEout"} = "enabled"; $sed{".IOPout"} = "enabled"; # Redirect log file in the unique cfg dir #$sed{"ConsoleToStdio"} = "enabled"; # was to redirect stdio (but windows...) # Still requires to force the flush $sed{"Logs"} = cyg_abs_path($out_dir); $sed{"UseDefaultLogs"} = "disabled"; # FIXME add interpreter vs recompiler # FIXME add clamping / rounding option # FIXME need separate cfg dir ! foreach my $k (keys(%o_pcsx2_opt)) { my $v = $o_pcsx2_opt{$k}; $sed{$k} = $v; } tie my @ui, 'Tie::File', File::Spec->catfile($out_dir, "PCSX2_ui.ini") or die "Fail to tie $!\n"; for (@ui) { foreach my $option (keys(%sed)) { my $v = $sed{$option}; s/$option=.*/$option=$v/; } } untie @ui; tie my @vm, 'Tie::File', File::Spec->catfile($out_dir, "PCSX2_vm.ini") or die "Fail to tie $!\n"; for (@vm) { foreach my $option (keys(%sed)) { my $v = $sed{$option}; s/$option=.*/$option=$v/; } } untie @vm; # Disable sound emulation (avoid spurious "ALSA lib pcm.c:7843:(snd_pcm_recover) underrun occurred") tie my @spu, 'Tie::File', File::Spec->catfile($out_dir, "SPU2.ini") or die "Fail to tie $!\n"; for (@spu) { s/Output_Module=.*/Output_Module=nullout/; } untie @spu; } sub run_elf { my $elf = shift; my $cfg = shift; my $out = shift; ###################################################################### # FORK test ###################################################################### my $command = test_cmd($elf, $cfg); print "FORK $command\n" if $o_debug_me; return unless ($command ne ""); my $pid = 0; my $log_file = File::Spec->catfile($cfg, "emuLog.txt"); if ($o_dry_run) { print "INFO-DRY: fork process $pid\n"; # Delete old log unlink($out) or die "Impossible to open $!"; return; } $pid = open(my $fork, "|$command ") or die "Impossible to fork $!"; print "INFO: fork process $pid\n"; # Kill me $SIG{'KILL'} = sub { print "ERROR: timeout detected on pid $pid.\n"; unless ($o_dry_run) { kill 'KILL', $pid; } threads->exit(); }; ###################################################################### # Parse test log ###################################################################### my $try = ($o_timeout > 3) ? $o_timeout - 3 : 1; while ($try > 0) { sleep(1); $try--; open(my $emulog, "<$log_file") or next; my @all_data = <$emulog>; close($emulog); foreach my $line (@all_data) { if ($line =~ /-- TEST END/) { $try = 0; } } } ###################################################################### # Test done ###################################################################### # Kill the process print "INFO: kill process $pid\n" if $o_debug_me; kill 'TERM', $pid; threads->exit(); } sub test_cmd { my $elf = shift; my $cfg = shift; $elf = cyg_abs_path($elf); $cfg = cyg_abs_path($cfg); if ($elf =~ /\.elf/) { return "$o_exe --elf $elf --cfgpath=$cfg" } elsif ($elf =~ /\.irx/) { return "$o_exe --irx $elf --cfgpath=$cfg" } else { print "ERROR: bad command parameters $elf $cfg\n"; return ""; } } sub diff { my $ref_ = shift; my $out_ = shift; my $quiet = shift; open (my $ref_h, "<$ref_"); my @ref = <$ref_h>; chomp(@ref); open (my $out_h, "<$out_") or return "T"; my @out = <$out_h>; chomp(@out); return "T" if (scalar(@out) < 2); return "T" if ($out[-1] !~ /-- TEST END/); return "KO" if ((scalar(@out) != scalar(@ref)) and $quiet); my $status = "OK"; my $show = 10; for (my $l = 0; $l < scalar(@ref); $l++) { $ref[$l] =~ s/\r//g; $out[$l] =~ s/\r//g; if ($ref[$l] ne $out[$l]) { $status = "KO"; if ($o_show_diff and not $quiet and $show > 0) { print "EXPECTED: \"$ref[$l]\"\n"; print "BUT GOT : \"$out[$l]\"\n"; $show--; } } } return $status; } ##################################################### # Thread management ##################################################### my $g_counter = 0; sub create_thread { my $cmd = shift; my $thr = threads->create(\&run_thread, $cmd ); $g_counter++; } sub close_joinnable_threads { my $closed = 0; foreach my $thr (threads->list(threads::joinable)) { $thr->join(); $closed = 1; $g_counter--; } return $closed; } sub wait_all_threads { # wait free CPU slot while( scalar(threads->list() > 0) and $mt_timeout > 0) { if (close_joinnable_threads() == 0) { sleep(1); # test are often fast so 1s is more than enough $mt_timeout--; } } kill_thread_if_timeout() } sub kill_thread_if_timeout { if ($mt_timeout <= 0) { foreach my $thr (threads->list()) { # Farewell my friend print "ERROR: send kill on timeout process\n"; $thr->kill('KILL')->join(); } $mt_timeout = 100; } }