|
@@ -0,0 +1,285 @@
|
|
|
|
|
+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.LEDType][2]uint8
|
|
|
|
|
+ appliedBright map[via.LEDType]uint8
|
|
|
|
|
+ shortColorRead bool
|
|
|
|
|
+ setColorErr error
|
|
|
|
|
+ getErr error
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type colorWrite struct {
|
|
|
|
|
+ channel via.LEDType
|
|
|
|
|
+ hue uint8
|
|
|
|
|
+ sat uint8
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (p *colorProtocol) SetValue(_ via.LEDType, param, value uint8) error {
|
|
|
|
|
+ if param == uint8(intrgb.Brightness) {
|
|
|
|
|
+ p.brightnessWrites = append(p.brightnessWrites, value)
|
|
|
|
|
+ }
|
|
|
|
|
+ return nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (p *colorProtocol) SetColor(channel via.LEDType, 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.LEDType, 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.LEDType][2]uint8{via.RGBLight: {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.LEDType][2]uint8{via.RGBLight: {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.LEDType][2]uint8 {
|
|
|
|
|
+ return map[via.LEDType][2]uint8{
|
|
|
|
|
+ via.RGBLight: {hue, sat},
|
|
|
|
|
+ via.RGBMatrix: {hue, sat},
|
|
|
|
|
+ via.SideLight: {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.LEDType]uint8{via.RGBLight: 200, via.RGBMatrix: 200, via.SideLight: 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.LEDType]uint8{via.RGBLight: 160, via.RGBMatrix: 255, via.SideLight: 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)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|