Files
cthulhu/internal/app/date_time_test.go
2026-10-14 01:22:04 -04:00

156 lines
5.7 KiB
Go

package app
import (
"context"
"log/slog"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/datetime"
"git.stormux.org/storm/cthulhu/internal/input"
"git.stormux.org/storm/cthulhu/internal/preferences"
)
func TestDateTimeFormats(t *testing.T) {
// datetime initializes libc's locale before tests run. Use a child process
// so these fixed English expectations do not depend on the build locale.
if os.Getenv("CTHULHU_DATE_TIME_TEST_CHILD") != "1" {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
command := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestDateTimeFormats$", "-test.count=1")
command.Env = append(os.Environ(), "LC_ALL=C", "CTHULHU_DATE_TIME_TEST_CHILD=1")
if output, err := command.CombinedOutput(); err != nil {
t.Fatalf("C-locale date/time test: %v\n%s", err, output)
}
return
}
now := time.Date(2026, 9, 17, 19, 5, 0, 0, time.UTC)
for _, tc := range []struct {
command input.Command
format, want string
}{
{input.PresentTime, "locale", "19:05:00"},
{input.PresentTime, "12-hour", "07:05 PM"},
{input.PresentTime, "12-hour-seconds", "07:05:00 PM"},
{input.PresentTime, "24-hour-seconds", "19:05:00"},
{input.PresentTime, "24-hour-seconds-words", "19 hours, 05 minutes and 00 seconds"},
{input.PresentTime, "24-hour", "19:05"},
{input.PresentTime, "24-hour-words", "19 hours and 05 minutes"},
{input.PresentDate, "locale", "09/17/26"},
{input.PresentDate, "numbers-dm", "17/09"},
{input.PresentDate, "numbers-md", "09/17"},
{input.PresentDate, "numbers-dmy", "17/09/2026"},
{input.PresentDate, "numbers-mdy", "09/17/2026"},
{input.PresentDate, "numbers-ymd", "2026/09/17"},
{input.PresentDate, "full-dm", "Thursday, 17 September"},
{input.PresentDate, "full-md", "Thursday, September 17"},
{input.PresentDate, "full-dmy", "Thursday, 17 September, 2026"},
{input.PresentDate, "full-mdy", "Thursday, September 17, 2026"},
{input.PresentDate, "full-ymd", "2026. September 17, Thursday"},
{input.PresentDate, "abbreviated-dm", "Thu, 17 Sep"},
{input.PresentDate, "abbreviated-md", "Thu, Sep 17"},
{input.PresentDate, "abbreviated-dmy", "Thu, 17 Sep, 2026"},
{input.PresentDate, "abbreviated-mdy", "Thu, Sep 17, 2026"},
{input.PresentDate, "abbreviated-ymd", "2026. Sep 17, Thu"},
} {
settings := config.DefaultSettings()
if tc.command == input.PresentTime {
settings.TimeFormat = tc.format
} else {
settings.DateFormat = tc.format
}
queue := &fakeSpeechQueue{spoken: make(chan string, 1), interrupt: make(chan struct{}, 1)}
presentDateTime(queue, settings, tc.command, now)
if got := <-queue.spoken; got != tc.want {
t.Errorf("%s: %q want %q", tc.format, got, tc.want)
}
if len(queue.interrupt) != 1 {
t.Fatal("command did not interrupt")
}
}
}
func TestDateTimeOptionCounts(t *testing.T) {
now := time.Date(2026, 9, 17, 19, 5, 0, 0, time.UTC)
timeOptions := datetime.TimeOptions(now)
dateOptions := datetime.DateOptions(now)
wantTime := []string{
"locale", "12-hour", "12-hour-seconds", "24-hour-seconds",
"24-hour-seconds-words", "24-hour", "24-hour-words",
}
wantDate := []string{
"locale", "numbers-dm", "numbers-md", "numbers-dmy", "numbers-mdy",
"numbers-ymd", "full-dm", "full-md", "full-dmy", "full-mdy",
"full-ymd", "abbreviated-dm", "abbreviated-md", "abbreviated-dmy",
"abbreviated-mdy", "abbreviated-ymd",
}
if len(timeOptions) != len(wantTime) {
t.Fatalf("time options: got %d, want %d", len(timeOptions), len(wantTime))
}
if len(dateOptions) != len(wantDate) {
t.Fatalf("date options: got %d, want %d", len(dateOptions), len(wantDate))
}
for _, test := range []struct {
name string
options []datetime.Option
expected []string
}{
{"time", timeOptions, wantTime},
{"date", dateOptions, wantDate},
} {
for index, option := range test.options {
if option.Value != test.expected[index] {
t.Fatalf("%s option %d = %q, want %q", test.name, index,
option.Value, test.expected[index])
}
}
}
for _, option := range append(timeOptions, dateOptions...) {
if option.Value == "" || option.Label == "" {
t.Fatalf("empty option: %+v", option)
}
}
}
func TestDateTimePreferencesLiveAndSaved(t *testing.T) {
settings := config.DefaultSettings()
settings.Path = filepath.Join(t.TempDir(), "settings.toml")
live := &liveSettings{}
live.Store(settings)
worker := newPreferencesWorker(slog.New(slog.DiscardHandler), settings, nil)
worker.live = live
values := preferences.FromSettings(settings)
values.TimeFormat, values.DateFormat = "24-hour-words", "abbreviated-mdy"
response := worker.apply(context.Background(), preferences.Request{Values: values, Save: true, Close: true})
if !response.Accepted || !response.Close {
t.Fatalf("apply: %+v", response)
}
loaded, err := config.Load(settings.Path)
if err != nil || loaded.TimeFormat != "24-hour-words" || loaded.DateFormat != "abbreviated-mdy" {
t.Fatalf("saved formats: %+v %v", loaded, err)
}
queue := &fakeSpeechQueue{spoken: make(chan string, 1)}
presentDateTime(queue, live.Load(), input.PresentTime, time.Date(2026, 9, 17, 0, 1, 0, 0, time.Local))
if got := <-queue.spoken; got != "00 hours and 01 minutes" {
t.Fatalf("live time: %s", got)
}
for _, field := range []string{"time", "date"} {
bad := values
if field == "time" {
bad.TimeFormat = "invalid"
} else {
bad.DateFormat = "invalid"
}
if worker.apply(context.Background(), preferences.Request{Values: bad, Save: true}).Accepted {
t.Fatal("invalid format accepted")
}
}
if live.Load().TimeFormat != "24-hour-words" || live.Load().DateFormat != "abbreviated-mdy" {
t.Fatal("invalid draft changed live settings")
}
}