232 lines
7.8 KiB
Go
232 lines
7.8 KiB
Go
// Fenrir console screen reader.
|
|
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package punctuation
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestShippedProfilesAndLevels(t *testing.T) {
|
|
paths, err := filepath.Glob("../../config/punctuation/*.toml")
|
|
if err != nil || len(paths) != 7 {
|
|
t.Fatal(paths, err)
|
|
}
|
|
for _, path := range paths {
|
|
processor, _, err := Load([]string{path})
|
|
if err != nil {
|
|
t.Fatal(path, err)
|
|
}
|
|
for _, level := range Levels() {
|
|
if _, exists := processor.levels[level]; !exists {
|
|
t.Fatal(path, level)
|
|
}
|
|
}
|
|
if processor.Character('.') == "." {
|
|
t.Fatal("missing character name", path)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTextLevelsPreservePausesAndDoNotReprocessNames(t *testing.T) {
|
|
processor, _, err := Load([]string{"../../config/punctuation/default.toml"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, testCase := range []struct{ level, input, want string }{
|
|
{"none", "don't 3.14 [X]mute a_b", "don't 3.14 X mute a b"},
|
|
{"some", "a/b#c!", "a slash b hash c!"},
|
|
{"most", "a,b!", "a comma b bang! "},
|
|
{"all", "?", " question?? "},
|
|
{"all", "a b", "a space b"},
|
|
{"some", "é→Ω", "é→Ω"},
|
|
} {
|
|
if got := processor.Text(testCase.input, testCase.level); got != testCase.want {
|
|
t.Errorf("%s %q: %q, want %q", testCase.level, testCase.input, got, testCase.want)
|
|
}
|
|
}
|
|
if got := processor.Character('?'); got != "question?" {
|
|
t.Fatal(got)
|
|
}
|
|
if got := processor.Character('E'); got != "E" {
|
|
t.Fatal(got)
|
|
}
|
|
}
|
|
|
|
func TestEmbeddedPunctuation(t *testing.T) {
|
|
processor, _, err := Load([]string{"../../config/punctuation/default.toml"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, trial := range []struct{ input, want string }{
|
|
{"https://stormux.org", "https colon slash slash stormux dot org"},
|
|
{"Visit https://example.invalid/path?q=a&n=2.", "Visit https colon slash slash example dot invalid slash path question? q equals a and n equals 2."},
|
|
{"user@example.invalid", "user at example dot invalid"},
|
|
{"file_name.txt", "file line name dot txt"},
|
|
{"你好.世界 café:menu", "你好 dot 世界 café colon menu"},
|
|
{"cafe\u0301.txt", "cafe\u0301 dot txt"},
|
|
{"don't can't", "don't can't"},
|
|
{"3.14 1,000 12:30", "3.14 1,000 12:30"},
|
|
{"Hello, world! End. Next: word?", "Hello, world! End. Next: word?"},
|
|
{"(word) [word]", "word word"},
|
|
{"a...b", "a dot dot dot b"},
|
|
} {
|
|
if got := strings.Join(strings.Fields(processor.Text(trial.input, "some")), " "); got != trial.want {
|
|
t.Errorf("%q: %q, want %q", trial.input, got, trial.want)
|
|
}
|
|
}
|
|
for _, trial := range []struct{ level, want string }{
|
|
{"none", "https: stormux.org"},
|
|
{"some", "https colon slash slash stormux dot org"},
|
|
{"most", "https colon slash slash stormux dot org"},
|
|
{"all", "https colon slash slash stormux dot org"},
|
|
} {
|
|
if got := processor.Text("https://stormux.org", trial.level); got != trial.want {
|
|
t.Errorf("%s: %q, want %q", trial.level, got, trial.want)
|
|
}
|
|
}
|
|
french, _, err := Load([]string{"../../config/punctuation/fr.toml"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := french.Text("example.invalid", "some"); got != "example point invalid" {
|
|
t.Fatal("embedded punctuation lost profile name", got)
|
|
}
|
|
}
|
|
|
|
func TestDottedNumericSequences(t *testing.T) {
|
|
processor, _, err := Load([]string{"../../config/punctuation/default.toml"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, trial := range []struct{ input, want string }{
|
|
{"2026.10.31", "2026 dot 10 dot 31"},
|
|
{"(2026.10.31).", "2026 dot 10 dot 31 ."},
|
|
{"v1.2.3 127.0.0.1", "v1 dot 2 dot 3 127 dot 0 dot 0 dot 1"},
|
|
{"2026.10.31", "2026 dot 10 dot 31"},
|
|
{"3.14 2026.10.31 2.5", "3.14 2026 dot 10 dot 31 2.5"},
|
|
{"3.14. 2.5", "3.14. 2.5"},
|
|
{"1,234.56 1.234,56 1,000,000 12:30:45", "1,234.56 1.234,56 1,000,000 12:30:45"},
|
|
} {
|
|
got := strings.Join(strings.Fields(processor.Text(trial.input, "some")), " ")
|
|
if got != trial.want {
|
|
t.Errorf("%q: got %q, want %q", trial.input, got, trial.want)
|
|
}
|
|
}
|
|
for _, trial := range []struct{ level, want string }{
|
|
{"some", "2026 dot 10 dot 31."},
|
|
{"most", "2026 dot 10 dot 31 dot. "},
|
|
{"all", "2026 dot 10 dot 31 dot. "},
|
|
} {
|
|
if got := processor.Text("2026.10.31.", trial.level); got != trial.want {
|
|
t.Errorf("%s: incorrect embedded or sentence pause: %q", trial.level, got)
|
|
}
|
|
}
|
|
if got := processor.Text("2026.10.31", "none"); got != "2026.10.31" {
|
|
t.Fatal("none level changed", got)
|
|
}
|
|
}
|
|
|
|
func TestNumericPercentSpeech(t *testing.T) {
|
|
processor, _, err := Load([]string{"../../config/punctuation/default.toml"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, trial := range []struct{ text, want string }{
|
|
{"100%", "100 percent"},
|
|
{"0%", "0 percent"},
|
|
{"12.5%", "12.5 percent"},
|
|
{"-5%", "dash 5 percent"},
|
|
{"(100%)", "100 percent"},
|
|
{"100%, done", "100 percent , done"},
|
|
{"100%", "100 percent"},
|
|
{"%", ""},
|
|
{"text%", "text"},
|
|
{"100 %", "100"},
|
|
{"don't 3.14 1,000 12:30", "don't 3.14 1,000 12:30"},
|
|
} {
|
|
for _, options := range []TextOptions{{}, {Emoticons: true, RepeatedSymbols: true, RepeatedSymbolsMin: 4}} {
|
|
if got := strings.Join(strings.Fields(processor.Process_text(trial.text, "some", options)), " "); got != trial.want {
|
|
t.Errorf("%q, options %+v: got %q, want %q", trial.text, options, got, trial.want)
|
|
}
|
|
}
|
|
}
|
|
for _, level := range Levels() {
|
|
want := "100 percent"
|
|
if level == "none" {
|
|
want = "100"
|
|
}
|
|
if got := strings.Join(strings.Fields(processor.Text("100%", level)), " "); got != want {
|
|
t.Errorf("level %s: got %q, want %q", level, got, want)
|
|
}
|
|
}
|
|
data, err := os.ReadFile("../../config/punctuation/default.toml")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
custom, err := Decode([]byte(strings.Replace(string(data), `"%" = "percent"`, `"%" = "per/cent"`, 1)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := custom.Text("100%", "some"); got != "100 per/cent " {
|
|
t.Fatal("percent suffix lost or reprocessed profile name", got)
|
|
}
|
|
}
|
|
|
|
func TestCustomNamesAndInvalidProfiles(t *testing.T) {
|
|
data, err := os.ReadFile("../../config/punctuation/default.toml")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
custom := strings.Replace(string(data), `"/" = "slash"`, `"/" = "path/separator"`, 1)
|
|
processor, err := Decode([]byte(custom))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := processor.Text("a/b", "some"); got != "a path/separator b" {
|
|
t.Fatal("recursive replacement", got)
|
|
}
|
|
for _, invalid := range []string{
|
|
strings.Replace(string(data), "version = 1", "version = 99", 1),
|
|
strings.Replace(string(data), `"none" = ""`, `"none" = "/"`, 1),
|
|
strings.Replace(string(data), `"/" = "slash"`, `"//" = "slash"`, 1),
|
|
strings.Replace(string(data), `"/" = "slash"`, `"/" = ""`, 1),
|
|
string(data) + "\n[unknown]\nkey = 1\n",
|
|
} {
|
|
if _, err := Decode([]byte(invalid)); err == nil {
|
|
t.Fatal("accepted malformed profile", invalid)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProfileSelectionAndInvalidShadow(t *testing.T) {
|
|
home := t.TempDir()
|
|
paths, err := Paths("default", home, 1000, "bundle")
|
|
if err != nil || len(paths) != 3 || !strings.HasPrefix(paths[0], home) || paths[2] != filepath.Join("bundle", "default.toml") {
|
|
t.Fatal(paths, err)
|
|
}
|
|
paths, err = Paths("default.toml", home, 0, "bundle")
|
|
if err != nil || len(paths) != 2 || strings.HasPrefix(paths[0], home) {
|
|
t.Fatal(paths, err)
|
|
}
|
|
for _, name := range []string{"", ".", "..", "old.conf", "dir/profile", "\x00"} {
|
|
if _, err := Paths(name, home, 0, "bundle"); err == nil {
|
|
t.Fatal("accepted invalid selection", name)
|
|
}
|
|
}
|
|
shadow := filepath.Join(home, "bad.toml")
|
|
if err := os.WriteFile(shadow, []byte("not TOML"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, selected, err := Load([]string{shadow, "../../config/punctuation/default.toml"}); err == nil || selected != shadow {
|
|
t.Fatal("invalid shadow was skipped", selected, err)
|
|
}
|
|
if _, _, err := Load([]string{filepath.Join(home, "missing.toml"), "../../config/punctuation/default.toml"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|