259 lines
9.8 KiB
Go
259 lines
9.8 KiB
Go
// Fenrir console screen reader.
|
|
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify it
|
|
// under the terms of the GNU General Public License as published by the Free
|
|
// Software Foundation, either version 3 of the License, or (at your option)
|
|
// any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful, but WITHOUT
|
|
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
// more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License along with
|
|
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
|
|
|
|
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestDefaultsStrictValuesAndUnknownSettings(t *testing.T) {
|
|
settings, err := Decode([]byte("[sound]\nenabled = false\n[speech]\nrate = 55\n"))
|
|
if err != nil || settings.Presentation.Sound.Enabled || settings.Presentation.Speech.Rate != 55 || !settings.Reading.CursorEcho {
|
|
t.Fatalf("partial settings: %+v, %v", settings, err)
|
|
}
|
|
for _, data := range []string{"version = 4", "[sound]\ndriver = 'external'", "[sound]\nvolume = -0.1", "[sound]\nvolume = 1.1", "[sound]\nvolume = nan", "[sound]\nvolume = inf", "[speech]\nrate = 101", "[speech]\nmodule = 'bad\\nname'", "[keyboard]\ndevices = ['']", "[sound]\nenabled = 'false'", "[reading]\ncursorEcho = false", "[sound"} {
|
|
if _, err := Decode([]byte(data)); err == nil {
|
|
t.Errorf("accepted invalid settings: %q", data)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTrackingModeDefaultsValidationAndSave(t *testing.T) {
|
|
for _, version := range []string{"1", "2", "3"} {
|
|
settings, err := Decode([]byte("version=" + version + "\n[reading]\ncursor_echo=false\n"))
|
|
if err != nil || settings.Reading.Mode != "cursor" || settings.Reading.CursorEcho {
|
|
t.Fatal("older settings lost cursor behavior", version, settings, err)
|
|
}
|
|
}
|
|
for _, mode := range []string{"cursor", "table", "highlight"} {
|
|
settings, err := Decode([]byte("version=3\n[reading]\nmode='" + mode + "'\n"))
|
|
if err != nil || settings.Reading.Mode != mode {
|
|
t.Fatal(settings, err)
|
|
}
|
|
path := filepath.Join(t.TempDir(), "settings.toml")
|
|
if err := Save(path, settings); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loaded, _, err := Load(path, nil)
|
|
if err != nil || loaded.Reading.Mode != mode {
|
|
t.Fatal(loaded, err)
|
|
}
|
|
}
|
|
for _, mode := range []string{"", "invalid", "Cursor"} {
|
|
if _, err := Decode([]byte("version=3\n[reading]\nmode='" + mode + "'\n")); err == nil {
|
|
t.Fatal("unsupported tracking mode accepted", mode)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSettingsSelectionDoesNotHideInvalidOrExplicitMissingFiles(t *testing.T) {
|
|
root := t.TempDir()
|
|
userPath, systemPath := filepath.Join(root, "user.toml"), filepath.Join(root, "system.toml")
|
|
if err := os.WriteFile(systemPath, []byte("[speech]\nrate = 30\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, selected, err := Load("", []string{userPath, systemPath})
|
|
if err != nil || selected != systemPath || settings.Presentation.Speech.Rate != 30 {
|
|
t.Fatalf("system selection: %+v %s %v", settings, selected, err)
|
|
}
|
|
if err := os.WriteFile(userPath, []byte("[sound]\nenabled = false\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, selected, err = Load("", []string{userPath, systemPath})
|
|
if err != nil || selected != userPath || settings.Presentation.Sound.Enabled || settings.Presentation.Speech.Rate != 0 {
|
|
t.Fatalf("user selection merged system values: %+v %s %v", settings, selected, err)
|
|
}
|
|
if err := os.WriteFile(userPath, []byte("invalid = true"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, selected, err := Load("", []string{userPath, systemPath}); err == nil || selected != userPath {
|
|
t.Fatal("hid invalid selected settings", selected, err)
|
|
}
|
|
if _, _, err := Load(filepath.Join(root, "missing.toml"), []string{systemPath}); err == nil {
|
|
t.Fatal("ignored explicit missing file")
|
|
}
|
|
if got := Paths(root, 0); !reflect.DeepEqual(got, []string{SystemPath}) {
|
|
t.Fatal(got)
|
|
}
|
|
if got := Paths(root, 1000); len(got) != 2 || !strings.HasPrefix(got[0], root) || got[1] != SystemPath {
|
|
t.Fatal(got)
|
|
}
|
|
}
|
|
|
|
func TestAtomicSaveRoundTripAndRejectedSavePreservesOriginal(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "settings", "settings.toml")
|
|
settings := Defaults()
|
|
settings.Presentation.Sound.Enabled, settings.Presentation.Speech.Enabled, settings.Reading.CursorEcho = false, false, false
|
|
settings.Presentation.Speech.Rate, settings.Presentation.Speech.Module = 45, "example"
|
|
settings.Presentation.Speech.Pitch, settings.Presentation.Speech.Volume = -30, 75
|
|
settings.Presentation.Sound.Volume = 0.25
|
|
settings.Keyboard.Devices = []string{"/dev/input/event1"}
|
|
settings.Keyboard.CharacterEcho, settings.Keyboard.WordEcho, settings.Keyboard.EchoOnCapsLock = false, true, true
|
|
if err := Save(path, settings); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, _, err := Load(path, nil)
|
|
if err != nil || !reflect.DeepEqual(got, settings) {
|
|
t.Fatalf("round trip: %+v, %v", got, err)
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil || info.Mode().Perm() != 0600 {
|
|
t.Fatal("settings are not private", info, err)
|
|
}
|
|
original, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings.Presentation.Speech.Rate = 101
|
|
if err := Save(path, settings); err == nil {
|
|
t.Fatal("saved invalid settings")
|
|
}
|
|
current, err := os.ReadFile(path)
|
|
if err != nil || string(current) != string(original) {
|
|
t.Fatal("rejected save changed original", err)
|
|
}
|
|
entries, err := os.ReadDir(filepath.Dir(path))
|
|
if err != nil || len(entries) != 1 {
|
|
t.Fatal("temporary save leaked", entries, err)
|
|
}
|
|
}
|
|
|
|
func TestShippedExampleMatchesDefaults(t *testing.T) {
|
|
settings, _, err := Load("../../config/settings/settings.toml", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings.Keyboard.Devices = nil // TOML's explicit empty array is equivalent.
|
|
if !reflect.DeepEqual(settings, Defaults()) {
|
|
t.Fatalf("example differs: %+v", settings)
|
|
}
|
|
}
|
|
|
|
func TestSpeechParameterDefaultsAndBounds(t *testing.T) {
|
|
for _, data := range []string{"version=1\n", "version=2\n", "version=3\n"} {
|
|
settings, err := Decode([]byte(data))
|
|
if err != nil || settings.Presentation.Speech.Rate != 0 || settings.Presentation.Speech.Pitch != 0 || settings.Presentation.Speech.Volume != 100 {
|
|
t.Fatal(settings, err)
|
|
}
|
|
}
|
|
for _, setting := range []string{"pitch", "volume"} {
|
|
for _, invalid := range []string{"-101", "101"} {
|
|
if _, err := Decode([]byte("version=3\n[presentation.speech]\n" + setting + "=" + invalid)); err == nil {
|
|
t.Fatal(setting, invalid)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClipboardPersistenceDefaultsAndSavedHistoryLimit(t *testing.T) {
|
|
settings := Defaults()
|
|
if settings.Clipboard.Persistent || settings.Clipboard.NumberOfClipboards != 200 {
|
|
t.Fatal(settings.Clipboard)
|
|
}
|
|
for _, version := range []string{"1", "2", "3"} {
|
|
loaded, err := Decode([]byte("version=" + version + "\n[clipboard]\nnumber_of_clipboards=50\n"))
|
|
if err != nil || loaded.Clipboard.Persistent || loaded.Clipboard.NumberOfClipboards != 50 {
|
|
t.Fatal(loaded.Clipboard, err)
|
|
}
|
|
}
|
|
for _, limit := range []string{"0", "10001"} {
|
|
if _, err := Decode([]byte("version=3\n[clipboard]\nnumber_of_clipboards=" + limit)); err == nil {
|
|
t.Fatal("invalid history size accepted", limit)
|
|
}
|
|
}
|
|
settings.Clipboard.Persistent = true
|
|
path := filepath.Join(t.TempDir(), "settings.toml")
|
|
if err := Save(path, settings); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loaded, _, err := Load(path, nil)
|
|
if err != nil || loaded.Clipboard != settings.Clipboard {
|
|
t.Fatal(loaded.Clipboard, err)
|
|
}
|
|
}
|
|
|
|
func TestInternalUIDefaultsAndPersistence(t *testing.T) {
|
|
for _, data := range []string{"", "version=3\n[internal_ui]\nannounce_grid_coordinates=false\n"} {
|
|
settings, err := Decode([]byte(data))
|
|
if err != nil || settings.InternalUI.AnnounceGridCoordinates != (data == "") || !settings.InternalUI.AnnounceItemPosition {
|
|
t.Fatal("omitted options lost defaults", settings.InternalUI, err)
|
|
}
|
|
}
|
|
for _, coordinates := range []bool{false, true} {
|
|
for _, position := range []bool{false, true} {
|
|
settings := Defaults()
|
|
settings.InternalUI = InternalUI{AnnounceGridCoordinates: coordinates, AnnounceItemPosition: position}
|
|
path := filepath.Join(t.TempDir(), "settings.toml")
|
|
if err := Save(path, settings); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loaded, _, err := Load(path, nil)
|
|
if err != nil || loaded.InternalUI != settings.InternalUI {
|
|
t.Fatal(loaded.InternalUI, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAtomicSavePermissionsForSystemAndPersonalSettings(t *testing.T) {
|
|
for _, system := range []bool{false, true} {
|
|
name := "personal"
|
|
fileMode, dirMode := os.FileMode(0600), os.FileMode(0700)
|
|
if system {
|
|
name, fileMode, dirMode = "system", 0644, 0755
|
|
}
|
|
t.Run(name, func(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "settings", "settings.toml")
|
|
save := func(data []byte) error {
|
|
if system {
|
|
// Exercise the system writer in a private directory, without
|
|
// writing to the installed configuration or requiring root.
|
|
return save_data_with_permissions(path, data, fileMode, dirMode)
|
|
}
|
|
return save_data(path, data)
|
|
}
|
|
for _, data := range []string{"initial", "replacement"} {
|
|
if err := save([]byte(data)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil || info.Mode().Perm() != fileMode {
|
|
t.Fatal("file permissions", info, err)
|
|
}
|
|
info, err = os.Stat(filepath.Dir(path))
|
|
if err != nil || info.Mode().Perm() != dirMode {
|
|
t.Fatal("directory permissions", info, err)
|
|
}
|
|
content, err := os.ReadFile(path)
|
|
if err != nil || string(content) != data {
|
|
t.Fatal("contents", string(content), err)
|
|
}
|
|
// Simulate the permissions left by an older save (or an
|
|
// overly permissive personal file) before replacing it again.
|
|
if err := os.Chmod(path, fileMode^0044); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|