sbol-rs is correctness-critical: every claim about SBOL conformance,
for both SBOL 2 and SBOL 3, has to survive cargo test. This document
maps the test surface so contributors know what each layer gates, where
to add a new case, and how the fixture corpora are produced.
| Layer | Where | What it gates |
|---|---|---|
| Unit tests | Inline #[cfg(test)] mods (minimal; most logic moves to integration tests) |
Type-local invariants too small to integrate. |
| SBOL 3 integration tests | crates/sbol3/tests/*.rs |
Public-API behavior: builders, traversal, parsing, validation entry points. |
| SBOL 2 integration tests | crates/sbol2/tests/*.rs |
The SBOL 2 peer: builders, client API, RDF round-trip, validation. |
| Validation rule regressions | crates/sbol3/tests/rule_cases/{spec-area}.rs, crates/sbol2/tests/rule_cases.rs |
One negative case per non-deferred rule ID; positive cases per spec cluster. |
| Schema consistency | crates/sbol3/tests/schema_consistency.rs |
to_rdf ↔ from_rdf ↔ FieldDescriptor predicate-set parity across every class. |
| Property tests | crates/sbol3/tests/properties.rs |
Spec-derived invariants under proptest-generated input. |
| SBOL 3 fixtures | crates/sbol3/tests/sbol3_fixtures.rs |
All 33 valid SBOL 3 fixtures parse, validate, and round-trip in every RDF format. |
| SBOL 2 corpora | crates/sbol2/tests/conformance.rs, crates/sbol2/tests/invalid_files.rs |
The four SBOLTestSuite SBOL 2 corpora and the per-rule negative corpus. |
| Migration round-trip | crates/sbol-convert/tests/*.rs |
SBOL 2 → 3 upgrade, SBOL 3 → 2 downgrade, 2 → 3 → 2 fixed-point over the full corpus, and a differential gate against the SynBioDex/SBOL-Converter reference. |
| Importer oracles | crates/sbol-fasta/tests/*.rs, crates/sbol-genbank/tests/*.rs |
FASTA / GenBank import against SO-mapping and BioPython reference oracles. |
| Cross-implementation conformance | crates/sbol3/tests/cross_impl.rs, crates/sbol3/tests/cross_impl_pysbol3.rs, crates/sbol2/tests/cross_impl.rs |
Triple-set equivalence against libSBOLj3 1.0.5.2 and pySBOL3 1.2 (132 combos each), plus SBOL 2 RDF/XML equivalence against libSBOLj 2.4.0. |
| Catalog freshness gates | crates/sbol3/tests/validation_rules.rs, crates/sbol3/tests/conformance_report.rs, crates/sbol2/tests/conformance_report.rs |
rules.toml invariants and docs/conformance.md / docs/sbol2-conformance.md freshness. |
| RDF backend | crates/sbol-rdf/tests/basic.rs |
Per-format round-trip, cross-format equivalence, extension parsing. |
| Fuzz tests | fuzz/fuzz_targets/*.rs |
No panics on arbitrary bytes in any of the four RDF formats. |
| Doctests | /// examples across the crates |
README / prelude / Document::read* examples compile and run. |
Run everything: cargo test --workspace. No Docker or JDK is needed; the
cross-implementation references are committed.
Each version carries its own validation catalog with the same shape;
testing it has structure beyond "a #[test] per rule."
crates/sbol3/rules.toml (149 rules) and crates/sbol2/rules.toml (268
rules) are the single sources of truth for the SBOL 3.1.0 and SBOL 2.3.0
validation rules. Each entry carries:
id: the canonical SBOL rule ID (e.g.sbol3-10101,sbol2-10101).status: one ofError,Warning,Configurable,MachineUncheckable(spec ▲), orUnimplemented.normative_severity: RFC 2119 force (MUST/SHOULD/MAY).spec_section: section reference into the version's spec.blocker(Configurable / MachineUncheckable / Unimplemented):Ontology/Resolver/Policy/External/StrictDatatype. Names the configuration axis or what a fuller check would need.gate: the validation family (Always,Compliant,Complete, orBestPractice) selecting whichValidationConfigflag runs the rule.validator_function/note: the emitting function (where applicable) and a short description of what is enforced.
Each crate's build.rs parses its TOML at compile time via
sbol-rulegen and emits the
validation_rule_statuses() slice into OUT_DIR. Adding a rule means
editing the TOML, not Rust. Both catalogs classify every machine-checkable
rule as implemented: 109 of 109 for SBOL 3, 239 of 239 for SBOL 2.
crates/sbol3/tests/validation_rules.rs locks the SBOL 3 catalog against
silent drift. Its meta-tests include
implemented_validation_rules_have_regression_cases (every implemented
rule has a negative case under tests/rule_cases/),
validation_rule_regression_cases_report_expected_rule_ids (each case
fires its claimed rule ID), positive_rule_cases_do_not_report_their_rule,
every_rule_appears_in_exactly_one_coverage_bucket,
every_appendix_b_triangle_rule_is_marked_machine_uncheckable, and
every_policy_blocked_rule_has_an_adr_file. Each spec area
(component.rs, feature.rs, …) holds a &[RuleCase] array declaring a
rule ID, a Turtle document that must fire it, and the expected Severity.
The SBOL 2 peer is crates/sbol2/tests/rule_cases.rs: hermetic in-memory
documents that isolate a single rule with a positive fixture (must not
report it) and a negative fixture (must). To add coverage, edit the
version's rules.toml, add the case, and run
cargo test -p sbol3 --test validation_rules (or
cargo test -p sbol2 --test rule_cases).
The per-rule status grids are generated from the catalogs and gated in CI:
cargo run -p sbol3 --bin generate-conformance-report # docs/conformance.md
cargo run -p sbol2 --bin generate-sbol2-conformance-report # docs/sbol2-conformance.md
cargo run -p sbol3 --bin generate-sbol-owl3-conformance-report # docs/sbol-owl3-conformance.mdcrates/sbol3/tests/conformance_report.rs and
crates/sbol2/tests/conformance_report.rs fail if the committed file
drifts from the rendered output.
crates/sbol2/tests/conformance.rs validates the four SBOLTestSuite SBOL 2
corpora, each isolating one axis of the shared ValidationConfig: the
SBOL2 corpus (179 files) passes under both the default and all_on; the
SBOL2_ic corpus (71) fails only under complete; SBOL2_nc (30) fails
only under compliant; SBOL2_bp (11) is flagged only under
best_practice, at warning severity. Every file parses: 0 parse
failures across all four corpora.
crates/sbol2/tests/invalid_files.rs runs the per-rule negative corpus:
each InvalidFiles/sbol-NNNNN.xml violates rule sbol2-NNNNN and must be
rejected under all_on. The current split is 161 strict (the exact rule
fires), 17 loose (rejected via a related error), 3 parse-rejected, and 50
deferred (resolver / cross-document, spec-▲, or SHOULD-level cases tracked
explicitly in invalid_files_deferred.in). The full grid is rendered into
sbol2-conformance.md.
crates/sbol-convert/tests/ gates the conversion pipeline both ways:
upgrade_conformance.rs/upgrade_fixtures.rs: SBOL 2 → SBOL 3 against self-snapshots (seesbol2-upgrade-conformance.md).downgrade_round_trip.rs/downgrade_dual_role.rs/downgrade_semantic.rs: SBOL 3 → SBOL 2, including the Component → ComponentDefinition / ModuleDefinition classification (seesbol3-downgrade-conformance.md).corpus_round_trip.rs: the SBOL 2 → 3 → 2 fixed-point cycle over the full SBOLTestSuite SBOL 2 corpora, asserting triple-for-triple equality: 290 fixtures round-trip clean, 0 unexpected drift, 0 parse failures, 0 upgrade-unsupported, with a single documented-lossy fixture (a non-compliant-identitySBOL2_nccase) allowlisted.cross_impl_reference.rs: the differential gate against the reference Java converter (SynBioDex/SBOL-Converter) — SBOL 2 → SBOL 3 via the reference vs sbol-rs and SBOL 3 → SBOL 2 vice versa, plus cross-tool round-trips, compared on logical equivalence against committed goldens (no JDK in CI). Seesbol-converter-differential.md.
The informational per-fixture report over the committed real-world
fixtures is sbol3-round-trip-report.md,
regenerated with cargo run -p sbol-convert --bin generate-round-trip-report.
The FASTA and GenBank importers are validated against external references rather than only self-snapshots:
- SO-mapping parity: GenBank feature-key → Sequence Ontology role
mappings are diffed against the sbol-utilities
gb2soreference table (crates/sbol-genbank/tests/); reconciling them exposed and fixed 9 real mapping bugs. - BioPython feature-parity oracle:
biopython_oracle.rsin bothsbol-fastaandsbol-genbankchecks that parsed records match a BioPython reference decomposition. - Multi-span locations:
crates/sbol-genbank/tests/multispan.rshand-verifiesjoin/order/complementlocation fixtures. - Malformed input:
crates/sbol-genbank/tests/malformed.rsand the FASTA edge tests confirm graceful rejection of broken input.
See fasta-import-conformance.md and
genbank-import-conformance.md.
This section covers RDF serialization equivalence for the sbol3 / sbol2
crates. The sbol-convert converter has its own differential against the
SynBioDex/SBOL-Converter reference — see
sbol-converter-differential.md.
tests/fixtures/cross-impl/ holds libSBOLj3 1.0.5.2 reference outputs for
every SBOLTestSuite SBOL 3 fixture, one file per format:
<stem>.libSBOLj3.expected.ttl
<stem>.libSBOLj3.expected.rdf
<stem>.libSBOLj3.expected.jsonld
<stem>.libSBOLj3.expected.nt
cargo test -p sbol3 --test cross_impl parses each reference in its own
format and asserts the normalized triple set matches what sbol-rs produces
by serializing the same source fixture in that format. CI runs only
cargo test; no Docker, no JDK.
A known-compliant divergence is allowlisted by adding the reference
filename to tests/fixtures/cross-impl/allowlist.txt with a written
rationale. The allowlist is currently empty.
# build the pinned Docker image (one-time per version bump)
docker build -t libsbolj3-pinned tests/fixtures/cross-impl/
# regenerate all four format references for every fixture
cargo run -p sbol3 --bin regenerate-cross-impl-expectationsThe regenerate binary iterates RdfFormat::ALL per fixture and is
fail-loud about Docker availability and image existence. Commit the
regenerated *.expected.{ttl,rdf,jsonld,nt} files and bump the
LIBSBOLJ3_VERSION arg in tests/fixtures/cross-impl/Dockerfile in the
same commit.
The Java wrapper at tests/fixtures/cross-impl/RoundTrip.java takes the
output format as a CLI arg (turtle/rdfxml/jsonld/ntriples). If you
add a new format to RdfFormat, extend the parseFormat switch there and
the libsbolj3_format_name mapping in the regenerate binary.
tests/fixtures/cross-impl-pysbol3/ mirrors the libSBOLj3 layout for
pySBOL3 references. File suffix is .pySBOL3.expected.* and the test file
is crates/sbol3/tests/cross_impl_pysbol3.rs. The committed references
cover the same 33 fixtures in all four formats (132 combinations),
generated from pySBOL3 1.2, and cross_impl_pysbol3 diffs sbol-rs's
serialization against them exactly as the libSBOLj3 harness does. Refresh
the references on a contributor machine:
docker build -t pysbol3-pinned tests/fixtures/cross-impl-pysbol3/
cargo run -p sbol3 --bin regenerate-cross-impl-pysbol3-expectationsCommit the regenerated *.pySBOL3.expected.* files alongside any
allowlist entries needed for known-compliant divergences and bump the
PYSBOL3_VERSION arg in the Dockerfile in the same commit.
crates/sbol2/tests/cross_impl.rs gates sbol-rs's SBOL 2 serialization
against libSBOLj 2.4.0, the SBOL 2 Java implementation. SBOL 2 is exchanged
as RDF/XML, so the harness compares RDF/XML output only over 10 committed
fixtures; the reference files live in tests/fixtures/cross-impl-sbol2/
with suffix .libSBOLj.expected.rdf. Building references on a contributor
machine:
docker build -t libsbolj-pinned benches/cross-impl/libsbolj/
cargo run -p sbol2 --bin regenerate-cross-impl-sbol2-expectationsThe benches/cross-impl/libsbolj/RoundTrip.java wrapper reads a fixture
and writes RDF/XML to stdout; the regenerate binary captures that as the
reference. Bump the LIBSBOLJ_VERSION arg in the Dockerfile when a new
release ships, then regenerate and commit.
See rdf-io.md for the user-facing I/O subsystem reference.
Separate from the correctness harness, crates/sbol-bench times four
phases (parse, serialize, convert, and validate) for both
SBOL versions sbol-rs implements. The matrix has an SBOL 2 side and
an SBOL 3 side. On the SBOL 3 side it compares sbol-rs against
pySBOL3, libSBOLj3, and sboljs; on the SBOL 2 side it compares sbol-rs
against libSBOLj (the SBOL 2 Java implementation). Every foreign tool
is pinned in its own Docker image (see benches/cross-impl/), and
sbol-rs runs in its own image too so every row pays the same Linux-VM
overhead.
The serialize phase covers both same-format round trips
(turtle -> turtle) and cross-format conversions (turtle -> rdfxml),
so conversion cost is measured directly. The validate phase runs for
the implementations that ship a validator on the relevant version:
sbol-rs on both versions, pySBOL3 and libSBOLj3 on SBOL 3. SBOL 2 is
exchanged as RDF/XML; sbol-rs additionally reads and writes Turtle,
JSON-LD, and N-Triples for SBOL 2, so those formats are benchmarked
for sbol-rs. This is not a CI gate (there is no perf-regression check),
but it gives contributors a reproducible way to compare
implementations head-to-head on the same fixtures. Run
cargo run --release -p sbol-bench; see
benches/cross-impl/README.md for
the version-by-version format-support matrix, knobs, and limitations.
Spec-derived invariants run under proptest in CI, including:
| Property | Asserts |
|---|---|
displayId lexical agreement |
DisplayId::new accepts iff the input matches sbol3-10201's regex. |
| Compliant URL round trip | SbolIdentity produces identities that parse back to their components. |
Range bounds |
Range::new(start, end) rejects iff start > end. |
| Derivation cycle detection | Validator catches prov:wasDerivedFrom cycles regardless of insertion order. |
| Component builder required fields | ComponentBuilder::build returns MissingRequired iff types is empty. |
| Catalog coverage partitioning | Every rule falls into exactly one coverage bucket; overrides compose. |
PROPTEST_CASES=10000 cargo test -p sbol3 --test propertiesA failing seed is persisted to
crates/sbol3/tests/properties.proptest-regressions (gitignored). To share
a reproducer, copy the failing seed into a dedicated #[test] and commit
it.
When the rule under test has infinite valid inputs and the cost of
generating them programmatically is lower than enumerating witness cases by
hand. Single-witness facts go in tests/rule_cases/ instead.
fuzz/ is a separate cargo package (not a workspace member) that wires up
cargo-fuzz against the parser and validator across all four RDF formats.
The first byte of the fuzz input selects the format; the remainder is fed
to the parser as UTF-8 text.
Prerequisites:
- Nightly Rust toolchain (
rustup install nightly) cargo install cargo-fuzz
Run a target:
cd fuzz
cargo +nightly fuzz run read_turtleBound the run to a fixed wall clock (CI smoke uses 60s):
cargo +nightly fuzz run read_turtle -- -max_total_time=60Available targets:
| Target | What it stresses |
|---|---|
read_turtle |
Parses arbitrary byte inputs in a fuzz-selected format. Asserts no panic from the parser or backend. |
round_trip |
Parses, serializes back in the same format, and reparses. Asserts the second parse does not panic. |
validate |
Parses then validates. Asserts the validator never panics on arbitrary input. |
round_trip_validate |
Combines the round-trip and validate targets. |
CI smoke-runs the parser targets for 60s each; the validator targets are local-only until corpus and budget are tuned.
Corpus and crash artifacts land in fuzz/corpus/ and fuzz/artifacts/
respectively (both gitignored). To minimize a found crash:
cargo +nightly fuzz tmin read_turtle fuzz/artifacts/read_turtle/<crash-file>SBOL 3 source fixtures (Turtle) are cached under tests/fixtures/sbol3
(gitignored). The first cargo test invocation fetches and populates the
cache from the pinned SynBioDex/SBOLTestSuite archive at commit
0044284331b2f915a6e4b9d50e1cbf3ea2f62dcd. The checked-in manifest is
tests/sbol3_fixtures_manifest.tsv.
The valid-input set is 31 upstream fixtures plus 2 local supplements
(experimental_data.ttl, variable_feature.ttl) that exercise features
the upstream corpus doesn't cover, 33 in total.
The SBOLTestSuite SBOL 2 corpora (SBOL2, SBOL2_bp, SBOL2_ic,
SBOL2_nc, InvalidFiles) bootstrap on demand into
tests/fixtures/sbol2/SBOLTestSuite (gitignored), reusing the same pinned
archive the SBOL 3 tests download. The hand-written and real-world SBOL 2
fixtures used by the conversion round-trip are vendored under
tests/fixtures/sbol2/real/.
Committed under tests/fixtures/cross-impl/ (libSBOLj3, ~132 files),
tests/fixtures/cross-impl-pysbol3/ (pySBOL3, ~132 files), and
tests/fixtures/cross-impl-sbol2/ (libSBOLj, 10 files). See the
cross-implementation conformance section above.
| You want to test… | File / pattern |
|---|---|
A new SBOL 3 builder, accessor, or Document method |
Existing or new file under crates/sbol3/tests/*.rs. |
A new SBOL 2 builder, accessor, or Document method |
Existing or new file under crates/sbol2/tests/*.rs. |
| A new validation rule's failure case | Add a RuleCase to the matching tests/rule_cases/*.rs (or crates/sbol2/tests/rule_cases.rs). |
| A new validation rule's positive case | Add a PositiveCase to the matching rule-case file. |
| An invariant over generated input | Add to crates/sbol3/tests/properties.rs. |
| A new SBOL feature against the SBOLTestSuite corpus | Extend crates/sbol3/tests/sbol3_fixtures.rs or crates/sbol2/tests/conformance.rs. |
| A conversion behavior | Add to the matching crates/sbol-convert/tests/*.rs. |
| A divergence from a reference implementation | Either fix sbol-rs, or allowlist with rationale in the relevant allowlist.txt. |
| Backend / format behavior on synthetic triples | crates/sbol-rdf/tests/basic.rs. |
| Parser robustness on arbitrary bytes | Already covered by fuzz; no per-input test needed. |
If cargo test --workspace is green and the relevant drift gates fire on
the corresponding rule-case file, the change is shippable.