package main import ( "encoding/json" "errors" "os" "os/exec" "path/filepath" "strings" "testing" "time" ) // Defends novox/hq ADR 0260: a value another module keeps reaches the bar over the bus, through this // module's own bundle and directory; a dead watch is never silent; a removed block is no longer kept; // and a value frozen at its source hides. func TestAValueIsKeptAsItsTextDatedByItsAtAndGoneWhenDeleted(t *testing.T) { dir := t.TempDir() w := Wanted{"power.draw", "laptop"} at := time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC) if err := keep(dir, w, "put", json.RawMessage(`{"text":"18.2 W machine","at":"2026-10-08T12:00:00Z"}`)); err != nil { t.Fatal(err) } path := filepath.Join(dir, "power.draw@laptop") b, err := os.ReadFile(path) info, _ := os.Stat(path) if err != nil || string(b) != "18.2 W machine\n" || !info.ModTime().Equal(at) { t.Fatalf("%q %v, dated %v", b, err, info.ModTime()) } if err := keep(dir, w, "delete", nil); err != nil { t.Fatal(err) } if _, err := os.Stat(path); !os.IsNotExist(err) { t.Fatal("a deleted value is still shown") } if err := keep(dir, w, "put", json.RawMessage(`"no text"`)); err == nil { t.Fatal("a value with no text was kept") } } type fakeBus struct { refuse bool watched []Wanted handles []func(string, json.RawMessage) error current json.RawMessage // handed to a new watch before Watch returns, as the SDK does gets int value json.RawMessage keyFound bool } func (b *fakeBus) bus() Bus { return Bus{ Watch: func(state, key string, h func(string, json.RawMessage) error) error { if b.refuse { return errors.New("not granted") } b.watched = append(b.watched, Wanted{state, key}) b.handles = append(b.handles, h) if b.current != nil { return h("put", b.current) } return nil }, Get: func(string, string) (json.RawMessage, bool, error) { b.gets++; return b.value, b.keyFound, nil }, } } func (b *fakeBus) latest() func(string, json.RawMessage) error { return b.handles[len(b.handles)-1] } func barFiles(t *testing.T, content string) []string { t.Helper() f := filepath.Join(t.TempDir(), "bottom-bar.toml") if err := os.WriteFile(f, []byte(content), 0o644); err != nil { t.Fatal(err) } return []string{f} } type follow struct { t *testing.T now time.Time said []string bus *fakeBus dir string f *Follower bar []string } func newFollow(t *testing.T) *follow { x := &follow{t: t, now: time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC), bus: &fakeBus{}, dir: t.TempDir()} x.f = &Follower{Dir: x.dir, Bus: x.bus.bus(), Now: func() time.Time { return x.now }, Say: func(s string) { x.said = append(x.said, s) }} x.bar = barFiles(t, `command = "~/.local/bin/i3status-value power.draw laptop"`) return x } func (x *follow) shown() string { b, err := os.ReadFile(filepath.Join(x.dir, "power.draw@laptop")) if err != nil { return "" } return string(b) } func TestTheValueAWatchHandsOverAtOnceIsKept(t *testing.T) { x := newFollow(t) x.bus.current = json.RawMessage(`{"text":"18.2 W machine"}`) x.f.Round(x.bar) if x.shown() != "18.2 W machine\n" { t.Fatalf("the value handed over before Watch returned was dropped: %q", x.shown()) } } func TestARefusedWatchIsSaidAndTriedAgain(t *testing.T) { x := newFollow(t) x.bus.refuse = true x.f.Round(x.bar) if len(x.said) != 1 || !strings.Contains(x.said[0], "cannot watch power.draw laptop: not granted") { t.Fatalf("%v", x.said) } x.bus.refuse = false x.f.Round(x.bar) if len(x.bus.watched) != 1 { t.Fatalf("watched %v", x.bus.watched) } } func TestASilentWatchWhoseValueWentOnIsWatchedAgainAndSaidOnce(t *testing.T) { x := newFollow(t) x.f.Round(x.bar) x.bus.value, x.bus.keyFound = json.RawMessage(`{"text":"9.0 W CPU"}`), true for i := 0; i < 3; i++ { x.now = x.now.Add(Silent + time.Second) x.f.Round(x.bar) } if len(x.bus.watched) != 4 || x.shown() != "9.0 W CPU\n" { t.Fatalf("watched %d times, shows %q", len(x.bus.watched), x.shown()) } dead := 0 for _, s := range x.said { if strings.Contains(s, "watching it again") { dead++ } } if dead != 1 { t.Fatalf("a dead watch said %d times: %v", dead, x.said) } // What an old watch hands over is dropped; the current one's is kept. if err := x.bus.handles[0]("put", json.RawMessage(`{"text":"old"}`)); err != nil || x.shown() != "9.0 W CPU\n" { t.Fatalf("an old watch wrote: %q %v", x.shown(), err) } if err := x.bus.latest()("put", json.RawMessage(`{"text":"10.0 W CPU"}`)); err != nil || x.shown() != "10.0 W CPU\n" { t.Fatalf("the current watch did not: %q %v", x.shown(), err) } } func TestAnExpiredKeyIsExpiredNotADeadWatch(t *testing.T) { x := newFollow(t) x.bus.current = json.RawMessage(`{"text":"9.0 W CPU"}`) x.f.Round(x.bar) x.bus.current, x.bus.keyFound = nil, false for i := 0; i < 3; i++ { x.now = x.now.Add(Silent + time.Second) x.f.Round(x.bar) } if len(x.bus.watched) != 1 || x.shown() != "" { t.Fatalf("watched %d times, shows %q", len(x.bus.watched), x.shown()) } for _, s := range x.said { if strings.Contains(s, "watching it again") { t.Fatalf("an expired key was taken for a dead watch: %v", x.said) } } if n := strings.Count(strings.Join(x.said, "|"), "expired"); n != 1 { t.Fatalf("expiry said %d times: %v", n, x.said) } } func TestARemovedBlockIsNoLongerKept(t *testing.T) { x := newFollow(t) x.bus.current = json.RawMessage(`{"text":"9.0 W CPU"}`) x.f.Round(x.bar) x.f.Round(barFiles(t, "no values here")) if x.shown() != "" || !strings.Contains(x.said[len(x.said)-1], "no longer kept") { t.Fatalf("%q %v", x.shown(), x.said) } if err := x.bus.latest()("put", json.RawMessage(`{"text":"after"}`)); err != nil || x.shown() != "" { t.Fatal("a stopped watch still writes") } } func TestAWatchAndAReadWritingAtOnceLeaveOneWholeValue(t *testing.T) { dir := t.TempDir() w := Wanted{"power.draw", "laptop"} done := make(chan error, 20) for i := 0; i < 20; i++ { go func(i int) { done <- keep(dir, w, "put", json.RawMessage(`{"text":"value `+strings.Repeat("x", i)+`"}`)) }(i) } for i := 0; i < 20; i++ { if err := <-done; err != nil { t.Fatal(err) } } b, _ := os.ReadFile(filepath.Join(dir, "power.draw@laptop")) if !strings.HasPrefix(string(b), "value ") || strings.Count(string(b), "\n") != 1 { t.Fatalf("%q", b) } entries, _ := os.ReadDir(dir) if len(entries) != 1 { t.Fatalf("temporary files left: %v", entries) } } func TestAKeyIsAMachinesName(t *testing.T) { if w := wantedIn(barFiles(t, `i3status-value power.draw ../etc`+"\n"+`i3status-value power.draw Laptop`)); len(w) != 0 { t.Fatalf("%v", w) } } func TestTheBlocksCommandShowsOnlyAFreshValue(t *testing.T) { runtime := t.TempDir() dir := filepath.Join(runtime, "i3status-rust", "values") run := func() string { cmd := exec.Command("sh", "../../files/bin/i3status-value", "power.draw", "laptop") cmd.Env = append(os.Environ(), "XDG_RUNTIME_DIR="+runtime) out, err := cmd.Output() if err != nil { t.Fatal(err) } return strings.TrimSpace(string(out)) } if got := run(); got != "" { t.Fatalf("no value: %q", got) } fresh := time.Now().UTC().Format(time.RFC3339) if err := keep(dir, Wanted{"power.draw", "laptop"}, "put", json.RawMessage(`{"text":"8.0 W CPU","at":"`+fresh+`"}`)); err != nil { t.Fatal(err) } if got := run(); got != "8.0 W CPU" { t.Fatalf("fresh: %q", got) } // A value measured 31 s ago hides, however recently it arrived. old := time.Now().Add(-31 * time.Second).UTC().Format(time.RFC3339) if err := keep(dir, Wanted{"power.draw", "laptop"}, "put", json.RawMessage(`{"text":"8.0 W CPU","at":"`+old+`"}`)); err != nil { t.Fatal(err) } if got := run(); got != "" { t.Fatalf("measured 31 s ago: %q", got) } }