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) } }