1package registry23import (4 "go/ast"5 "go/parser"6 "go/token"7 "path/filepath"8 "reflect"9 "runtime"10 "slices"11 "strings"12 "testing"13)1415func TestResolveNormalizesSpacingAndAliases(t *testing.T) {16 tests := []struct {17 name string18 want string19 }{20 {name: "word search", want: "Word Search"},21 {name: "hashi", want: "Hashiwokakero"},22 {name: "network", want: "Netwalk"},23 {name: "polyomino", want: "Fillomino"},24 }2526 for _, tt := range tests {27 entry, ok := Resolve(tt.name)28 if !ok {29 t.Fatalf("Resolve(%q) = false, want true", tt.name)30 }31 if got := entry.Definition.Name; got != tt.want {32 t.Fatalf("Resolve(%q) = %q, want %q", tt.name, got, tt.want)33 }34 }35}3637func TestModeSeededFlagMatchesSpawnerAvailability(t *testing.T) {38 for _, entry := range Entries() {39 for _, mode := range entry.Modes {40 if got, want := mode.Definition.Seeded, mode.Seeded != nil; got != want {41 t.Fatalf("%s/%s seeded flag = %v, want %v",42 entry.Definition.Name, mode.Definition.Title, got, want)43 }44 }45 }46}4748func TestEntriesStayAlignedWithDefinitions(t *testing.T) {49 definitions := Definitions()50 entries := Entries()51 if got, want := len(entries), len(definitions); got != want {52 t.Fatalf("len(Entries()) = %d, want %d", got, want)53 }5455 for _, def := range definitions {56 entry, ok := Lookup(def.Name)57 if !ok {58 t.Fatalf("Lookup(%q) = false", def.Name)59 }60 if entry.Import == nil {61 t.Fatalf("%s missing import function", def.Name)62 }63 if got, want := len(entry.Modes), len(def.Modes); got != want {64 t.Fatalf("%s mode count = %d, want %d", def.Name, got, want)65 }66 }67}6869func TestRegistryAllMatchesConcreteGamePackages(t *testing.T) {70 got := registryAllImportPaths(t)71 want := concreteGameImportPaths(t)72 if !reflect.DeepEqual(got, want) {73 t.Fatalf("registry all import paths = %v, want %v", got, want)74 }75}7677func TestPrintAdaptersStayAlignedWithRegistryEntries(t *testing.T) {78 for _, entry := range Entries() {79 if entry.Definition.Name == "Lights Out" {80 if entry.Print != nil {81 t.Fatal("Lights Out should not expose a print adapter")82 }83 continue84 }85 if entry.Print == nil {86 t.Fatalf("%s missing print adapter", entry.Definition.Name)87 }88 }89}9091func registryAllImportPaths(t *testing.T) []string {92 t.Helper()9394 fset := token.NewFileSet()95 file, err := parser.ParseFile(fset, "registry.go", nil, 0)96 if err != nil {97 t.Fatalf("parse registry.go: %v", err)98 }99100 imports := make(map[string]string)101 for _, imp := range file.Imports {102 path := strings.Trim(imp.Path.Value, `"`)103 name := importName(path, imp.Name)104 imports[name] = path105 }106107 paths := registryAllSelectorImportPaths(t, file, imports)108 slices.Sort(paths)109 return paths110}111112func registryAllSelectorImportPaths(t *testing.T, file *ast.File, imports map[string]string) []string {113 t.Helper()114115 for _, decl := range file.Decls {116 genDecl, ok := decl.(*ast.GenDecl)117 if !ok || genDecl.Tok != token.VAR {118 continue119 }120 for _, spec := range genDecl.Specs {121 valueSpec, ok := spec.(*ast.ValueSpec)122 if !ok || len(valueSpec.Names) != 1 || valueSpec.Names[0].Name != "all" || len(valueSpec.Values) != 1 {123 continue124 }125126 lit, ok := valueSpec.Values[0].(*ast.CompositeLit)127 if !ok {128 t.Fatalf("registry all value is %T, want composite literal", valueSpec.Values[0])129 }130131 paths := make([]string, 0, len(lit.Elts))132 for _, elt := range lit.Elts {133 selector, ok := elt.(*ast.SelectorExpr)134 if !ok {135 t.Fatalf("registry all element is %T, want selector", elt)136 }137 pkg, ok := selector.X.(*ast.Ident)138 if !ok {139 t.Fatalf("registry all selector receiver is %T, want identifier", selector.X)140 }141142 path, ok := imports[pkg.Name]143 if !ok {144 t.Fatalf("registry all package %q missing from imports", pkg.Name)145 }146 paths = append(paths, path)147 }148 return paths149 }150 }151152 t.Fatal("registry all declaration not found")153 return nil154}155156func importName(path string, name *ast.Ident) string {157 if name != nil {158 return name.Name159 }160 parts := strings.Split(path, "/")161 return parts[len(parts)-1]162}163164func concreteGameImportPaths(t testing.TB) []string {165 t.Helper()166167 pattern := filepath.Join(repoRoot(t), "*", "gamemode.go")168 matches, err := filepath.Glob(pattern)169 if err != nil {170 t.Fatalf("glob concrete game packages: %v", err)171 }172173 importPaths := make([]string, 0, len(matches))174 for _, match := range matches {175 dir := filepath.Base(filepath.Dir(match))176 importPaths = append(importPaths, "github.com/FelineStateMachine/puzzletea/"+dir)177 }178 slices.Sort(importPaths)179 return importPaths180}181182func repoRoot(t testing.TB) string {183 t.Helper()184185 _, path, _, ok := runtime.Caller(0)186 if !ok {187 t.Fatal("runtime.Caller failed")188 }189 return filepath.Clean(filepath.Join(filepath.Dir(path), ".."))190}