detector_proc_test.go RawHistoryFolder

1package hints
2
3import (
4	"context"
5	"io"
6	"os"
7	"os/exec"
8	"strconv"
9	"strings"
10	"testing"
11	"time"
12
13	"github.com/stretchr/testify/require"
14)
15
16// The detector child of these tests is the test binary itself, re-executed:
17// a real process, started with the detector's own arguments. TestMain turns
18// it into helperChild when helperEnv is set.
19const (
20	helperEnv      = "NEFERWL_HINTS_HELPER"
21	helperPIDFile  = "NEFERWL_HINTS_HELPER_PIDFILE"
22	helperChildPID = "NEFERWL_HINTS_HELPER_CHILDPID"
23)
24
25func TestMain(m *testing.M) {
26	if mode := os.Getenv(helperEnv); mode != "" {
27		helperChild(mode)
28		os.Exit(0)
29	}
30	os.Exit(m.Run())
31}
32
33func helperHello() []byte {
34	return cat(magicHello[:], u32(protocolVersion), u32(0), u32(4), []byte("test"), u32(imgsz))
35}
36
37// helperChild plays the detector for mode:
38//
39//	nohello      never says hello
40//	silent       says hello, then never answers
41//	grandchild   like nohello, after spawning a grandchild that shares the
42//	             child's stdout, stderr and process group
43//	ok           says hello and answers every request with no box
44func helperChild(mode string) {
45	if f := os.Getenv(helperChildPID); f != "" {
46		_ = os.WriteFile(f, []byte(strconv.Itoa(os.Getpid())), 0o600)
47	}
48	if mode == "grandchild" {
49		gc := exec.Command("sleep", "60")
50		gc.Stdout, gc.Stderr = os.Stdout, os.Stderr
51		if err := gc.Start(); err != nil {
52			os.Exit(2)
53		}
54		_ = os.WriteFile(os.Getenv(helperPIDFile), []byte(strconv.Itoa(gc.Process.Pid)), 0o600)
55		mode = "nohello"
56	}
57	if mode != "nohello" {
58		_, _ = os.Stdout.Write(helperHello())
59	}
60	if mode != "ok" {
61		time.Sleep(time.Minute)
62		return
63	}
64	for {
65		var h [40]byte
66		if _, err := io.ReadFull(os.Stdin, h[:]); err != nil {
67			return
68		}
69		_, err := io.CopyN(io.Discard, os.Stdin, int64(le.Uint32(h[20:])*le.Uint32(h[24:])))
70		if err != nil {
71			return
72		}
73		_, _ = os.Stdout.Write(cat(magicResponse[:], u32(protocolVersion), h[8:16], u32(0), u32(0), u32(0)))
74	}
75}
76
77func helperDetector(t *testing.T, mode string) *procDetector {
78	t.Helper()
79	t.Setenv(helperEnv, mode)
80	d := newProcDetector(os.Args[0], t.TempDir(), 0.05)
81	d.helloTimeout, d.reqTimeout = 300*time.Millisecond, 300*time.Millisecond
82	t.Cleanup(func() { _ = d.Close() })
83	return d
84}
85
86func tinyFrame() detFrame {
87	return detFrame{width: 1, height: 1, stride: 4, m: &mapping{b: make([]byte, 4), live: new(int)}}
88}
89
90// processGone reports whether pid is dead (or a zombie waiting for its
91// reaper).
92func processGone(pid int) bool {
93	b, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/stat")
94	if err != nil {
95		return true
96	}
97	s := string(b)
98	return strings.HasPrefix(s[strings.LastIndexByte(s, ')')+2:], "Z")
99}
100
101func readPID(t *testing.T, file string) int {
102	t.Helper()
103	var pid int
104	require.Eventually(t, func() bool {
105		b, err := os.ReadFile(file)
106		if err != nil {
107			return false
108		}
109		pid, err = strconv.Atoi(string(b))
110		return err == nil
111	}, time.Second, 10*time.Millisecond)
112	return pid
113}
114
115func TestProcDetectorHungChildIsKilledAndRestarted(t *testing.T) {
116	for _, mode := range []string{"nohello", "silent"} {
117		t.Run(mode, func(t *testing.T) {
118			pidFile := t.TempDir() + "/child"
119			t.Setenv(helperChildPID, pidFile)
120			d := helperDetector(t, mode)
121			ctx := context.Background()
122			start := time.Now()
123			_, err := d.Detect(ctx, tinyFrame())
124			require.ErrorContains(t, err, "timed out")
125			if mode == "nohello" {
126				require.ErrorContains(t, err, "hello")
127			}
128			require.Less(t, time.Since(start), 2*time.Second)
129			require.Nil(t, d.proc)
130			pid := readPID(t, pidFile)
131			require.True(t, processGone(pid), "child %d survives", pid)
132
133			// The next Detect starts a new, healthy child.
134			t.Setenv(helperEnv, "ok")
135			t.Setenv(helperChildPID, "")
136			_, err = d.Detect(ctx, tinyFrame())
137			require.NoError(t, err)
138			require.NotNil(t, d.proc)
139			start = time.Now()
140			require.NoError(t, d.Close())
141			require.Less(t, time.Since(start), 3*time.Second)
142		})
143	}
144}
145
146// A grandchild holding the child's stdout and stderr open must neither
147// survive the kill nor keep Wait blocked.
148func TestProcDetectorKillsGrandchild(t *testing.T) {
149	pidFile := t.TempDir() + "/pid"
150	t.Setenv(helperPIDFile, pidFile)
151	d := helperDetector(t, "grandchild")
152	start := time.Now()
153	_, err := d.Detect(context.Background(), tinyFrame())
154	require.ErrorContains(t, err, "hello timed out")
155	require.Less(t, time.Since(start), 2500*time.Millisecond)
156	pid := readPID(t, pidFile)
157	require.Eventually(t, func() bool { return processGone(pid) }, 2*time.Second, 20*time.Millisecond, "grandchild %d survives", pid)
158	require.Nil(t, d.proc)
159}
160
161// Cancelling the context interrupts a pending read.
162func TestProcDetectorContextCancel(t *testing.T) {
163	d := helperDetector(t, "silent")
164	d.reqTimeout = time.Minute
165	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
166	defer cancel()
167	start := time.Now()
168	_, err := d.Detect(ctx, tinyFrame())
169	require.ErrorIs(t, err, context.DeadlineExceeded)
170	require.Less(t, time.Since(start), 2*time.Second)
171	require.Nil(t, d.proc)
172}

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 23

Tags 10

↑↓ moveenter openesc close