summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-03 23:12:15 +0200
committerPaul Buetow <paul@buetow.org>2026-03-03 23:12:15 +0200
commitb64e20fe58da8d9afecda06c2d701096b267cca0 (patch)
tree39cd1eb61f527de784e9bb873505d3b52e3312ba
parente96b0b370bcdd55ad2d5b20187e4bbae78785ff2 (diff)
Task 352: expand comprehensive test coverage
-rw-r--r--internal/duration/parse.go8
-rw-r--r--internal/duration/parse_test.go2
-rw-r--r--internal/worktime/comprehensive_test.go80
3 files changed, 90 insertions, 0 deletions
diff --git a/internal/duration/parse.go b/internal/duration/parse.go
index f2febe8..f144ac1 100644
--- a/internal/duration/parse.go
+++ b/internal/duration/parse.go
@@ -11,6 +11,11 @@ import (
var bareIntegerPattern = regexp.MustCompile(`^[+-]?\d+$`)
+const (
+ maxInt64 = int64(^uint64(0) >> 1)
+ minInt64 = -maxInt64 - 1
+)
+
// Parse converts duration text into time.Duration.
// Go-style durations (e.g. "1h30m") are supported, and bare integers are seconds.
func Parse(value string) (time.Duration, error) {
@@ -24,6 +29,9 @@ func Parse(value string) (time.Duration, error) {
if err != nil {
return 0, fmt.Errorf("parse seconds %q: %w", value, err)
}
+ if seconds > maxInt64/int64(time.Second) || seconds < minInt64/int64(time.Second) {
+ return 0, fmt.Errorf("duration seconds %q overflows time.Duration", value)
+ }
return time.Duration(seconds) * time.Second, nil
}
diff --git a/internal/duration/parse_test.go b/internal/duration/parse_test.go
index 3b812cd..931f904 100644
--- a/internal/duration/parse_test.go
+++ b/internal/duration/parse_test.go
@@ -63,6 +63,8 @@ func TestParseInvalidDurations(t *testing.T) {
" ",
"abc",
"1h30x",
+ "9223372036854775807",
+ "-9223372036854775808",
}
for _, input := range tests {
diff --git a/internal/worktime/comprehensive_test.go b/internal/worktime/comprehensive_test.go
new file mode 100644
index 0000000..0f68813
--- /dev/null
+++ b/internal/worktime/comprehensive_test.go
@@ -0,0 +1,80 @@
+package worktime
+
+import (
+ "strings"
+ "testing"
+ "time"
+
+ "codeberg.org/snonux/timr/internal/config"
+)
+
+func TestComprehensiveDBRoundTripAndReportFixture(t *testing.T) {
+ dbDir := t.TempDir()
+ host := "fixture-host"
+
+ // Seed deterministic fixture entries via public operations.
+ if _, err := Login(dbDir, host, "work", mustDate(2026, 1, 5, 9, 0, 0), "start"); err != nil {
+ t.Fatalf("Login() error = %v", err)
+ }
+ if _, err := Logout(dbDir, host, "work", mustDate(2026, 1, 5, 17, 0, 0), "stop"); err != nil {
+ t.Fatalf("Logout() error = %v", err)
+ }
+ if _, err := Add(dbDir, host, "lunch", time.Hour, mustDate(2026, 1, 5, 12, 0, 0), "lunch"); err != nil {
+ t.Fatalf("Add(lunch) error = %v", err)
+ }
+ if _, err := Add(dbDir, host, "off", 8*time.Hour, mustDate(2026, 1, 6, 10, 0, 0), "off"); err != nil {
+ t.Fatalf("Add(off) error = %v", err)
+ }
+
+ // Round-trip from host file back to merged entries.
+ hostDB, err := LoadHost(dbDir, host)
+ if err != nil {
+ t.Fatalf("LoadHost() error = %v", err)
+ }
+ if len(hostDB.Entries[host]) != 4 {
+ t.Fatalf("host entries len = %d, want 4", len(hostDB.Entries[host]))
+ }
+
+ merged, err := LoadAll(dbDir)
+ if err != nil {
+ t.Fatalf("LoadAll() error = %v", err)
+ }
+ if len(merged) != 4 {
+ t.Fatalf("merged entries len = %d, want 4", len(merged))
+ }
+
+ cfg := config.Default()
+ report, err := BuildReport(merged, cfg)
+ if err != nil {
+ t.Fatalf("BuildReport() error = %v", err)
+ }
+ if len(report) != 1 {
+ t.Fatalf("weeks len = %d, want 1", len(report))
+ }
+
+ week := report[0]
+ if week.RequiredSeconds != 32*secondsPerHour {
+ t.Fatalf("required seconds = %d, want %d", week.RequiredSeconds, 32*secondsPerHour)
+ }
+ if week.Values["work"] != 7*secondsPerHour {
+ t.Fatalf("week work seconds = %d, want %d", week.Values["work"], 7*secondsPerHour)
+ }
+ if week.WeeklyBalanceSeconds != -25*secondsPerHour {
+ t.Fatalf("weekly balance = %d, want %d", week.WeeklyBalanceSeconds, -25*secondsPerHour)
+ }
+
+ rendered := FormatReport(report, false, false)
+ if !strings.Contains(rendered, "work:7.00h") {
+ t.Fatalf("rendered report missing expected work value: %q", rendered)
+ }
+ if !strings.Contains(rendered, "off:8.00h") {
+ t.Fatalf("rendered report missing expected off value: %q", rendered)
+ }
+ if !strings.Contains(rendered, "balance:-25.00h") {
+ t.Fatalf("rendered report missing expected balance value: %q", rendered)
+ }
+}
+
+func mustDate(year int, month time.Month, day int, hour int, minute int, second int) time.Time {
+ return time.Date(year, month, day, hour, minute, second, 0, time.Local)
+}