vercel-labs--zero-native
141 行
4.7 KiB
Zig
141 行
4.7 KiB
Zig
//! Behavioral harness for dual-pomodoro-timer, ts track. The grader copies
|
|
//! this next to the transpiled core.zig, the rt kernel, and cmdview.zig,
|
|
//! then runs `zig test harness.zig`. Asserts the shared behavioral spec:
|
|
//! the timer subscription exists exactly while running, ticks count down
|
|
//! and auto-advance with the completion sound, work-only counting, stale
|
|
//! ticks and re-entry guarded, reset semantics.
|
|
|
|
const std = @import("std");
|
|
const core = @import("core.zig");
|
|
const cmdview = @import("cmdview.zig");
|
|
const rt = core.rt;
|
|
|
|
var g_model: *const core.Model = undefined;
|
|
var g_buf: [4096]u8 = undefined;
|
|
|
|
fn fresh() void {
|
|
rt.resetAll();
|
|
g_model = core.commitModelRoot(core.initialModel());
|
|
rt.frameReset();
|
|
}
|
|
|
|
fn dispatch(msg: core.Msg) []const u8 {
|
|
const r = core.update(g_model, msg);
|
|
g_model = core.commitModelRoot(r.model);
|
|
@memcpy(g_buf[0..r.cmd.len], r.cmd);
|
|
rt.frameReset();
|
|
return g_buf[0..r.cmd.len];
|
|
}
|
|
|
|
/// The declared timer subscription on the current model, or null.
|
|
fn declaredTimer() ?@FieldType(cmdview.SubOp, "timer") {
|
|
const subs = core.subscriptions(g_model);
|
|
const timer = cmdview.findTimer(subs);
|
|
rt.frameReset();
|
|
return timer;
|
|
}
|
|
|
|
fn tick() []const u8 {
|
|
return dispatch(.{ .tick = 0 });
|
|
}
|
|
|
|
/// Run a whole session down to its completing tick; returns that tick's cmd.
|
|
fn runSession(seconds: usize) []const u8 {
|
|
var remaining = seconds;
|
|
while (remaining > 1) : (remaining -= 1) {
|
|
_ = tick();
|
|
}
|
|
return tick();
|
|
}
|
|
|
|
test "starts as a paused full focus session with no timer declared" {
|
|
fresh();
|
|
try std.testing.expect(g_model.phase == .focus);
|
|
try std.testing.expect(g_model.remaining_seconds == 1500);
|
|
try std.testing.expect(!g_model.running);
|
|
try std.testing.expect(g_model.completed_count == 0);
|
|
try std.testing.expect(declaredTimer() == null);
|
|
}
|
|
|
|
test "start declares the 1-second timer; pause and reset take it down" {
|
|
fresh();
|
|
_ = dispatch(.start);
|
|
try std.testing.expect(g_model.running);
|
|
const timer = declaredTimer() orelse return error.NoTimerDeclared;
|
|
try std.testing.expectEqual(@as(f64, 1000), timer.every_ms);
|
|
try std.testing.expectEqual(@intFromEnum(std.meta.Tag(core.Msg).tick), timer.msg_tag);
|
|
|
|
_ = dispatch(.pause);
|
|
try std.testing.expect(!g_model.running);
|
|
try std.testing.expect(declaredTimer() == null);
|
|
|
|
_ = dispatch(.start);
|
|
try std.testing.expect(declaredTimer() != null);
|
|
_ = dispatch(.reset);
|
|
try std.testing.expect(declaredTimer() == null);
|
|
}
|
|
|
|
test "ticks count down while running; stale ticks while paused change nothing" {
|
|
fresh();
|
|
_ = dispatch(.start);
|
|
_ = tick();
|
|
_ = tick();
|
|
try std.testing.expect(g_model.remaining_seconds == 1498);
|
|
|
|
_ = dispatch(.pause);
|
|
_ = tick();
|
|
try std.testing.expect(g_model.remaining_seconds == 1498);
|
|
|
|
_ = dispatch(.start);
|
|
try std.testing.expect(g_model.remaining_seconds == 1498);
|
|
_ = tick();
|
|
try std.testing.expect(g_model.remaining_seconds == 1497);
|
|
}
|
|
|
|
test "start while running changes nothing" {
|
|
fresh();
|
|
_ = dispatch(.start);
|
|
_ = tick();
|
|
_ = dispatch(.start);
|
|
try std.testing.expect(g_model.remaining_seconds == 1499);
|
|
try std.testing.expect(g_model.running);
|
|
}
|
|
|
|
test "a focus session completes into a running rest with the sound and a count" {
|
|
fresh();
|
|
_ = dispatch(.start);
|
|
const completing = runSession(1500);
|
|
try std.testing.expect(g_model.phase == .rest);
|
|
try std.testing.expect(g_model.remaining_seconds == 300);
|
|
try std.testing.expect(g_model.running);
|
|
try std.testing.expect(g_model.completed_count == 1);
|
|
const play = cmdview.findOp(completing, .audio_play) orelse return error.NoCompletionSound;
|
|
try std.testing.expectEqualStrings("assets/ding.wav", play.path);
|
|
// The countdown keeps running: the timer stays declared.
|
|
try std.testing.expect(declaredTimer() != null);
|
|
}
|
|
|
|
test "a rest completes into a running focus session without counting" {
|
|
fresh();
|
|
_ = dispatch(.start);
|
|
_ = runSession(1500);
|
|
const completing = runSession(300);
|
|
try std.testing.expect(g_model.phase == .focus);
|
|
try std.testing.expect(g_model.remaining_seconds == 1500);
|
|
try std.testing.expect(g_model.running);
|
|
try std.testing.expect(g_model.completed_count == 1);
|
|
try std.testing.expect(cmdview.findOp(completing, .audio_play) != null);
|
|
}
|
|
|
|
test "reset returns to a paused full focus session and keeps the count" {
|
|
fresh();
|
|
_ = dispatch(.start);
|
|
_ = runSession(1500);
|
|
_ = tick();
|
|
_ = dispatch(.reset);
|
|
try std.testing.expect(g_model.phase == .focus);
|
|
try std.testing.expect(g_model.remaining_seconds == 1500);
|
|
try std.testing.expect(!g_model.running);
|
|
try std.testing.expect(g_model.completed_count == 1);
|
|
}
|