The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl
use warnings;
use strict;
use Test::More;
use Test::Exception;
use Test::MockModule;
use DBI;
use Narada::Config qw( set_config );

require 'blib/script/narada-mysqldump';

chomp(my ($db, $login, $pass) = `cat t/.answers`);

if ($db eq q{}) {
    plan skip_all => 'No database provided for testing';
} else {
    plan tests => 53;
}

use File::Temp qw( tempdir );
chomp(my $cwd=`pwd`);
$ENV{PATH} = "$cwd/blib/script:$ENV{PATH}";
$ENV{PERL5LIB} = "$cwd/blib:$ENV{PERL5LIB}";
chdir tempdir( CLEANUP => 1 )
    and system('narada-new') == 0
    or die "Unable to create project: $!";


# - init_globals()
#   * return false without db/db
#   * throw with wrong db/pass
#   * create $::dbh and $::MYSQLDUMP

ok !init_globals(), 'init_globals: return false';

set_config('db/db', $db);
set_config('db/login', $login);
set_config('db/pass', $pass);
system('narada-setup-mysql');

set_config('db/pass', 'wrong pass');
throws_ok { init_globals() } qr/Access denied/i,
    'init_globals: throw with wrong db/pass';
set_config('db/pass', $pass);

ok !$::dbh,         'init_globals: $::dbh undefined';
ok !$::MYSQLDUMP,   'init_globals: $::MYSQLDUMP undefined';
ok init_globals(),  'init_globals return true';
ok $::dbh,          'init_globals: $::dbh defined';
ok $::MYSQLDUMP,    'init_globals: $::MYSQLDUMP defined';
$::dbh->do('SET default_storage_engine=MYISAM');

# - list_tables()
#   * db/dump/incremental: required
#   * db/dump/empty: required
#   * db/dump/ignore: optional
#   * incremental, empty, ignore: EMPTY, no tables
#   * incremental, empty, ignore: EMPTY, some tables
#   * incremental, empty, ignore: NOT EMPTY, no other tables
#   * incremental, empty, ignore: NOT EMPTY, some other tables

$::dbh->do('CREATE TABLE a (id INT AUTO_INCREMENT PRIMARY KEY, n INT)');
$::dbh->do('CREATE TABLE b (s TEXT)');

set_config('db/dump/incremental', "a\nnosuch\nb\n");
throws_ok { list_tables() } qr{Table nosuch listed in db/dump/incremental does not exists}i,
    'list_tables: db/dump/incremental REQUIRED';
set_config('db/dump/incremental', q{});

set_config('db/dump/empty', "a\nnosuch\nb\n");
throws_ok { list_tables() } qr{Table nosuch listed in db/dump/empty does not exists}i,
    'list_tables: db/dump/empty REQUIRED';
set_config('db/dump/empty', q{});

set_config('db/dump/ignore', "a\nnosuch\nb\n");
lives_ok { list_tables() }
    'list_tables: db/dump/ignore OPTIONAL';
set_config('db/dump/ignore', q{});

$::dbh->do('DROP TABLE a');
$::dbh->do('DROP TABLE b');

is_deeply [list_tables()], [[],[],[]],
    'list_tables: incremental, empty, ignore: EMPTY, no tables';

$::dbh->do('CREATE TABLE a (id INT AUTO_INCREMENT PRIMARY KEY, n INT)');
$::dbh->do('CREATE TABLE b (s TEXT)');
is_deeply [list_tables()], [['a','b'],[],[]],
    'list_tables: incremental, empty, ignore: EMPTY, some tables';
$::dbh->do('DROP TABLE a');
$::dbh->do('DROP TABLE b');

set_config('db/dump/incremental', "a\nb");
set_config('db/dump/empty', "c\nd");
set_config('db/dump/ignore', "e\nf");
$::dbh->do('CREATE TABLE a (i INT)');
$::dbh->do('CREATE TABLE b (i INT)');
$::dbh->do('CREATE TABLE c (i INT)');
$::dbh->do('CREATE TABLE d (i INT)');
$::dbh->do('CREATE TABLE e (i INT)');
is_deeply [list_tables()], [[],['a','b'],['e']],
    'list_tables: incremental, empty, ignore: NOT EMPTY, no other tables';
$::dbh->do('CREATE TABLE g (i INT)');
$::dbh->do('CREATE TABLE h (i INT)');
set_config('db/dump/ignore', "e\n");
is_deeply [list_tables()], [['g','h'],['a','b'],['e']],
    'list_tables: incremental, empty, ignore: NOT EMPTY, some other tables';
$::dbh->do('DROP TABLES a, b, c, d, e, g, h');
set_config('db/dump/incremental', q{});
set_config('db/dump/empty', q{});
set_config('db/dump/ignore', q{});

# - detect_unchanged()
#   * no dumps
#   * some dumps, some unchanged
#   * some dumps, no unchanged

$::dbh->do('CREATE TABLE a (i INT)');
$::dbh->do('CREATE TABLE b (i INT)');
$::dbh->do('CREATE TABLE c (i INT)');

$::dbh->do('INSERT INTO a VALUES (1),(2),(3)');
$::dbh->do('INSERT INTO b VALUES (10)');
my $full = ['a','b','c'];
is_deeply detect_unchanged($full), [],
    'detect_unchanged: no dumps';

SKIP: {
    skip 'user do not have LOCK TABLES privilege', 38
        if `echo "lock table a read;" | narada-mysql 2>&1` =~ /Access denied/i;

dump_full($full, []);
sleep 1;    # emulate delay in main()
$::dbh->do('INSERT INTO b VALUES (20),(30)');
is_deeply detect_unchanged($full), ['a','c'],
    'detect_unchanged: some unchanged';

$::dbh->do('INSERT INTO a VALUE (4)');
$::dbh->do('INSERT INTO c VALUE (100)');
is_deeply detect_unchanged($full), [],
    'detect_unchanged: no unchanged';

$::dbh->do('DROP TABLES a, b, c');
unlink glob 'var/sql/*.sql';

# - del_dumps_except()
#   * empty $incremental & $unchanged, no dumps - do nothing
#   * empty $incremental & $unchanged, some incremental and full dumps - all deleted
#   * many full and incremental dumps with some in $incremental and $unchanged:
#     some incremental ALTERed - deleted,
#     some incremental TRUNCATED - deleted,
#     some incremental with new rows - not deleted,
#     some incremental not changed - not deleted,
#     some full not changed - not deleted,
#     some full changed - deleted

del_dumps_except([], []);
is_deeply [glob 'var/sql/*.sql'], [],
    'del_dumps_except: no dumps - do nothing';

$::dbh->do('CREATE TABLE incr_a (i INT AUTO_INCREMENT PRIMARY KEY, s TEXT)');
$::dbh->do('CREATE TABLE full_a (i INT)');
$::dbh->do('INSERT INTO incr_a SET s="first"');
$::dbh->do('INSERT INTO full_a SET i=10');
dump_scheme_except([]);
dump_full(['full_a'], []);
dump_incremental(['incr_a']);
$::dbh->do('INSERT INTO incr_a SET s="second"');
dump_incremental(['incr_a']);
is_deeply [sort glob 'var/sql/*.sql'], [
    'var/sql/db.scheme.sql',
    'var/sql/full_a.sql',
    'var/sql/incr_a.1-1.sql',
    'var/sql/incr_a.2-2.sql',
    ],
    'del_dumps_except: dumped some full and incremental';
del_dumps_except([], []);
is_deeply [glob 'var/sql/*.sql'], [],
    'del_dumps_except: some incremental and full dumps - all deleted';
$::dbh->do('DROP TABLES incr_a, full_a');

$::dbh->do('CREATE TABLE incr_a (i INT AUTO_INCREMENT PRIMARY KEY, s TEXT)');
$::dbh->do('CREATE TABLE incr_b (i INT AUTO_INCREMENT PRIMARY KEY, s TEXT)');
$::dbh->do('CREATE TABLE incr_c (i INT AUTO_INCREMENT PRIMARY KEY, s TEXT)');
$::dbh->do('CREATE TABLE incr_d (i INT AUTO_INCREMENT PRIMARY KEY, s TEXT)');
$::dbh->do('CREATE TABLE full_a (i INT)');
$::dbh->do('CREATE TABLE full_b (i INT)');
$::dbh->do('INSERT INTO incr_a SET s="first"');
$::dbh->do('INSERT INTO incr_b SET s="second"');
$::dbh->do('INSERT INTO incr_c SET s="third"');
$::dbh->do('INSERT INTO incr_d SET s="fourth"');
$::dbh->do('INSERT INTO full_a SET i=10');
$::dbh->do('INSERT INTO full_b SET i=100');
dump_scheme_except([]);
dump_full(['full_a','full_b'], []);
dump_incremental(['incr_a','incr_b','incr_c','incr_d']);
is_deeply [sort glob 'var/sql/*.sql'], [
    'var/sql/db.scheme.sql',
    'var/sql/full_a.sql',
    'var/sql/full_b.sql',
    'var/sql/incr_a.1-1.sql',
    'var/sql/incr_b.1-1.sql',
    'var/sql/incr_c.1-1.sql',
    'var/sql/incr_d.1-1.sql',
    ],
    'del_dumps_except: dumped many full and incremental';
sleep 1;
$::dbh->do('ALTER TABLE incr_a ADD COLUMN d DATETIME');
$::dbh->do('TRUNCATE TABLE incr_b');
$::dbh->do('INSERT INTO incr_c SET s="another third"');
$::dbh->do('INSERT INTO full_a SET i=20');
del_dumps_except(['incr_a','incr_b','incr_c','incr_d'], ['full_b']);
is_deeply [sort glob 'var/sql/*.sql'], [
    'var/sql/full_b.sql',
    'var/sql/incr_c.1-1.sql',
    'var/sql/incr_d.1-1.sql',
    ],
    'del_dumps_except: complex test';

$::dbh->do('DROP TABLES incr_a, incr_b, incr_c, incr_d, full_a, full_b');
unlink glob 'var/sql/*.sql';

# - dump_scheme_except()
#   * no tables, no $ignore
#   * some tables, no $ignore
#   * some tables, some $ignore
#   * some tables, all $ignore

sub helper_dump_scheme_except {
    my ($ignore) = @_;
    dump_scheme_except($ignore);
    ok -f 'var/sql/db.scheme.sql', 'dump_scheme_except: scheme dumped';
    my $scheme = `cat var/sql/db.scheme.sql`;
    unlink 'var/sql/db.scheme.sql' or die "unlink: $!";
    my @tables = $scheme =~ /CREATE TABLE `(\w+)`/g;
    return [sort @tables];
}

is_deeply helper_dump_scheme_except([]), [],
    'dump_scheme_except: no tables in dump';

$::dbh->do('CREATE TABLE incr_a (i INT AUTO_INCREMENT PRIMARY KEY, s TEXT)');
$::dbh->do('CREATE TABLE full_a (i INT)');
is_deeply helper_dump_scheme_except([]), ['full_a','incr_a'],
    'dump_scheme_except: all tables in dump';
is_deeply helper_dump_scheme_except(['full_a']), ['incr_a'],
    'dump_scheme_except: some tables ignored';
is_deeply helper_dump_scheme_except(['full_a','incr_a']), [],
    'dump_scheme_except: all tables ignored';

$::dbh->do('DROP TABLES incr_a, full_a');

# - dump_full()
#   * empty $full and $unchanged
#   * some $full, empty $unchanged
#   * some $full, some $unchanged
#   * some $full, all $unchanged

$::dbh->do('CREATE TABLE full_a (i INT)');
$::dbh->do('CREATE TABLE full_b (i INT)');

dump_full([], []);
is_deeply [sort glob 'var/sql/*.sql'], [],
    'dump_full: nothing to dump';
unlink glob 'var/sql/*.sql';

dump_full(['full_a','full_b'], []);
is_deeply [sort glob 'var/sql/*.sql'], [
    'var/sql/full_a.sql',
    'var/sql/full_b.sql',
    ],
    'dump_full: some full, no unchanged';
unlink glob 'var/sql/*.sql';

dump_full(['full_a','full_b'], ['full_a']);
is_deeply [sort glob 'var/sql/*.sql'], [
    'var/sql/full_b.sql',
    ],
    'dump_full: some full, some unchanged';
unlink glob 'var/sql/*.sql';

dump_full(['full_a','full_b'], ['full_a','full_b']);
is_deeply [sort glob 'var/sql/*.sql'], [],
    'dump_full: some full, all unchanged';
unlink glob 'var/sql/*.sql';

$::dbh->do('DROP TABLES full_a, full_b');

# - dump_incremental()
#   * empty $incremental
#   * throw on $incremental for table with wrong PRIMARY KEY (not *INT, not
#     AUTO_INCREMENTAL, multicolumn PRIMARY KEY, PRIMARY KEY not first column)
#   * some $incremental changed, some unchanged, some has no previous dumps

$::dbh->do('CREATE TABLE a (i INT AUTO_INCREMENT PRIMARY KEY, s TEXT)');
$::dbh->do('CREATE TABLE b (i MEDIUMINT AUTO_INCREMENT PRIMARY KEY, s TEXT)');
$::dbh->do('CREATE TABLE c (i TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, s TEXT)');
$::dbh->do('CREATE TABLE e1 (i FLOAT AUTO_INCREMENT PRIMARY KEY, s TEXT)');
$::dbh->do('CREATE TABLE e2 (i INT PRIMARY KEY, s TEXT)');
$::dbh->do('CREATE TABLE e3 (s TEXT, i INT AUTO_INCREMENT PRIMARY KEY)');
$::dbh->do('CREATE TABLE e4 (i INT AUTO_INCREMENT, s TEXT, PRIMARY KEY(i,s(256)))');

dump_incremental([]);
is_deeply [sort glob 'var/sql/*.sql'], [],
    'dump_incremental: do nothing';
unlink glob 'var/sql/*.sql';

$::dbh->do('INSERT INTO e1 SET s="error1"');
$::dbh->do('INSERT INTO e2 SET s="error2"');
$::dbh->do('INSERT INTO e3 SET s="error3"');
$::dbh->do('INSERT INTO e4 SET s="error4"');
throws_ok { dump_incremental(['e1']); } qr/must be: INT AUTO_INCREMENT PRIMARY KEY/,
    'dump_incremental: wrong incremental table format';
throws_ok { dump_incremental(['e2']); } qr/must be: INT AUTO_INCREMENT PRIMARY KEY/,
    'dump_incremental: wrong incremental table format';
throws_ok { dump_incremental(['e3']); } qr/must be: INT AUTO_INCREMENT PRIMARY KEY/,
    'dump_incremental: wrong incremental table format';
throws_ok { dump_incremental(['e4']); } qr/must be: INT AUTO_INCREMENT PRIMARY KEY/,
    'dump_incremental: wrong incremental table format';

$::dbh->do('INSERT INTO a SET s="first"');
$::dbh->do('INSERT INTO a SET s="one more first"');
$::dbh->do('INSERT INTO b SET s="second"');
dump_incremental(['a','b','c']);
is_deeply [sort glob 'var/sql/*.sql'], [
    'var/sql/a.1-2.sql',
    'var/sql/b.1-1.sql',
    ],
    'dump_incremental: some tables dumped';
$::dbh->do('INSERT INTO b SET s="one more second"');
$::dbh->do('INSERT INTO c SET s="third"');
dump_incremental(['a','b','c']);
is_deeply [sort glob 'var/sql/*.sql'], [
    'var/sql/a.1-2.sql',
    'var/sql/b.1-1.sql',
    'var/sql/b.2-2.sql',
    'var/sql/c.1-1.sql',
    ],
    'dump_incremental: some unchanged, some changed and some new';
unlink glob 'var/sql/*.sql';

$::dbh->do('DROP TABLES a, b, c, e1, e2, e3, e4');

# - main()
#   * no previous dumps, no tables
#   * some full, incremental, empty and ignore
#   * some full/incremental unchanged/changed

throws_ok { main('param') }    qr/Usage:/,
    'main: too many params';

main();
is_deeply [sort glob 'var/sql/*.sql'], [
    'var/sql/db.scheme.sql',
    ],
    'main: no previous dumps, no tables';

# XXX why I have to repeat this here?
$::dbh->do('SET default_storage_engine=MYISAM');

$::dbh->do('CREATE TABLE incr_a (i INT AUTO_INCREMENT PRIMARY KEY, s TEXT)');
$::dbh->do('CREATE TABLE incr_b (i INT AUTO_INCREMENT PRIMARY KEY, s TEXT)');
$::dbh->do('CREATE TABLE full_a (i INT)');
$::dbh->do('CREATE TABLE full_b (i INT)');
$::dbh->do('CREATE TABLE empty_a (i INT)');
$::dbh->do('CREATE TABLE ignore_a (i INT)');
$::dbh->do('INSERT INTO incr_a SET s="first"');
$::dbh->do('INSERT INTO incr_a SET s="another first"');
$::dbh->do('INSERT INTO incr_b SET s="second"');
$::dbh->do('INSERT INTO full_a SET i=10');
$::dbh->do('INSERT INTO full_b SET i=100');
$::dbh->do('INSERT INTO empty_a SET i=1000');
$::dbh->do('INSERT INTO ignore_a SET i=10000');
set_config('db/dump/incremental', "incr_a\nincr_b\n");
set_config('db/dump/empty', "empty_a\n");
set_config('db/dump/ignore', "ignore_a\n");
main();
is_deeply [sort glob 'var/sql/*.sql'], [
    'var/sql/db.scheme.sql',
    'var/sql/full_a.sql',
    'var/sql/full_b.sql',
    'var/sql/incr_a.1-2.sql',
    'var/sql/incr_b.1-1.sql',
    ],
    'main: some full, incremental, empty and ignore';

$::dbh->do('INSERT INTO incr_b SET s="another second"');
$::dbh->do('INSERT INTO full_b SET i=200');
my %old_mtime = map {$_ => mtime($_)} glob 'var/sql/*.sql';
main();
my %new_mtime = map {$_ => mtime($_)} glob 'var/sql/*.sql';
is_deeply [sort glob 'var/sql/*.sql'], [
    'var/sql/db.scheme.sql',
    'var/sql/full_a.sql',
    'var/sql/full_b.sql',
    'var/sql/incr_a.1-2.sql',
    'var/sql/incr_b.1-1.sql',
    'var/sql/incr_b.2-2.sql',
    ],
    'main: some full/incremental unchanged/changed';
my @unchanged = (
    'var/sql/full_a.sql',
    'var/sql/incr_a.1-2.sql',
    'var/sql/incr_b.1-1.sql',
    );
my @changed = (
    'var/sql/db.scheme.sql',
    'var/sql/full_b.sql',
    'var/sql/incr_b.2-2.sql',
    );
is   $old_mtime{$_}, $new_mtime{$_}, "unchanged $_" for @unchanged;
isnt $old_mtime{$_}, $new_mtime{$_}, "changed   $_" for @changed;

set_config('db/db', q{});
main();
is   $new_mtime{'var/sql/db.scheme.sql'}, mtime('var/sql/db.scheme.sql'),
    'main: do nothing without db/db';
set_config('db/db', $db);
main();
isnt $new_mtime{'var/sql/db.scheme.sql'}, mtime('var/sql/db.scheme.sql'),
    'main: recreate SCHEME with db/db';

}
###

system('narada-setup-mysql --clean');
chdir '/';  # work around warnings in File::Temp CLEANUP handler