Plan M4a: gatewayd in 15 tasks, with skeletons and given tests
Each task's tests were run against a reference at its end state; the end states were replayed from master in order with the gate at each step (650 to 762 tests); each skeleton compiles against its tests and fails them. The reference is kept off this machine. Lessons T27 (every wait in a test has a limit) and T28 (mutate the reference before hand-over) come from this work. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
//! The state file: a first start, surviving a restart, its limits, and refusing a damaged file
|
||||
//! instead of guessing (M4a spec, section 9). Do not edit.
|
||||
|
||||
#[path = "support/tmp.rs"]
|
||||
mod tmp;
|
||||
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use gatewayd::state::{InFlight, RECENT_KEPT, State, StateError, THREADS_KEPT};
|
||||
use tmp::TempDir;
|
||||
|
||||
const CHAN: &str = "c0000000000000000000000000";
|
||||
const DM: &str = "d0000000000000000000000000";
|
||||
|
||||
fn id(n: usize) -> String {
|
||||
format!("p{n:025}")
|
||||
}
|
||||
|
||||
fn turn(n: usize) -> InFlight {
|
||||
InFlight {
|
||||
session: format!("mm-{}", id(n)),
|
||||
channel: DM.to_string(),
|
||||
root: id(n),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_first_start_then_a_restart() {
|
||||
let dir = TempDir::new("state-restart");
|
||||
let path = dir.path().join("gateway/state.json");
|
||||
let mut s = State::load(&path).unwrap();
|
||||
assert!(!path.exists(), "loading writes nothing");
|
||||
assert_eq!(
|
||||
(s.since(CHAN), s.channels().len(), s.seen(&id(1))),
|
||||
(None, 0, false)
|
||||
);
|
||||
s.handled(&id(1), CHAN, 2000).unwrap();
|
||||
s.handled(&id(2), CHAN, 1500).unwrap();
|
||||
s.mark(DM, 3000).unwrap();
|
||||
s.mark(DM, 9000).unwrap();
|
||||
s.join_thread(&id(1)).unwrap();
|
||||
s.start_turn(turn(1)).unwrap();
|
||||
s.start_turn(turn(2)).unwrap();
|
||||
s.end_turn(&format!("mm-{}", id(2))).unwrap();
|
||||
|
||||
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
|
||||
let dir_mode = std::fs::metadata(path.parent().unwrap())
|
||||
.unwrap()
|
||||
.permissions()
|
||||
.mode()
|
||||
& 0o777;
|
||||
assert_eq!((mode, dir_mode), (0o600, 0o700));
|
||||
assert!(!dir.path().join("gateway/state.json.tmp").exists());
|
||||
|
||||
let mut again = State::load(&path).unwrap();
|
||||
assert_eq!(again.since(CHAN), Some(2000), "the mark never moves back");
|
||||
assert_eq!(
|
||||
again.since(DM),
|
||||
Some(3000),
|
||||
"mark only sets a channel without one"
|
||||
);
|
||||
assert_eq!(again.channels(), [CHAN, DM]);
|
||||
assert!(again.seen(&id(1)) && again.seen(&id(2)));
|
||||
assert!(again.knows_thread(&id(1)) && !again.knows_thread(&id(2)));
|
||||
assert_eq!(again.take_in_flight().unwrap(), [turn(1)]);
|
||||
assert!(
|
||||
State::load(&path)
|
||||
.unwrap()
|
||||
.take_in_flight()
|
||||
.unwrap()
|
||||
.is_empty(),
|
||||
"taking is saved"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_newest_posts_and_threads_are_kept() {
|
||||
let dir = TempDir::new("state-limits");
|
||||
// A full file to start from: posts 0.. and threads 0.., at their limits.
|
||||
let recent: Vec<String> = (0..RECENT_KEPT).map(id).collect();
|
||||
let threads: Vec<String> = (0..THREADS_KEPT).map(id).collect();
|
||||
let full =
|
||||
serde_json::json!({"channels": {}, "recent": recent, "threads": threads, "in_flight": []});
|
||||
let path = dir.write("state.json", &full.to_string());
|
||||
let mut s = State::load(&path).unwrap();
|
||||
s.handled(&id(RECENT_KEPT), CHAN, 1).unwrap();
|
||||
s.handled(&id(RECENT_KEPT + 1), CHAN, 1).unwrap();
|
||||
s.handled(&id(RECENT_KEPT + 1), CHAN, 1).unwrap();
|
||||
s.join_thread(&id(THREADS_KEPT)).unwrap();
|
||||
s.join_thread(&id(3)).unwrap();
|
||||
let s = State::load(&path).unwrap();
|
||||
assert!(!s.seen(&id(0)) && !s.seen(&id(1)) && s.seen(&id(2)) && s.seen(&id(RECENT_KEPT + 1)));
|
||||
assert!(!s.knows_thread(&id(0)) && s.knows_thread(&id(1)) && s.knows_thread(&id(THREADS_KEPT)));
|
||||
let text = std::fs::read_to_string(&path).unwrap();
|
||||
let v: serde_json::Value = serde_json::from_str(&text).unwrap();
|
||||
assert_eq!(
|
||||
v["recent"].as_array().unwrap().len(),
|
||||
RECENT_KEPT,
|
||||
"a repeat is not stored twice"
|
||||
);
|
||||
assert_eq!(v["threads"].as_array().unwrap().len(), THREADS_KEPT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_damaged_file_stops_with_its_pointer() {
|
||||
let dir = TempDir::new("state-damaged");
|
||||
let good_turn = r#"{"session":"mm-p0000000000000000000000001","channel":"d0000000000000000000000000","root":"p0000000000000000000000001"}"#;
|
||||
let cases = [
|
||||
"".to_string(),
|
||||
"{".to_string(),
|
||||
"[]".to_string(),
|
||||
r#"{"channels":{},"recent":[],"threads":[]}"#.to_string(),
|
||||
r#"{"channels":{},"recent":[],"threads":[],"in_flight":[],"extra":1}"#.to_string(),
|
||||
r#"{"channels":{"../x":1},"recent":[],"threads":[],"in_flight":[]}"#.to_string(),
|
||||
r#"{"channels":{},"recent":["short"],"threads":[],"in_flight":[]}"#.to_string(),
|
||||
r#"{"channels":{},"recent":[],"threads":["P0000000000000000000000000"],"in_flight":[]}"#
|
||||
.to_string(),
|
||||
format!(
|
||||
r#"{{"channels":{{}},"recent":[],"threads":[],"in_flight":[{}]}}"#,
|
||||
good_turn.replace("mm-", "xx-")
|
||||
),
|
||||
format!(
|
||||
r#"{{"channels":{{}},"recent":[],"threads":[],"in_flight":[{}]}}"#,
|
||||
good_turn.replace("\"d0", "\"D0")
|
||||
),
|
||||
];
|
||||
for (n, text) in cases.iter().enumerate() {
|
||||
let path = dir.write(&format!("s{n}.json"), text);
|
||||
match State::load(&path) {
|
||||
Err(e @ StateError::Read(..)) => {
|
||||
let message = e.to_string();
|
||||
assert!(
|
||||
message.starts_with(&path.display().to_string()),
|
||||
"{message}"
|
||||
);
|
||||
assert!(
|
||||
message.ends_with("\nsee docs/runbook.md#gateway-state-damaged"),
|
||||
"{message}"
|
||||
);
|
||||
}
|
||||
Err(e) => panic!("{text}: {e}"),
|
||||
Ok(_) => panic!("accepted: {text}"),
|
||||
}
|
||||
}
|
||||
let ok = dir.write(
|
||||
"ok.json",
|
||||
&format!(
|
||||
r#"{{"channels":{{"{CHAN}":5}},"recent":[],"threads":[],"in_flight":[{good_turn}]}}"#
|
||||
),
|
||||
);
|
||||
assert_eq!(State::load(&ok).unwrap().since(CHAN), Some(5));
|
||||
std::fs::create_dir(dir.path().join("adir")).unwrap();
|
||||
assert!(matches!(
|
||||
State::load(&dir.path().join("adir")),
|
||||
Err(StateError::Read(..))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_write_is_an_error_and_keeps_the_old_file() {
|
||||
let dir = TempDir::new("state-readonly");
|
||||
let sub = dir.path().join("gateway");
|
||||
let path = sub.join("state.json");
|
||||
let mut s = State::load(&path).unwrap();
|
||||
s.handled(&id(1), CHAN, 5).unwrap();
|
||||
let before = std::fs::read(&path).unwrap();
|
||||
std::fs::set_permissions(&sub, std::fs::Permissions::from_mode(0o500)).unwrap();
|
||||
let got = s.handled(&id(2), CHAN, 6);
|
||||
std::fs::set_permissions(&sub, std::fs::Permissions::from_mode(0o700)).unwrap();
|
||||
let e = got.unwrap_err();
|
||||
assert!(matches!(e, StateError::Write(..)), "{e}");
|
||||
assert!(
|
||||
e.to_string()
|
||||
.ends_with("see docs/runbook.md#gateway-state-damaged"),
|
||||
"{e}"
|
||||
);
|
||||
assert_eq!(std::fs::read(&path).unwrap(), before);
|
||||
}
|
||||
Reference in New Issue
Block a user