-
Notifications
You must be signed in to change notification settings - Fork 3
kithara test fixtures
Documentation reviewed from source revision 19ca073f2. This records the documented contract at that revision; it is not a new runtime validation. API and usage · All crates.
<root>/<fingerprint>/<id>.<ext>
<root>/<fingerprint>/<id>.lock
-
root —
KITHARA_FIXTURE_CACHEwhen set, otherwise<temp_dir>/kithara-fixture-cache. CI points it at a persisted directory, so a fresh job finds every entry already there. - fingerprint — 16 hex characters identifying the build that produced the entries below it. See Invalidation.
-
id —
sha2-256(func ‖ 0x00 ‖ case), first 16 bytes, hex. The separator is what keeps("sine", "wav_6s")and("sine_wav", "6s")apart.
An entry is one file. A zero-length file is a miss, not an empty asset: a
producer that dies mid-write must not leave something a reader will serve.
Writes go to <id>.<ext>.tmp.<pid>, are sync_alled, then renamed — a reader
sees the entry whole or not at all.
The id hashes the function name and the case name, and nothing else. It
deliberately omits the module path, which inside a build script expands to
build_script_build::defs::… — an artefact of how the generators are compiled,
not a property of the asset. Uniqueness holds by construction: every accessor
lands in one flat generated module, so two cases sharing both halves could not
coexist there. The build script asserts it anyway.
Content-addressing over the accessor name alone cannot notice that a generator changed its output. The fingerprint is what stands between a changed generator and the bytes the previous one produced: it hashes the generator sources and the inputs they encode with. A changed generator lands in a new namespace and regenerates; the old namespace stays until the store is pruned, which is what lets a branch switch back and forth without paying twice.
The fingerprint is a build-script concern; the store only receives it as an opaque directory name.
Every consumer of the store — parallel build scripts, several checkouts sharing one root — races on the same entries. The protocol is double-checked:
- Read the entry. A hit ends it.
- Take
lock_entry, which blocks until the entry is this process's to produce. - Read again. Another producer may have finished while this one waited.
- Produce, write atomically, release.
EntryLock releases with its file handle, so a producer that panics or is
killed does not wedge the store.
An asset with named #[case::...(...)] cases may use {case} in a
depends_on entry. The macro substitutes the current case name before the
registry is built, so parallel fixture families can declare one dependency
pattern without a second hand-maintained case map.
#[kithara::asset(..., embed)] changes how an asset is served, never how it is
produced. The build script materializes it into the store exactly as it does any
other asset, then emits an accessor that reads that file back with
include_bytes! — one generation pass, bytes baked into the binary.
Consequences, in the order they matter:
-
Asset::path()returnsNone. An embedded asset has no file at run time, so a test that needs a path on disk must not embed. - The bytes cost binary size in every test binary that links the accessor. Embed where disk access is the thing under test or unavailable — wasm has no filesystem, and only embedded accessors compile there.
- rustc records
include_bytes!paths in dep-info, so cargo rebuilds the accessor when the store entry it was built from changes.
#[kithara::asset(..., optional)] is the only asset shape allowed to fail
without failing the build. It is reserved for opt-in remote hydration:
- Normal builds stay offline.
KITHARA_REMOTE_FIXTURESenables optional producers; without it their generated accessors report that hydration is disabled. -
env = ["NAME", ...]records credential dependencies withcargo:rerun-if-env-changed. Values are read only by the producer and are never written to generated source, manifests, diagnostics, or the store. - A failed optional producer emits a redacted Cargo warning and leaves no final
entry.
Asset::try_bytes()returns the recordedAssetError::Unavailable, and the next enabled build retries because the entry is still absent. - Required producers retain the old fail-closed contract.
optionalcannot be combined withembedbecause unavailable bytes cannot be compiled into a binary.
The HLS hydrator stores every fetched playlist, media resource, init section,
and AES-128 key as an atomic child entry through BuildContext. It rewrites
their URIs to stable local routes and returns one bundle manifest only after the
complete VOD graph is valid and stored. The ordinary asset writer commits that
manifest last. Child entries left by a storage failure are unreachable without
the manifest and safe to reuse on a retry.
HlsBundle::try_from(&Asset) is the runtime read side. It accepts only one-level
store filenames, absolute routes, unique routes, and a master route present in
the manifest, so a hydrated manifest cannot escape its fixture namespace when
served by a test.
src/defs/ is reached only through #[path] from build.rs. Two consequences,
both load-bearing:
- Encoding is a build-dependency.
kithara-encodeenters the target build for itsPcmSourcetrait alone, with FFmpeg's encoder features off the library's own default path; nothing in a target build calls an encoder. - Generation happens exactly once per fingerprint, in the build script. Nothing in the library can synthesize an asset, which is the whole point: a test's deadline never contains an encode.
src/signal/ is the exception, and the only one: it is the library's, and the
build script reaches it through #[path] exactly as it reaches src/store.rs.
The same source file, two roots, one visibility.
It exists because a waveform is needed on both sides of the build line. defs/
renders assets from it before the tests start; the integration suite renders
the bodies its HTTP fixtures serve from it while they run. Two implementations
of a sine would drift, and a test that asserts on decoded samples cannot tell
which one it is asserting against.
-
Wave— the waveform vocabulary. One enum, onesample(frame, sample_rate).TONEnames the 440 Hz full-scale sine that a fixture carries unless it says otherwise. -
Pcm— interleaved 16-bit PCM in memory;PcmSourceoff wasm. -
wav/wav_of_size/header— the RIFF writer, including the streaming header whose size fields say0xFFFFFFFFbecause the total is not known when it is written.
Pcm::new and wav take a Wave; Pcm::from_fn and wav_from_fn take a
per-frame closure, for the bodies no single waveform describes.
The same module owns the reading side, because a reader that disagrees with the writer is worse than no reader at all:
-
phase— the saw decoder.unitsrecovers a frame's phase from a decoded sample,deltais the shortest signed step between two phases,distancethe same ignoring direction.SAW_PERIODis the one period, andphasefails to compile if its signed twin ever stops matching it. -
detect_direction/SignalDirection— which way a saw runs, by voting over the first frames of a buffer. -
classify_windows/ascending_phase_replays/FrameClass/Replay— per-window provenance: which stretch of a decoded stream ascends, descends, or is silent, and where a stream replays phase it already served. -
goertzel_magnitude— energy at one frequency, for telling two generated tones apart without a full transform.
SignalAsset sits beside signal rather than inside assets, because it has
to compile for wasm and assets does not: the store is a host filesystem, and
the browser lane names an asset and fetches its bytes over HTTP. Its census
tests hold the two sides together — every declared asset is registered by a
generator, every registered one is declarable, and the extensions agree.
This is the workspace's only route to a generated signal. Two others — a
per-waveform trait with a lazy renderer, and a second RIFF writer — were
removed, and nothing may grow back beside signal: a suite that asserts on decoded samples has to know which sine it is
asserting against.
src/fmp4/ is shared exactly as src/signal/ is, and for the same reason: the
build script packages the HE-AAC bodies it embeds, and the integration suite's
HLS server packages its variants while the tests run. mux_audio_track turns an
EncodedTrack into an Fmp4Package: one init segment plus one media segment
per packets_per_segment access units. mux_audio_track_at accepts explicit
access-unit end boundaries for non-uniform HLS and can package a prefix of one
longer encode. Both derive segment duration from the track's timescale and
keep one continuous decode timeline. This is how the long, gapless, and RSS HLS
bundles reuse one encode per variant. Vec::<u8>::from(package) concatenates
the package into the single body a decoder reads.
It lives here rather than in kithara-encode because nothing ships it: the
workspace reads fMP4 in kithara-decode and broadcasts ADTS in
kithara-broadcast. A muxer exported from a product crate with no production
caller is what the dead_exports ratchet is for.
- Supported codecs are exactly
AacLc,AacHe,AacHeV2, andFlac; anything else isFmp4MuxError::UnsupportedCodec. AAC-LC gets a synthesized AudioSpecificConfig because itscodec_configis empty; HE-AAC and FLAC carry theirs through. -
GaplessEncodingpicks which gapless metadata the init segment carries.AVPlayerreadsiTunSMPBand this workspace's decoder preferselst, so a caller that pins one path must be able to say so;Bothwrites both, and the decoder contract is thatelstwins. It carries no serde derives — the one wire format that ships it owns its own mapping, intests/src/fixture_protocol.rs, beside the identical one forAudioCodec. - The mux folds the codec's own native priming (1024 frames for AAC-LC, none for
the rest) into the
encoder_delayit writes, so a downstream gapless probe reports the same trim total FFmpeg would have written. - RFC 6381 codec strings are not produced here. Production only parses
CODECS="..."from incoming playlists, so generating one stays in the harness that writes manifests. - Box writers panic instead of returning an error when a length does not fit its
field: a 32-bit box size, a 32-bit
trunsample size, a signed 32-bittrundata offset, or an AAC channel configuration outside 1..=7. Every one measures a buffer already held in memory, so the value cannot arise from input the mux is meant to accept, and threading aResultthrough every box builder would trade a loud impossible failure for noise on every call site.
Two workspace checks are told about this crate rather than worked around at the
call site, because both misread it for the same reason: the code that consumes
these exports is generated into OUT_DIR or lives in the build script, and a
source scanner sees neither.
-
dead_exports(.config/arch/thresholds.toml) counts kithara-test-fixtures among the test crates. Its exports are reached only from generated accessors and its own build script, and a call it makes into a workspace API is a fixture, not a shipped caller. -
perf.prefer-primitive-poolskips this crate, alongside the other test scaffolding. A generator returns one complete asset that crosses an ownership boundary into the store; there is no pool in a build script to lease from, and the rule itself exempts that shape.
The legacy fixture cache is not an extension point. Fixture generation and caching belong to this crate. Migration tracking belongs in repository issues.