package via import ( "errors" "strings" "testing" ) // probeQueue scripts one answer per channel the probe visits, so a test can say // which channels the keyboard has. func probeQueue(present ...Channel) [][]byte { queue := make([][]byte, 0, probeChannelMax) for c := 1; c <= probeChannelMax; c++ { buf := make([]byte, 32) if containsChannel(present, Channel(c)) { buf[0] = byte(CustomGet) buf[3] = 160 } else { buf[0] = byte(Unhandled) } buf[1] = byte(c) buf[2] = probeValueID queue = append(queue, buf) } return queue } func containsChannel(channels []Channel, want Channel) bool { for _, c := range channels { if c == want { return true } } return false } func TestDetectChannelsFindsTheAnsweredChannels(t *testing.T) { transport := &fakeTransport{queue: probeQueue(ChannelRgblight, ChannelRgbMatrix, ChannelAudio)} protocol := Protocol{handle: transport} got, err := protocol.DetectChannels() if err != nil { t.Fatalf("DetectChannels() error = %v", err) } want := []Channel{ChannelRgblight, ChannelRgbMatrix, ChannelAudio} if len(got) != len(want) { t.Fatalf("DetectChannels() = %v, want %v", got, want) } for i := range want { if got[i] != want[i] { t.Errorf("DetectChannels()[%d] = %d, want %d", i, got[i], want[i]) } } } func TestDetectChannelsAsksBrightnessOnEveryChannelFromOneToFifteen(t *testing.T) { transport := &fakeTransport{queue: probeQueue()} protocol := Protocol{handle: transport} if _, err := protocol.DetectChannels(); err != nil { t.Fatalf("DetectChannels() error = %v", err) } if len(transport.reports) != probeChannelMax { t.Fatalf("requests = %d, want %d", len(transport.reports), probeChannelMax) } for i, report := range transport.reports { wantChannel := byte(i + 1) if report[0] != byte(CustomGet) { t.Errorf("request %d command = 0x%02x, want 0x%02x", i, report[0], CustomGet) } if report[1] != wantChannel { t.Errorf("request %d channel = %d, want %d", i, report[1], wantChannel) } if report[2] != probeValueID { t.Errorf("request %d value = 0x%02x, want 0x%02x (brightness)", i, report[2], probeValueID) } } } // A channel list missing entries is indistinguishable from a complete one, so a // transport failure has to abort the probe instead of shrinking it. func TestDetectChannelsFailsOnATransportErrorMidProbe(t *testing.T) { transport := &fakeTransport{ queue: probeQueue(ChannelRgblight, ChannelRgbMatrix, ChannelAudio), readErr: errors.New("read: interrupted system call"), readErrAfter: 3, } protocol := Protocol{handle: transport} _, err := protocol.DetectChannels() if err == nil { t.Fatal("DetectChannels() expected an error, got nil") } if !strings.Contains(err.Error(), "channel 4") { t.Errorf("error = %q, want it to name the channel that failed", err) } } // A 0xFF that matches nothing is a valid "no such channel". Only a 0xFF whose // echoed channel disagrees is a desynchronised stream. func TestDetectChannelsAcceptsAWellFormedUnhandledAnswer(t *testing.T) { transport := &fakeTransport{queue: probeQueue(ChannelRgblight)} protocol := Protocol{handle: transport} got, err := protocol.DetectChannels() if err != nil { t.Fatalf("DetectChannels() error = %v", err) } if len(got) != 1 || got[0] != ChannelRgblight { t.Errorf("DetectChannels() = %v, want [2]", got) } } func TestDetectChannelsReportsNoChannelsWhenNoneArePresent(t *testing.T) { transport := &fakeTransport{queue: probeQueue()} protocol := Protocol{handle: transport} got, err := protocol.DetectChannels() if err != nil { t.Fatalf("DetectChannels() error = %v", err) } if len(got) != 0 { t.Errorf("DetectChannels() = %v, want empty", got) } } // effectResponse is one scripted answer to an effect get or set, carrying the // value the keyboard is left with. func effectResponse(command Message, ch Channel, value byte) []byte { buf := make([]byte, 32) buf[0] = byte(command) buf[1] = byte(ch) buf[2] = effectValueID buf[3] = value return buf } // QMK clamps rather than rejects: a mode at or above the top comes down to it. // A single write above the top therefore returns the top itself, which is the // whole reason the probe is one round trip instead of a search. func TestEffectTopReadsTheTopOffWhatTheFirmwareClampedTo(t *testing.T) { const ch = ChannelRgbMatrix transport := &fakeTransport{queue: [][]byte{ effectResponse(CustomGet, ch, 12), // the effect the channel is on effectResponse(CustomSet, ch, 255), // the probe, echoed effectResponse(CustomGet, ch, 45), // what the clamp left effectResponse(CustomSet, ch, 12), // the restore, echoed }} protocol := Protocol{handle: transport} got, err := protocol.EffectTop(ch) if err != nil { t.Fatalf("EffectTop() error = %v", err) } if got != 45 { t.Errorf("EffectTop() = %d, want 45", got) } if len(transport.reports) != 4 { t.Fatalf("requests = %d, want 4 (read, probe, read back, restore)", len(transport.reports)) } last := transport.reports[3] if last[0] != byte(CustomSet) || last[3] != 12 { t.Errorf("last request = %v, want the original effect 12 written back", last[:4]) } } // A channel already sitting on its last effect answers the probe with the value // it held, which is the same number the clamp would give. The two cases need not // be told apart, so the answer is still the top. func TestEffectTopReportsTheTopWhenTheChannelAlreadySatOnIt(t *testing.T) { const ch = ChannelRgbMatrix transport := &fakeTransport{queue: [][]byte{ effectResponse(CustomGet, ch, 45), effectResponse(CustomSet, ch, 255), effectResponse(CustomGet, ch, 45), effectResponse(CustomSet, ch, 45), }} protocol := Protocol{handle: transport} got, err := protocol.EffectTop(ch) if err != nil { t.Fatalf("EffectTop() error = %v", err) } if got != 45 { t.Errorf("EffectTop() = %d, want 45", got) } } // VIA reads a 0 on the effect parameter as "turn this channel off", so the probe // must not write one. The restore may: a channel that was on effect 0 goes back // to 0, which is the state it was in. func TestEffectTopProbesWithoutSwitchingTheChannelOff(t *testing.T) { if effectProbeValue == 0 { t.Fatal("effectProbeValue is 0, which switches the channel off instead of asking") } const ch = ChannelRgbMatrix transport := &fakeTransport{queue: [][]byte{ effectResponse(CustomGet, ch, 0), effectResponse(CustomSet, ch, effectProbeValue), effectResponse(CustomGet, ch, 6), effectResponse(CustomSet, ch, 0), }} protocol := Protocol{handle: transport} if _, err := protocol.EffectTop(ch); err != nil { t.Fatalf("EffectTop() error = %v", err) } probe := transport.reports[1] if probe[0] != byte(CustomSet) || probe[2] != effectValueID { t.Fatalf("request 1 = %v, want a set on the effect parameter", probe[:4]) } if probe[3] != effectProbeValue { t.Errorf("probe wrote effect %d, want %d", probe[3], effectProbeValue) } } // An effect that cannot be read is a channel the probe cannot restore, so it // stops before it writes rather than leaving the channel on the last effect. func TestEffectTopStopsBeforeWritingWhenTheEffectCannotBeRead(t *testing.T) { const ch = ChannelRgbMatrix transport := &fakeTransport{queue: [][]byte{effectResponse(Unhandled, ch, 0)}} protocol := Protocol{handle: transport} if _, err := protocol.EffectTop(ch); err == nil { t.Fatal("EffectTop() = nil error, want a failure for an effect that cannot be read") } if len(transport.reports) != 1 { t.Errorf("requests = %d, want only the read that failed", len(transport.reports)) } } func TestChannelSubsystemNames(t *testing.T) { tests := []struct { channel Channel want string }{ {ChannelBacklight, "backlight"}, {ChannelRgblight, "rgblight"}, {ChannelRgbMatrix, "rgb_matrix"}, {ChannelAudio, "audio"}, {ChannelLedMatrix, "led_matrix"}, {Channel(9), ""}, } for _, tt := range tests { if got := tt.channel.Subsystem(); got != tt.want { t.Errorf("Channel(%d).Subsystem() = %q, want %q", tt.channel, got, tt.want) } } } // A stale 0xFF — the answer to an earlier request, still in the read buffer — // looks exactly like absence. Reading the channel out of it is what tells the // two apart, and a mismatch means the request/response stream is out of step. func TestDetectChannelsRejectsAnUnhandledAnswerForAnotherChannel(t *testing.T) { queue := probeQueue(ChannelRgblight, ChannelRgbMatrix, ChannelAudio) // Channel 2 answers "unhandled", but for a different channel: the stream // is answering a request that is not this one. stale := make([]byte, 32) stale[0] = byte(Unhandled) stale[1] = 0x07 stale[2] = probeValueID queue[1] = stale transport := &fakeTransport{queue: queue} protocol := Protocol{handle: transport} _, err := protocol.DetectChannels() if err == nil { t.Fatal("DetectChannels() expected an error for an answer belonging to another channel") } if !strings.Contains(err.Error(), "channel 2") { t.Errorf("error = %q, want it to name the channel whose answer did not match", err) } }