1package registry23import (4 "go/ast"5 "go/parser"6 "go/token"7 "os"8 "path/filepath"9 "reflect"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 if got, want := len(entry.Variants), len(def.Variants); got != want {67 t.Fatalf("%s variant count = %d, want %d", def.Name, got, want)68 }69 }70}7172func TestEntriesExposeVariantsAndValidLegacyAliases(t *testing.T) {73 for _, entry := range Entries() {74 if len(entry.Variants) == 0 {75 t.Fatalf("%s has no variants", entry.Definition.Name)76 }7778 variantIDs := make(map[string]struct{}, len(entry.Variants))79 for _, variant := range entry.Variants {80 if variant.Elo == nil {81 t.Fatalf("%s/%s missing Elo spawner", entry.Definition.Name, variant.Definition.Title)82 }83 variantIDs[string(variant.Definition.ID)] = struct{}{}84 }8586 for _, alias := range entry.LegacyModes {87 if _, ok := variantIDs[string(alias.TargetVariantID)]; !ok {88 t.Fatalf("%s legacy alias %s targets missing variant %s", entry.Definition.Name, alias.Title, alias.TargetVariantID)89 }90 }91 }92}9394func TestRegistryAllMatchesConcreteGamePackages(t *testing.T) {95 got := registryAllImportPaths(t)96 want := concreteGameImportPaths(t)97 if !reflect.DeepEqual(got, want) {98 t.Fatalf("registry all import paths = %v, want %v", got, want)99 }100}101102func TestPrintAdaptersStayAlignedWithRegistryEntries(t *testing.T) {103 for _, entry := range Entries() {104 if entry.Definition.Name == "Lights Out" {105 if entry.Print != nil {106 t.Fatal("Lights Out should not expose a print adapter")107 }108 continue109 }110 if entry.Print == nil {111 t.Fatalf("%s missing print adapter", entry.Definition.Name)112 }113 }114}115116func registryAllImportPaths(t *testing.T) []string {117 t.Helper()118119 fset := token.NewFileSet()120 file, err := parser.ParseFile(fset, "registry.go", nil, 0)121 if err != nil {122 t.Fatalf("parse registry.go: %v", err)123 }124125 imports := make(map[string]string)126 for _, imp := range file.Imports {127 path := strings.Trim(imp.Path.Value, `"`)128 name := importName(path, imp.Name)129 imports[name] = path130 }131132 paths := registryAllSelectorImportPaths(t, file, imports)133 slices.Sort(paths)134 return paths135}136137func registryAllSelectorImportPaths(t *testing.T, file *ast.File, imports map[string]string) []string {138 t.Helper()139140 for _, decl := range file.Decls {141 genDecl, ok := decl.(*ast.GenDecl)142 if !ok || genDecl.Tok != token.VAR {143 continue144 }145 for _, spec := range genDecl.Specs {146 valueSpec, ok := spec.(*ast.ValueSpec)147 if !ok || len(valueSpec.Names) != 1 || valueSpec.Names[0].Name != "all" || len(valueSpec.Values) != 1 {148 continue149 }150151 lit, ok := valueSpec.Values[0].(*ast.CompositeLit)152 if !ok {153 t.Fatalf("registry all value is %T, want composite literal", valueSpec.Values[0])154 }155156 paths := make([]string, 0, len(lit.Elts))157 for _, elt := range lit.Elts {158 selector, ok := elt.(*ast.SelectorExpr)159 if !ok {160 t.Fatalf("registry all element is %T, want selector", elt)161 }162 pkg, ok := selector.X.(*ast.Ident)163 if !ok {164 t.Fatalf("registry all selector receiver is %T, want identifier", selector.X)165 }166167 path, ok := imports[pkg.Name]168 if !ok {169 t.Fatalf("registry all package %q missing from imports", pkg.Name)170 }171 paths = append(paths, path)172 }173 return paths174 }175 }176177 t.Fatal("registry all declaration not found")178 return nil179}180181func importName(path string, name *ast.Ident) string {182 if name != nil {183 return name.Name184 }185 parts := strings.Split(path, "/")186 return parts[len(parts)-1]187}188189func concreteGameImportPaths(t testing.TB) []string {190 t.Helper()191192 pattern := filepath.Join(repoRoot(t), "games", "*", "gamemode.go")193 matches, err := filepath.Glob(pattern)194 if err != nil {195 t.Fatalf("glob concrete game packages: %v", err)196 }197198 importPaths := make([]string, 0, len(matches))199 for _, match := range matches {200 dir := filepath.Base(filepath.Dir(match))201 importPaths = append(importPaths, "github.com/FelineStateMachine/puzzletea/games/"+dir)202 }203 slices.Sort(importPaths)204 return importPaths205}206207func repoRoot(t testing.TB) string {208 t.Helper()209210 wd, err := os.Getwd()211 if err != nil {212 t.Fatalf("get working directory: %v", err)213 }214215 for dir := filepath.Clean(wd); ; dir = filepath.Dir(dir) {216 path := filepath.Join(dir, "go.mod")217 if _, err := os.Stat(path); err == nil {218 return dir219 } else if !os.IsNotExist(err) {220 t.Fatalf("stat %s: %v", path, err)221 }222223 parent := filepath.Dir(dir)224 if parent == dir {225 t.Fatalf("find repo root from %s: go.mod not found", wd)226 }227 }228}