#[cfg(feature = "conformance-fixtures")]
use std::thread;
#[cfg(feature = "conformance-fixtures")]
use std::time::{Duration, Instant};
#[cfg(feature = "conformance-fixtures")]
use pocketstation::SessionRecordingState;
use pocketstation::{Session, Source};
#[test]
fn given_public_facade_when_session_declared_then_canonical_types_are_used() {
let require_source: fn(Source) -> Source = |source| source;
let _ = require_source(Source::microphone_default());
let session_constructor = Session::new;
let _ = session_constructor;
let configured = Session::builder().recording_root("recordings").build();
let _ = configured.id();
}
#[cfg(feature = "conformance-fixtures")]
#[test]
fn given_public_facade_when_external_destinations_run_then_all_branches_receive_media() {
let recording_root = tempfile::tempdir().expect("temporary recording root");
let session = pocketstation::conformance::session_with_recording(recording_root.path())
.expect("canonical conformance Session");
let connector = pocketstation::conformance::observed_connector(&session, Duration::ZERO)
.expect("observed connector");
let browser = pocketstation::conformance::observed_browser(&session, Duration::from_millis(25))
.expect("observed browser");
let app = session
.capture(Source::application(
pocketstation::ApplicationSelector::name("PocketStation Fixture"),
))
.expect("application stem");
let mic = session
.capture(Source::microphone_default())
.expect("microphone stem");
let app_connector_route = app.send(connector).expect("application to connector");
let mic_connector_route = mic.send(connector).expect("microphone to connector");
let app_browser_route = app.send(browser).expect("application to browser");
let mic_browser_route = mic.send(browser).expect("microphone to browser");
app.record("application").expect("application recording");
mic.record("microphone").expect("microphone recording");
let mut running = session.start().expect("running Session");
wait_for_external_routes(
&running,
&[
app_connector_route.get(),
mic_connector_route.get(),
app_browser_route.get(),
mic_browser_route.get(),
],
);
let stop = running.stop();
assert!(stop.is_success(), "all endpoint branches must finalize");
let recording = running
.recording_outcome()
.expect("Session-owned recording outcome");
assert_eq!(recording.state, SessionRecordingState::Complete);
assert_eq!(recording.completed_stems, 2);
assert_eq!(recording.failed_stems, 0);
}
#[cfg(feature = "conformance-fixtures")]
#[test]
fn given_public_facade_when_session_trace_enabled_then_trace_replays_complete_lifecycle() {
let directory = tempfile::tempdir().expect("temporary session trace root");
let trace_path = directory.path().join("session.pkstrace");
let session = pocketstation::conformance::session_with_trace(&trace_path, 32)
.expect("canonical conformance Session");
let session_id = session.id();
let application = session
.capture(Source::application(
pocketstation::ApplicationSelector::name("PocketStation Fixture"),
))
.expect("application stem");
let microphone = session
.capture(Source::microphone_default())
.expect("microphone stem");
application
.send(session.polled_audio().expect("application audio endpoint"))
.expect("application audio route");
microphone
.send(session.polled_audio().expect("microphone audio endpoint"))
.expect("microphone audio route");
let mut running = session.start().expect("running Session");
wait_for_both_stems(&running);
let stop = running.stop();
assert!(stop.is_success(), "Session must stop cleanly");
let outcome = running
.session_trace_outcome()
.expect("session trace outcome")
.expect("session trace finalization");
assert!(outcome.is_complete(), "Session trace must be lossless");
let trace = pocketstation::SessionTrace::read(&trace_path).expect("read Session trace");
let validation = trace.validate().expect("validate Session trace");
assert_eq!(validation.session_id, session_id);
assert_eq!(
validation.lifecycle.as_ref(),
&[
pocketstation::SessionLifecycleState::Starting,
pocketstation::SessionLifecycleState::Running,
pocketstation::SessionLifecycleState::Stopping,
pocketstation::SessionLifecycleState::Stopped,
]
);
assert_eq!(
validation.terminal.state,
pocketstation::SessionTerminalState::Stopped
);
assert_eq!(validation.records_validated_total, 5);
}
#[cfg(feature = "conformance-fixtures")]
#[test]
fn given_stopped_public_session_when_new_session_starts_then_capture_restarts_cleanly() {
for _ in 0..2 {
let session = pocketstation::conformance::session().expect("canonical conformance Session");
let application = session
.capture(Source::application(
pocketstation::ApplicationSelector::name("PocketStation Fixture"),
))
.expect("application stem");
let microphone = session
.capture(Source::microphone_default())
.expect("microphone stem");
let audio = session.polled_audio().expect("polled audio endpoint");
application.send(audio).expect("application audio route");
microphone.send(audio).expect("microphone audio route");
let mut running = session.start().expect("running Session");
wait_for_both_stems(&running);
assert!(running.stop().is_success(), "Session must stop cleanly");
}
}
#[cfg(feature = "conformance-fixtures")]
fn wait_for_both_stems(running: &pocketstation::RunningSession) {
let deadline = Instant::now() + Duration::from_secs(5);
let mut stems = std::collections::BTreeSet::new();
loop {
if let Ok(batch) = running.try_poll_audio() {
for index in 0..batch.len() {
let frame = batch.frame(index).expect("valid bounded audio frame");
stems.insert(frame.lineage().stem_id().get());
}
if stems.len() == 2 {
return;
}
}
assert!(
Instant::now() < deadline,
"application and microphone media must arrive before the deadline"
);
thread::sleep(Duration::from_millis(2));
}
}
#[cfg(feature = "conformance-fixtures")]
fn wait_for_external_routes(running: &pocketstation::RunningSession, expected_route_ids: &[u64]) {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
let snapshot = running.metrics_snapshot().expect("Session metrics");
let routes: Vec<_> = (0..snapshot.route_count())
.filter_map(|index| snapshot.route(index).copied())
.filter(|route| expected_route_ids.contains(&route.route_id.get()))
.collect();
if routes.len() == expected_route_ids.len()
&& routes.iter().all(|route| {
route.edge.frames_delivered_total > 0
&& route
.endpoint
.is_some_and(|endpoint| endpoint.frames_received_total > 0)
})
{
return;
}
assert!(
Instant::now() < deadline,
"all connector and browser routes must deliver before the deadline"
);
thread::sleep(Duration::from_millis(2));
}
}