This document defines the testing architecture for the FreeBSD 15
pthread_workqueue effort that now carries the project name GCDX.
It is intentionally separate from the main implementation plan because testing is not a side concern here. The quality of this port depends on having a test stack that is less fragile than the traditional FreeBSD shell-based approach and honest enough to catch both semantic regressions and performance drift.
The primary testing stack should be:
- Elixir for most feature, integration, system, and comparison testing.
- Zig for low-level, C-facing, ABI-sensitive, and performance-sensitive tests.
- Shell only as a thin transport and orchestration layer when unavoidable.
This means the project should not treat atf-sh style shell tests as the
main test framework for this feature.
The test stack exists to keep the port honest while pushing it toward strong
macOS-like libdispatch semantics in a way that still feels natural on
FreeBSD.
That means:
- phase 1 should prioritize semantic correctness and real kernel workqueue behavior over exact macOS performance;
- the framework should clearly prove that the system is doing something more real than a compatibility-library story;
- macOS comparison is a reference lane, not a mandatory gate for every run;
- performance expectations should tighten gradually as the implementation matures.
Shell-based testing is fragile for this project because:
- it encourages text scraping instead of structured assertions;
- process control, retries, timing windows, and multi-step orchestration become hard to reason about;
- kernel-feature testing already has enough nondeterminism without adding more parser and quoting failure modes;
- cross-host comparison between FreeBSD and macOS becomes unpleasant when the harness is mostly shell.
Shell still has a role, but only as glue:
- launching
bhyve; - mounting or updating VM images;
- invoking
doaswhere needed; - performing one-shot setup that higher-level code can call.
Pass/fail logic should not live in shell.
Elixir is a good fit because it gives us:
- a strong test framework in
ExUnit; - good concurrency primitives for orchestration;
- practical process supervision when dealing with VMs, SSH sessions, serial capture, and external helper programs;
- a clean path to property-based and state-machine testing;
- easy structured data handling for logs, metrics, and result comparison;
- a single harness that can run on FreeBSD and on the Apple Silicon macOS reference host.
For this project, Elixir should be the default answer to:
- "How do we test this feature end to end?"
- "How do we coordinate the VM and assert on behavior?"
- "How do we compare FreeBSD against canonical macOS behavior?"
It is also the right default for the long-term shape of the project:
- most tests should live in a durable, structured, readable language;
- the harness should remain maintainable as the feature set grows.
Zig is a good fit because it gives us:
- first-class C interop through
@cImport; - direct control over low-level timing and memory behavior;
- easy production of small helper binaries;
- a practical test runner via
zig test; - a clean way to write benchmarks without committing to a large framework.
For this project, Zig should be the default answer to:
- syscall ABI probes;
- struct layout and constant validation against C headers;
- microbenchmarks for thread request, return, narrow, and stack reuse paths;
- performance-focused tests where Elixir would add too much harness overhead.
This matches the explicit requirement to use Zig for the C-facing and performance-sensitive parts of the project.
These requirements are now explicit:
- most tests should be written in Elixir;
- the project should avoid relying on FreeBSD's traditional shell-based test style for primary coverage;
- Zig should be used for the most performance-sensitive and C-facing tests;
- the test framework must be ready early enough to make performance and regression claims credible;
- the same repo will later be cloned onto the Apple M5-based macOS 26.4 host,
and that machine should act as the canonical
libdispatchbehavior reference.
Tests and helpers should emit structured data, not ad hoc human-oriented text.
Preferred formats:
- JSON lines for streaming events;
- JSON summaries for final results;
- machine-readable counters and histograms where possible.
Reason:
If a test needs grep | sed | awk to understand its result, the design is
already too fragile.
It is acceptable to use shell to boot a VM. It is not acceptable to let shell decide whether a complex workqueue test passed.
Reason:
This keeps the control path thin and the assertion logic readable.
FreeBSD-in-bhyve is the main engineering target.
Apple Silicon macOS is the canonical behavior reference.
Reason:
The FreeBSD side is where kernel iteration, crashes, and instrumentation live.
The macOS side is where canonical libdispatch behavior lives.
No claim about thread pressure, narrowing, or workqueue quality should rest only on functional tests.
Reason:
A port can be semantically correct and still regress badly in thread growth, latency, or worker reuse.
macOS should be used as the oracle for natural libdispatch behavior.
It should not be used as a demand for bit-for-bit Darwin behavior in every
corner.
Reason:
This project is for FreeBSD, not a full Darwin reimplementation.
The first iterations do not need to match macOS performance.
The framework should initially answer:
- are the semantics correct;
- is the real kernel-backed path active;
- is behavior clearly stronger than the fallback or compatibility path;
- are there obvious failures such as thread explosion, useless narrowing, or poor reuse.
Reason:
This matches the project direction: semantics first, then gradual performance improvement toward macOS where that remains natural.
When code work begins, the repo should grow toward a layout like this:
docs/
pthread-workqueue-port-plan.md
pthread-workqueue-testing-strategy.md
elixir/
mix.exs
mix.lock
config/
lib/
test/
test/support/
priv/
zig/
build.zig
build.zig.zon
src/
test/
bench/
c/
scripts/
bhyve/
image/
capture/
fixtures/
configs/
workloads/
baselines/
Role of each area:
docs/holds stable design documents;elixir/holds the main harness and most tests;zig/holds low-level helpers, ABI probes, and benchmarks;scripts/holds thin wrappers only;fixtures/holds non-generated inputs and expected baselines.
Generated logs, dumps, and heavy artifacts should remain outside the repo or in ignored paths.
The local ports tree already contains:
/usr/ports/lang/elixir/usr/ports/lang/zig
Current local ports snapshot shows:
- Elixir
1.17.3 - Zig
0.15.2
These versions should be treated as the current starting point, not as a forever pin.
The project should have three test layers, not one:
This is the default test layer.
It should cover:
- syscall semantics from the outside;
- process lifecycle behavior;
libthrbridge behavior;libdispatchbehavior;- Swift concurrency scenarios;
- FreeBSD versus macOS comparison runs;
- orchestration of guests, traces, logs, and metrics.
This is the precision layer.
It should cover:
- syscall ABI layouts;
- header and constant validation;
- C interop checks against FreeBSD headers;
- microbenchmarks for hot paths;
- minimal helper binaries for guest-side probing.
This is the utility layer.
It should cover only:
bhyvelaunch;- image preparation and kernel replacement;
- dump capture helpers;
- convenience commands for developers.
It should not contain core assertions.
The Elixir side should be a normal Mix project.
Suggested internal modules:
TwqTest.EnvTwqTest.VMTwqTest.ImageTwqTest.GuestTwqTest.SSHTwqTest.SerialTwqTest.SysctlTwqTest.TraceTwqTest.ResultsTwqTest.MacRefTwqTest.WorkloadsTwqTest.Assert
Suggested responsibilities:
Envresolves host role, paths, and configuration;VMcontrolsbhyvelifecycle;Imageupdates kernels and guest roots;Guestexecutes commands in the guest in a structured way;SSHhandles remote transport when SSH is used;Serialcaptures console and panic output;Sysctlreads kernel counters and tunables;Tracewrapstruss,ktrace,kdump, or future tracing helpers;Resultswrites machine-readable summaries;MacRefruns the same workloads on macOS and aligns result schemas;Workloadsdefines reusable dispatch and Swift scenarios;Assertcentralizes interpretation of metrics and pass/fail policy.
Use ExUnit as the main test entrypoint.
Advantages:
- standard tagging and filtering;
- clean setup and teardown hooks;
- async tests where safe;
- deterministic reporting;
- easy integration with helper processes.
Suggested tags:
:unit:guest:kernel:dispatch:swift:macos_ref:bench:slow:destructive
Elixir should also host model-based testing for the workqueue contract.
Recommended uses:
- random sequences of
INIT,REQTHREADS,THREAD_RETURN, andSHOULD_NARROWrequests against a reference model; - lifecycle checks around
exec, exit, duplicate init, and reuse behavior; - invariants such as "active count never negative", "narrow never true when the system is clearly underutilized", and "thread request totals converge back to zero".
Use StreamData or a comparable property-testing library on the Elixir side.
Reason:
This gives the project a high-level correctness model that is much easier to evolve than a pile of shell scripts.
The Elixir suite should be organized around the following categories.
Run without a VM.
Examples:
- result schema validation;
- parsing and normalization of trace output;
- state-machine model tests for request and narrow behavior;
- threshold comparison logic.
Verify the harness itself.
Examples:
- VM boots;
- serial capture works;
- guest command execution works;
- kernel replacement helper works;
- dump capture helpers work.
These are simple behavioral tests but still use structured Elixir assertions.
Examples:
- workqueue init succeeds once;
- duplicate init is rejected;
- requesting constrained threads updates stats as expected;
- overcommit requests bypass constrained limits;
- worker return parks or reassigns a thread correctly;
should_narrowanswers change with load;execand exit clean up per-process workqueue state.
Examples:
_pthread_workqueue_init()path is used;- thread requests are visible in kernel counters;
- fallback is not silently used;
- dispatch workloads show bounded growth and recovery.
Examples:
- task groups under CPU load;
- mixed blocking and compute workloads;
- detached tasks;
- fan-out and fan-in stress;
- priority-sensitive task mixes.
Examples:
- identical workload definitions run on both FreeBSD and macOS;
- resulting worker growth, narrowing, and completion timing are collected into a common schema;
- the harness flags meaningful divergence for review.
The Zig side should be a normal zig build project.
Suggested targets:
zig build test-abizig build test-syscallzig build test-c-interopzig build bench-syscallzig build bench-thread-createzig build bench-thread-returnzig build bench-narrowzig build bench-stack-reuse
Zig should @cImport the project headers and validate:
- command constants;
- struct sizes;
- struct alignment;
- field offsets where relevant;
- flag encodings.
Reason:
This is the kind of low-level validation that is awkward in Elixir and too important to leave implicit.
Small Zig programs should directly exercise:
- init;
- request threads;
- thread return;
- should narrow;
- invalid argument cases.
These programs should emit structured JSON summaries so the Elixir harness can consume them directly.
Reason:
This keeps the probe binaries small and honest while leaving orchestration and assertion in Elixir.
Zig should validate the C-facing parts that matter to this port:
- header inclusion;
- calling convention assumptions;
pthread_priority_ttranslation helpers;- syscall parameter marshalling;
- interop with test helper C code where needed.
Reason:
Zig is especially good at sitting next to C APIs without forcing a large separate framework.
Zig should host the serious microbenchmarks.
Initial benchmark candidates:
- request-thread syscall overhead;
- should-narrow query overhead;
- thread create latency;
- thread return and park latency;
- wakeup latency for reused workers;
- stack reuse benefit versus fresh stack allocation;
- constrained versus overcommit throughput under synthetic load.
Reason:
These are exactly the areas where a high-level runtime would distort the measurement too much.
Every Zig benchmark should emit:
- test name;
- kernel build identifier;
- workload parameters;
- sample count;
- mean;
- median;
- p95;
- p99 where useful;
- standard deviation or equivalent spread metric.
Prefer JSON output.
Reason:
That allows the Elixir layer to archive, compare, and gate regressions without parsing free-form text.
- orchestration;
- system setup and teardown;
- guest management;
- high-level feature assertions;
- comparison testing against macOS;
- result collation and reporting;
- property and state-machine testing.
- ABI and layout validation;
- direct syscall helper programs;
- C interop checks;
- hot-path microbenchmarks;
- low-overhead probes used by Elixir.
- bootstrapping;
- VM launch and stop wrappers;
- image and dump utilities;
- very small helpers that are not worth reimplementing elsewhere.
The FreeBSD guest should eventually contain enough tooling to run:
- guest-side Zig helper binaries;
- guest-side userland test programs;
- optional Elixir workloads if that becomes practical.
Preferred model:
- the main harness runs on the host in Elixir;
- the host drives guest programs through SSH or serial;
- guest programs emit structured results;
- the host owns pass/fail interpretation.
Reason:
This avoids overloading the guest while still keeping test logic out of shell.
Default assumption:
- Elixir runs on the host as the main controller.
Optional later expansion:
- some guest-side test runners may also use Elixir if it becomes convenient and the guest image budget allows it.
Reason:
Running Elixir on the host is enough to get the main benefits immediately. Guest-side Elixir is optional, not required for the architecture to work.
The Apple M5-based macOS 26.4 machine is the canonical behavior reference for
native libdispatch.
It should be used to answer questions like:
- how aggressively does canonical
libdispatchrequest workers here? - when does the native implementation narrow?
- how does blocked-worker expansion behave?
- how different is FreeBSD under the same workload?
The macOS clone of this repo should run:
- the Elixir harness;
- the same userland workload definitions where possible;
- comparison-only tests that make sense on a Darwin host;
- optionally Zig microbenchmarks when they are userland-only and portable.
The macOS lane should not be expected to run FreeBSD kernel probes.
When the same workload runs on FreeBSD and macOS, classify the result as:
- comparable;
- acceptable divergence;
- concerning divergence.
Use the following interpretation:
- comparable means behavior is directionally aligned and feature quality looks healthy;
- acceptable divergence means FreeBSD differs, but the difference is natural and not obviously harmful;
- concerning divergence means FreeBSD behavior suggests a missing feature, unstable policy, or poor regression.
Reason:
This keeps the project aligned with canonical behavior without turning the comparison into a rigid compatibility trap.
Operational note:
The macOS lane does not need to be fully automated from day one. When canonical comparison runs are needed, the repo can be moved or cloned onto the Apple Silicon machine intentionally.
The project should adopt a hard rule:
- shell may launch things;
- shell may collect raw files;
- shell may not be the main place where correctness is decided.
Bad pattern:
- launch workload;
greplog text;- infer pass/fail from a string.
Good pattern:
- launch workload;
- collect JSON counters, trace summaries, and benchmark results;
- let Elixir or Zig assert on the structured data.
Because silent fallback is such a large risk, the test framework must make it easy to prove that the real workqueue path is in use.
The combined Elixir and Zig stack should support:
- checking that the relevant symbols and initialization path are present;
- tracing workqueue syscalls during workload execution;
- reading kernel stats after the workload;
- proving non-zero request and narrow activity where appropriate.
This should become a reusable assertion helper, not a one-off manual step.
Functional regressions are hard failures.
Examples:
- wrong error codes;
- missing cleanup;
- fallback path being used silently;
- narrowing never triggering when it clearly should.
Performance regressions should be tracked in two ways:
- FreeBSD versus its own previous baselines;
- FreeBSD versus canonical macOS behavior for comparable workloads.
Policy:
- FreeBSD self-regressions are hard failures once the baseline is stable;
- macOS differences are review triggers, not automatic failures, unless the behavior clearly indicates a broken or missing feature.
- early in the project, "beats the compatibility story and stays stable" is a sufficient performance bar;
- later in the project, the baseline should be tightened with captured data.
Reason:
This is the right balance between honesty and practicality.
The framework should be able to capture:
- kernel identifier and
/usr/srcrevision; - ports snapshot identifiers for Elixir and Zig if relevant;
- VM image identifier;
- workload definition identifier;
- FreeBSD counters before and after the workload;
- benchmark result sets;
- macOS comparison results where available.
These should be written as structured artifacts in ignored output directories.
Build:
- Elixir
Mixproject; - Zig
build.zig; - shell wrappers for
bhyveand image updates; - result schema.
Exit criteria:
- the harness can boot a guest;
- the harness can run a guest command;
- a Zig helper binary can run and report JSON;
- ExUnit can assert on the result.
Build:
- Elixir feature tests for syscall semantics;
- Zig syscall probe binaries;
- structured stats capture.
Exit criteria:
- init, request, return, and narrow are all testable through the framework;
- fallback detection helpers exist;
- the suite runs repeatably in the guest.
Build:
- reusable workload definitions;
- dispatch and Swift scenario runners;
- comparison reporting.
Exit criteria:
- FreeBSD guest runs the workload set;
- macOS reference host runs the comparable workload set;
- differences are surfaced as structured reports.
Build:
- Zig microbench lane;
- Elixir baseline comparison logic;
- stable threshold policy.
Exit criteria:
- thread and narrow regressions are visible immediately;
- performance discussions can use captured data, not impressions.
- keep the main implementation plan and this testing plan separate;
- make Elixir the first-class harness before kernel work gets too far ahead of observability;
- create the Zig helper and benchmark layer early, not after the feature is "done";
- never let shell become the place where important workqueue behavior is asserted;
- wire the Apple Silicon macOS clone into the same workload definitions as soon as the repo remote exists.
The testing strategy should be opinionated:
- Elixir is the primary framework.
- Zig is the precision and performance layer.
- Shell is only glue.
- FreeBSD
bhyveguests are the main engineering target. - Apple Silicon macOS is the canonical behavior reference.
If we do this early, the project will have a testing stack strong enough to catch semantic breakage, prevent silent fallback, and keep performance claims honest.