10 KiB
Dever 应用测试体系技术设计
Architecture
module/ + test/
-> one SourceMap with compiler-assigned Application/Test origins
-> existing parser and Markdown extraction
-> existing declaration registration and semantic checker
-> Test layout/access rules
-> filename-matched TestCase metadata
-> test-only assert/assert_eq lowering
-> checked Program
-> one native suite containing every checked TestCase specialization
-> case-id dispatch into one isolated native process + temporary project directory per case
-> captured result + deterministic CLI summary
There is no test interpreter, dynamic test registry, source macro, runtime reflection or production test symbol. Test behavior is a thin compiler-owned entry mode over the same checked Program and native backend.
Source Ownership And Identity
SourceFile gains a compiler-assigned source origin rather than inferring test authority from a user-controlled path. Production loading remains the current SourceMap::load(module_root) behavior. A dedicated project-test loader loads module/ as application sources and an existing test/ as test sources; a missing test root is an empty set, while a non-directory or unreadable root is an explicit load error.
Test sources retain two paths:
- display path:
test/user/account/register.dever, used by diagnostics and CLI output; - logical path:
user/account/register.dever, used for package identity and Markdown package documentation.
The new SourceLayout::Test { component, domain, topic } is valid only for exactly three logical segments. It applies existing lower-snake-case and generic-bucket rules. .dever and .dever.md with the same logical stem are one duplicate test identity.
The checker cache identity includes source origin/logical path so a strict application source and a test source with equal bytes cannot share the wrong checked result.
Test Entry Contract
For test/user/account/register.dever, the logical package is user.account.register; the checker selects its only ordinary register() () function and records a TestCase containing:
- stable public identity
user/account/register; - checked private function id;
- source span and whether its reachable graph uses a Model.
Other declarations stay private to the same test file. A same-name overload, transaction/recovery entry, parameters or outputs are rejected before native compilation. Test functions are never marked public and never enter API/model snapshots.
Program exposes read-only test metadata to the CLI. Native compilation accepts an already checked test-case id through a dedicated method instead of weakening Program::entry or making test functions public.
Visibility
Test resolution extends the existing role-aware symbol owner rather than adding a parallel resolver:
- same file: private helper functions/types;
- same domain: App and Domain functions/types;
- other domains: App public surface only;
- Model values/types: readable under existing public/type rules;
- Model operations: still rejected because only the owning App role may issue CRUD;
- Adapter, Port, API and other Test owners: not callable.
All production callers reject Test targets before ordinary same-domain checks, preventing reverse dependencies. Test assertions are available only while checking a Test owner.
Assertions
assert and assert_eq are compiler-owned test intrinsics, not bundled library functions. They reuse the existing Intrinsic expression shape so dependency, effect, liveness and native traversal do not gain a second assertion IR hierarchy.
assert(value)checks oneBoolexpression.assert_eq(actual, expected)checks the left expression first, uses its exact type as the right-hand expectation, and requires the existingType::comparablecontract.- both return Unit and are legal only as ordinary zero-output actions in Test-owned functions;
- declarations named
assertorassert_eqremain ordinary outside tests and are reserved inside a test file.
Native emission evaluates each operand once and preserves source order. On failure it creates a source-located assertion error. assert_eq renders concrete typed values through the existing Render implementations; generated records/choices retain their source-shaped renderer. No generic runtime Value, serialization round-trip or reflection is introduced.
Native Execution And Reporting
The CLI checks the combined source set once, sorts TestCase metadata, then compiles one native test suite and runs each case serially. The suite unions the existing specialization graphs and dispatches one checked private entry by its stable sorted case index. It does not introduce a dynamic function registry or runtime reflection.
Every case still starts a fresh process from the same immutable suite executable. The runner stages that executable beside a new temporary project for each process, so runtime singletons, generated SQLite settings, Model initialization and data remain isolated without an in-process reset protocol. A suite that contains any database test links the SQLite-capable runtime once; only a selected database branch loads its generated setting and initializes Models. A non-database branch does neither.
Native cache identity continues to cover generated code, compiler identity, runtime bytes, toolchain, platform, environment and compiler options. Tests use a dedicated fast compilation profile because they validate behavior rather than production code generation performance; run and build retain the production optimization contract.
For each case the runner:
- creates an owned temporary project directory;
- reuses the single compiled suite and selects the checked private test entry by internal case index;
- stages the executable in that directory;
- writes a generated
config/setting.jsononly when the reachable graph uses Model operations; - runs the executable with the temporary project as its executable/config root;
- captures stdout/stderr and records pass/failure;
- drops all owned native and runtime artifacts before continuing.
Successful process output is captured and omitted from runner status. Failed output is printed in clearly labelled blocks below the case result. A source/check failure prevents all execution; a per-case compile/start/run/assert/business/fault failure is recorded and later cases continue. The final exit status is nonzero when any case failed or was not run.
The first release has no child timeout or signal manager. Explicit cancellation may terminate the command; recoverable success/failure paths own deterministic cleanup.
SQLite Isolation
The runner never loads the project's deployment database configuration for tests. If no test graph has a database effect, the suite uses the base runtime profile. A mixed suite links the SQLite-capable runtime once, but a selected non-database case writes no setting file and does not call database bootstrap or Model initialization.
If the graph has a database effect, the runner generates a temporary setting with:
database.defaultas SQLite;- one SQLite entry for each explicit Model connection name;
- paths below that case's
data/db/; - one connection and the existing bounded page defaults.
Package-root selectors retain runtime resolution semantics: an undeclared root falls back to default; explicit selectors retain separate connections. The generated settings are parsed by dever_runtime::config::Settings, validated by Program::validate_database_settings, and consumed by the normal bootstrap, schema migration, Seed, transaction and shutdown code. No test-only database handle reaches Dever source.
The emitter currently initializes the application's complete Model schema once a reachable test uses the database. This preserves foreign keys, migrations and Seed as an application-wide contract. A non-database test uses no database profile even when unrelated Models exist in the module.
CLI And Formatting
Action::Test is added to the existing explicit CLI argument parser; there is no generic subcommand framework. The command performs semantic checking, normal warnings and API baseline validation before execution.
Formatting remains syntax-only. fmt discovers both source roots, prepares every replacement before committing any, validates both snapshots again, then performs atomic per-file renames. A missing test/ contributes no files. This prevents module files from changing when a test Markdown contract or file permission fails.
Human output follows one stable shape:
running 2 tests
test news/article/publish ... ok
test user/account/register ... FAILED
test result: FAILED. 1 passed; 1 failed; 0 not run
No JSON/JUnit output or test filter is introduced.
Markdown And CMS
Test Markdown uses the existing strict document contract. Its package metadata names the logical package (for example user.account.register), and its function section documents the filename-matched entry. No test-specific Markdown headings are added.
The maintained CMS Dever and Markdown projects receive equivalent application tests. They call existing App boundaries and assert observable business values and Model counts. They do not add test-only production App functions or duplicate CRUD.
Compatibility And Trade-offs
- Production
check/api/run/buildcontinue loading onlymodule/; their checked Program and native cache inputs remain unchanged. fmtintentionally expands to the optional test root because tests are first-class Dever source.- One suite link removes per-case native compilation. Process-per-case dispatch retains runtime/database isolation, while the suite may be larger than one reachability-pruned test binary.
- Real PostgreSQL application tests are deferred. Adding them later requires an explicit safe test connection and cleanup contract; it must not alter default SQLite behavior.
- There are no fixture hooks or shared test modules. A later addition must prove that it does not introduce hidden order or shared mutable state.
Cleanup And Rollback
All test execution artifacts are owned by RAII temporary-directory and native-program guards. Cleanup errors do not replace the primary test result. No project source, deployment setting or data path is rewritten by dever test.
The change is additive. Rollback removes the Test source origin/layout, assertion lowering, CLI action and example tests; no production source migration or database migration is required.