package index import ( "os" "path/filepath" "testing" ) // writeFixture lays down a single-module Einherjar-like repo under a temp root // and returns the root path for Build to walk. func writeFixture(t *testing.T) string { t.Helper() root := t.TempDir() modDir := filepath.Join(root, "demo") if err := os.MkdirAll(modDir, 0o755); err != nil { t.Fatal(err) } write := func(name, body string) { if err := os.WriteFile(filepath.Join(modDir, name), []byte(body), 0o644); err != nil { t.Fatal(err) } } write("go.mod", "module example.com/demo\n\ngo 1.26\n") write("demo.go", `// Package demo is a build fixture. package demo import "context" // Config holds settings. type Config struct { // Host is the bind address. Host string `+"`"+`env:"DEMO_HOST" envDefault:"0.0.0.0"`+"`"+` Port int `+"`"+`env:"DEMO_PORT"`+"`"+` Inner } // Inner is embedded into Config. type Inner struct { Name string } // Store reads values. type Store interface { // Get fetches one value. Get(ctx context.Context, id string) (string, error) context.Context } `) return root } func findSymbol(t *testing.T, idx *Index, mod, name string) Symbol { t.Helper() m := idx.FindModule(mod) if m == nil { t.Fatalf("module %q not found", mod) } for _, s := range m.Symbols { if s.Name == name { return s } } t.Fatalf("symbol %q not found in module %q", name, mod) return Symbol{} } func TestBuildCapturesStructFields(t *testing.T) { idx, err := Build(writeFixture(t)) if err != nil { t.Fatal(err) } cfg := findSymbol(t, idx, "demo", "Config") if cfg.Kind != "type" { t.Errorf("Config kind = %q, want type", cfg.Kind) } if len(cfg.Fields) != 3 { t.Fatalf("Config has %d fields, want 3: %+v", len(cfg.Fields), cfg.Fields) } host := cfg.Fields[0] if host.Name != "Host" || host.Type != "string" { t.Errorf("field[0] = %+v, want Host string", host) } if host.Tag != `env:"DEMO_HOST" envDefault:"0.0.0.0"` { t.Errorf("Host tag = %q, want the env/envDefault tag (backticks stripped)", host.Tag) } if host.Doc != "Host is the bind address." { t.Errorf("Host doc = %q", host.Doc) } if port := cfg.Fields[1]; port.Name != "Port" || port.Tag != `env:"DEMO_PORT"` { t.Errorf("field[1] = %+v, want Port with env tag", port) } if inner := cfg.Fields[2]; !inner.Embedded || inner.Name != "" || inner.Type != "Inner" { t.Errorf("field[2] = %+v, want embedded Inner", inner) } } func TestBuildCapturesInterfaceMethods(t *testing.T) { idx, err := Build(writeFixture(t)) if err != nil { t.Fatal(err) } store := findSymbol(t, idx, "demo", "Store") if store.Kind != "interface" { t.Errorf("Store kind = %q, want interface", store.Kind) } if len(store.Methods) != 2 { t.Fatalf("Store has %d methods, want 2: %+v", len(store.Methods), store.Methods) } get := store.Methods[0] if get.Name != "Get" { t.Errorf("method[0] name = %q, want Get", get.Name) } if get.Signature != "Get(ctx context.Context, id string) (string, error)" { t.Errorf("Get signature = %q", get.Signature) } if get.Doc != "Get fetches one value." { t.Errorf("Get doc = %q", get.Doc) } if emb := store.Methods[1]; emb.Name != "" || emb.Signature != "context.Context" { t.Errorf("method[1] = %+v, want embedded context.Context", emb) } } // A struct must be discoverable by one of its struct tags — the failure mode // that motivated capturing fields in the first place. func TestSearchSymbolsByStructTag(t *testing.T) { idx, err := Build(writeFixture(t)) if err != nil { t.Fatal(err) } got := idx.SearchSymbols("DEMO_HOST", 10) found := false for _, s := range got { if s.Name == "Config" { found = true } } if !found { t.Errorf("searching a struct tag did not surface its type; got %d results", len(got)) } }