use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::thread;
use std::time::{Duration, Instant};
use bstr::BString;
use jamjam::jam::{JamMessage, JamMessageBase};
use jamjam::util::echomail::EchomailAddress;
use tempfile::TempDir;
const MODE: &str = "JAMJAM_CROSS_PROCESS_MODE";
const BASE: &str = "JAMJAM_CROSS_PROCESS_BASE";
const ID: &str = "JAMJAM_CROSS_PROCESS_ID";
const MESSAGES_PER_WRITER: u32 = 15;
const WRITERS: u32 = 3;
fn message(text: &str) -> JamMessage {
JamMessage::new(&EchomailAddress::default())
.with_to(BString::from("all"))
.with_subject(BString::from(text))
.with_text(BString::from(text))
}
fn held(path: &Path) -> PathBuf {
path.with_extension("held")
}
fn release(path: &Path) -> PathBuf {
path.with_extension("release")
}
#[test]
#[ignore = "spawned by the cross process tests"]
fn lock_helper() {
let Ok(mode) = std::env::var(MODE) else {
return;
};
let path = PathBuf::from(std::env::var(BASE).unwrap());
let mut base = JamMessageBase::open(&path).unwrap();
if mode == "write" {
let id = std::env::var(ID).unwrap();
for index in 0..MESSAGES_PER_WRITER {
base.write_message(&message(&format!("{id}-{index}")))
.unwrap();
}
return;
}
match mode.as_str() {
"exclusive" => base.lock().unwrap(),
"shared" => base.lock_shared().unwrap(),
other => panic!("unknown mode {other}"),
}
std::fs::write(held(&path), []).unwrap();
wait_for(&release(&path));
base.unlock();
}
fn spawn(mode: &str, path: &Path) -> Child {
Command::new(std::env::current_exe().unwrap())
.args(["--exact", "lock_helper", "--ignored"])
.env(MODE, mode)
.env(BASE, path)
.stdout(Stdio::null())
.spawn()
.unwrap()
}
fn wait_for(path: &Path) {
let deadline = Instant::now() + Duration::from_secs(30);
while !path.exists() {
assert!(
Instant::now() < deadline,
"timed out waiting for {}",
path.display()
);
thread::sleep(Duration::from_millis(5));
}
}
fn finish(mut child: Child, path: &Path) {
std::fs::write(release(path), []).unwrap();
assert!(child.wait().unwrap().success(), "the helper process failed");
}
fn filled(path: &Path) {
let mut base = JamMessageBase::create(path).unwrap();
base.write_message(&message("one")).unwrap();
}
#[test]
fn test_an_exclusive_lock_in_another_process_keeps_everyone_out() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("exclusive");
filled(&path);
let mut base = JamMessageBase::open(&path).unwrap();
let child = spawn("exclusive", &path);
wait_for(&held(&path));
assert!(!base.try_lock().unwrap(), "a second writer got the lock");
assert!(
!base.try_lock_shared().unwrap(),
"a reader got past a writer"
);
finish(child, &path);
assert!(base.try_lock().unwrap(), "the lock outlived the process");
base.unlock();
}
#[test]
fn test_a_shared_lock_in_another_process_lets_readers_in() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("shared");
filled(&path);
let mut base = JamMessageBase::open(&path).unwrap();
let child = spawn("shared", &path);
wait_for(&held(&path));
assert!(
base.try_lock_shared().unwrap(),
"two readers cannot read at the same time"
);
base.unlock();
assert!(!base.try_lock().unwrap(), "a writer got past a reader");
finish(child, &path);
assert!(base.try_lock().unwrap());
base.unlock();
}
#[test]
fn test_opening_waits_for_a_writer_in_another_process() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("waiting");
filled(&path);
let child = spawn("exclusive", &path);
wait_for(&held(&path));
let opening = thread::spawn({
let path = path.clone();
move || JamMessageBase::open(&path).map(|_| ())
});
thread::sleep(Duration::from_millis(200));
assert!(
!opening.is_finished(),
"opening did not wait for the lock holder"
);
finish(child, &path);
opening.join().unwrap().unwrap();
}
#[test]
fn test_writers_in_other_processes_do_not_lose_messages() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("writers");
drop(JamMessageBase::create(&path).unwrap());
let children: Vec<Child> = (0..WRITERS)
.map(|id| {
Command::new(std::env::current_exe().unwrap())
.args(["--exact", "lock_helper", "--ignored"])
.env(MODE, "write")
.env(BASE, &path)
.env(ID, id.to_string())
.stdout(Stdio::null())
.spawn()
.unwrap()
})
.collect();
for mut child in children {
assert!(child.wait().unwrap().success(), "a writer process failed");
}
let mut base = JamMessageBase::open(&path).unwrap();
let expected = WRITERS * MESSAGES_PER_WRITER;
assert_eq!(base.active_messages(), expected);
let report = base.verify().unwrap();
assert!(report.is_ok(), "{report}");
assert_eq!(report.live_messages, expected);
let numbers: BTreeSet<u32> = base
.messages()
.map(|header| header.unwrap().message_number)
.collect();
assert_eq!(numbers.len(), expected as usize);
let texts: BTreeSet<String> = base
.messages_full()
.map(|message| message.unwrap().text().to_string())
.collect();
let all: BTreeSet<String> = (0..WRITERS)
.flat_map(|id| (0..MESSAGES_PER_WRITER).map(move |index| format!("{id}-{index}")))
.collect();
assert_eq!(texts, all);
}