nitai/projects
noxstrap / src / crates / nox-roblox / tests / activity.rs
125 lines · 4.8 KB Raw
1use nox_roblox::activity::{parse_ids, parse_joining, parse_server_id, Event, Watcher};
2
3const JOIN: &str = "2026-08-06T18:20:11.123Z,0.123,abcd,6 [FLog::Output] ! Joining game '9f1e2d3c-4b5a-6789-0abc-def012345678' place 920587237 at 128.116.0.42";
4const LOAD: &str = "2026-08-06T18:20:12.000Z,0.2,abcd,6 [FLog::GameJoinLoadTime] Report game_join_loadtime: placeid:920587237, universeid:245662583, userid:1421948, join_time:2.1";
5const SERVER: &str = "2026-08-06T18:20:13.000Z,0.3,abcd,6 [FLog::Network] serverId: 128.116.0.42|53640";
6const LEFT: &str = "2026-08-06T18:25:00.000Z,0.9,abcd,6 [FLog::Network] Time to disconnect replication data: 0.35";
7
8#[test]
9fn parses_the_join_line() {
10 let (job, place, addr) = parse_joining(JOIN).expect("join line");
11 assert_eq!(job, "9f1e2d3c-4b5a-6789-0abc-def012345678");
12 assert_eq!(place, 920587237);
13 assert_eq!(addr, "128.116.0.42");
14}
15
16#[test]
17fn parses_server_and_ids() {
18 assert_eq!(parse_server_id(SERVER).as_deref(), Some("128.116.0.42"));
19 let (universe, user) = parse_ids(LOAD);
20 assert_eq!(universe, Some(245662583));
21 assert_eq!(user, Some(1421948));
22}
23
24#[test]
25fn ignores_lines_that_do_not_match() {
26 assert!(parse_joining("[FLog::Output] something else entirely").is_none());
27 assert!(parse_joining("! Joining game 'unterminated place 1 at 2").is_none());
28 assert!(parse_server_id("serverId: |123").is_none());
29 assert!(parse_server_id("[FLog::Network] nothing here").is_none());
30 assert_eq!(parse_ids("no ids at all"), (None, None));
31}
32
33#[test]
34fn tracks_a_whole_session_through_the_state_machine() {
35 let mut w = Watcher::new(std::env::temp_dir().join("nox-no-such-dir"));
36 let mut ev = Vec::new();
37 w.feed(JOIN, &mut ev);
38 assert!(matches!(ev[0], Event::Joining(_)));
39 assert!(!w.in_game(), "joining is not yet joined");
40
41 w.feed(LOAD, &mut ev);
42 w.feed(SERVER, &mut ev);
43 assert!(w.in_game(), "the server line confirms the join");
44 let a = w.current().expect("activity");
45 assert_eq!(a.place_id, 920587237);
46 assert_eq!(a.universe_id, 245662583);
47 assert_eq!(a.server, "128.116.0.42");
48 assert!(a.confirmed);
49
50 // A second server line must not fire a duplicate join.
51 let before = ev.len();
52 w.feed(SERVER, &mut ev);
53 assert_eq!(ev.len(), before, "already in game, no repeat event");
54
55 w.feed(LEFT, &mut ev);
56 assert!(!w.in_game());
57 assert!(w.current().is_none());
58 assert!(matches!(ev.last().unwrap(), Event::Left));
59
60 // Leaving twice is not an event either.
61 let before = ev.len();
62 w.feed(LEFT, &mut ev);
63 assert_eq!(ev.len(), before);
64}
65
66#[test]
67fn tails_a_growing_log_file() {
68 let dir = std::env::temp_dir().join("nox-activity-logs");
69 let _ = std::fs::remove_dir_all(&dir);
70 std::fs::create_dir_all(&dir).unwrap();
71 let log = dir.join("0.2026-08-06T18-20-00Z_Player_abcdef_last.log");
72 std::fs::write(&log, format!("{JOIN}\n")).unwrap();
73
74 let mut w = Watcher::new(&dir);
75 let ev = w.poll();
76 assert_eq!(ev.len(), 1, "the first poll reads what is already there");
77 assert!(matches!(ev[0], Event::Joining(_)));
78
79 // Nothing new appended: no events, and no re-reading of old lines.
80 assert!(w.poll().is_empty());
81
82 // Append the rest of the join, including a line split across two polls.
83 let mut f = std::fs::OpenOptions::new().append(true).open(&log).unwrap();
84 use std::io::Write;
85 write!(f, "{LOAD}\n{}", &SERVER[..40]).unwrap();
86 f.flush().unwrap();
87 let ev = w.poll();
88 assert!(ev.is_empty(), "a partial line must not be parsed yet");
89 write!(f, "{}\n", &SERVER[40..]).unwrap();
90 f.flush().unwrap();
91 let ev = w.poll();
92 assert_eq!(ev.len(), 1, "the completed line now confirms the join");
93 assert!(w.in_game());
94 assert_eq!(w.current().unwrap().universe_id, 245662583);
95}
96
97#[test]
98fn switches_to_a_newer_log_file() {
99 let dir = std::env::temp_dir().join("nox-activity-logs2");
100 let _ = std::fs::remove_dir_all(&dir);
101 std::fs::create_dir_all(&dir).unwrap();
102 let old = dir.join("0.2026-08-06T10-00-00Z_Player_old_last.log");
103 std::fs::write(&old, format!("{JOIN}\n{SERVER}\n")).unwrap();
104 let mut w = Watcher::new(&dir);
105 assert_eq!(w.poll().len(), 2);
106 assert!(w.in_game());
107
108 // A new session writes a new file; it must be picked up and read from zero.
109 std::thread::sleep(std::time::Duration::from_millis(1100));
110 let new = dir.join("0.2026-08-06T11-00-00Z_Player_new_last.log");
111 std::fs::write(&new, format!("{JOIN}\n")).unwrap();
112 let ev = w.poll();
113 assert!(
114 ev.iter().any(|e| matches!(e, Event::Joining(_))),
115 "the newer log should be picked up: {ev:?}"
116 );
117 assert_eq!(w.log_file().unwrap(), new);
118}
119
120#[test]
121fn a_missing_log_directory_is_harmless() {
122 let mut w = Watcher::new("/definitely/not/a/directory");
123 assert!(w.poll().is_empty());
124 assert!(!w.in_game());
125}