Files

228 lines
7.7 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package spellcheck
import (
"context"
"errors"
"os"
"path/filepath"
"reflect"
"testing"
"time"
)
type TestDictionary struct {
calls []string
gate <-chan struct{}
}
func (d *TestDictionary) Check(word string) (bool, error) {
if d.gate != nil {
<-d.gate
}
d.calls = append(d.calls, word)
return word == "hello", nil
}
func (*TestDictionary) Suggest(string) ([]string, error) {
return []string{"hello", "help", "held"}, nil
}
func (*TestDictionary) Close() {}
func receive_result(t *testing.T, service *Service, request Request) Result {
t.Helper()
if !service.Submit(request) {
t.Fatal("request not accepted")
}
select {
case result := <-service.Results():
return result
case <-time.After(2 * time.Second):
t.Fatal("worker timed out")
return Result{}
}
}
func TestSessionLanguagePrecedenceAndNoInventedFallback(t *testing.T) {
for _, name := range []string{"LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG"} {
t.Setenv(name, "")
}
t.Setenv("LANG", "en-GB.UTF-8")
t.Setenv("LANGUAGE", "fr:de")
if Language() != "en_GB" {
t.Fatal(Language())
}
t.Setenv("LC_MESSAGES", "de_DE.UTF-8@euro")
if Language() != "de_DE" {
t.Fatal(Language())
}
t.Setenv("LC_ALL", "fr_FR.UTF-8")
if Language() != "fr_FR" {
t.Fatal(Language())
}
for _, language := range []string{"C", "C.UTF-8", "POSIX", "../../en_US", "en_US:fr"} {
t.Setenv("LC_ALL", language)
if Language() != "" {
t.Fatal(language, Language())
}
}
}
func TestWordExtractionUnicodeContractionsAndTerminalTokens(t *testing.T) {
for input, want := range map[string]string{"hello,": "hello", "(café)": "café", "don't": "don't", "l’été": "l’été", "naïve": "naïve", "co-operate": "co-operate", "猫": "猫", "123": "", "123": "", "foo42": "", "-foo": "", "./hello": "", "/tmp/hello": "", "name@example.com": "", "https://example.com": "", "some_identifier": "", "foo.bar": "", "--option": "", "@name": "", "#topic": "", "VAR=value": "", "\x00": "", "": ""} {
if got := Word(input); got != want {
t.Errorf("%q: %q, want %q", input, got, want)
}
}
}
func TestGlobalExceptionsPersistenceRemoveAndFailedWriteIsolation(t *testing.T) {
path := filepath.Join(t.TempDir(), "spelling", "exceptions.json")
dict := &TestDictionary{}
options := Options{Language: "en_US", ExceptionsPath: path, OpenDictionary: func(string) (Dictionary, error) { return dict, nil }}
service := New_service(context.Background(), options)
defer service.Close()
if got := receive_result(t, service, Request{Word: "helo"}); got.Correct || !reflect.DeepEqual(got.Suggestions, []string{"hello", "help", "held"}) {
t.Fatal(got)
}
for _, word := range []string{"Fenrir", "Café"} {
if got := receive_result(t, service, Request{Operation: Add, Word: word}); got.Err != nil {
t.Fatal(got)
}
}
for _, operation := range []Operation{Inspect, Automatic} {
if got := receive_result(t, service, Request{Operation: operation, Word: "FENRIR"}); !got.Correct || len(got.Suggestions) != 0 {
t.Fatal(got)
}
}
if len(dict.calls) != 1 {
t.Fatal("exception reached dictionary", dict.calls)
}
info, err := os.Stat(path)
if err != nil || info.Mode().Perm() != 0600 {
t.Fatal(info, err)
}
service.Close()
options.Language = "de_DE"
service = New_service(context.Background(), options)
defer service.Close()
if got := receive_result(t, service, Request{Word: "café"}); !got.Correct {
t.Fatal("exception was language-specific", got)
}
if got := receive_result(t, service, Request{Operation: Remove, Word: "Fenrir"}); got.Err != nil || !reflect.DeepEqual(got.Exceptions, []string{"Café"}) {
t.Fatal(got)
}
if got := receive_result(t, service, Request{Word: "Fenrir"}); got.Correct {
t.Fatal("removed exception accepted", got)
}
// Replace the parent with a file so failure is deterministic even as root.
if err := os.Rename(filepath.Dir(path), filepath.Dir(path)+"-saved"); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Dir(path), []byte("blocked"), 0600); err != nil {
t.Fatal(err)
}
if got := receive_result(t, service, Request{Operation: Add, Word: "wrongword"}); got.Err == nil {
t.Fatal("failed write announced success", got)
}
if got := receive_result(t, service, Request{Word: "wrongword"}); got.Correct {
t.Fatal("failed addition changed memory", got)
}
if got := receive_result(t, service, Request{Operation: Remove, Word: "Café"}); got.Err == nil {
t.Fatal(got)
}
if got := receive_result(t, service, Request{Word: "café"}); !got.Correct {
t.Fatal("failed removal changed memory", got)
}
}
func TestPATHCommandsBuiltinsAndNeverExecute(t *testing.T) {
root := t.TempDir()
t.Setenv("PATH", root)
marker := filepath.Join(root, "executed")
if err := os.WriteFile(filepath.Join(root, "tac"), []byte("#!/bin/sh\necho bad > '"+marker+"'\n"), 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "notexecutable"), []byte("data"), 0600); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(filepath.Join(root, "directory"), 0700); err != nil {
t.Fatal(err)
}
dict := &TestDictionary{}
service := New_service(context.Background(), Options{ExceptionsPath: filepath.Join(root, "exceptions.json"), OpenDictionary: func(string) (Dictionary, error) { return dict, nil }})
defer service.Close()
for _, word := range []string{"tac", "cd", "readarray", "ulimit"} {
if got := receive_result(t, service, Request{Operation: Automatic, Word: word}); !got.Correct {
t.Fatal(word, got)
}
}
if len(dict.calls) != 0 {
t.Fatal(dict.calls)
}
for _, word := range []string{"notexecutable", "directory", "typo"} {
if got := receive_result(t, service, Request{Operation: Automatic, Word: word}); got.Correct {
t.Fatal(word, got)
}
}
if _, err := os.Stat(marker); !errors.Is(err, os.ErrNotExist) {
t.Fatal("command executed", err)
}
if got := receive_result(t, service, Request{Word: "tac"}); got.Correct {
t.Fatal("manual lexical check skipped", got)
}
}
func TestMissingDictionaryStillAllowsExceptionManagement(t *testing.T) {
service := New_service(context.Background(), Options{ExceptionsPath: filepath.Join(t.TempDir(), "exceptions.json"), OpenDictionary: func(string) (Dictionary, error) { return nil, errors.New("missing dictionary") }})
defer service.Close()
if got := receive_result(t, service, Request{Word: "hello"}); got.Available || got.DictionaryError == nil || got.Correct {
t.Fatal(got)
}
if got := receive_result(t, service, Request{Operation: Add, Word: "Fenrir"}); got.Err != nil {
t.Fatal(got)
}
if got := receive_result(t, service, Request{Word: "fenrir"}); !got.Correct {
t.Fatal(got)
}
}
func TestSlowDictionaryDoesNotBlockInputSubmissionOrShutdown(t *testing.T) {
gate := make(chan struct{})
service := New_service(context.Background(), Options{ExceptionsPath: filepath.Join(t.TempDir(), "exceptions.json"), OpenDictionary: func(string) (Dictionary, error) { return &TestDictionary{gate: gate}, nil }})
started := time.Now()
for range 100 {
service.Submit(Request{Operation: Automatic, Word: "helo"})
}
if time.Since(started) > 100*time.Millisecond {
t.Fatal("input blocked")
}
close(gate)
service.Close()
if service.Submit(Request{}) {
t.Fatal("closed worker accepted request")
}
}
func TestActualEnchantDictionaryAndRankedSuggestions(t *testing.T) {
dict, err := open_dictionary("en_US")
if err != nil {
t.Skip(err)
}
defer dict.Close()
if correct, err := dict.Check("hello"); err != nil || !correct {
t.Fatal(correct, err)
}
if correct, err := dict.Check("helo"); err != nil || correct {
t.Fatal(correct, err)
}
if suggestions, err := dict.Suggest("helo"); err != nil || len(suggestions) == 0 {
t.Fatal(suggestions, err)
}
if dict, err := open_dictionary("zz_ZZ"); err == nil {
dict.Close()
t.Fatal("missing dictionary accepted")
}
}