1
2=pod
3
4=head1 NAME
5
6TestLib - helper module for writing PostgreSQL's C<prove> tests.
7
8=head1 SYNOPSIS
9
10  use TestLib;
11
12  # Test basic output of a command
13  program_help_ok('initdb');
14  program_version_ok('initdb');
15  program_options_handling_ok('initdb');
16
17  # Test option combinations
18  command_fails(['initdb', '--invalid-option'],
19              'command fails with invalid option');
20  my $tempdir = TestLib::tempdir;
21  command_ok('initdb', '-D', $tempdir);
22
23  # Miscellanea
24  print "on Windows" if $TestLib::windows_os;
25  my $path = TestLib::perl2host($backup_dir);
26  ok(check_mode_recursive($stream_dir, 0700, 0600),
27    "check stream dir permissions");
28  TestLib::system_log('pg_ctl', 'kill', 'QUIT', $slow_pid);
29
30=head1 DESCRIPTION
31
32C<TestLib> contains a set of routines dedicated to environment setup for
33a PostgreSQL regression test run and includes some low-level routines
34aimed at controlling command execution, logging and test functions.
35
36=cut
37
38# This module should never depend on any other PostgreSQL regression test
39# modules.
40
41package TestLib;
42
43use strict;
44use warnings;
45
46use Config;
47use Cwd;
48use Exporter 'import';
49use Fcntl qw(:mode :seek);
50use File::Basename;
51use File::Find;
52use File::Spec;
53use File::stat qw(stat);
54use File::Temp ();
55use IPC::Run;
56use SimpleTee;
57
58# specify a recent enough version of Test::More to support the
59# done_testing() function
60use Test::More 0.87;
61
62our @EXPORT = qw(
63  generate_ascii_string
64  slurp_dir
65  slurp_file
66  append_to_file
67  check_mode_recursive
68  chmod_recursive
69  check_pg_config
70  system_or_bail
71  system_log
72  run_log
73  run_command
74
75  command_ok
76  command_fails
77  command_exit_is
78  program_help_ok
79  program_version_ok
80  program_options_handling_ok
81  command_like
82  command_like_safe
83  command_fails_like
84  command_checks_all
85
86  $windows_os
87  $use_unix_sockets
88);
89
90our ($windows_os, $use_unix_sockets, $tmp_check, $log_path, $test_logfile);
91
92BEGIN
93{
94
95	# Set to untranslated messages, to be able to compare program output
96	# with expected strings.
97	delete $ENV{LANGUAGE};
98	delete $ENV{LC_ALL};
99	$ENV{LC_MESSAGES} = 'C';
100
101	# This list should be kept in sync with pg_regress.c.
102	my @envkeys = qw (
103	  PGCHANNELBINDING
104	  PGCLIENTENCODING
105	  PGCONNECT_TIMEOUT
106	  PGDATA
107	  PGDATABASE
108	  PGGSSENCMODE
109	  PGGSSLIB
110	  PGHOSTADDR
111	  PGKRBSRVNAME
112	  PGPASSFILE
113	  PGPASSWORD
114	  PGREQUIREPEER
115	  PGREQUIRESSL
116	  PGSERVICE
117	  PGSERVICEFILE
118	  PGSSLCERT
119	  PGSSLCRL
120	  PGSSLKEY
121	  PGSSLMAXPROTOCOLVERSION
122	  PGSSLMINPROTOCOLVERSION
123	  PGSSLMODE
124	  PGSSLROOTCERT
125	  PGTARGETSESSIONATTRS
126	  PGUSER
127	  PGPORT
128	  PGHOST
129	  PG_COLOR
130	);
131	delete @ENV{@envkeys};
132
133	$ENV{PGAPPNAME} = basename($0);
134
135	# Must be set early
136	$windows_os = $Config{osname} eq 'MSWin32' || $Config{osname} eq 'msys';
137	if ($windows_os)
138	{
139		require Win32API::File;
140		Win32API::File->import(qw(createFile OsFHandleOpen CloseHandle));
141	}
142
143	# Specifies whether to use Unix sockets for test setups.  On
144	# Windows we don't use them by default since it's not universally
145	# supported, but it can be overridden if desired.
146	$use_unix_sockets =
147	  (!$windows_os || defined $ENV{PG_TEST_USE_UNIX_SOCKETS});
148}
149
150=pod
151
152=head1 EXPORTED VARIABLES
153
154=over
155
156=item C<$windows_os>
157
158Set to true when running under Windows, except on Cygwin.
159
160=back
161
162=cut
163
164INIT
165{
166
167	# Return EPIPE instead of killing the process with SIGPIPE.  An affected
168	# test may still fail, but it's more likely to report useful facts.
169	$SIG{PIPE} = 'IGNORE';
170
171	# Determine output directories, and create them.  The base path is the
172	# TESTDIR environment variable, which is normally set by the invoking
173	# Makefile.
174	$tmp_check = $ENV{TESTDIR} ? "$ENV{TESTDIR}/tmp_check" : "tmp_check";
175	$log_path = "$tmp_check/log";
176
177	mkdir $tmp_check;
178	mkdir $log_path;
179
180	# Open the test log file, whose name depends on the test name.
181	$test_logfile = basename($0);
182	$test_logfile =~ s/\.[^.]+$//;
183	$test_logfile = "$log_path/regress_log_$test_logfile";
184	open my $testlog, '>', $test_logfile
185	  or die "could not open STDOUT to logfile \"$test_logfile\": $!";
186
187	# Hijack STDOUT and STDERR to the log file
188	open(my $orig_stdout, '>&', \*STDOUT);
189	open(my $orig_stderr, '>&', \*STDERR);
190	open(STDOUT,          '>&', $testlog);
191	open(STDERR,          '>&', $testlog);
192
193	# The test output (ok ...) needs to be printed to the original STDOUT so
194	# that the 'prove' program can parse it, and display it to the user in
195	# real time. But also copy it to the log file, to provide more context
196	# in the log.
197	my $builder = Test::More->builder;
198	my $fh      = $builder->output;
199	tie *$fh, "SimpleTee", $orig_stdout, $testlog;
200	$fh = $builder->failure_output;
201	tie *$fh, "SimpleTee", $orig_stderr, $testlog;
202
203	# Enable auto-flushing for all the file handles. Stderr and stdout are
204	# redirected to the same file, and buffering causes the lines to appear
205	# in the log in confusing order.
206	autoflush STDOUT 1;
207	autoflush STDERR 1;
208	autoflush $testlog 1;
209}
210
211END
212{
213
214	# Test files have several ways of causing prove_check to fail:
215	# 1. Exit with a non-zero status.
216	# 2. Call ok(0) or similar, indicating that a constituent test failed.
217	# 3. Deviate from the planned number of tests.
218	#
219	# Preserve temporary directories after (1) and after (2).
220	$File::Temp::KEEP_ALL = 1 unless $? == 0 && all_tests_passing();
221}
222
223=pod
224
225=head1 ROUTINES
226
227=over
228
229=item all_tests_passing()
230
231Return 1 if all the tests run so far have passed. Otherwise, return 0.
232
233=cut
234
235sub all_tests_passing
236{
237	foreach my $status (Test::More->builder->summary)
238	{
239		return 0 unless $status;
240	}
241	return 1;
242}
243
244=pod
245
246=item tempdir(prefix)
247
248Securely create a temporary directory inside C<$tmp_check>, like C<mkdtemp>,
249and return its name.  The directory will be removed automatically at the
250end of the tests.
251
252If C<prefix> is given, the new directory is templated as C<${prefix}_XXXX>.
253Otherwise the template is C<tmp_test_XXXX>.
254
255=cut
256
257sub tempdir
258{
259	my ($prefix) = @_;
260	$prefix = "tmp_test" unless defined $prefix;
261	return File::Temp::tempdir(
262		$prefix . '_XXXX',
263		DIR     => $tmp_check,
264		CLEANUP => 1);
265}
266
267=pod
268
269=item tempdir_short()
270
271As above, but the directory is outside the build tree so that it has a short
272name, to avoid path length issues.
273
274=cut
275
276sub tempdir_short
277{
278
279	return File::Temp::tempdir(CLEANUP => 1);
280}
281
282=pod
283
284=item perl2host()
285
286Translate a Perl file name to a host file name.  Currently, this is a no-op
287except for the case of Perl=msys and host=mingw32.  The subject need not
288exist, but its parent directory must exist.
289
290=cut
291
292sub perl2host
293{
294	my ($subject) = @_;
295	return $subject unless $Config{osname} eq 'msys';
296	my $here = cwd;
297	my $leaf;
298	if (chdir $subject)
299	{
300		$leaf = '';
301	}
302	else
303	{
304		$leaf = '/' . basename $subject;
305		my $parent = dirname $subject;
306		chdir $parent or die "could not chdir \"$parent\": $!";
307	}
308
309	# this odd way of calling 'pwd -W' is the only way that seems to work.
310	my $dir = qx{sh -c "pwd -W"};
311	chomp $dir;
312	chdir $here;
313	return $dir . $leaf;
314}
315
316=pod
317
318=item system_log(@cmd)
319
320Run (via C<system()>) the command passed as argument; the return
321value is passed through.
322
323=cut
324
325sub system_log
326{
327	print("# Running: " . join(" ", @_) . "\n");
328	return system(@_);
329}
330
331=pod
332
333=item system_or_bail(@cmd)
334
335Run (via C<system()>) the command passed as argument, and returns
336if the command is successful.
337On failure, abandon further tests and exit the program.
338
339=cut
340
341sub system_or_bail
342{
343	if (system_log(@_) != 0)
344	{
345		BAIL_OUT("system $_[0] failed");
346	}
347	return;
348}
349
350=pod
351
352=item run_log(@cmd)
353
354Run the given command via C<IPC::Run::run()>, noting it in the log.
355The return value from the command is passed through.
356
357=cut
358
359sub run_log
360{
361	print("# Running: " . join(" ", @{ $_[0] }) . "\n");
362	return IPC::Run::run(@_);
363}
364
365=pod
366
367=item run_command(cmd)
368
369Run (via C<IPC::Run::run()>) the command passed as argument.
370The return value from the command is ignored.
371The return value is C<($stdout, $stderr)>.
372
373=cut
374
375sub run_command
376{
377	my ($cmd) = @_;
378	my ($stdout, $stderr);
379	my $result = IPC::Run::run $cmd, '>', \$stdout, '2>', \$stderr;
380	foreach ($stderr, $stdout) { s/\r\n/\n/g if $Config{osname} eq 'msys'; }
381	chomp($stdout);
382	chomp($stderr);
383	return ($stdout, $stderr);
384}
385
386=pod
387
388=item generate_ascii_string(from_char, to_char)
389
390Generate a string made of the given range of ASCII characters.
391
392=cut
393
394sub generate_ascii_string
395{
396	my ($from_char, $to_char) = @_;
397	my $res;
398
399	for my $i ($from_char .. $to_char)
400	{
401		$res .= sprintf("%c", $i);
402	}
403	return $res;
404}
405
406=pod
407
408=item slurp_dir(dir)
409
410Return the complete list of entries in the specified directory.
411
412=cut
413
414sub slurp_dir
415{
416	my ($dir) = @_;
417	opendir(my $dh, $dir)
418	  or die "could not opendir \"$dir\": $!";
419	my @direntries = readdir $dh;
420	closedir $dh;
421	return @direntries;
422}
423
424=pod
425
426=item slurp_file(filename [, $offset])
427
428Return the full contents of the specified file, beginning from an
429offset position if specified.
430
431=cut
432
433sub slurp_file
434{
435	my ($filename, $offset) = @_;
436	local $/;
437	my $contents;
438	my $fh;
439
440	# On windows open file using win32 APIs, to allow us to set the
441	# FILE_SHARE_DELETE flag ("d" below), otherwise other accesses to the file
442	# may fail.
443	if ($Config{osname} ne 'MSWin32')
444	{
445		open($fh, '<', $filename)
446		  or die "could not read \"$filename\": $!";
447	}
448	else
449	{
450		my $fHandle = createFile($filename, "r", "rwd")
451		  or die "could not open \"$filename\": $^E";
452		OsFHandleOpen($fh = IO::Handle->new(), $fHandle, 'r')
453		  or die "could not read \"$filename\": $^E\n";
454	}
455
456	if (defined($offset))
457	{
458		seek($fh, $offset, SEEK_SET)
459		  or die "could not seek \"$filename\": $!";
460	}
461
462	$contents = <$fh>;
463	close $fh;
464
465	$contents =~ s/\r\n/\n/g if $Config{osname} eq 'msys';
466	return $contents;
467}
468
469=pod
470
471=item append_to_file(filename, str)
472
473Append a string at the end of a given file.  (Note: no newline is appended at
474end of file.)
475
476=cut
477
478sub append_to_file
479{
480	my ($filename, $str) = @_;
481	open my $fh, ">>", $filename
482	  or die "could not write \"$filename\": $!";
483	print $fh $str;
484	close $fh;
485	return;
486}
487
488=pod
489
490=item check_mode_recursive(dir, expected_dir_mode, expected_file_mode, ignore_list)
491
492Check that all file/dir modes in a directory match the expected values,
493ignoring files in C<ignore_list> (basename only).
494
495=cut
496
497sub check_mode_recursive
498{
499	my ($dir, $expected_dir_mode, $expected_file_mode, $ignore_list) = @_;
500
501	# Result defaults to true
502	my $result = 1;
503
504	find(
505		{
506			follow_fast => 1,
507			wanted      => sub {
508				# Is file in the ignore list?
509				foreach my $ignore ($ignore_list ? @{$ignore_list} : [])
510				{
511					if ("$dir/$ignore" eq $File::Find::name)
512					{
513						return;
514					}
515				}
516
517				# Allow ENOENT.  A running server can delete files, such as
518				# those in pg_stat.  Other stat() failures are fatal.
519				my $file_stat = stat($File::Find::name);
520				unless (defined($file_stat))
521				{
522					my $is_ENOENT = $!{ENOENT};
523					my $msg       = "unable to stat $File::Find::name: $!";
524					if ($is_ENOENT)
525					{
526						warn $msg;
527						return;
528					}
529					else
530					{
531						die $msg;
532					}
533				}
534
535				my $file_mode = S_IMODE($file_stat->mode);
536
537				# Is this a file?
538				if (S_ISREG($file_stat->mode))
539				{
540					if ($file_mode != $expected_file_mode)
541					{
542						print(
543							*STDERR,
544							sprintf("$File::Find::name mode must be %04o\n",
545								$expected_file_mode));
546
547						$result = 0;
548						return;
549					}
550				}
551
552				# Else a directory?
553				elsif (S_ISDIR($file_stat->mode))
554				{
555					if ($file_mode != $expected_dir_mode)
556					{
557						print(
558							*STDERR,
559							sprintf("$File::Find::name mode must be %04o\n",
560								$expected_dir_mode));
561
562						$result = 0;
563						return;
564					}
565				}
566
567				# Else something we can't handle
568				else
569				{
570					die "unknown file type for $File::Find::name";
571				}
572			}
573		},
574		$dir);
575
576	return $result;
577}
578
579=pod
580
581=item chmod_recursive(dir, dir_mode, file_mode)
582
583C<chmod> recursively each file and directory within the given directory.
584
585=cut
586
587sub chmod_recursive
588{
589	my ($dir, $dir_mode, $file_mode) = @_;
590
591	find(
592		{
593			follow_fast => 1,
594			wanted      => sub {
595				my $file_stat = stat($File::Find::name);
596
597				if (defined($file_stat))
598				{
599					chmod(
600						S_ISDIR($file_stat->mode) ? $dir_mode : $file_mode,
601						$File::Find::name
602					) or die "unable to chmod $File::Find::name";
603				}
604			}
605		},
606		$dir);
607	return;
608}
609
610=pod
611
612=item check_pg_config(regexp)
613
614Return the number of matches of the given regular expression
615within the installation's C<pg_config.h>.
616
617=cut
618
619sub check_pg_config
620{
621	my ($regexp) = @_;
622	my ($stdout, $stderr);
623	my $result = IPC::Run::run [ 'pg_config', '--includedir' ], '>',
624	  \$stdout, '2>', \$stderr
625	  or die "could not execute pg_config";
626	chomp($stdout);
627	$stdout =~ s/\r$//;
628
629	open my $pg_config_h, '<', "$stdout/pg_config.h" or die "$!";
630	my $match = (grep { /^$regexp/ } <$pg_config_h>);
631	close $pg_config_h;
632	return $match;
633}
634
635=pod
636
637=back
638
639=head1 Test::More-LIKE METHODS
640
641=over
642
643=item command_ok(cmd, test_name)
644
645Check that the command runs (via C<run_log>) successfully.
646
647=cut
648
649sub command_ok
650{
651	local $Test::Builder::Level = $Test::Builder::Level + 1;
652	my ($cmd, $test_name) = @_;
653	my $result = run_log($cmd);
654	ok($result, $test_name);
655	return;
656}
657
658=pod
659
660=item command_fails(cmd, test_name)
661
662Check that the command fails (when run via C<run_log>).
663
664=cut
665
666sub command_fails
667{
668	local $Test::Builder::Level = $Test::Builder::Level + 1;
669	my ($cmd, $test_name) = @_;
670	my $result = run_log($cmd);
671	ok(!$result, $test_name);
672	return;
673}
674
675=pod
676
677=item command_exit_is(cmd, expected, test_name)
678
679Check that the command exit code matches the expected exit code.
680
681=cut
682
683sub command_exit_is
684{
685	local $Test::Builder::Level = $Test::Builder::Level + 1;
686	my ($cmd, $expected, $test_name) = @_;
687	print("# Running: " . join(" ", @{$cmd}) . "\n");
688	my $h = IPC::Run::start $cmd;
689	$h->finish();
690
691	# On Windows, the exit status of the process is returned directly as the
692	# process's exit code, while on Unix, it's returned in the high bits
693	# of the exit code (see WEXITSTATUS macro in the standard <sys/wait.h>
694	# header file). IPC::Run's result function always returns exit code >> 8,
695	# assuming the Unix convention, which will always return 0 on Windows as
696	# long as the process was not terminated by an exception. To work around
697	# that, use $h->full_results on Windows instead.
698	my $result =
699	    ($Config{osname} eq "MSWin32")
700	  ? ($h->full_results)[0]
701	  : $h->result(0);
702	is($result, $expected, $test_name);
703	return;
704}
705
706=pod
707
708=item program_help_ok(cmd)
709
710Check that the command supports the C<--help> option.
711
712=cut
713
714sub program_help_ok
715{
716	local $Test::Builder::Level = $Test::Builder::Level + 1;
717	my ($cmd) = @_;
718	my ($stdout, $stderr);
719	print("# Running: $cmd --help\n");
720	my $result = IPC::Run::run [ $cmd, '--help' ], '>', \$stdout, '2>',
721	  \$stderr;
722	ok($result, "$cmd --help exit code 0");
723	isnt($stdout, '', "$cmd --help goes to stdout");
724	is($stderr, '', "$cmd --help nothing to stderr");
725	return;
726}
727
728=pod
729
730=item program_version_ok(cmd)
731
732Check that the command supports the C<--version> option.
733
734=cut
735
736sub program_version_ok
737{
738	local $Test::Builder::Level = $Test::Builder::Level + 1;
739	my ($cmd) = @_;
740	my ($stdout, $stderr);
741	print("# Running: $cmd --version\n");
742	my $result = IPC::Run::run [ $cmd, '--version' ], '>', \$stdout, '2>',
743	  \$stderr;
744	ok($result, "$cmd --version exit code 0");
745	isnt($stdout, '', "$cmd --version goes to stdout");
746	is($stderr, '', "$cmd --version nothing to stderr");
747	return;
748}
749
750=pod
751
752=item program_options_handling_ok(cmd)
753
754Check that a command with an invalid option returns a non-zero
755exit code and error message.
756
757=cut
758
759sub program_options_handling_ok
760{
761	local $Test::Builder::Level = $Test::Builder::Level + 1;
762	my ($cmd) = @_;
763	my ($stdout, $stderr);
764	print("# Running: $cmd --not-a-valid-option\n");
765	my $result = IPC::Run::run [ $cmd, '--not-a-valid-option' ], '>',
766	  \$stdout,
767	  '2>', \$stderr;
768	ok(!$result, "$cmd with invalid option nonzero exit code");
769	isnt($stderr, '', "$cmd with invalid option prints error message");
770	return;
771}
772
773=pod
774
775=item command_like(cmd, expected_stdout, test_name)
776
777Check that the command runs successfully and the output
778matches the given regular expression.
779
780=cut
781
782sub command_like
783{
784	local $Test::Builder::Level = $Test::Builder::Level + 1;
785	my ($cmd, $expected_stdout, $test_name) = @_;
786	my ($stdout, $stderr);
787	print("# Running: " . join(" ", @{$cmd}) . "\n");
788	my $result = IPC::Run::run $cmd, '>', \$stdout, '2>', \$stderr;
789	ok($result, "$test_name: exit code 0");
790	is($stderr, '', "$test_name: no stderr");
791	$stdout =~ s/\r\n/\n/g if $Config{osname} eq 'msys';
792	like($stdout, $expected_stdout, "$test_name: matches");
793	return;
794}
795
796=pod
797
798=item command_like_safe(cmd, expected_stdout, test_name)
799
800Check that the command runs successfully and the output
801matches the given regular expression.  Doesn't assume that the
802output files are closed.
803
804=cut
805
806sub command_like_safe
807{
808	local $Test::Builder::Level = $Test::Builder::Level + 1;
809
810	# Doesn't rely on detecting end of file on the file descriptors,
811	# which can fail, causing the process to hang, notably on Msys
812	# when used with 'pg_ctl start'
813	my ($cmd, $expected_stdout, $test_name) = @_;
814	my ($stdout, $stderr);
815	my $stdoutfile = File::Temp->new();
816	my $stderrfile = File::Temp->new();
817	print("# Running: " . join(" ", @{$cmd}) . "\n");
818	my $result = IPC::Run::run $cmd, '>', $stdoutfile, '2>', $stderrfile;
819	$stdout = slurp_file($stdoutfile);
820	$stderr = slurp_file($stderrfile);
821	ok($result, "$test_name: exit code 0");
822	is($stderr, '', "$test_name: no stderr");
823	like($stdout, $expected_stdout, "$test_name: matches");
824	return;
825}
826
827=pod
828
829=item command_fails_like(cmd, expected_stderr, test_name)
830
831Check that the command fails and the error message matches
832the given regular expression.
833
834=cut
835
836sub command_fails_like
837{
838	local $Test::Builder::Level = $Test::Builder::Level + 1;
839	my ($cmd, $expected_stderr, $test_name) = @_;
840	my ($stdout, $stderr);
841	print("# Running: " . join(" ", @{$cmd}) . "\n");
842	my $result = IPC::Run::run $cmd, '>', \$stdout, '2>', \$stderr;
843	ok(!$result, "$test_name: exit code not 0");
844	$stderr =~ s/\r\n/\n/g if $Config{osname} eq 'msys';
845	like($stderr, $expected_stderr, "$test_name: matches");
846	return;
847}
848
849=pod
850
851=item command_checks_all(cmd, ret, out, err, test_name)
852
853Run a command and check its status and outputs.
854Arguments:
855
856=over
857
858=item C<cmd>: Array reference of command and arguments to run
859
860=item C<ret>: Expected exit code
861
862=item C<out>: Expected stdout from command
863
864=item C<err>: Expected stderr from command
865
866=item C<test_name>: test name
867
868=back
869
870=cut
871
872sub command_checks_all
873{
874	local $Test::Builder::Level = $Test::Builder::Level + 1;
875
876	my ($cmd, $expected_ret, $out, $err, $test_name) = @_;
877
878	# run command
879	my ($stdout, $stderr);
880	print("# Running: " . join(" ", @{$cmd}) . "\n");
881	IPC::Run::run($cmd, '>', \$stdout, '2>', \$stderr);
882
883	# See http://perldoc.perl.org/perlvar.html#%24CHILD_ERROR
884	my $ret = $?;
885	die "command exited with signal " . ($ret & 127)
886	  if $ret & 127;
887	$ret = $ret >> 8;
888
889	foreach ($stderr, $stdout) { s/\r\n/\n/g if $Config{osname} eq 'msys'; }
890
891	# check status
892	ok($ret == $expected_ret,
893		"$test_name status (got $ret vs expected $expected_ret)");
894
895	# check stdout
896	for my $re (@$out)
897	{
898		like($stdout, $re, "$test_name stdout /$re/");
899	}
900
901	# check stderr
902	for my $re (@$err)
903	{
904		like($stderr, $re, "$test_name stderr /$re/");
905	}
906
907	return;
908}
909
910=pod
911
912=back
913
914=cut
915
9161;
917