← board

Pascal corpus: fpcunit — OOP + RTTI test framework (and the harness for the rest)

Why

Two payoffs in one:

  1. It is OOP by constructionTTestCase inheritance, TTestSuite composite, ITestListener / ITestResult interfaces, exception classes, and — the interesting part — published-method enumeration via RTTI to discover Test* methods. That RTTI path is exactly the surface self-host never touches.
  2. It is the harness. Nearly every FPC library ships its tests as fpcunit suites. Land fpcunit and the marginal cost of the next library drops to "vendor + run".

Shape (verify at vendor time, do not re-derive from memory)

Plan

  1. Vendor pinned fcl-fpcunit source via tools/install_lib_candidates.sh (PROVENANCE.md with the FPC tag/commit). Keep it read-only vendor; do not fork.
  2. Compile fpcunit.pp + testregistry + consoletestrunner with $(PXX_STABLE). Expect the first wall around RTTI published-method lookup (GetMethodName / MethodAddress / TypeInfo on classes) and possibly TStringList/TFPList breadth in the RTL.
  3. make test-fpcunit: build and run the framework's OWN suite, plus a hand-written ~10-case suite of ours (asserts pass/fail/error/ignore, setup/teardown ordering, nested suites, exception expectation).
  4. Each failure → minimal repro vs FPC → fix ONE in the owning lane → bXXX regression.

Acceptance

make test-fpcunit green: framework compiles, self-discovers test methods by RTTI, runs a suite, and the console runner's summary matches FPC's for the same suite.

Gate

Frontend/IR changed → make test + self-host byte-identical → make stabilize && make pin. Cross where a backend/runtime is touched.

Log

2026-07-13 — the reflection half is DONE, and a DESIGN BOUNDARY is now visible

Everything the rung actually existed to prove now works, and testutils walks four more walls before hitting something that is not a bug:

The boundary: testutils cannot compile unmodified, and that is CORRECT

testutils.GetMethodList does not use any public API. It hand-walks FPC's internal VMT layout:

vmt := PVmt(aClass);
methodTable := pMethodNameTable(vmt^.vMethodTable);
pmr := @methodTable^.entries[0];

PVmt / vMethodTable / TMethodNameTable are FPC System internals. pxx has its own VMT and its own RTTI blob and will never match FPC's byte layout — nor should it. No amount of frontend work fixes this, and emulating FPC's VMT layout to satisfy one helper would be the tail wagging the dog.

So the plan changes: substitute testutils, do not fork fpcunit

testutils is the one unit in the chain that is platform-internals code. Provide a pxx-native testutils on the unit search path (ahead of the vendor copy) exposing the same public surface — FreeObjects, GetMethodList, TNoRefCountObject — over our own reflection. This is not forking the vendor: it is supplying the platform half, which is exactly what that unit is.

It should be cheap: a pxx TClass value IS the RTTI blob pointer (AN_CLASSREF), so GetMethodList(AClass, AList) can enumerate the published-method table straight from the blob — the same walk lib/rtl/rtti.pas already does from an instance. The overridden- method dedup in the FPC version falls out of walking own-then-parent.

fpcunit.pp itself needs no such treatment — its discovery goes through Self.MethodAddress(FName), which now works.

Remaining after that

TFPList and the rest of the FPC container surface. Still parked; nothing half-applied, every compiler change above is committed, gated and pushed.

2026-07-13 — RESOLVED. fpcunit COMPILES AND RUNS.

run:      3
failures: 1      <- the deliberate one
errors:   0

A TTestCase descendant's published Test* methods are discovered through RTTI, each is turned into a callable method pointer, invoked, and its assertion failures recorded. The whole chain works: RTTI method discovery -> method-pointer construction -> invocation -> assertion -> exception -> failure recording. fpcunit.pp itself is used UNMODIFIED (637 procs); only testutils is substituted, which is the platform-internals half and always was the plan (see the note above).

What it took, in the order the walls came

Each of these is a real Pascal-frontend feature, landed green with its own regression:

wall what landed
get_frame / get_pc_addr / get_caller_stackinfo real frame walk, one backend op (b270)
TObject.ClassName + an RTTI header for EVERY class, not just published ones (b271)
bare sibling CLASS-method call a static method needs no Self, it just had to be FOUND (b272)
AMethod; bare parenless call of a proc-var / method pointer, as a statement (b273)
ClassType / InheritsFrom + the chain E.ClassType.InheritsFrom(C) (b274)
Self in a class method the METACLASS, and the RUNTIME class (b275)
E is <class-ref VALUE> a TClass field/var, not a class name (b276)
TRunMethod(m) a method pointer built by hand from a TMethod record (b277)
typed metaclasses ANY constructor name + class methods through class of T (b278)

Plus RTL: ExceptClass, BackTraceStrFunc (+ a real default), BoolToStr, AnsiCompareStr/Text, UnicodeFormat, System.TMethod, and ExceptAddr as an explicit nil stub.

The one that mattered

Self in a class method (b275). The cheap fix -- make it the statically-known class -- compiles, runs, and silently builds a suite for the WRONG class: FPC's whole idiom is that TMyTest.Suite reaches TAssert.Suite's body with Self = TMyTest. So the class is now passed as a real hidden argument, param 0 of every class method, and it propagates through bare sibling calls. Everything else in the list is reachable from that one decision.

Known gaps, filed not hidden

Next rung

[[feature-pascal-corpus-fpjson]] (rung 2) — and fpcunit being green is what unlocks it, since every FPC library's own suite is written against fpcunit.

2026-07-13 (later) — the chain is CROSS-PORTABLE too

Both blockers named above are fixed, and fpcunit now runs on the cross targets with identical results (run: 3 / failures: 1 / errors: 0 on x86-64, i386 and aarch64 under qemu):

Both were pre-existing and both were SILENT. Neither would have been found without pushing a real library through the compiler — which is the entire argument for the corpus ladder.