2026-05-16 02:38:58 +03:00
|
|
|
= Blaise Testing Strategy
|
|
|
|
|
:author: Graeme Geldenhuys
|
|
|
|
|
:revdate: 2026-05-16
|
|
|
|
|
:toc:
|
|
|
|
|
|
|
|
|
|
== Overview
|
|
|
|
|
|
|
|
|
|
The Blaise project uses three test layers, each targeting a different level of
|
|
|
|
|
the system. This document records the rationale behind each layer, which
|
|
|
|
|
framework it uses, where the tests live, and why.
|
|
|
|
|
|
|
|
|
|
== Project directory structure
|
|
|
|
|
|
|
|
|
|
The Blaise repository separates *runtime* code (always linked into every
|
|
|
|
|
binary) from *standard library* code (opt-in via `uses`):
|
|
|
|
|
|
|
|
|
|
----
|
|
|
|
|
new-pascal-compiler/
|
|
|
|
|
compiler/ — compiler source and compiler tests
|
|
|
|
|
runtime/ — always-linked runtime (system.pas, ARC, strings, platform)
|
|
|
|
|
stdlib/ — opt-in standard library (sysutils, strutils, math, etc.)
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
This separation mirrors the approach taken by Go (`runtime/` vs standard
|
|
|
|
|
library packages), Rust (`core`/`alloc` vs `std`), and Swift (`SwiftRuntime`
|
|
|
|
|
vs `SwiftCore`/`Foundation`).
|
|
|
|
|
|
|
|
|
|
== Test frameworks
|
|
|
|
|
|
|
|
|
|
The project provides two test frameworks:
|
|
|
|
|
|
|
|
|
|
blaise.testing::
|
|
|
|
|
A full-featured xUnit-style framework written in Blaise. Provides
|
|
|
|
|
`TTestCase`, `SetUp`/`TearDown`, `AssertEquals`/`AssertTrue`/`AssertNotNil`,
|
|
|
|
|
`--suite` filtering, and a text runner (`blaise.testing.runner.text`). This
|
|
|
|
|
is the default framework for all tests that can use the full language
|
|
|
|
|
(strings, classes, exceptions, ARC).
|
|
|
|
|
|
|
|
|
|
punit::
|
|
|
|
|
A minimal, dependency-free test harness using raw `GetMem`/`ZeroMem` and
|
|
|
|
|
`IntToStr`. Designed for testing code that operates *beneath* the standard
|
|
|
|
|
library — specifically the memory allocator, ARC engine, and other runtime
|
|
|
|
|
primitives that cannot depend on strings or classes without creating circular
|
|
|
|
|
dependencies.
|
|
|
|
|
|
|
|
|
|
== Test layers
|
|
|
|
|
|
|
|
|
|
[cols="1,1,1,2,2"]
|
|
|
|
|
|===
|
|
|
|
|
| Layer | Location | Framework | What it tests | Why this framework
|
|
|
|
|
|
|
|
|
|
| Compiler IR tests
|
|
|
|
|
| `compiler/src/test/pascal/cp.test.*.pas`
|
|
|
|
|
| blaise.testing
|
|
|
|
|
| Parser, semantic analysis, and codegen logic. Feed source strings through
|
|
|
|
|
the compiler pipeline and assert on generated QBE IR substrings.
|
2026-06-10 14:12:14 +03:00
|
|
|
| Tests import compiler internals (`uParser`, `uSemantic`, `blaise.codegen.qbe`) and
|
2026-05-16 02:38:58 +03:00
|
|
|
run in-process. Fast (~5 s for 1800+ tests).
|
|
|
|
|
|
|
|
|
|
| Compiler E2E tests
|
|
|
|
|
| `compiler/src/test/pascal/cp.test.e2e.*.pas`
|
|
|
|
|
| blaise.testing
|
|
|
|
|
| Full toolchain: compile -> QBE -> gcc -> run -> assert on stdout/exit code.
|
|
|
|
|
Catches QBE-validity errors, ABI mismatches, and RTL-contract bugs that the
|
|
|
|
|
IR harness cannot see.
|
|
|
|
|
| Same runner as IR tests. ~150 ms per test; kept focused on behaviour the
|
|
|
|
|
IR harness cannot catch.
|
|
|
|
|
|
|
|
|
|
| Standard library tests
|
|
|
|
|
| `stdlib/src/test/pascal/`
|
|
|
|
|
| blaise.testing
|
|
|
|
|
| Library behaviour at runtime. Tests exercise stdlib units (`strutils`,
|
|
|
|
|
`dateutils`, `generics.collections`, etc.) directly via assertions, without
|
|
|
|
|
going through the compiler pipeline.
|
|
|
|
|
| Stdlib code has full access to strings, classes, and exceptions — no reason
|
|
|
|
|
to use the constrained punit harness.
|
|
|
|
|
|
|
|
|
|
| Runtime tests
|
|
|
|
|
| `runtime/src/test/pascal/`
|
|
|
|
|
| punit
|
|
|
|
|
| Memory allocator, ARC engine, string primitives, and other low-level runtime
|
|
|
|
|
code that cannot depend on the standard library.
|
|
|
|
|
| punit has zero dependencies on stdlib or ARC, avoiding circular dependency
|
|
|
|
|
issues when testing the very subsystems that stdlib depends on.
|
|
|
|
|
|===
|
|
|
|
|
|
|
|
|
|
== Why compiler tests stay in compiler/
|
|
|
|
|
|
|
|
|
|
Tests in `compiler/src/test/pascal/` — including those that exercise stdlib
|
|
|
|
|
units like `strutils`, `dateutils`, and `generics.collections` — are
|
|
|
|
|
*compiler integration tests*, not library tests. They belong in the compiler
|
|
|
|
|
module for the following reasons:
|
|
|
|
|
|
|
|
|
|
Import dependencies::
|
|
|
|
|
IR tests import compiler internals (`uLexer`, `uParser`, `uAST`,
|
2026-06-10 14:12:14 +03:00
|
|
|
`uSymbolTable`, `uSemantic`, `blaise.codegen.qbe`, `uUnitLoader`). Moving them
|
2026-05-16 02:38:58 +03:00
|
|
|
to `stdlib/` would create a circular build dependency: stdlib tests would
|
|
|
|
|
depend on the compiler module, while the compiler module already depends on
|
|
|
|
|
stdlib for its unit search path.
|
|
|
|
|
|
|
|
|
|
What they verify::
|
|
|
|
|
These tests verify that the *compiler* correctly parses, type-checks, and
|
|
|
|
|
emits code for programs that use stdlib units. They are testing the
|
|
|
|
|
compiler's handling of stdlib constructs, not the stdlib's runtime
|
|
|
|
|
correctness. A test named `TestCodegen_ContainsStr_Found` is asserting
|
|
|
|
|
that the codegen emits the right IR for a `ContainsStr` call — it is not
|
|
|
|
|
testing whether `ContainsStr` returns the right answer at runtime.
|
|
|
|
|
|
|
|
|
|
E2E tests compile, link, and execute::
|
|
|
|
|
E2E tests shell out to the Blaise compiler binary. They are toolchain
|
|
|
|
|
smoke tests that happen to use stdlib units as representative input. Their
|
|
|
|
|
true purpose is to verify the compile -> QBE -> link -> run pipeline.
|
|
|
|
|
|
|
|
|
|
Complementary, not redundant::
|
|
|
|
|
Compiler tests and stdlib tests cover different failure modes. A compiler
|
|
|
|
|
IR test catches "the codegen emits invalid QBE for this construct." A
|
|
|
|
|
stdlib test catches "this function returns the wrong result at runtime."
|
|
|
|
|
Both are needed; neither replaces the other.
|
|
|
|
|
|
|
|
|
|
== Required tests for new features
|
|
|
|
|
|
|
|
|
|
Every new language feature requires both an IR test and an E2E test in the
|
|
|
|
|
compiler module — see the project CLAUDE.md for details.
|
|
|
|
|
|
|
|
|
|
Every new stdlib unit or function should additionally have direct runtime
|
|
|
|
|
tests in `stdlib/src/test/pascal/` using blaise.testing. These tests
|
|
|
|
|
exercise the library at full speed without the overhead of shelling out to
|
|
|
|
|
the compiler.
|
|
|
|
|
|
|
|
|
|
Runtime primitives (memory allocator, ARC, string internals) are tested via
|
|
|
|
|
punit in `runtime/src/test/pascal/`.
|