Files
mesh-catalog/modules/i3status-rust/cmd/i3status-rust-tools/values_test.go
T
jochen 0d54ecc764
mesh/merge-gate pass: builds asus-zephyrus-g14, i3status-rust, power → ace, g14, novox, shanks; no bus step; every machine composes with the change as it d…
mesh/repo-check fail: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery superseded: a newer head of the same pull request
Keep the value a watch hands over at once, watch a dead watch again, and let the path lists grow only in the open (hq ADR 0260)
The SDK hands the current value over before Watch returns, and the follower had no
entry for it yet, so the first value was dropped. A silent watch is now told from an
expired key and watched again rather than polled, each write takes its own temporary
file, drop-in directories are no module's, and an entry added to the path check's lists
is refused unless its line says why.
2026-10-08 16:12:37 +02:00

253 lines
7.6 KiB
Go

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)
}
}