From 1aed9a7572bcdfbd789a69eea5d4a7c7a47b300a Mon Sep 17 00:00:00 2001 From: Bryan Green Date: Tue, 21 Oct 2025 19:31:29 -0500 Subject: [PATCH v2] Add Windows support for backtrace_functions. backtrace_functions has been Unix-only up to now, because we relied on glibc's backtrace() or similar platform facilities. Windows doesn't have anything equivalent in its standard C library, but it does have the DbgHelp API, which can do the same thing if you ask it nicely. The tricky bit is that DbgHelp needs to be initialized with SymInitialize() before you can use it, and that's a fairly expensive operation. We don't want to do that every time we generate a backtrace. Fortunately, it turns out that we can initialize once per process and reuse the handle, which is safe since we're holding an exclusive lock during error reporting anyway. So the code just initializes lazily on first use. If SymInitialize fails, we don't consider that an error; we just silently decline to generate backtraces. This seems reasonable since backtraces are a debugging aid, not critical to operation. It also matches the behavior on platforms where backtrace() isn't available. Symbol resolution quality depends on whether PDB files are present. If they are, DbgHelp can give us source file paths and line numbers, which is great. If not, it can still give us function names by reading the export table, and that turns out to be good enough because postgres.exe exports thousands of functions. (You get export symbols on Windows whether you like it or not, unless you go out of your way to suppress them. Might as well take advantage of that.) Fully stripped binaries would only show addresses, but that's not a scenario that applies to Postgres, so we don't worry about it. The TAP test verifies that symbol resolution works correctly in both the with-PDB and without-PDB cases. We have to use TAP because backtraces go to the server log, not to psql. The test figures out which case should apply by checking whether postgres.pdb exists on disk, then parses the backtrace output to see what we actually got. If those don't match, that's a bug. This should catch the case where the PDB exists but DbgHelp fails to load it, which seems like the most likely way this could break. The test also verifies that we can generate a bunch of backtraces in quick succession without crashing, which is really just a basic sanity check. It doesn't attempt to detect memory leaks, since that would require external tools we don't want to depend on. Author: Bryan Green --- src/backend/meson.build | 5 + src/backend/utils/error/elog.c | 174 +++++++ src/test/modules/meson.build | 1 + src/test/modules/test_backtrace/Makefile | 33 ++ src/test/modules/test_backtrace/README | 224 +++++++++ src/test/modules/test_backtrace/meson.build | 13 + .../test_backtrace/t/t_windows_backtrace.pl | 428 ++++++++++++++++++ .../test_backtrace/test_backtrace--1.0.sql | 66 +++ .../test_backtrace/test_backtrace.control | 5 + 9 files changed, 949 insertions(+) create mode 100644 src/test/modules/test_backtrace/Makefile create mode 100644 src/test/modules/test_backtrace/README create mode 100644 src/test/modules/test_backtrace/meson.build create mode 100644 src/test/modules/test_backtrace/t/t_windows_backtrace.pl create mode 100644 src/test/modules/test_backtrace/test_backtrace--1.0.sql create mode 100644 src/test/modules/test_backtrace/test_backtrace.control diff --git a/src/backend/meson.build b/src/backend/meson.build index b831a54165..eeb69c4079 100644 --- a/src/backend/meson.build +++ b/src/backend/meson.build @@ -1,6 +1,11 @@ # Copyright (c) 2022-2025, PostgreSQL Global Development Group backend_build_deps = [backend_code] + +if host_system == 'windows' and cc.get_id() == 'msvc' + backend_build_deps += cc.find_library('dbghelp') +endif + backend_sources = [] backend_link_with = [pgport_srv, common_srv] diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c index 29643c5143..fc421ce444 100644 --- a/src/backend/utils/error/elog.c +++ b/src/backend/utils/error/elog.c @@ -140,6 +140,13 @@ static void write_syslog(int level, const char *line); static void write_eventlog(int level, const char *line, int len); #endif +#ifdef _MSC_VER +#include +#include +static bool win32_backtrace_symbols_initialized = false; +static HANDLE win32_backtrace_process = NULL; +#endif + /* We provide a small stack of ErrorData records for re-entrant cases */ #define ERRORDATA_STACK_SIZE 5 @@ -1116,6 +1123,18 @@ errbacktrace(void) return 0; } +#ifdef _MSC_VER +/* + * Cleanup function for DbgHelp resources. + * Called via on_proc_exit() to release resources allocated by SymInitialize(). + */ +static void +win32_backtrace_cleanup(int code, Datum arg) +{ + SymCleanup(win32_backtrace_process); +} +#endif + /* * Compute backtrace data and add it to the supplied ErrorData. num_skip * specifies how many inner frames to skip. Use this to avoid showing the @@ -1147,6 +1166,161 @@ set_backtrace(ErrorData *edata, int num_skip) appendStringInfoString(&errtrace, "insufficient memory for backtrace generation"); } +#elif defined(_MSC_VER) + { + void *stack[100]; + DWORD frames; + DWORD i; + wchar_t buffer[sizeof(SYMBOL_INFOW) + MAX_SYM_NAME * sizeof(wchar_t)]; + PSYMBOL_INFOW symbol; + char *utf8_buffer; + int utf8_len; + + if (!win32_backtrace_symbols_initialized) + { + win32_backtrace_process = GetCurrentProcess(); + + SymSetOptions(SYMOPT_UNDNAME | + SYMOPT_DEFERRED_LOADS | + SYMOPT_LOAD_LINES | + SYMOPT_FAIL_CRITICAL_ERRORS); + + if (SymInitialize(win32_backtrace_process, NULL, TRUE)) + { + win32_backtrace_symbols_initialized = true; + on_proc_exit(win32_backtrace_cleanup, 0); + } + else + { + DWORD error = GetLastError(); + + elog(WARNING, "SymInitialize failed with error %lu", error); + } + } + + frames = CaptureStackBackTrace(num_skip, lengthof(stack), stack, NULL); + + if (frames == 0) + { + appendStringInfoString(&errtrace, "\nNo stack frames captured"); + edata->backtrace = errtrace.data; + return; + } + + symbol = (PSYMBOL_INFOW) buffer; + symbol ->MaxNameLen = MAX_SYM_NAME; + symbol ->SizeOfStruct = sizeof(SYMBOL_INFOW); + + for (i = 0; i < frames; i++) + { + DWORD64 address = (DWORD64) (stack[i]); + DWORD64 displacement = 0; + BOOL sym_result; + + sym_result = SymFromAddrW(win32_backtrace_process, + address, + &displacement, + symbol); + + if (sym_result) + { + IMAGEHLP_LINEW64 line; + DWORD line_displacement = 0; + + line.SizeOfStruct = sizeof(IMAGEHLP_LINEW64); + + if (SymGetLineFromAddrW64(win32_backtrace_process, + address, + &line_displacement, + &line)) + { + /* Convert symbol name to UTF-8 */ + utf8_len = WideCharToMultiByte(CP_UTF8, 0, symbol->Name, -1, + NULL, 0, NULL, NULL); + if (utf8_len > 0) + { + char *filename_utf8; + int filename_len; + + utf8_buffer = palloc(utf8_len); + WideCharToMultiByte(CP_UTF8, 0, symbol->Name, -1, + utf8_buffer, utf8_len, NULL, NULL); + + /* Convert file name to UTF-8 */ + filename_len = WideCharToMultiByte(CP_UTF8, 0, + line.FileName, -1, + NULL, 0, NULL, NULL); + if (filename_len > 0) + { + filename_utf8 = palloc(filename_len); + WideCharToMultiByte(CP_UTF8, 0, line.FileName, -1, + filename_utf8, filename_len, + NULL, NULL); + + appendStringInfo(&errtrace, + "\n%s+0x%llx [%s:%lu]", + utf8_buffer, + (unsigned long long) displacement, + filename_utf8, + (unsigned long) line.LineNumber); + + pfree(filename_utf8); + } + else + { + appendStringInfo(&errtrace, + "\n%s+0x%llx [0x%llx]", + utf8_buffer, + (unsigned long long) displacement, + (unsigned long long) address); + } + + pfree(utf8_buffer); + } + else + { + /* Conversion failed, use address only */ + appendStringInfo(&errtrace, + "\n[0x%llx]", + (unsigned long long) address); + } + } + else + { + /* No line info, convert symbol name only */ + utf8_len = WideCharToMultiByte(CP_UTF8, 0, symbol->Name, -1, + NULL, 0, NULL, NULL); + if (utf8_len > 0) + { + utf8_buffer = palloc(utf8_len); + WideCharToMultiByte(CP_UTF8, 0, symbol->Name, -1, + utf8_buffer, utf8_len, NULL, NULL); + + appendStringInfo(&errtrace, + "\n%s+0x%llx [0x%llx]", + utf8_buffer, + (unsigned long long) displacement, + (unsigned long long) address); + + pfree(utf8_buffer); + } + else + { + /* Conversion failed, use address only */ + appendStringInfo(&errtrace, + "\n[0x%llx]", + (unsigned long long) address); + } + } + } + else + { + appendStringInfo(&errtrace, + "\n[0x%llx]", + (unsigned long long) address); + } + } + } #else appendStringInfoString(&errtrace, "backtrace generation is not supported by this installation"); diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index 14fc761c4c..ccb63f2b57 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -14,6 +14,7 @@ subdir('plsample') subdir('spgist_name_ops') subdir('ssl_passphrase_callback') subdir('test_aio') +subdir('test_backtrace') subdir('test_binaryheap') subdir('test_bitmapset') subdir('test_bloomfilter') diff --git a/src/test/modules/test_backtrace/Makefile b/src/test/modules/test_backtrace/Makefile new file mode 100644 index 0000000000..3e3112cd74 --- /dev/null +++ b/src/test/modules/test_backtrace/Makefile @@ -0,0 +1,33 @@ +# src/test/modules/test_backtrace/Makefile +# +# Makefile for Windows backtrace testing module + +MODULE_big = test_backtrace +OBJS = test_backtrace.o + +EXTENSION = test_backtrace +DATA = test_backtrace--1.0.sql + +# Only TAP tests - no SQL regression tests since backtraces +# go to server logs, not to client output +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_backtrace +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +# Platform-specific test handling +ifeq ($(PORTNAME),win32) + # Run all tests on Windows + PROVE_FLAGS += -v +else + # Skip tests on non-Windows platforms + TAP_TESTS = 0 +endif diff --git a/src/test/modules/test_backtrace/README b/src/test/modules/test_backtrace/README new file mode 100644 index 0000000000..7388320ad9 --- /dev/null +++ b/src/test/modules/test_backtrace/README @@ -0,0 +1,224 @@ +================================================================================ + Windows Backtrace Tests +================================================================================ + +TAP tests for the Windows backtrace implementation, which uses the DbgHelp API +to capture and format stack traces. + +-------------------------------------------------------------------------------- +Why TAP Tests? +-------------------------------------------------------------------------------- + +Backtraces appear in the server log, not in psql output. So we can't use SQL +tests to validate them; we need TAP tests that can read the actual log files. + +-------------------------------------------------------------------------------- +Test File +-------------------------------------------------------------------------------- + +t_windows_backtrace.pl + + Tests backtrace generation in two scenarios: + + (1) WITH PDB FILE: postgres.pdb exists alongside postgres.exe + Expected: Function names, source files, line numbers, addresses + Validates: DbgHelp loads the PDB and resolves full symbols + + (2) WITHOUT PDB FILE: postgres.pdb is absent + Expected: Function names and addresses (from export table) + Validates: We can still get useful backtraces without debug info + + The test figures out which scenario applies by checking whether postgres.pdb + exists, then validates that the actual backtrace format matches expectations. + If the PDB is present but we don't get source files, that's a bug. + + What gets tested: + + - Basic functionality (backtraces appear in logs) + - Symbol resolution (correct format for scenario) + - Various error types (div-by-zero, constraints, etc) + - PL/pgSQL and trigger integration + - Stability (20 rapid errors don't crash anything) + - DbgHelp initialization doesn't fail + + What doesn't get tested: + + - Memory leaks (would need Dr. Memory or similar) + - Symbol servers (would need network setup) + - Stripped binaries (not relevant for Postgres) + +-------------------------------------------------------------------------------- +Running the Tests +-------------------------------------------------------------------------------- + +Prerequisites: + + - Windows (test skips on other platforms) + - MSVC build (needs DbgHelp) + - Perl with PostgreSQL::Test modules + +From the PostgreSQL build directory: + + meson test --suite test_backtrace --verbose + +Or with prove: + + set PERL5LIB=C:\path\to\postgres\src\test\perl + prove src/test/modules/test_backtrace/t/t_windows_backtrace.pl + +To test both scenarios: + + (1) WITH PDB: Just run the test normally + - PDB should be in the same directory as postgres.exe + - Test expects to find source file information + + (2) WITHOUT PDB: Delete the PDB file and re-run + - del build_dir\tmp_install\...\bin\postgres.pdb + - Test expects export symbols only + +-------------------------------------------------------------------------------- +Expected Output +-------------------------------------------------------------------------------- + +With PDB: + + PDB file found: C:\...\postgres.pdb + EXPECTED: Scenario 1 (full PDB symbols) + ACTUAL: Scenario 1 (found source files and symbols) + ok - Scenario matches expectation: Scenario 1 + + ERROR: division by zero + BACKTRACE: + int4div+0x2a [C:\postgres\src\backend\utils\adt\int.c:841] [0x00007FF6...] + ExecInterpExpr+0x1b3 [C:\postgres\src\backend\executor\execExprInterp.c:2345] + ... + +Without PDB: + + PDB file not found: C:\...\postgres.pdb + EXPECTED: Scenario 2 (export symbols only) + ACTUAL: Scenario 2 (found symbols but no source files) + ok - Scenario matches expectation: Scenario 2 + + ERROR: division by zero + BACKTRACE: + int4div+0x2a [0x00007FF6...] + ExecInterpExpr+0x1b3 [0x00007FF6...] + ... + +Note: Postgres exports ~11,000 functions, so even without a PDB, you get +function names. Fully stripped binaries would only show addresses, but +that's not a scenario we care about for Postgres. + +Failure (PDB exists but doesn't load): + + PDB file found: C:\...\postgres.pdb + EXPECTED: Scenario 1 (full PDB symbols) + ACTUAL: Scenario 2 (found symbols but no source files) + not ok - PDB file exists but symbols not loading! + + This means DbgHelp couldn't load the PDB. Possible causes: corrupted PDB, + mismatched PDB (from different build), or DbgHelp initialization failed. + +-------------------------------------------------------------------------------- +How It Works +-------------------------------------------------------------------------------- + +The test validates expected vs actual: + + 1. Check if postgres.pdb exists on disk + -> If yes, expect full PDB symbols + -> If no, expect export symbols only + + 2. Parse the backtrace output + -> If source files present, got PDB symbols + -> If no source files, got exports only + + 3. Compare expected to actual + -> Pass if they match + -> Fail if they don't (indicates a problem) + +This catches the case where the PDB exists but DbgHelp fails to load it, +which is the most likely failure mode. + +-------------------------------------------------------------------------------- +Configuration +-------------------------------------------------------------------------------- + +The test configures the server with: + + backtrace_functions = 'int4div,int4in,ExecInterpExpr' + log_error_verbosity = verbose + logging_collector = on + log_destination = 'stderr' + log_min_messages = error + +Nothing fancy. Just enough to generate backtraces and make them easy to find +in the logs. + +-------------------------------------------------------------------------------- +Limitations +-------------------------------------------------------------------------------- + +This test verifies basic functionality. It does not: + + - Detect memory leaks (would need Dr. Memory, ASAN, or similar) + - Test symbol server scenarios (would need network setup and config) + - Validate symbol accuracy in detail (just checks format) + - Test performance or memory usage + - Validate path remapping (future work) + +The test ensures the feature works and doesn't crash. That's about it. + +-------------------------------------------------------------------------------- +Troubleshooting +-------------------------------------------------------------------------------- + +"PDB file exists but symbols not loading!" + + The PDB is there but DbgHelp couldn't use it. + + Check: + - Is the PDB corrupted? + - Does the PDB match the executable? (same build) + - Are there SymInitialize errors in the log? + +"Scenario mismatch" (other than PDB not loading) + + Something weird happened. Look at the test output to see what was expected + vs what was found, and figure out what's going on. + +No backtraces at all + + Check: + - Is backtrace_functions configured? + - Is logging_collector enabled? + - Are there SymInitialize failures in the log? + +Only addresses, no function names (even without PDB) + + This would be very strange, since Postgres exports thousands of functions. + DbgHelp should be able to get them from the export table. Check that + postgres.exe was linked normally. + +Test hangs + + Probably a logging issue. Check that logging_collector is working and + log files are appearing in the data directory. + +-------------------------------------------------------------------------------- +Symbol Servers +-------------------------------------------------------------------------------- + +This test doesn't try to exercise symbol server functionality. It just checks +whether a local PDB file gets used. Symbol servers are a deployment concern, +not a functionality test. + +For deployments, you'd typically either: + - Ship PDB files alongside executables (development/staging) + - Don't ship PDB files (production, smaller footprint) + +In the latter case, you still get useful backtraces from the export table. +Whether that's sufficient depends on your debugging needs. + +================================================================================ diff --git a/src/test/modules/test_backtrace/meson.build b/src/test/modules/test_backtrace/meson.build new file mode 100644 index 0000000000..b8b1b8e198 --- /dev/null +++ b/src/test/modules/test_backtrace/meson.build @@ -0,0 +1,13 @@ +# TAP tests - only run on Windows +if host_system == 'windows' + tests += { + 'name': 'test_backtrace', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'tap': { + 'tests': [ + 't/t_windows_backtrace.pl', + ], + }, + } +endif diff --git a/src/test/modules/test_backtrace/t/t_windows_backtrace.pl b/src/test/modules/test_backtrace/t/t_windows_backtrace.pl new file mode 100644 index 0000000000..7609da99eb --- /dev/null +++ b/src/test/modules/test_backtrace/t/t_windows_backtrace.pl @@ -0,0 +1,428 @@ +#!/usr/bin/perl + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Test Windows backtrace generation using DbgHelp API. +# +# The main thing we need to verify is that the DbgHelp integration actually +# works, and that we can resolve symbols both with and without PDB files +# present. We don't try to test symbol server scenarios or anything fancy; +# just check that if you have a PDB, you get source file info, and if you +# don't, you still get function names from the export table (which works +# because postgres.exe exports thousands of functions). +# +# The test automatically detects which situation applies by checking whether +# postgres.pdb exists alongside postgres.exe. This avoids the need to know +# how the executable was built or what build type we're testing. If the PDB +# is there but we don't get source info, that's a bug. If it's not there +# but we somehow get source info anyway, that's weird and worth investigating. + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Skip if not Windows +if ($^O ne 'MSWin32') +{ + plan skip_all => 'Windows-specific backtrace tests'; +} + +# Initialize node +my $node = PostgreSQL::Test::Cluster->new('backtrace_test'); +$node->init; + +# Configure for detailed logging with backtraces +$node->append_conf('postgresql.conf', qq{ +backtrace_functions = 'int4div,int4in,ExecInterpExpr' +log_error_verbosity = verbose +logging_collector = on +log_destination = 'stderr' +log_min_messages = error +}); + +$node->start; + +# Helper to get recent log content. +# Backtraces go to the server log, not to psql output, so we need to read +# the actual log files to validate anything. +sub get_recent_log_content +{ + my $log_dir = $node->data_dir . '/log'; + my @log_files = glob("$log_dir/*.log $log_dir/*.csv"); + + # Get the most recent log file + my $latest_log = (sort { -M $a <=> -M $b } @log_files)[0]; + + return '' unless defined $latest_log && -f $latest_log; + + my $content = ''; + open(my $fh, '<', $latest_log) or return ''; + { + local $/; + $content = <$fh>; + } + close($fh); + + return $content; +} + +############################################################################### +# First, verify basic functionality and figure out what scenario we're in. +# +# We trigger an error and check that (a) it actually generates a backtrace, +# and (b) we can tell from the backtrace format whether we have PDB symbols +# or just exports. Then we compare that to what we should have based on +# whether postgres.pdb exists on disk. +############################################################################### + +note(''); +note('=== PART 1: Basic Error Tests & Scenario Detection ==='); + +### Test 1: Division by zero +my ($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 1/0;"); +ok($ret != 0, 'division by zero error occurred'); +like($stderr, qr/division by zero/i, 'division by zero error message in psql'); + +sleep 1; +my $log_content = get_recent_log_content(); +like($log_content, qr/ERROR:.*division by zero/i, 'error logged to server log'); +like($log_content, qr/BACKTRACE:/i, 'BACKTRACE header in log'); + +### Test 2: Detect scenario and validate it matches expectations +# +# The backtrace format tells us what DbgHelp actually gave us. Source file +# paths mean we got PDB symbols; lack thereof means export symbols only. +# +# We then check whether postgres.pdb exists on disk. If it does, we should +# have gotten PDB symbols; if not, we should have gotten exports only. +# Mismatches indicate a problem with DbgHelp initialization or PDB loading. +my $has_source_files = ($log_content =~ /\[[\w:\\\/]+\.\w+:\d+\]/); # [file.c:123] +my $has_symbols = ($log_content =~ /\w+\+0x[0-9a-fA-F]+/); # function+0xABC +my $has_addresses = ($log_content =~ /\[0x[0-9a-fA-F]+\]/); # [0xABCDEF] + +ok($has_symbols || $has_addresses, 'backtrace has valid format'); + +# Determine EXPECTED scenario based on PDB file existence. +# +# We need to find where postgres.exe actually lives. The TAP test framework +# creates a temporary install, and we can find the bin directory by looking +# at the parent of the data directory. If that doesn't work, search PATH. +# (This is a bit ugly but there's no direct way to ask the test node for +# its bin directory.) +my $datadir = $node->data_dir; +my $postgres_exe; + +# Try to find the bin directory relative to data directory +# Typical structure: .../tmp_install/PORT_XXX/data and .../tmp_install/PORT_XXX/bin +if ($datadir =~ /^(.+)[\\\/][^\\\/]+$/) +{ + my $base = $1; # Get parent directory + $postgres_exe = "$base/bin/postgres.exe"; +} + +# Fallback: try to construct from test environment +if (!defined($postgres_exe) || !-f $postgres_exe) +{ + # Try using PATH - the test sets up PATH to include the bin directory + my $path_dirs = $ENV{PATH}; + foreach my $dir (split(/;/, $path_dirs)) + { + my $candidate = "$dir/postgres.exe"; + if (-f $candidate) + { + $postgres_exe = $candidate; + last; + } + } +} + +# If still not found, just use the command name and note that we couldn't find it +if (!defined($postgres_exe)) +{ + $postgres_exe = 'postgres.exe'; +} + +my $postgres_pdb = $postgres_exe; +$postgres_pdb =~ s/\.exe$/.pdb/i; + +my $expected_scenario; +if (-f $postgres_pdb) +{ + $expected_scenario = 1; # PDB exists, we SHOULD have full symbols + note("PDB file found: $postgres_pdb"); + note("EXPECTED: Scenario 1 (full PDB symbols)"); +} +else +{ + $expected_scenario = 2; # No PDB, we SHOULD have export symbols only + note("PDB file not found: $postgres_pdb"); + note("EXPECTED: Scenario 2 (export symbols only)"); +} + +# Determine ACTUAL scenario from log output +my $actual_scenario; +if ($has_source_files && $has_symbols) +{ + $actual_scenario = 1; # Full PDB symbols + note('ACTUAL: Scenario 1 (found source files and symbols)'); +} +elsif ($has_symbols && !$has_source_files) +{ + $actual_scenario = 2; # Export symbols only + note('ACTUAL: Scenario 2 (found symbols but no source files)'); +} +else +{ + $actual_scenario = 0; # Unknown/invalid + fail('Unable to determine scenario - PostgreSQL should always have export symbols'); + note('Expected either Scenario 1 (with PDB) or Scenario 2 (without PDB)'); +} + +# CRITICAL TEST: Validate actual matches expected. +# +# This is the main point of the test. We need to verify that DbgHelp is +# actually loading symbols correctly. If the PDB exists but we don't get +# source files, that's a bug. If the PDB doesn't exist but we somehow get +# source files anyway, that's bizarre and worth investigating. +if ($actual_scenario == $expected_scenario) +{ + pass("Scenario matches expectation: Scenario $actual_scenario"); + note(''); + if ($actual_scenario == 1) { + note('*** SCENARIO 1: Full PDB symbols (build WITH .pdb file) ***'); + note('Build type: Release/Debug/DebugOptimized WITH .pdb file'); + note('Format: function+offset [file.c:line] [0xaddress]'); + } + elsif ($actual_scenario == 2) { + note('*** SCENARIO 2: Export symbols only (build WITHOUT .pdb file) ***'); + note('Build type: Release WITHOUT .pdb file'); + note('Format: function+offset [0xaddress]'); + } + note(''); +} +elsif ($expected_scenario == 1 && $actual_scenario == 2) +{ + fail('PDB file exists but symbols not loading!'); + note("PDB file found at: $postgres_pdb"); + note('Expected: Full PDB symbols with source files'); + note('Actual: Only export symbols (no source files)'); + note('This indicates PDB is not being loaded by DbgHelp'); +} +elsif ($expected_scenario == 2 && $actual_scenario == 1) +{ + fail('Found PDB symbols but PDB file does not exist!'); + note("PDB file not found at: $postgres_pdb"); + note('Expected: Export symbols only'); + note('Actual: Full PDB symbols with source files'); + note('This is unexpected - where are the symbols coming from?'); +} +else +{ + fail("Scenario mismatch: expected $expected_scenario, got $actual_scenario"); +} + +my $scenario = $actual_scenario; + +############################################################################### +# Now validate the backtrace format matches what we expect for this scenario. +# +# If we have PDB symbols, check for function names, source files, and addresses. +# If we only have exports, check that source files are absent (as expected). +# The point is to verify that the format is sane, not just that it exists. +############################################################################### + +note(''); +note('=== PART 2: Format Validation ==='); + +if ($scenario == 1) +{ + # Scenario 1: Full PDB symbols + like($log_content, qr/\w+\+0x[0-9a-fA-F]+/, + 'Scenario 1: function+offset format present'); + + like($log_content, qr/\[[\w:\\\/]+\.\w+:\d+\]/, + 'Scenario 1: source files and line numbers present'); + + like($log_content, qr/\[0x[0-9a-fA-F]+\]/, + 'Scenario 1: addresses present'); + + # Extract example paths to show in test output + my @example_paths = ($log_content =~ /\[([^\]]+\.\w+:\d+)\]/g); + + if (@example_paths) + { + pass('Scenario 1: backtrace includes source file paths'); + note("Example path: $example_paths[0]"); + } + else + { + fail('Scenario 1: no source file paths found'); + } +} +elsif ($scenario == 2) +{ + # Scenario 2: Export symbols only + like($log_content, qr/\w+\+0x[0-9a-fA-F]+/, + 'Scenario 2: function+offset format present'); + + like($log_content, qr/\[0x[0-9a-fA-F]+\]/, + 'Scenario 2: addresses present'); + + unlike($log_content, qr/\[[\w:\\\/]+\.\w+:\d+\]/, + 'Scenario 2: no source files (expected without PDB)'); +} + +############################################################################### +# Test that backtraces work for various types of errors. +# +# The backtrace mechanism should work regardless of what triggered the error. +# Try a few different error types to make sure we're not somehow dependent on +# the error path. Also check that PL/pgSQL and triggers work, since those go +# through different code paths. +############################################################################### + +note(''); +note('=== PART 3: Error Scenario Coverage ==='); + +### Test: Type conversion error +($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 'invalid'::integer;"); +ok($ret != 0, 'type conversion error occurred'); +like($stderr, qr/invalid input syntax/i, 'type conversion error message'); + +### Test: Constraint violation +$node->safe_psql('postgres', "CREATE TABLE test_table (id integer PRIMARY KEY);"); +($ret, $stdout, $stderr) = $node->psql('postgres', + "INSERT INTO test_table VALUES (1), (1);"); +ok($ret != 0, 'constraint violation occurred'); +like($stderr, qr/(duplicate key|unique constraint)/i, 'constraint violation message'); + +### Test: PL/pgSQL nested function +$node->safe_psql('postgres', q{ + CREATE FUNCTION nested_func() RETURNS void AS $$ + BEGIN + PERFORM 1/0; + END; + $$ LANGUAGE plpgsql; +}); + +($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT nested_func();"); +ok($ret != 0, 'nested function error occurred'); + +sleep 1; +$log_content = get_recent_log_content(); +my @addresses = ($log_content =~ /\[0x[0-9a-fA-F]+\]/g); +my $frame_count = scalar @addresses; +ok($frame_count >= 3, + "PL/pgSQL error has deeper stack (found $frame_count frames)"); + +### Test: Trigger error +$node->safe_psql('postgres', q{ + CREATE TABLE trigger_test (val integer); + + CREATE FUNCTION trigger_func() RETURNS trigger AS $$ + BEGIN + PERFORM 1/0; + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER test_trigger BEFORE INSERT ON trigger_test + FOR EACH ROW EXECUTE FUNCTION trigger_func(); +}); + +($ret, $stdout, $stderr) = $node->psql('postgres', + "INSERT INTO trigger_test VALUES (1);"); +ok($ret != 0, 'trigger error occurred'); +like($stderr, qr/division by zero/i, 'trigger error message'); + +############################################################################### +# Verify that repeated backtrace generation doesn't cause problems. +# +# We don't have a good way to detect memory leaks in this test, but we can +# at least check that the server doesn't crash or start spewing errors after +# we've generated a bunch of backtraces. Also verify that DbgHelp doesn't +# log any initialization failures. +############################################################################### + +note(''); +note('=== PART 4: Stability Tests ==='); + +### Test: Multiple errors don't crash the server +my $error_count = 0; +for my $i (1..20) +{ + ($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 1/0;"); + $error_count++ if ($ret != 0); +} + +is($error_count, 20, 'all 20 rapid errors occurred (DbgHelp stable)'); + +sleep 2; +$log_content = get_recent_log_content(); +@addresses = ($log_content =~ /\[0x[0-9a-fA-F]+\]/g); +ok(scalar(@addresses) >= 20, + 'multiple rapid errors produced backtraces (' . scalar(@addresses) . ' addresses found)'); + +### Test: No SymInitialize failures +unlike($log_content, qr/SymInitialize.*failed/i, + 'no SymInitialize failures in log'); + +### Test: Repeated errors in same session +$node->safe_psql('postgres', q{ + DO $$ + BEGIN + FOR i IN 1..10 LOOP + BEGIN + EXECUTE 'SELECT 1/0'; + EXCEPTION + WHEN division_by_zero THEN + NULL; -- Swallow the error + END; + END LOOP; + END $$; +}); + +pass('repeated errors in same session did not crash'); + +############################################################################### +# Summary +############################################################################### + +note(''); +note('=== TEST SUMMARY ==='); +note("Scenario: $scenario"); +note(''); + +if ($scenario == 1) { + note('BUILD TYPE: Release/Debug/DebugOptimized WITH .pdb file'); + note(''); + note('Validated:'); + note(' ✓ Function names with offsets'); + note(' ✓ Source files and line numbers'); + note(' ✓ Memory addresses'); + note(' ✓ Stack depth (shallow and nested)'); + note(' ✓ Multiple error scenarios'); + note(' ✓ Stability (20 rapid errors, no crashes)'); + note(' ✓ No DbgHelp initialization failures'); +} +elsif ($scenario == 2) { + note('BUILD TYPE: Release WITHOUT .pdb file'); + note(''); + note('Validated:'); + note(' ✓ Function names with offsets'); + note(' ✓ Memory addresses'); + note(' ✗ No source files (expected - no PDB)'); + note(' ✓ Stack depth (shallow and nested)'); + note(' ✓ Multiple error scenarios'); + note(' ✓ Stability (20 rapid errors, no crashes)'); + note(' ✓ No DbgHelp initialization failures'); +} +note('===================='); +note(''); + +$node->stop; + +done_testing(); diff --git a/src/test/modules/test_backtrace/test_backtrace--1.0.sql b/src/test/modules/test_backtrace/test_backtrace--1.0.sql new file mode 100644 index 0000000000..f2e614a18d --- /dev/null +++ b/src/test/modules/test_backtrace/test_backtrace--1.0.sql @@ -0,0 +1,66 @@ +/* src/test/modules/test_backtrace/test_backtrace--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_backtrace" to load this file. \quit + +-- +-- Test functions for Windows backtrace functionality +-- + +-- Function that triggers a division by zero error +CREATE FUNCTION test_backtrace_div_by_zero() +RETURNS void +LANGUAGE plpgsql +AS $$ +BEGIN + PERFORM 1/0; +END; +$$; + +-- Function that triggers a type conversion error +CREATE FUNCTION test_backtrace_type_error() +RETURNS integer +LANGUAGE plpgsql +AS $$ +BEGIN + RETURN 'not a number'::integer; +END; +$$; + +-- Nested function calls to test call stack depth +CREATE FUNCTION test_backtrace_level3() +RETURNS void +LANGUAGE plpgsql +AS $$ +BEGIN + PERFORM 1/0; +END; +$$; + +CREATE FUNCTION test_backtrace_level2() +RETURNS void +LANGUAGE plpgsql +AS $$ +BEGIN + PERFORM test_backtrace_level3(); +END; +$$; + +CREATE FUNCTION test_backtrace_level1() +RETURNS void +LANGUAGE plpgsql +AS $$ +BEGIN + PERFORM test_backtrace_level2(); +END; +$$; + +-- Test array bounds error +CREATE FUNCTION test_backtrace_array_bounds() +RETURNS integer +LANGUAGE plpgsql +AS $$ +BEGIN + RETURN ('{1,2,3}'::int[])[10]; +END; +$$; diff --git a/src/test/modules/test_backtrace/test_backtrace.control b/src/test/modules/test_backtrace/test_backtrace.control new file mode 100644 index 0000000000..ac27b28287 --- /dev/null +++ b/src/test/modules/test_backtrace/test_backtrace.control @@ -0,0 +1,5 @@ +# test_backtrace extension +comment = 'Test module for Windows backtrace functionality' +default_version = '1.0' +module_pathname = '$libdir/test_backtrace' +relocatable = true -- 2.46.0.windows.1