After commit c018f5fd29 Fenrir is being switched over to go. If you do not want to be part of the testing process, please switch to master.

This commit is contained in:
Storm Dragon committed 2026-10-27 20:30:55 -04:00
1 parent c018f5fd29
commit 0e50de4c76
1047 files changed
+58364 -63747

No files matched your search

+188
View File
@@ -0,0 +1,188 @@
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package spellcheck
import (
"context"
"encoding/json"
"errors"
"io"
"os"
"os/exec"
"path/filepath"
"reflect"
"syscall"
"testing"
"time"
)
func TestExceptionEditsPreserveOtherReadersChanges(t *testing.T) {
path := filepath.Join(t.TempDir(), "exceptions.json")
options := Options{ExceptionsPath: path, OpenDictionary: func(string) (Dictionary, error) { return &TestDictionary{}, nil }}
first, second := New_service(context.Background(), options), New_service(context.Background(), options)
defer first.Close()
defer second.Close()
// Both readers load the original list, then the user switches between them.
receive_result(t, first, Request{Operation: Inspect})
receive_result(t, second, Request{Operation: Inspect})
for _, edit := range []struct {
service *Service
request Request
want []string
}{
{first, Request{Operation: Add, Word: "Alpha"}, []string{"Alpha"}},
{second, Request{Operation: Add, Word: "Beta"}, []string{"Alpha", "Beta"}},
{first, Request{Operation: Remove, Word: "ALPHA"}, []string{"Beta"}},
{second, Request{Operation: Add, Word: "Gamma"}, []string{"Beta", "Gamma"}},
{first, Request{Operation: Add, Word: "Alpha"}, []string{"Alpha", "Beta", "Gamma"}},
} {
result := receive_result(t, edit.service, edit.request)
if result.Err != nil || !reflect.DeepEqual(result.Exceptions, edit.want) {
t.Fatalf("edit %v: result=%+v, want=%v", edit.request, result, edit.want)
}
words, err := load_exceptions(path)
if err != nil || !reflect.DeepEqual(exception_list(words), edit.want) {
t.Fatalf("saved exceptions=%v, error=%v, want=%v", words, err, edit.want)
}
}
}
// Separate processes verify that the transaction lock works across readers.
func TestExceptionWriterProcess(t *testing.T) {
path := os.Getenv("FENRIR_EXCEPTION_TEST_PATH")
if path == "" {
return
}
service := New_service(context.Background(), Options{ExceptionsPath: path, OpenDictionary: func(string) (Dictionary, error) { return &TestDictionary{}, nil }})
defer service.Close()
encoder, decoder := json.NewEncoder(os.Stdout), json.NewDecoder(os.Stdin)
result := receive_result(t, service, Request{Operation: Inspect})
if result.Err != nil {
t.Fatal(result.Err)
}
if err := encoder.Encode(result.Exceptions); err != nil {
t.Fatal(err)
}
for {
var request Request
if err := decoder.Decode(&request); err != nil {
if errors.Is(err, io.EOF) {
return
}
t.Fatal(err)
}
result := receive_result(t, service, request)
if result.Err != nil {
t.Fatal(result.Err)
}
if err := encoder.Encode(result.Exceptions); err != nil {
t.Fatal(err)
}
}
}
func TestConcurrentExceptionProcessesKeepBothEdits(t *testing.T) {
path := filepath.Join(t.TempDir(), "exceptions.json")
type Writer struct {
encoder *json.Encoder
decoder *json.Decoder
}
start_writer := func() Writer {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestExceptionWriterProcess$")
cmd.Env = append(os.Environ(), "FENRIR_EXCEPTION_TEST_PATH="+path)
cmd.Stderr = os.Stderr
input, err := cmd.StdinPipe()
if err != nil {
cancel()
t.Fatal(err)
}
output, err := cmd.StdoutPipe()
if err != nil {
cancel()
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
cancel()
t.Fatal(err)
}
t.Cleanup(func() {
input.Close()
err := cmd.Wait()
cancel()
if err != nil {
t.Error(err)
}
})
writer := Writer{json.NewEncoder(input), json.NewDecoder(output)}
var ready []string
if err := writer.decoder.Decode(&ready); err != nil {
t.Fatal(err)
}
return writer
}
first, second := start_writer(), start_writer()
for _, edit := range []struct {
writer Writer
word string
}{{first, "Alpha"}, {second, "Beta"}} {
if err := edit.writer.encoder.Encode(Request{Operation: Add, Word: edit.word}); err != nil {
t.Fatal(err)
}
}
for _, writer := range []Writer{first, second} {
var words []string
if err := writer.decoder.Decode(&words); err != nil {
t.Fatal(err)
}
}
words, err := load_exceptions(path)
if err != nil || !reflect.DeepEqual(exception_list(words), []string{"Alpha", "Beta"}) {
t.Fatal("lost concurrent addition", words, err)
}
info, err := os.Stat(path + ".lock")
if err != nil || info.Mode().Perm() != 0600 {
t.Fatal("lock permissions", info, err)
}
}
func TestExceptionLockCancellationAndInvalidStorePreserveData(t *testing.T) {
path := filepath.Join(t.TempDir(), "exceptions.json")
if err := save_exceptions(path, map[string]string{"alpha": "Alpha"}); err != nil {
t.Fatal(err)
}
lock, err := os.OpenFile(path+".lock", os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
t.Fatal(err)
}
defer lock.Close()
if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
defer cancel()
if _, err := update_exceptions(ctx, path, Add, "Beta"); !errors.Is(err, context.DeadlineExceeded) {
t.Fatal("lock wait ignored cancellation", err)
}
words, err := load_exceptions(path)
if err != nil || !reflect.DeepEqual(exception_list(words), []string{"Alpha"}) {
t.Fatal("cancelled edit changed data", words, err)
}
if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_UN); err != nil {
t.Fatal(err)
}
if _, err := update_exceptions(context.Background(), path, Add, "Beta"); err != nil {
t.Fatal("edit failed after lock release", err)
}
if err := os.WriteFile(path, []byte("invalid JSON"), 0600); err != nil {
t.Fatal(err)
}
if _, err := update_exceptions(context.Background(), path, Add, "Gamma"); err == nil {
t.Fatal("edit overwrote invalid shared file")
}
data, err := os.ReadFile(path)
if err != nil || string(data) != "invalid JSON" {
t.Fatal("invalid file was replaced", string(data), err)
}
}