mirror of
https://github.com/Corfirean/coa-server-manager.git
synced 2026-11-12 04:01:01 +00:00
Phase 9.1: regression tests for the checkpoint after a Manager restart
An immediate checkpoint after a Host restart is kept; with nothing to report it makes no canonical revision, a real change makes exactly one. Fake-realm test and a live test on a real worldserver (restart, field-by-field and hash comparison, professions as the change). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5abba832e5
commit
d41a7c7494
4 files changed
+135
No files matched your search
@@ -0,0 +1,25 @@
|
||||
//! Field-by-field difference of two characters, for the messages of test failures.
|
||||
|
||||
pub(crate) fn diff(a: &crate::portable::model::PortableCharacter, b: &crate::portable::model::PortableCharacter) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
walk("", &serde_json::to_value(a).unwrap(), &serde_json::to_value(b).unwrap(), &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
fn walk(path: &str, a: &serde_json::Value, b: &serde_json::Value, out: &mut Vec<String>) {
|
||||
use serde_json::Value::{Array, Null, Object};
|
||||
match (a, b) {
|
||||
(Object(x), Object(y)) => {
|
||||
for k in x.keys().chain(y.keys().filter(|k| !x.contains_key(*k))) {
|
||||
walk(&format!("{path}/{k}"), x.get(k).unwrap_or(&Null), y.get(k).unwrap_or(&Null), out);
|
||||
}
|
||||
}
|
||||
(Array(x), Array(y)) if x.len() == y.len() => {
|
||||
for (i, (p, q)) in x.iter().zip(y).enumerate() {
|
||||
walk(&format!("{path}[{i}]"), p, q, out);
|
||||
}
|
||||
}
|
||||
_ if a != b => out.push(format!("{path}: {} -> {}", a.to_string().chars().take(160).collect::<String>(), b.to_string().chars().take(160).collect::<String>())),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -190,6 +190,71 @@ fn a_character_made_portable_on_a_running_realm_is_armed_on_it_without_a_restart
|
||||
drop(server);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn a_manager_restart_with_nothing_to_report_makes_no_revision_and_a_real_change_makes_exactly_one() {
|
||||
let (Some(r), Some(sp), Some(jobs)) = (realms(), spec(), job_dir()) else { return };
|
||||
reset_b(&r.b);
|
||||
sql(&r.b, "DELETE FROM acore_characters.mail_items WHERE receiver > 3010; DELETE FROM acore_characters.mail WHERE receiver > 3010;");
|
||||
std::fs::create_dir_all(&jobs).unwrap();
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = descriptor(dir.path());
|
||||
let quick = crate::portable::session::HostConfig { checkpoint_interval_secs: 4, collection_interval_secs: 300 };
|
||||
let open = |dir: &Path| {
|
||||
let mut svc = PortableService::open(&dir.join("portable"), vec![]).unwrap();
|
||||
svc.set_host_config(quick.clone());
|
||||
svc
|
||||
};
|
||||
let mut svc = open(dir.path());
|
||||
svc.add_prepared_realm(&file).unwrap();
|
||||
let profile = svc.owner_profile();
|
||||
let id = crate::portable::realm::live_projection::fixture_character(&r, svc.owner_mut(), profile);
|
||||
let id_text = id.to_string();
|
||||
let server = Server::start(sp);
|
||||
svc.tick();
|
||||
svc.tick();
|
||||
sql(&r.b, "REPLACE INTO acore_auth.realmcharacters (realmid, acctid, numchars) VALUES (1, 7001, 0)");
|
||||
let account = name_of_account(&r.b, 7001);
|
||||
svc.play(&id_text, REALM, Some(&account)).unwrap();
|
||||
let guid = svc.host().server_mappings(id).unwrap().into_iter().find(|m| m.server_id == REALM).unwrap().local_guid;
|
||||
server.ra().run(&format!("botcmd spawnbot {guid}")).unwrap();
|
||||
pump(&mut svc, 90, "playing", |s| status(s) == PlayStatus::Playing);
|
||||
server.ra().run(&format!("botcmd suspend {guid}")).unwrap();
|
||||
|
||||
let acked = |s: &PortableService| s.host().host_live_sessions(REALM).unwrap().first().map(|x| x.acked_sequence).unwrap_or(0);
|
||||
let revision = |s: &PortableService| s.owner().character(id).unwrap().revision;
|
||||
|
||||
// whatever the realm does by itself at the first login arrives with the first checkpoints; then the character is quiet
|
||||
pump(&mut svc, 90, "the first checkpoints", |s| acked(s) >= 1);
|
||||
let from = acked(&svc);
|
||||
pump(&mut svc, 90, "two quiet checkpoints", |s| acked(s) >= from + 2);
|
||||
let settled = svc.owner().load_current(id).unwrap();
|
||||
let n = revision(&svc);
|
||||
|
||||
// the Manager restarts; the new Host has no memory of its last checkpoint and checkpoints at once
|
||||
drop(svc);
|
||||
let mut svc = open(dir.path());
|
||||
let before = acked(&svc);
|
||||
pump(&mut svc, 60, "the Host to be recognised again", |s| status(s) == PlayStatus::Playing);
|
||||
pump(&mut svc, 60, "the immediate checkpoint after the restart", |s| acked(s) > before);
|
||||
assert_eq!(revision(&svc), n, "a restart with nothing to report is not a new revision");
|
||||
let after = svc.owner().load_current(id).unwrap();
|
||||
assert_eq!(after, settled, "field by field: {:?}", super::delta_probe::diff(&settled, &after));
|
||||
assert_eq!(crate::portable::snapshot::content_hash(&after).unwrap(), crate::portable::snapshot::content_hash(&settled).unwrap(), "by hash");
|
||||
let again = acked(&svc);
|
||||
pump(&mut svc, 60, "another quiet checkpoint", |s| acked(s) > again);
|
||||
assert_eq!(revision(&svc), n);
|
||||
|
||||
// a real change in the game: exactly one revision, however many checkpoints follow
|
||||
server.ra().run(&format!("botcmd professiontrainer {guid}")).unwrap();
|
||||
let mark = acked(&svc);
|
||||
pump(&mut svc, 90, "the checkpoints after the change", |s| acked(s) >= mark + 3);
|
||||
assert_eq!(revision(&svc), n + 1, "{:?}", super::delta_probe::diff(&settled, &svc.owner().load_current(id).unwrap()));
|
||||
let changed = svc.owner().load_current(id).unwrap();
|
||||
assert!(changed.build.skills.len() > settled.build.skills.len() || changed.build.spells.len() > settled.build.spells.len(), "the change is the professions");
|
||||
server.ra().run(&format!("botcmd despawn {guid}")).unwrap();
|
||||
}
|
||||
|
||||
fn name_of_account(db: &crate::db::Db, id: u32) -> String {
|
||||
sql(db, &format!("SELECT username FROM acore_auth.account WHERE id = {id}")).trim().to_string()
|
||||
}
|
||||
|
||||
@@ -9,3 +9,6 @@ pub use view::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod live_service;
|
||||
|
||||
#[cfg(test)]
|
||||
mod delta_probe;
|
||||
@@ -296,6 +296,48 @@ fn the_host_manager_can_restart_between_the_request_and_the_read_and_between_che
|
||||
assert_eq!(o.store().load_current(id).unwrap().progression.money, c0.progression.money + 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_immediate_checkpoint_after_a_host_restart_makes_no_revision_when_the_character_did_not_change() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let host_file = dir.path().join("host.db");
|
||||
let Setup { mut owner, host, mut realm, id, session, .. } = setup_in(Store::open_in_memory().unwrap(), Store::open_file(&host_file).unwrap());
|
||||
drop(host);
|
||||
let mut o = OwnerService::new(&mut owner);
|
||||
let settled = {
|
||||
let mut hs = Store::open_file(&host_file).unwrap();
|
||||
let mut h = HostService::new(&mut hs, SERVER, HostConfig::default());
|
||||
start(&mut h, &mut o, &mut realm);
|
||||
realm.play(|m| m.progression.money += 7);
|
||||
checkpoint(&mut h, &mut o, &mut realm, 100);
|
||||
assert_eq!(revision(&o, id), 11);
|
||||
checkpoint(&mut h, &mut o, &mut realm, 200);
|
||||
assert_eq!(revision(&o, id), 11, "an unchanged character is not a new revision");
|
||||
o.store().load_current(id).unwrap()
|
||||
};
|
||||
let hash_before = crate::portable::snapshot::content_hash(&settled).unwrap();
|
||||
|
||||
let mut hs = Store::open_file(&host_file).unwrap();
|
||||
let mut h = HostService::new(&mut hs, SERVER, HostConfig::default());
|
||||
h.tick(&mut realm, 210).unwrap();
|
||||
h.tick(&mut realm, 211).unwrap();
|
||||
let acks = deliver(&mut h, &mut o, &mut realm);
|
||||
assert_eq!(acks.len(), 1, "the restarted Host checkpoints at once");
|
||||
assert_eq!(acks[0].outcome, AckOutcome::Applied);
|
||||
assert_eq!(revision(&o, id), 11, "the restart alone changed nothing the player can see");
|
||||
let after = o.store().load_current(id).unwrap();
|
||||
assert_eq!(after, settled, "field by field");
|
||||
assert_eq!(crate::portable::snapshot::content_hash(&after).unwrap(), hash_before, "by hash");
|
||||
assert_eq!(o.session(session).unwrap().unwrap().0, "open");
|
||||
|
||||
realm.play(|m| m.progression.money += 3);
|
||||
h.tick(&mut realm, 300).unwrap();
|
||||
h.tick(&mut realm, 301).unwrap();
|
||||
let acks = deliver(&mut h, &mut o, &mut realm);
|
||||
assert_eq!((acks.len(), &acks[0].outcome), (1, &AckOutcome::Applied));
|
||||
assert_eq!(revision(&o, id), 12, "a real change makes exactly one revision");
|
||||
assert_eq!(o.store().load_current(id).unwrap().progression.money, settled.progression.money + 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_owner_manager_can_restart_at_any_point() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in new issue
Block a user