# telemetry_test.ludic - events queue as lines, a batch goes when due and not before, a failure # waits longer, off forgets, a test run never sends, and the queue survives a restart import "ludic.telemetry" import "ludic.base" program TelemetryTest { numbers float state TelemetryTestState { clock: int = 0 on: bool = true allowed: bool = true answer: int = -1 refuse: bool = false sends: int = 0 last_url: string = "" last_body: string = "" } function fake_now(telemetry_test_st: TelemetryTestState) -> int { return telemetry_test_st.clock } function fake_enabled(telemetry_test_st: TelemetryTestState) -> bool { return telemetry_test_st.on } function fake_can(telemetry_test_st: TelemetryTestState) -> bool { return telemetry_test_st.allowed } function fake_clock() -> int { return 1790337600 } function fake_send(telemetry_test_st: mut TelemetryTestState, url: string, body: []byte, n: int) -> int { if telemetry_test_st.refuse { return -1 } telemetry_test_st.sends += 1 telemetry_test_st.last_url = url telemetry_test_st.last_body = text_of(body, n) return 7 } function fake_poll(telemetry_test_st: TelemetryTestState, h: int) -> int { return telemetry_test_st.answer } function fake_drop(h: int) -> void { } bind TelemetryWorld { can_send: fn fake_can, enabled: fn fake_enabled, now_ms: fn fake_now, clock_s: fn fake_clock } bind TelemetryTransport { send: fn fake_send, poll: fn fake_poll, drop: fn fake_drop } function qfile() -> string { return Os.temp_dir() + "/ludic-telemetry-test-queue.txt" } function idfile() -> string { return Os.temp_dir() + "/ludic-telemetry-test-id.json" } function fresh(telemetry_st: mut TelemetryState, telemetry_test_st: mut TelemetryTestState) -> void { telemetry_test_st.clock = 0 telemetry_test_st.on = true telemetry_test_st.allowed = true telemetry_test_st.answer = -1 telemetry_test_st.refuse = false telemetry_test_st.sends = 0 telemetry_test_st.last_url = "" telemetry_test_st.last_body = "" telemetry_reset(telemetry_st) if Fs.exists(qfile()) { Fs.remove(qfile()) } Fs.write_text(idfile(), "{\"id\":\"p-1\",\"notices\":2}") let c = new TelemetryConfig c.host = "http://fake" c.key = "k-1" c.lib = "test-game" c.queue_file = qfile() c.id_file = idfile() c.flush_ms = 1000 c.flush_at = 5 c.batch = 3 c.queue_max = 8 c.backoff_max = 2 c.save_ms = 100000 telemetry_config(telemetry_st, c) telemetry_start(telemetry_st) telemetry_tick(telemetry_st) } function events(telemetry_st: mut TelemetryState, n: int) -> void { for i in 0 .. n { telemetry_event(telemetry_st, `e{i}`, null) } } function at(telemetry_st: mut TelemetryState, telemetry_test_st: mut TelemetryTestState, ms: int) -> void { telemetry_test_st.clock = ms telemetry_tick(telemetry_st) } function facts_of(telemetry_st: TelemetryState, what: int) -> int { let fs = q_drain(telemetry_facts(telemetry_st)) var n = 0 for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } return n } test "an event is one line with the player as a mark, and the batch puts the id in" (telemetry_st: mut TelemetryState, telemetry_test_st: mut TelemetryTestState) { fresh(telemetry_st, telemetry_test_st) expect(telemetry_id(telemetry_st) == "p-1") telemetry_event(telemetry_st, "hello", null) expect_eq(telemetry_queued(telemetry_st), 1) let line = telemetry_line(telemetry_st, 0) expect(Text.contains(line, "\"event\":\"hello\"")) expect(Text.contains(line, "test-game")) expect(not Text.contains(line, "p-1")) let body = telemetry_batch_body(telemetry_st, 1) expect(Text.contains(body, "\"api_key\":\"k-1\"")) expect(Text.contains(body, "\"distinct_id\":\"p-1\"")) } test "a batch goes when due, and only what the server took leaves the queue" (telemetry_st: mut TelemetryState, telemetry_test_st: mut TelemetryTestState) { fresh(telemetry_st, telemetry_test_st) events(telemetry_st, 4) at(telemetry_st, telemetry_test_st, 500) expect_eq(telemetry_test_st.sends, 0) at(telemetry_st, telemetry_test_st, 1000) expect_eq(telemetry_test_st.sends, 1) expect(telemetry_test_st.last_url == "http://fake/batch/") expect(telemetry_in_flight(telemetry_st)) events(telemetry_st, 1) telemetry_test_st.answer = 1 at(telemetry_st, telemetry_test_st, 1100) expect_eq(telemetry_queued(telemetry_st), 2) expect_eq(facts_of(telemetry_st, TELEMETRY_SENT), 1) } test "enough events go early" (telemetry_st: mut TelemetryState, telemetry_test_st: mut TelemetryTestState) { fresh(telemetry_st, telemetry_test_st) events(telemetry_st, 5) at(telemetry_st, telemetry_test_st, 10) expect_eq(telemetry_test_st.sends, 1) } test "a failure keeps the lines and waits twice as long, up to the cap" (telemetry_st: mut TelemetryState, telemetry_test_st: mut TelemetryTestState) { fresh(telemetry_st, telemetry_test_st) events(telemetry_st, 2) telemetry_test_st.answer = 0 at(telemetry_st, telemetry_test_st, 1000) at(telemetry_st, telemetry_test_st, 1001) expect_eq(telemetry_queued(telemetry_st), 2) expect_eq(telemetry_fails(telemetry_st), 1) expect_eq(telemetry_next_ms(telemetry_st), 1001 + 2000) expect_eq(facts_of(telemetry_st, TELEMETRY_FAILED), 1) at(telemetry_st, telemetry_test_st, 3001) at(telemetry_st, telemetry_test_st, 3002) at(telemetry_st, telemetry_test_st, 7002) at(telemetry_st, telemetry_test_st, 7003) expect_eq(telemetry_fails(telemetry_st), 2) expect_eq(telemetry_next_ms(telemetry_st), 7003 + 4000) } test "a request that cannot start backs off too" (telemetry_st: mut TelemetryState, telemetry_test_st: mut TelemetryTestState) { fresh(telemetry_st, telemetry_test_st) telemetry_test_st.refuse = true events(telemetry_st, 1) at(telemetry_st, telemetry_test_st, 1000) expect_eq(telemetry_fails(telemetry_st), 1) expect(not telemetry_in_flight(telemetry_st)) } test "off keeps nothing and deletes the file" (telemetry_st: mut TelemetryState, telemetry_test_st: mut TelemetryTestState) { fresh(telemetry_st, telemetry_test_st) events(telemetry_st, 3) telemetry_save(telemetry_st) expect(Fs.exists(qfile())) telemetry_test_st.on = false at(telemetry_st, telemetry_test_st, 10) expect_eq(telemetry_queued(telemetry_st), 0) expect(not Fs.exists(qfile())) events(telemetry_st, 3) expect_eq(telemetry_queued(telemetry_st), 0) } test "a run that may not send queues nothing and sends nothing" (telemetry_st: mut TelemetryState, telemetry_test_st: mut TelemetryTestState) { fresh(telemetry_st, telemetry_test_st) telemetry_test_st.allowed = false events(telemetry_st, 9) at(telemetry_st, telemetry_test_st, 100000) expect_eq(telemetry_queued(telemetry_st), 0) expect_eq(telemetry_test_st.sends, 0) } test "the queue is capped at its newest events" (telemetry_st: mut TelemetryState, telemetry_test_st: mut TelemetryTestState) { fresh(telemetry_st, telemetry_test_st) telemetry_test_st.allowed = true events(telemetry_st, 12) expect(telemetry_queued(telemetry_st) <= 11) expect(Text.contains(telemetry_line(telemetry_st, telemetry_queued(telemetry_st) - 1), "e11")) } test "what was not sent is there at the next start" (telemetry_st: mut TelemetryState, telemetry_test_st: mut TelemetryTestState) { fresh(telemetry_st, telemetry_test_st) events(telemetry_st, 3) telemetry_save(telemetry_st) telemetry_reset(telemetry_st) telemetry_start(telemetry_st) expect_eq(telemetry_queued(telemetry_st), 3) } test "an id set outright is written beside the file's other keys" (telemetry_st: mut TelemetryState, telemetry_test_st: mut TelemetryTestState) { fresh(telemetry_st, telemetry_test_st) telemetry_set_id(telemetry_st, "p-2") let v = Json.parse(Fs.read_text(idfile())) expect(sv_str(v, "id", "") == "p-2") expect_eq(sv_int(v, "notices", 0), 2) } test "no salt and no id file makes a random id" (telemetry_st: mut TelemetryState, telemetry_test_st: mut TelemetryTestState) { fresh(telemetry_st, telemetry_test_st) telemetry_reset(telemetry_st) Fs.remove(idfile()) telemetry_start(telemetry_st) expect(Text.starts_with(telemetry_id(telemetry_st), "r-")) expect_eq(len(telemetry_session(telemetry_st)), 32) } }