App-Sqitch
view release on metacpan or search on metacpan
#!/usr/bin/perl -w
use strict;
use warnings;
use utf8;
use Test::More tests => 243;
#use Test::More 'no_plan';
use App::Sqitch;
use App::Sqitch::Target;
use Locale::TextDomain qw(App-Sqitch);
use Path::Class;
use Test::Exception;
use Test::Warn;
use Test::Dir;
use File::Temp 'tempdir';
use Test::File qw(file_not_exists_ok file_exists_ok);
use Test::File::Contents 0.05;
use File::Path qw(make_path remove_tree);
use Test::NoWarnings 0.083;
use lib 't/lib';
use MockOutput;
use TestConfig;
my $CLASS = 'App::Sqitch::Command::add';
my $config = TestConfig->new(
'core.engine' => 'pg',
'core.top_dir' => dir('test-add')->stringify,
);
ok my $sqitch = App::Sqitch->new(config => $config),
'Load a sqitch sqitch object';
isa_ok my $add = App::Sqitch::Command->load({
sqitch => $sqitch,
command => 'add',
config => $config,
args => [],
}), $CLASS, 'add command';
my $target = $add->default_target;
sub dep($$) {
my $dep = App::Sqitch::Plan::Depend->new(
%{ App::Sqitch::Plan::Depend->parse( $_[1] ) },
plan => $add->default_target->plan,
conflicts => $_[0],
);
$dep->project;
return $dep;
}
can_ok $CLASS, qw(
options
requires
conflicts
variables
template_name
template_directory
with_scripts
templates
open_editor
configure
execute
_config_templates
all_templates
_slurp
_add
does
);
ok $CLASS->does("App::Sqitch::Role::ContextCommand"),
"$CLASS does ContextCommand";
is_deeply [$CLASS->options], [qw(
change-name|change|c=s
requires|r=s@
conflicts|x=s@
is_deeply $add->all_templates($tname), {
deploy => $tmp_dir->file('deploy/pg.tmpl'),
whatev => $tmp_dir->file('whatev/pg.tmpl'),
revert => file('etc/templates/revert/pg.tmpl'),
verify => file('etc/templates/verify/pg.tmpl'),
}, 'Template dir files should override others';
# Add in configured files.
ok $add = $CLASS->new(
sqitch => $sqitch,
template_directory => $tmp_dir,
templates => {
foo => file('foo'),
verify => file('verify'),
deploy => file('deploy'),
},
), 'Add object with configured templates';
is_deeply $add->all_templates($tname), {
deploy => file('deploy'),
verify => file('verify'),
foo => file('foo'),
whatev => $tmp_dir->file('whatev/pg.tmpl'),
revert => file('etc/templates/revert/pg.tmpl'),
}, 'Template dir files should override others';
# Should die when missing files.
$sysdir = $usrdir;
for my $script (qw(deploy revert verify)) {
ok $add = $CLASS->new(
sqitch => $sqitch,
with_scripts => { deploy => 0, revert => 0, verify => 0, $script => 1 },
), "Add object requiring $script template";
throws_ok { $add->all_templates($tname) } 'App::Sqitch::X',
"Should get error for missing $script template";
is $@->ident, 'add', qq{Missing $script template ident should be "add"};
is $@->message, __x(
'Cannot find {script} template',
script => $script,
), "Missing $script template message should be correct";
}
##############################################################################
# Test _slurp().
$tmpl = file(qw(etc templates deploy pg.tmpl));
is $ { $add->_slurp($tmpl)}, contents_of $tmpl,
'_slurp() should load a reference to file contents';
##############################################################################
# Test _add().
my $test_add = sub {
my $engine = shift;
make_path 'test-add';
my $fn = $target->plan_file;
open my $fh, '>', $fn or die "Cannot open $fn: $!";
say $fh "%project=add\n\n";
close $fh or die "Error closing $fn: $!";
END { remove_tree 'test-add' };
my $out = file 'test-add', 'sqitch_change_test.sql';
file_not_exists_ok $out;
ok my $add = $CLASS->new(
sqitch => $sqitch,
template_directory => $tmpldir,
), 'Create add command';
ok $add->_add('sqitch_change_test', $out, $tmpl, 'sqlite', 'add'),
'Write out a script';
file_exists_ok $out;
file_contents_is $out, <<EOF, 'The template should have been evaluated';
-- Deploy add:sqitch_change_test to sqlite
BEGIN;
-- XXX Add DDLs here.
COMMIT;
EOF
is_deeply +MockOutput->get_info, [[__x 'Created {file}', file => $out ]],
'Info should show $out created';
unlink $out;
# Try with requires and conflicts.
ok $add = $CLASS->new(
sqitch => $sqitch,
requires => [qw(foo bar)],
conflicts => ['baz'],
template_directory => $tmpldir,
), 'Create add cmd with requires and conflicts';
$out = file 'test-add', 'another_change_test.sql';
ok $add->_add('another_change_test', $out, $tmpl, 'sqlite', 'add'),
'Write out a script with requires and conflicts';
is_deeply +MockOutput->get_info, [[__x 'Created {file}', file => $out ]],
'Info should show $out created';
file_contents_is $out, <<EOF, 'The template should have been evaluated with requires and conflicts';
-- Deploy add:another_change_test to sqlite
-- requires: foo
-- requires: bar
-- conflicts: baz
BEGIN;
-- XXX Add DDLs here.
COMMIT;
EOF
unlink $out;
# Test with file name having a double extension
$out = file 'test-add', 'duplicate_extension_test.sql.sql';
$add->_add('duplicate_extension_test.sql', $out, $tmpl, 'sqlite', 'add');
is_deeply +MockOutput->get_info, [[__x 'Created {file}', file => $out ]],
'Info should show $out created';
is_deeply +MockOutput->get_warn, [[__x(
'File {file} has a double extension of {ext}',
file => $out,
ext => 'sql',
)]], 'Should have warned about double extension';
unlink $out;
};
$_ = 'die "NO ONE HERE";';
return $i = !$i;
}, 1;
};
$test_add->('Template::Tiny');
# Test _add() with Template.
shift @INC;
delete $INC{'Template.pm'};
SKIP: {
skip 'Template Toolkit not installed', 16 unless eval 'use Template; 1';
$test_add->('Template Toolkit');
# Template Toolkit should throw an error on template syntax errors.
ok my $add = $CLASS->new(sqitch => $sqitch, template_directory => $tmpldir),
'Create add command';
my $mock_add = Test::MockModule->new($CLASS);
$mock_add->mock(_slurp => sub { \'[% IF foo %]' });
my $out = file 'test-add', 'sqitch_change_test.sql';
throws_ok { $add->_add('sqitch_change_test', $out, $tmpl) }
'App::Sqitch::X', 'Should get an exception on TT syntax error';
is $@->ident, 'add', 'TT exception ident should be "add"';
is $@->message, __x(
'Error executing {template}: {error}',
template => $tmpl,
error => 'file error - parse error - input text line 1: unexpected end of input',
), 'TT exception message should include the original error message';
}
##############################################################################
# Test execute.
ok $add = $CLASS->new(
sqitch => $sqitch,
template_directory => $tmpldir,
), 'Create another add with template_directory';
# Override request_note().
my $change_mocker = Test::MockModule->new('App::Sqitch::Plan::Change');
my %request_params;
$change_mocker->mock(request_note => sub {
my $self = shift;
%request_params = @_;
return $self->note;
});
# Set up a function to force the reload of the plan.
my $reload = sub {
my $plan = shift;
$plan->_plan( $plan->load);
delete $plan->{$_} for qw(_changes _lines project uri);
};
my $deploy_file = file qw(test-add deploy widgets_table.sql);
my $revert_file = file qw(test-add revert widgets_table.sql);
my $verify_file = file qw(test-add verify widgets_table.sql);
my $plan = $add->default_target->plan;
is $plan->get('widgets_table'), undef, 'Should not have "widgets_table" in plan';
dir_not_exists_ok +File::Spec->catdir('test-add', $_) for qw(deploy revert verify);
ok $add->execute('widgets_table'), 'Add change "widgets_table"';
# Reload the plan.
$reload->($plan);
# Make sure the change was written to the plan file.
isa_ok my $change = $plan->get('widgets_table'), 'App::Sqitch::Plan::Change',
'Added change';
is $change->name, 'widgets_table', 'Change name should be set';
is_deeply [$change->requires], [], 'It should have no requires';
is_deeply [$change->conflicts], [], 'It should have no conflicts';
is_deeply \%request_params, {
for => __ 'add',
scripts => [$change->deploy_file, $change->revert_file, $change->verify_file],
}, 'It should have prompted for a note';
file_exists_ok $_ for ($deploy_file, $revert_file, $verify_file);
file_contents_like $deploy_file, qr/^-- Deploy add:widgets_table/,
'Deploy script should look right';
file_contents_like $revert_file, qr/^-- Revert add:widgets_table/,
'Revert script should look right';
file_contents_like $verify_file, qr/^-- Verify add:widgets_table/,
'Verify script should look right';
is_deeply +MockOutput->get_info, [
[__x 'Created {file}', file => $deploy_file],
[__x 'Created {file}', file => $revert_file],
[__x 'Created {file}', file => $verify_file],
[__x 'Added "{change}" to {file}',
change => 'widgets_table',
file => $target->plan_file,
],
], 'Info should have reported file creation';
# Make sure conflicts are avoided and conflicts and requires are respected.
ok $add = $CLASS->new(
change_name => 'foo_table',
sqitch => $sqitch,
requires => ['widgets_table'],
conflicts => [qw(dr_evil joker)],
note => [qw(hello there)],
with_scripts => { verify => 0 },
template_directory => $tmpldir,
), 'Create another add with template_directory and no verify script';
$deploy_file = file qw(test-add deploy foo_table.sql);
$revert_file = file qw(test-add revert foo_table.sql);
$verify_file = file qw(test-add ferify foo_table.sql);
$deploy_file->touch;
file_exists_ok $deploy_file;
file_not_exists_ok $_ for ($revert_file, $verify_file);
is $plan->get('foo_table'), undef, 'Should not have "foo_table" in plan';
ok $add->execute, 'Add change "foo_table"';
file_exists_ok $_ for ($deploy_file, $revert_file);
file_not_exists_ok $verify_file;
$plan = $add->default_target->plan;
isa_ok $change = $plan->get('foo_table'), 'App::Sqitch::Plan::Change',
'"foo_table" change';
is_deeply \%request_params, {
for => __ 'add',
scripts => [$change->deploy_file, $change->revert_file],
}, 'It should have prompted for a note';
is $change->name, 'foo_table', 'Change name should be set to "foo_table"';
is_deeply [$change->requires], [dep 0, 'widgets_table'], 'It should have requires';
is_deeply [$change->conflicts], [map { dep 1, $_ } qw(dr_evil joker)], 'It should have conflicts';
is $change->note, "hello\n\nthere", 'It should have a comment';
is_deeply +MockOutput->get_info, [
[__x 'Skipped {file}: already exists', file => $deploy_file],
[__x 'Created {file}', file => $revert_file],
[__x 'Added "{change}" to {file}',
change => 'foo_table [widgets_table !dr_evil !joker]',
file => $target->plan_file,
],
], 'Info should report skipping file and include dependencies';
# Make sure we die on an unknown argument.
throws_ok { $add->execute(qw(foo bar)) } 'App::Sqitch::X',
'Should get an error on unkonwn argument';
is $@->ident, 'add', 'Unkown argument error ident should be "add"';
is $@->message, __nx(
'Unknown argument "{arg}"',
'Unknown arguments: {arg}',
2,
arg => 'foo, bar',
), 'Unknown argument error message should be correct';
# Make sure we die if the passed name conflicts with a target.
TARGET: {
my $mock_add = Test::MockModule->new($CLASS);
$mock_add->mock(parse_args => sub {
return undef, [$target];
});
$mock_add->mock(name => 'blog');
my $mock_target = Test::MockModule->new('App::Sqitch::Target');
$mock_target->mock(name => 'blog');
throws_ok { $add->execute('blog') } 'App::Sqitch::X',
'Should get an error for conflict with target name';
is $@->ident, 'add', 'Conflicting target error ident should be "add"';
is $@->message, __x(
'Name "{name}" identifies a target; use "--change {name}" to use it for the change name',
name => 'blog',
), 'Conflicting target error message should be correct';
}
# Make sure we get a usage message when no name specified.
USAGE: {
my @args;
my $mock_add = Test::MockModule->new($CLASS);
$mock_add->mock(usage => sub { @args = @_; die 'USAGE' });
my $add = $CLASS->new(sqitch => $sqitch);
throws_ok { $add->execute } qr/USAGE/,
'No name arg or option should yield usage';
my $plan = $add->default_target->plan;
is $plan->get('open_editor'), undef, 'Should not have "open_editor" in plan';
ok $add->execute('open_editor'), 'Add change "open_editor"';
# Instantiate fresh target and plan to force the file to be re-read.
$target = App::Sqitch::Target->new(sqitch => $sqitch);
$plan = App::Sqitch::Plan->new( sqitch => $sqitch, target => $target );
isa_ok my $change = $plan->get('open_editor'), 'App::Sqitch::Plan::Change',
'Added change';
is $change->name, 'open_editor', 'Change name should be set';
is $shell_cmd, join(' ', $sqitch->editor, $deploy_file, $revert_file, $verify_file),
'It should have prompted to edit sql files';
file_exists_ok $_ for ($deploy_file, $revert_file, $verify_file);
file_contents_like +File::Spec->catfile(qw(test-add deploy open_editor.sql)),
qr/^-- Deploy add:open_editor/, 'Deploy script should look right';
file_contents_like +File::Spec->catfile(qw(test-add revert open_editor.sql)),
qr/^-- Revert add:open_editor/, 'Revert script should look right';
file_contents_like +File::Spec->catfile(qw(test-add verify open_editor.sql)),
qr/^-- Verify add:open_editor/, 'Verify script should look right';
is_deeply +MockOutput->get_info, [
[__x 'Created {file}', file => $deploy_file],
[__x 'Created {file}', file => $revert_file],
[__x 'Created {file}', file => $verify_file],
[__x 'Added "{change}" to {file}',
change => 'open_editor',
file => $target->plan_file,
],
], 'Info should have reported file creation';
};
# Make sure an additional script and an exclusion work properly.
EXTRAS: {
ok my $add = $CLASS->new(
sqitch => $sqitch,
template_directory => $tmpldir,
with_scripts => { verify => 0 },
templates => { whatev => file(qw(etc templates verify mysql.tmpl)) },
note => ['Testing custom scripts'],
), 'Create another add with custom script and no verify';
my $deploy_file = file qw(test-add deploy custom_script.sql);
my $revert_file = file qw(test-add revert custom_script.sql);
my $verify_file = file qw(test-add verify custom_script.sql);
my $whatev_file = file qw(test-add whatev custom_script.sql);
ok $add->execute('custom_script'), 'Add change "custom_script"';
my $plan = $add->default_target->plan;
isa_ok my $change = $plan->get('custom_script'), 'App::Sqitch::Plan::Change',
'Added change';
is $change->name, 'custom_script', 'Change name should be set';
is_deeply [$change->requires], [], 'It should have no requires';
is_deeply [$change->conflicts], [], 'It should have no conflicts';
is_deeply \%request_params, {
for => __ 'add',
scripts => [ map { $change->script_file($_) } qw(deploy revert whatev)]
}, 'It should have prompted for a note';
file_exists_ok $_ for ($deploy_file, $revert_file, $whatev_file);
file_not_exists_ok $verify_file;
file_contents_like $deploy_file, qr/^-- Deploy add:custom_script/,
'Deploy script should look right';
file_contents_like $revert_file, qr/^-- Revert add:custom_script/,
'Revert script should look right';
file_contents_like $whatev_file, qr/^-- Verify add:custom_script/,
'Whatev script should look right';
file_contents_unlike $whatev_file, qr/^BEGIN/,
'Whatev script should be based on the MySQL verify script';
is_deeply +MockOutput->get_info, [
[__x 'Created {file}', file => $deploy_file],
[__x 'Created {file}', file => $revert_file],
[__x 'Created {file}', file => $whatev_file],
[__x 'Added "{change}" to {file}',
change => 'custom_script',
file => $target->plan_file,
],
], 'Info should have reported file creation';
# Relod the plan file to make sure change is written to it.
$reload->($plan);
isa_ok $change = $plan->get('custom_script'), 'App::Sqitch::Plan::Change',
'Added change in reloaded plan';
}
# Make sure a configuration with multiple plans works.
MULTIPLAN: {
make_path 'test-multiadd';
END { remove_tree 'test-multiadd' };
chdir 'test-multiadd';
my $config = TestConfig->new(
'core.engine' => 'pg',
'engine.pg.top_dir' => 'pg',
'engine.sqlite.top_dir' => 'sqlite',
'engine.mysql.top_dir' => 'mysql',
);
# Create plan files and determine the scripts that to be created.
my @scripts = map {
my $dir = dir $_;
$dir->mkpath;
$dir->file('sqitch.plan')->spew("%project=add\n\n");
map { $dir->file($_, 'widgets.sql') } qw(deploy revert verify);
} qw(pg sqlite mysql);
# Load up the configuration for this project.
my $sqitch = App::Sqitch->new(config => $config);
ok my $add = $CLASS->new(
sqitch => $sqitch,
note => ['Testing multiple plans'],
all => 1,
template_directory => dir->parent->subdir(qw(etc templates))
), 'Create another add with custom multiplan config';
my @targets = App::Sqitch::Target->all_targets(sqitch => $sqitch);
is @targets, 3, 'Should have three targets';
# Make sure the target list matches our script list order (by engine).
# pg always comes first, as primary engine, but the other two are random.
push @targets, splice @targets, 1, 1 if $targets[1]->engine_key ne 'sqlite';
for @targets;
file_exists_ok $_ for @scripts;
# Make sure we see the proper output.
my $info = MockOutput->get_info;
my $ekey = $targets[1]->engine_key;
if ($info->[4][0] !~ /$ekey/) {
# Got the targets in a different order. So reorder results to match.
push @{ $info } => splice @{ $info }, 4, 4;
}
is_deeply $info, [
(map { [__x 'Created {file}', file => $_] } @scripts[0..2]),
[
__x 'Added "{change}" to {file}',
change => 'widgets',
file => $targets[0]->plan_file,
],
(map { [__x 'Created {file}', file => $_] } @scripts[3..5]),
[
__x 'Added "{change}" to {file}',
change => 'widgets',
file => $targets[1]->plan_file,
],
(map { [__x 'Created {file}', file => $_] } @scripts[6..8]),
[
__x 'Added "{change}" to {file}',
change => 'widgets',
file => $targets[2]->plan_file,
],
], 'Info should have reported all script creations and plan updates';
# Make sure we get an error using --all and a target arg.
throws_ok { $add->execute('foo', 'pg' ) } 'App::Sqitch::X',
'Should get an error for --all and a target arg';
is $@->ident, 'add', 'Mixed arguments error ident should be "add"';
is $@->message, __(
'Cannot specify both --all and engine, target, or plan arugments'
), 'Mixed arguments error message should be correct';
# Now try adding a change to just one engine. Remove --all
ok $add = $CLASS->new(
sqitch => $sqitch,
note => ['Testing multiple plans'],
template_directory => dir->parent->subdir(qw(etc templates))
), 'Create yet another add with custom multiplan config';
ok $add->execute('choc', 'sqlite'), 'Add change "choc" to the sqlite plan';
my %targets = map { $_->engine_key => $_ }
App::Sqitch::Target->all_targets(sqitch => $sqitch);
is keys %targets, 3, 'Should still have three targets';
ok !$targets{pg}->plan->get('choc'), 'Should not have "choc" in the pg plan';
ok !$targets{mysql}->plan->get('choc'), 'Should not have "choc" in the mysql plan';
ok $targets{sqlite}->plan->get('choc'), 'Should have "choc" in the sqlite plan';
@scripts = map {
my $dir = dir $_;
$dir->mkpath;
map { $dir->file($_, 'choc.sql') } qw(deploy revert verify);
} qw(sqlite pg mysql);
file_exists_ok $_ for @scripts[0..2];
file_not_exists_ok $_ for @scripts[3..8];
is_deeply +MockOutput->get_info, [
(map { [__x 'Created {file}', file => $_] } @scripts[0..2]),
[
__x 'Added "{change}" to {file}',
change => 'choc',
file => $targets{sqlite}->plan_file,
],
], 'Info should have reported sqlite choc script creations and plan updates';
chdir File::Spec->updir;
}
# Make sure we update only one plan but write out multiple target files.
MULTITARGET: {
remove_tree 'test-multiadd';
make_path 'test-multiadd';
chdir 'test-multiadd';
my $config = TestConfig->new(
'core.engine' => 'pg',
'core.plan_file' => 'sqitch.plan',
'engine.pg.top_dir' => 'pg',
'engine.sqlite.top_dir' => 'sqlite',
'add.all' => 1,
);
file('sqitch.plan')->spew("%project=add\n\n");
# Create list of scripts to be created.
my @scripts = map {
my $dir = dir $_;
$dir->mkpath;
map { $dir->file($_, 'widgets.sql') } qw(deploy revert verify);
} qw(pg sqlite);
# Load up the configuration for this project.
my $sqitch = App::Sqitch->new(config => $config);
ok my $add = $CLASS->new(
sqitch => $sqitch,
note => ['Testing multiple targets'],
template_directory => dir->parent->subdir(qw(etc templates))
), 'Create another add with single plan, multi-target config';
my @targets = App::Sqitch::Target->all_targets(sqitch => $sqitch);
is @targets, 2, 'Should have two targets';
is $targets[0]->plan_file, $targets[1]->plan_file,
'Targets should use the same plan file';
# Let's do this thing!
ok $add->execute('widgets'), 'Add change "widgets" to all plans';
ok $targets[0]->plan->get('widgets'), 'Should have "widgets" in the plan';
file_exists_ok $_ for @scripts;
is_deeply \%request_params, {
for => __ 'add',
scripts => \@scripts,
}, 'Should have the proper files listed in the note promt';
is_deeply +MockOutput->get_info, [
(map { [__x 'Created {file}', file => $_] } @scripts),
[
__x 'Added "{change}" to {file}',
( run in 2.127 seconds using v1.01-cache-2.11-cpan-97f6503c9c8 )