Files
Maksim Totmin c398045664
ci / test (push) Failing after 5s
ci / lint (push) Failing after 4s
chore: initial release v0.1.0
forge-toolkit: хелперы для разработки MCP-серверов на Go.

- toolkit: args/schema/result/env/probe/server
- toolkit/validate: валидаторы синтаксиса
- configreload: live-reload конфига по контент-хэшу
- docs: ARCHITECTURE, quickstart, mcp-contract
- template/ и examples/hello-tool/
- CI (Gitea Actions), лицензия Apache-2.0
2026-09-14 00:23:08 +07:00

111 lines
2.9 KiB
Go

package configreload
import (
"errors"
"os"
"path/filepath"
"testing"
)
func write(t *testing.T, dir, content string) string {
t.Helper()
p := filepath.Join(dir, "cfg.json")
if err := os.WriteFile(p, []byte(content), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
return p
}
func parse(cfg map[string]string) func([]byte) (map[string]string, error) {
return func(b []byte) (map[string]string, error) {
if len(b) == 0 {
return nil, errors.New("empty")
}
return map[string]string{"v": string(b)}, nil
}
}
func TestLoader_CachesUntilContentChanges(t *testing.T) {
dir := t.TempDir()
p := write(t, dir, "one")
l := New(p, parse(nil))
if v, err := l.Get(); err != nil || v["v"] != "one" {
t.Fatalf("first get: %v %v", v, err)
}
// Не изменилось — отдаётся из кэша (нет системных вызовов на парсинг).
if v, err := l.Get(); err != nil || v["v"] != "one" {
t.Fatalf("cached get: %v %v", v, err)
}
// Изменилось содержимое на то же по размеру → хэш ловит.
if err := os.WriteFile(p, []byte("two"), 0o600); err != nil {
t.Fatalf("rewrite: %v", err)
}
if v, err := l.Get(); err != nil || v["v"] != "two" {
t.Fatalf("after change: %v %v", v, err)
}
}
func TestLoader_LastGoodOnInvalid(t *testing.T) {
dir := t.TempDir()
p := write(t, dir, "good")
l := New(p, parse(nil))
if _, err := l.Get(); err != nil {
t.Fatalf("get: %v", err)
}
// Пишем невалидный (пустой) файл — парсер вернёт ошибку => last-good.
if err := os.WriteFile(p, []byte(""), 0o600); err != nil {
t.Fatalf("write empty: %v", err)
}
v, err := l.Get()
if err == nil {
t.Fatal("expected parse error")
}
if v["v"] != "good" {
t.Fatalf("expected last-good value, got %v", v)
}
}
func TestLoader_NotFoundReset(t *testing.T) {
dir := t.TempDir()
p := write(t, dir, "x")
l := New(p, parse(nil))
if _, err := l.Get(); err != nil {
t.Fatalf("get: %v", err)
}
if err := os.Remove(p); err != nil {
t.Fatalf("remove: %v", err)
}
if _, err := l.Get(); !errors.Is(err, ErrNotFound) {
t.Fatalf("expected ErrNotFound, got %v", err)
}
// Восстановили файл — снова загружается.
write(t, dir, "y")
p2 := filepath.Join(dir, "cfg.json")
if v, err := l.Get(); err != nil || v["v"] != "y" {
t.Fatalf("after restore: %v %v", v, err)
} else if p2 == "" {
t.Fatal("unreachable")
}
}
func TestLoader_Concurrent(t *testing.T) {
dir := t.TempDir()
p := write(t, dir, "start")
l := New(p, parse(nil))
done := make(chan struct{})
for i := 0; i < 20; i++ {
go func() {
defer func() { done <- struct{}{} }()
_, _ = l.Get()
}()
}
for i := 0; i < 20; i++ {
<-done
}
// После конкурентных чтений — кэш валиден.
if v, err := l.Get(); err != nil || v["v"] != "start" {
t.Fatalf("after concurrent: %v %v", v, err)
}
}