v0.3.2 a514042

launcher_test.go RawHistoryFolder

1package visualproc
2
3import (
4	"context"
5	"io"
6	"os"
7	"os/exec"
8	"syscall"
9	"testing"
10	"time"
11
12	"github.com/bnema/neferafk/internal/ports"
13	"github.com/bnema/zerowrap"
14)
15
16// TestMain doubles as the child process: with NEFERAFK_TEST_CHILD set the test
17// binary behaves like a tiny visual on fds 3/4 instead of running tests.
18func TestMain(m *testing.M) {
19	switch os.Getenv("NEFERAFK_TEST_CHILD") {
20	case "":
21		os.Exit(m.Run())
22	case "echo": // confirm the lock it is asked for, then exit cleanly
23		if os.Getenv("NEFERAFK_TEST_PIN") != "" {
24			os.Exit(9) // the omitted variable leaked into the child
25		}
26		cmd, err := ReadCommand(os.NewFile(3, "cmd"))
27		lock, ok := cmd.(ports.VisualLock)
28		if err != nil || !ok || string(lock.Auth.EnvPIN) != "123456" {
29			os.Exit(10)
30		}
31		if WriteEvent(os.NewFile(4, "ev"), ports.LockConfirmed{Generation: lock.Generation}) != nil {
32			os.Exit(11)
33		}
34		os.Exit(0)
35	case "crash":
36		os.Exit(7)
37	}
38}
39
40func launcher(mode string) *Launcher {
41	exe, _ := os.Executable()
42	return &Launcher{Executable: exe, Env: append(os.Environ(), "NEFERAFK_TEST_CHILD="+mode, "NEFERAFK_TEST_PIN=123456"), Log: zerowrap.New(zerowrap.Config{Output: io.Discard})}
43}
44
45func next(t *testing.T, s ports.VisualSession) ports.VisualEvent {
46	t.Helper()
47	select {
48	case ev, ok := <-s.Events():
49		if !ok {
50			t.Fatal("events closed")
51		}
52		return ev
53	case <-time.After(5 * time.Second):
54		t.Fatal("no event")
55		return nil
56	}
57}
58
59func TestLauncherPrivatePipesAndEnvOmission(t *testing.T) {
60	s, err := launcher("echo").Launch(context.Background(), ports.VisualSpawn{OmitEnv: []string{"NEFERAFK_TEST_PIN"}})
61	if err != nil {
62		t.Fatal(err)
63	}
64	defer s.Close()
65	if err := s.Send(ports.VisualLock{Generation: 5, Auth: ports.AuthBootstrap{Generation: 5, EnvPIN: []byte("123456")}}); err != nil {
66		t.Fatal(err)
67	}
68	if ev := next(t, s); ev != (ports.LockConfirmed{Generation: 5}) {
69		t.Fatalf("%#v", ev)
70	}
71	ev := next(t, s)
72	if x, ok := ev.(ports.VisualExited); !ok || x.Err != nil {
73		t.Fatalf("clean exit reported as %#v", ev)
74	}
75	if _, ok := <-s.Events(); ok {
76		t.Fatal("events not closed after exit")
77	}
78}
79
80func TestLauncherReportsCrashAsExitNeverAsUnlock(t *testing.T) {
81	s, err := launcher("crash").Launch(context.Background(), ports.VisualSpawn{})
82	if err != nil {
83		t.Fatal(err)
84	}
85	defer s.Close()
86	ev := next(t, s)
87	if x, ok := ev.(ports.VisualExited); !ok || x.Err == nil {
88		t.Fatalf("crash reported as %#v", ev)
89	}
90}
91
92func TestLauncherDetachLeavesChildAndSecondCloseIsSafe(t *testing.T) {
93	s, err := launcher("echo").Launch(context.Background(), ports.VisualSpawn{OmitEnv: []string{"NEFERAFK_TEST_PIN"}})
94	if err != nil {
95		t.Fatal(err)
96	}
97	if err := s.Detach(); err != nil {
98		t.Fatal(err)
99	}
100	_ = s.Detach()
101}
102
103func TestLauncherLaunchFailure(t *testing.T) {
104	if _, err := (&Launcher{Executable: "/nonexistent/neferafk", Log: zerowrap.New(zerowrap.Config{Output: io.Discard})}).Launch(context.Background(), ports.VisualSpawn{}); err == nil {
105		t.Fatal("missing executable launched")
106	}
107}
108
109// sleeper starts an unrelated process in its own group standing in for a
110// reused pid/pgid.
111func sleeper(t *testing.T) *exec.Cmd {
112	t.Helper()
113	c := exec.Command("sleep", "30")
114	c.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
115	if err := c.Start(); err != nil {
116		t.Skipf("sleep unavailable: %v", err)
117	}
118	t.Cleanup(func() { _ = c.Process.Kill(); _ = c.Wait() })
119	return c
120}
121
122func alive(pid int) bool { return syscall.Kill(pid, 0) == nil }
123
124func TestCloseAfterReapNeverSignalsTheGroup(t *testing.T) {
125	victim := sleeper(t)
126	_, w, err := os.Pipe()
127	if err != nil {
128		t.Fatal(err)
129	}
130	exited := make(chan struct{})
131	close(exited) // the visual was already reaped: its pgid may be reused
132	s := &session{cmd: victim, w: w, exited: exited, log: zerowrap.New(zerowrap.Config{Output: io.Discard})}
133	if err := s.Close(); err != nil {
134		t.Fatal(err)
135	}
136	if err := s.Close(); err != nil { // idempotent
137		t.Fatal(err)
138	}
139	time.Sleep(50 * time.Millisecond)
140	if !alive(victim.Process.Pid) {
141		t.Fatal("Close signalled a process group after the child was reaped")
142	}
143}
144
145func TestCloseBeforeReapKillsTheGroup(t *testing.T) {
146	child := sleeper(t)
147	_, w, _ := os.Pipe()
148	s := &session{cmd: child, w: w, exited: make(chan struct{}), log: zerowrap.New(zerowrap.Config{Output: io.Discard})}
149	if err := s.Close(); err != nil {
150		t.Fatal(err)
151	}
152	done := make(chan error, 1)
153	go func() { done <- child.Wait() }()
154	select {
155	case <-done:
156	case <-time.After(5 * time.Second):
157		t.Fatal("live child group not killed")
158	}
159}

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