Stage 10: admin console & dictionary ops (complaint review, hot-reload, broadcasts) (#11)
This commit was merged in pull request #11.
This commit is contained in:
@@ -2,6 +2,7 @@ package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"sync"
|
||||
@@ -63,6 +64,36 @@ func Open(dir, version string, variants ...Variant) (*Registry, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// OpenWithVersions builds a Registry by loading the boot version from the flat
|
||||
// dir (every variant, as Open) and then every additional version held in an
|
||||
// immediate subdirectory of dir: a subdirectory named V contributes, under
|
||||
// version V, the variants whose committed DAWG it carries. This is the
|
||||
// restart-side of the admin dictionary reload — a version reloaded into dir/<V>/
|
||||
// at runtime is resident again after a restart. A subdirectory named like the
|
||||
// boot version is skipped (the flat dir already is the boot version). A partially
|
||||
// loaded registry is closed before any error is returned.
|
||||
func OpenWithVersions(dir, bootVersion string) (*Registry, error) {
|
||||
r, err := Open(dir, bootVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
_ = r.Close()
|
||||
return nil, fmt.Errorf("engine: scan dictionary dir %s: %w", dir, err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() || e.Name() == bootVersion {
|
||||
continue
|
||||
}
|
||||
if _, err := r.LoadAvailable(filepath.Join(dir, e.Name()), e.Name()); err != nil {
|
||||
_ = r.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Load reads the committed DAWG of variant from dir, builds a solver over it and
|
||||
// registers it under version. Reloading the same (variant, version) replaces the
|
||||
// previous entry, closing its finder. The most recently loaded version of a
|
||||
@@ -91,6 +122,29 @@ func (r *Registry) Load(v Variant, version, dir string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAvailable loads, under version, every variant whose committed DAWG is
|
||||
// present in dir, skipping a variant whose file is absent. It backs the admin
|
||||
// dictionary reload (a version subdirectory may carry only the variants that were
|
||||
// rebuilt) and OpenWithVersions' boot-time scan. It returns the variants it
|
||||
// loaded, in catalogue order, or the first load error.
|
||||
func (r *Registry) LoadAvailable(dir, version string) ([]Variant, error) {
|
||||
var loaded []Variant
|
||||
for _, v := range Variants() {
|
||||
path := filepath.Join(dir, dictFiles[v])
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return loaded, fmt.Errorf("engine: stat %s dictionary %q in %s: %w", v, version, dir, err)
|
||||
}
|
||||
if err := r.Load(v, version, dir); err != nil {
|
||||
return loaded, err
|
||||
}
|
||||
loaded = append(loaded, v)
|
||||
}
|
||||
return loaded, nil
|
||||
}
|
||||
|
||||
// Solver returns the solver for the (variant, version) pair, or ErrUnknownVariant
|
||||
// when the variant is absent and ErrUnknownVersion when only the version is.
|
||||
func (r *Registry) Solver(v Variant, version string) (*scrabble.Solver, error) {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// copyDawg copies the committed DAWG for v from srcDir into dstDir (creating
|
||||
// dstDir). It is the fixture builder for the dictionary-reload tests, which need
|
||||
// real DAWG files laid out in temporary version directories.
|
||||
func copyDawg(t *testing.T, srcDir, dstDir string, v Variant) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(dstDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir %s: %v", dstDir, err)
|
||||
}
|
||||
name := dictFiles[v]
|
||||
src, err := os.Open(filepath.Join(srcDir, name))
|
||||
if err != nil {
|
||||
t.Fatalf("open source dawg %s: %v", name, err)
|
||||
}
|
||||
defer func() { _ = src.Close() }()
|
||||
dst, err := os.Create(filepath.Join(dstDir, name))
|
||||
if err != nil {
|
||||
t.Fatalf("create dest dawg %s: %v", name, err)
|
||||
}
|
||||
defer func() { _ = dst.Close() }()
|
||||
if _, err := io.Copy(dst, src); err != nil {
|
||||
t.Fatalf("copy dawg %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadAvailableLoadsPresentSkipsAbsent verifies LoadAvailable registers only
|
||||
// the variants whose DAWG is present in the directory, under the given version.
|
||||
func TestLoadAvailableLoadsPresentSkipsAbsent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
copyDawg(t, testDictDir(), dir, VariantEnglish) // only English present
|
||||
|
||||
reg := NewRegistry()
|
||||
defer func() { _ = reg.Close() }()
|
||||
loaded, err := reg.LoadAvailable(dir, "v2")
|
||||
if err != nil {
|
||||
t.Fatalf("load available: %v", err)
|
||||
}
|
||||
if len(loaded) != 1 || loaded[0] != VariantEnglish {
|
||||
t.Fatalf("loaded = %v, want [english]", loaded)
|
||||
}
|
||||
if _, err := reg.Solver(VariantEnglish, "v2"); err != nil {
|
||||
t.Errorf("english v2 solver: %v", err)
|
||||
}
|
||||
if _, err := reg.Solver(VariantRussianScrabble, "v2"); !errors.Is(err, ErrUnknownVariant) {
|
||||
t.Errorf("russian v2 should be absent: got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOpenWithVersionsScansSubdirs verifies the boot helper loads the flat boot
|
||||
// version plus every version subdirectory, the subdir version becoming the
|
||||
// variant's latest while the boot version stays resident.
|
||||
func TestOpenWithVersionsScansSubdirs(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
for _, v := range Variants() { // flat boot version: all three variants
|
||||
copyDawg(t, testDictDir(), dir, v)
|
||||
}
|
||||
copyDawg(t, testDictDir(), filepath.Join(dir, "v2"), VariantEnglish) // v2 subdir: English only
|
||||
|
||||
reg, err := OpenWithVersions(dir, "v1")
|
||||
if err != nil {
|
||||
t.Fatalf("open with versions: %v", err)
|
||||
}
|
||||
defer func() { _ = reg.Close() }()
|
||||
|
||||
for _, v := range Variants() {
|
||||
if _, err := reg.Solver(v, "v1"); err != nil {
|
||||
t.Errorf("boot solver %s/v1: %v", v, err)
|
||||
}
|
||||
}
|
||||
if got := reg.Versions(VariantEnglish); len(got) != 2 {
|
||||
t.Errorf("english versions = %v, want two", got)
|
||||
}
|
||||
latest, _, err := reg.Latest(VariantEnglish)
|
||||
if err != nil {
|
||||
t.Fatalf("latest english: %v", err)
|
||||
}
|
||||
if latest != "v2" {
|
||||
t.Errorf("latest english = %q, want v2", latest)
|
||||
}
|
||||
if got := reg.Versions(VariantRussianScrabble); len(got) != 1 {
|
||||
t.Errorf("russian versions = %v, want one (no v2 file)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReloadRegistersNewVersion verifies Load adds a second version to a variant
|
||||
// already resident, moves the latest pointer and keeps the earlier version.
|
||||
func TestReloadRegistersNewVersion(t *testing.T) {
|
||||
reg, err := Open(testDictDir(), "v1", VariantEnglish)
|
||||
if err != nil {
|
||||
t.Fatalf("open: %v", err)
|
||||
}
|
||||
defer func() { _ = reg.Close() }()
|
||||
|
||||
if err := reg.Load(VariantEnglish, "v2", testDictDir()); err != nil {
|
||||
t.Fatalf("reload v2: %v", err)
|
||||
}
|
||||
if got := reg.Versions(VariantEnglish); len(got) != 2 {
|
||||
t.Fatalf("versions = %v, want two", got)
|
||||
}
|
||||
latest, _, err := reg.Latest(VariantEnglish)
|
||||
if err != nil {
|
||||
t.Fatalf("latest: %v", err)
|
||||
}
|
||||
if latest != "v2" {
|
||||
t.Errorf("latest = %q, want v2", latest)
|
||||
}
|
||||
if _, err := reg.Solver(VariantEnglish, "v1"); err != nil {
|
||||
t.Errorf("v1 still resident: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user