// 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 . package config import ( "os" "path/filepath" "reflect" "strings" "testing" ) func TestVersionOneMigrationPreservesValuesAndOnlyWritesOnSave(t *testing.T) { original := []byte(`version = 1 [clipboard] sync = true sync_display = ":42" number_of_clipboards = 7 export_path = "/tmp/fixture-clipboard" [sound] enabled = false volume = 0.3 [speech] enabled = false rate = 45 module = "fixture" socket = "/tmp/fixture.sock" [keyboard] layout = "laptop" devices = ["/dev/input/event-fixture"] character_echo = false word_echo = true echo_on_capslock = true [reading] automatic = false cursor_echo = false `) path := filepath.Join(t.TempDir(), "settings.toml") if err := os.WriteFile(path, original, 0600); err != nil { t.Fatal(err) } doc, err := Open_document(path, Defaults()) if err != nil { t.Fatal(err) } wanted := Defaults() wanted.Presentation.Sound = Sound{Enabled: false, Volume: 0.3, Driver: "pulse", Theme: "default", Device: "default"} wanted.Presentation.Speech = Speech{Enabled: false, Rate: 45, Volume: 100, Driver: "speech-dispatcher", Module: "fixture"} wanted.Keyboard = Keyboard{MultiPressTimeoutMS: 200, Layout: "laptop", Devices: []string{"/dev/input/event-fixture"}, WordEcho: true, EchoOnCapsLock: true} wanted.Reading = Reading{Emoticons: true, RepeatedSymbolsMin: 4, Spellcheck: true, SpellcheckSound: true, Attributes: Defaults().Reading.Attributes, AttributeFormat: Defaults().Reading.AttributeFormat, Indentation: "none", RaisePitchForCapitals: true, Mode: "cursor", PunctuationProfile: "default", PunctuationLevel: "some"} wanted.Clipboard = Clipboard{Sync: true, SyncDisplay: ":42", NumberOfClipboards: 7, ExportPath: "/tmp/fixture-clipboard"} if !reflect.DeepEqual(doc.Settings, wanted) { t.Fatal(doc.Settings, wanted) } data, err := os.ReadFile(path) if err != nil || string(data) != string(original) { t.Fatal("opening rewrote settings", err) } if err := doc.Save(doc.Settings); err != nil { t.Fatal(err) } data, err = os.ReadFile(path) if err != nil || !strings.Contains(string(data), "version = 3") || strings.Contains(string(data), "\n[sound]") || strings.Contains(string(data), "socket =") { t.Fatal(string(data), err) } got, err := Open_document(path, Defaults()) wantedOrder := []string{"clipboard", "presentation", "presentation.sound", "presentation.speech", "presentation.progress", "presentation.time_and_date", "keyboard", "reading", "internal_ui", "review", "screen", "general", "scripts", "remote", "prompt"} if err != nil || !reflect.DeepEqual(got.Settings, wanted) || !reflect.DeepEqual(got.Order, wantedOrder) { t.Fatal(got, err) } } func TestRejectMixedSchemasAndUnknownDriverEvenWhenSpeechDisabled(t *testing.T) { for _, input := range []string{ "version=1\n[presentation.speech]\nrate=40\n", "version=2\n[speech]\nrate=40\n", "version=2\n[presentation.speech]\ndriver='invented'\nenabled=false\n", "version=1\n[speech]\ndriver='invented'\n", "version=2\n[presentation.speech]\ndrivers=['speech-dispatcher']\n", "version=0\n", "version=4\n", "version=2\n[presentation.sound]\nvolume=nan\n", "version=2\n[presentation.progress]\nenabled=true\n", } { if _, err := Decode([]byte(input)); err == nil { t.Fatal("invalid schema accepted", input) } } } func TestNestedDottedAndInlineSectionsShareOrder(t *testing.T) { for _, input := range []string{ "version=2\n[reading]\nautomatic=false\n[presentation.sound]\nvolume=0.2\n[presentation.speech]\nrate=40\n", "version=2\nreading.automatic=false\npresentation.sound.volume=0.2\npresentation.speech.rate=40\n", "version=2\nreading={automatic=false}\npresentation.sound={volume=0.2}\npresentation.speech={rate=40}\n", "version=2\nreading={automatic=false}\npresentation={sound={volume=0.2}, speech={rate=40}}\n", "version=2\n[reading]\nautomatic=false\n[presentation]\nsound={volume=0.2}\nspeech={rate=40}\n", } { path := filepath.Join(t.TempDir(), "settings.toml") if err := os.WriteFile(path, []byte(input), 0600); err != nil { t.Fatal(err) } doc, err := Open_document(path, Defaults()) if err != nil { t.Fatal(err) } want := []string{"reading", "presentation", "presentation.sound", "presentation.speech"} if !reflect.DeepEqual(doc.Order, want) { t.Fatal(doc.Order) } if err := doc.Save(doc.Settings); err != nil { t.Fatal(err) } saved, err := Open_document(path, Defaults()) if err == nil { saved.Settings.Keyboard.Devices = doc.Settings.Keyboard.Devices } if err != nil || !reflect.DeepEqual(saved.Settings, doc.Settings) { t.Fatal(saved, err) } } } func TestVersionTwoMigrationDiscardsSocketAndCurrentSchemaRejectsIt(t *testing.T) { old := []byte("version=2\n[presentation.speech]\nsocket='/tmp/obsolete.sock'\nrate=45\nmodule='fixture'\n[presentation.sound]\nvolume=0.3\n") settings, err := Decode(old) if err != nil || settings.Version != 3 || settings.Presentation.Speech.Rate != 45 || settings.Presentation.Speech.Module != "fixture" || settings.Presentation.Sound.Volume != 0.3 { t.Fatal(settings, err) } path := filepath.Join(t.TempDir(), "settings.toml") if err := Save(path, settings); err != nil { t.Fatal(err) } data, err := os.ReadFile(path) if err != nil || strings.Contains(string(data), "socket =") { t.Fatal(string(data), err) } if _, err := Decode([]byte("version=3\n[presentation.speech]\nsocket='/tmp/unused.sock'\n")); err == nil { t.Fatal("removed setting accepted in current schema") } }