1package module
2
3import (
4 "bytes"
5 "context"
6 "github.com/bnema/neferbar/internal/racecheck"
7 "log/slog"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12 "time"
13)
14
15func newTest() (*Module, chan struct{}) {
16 wake := make(chan struct{}, 1)
17 return New("t", Left, "true", wake, slog.New(slog.DiscardHandler)), wake
18}
19
20func TestReadFramesSplitsOnNewlineAndFormFeed(t *testing.T) {
21 m, _ := newTest()
22 m.readFrames(strings.NewReader("one\ntwo\fthree"))
23 // "three" has no terminator yet: the latest complete frame is "two".
24 got, changed, _ := m.Take(nil)
25 if !changed || string(got) != "two" {
26 t.Fatalf("got %q changed=%v, want latest complete frame %q", got, changed, "two")
27 }
28 if _, changed, _ = m.Take(got); changed {
29 t.Fatal("Take must clear the dirty flag")
30 }
31}
32
33func TestFrameLongerThanMaxIsTruncated(t *testing.T) {
34 m, _ := newTest()
35 m.readFrames(strings.NewReader(strings.Repeat("x", MaxFrame*3) + "\n"))
36 got, _, _ := m.Take(nil)
37 if len(got) != MaxFrame {
38 t.Fatalf("len = %d, want %d", len(got), MaxFrame)
39 }
40}
41
42func TestPublishWakesWithoutBlocking(t *testing.T) {
43 m, wake := newTest()
44 for i := 0; i < 1000; i++ { // nobody drains wake: must never block
45 m.publish([]byte("x"))
46 }
47 if len(wake) != 1 {
48 t.Fatalf("wake queue = %d, want 1", len(wake))
49 }
50}
51
52func TestRunStreamsAndRestarts(t *testing.T) {
53 wake := make(chan struct{}, 1)
54 m := New("t", Left, "printf 'hello\\n'", wake, slog.New(slog.DiscardHandler))
55 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
56 defer cancel()
57 go m.Run(ctx)
58 var seen int
59 deadline := time.After(4 * time.Second)
60 for seen < 2 { // the script exits at once; it must be restarted
61 select {
62 case <-wake:
63 if b, ch, failed := m.Take(nil); ch && !failed && bytes.Equal(b, []byte("hello")) {
64 seen++
65 }
66 case <-deadline:
67 t.Fatalf("saw %d frames, want 2 (restart)", seen)
68 }
69 }
70}
71
72func TestPublishAllocs(t *testing.T) {
73 if racecheck.Enabled {
74 t.Skip("the race detector allocates")
75 }
76 m, _ := newTest()
77 frame := []byte("\x1b[31mred\x1b[0m text")
78 dst := make([]byte, 0, MaxFrame)
79 if got := testing.AllocsPerRun(500, func() {
80 m.publish(frame)
81 dst, _, _ = m.Take(dst)
82 }); got > 0 {
83 t.Errorf("publish+Take allocates %.1f objects; want 0", got)
84 }
85}
86
87// Stopping a module must also end the processes the script started, and Wait
88// must return once they are gone.
89func TestStopEndsTheScriptsChildren(t *testing.T) {
90 marker := filepath.Join(t.TempDir(), "alive")
91 // A background loop in a subshell: it outlives "sh" unless the whole group is killed.
92 script := "(while :; do echo x > " + marker + "; sleep 0.05; done) & wait"
93 m := New("t", Left, script, make(chan struct{}, 1), slog.New(slog.DiscardHandler))
94 ctx, cancel := context.WithCancel(context.Background())
95 m.Start(ctx)
96 deadline := time.Now().Add(3 * time.Second)
97 for {
98 if _, err := os.Stat(marker); err == nil {
99 break
100 }
101 if time.Now().After(deadline) {
102 t.Fatal("the script never started")
103 }
104 time.Sleep(20 * time.Millisecond)
105 }
106 cancel()
107 m.Wait(5 * time.Second)
108 os.Remove(marker)
109 time.Sleep(300 * time.Millisecond)
110 if _, err := os.Stat(marker); err == nil {
111 t.Fatal("a child of the script is still running after Wait returned")
112 }
113}