package main import ( "bytes" "strings" "testing" intdevice "netdome.biz/paul/qmk-rgb/internal/device" "netdome.biz/paul/qmk-rgb/internal/via" ) func TestResolveZoneNameAcceptsASubsystemName(t *testing.T) { got, err := resolveZoneName("rgb_matrix", map[uint16]string{2: "logo"}) if err != nil { t.Fatalf("resolveZoneName() error = %v", err) } if len(got) != 1 || got[0] != via.ChannelRgbMatrix { t.Errorf("resolveZoneName() = %v, want [3]", got) } } func TestResolveZoneNameAcceptsADisplayName(t *testing.T) { got, err := resolveZoneName("logo", map[uint16]string{2: "logo", 3: "backlight", 4: "side"}) if err != nil { t.Fatalf("resolveZoneName() error = %v", err) } if len(got) != 1 || got[0] != via.ChannelRgblight { t.Errorf("resolveZoneName() = %v, want [2]", got) } } func TestResolveZoneNameRejectsAnUnknownName(t *testing.T) { _, err := resolveZoneName("nope", map[uint16]string{2: "logo"}) if err == nil { t.Fatal("resolveZoneName() expected an error, got nil") } if !strings.Contains(err.Error(), "rgb_matrix") { t.Errorf("error = %q, want it to name an accepted form", err) } } // A board that supplies no display names keeps the subsystem vocabulary, so the // physical names that work on the Impact 80 do not work elsewhere. func TestResolveZoneNameIgnoresDisplayNamesForAnotherBoard(t *testing.T) { if _, err := resolveZoneName("logo", nil); err == nil { t.Fatal("resolveZoneName(\"logo\", nil) expected an error, got nil") } } // Two channels carrying the same display name are ambiguous, and the tool // reports that rather than picking one. Whether a display name shadows another // channel's subsystem name cannot be decided here, because presence is only // known after the probe; displayNameConflicts decides that. func TestResolveZoneNameReportsAnAmbiguousName(t *testing.T) { _, err := resolveZoneName("backlight", map[uint16]string{2: "logo", 3: "backlight", 4: "backlight"}) if err == nil { t.Fatal("resolveZoneName() expected an error for a name two channels answer to") } if !strings.Contains(err.Error(), "backlight") { t.Errorf("error = %q, want it to name the conflicting name", err) } } func TestDisplayNameConflictsRejectsAShadowedSubsystemName(t *testing.T) { // Channel 1 is present and its subsystem is "backlight", while the file // also calls channel 3 "backlight": two channels, one name. err := displayNameConflicts( map[uint16]string{3: "backlight"}, []via.Channel{via.ChannelBacklight, via.ChannelRgbMatrix}, ) if err == nil { t.Fatal("displayNameConflicts() expected an error, got nil") } if !strings.Contains(err.Error(), "backlight") { t.Errorf("error = %q, want it to name the conflicting name", err) } } func TestDisplayNameConflictsAllowsTheImpact80Naming(t *testing.T) { // The Impact 80 calls channel 3 "backlight" and has no channel 1, so // nothing shadows anything. err := displayNameConflicts( map[uint16]string{2: "logo", 3: "backlight", 4: "side"}, []via.Channel{via.ChannelRgblight, via.ChannelRgbMatrix, via.ChannelAudio}, ) if err != nil { t.Fatalf("displayNameConflicts() error = %v, want nil", err) } } func TestChannelNamePrefersTheDisplayName(t *testing.T) { display := map[uint16]string{2: "logo"} if got := channelName(via.ChannelRgblight, display); got != "logo" { t.Errorf("channelName(2) = %q, want %q", got, "logo") } if got := channelName(via.ChannelRgbMatrix, display); got != "rgb_matrix" { t.Errorf("channelName(3) = %q, want %q", got, "rgb_matrix") } } // The display names must come from the keyboard the command targets, not from // whichever one enumeration returned first. func TestPrepareTargetUsesTheSelectedKeyboard(t *testing.T) { devices := []intdevice.Device{ {VendorID: 0x6666, ProductID: 0x0001}, {VendorID: 0x36B0, ProductID: 0x309F, Name: "Wobkey Impact 80"}, } originalDiscover := discoverAll originalKeyboardFor := keyboardFor originalTarget := targetDevice originalZone := targetZone t.Cleanup(func() { discoverAll = originalDiscover keyboardFor = originalKeyboardFor targetDevice = originalTarget targetZone = originalZone }) discoverAll = func() ([]intdevice.Device, error) { return devices, nil } keyboardFor = func(vendorID, productID uint16) (intdevice.Keyboard, bool, error) { switch { case vendorID == 0x6666: return intdevice.Keyboard{Name: "Wobkey Rainy 75", Channels: map[uint16]string{2: "deck"}}, true, nil case vendorID == 0x36B0: return intdevice.Keyboard{Name: "Wobkey Impact 80", Channels: map[uint16]string{2: "logo"}}, true, nil } return intdevice.Keyboard{}, false, nil } targetDevice = "2" targetZone = "logo" got, err := prepareTarget() if err != nil { t.Fatalf("prepareTarget() error = %v", err) } if got.Display[2] != "logo" { t.Errorf("display names = %v, want the Impact 80's", got.Display) } } // A board renamed after a profile was written leaves keys that resolve to // nothing. Each one must be reported by name, and nothing may be written. func TestLoadWarnsForEveryUnresolvableZoneKey(t *testing.T) { dir := t.TempDir() profile := Profile{ Name: "renamed", Version: 1, Zones: map[string]*ZoneSettings{ "old-logo": {Enabled: true, Effect: "light", Brightness: 100, Speed: 1, Color: "00ff"}, "old-backlight": {Enabled: true, Effect: "wave", Brightness: 100, Speed: 1, Color: "00ff"}, }, } originalDir := profilesDir t.Cleanup(func() { profilesDir = originalDir }) profilesDir = dir if err := profile.Save(); err != nil { t.Fatalf("save profile: %v", err) } proto := &verifyingProtocol{} t.Cleanup(stubTargetForProfileTest(t, proto, "", impact80Display())) var stderr bytes.Buffer cmd := NewProfileLoadCmd() cmd.SetOut(&bytes.Buffer{}) cmd.SetErr(&stderr) cmd.SetArgs([]string{"renamed"}) if err := cmd.Execute(); err != nil { t.Fatalf("load returned error: %v", err) } for _, want := range []string{"old-logo", "old-backlight"} { if !strings.Contains(stderr.String(), want) { t.Errorf("stderr = %q, want it to name the unresolved key %q", stderr.String(), want) } } if len(proto.reports) != 0 { t.Errorf("reports = %v, want nothing written for unresolvable keys", proto.reports) } } // stubTargetForProfileTest points the profile commands at one protocol, one // profile directory and one --zone value, and returns the restore function. func stubTargetForProfileTest(t *testing.T, proto rgbProtocol, zoneFlag string, display map[uint16]string) func() { t.Helper() originalTarget := openTarget originalZone := targetZone openTarget = func() (rgbProtocol, targetDeviceData, []via.Channel, error) { // The board identity matters: the catalog is selected by VID/PID, and a // profile that names an effect needs one to resolve it against. target := targetDeviceData{ Device: intdevice.Device{VendorID: 0x36B0, ProductID: 0x309F}, Display: display, } requested, err := resolveZoneName(zoneFlag, display) if err != nil { return nil, target, nil, err } target.Requested = requested resolved, err := resolveChannels(proto, target) if err != nil { return nil, target, nil, err } return proto, target, resolved, nil } targetZone = zoneFlag return func() { openTarget = originalTarget targetZone = originalZone } } // A key that names a channel this keyboard does not have must be reported like // any other key it cannot place. "backlight" is the case that matters: it is a // display name on the Impact 80 and QMK's subsystem name for channel 1, so it // still resolves after a board renames channel 3 — to a channel that is absent. func TestLoadWarnsWhenAKeyNamesAnAbsentChannel(t *testing.T) { dir := t.TempDir() originalDir := profilesDir t.Cleanup(func() { profilesDir = originalDir }) profilesDir = dir profile := Profile{ Name: "renamed", Version: 1, Zones: map[string]*ZoneSettings{ "backlight": {Enabled: true, Effect: "light", Brightness: 100, Speed: 1, Color: "00ff"}, }, } if err := profile.Save(); err != nil { t.Fatalf("save profile: %v", err) } proto := &verifyingProtocol{} t.Cleanup(stubTargetForProfileTest(t, proto, "", nil)) var stderr bytes.Buffer cmd := NewProfileLoadCmd() cmd.SetOut(&bytes.Buffer{}) cmd.SetErr(&stderr) cmd.SetArgs([]string{"renamed"}) if err := cmd.Execute(); err != nil { t.Fatalf("load returned error: %v", err) } if !strings.Contains(stderr.String(), "backlight") { t.Errorf("stderr = %q, want the key reported by name", stderr.String()) } if len(proto.reports) != 0 { t.Errorf("reports = %v, want nothing written for an absent channel", proto.reports) } } // Without a catalog there are no effect names to look the profile's value up // in. The load must say that, not claim the name was not found. func TestLoadSaysSoWhenTheBoardHasNoCatalog(t *testing.T) { dir := t.TempDir() originalDir := profilesDir t.Cleanup(func() { profilesDir = originalDir }) profilesDir = dir profile := Profile{ Name: "p", Version: 1, Zones: map[string]*ZoneSettings{ "rgb_matrix": {Enabled: true, Effect: "breathing", Brightness: 100, Speed: 1, Color: "00ff"}, }, } if err := profile.Save(); err != nil { t.Fatalf("save profile: %v", err) } proto := &verifyingProtocol{} restore := stubTargetForUnknownBoard(t, proto, dir) t.Cleanup(restore) var stderr bytes.Buffer cmd := NewProfileLoadCmd() cmd.SetOut(&bytes.Buffer{}) cmd.SetErr(&stderr) cmd.SetArgs([]string{"p"}) if err := cmd.Execute(); err != nil { t.Fatalf("load returned error: %v", err) } if !strings.Contains(stderr.String(), "catalog") { t.Errorf("stderr = %q, want the missing catalog named", stderr.String()) } if strings.Contains(stderr.String(), `effect "breathing" not found`) { t.Errorf("stderr = %q, must not blame the effect name for a missing catalog", stderr.String()) } if len(proto.reports) != 0 { t.Errorf("reports = %v, want nothing written without a catalog", proto.reports) } } // stubTargetForUnknownBoard points the commands at a board that has channels but // no keyboards.json entry, so it has no catalog. func stubTargetForUnknownBoard(t *testing.T, proto rgbProtocol, dir string) func() { t.Helper() originalTarget := openTarget originalDir := profilesDir openTarget = func() (rgbProtocol, targetDeviceData, []via.Channel, error) { return proto, targetDeviceData{ Device: intdevice.Device{VendorID: 0x6666, ProductID: 0x0001}, }, impact80Channels(), nil } profilesDir = dir return func() { openTarget = originalTarget profilesDir = originalDir } }