use std::sync::{Arc, Barrier, mpsc};
use std::thread;
use sup_xml::{
parse_str, serialize_to_string, xpath_count, Document, ParseOptions, XmlError,
};
const SAMPLE: &str = r#"<?xml version="1.0"?>
<catalog xmlns:m="urn:meta" m:rev="3">
<!-- inventory -->
<book id="1"><title>Dune</title><price>9.99</price></book>
<book id="2"><title>Foundation & Empire</title><price>12.50</price></book>
<book id="3"><title>Neuromancer</title><price>8.00</price></book>
</catalog>"#;
fn doc(xml: &str) -> Document {
parse_str(xml, &ParseOptions::default()).expect("test document must parse")
}
fn assert_send<T: Send>() {}
fn assert_send_sync<T: Send + Sync>() {}
#[test]
fn type_contract() {
assert_send::<Document>();
assert_send_sync::<ParseOptions>();
assert_send_sync::<XmlError>();
}
#[test]
fn parallel_parsing_matches_serial_golden() {
let golden = doc(SAMPLE);
let golden_count = xpath_count(&golden, "//book").unwrap();
let golden_xml = Arc::new(serialize_to_string(&golden));
drop(golden);
const THREADS: usize = 8;
const ITERS: usize = 50;
let start = Arc::new(Barrier::new(THREADS));
let handles: Vec<_> = (0..THREADS)
.map(|_| {
let start = Arc::clone(&start);
let golden_xml = Arc::clone(&golden_xml);
thread::spawn(move || {
start.wait();
for _ in 0..ITERS {
let d = doc(SAMPLE);
assert_eq!(xpath_count(&d, "//book").unwrap(), golden_count);
assert_eq!(&serialize_to_string(&d), &*golden_xml);
}
})
})
.collect();
for h in handles {
h.join().expect("worker thread panicked");
}
}
#[test]
fn parallel_parsing_of_distinct_documents() {
const THREADS: usize = 16;
let start = Arc::new(Barrier::new(THREADS));
let handles: Vec<_> = (0..THREADS)
.map(|i| {
let start = Arc::clone(&start);
thread::spawn(move || {
let items: String = (0..=i).map(|_| "<item/>").collect();
let xml = format!("<root id=\"{i}\">{items}</root>");
start.wait();
for _ in 0..50 {
let d = doc(&xml);
assert_eq!(xpath_count(&d, "//item").unwrap(), i + 1);
assert_eq!(xpath_count(&d, "/root[@id]").unwrap(), 1);
}
})
})
.collect();
for h in handles {
h.join().expect("worker thread panicked");
}
}
#[test]
fn document_moves_into_worker_thread() {
let d = doc(SAMPLE);
let count = thread::spawn(move || {
let n = xpath_count(&d, "//book").unwrap();
assert!(serialize_to_string(&d).contains("Neuromancer"));
n
})
.join()
.unwrap();
assert_eq!(count, 3);
}
#[test]
fn document_returned_from_worker_thread() {
let (tx, rx) = mpsc::channel::<Document>();
thread::spawn(move || {
tx.send(doc(SAMPLE)).unwrap();
});
let d = rx.recv().unwrap();
assert_eq!(xpath_count(&d, "//book").unwrap(), 3);
assert!(serialize_to_string(&d).contains("Dune"));
}
#[test]
fn arenas_allocated_off_thread_drop_on_main() {
const THREADS: usize = 12;
let (tx, rx) = mpsc::channel::<Document>();
for i in 0..THREADS {
let tx = tx.clone();
thread::spawn(move || {
let xml = format!("<doc n=\"{i}\"><leaf/></doc>");
tx.send(doc(&xml)).unwrap();
});
}
drop(tx);
let collected: Vec<Document> = rx.iter().collect();
assert_eq!(collected.len(), THREADS);
for d in &collected {
assert_eq!(xpath_count(d, "//leaf").unwrap(), 1);
}
drop(collected); }
#[test]
fn simultaneous_first_parse_under_contention() {
const THREADS: usize = 32;
let start = Arc::new(Barrier::new(THREADS));
let handles: Vec<_> = (0..THREADS)
.map(|_| {
let start = Arc::clone(&start);
thread::spawn(move || {
start.wait();
let d = doc(SAMPLE);
xpath_count(&d, "//title").unwrap()
})
})
.collect();
for h in handles {
assert_eq!(h.join().expect("worker thread panicked"), 3);
}
}
fn stress_factor() -> usize {
std::env::var("SUPSO_THREAD_STRESS")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|&n| n > 0)
.unwrap_or(1)
}
#[test]
#[ignore = "heavy soak; run via `cargo stress-threads`"]
fn stress_parallel_parsing_matches_golden() {
let golden = doc(SAMPLE);
let golden_count = xpath_count(&golden, "//book").unwrap();
let golden_xml = Arc::new(serialize_to_string(&golden));
drop(golden);
const THREADS: usize = 32;
let iters = 2_000 * stress_factor();
let start = Arc::new(Barrier::new(THREADS));
let handles: Vec<_> = (0..THREADS)
.map(|_| {
let start = Arc::clone(&start);
let golden_xml = Arc::clone(&golden_xml);
thread::spawn(move || {
start.wait();
for _ in 0..iters {
let d = doc(SAMPLE);
assert_eq!(xpath_count(&d, "//book").unwrap(), golden_count);
assert_eq!(&serialize_to_string(&d), &*golden_xml);
}
})
})
.collect();
for h in handles {
h.join().expect("worker thread panicked");
}
}
#[test]
#[ignore = "heavy soak; run via `cargo stress-threads`"]
fn stress_cross_thread_handoff() {
let rounds = 500 * stress_factor();
const THREADS: usize = 16;
for _ in 0..rounds {
let (tx, rx) = mpsc::channel::<Document>();
for i in 0..THREADS {
let tx = tx.clone();
thread::spawn(move || {
let xml = format!("<doc n=\"{i}\"><leaf/></doc>");
tx.send(doc(&xml)).unwrap();
});
}
drop(tx);
let collected: Vec<Document> = rx.iter().collect();
assert_eq!(collected.len(), THREADS);
for d in &collected {
assert_eq!(xpath_count(d, "//leaf").unwrap(), 1);
}
}
}