| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381 |
- package rgb
- import (
- "os"
- "path/filepath"
- "strings"
- "testing"
- "netdome.biz/paul/qmk-rgb/internal/via"
- )
- // A definition may attach a number to an option that is not its position in the
- // list, so the ID has to come from the number and not from the index. This
- // fixture names three effects at IDs 1, 2 and 5 and leaves 0, 3 and 4 unnamed.
- func TestLoadDefinitionReadsEffectIDsFromTheOptions(t *testing.T) {
- def := loadDefinitionFixture(t, "tuple_options.json")
- effects := def.Catalog.Effects(via.ChannelRgbMatrix)
- if len(effects) != 3 {
- t.Fatalf("rgb_matrix effects = %d, want 3", len(effects))
- }
- want := []Effect{{ID: 1, Name: "Solid Color"}, {ID: 2, Name: "Breathing"}, {ID: 5, Name: "Rainbow"}}
- for i, w := range want {
- if effects[i] != w {
- t.Errorf("effects[%d] = %+v, want %+v", i, effects[i], w)
- }
- }
- // A gap must report unknown rather than the next name along.
- for _, id := range []uint8{0, 3, 4, 6} {
- if got := def.Catalog.EffectName(via.ChannelRgbMatrix, id); got != unknownEffectName {
- t.Errorf("EffectName(rgb_matrix, %d) = %q, want %q", id, got, unknownEffectName)
- }
- }
- }
- // The same effect is reachable by name, which is what `effect <name>` needs.
- func TestDefinitionEffectsResolveByName(t *testing.T) {
- def := loadDefinitionFixture(t, "tuple_options.json")
- id, ok := def.Catalog.EffectID(via.ChannelRgbMatrix, "Rainbow")
- if !ok || id != 5 {
- t.Errorf("EffectID(rgb_matrix, \"Rainbow\") = %d, %t, want 5, true", id, ok)
- }
- if _, ok := def.Catalog.EffectID(via.ChannelRgbMatrix, "Solid"); ok {
- t.Error("EffectID(rgb_matrix, \"Solid\") = found, want not found")
- }
- }
- // Plain string options carry no number, and the position is the ID.
- func TestLoadDefinitionNumbersStringOptionsByPosition(t *testing.T) {
- def := loadDefinitionFixture(t, "string_options.json")
- effects := def.Catalog.Effects(via.ChannelRgblight)
- want := []Effect{{ID: 0, Name: "All Off"}, {ID: 1, Name: "Solid Color"}, {ID: 2, Name: "Breathing 1"}, {ID: 3, Name: "Breathing 2"}}
- if len(effects) != len(want) {
- t.Fatalf("rgblight effects = %d, want %d", len(effects), len(want))
- }
- for i, w := range want {
- if effects[i] != w {
- t.Errorf("effects[%d] = %+v, want %+v", i, effects[i], w)
- }
- }
- }
- // VIA serves built definitions that carry vendorProductId as a number and no
- // vendorId/productId pair, so both spellings have to be read.
- func TestDefinitionIdentifiesTheBoardFromEitherSpelling(t *testing.T) {
- tests := []struct {
- file string
- wantVendor uint16
- wantProd uint16
- }{
- {"tuple_options.json", 0x1234, 0x5678},
- {"string_options.json", 0x1234, 0x5678},
- }
- for _, tt := range tests {
- def := loadDefinitionFixture(t, tt.file)
- if def.VendorID != tt.wantVendor || def.ProductID != tt.wantProd {
- t.Errorf("%s: vendor/product = 0x%04x/0x%04x, want 0x%04x/0x%04x",
- tt.file, def.VendorID, def.ProductID, tt.wantVendor, tt.wantProd)
- }
- }
- if got := loadDefinitionFixture(t, "string_options.json").Name; got != "Served Board" {
- t.Errorf("name = %q, want %q", got, "Served Board")
- }
- }
- // Brightness, color and speed controls are not effect lists and must not become
- // one.
- func TestLoadDefinitionIgnoresOtherControls(t *testing.T) {
- def := loadDefinitionFixture(t, "tuple_options.json")
- for _, ch := range []via.Channel{via.ChannelBacklight, via.ChannelRgbMatrix} {
- for _, e := range def.Catalog.Effects(ch) {
- if e.Name == "" {
- t.Errorf("channel %v has an effect without a name", ch)
- }
- }
- }
- if got := def.Catalog.Effects(via.ChannelRgbMatrix); len(got) != 3 {
- t.Errorf("rgb_matrix effects = %d, want 3 (brightness and color excluded)", len(got))
- }
- }
- // A definition is matched to a board by its VID/PID, so a directory can hold
- // many of them and the wrong one must not be used.
- func TestFindDefinitionMatchesTheBoard(t *testing.T) {
- dir := t.TempDir()
- copyFixture(t, dir, "tuple_options.json")
- defs, err := LoadDefinitionsDir(dir)
- if err != nil {
- t.Fatalf("LoadDefinitionsDir() error = %v", err)
- }
- if len(defs) != 1 {
- t.Fatalf("definitions = %d, want 1", len(defs))
- }
- if got := FindDefinition(defs, 0x1234, 0x5678); got == nil {
- t.Error("FindDefinition(matching) = nil, want the definition")
- }
- if got := FindDefinition(defs, 0x36B0, 0x309F); got != nil {
- t.Error("FindDefinition(other board) = a definition, want nil")
- }
- }
- // A file that is not a definition, or not one for any board, is reported rather
- // than skipped in silence.
- func TestLoadDefinitionsDirRejectsUnusableFiles(t *testing.T) {
- dir := t.TempDir()
- if err := os.WriteFile(filepath.Join(dir, "broken.json"), []byte("{not json"), 0o600); err != nil {
- t.Fatal(err)
- }
- if _, err := LoadDefinitionsDir(dir); err == nil {
- t.Error("LoadDefinitionsDir(broken json) = nil error, want an error")
- }
- }
- func TestLoadDefinitionRejectsAMissingFile(t *testing.T) {
- if _, err := LoadDefinition(filepath.Join(t.TempDir(), "nope.json")); err == nil {
- t.Error("LoadDefinition(missing) = nil error, want an error")
- }
- }
- func loadDefinitionFixture(t *testing.T, name string) *Definition {
- t.Helper()
- def, err := LoadDefinition(filepath.Join("testdata", name))
- if err != nil {
- t.Fatalf("LoadDefinition(%s) error = %v", name, err)
- }
- return def
- }
- func copyFixture(t *testing.T, dir, name string) {
- t.Helper()
- data, err := os.ReadFile(filepath.Join("testdata", name))
- if err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(filepath.Join(dir, name), data, 0o600); err != nil {
- t.Fatal(err)
- }
- }
- // A definition carries the manufacturer's own spelling, and the tool's aliases
- // are a spelling convenience rather than board knowledge, so they have to work
- // whichever spelling a definition happens to use. The vendor of this board
- // writes "breathe" where the compiled-in catalog writes "breathing".
- func TestDefinitionCatalogResolvesTheToolsAliasesBothWays(t *testing.T) {
- names := []Effect{{ID: 0, Name: "none"}, {ID: 3, Name: "spectrum"}, {ID: 4, Name: "breathe"}, {ID: 5, Name: "light"}}
- catalog := NewCatalog("vendor", map[via.Channel][]Effect{via.ChannelRgblight: names})
- tests := []struct {
- name string
- effect string
- want uint8
- }{
- {"the vendor's own spelling", "breathe", 4},
- {"the tool's canonical name", "breathing", 4},
- {"an alias that the channel has", "off", 0},
- {"an alias that resolves per channel", "rainbow", 3},
- {"a name no alias or entry reaches", "nonsense", 0},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- id, ok := catalog.EffectID(via.ChannelRgblight, tt.effect)
- if tt.effect == "nonsense" {
- if ok {
- t.Errorf("EffectID(%q) = %d, true, want not found", tt.effect, id)
- }
- return
- }
- if !ok || id != tt.want {
- t.Errorf("EffectID(%q) = %d, %t, want %d, true", tt.effect, id, ok, tt.want)
- }
- })
- }
- }
- // The compiled-in catalog keeps working exactly as before.
- func TestBuiltInCatalogStillResolvesItsAliases(t *testing.T) {
- catalog := impact80(t)
- tests := []struct {
- channel via.Channel
- effect string
- want uint8
- }{
- {via.ChannelRgblight, "breathing", 4},
- {via.ChannelRgblight, "breathe", 4},
- {via.ChannelRgblight, "off", 0},
- {via.ChannelRgbMatrix, "breathing", 5},
- {via.ChannelRgbMatrix, "rainbow", 17},
- {via.ChannelAudio, "rainbow", 3},
- }
- for _, tt := range tests {
- id, ok := catalog.EffectID(tt.channel, tt.effect)
- if !ok || id != tt.want {
- t.Errorf("EffectID(%v, %q) = %d, %t, want %d, true", tt.channel, tt.effect, id, ok, tt.want)
- }
- }
- }
- // A definition writes display spellings ("fixed wave") where the tool writes
- // identifiers ("fixed_wave"). The difference is whitespace, not a different
- // effect, so both must resolve — the tool's documented name may not stop working
- // because a definition file happens to be present.
- func TestDefinitionCatalogResolvesSpacesAsUnderscores(t *testing.T) {
- names := []Effect{{ID: 0, Name: "none"}, {ID: 1, Name: "wave"}, {ID: 2, Name: "fixed wave"}}
- catalog := NewCatalog("vendor", map[via.Channel][]Effect{via.ChannelRgblight: names})
- tests := []struct {
- effect string
- want uint8
- }{
- {"fixed wave", 2},
- {"fixed_wave", 2},
- {"wave", 1},
- }
- for _, tt := range tests {
- id, ok := catalog.EffectID(via.ChannelRgblight, tt.effect)
- if !ok || id != tt.want {
- t.Errorf("EffectID(%q) = %d, %t, want %d, true", tt.effect, id, ok, tt.want)
- }
- }
- }
- // The compiled-in catalog has no spaced names, so the rule changes nothing there.
- func TestBuiltInCatalogIsUnchangedByTheSpacingRule(t *testing.T) {
- catalog := impact80(t)
- if id, ok := catalog.EffectID(via.ChannelRgblight, "fixed_wave"); !ok || id != 2 {
- t.Errorf("EffectID(rgblight, \"fixed_wave\") = %d, %t, want 2, true", id, ok)
- }
- if _, ok := catalog.EffectID(via.ChannelRgbMatrix, "fixed_wave"); ok {
- t.Error("EffectID(rgb_matrix, \"fixed_wave\") = found, want not found: only the rgblight channel has it")
- }
- }
- // The sub-menu a definition puts a lighting channel under is that channel's name
- // in VIA's own interface, and it is board data the file already carries. Using
- // it means the tool and VIA call the same channel the same thing, instead of
- // the tool inventing a third name.
- func TestDefinitionNamesItsChannelsBySubMenu(t *testing.T) {
- def := loadDefinitionFixture(t, "tuple_options.json")
- want := map[uint16]string{2: "logo", 3: "Backlight", 4: "side"}
- for channel, label := range want {
- if got := def.Labels[channel]; got != label {
- t.Errorf("Labels[%d] = %q, want %q", channel, got, label)
- }
- }
- if len(def.Labels) != len(want) {
- t.Errorf("Labels = %v, want only the lighting channels", def.Labels)
- }
- }
- // The label is whatever the definition calls the channel, not a name this tool
- // knows: a different definition calls the same channel Underglow, and that name
- // is the one VIA shows.
- func TestDefinitionTakesWhateverTheChannelIsCalled(t *testing.T) {
- def := loadDefinitionFixture(t, "string_options.json")
- if got := def.Labels[2]; got != "Underglow" {
- t.Errorf("Labels[2] = %q, want %q", got, "Underglow")
- }
- if len(def.Labels) != 1 {
- t.Errorf("Labels = %v, want only the channel the definition names", def.Labels)
- }
- }
- // The definition files in the repository are read at runtime, so a truncated or
- // edited one would fail every command that names an effect rather than fail a
- // test. This parses the vendored Impact 80 file and checks what it has to
- // contain.
- func TestVendoredImpact80DefinitionIsUsable(t *testing.T) {
- def, err := LoadDefinition(filepath.Join("..", "..", "definitions", "impact80.json"))
- if err != nil {
- t.Fatalf("LoadDefinition(definitions/impact80.json) error = %v", err)
- }
- if def.VendorID != 0x36B0 || def.ProductID != 0x309F {
- t.Errorf("identifiers = 0x%04X/0x%04X, want 0x36B0/0x309F", def.VendorID, def.ProductID)
- }
- want := map[via.Channel]int{
- via.ChannelRgblight: 7,
- via.ChannelRgbMatrix: 46,
- via.ChannelAudio: 7,
- }
- for ch, count := range want {
- if got := len(def.Catalog.Effects(ch)); got != count {
- t.Errorf("channel %v has %d effects, want %d", ch, got, count)
- }
- }
- // The names are the vendor's, and the tool's spelling has to resolve too.
- if id, ok := def.Catalog.EffectID(via.ChannelRgblight, "fixed wave"); !ok || id != 2 {
- t.Errorf("EffectID(rgblight, \"fixed wave\") = %d, %t, want 2, true", id, ok)
- }
- if id, ok := def.Catalog.EffectID(via.ChannelRgblight, "fixed_wave"); !ok || id != 2 {
- t.Errorf("EffectID(rgblight, \"fixed_wave\") = %d, %t, want 2, true", id, ok)
- }
- if got := def.Labels[3]; got != "Backlight" {
- t.Errorf("Labels[3] = %q, want %q", got, "Backlight")
- }
- }
- // enable has to put a channel on an effect, and a definition carries no notion
- // of a default one. The rule is taken from the board's own list: the first
- // effect that is not the "off" entry. That is board data rather than a hardcoded
- // ID, so it works for any board with a definition and is gone for a board
- // without one, which is the honest answer there.
- func TestDefaultEffectIsTheBoardsFirstEffectThatIsNotOff(t *testing.T) {
- catalog := loadDefinitionFixture(t, "tuple_options.json").Catalog
- tests := []struct {
- channel via.Channel
- want uint8
- ok bool
- }{
- // Each of these lists starts at ID 0 with the off entry, so the first
- // effect that lights anything is the second one. The rgb_matrix list
- // starts at ID 1, so ID 0 does not exist on it at all.
- {via.ChannelRgbMatrix, 1, true},
- {via.ChannelRgblight, 1, true},
- {via.ChannelAudio, 1, true},
- }
- for _, tt := range tests {
- got, ok := catalog.DefaultEffect(tt.channel)
- if got != tt.want || ok != tt.ok {
- t.Errorf("DefaultEffect(%v) = %d, %t, want %d, %t", tt.channel, got, ok, tt.want, tt.ok)
- }
- }
- }
- // A list that names nothing cannot be turned on by an effect, and saying so is
- // the point.
- func TestDefaultEffectIsAbsentForAnEmptyChannel(t *testing.T) {
- catalog := NewCatalog("empty", map[via.Channel][]Effect{via.ChannelRgblight: {{ID: 0, Name: "none"}}})
- if got, ok := catalog.DefaultEffect(via.ChannelRgblight); ok {
- t.Errorf("DefaultEffect() = %d, true, want none: the channel has only the off entry", got)
- }
- }
- // The message a command fails with is the only documentation a user meets before
- // they read anything. It has to name the way out, and there are two: fetch the
- // board's definition, or set an effect by number.
- func TestMissingCatalogErrorNamesBothWaysOut(t *testing.T) {
- _, _, err := ResolveEffect(nil, "wave", []via.Channel{via.ChannelRgbMatrix}, false)
- if err == nil {
- t.Fatal("ResolveEffect(nil catalog) = nil error, want one")
- }
- for _, want := range []string{"`definition fetch`", "`effect <index>`"} {
- if !strings.Contains(err.Error(), want) {
- t.Errorf("error = %q, want it to mention %q", err, want)
- }
- }
- }
|