|
| 1 | + |
| 2 | +# Copyright (c) 2021-2024, PostgreSQL Global Development Group |
| 3 | +# Portions Copyright (c) 2023-2025, IvorySQL Global Development Team |
| 4 | + |
| 5 | +use strict; |
| 6 | +use warnings FATAL => 'all'; |
| 7 | + |
| 8 | +use PostgreSQL::Test::Cluster; |
| 9 | +use PostgreSQL::Test::Utils; |
| 10 | +use Test::More; |
| 11 | + |
| 12 | +my $tempdir = PostgreSQL::Test::Utils::tempdir; |
| 13 | + |
| 14 | +############################################################### |
| 15 | +# Definition of the pg_dump runs to make. |
| 16 | +# |
| 17 | +# Each of these runs are named and those names are used below |
| 18 | +# to define how each test should (or shouldn't) treat a result |
| 19 | +# from a given run. |
| 20 | +# |
| 21 | +# test_key indicates that a given run should simply use the same |
| 22 | +# set of like/unlike tests as another run, and which run that is. |
| 23 | +# |
| 24 | +# compile_option indicates if the commands run depend on a compilation |
| 25 | +# option, if any. This can be used to control if tests should be |
| 26 | +# skipped when a build dependency is not satisfied. |
| 27 | +# |
| 28 | +# dump_cmd is the pg_dump command to run, which is an array of |
| 29 | +# the full command and arguments to run. Note that this is run |
| 30 | +# using $node->command_ok(), so the port does not need to be |
| 31 | +# specified and is pulled from $PGPORT, which is set by the |
| 32 | +# PostgreSQL::Test::Cluster system. |
| 33 | +# |
| 34 | +# compress_cmd is the utility command for (de)compression, if any. |
| 35 | +# Note that this should generally be used on pg_dump's output |
| 36 | +# either to generate a text file to run the through the tests, or |
| 37 | +# to test pg_restore's ability to parse manually compressed files |
| 38 | +# that otherwise pg_dump does not compress on its own (e.g. *.toc). |
| 39 | +# |
| 40 | +# glob_patterns is an optional array consisting of strings compilable |
| 41 | +# with glob() to check the files generated after a dump. |
| 42 | +# |
| 43 | +# command_like is an optional utility that can be used to compare |
| 44 | +# the output generated by a command on the contents of an existing |
| 45 | +# dump, like the TOC description of a pg_restore command. |
| 46 | +# |
| 47 | +# restore_cmd is the pg_restore command to run, if any. Note |
| 48 | +# that this should generally be used when the pg_dump goes to |
| 49 | +# a non-text file and that the restore can then be used to |
| 50 | +# generate a text file to run through the tests from the |
| 51 | +# non-text file generated by pg_dump. |
| 52 | +# |
| 53 | +# TODO: Have pg_restore actually restore to an independent |
| 54 | +# database and then pg_dump *that* database (or something along |
| 55 | +# those lines) to validate that part of the process. |
| 56 | + |
| 57 | +my %pgdump_runs = ( |
| 58 | + packages => { |
| 59 | + dump_cmd => [ |
| 60 | + 'pg_dump', '--format=plain', |
| 61 | + "--file=$tempdir/packages.sql", '--no-owner','--schema-only', |
| 62 | + 'testdb', |
| 63 | + ], |
| 64 | + }, |
| 65 | + |
| 66 | + clean_if_exists => { |
| 67 | + dump_cmd => [ |
| 68 | + 'pg_dump', |
| 69 | + '--no-sync', |
| 70 | + "--file=$tempdir/clean_if_exists.sql", |
| 71 | + '-c', |
| 72 | + '--if-exists', |
| 73 | + '--encoding=UTF8', # no-op, just tests that option is accepted |
| 74 | + 'testdb', |
| 75 | + ], |
| 76 | + }, |
| 77 | +); |
| 78 | + |
| 79 | +############################################################### |
| 80 | +# Definition of the tests to run. |
| 81 | +# |
| 82 | +# Each test is defined using the log message that will be used. |
| 83 | +# |
| 84 | +# A regexp should be defined for each test which provides the |
| 85 | +# basis for the test. That regexp will be run against the output |
| 86 | +# file of each of the runs which the test is to be run against |
| 87 | +# and the success of the result will depend on if the regexp |
| 88 | +# result matches the expected 'like' or 'unlike' case. |
| 89 | +# |
| 90 | +# The runs listed as 'like' will be checked if they match the |
| 91 | +# regexp and, if so, the test passes. All runs which are not |
| 92 | +# listed as 'like' will be checked to ensure they don't match |
| 93 | +# the regexp; if they do, the test will fail. |
| 94 | +# |
| 95 | +# The below hashes provide convenience sets of runs. Individual |
| 96 | +# runs can be excluded from a general hash by placing that run |
| 97 | +# into the 'unlike' section. |
| 98 | +# |
| 99 | +# For example, there is an 'exclude_test_table' run which runs a |
| 100 | +# full pg_dump but with an exclude flag to not include the test |
| 101 | +# table. The CREATE TABLE test which creates the test table is |
| 102 | +# defined with %full_runs but then has 'exclude_test_table' in |
| 103 | +# its 'unlike' list, excluding that test. |
| 104 | +# |
| 105 | +# There can then be a 'create_sql' and 'create_order' for a |
| 106 | +# given test. The 'create_sql' commands are collected up in |
| 107 | +# 'create_order' and then run against the database prior to any |
| 108 | +# of the pg_dump runs happening. This is what "seeds" the |
| 109 | +# system with objects to be dumped out. |
| 110 | +# |
| 111 | +# There can be a flag called 'lz4', which can be set if the test |
| 112 | +# case depends on LZ4. Tests marked with this flag are skipped if |
| 113 | +# the build used does not support LZ4. |
| 114 | +# |
| 115 | +# Building of this hash takes a bit of time as all of the regexps |
| 116 | +# included in it are compiled. This greatly improves performance |
| 117 | +# as the regexps are used for each run the test applies to. |
| 118 | + |
| 119 | + |
| 120 | +# Tests which are considered 'full' dumps by pg_dump, but there |
| 121 | +# are flags used to exclude specific items (ACLs, LOs, etc). |
| 122 | +my %full_runs = ( |
| 123 | + packages => 1, |
| 124 | + clean_if_exists => 1,); |
| 125 | + |
| 126 | +# This is where the actual tests are defined. |
| 127 | +my %tests = ( |
| 128 | + 'CREATE PACKAGE test_pkg' => { |
| 129 | + create_order => 1, |
| 130 | + create_sql => |
| 131 | + 'CREATE OR REPLACE PACKAGE test_pkg AS |
| 132 | + FUNCTION hello() RETURN text; |
| 133 | + END test_pkg;', |
| 134 | + regexp => qr/^CREATE EDITIONABLE PACKAGE public.test_pkg AUTHID DEFINER AS/m, |
| 135 | + like => { %full_runs,}, |
| 136 | + }, |
| 137 | + |
| 138 | + 'CREATE PACKAGE BODY test_pkg' => { |
| 139 | + create_order => 2, |
| 140 | + create_sql => |
| 141 | + 'CREATE OR REPLACE PACKAGE BODY test_pkg AS |
| 142 | + FUNCTION hello() RETURN text IS |
| 143 | + BEGIN |
| 144 | + RETURN \'Hello from test package\'; |
| 145 | + END; |
| 146 | + END test_pkg;', |
| 147 | + regexp => qr/^CREATE PACKAGE BODY public.test_pkg AS/m, |
| 148 | + like => { %full_runs, }, |
| 149 | + }, |
| 150 | + |
| 151 | + 'DROP PACKAGE IF EXISTS public.test_pkg;' => { |
| 152 | + regexp => qr/^DROP PACKAGE IF EXISTS public.test_pkg;/m, |
| 153 | + like => { clean_if_exists => 1, }, |
| 154 | + }, |
| 155 | +); |
| 156 | + |
| 157 | +######################################### |
| 158 | +# Create a PG instance to test actually dumping from |
| 159 | + |
| 160 | +my $node = PostgreSQL::Test::Cluster->new('main'); |
| 161 | +$node->init; |
| 162 | +$node->start; |
| 163 | + |
| 164 | +my ($connect_to_oraport); |
| 165 | + |
| 166 | +# ICU doesn't work with some encodings |
| 167 | +my $encoding = $node->safe_psql('postgres', 'show server_encoding', connect_to_oraport => 1); |
| 168 | + |
| 169 | +# Create additional databases for mutations of schema public |
| 170 | +$node->psql('postgres', 'create database testdb;', connect_to_oraport => 1); |
| 171 | + |
| 172 | +######################################### |
| 173 | +# Set up schemas, tables, etc, to be dumped. |
| 174 | + |
| 175 | +# Build up the create statements |
| 176 | +my %create_sql = (); |
| 177 | + |
| 178 | +foreach my $test ( |
| 179 | + sort { |
| 180 | + if ($tests{$a}->{create_order} and $tests{$b}->{create_order}) |
| 181 | + { |
| 182 | + $tests{$a}->{create_order} <=> $tests{$b}->{create_order}; |
| 183 | + } |
| 184 | + elsif ($tests{$a}->{create_order}) |
| 185 | + { |
| 186 | + -1; |
| 187 | + } |
| 188 | + elsif ($tests{$b}->{create_order}) |
| 189 | + { |
| 190 | + 1; |
| 191 | + } |
| 192 | + else |
| 193 | + { |
| 194 | + 0; |
| 195 | + } |
| 196 | + } keys %tests) |
| 197 | +{ |
| 198 | + my $test_db = 'testdb'; |
| 199 | + |
| 200 | + if (defined($tests{$test}->{database})) |
| 201 | + { |
| 202 | + $test_db = $tests{$test}->{database}; |
| 203 | + } |
| 204 | + |
| 205 | + if ($tests{$test}->{create_sql}) |
| 206 | + { |
| 207 | + # Normalize command ending: strip all line endings, add |
| 208 | + # semicolon if missing, add two newlines. |
| 209 | + my $create_sql = $tests{$test}->{create_sql}; |
| 210 | + chomp $create_sql; |
| 211 | + $create_sql .= ';' unless substr($create_sql, -1) eq ';'; |
| 212 | + $create_sql{$test_db} .= $create_sql . "\n\n"; |
| 213 | + } |
| 214 | +} |
| 215 | + |
| 216 | +# Send the combined set of commands to psql |
| 217 | +foreach my $db (sort keys %create_sql) |
| 218 | +{ |
| 219 | + $node->safe_psql($db, $create_sql{$db}, connect_to_oraport => 1); |
| 220 | +} |
| 221 | + |
| 222 | +######################################### |
| 223 | +# Run all runs |
| 224 | + |
| 225 | +foreach my $run (sort keys %pgdump_runs) |
| 226 | +{ |
| 227 | + my $test_key = $run; |
| 228 | + my $run_db = 'testdb'; |
| 229 | + |
| 230 | + |
| 231 | + $node->command_ok(\@{ $pgdump_runs{$run}->{dump_cmd} }, |
| 232 | + "$run: pg_dump runs"); |
| 233 | + |
| 234 | + if ($pgdump_runs{$run}->{compress_cmd}) |
| 235 | + { |
| 236 | + my ($compress_cmd) = $pgdump_runs{$run}->{compress_cmd}; |
| 237 | + my $compress_program = $compress_cmd->{program}; |
| 238 | + |
| 239 | + # Skip the rest of the test if the compression program is |
| 240 | + # not defined. |
| 241 | + next if (!defined($compress_program) || $compress_program eq ''); |
| 242 | + |
| 243 | + # Arguments may require globbing. |
| 244 | + my @full_compress_cmd = ($compress_program); |
| 245 | + foreach my $arg (@{ $compress_cmd->{args} }) |
| 246 | + { |
| 247 | + push @full_compress_cmd, glob($arg); |
| 248 | + } |
| 249 | + |
| 250 | + command_ok(\@full_compress_cmd, "$run: compression commands"); |
| 251 | + } |
| 252 | + |
| 253 | + if ($pgdump_runs{$run}->{glob_patterns}) |
| 254 | + { |
| 255 | + my $glob_patterns = $pgdump_runs{$run}->{glob_patterns}; |
| 256 | + foreach my $glob_pattern (@{$glob_patterns}) |
| 257 | + { |
| 258 | + my @glob_output = glob($glob_pattern); |
| 259 | + is(scalar(@glob_output) > 0, 1, "$run: glob check for $glob_pattern"); |
| 260 | + } |
| 261 | + } |
| 262 | + |
| 263 | + if ($pgdump_runs{$run}->{command_like}) |
| 264 | + { |
| 265 | + my $cmd_like = $pgdump_runs{$run}->{command_like}; |
| 266 | + $node->command_like(\@{ $cmd_like->{command} }, |
| 267 | + $cmd_like->{expected}, |
| 268 | + "$run: " . $cmd_like->{name}) |
| 269 | + } |
| 270 | + |
| 271 | + if ($pgdump_runs{$run}->{restore_cmd}) |
| 272 | + { |
| 273 | + $node->command_ok(\@{ $pgdump_runs{$run}->{restore_cmd} }, |
| 274 | + "$run: pg_restore runs"); |
| 275 | + } |
| 276 | + |
| 277 | + if ($pgdump_runs{$run}->{test_key}) |
| 278 | + { |
| 279 | + $test_key = $pgdump_runs{$run}->{test_key}; |
| 280 | + } |
| 281 | + |
| 282 | + my $output_file = slurp_file("$tempdir/${run}.sql"); |
| 283 | + |
| 284 | + ######################################### |
| 285 | + # Run all tests where this run is included |
| 286 | + # as either a 'like' or 'unlike' test. |
| 287 | + |
| 288 | + foreach my $test (sort keys %tests) |
| 289 | + { |
| 290 | + my $test_db = 'testdb'; |
| 291 | + |
| 292 | + if (defined($pgdump_runs{$run}->{database})) |
| 293 | + { |
| 294 | + $run_db = $pgdump_runs{$run}->{database}; |
| 295 | + } |
| 296 | + |
| 297 | + if (defined($tests{$test}->{database})) |
| 298 | + { |
| 299 | + $test_db = $tests{$test}->{database}; |
| 300 | + } |
| 301 | + |
| 302 | + # Check for proper test definitions |
| 303 | + # |
| 304 | + # There should be a "like" list, even if it is empty. (This |
| 305 | + # makes the test more self-documenting.) |
| 306 | + if (!defined($tests{$test}->{like})) |
| 307 | + { |
| 308 | + die "missing \"like\" in test \"$test\""; |
| 309 | + } |
| 310 | + # Check for useless entries in "unlike" list. Runs that are |
| 311 | + # not listed in "like" don't need to be excluded in "unlike". |
| 312 | + if ($tests{$test}->{unlike}->{$test_key} |
| 313 | + && !defined($tests{$test}->{like}->{$test_key})) |
| 314 | + { |
| 315 | + die "useless \"unlike\" entry \"$test_key\" in test \"$test\""; |
| 316 | + } |
| 317 | + |
| 318 | + if ($run_db ne $test_db) |
| 319 | + { |
| 320 | + next; |
| 321 | + } |
| 322 | + |
| 323 | + # Run the test listed as a like, unless it is specifically noted |
| 324 | + # as an unlike (generally due to an explicit exclusion or similar). |
| 325 | + if ($tests{$test}->{like}->{$test_key} |
| 326 | + && !defined($tests{$test}->{unlike}->{$test_key})) |
| 327 | + { |
| 328 | + if (!ok($output_file =~ $tests{$test}->{regexp}, |
| 329 | + "$run: should dump $test")) |
| 330 | + { |
| 331 | + diag("Review $run results in $tempdir"); |
| 332 | + } |
| 333 | + } |
| 334 | + else |
| 335 | + { |
| 336 | + if (!ok($output_file !~ $tests{$test}->{regexp}, |
| 337 | + "$run: should not dump $test")) |
| 338 | + { |
| 339 | + diag("Review $run results in $tempdir"); |
| 340 | + } |
| 341 | + } |
| 342 | + } |
| 343 | +} |
| 344 | + |
| 345 | +######################################### |
| 346 | +# Stop the database instance, which will be removed at the end of the tests. |
| 347 | + |
| 348 | +$node->stop('fast'); |
| 349 | + |
| 350 | +done_testing(); |
0 commit comments