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
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.
253 lines
7.6 KiB
Go
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)
|
|
}
|
|
}
|