module_test.go RawHistoryFolder

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}
114
115func TestScriptDir(t *testing.T) {
116	home, _ := os.UserHomeDir()
117	cwd, _ := os.Getwd()
118	for command, want := range map[string]string{
119		"/opt/bar/left.sh":          "/opt/bar",
120		"/opt/bar/left.sh 60 30":    "/opt/bar",
121		"~/.config/neferbar/a.sh":   filepath.Join(home, ".config/neferbar"),
122		"examples/modules/clock.sh": filepath.Join(cwd, "examples/modules"),
123		"./local.sh":                cwd,
124		"date":                      "",
125		"sh -c 'echo hi'":           "",
126		"":                          "",
127		"   ":                       "",
128	} {
129		if got := ScriptDir(command); got != want {
130			t.Errorf("ScriptDir(%q) = %q, want %q", command, got, want)
131		}
132	}
133}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 5

Tags 4

↑↓ moveenter openesc close