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,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))
|
||||
}
|
||||
Reference in new issue
Block a user