240 lines
8.6 KiB
Go
240 lines
8.6 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package remote
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/godbus/dbus/v5"
|
|
)
|
|
|
|
type moduleRecordingHandler struct {
|
|
recordingHandler
|
|
value bool
|
|
command string
|
|
preferencesShown bool
|
|
quitRequested bool
|
|
}
|
|
|
|
func (handler *moduleRecordingHandler) ShowPreferences() bool {
|
|
handler.mutex.Lock()
|
|
defer handler.mutex.Unlock()
|
|
handler.preferencesShown = true
|
|
return true
|
|
}
|
|
func (handler *moduleRecordingHandler) Quit() bool {
|
|
handler.mutex.Lock()
|
|
defer handler.mutex.Unlock()
|
|
handler.quitRequested = true
|
|
return true
|
|
}
|
|
func (handler *moduleRecordingHandler) RuntimeStateSnapshot() string {
|
|
return "CTHULHU RUNTIME STATE SNAPSHOT\n[event_manager]\noverflow: 0"
|
|
}
|
|
|
|
func (handler *moduleRecordingHandler) RemoteGet(module, property string) (dbus.Variant, bool) {
|
|
if module == "TypingEchoManager" && property == "KeyEchoEnabled" {
|
|
handler.mutex.Lock()
|
|
defer handler.mutex.Unlock()
|
|
return dbus.MakeVariant(handler.value), true
|
|
}
|
|
return dbus.Variant{}, false
|
|
}
|
|
func (handler *moduleRecordingHandler) RemoteSet(module, property string, value dbus.Variant) bool {
|
|
if module != "TypingEchoManager" || property != "KeyEchoEnabled" {
|
|
return false
|
|
}
|
|
handler.mutex.Lock()
|
|
defer handler.mutex.Unlock()
|
|
handler.value = value.Value().(bool)
|
|
return true
|
|
}
|
|
func (handler *moduleRecordingHandler) RemoteCommand(module, command string, _ bool) bool {
|
|
handler.mutex.Lock()
|
|
defer handler.mutex.Unlock()
|
|
handler.command = module + "." + command
|
|
return true
|
|
}
|
|
|
|
func TestNativeAndOrcaModuleBus(t *testing.T) {
|
|
if os.Getenv("CTHULHU_NATIVE_DBUS_TEST") != "1" {
|
|
t.Skip("set CTHULHU_NATIVE_DBUS_TEST=1 inside a private session bus")
|
|
}
|
|
handler := &moduleRecordingHandler{recordingHandler: recordingHandler{result: true}}
|
|
logger := slog.New(slog.DiscardHandler)
|
|
native := New(logger, handler)
|
|
orca := NewOrca(logger, handler)
|
|
if err := native.Start(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { native.Stop(context.Background()) })
|
|
if err := orca.Start(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { orca.Stop(context.Background()) })
|
|
client, err := dbus.ConnectSessionBus()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer client.Close()
|
|
for _, test := range []struct {
|
|
name string
|
|
path dbus.ObjectPath
|
|
iface string
|
|
}{
|
|
{BusName, ObjectPath, InterfaceName}, {OrcaBusName, OrcaObjectPath, OrcaInterfaceName},
|
|
} {
|
|
object := client.Object(test.name, test.path)
|
|
var version string
|
|
if err := object.Call(test.iface+".GetVersion", 0).Store(&version); err != nil || !strings.Contains(version, "Cthulhu") {
|
|
t.Fatalf("GetVersion: %q, %v", version, err)
|
|
}
|
|
var accepted bool
|
|
if err := object.Call(test.iface+".ShowPreferences", 0).Store(&accepted); err != nil || !accepted {
|
|
t.Fatalf("ShowPreferences: %v, %v", accepted, err)
|
|
}
|
|
if err := object.Call(test.iface+".Quit", 0).Store(&accepted); err != nil || !accepted {
|
|
t.Fatalf("Quit: %v, %v", accepted, err)
|
|
}
|
|
}
|
|
var speechXML string
|
|
if err := client.Object(BusName, SpeechManagerPath).Call("org.freedesktop.DBus.Introspectable.Introspect", 0).Store(&speechXML); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, signature := range []string{`property name="Rate" type="i" access="readwrite"`, `property name="Pitch" type="d" access="readwrite"`, `method name="IncreaseRate"`} {
|
|
if !strings.Contains(speechXML, signature) {
|
|
t.Fatalf("speech introspection missing %s: %s", signature, speechXML)
|
|
}
|
|
}
|
|
handler.mutex.Lock()
|
|
shown, quit := handler.preferencesShown, handler.quitRequested
|
|
handler.mutex.Unlock()
|
|
if !shown || !quit {
|
|
t.Fatal("control calls did not reach runtime owner")
|
|
}
|
|
var snapshot string
|
|
if err := client.Object(BusName, ObjectPath).Call(InterfaceName+".GetRuntimeStateSnapshot", 0).Store(&snapshot); err != nil || !strings.Contains(snapshot, "RUNTIME STATE SNAPSHOT") {
|
|
t.Fatalf("snapshot = %q, %v", snapshot, err)
|
|
}
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
path dbus.ObjectPath
|
|
iface string
|
|
}{
|
|
{BusName, modulePath(ObjectPath, "TypingEchoManager"), typingEchoInterface},
|
|
{OrcaBusName, modulePath(OrcaObjectPath, "TypingEchoManager"), orcaModuleInterface},
|
|
} {
|
|
object := client.Object(test.name, test.path)
|
|
var xml string
|
|
if err := object.Call("org.freedesktop.DBus.Introspectable.Introspect", 0).Store(&xml); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(xml, `name="`+test.iface+`"`) {
|
|
t.Fatalf("missing interface %s in %s", test.iface, xml)
|
|
}
|
|
if test.name == BusName {
|
|
if !strings.Contains(xml, `property name="KeyEchoEnabled" type="b" access="readwrite"`) {
|
|
t.Fatalf("missing property: %s", xml)
|
|
}
|
|
if !strings.Contains(xml, `method name="CycleKeyEcho"`) {
|
|
t.Fatalf("missing command: %s", xml)
|
|
}
|
|
} else if !strings.Contains(xml, `method name="ExecuteRuntimeSetter"`) {
|
|
t.Fatalf("missing generic protocol: %s", xml)
|
|
}
|
|
}
|
|
|
|
nativeObject := client.Object(BusName, modulePath(ObjectPath, "TypingEchoManager"))
|
|
var value dbus.Variant
|
|
if err := nativeObject.Call("org.freedesktop.DBus.Properties.Get", 0, typingEchoInterface, "KeyEchoEnabled").Store(&value); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if value.Value() != false {
|
|
t.Fatalf("initial value = %v", value)
|
|
}
|
|
if err := nativeObject.Call("org.freedesktop.DBus.Properties.Set", 0, typingEchoInterface, "KeyEchoEnabled", dbus.MakeVariant(true)).Err; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
handler.mutex.Lock()
|
|
gotValue := handler.value
|
|
handler.mutex.Unlock()
|
|
if !gotValue {
|
|
t.Fatal("native setter did not reach runtime owner")
|
|
}
|
|
if err := nativeObject.Call("org.freedesktop.DBus.Properties.Set", 0, typingEchoInterface, "KeyEchoEnabled", dbus.MakeVariant("wrong type")).Err; err == nil {
|
|
t.Fatal("wrong type accepted")
|
|
}
|
|
|
|
orcaObject := client.Object(OrcaBusName, modulePath(OrcaObjectPath, "TypingEchoManager"))
|
|
if err := orcaObject.Call(orcaModuleInterface+".ExecuteRuntimeGetter", 0, "KeyEchoEnabled").Store(&value); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if value.Value() != true {
|
|
t.Fatalf("Orca getter = %v", value)
|
|
}
|
|
var accepted bool
|
|
if err := orcaObject.Call(orcaModuleInterface+".ExecuteRuntimeSetter", 0, "KeyEchoEnabled", dbus.MakeVariant(false)).Store(&accepted); err != nil || !accepted {
|
|
t.Fatalf("Orca setter = %v, %v", accepted, err)
|
|
}
|
|
if err := orcaObject.Call(orcaModuleInterface+".ExecuteCommand", 0, "CycleKeyEcho", true).Store(&accepted); err != nil || !accepted {
|
|
t.Fatalf("Orca command = %v, %v", accepted, err)
|
|
}
|
|
handler.mutex.Lock()
|
|
gotCommand := handler.command
|
|
handler.mutex.Unlock()
|
|
if gotCommand != "TypingEchoManager.CycleKeyEcho" {
|
|
t.Fatalf("command owner got %q", gotCommand)
|
|
}
|
|
if err := orcaObject.Call(orcaModuleInterface+".ExecuteCommand", 0, "Unsupported", true).Err; err == nil {
|
|
t.Fatal("unsupported command falsely succeeded")
|
|
}
|
|
|
|
var names []string
|
|
if err := client.Object(OrcaBusName, OrcaObjectPath).Call(OrcaInterfaceName+".ListModules", 0).Store(&names); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Join(names, ",") != "SpeechManager,TypingEchoManager" {
|
|
t.Fatalf("modules = %v", names)
|
|
}
|
|
var commands []namedDescription
|
|
if err := client.Object(OrcaBusName, OrcaObjectPath).Call(OrcaInterfaceName+".ListCommands", 0).Store(&commands); err != nil || len(commands) != 6 {
|
|
t.Fatalf("service commands = %v, %v", commands, err)
|
|
}
|
|
if err := orcaObject.Call(orcaModuleInterface+".ListRuntimeGetters", 0).Store(&commands); err != nil || len(commands) != 13 {
|
|
t.Fatalf("module getters = %v, %v", commands, err)
|
|
}
|
|
var parameterized []parameterizedDescription
|
|
if err := orcaObject.Call(orcaModuleInterface+".ListParameterizedCommands", 0).Store(¶meterized); err != nil || len(parameterized) != 0 {
|
|
t.Fatalf("parameterized commands = %v, %v", parameterized, err)
|
|
}
|
|
var result dbus.Variant
|
|
if err := orcaObject.Call(orcaModuleInterface+".ExecuteParameterizedCommand", 0, "Unsupported", map[string]dbus.Variant{}, false).Store(&result); err == nil {
|
|
t.Fatal("unsupported parameterized command falsely succeeded")
|
|
}
|
|
conflict := NewOrca(logger, handler)
|
|
if err := conflict.Start(context.Background()); err == nil {
|
|
conflict.Stop(context.Background())
|
|
t.Fatal("second Orca name acquired")
|
|
}
|
|
if err := orca.Stop(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := nativeObject.Call("org.freedesktop.DBus.Properties.Get", 0, typingEchoInterface, "KeyEchoEnabled").Store(&value); err != nil {
|
|
t.Fatalf("native service lost when Orca stopped: %v", err)
|
|
}
|
|
replacement := NewOrca(logger, handler)
|
|
if err := replacement.Start(context.Background()); err != nil {
|
|
t.Fatalf("Orca name not reacquired: %v", err)
|
|
}
|
|
if err := replacement.Stop(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|