File Coverage

lib/Log/Reproducible.pm
Criterion Covered Total %
statement 102 398 25.6
branch 20 114 17.5
condition 0 12 0.0
subroutine 24 55 43.6
pod n/a
total 146 579 25.2


line stmt bran cond sub pod time code
1             package Log::Reproducible;
2 1     1   41753 use strict;
  1         1  
  1         29  
3 1     1   4 use warnings;
  1         1  
  1         23  
4 1     1   3 use Cwd;
  1         5  
  1         44  
5 1     1   4 use File::Path 'make_path';
  1         1  
  1         36  
6 1     1   4 use File::Basename;
  1         1  
  1         46  
7 1     1   4 use File::Spec;
  1         2  
  1         12  
8 1     1   3 use File::Temp ();
  1         1  
  1         13  
9 1     1   439 use IPC::Open3;
  1         2137  
  1         46  
10 1     1   441 use List::MoreUtils 'first_index';
  1         730  
  1         64  
11 1     1   425 use POSIX qw(strftime difftime ceil floor);
  1         4976  
  1         4  
12 1     1   813 use Config;
  1         1  
  1         36  
13 1     1   431 use YAML::Old qw(Dump LoadFile); # YAML::XS & YAML::Syck aren't working properly
  1         5088  
  1         605  
14              
15             # TODO: Add tests for conflicting module checker
16             # TODO: Add verbose (or silent) option
17             # TODO: Standalone script that can be used upstream of any command line functions
18             # TODO: Auto-build README using POD
19              
20             our $VERSION = '0.12.1_1';
21              
22             =head1 NAME
23            
24             Log::Reproducible - Effortless record-keeping and enhanced reproducibility. Set it and forget it... until you need it!
25            
26             =head1 AUTHOR
27            
28             Michael F. Covington <mfcovington@gmail.com>
29            
30             =cut
31              
32             sub _check_for_known_conflicting_modules {
33 1     1   2     my @known_conflicts = @_;
34              
35             # Only check for conflicts if Module::Loaded is available (i.e. >= 5.9.4)
36 1     1   425     eval "use Module::Loaded";
  1         480  
  1         36  
  1         51  
37 1 50       5     return if $@;
38 1         4     require Module::Loaded;
39              
40 1         1     my @loaded_conflicts;
41 1         3     for (@known_conflicts) {
42 2 50       10         push @loaded_conflicts, $_ if defined is_loaded($_);
43                 }
44              
45 1 50       8     if (@loaded_conflicts) {
46 0         0         my $conflict_warning = <<EOF;
47            
48             KNOWN CONFLICT WARNING:
49 0         0 A module that accesses '\@ARGV' has been loaded before @{[__PACKAGE__]}.
  0         0  
50 0         0 This module is known to create a conflict with @{[__PACKAGE__]} functionality.
51             To avoid any conflicts, we strongly recommended changing your script such
52             that @{[__PACKAGE__]} is imported before the following module(s):
53            
54             EOF
55 0         0         $conflict_warning .= " $_\n" for sort @loaded_conflicts;
56 0         0         print STDERR "$conflict_warning\n";
57                 }
58             }
59              
60             sub _check_for_potentially_conflicting_modules {
61 1 50   1   1     my $code = do { open my $fh, '<', $0 or return; local $/; <$fh> };
  1         28  
  1         3  
  1         20  
62 1         1     my ($code_to_test) = $code =~ /(\A .*?) use \s+ @{[__PACKAGE__]}/sx;
  1         34  
63 1 50       3340     return unless defined $code_to_test; # Required for standalone perlr
64 0         0     my ( $temp_fh, $temp_filename ) = File::Temp::tempfile();
65 0         0     print $temp_fh $code_to_test;
66              
67 0         0     local ( *CIN, *COUT, *CERR );
68 0         0     my $perl = $Config{perlpath};
69 0         0     my $cmd = "$perl -MO=Xref,-r $temp_filename";
70 0         0     my $pid = open3( \*CIN, \*COUT, \*CERR, $cmd );
71              
72 0         0     my $re
73                     = '((?:'
74 0         0         . join( '|' => map { /^(?:\.[\\\/]?)?(.*)$/; "\Q$1" } @INC )
  0         0  
75                     . ')[\\\/]?\S+?)(?:\.\S+)?\s+(\S+)';
76 0         0     my %argv_modules;
77              
78 0         0     for (<COUT>) {
79 0 0       0         next unless /\@\s+ARGV/;
80 0         0         my ( $module_path, $object_path ) = /$re/;
81              
82             # Get overlap between end of module path and beginning of object path
83 0         0         $module_path =~ s|[\\\/]|::|g;
84 0         0         my @object_path_steps = split /::/, $object_path;
85 0         0         for my $step ( 0 .. $#object_path_steps ) {
86 0         0             my $module_name = join "::", @object_path_steps[ 0 .. $step ];
87 0 0       0             if ( $module_path =~ /$module_name$/ ) {
88 0         0                 $argv_modules{$module_name} = 1;
89 0         0                 last;
90                         }
91                     }
92                 }
93              
94 0         0     waitpid $pid, 0;
95 0 0       0     File::Temp::unlink0( $temp_fh, $temp_filename )
96                     or warn "Error unlinking file $temp_filename safely";
97              
98 0         0     my @warn_modules = sort keys %argv_modules;
99              
100 0 0       0     if (@warn_modules) {
101 0         0         my $conflict_warning = <<EOF;
102            
103             POTENTIAL CONFLICT WARNING:
104 0         0 A module that accesses '\@ARGV' has been loaded before @{[__PACKAGE__]}.
  0         0  
105             To avoid potential conflicts, we recommended changing your script such
106             that @{[__PACKAGE__]} is imported before the following module(s):
107            
108             EOF
109 0         0         $conflict_warning .= " $_\n" for sort @warn_modules;
110 0         0         print STDERR "$conflict_warning\n";
111                 }
112             }
113              
114             BEGIN {
115 1     1   2     _check_for_known_conflicting_modules( '', '' ); # Add when discovered
116 1         2     _check_for_potentially_conflicting_modules();
117             }
118              
119             sub import {
120 0     0   0     my ( $pkg, $custom_repro_opts ) = @_;
121 0         0     _reproducibility_is_important($custom_repro_opts);
122             }
123              
124             sub _reproducibility_is_important {
125 0     0   0     my $custom_repro_opts = shift;
126              
127 0         0     my $repro_opts = _parse_custom_repro_opts($custom_repro_opts);
128 0         0     my $dir = $$repro_opts{dir};
129 0         0     my $full_prog_name = $0;
130 0         0     my $argv_current = \@ARGV;
131 0         0     _set_dir( \$dir, $$repro_opts{reprodir}, $argv_current );
132 0         0     make_path $dir;
133              
134 0         0     my $current = {};
135 0         0     my ( $prog, $prog_dir )
136                     = _parse_command( $current, $full_prog_name, $$repro_opts{repronote},
137                     $argv_current );
138 0         0     my ( $repro_file, $start ) = _set_repro_file( $current, $dir, $prog );
139 0         0     _get_current_state( $current, $prog_dir );
140              
141 0         0     my $reproduce_opt = $$repro_opts{reproduce};
142 0         0     my $warnings = [];
143 0 0       0     if ( $$current{'COMMAND'} =~ /\s-?-$reproduce_opt\s+(\S+)/ ) {
144 0         0         my $old_repro_file = $1;
145 0         0         $$current{'COMMAND'}
146                         = _reproduce_cmd( $current, $prog, $old_repro_file, $repro_file,
147                         $dir, $argv_current, $warnings, $start );
148                 }
149 0         0     _archive_cmd( $current, $repro_file );
150 0         0     _exit_code( $repro_file, $start );
151             }
152              
153             sub _parse_custom_repro_opts {
154 0     0   0     my $custom_repro_opts = shift;
155              
156 0         0     my %default_opts = (
157                     dir => undef,
158                     reprodir => 'reprodir',
159                     reproduce => 'reproduce',
160                     repronote => 'repronote'
161                 );
162              
163 0 0       0     if ( !defined $custom_repro_opts ) {
    0          
164 0         0         return \%default_opts;
165                 }
166                 elsif ( ref($custom_repro_opts) eq 'HASH' ) {
167 0         0         for my $opt ( keys %default_opts ) {
168 0 0       0             $$custom_repro_opts{$opt} = $default_opts{$opt}
169                             unless exists $$custom_repro_opts{$opt};
170                     }
171 0         0         return $custom_repro_opts;
172                 }
173                 else {
174 0         0         $default_opts{dir} = $custom_repro_opts;
175 0         0         return \%default_opts;
176                 }
177             }
178              
179             sub _set_dir {
180 4     4   2711     my ( $dir, $reprodir_opt, $argv_current ) = @_;
181 4         14     my $cli_dir = _get_repro_arg( $reprodir_opt, $argv_current );
182              
183 4 100       19     if ( defined $cli_dir ) {
    100          
184 1         3         $$dir = $cli_dir;
185                 }
186                 elsif ( !defined $$dir ) {
187 2 100       7         if ( defined $ENV{REPRO_DIR} ) {
188 1         4             $$dir = $ENV{REPRO_DIR};
189                     }
190                     else {
191 1         10             my $cwd = getcwd;
192 1         5             $$dir = "$cwd/repro-archive";
193                     }
194                 }
195             }
196              
197             sub _parse_command {
198 1     1   1344     my ( $current, $full_prog_name, $repronote_opt, $argv_current ) = @_;
199 1         3     $$current{'NOTE'} = _get_repro_arg( $repronote_opt, $argv_current );
200 1         3     for (@$argv_current) {
201 6 100       20         $_ = "'$_'" if /\s/;
202                 }
203 1         34     my ( $prog, $prog_dir ) = fileparse $full_prog_name;
204 1         5     $$current{'COMMAND'} = join " ", $prog, @$argv_current;
205 1         3     return $prog, $prog_dir;
206             }
207              
208             sub _get_repro_arg {
209 7     7   1381     my ( $repro_opt, $argv_current ) = @_;
210 7         8     my $repro_arg;
211 7     4   47     my $argv_idx = first_index { $_ =~ /^-?-$repro_opt$/ } @$argv_current;
  4         94  
212 7 100       36     if ( $argv_idx > -1 ) {
213 4         8         $repro_arg = $$argv_current[ $argv_idx + 1 ];
214 4         9         splice @$argv_current, $argv_idx, 2;
215                 }
216 7         19     return $repro_arg;
217             }
218              
219             sub _set_repro_file {
220 0     0   0     my ( $current, $dir, $prog ) = @_;
221 0         0     my $start = _now();
222 0         0     $$current{'STARTED'} = $$start{'when'};
223 0         0     my $repro_file = "$dir/rlog-$prog-" . $$start{'timestamp'};
224 0         0     _is_file_unique( \$repro_file );
225 0         0     return $repro_file, $start;
226             }
227              
228             sub _now {
229 1     1   842111     my %now;
230 1         134     my @localtime = localtime;
231 1         57     $now{'timestamp'} = strftime "%Y%m%d.%H%M%S", @localtime;
232 1         29     $now{'when'} = strftime "at %H:%M:%S on %a %b %d, %Y", @localtime;
233 1         4     $now{'seconds'} = time();
234 1         10     return \%now;
235             }
236              
237             sub _is_file_unique {
238 0     0   0     my $file = shift;
239 0 0       0     return if !-e $$file;
240              
241 0         0     my ( $base, $counter ) = $$file =~ /(.+\d{8}\.\d{6})(?:\.(\d{3}$))?/;
242 0 0       0     if ( defined $counter ) {
243 0         0         $counter++;
244                 }
245                 else {
246 0         0         $counter = "001";
247                 }
248 0         0     $$file = "$base.$counter";
249 0         0     _is_file_unique($file);
250             }
251              
252             sub _reproduce_cmd {
253 0     0   0     my ( $current, $prog, $old_repro_file, $repro_file, $dir, $argv_current,
254                     $warnings, $start )
255                     = @_;
256              
257 0         0     my ( $raw_archived_state, $has_been_reproduced )
258                     = LoadFile($old_repro_file);
259              
260             # Convert array of single-key hashes to single multi-key hash
261 0         0     my %archived_state;
262 0         0     for (@$raw_archived_state) {
263 0         0         my (@keys) = keys %$_;
264 0 0       0         die "Something is wrong..." if scalar @keys != 1;
265 0         0         $archived_state{ $keys[0] } = $$_{ $keys[0] };
266                 }
267              
268 0         0     my $cmd = $archived_state{'COMMAND'};
269              
270 0         0     my ( $archived_prog, @archived_argv )
271                     = $cmd =~ /((?:\'[^']+\')|(?:\"[^"]+\")|(?:\S+))/g;
272 0         0     @$argv_current = @archived_argv;
273 0         0     print STDERR "Reproducing archive: $old_repro_file\n";
274 0         0     print STDERR "Reproducing command: $cmd\n";
275 0         0     _validate_prog_name( $archived_prog, $prog, @archived_argv );
276 0         0     _validate_archived_info( \%archived_state, $current, $warnings );
277 0         0     my $diff_file
278                     = _summarize_warnings( $warnings, $old_repro_file, $repro_file, $dir,
279                     $prog, $start );
280 0         0     _add_warnings_to_current_state( $current, $warnings, $old_repro_file,
281                     $diff_file );
282 0         0     _log_reproduction_event( $old_repro_file, $repro_file, $current,
283                     $has_been_reproduced );
284 0         0     return $cmd;
285             }
286              
287             sub _log_reproduction_event {
288 0     0   0     my ( $old_repro_file, $new_repro_file, $current, $has_been_reproduced )
289                     = @_;
290              
291 0 0       0     open my $old_repro_fh, ">>", $old_repro_file
292                     or die "Cannot open $old_repro_file for appending: $!";
293              
294 0 0       0     print $old_repro_fh "---\n- REPRODUCED AS:\n"
295                     unless defined $has_been_reproduced;
296 0         0     print $old_repro_fh " - $new_repro_file $$current{'STARTED'}\n";
297              
298 0         0     close $old_repro_fh;
299             }
300              
301             sub _archive_cmd {
302 0     0   0     my ( $current, $repro_file ) = @_;
303              
304 0 0       0     open my $repro_fh, ">", $repro_file
305                     or die "Cannot open $repro_file for writing: $!";
306 0         0     _dump_yaml_to_archive( $current, $repro_fh );
307 0         0     close $repro_fh;
308 0         0     print STDERR "Created new archive: $repro_file\n";
309             }
310              
311             sub _get_current_state {
312 0     0   0     my ( $current, $prog_dir ) = @_;
313 0         0     _archive_version($current);
314 0         0     _git_info( $current, $prog_dir );
315 0         0     _perl_info($current);
316 0         0     _dir_info( $current, $prog_dir );
317 0         0     _env_info($current);
318             }
319              
320             sub _archive_version {
321 0     0   0     my $current = shift;
322 0         0     $$current{'ARCHIVE VERSION'} = "@{[__PACKAGE__]} $VERSION";
  0         0  
323             }
324              
325             sub _git_info {
326 0     0   0     my ( $current, $prog_dir ) = @_;
327              
328 0         0     my $devnull = File::Spec->devnull();
329 0 0       0     return if `git --version 2> $devnull` eq '';
330              
331 0         0     my $original_dir = getcwd;
332 0         0     chdir $prog_dir;
333              
334 0         0     my $gitbranch = `git rev-parse --abbrev-ref HEAD 2>&1`;
335 0 0       0     return if $gitbranch =~ /fatal: Not a git repository/;
336 0         0     chomp $gitbranch;
337              
338 0         0     my $gitlog = `git log -n1 --oneline`;
339 0         0     chomp $gitlog;
340              
341 0         0     my @status = `git status --short`;
342 0         0     chomp @status;
343              
344 0         0     my $diffstaged = `git diff --cached`;
345 0         0     my $diff = `git diff`;
346              
347 0         0     $$current{'GIT'} = [
348                     { 'BRANCH' => $gitbranch },
349                     { 'COMMIT' => $gitlog },
350                     { 'STATUS' => \@status },
351                     { 'DIFF (STAGED)' => $diffstaged },
352                     { 'DIFF' => $diff }
353                 ];
354 0         0     chdir $original_dir;
355             }
356              
357             sub _perl_info {
358 0     0   0     my $current = shift;
359 0         0     my $path = $Config{perlpath};
360 0         0     my $version = sprintf "v%vd", $^V;
361 0         0     my $modules = _loaded_perl_module_versions();
362 0         0     $$current{'PERL'} = [
363                     { 'VERSION' => $version },
364                     { 'PATH' => $path },
365                     { 'INC' => [@INC] },
366                     { 'MODULES' => [@$modules] }
367                 ];
368             }
369              
370             sub _loaded_perl_module_versions {
371 0 0   0   0     my $code_to_test = do { open my $fh, '<', $0 or return; local $/; <$fh> };
  0         0  
  0         0  
  0         0  
372 0         0     my ($package) = @{ [__PACKAGE__] };
  0         0  
373 0         0     $code_to_test =~ s/use\s+$package[^;]*;//g;
374 0         0     my ( $temp_fh, $temp_filename ) = File::Temp::tempfile();
375 0         0     print $temp_fh $code_to_test;
376              
377 0         0     local ( *CIN, *COUT, *CERR );
378 0         0     my $perl = $Config{perlpath};
379 0         0     my $cmd = "$perl -MO=Xref $temp_filename";
380 0         0     my $pid = open3( \*CIN, \*COUT, \*CERR, $cmd );
381 0         0     my %loaded_modules;
382 0         0     for (<COUT>) {
383 0 0       0         next unless my ($mod) = $_ =~ /^\s*Package\s*([^\s]+)\s*$/;
384 0 0       0         next if $mod =~ /[()]/;
385 0 0       0         next unless $mod =~ /\w/;
386 0         0         $loaded_modules{$mod} = 1;
387                 }
388 0         0     waitpid $pid, 0;
389 0 0       0     File::Temp::unlink0( $temp_fh, $temp_filename )
390                     or warn "Error unlinking file $temp_filename safely";
391              
392 0         0     my @module_versions;
393 0         0     my $NOWARN = 0;
394 0 0   0   0     $SIG{'__WARN__'} = sub { warn $_[0] unless $NOWARN };
  0         0  
395 0         0     for my $mod ( sort keys %loaded_modules ) {
396 0         0         $NOWARN = 1;
397 0         0         eval "require $mod";
398 0 0       0         next if $@;
399 0         0         eval "$mod->VERSION";
400 0 0       0         my $version = $@ ? "?" : $mod->VERSION;
401 0         0         $NOWARN = 0;
402 0 0       0         next unless defined $version;
403 0         0         push @module_versions, "$mod $version";
404                 }
405 0         0     $NOWARN = 0;
406 0         0     return \@module_versions;
407             }
408              
409             sub _dir_info {
410 0     0   0     my ( $current, $prog_dir ) = @_;
411              
412 0         0     my $cwd = getcwd;
413 0         0     my $abs_dir = Cwd::realpath($prog_dir);
414              
415 0         0     $$current{'WORKING DIR'} = $cwd;
416 0 0       0     $$current{'SCRIPT DIR'}
417                     = $abs_dir eq $prog_dir
418                     ? $abs_dir
419                     : { 'ABSOLUTE' => $abs_dir, 'RELATIVE' => $prog_dir };
420             }
421              
422             sub _env_info {
423 0     0   0     my $current = shift;
424 0         0     $$current{'ENV'} = \%ENV;
425             }
426              
427             sub _dump_yaml_to_archive {
428 0     0   0     my ( $current, $repro_fh ) = @_;
429              
430 0         0     local $YAML::UseBlock = 1; # Force short multi-line notes to span lines
431              
432 0         0     my @to_yaml = (
433                     { 'COMMAND' => $$current{'COMMAND'} },
434                     { 'NOTE' => $$current{'NOTE'} },
435                 );
436 0 0       0     if ( exists $$current{'REPRODUCTION'} ) {
437 0         0         push @to_yaml, { 'REPRODUCTION' => $$current{'REPRODUCTION'} };
438                 }
439 0         0     push @to_yaml, { 'STARTED' => $$current{'STARTED'} },
440                                { 'WORKING DIR' => $$current{'WORKING DIR'} },
441                                { 'SCRIPT DIR' => $$current{'SCRIPT DIR'} },
442                                { 'ARCHIVE VERSION' => $$current{'ARCHIVE VERSION'} },
443                                { 'PERL' => $$current{'PERL'} };
444 0 0       0     if ( exists $$current{'GIT'} ) {
445 0         0         push @to_yaml, { 'GIT' => $$current{'GIT'} };
446                 }
447 0         0     push @to_yaml, { 'ENV' => $$current{'ENV'} };
448              
449 0         0     print $repro_fh Dump [@to_yaml];
450             }
451              
452             sub _add_warnings_to_current_state {
453 0     0   0     my ( $current, $warnings, $old_repro_file, $diff_file ) = @_;
454              
455 0 0       0     $diff_file
456                     = "Text::Diff needs to be installed to create summary of archive vs. current differences"
457                     unless defined $diff_file;
458 0         0     my @warning_messages = map { $$_{message} } @$warnings;
  0         0  
459 0 0       0     if ( scalar @warning_messages > 0 ) {
460 0         0         $$current{'REPRODUCTION'} = [
461                         { 'REPRODUCED ARCHIVE' => $old_repro_file },
462                         { 'WARNINGS' => [@warning_messages] },
463                         { 'DIFF FILE' => $diff_file }
464                     ];
465                 }
466                 else {
467 0         0         $$current{'REPRODUCTION'} = [
468                         { 'REPRODUCED ARCHIVE' => $old_repro_file },
469                         { 'WARNINGS' => 'NONE' },
470                     ];
471                 }
472             }
473              
474             sub _dump_yaml_to_archive_manually {
475 0     0   0     my ( $title, $comment, $repro_fh ) = @_;
476 0         0     print $repro_fh "- $title: $comment\n";
477             }
478              
479             sub _add_exit_code_preamble {
480 0     0   0     my $repro_fh = shift;
481 0         0     print $repro_fh _divider_message();
482 0         0     print $repro_fh _divider_message(
483                     "IF EXIT CODE IS MISSING, SCRIPT WAS CANCELLED OR IS STILL RUNNING!");
484 0         0     print $repro_fh _divider_message(
485                     "TYPICALLY: 0 == SUCCESS AND 255 == FAILURE");
486 0         0     print $repro_fh _divider_message();
487 0         0     print $repro_fh "- EXITCODE: "; # line left incomplete until exit
488             }
489              
490             sub _divider_message {
491 5     5   3354     my $message = shift;
492 5         6     my $width = 80;
493 5 100       11     if ( defined $message ) {
494 4         5         my $msg_len = length($message) + 2;
495 4         848         my $pad = ( $width - $msg_len ) / 2;
496 4 100       35         $message
497                         = $pad > 1
498                         ? join " ", "#" x ceil($pad), $message, "#" x floor($pad)
499                         : "# $message #";
500                 }
501                 else {
502 1         3         $message = "#" x $width;
503                 }
504 5         12     return "$message\n";
505             }
506              
507             sub _validate_prog_name {
508 0     0   0     my ( $archived_prog, $prog, @args ) = @_;
509 0     0   0     local $SIG{__DIE__} = sub { warn @_; exit 1 };
  0         0  
  0         0  
510 0 0       0     die <<EOF if $archived_prog ne $prog;
511             Current ($prog) and archived ($archived_prog) program names don't match!
512             If this was expected (e.g., filename was changed), please re-run as:
513            
514             perl $prog @args
515            
516             EOF
517             }
518              
519             sub _validate_archived_info {
520 0     0   0     my ( $archived_state, $current, $warnings ) = @_;
521              
522 0         0     _compare_archive_current_string( $archived_state, $current,
523                     'ARCHIVE VERSION', $warnings );
524 0         0     for my $group (qw(PERL GIT)) {
525 0         0         _compare_archive_current_array( $archived_state, $current, $group,
526                         $warnings );
527                 }
528 0         0     _compare_archive_current_hash( $archived_state, $current, 'ENV',
529                     $warnings );
530             }
531              
532             sub _compare_archive_current_string {
533 0     0   0     my ( $archive, $current, $key, $warnings ) = @_;
534              
535 0         0     my $arc_string = $$archive{$key};
536 0         0     my $cur_string = $$current{$key};
537 0 0       0     if ( $arc_string ne $cur_string ) {
538 0         0         _raise_warning( $warnings, $key, \$arc_string, \$cur_string );
539                 }
540             }
541              
542             sub _compare_archive_current_hash {
543 0     0   0     my ( $archive, $current, $key, $warnings ) = @_;
544              
545                 my @arc_array
546 0         0         = map {"$_: $$archive{$key}{$_}"} sort keys %{ $$archive{$key} };
  0         0  
  0         0  
547                 my @cur_array
548 0         0         = map {"$_: $$current{$key}{$_}"} sort keys %{ $$current{$key} };
  0         0  
  0         0  
549 0 0       0     if ( join( "", @arc_array ) ne join( "", @cur_array ) ) {
550 0         0         _raise_warning( $warnings, $key, \@arc_array, \@cur_array );
551                 }
552             }
553              
554             sub _compare_archive_current_array {
555 0     0   0     my ( $archive, $current, $group, $warnings ) = @_;
556              
557 0         0     for ( 0 .. $#{ $$archive{$group} } ) {
  0         0  
558 0         0         my %archive_subgroup;
559                     my %current_subgroup;
560 0         0         my ( $arc_key, $too_many_ak ) = keys %{ $$archive{$group}->[$_] };
  0         0  
561 0         0         my ( $cur_key, $too_many_ck ) = keys %{ $$current{$group}->[$_] };
  0         0  
562              
563 0 0 0     0         die "Something is wrong..."
      0        
564                         if $arc_key ne $cur_key
565                         || defined $too_many_ak
566                         || defined $too_many_ck;
567              
568 0         0         $archive_subgroup{$arc_key} = $$archive{$group}->[$_]{$arc_key};
569 0         0         $current_subgroup{$cur_key} = $$current{$group}->[$_]{$cur_key};
570              
571 0 0 0     0         if ( !ref( $archive_subgroup{$arc_key} )
    0 0        
572                         && !ref( $current_subgroup{$cur_key} ) )
573                     {
574 0 0       0             if ( $archive_subgroup{$arc_key} ne $current_subgroup{$cur_key} )
575                         {
576 0         0                 _raise_warning(
577                                 $warnings,
578                                 "$group $cur_key",
579                                 \$archive_subgroup{$arc_key},
580                                 \$current_subgroup{$cur_key}
581                             );
582                         }
583                     }
584                     elsif (ref( $archive_subgroup{$arc_key} ) eq "ARRAY"
585                         && ref( $current_subgroup{$cur_key} ) eq "ARRAY" )
586                     {
587 0 0       0             if (join( "", @{ $archive_subgroup{$arc_key} } ) ne
  0         0  
  0         0  
588                             join( "", @{ $current_subgroup{$cur_key} } ) )
589                         {
590 0         0                 _raise_warning(
591                                 $warnings,
592                                 "$group $cur_key",
593                                 $archive_subgroup{$arc_key},
594                                 $current_subgroup{$cur_key}
595                             );
596                         }
597                     }
598                     else {
599 0         0             die "Something is wrong...";
600                     }
601                 }
602             }
603              
604             sub _raise_warning {
605 0     0   0     my ( $warnings, $item, $archive, $current ) = @_;
606              
607 0         0     push @$warnings,
608                     {
609                     message => "Archived and current $item do NOT match",
610                     archive => $archive,
611                     current => $current
612                     };
613             }
614              
615             sub _summarize_warnings {
616 0     0   0     my ( $warnings, $old_repro_file, $repro_file, $dir, $prog, $start ) = @_;
617 0         0     my $diff_file;
618 0 0       0     if (@$warnings) {
619 0         0         print STDERR "\n";
620 0         0         for my $alert (@$warnings) {
621 0         0             print STDERR "WARNING: $$alert{message}\n";
622                     }
623 0         0         print STDERR <<EOF;
624            
625             There are inconsistencies between the archived and current conditions.
626             These differences might affect reproducibility. A summary can be found at:
627             EOF
628 0         0         $diff_file
629                         = _repro_diff( $warnings, $old_repro_file, $repro_file, $dir,
630                         $prog, $start );
631 0         0         _do_or_die();
632                 }
633 0         0     return $diff_file;
634             }
635              
636             sub _repro_diff {
637 0     0   0     my ( $warnings, $old_repro_file, $repro_file, $dir, $prog, $start ) = @_;
638              
639 0         0     eval "use Text::Diff";
640 0 0       0     if ($@) {
641 0         0         print STDERR
642                         " Uh oh, you need to install Text::Diff to see the summary! (http://www.cpan.org/modules/INSTALL.html)\n";
643 0         0         return;
644                 }
645 0         0     require Text::Diff;
646              
647 0         0     my ($old_timestamp) = $old_repro_file =~ /-(\d{8}\.\d{6}(?:\.\d{3})?)$/;
648 0         0     my $new_timestamp = $$start{'timestamp'};
649              
650 0         0     my $diff_file = "$dir/rdiff-$prog-$old_timestamp.vs.$new_timestamp";
651 0         0     _is_file_unique( \$diff_file );
652 0         0     open my $diff_fh, ">", $diff_file;
653 0         0     print $diff_fh <<HEAD;
654             The following inconsistencies between archived and current conditions were found when
655             reproducing a run from an archive. These have the potential to affect reproducibility.
656             ------------------------------------------------------------------------------------------
657             Archive: $old_repro_file
658             Current: $repro_file
659             ------------------------------------------------------------------------------------------
660             Note: This file is often best viewed with word wrapping disabled
661             ------------------------------------------------------------------------------------------
662            
663             HEAD
664 0         0     for my $alert (@$warnings) {
665 0         0         my $diff = diff( $$alert{archive}, $$alert{current},
666                         { STYLE => "Table" } );
667 0         0         print $diff_fh $$alert{message}, "\n";
668 0         0         print $diff_fh $diff, "\n";
669                 }
670 0         0     close $diff_fh;
671 0         0     print STDERR " $diff_file\n";
672 0         0     return $diff_file;
673             }
674              
675             sub _do_or_die {
676 0     0   0     print STDERR "Do you want to continue? (y/n) ";
677 0         0     my $response = <STDIN>;
678 0 0       0     if ( $response =~ /^Y(?:ES)?$/i ) {
    0          
679 0         0         return;
680                 }
681                 elsif ( $response =~ /^N(?:O)?$/i ) {
682 0         0         print STDERR "Better luck next time...\n";
683 0         0         exit;
684                 }
685                 else {
686 0         0         _do_or_die();
687                 }
688             }
689              
690             sub _exit_code {
691 0     0   0     our ( $repro_file, $start ) = @_;
692              
693 0 0       0     open my $repro_fh, ">>", $repro_file
694                     or die "Cannot open $repro_file for appending: $!";
695 0         0     _add_exit_code_preamble($repro_fh);
696 0         0     close $repro_fh;
697              
698                 END {
699 1 50   1   854         return unless defined $repro_file;
700 0         0         my $finish = _now();
701 0         0         my $elapsed = _elapsed( $$start{'seconds'}, $$finish{'seconds'} );
702 0 0       0         open my $repro_fh, ">>", $repro_file
703                         or die "Cannot open $repro_file for appending: $!";
704 0         0         print $repro_fh "$?\n"; # This completes EXITCODE line
705 0         0         _dump_yaml_to_archive_manually( "FINISHED", $$finish{'when'},
706                         $repro_fh );
707 0         0         _dump_yaml_to_archive_manually( "ELAPSED", $elapsed, $repro_fh );
708 0         0         close $repro_fh;
709                 }
710             }
711              
712             sub _elapsed {
713 1     1   1413     my ( $start_seconds, $finish_seconds ) = @_;
714              
715 1         8     my $secs = difftime $finish_seconds, $start_seconds;
716 1         7     my $mins = int $secs / 60;
717 1         3     $secs = $secs % 60;
718 1         3     my $hours = int $mins / 60;
719 1         1     $mins = $mins % 60;
720              
721 1         37     return join ":", map { sprintf "%02d", $_ } $hours, $mins, $secs;
  3         15  
722             }
723              
724             1;
725