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