process_linux_test.go RawHistoryFolder

1//go:build linux
2
3package platform
4
5import (
6	"fmt"
7	"os"
8	"path/filepath"
9	"reflect"
10	"strconv"
11	"testing"
12)
13
14func TestProcessCwdRejectsInvalidPID(t *testing.T) {
15	if _, err := processCwd("/proc", 0); err == nil {
16		t.Fatal("processCwd accepted invalid pid")
17	}
18}
19
20func TestProcessArgv(t *testing.T) {
21	argv, err := ProcessArgv(os.Getpid())
22	if err != nil {
23		t.Fatalf("ProcessArgv current process: %v", err)
24	}
25	if len(argv) == 0 {
26		t.Fatal("ProcessArgv returned empty argv")
27	}
28}
29
30func TestProcessArgvRejectsEmptyCmdline(t *testing.T) {
31	path := filepath.Join(t.TempDir(), "cmdline")
32	if err := os.WriteFile(path, []byte("\x00\x00"), 0o600); err != nil {
33		t.Fatal(err)
34	}
35	if argv, err := processArgv(path, 123); err == nil || argv != nil {
36		t.Fatalf("processArgv empty = %#v, %v; want nil error", argv, err)
37	}
38}
39
40func TestSelectProcessGroupPID(t *testing.T) {
41	tests := []struct {
42		name    string
43		recs    []processRecord
44		pgid    int
45		shell   int
46		wantPID int
47		wantOK  bool
48	}{
49		{
50			name:    "selects other member",
51			recs:    []processRecord{{pid: 10, pgrp: 7}, {pid: 12, pgrp: 7}, {pid: 20, pgrp: 9}},
52			pgid:    7,
53			shell:   10,
54			wantPID: 12,
55			wantOK:  true,
56		},
57		{
58			name:    "bare shell has no candidate",
59			recs:    []processRecord{{pid: 10, pgrp: 7}, {pid: 20, pgrp: 9}},
60			pgid:    7,
61			shell:   10,
62			wantPID: 0,
63			wantOK:  false,
64		},
65	}
66
67	for _, tt := range tests {
68		t.Run(tt.name, func(t *testing.T) {
69			pid, ok := selectProcessGroupPID(tt.recs, tt.pgid, tt.shell)
70			if ok != tt.wantOK || pid != tt.wantPID {
71				t.Fatalf("selectProcessGroupPID = %d, %v; want %d, %v", pid, ok, tt.wantPID, tt.wantOK)
72			}
73		})
74	}
75}
76
77func TestProcessInspectorUsesConfiguredRoot(t *testing.T) {
78	root := t.TempDir()
79	writeProcStat(t, root, 10, 7)
80	pidDir := filepath.Join(root, "10")
81	cwdTarget := t.TempDir()
82	if err := os.Symlink(cwdTarget, filepath.Join(pidDir, "cwd")); err != nil {
83		t.Fatal(err)
84	}
85	if err := os.WriteFile(filepath.Join(pidDir, "comm"), []byte("custom\n"), 0o600); err != nil {
86		t.Fatal(err)
87	}
88	if err := os.WriteFile(filepath.Join(pidDir, "cmdline"), []byte("custom\x00arg\x00"), 0o600); err != nil {
89		t.Fatal(err)
90	}
91	ins := newProcessInspector(root)
92
93	cwd, err := ins.Cwd(10)
94	if err != nil || cwd != cwdTarget {
95		t.Fatalf("Cwd = %q, %v; want %q, nil", cwd, err, cwdTarget)
96	}
97	comm, err := ins.Comm(10)
98	if err != nil || comm != "custom" {
99		t.Fatalf("Comm = %q, %v; want custom, nil", comm, err)
100	}
101	argv, err := ins.Argv(10)
102	if err != nil || !reflect.DeepEqual(argv, []string{"custom", "arg"}) {
103		t.Fatalf("Argv = %#v, %v; want custom argv, nil", argv, err)
104	}
105}
106
107func TestProcessInspectorCachesProcessRecordsBriefly(t *testing.T) {
108	root := t.TempDir()
109	writeProcStat(t, root, 10, 7)
110	ins := newProcessInspector(root)
111
112	first, err := ins.processRecords()
113	if err != nil {
114		t.Fatal(err)
115	}
116	writeProcStat(t, root, 12, 7)
117	second, err := ins.processRecords()
118	if err != nil {
119		t.Fatal(err)
120	}
121	if len(first) != 1 || len(second) != 1 {
122		t.Fatalf("cached processRecords lengths = %d, %d; want 1, 1", len(first), len(second))
123	}
124}
125
126func writeProcStat(t *testing.T, root string, pid int, pgrp int) {
127	t.Helper()
128	dir := filepath.Join(root, strconv.Itoa(pid))
129	if err := os.MkdirAll(dir, 0o700); err != nil {
130		t.Fatal(err)
131	}
132	stat := fmt.Sprintf("%d (cmd) S 1 %d 1 0 0", pid, pgrp)
133	if err := os.WriteFile(filepath.Join(dir, "stat"), []byte(stat), 0o600); err != nil {
134		t.Fatal(err)
135	}
136}
137
138func TestParseStatPgrp(t *testing.T) {
139	got, err := parseStatPgrp("123 (cmd with spaces) S 1 456 456 0")
140	if err != nil || got != 456 {
141		t.Fatalf("parseStatPgrp = %d, %v; want 456, nil", got, err)
142	}
143}
144
145func TestParseKernProcArgs2(t *testing.T) {
146	tests := []struct {
147		name    string
148		data    []byte
149		want    []string
150		wantErr bool
151	}{
152		{
153			name: "argv after executable and padding",
154			data: append([]byte{2, 0, 0, 0}, []byte("/bin/tool\x00\x00tool\x00arg\x00ENV=value\x00")...),
155			want: []string{"tool", "arg"},
156		},
157		{
158			name: "preserves empty argument",
159			data: append([]byte{3, 0, 0, 0}, []byte("/bin/tool\x00tool\x00\x00arg\x00")...),
160			want: []string{"tool", "", "arg"},
161		},
162		{
163			name:    "truncated argc",
164			data:    []byte{1, 0, 0},
165			wantErr: true,
166		},
167		{
168			name:    "unterminated executable path",
169			data:    append([]byte{1, 0, 0, 0}, []byte("/bin/tool")...),
170			wantErr: true,
171		},
172		{
173			name:    "truncated argv",
174			data:    append([]byte{1, 0, 0, 0}, []byte("/bin/tool\x00tool")...),
175			wantErr: true,
176		},
177		{
178			name:    "negative argc",
179			data:    append([]byte{0xff, 0xff, 0xff, 0xff}, []byte("/bin/tool\x00tool\x00")...),
180			wantErr: true,
181		},
182		{
183			name:    "argc exceeds remaining data",
184			data:    append([]byte{0xff, 0xff, 0xff, 0x7f}, []byte("/bin/tool\x00tool\x00")...),
185			wantErr: true,
186		},
187	}
188
189	for _, tt := range tests {
190		t.Run(tt.name, func(t *testing.T) {
191			got, err := parseKernProcArgs2(tt.data)
192			if (err != nil) != tt.wantErr {
193				t.Fatalf("parseKernProcArgs2 error = %v, wantErr %v", err, tt.wantErr)
194			}
195			if !reflect.DeepEqual(got, tt.want) {
196				t.Fatalf("parseKernProcArgs2 = %#v; want %#v", got, tt.want)
197			}
198		})
199	}
200}
201
202func TestProcessArgvFileParsing(t *testing.T) {
203	tests := []struct {
204		name string
205		data []byte
206		want []string
207	}{
208		{
209			name: "splits nul-delimited argv",
210			data: []byte("cmd\x00two words\x00"),
211			want: []string{"cmd", "two words"},
212		},
213		{
214			name: "preserves empty argument",
215			data: []byte("cmd\x00\x00arg\x00"),
216			want: []string{"cmd", "", "arg"},
217		},
218	}
219
220	for _, tt := range tests {
221		t.Run(tt.name, func(t *testing.T) {
222			path := filepath.Join(t.TempDir(), "cmdline")
223			if err := os.WriteFile(path, tt.data, 0o600); err != nil {
224				t.Fatal(err)
225			}
226			got, err := processArgv(path, 123)
227			if err != nil {
228				t.Fatal(err)
229			}
230			if !reflect.DeepEqual(got, tt.want) {
231				t.Fatalf("processArgv = %#v; want %#v", got, tt.want)
232			}
233		})
234	}
235}

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 12

Tags 11

↑↓ moveenter openesc close