package main import ( "bytes" "encoding/json" "errors" "strings" "testing" intdevice "netdome.biz/paul/qmk-rgb/internal/device" ) type deviceInfoOutput struct { Devices []keyboardLine `json:"devices"` Total int `json:"total"` } func stubDiscovery(t *testing.T, devices []intdevice.Device) { t.Helper() orig := discoverAll discoverAll = func() ([]intdevice.Device, error) { return devices, nil } t.Cleanup(func() { discoverAll = orig }) } // runRealKeyboardInfo executes the shipped command so a re-added // human-readable banner is caught, not just changes inside a helper. func runRealKeyboardInfo(t *testing.T) (stdout, stderr string, err error) { t.Helper() var out, errOut bytes.Buffer keyboardInfoCmd.SetOut(&out) keyboardInfoCmd.SetErr(&errOut) // Restored to nil rather than to whatever it was: OutOrStdout() answers // os.Stdout, and setting that back leaves the command with a writer of its // own, which a root's SetOut can no longer override. The next test that ran // this command through a root would then print its help to the terminal. t.Cleanup(func() { keyboardInfoCmd.SetOut(nil) keyboardInfoCmd.SetErr(nil) }) err = keyboardInfoCmd.RunE(keyboardInfoCmd, nil) return out.String(), errOut.String(), err } func TestKeyboardInfoWritesOnlyJSON(t *testing.T) { withJSON(t) stubDiscovery(t, []intdevice.Device{ {Index: 1, Path: "/dev/hidraw7", VendorID: 0x36b0, ProductID: 0x309f, Name: "Impact 80"}, }) stdout, stderr, err := runRealKeyboardInfo(t) if err != nil { t.Fatalf("keyboard info returned error: %v", err) } if stderr != "" { t.Errorf("keyboard info wrote %q to stderr, want no human-readable banner next to the JSON", stderr) } var got deviceInfoOutput if err := json.Unmarshal([]byte(stdout), &got); err != nil { t.Fatalf("stdout is not valid JSON: %v (output %q)", err, stdout) } if got.Total != 1 { t.Errorf("total = %d, want 1", got.Total) } if len(got.Devices) != 1 { t.Fatalf("devices = %d entries, want 1", len(got.Devices)) } if got.Devices[0].Index != 1 { t.Errorf("devices[0].index = %d, want 1", got.Devices[0].Index) } if got.Devices[0].Path != "/dev/hidraw7" { t.Errorf("devices[0].path = %q, want %q", got.Devices[0].Path, "/dev/hidraw7") } } func TestKeyboardInfoEmitsIndentedJSON(t *testing.T) { withJSON(t) stubDiscovery(t, []intdevice.Device{ {Index: 1, Path: "/dev/hidraw7", Name: "Impact 80"}, }) stdout, _, err := runRealKeyboardInfo(t) if err != nil { t.Fatalf("keyboard info returned error: %v", err) } if !strings.Contains(stdout, "\n \"devices\": [") { t.Errorf("stdout = %q, want two-space indented JSON like the other commands emit", stdout) } if !strings.HasSuffix(stdout, "\n") { t.Errorf("stdout = %q, want a trailing newline", stdout) } } func TestKeyboardInfoEmitsEmptyArrayNotNull(t *testing.T) { withJSON(t) stubDiscovery(t, nil) stdout, _, err := runRealKeyboardInfo(t) if err != nil { t.Fatalf("keyboard info returned error: %v", err) } if !strings.Contains(stdout, `"devices": []`) { t.Errorf("stdout = %q, want an empty array so consumers can iterate unconditionally", stdout) } } func TestKeyboardInfoPropagatesDiscoveryError(t *testing.T) { orig := discoverAll discoverAll = func() ([]intdevice.Device, error) { return nil, errStub } t.Cleanup(func() { discoverAll = orig }) _, _, err := runRealKeyboardInfo(t) if err == nil { t.Fatal("keyboard info expected discovery error, got nil") } if !strings.Contains(err.Error(), "enumerate") { t.Errorf("error = %v, want it to wrap the discovery failure", err) } } var errStub = errors.New("enumerate: stub failure") // A command that a test gave a writer of its own keeps it, and a writer set that // way outranks the root's. Restoring os.Stdout rather than nil therefore left the // shipped `keyboard info` writing to the terminal while a later test believed it // was capturing its help, and the failure showed up as noise in an unrelated // test's output rather than as a failing one. func TestKeyboardInfoHelpGoesWhereTheRootSendsIt(t *testing.T) { // In a subtest, so the restore has happened by the time the help runs: that // ordering is the whole defect, and doing both here would not test it. t.Run("a test gave it a writer", func(t *testing.T) { runRealKeyboardInfo(t) }) var out bytes.Buffer root := newRootCommand() registerCommands(root) root.SetOut(&out) root.SetErr(&out) root.SetArgs([]string{"keyboard", "info", "--help"}) if err := root.Execute(); err != nil { t.Fatalf("keyboard info --help returned error: %v", err) } if !strings.Contains(out.String(), "Usage:") { t.Errorf("captured %q, want the help, so the root's writer is in charge", out.String()) } }