LR-EDIT-DETAIL-DENOISE: run tiles on another computer's graphics card

- `lightcraft_denoise::remote`: protocol v1 over TCP (JSON lines + byte-shuffled,
  deflated half-float tiles), a `Remote` client (connection pool, four slots,
  greeting checks the model's SHA-256 and tile size, gives up after three failed
  tiles) and `serve` (shared token compared in constant time, size caps including
  payloads that inflate past their tile, connection limit, timeouts).
- Engine: with LIGHTCRAFT_DENOISE_URL / LIGHTCRAFT_DENOISE_TOKEN set, the loaded
  model greets the server on first use and adds its slots to this computer's
  tiles at once; a tile goes to a free server slot, else runs here, and a tile
  the server fails runs here too. `denoise.status` → `device.remote` says where.
- `lightcraft-cli denoise-serve` serves the installed, selected model on this
  computer's card (checked against the CPU, as in the app) or CPU.
- docs/denoise.md: setup, safety (unencrypted: VPN/LAN only) and the protocol.

Tested: protocol round trips, refusals (token, model, tile size, hostile
payloads), fallback and give-up; the engine model with a live, a mismatched
and a dead server; the CLI binary serving a synthetic model end to end.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
akvaithiandClaude Opus 5.5 committed 2026-10-10 15:21:48 -05:00
1 parent 964d9338ba
commit 84b8995d7b
10 files changed
+945 -15

No files matched your search

+2
View File
@@ -12,6 +12,7 @@
//! blend with the plain demosaic that the Amount slider controls.
//! - [`cpu`] and [`runtime`]: a single-thread pure-Rust CPU runner and its installation self-test.
//! - [`net`] and [`onnx`]: checked plain network data and bounded ONNX decoding, shared by CPU and GPU.
//! - [`remote`]: running tiles on another computer's graphics card over TCP (a [`run::TileRunner`] and its server).
//!
//! Everything here treats model files, manifests and cache files as hostile input: sizes are capped, numbers must
//! be finite, and a malformed file is an error, never a panic.
@@ -26,6 +27,7 @@ pub mod onnx;
mod onnx_proto;
pub mod reference;
pub mod remote;
#[cfg(feature = "runtime")]
pub mod runtime;
+562
View File
@@ -0,0 +1,562 @@
//! Remote denoise: run a model's tiles on another computer's graphics card over TCP. Meant for a LAN or a VPN such as
//! Tailscale or WireGuard: the connection itself is not encrypted.
//!
//! [`Remote`] is a [`TileRunner`] that sends each tile to a server; [`serve`] runs one (`lightcraft-cli
//! denoise-serve`). Both ends must run the same model, checked by its identity ([`ModelId`]) when they greet.
//!
//! Protocol (version 1). Each message is one JSON line (`\n`-terminated, ≤ 4 KiB), optionally followed by a payload of
//! `bytes` bytes: the tile's samples as little-endian half floats, byte-shuffled (every sample's low byte, then every
//! high byte) and deflated, which roughly halves what crosses a slow link.
//!
//! ```text
//! → {"v":1,"token":"…","op":"hello"}
//! ← {"ok":true,"v":1,"device":"GPU: …","model":{"id":"…","version":"…","sha256":"…"}}
//! → {"v":1,"token":"…","op":"run","model":"<sha256>","tile":T,"bytes":N} + N bytes (4 planes of T × T)
//! ← {"ok":true,"tile":T,"bytes":N} + N bytes (3 planes of 2T × 2T)
//! ← {"ok":false,"error":"…"} (any failure)
//! ```
//!
//! A connection serves any number of requests. The server refuses a wrong token, another model than its own, tiles of
//! another size, oversized headers and payloads (also ones that inflate past their tile), and caps concurrent
//! connections; the client gives up on a server that keeps failing, so the computer's own runner does the rest.
use std::io::{BufRead, BufReader, Read, Write};
use std::net::{TcpListener, TcpStream, ToSocketAddrs};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Duration;
use serde::Serialize;
use serde_json::{Value, json};
use crate::product::{from_f16, to_f16};
use crate::run::{Error, TileRunner};
pub const PROTOCOL: u64 = 1;
pub const DEFAULT_PORT: u16 = 7990;
const MAX_LINE: usize = 4096;
/// Largest tile side in cells (a 1024-cell tile is a 2048 × 2048 pixel answer).
pub const MAX_TILE: usize = 1024;
/// Shortest token a server accepts.
pub const MIN_TOKEN: usize = 16;
/// Which model a server runs: both ends must agree before tiles cross.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct ModelId {
pub id: String,
pub version: String,
/// SHA-256 of the model file (hex).
pub sha256: String,
}
fn bad(m: impl Into<String>) -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::InvalidData, m.into())
}
fn read_line(r: &mut impl BufRead) -> std::io::Result<Option<String>> {
let mut buf = Vec::new();
let n = r.by_ref().take(MAX_LINE as u64 + 1).read_until(b'\n', &mut buf)?;
if n == 0 {
return Ok(None);
}
if buf.last() != Some(&b'\n') {
return Err(bad("message header too long or truncated"));
}
String::from_utf8(buf).map(Some).map_err(|_| bad("header is not UTF-8"))
}
/// Samples → half floats → byte-shuffled → deflated.
pub fn pack(data: &[f32]) -> Vec<u8> {
let n = data.len();
let mut b = vec![0u8; n * 2];
let (lo, hi) = b.split_at_mut(n);
for ((v, l), h) in data.iter().zip(lo.iter_mut()).zip(hi.iter_mut()) {
[*l, *h] = to_f16(*v).to_le_bytes();
}
miniz_oxide::deflate::compress_to_vec(&b, 1)
}
/// The inverse of [`pack`] for exactly `n` samples; refuses anything that inflates to more.
pub fn unpack(bytes: &[u8], n: usize) -> std::io::Result<Vec<f32>> {
let b = miniz_oxide::inflate::decompress_to_vec_with_limit(bytes, n * 2).map_err(|_| bad("payload doesn't inflate to the tile size"))?;
if b.len() != n * 2 {
return Err(bad("payload size doesn't match the tile"));
}
let (lo, hi) = b.split_at(n);
Ok(lo.iter().zip(hi).map(|(l, h)| from_f16(u16::from_le_bytes([*l, *h]))).collect())
}
fn write_payload(w: &mut impl Write, mut header: Value, data: &[f32]) -> std::io::Result<()> {
let packed = pack(data);
header["bytes"] = json!(packed.len());
writeln!(w, "{header}")?;
w.write_all(&packed)
}
/// Read a `bytes`-byte payload holding `n` samples (`bytes` is capped by the tile, so a hostile header can't make us
/// allocate more than an uncompressed tile plus slack).
fn read_payload(r: &mut impl Read, bytes: usize, n: usize) -> std::io::Result<Vec<f32>> {
if bytes > n * 2 + 4096 {
return Err(bad("payload larger than its tile"));
}
let mut b = vec![0u8; bytes];
r.read_exact(&mut b)?;
unpack(&b, n)
}
fn field(v: &Value, k: &str) -> Option<usize> {
v.get(k).and_then(Value::as_u64).and_then(|x| usize::try_from(x).ok())
}
/// Constant-time comparison (the token must not leak through timing).
fn same(a: &str, b: &str) -> bool {
let (a, b) = (a.as_bytes(), b.as_bytes());
let mut d = a.len() ^ b.len();
for i in 0..a.len().max(b.len()) {
d |= (a.get(i).copied().unwrap_or(0) ^ b.get(i).copied().unwrap_or(0)) as usize;
}
d == 0
}
/// What a server runs a tile with: four planes of `tile × tile` cells in, three planes of `2·tile × 2·tile` out.
pub type RunTile = Arc<dyn Fn(&[f32]) -> Result<Vec<f32>, Error> + Send + Sync>;
/// A server's settings.
#[derive(Clone, Debug)]
pub struct ServerOptions {
/// Shared secret every request must carry (at least [`MIN_TOKEN`] characters).
pub token: String,
pub max_connections: usize,
/// Idle and transfer limit per connection.
pub timeout: Duration,
}
impl Default for ServerOptions {
fn default() -> Self {
ServerOptions { token: String::new(), max_connections: 8, timeout: Duration::from_secs(120) }
}
}
/// Serve `model` (tiles of `tile` cells, run by `run` on `device`) until the listener fails. `log` gets one line per event.
pub fn serve(
listener: TcpListener,
model: ModelId,
tile: usize,
device: String,
run: RunTile,
opts: ServerOptions,
log: Arc<dyn Fn(&str) + Send + Sync>,
) -> Result<(), String> {
if opts.token.trim().chars().count() < MIN_TOKEN {
return Err(format!("the token must be at least {MIN_TOKEN} characters"));
}
if tile == 0 || tile > MAX_TILE {
return Err(format!("tile size {tile} is outside 1..={MAX_TILE}"));
}
let active = Arc::new(AtomicUsize::new(0));
let shared = Arc::new((model, device));
for conn in listener.incoming() {
let Ok(stream) = conn else { continue };
let peer = stream.peer_addr().map(|a| a.to_string()).unwrap_or_else(|_| "?".into());
if active.load(Ordering::SeqCst) >= opts.max_connections.max(1) {
let mut s = stream;
let _ = writeln!(s, "{}", json!({"ok": false, "error": "server busy"}));
log(&format!("{peer}: refused (busy)"));
continue;
}
active.fetch_add(1, Ordering::SeqCst);
let (sh, r, o, l, n) = (shared.clone(), run.clone(), opts.clone(), log.clone(), active.clone());
let spawned = std::thread::Builder::new().name(format!("denoise {peer}")).spawn(move || {
if let Err(e) = handle(stream, &sh.0, tile, &sh.1, &*r, &o, &*l) {
l(&format!("{peer}: {e}"));
}
n.fetch_sub(1, Ordering::SeqCst);
});
if spawned.is_err() {
active.fetch_sub(1, Ordering::SeqCst);
log("could not start a connection thread");
}
}
Ok(())
}
fn handle(
stream: TcpStream,
model: &ModelId,
tile: usize,
device: &str,
run: &dyn Fn(&[f32]) -> Result<Vec<f32>, Error>,
opts: &ServerOptions,
log: &dyn Fn(&str),
) -> std::io::Result<()> {
stream.set_read_timeout(Some(opts.timeout))?;
stream.set_write_timeout(Some(opts.timeout))?;
stream.set_nodelay(true)?;
let peer = stream.peer_addr().map(|a| a.to_string()).unwrap_or_default();
let mut r = BufReader::new(stream.try_clone()?);
let mut w = stream;
let mut tiles = 0usize;
let fail = |w: &mut TcpStream, msg: &str| writeln!(w, "{}", json!({"ok": false, "error": msg}));
while let Some(line) = read_line(&mut r)? {
let Ok(req) = serde_json::from_str::<Value>(&line) else {
fail(&mut w, "malformed request")?;
break;
};
if req.get("v").and_then(Value::as_u64) != Some(PROTOCOL) {
fail(&mut w, &format!("protocol version {PROTOCOL} only"))?;
break;
}
if !same(req.get("token").and_then(Value::as_str).unwrap_or(""), &opts.token) {
fail(&mut w, "bad token")?;
log(&format!("{peer}: bad token"));
break;
}
match req.get("op").and_then(Value::as_str) {
Some("hello") => writeln!(w, "{}", json!({"ok": true, "v": PROTOCOL, "device": device, "model": model, "tile": tile}))?,
Some("run") => {
let (Some(t), Some(bytes)) = (field(&req, "tile"), field(&req, "bytes")) else {
fail(&mut w, "missing tile size or payload size")?;
break;
};
if t != tile {
fail(&mut w, &format!("this server runs {tile}-cell tiles, not {t}"))?;
break;
}
// the payload is read before any refusal below, so the stream stays in step
let input = match read_payload(&mut r, bytes, 4 * t * t) {
Ok(d) => d,
Err(e) => {
fail(&mut w, &e.to_string())?;
break;
}
};
if req.get("model").and_then(Value::as_str) != Some(model.sha256.as_str()) {
fail(&mut w, &format!("model mismatch: this server runs {} {}", model.id, model.version))?;
continue;
}
match run(&input) {
Ok(out) if out.len() == 3 * 4 * t * t && out.iter().all(|v| v.is_finite()) => {
write_payload(&mut w, json!({"ok": true, "tile": t}), &out)?;
tiles += 1;
}
Ok(_) => fail(&mut w, "the model's answer has the wrong size or numbers that are not finite")?,
Err(e) => fail(&mut w, &e.to_string())?,
}
}
_ => {
fail(&mut w, "unknown op")?;
break;
}
}
w.flush()?;
}
if tiles > 0 {
log(&format!("{peer}: {tiles} tiles"));
}
Ok(())
}
/// What a server said in its greeting.
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct ServerInfo {
pub address: String,
pub device: String,
pub model: ModelId,
pub tile: usize,
}
/// Tiles the server may get wrong (an error, a broken connection) before it stops being used.
pub const MAX_FAILURES: usize = 3;
/// A server as a [`TileRunner`], for one model: keeps up to `slots` connections open and gives up on the server after
/// [`MAX_FAILURES`] failed tiles (the caller runs those tiles itself).
pub struct Remote {
address: String,
token: String,
model_sha256: String,
tile: usize,
slots: usize,
timeout: Duration,
busy: AtomicUsize,
failures: AtomicUsize,
last_error: Mutex<Option<String>>,
pool: Mutex<Vec<(BufReader<TcpStream>, TcpStream)>>,
}
impl Remote {
/// `address` as `host[:port]` (default port [`DEFAULT_PORT`]; `tcp://` and a trailing `/` are ignored).
pub fn new(address: &str, token: &str, model_sha256: &str, tile: usize) -> Remote {
let a = address.trim().trim_start_matches("tcp://").trim_end_matches('/');
let has_port = a.rsplit_once(':').is_some_and(|(h, p)| !h.is_empty() && !h.ends_with(':') && p.parse::<u16>().is_ok()) && !a.ends_with(']');
let address = if has_port { a.to_string() } else { format!("{a}:{DEFAULT_PORT}") };
Remote {
address,
token: token.into(),
model_sha256: model_sha256.to_ascii_lowercase(),
tile,
slots: 4,
timeout: Duration::from_secs(60),
busy: AtomicUsize::new(0),
failures: AtomicUsize::new(0),
last_error: Mutex::new(None),
pool: Mutex::new(Vec::new()),
}
}
pub fn with_slots(mut self, n: usize) -> Remote {
self.slots = n.clamp(1, 16);
self
}
pub fn address(&self) -> &str {
&self.address
}
/// Tiles it can take at once.
pub fn slots(&self) -> usize {
self.slots
}
/// Still used (fewer than [`MAX_FAILURES`] failed tiles).
pub fn healthy(&self) -> bool {
self.failures.load(Ordering::Relaxed) < MAX_FAILURES
}
pub fn failures(&self) -> usize {
self.failures.load(Ordering::Relaxed)
}
/// Why the last tile failed.
pub fn last_error(&self) -> Option<String> {
self.last_error.lock().unwrap_or_else(PoisonError::into_inner).clone()
}
fn err(&self, reason: impl std::fmt::Display) -> Error {
Error::Runtime(format!("remote {}: {reason}", self.address))
}
fn connect(&self) -> Result<(BufReader<TcpStream>, TcpStream), Error> {
let addr = self.address.to_socket_addrs().map_err(|e| self.err(e))?.next().ok_or_else(|| self.err("the address doesn't resolve"))?;
let s = TcpStream::connect_timeout(&addr, Duration::from_secs(5)).map_err(|e| self.err(e))?;
s.set_read_timeout(Some(self.timeout)).map_err(|e| self.err(e))?;
s.set_write_timeout(Some(self.timeout)).map_err(|e| self.err(e))?;
s.set_nodelay(true).map_err(|e| self.err(e))?;
let r = BufReader::new(s.try_clone().map_err(|e| self.err(e))?);
Ok((r, s))
}
fn reply(&self, r: &mut BufReader<TcpStream>) -> Result<Value, Error> {
let line = read_line(r).map_err(|e| self.err(e))?.ok_or_else(|| self.err("the connection closed"))?;
let v: Value = serde_json::from_str(&line).map_err(|_| self.err("malformed reply"))?;
if v.get("ok").and_then(Value::as_bool) != Some(true) {
return Err(self.err(v.get("error").and_then(Value::as_str).unwrap_or("request failed")));
}
Ok(v)
}
/// Greet the server: its device and model. Fails when it runs another model or tile size than this one.
pub fn hello(&self) -> Result<ServerInfo, Error> {
let (mut r, mut s) = self.connect()?;
writeln!(s, "{}", json!({"v": PROTOCOL, "token": self.token, "op": "hello"})).map_err(|e| self.err(e))?;
let v = self.reply(&mut r)?;
let m = v.get("model").cloned().unwrap_or_default();
let text = |o: &Value, k: &str| o.get(k).and_then(Value::as_str).unwrap_or("").to_string();
let info = ServerInfo {
address: self.address.clone(),
device: text(&v, "device"),
model: ModelId { id: text(&m, "id"), version: text(&m, "version"), sha256: text(&m, "sha256") },
tile: field(&v, "tile").unwrap_or(0),
};
if !info.model.sha256.eq_ignore_ascii_case(&self.model_sha256) {
return Err(self.err(format!("it runs another model ({} {})", info.model.id, info.model.version)));
}
if info.tile != self.tile {
return Err(self.err(format!("it runs {}-cell tiles, this model {}", info.tile, self.tile)));
}
self.pool.lock().unwrap_or_else(PoisonError::into_inner).push((r, s));
Ok(info)
}
/// Run a tile if a slot is free right now; `None` when all are busy or the server was given up (the caller runs
/// the tile elsewhere). A failure counts towards giving up on the server.
pub fn try_run(&self, input: &[f32]) -> Option<Result<Vec<f32>, Error>> {
if !self.healthy() {
return None;
}
if self.busy.fetch_add(1, Ordering::SeqCst) >= self.slots {
self.busy.fetch_sub(1, Ordering::SeqCst);
return None;
}
let r = self.run_now(input);
self.busy.fetch_sub(1, Ordering::SeqCst);
if let Err(e) = &r {
self.failures.fetch_add(1, Ordering::Relaxed);
*self.last_error.lock().unwrap_or_else(PoisonError::into_inner) = Some(e.to_string());
}
Some(r)
}
fn run_now(&self, input: &[f32]) -> Result<Vec<f32>, Error> {
let t = self.tile;
if input.len() != 4 * t * t {
return Err(Error::Input(format!("a tile of {} samples for {t}-cell tiles", input.len())));
}
let pooled = self.pool.lock().unwrap_or_else(PoisonError::into_inner).pop();
let (mut r, mut s) = match pooled {
Some(c) => c,
None => self.connect()?,
};
let req = json!({"v": PROTOCOL, "token": self.token, "op": "run", "model": self.model_sha256, "tile": t});
write_payload(&mut s, req, input).map_err(|e| self.err(e))?;
s.flush().map_err(|e| self.err(e))?;
let v = self.reply(&mut r)?;
if field(&v, "tile") != Some(t) {
return Err(self.err("the answer is for another tile size"));
}
let bytes = field(&v, "bytes").ok_or_else(|| self.err("an answer without its size"))?;
let out = read_payload(&mut r, bytes, 3 * 4 * t * t).map_err(|e| self.err(e))?;
self.pool.lock().unwrap_or_else(PoisonError::into_inner).push((r, s));
Ok(out)
}
}
/// Tiles on the server when one of its slots is free, else (and for any tile it fails) on `local`: the server adds
/// speed to this computer's runner instead of replacing it.
pub struct Shared<'a> {
pub remote: &'a Remote,
pub local: &'a dyn TileRunner,
}
impl TileRunner for Shared<'_> {
fn run(&self, input: &[f32]) -> Result<Vec<f32>, Error> {
match self.remote.try_run(input) {
Some(Ok(out)) => Ok(out),
Some(Err(_)) | None => self.local.run(input),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOKEN: &str = "0123456789abcdef-test";
const T: usize = 8;
/// A stand-in model: each output pixel is its cell's first plane, times `k`.
fn model(k: f32) -> RunTile {
Arc::new(move |input: &[f32]| {
let t = T;
let mut out = vec![0f32; 3 * 4 * t * t];
for c in 0..3 {
for y in 0..2 * t {
for x in 0..2 * t {
out[(c * 2 * t + y) * 2 * t + x] = input[(y / 2) * t + x / 2] * k;
}
}
}
Ok(out)
})
}
fn id(sha: &str) -> ModelId {
ModelId { id: "test".into(), version: "1".into(), sha256: sha.into() }
}
fn start(sha: &str, run: RunTile) -> String {
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = l.local_addr().unwrap().to_string();
let opts = ServerOptions { token: TOKEN.into(), ..Default::default() };
let m = id(sha);
std::thread::spawn(move || serve(l, m, T, "CPU (test)".into(), run, opts, Arc::new(|_| {})));
addr
}
struct Local(RunTile, AtomicUsize);
impl TileRunner for Local {
fn run(&self, input: &[f32]) -> Result<Vec<f32>, Error> {
self.1.fetch_add(1, Ordering::SeqCst);
(self.0)(input)
}
}
fn tile_input(seed: f32) -> Vec<f32> {
(0..4 * T * T).map(|i| (i as f32 * 0.37 + seed).sin() * 0.5 + 0.5).collect()
}
#[test]
fn remote_tiles_match_local_ones_within_half_float_rounding() {
let addr = start("aa11", model(2.0));
let remote = Remote::new(&addr, TOKEN, "AA11", T);
let info = remote.hello().unwrap();
assert_eq!((info.device.as_str(), info.tile), ("CPU (test)", T));
let input = tile_input(0.3);
let got = remote.try_run(&input).unwrap().unwrap();
let want = model(2.0)(&input).unwrap();
assert!(got.iter().zip(&want).all(|(a, b)| (a - b).abs() < 2e-3));
}
#[test]
fn shared_runner_falls_back_and_gives_up_on_a_failing_server() {
let local = Local(model(2.0), AtomicUsize::new(0));
// nobody listening: every tile runs locally, and after a few the server is given up
let dead = Remote::new("127.0.0.1:1", TOKEN, "aa11", T);
let shared = Shared { remote: &dead, local: &local };
for i in 0..6 {
assert!(shared.run(&tile_input(i as f32)).is_ok());
}
assert_eq!(local.1.load(Ordering::SeqCst), 6);
assert!(!dead.healthy() && dead.last_error().is_some());
// a server with another model is refused at the greeting and per tile
let addr = start("bb22", model(2.0));
let other = Remote::new(&addr, TOKEN, "aa11", T);
assert!(other.hello().unwrap_err().to_string().contains("another model"));
assert!(other.try_run(&tile_input(0.0)).unwrap().unwrap_err().to_string().contains("model mismatch"));
// a wrong token
let addr = start("aa11", model(2.0));
assert!(Remote::new(&addr, "wrong-token-0000000", "aa11", T).hello().unwrap_err().to_string().contains("bad token"));
// all slots busy: the tile runs locally without waiting
let busy = Remote::new(&addr, TOKEN, "aa11", T).with_slots(1);
busy.busy.store(1, Ordering::SeqCst);
assert!(busy.try_run(&tile_input(0.0)).is_none());
}
#[test]
fn hostile_requests_are_refused_and_the_server_keeps_working() {
let addr = start("aa11", model(1.0));
let talk = |bytes: &[u8]| -> String {
let mut s = TcpStream::connect(&addr).unwrap();
s.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
s.write_all(bytes).unwrap();
let mut line = String::new();
let _ = BufReader::new(s).read_line(&mut line);
line
};
assert!(talk(b"not json\n").contains("malformed"));
let v2 = format!("{}\n", json!({"v": 2, "token": TOKEN, "op": "hello"}));
assert!(talk(v2.as_bytes()).contains("protocol"));
let big = format!("{}\n", json!({"v": 1, "token": TOKEN, "op": "run", "model": "aa11", "tile": 4096, "bytes": 10}));
assert!(talk(big.as_bytes()).contains("8-cell"));
// a payload that inflates past its tile
let bomb = miniz_oxide::deflate::compress_to_vec(&vec![0u8; 1 << 20], 9);
let hdr = format!("{}\n", json!({"v": 1, "token": TOKEN, "op": "run", "model": "aa11", "tile": T, "bytes": bomb.len()}));
assert!(talk(&[hdr.as_bytes(), &bomb].concat()).contains("false"));
assert!(Remote::new(&addr, TOKEN, "aa11", T).hello().is_ok());
assert!(same("abc", "abc") && !same("abc", "abd") && !same("abc", "abcd"));
// a short token is refused when the server starts
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let opts = ServerOptions { token: "short".into(), ..Default::default() };
assert!(serve(l, id("aa11"), T, "x".into(), model(1.0), opts, Arc::new(|_| {})).is_err());
}
#[test]
fn packing_round_trips_and_addresses_are_normalised() {
let v: Vec<f32> = (0..4096).map(|i| 0.5 + ((i * 7919 % 101) as f32 - 50.0) * 0.002).collect();
let back = unpack(&pack(&v), v.len()).unwrap();
assert!(v.iter().zip(&back).all(|(a, b)| (a - b).abs() < 1e-3));
assert!(unpack(&pack(&v), v.len() + 1).is_err());
assert_eq!(Remote::new("tcp://100.1.2.3/", TOKEN, "a", T).address(), "100.1.2.3:7990");
assert_eq!(Remote::new("box:8000", TOKEN, "a", T).address(), "box:8000");
assert_eq!(Remote::new("[::1]", TOKEN, "a", T).address(), "[::1]:7990");
}
}