Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion cmd/benchpod-cli/commands_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,13 @@ func runRegister(g *globalFlags, opts registerOptions) error {
return ctx.Err()
}
fmt.Fprint(os.Stderr, describeNotConnected(device.Name, host, opts.wait, status, pollErr))
return fmt.Errorf("pod did not connect within %s", opts.wait)
return fmt.Errorf("%w within %s", errRegisteredNotConnected, opts.wait)
}

// errRegisteredNotConnected: the registration stands and the pod keeps retrying on its own, it
// only did not reach the server in time (no cable yet, say). Setup carries on after it.
var errRegisteredNotConnected = errors.New("pod did not connect")

// cloudStatus is the part of the firmware's `cloud_status` reply that register reads.
// LastError is nil on firmware that predates the field.
type cloudStatus struct {
Expand Down
12 changes: 12 additions & 0 deletions cmd/benchpod-cli/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,18 @@ func withRefusalHint(err error) error {
return fmt.Errorf("%w (%s)", err, hint)
}

// withSavedConnectionHint adds where the address came from when the pod did not answer at the
// saved default (saved, "" when the run did not use it): after a pod is unplugged or another pod
// takes its place, that default is stale and only `discover` / `set-connection` fix it.
func withSavedConnectionHint(err error, saved string) error {
if saved == "" || !errors.Is(err, tcpclient.ErrUnreachable) {
return err
}
return fmt.Errorf("%w\n%s is the saved default connection. If that pod is unplugged or you use another one, "+
"run `benchpod discover` to list the pods, then `benchpod set-connection <address|usb:<name>>`, "+
"or pass --connection", err, saved)
}

// ── exit codes ──────────────────────────────────────────────────────────────

// The CLI's exit codes, so a script can tell a refusal from an unreachable pod. Documented in
Expand Down
9 changes: 8 additions & 1 deletion cmd/benchpod-cli/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,16 @@ func (g *globalFlags) rawConnection() (string, error) {
}
return "", fmt.Errorf("load config: %w", err)
}
return strings.TrimSpace(cfg.Connection), nil
savedConnectionUsed = strings.TrimSpace(cfg.Connection)
return savedConnectionUsed, nil
}

// savedConnectionUsed is the saved default this run fell back to ("" = none, or --connection /
// BENCHPOD_CONNECTION named the pod). An unreachable pod then gets a hint that the address came
// from the saved default: after a pod is swapped or moved it is a stale address, and the bare
// dial timeout does not say where it came from.
var savedConnectionUsed string

// resolveConnection classifies the effective connection (flag or stored default)
// into a ConnSpec for a command that only works over the network or USB; errors when
// no connection is set, the value is unusable, or it names a pod on embeddedci.com.
Expand Down
3 changes: 2 additions & 1 deletion cmd/benchpod-cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,12 @@ func Execute() int {

// run runs the CLI with args and returns its exit code.
func run(args []string) int {
savedConnectionUsed = ""
root := newRootCmd()
markUsageErrors(root)
root.SetArgs(args)
if err := root.Execute(); err != nil {
log.Printf("%v", withRefusalHint(err))
log.Printf("%v", withSavedConnectionHint(withRefusalHint(err), savedConnectionUsed))
return exitCode(err)
}
return exitOK
Expand Down
32 changes: 32 additions & 0 deletions cmd/benchpod-cli/saved_connection_hint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package main

import (
"errors"
"fmt"
"strings"
"testing"

"github.com/embeddedci-com/benchpod-cli/internal/tcpclient"
)

// An unreachable pod at the saved default says the address is the saved one and how to change it;
// an address the user passed, or another error, is left as it is.
func TestSavedConnectionHint(t *testing.T) {
dial := fmt.Errorf("status: %w", tcpclient.ErrUnreachable)
got := withSavedConnectionHint(dial, "10.237.8.85:8080").Error()
for _, want := range []string{"pod unreachable", "10.237.8.85:8080 is the saved default connection", "benchpod discover", "set-connection"} {
if !strings.Contains(got, want) {
t.Errorf("missing %q in %q", want, got)
}
}
if !errors.Is(withSavedConnectionHint(dial, "x"), tcpclient.ErrUnreachable) {
t.Error("the hint must keep the error's identity (exit code)")
}
if got := withSavedConnectionHint(dial, ""); got != dial {
t.Errorf("no saved default used: %v", got)
}
other := errors.New("refused: busy")
if got := withSavedConnectionHint(other, "10.0.0.1:8080"); got != other {
t.Errorf("not unreachable: %v", got)
}
}
7 changes: 6 additions & 1 deletion cmd/benchpod-cli/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,12 @@ func setupCloud(env setupEnv, ui *setupUI, o setupOpts, p setupPod, mv int) (str
}
}
if err := env.register(p.addr); err != nil {
return "", err
if !errors.Is(err, errRegisteredNotConnected) {
return "", err
}
// Registered; the pod connects once it has a network. Saving the connection (step 5)
// still matters, or the CLI keeps the previous default.
fmt.Fprintln(out, " Registered. It connects to embeddedci.com once it has a network; setup carries on.")
}
if c := env.cloudStatus(p.addr); c.known && c.DeviceID != "" {
id := strings.TrimSpace(c.DeviceID)
Expand Down
30 changes: 29 additions & 1 deletion cmd/benchpod-cli/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"errors"
"fmt"
"strings"
"testing"
"time"
Expand All @@ -25,6 +26,7 @@ type fakeSetupEnv struct {
profileMV int // la_mv in the stored wiring profile, 0 = none
saveErr error // what saveLaMV fails with
latest string // latest firmware release, "" = unknown
regErr error // what register returns after registering

calls []string
wifiSSID string
Expand Down Expand Up @@ -82,7 +84,7 @@ func (f *fakeSetupEnv) register(addr string) error {
f.calls = append(f.calls, "register")
f.registered = addr
f.cloud = f.afterReg
return nil
return f.regErr
}
func (f *fakeSetupEnv) save(target string) error {
f.calls = append(f.calls, "save")
Expand Down Expand Up @@ -374,6 +376,32 @@ func TestSetupSkipsTheProfileWhenNotOnTheAccount(t *testing.T) {
}
}

// A pod registered but not yet on the network (no cable) does not stop setup: step 5 still saves
// the connection, or the CLI keeps aiming at the previous default.
func TestSetupCarriesOnWhenTheRegisteredPodIsNotConnectedYet(t *testing.T) {
unreg := cloudState{known: true}
env := &fakeSetupEnv{
pods: []setupPod{{addr: "10.237.94.169:8080", cloud: unreg}}, cloud: unreg, laMV: 3300, who: "me@x.com",
afterReg: registeredMine, account: map[string]string{"dev-1": "bench-01"},
regErr: fmt.Errorf("%w within 30s", errRegisteredNotConnected),
}
out, err := runSetupWith(t, env, "", setupOpts{yes: true})
if err != nil {
t.Fatal(err)
}
if !env.did("save") || env.saved == "" || !strings.Contains(out, "embeddedci:bench-01") {
t.Fatalf("calls %v saved %q\n%s", env.calls, env.saved, out)
}
// Any other register error still stops it.
env = &fakeSetupEnv{
pods: []setupPod{{addr: "10.0.0.5:8080", cloud: unreg}}, cloud: unreg, laMV: 3300, who: "me@x.com",
regErr: errors.New("register the pod: 401"),
}
if _, err := runSetupWith(t, env, "", setupOpts{yes: true}); err == nil || env.did("save") {
t.Fatalf("err %v calls %v", err, env.calls)
}
}

func TestSetupSavesTheVoltageAfterRegistering(t *testing.T) {
unreg := cloudState{known: true}
env := &fakeSetupEnv{
Expand Down
Loading