package main import ( "bytes" "errors" "strings" "testing" intrgb "netdome.biz/paul/qmk-rgb/internal/rgb" "netdome.biz/paul/qmk-rgb/internal/via" ) // verifyingProtocol records writes and reports what the keyboard actually // applied, which on real hardware is not always the requested value. type verifyingProtocol struct { reports []commandReport applied map[via.Channel]uint8 channels []via.Channel getErr error getCalls int } // impact80Display is the channel naming the Impact 80's definition file // supplies, so summary lines name logo, backlight and side. func impact80Display() map[uint16]string { return map[uint16]string{2: "logo", 3: "backlight", 4: "side"} } func impact80Channels() []via.Channel { return []via.Channel{via.ChannelRgblight, via.ChannelRgbMatrix, via.ChannelAudio} } // DetectChannels reports the three channels of the board under test unless a // test asks for a different set. func (f *verifyingProtocol) DetectChannels() ([]via.Channel, error) { if f.channels == nil { return []via.Channel{via.ChannelRgblight, via.ChannelRgbMatrix, via.ChannelAudio}, nil } return f.channels, nil } func (f *verifyingProtocol) SetValue(channel via.Channel, param, value uint8) error { f.reports = append(f.reports, commandReport{channel: channel, param: param, value: value}) return nil } func (f *verifyingProtocol) SetColor(via.Channel, uint8, uint8) error { return nil } func (f *verifyingProtocol) GetValue(channel via.Channel, param uint8) ([]byte, error) { f.getCalls++ if f.getErr != nil { return nil, f.getErr } // The color value ID is two bytes; a one byte answer would make every // caller that reads a colour fail for the wrong reason. if param == uint8(intrgb.ColorValue) { return []byte{f.applied[channel], 255}, nil } return []byte{f.applied[channel]}, nil } func (f *verifyingProtocol) Close() error { return nil } func appliedFor(p *verifyingProtocol, channel via.Channel) uint8 { return p.applied[channel] } func TestSetBrightnessVerifiedReportsAppliedValue(t *testing.T) { tests := []struct { name string requested uint8 applied uint8 wantWarning bool }{ {"keyboard honours the value", 100, 100, false}, {"keyboard clamps to 160", 200, 160, true}, {"keyboard scales up", 100, 159, true}, {"both zones clamp", 255, 160, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { proto := &verifyingProtocol{ applied: map[via.Channel]uint8{ via.ChannelRgblight: tt.applied, via.ChannelRgbMatrix: tt.applied, via.ChannelAudio: tt.applied, }, } zones := []via.Channel{via.ChannelRgblight} results, err := setBrightnessVerified(proto, zones, impact80Display(), tt.requested) if err != nil { t.Fatalf("setBrightnessVerified() error = %v", err) } if len(results) != 1 { t.Fatalf("results = %d entries, want 1", len(results)) } got := results[0] if got.Name != "logo" { t.Errorf("results[0].Name = %q, want %q", got.Name, "logo") } if got.Requested != tt.requested { t.Errorf("results[0].Requested = %d, want %d", got.Requested, tt.requested) } if got.Applied != tt.applied { t.Errorf("results[0].Applied = %d, want %d", got.Applied, tt.applied) } if got.Mismatch() != tt.wantWarning { t.Errorf("results[0].Mismatch() = %t, want %t", got.Mismatch(), tt.wantWarning) } }) } } // The read-back must happen once per written zone, on the right channel. func TestSetBrightnessVerifiedReadsBackEveryZone(t *testing.T) { proto := &verifyingProtocol{ applied: map[via.Channel]uint8{ via.ChannelRgblight: 160, via.ChannelRgbMatrix: 255, via.ChannelAudio: 160, }, } results, err := setBrightnessVerified(proto, impact80Channels(), impact80Display(), 200) if err != nil { t.Fatalf("setBrightnessVerified() error = %v", err) } if len(results) != 3 { t.Fatalf("results = %d entries, want 3", len(results)) } if proto.getCalls != 3 { t.Errorf("GetValue calls = %d, want 3", proto.getCalls) } // A set that fails must not be reported as applied. if got := appliedFor(proto, via.ChannelRgblight); got != 160 { t.Errorf("unexpected applied value %d", got) } want := map[string]uint8{ "logo": 160, "backlight": 255, "side": 160, } for _, r := range results { if r.Applied != want[r.Name] { t.Errorf("%s applied = %d, want %d", r.Name, r.Applied, want[r.Name]) } if !r.Mismatch() { t.Errorf("%s: Mismatch() = false, want true (requested 200)", r.Name) } } } // A failed read-back must not be silently reported as success. func TestSetBrightnessVerifiedPropagatesReadError(t *testing.T) { proto := &verifyingProtocol{getErr: errors.New("read timeout")} if _, err := setBrightnessVerified(proto, []via.Channel{via.ChannelRgblight}, impact80Display(), 100); err == nil { t.Fatal("setBrightnessVerified() expected read error, got nil") } } func TestBrightnessCommandStaysQuietWhenApplied(t *testing.T) { proto := &verifyingProtocol{applied: map[via.Channel]uint8{via.ChannelRgblight: 200}} t.Cleanup(impact80Target(t, proto)) var out, errOut bytes.Buffer cmd := NewBrightnessCmd() cmd.SetOut(&out) cmd.SetErr(&errOut) cmd.SetArgs([]string{"200"}) if err := cmd.Execute(); err != nil { t.Fatalf("brightness returned error: %v", err) } if errOut.Len() != 0 { t.Errorf("stderr = %q, want no warning when the keyboard applied the requested value", errOut.String()) } if !strings.Contains(out.String(), "200") { t.Errorf("stdout = %q, want the applied value reported", out.String()) } }