From e2ab8832fa476b14b44f7452985b1b553bd44b6a Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@gmail.com>
Date: Sat, 18 Apr 2020 16:16:06 +1200
Subject: [PATCH v2 6/6] Add TAP test for snapshot-too-old.

---
 contrib/old_snapshot/t/002_too_old.pl |  75 ++++++++++++++++
 src/test/perl/PsqlSession.pm          | 122 ++++++++++++++++++++++++++
 2 files changed, 197 insertions(+)
 create mode 100644 contrib/old_snapshot/t/002_too_old.pl
 create mode 100644 src/test/perl/PsqlSession.pm

diff --git a/contrib/old_snapshot/t/002_too_old.pl b/contrib/old_snapshot/t/002_too_old.pl
new file mode 100644
index 0000000000..097a464763
--- /dev/null
+++ b/contrib/old_snapshot/t/002_too_old.pl
@@ -0,0 +1,75 @@
+# Simple test of early pruning and snapshot-too-old errors.
+use strict;
+use warnings;
+use PostgresNode;
+use PsqlSession;
+use TestLib;
+use Test::More tests => 5;
+
+my $node = get_new_node('master');
+$node->init;
+$node->append_conf("postgresql.conf", "timezone = UTC");
+$node->append_conf("postgresql.conf", "old_snapshot_threshold=10");
+$node->append_conf("postgresql.conf", "max_prepared_transactions=10");
+$node->start;
+$node->psql('postgres', 'create extension old_snapshot');
+$node->psql('postgres', 'create table t (i int)');
+$node->psql('postgres', 'insert into t select generate_series(1, 42)');
+
+# start an interactive session that we can use to interleave statements
+my $session = PsqlSession->new($node, "postgres");
+$session->send("\\set PROMPT1 ''\n", 2);
+$session->send("\\set PROMPT2 ''\n", 1);
+
+my @lines;
+my $command_tag;
+my $result;
+
+# begin a session that we can interleave with vacuum activity
+@lines = $session->send("begin transaction isolation level repeatable read;\n", 2);
+shift @lines;
+$command_tag = shift @lines;
+is($command_tag, "BEGIN");
+
+# take a snapshot at time 0
+$node->psql('postgres', "select pg_clobber_current_snapshot_timestamp('3000-01-01 00:00:00Z')");
+@lines = $session->send("select * from t order by i limit 1;\n", 2);
+shift @lines;
+$result = shift @lines;
+is($result, "1");
+
+# advance time by 10 minutes, then UPDATE and VACUUM the table
+$node->psql('postgres', "select pg_clobber_current_snapshot_timestamp('3000-01-01 00:10:00Z')");
+$node->psql('postgres', "update t set i = 1001 where i = 1");
+$node->psql('postgres', "vacuum analyze t");
+
+# our snapshot is not too old yet, so we can still use it
+@lines = $session->send("select * from t order by i limit 1;\n", 2);
+shift @lines;
+$result = shift @lines;
+is($result, "1");
+
+# advance time by 10 more minutes, then UPDATE and VACUUM the table
+$node->psql('postgres', "select pg_clobber_current_snapshot_timestamp('3000-01-01 00:20:00Z')");
+$node->psql('postgres', "update t set i = 1001 where i = 1");
+$node->psql('postgres', "vacuum analyze t");
+
+# our snapshot is not too old yet, so we can still use it
+@lines = $session->send("select * from t order by i limit 1;\n", 2);
+shift @lines;
+$result = shift @lines;
+is($result, "1");
+
+# advance time by just one more minute, then UPDATE and VACUUM the table
+$node->psql('postgres', "select pg_clobber_current_snapshot_timestamp('3000-01-01 00:21:00Z')");
+$node->psql('postgres', "update t set i = 1002 where i = 1");
+$node->psql('postgres', "vacuum analyze t");
+
+# our snapshot is too old!  the thing it wants to see has been removed
+@lines = $session->send("select * from t order by i limit 1;\n", 2);
+shift @lines;
+$result = shift @lines;
+is($result, "ERROR:  snapshot too old");
+
+$session->close;
+$node->stop;
diff --git a/src/test/perl/PsqlSession.pm b/src/test/perl/PsqlSession.pm
new file mode 100644
index 0000000000..b34dcca502
--- /dev/null
+++ b/src/test/perl/PsqlSession.pm
@@ -0,0 +1,122 @@
+=pod
+
+=head1 NAME
+
+PsqlSession - class representing psql connection
+
+=head1 SYNOPSIS
+
+  use PsqlSession;
+
+  my $node = PostgresNode->get_new_node('mynode');
+  my $session = PsqlSession->new($node, "dbname");
+
+  # send simple query and wait for one line response
+  my $result = $session->send("SELECT 42;", 1);
+
+  # close connection
+  $session->close();
+
+=head1 DESCRIPTION
+
+PsqlSession allows for tests of interleaved operations, similar to
+isolation tests.
+
+=cut
+
+package PsqlSession;
+
+use strict;
+use warnings;
+
+use PostgresNode;
+use TestLib;
+use IPC::Run qw(pump finish timer);
+
+our @EXPORT = qw(
+  new
+  send
+  close
+);
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item PsqlSession::new($class, $node, $dbname)
+
+Create a new PsqlSession instance, connected to a database.
+
+=cut
+
+sub new
+{
+	my ($class, $node, $dbname) = @_;
+	my $timer = timer(5);
+	my $stdin = '';
+	my $stdout = '';
+	my $harness = $node->interactive_psql($dbname, \$stdin, \$stdout, $timer);
+	my $self = {
+		_harness => $harness,
+		_stdin => \$stdin,
+		_stdout => \$stdout,
+		_timer => $timer
+	};
+	bless $self, $class;
+	return $self;
+}
+
+=pod
+
+=item $session->send($input, $lines)
+
+Send the given input to psql, and then wait for the given number of lines
+of output, or a timeout.
+
+=cut
+
+sub count_lines
+{
+	my ($s) = @_;
+	return $s =~ tr/\n//;
+}
+
+sub send
+{
+	my ($self, $statement, $lines) = @_;
+	${$self->{_stdout}} = '';
+	${$self->{_stdin}} .= $statement;
+	$self->{_timer}->start(5);
+	pump $self->{_harness} until count_lines(${$self->{_stdout}}) == $lines || $self->{_timer}->is_expired;
+	die "expected ${lines} lines but after timeout, received only: ${$self->{_stdout}}" if $self->{_timer}->is_expired;
+	my @result = split /\n/, ${$self->{_stdout}};
+	chop(@result);
+	return @result;
+}
+
+=pod
+
+=item $session->close()
+
+Close a PsqlSession connection.
+
+=cut
+
+sub close
+{
+	my ($self) = @_;
+	$self->{_timer}->start(5);
+	${$self->{_stdin}} .= "\\q\n";
+	finish $self->{_harness} or die "psql returned $?";
+	$self->{_timer}->reset;
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
-- 
2.20.1

