Normalize captured autorepeat for quarantined Remote

This commit is contained in:
Chase Cichorz committed 2026-11-06 14:30:43 -05:00
1 parent ee1c32f049
commit 6b3aa80138
6 files changed
+561 -3

No files matched your search

@@ -0,0 +1,141 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package xorg
import (
"context"
"os"
"os/exec"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/input"
)
// A held XTEST key exercises the server's own autorepeat through the real
// XRecord listener. The test records the raw shape and checks that the
// quarantined normalizer yields one down, repeated downs and one final up.
// It does not attach the normalizer to production capture or forwarding.
// Run only through tests/native_remote_input.py.
func TestNativeRemoteRepeatNormalizesHeldKey(t *testing.T) {
if os.Getenv("CTHULHU_NATIVE_REMOTE_INPUT") != "1" {
t.Skip("requires private Remote input harness")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
run := func(args ...string) {
t.Helper()
if out, err := exec.CommandContext(ctx, args[0], args[1:]...).CombinedOutput(); err != nil {
t.Fatalf("%v: %v %s", args, err, out)
}
}
listener := New()
if err := listener.Start(ctx); err != nil {
t.Fatal(err)
}
defer func() {
stopCtx, stopCancel := context.WithTimeout(context.Background(), 2*time.Second)
defer stopCancel()
if err := listener.Stop(stopCtx); err != nil {
t.Error(err)
}
}()
tick := time.NewTicker(20 * time.Millisecond)
defer tick.Stop()
ready:
for {
select {
case <-tick.C:
run("xdotool", "key", "F12")
case event := <-listener.Events():
if event.Name == "F12" && !event.Pressed {
break ready
}
case <-ctx.Done():
t.Fatal("XRecord did not observe readiness marker")
}
}
tick.Stop()
// Server autorepeat is a private Xvfb setting here, never the desktop's.
// Set it while the listener holds a connection: Xvfb resets defaults
// when its last client disconnects.
run("xset", "r", "on", "r", "rate", "200", "40")
normalizer, err := input.NewRemoteRepeatNormalizer(input.RemoteRepeatReleaseDelay)
if err != nil {
t.Fatal(err)
}
run("xdotool", "keydown", "a")
time.Sleep(700 * time.Millisecond)
run("xdotool", "keyup", "a")
var rawDowns, rawUps, classicPairs int
var lastUpStamp uint32
var normalized []input.RemoteRepeatTransition
timer := time.NewTimer(time.Second)
defer timer.Stop()
flush := time.NewTimer(time.Hour)
defer flush.Stop()
armFlush := func() {
if deadline, pending := normalizer.Deadline(); pending {
flush.Reset(time.Until(deadline))
}
}
collect:
for {
select {
case event := <-listener.Events():
if event.Name != "a" {
continue
}
if event.Pressed {
rawDowns++
if event.Timestamp != 0 && event.Timestamp == lastUpStamp {
classicPairs++
}
} else {
rawUps++
lastUpStamp = event.Timestamp
}
got, err := normalizer.Observe(event, time.Now())
if err != nil {
t.Fatal(err)
}
normalized = append(normalized, got...)
armFlush()
case now := <-flush.C:
normalized = append(normalized, normalizer.Flush(now)...)
armFlush()
case <-timer.C:
break collect
case <-ctx.Done():
t.Fatal("held key events missing")
}
}
normalized = append(normalized, normalizer.Flush(time.Now().Add(time.Hour))...)
if rawDowns < 6 {
t.Fatalf("server produced %d downs for a held key; autorepeat did not run", rawDowns)
}
if len(normalized) < 3 || normalized[0] != (input.RemoteRepeatTransition{Keycode: normalized[0].Keycode, Pressed: true}) {
t.Fatalf("normalized = %+v", normalized)
}
last := normalized[len(normalized)-1]
if last.Pressed || last.Keycode != normalized[0].Keycode {
t.Fatalf("final normalized transition = %+v", last)
}
for index, transition := range normalized[1 : len(normalized)-1] {
if !transition.Pressed || !transition.Repeat {
t.Fatalf("intermediate transition %d = %+v; normalized = %+v", index+1, transition, normalized)
}
}
if retired := normalizer.Retire(); len(retired) != 0 {
t.Fatalf("released key remained owned: %+v", retired)
}
if listener.Overflow() != 0 {
t.Fatal("unexpected observer overflow")
}
t.Logf("raw downs=%d ups=%d classic pairs=%d; normalized %d transitions with one final up",
rawDowns, rawUps, classicPairs, len(normalized))
}