From ddceb29bd57b6782ef51166dde95e9d3f01e5092 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 10 Mar 2020 13:44:46 -0400
Subject: [PATCH v11 4/5] Regression tests for pg_validatebackup.

Patch by me, based in part on ideas from Mark Dilger and
Rajkumar Raghuwanshi.
---
 src/bin/pg_validatebackup/.gitignore          |   1 +
 src/bin/pg_validatebackup/Makefile            |   6 +
 src/bin/pg_validatebackup/t/001_basic.pl      |  30 +++
 src/bin/pg_validatebackup/t/002_algorithm.pl  |  57 ++++++
 src/bin/pg_validatebackup/t/003_corruption.pl | 180 ++++++++++++++++++
 src/bin/pg_validatebackup/t/004_options.pl    |  89 +++++++++
 .../pg_validatebackup/t/005_bad_manifest.pl   | 139 ++++++++++++++
 7 files changed, 502 insertions(+)
 create mode 100644 src/bin/pg_validatebackup/t/001_basic.pl
 create mode 100644 src/bin/pg_validatebackup/t/002_algorithm.pl
 create mode 100644 src/bin/pg_validatebackup/t/003_corruption.pl
 create mode 100644 src/bin/pg_validatebackup/t/004_options.pl
 create mode 100644 src/bin/pg_validatebackup/t/005_bad_manifest.pl

diff --git a/src/bin/pg_validatebackup/.gitignore b/src/bin/pg_validatebackup/.gitignore
index 3ae1c1f03a..21e0a92429 100644
--- a/src/bin/pg_validatebackup/.gitignore
+++ b/src/bin/pg_validatebackup/.gitignore
@@ -1 +1,2 @@
 /pg_validatebackup
+/tmp_check/
diff --git a/src/bin/pg_validatebackup/Makefile b/src/bin/pg_validatebackup/Makefile
index dde7eb3c02..04ef7d3051 100644
--- a/src/bin/pg_validatebackup/Makefile
+++ b/src/bin/pg_validatebackup/Makefile
@@ -31,3 +31,9 @@ uninstall:
 
 clean distclean maintainer-clean:
 	rm -f pg_validatebackup$(X) $(OBJS)
+
+check:
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
diff --git a/src/bin/pg_validatebackup/t/001_basic.pl b/src/bin/pg_validatebackup/t/001_basic.pl
new file mode 100644
index 0000000000..6d4b8ea01a
--- /dev/null
+++ b/src/bin/pg_validatebackup/t/001_basic.pl
@@ -0,0 +1,30 @@
+use strict;
+use warnings;
+use TestLib;
+use Test::More tests => 16;
+
+my $tempdir = TestLib::tempdir;
+
+program_help_ok('pg_validatebackup');
+program_version_ok('pg_validatebackup');
+program_options_handling_ok('pg_validatebackup');
+
+command_fails_like(['pg_validatebackup'],
+				   qr/no backup directory specified/,
+				   'target directory must be specified');
+command_fails_like(['pg_validatebackup', $tempdir],
+				   qr/could not open file.*\/backup_manifest\"/,
+				   'pg_validatebackup requires a manifest');
+command_fails_like(['pg_validatebackup', $tempdir, $tempdir],
+				   qr/too many command-line arguments/,
+				   'multiple target directories not allowed');
+
+# create fake manifest file
+open(my $fh, '>', "$tempdir/backup_manifest") || die "open: $!";
+close($fh);
+
+# but then try to use an alternate, nonexisting manifest
+command_fails_like(['pg_validatebackup', '-m', "$tempdir/not_the_manifest",
+						$tempdir],
+				   qr/could not open file.*\/not_the_manifest\"/,
+				   'pg_validatebackup respects -m flag');
diff --git a/src/bin/pg_validatebackup/t/002_algorithm.pl b/src/bin/pg_validatebackup/t/002_algorithm.pl
new file mode 100644
index 0000000000..e411923411
--- /dev/null
+++ b/src/bin/pg_validatebackup/t/002_algorithm.pl
@@ -0,0 +1,57 @@
+# Verify that we can take and validate backups with various checksum types.
+
+use strict;
+use warnings;
+use Cwd;
+use Config;
+use File::Path qw(rmtree);
+use PostgresNode;
+use TestLib;
+use Test::More tests => 19;
+
+my $master = get_new_node('master');
+$master->init(allows_streaming => 1);
+$master->start;
+
+for my $algorithm (qw(bogus none crc32c sha224 sha256 sha384 sha512))
+{
+	my $backup_path = $master->backup_dir . '/' . $algorithm;
+	my @backup = ('pg_basebackup', '-D', $backup_path, '-m', $algorithm,
+				  '--no-sync');
+	my @validate = ('pg_validatebackup', '-e', $backup_path);
+
+	# A backup with a bogus algorithm should fail.
+	if ($algorithm eq 'bogus')
+	{
+		$master->command_fails(\@backup,
+							   "backup fails with algorithm \"$algorithm\"");
+		next;
+	}
+
+	# A backup with a valid algorithm should work.
+	$master->command_ok(\@backup, "backup ok with algorithm \"$algorithm\"");
+
+	# We expect each real checksum algorithm to be mentioned on every line of
+	# the backup manifest file except the first and last; for simplicity, we
+	# just check that it shows up lots of times. When the checksum algorithm
+	# is none, we just check that the manifest exists.
+	if ($algorithm eq 'none')
+	{
+		ok(-f "$backup_path/backup_manifest", "backup manifest exists");
+	}
+	else
+	{
+		my $manifest = slurp_file("$backup_path/backup_manifest");
+		my $count_of_algorithm_in_manifest =
+			(() = $manifest =~ /$algorithm/mig);
+		cmp_ok($count_of_algorithm_in_manifest, '>', 100,
+			   "$algorithm is mentioned many times in the manifest");
+	}
+
+	# Make sure that it validates OK.
+	$master->command_ok(\@validate,
+						"validate backup with algorithm \"$algorithm\"");
+
+	# Remove backup immediately to save disk space.
+	rmtree($backup_path);
+}
diff --git a/src/bin/pg_validatebackup/t/003_corruption.pl b/src/bin/pg_validatebackup/t/003_corruption.pl
new file mode 100644
index 0000000000..e845a3fee9
--- /dev/null
+++ b/src/bin/pg_validatebackup/t/003_corruption.pl
@@ -0,0 +1,180 @@
+# Verify that various forms of corruption are detected by pg_validatebackup.
+
+use strict;
+use warnings;
+use Cwd;
+use Config;
+use File::Path qw(rmtree);
+use PostgresNode;
+use TestLib;
+use Test::More tests => 32;
+
+my $master = get_new_node('master');
+$master->init(allows_streaming => 1);
+$master->start;
+
+# Include a user-defined tablespace in the hopes of detecting problems in that
+# area.
+my $source_ts_path = TestLib::tempdir;
+$master->safe_psql('postgres', <<EOM);
+CREATE TABLE x1 (a int);
+INSERT INTO x1 VALUES (111);
+CREATE TABLESPACE ts1 LOCATION '$source_ts_path';
+CREATE TABLE x2 (a int) TABLESPACE ts1;
+INSERT INTO x1 VALUES (222);
+EOM
+
+my @scenario = (
+	{
+		'name' => 'extra_file',
+		'mutilate' => \&mutilate_extra_file,
+		'fails_like' =>
+			qr/extra_file.*present on disk but not in the manifest/
+	},
+	{
+		'name' => 'extra_tablespace_file',
+		'mutilate' => \&mutilate_extra_tablespace_file,
+		'fails_like' =>
+			qr/extra_ts_file.*present on disk but not in the manifest/
+	},
+	{
+		'name' => 'missing_file',
+		'mutilate' => \&mutilate_missing_file,
+		'fails_like' =>
+			qr/pg_xact\/0000.*present in the manifest but not on disk/
+	},
+	{
+		'name' => 'missing_tablespace',
+		'mutilate' => \&mutilate_missing_tablespace,
+		'fails_like' =>
+			qr/pg_tblspc.*present in the manifest but not on disk/
+	},
+	{
+		'name' => 'append_to_file',
+		'mutilate' => \&mutilate_append_to_file,
+		'fails_like' =>
+			qr/has size \d+ on disk but size \d+ in the manifest/
+	},
+	{
+		'name' => 'truncate_file',
+		'mutilate' => \&mutilate_truncate_file,
+		'fails_like' =>
+			qr/has size 0 on disk but size \d+ in the manifest/
+	},
+	{
+		'name' => 'replace_file',
+		'mutilate' => \&mutilate_replace_file,
+		'fails_like' => qr/checksum mismatch for file/
+	},
+	{
+		'name' => 'bad_manifest',
+		'mutilate' => \&mutilate_bad_manifest,
+		'fails_like' => qr/manifest checksum mismatch/
+	}
+);
+
+for my $scenario (@scenario)
+{
+	my $name = $scenario->{'name'};
+
+	# Take a backup and check that it validates OK.
+	my $backup_path = $master->backup_dir . '/' . $name;
+	my $backup_ts_path = TestLib::tempdir;
+	$master->command_ok(['pg_basebackup', '-D', $backup_path, '--no-sync',
+						'-T', "${source_ts_path}=${backup_ts_path}"],
+						"base backup ok");
+	command_ok(['pg_validatebackup', $backup_path ],
+			   "intact backup validated");
+
+	# Mutilate the backup in some way.
+	$scenario->{'mutilate'}->($backup_path);
+
+	# Now check that the backup no longer validates.
+	command_fails_like(['pg_validatebackup', $backup_path ],
+					   $scenario->{'fails_like'},
+					   "corrupt backup fails validation: $name");
+}
+
+sub create_extra_file
+{
+	my ($backup_path, $relative_path) = @_;
+	my $pathname = "$backup_path/$relative_path";
+	open(my $fh, '>', $pathname) || die "open $pathname: $!";
+	print $fh "This is an extra file.\n";
+	close($fh);
+}
+
+# Add a file into the root directory of the backup.
+sub mutilate_extra_file
+{
+	my ($backup_path) = @_;
+	create_extra_file($backup_path, "extra_file");
+}
+
+# Add a file inside the user-defined tablespace.
+sub mutilate_extra_tablespace_file
+{
+	my ($backup_path) = @_;
+	my ($tsoid) = grep { $_ ne '.' && $_ ne '..' }
+		 slurp_dir("$backup_path/pg_tblspc");
+	my ($catvdir) = grep { $_ ne '.' && $_ ne '..' }
+		 slurp_dir("$backup_path/pg_tblspc/$tsoid");
+	my ($tsdboid) = grep { $_ ne '.' && $_ ne '..' }
+		 slurp_dir("$backup_path/pg_tblspc/$tsoid/$catvdir");
+	create_extra_file($backup_path,
+					  "pg_tblspc/$tsoid/$catvdir/$tsdboid/extra_ts_file");
+}
+
+# Remove a file.
+sub mutilate_missing_file
+{
+	my ($backup_path) = @_;
+	my $pathname = "$backup_path/pg_xact/0000";
+	unlink($pathname) || die "$pathname: $!";
+}
+
+# Remove the symlink to the user-defined tablespace.
+sub mutilate_missing_tablespace
+{
+	my ($backup_path) = @_;
+	my ($tsoid) = grep { $_ ne '.' && $_ ne '..' }
+		 slurp_dir("$backup_path/pg_tblspc");
+	my $pathname = "$backup_path/pg_tblspc/$tsoid";
+	unlink($pathname) || die "$pathname: $!";
+}
+
+# Append an additional bytes to a file.
+sub mutilate_append_to_file
+{
+	my ($backup_path) = @_;
+	append_to_file "$backup_path/global/pg_control", 'x';
+}
+
+# Truncate a file to zero length.
+sub mutilate_truncate_file
+{
+	my ($backup_path) = @_;
+	my $pathname = "$backup_path/global/pg_control";
+	open(my $fh, '>', $pathname) || die "open $pathname: $!";
+	close($fh);
+}
+
+# Replace a file's contents without changing the length of the file. This is
+# not a particularly efficient way to do this, so we pick a file that's
+# expected to be short.
+sub mutilate_replace_file
+{
+	my ($backup_path) = @_;
+	my $pathname = "$backup_path/PG_VERSION";
+	my $contents = slurp_file($pathname);
+	open(my $fh, '>', $pathname) || die "open $pathname: $!";
+	print $fh 'q' x length($contents);
+	close($fh);
+}
+
+# Corrupt the backup manifest.
+sub mutilate_bad_manifest
+{
+	my ($backup_path) = @_;
+	append_to_file "$backup_path/backup_manifest", "\n";
+}
diff --git a/src/bin/pg_validatebackup/t/004_options.pl b/src/bin/pg_validatebackup/t/004_options.pl
new file mode 100644
index 0000000000..8f185626ed
--- /dev/null
+++ b/src/bin/pg_validatebackup/t/004_options.pl
@@ -0,0 +1,89 @@
+# Verify the behavior of assorted pg_validatebackup options.
+
+use strict;
+use warnings;
+use Cwd;
+use Config;
+use File::Path qw(rmtree);
+use PostgresNode;
+use TestLib;
+use Test::More tests => 25;
+
+# Start up the server and take a backup.
+my $master = get_new_node('master');
+$master->init(allows_streaming => 1);
+$master->start;
+my $backup_path = $master->backup_dir . '/test_options';
+$master->command_ok(['pg_basebackup', '-D', $backup_path, '--no-sync' ],
+					"base backup ok");
+
+# Verify that pg_validatebackup -q succeeds and produces no output.
+my $stdout;
+my $stderr;
+my $result = IPC::Run::run ['pg_validatebackup', '-q', $backup_path ],
+	'>', \$stdout, '2>', \$stderr;
+ok($result, "-q succeeds: exit code 0");
+is($stdout, '', "-q succeeds: no stdout");
+is($stderr, '', "-q succeeds: no stderr");
+
+# Corrupt the PG_VERSION file.
+my $version_pathname = "$backup_path/PG_VERSION";
+my $version_contents = slurp_file($version_pathname);
+open(my $fh, '>', $version_pathname) || die "open $version_pathname: $!";
+print $fh 'q' x length($version_contents);
+close($fh);
+
+# Verify that pg_validatebackup -q now fails.
+command_fails_like(['pg_validatebackup', '-q', $backup_path ],
+				   qr/checksum mismatch for file \"PG_VERSION\"/,
+				   '-q checksum mismatch');
+
+# Since we didn't change the length of the file, validation should succeed
+# if we ignore checksums. Check that we get the right message, too.
+command_like(['pg_validatebackup', '-s', $backup_path ],
+			 qr/backup successfully verified/,
+			 '-s skips checksumming');
+
+# Validation should succeed if we ignore the problem file.
+command_like(['pg_validatebackup', '-i', 'PG_VERSION', $backup_path ],
+			 qr/backup successfully verified/,
+			 '-i ignores problem file');
+
+# PG_VERSION is already corrupt; let's try also removing all of pg_xact.
+rmtree($backup_path . "/pg_xact");
+
+# We're ignoring the problem with PG_VERSION, but not the problem with
+# pg_xact, so validation should fail here.
+command_fails_like(['pg_validatebackup', '-i', 'PG_VERSION', $backup_path ],
+				   qr/pg_xact.*is present in the manifest but not on disk/,
+				   '-i does not ignore all problems');
+
+# If we use -i twice, we should be able to ignore all of the problems.
+command_like(['pg_validatebackup', '-i', 'PG_VERSION', '-i', 'pg_xact',
+				$backup_path ],
+			 qr/backup successfully verified/,
+			 'multiple -i options work');
+
+# Verify that when -i is not used, both problems are reported.
+$result = IPC::Run::run ['pg_validatebackup', $backup_path ],
+	'>', \$stdout, '2>', \$stderr;
+ok(!$result, "multiple problems: fails");
+like($stderr, qr/pg_xact.*is present in the manifest but not on disk/,
+	 "multiple problems: missing files reported");
+like($stderr, qr/checksum mismatch for file \"PG_VERSION\"/,
+	 "multiple problems: checksum mismatch reported");
+
+# Verify that when -e is used, only the problem detected first is reported.
+$result = IPC::Run::run ['pg_validatebackup', '-e', $backup_path ],
+	'>', \$stdout, '2>', \$stderr;
+ok(!$result, "-e reports 1 error: fails");
+like($stderr, qr/pg_xact.*is present in the manifest but not on disk/,
+	 "-e reports 1 error: missing files reported");
+unlike($stderr, qr/checksum mismatch for file \"PG_VERSION\"/,
+	   "-e reports 1 error: checksum mismatch not reported");
+
+# Test valid manifest with nonexistent backup directory.
+command_fails_like(['pg_validatebackup', '-m', "$backup_path/backup_manifest",
+						"$backup_path/fake" ],
+				   qr/could not open directory/,
+				   'nonexistent backup directory');
diff --git a/src/bin/pg_validatebackup/t/005_bad_manifest.pl b/src/bin/pg_validatebackup/t/005_bad_manifest.pl
new file mode 100644
index 0000000000..8449c527d3
--- /dev/null
+++ b/src/bin/pg_validatebackup/t/005_bad_manifest.pl
@@ -0,0 +1,139 @@
+# Test the behavior of pg_validatebackup when the backup manifest has
+# problems.
+
+use strict;
+use warnings;
+use Cwd;
+use Config;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 38;
+
+my $tempdir = TestLib::tempdir;
+
+test_bad_manifest('input string ended unexpctedly',
+	  qr/could not parse backup manifest: The input string ended unexpectedly/,
+	  <<EOM);
+{
+EOM
+
+test_parse_error('unexpected object end', <<EOM);
+{}
+EOM
+
+test_parse_error('unexpected array start', <<EOM);
+[]
+EOM
+
+test_parse_error('expected version indicator', <<EOM);
+{"not-expected": 1}
+EOM
+
+test_parse_error('unexpected manifest version', <<EOM);
+{"PostgreSQL-Backup-Manifest-Version": "phooey"}
+EOM
+
+test_parse_error('unexpected scalar', <<EOM);
+{"PostgreSQL-Backup-Manifest-Version": 1, "Files": true}
+EOM
+
+test_parse_error('expected file list', <<EOM);
+{"PostgreSQL-Backup-Manifest-Version": 1, "Oops": 1}
+EOM
+
+test_parse_error('unexpected object start', <<EOM);
+{"PostgreSQL-Backup-Manifest-Version": 1, "Files": {}}
+EOM
+
+test_parse_error('missing pathname', <<EOM);
+{"PostgreSQL-Backup-Manifest-Version": 1, "Files": [{}]}
+EOM
+
+test_parse_error('unexpected file field', <<EOM);
+{"PostgreSQL-Backup-Manifest-Version": 1, "Files": [
+    {"Oops": 1}
+]}
+EOM
+
+test_parse_error('missing size', <<EOM);
+{"PostgreSQL-Backup-Manifest-Version": 1, "Files": [
+    {"Path": "x"}
+]}
+EOM
+
+test_parse_error('file size is not an integer', <<EOM);
+{"PostgreSQL-Backup-Manifest-Version": 1, "Files": [
+    {"Path": "x", "Size": "Oops"}
+]}
+EOM
+
+test_parse_error('checksum without algorithm', <<EOM);
+{"PostgreSQL-Backup-Manifest-Version": 1, "Files": [
+    {"Path": "x", "Size": 100, "Checksum": "Oops"}
+]}
+EOM
+
+test_fatal_error('unrecognized checksum algorithm', <<EOM);
+{"PostgreSQL-Backup-Manifest-Version": 1, "Files": [
+    {"Path": "x", "Size": 100, "Checksum-Algorithm": "Oops", "Checksum": "00"}
+]}
+EOM
+
+test_fatal_error('invalid checksum for file', <<EOM);
+{"PostgreSQL-Backup-Manifest-Version": 1, "Files": [
+    {"Path": "x", "Size": 100, "Checksum-Algorithm": "CRC32C", "Checksum": "0"}
+]}
+EOM
+
+test_parse_error('expected manifest checksum', <<EOM);
+{"PostgreSQL-Backup-Manifest-Version": 1, "Files": [],
+ "Oops": 1}
+EOM
+
+test_parse_error('expected at least 2 lines', <<EOM);
+{"PostgreSQL-Backup-Manifest-Version": 1, "Files": [], "Manifest-Checksum": null}
+EOM
+
+my $manifest_without_newline = <<EOM;
+{"PostgreSQL-Backup-Manifest-Version": 1,
+ "Files": [],
+ "Manifest-Checksum": null}
+EOM
+chomp($manifest_without_newline);
+test_parse_error('last line not newline-terminated',
+				 $manifest_without_newline);
+
+test_fatal_error('invalid manifest checksum', <<EOM);
+{"PostgreSQL-Backup-Manifest-Version": 1, "Files": [],
+ "Manifest-Checksum": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890-"}
+EOM
+
+sub test_parse_error
+{
+	my ($test_name, $manifest_contents) = @_;
+
+	test_bad_manifest($test_name,
+					  qr/could not parse backup manifest: $test_name/,
+					  $manifest_contents);
+}
+
+sub test_fatal_error
+{
+	my ($test_name, $manifest_contents) = @_;
+
+	test_bad_manifest($test_name,
+					  qr/fatal: $test_name/,
+					  $manifest_contents);
+}
+
+sub test_bad_manifest
+{
+	my ($test_name, $regexp, $manifest_contents) = @_;
+
+	open(my $fh, '>', "$tempdir/backup_manifest") || die "open: $!";
+	print $fh $manifest_contents;
+	close($fh);
+
+	command_fails_like(['pg_validatebackup', $tempdir], $regexp,
+					   $test_name);
+}
-- 
2.17.2 (Apple Git-113)

