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}