| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- package main
- import (
- "bytes"
- "reflect"
- "strings"
- "testing"
- "github.com/spf13/cobra"
- "netdome.biz/paul/qmk-rgb/internal/via"
- )
- // A command that writes lighting and named no zone used to write every channel
- // the keyboard has, which is the widest thing it can do and not something anybody
- // asked for. Its argument count refuses it now, before the keyboard is opened, so
- // a script gets the same answer whichever of them it called.
- func TestLightingCommandsRefuseToWriteWithoutAZone(t *testing.T) {
- cases := []struct {
- name string
- cmd func() *cobra.Command
- args []string
- }{
- {"effect", NewEffectCmd, nil},
- {"brightness", NewBrightnessCmd, []string{"160"}},
- {"speed", NewSpeedCmd, []string{"2"}},
- {"color", NewColorCmd, []string{"00ff00"}},
- {"enable", NewEnableCmd, nil},
- {"disable", NewDisableCmd, nil},
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- proto := &fakeZoneProtocol{failAt: -1}
- t.Cleanup(impact80Target(t, proto))
- var out bytes.Buffer
- cmd := tc.cmd()
- cmd.SetOut(&out)
- cmd.SetErr(&out)
- cmd.SilenceErrors = true
- cmd.SilenceUsage = true
- cmd.SetArgs(tc.args)
- err := cmd.Execute()
- if err == nil {
- t.Fatal("Execute() expected the missing zone to be refused, got nil")
- }
- // The message has to say which argument was missing, or the user
- // cannot act on it without reading the source.
- if !strings.Contains(err.Error(), "zone") {
- t.Errorf("error = %q, want it to name the missing zone", err)
- }
- if !strings.Contains(err.Error(), "usage:") {
- t.Errorf("error = %q, want the usage line so the order of the arguments is visible", err)
- }
- if len(proto.reports) != 0 {
- t.Errorf("reports = %v, want no write before the zone was named", proto.reports)
- }
- if out.Len() != 0 {
- t.Errorf("output = %q, want nothing written before the zone was named", out.String())
- }
- })
- }
- }
- // A list of zones reaches exactly the channels it names, and no others.
- func TestZoneListWritesEveryNamedChannel(t *testing.T) {
- cases := []struct {
- zone string
- want []commandReport
- }{
- {"logo", []commandReport{{channel: 2, param: 1, value: 160}}},
- {"side,logo", []commandReport{
- {channel: 2, param: 1, value: 160},
- {channel: 4, param: 1, value: 160},
- }},
- // The order written is not the order applied: channels come back in
- // channel order, so a script sees one order for every spelling.
- {"logo, side", []commandReport{
- {channel: 2, param: 1, value: 160},
- {channel: 4, param: 1, value: 160},
- }},
- // One name twice is one channel, not a double write.
- {"logo,logo", []commandReport{{channel: 2, param: 1, value: 160}}},
- // A display name and the subsystem name of the same channel are one
- // channel, and so is a name that matches through both.
- {"rgblight", []commandReport{{channel: 2, param: 1, value: 160}}},
- {"Backlight", []commandReport{{channel: 3, param: 1, value: 160}}},
- {"backlight", []commandReport{{channel: 3, param: 1, value: 160}}},
- }
- for _, tc := range cases {
- t.Run(tc.zone, func(t *testing.T) {
- proto := &fakeZoneProtocol{failAt: -1}
- t.Cleanup(impact80Target(t, proto))
- var out bytes.Buffer
- cmd := NewBrightnessCmd()
- cmd.SetOut(&out)
- cmd.SetErr(&out)
- cmd.SetArgs([]string{tc.zone, "160"})
- if err := cmd.Execute(); err != nil {
- t.Fatalf("brightness %s 160 returned error: %v", tc.zone, err)
- }
- if !reflect.DeepEqual(proto.reports, tc.want) {
- t.Errorf("reports = %v, want %v", proto.reports, tc.want)
- }
- })
- }
- }
- // all is every channel, and naming it next to one more channel is the same
- // request: the union is the whole keyboard either way.
- func TestAllMeansEveryChannel(t *testing.T) {
- cases := []string{"all", "ALL", "side,all", "all,logo", " backlight , all "}
- for _, zone := range cases {
- t.Run(zone, func(t *testing.T) {
- proto := &fakeZoneProtocol{failAt: -1}
- t.Cleanup(impact80Target(t, proto))
- var out bytes.Buffer
- cmd := NewBrightnessCmd()
- cmd.SetOut(&out)
- cmd.SetErr(&out)
- cmd.SetArgs([]string{zone, "160"})
- if err := cmd.Execute(); err != nil {
- t.Fatalf("brightness %q 160 returned error: %v", zone, err)
- }
- want := []commandReport{
- {channel: 2, param: 1, value: 160},
- {channel: 3, param: 1, value: 160},
- {channel: 4, param: 1, value: 160},
- }
- if !reflect.DeepEqual(proto.reports, want) {
- t.Errorf("reports = %v, want %v", proto.reports, want)
- }
- })
- }
- }
- // A zone the keyboard does not have is refused, and where a list names several,
- // every one that is missing is named: writing two of the three channels asked for
- // and reporting a success is the failure this prevents.
- func TestZoneListRefusesAChannelTheKeyboardLacks(t *testing.T) {
- cases := []struct {
- zone string
- want []string
- }{
- {"led_matrix", []string{"led_matrix"}},
- {"logo,led_matrix", []string{"led_matrix"}},
- {"logo,led_matrix,backlight", []string{"led_matrix"}},
- }
- for _, tc := range cases {
- t.Run(tc.zone, func(t *testing.T) {
- proto := &fakeZoneProtocol{failAt: -1}
- t.Cleanup(impact80Target(t, proto))
- var out bytes.Buffer
- cmd := NewBrightnessCmd()
- cmd.SetOut(&out)
- cmd.SetErr(&out)
- cmd.SetArgs([]string{tc.zone, "160"})
- err := cmd.Execute()
- if err == nil {
- t.Fatalf("brightness %s 160 = nil error, want the missing channel refused", tc.zone)
- }
- for _, want := range tc.want {
- if !strings.Contains(err.Error(), want) {
- t.Errorf("error = %q, want it to name %q", err, want)
- }
- }
- if len(proto.reports) != 0 {
- t.Errorf("reports = %v, want nothing written when a named channel is absent", proto.reports)
- }
- })
- }
- }
- // A name nobody wrote has to say what the alternatives are, and a list that names
- // one of them has to say which of the list it was.
- func TestUnknownZoneNamesTheVocabulary(t *testing.T) {
- cases := []struct {
- zone string
- want []string
- }{
- {"nonsense", []string{"nonsense", "rgb_matrix", "all"}},
- {"logo,nonsense", []string{"nonsense", `in "logo,nonsense"`}},
- }
- for _, tc := range cases {
- t.Run(tc.zone, func(t *testing.T) {
- proto := &fakeZoneProtocol{failAt: -1}
- t.Cleanup(impact80Target(t, proto))
- var out bytes.Buffer
- cmd := NewBrightnessCmd()
- cmd.SetOut(&out)
- cmd.SetErr(&out)
- cmd.SetArgs([]string{tc.zone, "160"})
- err := cmd.Execute()
- if err == nil {
- t.Fatalf("brightness %s 160 = nil error, want the unknown zone refused", tc.zone)
- }
- for _, want := range tc.want {
- if !strings.Contains(err.Error(), want) {
- t.Errorf("error = %q, want it to contain %q", err, want)
- }
- }
- })
- }
- }
- // A trailing or doubled comma names no channel, and guessing which one was meant
- // is worse than saying so.
- func TestZoneListRefusesAnEmptyName(t *testing.T) {
- for _, zone := range []string{"logo,", ",logo", "logo,,side", "logo, "} {
- t.Run(zone, func(t *testing.T) {
- proto := &fakeZoneProtocol{failAt: -1}
- t.Cleanup(impact80Target(t, proto))
- var out bytes.Buffer
- cmd := NewBrightnessCmd()
- cmd.SetOut(&out)
- cmd.SetErr(&out)
- cmd.SetArgs([]string{zone, "160"})
- err := cmd.Execute()
- if err == nil {
- t.Fatalf("brightness %q 160 = nil error, want the empty name refused", zone)
- }
- if !strings.Contains(err.Error(), "empty") {
- t.Errorf("error = %q, want it to say the list has an empty name", err)
- }
- if len(proto.reports) != 0 {
- t.Errorf("reports = %v, want no write", proto.reports)
- }
- })
- }
- }
- // A zone that a definition file gives to two channels of the same board would be
- // a silent retarget, so the list resolves it to neither.
- func TestZoneListRefusesANameTwoChannelsShare(t *testing.T) {
- proto := &fakeZoneProtocol{failAt: -1}
- display := map[uint16]string{2: "logo", 3: "logo", 4: "side"}
- t.Cleanup(stubOpenTarget(t, proto, display, impact80Channels(), 0x36B0, 0x309F))
- var out bytes.Buffer
- cmd := NewBrightnessCmd()
- cmd.SetOut(&out)
- cmd.SetErr(&out)
- cmd.SetArgs([]string{"logo", "160"})
- err := cmd.Execute()
- if err == nil {
- t.Fatal("Execute() = nil error, want the ambiguous name refused")
- }
- if !strings.Contains(err.Error(), "several channels") {
- t.Errorf("error = %q, want it to say the name is ambiguous", err)
- }
- if len(proto.reports) != 0 {
- t.Errorf("reports = %v, want no write", proto.reports)
- }
- }
- // The value is parsed before the keyboard is opened, so a typo in it costs
- // nothing and reports where the mistake is.
- func TestReversedArgumentsFailOnTheValue(t *testing.T) {
- proto := &fakeZoneProtocol{failAt: -1}
- opened := false
- original := openTarget
- t.Cleanup(func() { openTarget = original })
- stub := openTarget
- openTarget = func(zone string) (rgbProtocol, targetDeviceData, []via.Channel, error) {
- opened = true
- return stub(zone)
- }
- var out bytes.Buffer
- cmd := NewBrightnessCmd()
- cmd.SetOut(&out)
- cmd.SetErr(&out)
- cmd.SetArgs([]string{"160", "logo"})
- err := cmd.Execute()
- if err == nil {
- t.Fatal("Execute() = nil error, want the reversed arguments refused")
- }
- if !strings.Contains(err.Error(), "invalid value") {
- t.Errorf("error = %q, want it to say the value could not be read", err)
- }
- if opened {
- t.Error("the keyboard was opened, want the value rejected first")
- }
- if len(proto.reports) != 0 {
- t.Errorf("reports = %v, want no write", proto.reports)
- }
- }
|