Table of Contents

Testing and quality checks

The repository uses several test layers because a binary-format bug can affect values, byte positions, failure behavior, package compatibility, or performance. You do not need to run every expensive check after every edit. Start narrow, then widen according to the changed behavior.

Run a focused test on both frameworks

After building the test project, run the smallest relevant class or method separately for .NET 8 and .NET 10:

dotnet test tests/CStructSharpTests/CStructSharpTests.csproj -c Release -f net8.0 --no-build --filter "FullyQualifiedName~ManualLanguageFixtureTests"
dotnet test tests/CStructSharpTests/CStructSharpTests.csproj -c Release -f net10.0 --no-build --filter "FullyQualifiedName~ManualLanguageFixtureTests"

Replace the sample filter with the test that covers your change. Running both targets catches differences hidden by one runtime. A successful result reports no failed tests and exit code 0 for each command.

Then run the full managed suite:

dotnet test tests/CStructSharpTests/CStructSharpTests.csproj -c Release

This builds as needed and runs unit, integration, regression, property, stream-adapter, concurrency, limit, and compatibility tests on both frameworks.

Generator snapshots and parity

The source generator has two test projects of its own:

dotnet test tests/CStructSharp.Generators.Tests/CStructSharp.Generators.Tests.csproj -c Release
dotnet test tests/CStructSharp.Generated.Parity/CStructSharp.Generated.Parity.csproj -c Release

The first runs the generator in memory over small sources: the generated file for each fixture is compared byte for byte with the golden file in Snapshots/*.g.cs, and behavior tests compile the output and compare it with the runtime (ReaderParityTests, WriterParityTests, ConditionalParityTests, the analyzer's diagnostics). When a change to the emitter alters the generated text on purpose, rewrite the snapshots and review the diff in the commit:

UPDATE_SNAPSHOTS=1 dotnet test tests/CStructSharp.Generators.Tests/CStructSharp.Generators.Tests.csproj -c Release

A snapshot is never rewritten to make a failing test pass; the diff is the review.

The second project generates every layout fixture the runtime is tested with into one assembly. Its Layouts.g.cs and layouts.json come from the fixture sources; regenerate and check them with:

node tools/quality/generate-parity-layouts.mjs
node tools/quality/generate-parity-layouts.mjs --check

For each layout the tests compare the generated Parse with Layout.Parse member by member, round-trip the value through both writers, and cut the bytes at every length expecting the same exception type and text from both paths (the runtime is the oracle); at every cut TryParse must return false exactly when Parse throws, with the same message. The awaitable forms are compared the same way over a hidden-buffer stream (ParseAsync and WriteAsync on both sides, unwrapped by a reflection Await helper), and the fixture's bytes as one record, three times over, and with the third record cut short go through ParseMany and the generated Records - the same records member by member or the same failure text. The runtime suite pins the stream forms against their awaitable twins over three stream kinds (AsyncReadTests, AsyncWriteTests) and every manual fixture through ReadValueAsync/WriteAsync (ManualLanguageFixtureTests). A new fixture added to the runtime tests reaches the parity project through the generator tool; --check in CI fails when the generated files are stale.

Check repository reference data

Some behavior is also recorded in JSON/text files so tests, docs, and release automation agree. Run the checks related to your change:

node tools/quality/feature-operation-matrix.mjs
node tools/documentation/validate-canonical-reference.mjs
node tools/quality/compiler-fixture.mjs validate
node tools/quality/fuzz-corpus.mjs
node tools/quality/managed-api-baseline.mjs compare

These commands check, respectively, language operations, the Portable data tables, compiler observations, replayable fuzz inputs, and public API signatures. Each prints a concise pass summary or exits nonzero with the mismatched file/entry.

A language change normally updates parser tests, operation tests, manual fixtures, the feature matrix, and prose together. A public API change needs an explicit compatibility decision; do not regenerate the baseline merely to make the comparison pass.

Coverage and mutation testing

Coverage records which lines and branches the tests execute. CI requires at least 78% aggregate line coverage and 80% aggregate branch coverage. It also rejects every critical or high-risk runtime file. A critical file has less than 60% line coverage, or less than 50% branch coverage when it has at least ten branches. A high-risk file has less than 75% line coverage, or less than 65% branch coverage with at least ten branches.

Collect the same whole-library measurement used by CI after building CStructSharp.NonWeb.sln in Release:

node tools/quality/collect-library-coverage.mjs
node tools/quality/coverage-risk.mjs --coverage-path artifacts/test-results/library-coverage/coverage.cobertura.xml --collection-manifest artifacts/test-results/library-coverage/collection.json --population-policy contracts/quality/coverage-population.json --output-directory artifacts/test-results/library-risk --minimum-line-percent 78 --minimum-branch-percent 80 --maximum-high-risk-files 0 --maximum-critical-risk-files 0

The collector runs the core, compiled-parity and generator-consumer suites on .NET 10. Coverlet merges its JSON measurements sequentially before producing one Cobertura report, so distinct branch outcomes keep their identity. The measured assembly is CStructSharp, including its shared compiler sources; this is not a coverage claim for the separate generator assembly. Missing suite reports or failed tests stop collection. CI retains the intermediate reports, TRX results, collection hashes and final risk report.

Three generator attributes are configuration declarations, not runtime algorithms. Roslyn reads their arguments without executing their constructors or property accessors. contracts/quality/coverage-population.json lists these exact files, reviewed source hashes (with LF line endings), reasons and required generator tests. Changed sources or missing test evidence fail qualification. Review any new executable behavior before updating a hash; move runtime behavior into the runtime population. Do not expand this list to hide ordinary uncovered code.

Qualified declarations remain in aggregate totals and retain their actual measured hits and risk bands in the report. Only the runtime critical/high file counts exclude them. Passing generator tests is compile-time evidence, not invented runtime coverage. README coverage badges use the merged measurement; their test count remains the core suite once on .NET 10.

Mutation testing makes small changes to production code, such as reversing a condition, and checks whether tests fail. A surviving mutation can reveal an assertion gap even when line coverage is high. The permanent score floor is 75%.

coverage-risk.mjs applies the population and risk policy to the collector's merged report. mutation-report.mjs checks the permanent-scope Stryker report. The exact pinned mutation command is in the repository root MUTATION_TESTING.md.

Exact reviewed declarations with no mutation opportunities are reported as not applicable, not detected behavior. They remain in the configured scope and must have a present report with identical source and no mutants. contracts/quality/mutation-non-mutable.json pins their source hashes, Stryker version and reasons. Missing reports and compiler-rejected mutations do not qualify; executable code retains the score and survivor requirements. Individually proven equivalent survivors are recorded in contracts/quality/mutation-equivalents.json, pinned to their exact source, tool version, operator, location and replacement. They remain survivors in the raw 75% score calculation, not detected behavior. Every unexplained survivor, uncovered mutation and runtime error still fails. See MUTATION_TESTING.md for the review criteria and the distinction from ordinary missing assertions. Mutation runners use source-project context so Stryker honors the configured core test project. Reports containing tests from unintended projects fail validation. Generator/parity suites remain independently required in normal CI.

The layout parser has its own oracle: ParserDifferentialTests parses every fixture, contract, demo, and documentation layout - and thousands of deterministic mutations of them - through both LayoutParser and the frozen Pidgin reference grammar kept under tests/CStructSharpTests/Reference/, requiring identical accept/reject decisions and identical syntax trees except for explicitly tested grammar corrections (comment stars and empty alignment arguments). The Portable contract defines the intended behavior for these cases. Extend the language in LayoutParser and, for the differential test to keep its meaning, in the reference grammar too.

Do not lower thresholds, add broad exclusions, or classify a real survivor away to make a run green.

Property tests and fuzzing

A property test checks a rule across many generated values rather than one example. Round-trip properties distinguish meaningful value equality from identical bytes: padding can be normalized, pointers are not relocated, and a UnionValue explicitly retains raw storage.

The managed fuzz harness feeds bounded generated/corpus inputs to six targets. It records a stable seed and minimizes failures so they can be replayed. Add a minimized failure as a named regression; a random failure that cannot be reproduced is not enough. The generated-differential target reads every input with the [CStructLayout]-generated readers of the harness's layouts and with the runtime, and writes both values back: the two paths must fail the same way (type and message) or produce the same bytes, so the target has no documented failures - any disagreement fails the run.

The dissect corpus sweep (DissectCorpusSweepTests.Corpus_NeverRegresses) compiles every definition extracted from the dissect ecosystem and is in the OptIn test category, which tests/CStructSharpTests/default.runsettings excludes from ordinary runs. To run it, extract a corpus with node tools/quality/extract-dissect-corpus.mjs <ecosystem-dir> corpus.json, then:

CSTRUCTSHARP_DISSECT_CORPUS=corpus.json dotnet test tests/CStructSharpTests/CStructSharpTests.csproj -c Release -f net10.0 --settings tests/CStructSharpTests/opt-in.runsettings

Compiler-differential fixtures

Small Clang and GCC fixtures record how selected C11 objects were laid out under specific recorded environments. They help explain where Portable deliberately agrees or differs. They do not add a selectable compiler/ABI mode to CStructSharp.

Performance, packages, and release checks

BenchmarkDotNet scenarios compare timing and allocation for controlled before/after cases. Package checks inspect metadata, framework assets, symbols, Source Link, installed consumer behavior, dependency audit results, and raw or compressed sizes.

Performance work follows a recorded-baseline discipline. benchmarks/fixtures/ is a seeded corpus shared by the .NET, Node, and browser harnesses; CStructSharp.FixtureTool fill records the expected result of every fixture from the managed library and verify re-checks it, so a performance change that alters any parsed value fails before it is measured. contracts/performance/non-web-rc1.json is the enforced release gate (Gate job, medians and allocations with generous multipliers); non-web-rc2.json and web-benchmark-rc1.json are the wider baselines used by the soft drift report (tools/quality/compare-benchmark-baseline.mjs, benchmarks/js/bench/check.mjs, and the non-failing benchmark-drift workflow). Re-record a baseline only for an accepted change, with the --merge mode of tools/quality/record-benchmark-baseline.mjs or benchmarks/js/bench/record.mjs, and record what moved in the contract's updates note. The complete procedure (jobs, runtimes, profiling, browser harness, AOT variant) is in benchmarks/README.md and benchmarks/js/README.md in the repository.

The browser adapter's source can be compared with its recorded wire format without compiling Web/WASM. Run relevant frontend and browser checks locally when changing that application. Release automation builds the production WASM explorer and runs frontend unit tests, explorer end-to-end tests, and the extracted browser starter checks. It also runs the starter and recipe programs against the candidate NuGet package.

The npm package CI and release workflow test the installed tarball in Node.js 22.14 and 24.0 on Windows, Linux, and macOS, plus TypeScript and browser consumers. The browser checks cover Vite development and production, nested deployment paths, server rendering, and static assets. See npm package checks for the local commands.

Documentation

node tools/documentation/validate-documentation.mjs

Run this from the repository root after changing public behavior or the site. It builds only the core net10 assembly, executes documentation examples and language fixtures, generates API metadata, builds DocFX with warnings as errors, and validates Markdown, spelling, links, search, browser behavior, accessibility, and artifact size.

When a check fails, keep its first meaningful error and use Debugging contributor failures rather than rerunning the entire suite without narrowing the cause.