| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- 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"`
- Others []intdevice.HIDDevice `json:"otherHidDevices"`
- }
- // stubDiscovery replaces both enumerations the command makes. Only the
- // keyboards are of interest to most tests, so the devices that were passed over
- // are stubbed empty as well: leaving that one real would make the output depend
- // on what is plugged into the machine running the tests.
- func stubDiscovery(t *testing.T, devices []intdevice.Device) {
- t.Helper()
- orig, origOther := discoverAll, discoverOther
- discoverAll = func() ([]intdevice.Device, error) { return devices, nil }
- discoverOther = func() ([]intdevice.HIDDevice, error) { return nil, nil }
- t.Cleanup(func() { discoverAll, discoverOther = orig, origOther })
- }
- // 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)
- }
- if !strings.Contains(stdout, `"otherHidDevices": []`) {
- t.Errorf("stdout = %q, want the passed-over devices as an empty array, not null", stdout)
- }
- }
- // An empty result is the one case a user cannot read on their own: it looks the
- // same whether the keyboard is unplugged or its lighting is on a HID collection
- // this tool does not address. So the listing names the devices that are there,
- // with the usage pages they do expose — the missing 0xFF60/0x61 is the finding.
- func TestKeyboardInfoEmptyResultNamesTheDevicesPassedOver(t *testing.T) {
- stubDiscovery(t, nil)
- stubOtherDiscovery(t, []intdevice.HIDDevice{
- {
- VendorID: 0x320f,
- ProductID: 0x5044,
- Name: "GMMK Pro",
- UsagePairs: []intdevice.UsagePair{
- {UsagePage: 0x0001, Usage: 0x06},
- {UsagePage: 0xff80, Usage: 0x61},
- },
- },
- })
- stdout, _, err := runRealKeyboardInfo(t)
- if err != nil {
- t.Fatalf("keyboard info returned error: %v", err)
- }
- for _, want := range []string{
- "No keyboard with the QMK Raw HID interface found",
- "usage page 0xFF60, usage 0x61",
- "0x320F/0x5044",
- "GMMK Pro",
- "0xFF80/0x61",
- } {
- if !strings.Contains(stdout, want) {
- t.Errorf("stdout = %q, want it to contain %q", stdout, want)
- }
- }
- }
- // The preamble says what was looked for and why a board has it, and it must not
- // claim VIA was checked: nothing is opened here, and VIA is never what the
- // collection filter asks about. A sentence that says it did would send a user
- // looking for a probe that does not exist.
- func TestKeyboardInfoEmptyResultDoesNotClaimToHaveCheckedVIA(t *testing.T) {
- stubDiscovery(t, nil)
- stubOtherDiscovery(t, nil)
- stdout, _, err := runRealKeyboardInfo(t)
- if err != nil {
- t.Fatalf("keyboard info returned error: %v", err)
- }
- if !strings.Contains(stdout, "VIA's build cannot be built without it") {
- t.Errorf("stdout = %q, want it to say why a QMK firmware has the collection", stdout)
- }
- if strings.Contains(stdout, "no VIA") || strings.Contains(stdout, "does not support VIA") {
- t.Errorf("stdout = %q, want no claim about VIA: this command never opened the board", stdout)
- }
- }
- // And the opposite: a keyboard was found, so the devices that were passed over
- // are only ever a dock or a mouse, and a listing beside the line the user came
- // for buries it.
- func TestKeyboardInfoListsOtherDevicesOnlyWhenNoneWasFound(t *testing.T) {
- stubDiscovery(t, []intdevice.Device{
- {Index: 1, Path: "/dev/hidraw7", VendorID: 0x36b0, ProductID: 0x309f, Name: "Impact 80"},
- })
- stubOtherDiscovery(t, []intdevice.HIDDevice{
- {VendorID: 0x046d, ProductID: 0xc041, Name: "USB Gaming Mouse",
- UsagePairs: []intdevice.UsagePair{{UsagePage: 0xff00, Usage: 0x01}}},
- })
- stdout, _, err := runRealKeyboardInfo(t)
- if err != nil {
- t.Fatalf("keyboard info returned error: %v", err)
- }
- if !strings.Contains(stdout, "Impact 80") {
- t.Errorf("stdout = %q, want the keyboard that was found", stdout)
- }
- if strings.Contains(stdout, "USB Gaming Mouse") {
- t.Errorf("stdout = %q, want no listing of passed-over devices when a keyboard was found", stdout)
- }
- }
- // With nothing found and nothing else connected, the sentence has to say that
- // rather than print a heading with nothing under it.
- func TestKeyboardInfoEmptyResultWithNoHIDDevicesAtAll(t *testing.T) {
- stubDiscovery(t, nil)
- stubOtherDiscovery(t, nil)
- stdout, _, err := runRealKeyboardInfo(t)
- if err != nil {
- t.Fatalf("keyboard info returned error: %v", err)
- }
- if !strings.Contains(stdout, "No other HID device is connected") {
- t.Errorf("stdout = %q, want it to say that no HID device is connected", stdout)
- }
- if strings.Contains(stdout, "These HID devices are connected") {
- t.Errorf("stdout = %q, want no heading for a list that is not there", stdout)
- }
- }
- // A consumer asking "why not this one" reads the JSON, so the devices that were
- // passed over are in it whether or not a keyboard was found.
- func TestKeyboardInfoJSONCarriesTheOtherHIDDevicesWhateverItFinds(t *testing.T) {
- withJSON(t)
- stubDiscovery(t, []intdevice.Device{
- {Index: 1, Path: "/dev/hidraw7", VendorID: 0x36b0, ProductID: 0x309f, Name: "Impact 80"},
- })
- stubOtherDiscovery(t, []intdevice.HIDDevice{
- {VendorID: 0x320f, ProductID: 0x5044, Name: "GMMK Pro",
- UsagePairs: []intdevice.UsagePair{{UsagePage: 0xff80, Usage: 0x61}}},
- })
- stdout, _, err := runRealKeyboardInfo(t)
- if err != nil {
- t.Fatalf("keyboard info returned error: %v", err)
- }
- 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 len(got.Devices) != 1 {
- t.Fatalf("devices = %d entries, want 1", len(got.Devices))
- }
- if len(got.Others) != 1 {
- t.Fatalf("otherHidDevices = %d entries, want 1", len(got.Others))
- }
- if got.Others[0].VendorID != 0x320f {
- t.Errorf("otherHidDevices[0].vendorId = %04X, want 320F", got.Others[0].VendorID)
- }
- if len(got.Others[0].UsagePairs) != 1 || got.Others[0].UsagePairs[0].UsagePage != 0xff80 {
- t.Errorf("otherHidDevices[0].usagePages = %+v, want the page it was passed over for", got.Others[0].UsagePairs)
- }
- }
- // The passed-over devices are never selectable, so they carry no index and none
- // is printed: a number there would be one --device does not take.
- func TestKeyboardInfoGivesNoNumberToADeviceThatCannotBeSelected(t *testing.T) {
- stubDiscovery(t, nil)
- stubOtherDiscovery(t, []intdevice.HIDDevice{
- {VendorID: 0x320f, ProductID: 0x5044, Name: "GMMK Pro",
- UsagePairs: []intdevice.UsagePair{{UsagePage: 0xff80, Usage: 0x61}}},
- })
- stdout, _, err := runRealKeyboardInfo(t)
- if err != nil {
- t.Fatalf("keyboard info returned error: %v", err)
- }
- for _, line := range strings.Split(stdout, "\n") {
- if !strings.Contains(line, "0x320F/0x5044") {
- continue
- }
- if strings.HasPrefix(strings.TrimSpace(line), "1 ") {
- t.Errorf("stdout = %q, want no number on a device --device cannot select", stdout)
- }
- }
- }
- func stubOtherDiscovery(t *testing.T, others []intdevice.HIDDevice) {
- t.Helper()
- orig := discoverOther
- discoverOther = func() ([]intdevice.HIDDevice, error) { return others, nil }
- t.Cleanup(func() { discoverOther = orig })
- }
- 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())
- }
- }
|