use std::sync::Arc;
use std::time::Duration;
use crawlex::config::{Config, QueueBackend, StorageBackend};
use crawlex::events::MemorySink;
use crawlex::Crawler;
use serde::Serialize;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn golden_path() -> std::path::PathBuf {
let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
dir.join("tests")
.join("fixtures")
.join("runner_ndjson_golden.txt")
}
fn deterministic_cfg(queue_path: String, storage_path: String) -> Config {
Config {
max_concurrent_http: 1,
max_concurrent_render: 0,
max_depth: Some(0),
respect_robots_txt: false,
well_known_enabled: false,
pwa_enabled: false,
favicon_enabled: false,
robots_paths_enabled: false,
dns_enabled: false,
crtsh_enabled: false,
wayback_enabled: false,
rdap_enabled: false,
collect_peer_cert: false,
collect_net_timings: false,
collect_web_vitals: false,
queue_backend: QueueBackend::Sqlite { path: queue_path },
storage_backend: StorageBackend::Filesystem { root: storage_path },
..Config::default()
}
}
#[tokio::test]
async fn runner_ndjson_event_sequence_matches_golden() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/p/0"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("content-type", "text/html; charset=utf-8")
.set_body_string("<html><body><h1>ok</h1></body></html>"),
)
.mount(&server)
.await;
let tmp = tempfile::tempdir().unwrap();
let queue_path = tmp.path().join("q.db").to_string_lossy().to_string();
let storage_path = tmp.path().join("store").to_string_lossy().to_string();
std::fs::create_dir_all(&storage_path).unwrap();
let cfg = deterministic_cfg(queue_path, storage_path);
let sink = Arc::new(MemorySink::create());
let crawler = Crawler::new(cfg).unwrap().with_events(sink.clone());
let seeds: Vec<String> = vec![format!("{}/p/0", server.uri())];
crawler.seed(seeds).await.unwrap();
let _ = tokio::time::timeout(Duration::from_secs(30), crawler.run()).await;
let events = sink.take();
let observed: Vec<String> = events
.iter()
.map(|ev| event_kind_wire_name(&ev.event))
.collect();
let observed_joined = observed.join("\n") + "\n";
let golden = golden_path();
if std::env::var("UPDATE_GOLDEN").is_ok() {
std::fs::write(&golden, &observed_joined).expect("write golden");
eprintln!("UPDATE_GOLDEN=1 → wrote {}", golden.display());
return;
}
let expected =
std::fs::read_to_string(&golden).expect("golden file missing; run with UPDATE_GOLDEN=1");
assert_eq!(
observed_joined, expected,
"NDJSON event-kind sequence drifted from golden. \
If the change is intentional, re-record with \
UPDATE_GOLDEN=1 cargo test --test runner_ndjson_regression --all-features"
);
}
fn event_kind_wire_name<K: Serialize>(kind: &K) -> String {
let s = serde_json::to_string(kind).expect("event kind serializes");
s.trim_matches('"').to_string()
}