
# Copyright (c) 2021-2025, PostgreSQL Global Development Group

#
# pgbench tests which do not need a server
#

use strict;
use warnings FATAL => 'all';

use PostgreSQL::Test::Utils;
use Test::More;

# create a directory for scripts
my $testname = $0;
$testname =~ s,.*/,,;
$testname =~ s/\.pl$//;

my $testdir = "$PostgreSQL::Test::Utils::tmp_check/t_${testname}_stuff";
mkdir $testdir
  or BAIL_OUT("could not create test directory \"${testdir}\": $!");

# invoke pgbench
sub pgbench
{
	local $Test::Builder::Level = $Test::Builder::Level + 1;

	my ($opts, $stat, $out, $err, $name) = @_;
	command_checks_all([ 'pgbench', split(/\s+/, $opts) ],
		$stat, $out, $err, $name);
	return;
}

# invoke pgbench with scripts
sub pgbench_scripts
{
	local $Test::Builder::Level = $Test::Builder::Level + 1;

	my ($opts, $stat, $out, $err, $name, $files) = @_;
	my @cmd = ('pgbench', split /\s+/, $opts);
	if (defined $files)
	{
		for my $fn (sort keys %$files)
		{
			my $filename = $testdir . '/' . $fn;

			# cleanup file weight if any
			$filename =~ s/\@\d+$//;

			# cleanup from prior runs
			unlink $filename;
			append_to_file($filename, $$files{$fn});
			push @cmd, '--file' => $filename;
		}
	}
	command_checks_all(\@cmd, $stat, $out, $err, $name);
	return;
}

#
# Option various errors
#

my @options = (

	# name, options, stderr checks
	[
		'bad option',
		'-h home -p 5432 -U calvin ---debug --bad-option',
		[qr{--help.*more information}]
	],
	[
		'no file',
		'-f no-such-file',
		[qr{could not open file "no-such-file":}]
	],
	[
		'no builtin',
		'-b no-such-builtin',
		[qr{no builtin script .* "no-such-builtin"}]
	],
	[
		'invalid weight',
		'--builtin=select-only@one',
		[qr{invalid weight specification: \@one}]
	],
	[
		'invalid weight',
		'-b select-only@-1',
		[qr{weight spec.* out of range .*: -1}]
	],
	[ 'too many scripts', '-S ' x 129, [qr{at most 128 SQL scripts}] ],
	[
		'bad #clients', '-c three',
		[qr{invalid value "three" for option -c/--clients}]
	],
	[
		'bad #threads', '-j eleven',
		[qr{invalid value "eleven" for option -j/--jobs}]
	],
	[
		'bad scale', '-i -s two',
		[qr{invalid value "two" for option -s/--scale}]
	],
	[
		'invalid #transactions',
		'-t zil', [qr{invalid value "zil" for option -t/--transactions}]
	],
	[
		'invalid duration',
		'-T ten', [qr{invalid value "ten" for option -T/--time}]
	],
	[
		'-t XOR -T',
		'-N -l --aggregate-interval=5 --log-prefix=notused -t 1000 -T 1',
		[qr{specify either }]
	],
	[
		'-T XOR -t',
		'-P 1 --progress-timestamp -l --sampling-rate=0.001 -T 10 -t 1000',
		[qr{specify either }]
	],
	[ 'bad variable', '--define foobla', [qr{invalid variable definition}] ],
	[ 'invalid fillfactor', '-F 1', [qr{-F/--fillfactor must be in range}] ],
	[ 'invalid query mode', '-M no-such-mode', [qr{invalid query mode}] ],
	[
		'invalid progress', '--progress=0',
		[qr{-P/--progress must be in range}]
	],
	[ 'invalid rate', '--rate=0.0', [qr{invalid rate limit}] ],
	[ 'invalid latency', '--latency-limit=0.0', [qr{invalid latency limit}] ],
	[
		'invalid sampling rate', '--sampling-rate=0',
		[qr{invalid sampling rate}]
	],
	[
		'invalid aggregate interval',
		'--aggregate-interval=-3',
		[qr{--aggregate-interval must be in range}]
	],
	[
		'weight zero',
		'-b se@0 -b si@0 -b tpcb@0',
		[qr{weight must not be zero}]
	],
	[ 'init vs run', '-i -S', [qr{cannot be used in initialization}] ],
	[ 'run vs init', '-S -F 90', [qr{cannot be used in benchmarking}] ],
	[ 'ambiguous builtin', '-b s', [qr{ambiguous}] ],
	[
		'--progress-timestamp => --progress', '--progress-timestamp',
		[qr{allowed only under}]
	],
	[
		'-I without init option',
		'-I dtg',
		[qr{cannot be used in benchmarking mode}]
	],
	[
		'invalid init step',
		'-i -I dta',
		[
			qr{unrecognized initialization step},
			qr{Allowed step characters are}
		]
	],
	[
		'bad random seed',
		'--random-seed=one',
		[
			qr{unrecognized random seed option "one"},
			qr{Expecting an unsigned integer, "time" or "rand"},
			qr{error while setting random seed from --random-seed option}
		]
	],
	[
		'bad partition method',
		'-i --partition-method=BAD',
		[ qr{"range"}, qr{"hash"}, qr{"BAD"} ]
	],
	[
		'bad partition number',
		'-i --partitions -1',
		[qr{--partitions must be in range}]
	],
	[
		'partition method without partitioning',
		'-i --partition-method=hash',
		[qr{partition-method requires greater than zero --partitions}]
	],
	[
		'bad maximum number of tries',
		'--max-tries -10',
		[qr{invalid number of maximum tries: "-10"}]
	],
	[
		'an infinite number of tries',
		'--max-tries 0',
		[
			qr{an unlimited number of transaction tries can only be used with --latency-limit or a duration}
		]
	],

	# logging sub-options
	[
		'sampling => log', '--sampling-rate=0.01',
		[qr{log sampling .* only when}]
	],
	[
		'sampling XOR aggregate',
		'-l --sampling-rate=0.1 --aggregate-interval=3',
		[qr{sampling .* aggregation .* cannot be used at the same time}]
	],
	[
		'aggregate => log', '--aggregate-interval=3',
		[qr{aggregation .* only when}]
	],
	[ 'log-prefix => log', '--log-prefix=x', [qr{prefix .* only when}] ],
	[
		'duration & aggregation',
		'-l -T 1 --aggregate-interval=3',
		[qr{aggr.* not be higher}]
	],
	[
		'duration % aggregation',
		'-l -T 5 --aggregate-interval=3',
		[qr{multiple}]
	],);

for my $o (@options)
{
	my ($name, $opts, $err_checks) = @$o;
	pgbench($opts, 1, [qr{^$}], $err_checks,
		'pgbench option error: ' . $name);
}

# Help
pgbench(
	'--help', 0,
	[
		qr{benchmarking tool for PostgreSQL},
		qr{Usage},
		qr{Initialization options:},
		qr{Common options:},
		qr{Report bugs to}
	],
	[qr{^$}],
	'pgbench help');

# Version
pgbench('-V', 0, [qr{^pgbench .PostgreSQL. }], [qr{^$}], 'pgbench version');

# list of builtins
pgbench(
	'-b list',
	0,
	[qr{^$}],
	[
		qr{Available builtin scripts:}, qr{tpcb-like},
		qr{simple-update}, qr{select-only}
	],
	'pgbench builtin list');

# builtin listing
pgbench(
	'--show-script se',
	0,
	[qr{^$}],
	[
		qr{select-only: }, qr{SELECT abalance FROM pgbench_accounts WHERE},
		qr{(?!UPDATE)}, qr{(?!INSERT)}
	],
	'pgbench builtin listing');

my @script_tests = (

	# name, err, { file => contents }
	[
		'missing endif',
		[qr{\\if without matching \\endif}],
		{ 'if-noendif.sql' => '\if 1' }
	],
	[
		'missing if on elif',
		[qr{\\elif without matching \\if}],
		{ 'elif-noif.sql' => '\elif 1' }
	],
	[
		'missing if on else',
		[qr{\\else without matching \\if}],
		{ 'else-noif.sql' => '\else' }
	],
	[
		'missing if on endif',
		[qr{\\endif without matching \\if}],
		{ 'endif-noif.sql' => '\endif' }
	],
	[
		'elif after else',
		[qr{\\elif after \\else}],
		{ 'else-elif.sql' => "\\if 1\n\\else\n\\elif 0\n\\endif" }
	],
	[
		'else after else',
		[qr{\\else after \\else}],
		{ 'else-else.sql' => "\\if 1\n\\else\n\\else\n\\endif" }
	],
	[
		'if syntax error',
		[qr{syntax error in command "if"}],
		{ 'if-bad.sql' => "\\if\n\\endif\n" }
	],
	[
		'elif syntax error',
		[qr{syntax error in command "elif"}],
		{ 'elif-bad.sql' => "\\if 0\n\\elif +\n\\endif\n" }
	],
	[
		'else syntax error',
		[qr{unexpected argument in command "else"}],
		{ 'else-bad.sql' => "\\if 0\n\\else BAD\n\\endif\n" }
	],
	[
		'endif syntax error',
		[qr{unexpected argument in command "endif"}],
		{ 'endif-bad.sql' => "\\if 0\n\\endif BAD\n" }
	],
	[
		'not enough arguments for least',
		[qr{at least one argument expected \(least\)}],
		{ 'bad-least.sql' => "\\set i least()\n" }
	],
	[
		'not enough arguments for greatest',
		[qr{at least one argument expected \(greatest\)}],
		{ 'bad-greatest.sql' => "\\set i greatest()\n" }
	],
	[
		'not enough arguments for hash',
		[qr{unexpected number of arguments \(hash\)}],
		{ 'bad-hash-1.sql' => "\\set i hash()\n" }
	],
	[
		'too many arguments for hash',
		[qr{unexpected number of arguments \(hash\)}],
		{ 'bad-hash-2.sql' => "\\set i hash(1,2,3)\n" }
	],
	# overflow
	[
		'bigint overflow 1',
		[qr{bigint constant overflow}],
		{ 'overflow-1.sql' => "\\set i 100000000000000000000\n" }
	],
	[
		'double overflow 2',
		[qr{double constant overflow}],
		{ 'overflow-2.sql' => "\\set d 1.0E309\n" }
	],
	[
		'double overflow 3',
		[qr{double constant overflow}],
		{ 'overflow-3.sql' => "\\set d .1E310\n" }
	],
	[
		'set i',
		[ qr{set i 1 }, qr{\^ error found here} ],
		{ 'set_i_op' => "\\set i 1 +\n" }
	],
	[
		'not enough arguments to permute',
		[qr{unexpected number of arguments \(permute\)}],
		{ 'bad-permute-1.sql' => "\\set i permute(1)\n" }
	],
	[
		'too many arguments to permute',
		[qr{unexpected number of arguments \(permute\)}],
		{ 'bad-permute-2.sql' => "\\set i permute(1, 2, 3, 4)\n" }
	],);

for my $t (@script_tests)
{
	my ($name, $err, $files) = @$t;
	pgbench_scripts('', 1, [qr{^$}], $err, 'pgbench option error: ' . $name,
		$files);
}

done_testing();
