App-Access2CSV
view release on metacpan or search on metacpan
t/transaction.t view on Meta::CPAN
open STDERR, '>', "$dir/stderr" or die $!;
open STDOUT, '>', File::Spec->devnull() or die $!;
exec($^X, "-I$LIB", $SCRIPT, '--no-log', '--overwrite', '--output-dir', $out, $db) or die "exec: $!";
}
for (1 .. $CONFIG{wait_steps}) {
last if @{ temp_files($out) };
sleep $CONFIG{wait_step};
}
kill $signal, $to_group ? -$pid : $pid;
waitpid($pid, 0);
my $status = $? >> 8;
kill 'KILL', -$pid; # tidy away the stand-in mdb-export if it lingers
return { status => $status, stderr => slurp("$dir/stderr") // '', out => $out };
}
subtest 'Phase: interrupted mid-transaction -> rollback and stop' => sub {
# Ctrl-C reaches the whole process group; kill/service managers send
# TERM to the program. Either way the table in flight is rolled back
# and no further table is started.
foreach my $case (['INT', 1, 'Ctrl-C (SIGINT to the group)'], ['TERM', 0, 'SIGTERM to the program']) {
my ($signal, $group, $name) = @{$case};
my $result = interrupt($signal, $group);
verbose_diag($name, $result);
is($result->{status}, $CONFIG{exit_fatal}, "$name: stopped (exit 3)");
like($result->{stderr}, qr/access2csv: Interrupted by SIG$signal: stopped, and the table being exported was discarded/, "$name: says so");
is(slurp("$result->{out}/Slow.csv"), $CONFIG{old_content}, "$name: old file intact");
is_deeply(temp_files($result->{out}), [], "$name: temporary file rolled back");
ok(!-e "$result->{out}/Zebra.csv", "$name: the next table was not started");
}
};
subtest 'Phase: a caller\'s own signal handlers are respected' => sub {
# The exporter only takes over signals nobody else is handling, and
# gives them back afterwards
my $dir = tempdir(CLEANUP => 1);
my $db = make_database($dir, 'A');
my $mine = sub { };
local $SIG{TERM} = $mine;
local $SIG{HUP};
my %during;
before("$CONFIG{exporter}::_export_table", sub { %during = (TERM => $SIG{TERM}, HUP => $SIG{HUP}) });
export(db => $db, output_dir => "$dir/out");
restore_all();
is($during{TERM}, $mine, 'during: the caller\'s TERM handler untouched');
is(ref($during{HUP}), 'CODE', 'during: HUP handled by the exporter');
is($SIG{TERM}, $mine, 'after: TERM as before');
ok(!defined($SIG{HUP}) || $SIG{HUP} eq 'DEFAULT' || $SIG{HUP} eq '', 'after: HUP restored');
};
#######################################################################
# Phase 5: repeating the same transaction
#######################################################################
subtest 'Phase: repeat without --overwrite -> refused, existing data untouched' => sub {
# Documented: a second run refuses to replace files. That refusal
# must not damage what the first run committed, however often it runs.
my $dir = tempdir(CLEANUP => 1);
my $db = make_database($dir, 'A', 'B');
my $first = export(db => $db, output_dir => "$dir/out");
is($first->{status}, $CONFIG{exit_ok}, 'first run commits');
my %committed = map { $_ => [slurp("$dir/out/$_"), (stat("$dir/out/$_"))[9]] } qw(A.csv B.csv);
foreach my $repeat (1 .. 2) {
my $again = export(db => $db, output_dir => "$dir/out");
is($again->{status}, $CONFIG{exit_failure}, "repeat $repeat: refused");
is(scalar(() = $again->{stderr} =~ /Output file already exists/g), 2, "repeat $repeat: both tables refused");
foreach my $file (sort keys %committed) {
is(slurp("$dir/out/$file"), $committed{$file}[0], "repeat $repeat: $file content unchanged");
is((stat("$dir/out/$file"))[9], $committed{$file}[1], "repeat $repeat: $file not even rewritten");
}
is_deeply(temp_files("$dir/out"), [], "repeat $repeat: no temporary files");
}
};
subtest 'Phase: repeat with --overwrite -> the same result every time' => sub {
# With overwrite the transaction is idempotent: same names (even for
# colliding tables), same bytes; the log grows but earlier lines stay
my $dir = tempdir(CLEANUP => 1);
my $db = make_database($dir, 'A/B', 'A:B', 'Unicode');
my $log = "$dir/run.log";
my (@snapshots, @log_sizes);
foreach my $run (1 .. 3) {
my ($status);
capture { $status = App::Access2CSV->run('--log', $log, '--overwrite', '--no-progress', '--output-dir', "$dir/out", $db) };
is($status, $CONFIG{exit_ok}, "run $run: status 0");
opendir my $dh, "$dir/out" or die $!;
push @snapshots, { map { $_ => slurp("$dir/out/$_") } grep { !/\A\./ } readdir $dh };
push @log_sizes, -s $log;
}
is_deeply($snapshots[1], $snapshots[0], 'run 2 identical to run 1');
is_deeply($snapshots[2], $snapshots[0], 'run 3 identical to run 1');
is_deeply([sort keys %{ $snapshots[0] }], ['A_B.csv', 'A_B_2.csv', 'Unicode.csv'], 'same names each time');
ok($log_sizes[0] < $log_sizes[1] && $log_sizes[1] < $log_sizes[2], 'log appended, never truncated');
};
subtest 'Phase: repeated dry runs -> identical, and nothing written' => sub {
my $dir = tempdir(CLEANUP => 1);
my $db = make_database($dir, 'A/B', 'A:B');
my @outputs = map { export(db => $db, output_dir => "$dir/out", dry_run => 1)->{stdout} } 1 .. 3;
is($outputs[1], $outputs[0], 'second listing identical');
is($outputs[2], $outputs[0], 'third listing identical');
ok(!-e "$dir/out", 'nothing created');
};
subtest 'Phase: fail, then retry -> same final state as a clean run' => sub {
# A fault in the first attempt must not leave anything that makes the
# retry differ from a run that never failed
my $dir = tempdir(CLEANUP => 1);
my $db = make_database($dir, 'A', 'B');
{
my $guard = fake_export(sub { die "run3(): transient failure\n" if $_[0][-1] eq 'B'; print { $_[1] } "\"id\",\"name\"\n1,\"$_[0][-1]\"\n" });
my $first = export(db => $db, output_dir => "$dir/retry");
is($first->{status}, $CONFIG{exit_failure}, 'first attempt: B failed');
}
my $retry = export(db => $db, output_dir => "$dir/retry", overwrite => 1);
is($retry->{status}, $CONFIG{exit_ok}, 'retry succeeds');
export(db => $db, output_dir => "$dir/clean");
foreach my $file (qw(A.csv B.csv)) {
is(slurp("$dir/retry/$file"), slurp("$dir/clean/$file"), "$file: same as a clean run");
}
is_deeply(temp_files("$dir/retry"), [], 'no temporary files');
};
restore_all();
done_testing();
( run in 0.606 second using v1.01-cache-2.11-cpan-036bef1c656 )