v0.2.0 80c61e3

runner_test.go RawHistoryFolder

1package shellcmd
2
3import (
4	"context"
5	"errors"
6	"strings"
7	"testing"
8	"time"
9
10	"github.com/bnema/vev/internal/ports"
11	"github.com/stretchr/testify/require"
12)
13
14func TestRunnerRun(t *testing.T) {
15	tests := []struct {
16		name    string
17		command string
18		limit   int
19		want    string
20	}{
21		{name: "success", command: "printf 'ok'", limit: 1024, want: "ok"},
22		{name: "bounded stdout", command: "yes a | head -c 4096", limit: 8, want: "a\na\na\na\n"},
23		{name: "stderr does not leak into stdout", command: "printf out; printf err >&2", limit: 1024, want: "out"},
24		{name: "zero limit captures nothing", command: "printf output", limit: 0, want: ""},
25		{name: "negative limit captures nothing", command: "printf output", limit: -1, want: ""},
26	}
27	for _, tt := range tests {
28		t.Run(tt.name, func(t *testing.T) {
29			ctx, cancel := context.WithTimeout(context.Background(), time.Second)
30			defer cancel()
31
32			got, err := New().Run(ctx, ports.CommandSpec{Command: tt.command, Timeout: time.Second, StdoutLimit: tt.limit})
33			if err != nil {
34				t.Fatalf("Run() error = %v", err)
35			}
36			if string(got.Stdout) != tt.want {
37				t.Fatalf("Run() = %q, want %q", string(got.Stdout), tt.want)
38			}
39		})
40	}
41}
42
43func TestRunnerRunReturnsPartialStdoutOnError(t *testing.T) {
44	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
45	defer cancel()
46
47	got, err := New().Run(ctx, ports.CommandSpec{Command: "printf 'abcdef'; exit 7", Timeout: time.Second, StdoutLimit: 3})
48	if err == nil {
49		t.Fatal("Run() error = nil, want non-zero exit error")
50	}
51	if string(got.Stdout) != "abc" {
52		t.Fatalf("Run() = %q, want bounded partial stdout %q", string(got.Stdout), "abc")
53	}
54}
55
56func TestRunnerRunTimeoutReturnsPromptly(t *testing.T) {
57	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
58	defer cancel()
59	start := time.Now()
60
61	_, err := New().Run(ctx, ports.CommandSpec{Command: "sleep 1", Timeout: 50 * time.Millisecond, StdoutLimit: 1024})
62
63	if err == nil {
64		t.Fatal("Run() error = nil, want timeout or wait error")
65	}
66	if !errors.Is(ctx.Err(), context.DeadlineExceeded) {
67		t.Fatalf("context error = %v, want DeadlineExceeded", ctx.Err())
68	}
69	if elapsed := time.Since(start); elapsed > 500*time.Millisecond {
70		t.Fatalf("Run() elapsed = %s, want <= 500ms", elapsed)
71	}
72}
73
74func TestRunnerRunBackgroundChildDoesNotWedgeAfterTimeout(t *testing.T) {
75	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
76	defer cancel()
77	start := time.Now()
78
79	_, err := New().Run(ctx, ports.CommandSpec{Command: "sleep 1 &", Timeout: 50 * time.Millisecond, StdoutLimit: 1024})
80
81	if err == nil {
82		t.Fatal("Run() error = nil, want timeout or wait error")
83	}
84	if !errors.Is(ctx.Err(), context.DeadlineExceeded) {
85		t.Fatalf("context error = %v, want DeadlineExceeded", ctx.Err())
86	}
87	if elapsed := time.Since(start); elapsed > 500*time.Millisecond {
88		t.Fatalf("Run() elapsed = %s, want <= 500ms", elapsed)
89	}
90}
91
92func TestRunnerRunUsesProvidedEnvironment(t *testing.T) {
93	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
94	defer cancel()
95
96	got, err := New().Run(ctx, ports.CommandSpec{
97		Command:     "printf '%s' \"$VEV_TEST_VALUE\"",
98		Env:         []string{"PATH=/bin:/usr/bin", "VEV_TEST_VALUE=from-env"},
99		Timeout:     time.Second,
100		StdoutLimit: 1024,
101	})
102	if err != nil {
103		t.Fatalf("Run() error = %v", err)
104	}
105	if strings.TrimSpace(string(got.Stdout)) != "from-env" {
106		t.Fatalf("Run() env output = %q, want from-env", string(got.Stdout))
107	}
108}
109
110func TestRunnerCapturesStderrAndExitCode(t *testing.T) {
111	tests := []struct {
112		name         string
113		command      string
114		wantExitCode int
115		wantStderr   string
116		wantStdout   string
117	}{
118		{
119			name:         "command not found reports 127 and stderr",
120			command:      "definitely-not-a-real-command-xyz",
121			wantExitCode: 127,
122			wantStderr:   "not found",
123		},
124		{
125			name:         "explicit exit code with stderr",
126			command:      "echo oops >&2; exit 3",
127			wantExitCode: 3,
128			wantStderr:   "oops",
129		},
130		{
131			name:         "success reports zero and stdout",
132			command:      "echo hello",
133			wantExitCode: 0,
134			wantStdout:   "hello",
135		},
136	}
137
138	for _, tc := range tests {
139		t.Run(tc.name, func(t *testing.T) {
140			res, err := New().Run(context.Background(), ports.CommandSpec{
141				Command:     tc.command,
142				Timeout:     5 * time.Second,
143				StdoutLimit: 1024,
144				StderrLimit: 1024,
145			})
146			if tc.wantExitCode == 0 {
147				require.NoError(t, err)
148			} else {
149				require.Error(t, err)
150			}
151			require.Equal(t, tc.wantExitCode, res.ExitCode)
152			if tc.wantStderr != "" {
153				require.Contains(t, string(res.Stderr), tc.wantStderr)
154			}
155			if tc.wantStdout != "" {
156				require.Contains(t, string(res.Stdout), tc.wantStdout)
157			}
158		})
159	}
160}
161
162func TestRunnerBoundsStderr(t *testing.T) {
163	res, _ := New().Run(context.Background(), ports.CommandSpec{
164		Command:     "yes badness 2>/dev/null | head -c 100000 >&2",
165		Timeout:     5 * time.Second,
166		StdoutLimit: 1024,
167		StderrLimit: 64,
168	})
169	require.Equal(t, 64, len(res.Stderr), "stderr must be captured and truncated to StderrLimit")
170}

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