The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl
# test_sandbox
#    The MySQL Sandbox
#    Copyright (C) 2009-2010 Giuseppe Maxia
#    Contacts: http://datacharmer.org
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; version 2 of the License
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
use strict;
use warnings;
use Data::Dumper;
use Getopt::Long qw(:config no_ignore_case );
use MySQL::Sandbox qw(use_env);
use English qw( -no_match_vars );
use Carp;

my $DEBUG = $MySQL::Sandbox::DEBUG;

for my $prog (qw( make_sandbox 
                make_replication_sandbox 
                make_multiple_sandbox 
                make_multiple_custom_sandbox
                sbtool ) ) {
    unless ( exists_in_path ($prog) ) {
        die "script <$prog> not found\n";
    } 
}

sub tprint;
sub tprintf;

sub get_exec_result ;
my $use_open3 = 0;

eval "use IPC::Open3";
if ($@) {
        $use_open3 =0;
}

#
# defaults
#
# my @versions = ( '5.0.51', '5.0.64', '5.1.23', '5.1.24', '5.1.25', '5.1.26', '6.0.6' );
my @versions = ( '5.0.86', '5.1.43');
my $verbose = $DEBUG || $ENV{'VERBOSE'} || 0;

my %tests = (
    single      => 1,
    replication => 1,
    circular    => 1,
    multiple    => 1,
    custom      => 1,
    tuning      => 1,
    sbtool      => 0,
    smoke       => 0,
    user        => 0,
);

# my %custom_tests = ();

my ($user_tests, $user_versions, $user_defined_test, $get_help, $preserve_tests, );


GetOptions( 
   "tarball|versions=s" => \$user_versions,
   "tests=s"    => \$user_tests, 
   "user_test=s"=> \$user_defined_test, 
   "verbose"    => \$verbose,
   "preserve_tests" => \$preserve_tests,
   "help|h"     => \$get_help,
) or help();

help() if $get_help;

if ($user_versions) {
    my @new_versions = grep {$_} split /,/, $user_versions;
    die "at least one version is required\n" unless @new_versions;
    @versions =();
    # ensuring that each version is used only once.
    @new_versions = 
        map     { $_->[0] } # sorting with the Schwartzian Transform
        sort    { $a->[1] cmp $b->[1] }
        map     { /(\d+)\.(\d+)\.(\d+)/; 
                    [ $_, sprintf('%02d-%02d-%02d',$1,$2,$3)] } 
        keys    %{{ map { $_, 1} @new_versions }};
    for my $ver (@new_versions) {
            push @versions, $ver;
    }
}
for my $ver (@versions) {
    $ver =~ s/^~/$ENV{'HOME'}/;
    unless (( -d "/opt/mysql/$ver") 
            or ( -d "$ENV{'HOME'}/opt/mysql/$ver") 
            or ( -f $ver )) {
        print "version $ver not found in either /opt/mysql or $ENV{'HOME'}/opt/mysql\n";
        die "use --versions to list the versions you want to test\n";
    }
}

if ($user_defined_test and !$user_tests) {
    $user_tests = 'user';
}

if ($user_tests) {
    my @todo = grep {$_} split /,/, $user_tests;
    die "at least one test is required\n" unless @todo;
    my %new_tests;
    for my $t (@todo) {
        if (exists $tests{$t} ) {
            $new_tests{$t}++;
        }
        else {
            die "unrecognized test <$t>\n";
        }
    }
    for my $t (keys %tests) {
        if ( exists $new_tests{$t} ) {
            $tests{$t} = 1;
        }
        else {
            $tests{$t} = 0;
        }
    }
}

my $sandbox_home = "$ENV{'HOME'}/sandboxes";
my %test_results = (
    run     => 0,
    passed  => 0,
    failed  => 0,
    skipped => 0,
);
#
# cleaning up existing sandbox directory
#
if ( $ENV{'SANDBOX_HOME'} ) {
    $sandbox_home = $ENV{'SANDBOX_HOME'};
}
else {
    $ENV{'SANDBOX_HOME'} = $sandbox_home;
}
my $sh_stop_all = "$sandbox_home/stop_all";

if ( -x $sh_stop_all ) {
    system("$sh_stop_all > /dev/null 2>&1");
}

# 
# setting the current sandbox directory for this test
#
$sandbox_home = $ENV{'TEST_SANDBOX_HOME'} || "$ENV{'HOME'}/test_sb";
$ENV{'SANDBOX_HOME'} = $sandbox_home;
$sh_stop_all = "$sandbox_home/stop_all";
my $sh_clear_all = "$sandbox_home/clear_all";
my $sh_start_all = "$sandbox_home/start_all";
my $sh_use_all = "$sandbox_home/use_all";

if ($tests{'smoke'} and $tests{'sbtool'}) {
    die "test <smoke> and test <sbtool> should not run together\n";
}

# 
# cleaning up the test directory if exists
#
if ( -x $sh_stop_all ) {
    system("$sh_stop_all > /dev/null 2>&1");
    system qq(rm -rf $sandbox_home) ;
}

#
# checking if there are other servers running
#
my $how_many_mysqld = get_number_of_processes('mysqld');
my $how_many_mysqld_safe = get_number_of_processes('mysqld_safe');
tprintf "** currently there are (%d) mysqld processes and (%d) mysqld_safe processes\n", 
    $how_many_mysqld, 
    $how_many_mysqld_safe;

#
# starting the tests
#
for my $ver (@versions) {
    my ($bare_version, $version) = get_bare_version ($ver);

    if ($tests{'single'}) {
        # print "run_single_test($ver, $version, $bare_version)\n";
        run_single_test($ver, $version, $bare_version);
    }

    if ($tests{'replication'} ) {
        run_replication_test($ver, $version, $bare_version);
    }

    if ($tests{'circular'}) {
        run_circular_test($ver, $version, $bare_version);
    }

    if ($tests{'multiple'}) {
        run_multiple_test($ver, $version, $bare_version);
    }
}

if ($tests{'custom'}) {
    run_mcustom_test();
}

if ($tests{'user'}) {
    die "test file required\n" unless $user_defined_test;
    run_user_defined_test($user_defined_test);    
}

my $summaries  = 0;
for my $test (keys %tests) {
    if ($tests{$test} && ($test !~ /^(?:tuning|smoke|sbtool|user)$/)) {
        $summaries = 1;
        last;
    }
}

if ($summaries) {
    run_summary_tests();
}

if ($tests{'tuning'}) {
    run_tuning_test();
}

if ($tests{'smoke'}) {
    for my $ver ( @versions ) {
        if (-f "$sandbox_home/stop_all") {
            system qq("$sandbox_home/stop_all > /dev/null 2>&1"); 
            system qq(rm -rf $sandbox_home/*) ;
        }
        run_smoke_test($ver);
    }
}

if ($tests{'sbtool'}) {
    for my $ver (@versions) {
        if ( -d $sandbox_home ) {
            system "$sandbox_home/stop_all > /dev/null 2>&1";
            system "rm -rf $sandbox_home";
        }
        run_sbtool_test ($ver);
    }
}


tprintf "*** Executed %d tests. Passed %d (%5.2f%%). Failed %d (%5.2f%%)\n",
        $test_results{'run'},
        $test_results{'passed'},
        $test_results{'passed'} / $test_results{'run'} * 100,
        $test_results{'failed'},
        $test_results{'failed'} / $test_results{'run'} * 100
        ;

if ($test_results{'failed'}) {
    exit 1;
}

# 
# ROUTINES
#
 
# 
# get_exec_result 
#
# runs a shell command and returns the output
#
sub get_exec_result {
    if ($use_open3) {
        return get_exec_result_open3(@_);
    }
    else {
        return get_exec_result_qx(@_);
    }
}

sub get_exec_result_qx {
    my ($cmd) = @_; 
    print "qx(shell) $cmd\n" if $verbose;
    my $output = qx($cmd );
    if ($?) {
        die ("error executing $cmd ($!)\n");
    }
    print "qx(shell) $output\n" if $verbose;
    return $output;
}

sub get_exec_result_open3 {
    my ($cmd) = @_; 
    print "o3(shell) $cmd\n" if $verbose;
    my ($out, $in, $err) = (undef, undef, 1);
    my $result = open3($in, $out, $err, $cmd );
    my $output ='';
    my $err_output ='';
    while (my $line = <$out>) {
        $output .= $line;
    }
    while (my $line = <$err>) {
        $err_output .= $line;
    }
    if ($? or $err_output) {
        die ("error executing $cmd ($! - $err_output)\n");
    }
    print "o3(shell) $output - $err_output\n" if $verbose;
    return $output;
}

#
# get_sql_result
#
#  runs a SQL command and returns the output
#
sub get_sql_result {
    my ($sb, $query) = @_;
    print "(sql) $query\n" if $verbose;
    if ( -f "$sb/use" ) {
        # print qq(<echo "$query" | $sb/use -N -B >\n);
        my $command = 'use -N -B ';
        if ($query =~ /show\s+slave\s+status.*\\G/i) {
            $command = 'use -B';
        }
        my $output = qx(echo "$query" | $sb/$command );
        if (defined $output) {
            chomp $output;
        }
        else {
            $output = '';
        }
        if ($verbose && $verbose > 1) {
            print "$output\n";
        }
        die "error executing query $query on sandbox $sb\n" if $?;
        return $output;
    }
    else {
        die "can't find a 'use' command on $sb\n";
    }
}

 
#
# get_number_of_processes
#
# returns the number of processes for a given name
#
sub get_number_of_processes {
    my ($proc_name) = @_;

    my $grep_cmd = 'ps -ef | grep -w %s | grep -v "grep -w %s" | wc -l ';
    my $cmd = sprintf($grep_cmd, $proc_name, $proc_name );
    tprint "$cmd\n" if ($verbose && ($verbose > 1));
    my $how_many = get_exec_result($cmd, 0);
    return $how_many;
}

#
# help
#
# displays option for this program
#
sub help {
my $HELP = <<"HELP";

test for MySQL Sandbox
usage: $0 [options]
    --versions=version1[,version2,version3]
      uses specific versions for testing. 
      currently: (@{[join ",", @versions]})

    --tarball=/path/to/tarball
      it's an alias for --versions

    --tests=testname[,testname,testname]
      executes specific tests.
      currently: (@{[join ",", grep {$tests{$_}} keys %tests]})

    --user_test=filename
      executes user defined tests from given file 
      (implies --tests=user)

    --preserve_tests
      does not remove sandboxes at the end of the suite. 

    --verbose
      shows the commands executed during tests

    --help
      shows this help

HELP
    print $HELP;
    exit(1);
}

#
# TEST ROUTINES
#

#
# ok
#
# evaluates a condition and prints a ok/not ok message
#
sub ok ($;$) {
    my ($condition, $msg) = @_;
    # print Dumper \@_;
    $msg = '***' unless defined $msg;
    $test_results{'run'}++;
    if ($condition) {
        $test_results{'passed'}++;
    }
    else {
        print "not ";
        $test_results{'failed'}++;
    }
    print "ok $test_results{'run'} - $msg\n";
    if ($verbose) { die "halting the test on verbose\n" unless $condition; }
    return $condition;
}

sub run_single_test {
    my ($ver, $version, $bare_version) = @_;
    ok_exec( {
            command  => "make_sandbox $ver -- --no_confirm", 
            expected => 'sandbox server started',
            msg      => "single SB ($bare_version) started"
            });
    ok( -f "$sandbox_home/plugin.conf", "plugin template file found");
    ok_exec( {
            command  => "perl -c $sandbox_home/plugin.conf > /dev/null 2>&1", 
            expected => 'ok',
            msg      => "plugin template compiles OK"
            });
     ok_sql( {
            path     => "$sandbox_home/msb_$version", 
            query    => 'select version(), @@server_id',
            expected => $bare_version,
            msg      => "single SB ($bare_version) SQL"
            });
    get_sql_result(
            "$sandbox_home/msb_$version", 
            'create database \`a-a\`', # Bug#278394 - this will fail on 'clear' if not fixed
            $verbose );
}

sub ok_sql {
    my ($params, $wanted_result) = @_;
    for my $p (qw(path query expected msg)) {
        unless (defined $params->{$p}) {
            croak "parameter <$p> not defined\n";
        }
    }
    my $result = get_sql_result(
            $params->{'path'},
            $params->{'query'}, 
            $verbose);
    if ($wanted_result && ref($wanted_result) eq 'SCALAR')  {
        $$wanted_result = $result;
    }
    if (! ref($params->{'expected'}) ) {
        if ($params->{'expected'} =~ /^ok$/i) {
            return ok($CHILD_ERROR == 0 , $params->{'msg'});
        }
        elsif ($params->{'expected'} =~ s/^\!//) {
            return ok( $result !~ /$params->{'expected'}/i , $params->{'msg'});
        }
        else {
            return ok( $result =~ /$params->{'expected'}/i , $params->{'msg'});
        }
    }
    elsif (ref($params->{'expected'}) eq 'ARRAY') {
        my $count = 0;
        for my $expected (@{$params->{'expected'}}) {
            $count++;
            if ($expected =~ /^ok$/i) {
                ok($CHILD_ERROR == 0 , $params->{'msg'} . "-[$count]");
            }
            elsif ( $expected =~ s/^!//) {
                ok( $result !~ /$expected/i , $params->{'msg'} . "-[$count]");
            }
            else {
                ok( $result =~ /$expected/i , $params->{'msg'} . "-[$count]");
            }
        }
    }
    else {
        croak "parameter 'expected' is neither a scalar nor an array reference\n";
    }
}

sub ok_exec {
    my ($params) = @_;
    for my $p (qw(command expected msg)) {
        unless (defined $params->{$p}) {
            croak "parameter <$p> not defined\n";
        }
    }
    my $return_code_wanted = 0;
    
    if (ref $params->{'expected'}) {
        $return_code_wanted = grep { /^ok$/i} @{ $params->{'expected'}};
    }
    else {
        $return_code_wanted = $params->{'expected'} =~ /^ok$/i;
    }
    my $result;
    if ($return_code_wanted) {
        $result = get_exec_result_qx($params->{'command'});
    }
    else {
        $result = get_exec_result($params->{'command'});
    }
    if ($verbose) {
        print $result, "\n";
    }
    if (! ref $params->{'expected'} ) {
        if ($return_code_wanted) {
            return ok($CHILD_ERROR == 0 , $params->{'msg'});
        }
        elsif ($params->{'expected'} =~ s/^\!//) {
            return ok( $result !~ /\Q$params->{'expected'}\E/, $params->{'msg'} );
        }
        else {
            return ok( $result =~ /\Q$params->{'expected'}\E/, $params->{'msg'} );
        }
    }
    elsif (ref( $params->{'expected'}) eq 'ARRAY') {
        my $count =0;
        for my $expected (@{$params->{'expected'}}) {
            $count++;
            if ($expected =~ /^ok$/i) {
                ok( $CHILD_ERROR == 0 , 
                    $params->{'msg'} . "-[$count]");
            }
            elsif ($expected =~ s/^\!//) {
                ok( $result !~ /\Q$expected\E/, $params->{'msg'} . "-[$count]" );
            }
            else {
                ok( $result =~ /\Q$expected\E/, $params->{'msg'} . "-[$count]" );
            }
        }
    }
    else {
        croak "parameter 'expected' is neither a scalar nor an array reference\n";
    }
}

sub run_replication_test {
    my ($ver, $version, $bare_version) = @_;

    ok_exec({ command  => "make_replication_sandbox $ver",
              expected => '!not started yet',
              msg      => "replication sandbox ($bare_version) started"
            });

    ok_sql ({ path => "$sandbox_home/rsandbox_$version/master",
             query => 'select version(), @@server_id',
             expected => [ $bare_version, '\b1\s*$'],
             msg => "replication SB master ($bare_version) SQL - version - server_id"
            });
    
    sleep 1;

    for my $node (1, 2) {
        ok_sql ({ path => "$sandbox_home/rsandbox_$version/node$node",
             query => 'select version(), @@server_id',
             expected => [ $bare_version, '\b10' . $node . '\s*$'],
             msg => "replication SB slave$node ($bare_version) SQL - version - server_id"
            });
    }
 
    ok_sql ({ path => "$sandbox_home/rsandbox_$version/master",
             query => q{drop table if exists test.t1; create table test.t1 (id int); show tables from test},
             expected => 't1',
             msg => "replication SB - table created on master"
            });
 
    for my $node (1, 2) {
        ok_sql ({ path => "$sandbox_home/rsandbox_$version/node$node",
                 query => q{show tables from test},
                 expected => 't1',
                 msg => "replication SB - table exists on slave$node"
                });
    }
}

sub run_circular_test {
    my ($ver, $version, $bare_version) = @_;
    
    ok_exec({ command  => "make_replication_sandbox --how_many_slaves=3 --topology=circular $ver",
              expected => '!not started yet',
              msg      => "circular replication sandbox ($bare_version) started",
        });

    for my $node ( 1 .. 3) {
        ok_sql ({ path     => "$sandbox_home/rcsandbox_$version/node$node",
                  query    => 'show slave status\G',
                  expected => [ 'IO_Running.+Yes', 'SQL_Running.+Yes' ], 
                  msg      => "circular replication SB node$node ($bare_version) SQL - Running"
            }
        );
        sleep 1;
    }
}

sub run_multiple_test {
    my ($ver, $version, $bare_version) = @_;

    ok_exec({ command => "make_multiple_sandbox $ver",
              expected => 'group directory installed',
              msg      => "multiple sandbox ($bare_version) started"
            });

    for my $node (1 .. 3 ) {

        ok_sql({ path  => "$sandbox_home/multi_msb_$version/node$node",
                 query => 'select version(), @@server_id', 
                 expected => [ $bare_version, '\b10' . $node . '\s*$' ],
                 msg      => "multiple SB node $node ($bare_version) SQL - version - server_id"
                });
    }
}

sub run_mcustom_test {

    my $gdname = 'multi_cmsb_' . get_bare_version($versions[0]) ;
    #$custom_dir =~ s/ /-/g;
    #$custom_dir =~ s/\./_/g;
    # $DEBUG=2; $verbose=2; 
    $gdname =~ s/\./_/g;
    my $custom_dir = "$sandbox_home/$gdname";
    ok_exec({ command => "make_multiple_custom_sandbox --group_directory=$gdname @versions ",
              expected => [
                    '!not started yet', 
                    "group directory installed in " . use_env($custom_dir) 
                    ],
              msg      => "custom sandbox ( ". use_env($custom_dir)." ) started"
            });
    ok(-d $custom_dir, 'custom group directory exists')
            or die "custom group directory not created\n";
    my $counter = 0;
    for my $ver (@versions) {
        my $bare_version = get_bare_version($ver);
        $counter++;
	if ( -d  "$custom_dir/node$counter" ) {
            ok_sql({ path   => "$custom_dir/node$counter",
                 query  => 'select version(), @@server_id',
                 expected => [$bare_version, '\b10' . $counter . '\s*$'],
                 msg      => "multiple custom SB node $counter ($bare_version) SQL - version - server_id"
                });
	}
	else {
		ok(1, 'test skipped');
	}
    }
}

sub run_summary_tests {
    my $new_mysqld_procs = get_number_of_processes('mysqld');
    my $new_mysqld_safe_procs = get_number_of_processes('mysqld_safe');

    tprintf "** created (%d) mysqld processes and (%d) mysqld_safe processes\n", 
        $new_mysqld_procs - $how_many_mysqld, 
        $new_mysqld_safe_procs - $how_many_mysqld_safe;

    my $instances = 
               (  1 * $tests{'single'}          # single 
                + 3 * $tests{'replication'}     # replicated
                + 3 * $tests{'circular'}        # circular
                + 3 * $tests{'multiple'}        # multiple
              );
    my $expected_processes = 
                $instances * scalar(@versions)
                + (scalar(@versions) * $tests{'custom'})  ; # custom counts only once

    ok( $expected_processes == ($new_mysqld_safe_procs - $how_many_mysqld_safe), 
            "expected processes ($expected_processes)" );

    tprint "** stopping all - please wait\n";
    my $stop_all = get_exec_result("$sandbox_home/stop_all");

    $new_mysqld_procs = get_number_of_processes('mysqld');
    $new_mysqld_safe_procs = get_number_of_processes('mysqld_safe');

    tprintf "** (%d) mysqld processes and (%d) mysqld_safe processes\n", 
        $new_mysqld_procs - $how_many_mysqld, 
        $new_mysqld_safe_procs - $how_many_mysqld_safe;
    ok( ($new_mysqld_safe_procs - $how_many_mysqld_safe ) == 0,
            'expected processes (0)' );

    tprint "** starting all - please wait\n";
    my $start_all = get_exec_result("$sandbox_home/start_all");

    $new_mysqld_procs = get_number_of_processes('mysqld');
    $new_mysqld_safe_procs = get_number_of_processes('mysqld_safe');
    tprintf "** created (%d) mysqld processes and (%d) mysqld_safe processes\n", 
        $new_mysqld_procs - $how_many_mysqld, 
        $new_mysqld_safe_procs - $how_many_mysqld_safe;

    ok( $expected_processes == ($new_mysqld_safe_procs - $how_many_mysqld_safe), 
            "expected processes ($expected_processes)" );

    unless ($ENV{'PRESERVE_TESTS'} or $preserve_tests) { 
        tprint "** cleaning up - please wait\n";
        my $clear_all = get_exec_result("$sandbox_home/clear_all");

        $new_mysqld_procs = get_number_of_processes('mysqld');
        $new_mysqld_safe_procs = get_number_of_processes('mysqld_safe');

        tprintf "** (%d) mysqld processes and (%d) mysqld_safe processes\n", 
            $new_mysqld_procs - $how_many_mysqld, 
            $new_mysqld_safe_procs - $how_many_mysqld_safe;
        ok( ($new_mysqld_safe_procs - $how_many_mysqld_safe ) == 0,
            'expected processes (0)' ) 
            or die "can't continue without a clean environment\n";

        system qq(rm -rf $sandbox_home/*) ;
    }
}

sub run_tuning_test {
    my $ver;
    for my $v (@versions) {
        $ver = $v;
        if ($ver =~ /^[^34]/) {
            last;
        }
    }
    if ($ver =~ /^[34]/) {
        print "skipping tuning test. It requires version >=5\n";
        return;
    }
    my ($bare_version, $version) = get_bare_version($ver);

    if ($bare_version =~ /^5\.([56])/ ) {
         for my $i ( 1 .. 6) { 
            print "ok - skipped (this test is not available in MySQL 5.$1)\n";
         }
         return;
    }

    ok_exec({ command  => "make_sandbox $ver -- --no_confirm -c skip-innodb -c sql_mode=strict_all_tables",
              expected => 'sandbox server started',
              msg      => 'tuning server created',
            });

    ok_sql({ path   => "$sandbox_home/msb_$version",
             query  => 'show engines',
             expected => '!innodb\s*yes',
             msg      => "single SB with option skip-innodb ($bare_version) SQL"
            });
     
    ok_sql({  path => "$sandbox_home/msb_$version",
              query => q(show variables like 'SQL_MODE'),
              expected => 'STRICT_ALL_TABLES',
              msg      => "single SB with option sql_mode ($bare_version) SQL "
            });

    ok_sql({ path   => "$sandbox_home/msb_$version",
             query  => q(create database xyz; show databases like 'xyz'),
             expected => 'xyz',
             msg => "single SB ($bare_version) - create database"
            });

    ok_exec({  command => "$sandbox_home/msb_$version/clear",
               expected => '!error',
               msg      => "single SB ($bare_version) - clear result"
            });

    # my $sandbox_dirs = get_exec_result("ls -d $sandbox_home/msb_$version/data/*/ | wc -l ");
    my $sandbox_dirs = how_many_dirs("$sandbox_home/msb_$version/data");

    ok($sandbox_dirs == ($bare_version ge '5.5'? 3 : 2) , "single SB ($bare_version) - effective clear ");

    tprint "** cleaning up - please wait\n";
    my $clear_all = get_exec_result("$sandbox_home/clear_all");
}

sub get_bare_version {
    my ($ver) = @_;
    $ver =~ s{.*/}{};
    if ($ver =~ /((?:\w+)?\d+\.\d+\.\d+)/) {
        my $bv = $1;
        my $underscored_version = $bv;
        $underscored_version =~ s/\./_/g;
        $bv =~ s/^\D+//;
        # print STDERR "$ver, $bv, $underscored_version \n"; exit;
        if (wantarray) {
            return ($bv, $underscored_version);
        }
        else {
            return $bv;
        }
    }
    else {
        die "'$ver'  does not contain a valid version\n";
    }
}

sub run_smoke_test {
    my ($ver) = @_;
    if ($ver =~ /^[34]/) {
        tprint "skipping smoke test. It requires version >=5\n";
        return;
    }
    my ($bare_version, $version) = get_bare_version($ver);
    $version =~ s/\./_/g;
    if ($ver =~ m{(.+)/[^/]+(?:tgz|tar\.gz)$}) {
        my $bindir = $1;
        if ( -d "$bindir/$bare_version" ) {
            system "rm -rf $bindir/$bare_version";
        } 
    }

    my $previous_mysqld = get_number_of_processes('mysqld');
    my $previous_mysqld_safe = get_number_of_processes('mysqld_safe');

    ok_exec({ command => "make_sandbox $ver -- --no_confirm",
              expected => 'sandbox server started',
              msg      => "single SB ($bare_version) started"
            });

    my $first_mysqld = get_number_of_processes('mysqld');
    my $first_mysqld_safe = get_number_of_processes('mysqld_safe');

    ok($first_mysqld_safe > $previous_mysqld_safe, 'mysqld_safe started') ;
    ok($first_mysqld > $previous_mysqld, 'mysqld started') ;

    my $sql_result = get_sql_result(
            "$sandbox_home/msb_$version", 
            q{show variables like 'pid_file'},
            $verbose);
    my $pid_file ;
    if ($sql_result =~ m{\s*(\S+\.pid)} ) {
        $pid_file = $1;
    }
    ok ($pid_file , 'pid_file found')
        or die "can't find pid file\n";

    my $pid = get_pid($pid_file)
        or die "can't get PID from $pid_file\n" ;
    my $pid_ts1 = get_pid_timestamp($pid_file)
        or die "can't get timestamp for file $pid_file\n";

    my $kill_result = get_exec_result("kill -9 $pid");
    ok (! $kill_result, "mysqld killed");

    my $timeout = 20;
    my $counter = 0;
    my $second_mysqld = 0;
    my $started ;
    while (! $started ) {
        $counter++;
        if ($counter >= $timeout) {
            last;
        }
        $second_mysqld = get_number_of_processes('mysqld');
        $started = $second_mysqld >= $first_mysqld;
        unless ($started) {
            tprint "-- waiting for mysqld to restart ($counter)\n";
            sleep 1;
        }
    }
    my $pid_ts2 = get_pid_timestamp($pid_file);
    $counter = 0;
    # print "PID_TS ==== $pid_ts1 $pid_ts2\n";
    while ((!$pid_ts2) or ($pid_ts2 eq $pid_ts1)) {
        $counter++;
        tprint ">> waiting for mysqld to restart ($counter)\n";
        sleep 1; 
        $pid_ts2 = get_pid($pid_file);
        if ($counter > $timeout) {
            die "error recovering killed process\n";
        }
    }
    ok ($second_mysqld >= $first_mysqld, "new mysqld process created (1)") ;
    my $secondpid = get_pid($pid_file)
        or die "can't get PID from $pid_file\n" ;

    chomp $secondpid;
    tprint "previous pid ($pid) current pid ($secondpid)\n" if $verbose;
    ok ($secondpid ne $pid, "new mysqld process created (2)");

    ok_sql({ path   => "$sandbox_home/msb_$version",
             query  => 'select version()',
             expected => $bare_version,
             msg      => "single SB ($bare_version) version (1)"
            });

    my $visual_version ;

    ok_sql({ path   => "$sandbox_home/msb_$version",
             query  => q{show variables like 'version'},
             expected => $bare_version,
             msg      => "single SB ($bare_version) version (2)"
            }, \$visual_version);

    chomp $visual_version;
    $sql_result = get_sql_result(
            "$sandbox_home/msb_$version", 
            'select @@version_comment limit 1',
            $verbose );
    my $visual_comment = $sql_result;
    chomp $visual_comment;

    $sql_result = get_sql_result(
           "$sandbox_home/msb_$version", 
            'HELP SELECT',
            $verbose );
    # print "HELP: $sql_result\n";
    ok( $sql_result && (! ( $sql_result =~ /nothing\s*found/i)), 
            "single SB ($bare_version) HELP tables filled " );
     
    ok_sql ({ path      => "$sandbox_home/msb_$version",
              query     => q(create database install_test; show databases like 'install_test'),
              expected  => 'install_test',
              msg       => "single SB ($bare_version) - create database"
            });

    my @test_engines = (
        {
            min_version => 5 ,
            engine  => 'innodb'
        },
        {
            min_version => 6 ,
            engine  => 'falcon'
        },
        {
            min_version => 6 ,
            engine  => 'maria'
        },
    );

    for my $test_engine (@test_engines) {
        my $major_version = 0; 
        if ($version =~ /(\d)/) {
            $major_version = $1;
        }
        if ($major_version <  $test_engine->{'min_version'} ) {
            next;
        }
        my $engine = $test_engine->{'engine'};

        ok_sql ({ path      => "$sandbox_home/msb_$version",
              query     => 
                qq{drop table if exists install_test.testib;
                create table install_test.testib ( 
                id int UNSIGNED NOT NULL AUTO_INCREMENT , 
                nr int UNSIGNED NOT NULL , 
                PRIMARY KEY ( id)
                ) engine=$engine; show tables from install_test },
              expected  => 'testib',
              msg       => "single SB ($bare_version) - create table"
            });

        ok_sql ({ path      => "$sandbox_home/msb_$version",
              query     => q{select engine from information_schema.tables
                    where table_schema='install_test'
                    and table_name = 'testib' },
              expected  => $engine,
              msg       => "single SB ($bare_version) - create table - check engine ($engine)"
            });

        ok_sql ({ path      => "$sandbox_home/msb_$version",
              query     => q{alter table install_test.testib add index nr (nr);
            insert into install_test.testib(id,nr) values ( '1','1');
            insert into install_test.testib(id,nr) values ( '2','2');
            insert into install_test.testib(id,nr) values ( '3','2');
            select * from install_test.testib where nr=2 order by id asc;},
              expected  => '2\t2\s*3\t2',
              msg       => "smoke 1"
            });

    }

    ok_sql ({ path      => "$sandbox_home/msb_$version",
              query     =>  q{select * from install_test.testib where nr=2 order by id desc;},
              expected  => '3\t2\s*2\t2',
              msg       => "smoke 2"
            });

    ok_sql ({ path      => "$sandbox_home/msb_$version",
              query     =>  q{select count(*) from install_test.testib},
              expected  => '^3$',
              msg       => "rows in table"
            });

    ok_sql ({ path      => "$sandbox_home/msb_$version",
              query     =>    q{truncate table install_test.testib;}
                            . q{select count(*) from install_test.testib},
              expected  => '^0$',
              msg       => "table truncation"
            });

    ok_sql ({ path      => "$sandbox_home/msb_$version",
              query     =>    q{drop table install_test.testib;}
                            . q{select count(*) from information_schema.tables }
                            . q{where table_schema='install_test'},
              expected  => '^0$',
              msg       => "tables in install_test "
            });

    $sql_result = get_sql_result(
           "$sandbox_home/msb_$version", 
            q{ drop database install_test;},
            $verbose);
    my $sandbox_dirs = how_many_dirs("$sandbox_home/msb_$version/data");

    ok($sandbox_dirs == ($bare_version ge '5.5'? 3 : 2) , "single SB ($bare_version) - effective clean up ");

    tprint "** cleaning up - please wait\n";
    my $clear = get_exec_result("$sandbox_home/msb_$version/clear");
    if ($ver =~ m{(.+)/[^/]+(?:tgz|tar\.gz)$}) {
        my $bindir = $1;
        if ( -d "$bindir/$bare_version" ) {
            system "rm -rf $bindir/$bare_version";
            system "rm -rf $sandbox_home/msb_$version";
        } 
    }

    tprint "\n[VISUAL IDENTIFICATION]\n";
    tprint "\tcheck version: <$visual_version>\n";
    tprint "\tcheck comment: <$visual_comment>\n\n";
}


sub run_sbtool_test {
    my ($ver) = @_;

    # 
    # preparing sandboxes
    #
    my $first_sandbox = get_exec_result(
            "make_sandbox $ver -- --no_confirm --sandbox_port=5100 --sandbox_directory=first_sb", 
            $verbose);
    ok( $first_sandbox =~ /sandbox server started/,
            "first SB (5100) started" );


    my $second_sandbox = get_exec_result(
            "make_sandbox $ver -- --no_confirm --sandbox_port=5200 --sandbox_directory=second_sb", 
            $verbose);
    ok( $second_sandbox =~ /sandbox server started/,
            "second SB (5200) started" );

    #
    # sbtool -o move
    #
    my $move_sandbox = get_exec_result("sbtool -o move -s $sandbox_home/first_sb -d $sandbox_home/xxx");
    ok( $move_sandbox =~ /The old scripts have been saved/, 'first SB moved to xxx');

    $first_sandbox = get_exec_result("$sandbox_home/xxx/start");
    ok( $first_sandbox =~ /sandbox server started/, "moved sandbox started");

    $move_sandbox = get_exec_result("sbtool -o move -d $sandbox_home/first_sb -s $sandbox_home/xxx");
    ok( $move_sandbox =~ /The old scripts have been saved/, 'xxx moved back to first SB');

    $first_sandbox = get_exec_result("$sandbox_home/first_sb/start");
    ok( $first_sandbox =~ /sandbox server started/, "moved back sandbox started");
    
    #
    # sbtool -o copy
    #
    my $sql_result = get_sql_result(
            "$sandbox_home/first_sb",
           q{drop table if exists test.t1; create table test.t1 (id int); show tables from test},
          $verbose); 
    ok( ($sql_result =~ /t1/) , 
            "first SB - table created" );

    my $copy_sandbox = get_exec_result("sbtool -o copy -s $sandbox_home/first_sb -d $sandbox_home/second_sb");
    ok( $CHILD_ERROR == 0 , 'first SB copied to second SB');

    $second_sandbox = get_exec_result("$sandbox_home/second_sb/start");
    ok( $second_sandbox =~ /sandbox server started/, "copied sandbox started");

    $sql_result = get_sql_result(
            "$sandbox_home/second_sb",
           q{show tables from test},
          $verbose); 
    ok( ($sql_result =~ /t1/) , 
            "second SB - table copied" );

    # 
    # sbtool -o ports
    #
    my $get_ports = get_exec_result("sbtool -o ports");
    ok ($get_ports =~ /5100\s+0/ && $get_ports =~ /5200\s+1/, 'get ports');
    $get_ports = get_exec_result("sbtool --only_used -o ports");
    ok ($get_ports !~ /5100\s+0/ && $get_ports =~ /5200\s+1/, 'get used ports');

    $get_ports = get_exec_result("sbtool --format=perl -o ports");
    # print $get_ports, "\n";
    my $ports = undef;
    eval "$get_ports";
    ok ( $ports && !$ports->{'5100'} && $ports->{'5200'}, 'get ports (perl format)');

    # 
    # sbtool -o info
    #
    my $get_info = get_exec_result("sbtool -o info");
    my $all_info = undef;
    eval "$get_info";
    ok($all_info && $all_info->{'5100'}{'opt'}{'port'} == 5100, 'get info 1');
    ok($all_info && $all_info->{'5200'}{'opt'}{'port'} == 5200, 'get info 2');

    # 
    # sbtool -o port 
    # (change port)
    #
    my $change_ports = get_exec_result("sbtool -o port --new_port=9000 -s $sandbox_home/first_sb");
    
    ok( $change_ports =~ /The old scripts have been saved/, 'port changed to first SB');

    $change_ports = get_exec_result("$sandbox_home/first_sb/start");
    ok( $change_ports =~ /sandbox server started/, 'first SB restarted after port changed');

    $sql_result = get_sql_result( "$sandbox_home/first_sb", q{show variables like 'port'});
    ok($sql_result =~ /\b9000\b/, 'wanted port assigned to first SB');

    # 
    # sbtool -o range
    #
    my $ports_range = get_exec_result("sbtool -o range");
    ok ($ports_range =~ /5010/, 'ports range');
    $ports_range = get_exec_result("sbtool -o range -i 5200");
    ok ($ports_range =~ /5201/, 'ports range 2');
    system("$sandbox_home/stop_all > /dev/null 2>&1");
    system("$sandbox_home/second_sb/start > /dev/null 2>&1");
    
    # 
    # sbtool -o delete
    #
    my $delete_sandbox = get_exec_result("sbtool -o delete -s $sandbox_home/first_sb");
    ok($delete_sandbox =~ /has been removed/, 'delete stopped sandbox');
    $delete_sandbox = get_exec_result("sbtool -o delete -s $sandbox_home/second_sb");
    ok($delete_sandbox =~ /has been removed/, 'delete active sandbox');

    $second_sandbox = get_exec_result(
            "make_sandbox $ver -- --no_confirm --sandbox_port=5200 --sandbox_directory=second_sb", 
            $verbose);
    ok( $second_sandbox =~ /sandbox server started/,
            "second SB (5200) started" );

    # 
    # sbtool -o preserve
    #
    my $preserve_sandbox = get_exec_result("sbtool -o preserve -s $sandbox_home/second_sb");
    ok($preserve_sandbox =~ /is now permanent/, 'preserve active sandbox');
    $delete_sandbox = get_exec_result("sbtool -o delete -s $sandbox_home/second_sb");
    ok($delete_sandbox =~ /can't be deleted/, '(not) deleting permanent sandbox');

    my $unpreserve_sandbox = get_exec_result("sbtool -o unpreserve -s $sandbox_home/second_sb");
    ok($unpreserve_sandbox =~ /is now NOT PERMANENT/, 'unpreserve active sandbox');

    $delete_sandbox = get_exec_result("sbtool -o delete -s $sandbox_home/second_sb");
    ok($delete_sandbox =~ /has been removed/, 'deleting unpreserved sandbox');

    # 
    # sbtool -o tree
    #
    my $group_sandbox = get_exec_result(
            "make_multiple_sandbox --group_directory=tree_sb --how_many_nodes=9 $ver", 
            $verbose);

    ok( $group_sandbox =~ /installing node 9/,
            "group SB (node 9) installed" );

    ok( $group_sandbox =~ /group directory installed/,
            "group SB installed" );

    my $tree_sandbox = get_exec_result("sbtool -o tree --tree_dir=$sandbox_home/tree_sb"
            . " --tree_nodes='1-2 3 4-5 6|7|8 9'" );
    ok($tree_sandbox =~ 'node 1 is master', 'tree SB master');
    ok($tree_sandbox =~ 'node 2 is slave of node 1', 'tree SB - level 1');
    ok($tree_sandbox =~ 'node 7 is slave of node 3', 'tree SB - level 2');

    $sql_result = get_sql_result(
            "$sandbox_home/tree_sb/node1",
           q{drop table if exists test.t1; create table test.t1 (id int); show tables from test},
          $verbose); 
    ok( ($sql_result =~ /t1/) , 
            "tree SB - table created" );

    sleep 1;
    $sql_result = get_sql_result(
            "$sandbox_home/tree_sb/node8",
           q{show tables from test},
          $verbose); 
    #print "$sql_result\n";
    ok( ($sql_result =~ /t1/) , 
            "tree SB - table replicated to level 3" );

    system("$sandbox_home/send_kill_all > /dev/null 2>&1");
    system("$sandbox_home/clear_all > /dev/null 2>&1");
}

sub get_pid {
    my ($pfile) = @_;
    my $timeout = 5;
    my $counter = 0;
    my $PFILE;
    while (! $PFILE) {
        eval {
            open( $PFILE, q{<}, $pfile)
                or die;
        };
        next if $@;
        last if $PFILE;
        if ($counter >= $timeout) {
            die "can't open $pfile\n";
        }
        else {
            sleep 1;
        }
        $counter++;
    }
    print Dumper $PFILE if $verbose;
    die "can't open $pfile\n" unless $PFILE;
    my $pid = <$PFILE>;
    close $PFILE;
    chomp $pid;
    if ($pid &&  ($pid =~ /^\d+$/) ) {
        return $pid;
    }
    return 0;
}

sub get_pid_timestamp {
    my ($pidfile) = @_;

    my @stats = stat $pidfile
        or return 0;
    #    or die "can't get timestamp for file $pidfile ($!)\n";    
    return $stats[8];
}

sub how_many_dirs {
    my ($path) = @_;
    my @subdirs = glob("$path/*/");
    my $dir_count =0;
    for (@subdirs) {
        $dir_count++ if -d $_;
    }
    return $dir_count;
}

sub exists_in_path {
    my ($fname) = @_;
    my @paths = split /:/, $ENV{'PATH'};
    for my $path (@paths) {
        $path =~ s{/$}{};
        if ( -f "$path/$fname") {
            return 1;
        }
    }
    return 0;
}

sub tprint {
    unless ($ENV{'TAP_MODE'}) {
        print @_;
    }
}

sub tprintf {
    unless ($ENV{'TAP_MODE'}) {
        printf @_;
    }
}

sub run_user_defined_test {
    my ($test_file) = @_;
    if ($test_file =~ /\.sb\.pl$/) {
        return run_user_defined_perl_test($test_file);
    }
    my %test_structs = (
        shell => {required => [qw(command expected msg)], rec => undef},
        sql   => {required => [qw(path query expected msg)], rec => undef},
    );
    my @user_tests = ();
    my $current_struct = undef;
    open my $TFILE, q{<}, $test_file
        or die "can't open $test_file ($!)\n";
    while (my $line = <$TFILE>) {
        next if $line =~ /^\s*$/;
        next if $line =~ /^\s*#/;
        chomp($line);
        if ($line =~ /(\w+):/) {
            my $struct = $1;
            unless ($test_structs{$struct}) {
                die "unrecognized test type\n"
            }
            if ($current_struct) {
                my %rec = %{$test_structs{$current_struct}{rec}};
                push @user_tests, [$current_struct,\%rec];
                $test_structs{$current_struct}{rec} = undef;
            }
            $current_struct = $struct;
        }
        elsif ( $line =~ /(\w+)\s*=\s*(.*)/ ) {
            my $key = $1;
            my $value = $2;
            $value =~ s/\$(\w+)/$ENV{$1}/g;
            $test_structs{$current_struct}{rec}{$key} = $value;
        }
        else {
            die "error parsing line <$.> at file <$test_file>\n";
        }
    }
    close $TFILE;
    if ($current_struct) {
        my %rec = %{$test_structs{$current_struct}{rec}};
        push @user_tests, [$current_struct,\%rec];
        $test_structs{$current_struct}{rec} = undef;
    }
    for my $test (@user_tests) {
        for my $req (@{ $test_structs{$test->[0]}{required}}) {
            unless (defined $test->[1]{$req}) {
                for my $key (keys %{ $test->[1] } ) {
                    tprintf "%20s => %s\n", $key, $test->[1]{$key};
                }
                die "incomplete test. Missing required <$req> label\n";        
            }
        }
    }
    # print Dumper \@user_tests; exit;
    for my $test (@user_tests) {
        my $rec = $test->[1];
        # print Dumper $test;
        if ($test->[0] eq 'shell') {
            ok_exec({ command  => $rec->{command},
                      expected => $rec->{expected},
                      msg      => $rec->{msg},
                    });
        }
        elsif ($test->[0] eq 'sql') {
            ok_sql({ path     => $rec->{path},
                     query    => $rec->{query},
                     expected => $rec->{expected},
                      msg     => $rec->{msg},
                    });
        
        }
        else {
            die "unhandled test <$test->[0]>\n";
        }
    }

}

sub run_user_defined_perl_test {
    my ($test_file) = @_;
    open my $FH, q{<}, $test_file
        or die "can't open $test_file ($!)\n";
    my $test_contents ='';
    while (my $line = <$FH>) {
        $test_contents .= $line;
    }
    close ($FH);
    eval $test_contents;
    if ($@) {
        die "failed test $test_file\n$@\n";
    } 
}