Skip to content

Commit 0812686

Browse files
committed
Make the first contact work: usage, and honest sidecar resolution
`devicedeck --help` printed the version and nothing else. That is the first command anyone types against a tarball, and it told them neither what the binary does nor that `serve` exists. It now prints usage — on --help, on no arguments, and on an unknown command, which exits 2 with the message on stderr. Testing the first-run paths also turned up a quieter problem: a sidecar path given explicitly, by flag or environment variable, was treated as a hint. If it did not exist, discovery carried on and found a different binary, so a typo ran a build nobody asked for and said nothing. A named path is now honoured or refused, and the refusal names both the source and the path. CI runs build, vet, gofmt and the race suite on macOS, plus the Swift sidecar build and its tests. It deliberately excludes the device suites: those need a booted simulator and an emulator, which no hosted runner has, and pretending otherwise would produce a green tick that means less than it appears to.
1 parent 7030648 commit 0812686

4 files changed

Lines changed: 157 additions & 2 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Go build, vet, format and tests. Deliberately not the device suites:
2+
# those need a booted simulator and an emulator, which no hosted runner
3+
# has — so this covers what can honestly be checked without hardware,
4+
# and the device work stays a local gate.
5+
name: CI
6+
7+
on:
8+
push:
9+
branches: [main]
10+
pull_request:
11+
12+
jobs:
13+
go:
14+
# macOS because the sidecars and the simulator paths are macOS-only,
15+
# even though this job does not boot a device.
16+
runs-on: macos-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: actions/setup-go@v5
20+
with:
21+
go-version-file: go.mod
22+
cache: true
23+
24+
- name: Format
25+
run: |
26+
unformatted=$(gofmt -l .)
27+
if [ -n "$unformatted" ]; then
28+
echo "these files need gofmt:"; echo "$unformatted"; exit 1
29+
fi
30+
31+
- name: Vet
32+
run: go vet ./...
33+
34+
- name: Test
35+
run: go test ./... -race -coverprofile=coverage.out
36+
37+
- name: Coverage
38+
run: go tool cover -func=coverage.out | tail -1
39+
40+
sidecar:
41+
runs-on: macos-latest
42+
steps:
43+
- uses: actions/checkout@v4
44+
- name: Build the Swift sidecars
45+
run: make sidecar
46+
- name: Sidecar tests
47+
run: make sidecar-test

‎cmd/devicedeck/main.go‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package main
55

66
import (
77
"fmt"
8+
"io"
89
"os"
910

1011
"github.com/devicelab-dev/DeviceDeck/internal/version"
@@ -28,5 +29,45 @@ func main() {
2829
}
2930
return
3031
}
31-
fmt.Fprintln(os.Stdout, version.Line())
32+
switch arg(1) {
33+
case "version", "--version", "-version", "-v":
34+
fmt.Fprintln(os.Stdout, version.Line())
35+
case "", "help", "--help", "-help", "-h":
36+
usage(os.Stdout)
37+
default:
38+
fmt.Fprintf(os.Stderr, "devicedeck: unknown command %q\n\n", os.Args[1])
39+
usage(os.Stderr)
40+
os.Exit(2)
41+
}
42+
}
43+
44+
// arg returns os.Args[i], or "" when it was not given, so the dispatch
45+
// reads the same whether or not a command was typed.
46+
func arg(i int) string {
47+
if len(os.Args) > i {
48+
return os.Args[i]
49+
}
50+
return ""
51+
}
52+
53+
// usage is what someone sees when they run the binary with no idea what
54+
// it does — which, for a tool distributed as a tarball, is most first
55+
// contacts. It names the one command that matters and where to go next.
56+
func usage(w io.Writer) {
57+
fmt.Fprintf(w, `%s
58+
59+
Streams iOS Simulators and Android emulators to a browser, and turns a
60+
session driven by hand into a replayable test.
61+
62+
Usage:
63+
devicedeck serve [flags] start the server and console
64+
devicedeck version print the version
65+
devicedeck help print this message
66+
67+
Run "devicedeck serve --help" for the server's flags.
68+
69+
Once running, open the console at the address it prints (by default
70+
http://127.0.0.1:8787) to pick a device. Point your own tests at
71+
/device/{udid} and drive it with ordinary web selectors.
72+
`, version.Line())
3273
}

‎cmd/devicedeck/serve.go‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,24 @@ func defaultRunnerHome() {
118118
// Swift build output (developer setup).
119119
func resolveBinary(name, explicit string) (string, error) {
120120
envVar := strings.ToUpper(strings.ReplaceAll(name, "-", "_")) // devicedeck-hid → DEVICEDECK_HID
121-
candidates := []string{explicit, os.Getenv(envVar)}
121+
// A path given deliberately must be honoured or refused, never
122+
// quietly replaced: falling through to a different binary than the
123+
// one someone named turns a typo into a mystery, and it would run
124+
// the wrong build without ever saying so.
125+
for _, named := range []struct{ source, path string }{
126+
{"--sidecar/--video-sidecar", explicit},
127+
{envVar, os.Getenv(envVar)},
128+
} {
129+
if named.path == "" {
130+
continue
131+
}
132+
if info, err := os.Stat(named.path); err != nil || info.IsDir() {
133+
return "", fmt.Errorf("%s points at %q, which is not an executable file (%s)",
134+
named.source, named.path, name)
135+
}
136+
return named.path, nil
137+
}
138+
var candidates []string
122139
if exe, err := os.Executable(); err == nil {
123140
candidates = append(candidates, filepath.Join(filepath.Dir(exe), name))
124141
}

‎cmd/devicedeck/serve_test.go‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"os"
55
"path/filepath"
6+
"strings"
67
"testing"
78
)
89

@@ -90,3 +91,52 @@ func TestDefaultRunnerHome(t *testing.T) {
9091
}
9192
})
9293
}
94+
95+
// A path someone named explicitly must be honoured or refused. Falling
96+
// through to a different binary would run a build they did not ask for
97+
// and never mention it — a typo becomes a mystery.
98+
func TestResolveBinaryRefusesBadExplicitPath(t *testing.T) {
99+
// A working discovery path exists, so a fallthrough would silently
100+
// succeed and hide the mistake.
101+
root := t.TempDir()
102+
release := filepath.Join(root, "sidecar/.build/release")
103+
if err := os.MkdirAll(release, 0o755); err != nil {
104+
t.Fatal(err)
105+
}
106+
writeStub(t, release, "devicedeck-hid")
107+
t.Chdir(root)
108+
109+
_, err := resolveBinary("devicedeck-hid", "/nonexistent/hid")
110+
if err == nil {
111+
t.Fatal("a named path that does not exist must be refused")
112+
}
113+
if !strings.Contains(err.Error(), "/nonexistent/hid") {
114+
t.Errorf("error should name the offending path: %v", err)
115+
}
116+
117+
t.Setenv("DEVICEDECK_HID", "/also/missing")
118+
if _, err := resolveBinary("devicedeck-hid", ""); err == nil ||
119+
!strings.Contains(err.Error(), "DEVICEDECK_HID") {
120+
t.Errorf("a bad env path should be refused and named: %v", err)
121+
}
122+
}
123+
124+
// A directory is not an executable, however real the path is.
125+
func TestResolveBinaryRefusesDirectory(t *testing.T) {
126+
dir := t.TempDir()
127+
if _, err := resolveBinary("devicedeck-hid", dir); err == nil {
128+
t.Fatal("a directory must not resolve as the sidecar")
129+
}
130+
}
131+
132+
// The usage text is what a first contact sees; it has to name the
133+
// command that matters and where the console appears.
134+
func TestUsageNamesTheEssentials(t *testing.T) {
135+
var b strings.Builder
136+
usage(&b)
137+
for _, want := range []string{"devicedeck serve", "127.0.0.1:8787", "/device/", "version"} {
138+
if !strings.Contains(b.String(), want) {
139+
t.Errorf("usage does not mention %q:\n%s", want, b.String())
140+
}
141+
}
142+
}

0 commit comments

Comments
 (0)