package main import ( "errors" "reflect" "testing" "netdome.biz/paul/qmk-rgb/internal/via" ) type commandReport struct { channel via.Channel param uint8 value uint8 hue uint8 saturation uint8 } type fakeZoneProtocol struct { reports []commandReport failAt int channels []via.Channel } // DetectChannels reports the board's three channels unless a test asks for a // different set. func (f *fakeZoneProtocol) DetectChannels() ([]via.Channel, error) { if f.channels == nil { return impact80Channels(), nil } return f.channels, nil } func (f *fakeZoneProtocol) SetValue(channel via.Channel, param, value uint8) error { if f.failAt >= 0 && f.failAt == len(f.reports) { return errors.New("write failed") } f.reports = append(f.reports, commandReport{ channel: channel, param: param, value: value, }) return nil } func (f *fakeZoneProtocol) SetColor(channel via.Channel, hue, saturation uint8) error { if f.failAt >= 0 && f.failAt == len(f.reports) { return errors.New("write failed") } f.reports = append(f.reports, commandReport{ channel: channel, param: 4, hue: hue, saturation: saturation, }) return nil } // GetValue reports the last value written to a channel, so a command that reads // back what it set sees a keyboard that accepted it. A channel that was never // written to still fails, so a read-back without a preceding set stays visible. func (f *fakeZoneProtocol) GetValue(channel via.Channel, param uint8) ([]byte, error) { for i := len(f.reports) - 1; i >= 0; i-- { if f.reports[i].channel == channel && f.reports[i].param == param { return []byte{f.reports[i].value}, nil } } return nil, errors.New("unexpected GetValue call") } func (f *fakeZoneProtocol) Close() error { return nil } func TestSelectedChannelOperations(t *testing.T) { defaultZones := []via.Channel{via.ChannelRgblight, via.ChannelRgbMatrix, via.ChannelAudio} sideZones := []via.Channel{via.ChannelAudio} operations := []struct { name string zones []via.Channel sideZones []via.Channel run func(zoneProtocol, []via.Channel) error want []commandReport wantSide []commandReport }{ { name: "brightness", zones: defaultZones, sideZones: sideZones, run: func(protocol zoneProtocol, zones []via.Channel) error { return setBrightnessOnChannels(protocol, zones, 160) }, want: []commandReport{ {channel: 2, param: 1, value: 160}, {channel: 3, param: 1, value: 160}, {channel: 4, param: 1, value: 160}, }, wantSide: []commandReport{{channel: 4, param: 1, value: 160}}, }, { name: "speed", zones: defaultZones, sideZones: sideZones, run: func(protocol zoneProtocol, zones []via.Channel) error { return setSpeedChannels(protocol, zones, 2) }, want: []commandReport{ {channel: 2, param: 3, value: 2}, {channel: 3, param: 3, value: 2}, {channel: 4, param: 3, value: 2}, }, wantSide: []commandReport{{channel: 4, param: 3, value: 2}}, }, { name: "color", zones: defaultZones, sideZones: sideZones, run: func(protocol zoneProtocol, zones []via.Channel) error { return setColorOnChannels(protocol, zones, 85, 255) }, want: []commandReport{ {channel: 2, param: 4, hue: 85, saturation: 255}, {channel: 3, param: 4, hue: 85, saturation: 255}, {channel: 4, param: 4, hue: 85, saturation: 255}, }, wantSide: []commandReport{{channel: 4, param: 4, hue: 85, saturation: 255}}, }, { name: "disable", zones: defaultZones, sideZones: sideZones, run: disableLightingOnChannels, want: []commandReport{ {channel: 2, param: 2, value: 0}, {channel: 2, param: 1, value: 0}, {channel: 3, param: 2, value: 0}, {channel: 3, param: 1, value: 0}, {channel: 4, param: 2, value: 0}, {channel: 4, param: 1, value: 0}, }, wantSide: []commandReport{ {channel: 4, param: 2, value: 0}, {channel: 4, param: 1, value: 0}, }, }, { name: "enable", zones: defaultZones, sideZones: sideZones, run: func(protocol zoneProtocol, channels []via.Channel) error { return enableLightingOnChannels(protocol, channels, impact80Catalog(t)) }, // enable writes the first effect each channel's own list names, which // is wave on logo and side and solid_color on the backlight. want: []commandReport{ {channel: 2, param: 2, value: 1}, {channel: 2, param: 1, value: 160}, {channel: 3, param: 2, value: 1}, {channel: 3, param: 1, value: 160}, {channel: 4, param: 2, value: 1}, {channel: 4, param: 1, value: 160}, }, wantSide: []commandReport{ {channel: 4, param: 2, value: 1}, {channel: 4, param: 1, value: 160}, }, }, } for _, tc := range operations { t.Run(tc.name+"/default", func(t *testing.T) { protocol := &fakeZoneProtocol{failAt: -1} if err := tc.run(protocol, tc.zones); err != nil { t.Fatalf("operation error = %v", err) } if !reflect.DeepEqual(protocol.reports, tc.want) { t.Errorf("reports = %v, want %v", protocol.reports, tc.want) } }) t.Run(tc.name+"/side", func(t *testing.T) { protocol := &fakeZoneProtocol{failAt: -1} if err := tc.run(protocol, tc.sideZones); err != nil { t.Fatalf("operation error = %v", err) } if !reflect.DeepEqual(protocol.reports, tc.wantSide) { t.Errorf("reports = %v, want %v", protocol.reports, tc.wantSide) } }) } } func TestSelectedChannelStopsOnFirstError(t *testing.T) { protocol := &fakeZoneProtocol{failAt: 1} err := setBrightnessOnChannels(protocol, impact80Channels(), 160) if err == nil { t.Fatal("setBrightnessOnChannels() expected error, got nil") } if len(protocol.reports) != 1 { t.Errorf("reports = %v, want one report before failure", protocol.reports) } } // README.md promises the commands that take no arguments reject a stray token. // keyboard info declared none, so cobra accepted any number of them. func TestKeyboardInfoRejectsAStrayToken(t *testing.T) { if keyboardInfoCmd.Args == nil { t.Fatal("keyboard info declares no Args, so a stray token is accepted") } if err := keyboardInfoCmd.Args(keyboardInfoCmd, []string{"bogus"}); err == nil { t.Error("keyboard info accepted a stray token, want it rejected") } if err := keyboardInfoCmd.Args(keyboardInfoCmd, nil); err != nil { t.Errorf("keyboard info rejected no token: %v", err) } }