use std::time::Duration;
use futures::StreamExt;
use microsandbox::Sandbox;
use microsandbox::logs::{
LogEntry, LogOptions, LogRegistry, LogSource, LogStreamOptions, LogStreamStart,
};
use test_utils::msb_test;
const ALPINE: &str = "mirror.gcr.io/library/alpine";
async fn start_alpine(name: &str) -> Sandbox {
Sandbox::builder(name)
.image(ALPINE)
.cpus(1)
.memory(512)
.replace()
.create()
.await
.expect("create sandbox")
}
async fn stop_and_remove(name: &str) {
let handle = Sandbox::get(name).await.expect("get");
handle.stop().await.expect("stop");
Sandbox::remove(name).await.expect("remove");
}
fn contains(entry: &LogEntry, needle: &str) -> bool {
std::str::from_utf8(&entry.data)
.map(|s| s.contains(needle))
.unwrap_or(false)
}
#[msb_test]
async fn registry_reads_sandbox_logs() {
let name = "log-watch-registry-e2e-read";
let marker = "registry-read-marker-4c1a";
let registry = LogRegistry::new().expect("registry");
let sandbox = start_alpine(name).await;
let logger = sandbox.logger().expect("logger");
let registered = registry.register(logger).await.expect("register");
assert_eq!(registry.stats().registered_dirs, 1);
sandbox
.exec("sh", ["-c", &format!("echo {marker}")])
.await
.expect("exec");
let entries = registered
.read(&LogOptions::default())
.await
.expect("read logs");
stop_and_remove(name).await;
let matched: Vec<_> = entries.iter().filter(|e| contains(e, marker)).collect();
assert!(
!matched.is_empty(),
"expected marker {marker:?} via registered logger; saw {} entries",
entries.len(),
);
assert_eq!(matched[0].source, LogSource::Stdout);
}
#[msb_test]
async fn registry_follow_catches_live_writes() {
let name = "log-watch-registry-e2e-follow";
let marker = "registry-follow-marker-9d2f";
let registry = LogRegistry::new().expect("registry");
let sandbox = start_alpine(name).await;
let logger = sandbox.logger().expect("logger");
let registered = registry.register(logger).await.expect("register");
let cutoff = chrono::Utc::now();
let mut stream = registered
.stream(&LogStreamOptions {
sources: Vec::new(),
start: LogStreamStart::Since(cutoff),
until: None,
follow: true,
})
.await
.expect("open registry stream");
sandbox
.exec("sh", ["-c", &format!("echo {marker}")])
.await
.expect("exec");
let found = tokio::time::timeout(Duration::from_secs(8), async {
while let Some(item) = stream.next().await {
let entry = item.expect("stream item");
if contains(&entry, marker) {
return entry;
}
}
panic!("stream ended without ever seeing marker {marker:?}");
})
.await
.expect("marker arrived within timeout");
stop_and_remove(name).await;
assert_eq!(found.source, LogSource::Stdout);
}
#[msb_test]
async fn registry_serves_two_sandboxes() {
let name_a = "log-watch-registry-e2e-multi-a";
let name_b = "log-watch-registry-e2e-multi-b";
let marker_a = "registry-multi-marker-A-6b3c";
let marker_b = "registry-multi-marker-B-1e8d";
let registry = LogRegistry::new().expect("registry");
let sandbox_a = start_alpine(name_a).await;
let sandbox_b = start_alpine(name_b).await;
let registered_a = registry
.register(sandbox_a.logger().expect("logger a"))
.await
.expect("register a");
let registered_b = registry
.register(sandbox_b.logger().expect("logger b"))
.await
.expect("register b");
assert_eq!(registry.stats().registered_dirs, 2);
sandbox_a
.exec("sh", ["-c", &format!("echo {marker_a}")])
.await
.expect("exec a");
sandbox_b
.exec("sh", ["-c", &format!("echo {marker_b}")])
.await
.expect("exec b");
let entries_a = registered_a
.read(&LogOptions::default())
.await
.expect("read a");
let entries_b = registered_b
.read(&LogOptions::default())
.await
.expect("read b");
stop_and_remove(name_a).await;
stop_and_remove(name_b).await;
assert!(
entries_a.iter().any(|e| contains(e, marker_a)),
"sandbox A logs missing its own marker",
);
assert!(
!entries_a.iter().any(|e| contains(e, marker_b)),
"sandbox A logs leaked sandbox B's marker",
);
assert!(
entries_b.iter().any(|e| contains(e, marker_b)),
"sandbox B logs missing its own marker",
);
assert!(
!entries_b.iter().any(|e| contains(e, marker_a)),
"sandbox B logs leaked sandbox A's marker",
);
}