As for me, I would do expr_scanner_chomp_substring(PsqlScanState, int, int&);
that changes end_offset as desired...

Why not.

And use it instead of end_offset = expr_scanner_offset(sstate) - 1;

I removed these?

The second issue: you are removing all trailing \n and \r. I think you should
remove only one \n at the end of the string, and one \r before \n if there was
one.

So chomp one eol.

Is the attached version better to your test?

--
Fabien.
diff --git a/src/bin/pgbench/exprscan.l b/src/bin/pgbench/exprscan.l
index dc1367b..0c802af 100644
--- a/src/bin/pgbench/exprscan.l
+++ b/src/bin/pgbench/exprscan.l
@@ -360,6 +360,23 @@ expr_scanner_get_substring(PsqlScanState state,
 }
 
 /*
+ * get current expression line without one ending newline
+ */
+char *
+expr_scanner_chomp_substring(PsqlScanState state, int start_offset, int end_offset)
+{
+	const char *p = state->scanbuf;
+	/* chomp eol */
+	if (end_offset > start_offset && p[end_offset] == '\n')
+	{
+		end_offset--;
+		if (end_offset > start_offset && p[end_offset] == '\r')
+			end_offset--;
+	}
+	return expr_scanner_get_substring(state, start_offset, end_offset + 1);
+}
+
+/*
  * Get the line number associated with the given string offset
  * (which must not be past the end of where we've lexed to).
  */
diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 14aa587..0f4e125 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -229,7 +229,7 @@ typedef struct SimpleStats
 typedef struct StatsData
 {
 	time_t		start_time;		/* interval start time, for aggregates */
-	int64		cnt;			/* number of transactions */
+	int64		cnt;			/* number of transactions, including skipped */
 	int64		skipped;		/* number of transactions skipped under --rate
 								 * and --latency-limit */
 	SimpleStats latency;
@@ -329,7 +329,7 @@ typedef struct
 	bool		prepared[MAX_SCRIPTS];	/* whether client prepared the script */
 
 	/* per client collected stats */
-	int64		cnt;			/* transaction count */
+	int64		cnt;			/* client transaction count, for -t */
 	int			ecnt;			/* error count */
 } CState;
 
@@ -2045,7 +2045,8 @@ doCustom(TState *thread, CState *st, StatsData *agg)
 					if (INSTR_TIME_IS_ZERO(now))
 						INSTR_TIME_SET_CURRENT(now);
 					now_us = INSTR_TIME_GET_MICROSEC(now);
-					while (thread->throttle_trigger < now_us - latency_limit)
+					while (thread->throttle_trigger < now_us - latency_limit &&
+						   (nxacts <= 0 || st->cnt < nxacts)) /* with -t, do not overshoot */
 					{
 						processXactStats(thread, st, &now, true, agg);
 						/* next rendez-vous */
@@ -2053,6 +2054,12 @@ doCustom(TState *thread, CState *st, StatsData *agg)
 						thread->throttle_trigger += wait;
 						st->txn_scheduled = thread->throttle_trigger;
 					}
+
+					if (nxacts > 0 && st->cnt >= nxacts)
+					{
+						st->state = CSTATE_FINISHED;
+						break;
+					}
 				}
 
 				st->state = CSTATE_THROTTLE;
@@ -2364,15 +2371,8 @@ doCustom(TState *thread, CState *st, StatsData *agg)
 				 */
 			case CSTATE_END_TX:
 
-				/*
-				 * transaction finished: calculate latency and log the
-				 * transaction
-				 */
-				if (progress || throttle_delay || latency_limit ||
-					per_script_stats || use_log)
-					processXactStats(thread, st, &now, false, agg);
-				else
-					thread->stats.cnt++;
+				/* transaction finished: calculate latency and do log */
+				processXactStats(thread, st, &now, false, agg);
 
 				if (is_connect)
 				{
@@ -2381,7 +2381,6 @@ doCustom(TState *thread, CState *st, StatsData *agg)
 					INSTR_TIME_SET_ZERO(now);
 				}
 
-				++st->cnt;
 				if ((st->cnt >= nxacts && duration <= 0) || timer_exceeded)
 				{
 					/* exit success */
@@ -2519,17 +2518,20 @@ processXactStats(TState *thread, CState *st, instr_time *now,
 {
 	double		latency = 0.0,
 				lag = 0.0;
+	bool		detailed = progress || throttle_delay || latency_limit ||
+						per_script_stats || use_log;
 
-	if ((!skipped) && INSTR_TIME_IS_ZERO(*now))
-		INSTR_TIME_SET_CURRENT(*now);
-
-	if (!skipped)
+	if (detailed && !skipped)
 	{
+		if (INSTR_TIME_IS_ZERO(*now))
+			INSTR_TIME_SET_CURRENT(*now);
+
 		/* compute latency & lag */
 		latency = INSTR_TIME_GET_MICROSEC(*now) - st->txn_scheduled;
 		lag = INSTR_TIME_GET_MICROSEC(st->txn_begin) - st->txn_scheduled;
 	}
 
+	/* detailed thread stats */
 	if (progress || throttle_delay || latency_limit)
 	{
 		accumStats(&thread->stats, skipped, latency, lag);
@@ -2539,7 +2541,13 @@ processXactStats(TState *thread, CState *st, instr_time *now,
 			thread->latency_late++;
 	}
 	else
+	{
+		/* no detailed stats, just count */
 		thread->stats.cnt++;
+	}
+
+	/* client stat is just counting */
+	st->cnt ++;
 
 	if (use_log)
 		doLog(thread, st, agg, skipped, latency, lag);
@@ -3030,8 +3038,7 @@ process_backslash_command(PsqlScanState sstate, const char *source)
 	PQExpBufferData word_buf;
 	int			word_offset;
 	int			offsets[MAX_ARGS];		/* offsets of argument words */
-	int			start_offset,
-				end_offset;
+	int			start_offset;
 	int			lineno;
 	int			j;
 
@@ -3085,13 +3092,10 @@ process_backslash_command(PsqlScanState sstate, const char *source)
 
 		my_command->expr = expr_parse_result;
 
-		/* Get location of the ending newline */
-		end_offset = expr_scanner_offset(sstate) - 1;
-
-		/* Save line */
-		my_command->line = expr_scanner_get_substring(sstate,
-													  start_offset,
-													  end_offset);
+		/* Save line (which may include continuations) */
+		my_command->line = expr_scanner_chomp_substring(sstate,
+														start_offset,
+														expr_scanner_offset(sstate));
 
 		expr_scanner_finish(yyscanner);
 
@@ -3112,13 +3116,10 @@ process_backslash_command(PsqlScanState sstate, const char *source)
 		my_command->argc++;
 	}
 
-	/* Get location of the ending newline */
-	end_offset = expr_scanner_offset(sstate) - 1;
-
 	/* Save line */
-	my_command->line = expr_scanner_get_substring(sstate,
-												  start_offset,
-												  end_offset);
+	my_command->line = expr_scanner_chomp_substring(sstate,
+													start_offset,
+													expr_scanner_offset(sstate));
 
 	if (pg_strcasecmp(my_command->argv[0], "sleep") == 0)
 	{
@@ -3509,7 +3510,7 @@ printResults(TState *threads, StatsData *total, instr_time total_time,
 	{
 		printf("number of transactions per client: %d\n", nxacts);
 		printf("number of transactions actually processed: " INT64_FORMAT "/%d\n",
-			   total->cnt, nxacts * nclients);
+			   total->cnt - total->skipped, nxacts * nclients);
 	}
 	else
 	{
@@ -3525,12 +3526,12 @@ printResults(TState *threads, StatsData *total, instr_time total_time,
 	if (throttle_delay && latency_limit)
 		printf("number of transactions skipped: " INT64_FORMAT " (%.3f %%)\n",
 			   total->skipped,
-			   100.0 * total->skipped / (total->skipped + total->cnt));
+			   100.0 * total->skipped / total->cnt);
 
 	if (latency_limit)
 		printf("number of transactions above the %.1f ms latency limit: %d (%.3f %%)\n",
 			   latency_limit / 1000.0, latency_late,
-			   100.0 * latency_late / (total->skipped + total->cnt));
+			   100.0 * latency_late / total->cnt);
 
 	if (throttle_delay || progress || latency_limit)
 		printSimpleStats("latency", &total->latency);
@@ -3580,7 +3581,7 @@ printResults(TState *threads, StatsData *total, instr_time total_time,
 				printf(" - number of transactions skipped: " INT64_FORMAT " (%.3f%%)\n",
 					   sql_script[i].stats.skipped,
 					   100.0 * sql_script[i].stats.skipped /
-					(sql_script[i].stats.skipped + sql_script[i].stats.cnt));
+					   sql_script[i].stats.cnt);
 
 			if (num_scripts > 1)
 				printSimpleStats(" - latency", &sql_script[i].stats.latency);
@@ -4106,6 +4107,12 @@ main(int argc, char **argv)
 		exit(1);
 	}
 
+	if (progress_timestamp && progress == 0)
+	{
+		fprintf(stderr, "--progress-timestamp is allowed only under --progress\n");
+		exit(1);
+	}
+
 	/*
 	 * save main process id in the global variable because process id will be
 	 * changed after fork.
diff --git a/src/bin/pgbench/pgbench.h b/src/bin/pgbench/pgbench.h
index 38b3af5..09ea4b8 100644
--- a/src/bin/pgbench/pgbench.h
+++ b/src/bin/pgbench/pgbench.h
@@ -129,6 +129,8 @@ extern void expr_scanner_finish(yyscan_t yyscanner);
 extern int	expr_scanner_offset(PsqlScanState state);
 extern char *expr_scanner_get_substring(PsqlScanState state,
 						   int start_offset, int end_offset);
+extern char *expr_scanner_chomp_substring(PsqlScanState state,
+								   int start_offset, int end_offset);
 extern int	expr_scanner_get_lineno(PsqlScanState state, int offset);
 
 extern void syntax_error(const char *source, int lineno, const char *line,
diff --git a/src/bin/pgbench/t/001_pgbench.pl b/src/bin/pgbench/t/001_pgbench.pl
deleted file mode 100644
index 34d686e..0000000
--- a/src/bin/pgbench/t/001_pgbench.pl
+++ /dev/null
@@ -1,25 +0,0 @@
-use strict;
-use warnings;
-
-use PostgresNode;
-use TestLib;
-use Test::More tests => 3;
-
-# Test concurrent insertion into table with UNIQUE oid column.  DDL expects
-# GetNewOidWithIndex() to successfully avoid violating uniqueness for indexes
-# like pg_class_oid_index and pg_proc_oid_index.  This indirectly exercises
-# LWLock and spinlock concurrency.  This test makes a 5-MiB table.
-my $node = get_new_node('main');
-$node->init;
-$node->start;
-$node->safe_psql('postgres',
-	    'CREATE UNLOGGED TABLE oid_tbl () WITH OIDS; '
-	  . 'ALTER TABLE oid_tbl ADD UNIQUE (oid);');
-my $script = $node->basedir . '/pgbench_script';
-append_to_file($script,
-	'INSERT INTO oid_tbl SELECT FROM generate_series(1,1000);');
-$node->command_like(
-	[   qw(pgbench --no-vacuum --client=5 --protocol=prepared
-		  --transactions=25 --file), $script ],
-	qr{processed: 125/125},
-	'concurrent OID generation');
diff --git a/src/bin/pgbench/t/001_pgbench_with_server.pl b/src/bin/pgbench/t/001_pgbench_with_server.pl
new file mode 100644
index 0000000..b8d4295
--- /dev/null
+++ b/src/bin/pgbench/t/001_pgbench_with_server.pl
@@ -0,0 +1,450 @@
+use strict;
+use warnings;
+
+use PostgresNode;
+use TestLib;
+use Test::More tests => 244;
+
+# start a pgbench specific server
+my $node = get_new_node('main');
+$node->init;
+$node->start;
+
+# invoke pgbench
+sub pgbench
+{
+	my ($opts, $stat, $out, $err, $name, $files) = @_;
+	my @cmd = ('pgbench', split /\s+/, $opts);
+	my @filenames = ();
+	if (defined $files)
+	{
+		# note: files are ordered for determinism
+		for my $fn (sort keys %$files)
+		{
+			my $filename = $node->basedir . '/' . $fn;
+			push @cmd, '-f', $filename;
+			# cleanup file weight
+			$filename =~ s/\@\d+$//;
+			#push @filenames, $filename;
+			append_to_file($filename, $$files{$fn});
+		}
+	}
+	$node->command_checks_all(\@cmd, $stat, $out, $err, $name);
+	# cleanup?
+	#unlink @filenames or die "cannot unlink files (@filenames): $!";
+}
+
+# Test concurrent insertion into table with UNIQUE oid column.  DDL expects
+# GetNewOidWithIndex() to successfully avoid violating uniqueness for indexes
+# like pg_class_oid_index and pg_proc_oid_index.  This indirectly exercises
+# LWLock and spinlock concurrency.  This test makes a 5-MiB table.
+
+$node->safe_psql('postgres',
+	    'CREATE UNLOGGED TABLE oid_tbl () WITH OIDS; '
+	  . 'ALTER TABLE oid_tbl ADD UNIQUE (oid);');
+
+# 3 checks
+pgbench(
+  '--no-vacuum --client=5 --protocol=prepared --transactions=25',
+  0, qr{processed: 125/125}, qr{^$}, 'concurrency OID generation',
+  { '001_pgbench_concurrent_oid_generation' =>
+    'INSERT INTO oid_tbl SELECT FROM generate_series(1,1000);' });
+
+# cleanup
+$node->safe_psql('postgres', 'DROP TABLE oid_tbl;');
+
+# Trigger various connection errors
+pgbench(
+  'no-such-database', 1, qr{^$},
+  [ qr{connection to database "no-such-database" failed},
+    qr{FATAL:  database "no-such-database" does not exist} ],
+  'no such database');
+
+pgbench(
+  '-U no-such-user template0', 1, qr{^$},
+  [ qr{connection to database "template0" failed},
+    qr{FATAL:  role "no-such-user" does not exist} ],
+  'no such user');
+
+pgbench(
+  '-h no-such-host.postgresql.org', 1, qr{^$},
+  [ qr{connection to database "postgres" failed},
+    # actual message may vary:
+    # - Name or service not knowni
+    # - Temporary failure in name resolution
+    qr{could not translate host name "no-such-host.postgresql.org" to address: } ],
+  'no such host');
+
+pgbench(
+  '-S -t 1',
+  1, qr{^$}, qr{Perhaps you need to do initialization},
+  'run without init');
+
+# Initialize pgbench tables scale 1
+pgbench(
+  '-i',
+  0, qr{^$},
+  [ qr{creating tables}, qr{vacuum}, qr{set primary keys}, qr{done\.} ],
+  'pgbench scale 1 initialization',
+);
+
+# Again, with all possible options
+pgbench(
+  # unlogged => faster test
+  '--initialize --scale=1 --unlogged --fillfactor=98 --foreign-keys --quiet' .
+  ' --tablespace=pg_default --index-tablespace=pg_default',
+  0, qr{^$},
+  [ qr{creating tables}, qr{vacuum}, qr{set primary keys},
+    qr{set foreign keys}, qr{done\.} ],
+  'pgbench scale 1 initialization');
+
+# Run all builtins for a few transactions: 20 checks
+pgbench(
+  '--transactions=5 -Dfoo=bla --client=2 --protocol=simple --builtin=t' .
+  ' --connect -n -v -n',
+  0,
+  [ qr{builtin: TPC-B}, qr{clients: 2\b}, qr{processed: 10/10},
+    qr{mode: simple} ],
+  qr{^$}, 'pgbench tpcb-like');
+
+pgbench(
+  '--transactions=20 --client=5 -M extended --builtin=si -C --no-vacuum -s 1',
+  0,
+  [ qr{builtin: simple update}, qr{clients: 5\b}, qr{threads: 1\b},
+    qr{processed: 100/100}, qr{mode: extended} ],
+  qr{scale option ignored},
+  'pgbench simple update');
+
+pgbench(
+  '-t 100 -c 7 -M prepared -b se --debug',
+  0,
+  [ qr{builtin: select only}, qr{clients: 7\b}, qr{threads: 1\b},
+    qr{processed: 700/700}, qr{mode: prepared} ],
+  [ qr{vacuum}, qr{client 0}, qr{client 1}, qr{sending}, qr{receiving},
+    qr{executing} ],
+  'pgbench select only');
+
+# run custom scripts: 8 checks
+pgbench(
+  '-t 100 -c 1 -j 2 -M prepared -n',
+  0,
+  [ qr{type: multiple scripts}, qr{mode: prepared},
+    qr{script 1: .*/001_pgbench_custom_script_1}, qr{weight: 2},
+    qr{script 2: .*/001_pgbench_custom_script_2}, qr{weight: 1},
+    qr{processed: 100/100} ],
+  qr{^$},
+  'pgbench custom scripts',
+  { '001_pgbench_custom_script_1@1' => q{-- select only
+\set aid random(1, :scale * 100000)
+SELECT abalance::INTEGER AS balance
+  FROM pgbench_accounts
+  WHERE aid=:aid;
+},
+    '001_pgbench_custom_script_2@2' => q{-- special variables
+BEGIN;
+\set foo 1
+-- cast are needed for typing under -M prepared
+SELECT :foo::INT + :scale::INT * :client_id::INT AS bla;
+COMMIT;
+} }
+);
+
+pgbench(
+  '-n -t 10 -c 1 -M simple',
+  0,
+  [ qr{type: .*/001_pgbench_custom_script_3}, qr{processed: 10/10},
+    qr{mode: simple} ],
+  qr{^$},
+  'pgbench custom script',
+  { '001_pgbench_custom_script_3' => q{-- select only variant
+\set aid random(1, :scale * 100000)
+BEGIN;
+SELECT abalance::INTEGER AS balance
+  FROM pgbench_accounts
+  WHERE aid=:aid;
+COMMIT;
+}}
+);
+
+pgbench(
+  '-n -t 10 -c 2 -M extended',
+  0,
+  [ qr{type: .*/001_pgbench_custom_script_4}, qr{processed: 20/20},
+    qr{mode: extended} ],
+  qr{^$}, 'pgbench custom script',
+  { '001_pgbench_custom_script_4' => q{-- select only variant
+\set aid random(1, :scale * 100000)
+BEGIN;
+SELECT abalance::INTEGER AS balance
+  FROM pgbench_accounts
+  WHERE aid=:aid;
+COMMIT;
+}}
+);
+
+# test expressions: 23 checks
+pgbench(
+  '-t 1 -Dfoo=-10.1 -Dbla=false -Di=+3 -Dminint=-9223372036854775808',
+  0,
+  [ qr{type: .*/001_pgbench_expressions}, qr{processed: 1/1} ],
+  [ qr{command=4.: int 4\b},
+    qr{command=5.: int 5\b},
+    qr{command=6.: int 6\b},
+    qr{command=7.: int 7\b},
+    qr{command=8.: int 8\b},
+    qr{command=9.: int 9\b},
+    qr{command=10.: int 10\b},
+    qr{command=11.: int 11\b},
+    qr{command=12.: int 12\b},
+    qr{command=13.: double 13\b},
+    qr{command=14.: double 14\b},
+    qr{command=15.: double 15\b},
+    qr{command=16.: double 16\b},
+    qr{command=17.: double 17\b},
+    qr{command=18.: double 18\b},
+    qr{command=19.: double 19\b},
+    qr{command=20.: double 20\b},
+    qr{command=21.: double -?nan\b},
+    qr{command=22.: double inf\b},
+    qr{command=23.: double -inf\b},
+    qr{command=24.: int 9223372036854775807\b},
+  ],
+  'pgbench expressions',
+  { '001_pgbench_expressions' => q{-- integer functions
+\set i1 debug(random(1, 100))
+\set i2 debug(random_exponential(1, 100, 10.0))
+\set i3 debug(random_gaussian(1, 100, 10.0))
+\set i4 debug(abs(-4))
+\set i5 debug(greatest(5, 4, 3, 2))
+\set i6 debug(11 + least(-5, -4, -3, -2))
+\set i7 debug(int(7.3))
+-- integer operators
+\set i8 debug(17 / 5 + 5)
+\set i9 debug(- (3 * 4 - 3) / -1 + 3 % -1)
+\set ia debug(10 + (0 + 0 * 0 - 0 / 1))
+\set ib debug(:ia + :scale)
+\set ic debug(64 % 13)
+-- double functions
+\set d1 debug(sqrt(3.0) * abs(-0.8E1))
+\set d2 debug(double(1 + 1) * 7)
+\set pi debug(pi() * 4.9)
+\set d4 debug(greatest(4, 2, -1.17) * 4.0)
+\set d5 debug(least(-5.18, .0E0, 1.0/0) * -3.3)
+-- double operators
+\set d6 debug((0.5 * 12.1 - 0.05) * (31.0 / 10))
+\set d7 debug(11.1 + 7.9)
+\set d8 debug(:foo * -2)
+-- special values
+\set nan debug(0.0 / 0.0)
+\set pin debug(1.0 / 0.0)
+\set nin debug(-1.0 / 0.0)
+\set maxint debug(:minint - 1)
+-- reset a variable
+\set i1 0
+}});
+
+# backslash commands: 4 checks
+pgbench(
+  '-t 1',
+  0,
+  [ qr{type: .*/001_pgbench_backslash_commands}, qr{processed: 1/1},
+    qr{shell-echo-output} ],
+  qr{command=8.: int 2\b},
+  'pgbench backslash commands',
+  { '001_pgbench_backslash_commands' => q{-- run set
+\set zero 0
+\set one 1.0
+-- sleep
+\sleep :one ms
+\sleep 100 us
+\sleep 0 s
+\sleep :zero
+-- setshell and continuation
+\setshell two\
+  expr \
+    1 + :one
+\set n debug(:two)
+-- shell
+\shell echo shell-echo-output
+}});
+
+# trigger many expression errors
+my @errors = (
+  # [ test name, script number, status, stderr match ]
+  # SQL
+  [ 'sql syntax error', 0,
+    [ qr{ERROR:  syntax error}, qr{prepared statement .* does not exist} ],
+    q{-- SQL syntax error
+    SELECT 1 + ;
+}],
+  [ 'sql too many args', 1, qr{statement has too many arguments.*\b9\b},
+    q{-- MAX_ARGS=10 for prepared
+\set i 0
+SELECT LEAST(:i, :i, :i, :i, :i, :i, :i, :i, :i, :i, :i);
+}],
+  # SHELL
+  [ 'shell bad command', 0, qr{meta-command 'shell' failed},
+    q{\shell no-such-command} ],
+  [ 'shell undefined variable', 0,
+    qr{undefined variable ":nosuchvariable"},
+    q{-- undefined variable in shell
+\shell echo ::foo :nosuchvariable
+}],
+  [ 'shell missing command', 1, qr{missing command }, q{\shell} ],
+  [ 'shell too many args', 1, qr{too many arguments in command "shell"},
+    q{-- 257 arguments to \shell
+\shell echo \
+ 0 1 2 3 4 5 6 7 8 9 A B C D E F \
+ 0 1 2 3 4 5 6 7 8 9 A B C D E F \
+ 0 1 2 3 4 5 6 7 8 9 A B C D E F \
+ 0 1 2 3 4 5 6 7 8 9 A B C D E F \
+ 0 1 2 3 4 5 6 7 8 9 A B C D E F \
+ 0 1 2 3 4 5 6 7 8 9 A B C D E F \
+ 0 1 2 3 4 5 6 7 8 9 A B C D E F \
+ 0 1 2 3 4 5 6 7 8 9 A B C D E F \
+ 0 1 2 3 4 5 6 7 8 9 A B C D E F \
+ 0 1 2 3 4 5 6 7 8 9 A B C D E F \
+ 0 1 2 3 4 5 6 7 8 9 A B C D E F \
+ 0 1 2 3 4 5 6 7 8 9 A B C D E F \
+ 0 1 2 3 4 5 6 7 8 9 A B C D E F \
+ 0 1 2 3 4 5 6 7 8 9 A B C D E F \
+ 0 1 2 3 4 5 6 7 8 9 A B C D E F \
+ 0 1 2 3 4 5 6 7 8 9 A B C D E F
+} ],
+  # SET
+  [ 'set syntax error', 1, qr{syntax error in command "set"},
+    q{\set i 1 +} ],
+  [ 'set no such function', 1, qr{unexpected function name},
+    q{\set i noSuchFunction()} ],
+  [ 'set invalid variable name', 0, qr{invalid variable name},
+    q{\set . 1} ],
+  [ 'set int overflow', 0, qr{double to int overflow for 100},
+    q{\set i int(1E32)} ],
+  [ 'set division by zero', 0, qr{division by zero},
+    q{\set i 1/0} ],
+  [ 'set bigint out of range', 0, qr{bigint out of range},
+    q{\set i 9223372036854775808 / -1} ],
+  [ 'set undefined variable', 0, qr{undefined variable "nosuchvariable"},
+    q{\set i :nosuchvariable} ],
+  [ 'set unexpected char', 1, qr{unexpected character .;.},
+    q{\set i ;} ],
+  [ 'set too many args', 0, qr{too many function arguments},
+    q{\set i least(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16)} ],
+  [ 'set empty random range', 0, qr{empty range given to random},
+    q{\set i random(5,3)} ],
+  [ 'set random range too large', 0, qr{random range is too large},
+    q{\set i random(-9223372036854775808, 9223372036854775807)} ],
+  [ 'set gaussian param too small', 0, qr{gaussian param.* at least 2},
+    q{\set i random_gaussian(0, 10, 1.0)} ],
+  [ 'set exponential param > 0', 0, qr{exponential parameter must be greater },
+    q{\set i random_exponential(0, 10, 0.0)} ],
+  [ 'set non numeric value', 0, qr{malformed variable "foo" value: "bla"},
+    q{\set i :foo + 1} ],
+  [ 'set no expression', 1, qr{syntax error}, q{\set i} ],
+  [ 'set missing argument', 1, qr{missing argument}, q{\set} ],
+  # SETSHELL
+  [ 'setshell not an int', 0, qr{command must return an integer},
+    q{\setshell i echo -n one} ],
+  [ 'setshell missing arg', 1, qr{missing argument }, q{\setshell var} ],
+  [ 'setshell no such command', 0, qr{could not read result },
+    q{\setshell var no-such-command} ],
+  # SLEEP
+  [ 'sleep undefined variable', 0, qr{sleep: undefined variable},
+    q{\sleep :nosuchvariable} ],
+  [ 'sleep too many args', 1, qr{too many arguments}, q{\sleep too many args} ],
+  [ 'sleep missing arg', 1, [ qr{missing argument}, qr{\\sleep} ],
+    q{\sleep} ],
+  [ 'sleep unknown unit', 1, qr{unrecognized time unit}, q{\sleep 1 week} ],
+  # MISC
+  [ 'misc invalid backslash command', 1,
+    qr{invalid command .* "nosuchcommand"},
+    q{\nosuchcommand} ],
+  [ 'misc empty script', 1, qr{empty command list for script}, q{} ],
+);
+
+for my $e (@errors)
+{
+  my ($name, $status, $re, $script) = @$e;
+  my $n = '001_pgbench_error_' . $name;
+  $n =~ s/ /_/g;
+  pgbench(
+    '-n -t 1 -Dfoo=bla -M prepared',
+    $status, $status ? qr{^$} : qr{processed: 0/1}, $re,
+    'pgbench script error: ' . $name, { $n => $script });
+}
+
+# throttling
+pgbench(
+  '-t 100 -S --rate=100000 --latency-limit=1000000 -c 2 -n -r',
+  0, [ qr{processed: 200/200}, qr{builtin: select only} ], qr{^$},
+  'pgbench throttling');
+
+pgbench(
+  # given the expected rate and the 2 ms tx duration, at most one is executed
+  '-t 10 --rate=100000 --latency-limit=1 -n -r',
+  0,
+  [ qr{processed: [01]/10}, qr{type: .*/001_pgbench_sleep},
+    qr{above the 1.0 ms latency limit: [01] }],
+  qr{^$},
+  'pgbench late throttling',
+  { '001_pgbench_sleep' => q{\sleep 2ms} });
+
+# check log contents and cleanup
+sub check_pgbench_logs($$$$$)
+{
+  my ($prefix, $nb, $min, $max, $re) = @_;
+
+  my @logs = <$prefix.*>;
+  ok(@logs == $nb, "number of log files");
+  ok(grep(/^$prefix\.\d+(\.\d+)?$/, @logs) == $nb, "file name format");
+
+  my $log_number = 0;
+  for my $log (sort @logs)
+  {
+    eval {
+      open LOG, $log or die "$@";
+      my @contents = <LOG>;
+      my $clen = @contents;
+      ok($min <= $clen && $clen <= $max, "transaction count for $log ($clen)");
+      ok(grep($re, @contents) == $clen, "transaction format for $prefix");
+      close LOG or die "$@";
+    };
+  }
+  ok(unlink(@logs), "remove log files");
+}
+
+# note: --progress-timestamp is not tested
+pgbench(
+  '-T 2 -P 1 -l --log-prefix=001_pgbench_log_1 --aggregate-interval=1' .
+  ' -S -b se@2 --rate=20 --latency-limit=1000 -j 2 -c 3 -r',
+  0,
+  [ qr{type: multiple}, qr{clients: 3}, qr{threads: 2}, qr{duration: 2 s},
+    qr{script 1: .* select only}, qr{script 2: .* select only},
+    qr{statement latencies in milliseconds}, qr{FROM pgbench_accounts} ],
+  [ qr{vacuum}, qr{progress: 1\b} ],
+  'pgbench progress');
+
+# 2 threads 2 seconds, sometimes only one aggregated line is written
+check_pgbench_logs('001_pgbench_log_1', 2, 1, 2,
+		   qr{^\d+ \d{1,2} \d+ \d+ \d+ \d+ \d+ \d+ \d+ \d+ \d+$});
+
+# with sampling rate
+pgbench(
+  '-n -S -t 50 -c 2 --log --log-prefix=001_pgbench_log_2 --sampling-rate=0.5',
+  0, [ qr{select only}, qr{processed: 100/100} ], qr{^$},
+  'pgbench logs');
+
+check_pgbench_logs('001_pgbench_log_2', 1, 8, 92,
+		   qr{^0 \d{1,2} \d+ \d \d+ \d+$});
+
+# check log file in some detail
+pgbench(
+  '-n -b se -t 10 -l --log-prefix=001_pgbench_log_3',
+  0, [ qr{select only}, qr{processed: 10/10} ], qr{^$},
+  'pgbench logs contents');
+
+check_pgbench_logs('001_pgbench_log_3', 1, 10, 10,
+		   qr{^\d \d{1,2} \d+ \d \d+ \d+$});
+
+# done
+$node->stop;
diff --git a/src/bin/pgbench/t/002_pgbench_no_server.pl b/src/bin/pgbench/t/002_pgbench_no_server.pl
new file mode 100644
index 0000000..3629800
--- /dev/null
+++ b/src/bin/pgbench/t/002_pgbench_no_server.pl
@@ -0,0 +1,90 @@
+#
+# pgbench tests which do not need a server
+#
+
+use strict;
+use warnings;
+
+use TestLib;
+use Test::More tests => 116;
+
+# invoke pgbench
+sub pgbench($$$$$)
+{
+	my ($opts, $stat, $out, $err, $name) = @_;
+	print STDERR "opts=$opts, stat=$stat, out=$out, err=$err, name=$name";
+	command_checks_all([ 'pgbench', split(/\s+/, $opts) ],
+		       $stat, $out, $err, $name);
+}
+
+#
+# Option various errors
+#
+
+my @options = (
+  # name, options, stderr checks
+  [ 'bad option', '-h home -p 5432 -U calvin -d stuff --bad-option',
+    [ qr{unrecognized 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 number of clients: "three"} ],
+  [ 'bad #threads', '-j eleven', qr{invalid number of threads: "eleven"} ],
+  [ 'bad scale', '-i -s two', qr{invalid scaling factor: "two"} ],
+  [ 'invalid #transactions', '-t zil', qr{invalid number of transactions: "zil"} ],
+  [ 'invalid duration', '-T ten', qr{invalid duration: "ten"} ],
+  [ '-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{invalid fillfactor} ],
+  [ 'invalid query mode', '-M no-such-mode', qr{invalid query mode} ],
+  [ 'invalid progress', '--progress=0', qr{invalid thread progress delay} ],
+  [ '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{invalid .* seconds for} ],
+  [ 'weight zero', '-b se@0 -b si@0 -b tpcb@0', qr{weight must not be zero} ],
+  [ 'prepare after script', '-S -M prepared', qr{query mode .* before any} ],
+  [ '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} ],
+  # loging 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: 7 checks
+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 buitlin list');
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
index 42e66ed..b1dc271 100644
--- a/src/test/perl/PostgresNode.pm
+++ b/src/test/perl/PostgresNode.pm
@@ -1306,6 +1306,25 @@ sub command_like
 
 =pod
 
+=item $node->command_checks_all(...)
+
+Runs a shell command like TestLib::command_checks_all, but with PGPORT
+set so that the command will default to connecting to this
+PostgresNode.
+
+=cut
+
+sub command_checks_all
+{
+	my $self = shift;
+
+	local $ENV{PGPORT} = $self->port;
+
+	TestLib::command_checks_all(@_);
+}
+
+=pod
+
 =item $node->issues_sql_like(cmd, expected_sql, test_name)
 
 Run a command on the node, then verify that $expected_sql appears in the
diff --git a/src/test/perl/TestLib.pm b/src/test/perl/TestLib.pm
index fe09689..02e3cef 100644
--- a/src/test/perl/TestLib.pm
+++ b/src/test/perl/TestLib.pm
@@ -38,6 +38,7 @@ our @EXPORT = qw(
   program_options_handling_ok
   command_like
   command_fails_like
+  command_checks_all
 
   $windows_os
 );
@@ -310,4 +311,43 @@ sub command_fails_like
 	like($stderr, $expected_stderr, "$test_name: matches");
 }
 
+# run a command and checks its status and outputs.
+# The 5 arguments are:
+# - cmd: space-separated string or [] for command to run
+# - ret: expected exit status
+# - out: one re or [] of re to be checked against stdout
+# - err: one re or [] of re to be checked against stderr
+# - test_name: name of test
+sub command_checks_all
+{
+	my ($cmd, $ret, $out, $err, $test_name) = @_;
+
+	# split command if provided as a string instead of array ref
+	$cmd = [ split /\s+/, $cmd ] unless ref $cmd eq 'ARRAY';
+
+	# run command
+	my ($stdout, $stderr);
+	print("# Running: " . join(" ", @{$cmd}) . "\n");
+	IPC::Run::run($cmd, '>', \$stdout, '2>', \$stderr);
+	my $status = $? >> 8;
+
+	# check status
+	ok($ret == $status,
+	   "$test_name status (got $status vs expected $ret)");
+
+	# check stdout
+	$out = [ $out ] unless ref $out eq 'ARRAY';
+	for my $re (@$out)
+	{
+		like($stdout, $re, "$test_name out /$re/");
+	}
+
+	# check stderr
+	$err = [ $err] unless ref $err eq 'ARRAY';
+	for my $re (@$err)
+	{
+	  like($stderr, $re, "$test_name err /$re/");
+	}
+}
+
 1;
-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Reply via email to