Normalize captured autorepeat for quarantined Remote
This commit is contained in:
1 parent
ee1c32f049
commit
6b3aa80138
6 files changed
+561
-3
No files matched your search
@@ -0,0 +1,218 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package input
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type repeatStep struct {
|
||||
code uint8
|
||||
pressed bool
|
||||
stamp uint32
|
||||
want []RemoteRepeatTransition
|
||||
}
|
||||
|
||||
func down(code uint8) RemoteRepeatTransition {
|
||||
return RemoteRepeatTransition{Keycode: code, Pressed: true}
|
||||
}
|
||||
func repeat(code uint8) RemoteRepeatTransition {
|
||||
return RemoteRepeatTransition{Keycode: code, Pressed: true, Repeat: true}
|
||||
}
|
||||
func up(code uint8) RemoteRepeatTransition { return RemoteRepeatTransition{Keycode: code} }
|
||||
|
||||
func newTestNormalizer(t *testing.T) *RemoteRepeatNormalizer {
|
||||
t.Helper()
|
||||
normalizer, err := NewRemoteRepeatNormalizer(RemoteRepeatReleaseDelay)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return normalizer
|
||||
}
|
||||
|
||||
func runRepeatSteps(t *testing.T, normalizer *RemoteRepeatNormalizer, now time.Time, steps []repeatStep) {
|
||||
t.Helper()
|
||||
for index, step := range steps {
|
||||
got, err := normalizer.Observe(Event{Keycode: step.code, Pressed: step.pressed, Timestamp: step.stamp}, now)
|
||||
if err != nil {
|
||||
t.Fatalf("step %d: %v", index, err)
|
||||
}
|
||||
if len(got) == 0 && len(step.want) == 0 {
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(got, step.want) {
|
||||
t.Fatalf("step %d: transitions = %+v, want %+v", index, got, step.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBehaviorRemoteRepeatNormalizesClassicPairs(t *testing.T) {
|
||||
normalizer := newTestNormalizer(t)
|
||||
now := time.Unix(100, 0)
|
||||
runRepeatSteps(t, normalizer, now, []repeatStep{
|
||||
{38, true, 100, []RemoteRepeatTransition{down(38)}},
|
||||
{38, false, 660, nil}, // Classic pair: release held.
|
||||
{38, true, 660, []RemoteRepeatTransition{repeat(38)}},
|
||||
{38, false, 700, nil},
|
||||
{38, true, 700, []RemoteRepeatTransition{repeat(38)}},
|
||||
{38, false, 750, nil}, // Real release: no matching press follows.
|
||||
{39, true, 760, []RemoteRepeatTransition{up(38), down(39)}},
|
||||
{39, false, 770, nil},
|
||||
{39, true, 780, []RemoteRepeatTransition{up(39), down(39)}}, // Different stamp is a fresh press.
|
||||
})
|
||||
if _, pending := normalizer.Deadline(); pending {
|
||||
t.Fatal("fresh press left a pending release")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBehaviorRemoteRepeatPassesDetectableRepeat(t *testing.T) {
|
||||
normalizer := newTestNormalizer(t)
|
||||
runRepeatSteps(t, normalizer, time.Unix(1, 0), []repeatStep{
|
||||
{50, true, 10, []RemoteRepeatTransition{down(50)}},
|
||||
{50, true, 20, []RemoteRepeatTransition{repeat(50)}},
|
||||
{50, true, 30, []RemoteRepeatTransition{repeat(50)}},
|
||||
{38, true, 40, []RemoteRepeatTransition{down(38)}},
|
||||
{38, false, 0, []RemoteRepeatTransition{up(38)}}, // Unknown stamp releases immediately.
|
||||
{38, true, 0, []RemoteRepeatTransition{down(38)}},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBehaviorRemoteRepeatPendingReleaseFlushesAtDeadline(t *testing.T) {
|
||||
normalizer := newTestNormalizer(t)
|
||||
start := time.Unix(5, 0)
|
||||
runRepeatSteps(t, normalizer, start, []repeatStep{
|
||||
{38, true, 100, []RemoteRepeatTransition{down(38)}},
|
||||
{38, false, 200, nil},
|
||||
})
|
||||
deadline, pending := normalizer.Deadline()
|
||||
if !pending || !deadline.Equal(start.Add(RemoteRepeatReleaseDelay)) {
|
||||
t.Fatalf("deadline = %v %t", deadline, pending)
|
||||
}
|
||||
if got := normalizer.Flush(deadline.Add(-time.Nanosecond)); got != nil {
|
||||
t.Fatalf("early flush = %+v", got)
|
||||
}
|
||||
if got := normalizer.Flush(deadline); !reflect.DeepEqual(got, []RemoteRepeatTransition{up(38)}) {
|
||||
t.Fatalf("flush = %+v", got)
|
||||
}
|
||||
if got := normalizer.Flush(deadline.Add(time.Second)); got != nil {
|
||||
t.Fatalf("repeated flush = %+v", got)
|
||||
}
|
||||
// A press with the old stamp after the flush is a fresh, owned press.
|
||||
runRepeatSteps(t, normalizer, deadline, []repeatStep{
|
||||
{38, true, 200, []RemoteRepeatTransition{down(38)}},
|
||||
})
|
||||
if got := normalizer.Retire(); !reflect.DeepEqual(got, []RemoteRepeatTransition{up(38)}) {
|
||||
t.Fatalf("retire = %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBehaviorRemoteRepeatSuppressesUnownedReleases(t *testing.T) {
|
||||
normalizer := newTestNormalizer(t)
|
||||
runRepeatSteps(t, normalizer, time.Unix(1, 0), []repeatStep{
|
||||
{64, false, 10, nil}, // Held before capture began.
|
||||
{64, false, 0, nil},
|
||||
{38, true, 20, []RemoteRepeatTransition{down(38)}},
|
||||
{38, false, 30, nil},
|
||||
{64, false, 31, []RemoteRepeatTransition{up(38)}}, // Pending release flushed; unowned up suppressed.
|
||||
})
|
||||
if _, pending := normalizer.Deadline(); pending {
|
||||
t.Fatal("unowned release became pending")
|
||||
}
|
||||
if got := normalizer.Retire(); len(got) != 0 {
|
||||
t.Fatalf("retire released unowned keys: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBehaviorRemoteRepeatRetireReleasesReverseOrderOnce(t *testing.T) {
|
||||
normalizer := newTestNormalizer(t)
|
||||
runRepeatSteps(t, normalizer, time.Unix(1, 0), []repeatStep{
|
||||
{50, true, 1, []RemoteRepeatTransition{down(50)}},
|
||||
{37, true, 2, []RemoteRepeatTransition{down(37)}},
|
||||
{38, true, 3, []RemoteRepeatTransition{down(38)}},
|
||||
{37, false, 4, nil}, // Pending release is still owned at retirement.
|
||||
})
|
||||
want := []RemoteRepeatTransition{up(38), up(37), up(50)}
|
||||
if got := normalizer.Retire(); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("retire = %+v, want %+v", got, want)
|
||||
}
|
||||
if got := normalizer.Retire(); got != nil {
|
||||
t.Fatalf("second retire = %+v", got)
|
||||
}
|
||||
if _, pending := normalizer.Deadline(); pending {
|
||||
t.Fatal("retired normalizer kept a deadline")
|
||||
}
|
||||
if got := normalizer.Flush(time.Unix(100, 0)); got != nil {
|
||||
t.Fatalf("retired flush = %+v", got)
|
||||
}
|
||||
if _, err := normalizer.Observe(Event{Keycode: 38, Pressed: true, Timestamp: 9}, time.Unix(2, 0)); !errors.Is(err, ErrRemoteKeyState) {
|
||||
t.Fatalf("observe after retire = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBehaviorRemoteRepeatStopsOnInvalidOrExcessKeys(t *testing.T) {
|
||||
normalizer := newTestNormalizer(t)
|
||||
now := time.Unix(1, 0)
|
||||
runRepeatSteps(t, normalizer, now, []repeatStep{{38, true, 1, []RemoteRepeatTransition{down(38)}}})
|
||||
if _, err := normalizer.Observe(Event{Keycode: 7, Pressed: true, Timestamp: 2}, now); !errors.Is(err, ErrRemoteKeyUnavailable) {
|
||||
t.Fatalf("invalid keycode = %v", err)
|
||||
}
|
||||
if _, err := normalizer.Observe(Event{Keycode: 39, Pressed: true, Timestamp: 3}, now); !errors.Is(err, ErrRemoteKeyState) {
|
||||
t.Fatalf("stopped admission = %v", err)
|
||||
}
|
||||
if got := normalizer.Retire(); !reflect.DeepEqual(got, []RemoteRepeatTransition{up(38)}) {
|
||||
t.Fatalf("retire after invalid = %+v", got)
|
||||
}
|
||||
|
||||
normalizer = newTestNormalizer(t)
|
||||
for index := 0; index < RemoteHeldKeyLimit; index++ {
|
||||
code := uint8(remoteRepeatMinKeycode + index)
|
||||
runRepeatSteps(t, normalizer, now, []repeatStep{{code, true, uint32(index + 1), []RemoteRepeatTransition{down(code)}}})
|
||||
}
|
||||
// Repeats of owned keys remain admissible at the limit.
|
||||
runRepeatSteps(t, normalizer, now, []repeatStep{{8, true, 100, []RemoteRepeatTransition{repeat(8)}}})
|
||||
// A release followed by a different fresh key emits the release before the
|
||||
// limit is checked again, so the new key fits.
|
||||
runRepeatSteps(t, normalizer, now, []repeatStep{
|
||||
{8, false, 101, nil},
|
||||
{200, true, 102, []RemoteRepeatTransition{up(8), down(200)}},
|
||||
})
|
||||
if _, err := normalizer.Observe(Event{Keycode: 201, Pressed: true, Timestamp: 103}, now); !errors.Is(err, ErrRemoteKeyLimit) {
|
||||
t.Fatalf("overflow = %v", err)
|
||||
}
|
||||
got := normalizer.Retire()
|
||||
if len(got) != RemoteHeldKeyLimit || got[0] != up(200) || got[len(got)-1] != up(9) {
|
||||
t.Fatalf("retire after overflow = %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBehaviorRemoteRepeatPendingDoesNotPairOtherKeysOrReleases(t *testing.T) {
|
||||
normalizer := newTestNormalizer(t)
|
||||
runRepeatSteps(t, normalizer, time.Unix(1, 0), []repeatStep{
|
||||
{38, true, 1, []RemoteRepeatTransition{down(38)}},
|
||||
{39, true, 2, []RemoteRepeatTransition{down(39)}},
|
||||
{38, false, 10, nil},
|
||||
{39, true, 10, []RemoteRepeatTransition{up(38), repeat(39)}}, // Same stamp, other key.
|
||||
{39, false, 11, nil},
|
||||
{39, false, 11, []RemoteRepeatTransition{up(39)}}, // Duplicate release is unowned once flushed.
|
||||
})
|
||||
}
|
||||
|
||||
func TestBehaviorRemoteRepeatRejectsInvalidDelay(t *testing.T) {
|
||||
for _, delay := range []time.Duration{0, -time.Millisecond, RemoteRepeatReleaseDelayMax + time.Nanosecond} {
|
||||
if _, err := NewRemoteRepeatNormalizer(delay); !errors.Is(err, ErrRemoteRepeatDelay) {
|
||||
t.Fatalf("delay %v: %v", delay, err)
|
||||
}
|
||||
}
|
||||
var normalizer *RemoteRepeatNormalizer
|
||||
if _, err := normalizer.Observe(Event{Keycode: 38, Pressed: true}, time.Time{}); !errors.Is(err, ErrRemoteKeyState) {
|
||||
t.Fatalf("nil observe = %v", err)
|
||||
}
|
||||
if normalizer.Retire() != nil || normalizer.Flush(time.Time{}) != nil {
|
||||
t.Fatal("nil normalizer emitted transitions")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user