| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- package main
- import (
- "bytes"
- "errors"
- "strings"
- "testing"
- intrgb "netdome.biz/paul/qmk-rgb/internal/rgb"
- "netdome.biz/paul/qmk-rgb/internal/via"
- )
- // colorProtocol reports the two byte color the keyboard holds for the color
- // value ID, and the single byte brightness for the brightness value ID, so a
- // test can describe a keyboard that applied something other than the request.
- type colorProtocol struct {
- colorWrites []colorWrite
- brightnessWrites []uint8
- appliedColor map[via.Channel][2]uint8
- appliedBright map[via.Channel]uint8
- shortColorRead bool
- setColorErr error
- getErr error
- }
- type colorWrite struct {
- channel via.Channel
- hue uint8
- sat uint8
- }
- func (p *colorProtocol) SetValue(_ via.Channel, param, value uint8) error {
- if param == uint8(intrgb.Brightness) {
- p.brightnessWrites = append(p.brightnessWrites, value)
- }
- return nil
- }
- func (p *colorProtocol) SetColor(channel via.Channel, hue, sat uint8) error {
- if p.setColorErr != nil {
- return p.setColorErr
- }
- p.colorWrites = append(p.colorWrites, colorWrite{channel: channel, hue: hue, sat: sat})
- return nil
- }
- func (p *colorProtocol) GetValue(channel via.Channel, param uint8) ([]byte, error) {
- if p.getErr != nil {
- return nil, p.getErr
- }
- if param == uint8(intrgb.ColorValue) {
- if p.shortColorRead {
- return []byte{0}, nil
- }
- c := p.appliedColor[channel]
- return []byte{c[0], c[1]}, nil
- }
- return []byte{p.appliedBright[channel]}, nil
- }
- func (p *colorProtocol) Close() error { return nil }
- // A color is two bytes on the wire, so the read-back has to look at both. A
- // one byte answer would leave the saturation unverified.
- func TestSetColorVerifiedReadsBackHueAndSaturation(t *testing.T) {
- proto := &colorProtocol{
- appliedColor: map[via.Channel][2]uint8{via.ChannelRgblight: {99, 255}},
- }
- results, err := setColorVerified(proto, []intrgb.Zone{intrgb.ZoneLogo}, 85, 255)
- if err != nil {
- t.Fatalf("setColorVerified() error = %v", err)
- }
- if len(results) != 1 {
- t.Fatalf("results = %d entries, want 1", len(results))
- }
- got := results[0]
- if got.Zone != intrgb.ZoneLogo {
- t.Errorf("results[0].Zone = %q, want %q", got.Zone, intrgb.ZoneLogo)
- }
- if got.RequestedHue != 85 || got.RequestedSaturation != 255 {
- t.Errorf("requested = hue %d sat %d, want hue 85 sat 255", got.RequestedHue, got.RequestedSaturation)
- }
- if got.Hue != 99 || got.Saturation != 255 {
- t.Errorf("applied = hue %d sat %d, want hue 99 sat 255", got.Hue, got.Saturation)
- }
- if !got.Mismatch() {
- t.Error("Mismatch() = false, want true when the keyboard stored another hue")
- }
- }
- func TestSetColorVerifiedReportsNoMismatchWhenKeyboardAppliesTheRequest(t *testing.T) {
- proto := &colorProtocol{
- appliedColor: map[via.Channel][2]uint8{via.ChannelRgblight: {0, 255}},
- }
- results, err := setColorVerified(proto, []intrgb.Zone{intrgb.ZoneLogo}, 0, 255)
- if err != nil {
- t.Fatalf("setColorVerified() error = %v", err)
- }
- if results[0].Mismatch() {
- t.Error("Mismatch() = true, want false when the keyboard applied the request")
- }
- }
- // A truncated answer means the keyboard did not report both components, so the
- // command must not claim it knows the applied color.
- func TestSetColorVerifiedRejectsATruncatedColorRead(t *testing.T) {
- proto := &colorProtocol{shortColorRead: true}
- _, err := setColorVerified(proto, []intrgb.Zone{intrgb.ZoneLogo}, 0, 255)
- if err == nil {
- t.Fatal("setColorVerified() expected an error, got nil")
- }
- if !strings.Contains(err.Error(), "logo") {
- t.Errorf("error = %q, want it to name the zone whose read back was unusable", err)
- }
- }
- func TestSetColorVerifiedPropagatesWriteErrors(t *testing.T) {
- proto := &colorProtocol{setColorErr: errors.New("write refused")}
- _, err := setColorVerified(proto, []intrgb.Zone{intrgb.ZoneLogo}, 0, 255)
- if err == nil {
- t.Fatal("setColorVerified() expected an error, got nil")
- }
- if !strings.Contains(err.Error(), "write refused") {
- t.Errorf("error = %q, want the keyboard's own reason", err)
- }
- }
- func runColor(t *testing.T, proto *colorProtocol, zoneFlag string, args ...string) (stdout, stderr string, err error) {
- t.Helper()
- originalOpen := openRGBProtocol
- originalZone := targetZone
- t.Cleanup(func() {
- openRGBProtocol = originalOpen
- targetZone = originalZone
- })
- openRGBProtocol = func() (rgbProtocol, error) { return proto, nil }
- targetZone = zoneFlag
- var out, errOut bytes.Buffer
- cmd := NewColorCmd()
- cmd.SetOut(&out)
- cmd.SetErr(&errOut)
- cmd.SetArgs(args)
- err = cmd.Execute()
- return out.String(), errOut.String(), err
- }
- func allZonesAppliedColor(hue, sat uint8) map[via.Channel][2]uint8 {
- return map[via.Channel][2]uint8{
- via.ChannelRgblight: {hue, sat},
- via.ChannelRgbMatrix: {hue, sat},
- via.ChannelAudio: {hue, sat},
- }
- }
- func TestColorReportsTheAppliedHueAndSaturation(t *testing.T) {
- // 00ff00 is hue 85 at full saturation.
- proto := &colorProtocol{appliedColor: allZonesAppliedColor(85, 255)}
- stdout, stderr, err := runColor(t, proto, "logo", "00ff00")
- if err != nil {
- t.Fatalf("color returned error: %v", err)
- }
- if strings.TrimSpace(stdout) != "Color set to hue 85 sat 255" {
- t.Errorf("stdout = %q, want the exact success message", stdout)
- }
- if stderr != "" {
- t.Errorf("stderr = %q, want nothing on stderr when the request was met", stderr)
- }
- }
- // The hex notations carry no brightness, so the command must not write one and
- // must not report one.
- func TestColorHexNotationLeavesBrightnessAlone(t *testing.T) {
- proto := &colorProtocol{appliedColor: allZonesAppliedColor(0, 255)}
- stdout, _, err := runColor(t, proto, "", "rgb:ff0000")
- if err != nil {
- t.Fatalf("color returned error: %v", err)
- }
- if len(proto.brightnessWrites) != 0 {
- t.Errorf("brightness writes = %v, want none for a hex notation", proto.brightnessWrites)
- }
- if strings.Contains(stdout, "brightness") {
- t.Errorf("stdout = %q, must not report a brightness the command did not set", stdout)
- }
- }
- // hsv: carries a value, and the keyboard has no value register for it, so the
- // value goes to the brightness of the same zones.
- func TestColorHSVNotationWritesTheValueAsBrightness(t *testing.T) {
- proto := &colorProtocol{
- appliedColor: allZonesAppliedColor(85, 255),
- appliedBright: map[via.Channel]uint8{via.ChannelRgblight: 200, via.ChannelRgbMatrix: 200, via.ChannelAudio: 200},
- }
- stdout, _, err := runColor(t, proto, "", "hsv:85,255,200")
- if err != nil {
- t.Fatalf("color returned error: %v", err)
- }
- if len(proto.brightnessWrites) != 3 {
- t.Fatalf("brightness writes = %v, want one per selected zone", proto.brightnessWrites)
- }
- for _, v := range proto.brightnessWrites {
- if v != 200 {
- t.Errorf("brightness write = %d, want 200", v)
- }
- }
- if strings.TrimSpace(stdout) != "Color set to hue 85 sat 255 brightness 200" {
- t.Errorf("stdout = %q, want hue, saturation and brightness", stdout)
- }
- }
- // The Impact 80 clamps brightness at 160 on logo and side and scales it up on
- // the backlight channel, so an hsv: value is reported the way brightness is.
- func TestColorHSVSummarisesTheBrightnessTheKeyboardApplied(t *testing.T) {
- proto := &colorProtocol{
- appliedColor: allZonesAppliedColor(0, 255),
- appliedBright: map[via.Channel]uint8{via.ChannelRgblight: 160, via.ChannelRgbMatrix: 255, via.ChannelAudio: 160},
- }
- stdout, _, err := runColor(t, proto, "", "hsv:0,255,200")
- if err != nil {
- t.Fatalf("color returned error: %v", err)
- }
- for _, want := range []string{
- "logo hue 0 sat 255 brightness 160",
- "backlight hue 0 sat 255 brightness 255",
- "side hue 0 sat 255 brightness 160",
- "requested hue 0 sat 255 brightness 200",
- } {
- if !strings.Contains(stdout, want) {
- t.Errorf("stdout = %q, want it to contain %q", stdout, want)
- }
- }
- if strings.Contains(stdout, "set to") {
- t.Errorf("stdout = %q, must not claim the request was met when a zone clamped it", stdout)
- }
- if strings.Count(strings.TrimSpace(stdout), "\n") != 0 {
- t.Errorf("stdout = %q, want exactly one line", stdout)
- }
- }
- // A notation the tool cannot parse must fail before the keyboard is opened.
- func TestColorRejectsAnUnknownNotationWithoutOpeningTheDevice(t *testing.T) {
- proto := &colorProtocol{}
- opened := false
- originalOpen := openRGBProtocol
- originalZone := targetZone
- t.Cleanup(func() {
- openRGBProtocol = originalOpen
- targetZone = originalZone
- })
- openRGBProtocol = func() (rgbProtocol, error) {
- opened = true
- return proto, nil
- }
- targetZone = ""
- var out bytes.Buffer
- cmd := NewColorCmd()
- cmd.SetOut(&out)
- cmd.SetErr(&out)
- cmd.SetArgs([]string{"xyz:1"})
- err := cmd.Execute()
- if err == nil {
- t.Fatal("color expected an error, got nil")
- }
- if opened {
- t.Error("the keyboard was opened, want the notation rejected first")
- }
- if len(proto.colorWrites) != 0 {
- t.Errorf("color writes = %v, want none", proto.colorWrites)
- }
- }
|