8 use File::Path qw(mkpath remove_tree);
9 use Getopt::Long qw(:config posix_default bundling no_ignore_case);
11 use Storable qw(dclone);
15 #use Data::Dumper qw(Dumper);
17 # NiHTest -- package to run regression tests
18 # Copyright (C) 2002-2016 Dieter Baron and Thomas Klausner
20 # This file is part of ckmame, a program to check rom sets for MAME.
21 # The authors can be contacted at <ckmame@nih.at>
23 # Redistribution and use in source and binary forms, with or without
24 # modification, are permitted provided that the following conditions
26 # 1. Redistributions of source code must retain the above copyright
27 # notice, this list of conditions and the following disclaimer.
28 # 2. Redistributions in binary form must reproduce the above copyright
29 # notice, this list of conditions and the following disclaimer in
30 # the documentation and/or other materials provided with the
32 # 3. The names of the authors may not be used to endorse or promote
33 # products derived from this software without specific prior
36 # THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
37 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
38 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
39 # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
40 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
41 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
42 # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
43 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
44 # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
45 # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
46 # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
51 # TESTNAME.test: test scenario
54 # Lines beginning with # are comments.
56 # The following commands are recognized; return and args must
57 # appear exactly once, the others are optional.
60 # run program with command line arguments ARGS
63 # description of what test is for
65 # features FEATURE ...
66 # only run test if all FEATUREs are present, otherwise skip it.
69 # copy file IN as TEST, compare against OUT after program run.
72 # copy file IN as TEST, check that it is removed by program.
75 # check that file TEST is created by program and compare
79 # create directory NAME with permissions MODE.
82 # pipe FILE to program's stdin.
84 # pipein COMMAND ARGS ...
85 # pipe output of running COMMAND to program's stdin.
87 # precheck COMMAND ARGS ...
88 # if COMMAND exits with non-zero status, skip test.
91 # pre-load LIBRARY before running program.
94 # run PRG instead of ckmame.
97 # RET is the expected exit code
100 # set environment variable VAR to VALUE.
103 # program is expected to produce the error message TEXT. If
104 # multiple stderr commands are used, the messages are
105 # expected in the order given.
107 # stderr-replace REGEX REPLACEMENT
108 # run regex replacement over expected and got stderr output.
111 # program is expected to print TEXT to stdout. If multiple
112 # stdout commands are used, the messages are expected in
116 # set last modified timestamp of FILE to MTIME (seconds since epoch).
117 # If FILE doesn't exist, an empty file is created.
120 # set ulimit -C to VALUE while running the program.
123 # runtest uses the following exit codes:
127 # 77: test was skipped
129 # environment variables:
130 # RUN_GDB: if set, run gdb on program in test environment
131 # KEEP_BROKEN: if set, don't delete test environment if test failed
132 # NO_CLEANUP: if set, don't delete test environment
133 # SETUP_ONLY: if set, exit after creating test environment
134 # VERBOSE: if set, be more verbose (e. g., output diffs)
146 my $class = UNIVERSAL::isa ($_[0], __PACKAGE__) ? shift : __PACKAGE__;
147 my $self = bless {}, $class;
151 $self->{default_program} = $opts->{default_program};
152 $self->{zipcmp} = $opts->{zipcmp} // 'zipcmp';
153 $self->{zipcmp_flags} = $opts->{zipcmp_flags} // '-p';
155 $self->{directives} = {
156 args => { type => 'string...', once => 1, required => 1 },
157 description => { type => 'string', once => 1 },
158 features => { type => 'string...', once => 1 },
159 file => { type => 'string string string' },
160 'file-del' => { type => 'string string' },
161 'file-new' => { type => 'string string' },
162 mkdir => { type => 'string string' },
163 pipefile => { type => 'string', once => 1 },
164 pipein => { type => 'string', once => 1 },
165 precheck => { type => 'string...' },
166 preload => { type => 'string', once => 1 },
167 program => { type => 'string', once => 1 },
168 'return' => { type => 'int', once => 1, required => 1 },
169 setenv => { type => 'string string' },
170 stderr => { type => 'string' },
171 'stderr-replace' => { type => 'string string' },
172 stdout => { type => 'string' },
173 touch => { type => 'int string' },
174 ulimit => { type => 'char string' }
177 $self->{compare_by_type} = {};
178 $self->{copy_by_type} = {};
181 $self->get_variable('srcdir', $opts);
182 $self->get_variable('top_builddir', $opts);
184 $self->{in_sandbox} = 0;
186 $self->{verbose} = $ENV{VERBOSE};
187 $self->{keep_broken} = $ENV{KEEP_BROKEN};
188 $self->{no_cleanup} = $ENV{NO_CLEANUP};
189 $self->{setup_only} = $ENV{SETUP_ONLY};
196 my ($self, $ext, $sub) = @_;
198 return $self->add_file_proc('compare_by_type', $ext, $sub);
203 my ($self, $ext, $sub) = @_;
205 return $self->add_file_proc('copy_by_type', $ext, $sub);
210 my ($self, $name, $def) = @_;
212 if (exists($self->{directives}->{$name})) {
213 $self->die("directive $name already defined");
216 # TODO: validate $def
218 $self->{directives}->{$name} = $def;
225 my ($self, $proc, $ext, $sub) = @_;
227 $self->{$proc}->{$ext} = [] unless (defined($self->{$proc}->{$ext}));
228 unshift @{$self->{$proc}->{$ext}}, $sub;
235 my ($self, $hook, $sub) = @_;
237 $self->{hooks}->{$hook} = [] unless (defined($self->{hooks}->{$hook}));
238 push @{$self->{hooks}->{$hook}}, $sub;
245 my ($self, $name, $hooks) = @_;
247 if (!defined($self->{variants})) {
248 $self->{variants} = [];
249 $self->add_directive('variants' => { type => 'string...', once => 1 });
251 for my $variant (@{$self->{variants}}) {
252 if ($variant->{name} eq $name) {
253 $self->die("variant $name already defined");
257 push @{$self->{variants}}, { name => $name, hooks => $hooks };
264 my ($self, @results) = @_;
268 for my $r (@results) {
269 if ($r eq 'ERROR' || ($r eq 'FAIL' && $result ne 'ERROR')) {
274 $self->end_test($result);
279 my ($self, @argv) = @_;
283 $self->end($self->runtest());
290 if (defined($self->{variants})) {
292 $self->{original_test} = $self->{test};
296 if (defined($self->{test}->{variants})) {
297 %variants = map { $_ => 1; } @{$self->{test}->{variants}};
300 for my $variant (@{$self->{variants}}) {
301 next if (defined($self->{test}->{variants}) && !exists($variants{$variant->{name}}));
303 $self->{variant_hooks} = $variant->{hooks};
304 $self->{test} = dclone($self->{original_test});
305 $self->{variant} = $variant->{name};
306 $self->mangle_test_for_variant();
307 push @results, $self->runtest_one($variant->{name});
313 return $self->runtest_one();
319 my ($self, $tag) = @_;
322 $ENV{LC_CTYPE} = "C";
323 $ENV{POSIXLY_CORRECT} = 1;
324 $self->sandbox_create($tag);
325 $self->sandbox_enter();
328 $ok &= $self->copy_files();
329 $ok &= $self->run_hook('post_copy_files');
330 $ok &= $self->touch_files();
331 $ok &= $self->run_hook('prepare_sandbox');
332 return 'ERROR' unless ($ok);
334 if ($self->{setup_only}) {
335 $self->sandbox_leave();
339 for my $env (@{$self->{test}->{'setenv'}}) {
340 $ENV{$env->[0]} = $env->[1];
342 my $preload_env_var = 'LD_PRELOAD';
343 if ($^O eq 'darwin') {
344 $preload_env_var = 'DYLD_INSERT_LIBRARIES';
346 if (defined($self->{test}->{'preload'})) {
347 if (-f cwd() . "/../.libs/$self->{test}->{'preload'}") {
348 $ENV{$preload_env_var} = cwd() . "/../.libs/$self->{test}->{'preload'}";
350 $ENV{$preload_env_var} = cwd() . "/../lib$self->{test}->{'preload'}";
354 $self->run_program();
356 for my $env (@{$self->{test}->{'setenv'}}) {
357 delete ${ENV{$env->[0]}};
359 if (defined($self->{test}->{'preload'})) {
360 delete ${ENV{$preload_env_var}};
363 if ($self->{test}->{stdout}) {
364 $self->{expected_stdout} = [ @{$self->{test}->{stdout}} ];
367 $self->{expected_stdout} = [];
369 if ($self->{test}->{stderr}) {
370 $self->{expected_stderr} = [ @{$self->{test}->{stderr}} ];
373 $self->{expected_stderr} = [];
376 $self->run_hook('post_run_program');
380 if ($self->{exit_status} != ($self->{test}->{return} // 0)) {
381 push @failed, 'exit status';
382 if ($self->{verbose}) {
383 print "Unexpected exit status:\n";
384 print "-" . ($self->{test}->{return} // 0) . "\n+$self->{exit_status}\n";
388 if (!$self->compare_arrays($self->{expected_stdout}, $self->{stdout}, 'output')) {
389 push @failed, 'output';
391 if (!$self->compare_arrays($self->{expected_stderr}, $self->{stderr}, 'error output')) {
392 push @failed, 'error output';
394 if (!$self->compare_files()) {
395 push @failed, 'files';
398 $self->{failed} = \@failed;
400 $self->run_hook('checks');
402 my $result = scalar(@{$self->{failed}}) == 0 ? 'PASS' : 'FAIL';
404 $self->sandbox_leave();
405 if (!($self->{no_cleanup} || ($self->{keep_broken} && $result eq 'FAIL'))) {
406 $self->sandbox_remove();
409 $self->print_test_result($tag, $result, join ', ', @{$self->{failed}});
416 my ($self, @argv) = @_;
418 my @save_argv = @ARGV;
421 'help|h' => \my $help,
422 'keep-broken|k' => \$self->{keep_broken},
423 'no-cleanup' => \$self->{no_cleanup},
424 # 'run-gdb' => \$self->{run_gdb},
425 'setup-only' => \$self->{setup_only},
426 'verbose|v' => \$self->{verbose}
431 if (!$ok || scalar(@argv) != 1 || $help) {
432 print STDERR "Usage: $0 [-hv] [--keep-broken] [--no-cleanup] [--setup-only] testcase\n";
436 my $testcase = shift @argv;
438 $testcase .= '.test' unless ($testcase =~ m/\.test$/);
440 my $testcase_file = $self->find_file($testcase);
442 $self->die("cannot find test case $testcase") unless ($testcase_file);
444 $testcase =~ s,^(?:.*/)?([^/]*)\.test$,$1,;
445 $self->{testname} = $testcase;
447 $self->die("error in test case definition") unless $self->parse_case($testcase_file);
449 $self->check_features_requirement() if ($self->{test}->{features});
450 $self->run_precheck() if ($self->{test}->{precheck});
452 $self->end_test('SKIP') if ($self->{test}->{preload} && $^O eq 'darwin');
456 # MARK: - Internal Methods
459 my ($self, $file) = @_;
461 if (defined($self->{files}->{$file->{destination}})) {
462 $self->warn("duplicate specification for input file $file->{destination}");
466 $self->{files}->{$file->{destination}} = $file;
472 sub check_features_requirement() {
478 unless (open($fh, '<', "$self->{top_builddir}/config.h")) {
479 $self->die("cannot open config.h in top builddir $self->{top_builddir}");
481 while (my $line = <$fh>) {
482 if ($line =~ m/^#define HAVE_([A-Z0-9_a-z]*)/) {
489 for my $feature (@{$self->{test}->{features}}) {
490 if (!$features{$feature}) {
491 push @missing, $feature;
495 if (scalar @missing > 0) {
496 my $reason = "missing features";
497 if (scalar(@missing) == 1) {
498 $reason = "missing feature";
500 $self->print_test_result('SKIP', "$reason: " . (join ' ', @missing));
501 $self->end_test('SKIP');
509 my ($self, $got, $expected) = @_;
511 my @args = ($self->{zipcmp}, $self->{verbose} ? '-v' : '-q');
512 push @args, $self->{zipcmp_flags} if ($self->{zipcmp_flags});
513 push @args, ($expected, $got);
515 my $ret = system(@args);
521 sub compare_arrays() {
522 my ($self, $a, $b, $tag) = @_;
526 if (scalar(@$a) != scalar(@$b)) {
530 for (my $i = 0; $i < scalar(@$a); $i++) {
531 if ($a->[$i] ne $b->[$i]) {
538 if (!$ok && $self->{verbose}) {
539 print "Unexpected $tag:\n";
540 print "--- expected\n+++ got\n";
551 open my $fha, "< $a";
552 open my $fhb, "< $b";
555 BYTE: while (!eof $fha && !eof $fhb) {
556 if (getc $fha ne getc $fhb) {
561 $result = 1 if eof $fha != eof $fhb;
567 sub compare_file($$$) {
568 my ($self, $got, $expected) = @_;
570 my $real_expected = $self->find_file($expected);
571 unless ($real_expected) {
572 $self->warn("cannot find expected result file $expected");
576 my $ok = $self->run_comparator($got, $real_expected);
580 if ($self->{verbose}) {
581 $ret = system('diff', '-u', $real_expected, $got);
584 $ret = file_cmp($real_expected, $got);
599 while (scalar(@dirs) > 0) {
600 my $dir = shift @dirs;
606 while (my $entry = readdir($ls)) {
607 my $file = "$dir/$entry";
613 push @files, "$file";
615 if (-d $file && $entry ne '.' && $entry ne '..') {
625 sub compare_files() {
631 my @files_got = sort(list_files("."));
632 my @files_should = ();
634 for my $file (sort keys %{$self->{files}}) {
635 push @files_should, $file if ($self->{files}->{$file}->{result} || $self->{files}->{$file}->{ignore});
638 $self->{files_got} = \@files_got;
639 $self->{files_should} = \@files_should;
641 unless ($self->run_hook('post_list_files')) {
645 $ok = $self->compare_arrays($self->{files_should}, $self->{files_got}, 'files');
647 for my $file (@{$self->{files_got}}) {
648 my $file_def = $self->{files}->{$file};
649 next unless ($file_def && $file_def->{result});
651 $ok &= $self->compare_file($file, $file_def->{result});
663 for my $filename (sort keys %{$self->{files}}) {
664 my $file = $self->{files}->{$filename};
665 next unless ($file->{source});
667 my $src = $self->find_file($file->{source});
669 $self->warn("cannot find input file $file->{source}");
674 if ($file->{destination} =~ m,/,) {
675 my $dir = $file->{destination};
682 my $this_ok = $self->run_copier($src, $file->{destination});
683 if (defined($this_ok)) {
687 unless (copy($src, $file->{destination})) {
688 $self->warn("cannot copy $src to $file->{destination}: $!");
694 if (defined($self->{test}->{mkdir})) {
695 for my $dir_spec (@{$self->{test}->{mkdir}}) {
696 my ($mode, $dir) = @$dir_spec;
698 unless (mkdir($dir, oct($mode))) {
699 $self->warn("cannot create directory $dir: $!");
706 $self->die("failed to copy input files") unless ($ok);
711 my ($self, $msg) = @_;
713 print STDERR "$0: $msg\n" if ($msg);
715 $self->end_test('ERROR');
720 my ($self, $status) = @_;
722 my $exit_code = $EXIT_CODES{$status} // $EXIT_CODES{ERROR};
724 $self->exit($exit_code);
730 my ($self, $status) = @_;
738 my ($self, $fname) = @_;
740 for my $dir (('', "$self->{srcdir}/")) {
741 my $f = "$dir$fname";
742 $f = "../$f" if ($self->{in_sandbox} && $dir !~ m,^/,);
744 return $f if (-f $f);
752 my ($self, $fname) = @_;
767 my ($self, $name, $opts) = @_;
769 $self->{$name} = $opts->{$name} // $ENV{$name};
770 if (!defined($self->{$name}) || $self->{$name} eq '') {
772 unless (open($fh, '<', 'Makefile')) {
773 $self->die("cannot open Makefile: $!");
775 while (my $line = <$fh>) {
777 if ($line =~ m/^$name = (.*)/) {
784 if (!defined($self->{$name} || $self->{$name} eq '')) {
785 $self->die("cannot get variable $name");
790 sub mangle_test_for_variant {
793 $self->{test}->{stdout} = $self->strip_tags($self->{variant}, $self->{test}->{stdout});
794 $self->{test}->{stderr} = $self->strip_tags($self->{variant}, $self->{test}->{stderr});
795 $self->run_hook('mangle_test');
801 my ($self, $type, $str) = @_;
803 if ($type eq 'string...') {
807 if ($str =~ m/^\"/) {
808 unless ($str =~ m/^\"([^\"]*)\"\s*(.*)/) {
809 $self->warn_file_line("unclosed quote in [$str]");
816 $str =~ m/^(\S+)\s*(.*)/;
824 elsif ($type =~ m/(\s|\.\.\.$)/) {
826 if ($type =~ m/(.*)\.\.\.$/) {
830 my @types = split /\s+/, $type;
831 my @strs = split /\s+/, $str;
833 for (my $i = scalar(@types) - 1; $i >= 0; $i--) {
834 last unless ($types[$i] =~ m/(.*)\?$/);
839 if ($ellipsis && $optional > 0) {
840 # TODO: check this when registering a directive
841 $self->warn_file_line("can't use ellipsis together with optional arguments");
844 if (!$ellipsis && (scalar(@strs) < scalar(@types) - $optional || scalar(@strs) > scalar(@types))) {
845 my $expected = scalar(@types);
847 $expected = ($expected - $optional) . "-$expected";
849 $self->warn_file_line("expected $expected arguments, got " . (scalar(@strs)));
855 my $n = scalar(@types);
856 for (my $i=0; $i<scalar(@strs); $i++) {
857 my $val = $self->parse_args(($i >= $n ? $types[$n-1] : $types[$i]), $strs[$i]);
858 return undef unless (defined($val));
865 if ($type eq 'string') {
868 elsif ($type eq 'int') {
869 if ($str !~ m/^\d+$/) {
870 $self->warn_file_line("illegal int [$str]");
875 elsif ($type eq 'char') {
876 if ($str !~ m/^.$/) {
877 $self->warn_file_line("illegal char [$str]");
883 $self->warn_file_line("unknown type $type");
891 my ($self, $fname) = @_;
895 open TST, "< $fname" or $self->die("cannot open test case $fname: $!");
897 $self->{testcase_fname} = $fname;
901 while (my $line = <TST>) {
904 next if ($line =~ m/^\#/);
906 unless ($line =~ m/(\S*)(?:\s(.*))?/) {
907 $self->warn_file_line("cannot parse line $line");
911 my ($cmd, $argstring) = ($1, $2//"");
913 my $def = $self->{directives}->{$cmd};
916 $self->warn_file_line("unknown directive $cmd in test file");
921 my $args = $self->parse_args($def->{type}, $argstring);
923 unless (defined($args)) {
929 if (defined($test{$cmd})) {
930 $self->warn_file_line("directive $cmd appeared twice in test file");
935 $test{$cmd} = [] unless (defined($test{$cmd}));
936 push @{$test{$cmd}}, $args;
942 return undef unless ($ok);
944 for my $cmd (sort keys %test) {
945 if ($self->{directives}->{$cmd}->{required} && !defined($test{$cmd})) {
946 $self->warn_file("required directive $cmd missing in test file");
951 if ($test{pipefile} && $test{pipein}) {
952 $self->warn_file("both pipefile and pipein set, choose one");
956 if (defined($self->{variants})) {
957 if (defined($test{variants})) {
958 for my $name (@{$test{variants}}) {
960 for my $variant (@{$self->{variants}}) {
961 if ($name eq $variant->{name}) {
967 $self->warn_file("unknown variant $name");
974 return undef unless ($ok);
976 if (defined($test{'stderr-replace'}) && defined($test{stderr})) {
977 $test{stderr} = [ map { $self->stderr_rewrite($test{'stderr-replace'}, $_); } @{$test{stderr}} ];
980 if (!defined($test{program})) {
981 $test{program} = $self->{default_program};
984 $self->{test} = \%test;
986 $self->run_hook('mangle_program');
988 if (!$self->parse_postprocess_files()) {
992 return $self->run_hook('post_parse');
996 sub parse_postprocess_files {
1003 for my $file (@{$self->{test}->{file}}) {
1004 $ok = 0 unless ($self->add_file({ source => $file->[1], destination => $file->[0], result => $file->[2] }));
1007 for my $file (@{$self->{test}->{'file-del'}}) {
1008 $ok = 0 unless ($self->add_file({ source => $file->[1], destination => $file->[0], result => undef }));
1011 for my $file (@{$self->{test}->{'file-new'}}) {
1012 $ok = 0 unless ($self->add_file({ source => undef, destination => $file->[0], result => $file->[1] }));
1019 sub print_test_result {
1020 my ($self, $tag, $result, $reason) = @_;
1022 if ($self->{verbose}) {
1023 print "$self->{testname}";
1024 print " ($tag)" if ($tag);
1025 print " -- $result";
1026 print ": $reason" if ($reason);
1032 sub run_comparator {
1033 my ($self, $got, $expected) = @_;
1035 return $self->run_file_proc('compare_by_type', $got, $expected);
1040 my ($self, $src, $dest) = @_;
1042 return $self->run_file_proc('copy_by_type', $src, $dest);
1047 my ($self, $proc, $got, $expected) = @_;
1049 my $ext = ($self->get_extension($got)) . '/' . ($self->get_extension($expected));
1051 if ($self->{variant}) {
1052 if (defined($self->{$proc}->{"$self->{variant}/$ext"})) {
1053 for my $sub (@{$self->{$proc}->{"$self->{variant}/$ext"}}) {
1054 my $ret = $sub->($self, $got, $expected);
1055 return $ret if (defined($ret));
1059 if (defined($self->{$proc}->{$ext})) {
1060 for my $sub (@{$self->{$proc}->{$ext}}) {
1061 my $ret = $sub->($self, $got, $expected);
1062 return $ret if (defined($ret));
1071 my ($self, $hook) = @_;
1077 if (defined($self->{variant_hooks}) && defined($self->{variant_hooks}->{$hook})) {
1078 push @hooks, $self->{variant_hooks}->{$hook};
1080 if (defined($self->{hooks}->{$hook})) {
1081 push @hooks, @{$self->{hooks}->{$hook}};
1084 for my $sub (@hooks) {
1085 unless ($sub->($self, $hook, $self->{variant})) {
1086 $self->warn("hook $hook failed");
1096 my ($str, $srcdir) = @_;
1098 if ($str =~ m/\\/) {
1099 $str =~ s/\\a/\a/gi;
1100 $str =~ s/\\b/\b/gi;
1101 $str =~ s/\\f/\f/gi;
1102 $str =~ s/\\n/\n/gi;
1103 $str =~ s/\\r/\r/gi;
1104 $str =~ s/\\t/\t/gi;
1105 $str =~ s/\\v/\cK/gi;
1108 $str =~ s/\\(.)/$1/g;
1111 if ($srcdir !~ m,^/,) {
1112 $srcdir = "../$srcdir";
1115 if ($str =~ m/^\$srcdir(.*)/) {
1126 for my $precheck (@{$self->{test}->{precheck}}) {
1127 unless (system(@{$precheck}) == 0) {
1128 $self->print_test_result('SKIP', "precheck failed");
1129 $self->end_test('SKIP');
1139 goto &pipein_win32 if $^O eq 'MSWin32' && $self->{test}->{pipein};
1140 my ($stdin, $stdout, $stderr);
1143 my @cmd = ('../' . $self->{test}->{program}, map ({ args_decode($_, $self->{srcdir}); } @{$self->{test}->{args}}));
1145 ### TODO: catch errors?
1148 if ($self->{test}->{pipefile}) {
1149 open(SPLAT, '<', $self->{test}->{pipefile});
1150 my $is_marked = eof SPLAT; # mark used
1151 $pid = open3("<&SPLAT", $stdout, $stderr, @cmd);
1154 $pid = open3($stdin, $stdout, $stderr, @cmd);
1156 $self->{stdout} = [];
1157 $self->{stderr} = [];
1159 if ($self->{test}->{pipein}) {
1161 open($fh, "$self->{test}->{pipein} |");
1162 if (!defined($fh)) {
1163 $self->die("cannot run pipein command [$self->{test}->{pipein}: $!");
1165 while (my $line = <$fh>) {
1172 while (my $line = <$stdout>) {
1173 if ($^O eq 'MSWin32') {
1174 $line =~ s/[\r\n]+$//;
1179 push @{$self->{stdout}}, $line;
1181 my $prg = $self->{test}->{program};
1183 while (my $line = <$stderr>) {
1184 if ($^O eq 'MSWin32') {
1185 $line =~ s/[\r\n]+$//;
1191 $line =~ s/^[^: ]*$prg: //;
1192 if (defined($self->{test}->{'stderr-replace'})) {
1193 $line = $self->stderr_rewrite($self->{test}->{'stderr-replace'}, $line);
1195 push @{$self->{stderr}}, $line;
1200 $self->{exit_status} = $? >> 8;
1203 sub pipein_win32() {
1206 my $cmd = "$self->{test}->{pipein}| ..\\$self->{test}->{program} " . join(' ', map ({ args_decode($_, $self->{srcdir}); } @{$self->{test}->{args}}));
1207 my ($success, $error_message, $full_buf, $stdout_buf, $stderr_buf) = IPC::Cmd::run(command => $cmd);
1209 ### TODO: catch errors?
1212 my @stdout = map { s/[\r\n]+$// } @$stdout_buf;
1213 $self->{stdout} = \@stdout;
1214 $self->{stderr} = [];
1216 my $prg = $self->{test}->{program};
1218 foreach my $line (@$stderr_buf) {
1219 $line =~ s/[\r\n]+$//;
1221 $line =~ s/^[^: ]*$prg: //;
1222 if (defined($self->{test}->{'stderr-replace'})) {
1223 $line = $self->stderr_rewrite($self->{test}->{'stderr-replace'}, $line);
1225 push @{$self->{stderr}}, $line;
1228 $self->{exit_status} = 1;
1230 $self->{exit_status} = 0;
1232 elsif ($error_message =~ /exited with value ([0-9]+)$/) {
1233 $self->{exit_status} = $1 + 0;
1237 sub sandbox_create {
1238 my ($self, $tag) = @_;
1240 $tag = ($tag ? "-$tag" : "");
1241 $self->{sandbox_dir} = "sandbox-$self->{testname}$tag.d$$";
1243 $self->die("sandbox $self->{sandbox_dir} already exists") if (-e $self->{sandbox_dir});
1245 mkdir($self->{sandbox_dir}) or $self->die("cannot create sandbox $self->{sandbox_dir}: $!");
1254 $self->die("internal error: cannot enter sandbox before creating it") unless (defined($self->{sandbox_dir}));
1256 return if ($self->{in_sandbox});
1258 chdir($self->{sandbox_dir}) or $self->die("cannot cd into sandbox $self->{sandbox_dir}: $!");
1260 $self->{in_sandbox} = 1;
1267 return if (!$self->{in_sandbox});
1269 chdir('..') or $self->die("cannot leave sandbox: $!");
1271 $self->{in_sandbox} = 0;
1275 sub sandbox_remove {
1279 remove_tree($self->{sandbox_dir});
1286 my ($self, $tag, $lines) = @_;
1290 for my $line (@$lines) {
1291 if ($line =~ m/^<([a-zA-Z0-9_]*)> (.*)/) {
1297 push @stripped, $line;
1310 if (defined($self->{test}->{touch})) {
1311 for my $args (@{$self->{test}->{touch}}) {
1312 my ($mtime, $fname) = @$args;
1316 unless (open($fh, "> $fname") and close($fh)) {
1317 # TODO: error message
1322 unless (utime($mtime, $mtime, $fname) == 1) {
1323 # TODO: error message
1334 my ($self, $msg) = @_;
1336 print STDERR "$0: $msg\n";
1341 my ($self, $msg) = @_;
1343 $self->warn("$self->{testcase_fname}: $msg");
1347 sub warn_file_line {
1348 my ($self, $msg) = @_;
1350 $self->warn("$self->{testcase_fname}:$.: $msg");
1353 sub stderr_rewrite {
1354 my ($self, $pattern, $line) = @_;
1355 for my $repl (@{$pattern}) {
1356 $line =~ s/$repl->[0]/$repl->[1]/;
1368 for ($i = $j = 0; $i < scalar(@$a) || $j < scalar(@$b);) {
1369 if ($i >= scalar(@$a)) {
1370 print "+$b->[$j]\n";
1373 elsif ($j >= scalar(@$b)) {
1374 print "-$a->[$i]\n";
1377 elsif ($a->[$i] eq $b->[$j]) {
1378 print " $a->[$i]\n";
1383 my ($off_a, $off_b) = find_best_offsets($a, $i, $b, $j);
1384 my ($off_b_2, $off_a_2) = find_best_offsets($b, $j, $a, $i);
1386 if ($off_a + $off_b > $off_a_2 + $off_b_2) {
1391 for (my $off = 0; $off < $off_a; $off++) {
1392 print "-$a->[$i]\n";
1395 for (my $off = 0; $off < $off_b; $off++) {
1396 print "+$b->[$j]\n";
1404 sub find_best_offsets {
1405 my ($a, $i, $b, $j) = @_;
1407 my ($best_a, $best_b);
1409 for (my $off_a = 0; $off_a < (defined($best_a) ? $best_a + $best_b : scalar(@$a) - $i); $off_a++) {
1410 my $off_b = find_entry($a->[$i+$off_a], $b, $j, defined($best_a) ? $best_a + $best_b - $off_a : scalar(@$b) - $j);
1412 next unless (defined($off_b));
1414 if (!defined($best_a) || $best_a + $best_b > $off_a + $off_b) {
1420 if (!defined($best_a)) {
1421 return (scalar(@$a) - $i, scalar(@$b) - $j);
1424 return ($best_a, $best_b);
1428 my ($entry, $array, $start, $max_offset) = @_;
1430 for (my $offset = 0; $offset < $max_offset; $offset++) {
1431 return $offset if ($array->[$start + $offset] eq $entry);