Files

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)
}
}