After commit c018f5fd29 Fenrir is being switched over to go. If you do not want to be part of the testing process, please switch to master.
This commit is contained in:
1 parent
c018f5fd29
commit
0e50de4c76
1047 files changed
+58364
-63747
No files matched your search
@@ -0,0 +1,220 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/fenrir/internal/command"
|
||||
"git.stormux.org/storm/fenrir/internal/config"
|
||||
"git.stormux.org/storm/fenrir/internal/control"
|
||||
"git.stormux.org/storm/fenrir/internal/input"
|
||||
"git.stormux.org/storm/fenrir/internal/screen"
|
||||
)
|
||||
|
||||
func persistence_application(t *testing.T, terminalMode bool) (*application, *fakeVoice) {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
base := config.Defaults()
|
||||
base.Presentation.Speech.Rate = 24
|
||||
base.Screen.IgnoredVTs = []int{2}
|
||||
path := filepath.Join(t.TempDir(), "settings.toml")
|
||||
if err := config.Save(path, base); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
voice := &fakeVoice{}
|
||||
source := &fakeSource{snapshot: screen.Snapshot{Terminal: 1, Active: true, Rows: 1, Cols: 5, Cells: []rune("alpha")}}
|
||||
keys := &fakeKeys{poll: func(bool, func(input.Decision)) error { return nil }}
|
||||
a, err := new_application(ctx, source, keys, voice, RunOptions{Settings: base, ConfigPath: path, TerminalMode: terminalMode, Sounds: &fakeSounds{}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(a.ui.close)
|
||||
if err := a.read_screen(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return a, voice
|
||||
}
|
||||
|
||||
func persistence_request(a *application, text string) chan string {
|
||||
reply := make(chan string, 1)
|
||||
a.remote_request(control.Request{UID: os.Geteuid(), Text: "SETTING " + text, Reply: reply})
|
||||
return reply
|
||||
}
|
||||
|
||||
func persistence_reply(t *testing.T, reply chan string, wantError bool) {
|
||||
t.Helper()
|
||||
select {
|
||||
case text := <-reply:
|
||||
if (text != "") != wantError {
|
||||
t.Fatal("unexpected persistence response", text, wantError)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("persistence reply missing")
|
||||
}
|
||||
}
|
||||
|
||||
func finish_persistence(t *testing.T, a *application, wantError bool) {
|
||||
t.Helper()
|
||||
select {
|
||||
case err := <-a.saver.results:
|
||||
if (err != nil) != wantError {
|
||||
t.Fatal("unexpected save result", err)
|
||||
}
|
||||
a.settings_save_result(err)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("save worker stalled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteResetRestoresLoadedValuesAndPreservesSessionState(t *testing.T) {
|
||||
for _, terminalMode := range []bool{false, true} {
|
||||
a, _ := persistence_application(t, terminalMode)
|
||||
base := a.settings.resetBase
|
||||
windowReply := make(chan string, 1)
|
||||
a.remote_request(control.Request{UID: os.Geteuid(), Text: "COMMAND WINDOW 0 0 4 0", Reply: windowReply})
|
||||
persistence_reply(t, windowReply, false)
|
||||
regionRevision := a.engine.Region_revision()
|
||||
a.settings.saveDraft.Keyboard.Layout = "laptop"
|
||||
for _, id := range []command.ID{command.Mute, command.AutoRead, command.ToggleAttributes} {
|
||||
a.handle_input(input.Decision{Command: id})
|
||||
}
|
||||
a.handle_input(input.Decision{Command: command.NextQuickValue})
|
||||
persistence_reply(t, persistence_request(a, "SET speech#rate=0.2;sound#volume=0.3;screen#ignore_screen=7,8;general#punctuation_level=all;keyboard#word_echo=True"), false)
|
||||
persistence_reply(t, persistence_request(a, "RESET"), false)
|
||||
if !a.muted || a.engine.Automatic_reading() || a.settings.live.Presentation.Speech.Rate != base.Presentation.Speech.Rate || !reflect.DeepEqual(a.settings.live.Screen, base.Screen) || a.settings.live.Keyboard.WordEcho || a.settings.live.Presentation.Sound != base.Presentation.Sound || a.settings.live.Reading.PunctuationLevel != base.Reading.PunctuationLevel || a.settings.saveDraft.Keyboard.Layout != "laptop" {
|
||||
t.Fatal("reset lost session overrides, loaded values or next-launch choices", a.settings.live, a.settings.saveDraft)
|
||||
}
|
||||
if got := a.engine.Command(command.ToggleAttributes).Text; got != "announcement of attributes disabled" {
|
||||
t.Fatal("unrelated remote update/reset lost the session attribute toggle", got)
|
||||
}
|
||||
if !a.settings.saveDraft.Presentation.Speech.Enabled || !a.settings.saveDraft.Reading.Automatic || a.quick.changed || len(a.settings.remoteFields) != 0 || a.engine.Region_revision() != regionRevision {
|
||||
t.Fatal("reset persisted unrelated session state or retained overrides")
|
||||
}
|
||||
// A successful menu save becomes the next reset baseline too.
|
||||
menuSaved := a.settings.saveDraft
|
||||
menuSaved.Presentation.Speech.Rate = 44
|
||||
a.apply_saved_settings(menuSaved, false)
|
||||
persistence_reply(t, persistence_request(a, "SET speech#rate=0.1;speech#enabled=False;reading#automatic=False;sound#enabled=False;sound#progress_monitoring=False;reading#cursor_echo=False;keyboard#char_echo_mode=2;keyboard#echo_on_capslock=True"), false)
|
||||
persistence_reply(t, persistence_request(a, "RESET"), false)
|
||||
if a.muted || !a.engine.Automatic_reading() || a.settings.live.Presentation != menuSaved.Presentation || a.settings.live.Keyboard.CharacterEcho != menuSaved.Keyboard.CharacterEcho || a.settings.live.Keyboard.EchoOnCapsLock != menuSaved.Keyboard.EchoOnCapsLock || a.settings.live.Reading.CursorEcho != menuSaved.Reading.CursorEcho {
|
||||
t.Fatal("reset did not restore explicitly overridden booleans or menu baseline", a.settings.live)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteSaveAsRepliesAfterWriteAndRetainsNewerOverrides(t *testing.T) {
|
||||
for _, terminalMode := range []bool{false, true} {
|
||||
a, _ := persistence_application(t, terminalMode)
|
||||
defaultPath := a.options.ConfigPath
|
||||
exportPath := filepath.Join(t.TempDir(), "export with spaces.toml")
|
||||
for _, id := range []command.ID{command.Mute, command.AutoRead} {
|
||||
a.handle_input(input.Decision{Command: id})
|
||||
}
|
||||
persistence_reply(t, persistence_request(a, "SET speech#rate=0.2;sound#volume=0.3"), false)
|
||||
reply := persistence_request(a, "SAVEAS "+exportPath)
|
||||
select {
|
||||
case response := <-reply:
|
||||
t.Fatal("save acknowledged before result processing", response)
|
||||
default:
|
||||
}
|
||||
persistence_reply(t, persistence_request(a, "RESET"), true)
|
||||
persistence_reply(t, persistence_request(a, "SAVE"), true)
|
||||
// Keep input and later SET requests responsive while disk work runs.
|
||||
a.handle_input(input.Decision{Command: command.CurrentLine})
|
||||
persistence_reply(t, persistence_request(a, "SET speech#rate=0.8;keyboard#word_echo=True"), false)
|
||||
finish_persistence(t, a, false)
|
||||
persistence_reply(t, reply, false)
|
||||
saved, _, err := config.Load(exportPath, nil)
|
||||
if err != nil || saved.Presentation.Speech.Rate != -60 || saved.Presentation.Sound.Volume != 0.3 || !saved.Presentation.Speech.Enabled || !saved.Reading.Automatic || saved.Keyboard.WordEcho {
|
||||
t.Fatal("save did not retain its admitted snapshot", saved, err)
|
||||
}
|
||||
info, err := os.Stat(exportPath)
|
||||
if err != nil || info.Mode().Perm() != 0600 {
|
||||
t.Fatal("export permissions", info, err)
|
||||
}
|
||||
original, _, err := config.Load(defaultPath, nil)
|
||||
if err != nil || original.Presentation.Speech.Rate != 24 || a.options.ConfigPath != defaultPath {
|
||||
t.Fatal("SAVEAS changed the default destination", original, err)
|
||||
}
|
||||
persistence_reply(t, persistence_request(a, "RESET"), false)
|
||||
if a.settings.live.Presentation.Speech.Rate != -60 || a.settings.live.Keyboard.WordEcho || !a.muted || a.engine.Automatic_reading() {
|
||||
t.Fatal("save completion lost newer changes or reset session state", a.settings.live)
|
||||
}
|
||||
persistence_reply(t, persistence_request(a, "SET speech#rate=0.9"), false)
|
||||
reply = persistence_request(a, "SAVE")
|
||||
// The existing normal quit waits for the admitted write and reply.
|
||||
a.handle_input(input.Decision{Command: command.Quit})
|
||||
if err := a.loop(nil); err != nil || a.ctx.Err() != nil {
|
||||
t.Fatal("save/quit failed", err, a.ctx.Err())
|
||||
}
|
||||
persistence_reply(t, reply, false)
|
||||
current, _, err := config.Load(defaultPath, nil)
|
||||
if err != nil || current.Presentation.Speech.Rate != 80 {
|
||||
t.Fatal("SAVE did not use original configured path", current, err)
|
||||
}
|
||||
unchanged, _, err := config.Load(exportPath, nil)
|
||||
if err != nil || !reflect.DeepEqual(unchanged, saved) {
|
||||
t.Fatal("later SAVE overwrote export", unchanged, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemotePersistenceFailureAndAdmissionPreserveState(t *testing.T) {
|
||||
a, _ := persistence_application(t, true)
|
||||
base := a.settings.resetBase
|
||||
persistence_reply(t, persistence_request(a, "SET speech#pitch=0.2;speech#volume=0.6;speech#auto_read_incoming=False"), false)
|
||||
for _, text := range []string{"SAVEAS", "SAVE unexpected", "RESET unexpected", "SAVEAS bad\x00path", "SAVEAS bad\npath"} {
|
||||
persistence_reply(t, persistence_request(a, text), true)
|
||||
if a.saver.busy {
|
||||
t.Fatal("malformed operation started a worker", text)
|
||||
}
|
||||
}
|
||||
a.ui.active = true
|
||||
persistence_reply(t, persistence_request(a, "RESET"), true)
|
||||
persistence_reply(t, persistence_request(a, "SAVE"), true)
|
||||
a.ui.active = false
|
||||
a.settings.live.Remote.EnableSettingsRemote = false
|
||||
persistence_reply(t, persistence_request(a, "RESET"), true)
|
||||
persistence_reply(t, persistence_request(a, "SAVEAS "+filepath.Join(t.TempDir(), "denied.toml")), true)
|
||||
a.settings.live.Remote.EnableSettingsRemote = true
|
||||
denied := make(chan string, 1)
|
||||
a.remote_request(control.Request{UID: os.Geteuid() + 1, Text: "SETTING RESET", Reply: denied})
|
||||
persistence_reply(t, denied, true)
|
||||
if response, deferred := a.remote_settings_request("SAVEAS ignored.toml", control.Request{UID: os.Geteuid() + 1}); response != "SAVEAS requires the reader owner or root" || deferred || a.saver.busy {
|
||||
t.Fatal("SAVEAS granted arbitrary privileged writes", response, deferred)
|
||||
}
|
||||
reply := persistence_request(a, "SAVEAS "+t.TempDir()) // A directory cannot be a TOML file.
|
||||
finish_persistence(t, a, true)
|
||||
select {
|
||||
case response := <-reply:
|
||||
if !strings.HasPrefix(response, "Could not save settings:") {
|
||||
t.Fatal(response)
|
||||
}
|
||||
default:
|
||||
t.Fatal("failed write did not return an error")
|
||||
}
|
||||
if !reflect.DeepEqual(a.settings.resetBase, base) || len(a.settings.remoteFields) == 0 {
|
||||
t.Fatal("failed save advanced the reset baseline")
|
||||
}
|
||||
pipePath := filepath.Join(t.TempDir(), "blocked-settings")
|
||||
if err := syscall.Mkfifo(pipePath, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reply = persistence_request(a, "SAVEAS "+pipePath)
|
||||
finish_persistence(t, a, true)
|
||||
persistence_reply(t, reply, true)
|
||||
persistence_reply(t, persistence_request(a, "RESET"), false)
|
||||
if a.settings.live.Presentation.Speech != base.Presentation.Speech || !a.engine.Automatic_reading() {
|
||||
t.Fatal("failed-save overrides were not reset", a.settings.live)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user