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:
parent
0e08dc3a53
commit
95f71e1acc
138
compiler/src/it/run_tests.sh
Executable file
138
compiler/src/it/run_tests.sh
Executable 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
|
||||
11
compiler/src/it/test_02_breakpoint_next.commands
Normal file
11
compiler/src/it/test_02_breakpoint_next.commands
Normal 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
|
||||
5
compiler/src/it/test_02_breakpoint_next.expected
Normal file
5
compiler/src/it/test_02_breakpoint_next.expected
Normal 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
|
||||
23
compiler/src/it/test_02_breakpoint_next.pas
Normal file
23
compiler/src/it/test_02_breakpoint_next.pas
Normal 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.
|
||||
10
compiler/src/it/test_06_local_variables.commands
Normal file
10
compiler/src/it/test_06_local_variables.commands
Normal 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
|
||||
2
compiler/src/it/test_06_local_variables.expected
Normal file
2
compiler/src/it/test_06_local_variables.expected
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Sum = 15
|
||||
Product = 50
|
||||
24
compiler/src/it/test_06_local_variables.pas
Normal file
24
compiler/src/it/test_06_local_variables.pas
Normal 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.
|
||||
7
compiler/src/it/test_14_locals.commands
Normal file
7
compiler/src/it/test_14_locals.commands
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
run
|
||||
break test_14_locals.pas:17
|
||||
continue
|
||||
locals
|
||||
break test_14_locals.pas:23
|
||||
continue
|
||||
quit
|
||||
2
compiler/src/it/test_14_locals.expected
Normal file
2
compiler/src/it/test_14_locals.expected
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Sum = 10
|
||||
Product = 21
|
||||
24
compiler/src/it/test_14_locals.pas
Normal file
24
compiler/src/it/test_14_locals.pas
Normal 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.
|
||||
6
compiler/src/it/test_26_step_over.commands
Normal file
6
compiler/src/it/test_26_step_over.commands
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
run
|
||||
break test_26_step_over.pas:19
|
||||
continue
|
||||
next
|
||||
print Counter
|
||||
quit
|
||||
2
compiler/src/it/test_26_step_over.expected
Normal file
2
compiler/src/it/test_26_step_over.expected
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Stepped to line: test_26_step_over.pas:20
|
||||
Counter = 99
|
||||
21
compiler/src/it/test_26_step_over.pas
Normal file
21
compiler/src/it/test_26_step_over.pas
Normal 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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
63
plugins/pasbuild-integration-test
Executable file
63
plugins/pasbuild-integration-test
Executable 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"
|
||||
Loading…
Reference in a new issue