polardbxengine/mysql-test/suite/xengine_stress/rqg/gentest.pl

736 lines
22 KiB
Perl
Executable File

#!/usr/bin/perl
# Copyright (C) 2008-2010 Sun Microsystems, Inc. All rights reserved.
# Use is subject to license terms.
#
# 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 lib 'lib';
use lib "$ENV{RQG_HOME}/lib";
use strict;
use Carp;
use Data::Dumper;
use IPC::ShareLite;
use IPC::SysV qw(S_IRUSR S_IWUSR IPC_CREAT IPC_NOWAIT SEM_UNDO);
use IPC::Semaphore;
use GenTest;
use GenTest::Properties;
use GenTest::Constants;
use GenTest::App::Gendata;
use GenTest::App::GendataSimple;
use GenTest::IPC::Channel;
use GenTest::IPC::Process;
use GenTest::ErrorFilter;
use GenTest::Controller;
use GenTest::Controller::KillRandom;
use GenTest::Controller::Stage;
$| = 1;
my $ctrl_c = 0;
$SIG{INT} = sub { $ctrl_c = 1 };
$SIG{TERM} = sub { exit(0) };
$SIG{CHLD} = "IGNORE" if windows();
if (defined $ENV{RQG_HOME}) {
$ENV{RQG_HOME} = windows() ? $ENV{RQG_HOME}.'\\' : $ENV{RQG_HOME}.'/';
}
use constant PROCESS_TYPE_PARENT => 0;
use constant PROCESS_TYPE_PERIODIC => 1;
use constant PROCESS_TYPE_CHILD => 2;
use constant PROCESS_TYPE_CONTROLLER => 3;
use constant PROCESS_TYPE_STAGE => 4;
use constant PROCESS_TYPE_KILL => 5;
# use POSIX;
use Getopt::Long;
use Time::HiRes;
use GenTest::XML::Report;
use GenTest::XML::Test;
use GenTest::XML::BuildInfo;
use GenTest::Constants;
use GenTest::Result;
use GenTest::Validator;
use GenTest::Generator::FromGrammar;
use GenTest::Executor;
use GenTest::Mixer;
use GenTest::Reporter;
use GenTest::ReporterManager;
use GenTest::Filter::Regexp;
my $DEFAULT_THREADS = 10;
my $DEFAULT_QUERIES = 1000;
my $DEFAULT_DURATION = 3600;
my $DEFAULT_DSN = 'dbi:mysql:host=127.0.0.1:port=9306:user=root:database=test';
my @ARGV_saved = @ARGV;
my $options = {};
my $shutdown = IPC::ShareLite->new(
-key => "shut",
-create => 'yes',
-destroy => 'no'
) or die $!;
my $kill_random_sem;
my $opt_result = GetOptions($options,
'config=s',
'dsn=s@',
'dsn1=s',
'dsn2=s',
'dsn3=s',
'engine=s',
'gendata:s',
'grammar=s',
'redefine=s',
'threads=i',
'queries=s',
'duration=s',
'help',
'debug',
'rpl_mode=s',
'validators:s@',
'reporters:s@',
'seed=s',
'mask=i',
'mask-level=i',
'rows=i',
'varchar-length=i',
'xml_output=s',
'views',
'start-dirty',
'filter=s',
'stage=i',
'kill=i',
'errorlevel=s',
'mycnf=s',
'valgrind');
backwardCompatability($options);
my $config = GenTest::Properties->new(
options => $options,
defaults => {dsn=>[$DEFAULT_DSN],
seed => 1,
queries => $DEFAULT_QUERIES,
duration => $DEFAULT_DURATION,
threads => $DEFAULT_THREADS},
required => ['grammar'],
legal => ['dsn',
'engine',
'gendata',
'redefine',
'threads',
'queries',
'duration',
'help',
'debug',
'rpl_mode',
'validators',
'reporters',
'seed',
'mask',
'mask-level',
'rows',
'varchar-length',
'xml-output',
'views',
'start-dirty',
'filter',
'valgrind',
'stage',
'kill',
'mycnf',
'errorlevel'],
help => \&help);
my $seed = $config->seed;
if ($seed eq 'time') {
$seed = time();
say("Converting --seed=time to --seed=$seed");
}
my $failure_level = STATUS_OK;
if ($config->errorlevel eq 'CRITICAL') {
$failure_level = STATUS_CRITICAL_FAILURE;
}
if ($config->errorlevel eq 'FAILURE') {
$failure_level = STATUS_TEST_FAILURE;
}
$ENV{RQG_DEBUG} = 1 if $config->debug;
my $queries = $config->queries;
$queries =~ s{K}{000}so;
$queries =~ s{M}{000000}so;
help() if !$opt_result || $config->help;
say("Starting \n $0 \\ \n ".join(" \\ \n ", @ARGV_saved));
say("-------------------------------\nConfiguration");
$config->printProps;
if ((defined $config->gendata) &&
(not defined $config->property('start-dirty'))) {
foreach my $dsn (@{$config->dsn}) {
next if $dsn eq '';
my $gendata_result;
my $datagen;
if ($config->gendata eq '') {
$datagen = GenTest::App::GendataSimple->new(dsn => $dsn,
views => $config->views,
engine => $config->engine);
} else {
$datagen = GenTest::App::Gendata->new(spec_file => $config->gendata,
dsn => $dsn,
engine => $config->engine,
seed => $seed,
debug => $config->debug,
rows => $config->rows,
views => $config->views,
varchar_length => $config->property('varchar-length'));
}
$gendata_result = $datagen->run();
safe_exit ($gendata_result >> 8) if $gendata_result > STATUS_OK;
}
}
my $test_start = time();
my $test_end = $test_start + $config->duration;
my $grammar = GenTest::Grammar->new(
grammar_file => $config->grammar
);
exit(STATUS_ENVIRONMENT_FAILURE) if not defined $grammar;
if (defined $config->redefine) {
my $patch_grammar = GenTest::Grammar->new(
grammar_file => $config->redefine);
$grammar = $grammar->patch($patch_grammar);
}
exit(STATUS_ENVIRONMENT_FAILURE) if not defined $grammar;
my $channel = GenTest::IPC::Channel->new();
my @executors;
foreach my $i (0..2) {
next if $config->dsn->[$i] eq '';
push @executors, GenTest::Executor->newFromDSN($config->dsn->[$i],
windows()?undef:$channel);
}
my $drizzle_only = $executors[0]->type == DB_DRIZZLE;
$drizzle_only = $drizzle_only && $executors[1]->type == DB_DRIZZLE if $#executors > 0;
my $mysql_only = $executors[0]->type == DB_MYSQL;
$mysql_only = $mysql_only && $executors[1]->type == DB_MYSQL if $#executors > 0;
if (not defined $config->reporters or $#{$config->reporters} < 0) {
$config->reporters([]);
if ($mysql_only || $drizzle_only) {
$config->reporters(['ErrorLog', 'Backtrace']);
}
} else {
## Remove the "None" reporter
foreach my $i (0..$#{$config->reporters}) {
delete $config->reporters->[$i]
if $config->reporters->[$i] eq "None"
or $config->reporters->[$i] eq '';
}
}
say("Reporters: ".($#{$config->reporters} > -1 ? join(', ', @{$config->reporters}) : "(none)"));
my $reporter_manager = GenTest::ReporterManager->new();
if ($mysql_only ) {
foreach my $i (0..2) {
next if $config->dsn->[$i] eq '';
foreach my $reporter (@{$config->reporters}) {
my $add_result = $reporter_manager->addReporter($reporter, {
dsn => $config->dsn->[$i],
test_start => $test_start,
test_end => $test_end,
test_duration => $config->duration
} );
exit($add_result) if $add_result > STATUS_OK;
}
}
}
if (not defined $config->validators or $#{$config->validators} < 0) {
$config->validators([]);
push(@{$config->validators}, 'ErrorMessageCorruption')
if ($mysql_only || $drizzle_only);
if ($config->dsn->[2] ne '') {
push @{$config->validators}, 'ResultsetComparator3';
} elsif ($config->dsn->[1] ne '') {
push @{$config->validators}, 'ResultsetComparator';
}
push @{$config->validators}, 'ReplicationSlaveStatus'
if $config->rpl_mode ne '' && ($mysql_only || $drizzle_only);
push @{$config->validators}, 'MarkErrorLog'
if (defined $config->valgrind) && ($mysql_only || $drizzle_only);
push @{$config->validators}, 'QueryProperties'
if $grammar->hasProperties() && ($mysql_only || $drizzle_only);
} else {
## Remove the "None" validator
foreach my $i (0..$#{$config->validators}) {
delete $config->validators->[$i]
if $config->validators->[$i] eq "None"
or $config->validators->[$i] eq '';
}
}
say("Validators: ".($config->validators and $#{$config->validators} > -1 ? join(', ', @{$config->validators}) : "(none)"));
my $filter_obj;
$filter_obj = GenTest::Filter::Regexp->new( file => $config->filter )
if defined $config->filter;
say("Starting ".$config->threads." processes, ".
$config->queries." queries each, duration ".
$config->duration." seconds.");
my $buildinfo;
if (defined $config->property('xml-output')) {
$buildinfo = GenTest::XML::BuildInfo->new(
dsns => $config->dsn
);
}
my $test = GenTest::XML::Test->new(
id => Time::HiRes::time(),
attributes => {
engine => $config->engine,
gendata => $config->gendata,
grammar => $config->grammar,
threads => $config->threads,
queries => $config->queries,
validators => join (',', @{$config->validators}),
reporters => join (',', @{$config->reporters}),
seed => $seed,
mask => $config->mask,
mask_level => $config->property('mask-level'),
rows => $config->rows,
'varchar-length' => $config->property('varchar-length')
}
);
my $report = GenTest::XML::Report->new(
buildinfo => $buildinfo,
tests => [ $test ]
);
my $errorfilter = GenTest::ErrorFilter->new(channel=>$channel);
my $errorfilter_p = GenTest::IPC::Process->new(object=>$errorfilter);
if (!windows()) {
$errorfilter_p->start();
}
my $process_type;
my %child_pids;
my $id = 1;
my $periodic_pid = fork();
# init some controllers even if we don't need.
my $controller_pid;
my $kill_random = GenTest::Controller::KillRandom->new((
dsn => $config->dsn->[0],
test_start => $test_start,
test_end => $test_end,
test_duration => $config->duration,
grammar => $grammar,
));
$shutdown->store(0);
say('thread: '.$config->threads);
$kill_random_sem = IPC::Semaphore->new('kill_random_sem',
$config->threads,
S_IRUSR | S_IWUSR | IPC_CREAT);
say("semaphore id ".$kill_random_sem->id);
$kill_random_sem->setall((1) x ($config->threads));
$kill_random->init($kill_random_sem, $config->threads, $config->mycnf);
my $stage_roller = GenTest::Controller::Stage->new((
dsn => $config->dsn->[0],
test_start => $test_start,
test_end => $test_end,
test_duration => $config->duration,
grammar => $grammar,
));
my $stage_pid;
if ($periodic_pid == 0) {
Time::HiRes::sleep(($config->threads + 1) / 10);
say("Started periodic reporting process...");
$process_type = PROCESS_TYPE_PERIODIC;
$id = 0;
$kill_random_sem->remove;
} else {
foreach my $i (1..$config->threads) {
my $child_pid = fork();
$channel->writer;
if ($child_pid == 0) { # This is a child
$process_type = PROCESS_TYPE_CHILD;
last;
} else {
$child_pids{$child_pid} = 1;
$process_type = PROCESS_TYPE_PARENT;
$seed++;
$id++;
Time::HiRes::sleep(0.1); # fork slowly for more predictability
next;
}
}
if ($process_type == PROCESS_TYPE_PARENT) {
foreach my $i (1..2) {
if ($i == 1) {
$stage_pid = fork();
if ($stage_pid == 0) {
$process_type = PROCESS_TYPE_STAGE;
if (not $config->stage) {
exit(0);
}
$stage_roller->init($config);
say("Started ".$i." stage process ".$$." ...");
last;
}
} else {
$controller_pid = fork();
if ($controller_pid == 0) {
$process_type = PROCESS_TYPE_KILL;
if (not $config->kill) {
exit(0);
}
say("Started ".$i." kill process ...");
last;
}
}
}
}
}
if ($process_type == PROCESS_TYPE_PARENT) {
if (windows()) {
## Important that this is done here in the parent after the last
## fork since on windows Process.pm uses threads
$errorfilter_p->start();
}
# We are the parent process, wait for for all spawned processes to terminate
my $children_died = 0;
my $total_status = STATUS_OK;
my $periodic_died = 0;
## Parent thread does not use channel
$channel->close;
while (1) {
my $child_pid = wait();
my $exit_status = $? > 0 ? ($? >> 8) : 0;
$total_status = $exit_status if $exit_status > $total_status;
if ($child_pid == $periodic_pid) {
$periodic_died = 1;
last;
} else {
$children_died++;
delete $child_pids{$child_pid};
}
last if $exit_status >= STATUS_CRITICAL_FAILURE;
last if $children_died == $config->threads;
last if $child_pid == -1;
}
$kill_random_sem->remove;
foreach my $child_pid (keys %child_pids) {
say("Killing child process with pid $child_pid...");
say('kill15 '.$child_pid);
kill(15, $child_pid);
}
if ($periodic_died == 0) {
# Wait for periodic process to return the status of its last execution
Time::HiRes::sleep(1);
say("Killing periodic reporting process with pid $periodic_pid...");
say('kill15 '.$periodic_pid);
kill(15, $periodic_pid);
if (windows()) {
# We use sleep() + non-blocking waitpid() due to a bug in ActiveState Perl
Time::HiRes::sleep(1);
waitpid($periodic_pid, &POSIX::WNOHANG() );
} else {
waitpid($periodic_pid, 0);
}
if ($? > -1 ) {
my $periodic_status = $? > 0 ? $? >> 8 : 0;
$total_status = $periodic_status if $periodic_status > $total_status;
}
}
say('kill9 '.$controller_pid);
kill(9, $controller_pid);
say('kill9 '.$stage_pid);
kill(9, $stage_pid);
$errorfilter_p->kill();
my @report_results;
if ($total_status == STATUS_OK) {
@report_results = $reporter_manager->report(REPORTER_TYPE_SUCCESS | REPORTER_TYPE_ALWAYS);
} elsif (
($total_status == STATUS_LENGTH_MISMATCH) ||
($total_status == STATUS_CONTENT_MISMATCH)
) {
@report_results = $reporter_manager->report(REPORTER_TYPE_DATA);
} elsif ($total_status == STATUS_SERVER_CRASHED) {
say("Server crash reported, initiating post-crash analysis...");
@report_results = $reporter_manager->report(REPORTER_TYPE_CRASH | REPORTER_TYPE_ALWAYS);
} elsif ($total_status == STATUS_SERVER_DEADLOCKED) {
say("Server deadlock reported, initiating analysis...");
@report_results = $reporter_manager->report(REPORTER_TYPE_DEADLOCK | REPORTER_TYPE_ALWAYS);
} elsif ($total_status == STATUS_SERVER_KILLED) {
@report_results = $reporter_manager->report(REPORTER_TYPE_SERVER_KILLED | REPORTER_TYPE_ALWAYS);
} else {
@report_results = $reporter_manager->report(REPORTER_TYPE_ALWAYS);
}
my $report_status = shift @report_results;
$total_status = $report_status if $report_status > $total_status;
$total_status = STATUS_OK if $total_status == STATUS_SERVER_KILLED;
foreach my $incident (@report_results) {
$test->addIncident($incident);
}
$test->end($total_status == STATUS_OK ? "pass" : "fail");
if (defined $config->property('xml-output')) {
open (XML , ">$config->property('xml-output')") or say("Unable to open $config->property('xml-output'): $!");
print XML $report->xml();
close XML;
}
$kill_random_sem->remove;
if ($total_status == STATUS_OK) {
say("Test completed successfully.");
safe_exit(0);
} else {
say("Test completed with failure status $total_status.");
safe_exit($total_status);
}
} elsif ($process_type == PROCESS_TYPE_PERIODIC) {
## Periodic does not use channel
$channel->close();
while (1) {
my $reporter_status = $reporter_manager->monitor(REPORTER_TYPE_PERIODIC);
exit($reporter_status) if $reporter_status > STATUS_CRITICAL_FAILURE;
sleep(10);
# $controller->monitor(REPORTER_TYPE_PERIODIC);
# $controller->report(REPORTER_TYPE_SERVER_KILLED);
}
$kill_random_sem->remove;
} elsif ($process_type == PROCESS_TYPE_STAGE) {
while (1) {
sleep(10);
say('stage roller start to run');
my $thread_index = $config->threads - 1;
while ($thread_index >= 0) {
my $res = $kill_random_sem->op($thread_index, -1, IPC_NOWAIT);
last if $ctrl_c;
next if not $res;
$thread_index -= 1;
}
if ($ctrl_c == 1) {
$kill_random_sem->remove();
last;
}
my $roller_res = $stage_roller->run();
if ($roller_res > STATUS_OK) {
say('exit stage roller2 '.$roller_res);
exit($roller_res);
}
$thread_index = $config->threads - 1;
while ($thread_index >= 0) {
$kill_random_sem->op($thread_index, 1, SEM_UNDO);
$thread_index -= 1;
}
if ($ctrl_c == 1) {
$kill_random_sem->remove();
last;
}
}
$kill_random_sem->remove;
} elsif ($process_type == PROCESS_TYPE_KILL) {
while (1) {
sleep(10);
$kill_random->run();
if ($ctrl_c == 1) {
last;
}
}
$kill_random_sem->remove;
} elsif ($process_type == PROCESS_TYPE_CHILD) {
# We are a child process, execute the desired queries and terminate
my $generator = GenTest::Generator::FromGrammar->new(
grammar => $grammar,
varchar_length => $config->property('varchar-length'),
seed => $seed + $id,
thread_id => $id,
mask => $config->mask,
mask_level => $config->property('mask-level')
);
exit (STATUS_ENVIRONMENT_FAILURE) if not defined $generator;
my $mixer = GenTest::Mixer->new(
generator => $generator,
executors => \@executors,
validators => $config->validators,
filters => defined $filter_obj ? [ $filter_obj ] : undef,
group_checkers => {}
);
exit (STATUS_ENVIRONMENT_FAILURE) if not defined $mixer;
my $max_result = 0;
foreach my $i (1..$queries) {
foreach my $executor (@executors) {
$executor->execute("ROLLBACK;");
}
while (1) {
my $res = $kill_random_sem->op($id-1, -1, IPC_NOWAIT);
last if $ctrl_c == 1;
last if $res;
}
last if $ctrl_c == 1;
my $result = $mixer->next();
$kill_random_sem->op($id-1, 1, 0);
if ($result > $failure_level) {
say("mixer status: ".$result." and ".$failure_level);
}
exit($result) if $result > $failure_level;
$max_result = $result if $result > $max_result && $result > STATUS_TEST_FAILURE;
last if $result == STATUS_EOF;
last if $ctrl_c == 1;
last if time() > $test_end;
}
if ($max_result > 0) {
say("Child process completed with error code $max_result.");
exit($max_result);
} else {
say("Child process completed successfully.");
exit(0);
}
$kill_random_sem->remove;
} else {
croak ("Unknown process type $process_type");
}
sub help {
print <<EOF
$0 - Testing via random query generation. Options:
--dsn : DBI resources to connect to (default $DEFAULT_DSN).
Supported databases are MySQL, Drizzle, PostgreSQL, JavaDB
first --dsn must be to MySQL or Drizzle
--gendata : Execute gendata-old.pl in order to populate tables with simple data (default NO)
--gendata=s : Execute gendata.pl in order to populate tables with data
using the argument as specification file to gendata.pl
--engine : Table engine to use when creating tables with gendata (default: no ENGINE for CREATE TABLE)
--threads : Number of threads to spawn (default $DEFAULT_THREADS)
--queries : Numer of queries to execute per thread (default $DEFAULT_QUERIES);
--duration : Duration of the test in seconds (default $DEFAULT_DURATION seconds);
--grammar : Grammar file to use for generating the queries (REQUIRED);
--redefine : Grammar file to redefine and/or add rules to the given grammar
--seed : PRNG seed (default 1). If --seed=time, the current time will be used.
--rpl_mode : Replication mode
--validator : Validator classes to be used. Defaults
ErrorMessageCorruption if one or two MySQL dsns
ResultsetComparator3 if 3 dsns
ResultsetComparartor if 2 dsns
--reporter : ErrorLog, Backtrace if one or two MySQL dsns
--mask : A seed to a random mask used to mask (reduce) the grammar.
--mask-level: How many levels deep the mask is applied (default 1)
--rows : Number of rows to generate for each table in gendata.pl, unless specified in the ZZ file
--varchar-length: maximum length of strings (deault 1) in gendata.pl
--views : Pass --views to gendata-old.pl or gendata.pl
--filter : ......
--start-dirty: Do not generate data (use existing database(s))
--xml-output: ......
--valgrind : ......
--filter : ......
--help : This help message
--debug : Provide debug output
EOF
;
safe_exit(1);
}
sub backwardCompatability {
my ($options) = @_;
if (defined $options->{dsn}) {
croak ("Do not combine --dsn and --dsnX")
if defined $options->{dsn1} or
defined $options->{dsn2} or
defined $options->{dsn3};
} else {
my @dsns;
foreach my $i (1..3) {
if (defined $options->{'dsn'.$i}) {
push @dsns, $options->{'dsn'.$i};
delete $options->{'dsn'.$i};
}
}
$options->{dsn} = \@dsns;
}
if (grep (/,/,@{$options->{reporters}})) {
my $newreporters = [];
map {push(@$newreporters,split(/,/,$_))} @{$options->{reporters}};
$options->{reporters}=$newreporters ;
}
if (grep (/,/,@{$options->{validators}})) {
my $newvalidators = [];
map {push(@$newvalidators,split(/,/,$_))} @{$options->{validators}};
$options->{validators}=$newvalidators ;
}
}