| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473 |
- 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"}, nil)
- 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"}, nil)
- 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"}, nil)
- 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, nil); err == nil {
- t.Fatal("resolveZoneName(\"logo\", nil, 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"}, nil)
- 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. They come from that board's
- // definition file, which is where a channel's name lives now.
- func TestPrepareTargetUsesTheSelectedKeyboard(t *testing.T) {
- devices := []intdevice.Device{
- {VendorID: 0x6666, ProductID: 0x0001},
- {VendorID: 0x36B0, ProductID: 0x309F, Name: "Impact 80"},
- }
- dir := t.TempDir()
- writeDefinition(t, dir, "rainy75.json", `{
- "name": "Rainy 75", "vendorId": "0x6666", "productId": "0x0001",
- "menus": [{"label":"Lighting","content":[{"label":"deck","content":[
- {"label":"Effect","type":"dropdown","content":["id_qmk_rgblight_effect",2,2],"options":["none"]}]}]}]}`)
- writeDefinition(t, dir, "impact80.json", `{
- "name": "Impact 80", "vendorId": "0x36B0", "productId": "0x309F",
- "menus": [{"label":"Lighting","content":[{"label":"logo","content":[
- {"label":"Effect","type":"dropdown","content":["id_qmk_rgblight_effect",2,2],"options":["none","wave"]}]}]}]}`)
- t.Cleanup(forceDefinitionsDir(t, dir))
- originalDiscover := discoverAll
- originalTarget := targetDevice
- originalZone := targetZone
- t.Cleanup(func() {
- discoverAll = originalDiscover
- targetDevice = originalTarget
- targetZone = originalZone
- })
- discoverAll = func() ([]intdevice.Device, error) { return devices, 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)
- }
- if got.Device.Name != "Impact 80" {
- t.Errorf("name = %q, want the name the definition gives the board", got.Device.Name)
- }
- }
- // 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) {
- t.Cleanup(vendoredDefinitions(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, nil)
- 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) {
- t.Cleanup(vendoredDefinitions(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{applied: map[via.Channel]uint8{
- via.ChannelRgblight: 160,
- via.ChannelRgbMatrix: 255,
- via.ChannelAudio: 160,
- }}
- 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(), "no effect names") {
- t.Errorf("stderr = %q, want the missing names 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 definition and no compiled-in catalog, so it has no effect names.
- 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
- }
- }
- // A board without a catalog cannot name its effects, so a saved profile records
- // "unknown" for every channel and cannot be loaded back. That is worth saying at
- // save time, where the user can still do something about it.
- func TestSaveWarnsThatEffectNamesCannotBeRecorded(t *testing.T) {
- dir := t.TempDir()
- originalDir := profilesDir
- t.Cleanup(func() { profilesDir = originalDir })
- profilesDir = dir
- proto := &verifyingProtocol{applied: map[via.Channel]uint8{
- via.ChannelRgblight: 160,
- via.ChannelRgbMatrix: 255,
- via.ChannelAudio: 160,
- }}
- restore := stubTargetForUnknownBoard(t, proto, dir)
- t.Cleanup(restore)
- var stderr bytes.Buffer
- cmd := NewProfileSaveCmd()
- cmd.SetOut(&bytes.Buffer{})
- cmd.SetErr(&stderr)
- cmd.SetArgs([]string{"p"})
- if err := cmd.Execute(); err != nil {
- t.Fatalf("save returned error: %v", err)
- }
- if !strings.Contains(stderr.String(), "unknown") {
- t.Errorf("stderr = %q, want the effect name it cannot record named", stderr.String())
- }
- if !strings.Contains(stderr.String(), "no effect names") {
- t.Errorf("stderr = %q, want the missing names named", stderr.String())
- }
- }
- // A channel's own definition label and the subsystem name it replaces can both
- // match one name, and that is one channel rather than a conflict: the
- // documented `--zone backlight` has to keep working on a board whose definition
- // calls the channel "Backlight".
- func TestResolveZoneNameAcceptsTheNameADefinitionReplaced(t *testing.T) {
- display := map[uint16]string{2: "logo", 3: "Backlight", 4: "side"}
- alternatives := map[uint16][]string{3: {"backlight"}}
- for _, name := range []string{"backlight", "Backlight", "BACKLIGHT"} {
- got, err := resolveZoneName(name, display, alternatives)
- if err != nil {
- t.Errorf("resolveZoneName(%q) error = %v", name, err)
- continue
- }
- if len(got) != 1 || got[0] != 3 {
- t.Errorf("resolveZoneName(%q) = %v, want [3]", name, got)
- }
- }
- }
- // Two different channels answering to one name is still a conflict, whichever
- // source the names come from.
- func TestResolveZoneNameStillReportsARealConflict(t *testing.T) {
- display := map[uint16]string{2: "Backlight", 3: "backlight"}
- if _, err := resolveZoneName("backlight", display, map[uint16][]string{4: {"backlight"}}); err == nil {
- t.Error("resolveZoneName() = nil error, want a conflict for one name on two channels")
- }
- }
- // A board whose definition calls channel 3 "Backlight" has no channel 1, and
- // naming an unused channel would make "backlight" reach two channels at once —
- // the subsystem name of the one it does not have, and the label of the one it
- // does. The label has to win, because it is what the board calls that channel.
- func TestZoneNamePrefersTheDefinitionLabelOverAnotherChannelsSubsystem(t *testing.T) {
- dir := t.TempDir()
- writeDefinition(t, dir, "impact80.json", `{
- "name": "Impact 80", "vendorId": "0x36B0", "productId": "0x309F",
- "menus": [{"label":"Lighting","content":[{"label":"Backlight","content":[
- {"label":"Effect","type":"dropdown","content":["id_qmk_rgb_matrix_effect",3,2],
- "options":[["cycle_left_right",13]]}]}]}]}`)
- t.Cleanup(forceDefinitionsDir(t, dir))
- display, alternatives := applyDefinitionLabels(0x36B0, 0x309F)
- if display[3] != "Backlight" {
- t.Errorf("display = %v, want channel 3 named by the definition", display)
- }
- if _, ok := display[1]; ok {
- t.Errorf("display = %v, want no entry for a channel the definition does not name", display)
- }
- for _, name := range []string{"backlight", "Backlight", "BACKLIGHT", "rgb_matrix"} {
- got, err := resolveZoneName(name, display, alternatives)
- if err != nil {
- t.Errorf("resolveZoneName(%q) error = %v, want channel 3", name, err)
- continue
- }
- if len(got) != 1 || got[0] != 3 {
- t.Errorf("resolveZoneName(%q) = %v, want [3]", name, got)
- }
- }
- }
- // Fifteen commands in one flat list is where a user starts reading the help to
- // find a command and gives up. The groups say what a command is for, and a
- // command that belongs to none of them is a hole in the list.
- func TestRootHelpGroupsTheCommands(t *testing.T) {
- // The same registration the binary runs, so a command that someone adds
- // without a group is caught here rather than in the help output.
- root := newRootCommand()
- registerCommands(root)
- var out bytes.Buffer
- root.SetOut(&out)
- root.SetArgs([]string{"--help"})
- if err := root.Execute(); err != nil {
- t.Fatalf("--help returned error: %v", err)
- }
- help := out.String()
- for _, want := range []string{
- "Lighting Commands:",
- "Profile Commands:",
- "Keyboard Commands:",
- "save", "load", "list", "delete", "effect", "definition", "keyboard",
- } {
- if !strings.Contains(help, want) {
- t.Errorf("--help does not mention %q", want)
- }
- }
- // Every command the tool adds has to be in a group, or it lands in
- // "Additional Commands" where the grouping says nothing about it.
- grouped := make(map[string]bool)
- for _, c := range root.Commands() {
- if c.GroupID != "" {
- grouped[c.Name()] = true
- }
- }
- for _, c := range root.Commands() {
- if c.Name() == "help" || c.Name() == "completion" {
- continue
- }
- if !grouped[c.Name()] {
- t.Errorf("command %q is in no group, so it appears ungrouped in --help", c.Name())
- }
- }
- }
|