Changes
11 changed files (+442/-0)
-
-
@@ -85,4 +85,47 @@ // Default install step{ b.installArtifact(exe); } // "zig build test" { var valac = try CompileVala.init(b, .{ .src = b.path("src"), .includeTestFiles = true, .includeMainVala = false, }); for (system_libraries) |lib| { valac.addPackage(lib); } valac.addPackage("posix"); valac.addPackage("Config"); valac.addVapi(b.path("src/Config.vapi")); const test_exe = b.addExecutable(.{ .name = "test", .root_module = b.createModule(.{ .link_libc = true, .target = target, .optimize = optimize, }), }); test_exe.root_module.addCMacro("APP_VERSION", b.fmt("\"{s}-test\"", .{version})); test_exe.root_module.addCMacro("APP_ID", b.fmt("\"{s}.test\"", .{app_id})); for (system_libraries) |lib| { test_exe.linkSystemLibrary(lib); } for (valac.compiled_c_files) |file| { test_exe.addCSourceFile(.{ .file = file }); } const run = b.addRunArtifact(test_exe); const step = b.step("test", "Run unit tests"); step.dependOn(&run.step); } }
-
-
-
@@ -21,6 +21,7 @@ /// This must be an existing path.src: std.Build.LazyPath, includeTestFiles: bool = false, includeMainVala: bool = true, suppressDeprecationWarning: bool = false, };
-
@@ -40,6 +41,10 @@while (try walker.next()) |entry| { const ext = std.fs.path.extension(entry.basename); if (std.mem.eql(u8, ".vala", ext)) { if (!opts.includeMainVala and std.mem.eql(u8, entry.basename, "main.vala")) { continue; } if (!opts.includeTestFiles and std.mem.endsWith(u8, entry.basename, ".test.vala")) { continue; }
-
-
src/Event/Event.vala (new)
-
@@ -0,0 +1,24 @@// Copyright 2025 Shota FUJI // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // // SPDX-License-Identifier: MPL-2.0 using GLib; namespace TimeTracker.Event { private static string new_id() { return Uuid.string_random(); } public abstract class Event : Object { public string event_id { get; construct; } public int64 created_at { get; construct; } public bool eq(Event a, Event b) { return a.event_id == b.event_id; } } }
-
-
-
@@ -0,0 +1,25 @@// Copyright 2025 Shota FUJI // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // // SPDX-License-Identifier: MPL-2.0 using GLib; namespace TimeTracker.Event { public sealed class TimerDeleted : Event { public string timer_id { get; construct; } public TimerDeleted(string timer_id) { var now = new DateTime.now_utc(); Object( event_id: new_id(), created_at: now.to_unix(), timer_id: timer_id ); } } }
-
-
-
@@ -0,0 +1,27 @@// Copyright 2025 Shota FUJI // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // // SPDX-License-Identifier: MPL-2.0 using GLib; namespace TimeTracker.Event { public sealed class TimerStarted : Event { public string timer_id { get; construct; } public string title { get; construct; } public TimerStarted(string title) { var now = new DateTime.now_utc(); Object( event_id: new_id(), created_at: now.to_unix(), title: title, timer_id: new_id() ); } } }
-
-
-
@@ -0,0 +1,25 @@// Copyright 2025 Shota FUJI // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // // SPDX-License-Identifier: MPL-2.0 using GLib; namespace TimeTracker.Event { public sealed class TimerStopped : Event { public string timer_id { get; construct; } public TimerStopped(string timer_id) { var now = new DateTime.now_utc(); Object( event_id: new_id(), created_at: now.to_unix(), timer_id: timer_id ); } } }
-
-
-
@@ -0,0 +1,27 @@// Copyright 2025 Shota FUJI // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // // SPDX-License-Identifier: MPL-2.0 using GLib; namespace TimeTracker.Event { public sealed class TimerTitleChanged : Event { public string timer_id { get; construct; } public string new_title { get; construct; } public TimerTitleChanged(string timer_id, string new_title) { var now = new DateTime.now_utc(); Object( event_id: new_id(), created_at: now.to_unix(), timer_id: timer_id, new_title: new_title ); } } }
-
-
-
@@ -0,0 +1,130 @@// Copyright 2025 Shota FUJI // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // // SPDX-License-Identifier: MPL-2.0 using GLib; namespace TimeTracker.Model.Tests { public void add_event_tests() { Test.add_func("/model/start-timer", () => { var model = new Model(); assert_cmpuint(model.active_timers.n_items, EQ, 0); assert_cmpuint(model.stopped_timers.n_items, EQ, 0); var started = new Event.TimerStarted("Foo"); model.update(started); assert_cmpuint(model.active_timers.n_items, EQ, 1); assert_cmpuint(model.stopped_timers.n_items, EQ, 0); var new_timer = (Timer) model.active_timers.get_item(0); assert_nonnull(new_timer); assert_cmpstr(new_timer.id, EQ, started.timer_id); assert_cmpstr(new_timer.title, EQ, "Foo"); assert(new_timer.started_at == started.created_at); assert_false(new_timer.is_stopped); }); Test.add_func("/model/stop-timer", () => { var model = new Model(); assert_cmpuint(model.active_timers.n_items, EQ, 0); assert_cmpuint(model.stopped_timers.n_items, EQ, 0); var started = new Event.TimerStarted("Foo"); model.update(started); assert_cmpuint(model.active_timers.n_items, EQ, 1); assert_cmpuint(model.stopped_timers.n_items, EQ, 0); var new_timer = (Timer) model.active_timers.get_item(0); assert_nonnull(new_timer); var stopped = new Event.TimerStopped(new_timer.id); model.update(stopped); assert_cmpuint(model.active_timers.n_items, EQ, 0); assert_cmpuint(model.stopped_timers.n_items, EQ, 1); var timer = (Timer) model.stopped_timers.get_item(0); assert_nonnull(timer); assert_cmpstr(timer.id, EQ, started.timer_id); assert_cmpstr(timer.title, EQ, "Foo"); assert_true(timer.is_stopped); assert(timer.started_at == started.created_at); assert(timer.stopped_at == stopped.created_at); }); Test.add_func("/model/change-title", () => { var model = new Model(); assert_cmpuint(model.active_timers.n_items, EQ, 0); assert_cmpuint(model.stopped_timers.n_items, EQ, 0); var started = new Event.TimerStarted("Foo"); model.update(started); assert_cmpuint(model.active_timers.n_items, EQ, 1); assert_cmpuint(model.stopped_timers.n_items, EQ, 0); var new_timer = (Timer) model.active_timers.get_item(0); assert_nonnull(new_timer); var renamed = new Event.TimerTitleChanged(new_timer.id, "Bar"); model.update(renamed); assert_cmpuint(model.active_timers.n_items, EQ, 1); assert_cmpuint(model.stopped_timers.n_items, EQ, 0); var timer = (Timer) model.active_timers.get_item(0); assert_nonnull(timer); assert_cmpstr(timer.title, EQ, "Bar"); assert_false(timer.is_stopped); }); Test.add_func("/model/delete-timers", () => { var model = new Model(); assert_cmpuint(model.active_timers.n_items, EQ, 0); assert_cmpuint(model.stopped_timers.n_items, EQ, 0); var foo_started = new Event.TimerStarted("Foo"); model.update(foo_started); assert_cmpuint(model.active_timers.n_items, EQ, 1); assert_cmpuint(model.stopped_timers.n_items, EQ, 0); var foo = (Timer) model.active_timers.get_item(0); assert_nonnull(foo); model.update(new Event.TimerStopped(foo.id)); assert_cmpuint(model.active_timers.n_items, EQ, 0); assert_cmpuint(model.stopped_timers.n_items, EQ, 1); model.update(new Event.TimerDeleted(foo.id)); assert_cmpuint(model.active_timers.n_items, EQ, 0); assert_cmpuint(model.stopped_timers.n_items, EQ, 0); var bar_started = new Event.TimerStarted("Bar"); model.update(bar_started); var bar = (Timer) model.active_timers.get_item(0); assert_nonnull(bar); assert_cmpuint(model.active_timers.n_items, EQ, 1); assert_cmpuint(model.stopped_timers.n_items, EQ, 0); model.update(new Event.TimerDeleted(bar.id)); assert_cmpuint(model.active_timers.n_items, EQ, 0); assert_cmpuint(model.stopped_timers.n_items, EQ, 0); }); } }
-
-
src/Model/Model.vala (new)
-
@@ -0,0 +1,96 @@// Copyright 2025 Shota FUJI // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // // SPDX-License-Identifier: MPL-2.0 using GLib; namespace TimeTracker.Model { public sealed class Model : Object { public ListStore active_timers { get; internal set; } public ListStore stopped_timers { get; internal set; } public Model() { Object( active_timers: new ListStore(typeof (Timer)), stopped_timers: new ListStore(typeof (Timer)) ); } // Since Vala's type narrowing is half baked, cast inside branch may or may not be narrowed // down. Explicit cast means compiler fails narrowing at that branch. public void update(Event.Event event) { if (event is Event.TimerStarted) { var timer = new Timer(event.timer_id, event.title, event.created_at); active_timers.append(timer); return; } if (event is Event.TimerTitleChanged) { Event.TimerTitleChanged e = (Event.TimerTitleChanged) event; for (uint i = 0; i < active_timers.n_items; i++) { var timer = (Timer) active_timers.get_item(i); if (timer.id == e.timer_id) { timer.title = e.new_title; return; } } for (uint i = 0; i < stopped_timers.n_items; i++) { var timer = (Timer) stopped_timers.get_item(i); if (timer.id == e.timer_id) { timer.title = e.new_title; return; } } return; } if (event is Event.TimerStopped) { Event.TimerStopped e = (Event.TimerStopped) event; for (uint i = 0; i < active_timers.n_items; i++) { var timer = (Timer) active_timers.get_item(i); if (timer.id == e.timer_id) { active_timers.remove(i); timer.stopped_at = e.created_at; timer.is_stopped = true; stopped_timers.append(timer); return; } } return; } if (event is Event.TimerDeleted) { Event.TimerDeleted e = (Event.TimerDeleted) event; for (uint i = 0; i < active_timers.n_items; i++) { var timer = (Timer) active_timers.get_item(i); if (timer.id == e.timer_id) { active_timers.remove(i); return; } } for (uint i = 0; i < stopped_timers.n_items; i++) { var timer = (Timer) stopped_timers.get_item(i); if (timer.id == e.timer_id) { stopped_timers.remove(i); return; } } return; } info(@"Unknown event found: id=$(event.event_id)"); } } }
-
-
src/Model/Timer.vala (new)
-
@@ -0,0 +1,25 @@// Copyright 2025 Shota FUJI // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // // SPDX-License-Identifier: MPL-2.0 namespace TimeTracker.Model { public sealed class Timer : Object { public string id { get; construct; } public string title { get; internal set construct; } public int64 started_at { get; internal set construct; } public bool is_stopped { get; internal set construct; } public int64 stopped_at { get; internal set construct; } public Timer(string id, string title, int64 started_at) { Object(id: id, title: title, started_at: started_at, is_stopped: false, stopped_at: 0); } public static bool is_same(Timer a, Timer b) { return a.id == b.id; } } }
-
-
src/main.test.vala (new)
-
@@ -0,0 +1,15 @@// Copyright 2025 Shota FUJI // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // // SPDX-License-Identifier: MPL-2.0 static void main(string[] args) { GLib.Test.init(ref args); TimeTracker.Model.Tests.add_event_tests(); GLib.Test.run(); }
-