#!/usr/bin/perl -w
use strict;
use warnings;
use 5.010;
use utf8;
use Test::More tests => 306;
#use Test::More 'no_plan';
use App::Sqitch;
use App::Sqitch::Plan;
use Path::Class;
use Test::Exception;
use Test::NoWarnings;
use Test::MockModule;
use Locale::TextDomain qw(App-Sqitch);
use App::Sqitch::X qw(hurl);
use lib 't/lib';
use MockOutput;
my $CLASS;
BEGIN {
$CLASS = 'App::Sqitch::Engine';
use_ok $CLASS or die;
delete $ENV{PGDATABASE};
delete $ENV{PGUSER};
delete $ENV{USER};
$ENV{SQITCH_CONFIG} = 'nonexistent.conf';
}
can_ok $CLASS, qw(load new name no_prompt);
my ($is_deployed_tag, $is_deployed_change) = (0, 0);
my @deployed_changes;
my @resolved;
my @requiring;
my @load_changes;
my $offset_change;
my $die = '';
my $record_work = 1;
my $updated_idx;
my ( $earliest_change_id, $latest_change_id, $initialized );
ENGINE: {
# Stub out a engine.
package App::Sqitch::Engine::whu;
use Moose;
use App::Sqitch::X qw(hurl);
extends 'App::Sqitch::Engine';
$INC{'App/Sqitch/Engine/whu.pm'} = __FILE__;
my @SEEN;
for my $meth (qw(
run_file
log_deploy_change
log_revert_change
log_fail_change
)) {
no strict 'refs';
*$meth = sub {
hurl 'AAAH!' if $die eq $meth;
push @SEEN => [ $meth => $_[1] ];
};
}
sub is_deployed_tag { push @SEEN => [ is_deployed_tag => $_[1] ]; $is_deployed_tag }
sub is_deployed_change { push @SEEN => [ is_deployed_change => $_[1] ]; $is_deployed_change }
sub change_id_for { shift; push @SEEN => [ change_id_for => {@_} ]; shift @resolved }
sub change_offset_from_id { shift; push @SEEN => [ change_offset_from_id => [@_] ]; $offset_change }
sub changes_requiring_change { push @SEEN => [ changes_requiring_change => $_[1] ]; @requiring }
sub earliest_change_id { push @SEEN => [ earliest_change_id => $_[1] ]; $earliest_change_id }
sub latest_change_id { push @SEEN => [ latest_change_id => $_[1] ]; $latest_change_id }
sub initialized { push @SEEN => 'initialized'; $initialized }
sub initialize { push @SEEN => 'initialize' }
sub register_project { push @SEEN => 'register_project' }
sub deployed_changes { push @SEEN => [ deployed_changes => $_[1] ]; @deployed_changes }
sub load_change { push @SEEN => [ load_change => $_[1] ]; @load_changes }
sub deployed_changes_since { push @SEEN => [ deployed_changes_since => $_[1] ]; @deployed_changes }
sub begin_work { push @SEEN => ['begin_work'] if $record_work }
sub finish_work { push @SEEN => ['finish_work'] if $record_work }
sub _update_ids { push @SEEN => ['_update_ids']; $updated_idx }
sub seen { [@SEEN] }
after seen => sub { @SEEN = () };
sub name_for_change_id { return 'bugaboo' }
}
ok my $sqitch = App::Sqitch->new(
db_name => 'mydb',
plan_file => file qw(t plans multi.plan)
), 'Load a sqitch sqitch object';
##############################################################################
# Test new().
throws_ok { $CLASS->new }
qr/\QAttribute (sqitch) is required/,
'Should get an exception for missing sqitch param';
my $array = [];
throws_ok { $CLASS->new({ sqitch => $array }) }
qr/\QValidation failed for 'App::Sqitch' with value/,
'Should get an exception for array sqitch param';
throws_ok { $CLASS->new({ sqitch => 'foo' }) }
qr/\QValidation failed for 'App::Sqitch' with value/,
'Should get an exception for string sqitch param';
isa_ok $CLASS->new({sqitch => $sqitch}), $CLASS;
##############################################################################
# Test load().
ok my $engine = $CLASS->load({
sqitch => $sqitch,
engine => 'whu',
}), 'Load a "whu" engine';
isa_ok $engine, 'App::Sqitch::Engine::whu';
is $engine->sqitch, $sqitch, 'The sqitch attribute should be set';
# Test handling of an invalid engine.
throws_ok { $CLASS->load({ engine => 'nonexistent', sqitch => $sqitch }) }
'App::Sqitch::X', 'Should die on invalid engine';
is $@->message, 'Unable to load App::Sqitch::Engine::nonexistent',
'Should get load error message';
like $@->previous_exception, qr/\QCan't locate/,
'Should have relevant previoius exception';
NOENGINE: {
# Test handling of no engine.
throws_ok { $CLASS->load({ engine => '', sqitch => $sqitch }) }
'App::Sqitch::X',
'No engine should die';
is $@->message, 'Missing "engine" parameter to load()',
'It should be the expected message';
}
# Test handling a bad engine implementation.
use lib 't/lib';
throws_ok { $CLASS->load({ engine => 'bad', sqitch => $sqitch }) }
'App::Sqitch::X', 'Should die on bad engine module';
is $@->message, 'Unable to load App::Sqitch::Engine::bad',
'Should get another load error message';
like $@->previous_exception, qr/^LOL BADZ/,
'Should have relevant previoius exception from the bad module';
##############################################################################
# Test name.
can_ok $CLASS, 'name';
ok $engine = $CLASS->new({ sqitch => $sqitch }), "Create a $CLASS object";
is $CLASS->name, '', 'Base class name should be ""';
is $engine->name, '', 'Base object name should be ""';
ok $engine = App::Sqitch::Engine::whu->new({sqitch => $sqitch}),
'Create a subclass name object';
is $engine->name, 'whu', 'Subclass oject name should be "whu"';
is +App::Sqitch::Engine::whu->name, 'whu', 'Subclass class name should be "whu"';
##############################################################################
# Test config_vars.
can_ok $CLASS, 'config_vars';
is_deeply [App::Sqitch::Engine->config_vars], [],
'Should have no config vars in engine base class';
##############################################################################
# Test variables.
can_ok $CLASS, qw(variables set_variables clear_variables);
is_deeply [$engine->variables], [], 'Should have no variables';
ok $engine->set_variables(foo => 'bar'), 'Add a variable';
is_deeply [$engine->variables], [foo => 'bar'], 'Should have the variable';
ok $engine->set_variables(foo => 'baz', whu => 'hi', yo => 'stellar'),
'Set more variables';
is_deeply {$engine->variables}, {foo => 'baz', whu => 'hi', yo => 'stellar'},
'Should have all of the variables';
$engine->clear_variables;
is_deeply [$engine->variables], [], 'Should again have no variables';
##############################################################################
# Test abstract methods.
ok $engine = $CLASS->new({ sqitch => $sqitch }), "Create a $CLASS object again";
for my $abs (qw(
initialized
initialize
register_project
run_file
run_handle
log_deploy_change
log_fail_change
log_revert_change
is_deployed_tag
is_deployed_change
change_id_for
changes_requiring_change
earliest_change_id
latest_change_id
deployed_changes
deployed_changes_since
load_change
name_for_change_id
current_state
current_changes
current_tags
search_events
registered_projects
change_offset_from_id
)) {
throws_ok { $engine->$abs } qr/\Q$CLASS has not implemented $abs()/,
"Should get an unimplemented exception from $abs()"
}
##############################################################################
# Test deploy_change and revert_change.
ok $engine = App::Sqitch::Engine::whu->new( sqitch => $sqitch ),
'Create a subclass name object again';
can_ok $engine, 'deploy_change', 'revert_change';
my $change = App::Sqitch::Plan::Change->new( name => 'foo', plan => $sqitch->plan );
ok $engine->deploy_change($change), 'Deploy a change';
is_deeply $engine->seen, [
['begin_work'],
[run_file => $change->deploy_file ],
[log_deploy_change => $change ],
['finish_work'],
], 'deploy_change should have called the proper methods';
is_deeply +MockOutput->get_info, [[
' + ', 'foo'
]], 'Output should reflect the deployment';
# Make it fail.
$die = 'run_file';
throws_ok { $engine->deploy_change($change) } 'App::Sqitch::X',
'Deploy change with error';
is $@->message, 'AAAH!', 'Error should be from run_file';
is_deeply $engine->seen, [
['begin_work'],
[log_fail_change => $change ],
['finish_work'],
], 'Should have logged change failure';
$die = '';
is_deeply +MockOutput->get_info, [[
' + ', 'foo'
]], 'Output should reflect the deployment, even with failure';
ok $engine->revert_change($change), 'Revert a change';
is_deeply $engine->seen, [
['begin_work'],
[changes_requiring_change => $change ],
[run_file => $change->revert_file ],
[log_revert_change => $change ],
['finish_work'],
], 'revert_change should have called the proper methods';
is_deeply +MockOutput->get_info, [[
' - ', 'foo'
]], 'Output should reflect reversion';
$record_work = 0;
##############################################################################
# Test earliest_change() and latest_change().
chdir 't';
my $plan_file = file qw(sql sqitch.plan);
my $sqitch_old = $sqitch; # Hang on to this because $change does not retain it.
$sqitch = App::Sqitch->new( plan_file => $plan_file );
ok $engine = App::Sqitch::Engine::whu->new( sqitch => $sqitch ),
'Engine with sqitch with plan file';
my $plan = $sqitch->plan;
my @changes = $plan->changes;
$latest_change_id = $changes[0]->id;
is $engine->latest_change, $changes[0], 'Should get proper change from latest_change()';
is_deeply $engine->seen, [[ latest_change_id => undef ]],
'Latest change ID should have been called with no arg';
$latest_change_id = $changes[2]->id;
is $engine->latest_change(2), $changes[2],
'Should again get proper change from latest_change()';
is_deeply $engine->seen, [[ latest_change_id => 2 ]],
'Latest change ID should have been called with offset arg';
$latest_change_id = undef;
$earliest_change_id = $changes[0]->id;
is $engine->earliest_change, $changes[0], 'Should get proper change from earliest_change()';
is_deeply $engine->seen, [[ earliest_change_id => undef ]],
'Earliest change ID should have been called with no arg';
$earliest_change_id = $changes[2]->id;
is $engine->earliest_change(4), $changes[2],
'Should again get proper change from earliest_change()';
is_deeply $engine->seen, [[ earliest_change_id => 4 ]],
'Earliest change ID should have been called with offset arg';
$earliest_change_id = undef;
##############################################################################
# Test _sync_plan()
can_ok $CLASS, '_sync_plan';
$engine->seen;
is $plan->position, -1, 'Plan should start at position -1';
is $engine->start_at, undef, 'start_at should be undef';
ok $engine->_sync_plan, 'Sync the plan';
is $plan->position, -1, 'Plan should still be at position -1';
is $engine->start_at, undef, 'start_at should still be undef';
$plan->position(4);
is_deeply $engine->seen, [['latest_change_id', undef]],
'Should not have updated IDs';
ok $engine->_sync_plan, 'Sync the plan again';
is $plan->position, -1, 'Plan should again be at position -1';
is $engine->start_at, undef, 'start_at should again be undef';
is_deeply $engine->seen, [['latest_change_id', undef]],
'Still should not have updated IDs';
# Have latest_item return a tag.
$latest_change_id = $changes[1]->old_id;
$updated_idx = 2;
ok $engine->_sync_plan, 'Sync the plan to a tag';
is $plan->position, 2, 'Plan should now be at position 1';
is $engine->start_at, 'widgets@beta', 'start_at should now be widgets@beta';
is_deeply $engine->seen, [['latest_change_id', undef], ['_update_ids']],
'Should have updated IDs';
##############################################################################
# Test deploy.
can_ok $CLASS, 'deploy';
$latest_change_id = undef;
$plan->reset;
$engine->seen;
@changes = $plan->changes;
# Mock the deploy methods to log which were called.
my $mock_engine = Test::MockModule->new($CLASS);
my $deploy_meth;
for my $meth (qw(_deploy_all _deploy_by_tag _deploy_by_change)) {
my $orig = $CLASS->can($meth);
$mock_engine->mock($meth => sub {
$deploy_meth = $meth;
$orig->(@_);
});
}
ok $engine->deploy('@alpha'), 'Deploy to @alpha';
is $plan->position, 1, 'Plan should be at position 1';
is_deeply $engine->seen, [
[latest_change_id => undef],
'initialized',
'initialize',
'register_project',
[run_file => $changes[0]->deploy_file],
[log_deploy_change => $changes[0]],
[run_file => $changes[1]->deploy_file],
[log_deploy_change => $changes[1]],
], 'Should have deployed through @alpha';
is $deploy_meth, '_deploy_all', 'Should have called _deploy_all()';
is_deeply +MockOutput->get_info, [
[__x 'Adding metadata tables to {destination}',
destination => $engine->destination,
],
[__x 'Deploying changes through {target} to {destination}',
destination => $engine->destination,
target => $plan->get('@alpha')->format_name_with_tags,
],
[' + ', 'roles'],
[' + ', 'users @alpha'],
], 'Should have seen the output of the deploy to @alpha';
# Try with no need to initialize.
$initialized = 1;
$plan->reset;
ok $engine->deploy('@alpha', 'tag'), 'Deploy to @alpha with tag mode';
is $plan->position, 1, 'Plan should again be at position 1';
is_deeply $engine->seen, [
[latest_change_id => undef],
'initialized',
'register_project',
[run_file => $changes[0]->deploy_file],
[log_deploy_change => $changes[0]],
[run_file => $changes[1]->deploy_file],
[log_deploy_change => $changes[1]],
], 'Should have deployed through @alpha without initialization';
is $deploy_meth, '_deploy_by_tag', 'Should have called _deploy_by_tag()';
is_deeply +MockOutput->get_info, [
[__x 'Deploying changes through {target} to {destination}',
destination => $engine->destination,
target => $plan->get('@alpha')->format_name_with_tags,
],
[' + ', 'roles'],
[' + ', 'users @alpha'],
], 'Should have seen the output of the deploy to @alpha';
# Try a bogus target.
throws_ok { $engine->deploy('nonexistent') } 'App::Sqitch::X',
'Should get an error for an unknown target';
is $@->message, __x(
'Unknown deploy target: "{target}"',
target => 'nonexistent',
), 'The exception should report the unknown target';
is_deeply $engine->seen, [
[latest_change_id => undef],
], 'Only latest_item() should have been called';
# Start with @alpha.
$latest_change_id = ($changes[1]->tags)[0]->id;
ok $engine->deploy('@alpha'), 'Deploy to alpha thrice';
is_deeply $engine->seen, [
[latest_change_id => undef],
], 'Only latest_item() should have been called';
is_deeply +MockOutput->get_info, [
[__x 'Nothing to deploy (already at "{target}"', target => '@alpha'],
], 'Should notify user that already at @alpha';
# Start with widgets.
$latest_change_id = $changes[2]->id;
throws_ok { $engine->deploy('@alpha') } 'App::Sqitch::X',
'Should fail targeting older change';
is $@->ident, 'deploy', 'Should be a "deploy" error';
is $@->message, __ 'Cannot deploy to an earlier target; use "revert" instead',
'It should suggest using "revert"';
is_deeply $engine->seen, [
[latest_change_id => undef],
], 'Should have called latest_item() and latest_tag()';
# Make sure we can deploy everything by change.
$latest_change_id = undef;
$plan->reset;
$plan->add( name => 'lolz', note => 'ha ha' );
@changes = $plan->changes;
ok $engine->deploy(undef, 'change'), 'Deploy everything by change';
is $plan->position, 3, 'Plan should be at position 3';
is_deeply $engine->seen, [
[latest_change_id => undef],
'initialized',
'register_project',
[run_file => $changes[0]->deploy_file],
[log_deploy_change => $changes[0]],
[run_file => $changes[1]->deploy_file],
[log_deploy_change => $changes[1]],
[run_file => $changes[2]->deploy_file],
[log_deploy_change => $changes[2]],
[run_file => $changes[3]->deploy_file],
[log_deploy_change => $changes[3]],
], 'Should have deployed everything';
is $deploy_meth, '_deploy_by_change', 'Should have called _deploy_by_change()';
is_deeply +MockOutput->get_info, [
[__x 'Deploying changes to {destination}', destination => $engine->destination ],
[' + ', 'roles'],
[' + ', 'users @alpha'],
[' + ', 'widgets @beta'],
[' + ', 'lolz'],
], 'Should have seen the output of the deploy to the end';
# If we deploy again, it should be up-to-date.
$latest_change_id = $changes[-1]->id;
throws_ok { $engine->deploy } 'App::Sqitch::X',
'Should catch exception for attempt to deploy to up-to-date DB';
is $@->ident, 'deploy', 'Should be a "deploy" error';
is $@->message, __ 'Nothing to deploy (up-to-date)',
'And the message should reflect up-to-dateness';
is_deeply $engine->seen, [
[latest_change_id => undef],
], 'It should have just fetched the latest change ID';
$latest_change_id = undef;
# Try invalid mode.
throws_ok { $engine->deploy(undef, 'evil_mode') } 'App::Sqitch::X',
'Should fail on invalid mode';
is $@->ident, 'deploy', 'Should be a "deploy" error';
is $@->message, __x('Unknown deployment mode: "{mode}"', mode => 'evil_mode'),
'And the message should reflect the unknown mode';
is_deeply $engine->seen, [
[latest_change_id => undef],
'initialized',
'register_project',
], 'It should have check for initialization';
is_deeply +MockOutput->get_info, [
[__x 'Deploying changes to {destination}', destination => $engine->destination ],
], 'Should have announced destination';
# Try a plan with no changes.
NOSTEPS: {
my $plan_file = file qw(empty.plan);
my $fh = $plan_file->open('>') or die "Cannot open $plan_file: $!";
say $fh '%project=empty';
$fh->close or die "Error closing $plan_file: $!";
END { $plan_file->remove }
my $sqitch = App::Sqitch->new( plan_file => $plan_file );
ok $engine = App::Sqitch::Engine::whu->new( sqitch => $sqitch ),
'Engine with sqitch with no file';
throws_ok { $engine->deploy } 'App::Sqitch::X', 'Should die with no changes';
is $@->message, __"Nothing to deploy (empty plan)",
'Should have the localized message';
is_deeply $engine->seen, [
[latest_change_id => undef],
], 'It should have checked for the latest item';
}
##############################################################################
# Test _deploy_by_change()
$plan->reset;
$mock_engine->unmock('_deploy_by_change');
ok $engine->_deploy_by_change($plan, 1), 'Deploy changewise to index 1';
is_deeply $engine->seen, [
[run_file => $changes[0]->deploy_file],
[log_deploy_change => $changes[0]],
[run_file => $changes[1]->deploy_file],
[log_deploy_change => $changes[1]],
], 'Should changewise deploy to index 2';
is_deeply +MockOutput->get_info, [
[' + ', 'roles'],
[' + ', 'users @alpha'],
], 'Should have seen output of each change';
ok $engine->_deploy_by_change($plan, 3), 'Deploy changewise to index 2';
is_deeply $engine->seen, [
[run_file => $changes[2]->deploy_file],
[log_deploy_change => $changes[2]],
[run_file => $changes[3]->deploy_file],
[log_deploy_change => $changes[3]],
], 'Should changewise deploy to from index 2 to index 3';
is_deeply +MockOutput->get_info, [
[' + ', 'widgets @beta'],
[' + ', 'lolz'],
], 'Should have seen output of changes 2-3';
# Make it die.
$plan->reset;
$die = 'run_file';
throws_ok { $engine->_deploy_by_change($plan, 2) } 'App::Sqitch::X',
'Die in _deploy_by_change';
is $@->message, 'AAAH!', 'It should have died in run_file';
is_deeply $engine->seen, [
[log_fail_change => $changes[0] ],
], 'It should have logged the failure';
is_deeply +MockOutput->get_info, [
[' + ', 'roles'],
], 'Should have seen output for first change';
$die = '';
##############################################################################
# Test _deploy_by_tag().
$plan->reset;
$mock_engine->unmock('_deploy_by_tag');
ok $engine->_deploy_by_tag($plan, 1), 'Deploy tagwise to index 1';
is_deeply $engine->seen, [
[run_file => $changes[0]->deploy_file],
[log_deploy_change => $changes[0]],
[run_file => $changes[1]->deploy_file],
[log_deploy_change => $changes[1]],
], 'Should tagwise deploy to index 1';
is_deeply +MockOutput->get_info, [
[' + ', 'roles'],
[' + ', 'users @alpha'],
], 'Should have seen output of each change';
ok $engine->_deploy_by_tag($plan, 3), 'Deploy tagwise to index 3';
is_deeply $engine->seen, [
[run_file => $changes[2]->deploy_file],
[log_deploy_change => $changes[2]],
[run_file => $changes[3]->deploy_file],
[log_deploy_change => $changes[3]],
], 'Should tagwise deploy from index 2 to index 3';
is_deeply +MockOutput->get_info, [
[' + ', 'widgets @beta'],
[' + ', 'lolz'],
], 'Should have seen output of changes 3-3';
# Add another couple of changes.
$plan->add(name => 'tacos' );
$plan->add(name => 'curry' );
@changes = $plan->changes;
# Make it die.
$plan->position(1);
my $mock_whu = Test::MockModule->new('App::Sqitch::Engine::whu');
$mock_whu->mock(log_deploy_change => sub { hurl 'ROFL' if $_[1] eq $changes[-1] });
throws_ok { $engine->_deploy_by_tag($plan, $#changes) } 'App::Sqitch::X',
'Die in log_deploy_change';
is $@->message, __('Deploy failed'), 'Should get final deploy failure message';
is_deeply $engine->seen, [
[run_file => $changes[2]->deploy_file],
[run_file => $changes[3]->deploy_file],
[run_file => $changes[4]->deploy_file],
[run_file => $changes[5]->deploy_file],
[run_file => $changes[5]->revert_file],
[log_fail_change => $changes[5] ],
[changes_requiring_change => $changes[4] ],
[run_file => $changes[4]->revert_file],
[log_revert_change => $changes[4]],
[changes_requiring_change => $changes[3] ],
[run_file => $changes[3]->revert_file],
[log_revert_change => $changes[3]],
], 'It should have reverted back to the last deployed tag';
is_deeply +MockOutput->get_info, [
[' + ', 'widgets @beta'],
[' + ', 'lolz'],
[' + ', 'tacos'],
[' + ', 'curry'],
[' - ', 'curry'],
[' - ', 'tacos'],
[' - ', 'lolz'],
], 'Should have seen deploy and revert messages';
is_deeply +MockOutput->get_vent, [
['ROFL'],
[__x 'Reverting to {target}', target => 'widgets @beta']
], 'The original error should have been vented';
$mock_whu->unmock('log_deploy_change');
# Now have it fail back to the beginning.
$plan->reset;
$mock_whu->mock(run_file => sub { die 'ROFL' if $_[1]->basename eq 'users.sql' });
throws_ok { $engine->_deploy_by_tag($plan, $plan->count -1 ) } 'App::Sqitch::X',
'Die in _deploy_by_tag again';
is $@->message, __('Deploy failed'), 'Should again get final deploy failure message';
is_deeply $engine->seen, [
[log_deploy_change => $changes[0]],
[log_fail_change => $changes[1]],
[changes_requiring_change => $changes[0] ],
[log_revert_change => $changes[0]],
], 'Should have logged back to the beginning';
is_deeply +MockOutput->get_info, [
[' + ', 'roles'],
[' + ', 'users @alpha'],
[' - ', 'roles'],
], 'Should have seen deploy and revert messages';
my $vented = MockOutput->get_vent;
is @{ $vented }, 2, 'Should have one vented message';
my $errmsg = shift @{ $vented->[0] };
like $errmsg, qr/^ROFL\b/, 'And it should be the underlying error';
is_deeply $vented, [
[],
[__ 'Reverting all changes'],
], 'And it should had notified that all changes were reverted';
# Add a change and deploy to that, to make sure it rolls back any changes since
# last tag.
$plan->add(name => 'dr_evil' );
@changes = $plan->changes;
$plan->reset;
$mock_whu->mock(run_file => sub { hurl 'ROFL' if $_[1]->basename eq 'dr_evil.sql' });
throws_ok { $engine->_deploy_by_tag($plan, $plan->count -1 ) } 'App::Sqitch::X',
'Die in _deploy_by_tag yet again';
is $@->message, __('Deploy failed'), 'Should die "Deploy failed" again';
is_deeply $engine->seen, [
[log_deploy_change => $changes[0]],
[log_deploy_change => $changes[1]],
[log_deploy_change => $changes[2]],
[log_deploy_change => $changes[3]],
[log_deploy_change => $changes[4]],
[log_deploy_change => $changes[5]],
[log_fail_change => $changes[6]],
[changes_requiring_change => $changes[5] ],
[log_revert_change => $changes[5] ],
[changes_requiring_change => $changes[4] ],
[log_revert_change => $changes[4] ],
[changes_requiring_change => $changes[3] ],
[log_revert_change => $changes[3] ],
], 'Should have reverted back to last tag';
is_deeply +MockOutput->get_info, [
[' + ', 'roles'],
[' + ', 'users @alpha'],
[' + ', 'widgets @beta'],
[' + ', 'lolz'],
[' + ', 'tacos'],
[' + ', 'curry'],
[' + ', 'dr_evil'],
[' - ', 'curry'],
[' - ', 'tacos'],
[' - ', 'lolz'],
], 'Should have user change reversion messages';
is_deeply +MockOutput->get_vent, [
['ROFL'],
[__x 'Reverting to {target}', target => 'widgets @beta']
], 'Should see underlying error and reversion message';
# Make it choke on change reversion.
$mock_whu->unmock_all;
$die = '';
$plan->reset;
$mock_whu->mock(run_file => sub {
hurl 'ROFL' if $_[1] eq $changes[1]->deploy_file;
hurl 'BARF' if $_[1] eq $changes[0]->revert_file;
});
$mock_whu->mock(start_at => 'whatever');
throws_ok { $engine->_deploy_by_tag($plan, $plan->count -1 ) } 'App::Sqitch::X',
'Die in _deploy_by_tag again';
is $@->message, __('Deploy failed'), 'Should once again get final deploy failure message';
is_deeply $engine->seen, [
[log_deploy_change => $changes[0] ],
[log_fail_change => $changes[1] ],
[changes_requiring_change => $changes[0] ],
], 'Should have tried to revert one change';
is_deeply +MockOutput->get_info, [
[' + ', 'roles'],
[' + ', 'users @alpha'],
[' - ', 'roles'],
], 'Should have seen revert message';
is_deeply +MockOutput->get_vent, [
['ROFL'],
[__x 'Reverting to {target}', target => 'whatever'],
['BARF'],
[__ 'The schema will need to be manually repaired']
], 'Should get reversion failure message';
$mock_whu->unmock_all;
##############################################################################
# Test _deploy_all().
$plan->reset;
$mock_engine->unmock('_deploy_all');
ok $engine->_deploy_all($plan, 1), 'Deploy all to index 1';
is_deeply $engine->seen, [
[run_file => $changes[0]->deploy_file],
[log_deploy_change => $changes[0]],
[run_file => $changes[1]->deploy_file],
[log_deploy_change => $changes[1]],
], 'Should tagwise deploy to index 1';
is_deeply +MockOutput->get_info, [
[' + ', 'roles'],
[' + ', 'users @alpha'],
], 'Should have seen output of each change';
ok $engine->_deploy_all($plan, 2), 'Deploy tagwise to index 2';
is_deeply $engine->seen, [
[run_file => $changes[2]->deploy_file],
[log_deploy_change => $changes[2]],
], 'Should tagwise deploy to from index 1 to index 2';
is_deeply +MockOutput->get_info, [
[' + ', 'widgets @beta'],
], 'Should have seen output of changes 3-4';
# Make it die.
$plan->reset;
$mock_whu->mock(log_deploy_change => sub { hurl 'ROFL' if $_[1] eq $changes[2] });
throws_ok { $engine->_deploy_all($plan, 3) } 'App::Sqitch::X',
'Die in _deploy_all';
is $@->message, __('Deploy failed'), 'Should get final deploy failure message';
$mock_whu->unmock('log_deploy_change');
is_deeply $engine->seen, [
[run_file => $changes[0]->deploy_file],
[run_file => $changes[1]->deploy_file],
[run_file => $changes[2]->deploy_file],
[run_file => $changes[2]->revert_file],
[log_fail_change => $changes[2]],
[changes_requiring_change => $changes[1] ],
[run_file => $changes[1]->revert_file],
[log_revert_change => $changes[1]],
[changes_requiring_change => $changes[0] ],
[run_file => $changes[0]->revert_file],
[log_revert_change => $changes[0]],
], 'It should have logged up to the failure';
is_deeply +MockOutput->get_info, [
[' + ', 'roles'],
[' + ', 'users @alpha'],
[' + ', 'widgets @beta'],
[' - ', 'widgets @beta'],
[' - ', 'users @alpha'],
[' - ', 'roles'],
], 'Should have seen deploy and revert messages';
is_deeply +MockOutput->get_vent, [
['ROFL'],
[__ 'Reverting all changes'],
], 'The original error should have been vented';
$die = '';
# Now have it fail on a later change, should still go all the way back.
$plan->reset;
$mock_whu->mock(run_file => sub { hurl 'ROFL' if $_[1]->basename eq 'widgets.sql' });
throws_ok { $engine->_deploy_all($plan, $plan->count -1 ) } 'App::Sqitch::X',
'Die in _deploy_all again';
is $@->message, __('Deploy failed'), 'Should again get final deploy failure message';
is_deeply $engine->seen, [
[log_deploy_change => $changes[0]],
[log_deploy_change => $changes[1]],
[log_fail_change => $changes[2]],
[changes_requiring_change => $changes[1] ],
[log_revert_change => $changes[1]],
[changes_requiring_change => $changes[0] ],
[log_revert_change => $changes[0]],
], 'Should have reveted all changes and tags';
is_deeply +MockOutput->get_info, [
[' + ', 'roles'],
[' + ', 'users @alpha'],
[' + ', 'widgets @beta'],
[' - ', 'users @alpha'],
[' - ', 'roles'],
], 'Should see all changes revert';
is_deeply +MockOutput->get_vent, [
['ROFL'],
[__ 'Reverting all changes'],
], 'Should notifiy user of error and rollback';
# Die when starting from a later point.
$plan->position(2);
$engine->start_at('@alpha');
$mock_whu->mock(run_file => sub { hurl 'ROFL' if $_[1]->basename eq 'dr_evil.sql' });
throws_ok { $engine->_deploy_all($plan, $plan->count -1 ) } 'App::Sqitch::X',
'Die in _deploy_all on the last change';
is $@->message, __('Deploy failed'), 'Should once again get final deploy failure message';
is_deeply $engine->seen, [
[log_deploy_change => $changes[3]],
[log_deploy_change => $changes[4]],
[log_deploy_change => $changes[5]],
[log_fail_change => $changes[6]],
[changes_requiring_change => $changes[5] ],
[log_revert_change => $changes[5]],
[changes_requiring_change => $changes[4] ],
[log_revert_change => $changes[4]],
[changes_requiring_change => $changes[3] ],
[log_revert_change => $changes[3]],
], 'Should have deployed to dr_evil and revered down to @alpha';
is_deeply +MockOutput->get_info, [
[' + ', 'lolz'],
[' + ', 'tacos'],
[' + ', 'curry'],
[' + ', 'dr_evil'],
[' - ', 'curry'],
[' - ', 'tacos'],
[' - ', 'lolz'],
], 'Should see changes revert back to @alpha';
is_deeply +MockOutput->get_vent, [
['ROFL'],
[__x 'Reverting to {target}', target => '@alpha'],
], 'Should notifiy user of error and rollback to @alpha';
$mock_whu->unmock_all;
##############################################################################
# Test is_deployed().
my $tag = App::Sqitch::Plan::Tag->new(
name => 'foo',
change => $change,
plan => $sqitch->plan,
);
$is_deployed_tag = $is_deployed_change = 1;
ok $engine->is_deployed($tag), 'Test is_deployed(tag)';
is_deeply $engine->seen, [
[is_deployed_tag => $tag],
], 'It should have called is_deployed_tag()';
ok $engine->is_deployed($change), 'Test is_deployed(change)';
is_deeply $engine->seen, [
[is_deployed_change => $change],
], 'It should have called is_deployed_change()';
##############################################################################
# Test deploy_change.
can_ok $engine, 'deploy_change';
ok $engine->deploy_change($change), 'Deploy a change';
is_deeply $engine->seen, [
[run_file => $change->deploy_file],
[log_deploy_change => $change],
], 'It should have been deployed';
is_deeply +MockOutput->get_info, [
[' + ', $change->format_name]
], 'Should have shown change name';
my $make_deps = sub {
my $conflicts = shift;
return map {
my $dep = App::Sqitch::Plan::Depend->new(
change => $_,
plan => $plan,
conflicts => $conflicts,
);
$dep;
} @_;
};
CONFLICTS: {
# Die on conflicts.
my $mock_depend = Test::MockModule->new('App::Sqitch::Plan::Depend');
$mock_depend->mock(id => sub { undef });
my @conflicts = $make_deps->( 1, qw(foo bar) );
my $change = App::Sqitch::Plan::Change->new(
name => 'foo',
plan => $sqitch->plan,
conflicts => \@conflicts,
);
push @resolved, '2342', '253245';
throws_ok { $engine->deploy_change($change) } 'App::Sqitch::X',
'Conflict should throw exception';
is $@->ident, 'deploy', 'Should be a "deploy" error';
is $@->message, __nx(
'Conflicts with previously deployed change: {changes}',
'Conflicts with previously deployed changes: {changes}',
scalar 2,
changes => 'foo bar',
), 'Should have localized message about conflicts';
is_deeply $engine->seen, [
[ change_id_for => {
change_id => undef,
change => 'foo',
tag => undef,
project => 'sql',
} ],
[ change_id_for => {
change_id => undef,
change => 'bar',
tag => undef,
project => 'sql',
} ],
], 'Should have called change_id_for() twice';
is_deeply +MockOutput->get_info, [
[' + ', $change->format_name]
], 'Should again have shown change name';
is_deeply [ map { $_->resolved_id } @conflicts ], [undef, undef],
'Conflicting dependencies should have no resolved IDs';
}
REQUIRES: {
# Die on missing dependencies.
my $mock_depend = Test::MockModule->new('App::Sqitch::Plan::Depend');
$mock_depend->mock(id => sub { undef });
my @requires = $make_deps->( 0, qw(foo bar) );
my $change = App::Sqitch::Plan::Change->new(
name => 'foo',
plan => $sqitch->plan,
requires => \@requires,
);
push @resolved, undef, undef;
throws_ok { $engine->deploy_change($change) } 'App::Sqitch::X',
'Missing dependencies should throw exception';
is $@->ident, 'deploy', 'Should be another "deploy" error';
is $@->message, __nx(
'Missing required change: {changes}',
'Missing required changes: {changes}',
scalar 2,
changes => 'foo bar',
), 'Should have localized message missing dependencies';
is_deeply $engine->seen, [
[ change_id_for => {
change_id => undef,
change => 'foo',
tag => undef,
project => 'sql',
} ],
[ change_id_for => {
change_id => undef,
change => 'bar',
tag => undef,
project => 'sql',
} ],
], 'Should have called check_requires';
is_deeply +MockOutput->get_info, [
[' + ', $change->format_name]
], 'Should again have shown change name';
is_deeply [ map { $_->resolved_id } @requires ], [undef, undef],
'Missing requirements should not have resolved';
}
DEPLOYDIE: {
my $mock_depend = Test::MockModule->new('App::Sqitch::Plan::Depend');
$mock_depend->mock(id => sub { undef });
# Now make it die on the actual deploy.
$die = 'log_deploy_change';
my @requires = $make_deps->( 0, qw(foo bar) );
my @conflicts = $make_deps->( 1, qw(dr_evil) );
my $change = App::Sqitch::Plan::Change->new(
name => 'foo',
plan => $sqitch->plan,
requires => \@requires,
conflicts => \@conflicts,
);
@resolved = (0, '232213', '2352354');
throws_ok { $engine->deploy_change($change) } 'App::Sqitch::X',
'Shuld die on deploy failure';
is $@->message, __ 'Deploy failed', 'Should be told the deploy failed';
is_deeply $engine->seen, [
[ change_id_for => {
change_id => undef,
change => 'dr_evil',
tag => undef,
project => 'sql',
} ],
[ change_id_for => {
change_id => undef,
change => 'foo',
tag => undef,
project => 'sql',
} ],
[ change_id_for => {
change_id => undef,
change => 'bar',
tag => undef,
project => 'sql',
} ],
[run_file => $change->deploy_file],
[run_file => $change->revert_file],
[log_fail_change => $change],
], 'It should failed to have been deployed';
is_deeply +MockOutput->get_vent, [
['AAAH!'],
], 'Should have vented the original error';
is_deeply +MockOutput->get_info, [
[' + ', $change->format_name],
[' - ', $change->format_name],
], 'Should have shown change name';
is_deeply [ map { $_->resolved_id } @conflicts ], [undef],
'Non-conflicting dependency should not have resolved';
is_deeply [ map { $_->resolved_id } @requires ], ['232213', '2352354'],
'Satisffied requirements should have resolved';
$die = '';
}
##############################################################################
# Test revert_change().
can_ok $engine, 'revert_change';
ok $engine->revert_change($change), 'Revert the change';
is_deeply $engine->seen, [
[changes_requiring_change => $change ],
[run_file => $change->revert_file],
[log_revert_change => $change],
], 'It should have been reverted';
is_deeply +MockOutput->get_info, [
[' - ', $change->format_name]
], 'Should have shown reverted change name';
# Have revert change fail with requiring changes.
@requiring = (
{
change_id => '23234234',
change => 'blah',
project => 'empty',
asof_tag => undef,
},
{
change_id => '635462345',
change => 'urf',
project => 'elsewhere',
asof_tag => '@beta1',
},
);
throws_ok { $engine->revert_change($change) } 'App::Sqitch::X',
'Should get error reverting change others depend on';
is $@->ident, 'revert', 'Dependent error ident should be "revert"';
is $@->message, __nx(
'Required by currently deployed change: {changes}',
'Required by currently deployed changes: {changes}',
scalar 2,
changes => 'blah elsewhere:urf@beta1'
), 'Dependent error message should be correct';
is_deeply $engine->seen, [
[changes_requiring_change => $change ],
], 'It should have check for requiring changes';
is_deeply +MockOutput->get_info, [
[' - ', $change->format_name]
], 'Should have shown attempted revert change name';
@requiring = ();
##############################################################################
# Test revert().
can_ok $engine, 'revert';
my $mock_sqitch = Test::MockModule->new('App::Sqitch');
$mock_sqitch->mock(plan => $plan);
# Start with no deployed IDs.
@deployed_changes = ();
throws_ok { $engine->revert } 'App::Sqitch::X',
'Should get exception for no changes to revert';
is $@->ident, 'revert', 'Should be a revert exception';
is $@->message, __ 'Nothing to revert (nothing deployed)',
'Should have notified that there is nothing to revert';
is $@->exitval, 1, 'Exit val should be 1';
is_deeply $engine->seen, [
[deployed_changes => undef],
], 'It should only have called deployed_changes()';
is_deeply +MockOutput->get_info, [], 'Nothing should have been output';
# Try reverting to an unknown change.
throws_ok { $engine->revert('nonexistent') } 'App::Sqitch::X',
'Revert should die on unknown change';
is $@->ident, 'revert', 'Should be another "revert" error';
is $@->message, __x(
'Unknown revert target: "{target}"',
target => 'nonexistent',
), 'The message should mention it is an unknown target';
is_deeply $engine->seen, [['change_id_for', {
change_id => undef,
change => 'nonexistent',
tag => undef,
project => 'sql',
}]], 'Should have called change_id_for() with change name';
is_deeply +MockOutput->get_info, [], 'Nothing should have been output';
# Try reverting to an unknown change ID.
throws_ok { $engine->revert('8d77c5f588b60bc0f2efcda6369df5cb0177521d') } 'App::Sqitch::X',
'Revert should die on unknown change ID';
is $@->ident, 'revert', 'Should be another "revert" error';
is $@->message, __x(
'Unknown revert target: "{target}"',
target => '8d77c5f588b60bc0f2efcda6369df5cb0177521d',
), 'The message should mention it is an unknown target';
is_deeply $engine->seen, [['change_id_for', {
change_id => '8d77c5f588b60bc0f2efcda6369df5cb0177521d',
change => undef,
tag => undef,
project => 'sql',
}]], 'Shoudl have called change_id_for() with change ID';
is_deeply +MockOutput->get_info, [], 'Nothing should have been output';
# Revert an undeployed target.
throws_ok { $engine->revert('@alpha') } 'App::Sqitch::X',
'Revert should die on undeployed change';
is $@->ident, 'revert', 'Should be another "revert" error';
is $@->message, __x(
'Target not deployed: "{target}"',
target => '@alpha',
), 'The message should mention that the target is not deployed';
is_deeply $engine->seen, [['change_id_for', {
change => '',
change_id => undef,
tag => 'alpha',
project => 'sql',
}]], 'change_id_for';
is_deeply +MockOutput->get_info, [], 'Nothing should have been output';
# Revert to a point with no following changes.
$offset_change = $changes[0];
push @resolved => $offset_change->id;
throws_ok { $engine->revert($changes[0]->id) } 'App::Sqitch::X',
'Should get error reverting when no subsequent changes';
is $@->ident, 'revert', 'No subsequent change error ident should be "revert"';
is $@->exitval, 1, 'No subsequent change error exitval should be 1';
is $@->message, __x(
'No changes deployed since: "{target}"',
target => $changes[0]->id,
), 'No subsequent change error message should be correct';
is_deeply $engine->seen, [
[change_id_for => {
change_id => $changes[0]->id,
change => undef,
tag => undef,
project => 'sql',
}],
[ change_offset_from_id => [$changes[0]->id, 0] ],
[deployed_changes_since => $changes[0]],
], 'Should have called change_id_for and deployed_changes_since';
# Revert with nothing deployed.
throws_ok { $engine->revert } 'App::Sqitch::X',
'Should get error for known but undeployed change';
is $@->ident, 'revert', 'No changes error should be "revert"';
is $@->exitval, 1, 'No changes exitval should be 1';
is $@->message, __ 'Nothing to revert (nothing deployed)',
'No changes message should be correct';
is_deeply $engine->seen, [
[deployed_changes => undef],
], 'Should have called deployed_changes';
# Now revert from a deployed change.
my @dbchanges;
@deployed_changes = map {
my $plan_change = $_;
my $params = {
id => $plan_change->id,
name => $plan_change->name,
project => $plan_change->project,
note => $plan_change->note,
planner_name => $plan_change->planner_name,
planner_email => $plan_change->planner_email,
timestamp => $plan_change->timestamp,
tags => [ map { $_->name } $plan_change->tags ],
};
push @dbchanges => my $db_change = App::Sqitch::Plan::Change->new(
plan => $plan,
%{ $params },
);
$db_change->add_tag( App::Sqitch::Plan::Tag->new(
name => $_->name, plan => $plan, change => $db_change
) ) for $plan_change->tags;
$db_change->tags; # Autovivify _tags For changes with no tags.
$params;
} @changes[0..3];
MockOutput->ask_y_n_returns(1);
ok $engine->revert, 'Revert all changes';
is_deeply $engine->seen, [
[deployed_changes => undef],
[changes_requiring_change => $dbchanges[3] ],
[run_file => $dbchanges[3]->revert_file ],
[log_revert_change => $dbchanges[3] ],
[changes_requiring_change => $dbchanges[2] ],
[run_file => $dbchanges[2]->revert_file ],
[log_revert_change => $dbchanges[2] ],
[changes_requiring_change => $dbchanges[1] ],
[run_file => $dbchanges[1]->revert_file ],
[log_revert_change => $dbchanges[1] ],
[changes_requiring_change => $dbchanges[0] ],
[run_file => $dbchanges[0]->revert_file ],
[log_revert_change => $dbchanges[0] ],
], 'Should have reverted the changes in reverse order';
is_deeply +MockOutput->get_ask_y_n, [
[__x(
'Revert all changes from {destination}?',
destination => $engine->destination,
), 'Yes'],
], 'Should have prompt to revert all changes';
is_deeply +MockOutput->get_info, [
[' - ', 'lolz'],
[' - ', 'widgets @beta'],
[' - ', 'users @alpha'],
[' - ', 'roles'],
], 'It should have said it was reverting all changes and listed them';
# Should exit if the revert is declined.
MockOutput->ask_y_n_returns(0);
throws_ok { $engine->revert } 'App::Sqitch::X', 'Should abort declined revert';
is $@->ident, 'revert', 'Declined revert ident should be "revert"';
is $@->exitval, 1, 'Should have exited with value 1';
is $@->message, __ 'Nothing reverted', 'Should have exited with proper message';
is_deeply $engine->seen, [
[deployed_changes => undef],
], 'Should have called deployed_changes only';
is_deeply +MockOutput->get_ask_y_n, [
[__x(
'Revert all changes from {destination}?',
destination => $engine->destination,
), 'Yes'],
], 'Should have prompt to revert all changes';
is_deeply +MockOutput->get_info, [
], 'It should have emitted nothing else';
# Revert all changes with no prompt.
MockOutput->ask_y_n_returns(1);
my $no_prompt = 1;
$mock_engine->mock( no_prompt => sub { $no_prompt } );
ok $engine->revert, 'Revert all changes with no prompt';
is_deeply $engine->seen, [
[deployed_changes => undef],
[changes_requiring_change => $dbchanges[3] ],
[run_file => $dbchanges[3]->revert_file ],
[log_revert_change => $dbchanges[3] ],
[changes_requiring_change => $dbchanges[2] ],
[run_file => $dbchanges[2]->revert_file ],
[log_revert_change => $dbchanges[2] ],
[changes_requiring_change => $dbchanges[1] ],
[run_file => $dbchanges[1]->revert_file ],
[log_revert_change => $dbchanges[1] ],
[changes_requiring_change => $dbchanges[0] ],
[run_file => $dbchanges[0]->revert_file ],
[log_revert_change => $dbchanges[0] ],
], 'Should have reverted the changes in reverse order';
is_deeply +MockOutput->get_ask_y_n, [], 'Should have no prompt';
is_deeply +MockOutput->get_info, [
[__x(
'Reverting all changes from {destination}',
destination => $engine->destination,
)],
[' - ', 'lolz'],
[' - ', 'widgets @beta'],
[' - ', 'users @alpha'],
[' - ', 'roles'],
], 'It should have said it was reverting all changes and listed them';
# Now just revert to an earlier change.
$no_prompt = 0;
$offset_change = $dbchanges[1];
push @resolved => $offset_change->id;
@deployed_changes = @deployed_changes[2..3];
ok $engine->revert('@alpha'), 'Revert to @alpha';
is_deeply $engine->seen, [
[change_id_for => { change_id => undef, change => '', tag => 'alpha', project => 'sql' }],
[ change_offset_from_id => [$dbchanges[1]->id, 0] ],
[deployed_changes_since => $dbchanges[1]],
[changes_requiring_change => $dbchanges[3] ],
[run_file => $dbchanges[3]->revert_file ],
[log_revert_change => $dbchanges[3] ],
[changes_requiring_change => $dbchanges[2] ],
[run_file => $dbchanges[2]->revert_file ],
[log_revert_change => $dbchanges[2] ],
], 'Should have reverted only changes after @alpha';
is_deeply +MockOutput->get_ask_y_n, [
[__x(
'Revert changes to {target} from {destination}?',
destination => $engine->destination,
target => $dbchanges[1]->format_name_with_tags,
), 'Yes'],
], 'Should have prompt to revert to target';
is_deeply +MockOutput->get_info, [
[' - ', 'lolz'],
[' - ', 'widgets @beta'],
], 'Output should show what it reverts to';
MockOutput->ask_y_n_returns(0);
$offset_change = $dbchanges[1];
push @resolved => $offset_change->id;
throws_ok { $engine->revert('@alpha') } 'App::Sqitch::X',
'Should abort declined revert to @alpha';
is $@->ident, 'revert', 'Declined revert ident should be "revert"';
is $@->exitval, 1, 'Should have exited with value 1';
is $@->message, __ 'Nothing reverted', 'Should have exited with proper message';
is_deeply $engine->seen, [
[change_id_for => { change_id => undef, change => '', tag => 'alpha', project => 'sql' }],
[change_offset_from_id => [$dbchanges[1]->id, 0] ],
[deployed_changes_since => $dbchanges[1]],
], 'Should have called revert methods';
is_deeply +MockOutput->get_ask_y_n, [
[__x(
'Revert changes to {target} from {destination}?',
target => $dbchanges[1]->format_name_with_tags,
destination => $engine->destination,
), 'Yes'],
], 'Should have prompt to revert to @alpha';
is_deeply +MockOutput->get_info, [
], 'It should have emitted nothing else';
# Try to revert just the last change with no prompt
MockOutput->ask_y_n_returns(1);
$no_prompt = 1;
$offset_change = $dbchanges[-1];
push @resolved => $offset_change->id;
@deployed_changes = $deployed_changes[-1];
ok $engine->revert('@HEAD^'), 'Revert to @HEAD^';
is_deeply $engine->seen, [
[change_id_for => { change_id => undef, change => '', tag => 'HEAD', project => 'sql' }],
[change_offset_from_id => [$dbchanges[-1]->id, -1] ],
[deployed_changes_since => $dbchanges[-1]],
[changes_requiring_change => $dbchanges[-1] ],
[run_file => $dbchanges[-1]->revert_file ],
[log_revert_change => $dbchanges[-1] ],
], 'Should have reverted one changes for @HEAD^';
is_deeply +MockOutput->get_ask_y_n, [], 'Should have no prompt';
is_deeply +MockOutput->get_info, [
[__x(
'Reverting changes to {target} from {destination}',
destination => $engine->destination,
target => $dbchanges[-1]->format_name_with_tags,
)],
[' - ', $dbchanges[-1]->format_name_with_tags],
], 'Output should show what it reverts to';
##############################################################################
# Test change_id_for_depend().
can_ok $CLASS, 'change_id_for_depend';
$offset_change = $dbchanges[1];
my ($dep) = $make_deps->( 1, 'foo' );
throws_ok { $engine->change_id_for_depend( $dep ) } 'App::Sqitch::X',
'Should get error from change_id_for_depend when change not in plan';
is $@->ident, 'plan', 'Should get ident "plan" from change_id_for_depend';
is $@->message, __x(
'Unable to find change "{change}" in plan {file}',
change => $dep->key_name,
file => $sqitch->plan_file,
), 'Should have proper message from change_id_for_depend error';
PLANOK: {
my $mock_depend = Test::MockModule->new('App::Sqitch::Plan::Depend');
$mock_depend->mock(id => sub { undef });
$mock_depend->mock(change => sub { undef });
throws_ok { $engine->change_id_for_depend( $dep ) } 'App::Sqitch::X',
'Should get error from change_id_for_depend when no ID';
is $@->ident, 'engine', 'Should get ident "engine" when no ID';
is $@->message, __x(
'Invalid dependency: {dependency}',
dependency => $dep->as_string,
), 'Should have proper messag from change_id_for_depend error';
# Let it have the change.
$mock_depend->unmock('change');
push @resolved => $changes[1]->id;
is $engine->change_id_for_depend( $dep ), $changes[1]->id,
'Get a change id';
is_deeply $engine->seen, [
[change_id_for => {
change_id => $dep->id,
change => $dep->change,
tag => $dep->tag,
project => $dep->project,
}],
], 'Should have passed dependency params to change_id_for()';
}
##############################################################################
# Test find_change().
can_ok $CLASS, 'find_change';
push @resolved => $dbchanges[1]->id;
is $engine->find_change(
change_id => $resolved[0],
change => 'hi',
tag => 'yo',
), $dbchanges[1], 'find_change() should work';
is_deeply $engine->seen, [
[change_id_for => {
change_id => $dbchanges[1]->id,
change => 'hi',
tag => 'yo',
project => 'sql',
}],
[change_offset_from_id => [ $dbchanges[1]->id, undef ]],
], 'Its parameters should have been passed to change_id_for and change_offset_from_id';
# Pass a project and an ofset.
push @resolved => $dbchanges[1]->id;
is $engine->find_change(
change => 'hi',
offset => 1,
project => 'fred',
), $dbchanges[1], 'find_change() should work';
is_deeply $engine->seen, [
[change_id_for => {
change_id => undef,
change => 'hi',
tag => undef,
project => 'fred',
}],
[change_offset_from_id => [ $dbchanges[1]->id, 1 ]],
], 'Project and offset should have been passed off';