204c171e60
M5 iteration 1 (specae905de, plan22f02aa). Stands up the workspace-excluded `ail-embed` crate: - zero-dependency embedding core (`ail-embed/src/lib.rs`): extern "C" to the M3-frozen ABI + frozen-layout State/Tick box helpers + a Kernel price fold; the Rust port of the audited crates/ail/tests/embed/tick_roundtrip.c. Raw pointers never escape the type. - build.rs (no in-repo precedent): AIL_BIN env override else nested `cargo build -p ail` against the parent workspace (separate target dir → no cargo-lock deadlock), `ail build --emit=staticlib`, link directives. - hermetic data-server smoke (ail-embed/tests/smoke.rs): synthetic Pepperstone-format ZIP fixture via data-server's own public RawTickRecord type → real DataServer → Kernel, bit-exact vs a same-order host reference fold; runs with no /mnt. - `ail-embed` is its own cargo workspace root (empty [workspace] table); data-server is a dev-dependency only. Root Cargo.toml gains only a 4-line non-membership comment. Invariant 1 Boss-verified independently: full+no-deps cargo metadata on the AILang workspace shows data-server count 0; git status path filter empty (zero diff to crates/ailang-*, crates/ail/, runtime/, examples/*.ail); src/lib.rs zero code-level data_server; AILang cargo build --workspace still clean. ail-embed suite 2/2 green (kernel_run_sums_prices unit RED-first + hermetic_smoke integration), verified by me, not just the agent report. Two toolchain-forced corrections to the plan's verbatim ail-embed/Cargo.toml (added empty [workspace] table; sibling dev-dep path ../libs -> ../../libs, manifest-relative) — confined to the plan-created manifest, no acceptance gate altered, 0 review re-loops. Journal Concerns records the planner-recon implication for the next workspace-excluded-nested-crate plan. Adapter API + thread-swarm deferred to M5 iter 2+ per spec/plan. Includes the per-iter journal, stats, and the INDEX.md line.
81 lines
3.2 KiB
Rust
81 lines
3.2 KiB
Rust
//! Hermetic smoke (Testing strategy §1): synthesize a
|
|
//! Pepperstone-format `.tick` ZIP, point the REAL `DataServer` at it,
|
|
//! fold the stream through the embedding `Kernel`, assert the result
|
|
//! equals a host reference fold. Proves "adapter compiles, links,
|
|
//! ABI handshake holds" with no `/mnt` dependency. The fixture writer
|
|
//! dumps `data_server::records::RawTickRecord` bytes (the crate's own
|
|
//! public on-disk type) into a `.bin` ZIP entry — correct by
|
|
//! construction, mirroring `data-server/src/loader.rs:110-124`.
|
|
|
|
use std::io::Write;
|
|
use std::sync::Arc;
|
|
|
|
use ail_embed::Kernel;
|
|
use data_server::records::RawTickRecord;
|
|
use data_server::DataServer;
|
|
|
|
/// Writes `recs` as a Pepperstone tick file `<sym>_2017_01.tick`
|
|
/// (ZIP holding one packed-`RawTickRecord` `.bin`) into `dir`.
|
|
/// Filename satisfies the scanner regex `^(.+)_(\d{4})_(\d{2})\.tick$`
|
|
/// (`data-server/src/lib.rs:88`); the `.bin` entry name is arbitrary
|
|
/// as long as it ends in `.bin` (`loader.rs:50-55`).
|
|
fn write_tick_fixture(dir: &std::path::Path, sym: &str, recs: &[RawTickRecord]) {
|
|
let path = dir.join(format!("{sym}_2017_01.tick"));
|
|
let f = std::fs::File::create(&path).unwrap();
|
|
let mut zip = zip::ZipWriter::new(f);
|
|
zip.start_file::<String, ()>("TICK.bin".into(), Default::default())
|
|
.unwrap();
|
|
for r in recs {
|
|
// SAFETY: RawTickRecord is #[repr(C, packed)], 24 bytes; this
|
|
// is exactly data-server's own test serialisation
|
|
// (loader.rs:117-120).
|
|
let bytes: &[u8] = unsafe {
|
|
std::slice::from_raw_parts(r as *const RawTickRecord as *const u8, 24)
|
|
};
|
|
zip.write_all(bytes).unwrap();
|
|
}
|
|
zip.finish().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn hermetic_smoke_data_server_roundtrip() {
|
|
// 10 deterministic ticks. mid = (ask+bid)/2; with ask==bid==i+1,
|
|
// mid == i+1. Σ_{i=0..9}(i+1) == 55.0 exactly in f64; n == 10.
|
|
// `time` is any monotonic Delphi-day value (no window ⇒ unused by
|
|
// the fold; only the filename drives symbol/format scanning).
|
|
let recs: Vec<RawTickRecord> = (0..10)
|
|
.map(|i| RawTickRecord {
|
|
time: 42795.0 + i as f64 * 1e-4,
|
|
ask: (i + 1) as f64,
|
|
bid: (i + 1) as f64,
|
|
})
|
|
.collect();
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
write_tick_fixture(dir.path(), "TEST", &recs);
|
|
|
|
let server = Arc::new(DataServer::new(dir.path()));
|
|
assert!(server.has_symbol("TEST"), "fixture symbol not scanned");
|
|
|
|
// Collect the mid-price stream in data-server order.
|
|
let mut it = server.stream_tick("TEST").expect("TEST tick stream");
|
|
let mut prices: Vec<f64> = Vec::new();
|
|
while let Some(chunk) = it.next_chunk() {
|
|
for r in chunk.iter() {
|
|
prices.push((r.ask + r.bid) / 2.0);
|
|
}
|
|
}
|
|
assert_eq!(prices.len(), 10, "all ticks streamed");
|
|
|
|
// Host reference fold, same order, same ops ⇒ bit-exact.
|
|
let ref_acc: f64 = prices.iter().copied().fold(0.0, |a, p| a + p);
|
|
let ref_n = prices.len() as i64;
|
|
|
|
let (acc, n) = Kernel::new().run(prices.iter().copied());
|
|
|
|
assert_eq!(acc, ref_acc, "kernel acc bit-exact vs host reference");
|
|
assert_eq!(acc, 55.0, "deterministic fixture sum");
|
|
assert_eq!(n, ref_n);
|
|
assert_eq!(n, 10);
|
|
}
|