#!perl -T # # t/10-Net-FTP-Simple.t - Tests for Net::FTP::Simple # # Written by Wil Cooley # use lib qw( tlib ); use strict; use warnings; use English qw( -no_match_vars ); use File::Spec; use Test::More; BEGIN { eval 'use Test::MockObject'; if ($EVAL_ERROR) { plan skip_all => 'Test::MocObject required for unit tests'; } else { plan tests => 76; } } BEGIN { use_ok('Net::FTP::Simple'); } my @base_files_to_send = qw( file-a file-b file-c ); my @files_to_send = map { File::Spec->join('test-data', 'test-subdir', $_) } @base_files_to_send; my @input_file_list = ( 'drwxr-xr-x 2 2171 2172 4096 Sep 29 17:35 dir with spaces', 'prw-r--r-- 1 0 0 0 Sep 29 17:37 fifo-test', '-rw-r--r-- 1 2171 2172 55082 Sep 29 20:42 merccurl-auth-page.html', 'drwxr-xr-x 2 0 0 4096 Sep 29 16:46 testdir', '-rwxr-xr-x 1 0 0 6660 Oct 16 18:08 foo', ); my @expected_list = ( 'merccurl-auth-page.html', 'foo', ); # Treat all warnings as failing tests setup_warning_handler(); ####################################################################### # basic setup ####################################################################### { my $test = "Basic"; my ($ftp_simple, $fake_conn); # Croak if no 'server' eval { Net::FTP::Simple->_new({}); }; if ($EVAL_ERROR =~ m/requires at least 'server' parameter/) { pass("$test: Correctly fail on missing parameters"); } else { fail("$test: Did not fail with missing parameters"); } # Non-empty but bogus server name eval { $ftp_simple = Net::FTP::Simple->_new({ username => undef, password => undef, server => 1, }); }; if ($EVAL_ERROR =~ m/Error creating Net::FTP object/) { pass("$test: Correctly fail on bogus parameters"); } else { fail("$test: Should have thrown an error here"); } is($ftp_simple, undef, "Bogus connection produces undef object"); ok($fake_conn = new_mock_ftp(), "$test: New Net::FTP mock object"); ok($ftp_simple = Net::FTP::Simple->_new({ conn => $fake_conn, server => 'localhost', }), "$test: New object with fake connection"); is($ftp_simple->_set_conn(undef), undef, "$test: _set_conn() sets and returns undef"); is($ftp_simple->_conn(), undef, "$test: _conn() returns undef when set to undef"); { my $caller = $ftp_simple->_caller(); ok($ftp_simple->_caller(), "$test: Caller is '$caller'"); } eval { $ftp_simple->_error("Test error"); }; if ($EVAL_ERROR) { fail("$test: _error() should not have caused exception '$EVAL_ERROR'"); } else { pass("$test: _error() did not cause exception w/bad _conn"); } } ####################################################################### # _op_retry ####################################################################### { my $test = '_op_retry'; my $op = 'test_op'; my $op_msg = 'retry test message'; ok(my $fake_conn = new_mock_ftp(), "$test: New Net::FTP mock object"); $fake_conn->set_always('message', $op_msg); $Net::FTP::Simple::retry_max{$op} = 3; $Net::FTP::Simple::retryable_errors{$op} = [ $op_msg, ]; $Net::FTP::Simple::retry_wait{$op} = 0; ok(my $obj = Net::FTP::Simple->_new({ conn => $fake_conn, server => 'localhost', }), "$test: Net::FTP::Simple->_new()"); my @test_data = ( # Tries Sequence [ 1, [ 1, ], ], [ 2, [ 0, 1 ], ], [ 3, [ 0, 0, 1 ], ], ); for my $test_args_ref (@test_data) { my $tries = $test_args_ref->[0]; my @series = @{ $test_args_ref->[1] }; $fake_conn->set_series($op, @series); is($obj->_op_retry($op), $tries, "$test: $op succeeded after $tries tries"); } $fake_conn->set_series($op, 0, 0, 0, 1); eval { $obj->_op_retry($op); }; if ($EVAL_ERROR =~ m/'$op' failed after 4 attempts/) { pass("$test: $op correctly failed after 4 tries"); } elsif ($EVAL_ERROR) { fail("$test: $op failed after 4 tries with unexpected error" . "'$EVAL_ERROR'"); } else { fail("$test: $op did not fail after 4 tries as expected"); } $Net::FTP::Simple::retry_max{$op} = 0; # FIXME From here down is basically a copy and paste of the above; # refactor for my $test_args_ref ($test_data[0]) { my $tries = $test_args_ref->[0]; my @series = @{ $test_args_ref->[1] }; $fake_conn->set_series($op, @series); is($obj->_op_retry($op), $tries, "$test: $op succeeded after $tries tries (max 0)"); } $fake_conn->set_series($op, 0, 1); eval { $obj->_op_retry($op); }; if ($EVAL_ERROR =~ m/'$op' failed after 2 attempts/) { pass("$test: $op correctly failed after 2 tries"); } elsif ($EVAL_ERROR) { fail("$test: $op failed after 2 tries with unexpected error" . "'$EVAL_ERROR'"); } else { fail("$test: $op did not fail after 2 tries as expected"); } } ####################################################################### # _list_and_filter ####################################################################### { my $test = "_list_and_filter"; ok(my $fake_conn = new_mock_ftp(), "$test: New Net::FTP mock object"); my ($files_ref, @files); my %test_data = ( "basic w/o filter" => { input_list_ref => [ @input_file_list ], expected_list_ref => [ @expected_list ], filter => undef }, "filename w/spaces, w/o filter" => { input_list_ref => [ @input_file_list, '-rw-r--r-- 1 2171 2172 55082 Sep 29 20:42 merccurl auth page.html', ], expected_list_ref => [ @expected_list, 'merccurl auth page.html' ], filter => undef }, "basic w/wide filter" => { input_list_ref => [ @input_file_list ], expected_list_ref => [ @expected_list ], filter => qr/[aeou]+/, }, "basic w/narrow filter" => { input_list_ref => [ @input_file_list ], expected_list_ref => [ grep { /merccurl/ } @expected_list ], filter => qr/merccurl/, }, "empty list" => { input_list_ref => [], expected_list_ref => [], filter => undef, }, ); for my $subtest_name (keys %test_data) { my $subtest_ref = $test_data{$subtest_name}; my $subtest_name = "$test/$subtest_name"; $fake_conn->set_list('dir', @{ $subtest_ref->{'input_list_ref'} }); ok(my $obj = Net::FTP::Simple->_new({ conn => $fake_conn, server => 'localhost', file_filter => $subtest_ref->{'filter'}, }), "$subtest_name: Net::FTP::Simple->_new()"); my @files = $obj->_list_and_filter(); is_deeply(\@files, $subtest_ref->{'expected_list_ref'}, "$subtest_name: Returned list correct (list context)"); my $files_ref = $obj->_list_and_filter(); if (defined $files_ref) { is_deeply($files_ref, $subtest_ref->{'expected_list_ref'}, "$subtest_name: Returned list correct (scalar context)"); } else { is($files_ref, undef, "$subtest_name: Returned undef (scalar context)"); } } } ####################################################################### # list_files ####################################################################### { my $test = "list_files (basic)"; ok(my $fake_conn = new_mock_ftp(), "$test: New Net::FTP mock object"); my @files; $fake_conn->set_list('dir', @input_file_list); ok(@files = Net::FTP::Simple->list_files({ conn => $fake_conn, server => 'localhost', }), "$test: Returns something true"); is_deeply(\@files, \@expected_list, "$test: Returned list correct"); # Add a filter ok(@files = Net::FTP::Simple->list_files({ conn => $fake_conn, server => 'localhost', file_filter => qr/merccurl/, }), "$test: Returns something true"); is_deeply(\@files, [ $expected_list[0] ], "$test: Returned list w/filter correct"); } ####################################################################### # list_files # -> empty list ####################################################################### { my $test = "list_files (empty list)"; ok(my $fake_conn = new_mock_ftp(), "$test: New Net::FTP mock object"); $fake_conn->set_list('dir', ()); my @files = Net::FTP::Simple->list_files({ conn => $fake_conn, server => 'localhost', }); is_deeply(\@files, [], "$test: Returns empty list (class method)"); @files = Net::FTP::Simple::list_files({ conn => $fake_conn, server => 'localhost', }); is_deeply(\@files, [], "$test: Returns empty list (module subroutine)"); } ####################################################################### # send_files ####################################################################### { my $test = "send_files (basic)"; my @succ_transfers; ok(my $fake_conn = new_mock_ftp(), "$test: Net::FTP mock object"); $fake_conn->set_true( qw( put rename message ) ); ok(@succ_transfers = Net::FTP::Simple->send_files({ conn => $fake_conn, server => 'localhost', files => \@files_to_send, }), "$test: Returns non-empty list"); is_deeply(\@succ_transfers, \@files_to_send, "$test: Sends all files (class method)"); ok(@succ_transfers = Net::FTP::Simple->send_files({ conn => $fake_conn, server => 'localhost', files => \@files_to_send, }), "$test: Returns non-empty list"); is_deeply(\@succ_transfers, \@files_to_send, "$test: Sends all files (module subroutine)"); } ####################################################################### # send_files # -> test 'retry' ####################################################################### { my $test = "send_files (retry)"; # Don't really want it to wait the whole retry period $Net::FTP::Simple::retry_wait{'rename'} = 0; my ($retries, $pattern, $sub, @succ_transfers); my $warn_handlers = { qr/Error renaming '(file-.\.tmp)' to '(file-.)'/ms => sub { my ($from, $to) = @_; pass("$test: Correctly failed to rename '$from' to '$to'"); }, }; setup_warning_handler($test, $warn_handlers); ok(my $fake_conn = new_mock_ftp(), "$test: Net::FTP mock object"); $fake_conn->set_true( qw( put ) ); $fake_conn->set_always( 'message' => qq/The process cannot access the file because / . qq/it is being used by another process/ ); =begin comment Fail once on the first file. Fail once on the second file. Succeed immediately on the third file. =end comment =cut $fake_conn->set_series('rename', 0, 1, 0, 1, 1); ($pattern, $sub) = _gen_sub_count_transfer_tries($test, $files_to_send[0] => 2, $files_to_send[1] => 2, $files_to_send[2] => 0, ); $warn_handlers->{ $pattern } = $sub; ok(@succ_transfers = Net::FTP::Simple->send_files({ conn => $fake_conn, server => 'localhost', files => \@files_to_send, }), "$test: Returns non-empty list"); is(@succ_transfers, 3, "$test: 3 files sent"); =begin comment Fail once on the first file Twice on the second file Four times (which is the max) on the third There is a final one to catch the potential case where the third does not fail after four tries. Note that there is a potential off-by-one here: The $retry_max{'rename'} is 3, which means there should be I<4> tries (which is one try and I<3> retries!). =end comment =cut $fake_conn->set_series('rename', 0, 1, 0, 0, 1, 0, 0, 0, 0, 1); ($pattern, $sub) = _gen_sub_count_transfer_tries($test, $files_to_send[0] => 2, $files_to_send[1] => 3, $files_to_send[2] => 0, ); $warn_handlers->{ $pattern } = $sub; ok(@succ_transfers = Net::FTP::Simple->send_files({ conn => $fake_conn, server => 'localhost', files => \@files_to_send, }), "$test: Returns non-empty list"); is(@succ_transfers, 2, "$test: 2 files set, one failed"); #----------------------------------------------------- # # Fail the max number of tries to ensure that failure will happen # $retries = $Net::FTP::Simple::retry_max{'rename'}; $fake_conn->set_series('rename', map { 0 } 0..$retries ); @succ_transfers = Net::FTP::Simple->send_files({ conn => $fake_conn, server => 'localhost', files => [ $files_to_send[0] ], }); is_deeply(\@succ_transfers, [], "$test: 0 successes after max $retries retry failures"); setup_warning_handler(); } ####################################################################### # rename_files ####################################################################### { my $test = "rename_files (basic)"; my @succ_renames; ok(my $fake_conn = new_mock_ftp(), "$test: Net::FTP mock object"); $fake_conn->set_true( qw( rename ) ); ok(@succ_renames = Net::FTP::Simple->rename_files({ conn => $fake_conn, server => 'localhost', rename_files => { map { $_ => $_ . '.FOO' } @base_files_to_send }, }), "$test: Returned non-empty list"); is_deeply(\@succ_renames, \@base_files_to_send, "$test: Renamed files correctly"); } ####################################################################### # rename_files # -> retry ####################################################################### { my $test = "rename_files (retry)"; my ($pattern, $sub, $retries, @succ_renames); my %rename_files = map { $_ => $_ . '.FOO' } @base_files_to_send; # Don't really want it to wait the whole retry period $Net::FTP::Simple::retry_wait{'rename'} = 0; my $warn_handlers = { qr/Error renaming '(.*)' to '(.*)'/ms => sub { my ($from, $to) = @_; pass("$test: Correctly failed to rename '$from' to '$to'"); }, }; setup_warning_handler($test, $warn_handlers); ok(my $fake_conn = new_mock_ftp(), "$test: Net::FTP mock object"); $fake_conn->set_always( 'message' => qq/The process cannot access the file because / . qq/it is being used by another process/ ); =begin comment Fail once on the first file. Fail once on the second file. Succeed immediately on the third file. =end comment =cut $fake_conn->set_series('rename', 0, 1, 0, 1, 1); ($pattern, $sub) = _gen_sub_count_rename_tries($test, $base_files_to_send[0] => 2, $base_files_to_send[1] => 2, $base_files_to_send[2] => 0, ); $warn_handlers->{ $pattern } = $sub; ok(@succ_renames = Net::FTP::Simple->rename_files({ conn => $fake_conn, server => 'localhost', rename_files => \%rename_files, }), "$test: Returns non-empty list"); is(@succ_renames, 3, "$test: 3 files sent"); =begin comment Fail once on the first file Twice on the second file Four times (which is the max) on the third There is a final one to catch the potential case where the third does not fail after four tries. Note that there is a potential off-by-one here: The $retry_max{'rename'} is 3, which means there should be I<4> tries (which is one try and I<3> retries!). =end comment =cut $fake_conn->set_series('rename', 0, 1, 0, 0, 1, 0, 0, 0, 0, 1); ($pattern, $sub) = _gen_sub_count_rename_tries($test, $base_files_to_send[0] => 2, $base_files_to_send[1] => 3, $base_files_to_send[2] => 0, ); $warn_handlers->{ $pattern } = $sub; ok(@succ_renames = Net::FTP::Simple->rename_files({ conn => $fake_conn, server => 'localhost', rename_files => \%rename_files, }), "$test: Returns non-empty list"); is(@succ_renames, 2, "$test: 2 files renamed, one failed"); #----------------------------------------------------- # # Fail the max number of tries to ensure that failure will happen # $retries = $Net::FTP::Simple::retry_max{'rename'}; $fake_conn->set_series('rename', map { 0 } 0..$retries ); @succ_renames = Net::FTP::Simple->rename_files({ conn => $fake_conn, server => 'localhost', rename_files => \%rename_files, }); is_deeply(\@succ_renames, [], "$test: 0 successes after max $retries retry failures"); setup_warning_handler(); } # Do some basic setup of the mock object sub new_mock_ftp { my $fake_conn = Test::MockObject->new(); $fake_conn->set_true( qw( login binary ok quit ) ); return $fake_conn; } # # Setup $SIG{__WARN__} with a hash of I => I items. # I should be a regexp. I may be either something true or # false or a code ref. If a code ref, the code ref is called with any matches # from the pattern comparison. # # Does not re-throw the warnings as might be desirable in non-test code. # # Can be used as a universal warnings-as-test-failure with no parameters. # sub setup_warning_handler { # Clear handler my ($test, $expected_warnings); if (@_) { ($test, $expected_warnings) = @_; } else { $test = "Global"; $expected_warnings = {}; } $SIG{__WARN__} = sub { my ($err) = @_; chomp $err; for my $warning (keys %{ $expected_warnings }) { if (my @matches = $err =~ m{ $warning }xms) { if ( ref $expected_warnings->{ $warning } eq 'CODE' ) { $expected_warnings->{ $warning }->(@matches); } elsif ( $expected_warnings->{ $warning } ) { pass("$test: Expected warning '$err'"); } else { fail("$test: Expected but failing warning '$err'"); } return; } } fail("$test: Unexpected warning '$err'"); }; } # Yay for closures! # # _gen_sub_count_transfer_tries - This generates a code ref/anonymous # subroutine which is a (key, value) pair for use w/setup_warning_handler. # The $pattern matches the warning message from Net::FTP::Simple after a number # of tries. It expects a hash containing filenames and the expected number # of tries. # sub _gen_sub_count_transfer_tries { my ($test, %expected_tries) = @_; my $pattern = qr/Transfer of file '(.*)' succeeded after (\d+) tries/ms; return $pattern => sub { my ($filename, $tries) = @_; unless (exists $expected_tries{ $filename }) { fail("$test: Did not expect failure of file '$filename'"); return; } is($tries, $expected_tries{ $filename }, "$test: Successfully sent '$filename' after $tries tries"); }; } sub _gen_sub_count_rename_tries { my ($test, %expected_tries) = @_; my $pattern = qr/Rename of file from '(.*)' to '(.*)' succeeded after (\d+) tries/ms; return $pattern => sub { my ($file_from, $file_to, $tries) = @_; unless (exists $expected_tries{ $file_from }) { fail("$test: Did not expect failure of file '$file_from'"); return; } is($tries, $expected_tries{ $file_from }, "$test: Successfully renamed '$file_from' to '$file_to' after $tries tries"); }; }