Files
varde/varde-daemon/tests/support/mod.rs
T
bl 258fa072aa Milestone 6: DiscoveryProvider seam, signed announcements, redoal sketch
The discovery module defines the seam: DiscoveryProvider (subscribe/
announce over 32-byte TopicKeys) and a signed Announcement carrying
root hash, ed25519 author, metadata and provider addresses. Signatures
cover a deterministic postcard encoding including the topic (no cross-
channel replay) and the provider identities (addresses stay refreshable
hints). LanDiscovery conforms to the trait — mdns sightings become
locally-authored announcements, one per pinned root — and the daemon's
auto-sync now runs entirely through it: verify, index unconditionally,
fetch only for trusted authors from allowlisted providers on already-
pinned incomplete roots. The milestone-4 real-mdns sync test passes
unchanged through the new path. Verification unit tests cover round
trip, tampered root, wrong topic, forged author, mismatched signing
key, and serde survival. docs/redoal-integration.md sketches the
gesture-topic gossip provider against this contract.

Also: fix a flaky hang in the socket-activation test (dup2(3,3) leaves
CLOEXEC set when the listener already sits on fd 3; parent's listener
copy masked daemon death), and give the test client a read-timeout hang
guard. Add a top-level README.

Dependencies: ed25519-dalek (Signature type; same implementation iroh
keys use), postcard (deterministic signed encoding, iroh's canonical
compact codec).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 09:51:57 +02:00

162 lines
5.0 KiB
Rust

//! Shared helpers for integration tests: spawn the real daemon binary on
//! ephemeral paths and speak the JSON-lines protocol to it.
// Each test binary compiles its own copy of this module and none of them
// uses every helper.
#![allow(dead_code)]
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::process::{Child, Command};
use std::time::{Duration, Instant};
use varde_proto::{Envelope, Request};
/// A running daemon process, killed on drop.
pub struct DaemonProc {
pub child: Child,
pub socket: PathBuf,
pub store: PathBuf,
// Kept alive so the store/socket dirs survive as long as the daemon.
_dir: Option<tempfile::TempDir>,
}
impl DaemonProc {
/// SIGKILL the daemon — no cleanup handlers run, simulating a crash.
pub fn kill_dash_nine(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
impl Drop for DaemonProc {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
/// Spawn the compiled varde-daemon on a fresh tempdir.
pub fn spawn_daemon() -> DaemonProc {
let dir = tempfile::tempdir().expect("tempdir");
let mut proc = spawn_daemon_at(dir.path());
proc._dir = Some(dir);
proc
}
/// Spawn the daemon against an existing directory (for restart tests).
pub fn spawn_daemon_at(dir: &Path) -> DaemonProc {
spawn_daemon_with_env(dir, &[])
}
/// Spawn the daemon with extra environment variables (config overrides).
pub fn spawn_daemon_with_env(dir: &Path, envs: &[(&str, &str)]) -> DaemonProc {
let socket = dir.join("varde.sock");
let store = dir.join("store");
let mut command = Command::new(env!("CARGO_BIN_EXE_varde-daemon"));
command
.arg("--user")
.arg("--socket")
.arg(&socket)
.arg("--store")
.arg(&store)
// Isolate from any config file on the developer's machine.
.env("VARDE_CONFIG", dir.join("no-config.toml"))
.env("VARDE_DISCOVERY", "false");
for (key, value) in envs {
command.env(key, value);
}
let child = command.spawn().expect("spawning varde-daemon");
// An explicitly named config must exist for the daemon to start.
std::fs::write(dir.join("no-config.toml"), "").expect("writing empty config");
let proc = DaemonProc {
child,
socket,
store,
_dir: None,
};
wait_for_socket(&proc.socket);
proc
}
fn wait_for_socket(path: &Path) {
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
if UnixStream::connect(path).is_ok() {
return;
}
std::thread::sleep(Duration::from_millis(20));
}
panic!("daemon socket {} never appeared", path.display());
}
/// One connected API client.
pub struct Client {
reader: BufReader<UnixStream>,
writer: UnixStream,
}
impl Client {
pub fn connect(socket: &Path) -> Client {
let stream = UnixStream::connect(socket).expect("connecting to daemon");
// Hang guard: a daemon that accepts but never answers should fail
// the test, not wedge the whole suite.
stream
.set_read_timeout(Some(Duration::from_secs(120)))
.expect("setting read timeout");
let reader = BufReader::new(stream.try_clone().expect("cloning stream"));
Client {
reader,
writer: stream,
}
}
/// Send a request and read the response envelope.
pub fn request(&mut self, req: &Request) -> Envelope {
let mut line = serde_json::to_string(req).expect("serializing request");
line.push('\n');
self.writer
.write_all(line.as_bytes())
.expect("writing request");
self.read_envelope()
}
/// Send a raw line (for malformed-input tests).
pub fn send_raw(&mut self, raw: &str) -> Envelope {
self.writer
.write_all(raw.as_bytes())
.expect("writing raw line");
self.writer.write_all(b"\n").expect("writing newline");
self.read_envelope()
}
fn read_envelope(&mut self) -> Envelope {
let mut reply = String::new();
self.reader.read_line(&mut reply).expect("reading reply");
assert!(
!reply.is_empty(),
"daemon closed connection without replying"
);
serde_json::from_str(&reply).expect("parsing reply envelope")
}
/// Bound how long reads may block (applies to the shared socket).
pub fn set_read_timeout(&mut self, timeout: Duration) {
self.writer
.set_read_timeout(Some(timeout))
.expect("setting read timeout");
}
/// Read one line of the event stream (after Subscribe).
pub fn read_event_line(&mut self) -> Option<String> {
let mut line = String::new();
match self.reader.read_line(&mut line) {
Ok(0) => None,
Ok(_) => Some(line),
Err(e) => panic!("reading event line: {e}"),
}
}
}