diff --git a/client/mysqltest.cc b/client/mysqltest.cc
index 6677e23152d78..a6dbd7fe7529d 100644
--- a/client/mysqltest.cc
+++ b/client/mysqltest.cc
@@ -197,6 +197,9 @@ static uint my_end_arg= 0;
/* Number of lines of the result to include in failure report */
static uint opt_tail_lines= 0;
+/* Stop the test before the command at this line of the test file (0 = off) */
+static uint opt_exit_line= 0;
+
static uint opt_connect_timeout= 0;
static uint opt_wait_for_pos_timeout= 0;
static const uint default_wait_for_pos_timeout= 300;
@@ -3775,6 +3778,58 @@ static int execute_in_background(char *cmd)
mysqltest command(s) like "remove_file" for that
*/
+/*
+ Find where a wrapper command (e.g. "rr record", "gdb --args") must be
+ injected into an --exec command line. Leading shell environment-variable
+ assignments (NAME=VALUE ...) must stay in front of the wrapper so that the
+ wrapped tool - not the assignment word - is what gets traced/debugged.
+ Returns the byte offset of the first non-assignment token.
+*/
+
+static size_t exec_wrap_offset(const char *cmd)
+{
+ const char *p= cmd;
+ for (;;)
+ {
+ const char *tok;
+ while (*p == ' ' || *p == '\t')
+ p++;
+ tok= p;
+ /* A leading token counts as an assignment only if it is NAME= */
+ if (!(my_isalpha(charset_info, (uchar) *p) || *p == '_'))
+ return (size_t) (tok - cmd);
+ while (my_isalnum(charset_info, (uchar) *p) || *p == '_')
+ p++;
+ if (*p != '=')
+ return (size_t) (tok - cmd);
+ /* Skip the VALUE up to unquoted whitespace, honouring quotes/backslash */
+ for (p++; *p && *p != ' ' && *p != '\t'; )
+ {
+ if (*p == '\'')
+ {
+ for (p++; *p && *p != '\''; p++)
+ ;
+ if (*p)
+ p++;
+ }
+ else if (*p == '"')
+ {
+ for (p++; *p && *p != '"'; p++)
+ {
+ if (*p == '\\' && p[1])
+ p++;
+ }
+ if (*p)
+ p++;
+ }
+ else if (*p == '\\' && p[1])
+ p+= 2;
+ else
+ p++;
+ }
+ }
+}
+
void do_exec(struct st_command *command)
{
int error;
@@ -3824,6 +3879,28 @@ void do_exec(struct st_command *command)
dynstr_append_mem(&ds_cmd, STRING_WITH_LEN(" 2>&1"));
}
+ /*
+ --exec-rr / --exec-gdb: mariadb-test-run.pl sets MYSQLTEST_EXEC_WRAP to a
+ wrapper command (e.g. "rr record") that every --exec is run under. It is
+ injected after any leading NAME=VALUE assignments so the tool is wrapped,
+ not the assignment.
+ */
+ {
+ const char *wrap= getenv("MYSQLTEST_EXEC_WRAP");
+ if (wrap && *wrap)
+ {
+ size_t off= exec_wrap_offset(ds_cmd.str);
+ DYNAMIC_STRING ds_wrap;
+ init_dynamic_string(&ds_wrap, "", ds_cmd.length + 64, 256);
+ dynstr_append_mem(&ds_wrap, ds_cmd.str, off);
+ dynstr_append_mem(&ds_wrap, wrap, strlen(wrap));
+ dynstr_append_mem(&ds_wrap, STRING_WITH_LEN(" "));
+ dynstr_append_mem(&ds_wrap, ds_cmd.str + off, ds_cmd.length - off);
+ dynstr_set(&ds_cmd, ds_wrap.str);
+ dynstr_free(&ds_wrap);
+ }
+ }
+
DBUG_PRINT("info", ("Executing '%s' as '%s'",
command->first_argument, ds_cmd.str));
@@ -10060,6 +10137,11 @@ static struct my_option my_long_options[] =
{"debug-info", 0, "Print some debug info at exit.",
&debug_info_flag, &debug_info_flag,
0, GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0},
+ {"exit-line", 'l',
+ "Stop the test before executing the command at this line of the test "
+ "file, as if an --exit directive were placed there.",
+ &opt_exit_line, &opt_exit_line, 0,
+ GET_UINT, REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
{"host", 'h', "Connect to host.", &opt_host, &opt_host, 0,
GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
{"prologue", 0, "Include SQL before each test case.", &opt_prologue,
@@ -13099,6 +13181,19 @@ int main(int argc, char **argv)
{
my_bool ok_to_do;
int current_line_inc = 1, processed = 0;
+
+ /*
+ --exit-line: stop before the command at this line of the top-level test
+ file, exactly as if an --exit directive were placed there. Gated to the
+ main file so that line numbers of sourced files do not trigger it.
+ */
+ if (opt_exit_line && cur_file == file_stack &&
+ start_lineno >= opt_exit_line)
+ {
+ abort_flag= 1;
+ break;
+ }
+
if (command->type == Q_UNKNOWN || command->type == Q_COMMENT_WITH_COMMAND)
get_command_type(command);
diff --git a/mysql-test/lib/My/Config.pm b/mysql-test/lib/My/Config.pm
index c88b1170a80df..d8d99944d89f1 100644
--- a/mysql-test/lib/My/Config.pm
+++ b/mysql-test/lib/My/Config.pm
@@ -263,7 +263,7 @@ sub new {
My::Config::Group::OPT->new('OPT'),
] }, $class;
my $F= IO::File->new($path, "<")
- or croak "Could not open '$path': $!";
+ or die "Can't open config file '$path': $!\n";
while ( my $line= <$F> ) {
chomp($line);
@@ -282,21 +282,41 @@ sub new {
# Magic #! comments
elsif ( $line =~ /^(#\!\S+)(?:\s*(.*?)\s*)?$/) {
my ($magic, $arg)= ($1, $2);
- croak "Found magic comment '$magic' outside of group"
+ die "$path:$.: magic comment '$magic' outside of any group\n"
unless $group_name;
#print "$magic\n";
$self->insert($group_name, $magic, $arg);
}
- # Empty lines
- elsif ( $line =~ /^$/ ) {
+ # Empty lines (including whitespace-only)
+ elsif ( $line =~ /^\s*$/ ) {
# Skip empty lines
next;
}
+ # !includedir
- include all .cnf files in the directory
+ elsif ( $line =~ /^\!includedir\s+(.*?)\s*$/ ) {
+ # Resolve relative to the including file first, like !include; fall back
+ # to the raw path only if that directory does not exist.
+ my $dir= dirname($path)."/".$1;
+ $dir= $1 unless -d $dir;
+
+ # A missing includedir is silently ignored, as libmariadb does
+ if ( opendir(my $dh, $dir) ) {
+ foreach my $name ( sort grep { /\.cnf$/ } readdir($dh) ) {
+ my $file= "$dir/$name";
+ # Skip subdirectories or symlinks named *.cnf, as libmariadb does,
+ # so a stray non-file entry does not turn into a hard open failure.
+ next unless -f $file;
+ $self->append(My::Config->new($file));
+ }
+ closedir($dh);
+ }
+ }
+
# !include
- elsif ( $line =~ /^\!include\s*(.*?)\s*$/ ) {
+ elsif ( $line =~ /^\!include\s+(.*?)\s*$/ ) {
my $include_file_name= dirname($path)."/".$1;
# Check that the file exists relative to path of first config file
@@ -304,17 +324,23 @@ sub new {
# Try to include file relativ to current dir
$include_file_name= $1;
}
- croak "The include file '$include_file_name' does not exist"
+ die "$path:$.: include file '$include_file_name' does not exist\n"
unless -f $include_file_name;
$self->append(My::Config->new($include_file_name));
}
- #
+ # Any other !directive is ignored
+ elsif ( $line =~ /^\!/ ) {
+ next;
+ }
+
+ # (a leading '#' makes it a pseudo-option, e.g. #galera_port,
+ # used in .cnf templates as an addressable name - not a comment)
elsif ( $line =~ /^(#?[\w-]+)\s*$/ ) {
my $option= $1;
- croak "Found option '$option' outside of group"
+ die "$path:$.: option '$option' outside of any group\n"
unless $group_name;
#print "$option\n";
@@ -326,7 +352,7 @@ sub new {
my $option= $1;
my $value= $2;
- croak "Found option '$option=$value' outside of group"
+ die "$path:$.: option '$option=$value' outside of any group\n"
unless $group_name;
#print "$option=$value\n";
@@ -346,7 +372,7 @@ sub new {
$self->insert($group_name, $option, $value);
}
else {
- croak "Unexpected line '$line' found in '$path'";
+ die "$path:$.: unexpected line '$line'\n";
}
}
undef $F; # Close the file
diff --git a/mysql-test/lib/My/ConfigFactory.pm b/mysql-test/lib/My/ConfigFactory.pm
index 5a5b691998ca5..871e3453c834c 100644
--- a/mysql-test/lib/My/ConfigFactory.pm
+++ b/mysql-test/lib/My/ConfigFactory.pm
@@ -27,11 +27,214 @@ use My::Platform;
use File::Basename;
+=head1 NAME
+
+My::ConfigFactory - build a runtime server configuration for a test run
+
+=head1 SYNOPSIS
+
+ use My::ConfigFactory;
+
+ my $config = My::ConfigFactory->new_config({
+ basedir => $basedir,
+ vardir => $opt_vardir,
+ baseport => $baseport,
+ template_path => "include/default_my.cnf",
+ # ... other args ...
+ });
+
+ # $config is a My::Config object, later written out as var/my.cnf
+
+=head1 DESCRIPTION
+
+C turns a single my.cnf-format B into the concrete
+configuration used to launch the servers and clients of one test run. It reads
+the template with L, then layers in generated values (ports,
+directories, socket paths, server ids, ...) that depend on the current run.
+
+The result is a L object that C writes to
+C.
+
+=head2 Which template is read
+
+C receives exactly one template in C<< $args->{template_path} >>.
+The choice of I template is made upstream in C by
+precedence (highest first); only the first that exists is used, and the
+candidates are never merged with each other:
+
+=over
+
+=item 1. C<< .cnf >> in the test's C directory - per-test template
+
+=item 2. the suite's C - shared by all tests in that suite
+
+=item 3. a built-in default when neither exists:
+
+=over
+
+=item * C for replication tests
+
+=item * C otherwise
+
+=back
+
+=back
+
+The command line can also force a template with C<--defaults-file> (replacing
+the selection above) or add one with C<--defaults-extra-file>.
+
+Note: C<< $args->{extra_template_path} >> is passed in by C
+but is currently B read by this module.
+
+=head2 How the template is parsed and merged
+
+L parses the template as my.cnf/INI format: C<[group]> sections
+with C lines. It resolves C and C
+directives recursively, appending each included file's groups and options. The
+shared building blocks under C are pulled in this way, e.g.:
+
+ !include default_group_order.cnf # fixes server startup order
+ !include default_mysqld.cnf # baseline [mysqld] options
+ !include default_client.cnf # baseline client options
+
+(C is itself just a chain of such includes.) A per-test
+or per-suite C<.cnf> typically starts with one of these Cs so it
+I the defaults and then overrides only what it needs.
+
+Merging is last-value-wins: an option redefined later - by a subsequent
+include, or in the template after an C - overrides the earlier value.
+
+=head2 Generated settings (rules)
+
+On top of the parsed template, C runs rule sets that
+insert-or-override options with values computed for this run:
+
+=over
+
+=item * C<@pre_rules> - general auto-options
+
+=item * C<@mysqld_rules> - per C<[mysqld.N]>: port, datadir, socket, server-id, etc.
+
+=item * group rules for C<[mysqlbinlog]>, C<[mysql_upgrade]>, C<[client]>, C<[mysqltest]>, ... - client-side connection settings
+
+=item * C<@post_rules> - final adjustments
+
+=back
+
+Because rules use C<< $config->insert() >>, they override matching options that
+came from the template.
+
+=head2 Related files handled elsewhere
+
+These affect a test's servers but are B part of the template and are not
+merged into the configuration:
+
+=over
+
+=item * C<.opt>, C<-master.opt>, C<-slave.opt> - flat command-line argument
+lists (not my.cnf format); parsed by C and applied as mysqld
+startup arguments.
+
+=item * C<.combinations> - my.cnf format and also parsed by L, but
+in C (not here); each C<[group]> becomes one test combination
+whose options are added as mysqld startup arguments.
+
+=back
+
+=head1 USAGE
+
+ # Constructing a configuration
+
+ # Build a My::Config from a template plus this run's parameters.
+ # Real example from mariadb-test-run.pl (default_mysqld).
+ my $config= My::ConfigFactory->new_config({
+ basedir => $basedir,
+ testdir => $glob_mysql_test_dir,
+ template_path => "include/default_my.cnf",
+ vardir => $opt_vardir,
+ tmpdir => $opt_tmpdir,
+ baseport => 0,
+ user => $opt_user,
+ password => '',
+ });
+
+ # Fetching a group
+
+ # group($name) returns one My::Config::Group, or undef if absent
+ my $mysqld= $config->group('mysqld.1')
+ or mtr_error("Couldn't find mysqld.1 in default config");
+
+ # Iterating groups
+
+ # groups() lists every group
+ for my $group ($config->groups()) {
+ my $name = $group->name(); # e.g. "mysqld.1"
+ my @opts = $group->options(); # this group's options
+ if ($group == $mysqld) { # same object group('mysqld.1') returned above
+ print "This is mysqld.1 group!\n";
+ }
+ }
+
+ # like($prefix) filters by name prefix - e.g. all server groups
+ my @servers= $config->like('mysqld\.');
+
+ # Generating values with fix_*
+
+ # The fix_* generators are internal rule callbacks; most are called as
+ # fix_x($self, $config, $group_name, $group) and need the factory's private
+ # $self, so only these two - which ignore their arguments - are standalone:
+ my $host= My::ConfigFactory::fix_host(); # always "localhost"
+ my $bind= My::ConfigFactory::fix_bind_address(); # "127.0.0.1", or "*" on Windows
+
+ # Reading an option
+
+ # value($option) returns the value, or croaks if the option is missing
+ my $log_error= $mysqld->value('log-error');
+
+ # if_exist($option) returns undef instead of croaking when absent
+ my $bind= $mysqld->if_exist('bind-address');
+
+ # Writing the config to a file (var/my.cnf)
+
+ # There is no built-in writer; mariadb-test-run.pl serializes the config
+ # with mycnf_create(). option_groups() yields the "real" groups, skipping
+ # the auto-generated ENV and OPT groups.
+ open my $F, '>', "$opt_vardir/my.cnf" or die "$!";
+ for my $group ($config->option_groups()) {
+ print $F "[", $group->name(), "]\n";
+ for my $option ($group->options()) {
+ my $value= $option->value();
+ print $F $option->name(), (defined $value ? "=$value" : ""), "\n";
+ }
+ print $F "\n";
+ }
+ close $F;
+
+=head1 SUBROUTINES
+
+The public entry point is C. Everything else is internal and
+documented here for maintainers: the rule engine, the pre-rules, the C
+value generators and the C checks.
+
+=head2 Pre-rules (run first)
+
+=over
+
+=cut
#
# Rules to run first of all
#
+=item add_opt_values(\%self, \%config)
+
+Seed the auto-generated C<[OPT]> group (e.g. C) and add
+C for optional plugins.
+
+Called from: C, via C<@pre_rules>.
+
+=cut
+
sub add_opt_values {
my ($self, $config)= @_;
@@ -49,6 +252,25 @@ my @pre_rules=
my @share_locations= ("share/mariadb", "share/mysql", "sql/share", "share");
+=back
+
+=head2 Value generators (C and helpers)
+
+Each C is a rule callback invoked as
+C<< ($self, $config, $group_name, $group) >>, returning the value to insert for
+one option.
+
+=over
+
+=item get_basedir(\%self, \%group)
+
+Resolve the base directory, preferring an explicit value in the group, then
+C<%ARGS>.
+
+Called from: C and C.
+
+=cut
+
sub get_basedir {
my ($self, $group)= @_;
my $basedir= $group->if_exist('basedir') ||
@@ -56,6 +278,15 @@ sub get_basedir {
return $basedir;
}
+=item get_testdir(\%self, \%group)
+
+Resolve the test directory, preferring an explicit value in the group, then
+C<%ARGS>.
+
+Called from: nowhere - currently unused.
+
+=cut
+
sub get_testdir {
my ($self, $group)= @_;
my $testdir= $group->if_exist('testdir') ||
@@ -63,6 +294,14 @@ sub get_testdir {
return $testdir;
}
+=item get_bindir(\%self, \%group)
+
+Resolve the build directory: C<$ENV{MTR_BINDIR}> if set, else the basedir.
+
+Called from: C.
+
+=cut
+
# Retrive build directory (which is different from basedir in out-of-source build)
sub get_bindir {
if (defined $ENV{MTR_BINDIR})
@@ -73,40 +312,96 @@ sub get_bindir {
return $self->get_basedir($group);
}
+=item fix_charset_dir(\%self, \%config, $group_name, \%group)
+
+Locate the C directory under the share locations.
+
+Called from: C, as the C rule in C<@mysqld_rules> and C<@client_rules>.
+
+=cut
+
sub fix_charset_dir {
my ($self, $config, $group_name, $group)= @_;
return my_find_dir($self->get_basedir($group),
\@share_locations, "charsets");
}
+=item fix_language(\%self, \%config, $group_name, \%group)
+
+Locate the language/messages directory under the share locations.
+
+Called from: C, as the C rule in C<@mysqld_rules>.
+
+=cut
+
sub fix_language {
my ($self, $config, $group_name, $group)= @_;
return my_find_dir($self->get_bindir($group),
\@share_locations);
}
+=item fix_datadir(\%self, \%config, $group_name)
+
+Per-group data directory under C.
+
+Called from: C, as the C rule in C<@mysqld_rules>.
+
+=cut
+
sub fix_datadir {
my ($self, $config, $group_name)= @_;
my $vardir= $self->{ARGS}->{vardir};
return "$vardir/$group_name/data";
}
+=item fix_pidfile(\%self, \%config, $group_name, \%group)
+
+Per-group pid file under C.
+
+Called from: C, as the C rule in C<@mysqld_rules>.
+
+=cut
+
sub fix_pidfile {
my ($self, $config, $group_name, $group)= @_;
my $vardir= $self->{ARGS}->{vardir};
return "$vardir/run/$group_name.pid";
}
+=item fix_port(\%self, \%config, $group_name, \%group)
+
+Hand out the next sequential port from C<< $self->{PORT} >>.
+
+Called from: C, as the C rule in C<@mysqld_rules>; also from C for the C group.
+
+=cut
+
sub fix_port {
my ($self, $config, $group_name, $group)= @_;
return $self->{PORT}++;
}
+=item fix_host(\%self)
+
+Return C.
+
+Called from: C, as the C<#host> rule in C<@mysqld_rules>.
+
+=cut
+
sub fix_host {
my ($self)= @_;
'localhost'
}
+=item is_unique(\%config, $name, $value)
+
+Verify a value is not already used by another group.
+
+Called from: C.
+
+=cut
+
sub is_unique {
my ($config, $name, $value)= @_;
@@ -120,9 +415,17 @@ sub is_unique {
return 1;
}
+=item fix_server_id(\%self, \%config, $group_name, \%group)
+
+Assign a unique C; croak on a duplicate explicit id.
+
+Called from: C, as the C rule in C<@mysqld_rules>.
+
+=cut
+
sub fix_server_id {
my ($self, $config, $group_name, $group)= @_;
-#define in the order that mysqlds are listed in my.cnf
+#define in the order that mysqlds are listed in my.cnf
my $server_id= $group->if_exist('server-id');
if (defined $server_id){
@@ -140,6 +443,14 @@ sub fix_server_id {
return $server_id;
}
+=item fix_socket(\%self, \%config, $group_name, \%group)
+
+Per-group unix socket under C.
+
+Called from: C, as the C rule in C<@mysqld_rules>.
+
+=cut
+
sub fix_socket {
my ($self, $config, $group_name, $group)= @_;
# Put socket file in tmpdir
@@ -147,12 +458,28 @@ sub fix_socket {
return "$dir/$group_name.sock";
}
+=item fix_tmpdir(\%self, \%config, $group_name, \%group)
+
+Per-group tmp directory under C.
+
+Called from: C, as the C rule in C<@mysqld_rules>.
+
+=cut
+
sub fix_tmpdir {
my ($self, $config, $group_name, $group)= @_;
my $dir= $self->{ARGS}->{tmpdir};
return "$dir/$group_name";
}
+=item fix_log_error(\%self, \%config, $group_name, \%group)
+
+Error log path (or C<.trace> under valgrind+debug).
+
+Called from: C, as the C rule in C<@mysqld_rules>.
+
+=cut
+
sub fix_log_error {
my ($self, $config, $group_name, $group)= @_;
my $dir= $self->{ARGS}->{vardir};
@@ -163,12 +490,28 @@ sub fix_log_error {
}
}
+=item fix_log(\%self, \%config, $group_name, \%group)
+
+General query log path.
+
+Called from: C, as the C rule in C<@mysqld_rules>.
+
+=cut
+
sub fix_log {
my ($self, $config, $group_name, $group)= @_;
my $dir= dirname($group->value('datadir'));
return "$dir/mysqld.log";
}
+=item fix_bind_address()
+
+Return the bind address (C<*> on Windows, C<127.0.0.1> elsewhere).
+
+Called from: C, as the C rule in C<@mysqld_rules>.
+
+=cut
+
sub fix_bind_address {
if (IS_WINDOWS) {
return "*";
@@ -176,6 +519,15 @@ sub fix_bind_address {
return "127.0.0.1";
}
}
+
+=item fix_log_slow_queries(\%self, \%config, $group_name, \%group)
+
+Slow query log path.
+
+Called from: C, as the C rule in C<@mysqld_rules>.
+
+=cut
+
sub fix_log_slow_queries {
my ($self, $config, $group_name, $group)= @_;
my $dir= dirname($group->value('datadir'));
@@ -248,6 +600,21 @@ my @mysql_upgrade_rules=
);
+=back
+
+=head2 Post-rules (run last)
+
+=over
+
+=item post_check_client_group(\%self, \%config, $client_group_name, $mysqld_group_name)
+
+Generate one C<[client...]> group, copying port / socket / host / user /
+password from its mysqld.
+
+Called from: C.
+
+=cut
+
#
# Generate a [client.] group to be
# used for connecting to [mysqld.]
@@ -278,6 +645,15 @@ sub post_check_client_group {
}
+=item post_check_client_groups(\%self, \%config)
+
+Generate a C<[client]> group pointing at the first C<[mysqld.N]>, plus a
+matching C<[client.N]> per mysqld.
+
+Called from: C, via C<@post_rules>.
+
+=cut
+
sub post_check_client_groups {
my ($self, $config)= @_;
@@ -301,6 +677,16 @@ sub post_check_client_groups {
}
+=item post_check_embedded_group(\%self, \%config)
+
+When running embedded, build an C<[embedded]> group from the default
+C<[mysqld]> and the first C<[mysqld.N]>, skipping options that don't apply to
+the embedded server.
+
+Called from: C, via C<@post_rules>.
+
+=cut
+
#
# Generate [embedded] by copying the values
# needed from the default [mysqld] section
@@ -333,6 +719,15 @@ sub post_check_embedded_group {
}
+=item resolve_at_variable(\%self, \%config, \%group, \%option)
+
+Expand C<@group.option> references in one option's value, substituting the
+referenced option's value.
+
+Called from: C.
+
+=cut
+
sub resolve_at_variable {
my ($self, $config, $group, $option)= @_;
local $_ = $option->value();
@@ -363,6 +758,14 @@ sub resolve_at_variable {
}
+=item post_fix_resolve_at_variables(\%self, \%config)
+
+Expand C<@group.option> references across all option values.
+
+Called from: C, via C<@post_rules>.
+
+=cut
+
sub post_fix_resolve_at_variables {
my ($self, $config)= @_;
@@ -387,6 +790,24 @@ my @post_rules=
);
+=back
+
+=head2 Rule engine
+
+=over
+
+=item run_rules_for_group(\%self, \%config, \%group, @rules)
+
+Apply @rules to a single group. Each rule is a hashref
+C<< { option => value_or_coderef } >> and fires only if the option is not
+already set; a coderef is called as
+C<< $rule->($self, $config, $group_name, $group) >> and its defined return value
+is inserted. This is what lets template values win over generated defaults.
+
+Called from: C and C.
+
+=cut
+
sub run_rules_for_group {
my ($self, $config, $group, @rules)= @_;
foreach my $hash ( @rules ) {
@@ -410,6 +831,15 @@ sub run_rules_for_group {
}
+=item run_section_rules(\%self, \%config, $name, @rules)
+
+Apply @rules to every group whose name matches C^$name/> (e.g. all
+C sections).
+
+Called from: C.
+
+=cut
+
sub run_section_rules {
my ($self, $config, $name, @rules)= @_;
@@ -419,6 +849,21 @@ sub run_section_rules {
}
+=back
+
+=head2 Public
+
+=over
+
+=item new_config($class, \%args)
+
+Class method described under L. Required args: C,
+C, C, C. Returns a resolved L.
+
+Called from: C (e.g. C) and C.
+
+=cut
+
sub new_config {
my ($class, $args)= @_;
@@ -479,5 +924,12 @@ sub new_config {
}
-1;
+=back
+
+=head1 SEE ALSO
+
+L - the my.cnf-format parser used here.
+=cut
+
+1;
diff --git a/mysql-test/lib/My/CoreDump.pm b/mysql-test/lib/My/CoreDump.pm
index be6d21146d147..707b1a279b534 100644
--- a/mysql-test/lib/My/CoreDump.pm
+++ b/mysql-test/lib/My/CoreDump.pm
@@ -252,12 +252,16 @@ sub _gdb {
return if $? >> 8;
return unless $gdb_output;
- resfile_print < 1,
options => '-x {script} {exe}',
script => 'set args {args} < {input}',
+ exec => 'gdb --args',
},
ddd => {
interactive => 1,
@@ -80,6 +84,7 @@ my %debuggers = (
rr => {
options => '_RR_TRACE_DIR={log} rr record {exe} {args} --loose-skip-innodb-use-native-aio --loose-innodb-flush-method=fsync',
run => 'env',
+ exec => 'rr record',
pre => sub {
push @::global_suppressions, qr/InnoDB: native AIO failed/;
::mtr_error('rr requires kernel.perf_event_paranoid <= 1')
@@ -114,6 +119,7 @@ my %opt_vals;
my $debugger;
my $boot_debugger;
my $client_debugger;
+my $opt_terminal;
my $help = "\n\nOptions for running debuggers\n\n";
@@ -134,6 +140,53 @@ for my $k (sort keys %debuggers) {
register_opt "client-", $k, "Start mysqltest client";
register_opt "boot-", $k, "Start bootstrap server";
register_opt "manual-", "$k", "Before running test(s) let user manually start mariadbd";
+ register_opt "exec-", $k, "Run every mysqltest --exec command"
+ if $v->{exec};
+}
+
+# Terminal emulator used to run interactive ({term}) debuggers. The template
+# understands two placeholders: {title} (window title) and {command} (the
+# debugger invocation). --terminal takes precedence over $MTR_TERM; the
+# default reproduces the former hard-coded xterm behaviour.
+$opts{"terminal=s"} = \$opt_terminal;
+$help .= wrap(sprintf(" %-23s", "terminal=TEMPL"), ' 'x25,
+ "Terminal for interactive debuggers, with {title} and {command} ".
+ "placeholders (default 'xterm -title {title} -e {command}', ".
+ "also from \$MTR_TERM)\n");
+
+sub term_template()
+{
+ my $t= $opt_terminal;
+ undef $t if defined $t and $t eq ';'; # bare --terminal sentinel, ignore
+ return $t || $ENV{MTR_TERM} || 'xterm -title {title} -e {command}';
+}
+
+# Expand the terminal template into an argv list that runs @cmd (the debugger
+# and its arguments) in a window titled $title. {command} expands to the
+# words of @cmd; {title} is substituted textually within any word.
+sub term_argv($@)
+{
+ my $title= shift;
+ my @cmd= @_;
+ my $templ= term_template();
+ # Honour shell-style quoting: the argv is exec'd directly (no shell), so a
+ # quoted argument in the template must become a single argv element.
+ my @words= shellwords($templ);
+ mtr_error "Malformed --terminal template: $templ" unless @words;
+ my @argv;
+ for my $word (@words)
+ {
+ if ($word eq '{command}')
+ {
+ push @argv, @cmd;
+ }
+ else
+ {
+ $word =~ s/\{title\}/$title/g;
+ push @argv, $word;
+ }
+ }
+ return @argv;
}
sub subst($%) {
@@ -183,8 +236,9 @@ sub do_args($$$$$) {
print "$run $options\n";
$$exe= undef; # Indicate the exe should not be started
} elsif ($v->{term}) {
- unshift @$$args, '-title', $type, '-e', $run;
- $$exe = 'xterm';
+ my @argv= term_argv($type, $run, @$$args);
+ $$exe = shift @argv;
+ @$$args = @argv;
} else {
$$exe = $run;
}
@@ -255,6 +309,47 @@ sub pre_setup() {
$boot_debugger= (keys %boot_options)[0];
$client_debugger= (keys %client_options)[0];
+ # --exec-: wrap every mysqltest '--exec' command line by exporting
+ # MYSQLTEST_EXEC_WRAP, which do_exec() injects after any leading NAME=VALUE
+ # assignments. Independent of the mysqld/client/boot debuggers above.
+ my %exec_options;
+ for my $k (keys %debuggers) {
+ my $val= $opt_vals{"exec-$k"};
+ next unless $val;
+ mtr_error "--exec-$k is not supported" unless $debuggers{$k}->{exec};
+ $exec_options{$k}= $val;
+ }
+ if ((keys %exec_options) > 1) {
+ mtr_error "Multiple exec debuggers specified: ",
+ join (" ", map { "--exec-$_" } keys %exec_options);
+ }
+ if (my ($k)= keys %exec_options) {
+ my $v= $debuggers{$k};
+ # Run the debugger's one-time setup hook (a code ref, e.g. rr's
+ # perf_event_paranoid check and suppression registration). delete()
+ # after calling it so it runs only once even when the same debugger is
+ # also selected for another role (e.g. --rr together with --exec-rr) -
+ # the hook has side effects (push @global_suppressions, env vars) that
+ # must not be applied twice.
+ if ($v->{pre}) {
+ $v->{pre}->();
+ delete $v->{pre};
+ }
+ my $wrap= $v->{exec};
+ $ENV{_RR_TRACE_DIR}= "$::opt_vardir/log" if $k eq 'rr';
+ if ($v->{term}) {
+ # Here the wrapper is a shell string that do_exec() prepends before the
+ # tool, so expand the template textually; {command} must be last for the
+ # tool to end up as an argument of the debugger.
+ (my $t= term_template()) =~ s/\{title\}/exec/g;
+ $t =~ s/\{command\}/$wrap/;
+ $wrap= $t;
+ }
+ $ENV{MYSQLTEST_EXEC_WRAP}= $wrap;
+ $used= 1;
+ $interactive ||= $v->{term};
+ }
+
if ($used) {
$ENV{ASAN_OPTIONS}= 'abort_on_error=1:'.($ENV{ASAN_OPTIONS} || '');
::mtr_error("Can't use --extern when using debugger") if $ENV{USE_RUNNING_SERVER};
diff --git a/mysql-test/lib/mtr_cases.pm b/mysql-test/lib/mtr_cases.pm
index 420083d74f8a8..71e3ed9408364 100644
--- a/mysql-test/lib/mtr_cases.pm
+++ b/mysql-test/lib/mtr_cases.pm
@@ -250,6 +250,27 @@ our %file_combinations;
our %skip_combinations;
our %file_in_overlay;
+# Cache of result-directory listings. For each result dir we read it once and
+# index the .result/.rdiff files by their base test name (the leading run of
+# characters before the first '.' or ','), so collect_one_test_case can look up
+# a test's result files in memory instead of glob-scanning the whole directory
+# once per test (which is O(tests * dirsize)).
+my %resdir_index;
+sub resdir_files($) {
+ my ($dir)= @_;
+ return $resdir_index{$dir} if exists $resdir_index{$dir};
+ my %by_base;
+ if (opendir(my $dh, $dir)) {
+ while (defined(my $f= readdir $dh)) {
+ next unless $f =~ /\.(?:rdiff|result)$/;
+ my ($base)= $f =~ /^([^.,]+)/;
+ push @{$by_base{$base}}, $f if defined $base;
+ }
+ closedir $dh;
+ }
+ return $resdir_index{$dir}= \%by_base;
+}
+
sub load_suite_object {
my ($suitename, $suitedir) = @_;
my $suite;
@@ -316,6 +337,21 @@ sub combinations_from_file($$)
}
@combs = ({ skip => 'Requires: ' . basename($filename, '.combinations') }) unless @combs;
}
+
+ # --combination-select=N selects a single combination by position in the
+ # file, in [] section order (1-based; a negative N counts from the end,
+ # -1 being the last). Ignored for skipped or command-line combinations.
+ if (defined $::opt_comb_sel and @combs and not $combs[0]->{skip}) {
+ my $n= $::opt_comb_sel;
+ mtr_error("--combination-select must be a non-zero integer, not '$n'")
+ unless $n =~ /^-?[1-9][0-9]*$/;
+ my $i= $n > 0 ? $n - 1 : $n;
+ mtr_error("--combination-select=$n is out of range for '$filename' ".
+ "(it has ".scalar(@combs)." combination(s))")
+ if $i >= @combs or $i < -@combs;
+ @combs= ($combs[$i]);
+ }
+
@combs;
}
@@ -947,20 +983,25 @@ sub collect_one_test_case {
if ($tinfo->{combinations}) {
$re = '(?:' . join('|', @{$tinfo->{combinations}}) . ')';
}
- my $resdirglob = $suite->{rdir};
- $resdirglob.= ',' . $suite->{parent}->{rdir} if $suite->{parent};
+ my @resdirs = ($suite->{rdir});
+ push @resdirs, $suite->{parent}->{rdir} if $suite->{parent};
my %files;
- for (<{$resdirglob}/$tname*.{rdiff,result}>) {
- my ($path, $combs, $ext) =
- m@^(.*)/$tname((?:,$re)*)\.(rdiff|result)$@ or next;
- my @combs = sort split /,/, $combs;
- $files{$_} = join '~', ( # sort files by
- 99 - scalar(@combs), # number of combinations DESC
- join(',', sort @combs), # combination names ASC
- $path eq $suite->{rdir} ? 1 : 2, # overlay first
- $ext eq 'result' ? 1 : 2 # result before rdiff
- );
+ my ($tbase)= $tname =~ /^([^.,]+)/; # same base the index is keyed on
+ for my $dir (@resdirs) {
+ my $index= resdir_files($dir);
+ for my $f (@{ $index->{$tbase} || [] }) {
+ my $file = "$dir/$f";
+ my ($path, $combs, $ext) =
+ $file =~ m@^(.*)/$tname((?:,$re)*)\.(rdiff|result)$@ or next;
+ my @combs = sort split /,/, $combs;
+ $files{$file} = join '~', ( # sort files by
+ 99 - scalar(@combs), # number of combinations DESC
+ join(',', sort @combs), # combination names ASC
+ $path eq $suite->{rdir} ? 1 : 2, # overlay first
+ $ext eq 'result' ? 1 : 2 # result before rdiff
+ );
+ }
}
my @results = sort { $files{$a} cmp $files{$b} } keys %files;
diff --git a/mysql-test/lib/mtr_report.pm b/mysql-test/lib/mtr_report.pm
index ff239d676a9cb..c2f10f7922f91 100644
--- a/mysql-test/lib/mtr_report.pm
+++ b/mysql-test/lib/mtr_report.pm
@@ -455,7 +455,8 @@ sub mtr_report_stats ($$$$) {
print
"The log files in var/log may give you some hint of what went wrong.\n\n",
"If you want to report this error, MariaDB's bug tracker is found at\n",
- "https://jira.mariadb.org\n\n";
+ "https://jira.mariadb.org\n\n"
+ unless $::opt_strip_hints;
}
else
diff --git a/mysql-test/mariadb-test-run.pl b/mysql-test/mariadb-test-run.pl
index 7461c2aa0d0df..82b2bcda2f28f 100755
--- a/mysql-test/mariadb-test-run.pl
+++ b/mysql-test/mariadb-test-run.pl
@@ -88,6 +88,7 @@ BEGIN
use My::Platform;
use My::SafeProcess;
use My::ConfigFactory;
+use My::Config;
use My::Options;
use My::Tee;
use My::Find;
@@ -218,7 +219,7 @@ END
period-
sysschema-
);
-my $opt_suites;
+my @opt_suites;
our $opt_verbose= 0; # Verbose output, enable with --verbose
our $exe_patch;
@@ -234,14 +235,24 @@ END
our $opt_staging_run= 0;
our @opt_combinations;
+our $opt_comb_sel;
+our $opt_exit_line;
our @opt_extra_mysqld_opt;
our @opt_mysqld_envs;
my $opt_stress;
my $opt_tail_lines= 20;
+my $opt_head_log;
+my $opt_tail_log;
+our $opt_strip_hints= 0;
+my $opt_strip_limits= 0;
+my $opt_strip_backtrace= 0;
+my $opt_head_warnings;
+my $opt_tail_warnings;
my $opt_dry_run;
+my $opt_comb_list;
my $opt_compress;
my $opt_ssl;
@@ -404,18 +415,66 @@ sub main {
check_debug_support();
environment_setup();
- if (!$opt_suites) {
- $opt_suites= join ',', collect_default_suites(@DEFAULT_SUITES);
+ # Resolve the suite list. A "!NAME" entry excludes NAME; if only exclusions
+ # are given, they apply to the default suite set. Names accumulate across the
+ # command line and the [mtr] config file (see @opt_suites).
+ my $suites;
+ {
+ my (@names, %exclude);
+ for my $s (@opt_suites) {
+ if ($s =~ /^!(.+)/) { $exclude{(split /-/, $1)[0]}= 1; }
+ else { push @names, $s; }
+ }
+ @names= collect_default_suites(@DEFAULT_SUITES) unless @names;
+ @names= grep { !$exclude{(split /-/, $_)[0]} } @names;
+ $suites= join ',', @names;
}
- mtr_report("Using suites: $opt_suites") unless @opt_cases;
+ mtr_report("Using suites: $suites") unless @opt_cases;
mtr_report("Collecting tests...");
- my $tests= collect_test_cases($opt_reorder, $opt_suites, \@opt_cases, \@opt_skip_test_list);
+ my $tests= collect_test_cases($opt_reorder, $suites, \@opt_cases, \@opt_skip_test_list);
if (@$tests == 0) {
mtr_report("No tests to run...");
exit 0;
}
+ if ($opt_comb_list)
+ {
+ # For each collected test, list its combinations in the selectable
+ # "test,combination" form (one per line), preceded by a summary line.
+ my (%combs, @order);
+ for my $t (@$tests) {
+ my $name= $t->{name};
+ push @order, $name unless $combs{$name};
+ my $suffix= defined $t->{combinations}
+ ? join(',', sort @{$t->{combinations}}) : '';
+ push @{$combs{$name}}, $suffix if length $suffix;
+ }
+ for my $name (@order) {
+ my @c= @{$combs{$name} || []};
+ # Summary line: list each distinct combination name once, keeping names
+ # from the same .combinations file together. Names from one file never
+ # appear together in a variant, so group names that never co-occur;
+ # order follows first appearance.
+ my @variants= map { [ split /,/ ] } @c;
+ my (%seen, %co, @cnames);
+ for my $v (@variants) {
+ for my $a (@$v) {
+ push @cnames, $a unless $seen{$a}++;
+ $co{$a}{$_}= 1 for @$v;
+ }
+ }
+ my @groups;
+ for my $n (@cnames) {
+ my ($g)= grep { !grep { $co{$n}{$_} } @$_ } @groups;
+ $g ? push(@$g, $n) : push(@groups, [$n]);
+ }
+ print "Combinations for $name: ", join(',', map { @$_ } @groups), "\n";
+ print "$name,$_\n" for @c;
+ }
+ exit 0;
+ }
+
mark_time_used('collect');
if (!using_extern())
@@ -1154,6 +1213,163 @@ sub print_global_resfile {
}
+#
+# Read MTR's own options from the [mtr] group of the standard MariaDB option
+# files and merge them into @ARGV ahead of the command-line options, so the
+# latter take precedence. $args->{conf_file} (e.g. $ENV{MTR_CONFIG}) forces a
+# single file; otherwise the standard locations are searched. A
+# ---end-of-config--- marker separates config options from command-line
+# options so command_line_setup can report where an invalid option came from.
+# Parsing reuses My::Config.
+#
+#
+# get_defaults_options - mirror libmariadb's get_defaults_options(): read the
+# standard defaults options off @ARGV.
+#
+# --no-defaults, --defaults-group-suffix and --print-defaults are MTR-only, so
+# they are consumed from @ARGV (the main GetOptions never sees them).
+#
+# --defaults-file and --defaults-extra-file double as the server config
+# template (collected later by GetOptions), so they are only *peeked* here -
+# left in @ARGV for that second consumer. The two consumers read different
+# groups of the same file: [mysqld]/[client]/... for the template, [mtr] here.
+#
+sub get_defaults_options
+{
+ my %opt;
+
+ # Consume the MTR-only options from @ARGV
+ Getopt::Long::GetOptionsFromArray(
+ \@ARGV,
+ 'no-defaults' => \$opt{no_defaults},
+ 'defaults-group-suffix=s' => \$opt{group_suffix},
+ 'print-defaults' => \$opt{print_defaults},
+ 'mtr-config-only|M' => \$opt{mtr_config_only});
+
+ # Peek the file options without removing them (parse a copy)
+ my @copy= @ARGV;
+ Getopt::Long::GetOptionsFromArray(
+ \@copy,
+ 'defaults-file=s' => \$opt{conf_file},
+ 'defaults-extra-file=s' => \$opt{extra_file});
+
+ # --defaults-group-suffix falls back to the environment, as in libmariadb
+ $opt{group_suffix}= $ENV{MARIADB_GROUP_SUFFIX} unless defined $opt{group_suffix};
+ $opt{group_suffix}= $ENV{MYSQL_GROUP_SUFFIX} unless defined $opt{group_suffix};
+
+ return \%opt;
+}
+
+#
+# load_defaults - read MTR's own [mtr] options from the MariaDB option files
+# and merge them into @ARGV ahead of the command-line options (which follow the
+# ---end-of-config--- marker and therefore take precedence). Parsing reuses
+# My::Config; parse errors are reported through mtr_error as "file:line: reason".
+#
+sub load_defaults
+{
+ my $args= shift;
+ my $groups= $args->{groups} || [];
+ $groups= [ $groups ] unless ref $groups eq 'ARRAY';
+
+ # --defaults-group-suffix: also read [] for each group
+ my @groups= @$groups;
+ if (defined $args->{group_suffix} and length $args->{group_suffix})
+ {
+ push @groups, map { "$_$args->{group_suffix}" } @$groups;
+ }
+
+ # Build the ordered list of option files. The reading and merge semantics
+ # follow libmariadb's my_search_option_files(), but the default locations
+ # below are the Unix set only (no Windows directories, no my.ini); for other
+ # platforms use an explicit MTR_CONFIG / --defaults-file, which work anywhere.
+ my @files;
+ if ($args->{no_defaults})
+ {
+ # --no-defaults: read no option files at all
+ }
+ elsif (defined $args->{conf_file})
+ {
+ # --defaults-file (or $MTR_CONFIG): this file only - not the standard
+ # files, not --defaults-extra-file
+ # error on a missing explicit file, as libmariadb does
+ mtr_error("Config file '$args->{conf_file}' not found")
+ unless -f $args->{conf_file};
+ @files= ($args->{conf_file});
+ }
+ else
+ {
+ # Standard search order (Unix, no compiled-in DEFAULT_SYSCONFDIR), read
+ # cumulatively - every file that exists, in this order:
+ # /etc, /etc/mysql, $MARIADB_HOME|$MYSQL_HOME, --defaults-extra-file, ~/
+ push @files, "/etc/my.cnf", "/etc/mysql/my.cnf";
+ my ($home)= grep { defined $ENV{$_} } qw(MARIADB_HOME MYSQL_HOME);
+ push @files, "$ENV{$home}/my.cnf" if defined $home;
+ push @files, $args->{extra_file} if defined $args->{extra_file};
+ push @files, "$ENV{HOME}/.my.cnf" if defined $ENV{HOME};
+ }
+
+ # Collect the requested groups' options from every existing file, in order.
+ # A later file's option overrides an earlier one (My::Config is last-wins),
+ # matching "if an option is set multiple times, the later setting wins".
+ my @opts;
+ my $mtr_config_only= $args->{mtr_config_only};
+ my $group_re= join('|', map { quotemeta } @groups);
+ for my $file (@files)
+ {
+ next unless defined $file and -f $file;
+ my $config= eval { My::Config->new($file) };
+ if ($@)
+ {
+ # My::Config reports parse errors as "file:line: reason"; re-throw it
+ # through mtr_error for output flushing, the uniform mtr error format
+ # and a controlled exit.
+ chomp(my $err= $@);
+ mtr_error($err);
+ }
+ for my $group ($config->groups())
+ {
+ # Match group names case-insensitively, like libmariadb's find_type()
+ next unless $group->name() =~ /^(?:$group_re)$/i;
+ for my $option ($group->options())
+ {
+ # mtr-config-only (-M) may be set inside [mtr] itself; it is a
+ # directive, not a pass-through option, so consume it here.
+ if ($option->name() =~ /^mtr[-_]config[-_]only$/)
+ {
+ $mtr_config_only= 1;
+ next;
+ }
+ push @opts, $option->option();
+ }
+ }
+ }
+
+ # --mtr-config-only (-M): the --defaults-file / --defaults-extra-file we just
+ # read for [mtr] must NOT also become the server config template, so drop
+ # them from @ARGV before the main GetOptions/collect_option sees them.
+ if ($mtr_config_only)
+ {
+ Getopt::Long::GetOptionsFromArray(
+ \@ARGV,
+ 'defaults-file=s' => sub {},
+ 'defaults-extra-file=s' => sub {});
+ }
+
+ # --print-defaults: show the [mtr] options picked up from config, then exit
+ if ($args->{print_defaults})
+ {
+ print "$_\n" for @opts;
+ exit(0);
+ }
+
+ # Prepend the config options; command-line options (already in @ARGV) follow
+ # the marker, so GetOptions lets them take precedence. The marker also lets
+ # command_line_setup report whether a bad option came from config or cmdline.
+ @ARGV= (@opts, "---end-of-config---", @ARGV);
+ return 1;
+}
+
sub command_line_setup {
my $opt_comment;
@@ -1187,13 +1403,15 @@ sub command_line_setup {
# Control what test suites or cases to run
'force+' => \$opt_force,
'skip-not-found' => \$opt_skip_not_found,
- 'suite|suites=s' => \$opt_suites,
+ 'suite|suites=s' => sub { push @opt_suites, split(/,/, $_[1]) },
'skip-rpl' => \&collect_option,
'skip-test=s' => \&collect_option,
'do-test=s' => \&collect_option,
'start-from=s' => \&collect_option,
'big-test+' => \$opt_big_test,
'combination=s' => \@opt_combinations,
+ 'combination-select|c=s' => \$opt_comb_sel,
+ 'exit-line|l=i' => \$opt_exit_line,
'experimental=s' => \@opt_experimentals,
'staging-run' => \$opt_staging_run,
@@ -1271,8 +1489,25 @@ sub command_line_setup {
'report-times' => \$opt_report_times,
'result-file' => \$opt_resfile,
'stress=s' => \$opt_stress,
+ 'head=i' => sub { $opt_head_log= $opt_head_warnings=
+ $_[1];
+ $opt_tail_lines= $opt_tail_log=
+ $opt_tail_warnings= 0 },
+ 'tail=i' => sub { $opt_tail_lines= $opt_tail_log=
+ $opt_tail_warnings= $_[1];
+ $opt_head_log= $opt_head_warnings= 0 },
'tail-lines=i' => \$opt_tail_lines,
+ 'head-log=i' => \$opt_head_log,
+ 'tail-log=i' => \$opt_tail_log,
+ 'strip-hints' => \$opt_strip_hints,
+ 'strip-limits' => \$opt_strip_limits,
+ 'strip-backtrace' => \$opt_strip_backtrace,
+ 'strip-log' => sub { $opt_strip_hints= $opt_strip_limits=
+ $opt_strip_backtrace= 1 },
+ 'head-warnings=i' => \$opt_head_warnings,
+ 'tail-warnings=i' => \$opt_tail_warnings,
'dry-run' => \$opt_dry_run,
+ 'list-combinations|lc' => \$opt_comb_list,
'help|h' => \$opt_usage,
# list-options is internal, not listed in help
@@ -1286,12 +1521,31 @@ sub command_line_setup {
My::Platform::options()
);
- # fix options (that take an optional argument and *only* after = sign
+ # 1. Merge [mtr] options from the config file(s) into @ARGV. Command-line
+ # options take precedence (they come after the config options), and a
+ # command-line --defaults-file/--defaults-extra-file overrides the
+ # corresponding MTR_CONFIG / MTR_CONFIG_EXTRA environment variable.
+ my $defaults= get_defaults_options();
+ $defaults->{conf_file} //= $ENV{MTR_CONFIG};
+ $defaults->{extra_file} //= $ENV{MTR_CONFIG_EXTRA};
+ $defaults->{groups}= ['mtr'];
+ load_defaults($defaults);
+
+ # 2. Fix options (that take an optional argument and *only* after = sign)
@ARGV = My::Debugger::fix_options(@ARGV);
+
+ # 3. Run GetOptions() on the merged config (config file + command-line)
GetOptions(%options) or usage("Can't read options");
usage("") if $opt_usage;
list_options(\%options) if $opt_list_options;
+ # Validate --combination-select here, so a bad value is rejected even when
+ # the selected test(s) have no .combinations file (combinations_from_file,
+ # the other check point, is only reached for tests that do).
+ mtr_error("--combination-select must be a non-zero integer, not ".
+ "'$opt_comb_sel'")
+ if defined $opt_comb_sel and $opt_comb_sel !~ /^-?[1-9][0-9]*$/;
+
# --------------------------------------------------------------------------
# Setup verbosity
# --------------------------------------------------------------------------
@@ -1299,9 +1553,13 @@ sub command_line_setup {
report_option('verbose', $opt_verbose);
}
- # Negative values aren't meaningful on integer options
+ # Negative values aren't meaningful on integer options, except the tail-*
+ # options where a negative value means "everything".
+ my %tail_opt= map { $_ => 1 }
+ qw(head=i tail=i tail-lines=i head-log=i tail-log=i head-warnings=i tail-warnings=i);
foreach(grep(/=i$/, keys %options))
{
+ next if $tail_opt{$_};
if (defined ${$options{$_}} &&
do { no warnings "numeric"; int ${$options{$_}} < 0})
{
@@ -1310,6 +1568,32 @@ sub command_line_setup {
}
}
+ # mysqltest caps --tail-lines at 10000 and rejects negatives, so map a
+ # negative ("everything") to that maximum.
+ $opt_tail_lines= 10000 if $opt_tail_lines < 0;
+
+ # --head-log/--tail-log trim the server error log in a crash report to its
+ # first/last lines. With neither given the whole log is kept; giving one
+ # alone switches the other end off.
+ if (defined $opt_head_log || defined $opt_tail_log) {
+ $opt_head_log //= 0;
+ $opt_tail_log //= 0;
+ }
+ else {
+ $opt_head_log= 0;
+ $opt_tail_log= -1;
+ }
+
+ # Same for --head-warnings/--tail-warnings on the shutdown-warnings report.
+ if (defined $opt_head_warnings || defined $opt_tail_warnings) {
+ $opt_head_warnings //= 0;
+ $opt_tail_warnings //= 0;
+ }
+ else {
+ $opt_head_warnings= 0;
+ $opt_tail_warnings= -1;
+ }
+
# Find the absolute path to the test directory
$glob_mysql_test_dir= cwd();
if ($glob_mysql_test_dir =~ / /)
@@ -1430,6 +1714,16 @@ sub command_line_setup {
}
}
+ # load_defaults() unconditionally prepends the "---end-of-config---" marker
+ # between the [mtr] config options and the command line. If it is gone now,
+ # a value-requiring option in the [mtr] section was written without a value
+ # and Getopt::Long consumed the marker (or the option after it) as its
+ # value; report that instead of misattributing a later error.
+ mtr_error("A value-less option in the [mtr] config file consumed the ",
+ "config/command-line separator - check the [mtr] section for an ",
+ "option that requires a value but was given none")
+ unless grep { /^---end-of-config---$/ } @ARGV;
+ my $opt_source= 'config file';
foreach my $arg ( @ARGV )
{
if ( $arg =~ /^--skip-/ )
@@ -1442,9 +1736,19 @@ sub command_line_setup {
# that the lone '--' separating options from arguments survives,
# simply ignore it.
}
+ elsif ( $arg =~ /^---end-of-config---$/ )
+ {
+ $opt_source= 'command-line';
+ }
elsif ( $arg =~ /^-/ )
{
- usage("Invalid option \"$arg\"");
+ if ($opt_source eq 'config file')
+ {
+ # strip leading dashes and any =value, leaving the option name
+ $arg =~ s/^-+//;
+ $arg =~ s/=.*//s;
+ }
+ usage("Invalid ${opt_source} option \"${arg}\"");
}
else
{
@@ -1584,9 +1888,10 @@ sub command_line_setup {
# --------------------------------------------------------------------------
# Check parallel value
# --------------------------------------------------------------------------
- if ($opt_parallel ne "auto" && $opt_parallel < 1)
+ if ($opt_parallel ne "auto" &&
+ ($opt_parallel !~ /^\d+$/ || $opt_parallel < 1))
{
- mtr_error("0 or negative parallel value makes no sense, use 'auto' or positive number");
+ mtr_error("Invalid parallel value '$opt_parallel', use 'auto' or a positive number");
}
# --------------------------------------------------------------------------
@@ -1673,7 +1978,7 @@ sub command_line_setup {
mtr_error("--user-args only valid with --start options")
unless $start_only;
mtr_error("--user-args cannot be combined with named suites or tests")
- if $opt_suites || @opt_cases;
+ if @opt_suites || @opt_cases;
}
# --------------------------------------------------------------------------
@@ -1694,8 +1999,8 @@ sub command_line_setup {
$opt_stress=~ s/,/ /g;
$opt_user_args= 1;
mtr_error("--stress cannot be combined with named ordinary suites or tests")
- if $opt_suites || @opt_cases;
- $opt_suites="stress";
+ if @opt_suites || @opt_cases;
+ @opt_suites= ("stress");
@opt_cases= ("wrapper");
$ENV{MST_OPTIONS}= $opt_stress;
}
@@ -4392,13 +4697,140 @@ ($$)
# Return as a single string
#
+# Trim an excerpt (arrayref of lines) to its first $head and last $tail lines.
+# For each of $head/$tail: <0 = unbounded, 0 = none, N = that many lines.
+# A line is kept if it falls within the first $head or the last $tail; the
+# omitted middle is replaced with a "< snip M lines >" marker.
+sub splice_lines {
+ my ($lines, $head, $tail)= @_;
+ my $total= @$lines;
+ return @$lines if $head < 0 || $tail < 0 || $head + $tail >= $total;
+ return () if $head == 0 && $tail == 0;
+ my $snipped= $total - $head - $tail;
+ return (@$lines[0 .. $head - 1],
+ "< snip $snipped lines >\n",
+ @$lines[$total - $tail .. $total - 1]);
+}
+
+# When --strip-hints is given, drop the explanatory crash-report "hint" paragraphs
+# (bug-reporting boilerplate, backtrace instructions, ...) that the server
+# writes to its error log, keeping the actual data. Each hint is a paragraph
+# from an opener line to the next blank line.
+sub strip_crash_hints
+{
+ my @openers=
+ (
+ qr/^Sorry, we probably made a mistake/,
+ qr/^Your assistance in bug reporting/,
+ qr/^To report this bug/,
+ qr/^Please include the information from the server start/,
+ qr/^The information page at/,
+ qr/^The manual page at/,
+ qr/^Attempting backtrace/,
+ qr/^This could be because you hit a bug/,
+ qr/^We will try our best to scrape up/,
+ );
+ my (@out, $skip);
+ for my $line (@_) {
+ if ($skip) {
+ $skip= 0 if $line =~ /^\s*$/;
+ next;
+ }
+ if (grep { $line =~ $_ } @openers) {
+ $skip= 1;
+ next;
+ }
+ push @out, $line;
+ }
+ return @out;
+}
+
+# When --strip-limits is given, drop the "Resource Limits" table the server writes
+# to its error log on a crash. Unlike a hint paragraph it has no trailing
+# blank line: the block is the opener, the "Limit ... Soft Limit ..." header
+# and the "Max ..." rows, ending at the first line that is neither.
+sub strip_resource_limits
+{
+ my (@out, $skip);
+ for my $line (@_) {
+ if ($skip) {
+ next if $line =~ /^(?:Limit\s|Max )/;
+ $skip= 0;
+ }
+ if ($line =~ /^Resource Limits/) {
+ $skip= 1;
+ next;
+ }
+ push @out, $line;
+ }
+ return @out;
+}
+
+# When --strip-backtrace is given, drop the server's own (non-debugger) stack
+# backtrace from the error log: the "Attempting backtrace" intro, the thread
+# pointer and stack_bottom lines, addr2line diagnostics and the frame lines
+# (which end in an [0x...] address). The gdb/lldb backtrace produced from a
+# core file by My::CoreDump is a separate thing and is not affected.
+sub strip_backtrace
+{
+ # A server crash backtrace is a bounded block: it starts at an "Attempting
+ # backtrace" or "Thread pointer:" line and runs through the intro prose, the
+ # optional "stack_bottom"/"Stack range" header, the frame list (either
+ # "...[0x...]" symbol lines, or bare "0x..." addresses from the builds whose
+ # my_print_stacktrace uses the frame-pointer walker), and the interleaved
+ # addr2line / my_addr_resolve diagnostics. The block ends at the first line
+ # that is not recognisable backtrace content; that line and everything after
+ # it are kept, so later log sections (or a second backtrace) survive.
+ #
+ # Anything unrecognised ends the block (rather than being dropped), so a
+ # backtrace with no "stack_bottom" line and no bracketed frames - as the
+ # frame-pointer walker produces, and as an aborted backtrace produces - does
+ # not swallow the rest of the log.
+ my @out;
+ my $in= 0;
+ for my $line (@_) {
+ if (!$in) {
+ if ($line =~ /^Thread pointer:/ or $line =~ /^Attempting backtrace/) {
+ $in= 1;
+ } else {
+ push @out, $line;
+ }
+ next;
+ }
+ # Inside a backtrace: drop recognised content, otherwise the block has
+ # ended - fall back to keeping this line and resuming normal output.
+ next if $line =~ /^\s*$/ # blank line
+ or $line =~ /^Thread pointer:/
+ or $line =~ /^\(note: / # "(note: Retrieving ...)"
+ or $line =~ /^stack_bottom /
+ or $line =~ /^Stack range sanity check/ # frame-pointer walker
+ or $line =~ /wild guesses/ # (Alpha) warning
+ or $line =~ /^\(my_addr_resolve failure/
+ or $line =~ /^addr2line:/
+ or $line =~ /\[0x[0-9a-fA-F]+\]\s*$/ # symbol frame
+ or $line =~ /^0x[0-9a-fA-F]+\s*$/ # bare walker frame
+ or $line =~ /Aborting backtrace/
+ or $line =~ /frame pointer/
+ or $line =~ /Bogus stack limit/;
+ $in= 0;
+ push @out, $line;
+ }
+ return @out;
+}
+
sub get_log_from_proc ($$) {
my ($proc, $name)= @_;
my $srv_log= "";
+ return $srv_log if $opt_head_log == 0 && $opt_tail_log == 0;
+
foreach my $mysqld (all_servers()) {
if ($mysqld->{proc} eq $proc) {
- my @srv_lines= extract_server_log($mysqld->if_exist('log-error'), $name);
+ my @lines= extract_server_log($mysqld->if_exist('log-error'), $name);
+ @lines= strip_crash_hints(@lines) if $opt_strip_hints;
+ @lines= strip_resource_limits(@lines) if $opt_strip_limits;
+ @lines= strip_backtrace(@lines) if $opt_strip_backtrace;
+ my @srv_lines= splice_lines(\@lines, $opt_head_log, $opt_tail_log);
$srv_log= "\nServer log from this test:\n" .
"----------SERVER LOG START-----------\n". join ("", @srv_lines) .
"----------SERVER LOG END-------------\n";
@@ -4645,7 +5077,7 @@ ($$)
# Get the args needed for the embedded server
# and append them to args prefixed
- # with --sever-arg=
+ # with --server-arg=
my $mysqld= $config->group('embedded')
or mtr_error("Could not get [embedded] section");
@@ -4782,18 +5214,19 @@ ($)
sub check_warnings_post_shutdown {
my ($server_socket)= @_;
my $testname_hash= { };
- my $report= '';
+ my @match_all;
foreach my $mysqld ( mysqlds())
{
my ($testlist, $match_lines)=
extract_warning_lines($mysqld->value('log-error'), 1);
$testname_hash->{$_}= 1 for @$testlist;
- $report.= join('', @$match_lines);
+ push @match_all, @$match_lines;
}
my @warning_tests= keys(%$testname_hash);
if (@warning_tests) {
my $fake_test= My::Test->new(testnames => \@warning_tests);
- $fake_test->{'warnings'}= $report;
+ $fake_test->{'warnings'}=
+ join('', splice_lines(\@match_all, $opt_head_warnings, $opt_tail_warnings));
$fake_test->write_test($server_socket, 'WARNINGS');
}
}
@@ -5731,7 +6164,7 @@ ($)
# Get the args needed for the embedded server
# and append them to args prefixed
- # with --sever-arg=
+ # with --server-arg=
my $mysqld= $config->group('embedded')
or mtr_error("Could not get [embedded] section");
@@ -5766,6 +6199,10 @@ ($)
# Number of lines of resut to include in failure report
mtr_add_arg($args, "--tail-lines=%d", $opt_tail_lines);
+ # Stop the test before the command at this line (like an --exit directive)
+ mtr_add_arg($args, "--exit-line=%d", $opt_exit_line)
+ if $opt_exit_line;
+
if ( defined $tinfo->{'result_file'} ) {
mtr_add_arg($args, "--result-file=%s", $tinfo->{'result_file'});
}
@@ -5918,10 +6355,19 @@ ($)
tests
defaults-extra-file= Extra config template to add to
all generated configs
- combination= Use at least twice to run tests with specified
- options to mysqld
+ combination=OPTIONS Extra mysqld options making up one test combination.
+ Repeat it (two or more times) to run every test once
+ per combination. When given, it applies to all tests
+ and their .combinations files are ignored.
+ combination-select=N Run only the Nth combination from each .combinations
+ c=N file, counting [] sections in file order (1-based).
+ A negative N counts from the end, -1 being the last.
+ Ignored when --combination is used.
dry-run Don't run any tests, print the list of tests
that were selected for execution
+ list-combinations For the specified test(s), list the available
+ lc combinations in selectable "test,combination" form
+ and exit.
Options to control directories to use
tmpdir=DIR The directory where temporary files are stored
@@ -5960,7 +6406,10 @@ ($)
prefix may be suite.testname or just testname
suite[s]=NAME1,..,NAMEN
Collect tests in suites from the comma separated
- list of suite names.
+ list of suite names. A name prefixed with '!' is
+ excluded; if only '!'-prefixed names are given, they
+ are excluded from the default set. Names accumulate
+ across the command line and the [mtr] config file.
The default is: "@DEFAULT_SUITES"
skip-rpl Skip the replication test cases.
big-test Also run tests marked as "big". Repeat this option
@@ -6101,8 +6550,34 @@ ($)
stress=ARGS Run stress test, providing options to
mysql-stress-test.pl. Options are separated by comma.
xml-report= Output jUnit xml file of the results.
- tail-lines=N Number of lines of the result to include in a failure
- report.
+ tail-lines=N Number of lines of the mysqltest result to include in
+ a failure report. 0 disables it, negative includes all
+ (max 10000); default 20.
+ head=N Shortcut to set the head-* options to N and the tail-*
+ options to 0.
+ tail=N Shortcut to set the tail-* options to N and the head-*
+ options to 0.
+ head-log=N Keep the first N lines of the server error log in a
+ crash report (0 none, negative all). With neither
+ head-log nor tail-log given the whole log is kept;
+ giving one alone drops the opposite end, giving both
+ keeps both ends with the middle snipped.
+ tail-log=N Like head-log but keeps the last N lines.
+ strip-hints Omit the explanatory bug-reporting and backtrace
+ "hint" paragraphs from a crash report, keeping only
+ the actual server log and backtrace.
+ strip-limits Omit the "Resource Limits" table from a crash report.
+ strip-backtrace Omit the server's own (non-debugger) stack backtrace
+ from a crash report; the gdb/lldb backtrace from a
+ core file, if any, is kept.
+ strip-log Enable all of --strip-hints, --strip-limits and
+ --strip-backtrace.
+ head-warnings=N Keep the first N suspicious lines of the shutdown-
+ warnings report (0 none, negative all). With neither
+ head-warnings nor tail-warnings given the whole report
+ is kept; giving one alone drops the opposite end,
+ giving both keeps both ends with the middle snipped.
+ tail-warnings=N Like head-warnings but keeps the last N lines.
Some options that control enabling a feature for normal test runs,
can be turned off by prepending 'no' to the option, e.g. --notimer.
diff --git a/mysql-test/suite/mtr/misc/crash.test b/mysql-test/suite/mtr/misc/crash.test
new file mode 100644
index 0000000000000..f6bdf07ee4a47
--- /dev/null
+++ b/mysql-test/suite/mtr/misc/crash.test
@@ -0,0 +1,10 @@
+# Inner test for --tail-log: crash the server so the parent mtr dumps the real
+# server error log (a SIGSEGV backtrace) in its crash report.
+--source include/not_windows.inc
+
+--let $pidf=`SELECT @@global.pid_file`
+--exec kill -SEGV `cat $pidf`
+
+# the connection is gone now
+--error 2006,2013
+SELECT SLEEP(30);
diff --git a/mysql-test/suite/mtr/misc/empty.combinations b/mysql-test/suite/mtr/misc/empty.combinations
new file mode 100644
index 0000000000000..7e95b3b5d482d
--- /dev/null
+++ b/mysql-test/suite/mtr/misc/empty.combinations
@@ -0,0 +1,5 @@
+[timestamp]
+[trx_id]
+[heap]
+[traditional]
+[myisam]
diff --git a/mysql-test/suite/mtr/misc/empty.test b/mysql-test/suite/mtr/misc/empty.test
new file mode 100644
index 0000000000000..1f34d6e5ce7a5
--- /dev/null
+++ b/mysql-test/suite/mtr/misc/empty.test
@@ -0,0 +1,5 @@
+# Placeholder test with combinations, used only to demonstrate mtr
+# --list-combinations: empty.combinations plus the lcase_names / protocol
+# combination groups pulled in by the includes below.
+--source include/lcase_names.inc
+--source include/protocol.inc
diff --git a/mysql-test/suite/mtr/misc/warn.test b/mysql-test/suite/mtr/misc/warn.test
new file mode 100644
index 0000000000000..ed1bbdb5f4cda
--- /dev/null
+++ b/mysql-test/suite/mtr/misc/warn.test
@@ -0,0 +1,35 @@
+# Inner test for --tail-warnings: generate real server warnings.
+#
+# A system-versioned table partitioned by SYSTEM_TIME LIMIT emits
+# "... last HISTORY partition (...) is out of LIMIT ..." warnings once the
+# history partitions overflow (see suite/versioning/t/partition.test).
+#
+# This test has no .result, so it fails - which means mtr's per-test warning
+# check is skipped and the warnings survive to the post-shutdown report that
+# --tail-warnings trims.
+
+--source include/have_partition.inc
+
+create or replace table t1 (x int)
+ with system versioning
+ partition by system_time limit 2 partitions 3;
+
+--disable_query_log
+--disable_result_log
+let $i= 30;
+while ($i)
+{
+ insert into t1 values (1), (2), (3);
+ delete from t1;
+ dec $i;
+}
+--enable_result_log
+--enable_query_log
+
+drop table t1;
+
+# Force the test to fail (like the intentionally-wrong result in
+# versioning/partition.test) so mtr's per-test warning check is skipped and
+# the warnings fall through to the post-shutdown report that --tail-warnings
+# trims.
+select * from no_such_table_force_failure;
diff --git a/mysql-test/suite/mtr/t/feat.result b/mysql-test/suite/mtr/t/feat.result
new file mode 100644
index 0000000000000..eea0aed8f40e3
--- /dev/null
+++ b/mysql-test/suite/mtr/t/feat.result
@@ -0,0 +1,192 @@
+#### --head-log / --tail-log (crash report) ####
+mtr/misc.crash [ fail ]
+Server [mysqld.1 - pid: PID, winpid: PID, exit: 256] crashed
+Server log from this test:
+----------SERVER LOG START-----------
+< snip N lines >
+Resource Limits (excludes unlimited resources):
+Limit Soft Limit Hard Limit Units
+Max stack size <...cut...>
+Max processes <...cut...>
+Max open files <...cut...>
+Max locked memory <...cut...>
+Max pending signals <...cut...>
+Max msgqueue size <...cut...>
+Max nice priority <...cut...>
+Max realtime priority <...cut...>
+Core pattern: <...cut...>
+Kernel version: <...cut...>
+----------SERVER LOG END-------------
+#### --strip-log (crash report) ####
+mtr/misc.crash [ fail ]
+Server [mysqld.1 - pid: PID, winpid: PID, exit: 256] crashed
+Server log from this test:
+----------SERVER LOG START-----------
+[ERROR] got signal 11 ;
+Writing a core file...
+Core pattern: <...cut...>
+Kernel version: <...cut...>
+----------SERVER LOG END-------------
+#### --tail-warnings (shutdown-warnings report) ####
+# --tail-warnings=0
+mtr/misc.warn [ fail ]
+***Warnings generated in error logs during shutdown after running tests: mtr/misc.warn
+# --tail-warnings=5
+mtr/misc.warn [ fail ]
+***Warnings generated in error logs during shutdown after running tests: mtr/misc.warn
+< snip N lines >
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+# --tail-warnings=-1
+mtr/misc.warn [ fail ]
+***Warnings generated in error logs during shutdown after running tests: mtr/misc.warn
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+[Warning] Versioned table `test`.`t1`: last HISTORY partition (`p1`) is out of LIMIT, need more HISTORY partitions
+#### --list-combinations ####
+Combinations for mtr/misc.empty: lcase1,lcase2,lcase_def,nm,timestamp,trx_id,heap,traditional,myisam
+mtr/misc.empty,lcase1,nm,timestamp
+mtr/misc.empty,lcase1,nm,trx_id
+mtr/misc.empty,heap,lcase1,nm
+mtr/misc.empty,lcase1,nm,traditional
+mtr/misc.empty,lcase1,myisam,nm
+mtr/misc.empty,lcase2,nm,timestamp
+mtr/misc.empty,lcase2,nm,trx_id
+mtr/misc.empty,heap,lcase2,nm
+mtr/misc.empty,lcase2,nm,traditional
+mtr/misc.empty,lcase2,myisam,nm
+mtr/misc.empty,lcase_def,nm,timestamp
+mtr/misc.empty,lcase_def,nm,trx_id
+mtr/misc.empty,heap,lcase_def,nm
+mtr/misc.empty,lcase_def,nm,traditional
+mtr/misc.empty,lcase_def,myisam,nm
+#### --ps --list-combinations ####
+Combinations for mtr/misc.empty: lcase1,lcase2,lcase_def,ps,timestamp,trx_id,heap,traditional,myisam
+mtr/misc.empty,lcase1,ps,timestamp
+mtr/misc.empty,lcase1,ps,trx_id
+mtr/misc.empty,heap,lcase1,ps
+mtr/misc.empty,lcase1,ps,traditional
+mtr/misc.empty,lcase1,myisam,ps
+mtr/misc.empty,lcase2,ps,timestamp
+mtr/misc.empty,lcase2,ps,trx_id
+mtr/misc.empty,heap,lcase2,ps
+mtr/misc.empty,lcase2,ps,traditional
+mtr/misc.empty,lcase2,myisam,ps
+mtr/misc.empty,lcase_def,ps,timestamp
+mtr/misc.empty,lcase_def,ps,trx_id
+mtr/misc.empty,heap,lcase_def,ps
+mtr/misc.empty,lcase_def,ps,traditional
+mtr/misc.empty,lcase_def,myisam,ps
+#### --suite with negation ####
+Using suites:
+No tests to run...
+#### --exit-line (stop before line 5) ####
+Logging: <...cut...>
+VS config:
+vardir: <...cut...>
+Checking leftover processes...
+Removing old var directory...
+Creating var directory <...cut...>
+Checking supported features...
+MariaDB Version <...cut...>
+ - SSL <...cut...>
+ - binaries <...cut...>
+Collecting tests...
+Installing system database...
+
+==============================================================================
+
+TEST RESULT TIME (ms) or COMMENT
+--------------------------------------------------------------------------
+
+worker <...cut...>
+main.1st [ fail ]
+ Test ended at <...cut...>
+
+CURRENT_TEST: main.1st
+--- <...cut...>
++++ <...cut...>
+@@ -6,36 +6,3 @@
+ performance_schema
+ sys
+ test
+-show tables in mysql;
+-Tables_in_mysql
+-column_stats
+-columns_priv
+-db
+-event
+-func
+-general_log
+-global_priv
+-gtid_slave_pos
+-help_category
+-help_keyword
+-help_relation
+-help_topic
+-index_stats
+-innodb_index_stats
+-innodb_table_stats
+-plugin
+-proc
+-procs_priv
+-proxies_priv
+-roles_mapping
+-servers
+-slow_log
+-table_stats
+-tables_priv
+-time_zone
+-time_zone_leap_second
+-time_zone_name
+-time_zone_transition
+-time_zone_transition_type
+-transaction_registry
+-user
+
+Result length mismatch
+
+ - saving <...cut...>
+--------------------------------------------------------------------------
+The servers were restarted 0 times
+Spent <...cut...>
+
+Completed: Failed 1/1 tests, 0.00% were successful.
+
+Failing test(s): main.1st
+
+The log files in var/log may give you some hint of what went wrong.
+
+If you want to report this error, MariaDB's bug tracker is found at
+https://jira.mariadb.org
+
+mysql-test-run: *** ERROR: there were failing test cases
diff --git a/mysql-test/suite/mtr/t/feat.test b/mysql-test/suite/mtr/t/feat.test
new file mode 100644
index 0000000000000..f60d3d6d1cbf3
--- /dev/null
+++ b/mysql-test/suite/mtr/t/feat.test
@@ -0,0 +1,86 @@
+# MDEV-30281 - exercise several mtr features by running a child
+# mariadb-test-run.pl and checking its output:
+# * --head-log / --tail-log / --strip-log trimming of a crash report
+# (suite/mtr/misc.crash)
+# * --head-warnings / --tail-warnings trimming of the shutdown-warnings
+# report (suite/mtr/misc.warn)
+# * --list-combinations and --suite selection
+# * --exit-line stopping a test before a given line (main.1st)
+#
+# The child's output is full of volatile data (pids, timestamps, addresses, an
+# unbounded backtrace, ...). Each run is written to a scratch file, reduced to
+# its deterministic evidence lines with include/grep.inc, and the residual
+# numbers are normalized with --replace_regex ($NORM).
+
+--source include/not_windows.inc
+# spawns several child mtr runs (each starts/crashes a server) - slow
+--source include/big_test.inc
+
+# Run the child mtr in a sanitized environment: inheriting the parent's
+# per-run MTR_*/MYSQL* variables breaks path/port resolution (safe_process
+# fails to exec, ports collide). env -i keeps only what the child needs and
+# lets it recompute the rest; MTR_BUILD_THREAD=auto picks its own free ports.
+--let $CHILD_ENV= env -i PATH="$PATH" HOME="$HOME" MTR_BINDIR="$MTR_BINDIR" MTR_BUILD_THREAD=auto
+--let $CHILD_OPTS= --force --max-test-fail=0 --print-core=no
+--let $CHILD= $CHILD_ENV perl $MYSQL_TEST_DIR/mariadb-test-run.pl --vardir=$MYSQLTEST_VARDIR/tmp/tail_child $CHILD_OPTS
+--let $TAIL_OUT= $MYSQLTEST_VARDIR/tmp/tail_out.txt
+
+# include/grep.inc inputs, constant across cases
+--let grep_file= $TAIL_OUT
+--let grep_regex= misc[.](crash|warn) +[[]|Server log from this test|crashed while running|failed during test run|SERVER LOG (START|END)|< snip [0-9]+ lines >|got signal|Attempting backtrace|Thread pointer|stack_bottom|my_print_stacktrace|handle_fatal_signal|mariadbd[(]main|Writing a core|Resource Limits|Soft Limit|Max |Core pattern|Kernel version|Warnings generated in error logs|out of LIMIT|Combinations for|misc[.]empty,|Using suites:|No tests to run
+# normalize the residual volatile numbers - reused by every case below
+--let $NORM= /pid: [0-9]+, winpid: [0-9]+/pid: PID, winpid: PID/ /The server \[/Server [/ /crashed while running '[^']*'/crashed/ /failed during test run/crashed/ /< snip [0-9]+ lines >/< snip N lines >/ /[0-9]{4}-[0-9]{2}-[0-9]{2} +[0-9:]+ +[0-9]+ \[Warning\]/[Warning]/ /[0-9]+ +[0-9:]+ \[ERROR\] [^ ]+ got signal/[ERROR] got signal/ /[^\n]*mariadbd[(]my_print_stacktrace[^\n]*/mariadbd(my_print_stacktrace <...cut...>/ /[^\n]*handle_fatal_signal[^\n]*/<...cut...>/ /[^\n]*mariadbd[(]main[^\n]*/mariadbd(main <...cut...>/ /Thread pointer:[^\n]*/Thread pointer: <...cut...>/ /stack_bottom =[^\n]*/stack_bottom = <...cut...>/ /(Max [a-zA-Z ]+?) +[^\n]*/\1 <...cut...>/ /Core pattern:[^\n]*/Core pattern: <...cut...>/ /Kernel version:[^\n]*/Kernel version: <...cut...>/
+# --exit-line dumps a whole child mtr run (no grep), so cut the volatile
+# banner/footer lines by their stable prefix - this also removes every
+# absolute path, version, thread/port and timestamp without embedding one
+--let $NORM_RUN= /Logging:[^\n]*/Logging: <...cut...>/ /vardir:[^\n]*/vardir: <...cut...>/ /Creating var directory[^\n]*/Creating var directory <...cut...>/ /MariaDB Version[^\n]*/MariaDB Version <...cut...>/ / - SSL[^\n]*/ - SSL <...cut...>/ / - binaries[^\n]*/ - binaries <...cut...>/ /worker\[[0-9]+\][^\n]*/worker <...cut...>/ /Test ended at[^\n]*/Test ended at <...cut...>/ /--- [^\n]*/--- <...cut...>/ /\+\+\+ [^\n]*/+++ <...cut...>/ / - saving[^\n]*/ - saving <...cut...>/ /Spent[^\n]*/Spent <...cut...>/
+
+--echo #### --head-log / --tail-log (crash report) ####
+--exec $CHILD --nowarnings --head-log=15 --tail-log=15 mtr/misc.crash > $TAIL_OUT 2>&1 || true
+--replace_regex $NORM
+--source include/grep.inc
+
+--echo #### --strip-log (crash report) ####
+--exec $CHILD --nowarnings --strip-log mtr/misc.crash > $TAIL_OUT 2>&1 || true
+--replace_regex $NORM
+--source include/grep.inc
+
+--echo #### --tail-warnings (shutdown-warnings report) ####
+
+--echo # --tail-warnings=0
+--exec $CHILD --tail-warnings=0 mtr/misc.warn > $TAIL_OUT 2>&1 || true
+--replace_regex $NORM
+--source include/grep.inc
+
+--echo # --tail-warnings=5
+--exec $CHILD --tail-warnings=5 mtr/misc.warn > $TAIL_OUT 2>&1 || true
+--replace_regex $NORM
+--source include/grep.inc
+
+--echo # --tail-warnings=-1
+--exec $CHILD --tail-warnings=-1 mtr/misc.warn > $TAIL_OUT 2>&1 || true
+--replace_regex $NORM
+--source include/grep.inc
+
+--echo #### --list-combinations ####
+--exec $CHILD --list-combinations mtr/misc.empty > $TAIL_OUT 2>&1 || true
+--replace_regex $NORM
+--source include/grep.inc
+
+--echo #### --ps --list-combinations ####
+--exec $CHILD --ps --list-combinations mtr/misc.empty > $TAIL_OUT 2>&1 || true
+--replace_regex $NORM
+--source include/grep.inc
+
+--echo #### --suite with negation ####
+--exec $CHILD --suite=main,!main > $TAIL_OUT 2>&1 || true
+--replace_regex $NORM
+--source include/grep.inc
+
+# main/1st.test is: (4) show databases; (5) show tables in mysql;
+# --exit-line=5 runs line 4 but stops before line 5, so the recorded result's
+# "show tables in mysql;" section is missing - the test fails with a length
+# mismatch, proving the early exit landed exactly at the requested line.
+--echo #### --exit-line (stop before line 5) ####
+--replace_regex $NORM $NORM_RUN
+--exec $CHILD --exit-line=5 main.1st 2>&1 || true
diff --git a/mysys/my_default.c b/mysys/my_default.c
index 0b50235aa695d..0c7eeeba95eea 100644
--- a/mysys/my_default.c
+++ b/mysys/my_default.c
@@ -370,14 +370,18 @@ int load_defaults(const char *conf_file, const char **groups,
SYNOPSIS
my_load_defaults()
- conf_file Basename for configuration file to search for.
- If this is a path, then only this file is read.
- groups Which [group] entrys to read.
- Points to an null terminated array of pointers
- argc Pointer to argc of original program
- argv Pointer to argv of original program
- default_directories Pointer to a location where a pointer to the list
- of default directories will be stored
+ conf_file [in] Basename of the option file to look for (e.g.
+ "my"), searched for in each standard directory.
+ If it instead contains a directory part (i.e.
+ it is a path), only that single file is read.
+ groups [in] Which [group] entries to read.
+ Points to a null terminated array of pointers.
+ argc [in,out] Pointer to argc: original count in, count of the
+ merged argument vector out.
+ argv [in,out] Pointer to argv: original vector in, new merged
+ vector out. Free it with free_defaults().
+ default_directories [out] Optional; may be NULL. If not NULL, receives a
+ pointer to the list of searched directories.
IMPLEMENTATION