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:
1 parent
c018f5fd29
commit
0e50de4c76
1047 files changed
+58364
-63747
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user