feat(test): PDR integration test suite and pasbuild-integration-test plugin

Adds compiler/src/it/ (Maven-convention integration test directory) with
a Blaise-specific PDR driver. Four initial tests ported from the OPDF
integration suite: breakpoint+next, local variables, locals command,
and step-over. Test programs are adapted for Blaise (no FPC directives),
line numbers preserved to match the original commands files.

The pasbuild-integration-test plugin (phase: none) can be invoked as
'pasbuild integration-test'; it verifies pdr and the Blaise binary are
present, then delegates to compiler/src/it/run_tests.sh.

All four tests currently fail — line info in the OPDF section maps every
statement to the function-start address (per-stmt addresses require QBE
changes, tracked in future-improvements.adoc). The aspirational expected
files show exactly what each test should produce once OPDF is complete,
making failures a clear roadmap rather than noise.

Also adds PIE/ASLR support entry to future-improvements.adoc (Linux,
FreeBSD, macOS load-base strategies for the PDR debugger).
This commit is contained in:
Graeme Geldenhuys 2026-05-02 00:35:00 +01:00
parent 0e08dc3a53
commit 95f71e1acc
15 changed files with 376 additions and 0 deletions

138
compiler/src/it/run_tests.sh Executable file
View file

@ -0,0 +1,138 @@
#!/bin/bash
#
# Integration Test Runner for PDR Debugger — Blaise Compiler
#
# Compiles test programs using the Blaise compiler with --debug-opdf,
# then drives PDR interactively via .commands files and compares
# output against .expected files.
#
# Usage:
# ./run_tests.sh — run all test_*.pas files
# ./run_tests.sh test_02.pas — run a single test
# BLAISE=/path/to/blaise ./run_tests.sh
#
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Counters
PASSED=0
FAILED=0
SKIPPED=0
# PDR binary
PDR_BIN="pdr"
# Blaise compiler binary discovery:
# 1. BLAISE environment variable
# 2. compiler/target/blaise in the project tree
if [ -n "$BLAISE" ]; then
BLAISE_BIN="$BLAISE"
else
BLAISE_BIN="$PROJECT_ROOT/compiler/target/blaise"
fi
echo "=== PDR Integration Test Runner (Blaise) ==="
echo
if ! command -v "$PDR_BIN" > /dev/null 2>&1; then
echo -e "${RED}ERROR: pdr not found in PATH${NC}"
exit 1
fi
if [ ! -x "$BLAISE_BIN" ]; then
echo -e "${RED}ERROR: Blaise compiler not found: $BLAISE_BIN${NC}"
echo " Set BLAISE=/path/to/blaise or run 'pasbuild compile -m blaise-compiler' first"
exit 1
fi
echo "Blaise : $BLAISE_BIN"
echo "PDR : $(command -v $PDR_BIN)"
echo
# Filter non-deterministic output — identical to the OPDF integration runner.
# Keeps: variable assignments (2+ char names), step messages, callstack frames.
# Drops: single-char names (A, B), program WriteLn output, [INFO]/[DEBUG] lines.
filter_output() {
sed 's/^(pdr) //' | \
sed -E 's/ \(0x[0-9A-Fa-f ]+\)//' | \
sed -E 's/\(\$[0-9A-Fa-f]+\)/(<ptr>)/' | \
grep -E "^(([A-Za-z][A-Za-z0-9_.]+(\[[0-9]+\])? = )|(\[INFO\] )?[Ss]tepped to line:|\[CALLSTACK\]|#[0-9]+ |Exception: [A-Za-z]+ —)" | \
sed 's/^\[INFO\] //' || true
}
run_test() {
local test_name=$1
local test_base="${test_name%.pas}"
echo -e "${YELLOW}Running: $test_base${NC}"
# Compile with Blaise OPDF support
echo " [1/3] Compiling..."
if ! "$BLAISE_BIN" --source "$test_name" --output "$test_base" --debug-opdf \
> "$test_base.compile.log" 2>&1; then
echo -e "${RED} FAILED: Compilation${NC}"
cat "$test_base.compile.log"
((FAILED++))
return 1
fi
# Run PDR with command script
echo " [2/3] Running PDR..."
if [ ! -f "$test_base.commands" ]; then
echo -e "${YELLOW} SKIPPED: No commands file${NC}"
((SKIPPED++))
return 0
fi
cat "$test_base.commands" | "$PDR_BIN" --verbose "./$test_base" 2>&1 \
| filter_output > "$test_base.actual"
# Compare against expected output (case-insensitive, address-normalised)
echo " [3/3] Comparing output..."
if [ ! -f "$test_base.expected" ]; then
echo -e "${YELLOW} SKIPPED: No expected file${NC}"
((SKIPPED++))
return 0
fi
if diff -u \
<(sed -E 's/@\$[0-9A-Fa-f]+/@$<addr>/g' "$test_base.expected" | tr '[:upper:]' '[:lower:]') \
<(sed -E 's/@\$[0-9A-Fa-f]+/@$<addr>/g' "$test_base.actual" | tr '[:upper:]' '[:lower:]') \
> "$test_base.diff"; then
echo -e "${GREEN} PASSED${NC}"
((PASSED++))
return 0
else
echo -e "${RED} FAILED${NC}"
cat "$test_base.diff"
((FAILED++))
return 1
fi
}
cd "$SCRIPT_DIR"
if [ $# -eq 1 ]; then
TEST_NAME=$1
[[ ! "$TEST_NAME" =~ \.pas$ ]] && TEST_NAME="${TEST_NAME}.pas"
run_test "$TEST_NAME"
else
for test_file in test_*_*.pas; do
[ -f "$test_file" ] && run_test "$test_file" && echo
done
fi
echo "==================================="
echo -e "Passed : ${GREEN}$PASSED${NC}"
echo -e "Failed : ${RED}$FAILED${NC}"
echo -e "Skipped: ${YELLOW}$SKIPPED${NC}"
echo "==================================="
[ $FAILED -eq 0 ] && exit 0 || exit 1

View file

@ -0,0 +1,11 @@
run
break test_02_breakpoint_next.pas:16
continue
print MyInt
next
print MyInt
next
print MyInt
break test_02_breakpoint_next.pas:22
continue
quit

View file

@ -0,0 +1,5 @@
MyInt = 20
Stepped to line: test_02_breakpoint_next.pas:18
MyInt = 30
Stepped to line: test_02_breakpoint_next.pas:19
MyInt = 30

View file

@ -0,0 +1,23 @@
program test_02_breakpoint_next;
var
MyInt: Integer;
Sentinel: Integer;
begin
WriteLn('Test: Breakpoint and Next');
MyInt := 10;
WriteLn('Set MyInt to 10');
MyInt := 20;
WriteLn('Set MyInt to 20');
MyInt := 30;
WriteLn('Set MyInt to 30');
WriteLn('Done');
Sentinel := 1;
end.

View file

@ -0,0 +1,10 @@
run
break test_06_local_variables.pas:13
continue
print A
print B
print Sum
print Product
break test_06_local_variables.pas:23
continue
quit

View file

@ -0,0 +1,2 @@
Sum = 15
Product = 50

View file

@ -0,0 +1,24 @@
program test_06_local_variables;
{ Test program for debugging local variables and function parameters }
function Calculate(A, B: Integer): Integer;
var
Sum: Integer;
Product: Integer;
begin
Sum := A + B;
Product := A * B;
Result := Sum + Product; { Breakpoint line for testing locals }
end;
var
X, Y, ResultValue: Integer;
Sentinel: Integer;
begin
X := 5;
Y := 10;
ResultValue := Calculate(X, Y);
Sentinel := 1;
end.

View file

@ -0,0 +1,7 @@
run
break test_14_locals.pas:17
continue
locals
break test_14_locals.pas:23
continue
quit

View file

@ -0,0 +1,2 @@
Sum = 10
Product = 21

View file

@ -0,0 +1,24 @@
program test_14_locals;
{ Test program for 'locals' command — lists all in-scope variables }
var
GlobalCount: Integer;
Sentinel: Integer;
function Compute(A, B: Integer): Integer;
var
Sum: Integer;
Product: Integer;
begin
Sum := A + B;
Product := A * B;
Result := Sum + Product; { breakpoint here — line 17 }
end;
begin
WriteLn(Compute(3, 7));
WriteLn(GlobalCount);
Sentinel := 1;
end.

View file

@ -0,0 +1,6 @@
run
break test_26_step_over.pas:19
continue
next
print Counter
quit

View file

@ -0,0 +1,2 @@
Stepped to line: test_26_step_over.pas:20
Counter = 99

View file

@ -0,0 +1,21 @@
program test_26_step_over;
{ Step-over (next) must NOT enter SetResult; must land on the WriteLn line.
SetResult body is at higher line numbers than the call site, which tests
that step-over uses function scope (address range) rather than line numbers. }
procedure SetResult(var N: Integer; V: Integer);
begin
N := V;
end;
var
Counter: Integer;
begin
Counter := 0;
SetResult(Counter, 99); { line 19 - break here, then next }
WriteLn(Counter); { line 20 - should land here }
end.

View file

@ -420,6 +420,44 @@ stream chains.
*Effort.* Moderate. Blocked on abstract method support in the class system.
== Debugger
=== PIE (ASLR) support in PDR
*What.* When a Blaise binary is compiled with `--debug-opdf`, the compiler
currently passes `-no-pie` to gcc to ensure OPDF `.quad symbol` entries
contain absolute link-time addresses. The PDR debugger should instead read
the binary's actual load base at runtime and adjust all OPDF addresses by the
ASLR slide, allowing debug-opdf builds to remain PIE.
*Why it matters.* PIE (Position Independent Executable) is the default on all
modern Linux distributions and is required for ASLR protection. Disabling it
in debug builds is a pragmatic workaround that is acceptable for local
development but is not appropriate for any scenario where the binary is
distributed or run in a production-adjacent environment. Lifting the
restriction makes `--debug-opdf` safe to enable in CI and staging environments.
*Implementation notes.*
* On Linux, `/proc/<pid>/maps` lists each memory mapping with its base
address and the backing file. At debugger attach time, read this file,
find the entry whose backing path matches the target binary, and record
its base address as `LoadBase`.
* On FreeBSD, PIE and ASLR are supported by default from FreeBSD 12 onwards.
FreeBSD's procfs is not mounted by default, so `/proc/<pid>/map` is not
reliable. The correct approach is `sysctl(KERN_PROC_VMMAP, pid)`, which
returns the full VM map for a process; find the entry matching the binary
path and extract its start address as `LoadBase`.
* On macOS, the equivalent is `task_info(TASK_DYLD_INFO)` or parsing the
Mach-O load commands; `DYLD_IMAGE_BASE` is available via the dyld shared
cache APIs.
* At symbol-resolution time on all platforms: `RuntimeAddr = OPDFAddr + LoadBase`.
* Once PIE-aware address resolution is in place, remove the `-no-pie` guard
in `Blaise.pas` (`CompileToNative`).
*Effort.* Small on Linux and FreeBSD (one sysctl/procfs read at attach time);
moderate on macOS (dyld integration).
== macOS — Debugging and Code-Signing
=== Debugger entitlement requirements

View file

@ -0,0 +1,63 @@
#!/bin/sh
# pasbuild-integration-test — Run PDR integration tests for the Blaise compiler.
#
# Phase: none (standalone — invoke as: pasbuild integration-test)
#
# Preconditions:
# - pdr must be in PATH
# - compiler/target/blaise must exist (run 'pasbuild compile' first)
# - compiler/src/it/ must exist and contain test_*.pas files
#
# Usage:
# pasbuild integration-test
# BLAISE=/path/to/blaise pasbuild integration-test
set -e
case "$1" in
--pasbuild-phase)
echo "none"
exit 0
;;
esac
# ---- Locate project directory ------------------------------------------------
PROJ="${PASBUILD_PROJECT_DIR:-$1}"
if [ -z "$PROJ" ]; then
echo "[integration-test] ERROR: project directory not set" >&2
exit 1
fi
# ---- Locate integration test directory ---------------------------------------
IT_DIR="$PROJ/compiler/src/it"
if [ ! -d "$IT_DIR" ]; then
echo "[integration-test] ERROR: no integration tests found at $IT_DIR" >&2
exit 1
fi
if [ -z "$(find "$IT_DIR" -maxdepth 1 -name 'test_*.pas' 2>/dev/null)" ]; then
echo "[integration-test] ERROR: no test_*.pas files in $IT_DIR" >&2
exit 1
fi
# ---- Check prerequisites -----------------------------------------------------
if ! command -v pdr > /dev/null 2>&1; then
echo "[integration-test] ERROR: pdr not found in PATH" >&2
exit 1
fi
BLAISE_BIN="${BLAISE:-$PROJ/compiler/target/blaise}"
if [ ! -x "$BLAISE_BIN" ]; then
echo "[integration-test] ERROR: Blaise compiler not found: $BLAISE_BIN" >&2
echo " Run 'pasbuild compile -m blaise-compiler' first" >&2
exit 1
fi
echo "[integration-test] Project : $PROJ"
echo "[integration-test] Tests : $IT_DIR"
echo "[integration-test] Blaise : $BLAISE_BIN"
echo "[integration-test] PDR : $(command -v pdr)"
echo
# ---- Run tests ---------------------------------------------------------------
BLAISE="$BLAISE_BIN" "$IT_DIR/run_tests.sh"