v0.4.0 c0db34c

watch_test.go RawHistoryFolder

1package config
2
3import (
4	"context"
5	"github.com/bnema/neferafk/internal/ports"
6	"os"
7	"path/filepath"
8	"testing"
9	"time"
10)
11
12func recv[T any](t *testing.T, ch <-chan T) T {
13	t.Helper()
14	select {
15	case v := <-ch:
16		return v
17	case <-time.After(2 * time.Second):
18		t.Fatal("watch timeout")
19		var z T
20		return z
21	}
22}
23func save(t *testing.T, path, text string) {
24	t.Helper()
25	tmp := path + ".new"
26	if err := os.WriteFile(tmp, []byte(text), 0600); err != nil {
27		t.Fatal(err)
28	}
29	if err := os.Rename(tmp, path); err != nil {
30		t.Fatal(err)
31	}
32}
33func TestWatchAtomicInvalidTransactionalAndSymlinkRetarget(t *testing.T) {
34	dir := t.TempDir()
35	targetDir := t.TempDir()
36	path := filepath.Join(dir, "config")
37	target := filepath.Join(targetDir, "target")
38	save(t, path, "lock.after=5m\n")
39	out := make(chan ports.ConfigChanged, 4)
40	errs := make(chan error, 4)
41	ready := make(chan struct{})
42	ctx, cancel := context.WithCancel(context.Background())
43	done := make(chan error, 1)
44	go func() { done <- Watch(ctx, path, out, errs, ready) }()
45	recv(t, ready)
46	if initial := recv(t, out); initial.Config.LockAfter != 5*time.Minute {
47		t.Fatal(initial)
48	}
49	save(t, path, "lock.after=2m\n")
50	if got := recv(t, out); got.Config.LockAfter != 2*time.Minute {
51		t.Fatal(got)
52	}
53	save(t, path, "lock.after=-1s\n")
54	recv(t, errs)
55	select {
56	case v := <-out:
57		t.Fatalf("invalid reload emitted %+v", v)
58	default:
59	}
60	save(t, target, "lock.after=3m\n")
61	if err := os.Symlink(target, path+".link"); err != nil {
62		t.Fatal(err)
63	}
64	if err := os.Rename(path+".link", path); err != nil {
65		t.Fatal(err)
66	}
67	if got := recv(t, out); got.Config.LockAfter != 3*time.Minute {
68		t.Fatal(got)
69	}
70	save(t, target, "lock.after=4m\n")
71	if got := recv(t, out); got.Config.LockAfter != 4*time.Minute {
72		t.Fatal(got)
73	}
74	cancel()
75	if err := recv(t, done); err != nil {
76		t.Fatal(err)
77	}
78}
79func TestWatchCancellationWhileDeliveryBlocked(t *testing.T) {
80	path := filepath.Join(t.TempDir(), "config")
81	save(t, path, "lock.after=5m\n")
82	out := make(chan ports.ConfigChanged)
83	ready := make(chan struct{})
84	ctx, cancel := context.WithCancel(context.Background())
85	done := make(chan error, 1)
86	go func() { done <- Watch(ctx, path, out, nil, ready) }()
87	recv(t, ready)
88	save(t, path, "lock.after=1m\n")
89	cancel()
90	if err := recv(t, done); err != nil {
91		t.Fatal(err)
92	}
93}
94
95func TestWatchMissingDirectoryStartupThenRemoveRecreate(t *testing.T) {
96	root := t.TempDir()
97	dir := filepath.Join(root, "missing", "nested")
98	path := filepath.Join(dir, "config")
99	out := make(chan ports.ConfigChanged, 4)
100	ready := make(chan struct{})
101	ctx, cancel := context.WithCancel(context.Background())
102	defer cancel()
103	done := make(chan error, 1)
104	go func() { done <- Watch(ctx, path, out, nil, ready) }()
105	recv(t, ready)
106	if err := os.MkdirAll(dir, 0700); err != nil {
107		t.Fatal(err)
108	}
109	save(t, path, "lock.after=1m\n")
110	if got := recv(t, out); got.Config.LockAfter != time.Minute {
111		t.Fatal(got)
112	}
113	// Rename the entire watched directory out of the path, then recreate it.
114	if err := os.Rename(dir, dir+"-old"); err != nil {
115		t.Fatal(err)
116	}
117	if err := os.MkdirAll(dir, 0700); err != nil {
118		t.Fatal(err)
119	}
120	save(t, path, "lock.after=2m\n")
121	if got := recv(t, out); got.Config.LockAfter != 2*time.Minute {
122		t.Fatal(got)
123	}
124	cancel()
125	if err := recv(t, done); err != nil {
126		t.Fatal(err)
127	}
128}
129
130func TestWatchRetargetToUnavailableThenReappearKeepsLastValid(t *testing.T) {
131	root := t.TempDir()
132	path := filepath.Join(root, "config")
133	save(t, path, "lock.after=1m\n")
134	out := make(chan ports.ConfigChanged, 4)
135	ready := make(chan struct{})
136	ctx, cancel := context.WithCancel(context.Background())
137	defer cancel()
138	done := make(chan error, 1)
139	go func() { done <- Watch(ctx, path, out, nil, ready) }()
140	recv(t, ready)
141	recv(t, out)
142	target := filepath.Join(t.TempDir(), "absent", "entry")
143	if err := os.Symlink(target, path+".link"); err != nil {
144		t.Fatal(err)
145	}
146	if err := os.Rename(path+".link", path); err != nil {
147		t.Fatal(err)
148	}
149	select {
150	case reset := <-out:
151		t.Fatalf("missing target reset last valid %+v", reset)
152	case err := <-done:
153		t.Fatalf("watch stopped on missing target %v", err)
154	case <-time.After(200 * time.Millisecond):
155	}
156	if err := os.MkdirAll(filepath.Dir(target), 0700); err != nil {
157		t.Fatal(err)
158	}
159	save(t, target, "lock.after=3m\n")
160	if got := recv(t, out); got.Config.LockAfter != 3*time.Minute {
161		t.Fatal(got)
162	}
163	cancel()
164	if err := recv(t, done); err != nil {
165		t.Fatal(err)
166	}
167}
168
169func TestWatchFirstCandidateClosesLoadStartupGapAndMissingCancellation(t *testing.T) {
170	path := filepath.Join(t.TempDir(), "config")
171	save(t, path, "lock.after=1m\n")
172	old, err := Load(path)
173	if err != nil {
174		t.Fatal(err)
175	}
176	save(t, path, "lock.after=2m\n") // save after app's Load, before Watch
177	out := make(chan ports.ConfigChanged, 1)
178	ready := make(chan struct{})
179	ctx, cancel := context.WithCancel(context.Background())
180	done := make(chan error, 1)
181	go func() { done <- Watch(ctx, path, out, nil, ready) }()
182	recv(t, ready)
183	if got := recv(t, out); got.Config == old || got.Config.LockAfter != 2*time.Minute {
184		t.Fatal(got)
185	}
186	cancel()
187	if err := recv(t, done); err != nil {
188		t.Fatal(err)
189	}
190	ctx, cancel = context.WithCancel(context.Background())
191	done = make(chan error, 1)
192	ready = make(chan struct{})
193	go func() { done <- Watch(ctx, filepath.Join(t.TempDir(), "never", "config"), out, nil, ready) }()
194	recv(t, ready)
195	cancel()
196	if err := recv(t, done); err != nil {
197		t.Fatal(err)
198	}
199}

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 2

Tags 9

↑↓ moveenter openesc close