Files
percona-toolkit/t/lib/DSNParser.t
Sveta Smirnova e2207ea232 PT-2340 - Support MySQL 8.4
- Removed offensive terminology from library files and their tests
- Removed unused sandbox/prove2junit.pl
- Added option mysql_ssl to DSN and possibility to have DSN of multiple letters
2024-07-25 19:03:33 +03:00

671 lines
17 KiB
Perl

#!/usr/bin/perl
BEGIN {
die "The PERCONA_TOOLKIT_BRANCH environment variable is not set.\n"
unless $ENV{PERCONA_TOOLKIT_BRANCH} && -d $ENV{PERCONA_TOOLKIT_BRANCH};
unshift @INC, "$ENV{PERCONA_TOOLKIT_BRANCH}/lib";
};
use strict;
use warnings FATAL => 'all';
use English qw(-no_match_vars);
use Test::More;
use DSNParser;
use OptionParser;
use PerconaTest;
use Data::Dumper;
my $opts = [
{
key => 'A',
desc => 'Default character set',
dsn => 'charset',
copy => 1,
},
{
key => 'D',
desc => 'Database to use',
dsn => 'database',
copy => 1,
},
{
key => 'F',
desc => 'Only read default options from the given file',
dsn => 'mysql_read_default_file',
copy => 1,
},
{
key => 'h',
desc => 'Connect to host',
dsn => 'host',
copy => 1,
},
{
key => 'p',
desc => 'Password to use when connecting',
dsn => 'password',
copy => 1,
},
{
key => 'P',
desc => 'Port number to use for connection',
dsn => 'port',
copy => 1,
},
{
key => 'S',
desc => 'Socket file to use for connection',
dsn => 'mysql_socket',
copy => 1,
},
{
key => 'u',
desc => 'User for login if not current user',
dsn => 'user',
copy => 1,
},
];
my $dp = new DSNParser(opts => $opts);
is_deeply(
$dp->parse('u=a,p=b'),
{ u => 'a',
p => 'b',
S => undef,
h => undef,
P => undef,
F => undef,
D => undef,
A => undef,
},
'Basic DSN'
);
is_deeply(
$dp->parse('u=a,p=b,F=/something.cnf'),
{
F => '/something.cnf',
P => undef,
u => "a",
p => "b",
h => undef,
A => undef,
S => undef,
D => undef,
},
'Read from config overrides other params'
);
is_deeply(
$dp->parse('S=/tmp/sock'),
{ u => undef,
p => undef,
S => '/tmp/sock',
h => undef,
P => undef,
F => undef,
D => undef,
A => undef,
},
'Basic DSN with one part'
);
is_deeply(
$dp->parse('u=a,p=b,A=utf8'),
{ u => 'a',
p => 'b',
S => undef,
h => undef,
P => undef,
F => undef,
D => undef,
A => 'utf8',
},
'Basic DSN with charset'
);
# The test that was here is no longer needed now because
# all opts must be specified now.
is_deeply(
$dp->parse('u=a,p=b', { D => 'foo', h => 'me' }, { S => 'bar', h => 'host' } ),
{ D => 'foo',
F => undef,
h => 'me',
p => 'b',
P => undef,
S => 'bar',
u => 'a',
A => undef,
},
'DSN with defaults'
);
is(
$dp->as_string(
$dp->parse('u=a,p=b', { D => 'foo', h => 'me' }, { S => 'bar', h => 'host' } )
),
'D=foo,S=bar,h=me,p=...,u=a',
'DSN stringified when it gets DSN as arg'
);
is(
$dp->as_string(
'D=foo,S=bar,h=me,p=b,u=a',
),
'D=foo,S=bar,h=me,p=b,u=a',
'DSN stringified when it gets a string as arg'
);
is (
$dp->as_string({ bez => 'bat', h => 'foo' }),
'h=foo',
'DSN stringifies without extra crap',
);
is (
$dp->as_string({ h=>'localhost', P=>'3306',p=>'omg'}, [qw(h P)]),
'h=localhost,P=3306',
'DSN stringifies only requested parts'
);
# The test that was here is no longer need due to issue 55.
# DSN usage comes from the POD now.
$dp->prop('autokey', 'h');
is_deeply(
$dp->parse('automatic'),
{ D => undef,
F => undef,
h => 'automatic',
p => undef,
P => undef,
S => undef,
u => undef,
A => undef,
},
'DSN with autokey'
);
$dp->prop('autokey', 'h');
is_deeply(
$dp->parse('localhost,A=utf8'),
{ u => undef,
p => undef,
S => undef,
h => 'localhost',
P => undef,
F => undef,
D => undef,
A => 'utf8',
},
'DSN with an explicit key and an autokey',
);
is_deeply(
$dp->parse('automatic',
{ D => 'foo', h => 'me', p => 'b' },
{ S => 'bar', h => 'host', u => 'a' } ),
{ D => 'foo',
F => undef,
h => 'automatic',
p => 'b',
P => undef,
S => 'bar',
u => 'a',
A => undef,
},
'DSN with defaults and an autokey'
);
# The test that was here is no longer need due to issue 55.
# DSN usage comes from the POD now.
is_deeply (
[
$dp->get_cxn_params(
$dp->parse(
'u=a,p=b',
{ D => 'foo', h => 'me' },
{ S => 'bar', h => 'host' } ))
],
[
'DBI:mysql:foo;host=me;mysql_socket=bar;mysql_read_default_group=client',
'a',
'b',
],
'Got connection arguments',
);
is_deeply (
[
$dp->get_cxn_params(
$dp->parse(
'u=a,p=b,A=foo',
{ D => 'foo', h => 'me' },
{ S => 'bar', h => 'host' } ))
],
[
'DBI:mysql:foo;host=me;mysql_socket=bar;charset=foo;mysql_read_default_group=client',
'a',
'b',
],
'Got connection arguments with charset',
);
# Make sure we can connect to MySQL with a charset
my $d = $dp->parse('h=127.0.0.1,P=12345,A=utf8,u=msandbox,p=msandbox');
my $dbh;
eval {
$dbh = $dp->get_dbh($dp->get_cxn_params($d), {});
};
SKIP: {
skip 'Cannot connect to sandbox master', 6 if $EVAL_ERROR;
$dp->fill_in_dsn($dbh, $d);
is($d->{P}, 12345, 'Left port alone');
is($d->{u}, 'msandbox', 'Filled in username');
is($d->{S}, '/tmp/12345/mysql_sandbox12345.sock', 'Filled in socket');
is($d->{h}, '127.0.0.1', 'Left hostname alone');
my $want = $sandbox_version lt '8.0' ? [ qw(utf8 utf8 utf8) ]: [ qw(utf8mb3 utf8mb3 utf8mb3) ];
is_deeply(
$dbh->selectrow_arrayref('select @@character_set_client, @@character_set_connection, @@character_set_results'),
$want,
'Set charset'
);
$dbh->disconnect();
# Issue 1282: Enabling utf8 with --charset (-A) is case-sensitive
# This test really doesn't do anything because the problem is in the line,
# mysql_enable_utf8 => ($cxn_string =~ m/charset=utf8/ ? 1 : 0),
# in get_dbh(). That line is part of a hashref declaration so we
# have no access to it here. I keep this this test because it allows
# me to look manually via PTDEBUG and see that mysql_enable_utf8=>1
# even if A=UTF8.
$d = $dp->parse('h=127.0.0.1,P=12345,A=UTF8,u=msandbox,p=msandbox');
eval {
$dbh = $dp->get_dbh($dp->get_cxn_params($d), {});
};
is_deeply(
$dbh->selectrow_arrayref('select @@character_set_client, @@character_set_connection, @@character_set_results'),
$want,
'Set utf8 charset case-insensitively (issue 1282)'
);
};
$dp->prop('dbidriver', 'Pg');
is_deeply (
[
$dp->get_cxn_params(
{
u => 'a',
p => 'b',
h => 'me',
D => 'foo',
},
)
],
[
'DBI:Pg:dbname=foo;host=me',
'a',
'b',
],
'Got connection arguments for PostgreSQL',
);
$dp->prop('required', { h => 1 } );
throws_ok (
sub { $dp->parse('u=b') },
qr/Missing required DSN option 'h' in 'u=b'/,
'Missing host part',
);
throws_ok (
sub { $dp->parse('h=foo,Z=moo') },
qr/Unknown DSN option 'Z' in 'h=foo,Z=moo'/,
'Extra key',
);
# #############################################################################
# Test parse_options().
# #############################################################################
my $o = new OptionParser(
description => 'parses command line options.',
dp => $dp,
);
$o->_parse_specs(
{ spec => 'defaults-file|F=s', desc => 'defaults file' },
{ spec => 'password|p=s', desc => 'password' },
{ spec => 'host|h=s', desc => 'host' },
{ spec => 'port|P=i', desc => 'port' },
{ spec => 'socket|S=s', desc => 'socket' },
{ spec => 'user|u=s', desc => 'user' },
);
@ARGV = qw(--host slave1 --user foo);
$o->get_opts();
is_deeply(
$dp->parse_options($o),
{
D => undef,
F => undef,
h => 'slave1',
p => undef,
P => undef,
S => undef,
u => 'foo',
A => undef,
},
'Parses DSN from OptionParser obj'
);
# #############################################################################
# Test copy().
# #############################################################################
push @$opts, { key => 't', desc => 'table' };
$dp = new DSNParser(opts => $opts);
my $dsn_1 = {
D => undef,
F => undef,
h => 'slave1',
p => 'p1',
P => '12345',
S => undef,
t => undef,
u => 'foo',
A => undef,
};
my $dsn_2 = {
D => 'test',
F => undef,
h => undef,
p => 'p2',
P => undef,
S => undef,
t => 'tbl',
u => undef,
A => undef,
};
is_deeply(
$dp->copy($dsn_1, $dsn_2),
{
D => 'test',
F => undef,
h => 'slave1',
p => 'p2',
P => '12345',
S => undef,
t => 'tbl',
u => 'foo',
A => undef,
},
'Copy DSN without overwriting destination'
);
is_deeply(
$dp->copy($dsn_1, $dsn_2, overwrite=>1),
{
D => 'test',
F => undef,
h => 'slave1',
p => 'p1',
P => '12345',
S => undef,
t => 'tbl',
u => 'foo',
A => undef,
},
'Copy DSN and overwrite destination'
);
pop @$opts; # Remove t part.
# #############################################################################
# Issue 93: DBI error messages can include full SQL
# #############################################################################
SKIP: {
skip 'ShowErrorStatement requires DBD::mysql 4.003', 1 unless $DBD::mysql::VERSION ge '4.003';
skip 'Cannot connect to sandbox master', 1 unless $dbh;
eval { $dbh->do('SELECT * FROM doesnt.exist WHERE foo = 1'); };
like(
$EVAL_ERROR,
qr/SELECT \* FROM doesnt.exist WHERE foo = 1/,
'Includes SQL in error message (issue 93)'
);
};
# #############################################################################
# Issue 597: mk-slave-prefetch ignores --set-vars
# #############################################################################
# This affects all scripts because prop() doesn't match what get_dbh() does.
SKIP: {
skip 'Cannot connect to sandbox master', 1 unless $dbh;
$dbh->do('SET @@global.wait_timeout=1');
# This dbh is going to timeout too during this test so close
# it now else we'll get an error.
$dbh->disconnect();
$dp = new DSNParser(opts => $opts);
$dp->prop('set-vars', { wait_timeout => { val => 1000, default => 1}});
$d = $dp->parse('h=127.0.0.1,P=12345,A=utf8,u=msandbox,p=msandbox');
my $dbh2 = $dp->get_dbh($dp->get_cxn_params($d), {mysql_use_result=>1});
sleep 2;
eval {
$dbh2->do('SELECT DATABASE()');
};
is(
$EVAL_ERROR,
'',
'SET vars (issue 597)'
);
$dbh2->disconnect();
# Have to reconnect $dbh since it timedout too.
$dbh = $dp->get_dbh($dp->get_cxn_params($d), {});
$dbh->do('SET @@global.wait_timeout=28800');
};
# #############################################################################
# Issue 801: DSNParser clobbers SQL_MODE
# #############################################################################
diag('Setting SQL mode globally on 12345');
my $old_mode = `/tmp/12345/use -ss -e 'select \@\@sql_mode'`;
chomp $old_mode;
diag("Old SQL mode: $old_mode");
diag(`/tmp/12345/use -e 'set global sql_mode=no_zero_date'`);
my $new_mode = `/tmp/12345/use -ss -e 'select \@\@sql_mode'`;
chomp $new_mode;
diag("New SQL mode: $new_mode");
my $dsn = $dp->parse('h=127.1,P=12345,u=msandbox,p=msandbox');
my $mdbh = $dp->get_dbh($dp->get_cxn_params($dsn), {});
my $row = $mdbh->selectrow_arrayref('select @@sql_mode');
is(
$row->[0],
'NO_AUTO_VALUE_ON_ZERO,NO_ZERO_DATE',
"Did not clobber server SQL mode"
);
diag(`/tmp/12345/use -e "set global sql_mode='$old_mode'"`);
$mdbh->disconnect;
# #############################################################################
# Passwords with commas don't work, expose part of password
# https://bugs.launchpad.net/percona-toolkit/+bug/886077
# #############################################################################
sub test_password_comma {
my ($dsn_string, $pass, $port, $name) = @_;
my $dsn = $dp->parse($dsn_string);
is_deeply(
$dsn,
{ u => 'a',
p => $pass,
S => undef,
h => undef,
P => $port,
F => undef,
D => undef,
A => undef,
},
"$name (bug 886077)"
) or diag(Dumper($dsn));
}
my @password_commas = (
['u=a,p=foo\,xxx,P=12345', 'foo,xxx', 12345, 'Pass with comma'],
['u=a,p=foo\,xxx', 'foo,xxx', undef, 'Pass with comma, last part'],
['u=a,p=foo\,,P=12345', 'foo,', 12345, 'Pass ends with comma'],
['u=a,p=foo\,', 'foo,', undef, 'Pass ends with comma, last part'],
['u=a,p=\,,P=12345', ',', 12345, 'Pass is a comma'],
['u=a,p=foo=bar,P=12345', 'foo=bar', 12345, '= in a pass'],
);
foreach my $password_comma ( @password_commas ) {
test_password_comma(@$password_comma);
}
sub test_password_comma_with_auto {
my ($dsn_string, $pass, $port, $name) = @_;
my $dsn = $dp->parse($dsn_string);
is_deeply(
$dsn,
{ u => undef,
p => $pass,
S => undef,
h => 'host',
P => $port,
F => undef,
D => undef,
A => undef,
},
"$name (bug 886077)"
) or diag(Dumper($dsn));
}
@password_commas = (
['host,p=a\,z,P=9', 'a,z', 9, 'Comma-pass with leading bareword host'],
['p=a\,z,P=9,host', 'a,z', 9, 'Comma-pass with trailing bareword host'],
['p=foo=bar,P=9,host', 'foo=bar', 9, '= in a pass with trailing bareword host'],
);
foreach my $password_comma ( @password_commas ) {
test_password_comma_with_auto(@$password_comma);
}
# #############################################################################
# Bug 984915: SQL calls after creating the dbh aren't checked
# #############################################################################
# Make sure to disconnect any lingering dbhs, since full_output will fork
# and then die, which will cause rollback warnings for connected dbhs.
$dbh->disconnect() if $dbh;
$dsn = $dp->parse('h=127.1,P=12345,u=msandbox,p=msandbox');
my @opts = $dp->get_cxn_params($dsn);
$opts[0] .= ";charset=garbage_eh";
my ($out, undef) = full_output(sub { $dp->get_dbh(@opts, {}) });
like(
$out,
qr/\QUnknown character set/,
"get_dbh dies with an unknown charset"
);
$dp->prop('set-vars', { time_zoen => { val => 'UTC' }});
$out = output(
sub {
my $dbh = $dp->get_dbh($dp->get_cxn_params($dsn), {});
$dbh->disconnect();
},
stderr => 1,
);
like(
$out,
qr/\QUnknown system variable 'time_zoen'/,
"get_dbh dies with an unknown system variable"
);
$dp->prop('set-vars', undef);
# #############################################################################
# Bug 1078887: Don't clobber the sql_mode set by the script with set-vars
# https://bugs.launchpad.net/percona-toolkit/+bug/1078887
# #############################################################################
$dp->prop('set-vars', { sql_mode => { val=>'ANSI_QUOTES' }});
my $sql_mode_dbh = $dp->get_dbh($dp->get_cxn_params($dsn), {});
my (undef, $sql_mode) = $sql_mode_dbh->selectrow_array(q{SHOW VARIABLES LIKE 'sql\_mode'});
like(
$sql_mode,
qr/NO_AUTO_VALUE_ON_ZERO/,
"Bug 1078887: --set-vars doesn't clover the sql_mode set by DSNParser"
);
$sql_mode_dbh->disconnect();
# #############################################################################
# LOAD DATA LOCAL INFILE broken in some platforms
# https://bugs.launchpad.net/percona-toolkit/+bug/821715
# #############################################################################
SKIP: {
skip "LOAD DATA LOCAL INFILE already works here", 1 if $can_load_data;
local $dsn->{L} = 1;
my $dbh = $dp->get_dbh( $dp->get_cxn_params( $dsn ) );
use File::Temp qw(tempfile);
my ($fh, $filename) = tempfile( 'load_data_test.XXXXXXX', TMPDIR => 1 );
print { $fh } "42\n";
close $fh or die "Cannot close $filename: $!";
$dbh->do(q{DROP DATABASE IF EXISTS bug_821715});
$dbh->do(q{CREATE DATABASE bug_821715});
$dbh->do(q{CREATE TABLE IF NOT EXISTS bug_821715.load_data (i int)});
eval {
$dbh->do(qq{LOAD DATA LOCAL INFILE '$filename' INTO TABLE bug_821715.load_data});
};
is(
$EVAL_ERROR,
'',
"Even though LOCAL INFILE is off by default, the dbhs returned by DSNParser can use it if L => 1"
);
unlink $filename;
$dbh->do(q{DROP DATABASE IF EXISTS bug_821715});
$dbh->disconnect();
}
is_deeply(
$dp->parse('h=f000::1,P=12345,u=msandbox,p=msandbox'),
{ u => 'msandbox',
p => 'msandbox',
S => undef,
h => 'f000::1',
P => '12345',
F => undef,
D => undef,
A => undef,
},
'IPV6 support'
);
# #############################################################################
# Done.
# #############################################################################
done_testing;