feat/hints a471fa4

cli_test.go RawHistoryFolder

1package cli
2
3import (
4	"bytes"
5	"context"
6	"errors"
7	"io"
8	"os"
9	"strconv"
10	"strings"
11	"testing"
12)
13
14// execute runs args like Main and returns the exit code and output.
15func execute(t *testing.T, args ...string) (code int, stdout, stderr string) {
16	t.Helper()
17	var out, errOut bytes.Buffer
18	env := Env{Stdout: &out, Stderr: &errOut, Version: "1.2.3"}
19	code = exitCode(&errOut, Execute(context.Background(), env, args))
20	return code, out.String(), errOut.String()
21}
22
23func TestMergeDebug(t *testing.T) {
24	if got := mergeDebug([]string{"core", "app"}, "render,core"); got != "core,app,render,core" {
25		t.Fatal(got)
26	}
27	if got := mergeDebug(nil, ""); got != "" {
28		t.Fatal(got)
29	}
30	if got := mergeDebug([]string{"core"}, ""); got != "core" {
31		t.Fatal(got)
32	}
33	if n := testing.AllocsPerRun(10, func() { _ = mergeDebug([]string{"core", "app"}, "render") }); n > 1 {
34		t.Fatalf("%v allocations, want 1", n)
35	}
36}
37
38func TestParseSizes(t *testing.T) {
39	sizes, err := parseSizes("1920x1080, 1280x720")
40	if err != nil || len(sizes) != 2 || sizes[1] != [2]int{1280, 720} {
41		t.Fatal(sizes, err)
42	}
43}
44
45// TestCommands checks the exit code and where each command writes: data
46// and help on stdout, an error exactly once on stderr.
47func TestCommands(t *testing.T) {
48	dir := t.TempDir()
49	cfg := dir + "/config"
50	badCfg := dir + "/bad"
51	stateFile := dir + "/state.json"
52	pid := strconv.Itoa(os.Getpid())
53	for path, data := range map[string]string{
54		cfg:       "",
55		badCfg:    "unknown.key = 1\n",
56		stateFile: `{"output":"DP-1","outputs":[{"name":"DP-1","active":1,"count":1}],"windows":[{"id":1,"pid":` + pid + `,"output":"DP-1"}]}`,
57	} {
58		if err := os.WriteFile(path, []byte(data), 0o600); err != nil {
59			t.Fatal(err)
60		}
61	}
62	t.Setenv("NEFERWL_STATE", stateFile)
63	for _, tc := range []struct {
64		args           []string
65		code           int
66		stdout, stderr string // substrings; "" means empty
67	}{
68		{[]string{"--help"}, 0, "neferwl state [output-of <pid>]", ""},
69		{[]string{"state", "-h"}, 0, "usage: neferwl state [output-of <pid>]", ""},
70		{[]string{"version"}, 0, "1.2.3\n", ""},
71		{[]string{"validate-config", cfg}, 0, "ok: " + cfg + "\n", ""},
72		{[]string{"validate-config", badCfg}, 1, "", "line 1"},
73		{[]string{"state", "output-of", pid}, 0, `{"name":"DP-1","active":1,"count":1,"workspace":"","workspace_id":""}` + "\n", ""},
74		{[]string{"state"}, 0, `"output":"DP-1"`, ""},
75		{[]string{"--unknown"}, 2, "", "flag provided but not defined"},
76		{[]string{"--backend=invalid"}, 2, "", "invalid backend"},
77		{[]string{"--backend=drm", "--screenshot=" + dir}, 2, "", "--screenshot requires"},
78		{[]string{"--backend=drm", "--headless-hdr"}, 2, "", "--headless-hdr requires"},
79		{[]string{"--backend=drm", "--input=-"}, 2, "", "--input requires"},
80		{[]string{"--backend=headless", "--screenshot=" + dir, "--screenshot-raw"}, 2, "", "--screenshot-raw requires"},
81		{[]string{"--backend=headless", "--headless-hdr", "--screenshot-raw"}, 2, "", "--screenshot-raw requires"},
82		{[]string{"--size=1920"}, 2, "", "--size"},
83		{[]string{"--timeout=-1s"}, 2, "", "negative timeout"},
84		{[]string{"--debug=nope"}, 2, "", "--debug"},
85		{[]string{"extra"}, 2, "", "unexpected arguments"},
86		{[]string{"--config=/nonexistent/neferwl.conf"}, 1, "", "no such file"},
87		{[]string{"validate-config", "/nonexistent/neferwl.conf"}, 1, "", "no such file"},
88		{[]string{"validate-config", "a", "b"}, 2, "", "usage: neferwl validate-config [path]"},
89		{[]string{"state", "output-of", "x"}, 2, "", "usage: neferwl state [output-of <pid>]"},
90		{[]string{"state", "extra"}, 2, "", "unexpected arguments"},
91		{[]string{"version", "extra"}, 2, "", "usage: neferwl version"},
92	} {
93		code, stdout, stderr := execute(t, tc.args...)
94		if code != tc.code {
95			t.Errorf("%v: exit %d, want %d (%s)", tc.args, code, tc.code, stderr)
96		}
97		if (tc.stdout == "") != (stdout == "") || !strings.Contains(stdout, tc.stdout) {
98			t.Errorf("%v: stdout %q, want %q", tc.args, stdout, tc.stdout)
99		}
100		switch {
101		case tc.stderr == "" && stderr != "":
102			t.Errorf("%v: unexpected stderr %q", tc.args, stderr)
103		case tc.stderr != "" && strings.Count(stderr, tc.stderr) != 1:
104			t.Errorf("%v: stderr %q has %q %d times, want once", tc.args, stderr, tc.stderr, strings.Count(stderr, tc.stderr))
105		}
106		if code == 2 && strings.Count(stderr, "usage:") != 1 {
107			t.Errorf("%v: stderr %q needs one usage line", tc.args, stderr)
108		}
109	}
110}
111
112func TestExitCode(t *testing.T) {
113	for _, tc := range []struct {
114		name  string
115		err   error
116		code  int
117		print bool
118	}{
119		{"canceled", context.Canceled, 0, false},
120		{"logged", loggedError{errors.New("boom")}, 1, false},
121		{"plain", errors.New("boom"), 1, true},
122	} {
123		var b bytes.Buffer
124		if code := exitCode(&b, tc.err); code != tc.code || (b.Len() > 0) != tc.print {
125			t.Errorf("%s: exit %d printed %q", tc.name, code, b.String())
126		}
127	}
128}
129
130func TestOpenScript(t *testing.T) {
131	file := t.TempDir() + "/script"
132	if err := os.WriteFile(file, []byte("quit\n"), 0o600); err != nil {
133		t.Fatal(err)
134	}
135	for _, tc := range []struct {
136		name, path string
137		check      func(t *testing.T, script io.ReadCloser, err error)
138	}{
139		{"none", "", func(t *testing.T, script io.ReadCloser, err error) {
140			if err != nil || script != nil {
141				t.Fatalf("got %#v, %v; want a nil interface", script, err)
142			}
143		}},
144		{"stdin", "-", func(t *testing.T, script io.ReadCloser, err error) {
145			if err != nil || script != io.ReadCloser(os.Stdin) {
146				t.Fatalf("got %#v, %v; want os.Stdin", script, err)
147			}
148		}},
149		{"missing", "/nonexistent/script", func(t *testing.T, script io.ReadCloser, err error) {
150			if err == nil || script != nil {
151				t.Fatalf("got %#v, %v; want a nil interface and an error", script, err)
152			}
153		}},
154		{"file", file, func(t *testing.T, script io.ReadCloser, err error) {
155			if err != nil || script == nil {
156				t.Fatalf("got %#v, %v", script, err)
157			}
158			if got, err := io.ReadAll(script); err != nil || string(got) != "quit\n" {
159				t.Fatalf("read %q, %v", got, err)
160			}
161		}},
162	} {
163		t.Run(tc.name, func(t *testing.T) {
164			script, closeScript, err := openScript(tc.path)
165			if closeScript == nil {
166				t.Fatal("nil close func")
167			}
168			tc.check(t, script, err)
169			closeScript()
170			if tc.path == "-" {
171				if _, err := os.Stdin.Stat(); err != nil {
172					t.Fatalf("stdin closed: %v", err)
173				}
174			}
175			if tc.name == "file" {
176				if _, err := script.Read(make([]byte, 1)); err == nil || err == io.EOF {
177					t.Fatalf("file left open: %v", err)
178				}
179			}
180		})
181	}
182}

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 25

Tags 10

↑↓ moveenter openesc close