From 36e4ba81cb93f255579c5baa8ce951cebc64eb41 Mon Sep 17 00:00:00 2001 From: Alexandre Felipe Date: Sat, 21 Feb 2026 23:59:39 +0000 Subject: [PATCH v1] Relaxed matching in regress Regression failures after changing PostgreSQL blocksize it was pointed out by that with ./configure --with-blocksize=32 make && make check many tests fail. And that by experience it is known that those differences are expected. They often reflect output variations (like buffer counts, cost estimates, or physical storage details) rather than functional bugs. That said, it really needs to be examined case by case. If some differences are expected it would be useful to have a way to describe this in the tests, check what is required, ignore what is variable. This patch demonstrates one possible approach to that, where the expected file can tell pg_regress to ignore differences selectively. --- src/test/regress/.gitignore | 5 +- src/test/regress/GNUmakefile | 2 + src/test/regress/README.md | 401 +++++++++++++++++++++ src/test/regress/pg_regress.c | 428 +++++++++++++++++------ src/test/regress/test_readme_examples.sh | 130 +++++++ 5 files changed, 859 insertions(+), 107 deletions(-) create mode 100644 src/test/regress/README.md create mode 100755 src/test/regress/test_readme_examples.sh diff --git a/src/test/regress/.gitignore b/src/test/regress/.gitignore index 89129d7358a..cdab9e35141 100644 --- a/src/test/regress/.gitignore +++ b/src/test/regress/.gitignore @@ -6,6 +6,5 @@ /results/ /log/ -# Note: regression.* are only left behind on a failure; that's why they're not ignored -#/regression.diffs -#/regression.out +# Ignore hidden files and directories +.* diff --git a/src/test/regress/GNUmakefile b/src/test/regress/GNUmakefile index a8ba19e5971..5b18d4abae9 100644 --- a/src/test/regress/GNUmakefile +++ b/src/test/regress/GNUmakefile @@ -122,6 +122,8 @@ bigcheck: all | temp-install $(pg_regress_check) $(REGRESS_OPTS) --schedule=$(srcdir)/parallel_schedule $(MAXCONNOPT) numeric_big +self-test: pg_regress$(X) + ./test_readme_examples.sh ## ## Clean up ## diff --git a/src/test/regress/README.md b/src/test/regress/README.md new file mode 100644 index 00000000000..244301942f3 --- /dev/null +++ b/src/test/regress/README.md @@ -0,0 +1,401 @@ + +# Running tests + +Documentation concerning how to run these regression tests and interpret +the results can be found in the PostgreSQL manual, in the chapter +"Regression Tests". + + +# Writing tests + +Regression tests in PostgreSQL are used to automatically verify that the +database server produces the expected results for a wide variety of SQL +operations and features. Writing good regression tests helps ensure that new +changes do not break existing functionality. + +### 1. Locate or Create SQL and Expected Files + +- All regression test SQL files are located in `src/test/regress/sql/`. +- For each `.sql` test file, there should be a corresponding expected output +file in `src/test/regress/expected/` with a `.out` extension. + +### 2. Write a Test + +Each test comprises a `.sql` file in `src/test/regress/sql/`, and a `.out` file +in `src/test/regress/expected`. + +The `.sql` file executes commands that exercise the functionality you want to +test. Include queries, DDL, DML, and edge cases as appropriate. Add comments in +your SQL file describing what and why you are testing. + +Example (`mytest.sql`): +```sql +-- Test basic insert and select +CREATE TABLE mydemo(a int, b text); +INSERT INTO mydemo VALUES (1, 'foo'), (2, 'bar'); +SELECT * FROM mydemo ORDER BY a; +``` + +Create a `.out` file in `src/tst/regress/expected` that includes both the +queries of the `.sql` along with the desired output. Try to make test outputs +stable: avoid things that vary between runs (timestamps, OIDs, etc.) unless +necessary. Use pattern matching (`--@@IGNORE ...@@` directives) in the output +if needed for platform-specific or variable content (see below for details) and +document what you are testing and why. Finally, include both your `.sql` and +`.out`, along with any schedule changes. + +Edit `src/test/regress/parallel_schedule` or `serial_schedule` to add your test +file. List the base name (without .sql) in the appropriate place, depending on +whether the test can run in parallel. +Run all tests with `make check` in the `src/test/regress` directory. + +--- + +By following these steps for each new feature or bug fix, you will help keep +PostgreSQL reliable and trustworthy for all users. + + +# Pattern Matching for Regression Test Outputs + +This document specifies pattern matching options for comparing regression test +outputs against expected results. These options help tests pass across +different configurations (e.g., block sizes) where exact output may vary. + +## Syntax + +Options are specified using SQL comment directives: + +```sql +--@@IGNORE option1, option2@@ +``` + +To disable options within a section: + +```sql +--@@CHECK option1@@ +``` + +Options remain active until changed by another directive or end of file. + + +- Directives are processed line-by-line during comparison +- `CHECK [options]` re-enables strict matching for specified options +- `CHECK ALL` enables everything +- `CHECK DEFAULT` ignore space and case +- Unknown options generate a warning but don't fail the test +- Options are case-insensitive (`IGNORE` = `ignore` = `Ignore`) + + + +## Options + +### `case` - Ignore Case Differences + +Treats uppercase and lowercase letters as equivalent. + +**Pattern: case** +``` +--@@IGNORE: case@@ +SELECT * FROM users; +``` + +**Accepts:** +``` +SELECT * FROM users; +``` + +**Accepts:** +``` +select * from users; +``` + +**Accepts:** +``` +SELECT * FROM USERS; +``` + +**Rejects:** +``` +SELECT * FROM accounts; +``` + +--- + +### `comments` - Ignore SQL Comments + +Ignores SQL comments (`--` and `/* */`) when comparing. + +**Pattern: comments** +``` +--@@IGNORE: comments@@ +SELECT id FROM users; +``` + +**Accepts:** +``` +SELECT id FROM users; +``` + +**Accepts:** +``` +SELECT id FROM users; -- fetch all ids +``` + +**Accepts:** +``` +/* query */ SELECT id FROM users; +``` + +**Rejects:** +``` +SELECT name FROM users; +``` + +--- + +### `spaces` - Ignore Whitespace Differences + +Treats any whitespace sequence as equivalent (spaces, tabs, multiple spaces). + +**Pattern: spaces** +``` +--@@IGNORE: spaces@@ +Seq Scan on foo (cost=0.00..1.00) +``` + +**Accepts:** +``` +Seq Scan on foo (cost=0.00..1.00) +``` + +**Accepts:** +``` +Seq Scan on foo (cost=0.00..1.00) +``` + +**Rejects:** +``` +SeqScan on foo (cost=0.00..1.00) +``` + +--- + +### `numbers` - Ignore Numeric Value Differences + +Treats all numeric values (integers, decimals) as wildcards. + +**Pattern: numbers** +``` +--@@IGNORE: numbers@@ +Buffers: shared hit=10, read=5 +(cost=0.00..123.45 rows=1000 width=8) +``` + +**Accepts:** +``` +Buffers: shared hit=10, read=5 +(cost=0.00..123.45 rows=1000 width=8) +``` + +**Accepts:** +``` +Buffers: shared hit=42, read=17 +(cost=0.00..999.99 rows=5000 width=16) +``` + +**Rejects:** +``` +Buffers: shared hit=10 +(cost=0.00..123.45 rows=1000 width=8) +``` + +--- + +### `result-lines` - Ignore Extra Lines in Result + +Allows the actual result to contain lines not present in expected output. + +**Pattern: result-lines** +``` +--@@IGNORE: result-lines@@ +BEGIN +COMMIT +``` + +**Accepts:** +``` +BEGIN +COMMIT +``` + +**Accepts:** +``` +BEGIN +INSERT 0 1 +COMMIT +``` + +**Accepts:** +``` +BEGIN +INSERT 0 1 +UPDATE 5 +COMMIT +``` + +**Rejects:** +``` +BEGIN +ROLLBACK +``` + +--- + +### `expected-lines` - Ignore Expected Lines Missing from Result + +Allows the actual result to omit lines present in expected output. + +**Pattern: expected-lines** +``` +--@@IGNORE: expected-lines@@ +line one +line two +line three +``` + +**Accepts:** +``` +line one +line two +line three +``` + +**Accepts:** +``` +line one +line three +``` + +**Accepts:** +``` +line two +``` + +**Rejects:** +``` +line one +line four +line three +``` + +--- + +## Combining Options + +Multiple options can be combined: + +**Pattern: Combined options** +``` +--@@IGNORE: case, spaces, numbers@@ +Seq Scan on FOO (cost=0.00..100.00 rows=1000) +``` + +**Accepts:** +``` +seq scan on foo (cost=0.00..50.00 rows=500) +``` + +**Accepts:** +``` +SEQ SCAN ON FOO (cost=0.00..999.99 rows=9999) +``` + +**Rejects:** +``` +Index Scan on foo (cost=0.00..100.00 rows=1000) +``` + +--- + +## Toggling Options + +Options can be enabled and disabled within a file: + +```sql +-- Strict matching by default +SELECT 1; + ?column? +---------- + 1 +(1 row) + +--@@IGNORE: numbers@@ +-- Numbers can vary in this section +EXPLAIN SELECT * FROM foo; + QUERY PLAN +------------------------------------------------------ + Seq Scan on foo (cost=0.00..1.00 rows=100 width=32) +(1 row) + +--@@CHECK: numbers@@ +-- Back to strict matching +SELECT 2; + ?column? +---------- + 2 +(1 row) +``` + + + +**Pattern: toggling options** +``` +--@@IGNORE: numbers@@ +cost=0.00..10.00 +--@@IGNORE: spaces@@ +col1 col2 +--@@CHECK: numbers@@ +rows=50 +--@@IGNORE: case@@ +Select Done +``` + +**Accepts:** +``` +cost=0.00..99.99 +col1 col2 +rows=50 +select done +``` + +**Accepts:** +``` +cost=123.45..999.99 +col1 col2 +rows=50 +SELECT DONE +``` + +**Rejects:** +``` +cost=0.00..99.99 +col1 col2 +rows=999 +select done +``` + +**Rejects:** +``` +const=0.00 .. 99.99 +col1 col2 +rows=50 +Select Done +``` + +**Rejects:** +``` +const=0.00..99.99 +col2 col1 +rows=50 +Select Done +``` diff --git a/src/test/regress/pg_regress.c b/src/test/regress/pg_regress.c index b5c0cb647a8..e11d37fd29e 100644 --- a/src/test/regress/pg_regress.c +++ b/src/test/regress/pg_regress.c @@ -1279,30 +1279,6 @@ file_size(const char *file) 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) - { - diag("could not open file \"%s\" for reading: %m", file); - return -1; - } - while ((c = fgetc(f)) != EOF) - { - if (c == '\n') - l++; - } - fclose(f); - return l; -} - bool file_exists(const char *file) { @@ -1369,32 +1345,307 @@ get_alternative_expectfile(const char *expectfile, int i) } /* - * Run a "diff" command and also check that it didn't crash + * Pattern matching flags for flexible test output comparison. + * These allow expected files to specify which differences to ignore. + */ +#define IGNORE_NONE 0x00 +#define IGNORE_CASE 0x01 +#define IGNORE_COMMENTS 0x02 +#define IGNORE_SPACES 0x04 +#define IGNORE_WRAPPING 0x08 +#define IGNORE_NUMBERS 0x10 +#define IGNORE_RESULT 0x20 /* ignore extra lines in result */ +#define IGNORE_EXPECTED 0x40 /* ignore expected lines missing from result */ + +/* + * Parse a directive line (--@@IGNORE: ...@@ or --@@CHECK: ...@@). + * Returns true if this line is a directive, modifying *flags accordingly. */ -static int -run_diff(const char *cmd, const char *filename) +static bool +parse_directive(const char *line, unsigned int *flags) { - int r; + const char *p; + bool is_ignore; + char optbuf[64]; + int optlen; + unsigned int flag; - fflush(NULL); - r = system(cmd); - if (!WIFEXITED(r) || WEXITSTATUS(r) > 1) + p = line; + while (*p && isspace((unsigned char) *p)) + p++; + + if (strncmp(p, "--@@", 4) != 0) + return false; + p += 4; + + if (strncasecmp(p, "IGNORE", 6) == 0) { - bail("diff command failed with status %d: %s", r, cmd); + is_ignore = true; + p += 6; } -#ifdef WIN32 + else if (strncasecmp(p, "CHECK", 5) == 0) + { + is_ignore = false; + p += 5; + } + else + return false; - /* - * 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) + while (*p && (*p == ':' || *p == ' ' || *p == '\t')) + p++; + + while (*p && strncmp(p, "@@", 2) != 0) { - bail("diff command not found: %s", cmd); + while (*p && (*p == ',' || *p == ' ' || *p == '\t')) + p++; + if (strncmp(p, "@@", 2) == 0) + break; + + optlen = 0; + while (*p && *p != ',' && *p != '@' && !isspace((unsigned char) *p) && + optlen < (int) sizeof(optbuf) - 1) + optbuf[optlen++] = *p++; + optbuf[optlen] = '\0'; + + if (optlen == 0) + continue; + + flag = IGNORE_NONE; + if (strcasecmp(optbuf, "case") == 0) + flag = IGNORE_CASE; + else if (strcasecmp(optbuf, "comments") == 0) + flag = IGNORE_COMMENTS; + else if (strcasecmp(optbuf, "spaces") == 0) + flag = IGNORE_SPACES; + else if (strcasecmp(optbuf, "wrapping") == 0) + flag = IGNORE_WRAPPING; + else if (strcasecmp(optbuf, "numbers") == 0) + flag = IGNORE_NUMBERS; + else if (strcasecmp(optbuf, "result-lines") == 0) + flag = IGNORE_RESULT; + else if (strcasecmp(optbuf, "expected-lines") == 0) + flag = IGNORE_EXPECTED; + + if (is_ignore) + *flags |= flag; + else + *flags &= ~flag; } -#endif + return true; +} + +/* + * Normalize a line according to active ignore flags. + * Returns a newly allocated string. + */ +static char * +normalize_line(const char *line, unsigned int flags) +{ + char *result; + char *dst; + const char *src; + bool in_space = false; + bool in_block_comment = false; + + result = pg_malloc(strlen(line) + 1); + dst = result; + src = line; + + while (*src) + { + if ((flags & IGNORE_COMMENTS) && !in_block_comment && + src[0] == '/' && src[1] == '*') + { + in_block_comment = true; + src += 2; + continue; + } + if (in_block_comment) + { + if (src[0] == '*' && src[1] == '/') + { + in_block_comment = false; + src += 2; + while (*src && isspace((unsigned char) *src)) + src++; + } + else + src++; + continue; + } + if ((flags & IGNORE_COMMENTS) && src[0] == '-' && src[1] == '-') + break; + + if ((flags & IGNORE_SPACES) && isspace((unsigned char) *src)) + { + if (!in_space) + { + *dst++ = ' '; + in_space = true; + } + src++; + continue; + } + in_space = false; + + if ((flags & IGNORE_NUMBERS) && + (isdigit((unsigned char) *src) || + (*src == '.' && src[1] != '.' && isdigit((unsigned char) src[1])))) + { + *dst++ = '#'; + while (*src && (isdigit((unsigned char) *src) || + (*src == '.' && src[1] != '.'))) + src++; + continue; + } + + if (flags & IGNORE_CASE) + *dst++ = tolower((unsigned char) *src++); + else + *dst++ = *src++; + } + *dst = '\0'; + + /* Trim trailing whitespace */ + while (dst > result && isspace((unsigned char) dst[-1])) + *--dst = '\0'; + + return result; +} + +/* + * Compare two files using pattern matching. + * Returns true if files match, false otherwise. + * If diff_out is not NULL, writes diff-like output there on mismatch. + */ +static bool +compare_with_patterns(const char *expectfile, const char *resultfile, + char **diff_out) +{ + FILE *exp_fp; + FILE *res_fp; + char exp_line[8192]; + char res_line[8192]; + char **exp_lines = NULL; + char **res_lines = NULL; + unsigned int *exp_flags = NULL; + int exp_count = 0; + int res_count = 0; + int exp_alloc = 0; + int res_alloc = 0; + unsigned int flags = IGNORE_NONE; + int ei, ri, i; + bool match = true; + StringInfoData diff_buf; + + if (diff_out) + { + initStringInfo(&diff_buf); + *diff_out = NULL; + } + + exp_fp = fopen(expectfile, "r"); + if (!exp_fp) + return false; + + res_fp = fopen(resultfile, "r"); + if (!res_fp) + { + fclose(exp_fp); + return false; + } + + /* Read expected file, parse directives, store normalized lines and flags */ + while (fgets(exp_line, sizeof(exp_line), exp_fp)) + { + exp_line[strcspn(exp_line, "\r\n")] = '\0'; + if (parse_directive(exp_line, &flags)) + continue; + if (exp_count >= exp_alloc) + { + exp_alloc = exp_alloc ? exp_alloc * 2 : 64; + exp_lines = pg_realloc(exp_lines, exp_alloc * sizeof(char *)); + exp_flags = pg_realloc(exp_flags, exp_alloc * sizeof(unsigned int)); + } + exp_lines[exp_count] = normalize_line(exp_line, flags); + exp_flags[exp_count] = flags; + exp_count++; + } + fclose(exp_fp); + + /* Read result file and store raw lines */ + while (fgets(res_line, sizeof(res_line), res_fp)) + { + res_line[strcspn(res_line, "\r\n")] = '\0'; + if (res_count >= res_alloc) + { + res_alloc = res_alloc ? res_alloc * 2 : 64; + res_lines = pg_realloc(res_lines, res_alloc * sizeof(char *)); + } + res_lines[res_count++] = pg_strdup(res_line); + } + fclose(res_fp); + + /* Compare lines using two-pointer approach */ + ei = 0; + ri = 0; + while (ei < exp_count || ri < res_count) + { + flags = (ei < exp_count) ? exp_flags[ei] : IGNORE_NONE; + + if (ei < exp_count && ri < res_count) + { + char *norm_res = normalize_line(res_lines[ri], flags); + int cmp = strcmp(exp_lines[ei], norm_res); + free(norm_res); + + if (cmp == 0) + { + ei++; + ri++; + continue; + } + } - return WEXITSTATUS(r); + /* Mismatch - check ignore flags */ + if (ri < res_count && (flags & IGNORE_RESULT)) + { + ri++; + continue; + } + if (ei < exp_count && (flags & IGNORE_EXPECTED)) + { + ei++; + continue; + } + + match = false; + if (diff_out) + { + if (ei < exp_count) + appendStringInfo(&diff_buf, "-%s\n", exp_lines[ei++]); + if (ri < res_count) + appendStringInfo(&diff_buf, "+%s\n", res_lines[ri++]); + } + else + break; + } + + for (i = 0; i < exp_count; i++) + free(exp_lines[i]); + for (i = 0; i < res_count; i++) + free(res_lines[i]); + if (exp_lines) + free(exp_lines); + if (exp_flags) + free(exp_flags); + if (res_lines) + free(res_lines); + + if (diff_out) + *diff_out = diff_buf.data; + + return match; } /* @@ -1407,14 +1658,11 @@ 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; + char *diff_output = NULL; /* * We can pass either the resultsfile or the expectfile, they should have @@ -1435,25 +1683,13 @@ results_differ(const char *testname, const char *resultsfile, const char *defaul 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); + /* Try pattern-based comparison with primary expectfile */ + if (compare_with_patterns(expectfile, resultsfile, NULL)) return false; - } - /* There may be secondary comparison files that match better */ - best_line_count = file_line_count(diff); strcpy(best_expect_file, expectfile); + /* There may be secondary comparison files that match better */ for (i = 0; i <= 9; i++) { char *alt_expectfile; @@ -1468,24 +1704,11 @@ results_differ(const char *testname, const char *resultsfile, const char *defaul continue; } - snprintf(cmd, sizeof(cmd), - "diff %s \"%s\" \"%s\" > \"%s\"", - basic_diff_opts, alt_expectfile, resultsfile, diff); - - if (run_diff(cmd, diff) == 0) + if (compare_with_patterns(alt_expectfile, resultsfile, NULL)) { - 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); } @@ -1493,51 +1716,31 @@ results_differ(const char *testname, const char *resultsfile, const char *defaul * 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); + if (compare_with_patterns(default_expectfile, resultsfile, NULL)) 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. + * Use the primary comparison file to generate the diff output, + * which we append to the diffs summary file. */ + compare_with_patterns(best_expect_file, resultsfile, &diff_output); - /* Write diff header */ difffile = fopen(difffilename, "a"); if (difffile) { - fprintf(difffile, - "diff %s %s %s\n", - pretty_diff_opts, best_expect_file, resultsfile); + fprintf(difffile, "diff %s %s\n", best_expect_file, resultsfile); + if (diff_output) + fprintf(difffile, "%s", diff_output); + fprintf(difffile, "\n"); 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); + if (diff_output) + free(diff_output); - unlink(diff); return true; } @@ -2096,6 +2299,7 @@ regression_main(int argc, char *argv[], {"config-auth", required_argument, NULL, 24}, {"max-concurrent-tests", required_argument, NULL, 25}, {"expecteddir", required_argument, NULL, 26}, + {"compare", no_argument, NULL, 27}, {NULL, 0, NULL, 0} }; @@ -2224,6 +2428,22 @@ regression_main(int argc, char *argv[], case 26: expecteddir = pg_strdup(optarg); break; + case 27: + /* --compare: compare two files and exit */ + if (argc - optind >= 2) + { + char *diff_output = NULL; + bool match; + + match = compare_with_patterns(argv[optind], argv[optind + 1], &diff_output); + if (diff_output && *diff_output) + printf("%s", diff_output); + if (diff_output) + free(diff_output); + exit(match ? 0 : 1); + } + pg_log_error("--compare requires two file arguments"); + exit(2); default: /* getopt_long already emitted a complaint */ pg_log_error_hint("Try \"%s --help\" for more information.", diff --git a/src/test/regress/test_readme_examples.sh b/src/test/regress/test_readme_examples.sh new file mode 100755 index 00000000000..65de039793a --- /dev/null +++ b/src/test/regress/test_readme_examples.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# +# test_readme_examples.sh - Test pattern matching examples from README.md +# +# Extracts Pattern/Accepts/Rejects blocks and tests them against +# the pattern matching implementation. Saves test files to .test_pattern_files/ +# + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +README="$SCRIPT_DIR/README.md" +PG_REGRESS="$SCRIPT_DIR/pg_regress" +TESTDIR="$SCRIPT_DIR/.test_pattern_files" +RED="\033[0;31m" +GREEN="\033[0;32m" +RESET="\033[0m" +# Create test directory +mkdir -p "$TESTDIR" + +# Build pg_regress if needed +if [ ! -x "$PG_REGRESS" ]; then + echo "Building pg_regress..." + make -C "$SCRIPT_DIR" pg_regress >/dev/null 2>&1 +fi + +echo "=== Testing Pattern Matching Examples from README.md ===" +echo "" + +# State variables +pattern_label="" +pattern_line=0 +pattern_num=0 +passed=0 +failed=0 +skipped=0 + +# Block parsing state +block_type="" +block_label="" +block_content="" +block_start_line=0 +in_block=false + +line_num=0 +while IFS= read -r line || [[ -n "$line" ]]; do + ((line_num++)) + + # Check for **Pattern/Accepts/Rejects** marker + if [[ "$line" =~ ^\*\*(Pattern|Accepts|Rejects):?[[:space:]]*([^*]*)\*\*[[:space:]]*$ ]]; then + block_type="${BASH_REMATCH[1]}" + block_label="${BASH_REMATCH[2]}" + # Trim whitespace + block_label="${block_label#"${block_label%%[![:space:]]*}"}" + block_label="${block_label%"${block_label##*[![:space:]]}"}" + continue + fi + + # Check for ``` start after seeing a marker + if [[ -n "$block_type" && "$line" == '```' && "$in_block" == false ]]; then + in_block=true + block_content="" + block_start_line=$line_num + continue + fi + + # Check for ``` end + if [[ "$in_block" == true && "$line" == '```' ]]; then + in_block=false + + if [[ "$block_type" == "Pattern" ]]; then + ((pattern_num++)) + if [[ -n "$block_label" ]]; then + pattern_label="$block_label" + else + pattern_label="pattern_$pattern_num" + fi + pattern_line=$block_start_line + + # Save pattern file + printf '%s' "$block_content" > "$TESTDIR/pattern-L$pattern_line.out" + + elif [[ -n "$pattern_label" ]]; then + test_type=$(echo "$block_type" | tr '[:upper:]' '[:lower:]') + location="README.md:$block_start_line" + pattern_file="$TESTDIR/pattern-L$pattern_line.out" + test_file="$TESTDIR/L$pattern_line-$test_type-L$block_start_line.out" + diff_file="$TESTDIR/L$pattern_line-$test_type-L$block_start_line.diff" + pattern_loc="README.md:$pattern_line" + + # Save test file + printf '%s' "$block_content" > "$test_file" + + # Run comparison + if "$PG_REGRESS" --compare "$pattern_file" "$test_file" > "$diff_file" ; then + pg_regress_result=Accepts + else + pg_regress_result=Rejects + fi + + + if [[ "$block_type" == "$pg_regress_result" ]]; then + test_status="$GREEN[PASS]$RESET" + ((passed++)) + else + test_status="$RED[FAIL]$RESET" + ((failed++)) + fi + echo -e "$test_status $pattern_label ($pattern_loc) $test_type ($location)" + ((passed++)) + fi + + block_type="" + continue + fi + + # Accumulate block content + if [[ "$in_block" == true ]]; then + if [[ -n "$block_content" ]]; then + block_content+=$'\n' + fi + block_content+="$line" + fi +done < "$README" + +echo "" +echo "=== Results ===" +echo -e "Passed: $GREEN$passed$RESET" +echo -e "Failed: $RED$failed$RESET" +echo "Files: .test_pattern_files/" + +[[ $failed -eq 0 ]] -- 2.40.0