diff options
Diffstat (limited to 'src/test/regress/pg_regress.c')
-rw-r--r-- | src/test/regress/pg_regress.c | 2697 |
1 files changed, 2697 insertions, 0 deletions
diff --git a/src/test/regress/pg_regress.c b/src/test/regress/pg_regress.c new file mode 100644 index 0000000..27486cf --- /dev/null +++ b/src/test/regress/pg_regress.c @@ -0,0 +1,2697 @@ +/*------------------------------------------------------------------------- + * + * pg_regress --- regression test driver + * + * This is a C implementation of the previous shell script for running + * the regression tests, and should be mostly compatible with it. + * Initial author of C translation: Magnus Hagander + * + * This code is released under the terms of the PostgreSQL License. + * + * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/test/regress/pg_regress.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include <ctype.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <signal.h> +#include <unistd.h> + +#ifdef HAVE_SYS_RESOURCE_H +#include <sys/time.h> +#include <sys/resource.h> +#endif + +#include "common/logging.h" +#include "common/restricted_token.h" +#include "common/string.h" +#include "common/username.h" +#include "getopt_long.h" +#include "lib/stringinfo.h" +#include "libpq/pqcomm.h" /* needed for UNIXSOCK_PATH() */ +#include "pg_config_paths.h" +#include "pg_regress.h" +#include "portability/instr_time.h" + +/* for resultmap we need a list of pairs of strings */ +typedef struct _resultmap +{ + char *test; + char *type; + char *resultfile; + struct _resultmap *next; +} _resultmap; + +/* + * Values obtained from Makefile. + */ +char *host_platform = HOST_TUPLE; + +#ifndef WIN32 /* not used in WIN32 case */ +static char *shellprog = SHELLPROG; +#endif + +/* + * On Windows we use -w in diff switches to avoid problems with inconsistent + * newline representation. The actual result files will generally have + * Windows-style newlines, but the comparison files might or might not. + */ +#ifndef WIN32 +const char *basic_diff_opts = ""; +const char *pretty_diff_opts = "-U3"; +#else +const char *basic_diff_opts = "-w"; +const char *pretty_diff_opts = "-w -U3"; +#endif + +/* options settable from command line */ +_stringlist *dblist = NULL; +bool debug = false; +char *inputdir = "."; +char *outputdir = "."; +char *bindir = PGBINDIR; +char *launcher = NULL; +static _stringlist *loadextension = NULL; +static int max_connections = 0; +static int max_concurrent_tests = 0; +static char *encoding = NULL; +static _stringlist *schedulelist = NULL; +static _stringlist *extra_tests = NULL; +static char *temp_instance = NULL; +static _stringlist *temp_configs = NULL; +static bool nolocale = false; +static bool use_existing = false; +static char *hostname = NULL; +static int port = -1; +static bool port_specified_by_user = false; +static char *dlpath = PKGLIBDIR; +static char *user = NULL; +static _stringlist *extraroles = NULL; +static char *config_auth_datadir = NULL; + +/* internal variables */ +static const char *progname; +static char *logfilename; +static FILE *logfile; +static char *difffilename; +static const char *sockdir; +#ifdef HAVE_UNIX_SOCKETS +static const char *temp_sockdir; +static char sockself[MAXPGPATH]; +static char socklock[MAXPGPATH]; +#endif + +static _resultmap *resultmap = NULL; + +static PID_TYPE postmaster_pid = INVALID_PID; +static bool postmaster_running = false; + +static int success_count = 0; +static int fail_count = 0; +static int fail_ignore_count = 0; + +static bool directory_exists(const char *dir); +static void make_directory(const char *dir); + +static void header(const char *fmt,...) pg_attribute_printf(1, 2); +static void status(const char *fmt,...) pg_attribute_printf(1, 2); +static void psql_command(const char *database, const char *query,...) pg_attribute_printf(2, 3); + +/* + * allow core files if possible. + */ +#if defined(HAVE_GETRLIMIT) && defined(RLIMIT_CORE) +static void +unlimit_core_size(void) +{ + struct rlimit lim; + + getrlimit(RLIMIT_CORE, &lim); + if (lim.rlim_max == 0) + { + fprintf(stderr, + _("%s: could not set core size: disallowed by hard limit\n"), + progname); + return; + } + else if (lim.rlim_max == RLIM_INFINITY || lim.rlim_cur < lim.rlim_max) + { + lim.rlim_cur = lim.rlim_max; + setrlimit(RLIMIT_CORE, &lim); + } +} +#endif + + +/* + * Add an item at the end of a stringlist. + */ +void +add_stringlist_item(_stringlist **listhead, const char *str) +{ + _stringlist *newentry = pg_malloc(sizeof(_stringlist)); + _stringlist *oldentry; + + newentry->str = pg_strdup(str); + newentry->next = NULL; + if (*listhead == NULL) + *listhead = newentry; + else + { + for (oldentry = *listhead; oldentry->next; oldentry = oldentry->next) + /* skip */ ; + oldentry->next = newentry; + } +} + +/* + * Free a stringlist. + */ +static void +free_stringlist(_stringlist **listhead) +{ + if (listhead == NULL || *listhead == NULL) + return; + if ((*listhead)->next != NULL) + free_stringlist(&((*listhead)->next)); + free((*listhead)->str); + free(*listhead); + *listhead = NULL; +} + +/* + * Split a delimited string into a stringlist + */ +static void +split_to_stringlist(const char *s, const char *delim, _stringlist **listhead) +{ + char *sc = pg_strdup(s); + char *token = strtok(sc, delim); + + while (token) + { + add_stringlist_item(listhead, token); + token = strtok(NULL, delim); + } + free(sc); +} + +/* + * Print a progress banner on stdout. + */ +static void +header(const char *fmt,...) +{ + char tmp[64]; + va_list ap; + + va_start(ap, fmt); + vsnprintf(tmp, sizeof(tmp), fmt, ap); + va_end(ap); + + fprintf(stdout, "============== %-38s ==============\n", tmp); + fflush(stdout); +} + +/* + * Print "doing something ..." --- supplied text should not end with newline + */ +static void +status(const char *fmt,...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stdout, fmt, ap); + fflush(stdout); + va_end(ap); + + if (logfile) + { + va_start(ap, fmt); + vfprintf(logfile, fmt, ap); + va_end(ap); + } +} + +/* + * Done "doing something ..." + */ +static void +status_end(void) +{ + fprintf(stdout, "\n"); + fflush(stdout); + if (logfile) + fprintf(logfile, "\n"); +} + +/* + * shut down temp postmaster + */ +static void +stop_postmaster(void) +{ + if (postmaster_running) + { + /* We use pg_ctl to issue the kill and wait for stop */ + char buf[MAXPGPATH * 2]; + int r; + + /* On Windows, system() seems not to force fflush, so... */ + fflush(stdout); + fflush(stderr); + + snprintf(buf, sizeof(buf), + "\"%s%spg_ctl\" stop -D \"%s/data\" -s", + bindir ? bindir : "", + bindir ? "/" : "", + temp_instance); + r = system(buf); + if (r != 0) + { + fprintf(stderr, _("\n%s: could not stop postmaster: exit code was %d\n"), + progname, r); + _exit(2); /* not exit(), that could be recursive */ + } + + postmaster_running = false; + } +} + +#ifdef HAVE_UNIX_SOCKETS +/* + * Remove the socket temporary directory. pg_regress never waits for a + * postmaster exit, so it is indeterminate whether the postmaster has yet to + * unlink the socket and lock file. Unlink them here so we can proceed to + * remove the directory. Ignore errors; leaking a temporary directory is + * unimportant. This can run from a signal handler. The code is not + * acceptable in a Windows signal handler (see initdb.c:trapsig()), but + * on Windows, pg_regress does not use Unix sockets by default. + */ +static void +remove_temp(void) +{ + Assert(temp_sockdir); + unlink(sockself); + unlink(socklock); + rmdir(temp_sockdir); +} + +/* + * Signal handler that calls remove_temp() and reraises the signal. + */ +static void +signal_remove_temp(int signum) +{ + remove_temp(); + + pqsignal(signum, SIG_DFL); + raise(signum); +} + +/* + * Create a temporary directory suitable for the server's Unix-domain socket. + * The directory will have mode 0700 or stricter, so no other OS user can open + * our socket to exploit our use of trust authentication. Most systems + * constrain the length of socket paths well below _POSIX_PATH_MAX, so we + * place the directory under /tmp rather than relative to the possibly-deep + * current working directory. + * + * Compared to using the compiled-in DEFAULT_PGSOCKET_DIR, this also permits + * testing to work in builds that relocate it to a directory not writable to + * the build/test user. + */ +static const char * +make_temp_sockdir(void) +{ + char *template = psprintf("%s/pg_regress-XXXXXX", + getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp"); + + temp_sockdir = mkdtemp(template); + if (temp_sockdir == NULL) + { + fprintf(stderr, _("%s: could not create directory \"%s\": %s\n"), + progname, template, strerror(errno)); + exit(2); + } + + /* Stage file names for remove_temp(). Unsafe in a signal handler. */ + UNIXSOCK_PATH(sockself, port, temp_sockdir); + snprintf(socklock, sizeof(socklock), "%s.lock", sockself); + + /* Remove the directory during clean exit. */ + atexit(remove_temp); + + /* + * Remove the directory before dying to the usual signals. Omit SIGQUIT, + * preserving it as a quick, untidy exit. + */ + pqsignal(SIGHUP, signal_remove_temp); + pqsignal(SIGINT, signal_remove_temp); + pqsignal(SIGPIPE, signal_remove_temp); + pqsignal(SIGTERM, signal_remove_temp); + + return temp_sockdir; +} +#endif /* HAVE_UNIX_SOCKETS */ + +/* + * Check whether string matches pattern + * + * In the original shell script, this function was implemented using expr(1), + * which provides basic regular expressions restricted to match starting at + * the string start (in conventional regex terms, there's an implicit "^" + * at the start of the pattern --- but no implicit "$" at the end). + * + * For now, we only support "." and ".*" as non-literal metacharacters, + * because that's all that anyone has found use for in resultmap. This + * code could be extended if more functionality is needed. + */ +static bool +string_matches_pattern(const char *str, const char *pattern) +{ + while (*str && *pattern) + { + if (*pattern == '.' && pattern[1] == '*') + { + pattern += 2; + /* Trailing .* matches everything. */ + if (*pattern == '\0') + return true; + + /* + * Otherwise, scan for a text position at which we can match the + * rest of the pattern. + */ + while (*str) + { + /* + * Optimization to prevent most recursion: don't recurse + * unless first pattern char might match this text char. + */ + if (*str == *pattern || *pattern == '.') + { + if (string_matches_pattern(str, pattern)) + return true; + } + + str++; + } + + /* + * End of text with no match. + */ + return false; + } + else if (*pattern != '.' && *str != *pattern) + { + /* + * Not the single-character wildcard and no explicit match? Then + * time to quit... + */ + return false; + } + + str++; + pattern++; + } + + if (*pattern == '\0') + return true; /* end of pattern, so declare match */ + + /* End of input string. Do we have matching pattern remaining? */ + while (*pattern == '.' && pattern[1] == '*') + pattern += 2; + if (*pattern == '\0') + return true; /* end of pattern, so declare match */ + + return false; +} + +/* + * Replace all occurrences of "replace" in "string" with "replacement". + * The StringInfo will be suitably enlarged if necessary. + * + * Note: this is optimized on the assumption that most calls will find + * no more than one occurrence of "replace", and quite likely none. + */ +void +replace_string(StringInfo string, const char *replace, const char *replacement) +{ + int pos = 0; + char *ptr; + + while ((ptr = strstr(string->data + pos, replace)) != NULL) + { + /* Must copy the remainder of the string out of the StringInfo */ + char *suffix = pg_strdup(ptr + strlen(replace)); + + /* Truncate StringInfo at start of found string ... */ + string->len = ptr - string->data; + /* ... and append the replacement (this restores the trailing '\0') */ + appendStringInfoString(string, replacement); + /* Next search should start after the replacement */ + pos = string->len; + /* Put back the remainder of the string */ + appendStringInfoString(string, suffix); + free(suffix); + } +} + +/* + * Convert *.source found in the "source" directory, replacing certain tokens + * in the file contents with their intended values, and put the resulting files + * in the "dest" directory, replacing the ".source" prefix in their names with + * the given suffix. + */ +static void +convert_sourcefiles_in(const char *source_subdir, const char *dest_dir, const char *dest_subdir, const char *suffix) +{ + char testtablespace[MAXPGPATH]; + char indir[MAXPGPATH]; + char outdir_sub[MAXPGPATH]; + char **name; + char **names; + int count = 0; + + snprintf(indir, MAXPGPATH, "%s/%s", inputdir, source_subdir); + + /* Check that indir actually exists and is a directory */ + if (!directory_exists(indir)) + { + /* + * No warning, to avoid noise in tests that do not have these + * directories; for example, ecpg, contrib and src/pl. + */ + return; + } + + names = pgfnames(indir); + if (!names) + /* Error logged in pgfnames */ + exit(2); + + /* Create the "dest" subdirectory if not present */ + snprintf(outdir_sub, MAXPGPATH, "%s/%s", dest_dir, dest_subdir); + if (!directory_exists(outdir_sub)) + make_directory(outdir_sub); + + /* We might need to replace @testtablespace@ */ + snprintf(testtablespace, MAXPGPATH, "%s/testtablespace", outputdir); + + /* finally loop on each file and do the replacement */ + for (name = names; *name; name++) + { + char srcfile[MAXPGPATH]; + char destfile[MAXPGPATH]; + char prefix[MAXPGPATH]; + FILE *infile, + *outfile; + StringInfoData line; + + /* reject filenames not finishing in ".source" */ + if (strlen(*name) < 8) + continue; + if (strcmp(*name + strlen(*name) - 7, ".source") != 0) + continue; + + count++; + + /* build the full actual paths to open */ + snprintf(prefix, strlen(*name) - 6, "%s", *name); + snprintf(srcfile, MAXPGPATH, "%s/%s", indir, *name); + snprintf(destfile, MAXPGPATH, "%s/%s/%s.%s", dest_dir, dest_subdir, + prefix, suffix); + + infile = fopen(srcfile, "r"); + if (!infile) + { + fprintf(stderr, _("%s: could not open file \"%s\" for reading: %s\n"), + progname, srcfile, strerror(errno)); + exit(2); + } + outfile = fopen(destfile, "w"); + if (!outfile) + { + fprintf(stderr, _("%s: could not open file \"%s\" for writing: %s\n"), + progname, destfile, strerror(errno)); + exit(2); + } + + initStringInfo(&line); + + while (pg_get_line_buf(infile, &line)) + { + replace_string(&line, "@abs_srcdir@", inputdir); + replace_string(&line, "@abs_builddir@", outputdir); + replace_string(&line, "@testtablespace@", testtablespace); + replace_string(&line, "@libdir@", dlpath); + replace_string(&line, "@DLSUFFIX@", DLSUFFIX); + fputs(line.data, outfile); + } + + pfree(line.data); + fclose(infile); + fclose(outfile); + } + + /* + * If we didn't process any files, complain because it probably means + * somebody neglected to pass the needed --inputdir argument. + */ + if (count <= 0) + { + fprintf(stderr, _("%s: no *.source files found in \"%s\"\n"), + progname, indir); + exit(2); + } + + pgfnames_cleanup(names); +} + +/* Create the .sql and .out files from the .source files, if any */ +static void +convert_sourcefiles(void) +{ + convert_sourcefiles_in("input", outputdir, "sql", "sql"); + convert_sourcefiles_in("output", outputdir, "expected", "out"); +} + +/* + * Clean out the test tablespace dir, or create it if it doesn't exist. + * + * On Windows, doing this cleanup here makes it possible to run the + * regression tests under a Windows administrative user account with the + * restricted token obtained when starting pg_regress. + */ +static void +prepare_testtablespace_dir(void) +{ + char testtablespace[MAXPGPATH]; + + snprintf(testtablespace, MAXPGPATH, "%s/testtablespace", outputdir); + + if (directory_exists(testtablespace)) + { + if (!rmtree(testtablespace, true)) + { + fprintf(stderr, _("\n%s: could not remove test tablespace \"%s\"\n"), + progname, testtablespace); + exit(2); + } + } + make_directory(testtablespace); +} + +/* + * Scan resultmap file to find which platform-specific expected files to use. + * + * The format of each line of the file is + * testname/hostplatformpattern=substitutefile + * where the hostplatformpattern is evaluated per the rules of expr(1), + * namely, it is a standard regular expression with an implicit ^ at the start. + * (We currently support only a very limited subset of regular expressions, + * see string_matches_pattern() above.) What hostplatformpattern will be + * matched against is the config.guess output. (In the shell-script version, + * we also provided an indication of whether gcc or another compiler was in + * use, but that facility isn't used anymore.) + */ +static void +load_resultmap(void) +{ + char buf[MAXPGPATH]; + FILE *f; + + /* scan the file ... */ + snprintf(buf, sizeof(buf), "%s/resultmap", inputdir); + f = fopen(buf, "r"); + if (!f) + { + /* OK if it doesn't exist, else complain */ + if (errno == ENOENT) + return; + fprintf(stderr, _("%s: could not open file \"%s\" for reading: %s\n"), + progname, buf, strerror(errno)); + exit(2); + } + + while (fgets(buf, sizeof(buf), f)) + { + char *platform; + char *file_type; + char *expected; + int i; + + /* strip trailing whitespace, especially the newline */ + i = strlen(buf); + while (i > 0 && isspace((unsigned char) buf[i - 1])) + buf[--i] = '\0'; + + /* parse out the line fields */ + file_type = strchr(buf, ':'); + if (!file_type) + { + fprintf(stderr, _("incorrectly formatted resultmap entry: %s\n"), + buf); + exit(2); + } + *file_type++ = '\0'; + + platform = strchr(file_type, ':'); + if (!platform) + { + fprintf(stderr, _("incorrectly formatted resultmap entry: %s\n"), + buf); + exit(2); + } + *platform++ = '\0'; + expected = strchr(platform, '='); + if (!expected) + { + fprintf(stderr, _("incorrectly formatted resultmap entry: %s\n"), + buf); + exit(2); + } + *expected++ = '\0'; + + /* + * if it's for current platform, save it in resultmap list. Note: by + * adding at the front of the list, we ensure that in ambiguous cases, + * the last match in the resultmap file is used. This mimics the + * behavior of the old shell script. + */ + if (string_matches_pattern(host_platform, platform)) + { + _resultmap *entry = pg_malloc(sizeof(_resultmap)); + + entry->test = pg_strdup(buf); + entry->type = pg_strdup(file_type); + entry->resultfile = pg_strdup(expected); + entry->next = resultmap; + resultmap = entry; + } + } + fclose(f); +} + +/* + * Check in resultmap if we should be looking at a different file + */ +static +const char * +get_expectfile(const char *testname, const char *file) +{ + char *file_type; + _resultmap *rm; + + /* + * Determine the file type from the file name. This is just what is + * following the last dot in the file name. + */ + if (!file || !(file_type = strrchr(file, '.'))) + return NULL; + + file_type++; + + for (rm = resultmap; rm != NULL; rm = rm->next) + { + if (strcmp(testname, rm->test) == 0 && strcmp(file_type, rm->type) == 0) + { + return rm->resultfile; + } + } + + return NULL; +} + +/* + * Prepare environment variables for running regression tests + */ +static void +initialize_environment(void) +{ + /* + * Set default application_name. (The test_start_function may choose to + * override this, but if it doesn't, we have something useful in place.) + */ + setenv("PGAPPNAME", "pg_regress", 1); + + if (nolocale) + { + /* + * Clear out any non-C locale settings + */ + unsetenv("LC_COLLATE"); + unsetenv("LC_CTYPE"); + unsetenv("LC_MONETARY"); + unsetenv("LC_NUMERIC"); + unsetenv("LC_TIME"); + unsetenv("LANG"); + + /* + * Most platforms have adopted the POSIX locale as their + * implementation-defined default locale. Exceptions include native + * Windows, macOS with --enable-nls, and Cygwin with --enable-nls. + * (Use of --enable-nls matters because libintl replaces setlocale().) + * Also, PostgreSQL does not support macOS with locale environment + * variables unset; see PostmasterMain(). + */ +#if defined(WIN32) || defined(__CYGWIN__) || defined(__darwin__) + setenv("LANG", "C", 1); +#endif + } + + /* + * Set translation-related settings to English; otherwise psql will + * produce translated messages and produce diffs. (XXX If we ever support + * translation of pg_regress, this needs to be moved elsewhere, where psql + * is actually called.) + */ + unsetenv("LANGUAGE"); + unsetenv("LC_ALL"); + setenv("LC_MESSAGES", "C", 1); + + /* + * Set encoding as requested + */ + if (encoding) + setenv("PGCLIENTENCODING", encoding, 1); + else + unsetenv("PGCLIENTENCODING"); + + /* + * Set timezone and datestyle for datetime-related tests + */ + setenv("PGTZ", "PST8PDT", 1); + setenv("PGDATESTYLE", "Postgres, MDY", 1); + + /* + * Likewise set intervalstyle to ensure consistent results. This is a bit + * more painful because we must use PGOPTIONS, and we want to preserve the + * user's ability to set other variables through that. + */ + { + const char *my_pgoptions = "-c intervalstyle=postgres_verbose"; + const char *old_pgoptions = getenv("PGOPTIONS"); + char *new_pgoptions; + + if (!old_pgoptions) + old_pgoptions = ""; + new_pgoptions = psprintf("%s %s", + old_pgoptions, my_pgoptions); + setenv("PGOPTIONS", new_pgoptions, 1); + free(new_pgoptions); + } + + if (temp_instance) + { + /* + * Clear out any environment vars that might cause psql to connect to + * the wrong postmaster, or otherwise behave in nondefault ways. (Note + * we also use psql's -X switch consistently, so that ~/.psqlrc files + * won't mess things up.) Also, set PGPORT to the temp port, and set + * PGHOST depending on whether we are using TCP or Unix sockets. + * + * This list should be kept in sync with TestLib.pm. + */ + unsetenv("PGCHANNELBINDING"); + /* PGCLIENTENCODING, see above */ + unsetenv("PGCONNECT_TIMEOUT"); + unsetenv("PGDATA"); + unsetenv("PGDATABASE"); + unsetenv("PGGSSENCMODE"); + unsetenv("PGGSSLIB"); + /* PGHOSTADDR, see below */ + unsetenv("PGKRBSRVNAME"); + unsetenv("PGPASSFILE"); + unsetenv("PGPASSWORD"); + unsetenv("PGREQUIREPEER"); + unsetenv("PGREQUIRESSL"); + unsetenv("PGSERVICE"); + unsetenv("PGSERVICEFILE"); + unsetenv("PGSSLCERT"); + unsetenv("PGSSLCRL"); + unsetenv("PGSSLCRLDIR"); + unsetenv("PGSSLKEY"); + unsetenv("PGSSLMAXPROTOCOLVERSION"); + unsetenv("PGSSLMINPROTOCOLVERSION"); + unsetenv("PGSSLMODE"); + unsetenv("PGSSLROOTCERT"); + unsetenv("PGSSLSNI"); + unsetenv("PGTARGETSESSIONATTRS"); + unsetenv("PGUSER"); + /* PGPORT, see below */ + /* PGHOST, see below */ + +#ifdef HAVE_UNIX_SOCKETS + if (hostname != NULL) + setenv("PGHOST", hostname, 1); + else + { + sockdir = getenv("PG_REGRESS_SOCK_DIR"); + if (!sockdir) + sockdir = make_temp_sockdir(); + setenv("PGHOST", sockdir, 1); + } +#else + Assert(hostname != NULL); + setenv("PGHOST", hostname, 1); +#endif + unsetenv("PGHOSTADDR"); + if (port != -1) + { + char s[16]; + + sprintf(s, "%d", port); + setenv("PGPORT", s, 1); + } + } + else + { + const char *pghost; + const char *pgport; + + /* + * When testing an existing install, we honor existing environment + * variables, except if they're overridden by command line options. + */ + if (hostname != NULL) + { + setenv("PGHOST", hostname, 1); + unsetenv("PGHOSTADDR"); + } + if (port != -1) + { + char s[16]; + + sprintf(s, "%d", port); + setenv("PGPORT", s, 1); + } + if (user != NULL) + setenv("PGUSER", user, 1); + + /* + * However, we *don't* honor PGDATABASE, since we certainly don't wish + * to connect to whatever database the user might like as default. + * (Most tests override PGDATABASE anyway, but there are some ECPG + * test cases that don't.) + */ + unsetenv("PGDATABASE"); + + /* + * Report what we're connecting to + */ + pghost = getenv("PGHOST"); + pgport = getenv("PGPORT"); + if (!pghost) + { + /* Keep this bit in sync with libpq's default host location: */ +#ifdef HAVE_UNIX_SOCKETS + if (DEFAULT_PGSOCKET_DIR[0]) + /* do nothing, we'll print "Unix socket" below */ ; + else +#endif + pghost = "localhost"; /* DefaultHost in fe-connect.c */ + } + + if (pghost && pgport) + printf(_("(using postmaster on %s, port %s)\n"), pghost, pgport); + if (pghost && !pgport) + printf(_("(using postmaster on %s, default port)\n"), pghost); + if (!pghost && pgport) + printf(_("(using postmaster on Unix socket, port %s)\n"), pgport); + if (!pghost && !pgport) + printf(_("(using postmaster on Unix socket, default port)\n")); + } + + convert_sourcefiles(); + load_resultmap(); +} + +#ifdef ENABLE_SSPI + +/* support for config_sspi_auth() */ +static const char * +fmtHba(const char *raw) +{ + static char *ret; + const char *rp; + char *wp; + + wp = ret = realloc(ret, 3 + strlen(raw) * 2); + + *wp++ = '"'; + for (rp = raw; *rp; rp++) + { + if (*rp == '"') + *wp++ = '"'; + *wp++ = *rp; + } + *wp++ = '"'; + *wp++ = '\0'; + + return ret; +} + +/* + * Get account and domain/realm names for the current user. This is based on + * pg_SSPI_recvauth(). The returned strings use static storage. + */ +static void +current_windows_user(const char **acct, const char **dom) +{ + static char accountname[MAXPGPATH]; + static char domainname[MAXPGPATH]; + HANDLE token; + TOKEN_USER *tokenuser; + DWORD retlen; + DWORD accountnamesize = sizeof(accountname); + DWORD domainnamesize = sizeof(domainname); + SID_NAME_USE accountnameuse; + + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_READ, &token)) + { + fprintf(stderr, + _("%s: could not open process token: error code %lu\n"), + progname, GetLastError()); + exit(2); + } + + if (!GetTokenInformation(token, TokenUser, NULL, 0, &retlen) && GetLastError() != 122) + { + fprintf(stderr, + _("%s: could not get token information buffer size: error code %lu\n"), + progname, GetLastError()); + exit(2); + } + tokenuser = pg_malloc(retlen); + if (!GetTokenInformation(token, TokenUser, tokenuser, retlen, &retlen)) + { + fprintf(stderr, + _("%s: could not get token information: error code %lu\n"), + progname, GetLastError()); + exit(2); + } + + if (!LookupAccountSid(NULL, tokenuser->User.Sid, accountname, &accountnamesize, + domainname, &domainnamesize, &accountnameuse)) + { + fprintf(stderr, + _("%s: could not look up account SID: error code %lu\n"), + progname, GetLastError()); + exit(2); + } + + free(tokenuser); + + *acct = accountname; + *dom = domainname; +} + +/* + * Rewrite pg_hba.conf and pg_ident.conf to use SSPI authentication. Permit + * the current OS user to authenticate as the bootstrap superuser and as any + * user named in a --create-role option. + * + * In --config-auth mode, the --user switch can be used to specify the + * bootstrap superuser's name, otherwise we assume it is the default. + */ +static void +config_sspi_auth(const char *pgdata, const char *superuser_name) +{ + const char *accountname, + *domainname; + char *errstr; + bool have_ipv6; + char fname[MAXPGPATH]; + int res; + FILE *hba, + *ident; + _stringlist *sl; + + /* Find out the name of the current OS user */ + current_windows_user(&accountname, &domainname); + + /* Determine the bootstrap superuser's name */ + if (superuser_name == NULL) + { + /* + * Compute the default superuser name the same way initdb does. + * + * It's possible that this result always matches "accountname", the + * value SSPI authentication discovers. But the underlying system + * functions do not clearly guarantee that. + */ + superuser_name = get_user_name(&errstr); + if (superuser_name == NULL) + { + fprintf(stderr, "%s: %s\n", progname, errstr); + exit(2); + } + } + + /* + * Like initdb.c:setup_config(), determine whether the platform recognizes + * ::1 (IPv6 loopback) as a numeric host address string. + */ + { + struct addrinfo *gai_result; + struct addrinfo hints; + WSADATA wsaData; + + hints.ai_flags = AI_NUMERICHOST; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = 0; + hints.ai_protocol = 0; + hints.ai_addrlen = 0; + hints.ai_canonname = NULL; + hints.ai_addr = NULL; + hints.ai_next = NULL; + + have_ipv6 = (WSAStartup(MAKEWORD(2, 2), &wsaData) == 0 && + getaddrinfo("::1", NULL, &hints, &gai_result) == 0); + } + + /* Check a Write outcome and report any error. */ +#define CW(cond) \ + do { \ + if (!(cond)) \ + { \ + fprintf(stderr, _("%s: could not write to file \"%s\": %s\n"), \ + progname, fname, strerror(errno)); \ + exit(2); \ + } \ + } while (0) + + res = snprintf(fname, sizeof(fname), "%s/pg_hba.conf", pgdata); + if (res < 0 || res >= sizeof(fname)) + { + /* + * Truncating this name is a fatal error, because we must not fail to + * overwrite an original trust-authentication pg_hba.conf. + */ + fprintf(stderr, _("%s: directory name too long\n"), progname); + exit(2); + } + hba = fopen(fname, "w"); + if (hba == NULL) + { + fprintf(stderr, _("%s: could not open file \"%s\" for writing: %s\n"), + progname, fname, strerror(errno)); + exit(2); + } + CW(fputs("# Configuration written by config_sspi_auth()\n", hba) >= 0); + CW(fputs("host all all 127.0.0.1/32 sspi include_realm=1 map=regress\n", + hba) >= 0); + if (have_ipv6) + CW(fputs("host all all ::1/128 sspi include_realm=1 map=regress\n", + hba) >= 0); + CW(fclose(hba) == 0); + + snprintf(fname, sizeof(fname), "%s/pg_ident.conf", pgdata); + ident = fopen(fname, "w"); + if (ident == NULL) + { + fprintf(stderr, _("%s: could not open file \"%s\" for writing: %s\n"), + progname, fname, strerror(errno)); + exit(2); + } + CW(fputs("# Configuration written by config_sspi_auth()\n", ident) >= 0); + + /* + * Double-quote for the benefit of account names containing whitespace or + * '#'. Windows forbids the double-quote character itself, so don't + * bother escaping embedded double-quote characters. + */ + CW(fprintf(ident, "regress \"%s@%s\" %s\n", + accountname, domainname, fmtHba(superuser_name)) >= 0); + for (sl = extraroles; sl; sl = sl->next) + CW(fprintf(ident, "regress \"%s@%s\" %s\n", + accountname, domainname, fmtHba(sl->str)) >= 0); + CW(fclose(ident) == 0); +} + +#endif /* ENABLE_SSPI */ + +/* + * Issue a command via psql, connecting to the specified database + * + * Since we use system(), this doesn't return until the operation finishes + */ +static void +psql_command(const char *database, const char *query,...) +{ + char query_formatted[1024]; + char query_escaped[2048]; + char psql_cmd[MAXPGPATH + 2048]; + va_list args; + char *s; + char *d; + + /* Generate the query with insertion of sprintf arguments */ + va_start(args, query); + vsnprintf(query_formatted, sizeof(query_formatted), query, args); + va_end(args); + + /* Now escape any shell double-quote metacharacters */ + d = query_escaped; + for (s = query_formatted; *s; s++) + { + if (strchr("\\\"$`", *s)) + *d++ = '\\'; + *d++ = *s; + } + *d = '\0'; + + /* And now we can build and execute the shell command */ + snprintf(psql_cmd, sizeof(psql_cmd), + "\"%s%spsql\" -X -c \"%s\" \"%s\"", + bindir ? bindir : "", + bindir ? "/" : "", + query_escaped, + database); + + if (system(psql_cmd) != 0) + { + /* psql probably already reported the error */ + fprintf(stderr, _("command failed: %s\n"), psql_cmd); + exit(2); + } +} + +/* + * Spawn a process to execute the given shell command; don't wait for it + * + * Returns the process ID (or HANDLE) so we can wait for it later + */ +PID_TYPE +spawn_process(const char *cmdline) +{ +#ifndef WIN32 + pid_t pid; + + /* + * Must flush I/O buffers before fork. Ideally we'd use fflush(NULL) here + * ... does anyone still care about systems where that doesn't work? + */ + fflush(stdout); + fflush(stderr); + if (logfile) + fflush(logfile); + + pid = fork(); + if (pid == -1) + { + fprintf(stderr, _("%s: could not fork: %s\n"), + progname, strerror(errno)); + exit(2); + } + if (pid == 0) + { + /* + * In child + * + * Instead of using system(), exec the shell directly, and tell it to + * "exec" the command too. This saves two useless processes per + * parallel test case. + */ + char *cmdline2; + + cmdline2 = psprintf("exec %s", cmdline); + execl(shellprog, shellprog, "-c", cmdline2, (char *) NULL); + fprintf(stderr, _("%s: could not exec \"%s\": %s\n"), + progname, shellprog, strerror(errno)); + _exit(1); /* not exit() here... */ + } + /* in parent */ + return pid; +#else + PROCESS_INFORMATION pi; + char *cmdline2; + HANDLE restrictedToken; + const char *comspec; + + /* Find CMD.EXE location using COMSPEC, if it's set */ + comspec = getenv("COMSPEC"); + if (comspec == NULL) + comspec = "CMD"; + + memset(&pi, 0, sizeof(pi)); + cmdline2 = psprintf("\"%s\" /c \"%s\"", comspec, cmdline); + + if ((restrictedToken = + CreateRestrictedProcess(cmdline2, &pi)) == 0) + exit(2); + + CloseHandle(pi.hThread); + return pi.hProcess; +#endif +} + +/* + * Count bytes in file + */ +static long +file_size(const char *file) +{ + long r; + FILE *f = fopen(file, "r"); + + if (!f) + { + fprintf(stderr, _("%s: could not open file \"%s\" for reading: %s\n"), + progname, file, strerror(errno)); + return -1; + } + fseek(f, 0, SEEK_END); + r = ftell(f); + fclose(f); + return r; +} + +/* + * Count lines in file + */ +static int +file_line_count(const char *file) +{ + int c; + int l = 0; + FILE *f = fopen(file, "r"); + + if (!f) + { + fprintf(stderr, _("%s: could not open file \"%s\" for reading: %s\n"), + progname, file, strerror(errno)); + return -1; + } + while ((c = fgetc(f)) != EOF) + { + if (c == '\n') + l++; + } + fclose(f); + return l; +} + +bool +file_exists(const char *file) +{ + FILE *f = fopen(file, "r"); + + if (!f) + return false; + fclose(f); + return true; +} + +static bool +directory_exists(const char *dir) +{ + struct stat st; + + if (stat(dir, &st) != 0) + return false; + if (S_ISDIR(st.st_mode)) + return true; + return false; +} + +/* Create a directory */ +static void +make_directory(const char *dir) +{ + if (mkdir(dir, S_IRWXU | S_IRWXG | S_IRWXO) < 0) + { + fprintf(stderr, _("%s: could not create directory \"%s\": %s\n"), + progname, dir, strerror(errno)); + exit(2); + } +} + +/* + * In: filename.ext, Return: filename_i.ext, where 0 < i <= 9 + */ +static char * +get_alternative_expectfile(const char *expectfile, int i) +{ + char *last_dot; + int ssize = strlen(expectfile) + 2 + 1; + char *tmp; + char *s; + + if (!(tmp = (char *) malloc(ssize))) + return NULL; + + if (!(s = (char *) malloc(ssize))) + { + free(tmp); + return NULL; + } + + strcpy(tmp, expectfile); + last_dot = strrchr(tmp, '.'); + if (!last_dot) + { + free(tmp); + free(s); + return NULL; + } + *last_dot = '\0'; + snprintf(s, ssize, "%s_%d.%s", tmp, i, last_dot + 1); + free(tmp); + return s; +} + +/* + * Run a "diff" command and also check that it didn't crash + */ +static int +run_diff(const char *cmd, const char *filename) +{ + int r; + + r = system(cmd); + if (!WIFEXITED(r) || WEXITSTATUS(r) > 1) + { + fprintf(stderr, _("diff command failed with status %d: %s\n"), r, cmd); + exit(2); + } +#ifdef WIN32 + + /* + * On WIN32, if the 'diff' command cannot be found, system() returns 1, + * but produces nothing to stdout, so we check for that here. + */ + if (WEXITSTATUS(r) == 1 && file_size(filename) <= 0) + { + fprintf(stderr, _("diff command not found: %s\n"), cmd); + exit(2); + } +#endif + + return WEXITSTATUS(r); +} + +/* + * Check the actual result file for the given test against expected results + * + * Returns true if different (failure), false if correct match found. + * In the true case, the diff is appended to the diffs file. + */ +static bool +results_differ(const char *testname, const char *resultsfile, const char *default_expectfile) +{ + char expectfile[MAXPGPATH]; + char diff[MAXPGPATH]; + char cmd[MAXPGPATH * 3]; + char best_expect_file[MAXPGPATH]; + FILE *difffile; + int best_line_count; + int i; + int l; + const char *platform_expectfile; + + /* + * We can pass either the resultsfile or the expectfile, they should have + * the same type (filename.type) anyway. + */ + platform_expectfile = get_expectfile(testname, resultsfile); + + strlcpy(expectfile, default_expectfile, sizeof(expectfile)); + if (platform_expectfile) + { + /* + * Replace everything after the last slash in expectfile with what the + * platform_expectfile contains. + */ + char *p = strrchr(expectfile, '/'); + + if (p) + strcpy(++p, platform_expectfile); + } + + /* Name to use for temporary diff file */ + snprintf(diff, sizeof(diff), "%s.diff", resultsfile); + + /* OK, run the diff */ + snprintf(cmd, sizeof(cmd), + "diff %s \"%s\" \"%s\" > \"%s\"", + basic_diff_opts, expectfile, resultsfile, diff); + + /* Is the diff file empty? */ + if (run_diff(cmd, diff) == 0) + { + unlink(diff); + return false; + } + + /* There may be secondary comparison files that match better */ + best_line_count = file_line_count(diff); + strcpy(best_expect_file, expectfile); + + for (i = 0; i <= 9; i++) + { + char *alt_expectfile; + + alt_expectfile = get_alternative_expectfile(expectfile, i); + if (!alt_expectfile) + { + fprintf(stderr, _("Unable to check secondary comparison files: %s\n"), + strerror(errno)); + exit(2); + } + + if (!file_exists(alt_expectfile)) + { + free(alt_expectfile); + continue; + } + + snprintf(cmd, sizeof(cmd), + "diff %s \"%s\" \"%s\" > \"%s\"", + basic_diff_opts, alt_expectfile, resultsfile, diff); + + if (run_diff(cmd, diff) == 0) + { + unlink(diff); + free(alt_expectfile); + return false; + } + + l = file_line_count(diff); + if (l < best_line_count) + { + /* This diff was a better match than the last one */ + best_line_count = l; + strlcpy(best_expect_file, alt_expectfile, sizeof(best_expect_file)); + } + free(alt_expectfile); + } + + /* + * fall back on the canonical results file if we haven't tried it yet and + * haven't found a complete match yet. + */ + + if (platform_expectfile) + { + snprintf(cmd, sizeof(cmd), + "diff %s \"%s\" \"%s\" > \"%s\"", + basic_diff_opts, default_expectfile, resultsfile, diff); + + if (run_diff(cmd, diff) == 0) + { + /* No diff = no changes = good */ + unlink(diff); + return false; + } + + l = file_line_count(diff); + if (l < best_line_count) + { + /* This diff was a better match than the last one */ + best_line_count = l; + strlcpy(best_expect_file, default_expectfile, sizeof(best_expect_file)); + } + } + + /* + * Use the best comparison file to generate the "pretty" diff, which we + * append to the diffs summary file. + */ + + /* Write diff header */ + difffile = fopen(difffilename, "a"); + if (difffile) + { + fprintf(difffile, + "diff %s %s %s\n", + pretty_diff_opts, best_expect_file, resultsfile); + fclose(difffile); + } + + /* Run diff */ + snprintf(cmd, sizeof(cmd), + "diff %s \"%s\" \"%s\" >> \"%s\"", + pretty_diff_opts, best_expect_file, resultsfile, difffilename); + run_diff(cmd, difffilename); + + unlink(diff); + return true; +} + +/* + * Wait for specified subprocesses to finish, and return their exit + * statuses into statuses[] and stop times into stoptimes[] + * + * If names isn't NULL, print each subprocess's name as it finishes + * + * Note: it's OK to scribble on the pids array, but not on the names array + */ +static void +wait_for_tests(PID_TYPE * pids, int *statuses, instr_time *stoptimes, + char **names, int num_tests) +{ + int tests_left; + int i; + +#ifdef WIN32 + PID_TYPE *active_pids = pg_malloc(num_tests * sizeof(PID_TYPE)); + + memcpy(active_pids, pids, num_tests * sizeof(PID_TYPE)); +#endif + + tests_left = num_tests; + while (tests_left > 0) + { + PID_TYPE p; + +#ifndef WIN32 + int exit_status; + + p = wait(&exit_status); + + if (p == INVALID_PID) + { + fprintf(stderr, _("failed to wait for subprocesses: %s\n"), + strerror(errno)); + exit(2); + } +#else + DWORD exit_status; + int r; + + r = WaitForMultipleObjects(tests_left, active_pids, FALSE, INFINITE); + if (r < WAIT_OBJECT_0 || r >= WAIT_OBJECT_0 + tests_left) + { + fprintf(stderr, _("failed to wait for subprocesses: error code %lu\n"), + GetLastError()); + exit(2); + } + p = active_pids[r - WAIT_OBJECT_0]; + /* compact the active_pids array */ + active_pids[r - WAIT_OBJECT_0] = active_pids[tests_left - 1]; +#endif /* WIN32 */ + + for (i = 0; i < num_tests; i++) + { + if (p == pids[i]) + { +#ifdef WIN32 + GetExitCodeProcess(pids[i], &exit_status); + CloseHandle(pids[i]); +#endif + pids[i] = INVALID_PID; + statuses[i] = (int) exit_status; + INSTR_TIME_SET_CURRENT(stoptimes[i]); + if (names) + status(" %s", names[i]); + tests_left--; + break; + } + } + } + +#ifdef WIN32 + free(active_pids); +#endif +} + +/* + * report nonzero exit code from a test process + */ +static void +log_child_failure(int exitstatus) +{ + if (WIFEXITED(exitstatus)) + status(_(" (test process exited with exit code %d)"), + WEXITSTATUS(exitstatus)); + else if (WIFSIGNALED(exitstatus)) + { +#if defined(WIN32) + status(_(" (test process was terminated by exception 0x%X)"), + WTERMSIG(exitstatus)); +#else + status(_(" (test process was terminated by signal %d: %s)"), + WTERMSIG(exitstatus), pg_strsignal(WTERMSIG(exitstatus))); +#endif + } + else + status(_(" (test process exited with unrecognized status %d)"), + exitstatus); +} + +/* + * Run all the tests specified in one schedule file + */ +static void +run_schedule(const char *schedule, test_start_function startfunc, + postprocess_result_function postfunc) +{ +#define MAX_PARALLEL_TESTS 100 + char *tests[MAX_PARALLEL_TESTS]; + _stringlist *resultfiles[MAX_PARALLEL_TESTS]; + _stringlist *expectfiles[MAX_PARALLEL_TESTS]; + _stringlist *tags[MAX_PARALLEL_TESTS]; + PID_TYPE pids[MAX_PARALLEL_TESTS]; + instr_time starttimes[MAX_PARALLEL_TESTS]; + instr_time stoptimes[MAX_PARALLEL_TESTS]; + int statuses[MAX_PARALLEL_TESTS]; + _stringlist *ignorelist = NULL; + char scbuf[1024]; + FILE *scf; + int line_num = 0; + + memset(tests, 0, sizeof(tests)); + memset(resultfiles, 0, sizeof(resultfiles)); + memset(expectfiles, 0, sizeof(expectfiles)); + memset(tags, 0, sizeof(tags)); + + scf = fopen(schedule, "r"); + if (!scf) + { + fprintf(stderr, _("%s: could not open file \"%s\" for reading: %s\n"), + progname, schedule, strerror(errno)); + exit(2); + } + + while (fgets(scbuf, sizeof(scbuf), scf)) + { + char *test = NULL; + char *c; + int num_tests; + bool inword; + int i; + + line_num++; + + /* strip trailing whitespace, especially the newline */ + i = strlen(scbuf); + while (i > 0 && isspace((unsigned char) scbuf[i - 1])) + scbuf[--i] = '\0'; + + if (scbuf[0] == '\0' || scbuf[0] == '#') + continue; + if (strncmp(scbuf, "test: ", 6) == 0) + test = scbuf + 6; + else if (strncmp(scbuf, "ignore: ", 8) == 0) + { + c = scbuf + 8; + while (*c && isspace((unsigned char) *c)) + c++; + add_stringlist_item(&ignorelist, c); + + /* + * Note: ignore: lines do not run the test, they just say that + * failure of this test when run later on is to be ignored. A bit + * odd but that's how the shell-script version did it. + */ + continue; + } + else + { + fprintf(stderr, _("syntax error in schedule file \"%s\" line %d: %s\n"), + schedule, line_num, scbuf); + exit(2); + } + + num_tests = 0; + inword = false; + for (c = test;; c++) + { + if (*c == '\0' || isspace((unsigned char) *c)) + { + if (inword) + { + /* Reached end of a test name */ + char sav; + + if (num_tests >= MAX_PARALLEL_TESTS) + { + fprintf(stderr, _("too many parallel tests (more than %d) in schedule file \"%s\" line %d: %s\n"), + MAX_PARALLEL_TESTS, schedule, line_num, scbuf); + exit(2); + } + sav = *c; + *c = '\0'; + tests[num_tests] = pg_strdup(test); + num_tests++; + *c = sav; + inword = false; + } + if (*c == '\0') + break; /* loop exit is here */ + } + else if (!inword) + { + /* Start of a test name */ + test = c; + inword = true; + } + } + + if (num_tests == 0) + { + fprintf(stderr, _("syntax error in schedule file \"%s\" line %d: %s\n"), + schedule, line_num, scbuf); + exit(2); + } + + if (num_tests == 1) + { + status(_("test %-28s ... "), tests[0]); + pids[0] = (startfunc) (tests[0], &resultfiles[0], &expectfiles[0], &tags[0]); + INSTR_TIME_SET_CURRENT(starttimes[0]); + wait_for_tests(pids, statuses, stoptimes, NULL, 1); + /* status line is finished below */ + } + else if (max_concurrent_tests > 0 && max_concurrent_tests < num_tests) + { + fprintf(stderr, _("too many parallel tests (more than %d) in schedule file \"%s\" line %d: %s\n"), + max_concurrent_tests, schedule, line_num, scbuf); + exit(2); + } + else if (max_connections > 0 && max_connections < num_tests) + { + int oldest = 0; + + status(_("parallel group (%d tests, in groups of %d): "), + num_tests, max_connections); + for (i = 0; i < num_tests; i++) + { + if (i - oldest >= max_connections) + { + wait_for_tests(pids + oldest, statuses + oldest, + stoptimes + oldest, + tests + oldest, i - oldest); + oldest = i; + } + pids[i] = (startfunc) (tests[i], &resultfiles[i], &expectfiles[i], &tags[i]); + INSTR_TIME_SET_CURRENT(starttimes[i]); + } + wait_for_tests(pids + oldest, statuses + oldest, + stoptimes + oldest, + tests + oldest, i - oldest); + status_end(); + } + else + { + status(_("parallel group (%d tests): "), num_tests); + for (i = 0; i < num_tests; i++) + { + pids[i] = (startfunc) (tests[i], &resultfiles[i], &expectfiles[i], &tags[i]); + INSTR_TIME_SET_CURRENT(starttimes[i]); + } + wait_for_tests(pids, statuses, stoptimes, tests, num_tests); + status_end(); + } + + /* Check results for all tests */ + for (i = 0; i < num_tests; i++) + { + _stringlist *rl, + *el, + *tl; + bool differ = false; + + if (num_tests > 1) + status(_(" %-28s ... "), tests[i]); + + /* + * Advance over all three lists simultaneously. + * + * Compare resultfiles[j] with expectfiles[j] always. Tags are + * optional but if there are tags, the tag list has the same + * length as the other two lists. + */ + for (rl = resultfiles[i], el = expectfiles[i], tl = tags[i]; + rl != NULL; /* rl and el have the same length */ + rl = rl->next, el = el->next, + tl = tl ? tl->next : NULL) + { + bool newdiff; + + if (postfunc) + (*postfunc) (rl->str); + newdiff = results_differ(tests[i], rl->str, el->str); + if (newdiff && tl) + { + printf("%s ", tl->str); + } + differ |= newdiff; + } + + if (differ) + { + bool ignore = false; + _stringlist *sl; + + for (sl = ignorelist; sl != NULL; sl = sl->next) + { + if (strcmp(tests[i], sl->str) == 0) + { + ignore = true; + break; + } + } + if (ignore) + { + status(_("failed (ignored)")); + fail_ignore_count++; + } + else + { + status(_("FAILED")); + fail_count++; + } + } + else + { + status(_("ok ")); /* align with FAILED */ + success_count++; + } + + if (statuses[i] != 0) + log_child_failure(statuses[i]); + + INSTR_TIME_SUBTRACT(stoptimes[i], starttimes[i]); + status(_(" %8.0f ms"), INSTR_TIME_GET_MILLISEC(stoptimes[i])); + + status_end(); + } + + for (i = 0; i < num_tests; i++) + { + pg_free(tests[i]); + tests[i] = NULL; + free_stringlist(&resultfiles[i]); + free_stringlist(&expectfiles[i]); + free_stringlist(&tags[i]); + } + } + + free_stringlist(&ignorelist); + + fclose(scf); +} + +/* + * Run a single test + */ +static void +run_single_test(const char *test, test_start_function startfunc, + postprocess_result_function postfunc) +{ + PID_TYPE pid; + instr_time starttime; + instr_time stoptime; + int exit_status; + _stringlist *resultfiles = NULL; + _stringlist *expectfiles = NULL; + _stringlist *tags = NULL; + _stringlist *rl, + *el, + *tl; + bool differ = false; + + status(_("test %-28s ... "), test); + pid = (startfunc) (test, &resultfiles, &expectfiles, &tags); + INSTR_TIME_SET_CURRENT(starttime); + wait_for_tests(&pid, &exit_status, &stoptime, NULL, 1); + + /* + * Advance over all three lists simultaneously. + * + * Compare resultfiles[j] with expectfiles[j] always. Tags are optional + * but if there are tags, the tag list has the same length as the other + * two lists. + */ + for (rl = resultfiles, el = expectfiles, tl = tags; + rl != NULL; /* rl and el have the same length */ + rl = rl->next, el = el->next, + tl = tl ? tl->next : NULL) + { + bool newdiff; + + if (postfunc) + (*postfunc) (rl->str); + newdiff = results_differ(test, rl->str, el->str); + if (newdiff && tl) + { + printf("%s ", tl->str); + } + differ |= newdiff; + } + + if (differ) + { + status(_("FAILED")); + fail_count++; + } + else + { + status(_("ok ")); /* align with FAILED */ + success_count++; + } + + if (exit_status != 0) + log_child_failure(exit_status); + + INSTR_TIME_SUBTRACT(stoptime, starttime); + status(_(" %8.0f ms"), INSTR_TIME_GET_MILLISEC(stoptime)); + + status_end(); +} + +/* + * Create the summary-output files (making them empty if already existing) + */ +static void +open_result_files(void) +{ + char file[MAXPGPATH]; + FILE *difffile; + + /* create outputdir directory if not present */ + if (!directory_exists(outputdir)) + make_directory(outputdir); + + /* create the log file (copy of running status output) */ + snprintf(file, sizeof(file), "%s/regression.out", outputdir); + logfilename = pg_strdup(file); + logfile = fopen(logfilename, "w"); + if (!logfile) + { + fprintf(stderr, _("%s: could not open file \"%s\" for writing: %s\n"), + progname, logfilename, strerror(errno)); + exit(2); + } + + /* create the diffs file as empty */ + snprintf(file, sizeof(file), "%s/regression.diffs", outputdir); + difffilename = pg_strdup(file); + difffile = fopen(difffilename, "w"); + if (!difffile) + { + fprintf(stderr, _("%s: could not open file \"%s\" for writing: %s\n"), + progname, difffilename, strerror(errno)); + exit(2); + } + /* we don't keep the diffs file open continuously */ + fclose(difffile); + + /* also create the results directory if not present */ + snprintf(file, sizeof(file), "%s/results", outputdir); + if (!directory_exists(file)) + make_directory(file); +} + +static void +drop_database_if_exists(const char *dbname) +{ + header(_("dropping database \"%s\""), dbname); + psql_command("postgres", "DROP DATABASE IF EXISTS \"%s\"", dbname); +} + +static void +create_database(const char *dbname) +{ + _stringlist *sl; + + /* + * We use template0 so that any installation-local cruft in template1 will + * not mess up the tests. + */ + header(_("creating database \"%s\""), dbname); + if (encoding) + psql_command("postgres", "CREATE DATABASE \"%s\" TEMPLATE=template0 ENCODING='%s'%s", dbname, encoding, + (nolocale) ? " LC_COLLATE='C' LC_CTYPE='C'" : ""); + else + psql_command("postgres", "CREATE DATABASE \"%s\" TEMPLATE=template0%s", dbname, + (nolocale) ? " LC_COLLATE='C' LC_CTYPE='C'" : ""); + psql_command(dbname, + "ALTER DATABASE \"%s\" SET lc_messages TO 'C';" + "ALTER DATABASE \"%s\" SET lc_monetary TO 'C';" + "ALTER DATABASE \"%s\" SET lc_numeric TO 'C';" + "ALTER DATABASE \"%s\" SET lc_time TO 'C';" + "ALTER DATABASE \"%s\" SET bytea_output TO 'hex';" + "ALTER DATABASE \"%s\" SET timezone_abbreviations TO 'Default';", + dbname, dbname, dbname, dbname, dbname, dbname); + + /* + * Install any requested extensions. We use CREATE IF NOT EXISTS so that + * this will work whether or not the extension is preinstalled. + */ + for (sl = loadextension; sl != NULL; sl = sl->next) + { + header(_("installing %s"), sl->str); + psql_command(dbname, "CREATE EXTENSION IF NOT EXISTS \"%s\"", sl->str); + } +} + +static void +drop_role_if_exists(const char *rolename) +{ + header(_("dropping role \"%s\""), rolename); + psql_command("postgres", "DROP ROLE IF EXISTS \"%s\"", rolename); +} + +static void +create_role(const char *rolename, const _stringlist *granted_dbs) +{ + header(_("creating role \"%s\""), rolename); + psql_command("postgres", "CREATE ROLE \"%s\" WITH LOGIN", rolename); + for (; granted_dbs != NULL; granted_dbs = granted_dbs->next) + { + psql_command("postgres", "GRANT ALL ON DATABASE \"%s\" TO \"%s\"", + granted_dbs->str, rolename); + } +} + +static void +help(void) +{ + printf(_("PostgreSQL regression test driver\n")); + printf(_("\n")); + printf(_("Usage:\n %s [OPTION]... [EXTRA-TEST]...\n"), progname); + printf(_("\n")); + printf(_("Options:\n")); + printf(_(" --bindir=BINPATH use BINPATH for programs that are run;\n")); + printf(_(" if empty, use PATH from the environment\n")); + printf(_(" --config-auth=DATADIR update authentication settings for DATADIR\n")); + printf(_(" --create-role=ROLE create the specified role before testing\n")); + printf(_(" --dbname=DB use database DB (default \"regression\")\n")); + printf(_(" --debug turn on debug mode in programs that are run\n")); + printf(_(" --dlpath=DIR look for dynamic libraries in DIR\n")); + printf(_(" --encoding=ENCODING use ENCODING as the encoding\n")); + printf(_(" -h, --help show this help, then exit\n")); + printf(_(" --inputdir=DIR take input files from DIR (default \".\")\n")); + printf(_(" --launcher=CMD use CMD as launcher of psql\n")); + printf(_(" --load-extension=EXT load the named extension before running the\n")); + printf(_(" tests; can appear multiple times\n")); + printf(_(" --make-testtablespace-dir create testtablespace directory\n")); + printf(_(" --max-connections=N maximum number of concurrent connections\n")); + printf(_(" (default is 0, meaning unlimited)\n")); + printf(_(" --max-concurrent-tests=N maximum number of concurrent tests in schedule\n")); + printf(_(" (default is 0, meaning unlimited)\n")); + printf(_(" --outputdir=DIR place output files in DIR (default \".\")\n")); + printf(_(" --schedule=FILE use test ordering schedule from FILE\n")); + printf(_(" (can be used multiple times to concatenate)\n")); + printf(_(" --temp-instance=DIR create a temporary instance in DIR\n")); + printf(_(" --use-existing use an existing installation\n")); + printf(_(" -V, --version output version information, then exit\n")); + printf(_("\n")); + printf(_("Options for \"temp-instance\" mode:\n")); + printf(_(" --no-locale use C locale\n")); + printf(_(" --port=PORT start postmaster on PORT\n")); + printf(_(" --temp-config=FILE append contents of FILE to temporary config\n")); + printf(_("\n")); + printf(_("Options for using an existing installation:\n")); + printf(_(" --host=HOST use postmaster running on HOST\n")); + printf(_(" --port=PORT use postmaster running at PORT\n")); + printf(_(" --user=USER connect as USER\n")); + printf(_("\n")); + printf(_("The exit status is 0 if all tests passed, 1 if some tests failed, and 2\n")); + printf(_("if the tests could not be run for some reason.\n")); + printf(_("\n")); + printf(_("Report bugs to <%s>.\n"), PACKAGE_BUGREPORT); + printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL); +} + +int +regression_main(int argc, char *argv[], + init_function ifunc, + test_start_function startfunc, + postprocess_result_function postfunc) +{ + static struct option long_options[] = { + {"help", no_argument, NULL, 'h'}, + {"version", no_argument, NULL, 'V'}, + {"dbname", required_argument, NULL, 1}, + {"debug", no_argument, NULL, 2}, + {"inputdir", required_argument, NULL, 3}, + {"max-connections", required_argument, NULL, 5}, + {"encoding", required_argument, NULL, 6}, + {"outputdir", required_argument, NULL, 7}, + {"schedule", required_argument, NULL, 8}, + {"temp-instance", required_argument, NULL, 9}, + {"no-locale", no_argument, NULL, 10}, + {"host", required_argument, NULL, 13}, + {"port", required_argument, NULL, 14}, + {"user", required_argument, NULL, 15}, + {"bindir", required_argument, NULL, 16}, + {"dlpath", required_argument, NULL, 17}, + {"create-role", required_argument, NULL, 18}, + {"temp-config", required_argument, NULL, 19}, + {"use-existing", no_argument, NULL, 20}, + {"launcher", required_argument, NULL, 21}, + {"load-extension", required_argument, NULL, 22}, + {"config-auth", required_argument, NULL, 24}, + {"max-concurrent-tests", required_argument, NULL, 25}, + {"make-testtablespace-dir", no_argument, NULL, 26}, + {NULL, 0, NULL, 0} + }; + + bool use_unix_sockets; + bool make_testtablespace_dir = false; + _stringlist *sl; + int c; + int i; + int option_index; + char buf[MAXPGPATH * 4]; + char buf2[MAXPGPATH * 4]; + + pg_logging_init(argv[0]); + progname = get_progname(argv[0]); + set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_regress")); + + get_restricted_token(); + + atexit(stop_postmaster); + +#if !defined(HAVE_UNIX_SOCKETS) + use_unix_sockets = false; +#elif defined(WIN32) + + /* + * We don't use Unix-domain sockets on Windows by default, even if the + * build supports them. (See comment at remove_temp() for a reason.) + * Override at your own risk. + */ + use_unix_sockets = getenv("PG_TEST_USE_UNIX_SOCKETS") ? true : false; +#else + use_unix_sockets = true; +#endif + + if (!use_unix_sockets) + hostname = "localhost"; + + /* + * We call the initialization function here because that way we can set + * default parameters and let them be overwritten by the commandline. + */ + ifunc(argc, argv); + + if (getenv("PG_REGRESS_DIFF_OPTS")) + pretty_diff_opts = getenv("PG_REGRESS_DIFF_OPTS"); + + while ((c = getopt_long(argc, argv, "hV", long_options, &option_index)) != -1) + { + switch (c) + { + case 'h': + help(); + exit(0); + case 'V': + puts("pg_regress (PostgreSQL) " PG_VERSION); + exit(0); + case 1: + + /* + * If a default database was specified, we need to remove it + * before we add the specified one. + */ + free_stringlist(&dblist); + split_to_stringlist(optarg, ",", &dblist); + break; + case 2: + debug = true; + break; + case 3: + inputdir = pg_strdup(optarg); + break; + case 5: + max_connections = atoi(optarg); + break; + case 6: + encoding = pg_strdup(optarg); + break; + case 7: + outputdir = pg_strdup(optarg); + break; + case 8: + add_stringlist_item(&schedulelist, optarg); + break; + case 9: + temp_instance = make_absolute_path(optarg); + break; + case 10: + nolocale = true; + break; + case 13: + hostname = pg_strdup(optarg); + break; + case 14: + port = atoi(optarg); + port_specified_by_user = true; + break; + case 15: + user = pg_strdup(optarg); + break; + case 16: + /* "--bindir=" means to use PATH */ + if (strlen(optarg)) + bindir = pg_strdup(optarg); + else + bindir = NULL; + break; + case 17: + dlpath = pg_strdup(optarg); + break; + case 18: + split_to_stringlist(optarg, ",", &extraroles); + break; + case 19: + add_stringlist_item(&temp_configs, optarg); + break; + case 20: + use_existing = true; + break; + case 21: + launcher = pg_strdup(optarg); + break; + case 22: + add_stringlist_item(&loadextension, optarg); + break; + case 24: + config_auth_datadir = pg_strdup(optarg); + break; + case 25: + max_concurrent_tests = atoi(optarg); + break; + case 26: + make_testtablespace_dir = true; + break; + default: + /* getopt_long already emitted a complaint */ + fprintf(stderr, _("\nTry \"%s -h\" for more information.\n"), + progname); + exit(2); + } + } + + /* + * if we still have arguments, they are extra tests to run + */ + while (argc - optind >= 1) + { + add_stringlist_item(&extra_tests, argv[optind]); + optind++; + } + + if (config_auth_datadir) + { +#ifdef ENABLE_SSPI + if (!use_unix_sockets) + config_sspi_auth(config_auth_datadir, user); +#endif + exit(0); + } + + if (temp_instance && !port_specified_by_user) + + /* + * To reduce chances of interference with parallel installations, use + * a port number starting in the private range (49152-65535) + * calculated from the version number. This aids !HAVE_UNIX_SOCKETS + * systems; elsewhere, the use of a private socket directory already + * prevents interference. + */ + port = 0xC000 | (PG_VERSION_NUM & 0x3FFF); + + inputdir = make_absolute_path(inputdir); + outputdir = make_absolute_path(outputdir); + dlpath = make_absolute_path(dlpath); + + /* + * Initialization + */ + open_result_files(); + + initialize_environment(); + +#if defined(HAVE_GETRLIMIT) && defined(RLIMIT_CORE) + unlimit_core_size(); +#endif + + if (make_testtablespace_dir) + prepare_testtablespace_dir(); + + if (temp_instance) + { + FILE *pg_conf; + const char *env_wait; + int wait_seconds; + + /* + * Prepare the temp instance + */ + + if (directory_exists(temp_instance)) + { + header(_("removing existing temp instance")); + if (!rmtree(temp_instance, true)) + { + fprintf(stderr, _("\n%s: could not remove temp instance \"%s\"\n"), + progname, temp_instance); + exit(2); + } + } + + header(_("creating temporary instance")); + + /* make the temp instance top directory */ + make_directory(temp_instance); + + /* and a directory for log files */ + snprintf(buf, sizeof(buf), "%s/log", outputdir); + if (!directory_exists(buf)) + make_directory(buf); + + /* initdb */ + header(_("initializing database system")); + snprintf(buf, sizeof(buf), + "\"%s%sinitdb\" -D \"%s/data\" --no-clean --no-sync%s%s > \"%s/log/initdb.log\" 2>&1", + bindir ? bindir : "", + bindir ? "/" : "", + temp_instance, + debug ? " --debug" : "", + nolocale ? " --no-locale" : "", + outputdir); + if (system(buf)) + { + fprintf(stderr, _("\n%s: initdb failed\nExamine %s/log/initdb.log for the reason.\nCommand was: %s\n"), progname, outputdir, buf); + exit(2); + } + + /* + * Adjust the default postgresql.conf for regression testing. The user + * can specify a file to be appended; in any case we expand logging + * and set max_prepared_transactions to enable testing of prepared + * xacts. (Note: to reduce the probability of unexpected shmmax + * failures, don't set max_prepared_transactions any higher than + * actually needed by the prepared_xacts regression test.) + */ + snprintf(buf, sizeof(buf), "%s/data/postgresql.conf", temp_instance); + pg_conf = fopen(buf, "a"); + if (pg_conf == NULL) + { + fprintf(stderr, _("\n%s: could not open \"%s\" for adding extra config: %s\n"), progname, buf, strerror(errno)); + exit(2); + } + fputs("\n# Configuration added by pg_regress\n\n", pg_conf); + fputs("log_autovacuum_min_duration = 0\n", pg_conf); + fputs("log_checkpoints = on\n", pg_conf); + fputs("log_line_prefix = '%m %b[%p] %q%a '\n", pg_conf); + fputs("log_lock_waits = on\n", pg_conf); + fputs("log_temp_files = 128kB\n", pg_conf); + fputs("max_prepared_transactions = 2\n", pg_conf); + + for (sl = temp_configs; sl != NULL; sl = sl->next) + { + char *temp_config = sl->str; + FILE *extra_conf; + char line_buf[1024]; + + extra_conf = fopen(temp_config, "r"); + if (extra_conf == NULL) + { + fprintf(stderr, _("\n%s: could not open \"%s\" to read extra config: %s\n"), progname, temp_config, strerror(errno)); + exit(2); + } + while (fgets(line_buf, sizeof(line_buf), extra_conf) != NULL) + fputs(line_buf, pg_conf); + fclose(extra_conf); + } + + fclose(pg_conf); + +#ifdef ENABLE_SSPI + if (!use_unix_sockets) + { + /* + * Since we successfully used the same buffer for the much-longer + * "initdb" command, this can't truncate. + */ + snprintf(buf, sizeof(buf), "%s/data", temp_instance); + config_sspi_auth(buf, NULL); + } +#elif !defined(HAVE_UNIX_SOCKETS) +#error Platform has no means to secure the test installation. +#endif + + /* + * Check if there is a postmaster running already. + */ + snprintf(buf2, sizeof(buf2), + "\"%s%spsql\" -X postgres <%s 2>%s", + bindir ? bindir : "", + bindir ? "/" : "", + DEVNULL, DEVNULL); + + for (i = 0; i < 16; i++) + { + if (system(buf2) == 0) + { + char s[16]; + + if (port_specified_by_user || i == 15) + { + fprintf(stderr, _("port %d apparently in use\n"), port); + if (!port_specified_by_user) + fprintf(stderr, _("%s: could not determine an available port\n"), progname); + fprintf(stderr, _("Specify an unused port using the --port option or shut down any conflicting PostgreSQL servers.\n")); + exit(2); + } + + fprintf(stderr, _("port %d apparently in use, trying %d\n"), port, port + 1); + port++; + sprintf(s, "%d", port); + setenv("PGPORT", s, 1); + } + else + break; + } + + /* + * Start the temp postmaster + */ + header(_("starting postmaster")); + snprintf(buf, sizeof(buf), + "\"%s%spostgres\" -D \"%s/data\" -F%s " + "-c \"listen_addresses=%s\" -k \"%s\" " + "> \"%s/log/postmaster.log\" 2>&1", + bindir ? bindir : "", + bindir ? "/" : "", + temp_instance, debug ? " -d 5" : "", + hostname ? hostname : "", sockdir ? sockdir : "", + outputdir); + postmaster_pid = spawn_process(buf); + if (postmaster_pid == INVALID_PID) + { + fprintf(stderr, _("\n%s: could not spawn postmaster: %s\n"), + progname, strerror(errno)); + exit(2); + } + + /* + * Wait till postmaster is able to accept connections; normally this + * is only a second or so, but Cygwin is reportedly *much* slower, and + * test builds using Valgrind or similar tools might be too. Hence, + * allow the default timeout of 60 seconds to be overridden from the + * PGCTLTIMEOUT environment variable. + */ + env_wait = getenv("PGCTLTIMEOUT"); + if (env_wait != NULL) + { + wait_seconds = atoi(env_wait); + if (wait_seconds <= 0) + wait_seconds = 60; + } + else + wait_seconds = 60; + + for (i = 0; i < wait_seconds; i++) + { + /* Done if psql succeeds */ + if (system(buf2) == 0) + break; + + /* + * Fail immediately if postmaster has exited + */ +#ifndef WIN32 + if (waitpid(postmaster_pid, NULL, WNOHANG) == postmaster_pid) +#else + if (WaitForSingleObject(postmaster_pid, 0) == WAIT_OBJECT_0) +#endif + { + fprintf(stderr, _("\n%s: postmaster failed\nExamine %s/log/postmaster.log for the reason\n"), progname, outputdir); + exit(2); + } + + pg_usleep(1000000L); + } + if (i >= wait_seconds) + { + fprintf(stderr, _("\n%s: postmaster did not respond within %d seconds\nExamine %s/log/postmaster.log for the reason\n"), + progname, wait_seconds, outputdir); + + /* + * If we get here, the postmaster is probably wedged somewhere in + * startup. Try to kill it ungracefully rather than leaving a + * stuck postmaster that might interfere with subsequent test + * attempts. + */ +#ifndef WIN32 + if (kill(postmaster_pid, SIGKILL) != 0 && + errno != ESRCH) + fprintf(stderr, _("\n%s: could not kill failed postmaster: %s\n"), + progname, strerror(errno)); +#else + if (TerminateProcess(postmaster_pid, 255) == 0) + fprintf(stderr, _("\n%s: could not kill failed postmaster: error code %lu\n"), + progname, GetLastError()); +#endif + + exit(2); + } + + postmaster_running = true; + +#ifdef _WIN64 +/* need a series of two casts to convert HANDLE without compiler warning */ +#define ULONGPID(x) (unsigned long) (unsigned long long) (x) +#else +#define ULONGPID(x) (unsigned long) (x) +#endif + printf(_("running on port %d with PID %lu\n"), + port, ULONGPID(postmaster_pid)); + } + else + { + /* + * Using an existing installation, so may need to get rid of + * pre-existing database(s) and role(s) + */ + if (!use_existing) + { + for (sl = dblist; sl; sl = sl->next) + drop_database_if_exists(sl->str); + for (sl = extraroles; sl; sl = sl->next) + drop_role_if_exists(sl->str); + } + } + + /* + * Create the test database(s) and role(s) + */ + if (!use_existing) + { + for (sl = dblist; sl; sl = sl->next) + create_database(sl->str); + for (sl = extraroles; sl; sl = sl->next) + create_role(sl->str, dblist); + } + + /* + * Ready to run the tests + */ + header(_("running regression test queries")); + + for (sl = schedulelist; sl != NULL; sl = sl->next) + { + run_schedule(sl->str, startfunc, postfunc); + } + + for (sl = extra_tests; sl != NULL; sl = sl->next) + { + run_single_test(sl->str, startfunc, postfunc); + } + + /* + * Shut down temp installation's postmaster + */ + if (temp_instance) + { + header(_("shutting down postmaster")); + stop_postmaster(); + } + + /* + * If there were no errors, remove the temp instance immediately to + * conserve disk space. (If there were errors, we leave the instance in + * place for possible manual investigation.) + */ + if (temp_instance && fail_count == 0 && fail_ignore_count == 0) + { + header(_("removing temporary instance")); + if (!rmtree(temp_instance, true)) + fprintf(stderr, _("\n%s: could not remove temp instance \"%s\"\n"), + progname, temp_instance); + } + + fclose(logfile); + + /* + * Emit nice-looking summary message + */ + if (fail_count == 0 && fail_ignore_count == 0) + snprintf(buf, sizeof(buf), + _(" All %d tests passed. "), + success_count); + else if (fail_count == 0) /* fail_count=0, fail_ignore_count>0 */ + snprintf(buf, sizeof(buf), + _(" %d of %d tests passed, %d failed test(s) ignored. "), + success_count, + success_count + fail_ignore_count, + fail_ignore_count); + else if (fail_ignore_count == 0) /* fail_count>0 && fail_ignore_count=0 */ + snprintf(buf, sizeof(buf), + _(" %d of %d tests failed. "), + fail_count, + success_count + fail_count); + else + /* fail_count>0 && fail_ignore_count>0 */ + snprintf(buf, sizeof(buf), + _(" %d of %d tests failed, %d of these failures ignored. "), + fail_count + fail_ignore_count, + success_count + fail_count + fail_ignore_count, + fail_ignore_count); + + putchar('\n'); + for (i = strlen(buf); i > 0; i--) + putchar('='); + printf("\n%s\n", buf); + for (i = strlen(buf); i > 0; i--) + putchar('='); + putchar('\n'); + putchar('\n'); + + if (file_size(difffilename) > 0) + { + printf(_("The differences that caused some tests to fail can be viewed in the\n" + "file \"%s\". A copy of the test summary that you see\n" + "above is saved in the file \"%s\".\n\n"), + difffilename, logfilename); + } + else + { + unlink(difffilename); + unlink(logfilename); + } + + if (fail_count != 0) + exit(1); + + return 0; +} |