8 use File::Path qw(mkpath remove_tree);
9 use Getopt::Long qw(:config posix_default bundling no_ignore_case);
14 use Data::Dumper qw(Dumper);
16 # NiHTest -- package to run regression tests
17 # Copyright (C) 2002-2016 Dieter Baron and Thomas Klausner
19 # This file is part of ckmame, a program to check rom sets for MAME.
20 # The authors can be contacted at <ckmame@nih.at>
22 # Redistribution and use in source and binary forms, with or without
23 # modification, are permitted provided that the following conditions
25 # 1. Redistributions of source code must retain the above copyright
26 # notice, this list of conditions and the following disclaimer.
27 # 2. Redistributions in binary form must reproduce the above copyright
28 # notice, this list of conditions and the following disclaimer in
29 # the documentation and/or other materials provided with the
31 # 3. The names of the authors may not be used to endorse or promote
32 # products derived from this software without specific prior
35 # THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
36 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
37 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
38 # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
39 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
40 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
41 # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
42 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
43 # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
44 # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
45 # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
50 # TESTNAME.test: test scenario
53 # Lines beginning with # are comments.
55 # The following commands are recognized; return and args must
56 # appear exactly once, the others are optional.
59 # run program with command line arguments ARGS
62 # description of what test is for
64 # features FEATURE ...
65 # only run test if all FEATUREs are present, otherwise skip it.
68 # copy file IN as TEST, compare against OUT after program run.
71 # copy file IN as TEST, check that it is removed by program.
74 # check that file TEST is created by program and compare
78 # create directory NAME with permissions MODE.
80 # pipein COMMAND ARGS ...
81 # pipe output of running COMMAND to program's stdin.
84 # pre-load LIBRARY before running program.
87 # run PRG instead of ckmame.
90 # RET is the expected exit code
93 # set environment variable VAR to VALUE.
96 # program is expected to produce the error message TEXT. If
97 # multiple stderr commands are used, the messages are
98 # expected in the order given.
100 # stderr-replace REGEX REPLACEMENT
101 # run regex replacement over expected and got stderr output.
104 # program is expected to print TEXT to stdout. If multiple
105 # stdout commands are used, the messages are expected in
109 # set last modified timestamp of FILE to MTIME (seconds since epoch).
110 # If FILE doesn't exist, an empty file is created.
113 # set ulimit -C to VALUE while running the program.
116 # runtest uses the following exit codes:
120 # 77: test was skipped
122 # environment variables:
123 # RUN_GDB: if set, run gdb on program in test environment
124 # KEEP_BROKEN: if set, don't delete test environment if test failed
125 # NO_CLEANUP: if set, don't delete test environment
126 # SETUP_ONLY: if set, exit after creating test environment
127 # VERBOSE: if set, be more verbose (e. g., output diffs)
137 my $class = UNIVERSAL::isa ($_[0], __PACKAGE__) ? shift : __PACKAGE__;
138 my $self = bless {}, $class;
142 $self->{default_program} = $opts->{default_program};
143 $self->{zipcmp} = $opts->{zipcmp} // 'zipcmp';
144 $self->{zipcmp_flags} = $opts->{zipcmp_flags};
146 $self->{directives} = {
147 args => { type => 'string...', once => 1, required => 1 },
148 description => { type => 'string', once => 1 },
149 features => { type => 'string...', once => 1 },
150 file => { type => 'string string string' },
151 'file-del' => { type => 'string string' },
152 'file-new' => { type => 'string string' },
153 mkdir => { type => 'string string' },
154 pipein => { type => 'string', once => 1 },
155 preload => { type => 'string', once => 1 },
156 program => { type => 'string', once => 1 },
157 'return' => { type => 'int', once => 1, required => 1 },
158 setenv => { type => 'string string' },
159 stderr => { type => 'string' },
160 'stderr-replace' => { type => 'string string' },
161 stdout => { type => 'string' },
162 touch => { type => 'int string' },
163 ulimit => { type => 'char string' }
166 $self->{compare_by_type} = {};
167 $self->{copy_by_type} = {};
170 $self->add_comparator('zip/zip', \&comparator_zip);
172 $self->{srcdir} = $opts->{srcdir} // $ENV{srcdir};
174 if (!defined($self->{srcdir}) || $self->{srcdir} eq '') {
175 $self->{srcdir} = `sed -n 's/^srcdir = \(.*\)/\1/p' Makefile`;
176 chomp($self->{srcdir});
179 $self->{in_sandbox} = 0;
181 $self->{verbose} = $ENV{VERBOSE};
182 $self->{keep_broken} = $ENV{KEEP_BROKEN};
183 $self->{no_cleanup} = $ENV{NO_CLEANUP};
184 $self->{setup_only} = $ENV{SETUP_ONLY};
191 my ($self, $ext, $sub) = @_;
193 return $self->add_file_proc('compare_by_type', $ext, $sub);
198 my ($self, $ext, $sub) = @_;
200 return $self->add_file_proc('copy_by_type', $ext, $sub);
205 my ($self, $name, $def) = @_;
207 if (exists($self->{directives}->{$name})) {
208 $self->die("directive $name already defined");
211 # TODO: validate $def
213 $self->{directives}->{$name} = $def;
220 my ($self, $proc, $ext, $sub) = @_;
222 $self->{$proc}->{$ext} = [] unless (defined($self->{$proc}->{$ext}));
223 unshift @{$self->{$proc}->{$ext}}, $sub;
230 my ($self, $hook, $sub) = @_;
232 $self->{hooks}->{$hook} = [] unless (defined($self->{hooks}->{$hook}));
233 push @{$self->{hooks}->{$hook}}, $sub;
240 my ($self, @results) = @_;
244 for my $r (@results) {
245 if ($r eq 'ERROR' || ($r eq 'FAIL' && $result ne 'ERROR')) {
250 $self->end_test($result);
255 my ($self, @argv) = @_;
259 $self->end($self->runtest());
264 my ($self, $tag) = @_;
267 $ENV{LC_CTYPE} = "C";
268 $self->sandbox_create($tag);
269 $self->sandbox_enter();
272 $ok &= $self->copy_files();
273 $ok &= $self->run_hook('post_copy_files');
274 $ok &= $self->touch_files();
275 $ok &= $self->run_hook('prepare_sandbox');
276 return 'ERROR' unless ($ok);
278 if ($self->{setup_only}) {
279 $self->sandbox_leave();
283 for my $env (@{$self->{test}->{'setenv'}}) {
284 $ENV{$env->[0]} = $env->[1];
286 if (defined($self->{test}->{'preload'})) {
287 $ENV{LD_PRELOAD} = cwd() . "/../.libs/$self->{test}->{'preload'}";
290 $self->run_program();
292 for my $env (@{$self->{test}->{'setenv'}}) {
293 delete ${ENV{$env->[0]}};
295 if (defined($self->{test}->{'preload'})) {
296 delete ${ENV{LD_PRELOAD}};
299 if ($self->{test}->{stdout}) {
300 $self->{expected_stdout} = [ @{$self->{test}->{stdout}} ];
303 $self->{expected_stdout} = [];
305 if ($self->{test}->{stderr}) {
306 $self->{expected_stderr} = [ @{$self->{test}->{stderr}} ];
309 $self->{expected_stderr} = [];
312 $self->run_hook('post_run_program');
316 if ($self->{exit_status} != ($self->{test}->{return} // 0)) {
317 push @failed, 'exit status';
318 if ($self->{verbose}) {
319 print "Unexpected exit status:\n";
320 print "-" . ($self->{test}->{return} // 0) . "\n+$self->{exit_status}\n";
324 if (!$self->compare_arrays($self->{expected_stdout}, $self->{stdout}, 'output')) {
325 push @failed, 'output';
327 if (!$self->compare_arrays($self->{expected_stderr}, $self->{stderr}, 'error output')) {
328 push @failed, 'error output';
330 if (!$self->compare_files()) {
331 push @failed, 'files';
334 $self->{failed} = \@failed;
336 $self->run_hook('checks');
338 my $result = scalar(@{$self->{failed}}) == 0 ? 'PASS' : 'FAIL';
340 $self->sandbox_leave();
341 if (!($self->{no_cleanup} || ($self->{keep_broken} && $result eq 'FAIL'))) {
342 $self->sandbox_remove();
345 $self->print_test_result($tag, $result, join ', ', @{$self->{failed}});
352 my ($self, @argv) = @_;
354 my @save_argv = @ARGV;
357 'help|h' => \my $help,
358 'keep-broken' => \$self->{keep_broken},
359 'no-cleanup' => \$self->{no_cleanup},
360 # 'run-gdb' => \$self->{run_gdb},
361 'setup-only' => \$self->{setup_only},
362 'verbose|v' => \$self->{verbose}
367 if (!$ok || scalar(@argv) != 1 || $help) {
368 print STDERR "Usage: $0 [-hv] [--keep-broken] [--no-cleanup] [--setup-only] testcase\n";
372 my $testcase = shift @argv;
374 $testcase .= '.test' unless ($testcase =~ m/\.test$/);
376 my $testcase_file = $self->find_file($testcase);
378 $self->die("cannot find test case $testcase") unless ($testcase_file);
380 $testcase =~ s,^(?:.*/)?([^/]*)\.test$,$1,;
381 $self->{testname} = $testcase;
383 $self->die("error in test case definition") unless $self->parse_case($testcase_file);
385 $self->check_features_requirement() if ($self->{test}->{features});
387 $self->end_test('SKIP') if ($self->{test}->{preload} && $^O eq 'darwin');
396 my ($self, $file) = @_;
398 if (defined($self->{files}->{$file->{destination}})) {
399 $self->warn("duplicate specification for input file $file->{destination}");
403 $self->{files}->{$file->{destination}} = $file;
409 sub check_features_requirement() {
419 my ($self, $got, $expected) = @_;
421 my @args = ($self->{zipcmp}, $self->{verbose} ? '-pv' : '-pq');
422 push @args, $self->{zipcmp_flags} if ($self->{zipcmp_flags});
423 push @args, ($expected, $got);
425 my $ret = system(@args);
431 sub compare_arrays() {
432 my ($self, $a, $b, $tag) = @_;
436 if (scalar(@$a) != scalar(@$b)) {
440 for (my $i = 0; $i < scalar(@$a); $i++) {
441 if ($a->[$i] ne $b->[$i]) {
448 if (!$ok && $self->{verbose}) {
449 print "Unexpected $tag:\n";
450 print "--- expected\n+++ got\n";
461 open my $fha, "< $a";
462 open my $fhb, "< $b";
465 BYTE: while (!eof $fha && !eof $fhb) {
466 if (getc $fha ne getc $fhb) {
471 $result = 1 if eof $fha != eof $fhb;
477 sub compare_file($$$) {
478 my ($self, $got, $expected) = @_;
480 my $real_expected = $self->find_file($expected);
481 unless ($real_expected) {
482 $self->warn("cannot find expected result file $expected");
486 my $ok = $self->run_comparator($got, $real_expected);
490 if ($self->{verbose}) {
491 $ret = system('diff', '-u', $real_expected, $got);
494 $ret = file_cmp($real_expected, $got);
503 sub compare_files() {
508 opendir(my $ls, '.');
512 my @files_got = grep { -f } readdir($ls);
515 @files_got = sort @files_got;
516 my @files_should = ();
518 for my $file (sort keys %{$self->{files}}) {
519 push @files_should, $file if ($self->{files}->{$file}->{result} || $self->{files}->{$file}->{ignore});
522 $self->{files_got} = \@files_got;
523 $self->{files_should} = \@files_should;
525 unless ($self->run_hook('post_list_files')) {
529 $ok = $self->compare_arrays($self->{files_should}, $self->{files_got}, 'files');
531 for my $file (@{$self->{files_got}}) {
532 my $file_def = $self->{files}->{$file};
533 next unless ($file_def && $file_def->{result});
535 $ok &= $self->compare_file($file, $file_def->{result});
547 for my $filename (sort keys %{$self->{files}}) {
548 my $file = $self->{files}->{$filename};
549 next unless ($file->{source});
551 my $src = $self->find_file($file->{source});
553 $self->warn("cannot find input file $file->{source}");
558 if ($file->{destination} =~ m,/,) {
559 my $dir = $file->{destination};
566 my $this_ok = $self->run_copier($src, $file->{destination});
567 if (defined($this_ok)) {
571 unless (copy($src, $file->{destination})) {
572 $self->warn("cannot copy $src to $file->{destination}: $!");
578 if (defined($self->{test}->{mkdir})) {
579 for my $dir_spec (@{$self->{test}->{mkdir}}) {
580 my ($mode, $dir) = @$dir_spec;
582 unless (mkdir($dir, oct($mode))) {
583 $self->warn("cannot create directory $dir: $!");
590 $self->die("failed to copy input files") unless ($ok);
595 my ($self, $msg) = @_;
597 print STDERR "$0: $msg\n" if ($msg);
599 $self->end_test('ERROR');
604 my ($self, $status) = @_;
606 my $exit_code = $EXIT_CODES{$status} // $EXIT_CODES{ERROR};
608 $self->exit($exit_code);
614 my ($self, $status) = @_;
622 my ($self, $fname) = @_;
624 for my $dir (('', "$self->{srcdir}/")) {
625 my $f = "$dir$fname";
626 $f = "../$f" if ($self->{in_sandbox} && $dir !~ m,^/,);
628 return $f if (-f $f);
636 my ($self, $fname) = @_;
651 my ($self, $type, $str) = @_;
653 if ($type eq 'string...') {
657 if ($str =~ m/^\"/) {
658 unless ($str =~ m/^\"([^\"]*)\"\s*(.*)/) {
659 $self->warn_file_line("unclosed quote in [$str]");
666 $str =~ m/^(\S+)\s*(.*)/;
674 elsif ($type =~ m/(\s|\.\.\.$)/) {
676 if ($type =~ m/(.*)\.\.\.$/) {
680 my @types = split /\s+/, $type;
681 my @strs = split /\s+/, $str;
683 for (my $i = scalar(@types) - 1; $i >= 0; $i--) {
684 last unless ($types[$i] =~ m/(.*)\?$/);
689 if ($ellipsis && $optional > 0) {
690 # TODO: check this when registering a directive
691 $self->warn_file_line("can't use ellipsis together with optional arguments");
694 if (!$ellipsis && (scalar(@strs) < scalar(@types) - $optional || scalar(@strs) > scalar(@types))) {
695 my $expected = scalar(@types);
697 $expected = ($expected - $optional) . "-$expected";
699 $self->warn_file_line("expected $expected arguments, got " . (scalar(@strs)));
705 my $n = scalar(@types);
706 for (my $i=0; $i<scalar(@strs); $i++) {
707 my $val = $self->parse_args(($i >= $n ? $types[$n-1] : $types[$i]), $strs[$i]);
708 return undef unless (defined($val));
715 if ($type eq 'string') {
718 elsif ($type eq 'int') {
719 if ($str !~ m/^\d+$/) {
720 $self->warn_file_line("illegal int [$str]");
725 elsif ($type eq 'char') {
726 if ($str !~ m/^.$/) {
727 $self->warn_file_line("illegal char [$str]");
733 $self->warn_file_line("unknown type $type");
741 my ($self, $fname) = @_;
745 open TST, "< $fname" or $self->die("cannot open test case $fname: $!");
747 $self->{testcase_fname} = $fname;
751 while (my $line = <TST>) {
754 next if ($line =~ m/^\#/);
756 unless ($line =~ m/(\S*)(?:\s(.*))?/) {
757 $self->warn_file_line("cannot parse line $line");
761 my ($cmd, $argstring) = ($1, $2//"");
763 my $def = $self->{directives}->{$cmd};
766 $self->warn_file_line("unknown directive $cmd in test file");
771 my $args = $self->parse_args($def->{type}, $argstring);
773 unless (defined($args)) {
779 if (defined($test{$cmd})) {
780 $self->warn_file_line("directive $cmd appeared twice in test file");
785 $test{$cmd} = [] unless (defined($test{$cmd}));
786 push @{$test{$cmd}}, $args;
792 return undef unless ($ok);
794 for my $cmd (sort keys %test) {
795 if ($self->{directives}->{$cmd}->{required} && !defined($test{$cmd})) {
796 $self->warn_file("required directive $cmd missing in test file");
801 return undef unless ($ok);
803 if (defined($test{'stderr-replace'}) && defined($test{stderr})) {
804 $test{stderr} = [ map { $self->stderr_rewrite($test{'stderr-replace'}, $_); } @{$test{stderr}} ];
807 if (!defined($test{program})) {
808 $test{program} = $self->{default_program};
811 $self->{test} = \%test;
813 $self->run_hook('mangle_program');
815 if (!$self->parse_postprocess_files()) {
819 return $self->run_hook('post_parse');
823 sub parse_postprocess_files {
830 for my $file (@{$self->{test}->{file}}) {
831 $ok = 0 unless ($self->add_file({ source => $file->[1], destination => $file->[0], result => $file->[2] }));
834 for my $file (@{$self->{test}->{'file-del'}}) {
835 $ok = 0 unless ($self->add_file({ source => $file->[1], destination => $file->[0], result => undef }));
838 for my $file (@{$self->{test}->{'file-new'}}) {
839 $ok = 0 unless ($self->add_file({ source => undef, destination => $file->[0], result => $file->[1] }));
846 sub print_test_result {
847 my ($self, $tag, $result, $reason) = @_;
849 if ($self->{verbose}) {
850 print "$self->{testname}";
851 print " ($tag)" if ($tag);
853 print ": $reason" if ($reason);
860 my ($self, $got, $expected) = @_;
862 return $self->run_file_proc('compare_by_type', $got, $expected);
867 my ($self, $src, $dest) = @_;
869 return $self->run_file_proc('copy_by_type', $src, $dest);
874 my ($self, $proc, $got, $expected) = @_;
876 my $ext = ($self->get_extension($got)) . '/' . ($self->get_extension($expected));
878 if (defined($self->{$proc}->{$ext})) {
879 for my $sub (@{$self->{$proc}->{$ext}}) {
880 my $ret = $sub->($self, $got, $expected);
881 return $ret if (defined($ret));
890 my ($self, $hook) = @_;
894 if (defined($self->{hooks}->{$hook})) {
895 for my $sub (@{$self->{hooks}->{$hook}}) {
896 unless ($sub->($self, $hook)) {
897 $self->warn("hook $hook failed");
908 my ($str, $srcdir) = @_;
917 $str =~ s/\\v/\cK/gi;
920 $str =~ s/\\(.)/$1/g;
923 if ($srcdir !~ m,^/,) {
924 $srcdir = "../$srcdir";
927 if ($str =~ m/^\$srcdir(.*)/) {
937 goto &pipein_win32 if $^O eq 'MSWin32' && $self->{test}->{pipein};
938 my ($stdin, $stdout, $stderr);
941 my @cmd = ('../' . $self->{test}->{program}, map ({ args_decode($_, $self->{srcdir}); } @{$self->{test}->{args}}));
943 ### TODO: catch errors?
945 my $pid = open3($stdin, $stdout, $stderr, @cmd);
947 $self->{stdout} = [];
948 $self->{stderr} = [];
950 if ($self->{test}->{pipein}) {
952 open($fh, "$self->{test}->{pipein} |");
954 $self->die("cannot run pipein command [$self->{test}->{pipein}: $!");
956 while (my $line = <$fh>) {
963 while (my $line = <$stdout>) {
964 if ($^O eq 'MSWin32') {
965 $line =~ s/[\r\n]+$//;
970 push @{$self->{stdout}}, $line;
972 my $prg = $self->{test}->{program};
974 while (my $line = <$stderr>) {
975 if ($^O eq 'MSWin32') {
976 $line =~ s/[\r\n]+$//;
982 $line =~ s/^[^: ]*$prg: //;
983 if (defined($self->{test}->{'stderr-replace'})) {
984 $line = $self->stderr_rewrite($self->{test}->{'stderr-replace'}, $line);
986 push @{$self->{stderr}}, $line;
991 $self->{exit_status} = $? >> 8;
997 my $cmd = "$self->{test}->{pipein}| ..\\$self->{test}->{program} " . join(' ', map ({ args_decode($_, $self->{srcdir}); } @{$self->{test}->{args}}));
998 my ($success, $error_message, $full_buf, $stdout_buf, $stderr_buf) = IPC::Cmd::run(command => $cmd);
1000 ### TODO: catch errors?
1003 my @stdout = map { s/[\r\n]+$// } @$stdout_buf;
1004 $self->{stdout} = \@stdout;
1005 $self->{stderr} = [];
1007 my $prg = $self->{test}->{program};
1009 foreach my $line (@$stderr_buf) {
1010 $line =~ s/[\r\n]+$//;
1012 $line =~ s/^[^: ]*$prg: //;
1013 if (defined($self->{test}->{'stderr-replace'})) {
1014 $line = $self->stderr_rewrite($self->{test}->{'stderr-replace'}, $line);
1016 push @{$self->{stderr}}, $line;
1019 $self->{exit_status} = 1;
1021 $self->{exit_status} = 0;
1023 elsif ($error_message =~ /exited with value ([0-9]+)$/) {
1024 $self->{exit_status} = $1 + 0;
1028 sub sandbox_create {
1029 my ($self, $tag) = @_;
1031 $tag = ($tag ? "-$tag" : "");
1032 $self->{sandbox_dir} = "sandbox-$self->{testname}$tag.d$$";
1034 $self->die("sandbox $self->{sandbox_dir} already exists") if (-e $self->{sandbox_dir});
1036 mkdir($self->{sandbox_dir}) or $self->die("cannot create sandbox $self->{sandbox_dir}: $!");
1045 $self->die("internal error: cannot enter sandbox before creating it") unless (defined($self->{sandbox_dir}));
1047 return if ($self->{in_sandbox});
1049 chdir($self->{sandbox_dir}) or $self->die("cant cd into sandbox $self->{sandbox_dir}: $!");
1051 $self->{in_sandbox} = 1;
1058 return if (!$self->{in_sandbox});
1060 chdir('..') or $self->die("cannot leave sandbox: $!");
1062 $self->{in_sandbox} = 0;
1066 sub sandbox_remove {
1070 remove_tree($self->{sandbox_dir});
1081 if (defined($self->{test}->{touch})) {
1082 for my $args (@{$self->{test}->{touch}}) {
1083 my ($mtime, $fname) = @$args;
1087 unless (open($fh, "> $fname") and close($fh)) {
1088 # TODO: error message
1093 unless (utime($mtime, $mtime, $fname) == 1) {
1094 # TODO: error message
1105 my ($self, $msg) = @_;
1107 print STDERR "$0: $msg\n";
1112 my ($self, $msg) = @_;
1114 $self->warn("$self->{testcase_fname}: $msg");
1118 sub warn_file_line {
1119 my ($self, $msg) = @_;
1121 $self->warn("$self->{testcase_fname}:$.: $msg");
1124 sub stderr_rewrite {
1125 my ($self, $pattern, $line) = @_;
1126 for my $repl (@{$pattern}) {
1127 $line =~ s/$repl->[0]/$repl->[1]/;
1139 for ($i = $j = 0; $i < scalar(@$a) || $j < scalar(@$b);) {
1140 if ($i >= scalar(@$a)) {
1141 print "+$b->[$j]\n";
1144 elsif ($j >= scalar(@$b)) {
1145 print "-$a->[$i]\n";
1148 elsif ($a->[$i] eq $b->[$j]) {
1149 print " $a->[$i]\n";
1154 my ($off_a, $off_b) = find_best_offsets($a, $i, $b, $j);
1155 my ($off_b_2, $off_a_2) = find_best_offsets($b, $j, $a, $i);
1157 if ($off_a + $off_b > $off_a_2 + $off_b_2) {
1162 for (my $off = 0; $off < $off_a; $off++) {
1163 print "-$a->[$i]\n";
1166 for (my $off = 0; $off < $off_b; $off++) {
1167 print "+$b->[$j]\n";
1175 sub find_best_offsets {
1176 my ($a, $i, $b, $j) = @_;
1178 my ($best_a, $best_b);
1180 for (my $off_a = 0; $off_a < (defined($best_a) ? $best_a + $best_b : scalar(@$a) - $i); $off_a++) {
1181 my $off_b = find_entry($a->[$i+$off_a], $b, $j, defined($best_a) ? $best_a + $best_b - $off_a : scalar(@$b) - $j);
1183 next unless (defined($off_b));
1185 if (!defined($best_a) || $best_a + $best_b > $off_a + $off_b) {
1191 if (!defined($best_a)) {
1192 return (scalar(@$a) - $i, scalar(@$b) - $j);
1195 return ($best_a, $best_b);
1199 my ($entry, $array, $start, $max_offset) = @_;
1201 for (my $offset = 0; $offset < $max_offset; $offset++) {
1202 return $offset if ($array->[$start + $offset] eq $entry);