detached 5f5b000

run_linux_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3//go:build linux
4
5package gitremote
6
7import (
8	"context"
9	"os"
10	"os/exec"
11	"path/filepath"
12	"strconv"
13	"strings"
14	"sync"
15	"syscall"
16	"testing"
17	"time"
18
19	"github.com/stretchr/testify/assert"
20	"github.com/stretchr/testify/require"
21	"golang.org/x/sys/unix"
22)
23
24// recordKills replaces killGroup with one that records the groups it is asked
25// to kill, and does not kill them.
26func recordKills(t *testing.T, really bool) func() []int {
27	t.Helper()
28	var mu sync.Mutex
29	var got []int
30	old := killGroup
31	killGroup = func(pgid int) error {
32		mu.Lock()
33		got = append(got, pgid)
34		mu.Unlock()
35		if really {
36			return old(pgid)
37		}
38		return nil
39	}
40	t.Cleanup(func() { killGroup = old })
41	return func() []int {
42		mu.Lock()
43		defer mu.Unlock()
44		return append([]int(nil), got...)
45	}
46}
47
48func helperScript(t *testing.T) (cmd *exec.Cmd, pidFile string) {
49	t.Helper()
50	dir := t.TempDir()
51	pidFile = filepath.Join(dir, "pid")
52	// A leader with a helper in its group, which keeps its stdout open.
53	script := "sleep 30 &\necho $! > '" + pidFile + "'\nwait\n"
54	return exec.Command("/bin/sh", "-c", script), pidFile
55}
56
57func killPidFile(t *testing.T, pidFile string) {
58	t.Helper()
59	raw, err := os.ReadFile(pidFile)
60	if err != nil {
61		return
62	}
63	if pid, err := strconv.Atoi(strings.TrimSpace(string(raw))); err == nil {
64		_ = syscall.Kill(pid, syscall.SIGKILL)
65	}
66}
67
68// When waitid fails nothing proves that the leader is not reaped: the group
69// is not signalled, the leader alone is killed, and the error is returned.
70func TestRunDoesNotSignalTheGroupWhenWaitidFails(t *testing.T) {
71	calls := recordKills(t, true)
72	old := waitid
73	cmd, pidFile := helperScript(t)
74	// Fails once the helper is started, so that the test can tell it lives.
75	waitid = func(int, int, *unix.Siginfo, int, *unix.Rusage) error {
76		for i := 0; i < 200; i++ {
77			if _, err := os.Stat(pidFile); err == nil {
78				break
79			}
80			time.Sleep(10 * time.Millisecond)
81		}
82		return unix.ECHILD
83	}
84	t.Cleanup(func() { waitid = old })
85	t.Cleanup(func() { killPidFile(t, pidFile) })
86	cmd.WaitDelay = 100 * time.Millisecond
87	start := time.Now()
88	err := run(context.Background(), cmd)
89	require.ErrorIs(t, err, unix.ECHILD)
90	assert.Empty(t, calls(), "the group was signalled")
91	assert.Less(t, time.Since(start), 2*time.Second)
92	// The helper of the group was not killed.
93	require.Eventually(t, func() bool { _, err := os.Stat(pidFile); return err == nil }, time.Second, 10*time.Millisecond)
94	raw, _ := os.ReadFile(pidFile)
95	pid, _ := strconv.Atoi(strings.TrimSpace(string(raw)))
96	require.NoError(t, syscall.Kill(pid, 0), "the helper should still be alive")
97}
98
99// Same when the context is done: the group is signalled once, while the
100// leader is not reaped, and the leader is gone after run.
101func TestRunSignalsTheGroupBeforeTheLeaderIsReaped(t *testing.T) {
102	calls := recordKills(t, true)
103	cmd, pidFile := helperScript(t)
104	t.Cleanup(func() { killPidFile(t, pidFile) })
105	cmd.WaitDelay = time.Second
106	ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
107	defer cancel()
108	require.Error(t, run(ctx, cmd))
109	got := calls()
110	require.Len(t, got, 1)
111	assert.Equal(t, cmd.Process.Pid, got[0])
112	assert.True(t, cmd.ProcessState.Exited() || cmd.ProcessState.Sys() != nil)
113}

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 4

Tags 45

↑↓ moveenter openesc close