8c9a1b4630
A real OHLC source now builds from aura-ingest alone, in the engine-native epoch-ns Timestamp currency, with the one ms<->ns crossing owned by the seam. Library surface (src/lib.rs), all additive — the ms-based `open` and `unix_ms_to_epoch_ns` are untouched, so every existing call site is preserved byte-for-byte: - epoch_ns_to_unix_ms (private): the seam-owned inverse; consumers never convert (#80). - M1FieldSource::open_window: the Timestamp-window mirror of `open`, mapping each bound through the private inverse and delegating to `open`. - open_ohlc: the canonical OHLC bundle opener — four M1FieldSources in the fixed open/high/low/close C4 merge order, sharing one Arc<DataServer> (one cache, C12); the order lives in one vetted place (#92). - default_data_server + `pub use data_server::{DataServer, DEFAULT_DATA_PATH}`: a real-data source builds without naming the external data_server crate (#81). Consumer migration: the shared breakout_real.rs helpers and every GER40 example/test move to the Timestamp-native surface — open_ohlc_sources removed, utc_month_window_ms -> utc_month_window (Timestamp), report_from_trace retyped, the per-consumer ns_to_ms hand-divides deleted, the direct data_server imports replaced by the re-exports. ger40_breakout_compare.rs (which opened OHLC by its own hand-loop, reached by neither retyped helper) is migrated by inspection. Return type is Vec, not [_; 4]: Harness::run consumes Vec<Box<dyn Source>>, so it feeds straight in; order-safety comes from the single helper either way. Tests: a hermetic round-trip pinning the inverse; a gated behaviour-preservation test (open_ohlc Timestamp path == ms-path open, bit-identical recorded series); a hermetic absent-archive fixture pinning the file-level None contract and the #81 re-export firewall. Verified: cargo build --workspace --all-targets clean; cargo test --workspace green (the gated GER40 tests ran against the local archive — open_ohlc_seam proves byte-identity on real data, not a skip); clippy --all-targets -D warnings clean. Acceptance greps pass: open_ohlc_sources gone, no consumer-side ns_to_ms, no data_server import in any migrated GER40 consumer, epoch_ns_to_unix_ms private. closes #80, #81, #92
103 lines
5.3 KiB
Rust
103 lines
5.3 KiB
Rust
//! Hermetic coverage for the Task-1 OHLC source-open surface (#80/#81/#92): the
|
|
//! documented *file-level* `None` contract of the canonical openers, and the #81
|
|
//! re-export firewall (a real-data source builds from `aura-ingest` alone). Every
|
|
//! item here is reached through `aura_ingest` ONLY — this test never names the
|
|
//! external `data_server` crate, which is the firewall it pins.
|
|
//!
|
|
//! Unlike the gated GER40 tests (which skip without a local Pepperstone archive),
|
|
//! these run everywhere: they drive a `DataServer` over a guaranteed-EMPTY base
|
|
//! path, so no archived file overlaps any window and the openers take their
|
|
//! documented "no data source present" branch deterministically — same input,
|
|
//! same `None`, every run, on any machine.
|
|
|
|
use std::sync::Arc;
|
|
|
|
// Firewall (#81): the ENTIRE source-open stack is imported from `aura_ingest`.
|
|
// `DataServer` / `DEFAULT_DATA_PATH` are re-exports; if a future change drops
|
|
// the re-export, this `use` stops compiling — that is the test.
|
|
use aura_core::Timestamp;
|
|
use aura_ingest::{
|
|
default_data_server, open_ohlc, DataServer, M1Field, M1FieldSource, DEFAULT_DATA_PATH,
|
|
};
|
|
|
|
/// A `DataServer` over a base path that is guaranteed not to exist, so its symbol
|
|
/// index is empty on every machine (data-server's directory scan returns `Err`
|
|
/// for an absent path and yields no symbols). Gives a deterministic "no archive"
|
|
/// server with no filesystem writes and no dependence on local data.
|
|
fn empty_server() -> Arc<DataServer> {
|
|
// A path component that cannot exist as a real archive directory; uniqueness
|
|
// is irrelevant (an absent path scans empty regardless), but keep it obviously
|
|
// synthetic so an accidental match is impossible.
|
|
let absent = std::env::temp_dir().join("aura-ingest-no-such-archive-DO-NOT-CREATE");
|
|
Arc::new(DataServer::new(absent))
|
|
}
|
|
|
|
/// Property: `open_ohlc` returns the *file-level* `None` (the "no data source to
|
|
/// read from" branch) when no archived file overlaps the window — distinct from
|
|
/// the "overlaps a file but holds zero bars" case, which would return
|
|
/// `Some(empty)`. Pins the documented Option semantics of the canonical OHLC
|
|
/// bundle opener (#80/#92) so a future refactor cannot silently turn "no source"
|
|
/// into a panic or an empty `Some`.
|
|
#[test]
|
|
fn open_ohlc_yields_file_level_none_when_no_archive_overlaps() {
|
|
let server = empty_server();
|
|
let from = Timestamp(0);
|
|
let to = Timestamp(1_000_000_000_000_000_000); // ~2001, well-formed epoch-ns window
|
|
assert!(
|
|
open_ohlc(&server, "GER40", from, to).is_none(),
|
|
"open_ohlc over an empty archive must return the file-level None (no source present)",
|
|
);
|
|
}
|
|
|
|
/// Property: `M1FieldSource::open_window` — the engine-side epoch-ns `Timestamp`
|
|
/// mirror of the ms-path `open` — threads its `Timestamp` bounds through the
|
|
/// seam-private ms<->ns crossing and propagates data-server's file-level `None`
|
|
/// for an absent symbol, without panicking on the ns->ms divide. Pins that the
|
|
/// Timestamp opener is a faithful, total mirror of `open` on the "no source"
|
|
/// branch (the C3 one-crossing seam, #80).
|
|
#[test]
|
|
fn open_window_propagates_file_level_none_for_absent_symbol() {
|
|
let server = empty_server();
|
|
let from = Timestamp(0);
|
|
let to = Timestamp(1_000_000_000_000_000_000);
|
|
assert!(
|
|
M1FieldSource::open_window(&server, "GER40", Some(from), Some(to), M1Field::Close)
|
|
.is_none(),
|
|
"open_window over an empty archive must propagate the file-level None",
|
|
);
|
|
// An open-ended window (None bounds) takes the same no-source branch — the
|
|
// ns->ms mapping is a no-op on None, so the openness is preserved.
|
|
assert!(
|
|
M1FieldSource::open_window(&server, "GER40", None, None, M1Field::Close).is_none(),
|
|
"open-ended open_window over an empty archive must also propagate None",
|
|
);
|
|
}
|
|
|
|
/// Property: the #81 source-open surface is reachable from `aura_ingest` alone —
|
|
/// `default_data_server()` constructs the canonical-path server and the
|
|
/// re-exported `DEFAULT_DATA_PATH` is the path it points at, with the external
|
|
/// `data_server` crate never named by a consumer. Observable: the convenience
|
|
/// constructor agrees with constructing `DataServer::new(DEFAULT_DATA_PATH)` by
|
|
/// hand on the symbol-presence query (both report the same absence of a synthetic
|
|
/// symbol that no real archive can hold), so the convenience is a faithful alias.
|
|
#[test]
|
|
fn default_data_server_builds_from_ingest_alone_over_canonical_path() {
|
|
let convenience = default_data_server();
|
|
// The re-export is reachable and is what the convenience constructor uses.
|
|
let by_hand = Arc::new(DataServer::new(DEFAULT_DATA_PATH));
|
|
// A symbol no real Pepperstone archive holds: both servers agree it is absent,
|
|
// independent of whether the local archive exists — so the convenience
|
|
// constructor is a faithful alias for `DataServer::new(DEFAULT_DATA_PATH)`,
|
|
// and the whole stack was built without naming `data_server`.
|
|
const SYNTHETIC_ABSENT: &str = "AURA-INGEST-SYNTHETIC-NONEXISTENT-SYMBOL";
|
|
assert_eq!(
|
|
convenience.has_symbol(SYNTHETIC_ABSENT),
|
|
by_hand.has_symbol(SYNTHETIC_ABSENT),
|
|
"default_data_server() must agree with DataServer::new(DEFAULT_DATA_PATH)",
|
|
);
|
|
assert!(
|
|
!convenience.has_symbol(SYNTHETIC_ABSENT),
|
|
"a synthetic symbol no archive holds must be absent",
|
|
);
|
|
}
|