Review and strengthen LibreOffice code so far.

This commit is contained in:
Storm Dragon committed 2026-10-15 21:32:44 -04:00
1 parent f885333520
commit 04e6c08fff
8 files changed
+186 -78

No files matched your search

+63 -9
View File
@@ -130,12 +130,16 @@ func TestNativeLibreOfficeCalc(t *testing.T) {
}
}
}
activate := func(window string) {
t.Helper()
if err := exec.CommandContext(ctx, "xdotool", "windowactivate", "--sync", window).Run(); err != nil {
t.Fatal(err)
}
}
// Focus once. Refocusing before every key sequence can hide failures
// to retain or recover Calc's document focus during normal keyboard use.
if err := exec.CommandContext(ctx, "xdotool", "windowfocus", window).Run(); err != nil {
t.Fatal(err)
}
press("Escape", "ctrl+Home", "Right")
activate(window)
press("Escape", "F6", "ctrl+F6", "ctrl+Home", "Right")
waitSpeech("42, B1")
waitFrame("42 B1")
press("Down")
@@ -277,7 +281,9 @@ func TestNativeLibreOfficeCalc(t *testing.T) {
press("ctrl+Page_Up", "ctrl+Home", "Right", "Down")
waitSpeech("y row header, Beta, B2")
// A heading edit is visible on the next cell snapshot, without a cache.
press("Up", "8", "Return")
press("Up")
waitSpeech("Alpha row header, 42, B1")
press("8", "Return")
waitSpeech("y row header, Beta, B2")
waitFrame("y 8 Beta B2")
commands <- input.WhereAmI
@@ -320,10 +326,7 @@ func TestNativeLibreOfficeCalc(t *testing.T) {
if secondWindow == "" {
t.Fatal("second private workbook window missing")
}
// A virtual display without a window manager does not focus new windows.
if err := exec.CommandContext(ctx, "xdotool", "windowfocus", secondWindow).Run(); err != nil {
t.Fatal(err)
}
activate(secondWindow)
press("ctrl+F6", "ctrl+Page_Down", "ctrl+Home", "Down")
waitSpeech("blank, A2")
commands <- input.WhereAmI
@@ -338,4 +341,55 @@ func TestNativeLibreOfficeCalc(t *testing.T) {
waitSpeech("blank, A2")
commands <- input.WhereAmI
waitSpeech("d column header, blank, A2")
// Return to the first workbook, then back again: identical sheet names
// must retain independent choices and fresh cells in both directions.
// Use normal navigation here, without the Ctrl+F6 recovery shortcut.
activate(window)
press("ctrl+Home", "Down")
waitSpeech("blank, A2")
commands <- input.WhereAmI
waitSpeech("Gamma column header, blank, A2")
waitFrame("Gamma A2")
activate(secondWindow)
press("ctrl+Home", "Down")
waitSpeech("blank, A2")
commands <- input.WhereAmI
waitSpeech("d column header, blank, A2")
waitFrame("d A2")
// Dialog activation still resolves its focused control. Cancelling it
// must leave the document navigable with the same header choices.
press("ctrl+h")
dialogWindow := ""
deadline = time.Now().Add(5 * time.Second)
for dialogWindow == "" && time.Now().Before(deadline) {
output, _ := exec.CommandContext(ctx, "xdotool", "search", "--onlyvisible", "--name", "^Find and Replace$").Output()
if candidates := strings.Fields(string(output)); len(candidates) == 1 {
dialogWindow = candidates[0]
} else {
time.Sleep(20 * time.Millisecond)
}
}
if dialogWindow == "" {
t.Fatal("private Find and Replace dialog window missing")
}
activate(dialogWindow)
press("x")
waitSpeech("x")
press("Escape")
deadline = time.Now().Add(5 * time.Second)
for {
output, _ := exec.CommandContext(ctx, "xdotool", "search", "--onlyvisible", "--name", "^Find and Replace$").Output()
if len(strings.Fields(string(output))) == 0 {
break
}
if time.Now().After(deadline) {
t.Fatal("private Find and Replace dialog did not close")
}
time.Sleep(20 * time.Millisecond)
}
activate(secondWindow)
press("ctrl+F6", "ctrl+Home", "Down")
waitSpeech("blank, A2")
commands <- input.WhereAmI
waitSpeech("d column header, blank, A2")
}
+20 -10
View File
@@ -174,13 +174,19 @@ func TestRunReachesSoundStopAfterEarlierServiceStopError(t *testing.T) {
}
sounds := &recordingSound{started: make(chan struct{})}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
completed := make(chan error, 1)
// Sound starts first; cancellation there races later service startup.
// The startup callback runs only once the complete runtime is ready.
runtimeStarted := make(chan struct{})
go func() {
completed <- Run(ctx, Options{
Logger: slog.New(slog.DiscardHandler),
Settings: &settings,
newSound: func() soundService { return sounds },
Logger: slog.New(slog.DiscardHandler),
Settings: &settings,
OpenPreferences: true,
onPreferences: func() { close(runtimeStarted) },
newSound: func() soundService { return sounds },
newCommands: func() commandListener {
return silentCommands{}
},
@@ -190,9 +196,9 @@ func TestRunReachesSoundStopAfterEarlierServiceStopError(t *testing.T) {
}()
select {
case <-sounds.started:
case <-runtimeStarted:
case <-time.After(time.Second):
t.Fatal("sound service did not start")
t.Fatal("runtime did not finish startup")
}
cancel()
select {
@@ -217,13 +223,17 @@ func TestRunReportsSoundStopError(t *testing.T) {
soundError := errors.New("stop cue failed")
sounds := &recordingSound{started: make(chan struct{}), stopError: soundError}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
completed := make(chan error, 1)
runtimeStarted := make(chan struct{})
go func() {
completed <- Run(ctx, Options{
Logger: slog.New(slog.DiscardHandler),
Settings: &settings,
newSound: func() soundService { return sounds },
Logger: slog.New(slog.DiscardHandler),
Settings: &settings,
OpenPreferences: true,
onPreferences: func() { close(runtimeStarted) },
newSound: func() soundService { return sounds },
newCommands: func() commandListener {
return silentCommands{}
},
@@ -235,9 +245,9 @@ func TestRunReportsSoundStopError(t *testing.T) {
}()
select {
case <-sounds.started:
case <-runtimeStarted:
case <-time.After(time.Second):
t.Fatal("sound service did not start")
t.Fatal("runtime did not finish startup")
}
cancel()
select {
+8 -1
View File
@@ -184,7 +184,11 @@ func (adapter *Adapter) recoverForeground() {
continue
}
if state.Epoch == adapter.Foreground().Epoch && adapter.matchesForeground(window, nativeObjectID(window)) {
target := C.cthulhu_focused_descendant(window)
target := adapter.libreOfficeFrameTarget(window)
officeFrame := target != nil
if target == nil {
target = C.cthulhu_focused_descendant(window)
}
if target == nil {
target = (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(window)))
}
@@ -202,6 +206,9 @@ func (adapter *Adapter) recoverForeground() {
target = child
}
snapshot := snapshotFromNativeWithCaret(target, true, false, false, adapter.captureSound.Load(), adapter.captureTextAttributes.Load())
if officeFrame {
snapshot.ApplicationProcess = adapter.applicationProcess(target)
}
event := NewEvent(WindowActivatedEvent, snapshot)
event.Window = nativeObjectID(window)
name := C.atspi_accessible_get_name(window, nil)
+20 -4
View File
@@ -11,15 +11,19 @@ import "C"
import "unsafe"
// libreOfficeFocusTarget resolves a Calc editor's direct text children and
// preserves the sheet source for selection events. F2 focuses its temporary
// panel; the paragraph can appear later, on typing.
// libreOfficeFocusTarget keeps Office frame activation separate from document
// focus, resolves a Calc editor's direct text children, and preserves the sheet
// source for selection events. F2 focuses its temporary panel; the paragraph
// can appear later, on typing.
// The callback owns and releases the returned reference. No sheet is walked.
func (adapter *Adapter) libreOfficeFocusTarget(event *C.AtspiEvent, eventType string) *C.AtspiAccessible {
if eventType != FocusEvent && eventType != FocusedStateEvent && eventType != SelectionChangedEvent ||
if eventType != FocusEvent && eventType != FocusedStateEvent && eventType != SelectionChangedEvent && eventType != WindowActivatedEvent ||
!IsLibreOfficeProcess(adapter.applicationProcess(event.source)) {
return nil
}
if eventType == WindowActivatedEvent {
return adapter.libreOfficeFrameTarget(event.source)
}
role := C.atspi_accessible_get_role(event.source, nil)
if eventType == SelectionChangedEvent {
// Keep the sheet as the selection source. Its first selected cell
@@ -79,6 +83,18 @@ func (adapter *Adapter) libreOfficeFocusTarget(event *C.AtspiEvent, eventType st
return nil
}
// Both real activation events and X11 foreground recovery must avoid searching
// Calc's previous view while it rebuilds controls on workbook return. Own the
// frame and let the following cell/editor events establish document focus.
// Dialogs retain ordinary focused-control resolution. The caller owns the ref.
func (adapter *Adapter) libreOfficeFrameTarget(source *C.AtspiAccessible) *C.AtspiAccessible {
if !IsLibreOfficeProcess(adapter.applicationProcess(source)) ||
C.atspi_accessible_get_role(source, nil) != C.ATSPI_ROLE_FRAME {
return nil
}
return (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(source)))
}
func captureLibreOfficeContext(source *C.AtspiAccessible, snapshot *Snapshot) {
if !IsLibreOfficeProcess(snapshot.ApplicationProcess) || snapshot.Defunct {
return
+3 -1
View File
@@ -11,6 +11,7 @@ import (
"strconv"
"strings"
"sync"
"syscall"
"testing"
"time"
@@ -540,13 +541,14 @@ func TestDisableKillsGroupDescendantsAndJoinsPipes(t *testing.T) {
wait_signal(t, session.Done())
// SIGKILL delivery/reaping is scheduled by the kernel; wait for /proc to
// report disappearance or zombie state, not an arbitrary sleep duration.
// A task that exits during ReadFile can return ESRCH rather than ENOENT.
deadline := time.NewTimer(2 * time.Second)
defer deadline.Stop()
ticker := time.NewTicker(time.Millisecond)
defer ticker.Stop()
for {
data, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/stat")
if os.IsNotExist(err) {
if os.IsNotExist(err) || errors.Is(err, syscall.ESRCH) {
break
}
if err != nil {