Files

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(&parameterized); 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)
}
}