From fae9ed33978bba71cfcb4aeea4ababa6b036585c Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Fri, 21 Aug 2026 23:58:15 +0300 Subject: [PATCH 01/12] ci: add tests Signed-off-by: Mohammed Al Sahaf --- .github/copilot-instructions.md | 97 +++++ .gitignore | 1 + baseline_behavior_test.go | 99 +++++ concurrency_baseline_test.go | 65 +++ concurrency_edge_test.go | 163 ++++++++ core_functionality_test.go | 318 +++++++++++++++ go.mod | 73 ++-- go.sum | 704 +++++++++----------------------- integration_baseline_test.go | 213 ++++++++++ original_baseline_test.go | 231 +++++++++++ sourcecode_edge_test.go | 120 ++++++ specific_functions_test.go | 326 +++++++++++++++ storage_edge_test.go | 244 +++++++++++ synthesis_baseline_test.go | 578 ++++++++++++++++++++++++++ synthesis_edge_test.go | 154 +++++++ synthesis_real_test.go | 372 +++++++++++++++++ testdata/constid/module.go | 23 ++ testdata/crosspkg/module.go | 10 + testdata/exprid/module.go | 23 ++ testdata/gizmo.go | 21 + testdata/kinds/types.go | 52 +++ testdata/noimpl/module.go | 16 + testdata/unregistered/module.go | 32 ++ testdata/valuerecv/module.go | 21 + traverse_edge_test.go | 175 ++++++++ utils_edge_test.go | 128 ++++++ validation_baseline_test.go | 218 ++++++++++ workspace_edge_test.go | 133 ++++++ 28 files changed, 4055 insertions(+), 555 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 baseline_behavior_test.go create mode 100644 concurrency_baseline_test.go create mode 100644 concurrency_edge_test.go create mode 100644 core_functionality_test.go create mode 100644 integration_baseline_test.go create mode 100644 original_baseline_test.go create mode 100644 sourcecode_edge_test.go create mode 100644 specific_functions_test.go create mode 100644 storage_edge_test.go create mode 100644 synthesis_baseline_test.go create mode 100644 synthesis_edge_test.go create mode 100644 synthesis_real_test.go create mode 100644 testdata/constid/module.go create mode 100644 testdata/crosspkg/module.go create mode 100644 testdata/exprid/module.go create mode 100644 testdata/gizmo.go create mode 100644 testdata/kinds/types.go create mode 100644 testdata/noimpl/module.go create mode 100644 testdata/unregistered/module.go create mode 100644 testdata/valuerecv/module.go create mode 100644 traverse_edge_test.go create mode 100644 utils_edge_test.go create mode 100644 validation_baseline_test.go create mode 100644 workspace_edge_test.go diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..7bf936f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,97 @@ +# ModuleDoc AI Coding Instructions + +## Project Overview +ModuleDoc analyzes Go source code to generate JSON documentation for Caddy modules. It uses Go's AST parsing and type system to extract struct definitions, JSON tags, and documentation, specifically targeting Caddy's plugin architecture. + +## Core Architecture + +### Three-Layer System +1. **Driver** (`driver.go`) - Main API entry point with caching (`discoveredTypes` map) +2. **Workspace** (`workspace.go`) - Temporary Go module management with `go get` and package loading +3. **Synthesis** (`synthesis.go`) - Type representation building from Go AST/types + +### Key Data Flow +``` +LoadModulesFromImportingPackage → openWorkspace → findCaddyModuleIdents → buildRepresentation → Value struct +``` + +## Critical Patterns + +### AST Analysis for Caddy Modules +- Look for `caddy.RegisterModule()` calls AND `CaddyModule()` method implementations +- Extract module IDs from `caddy.ModuleInfo{ID: "module.name"}` return statements +- Both registration and implementation must exist (validated in `findCaddyModuleIdents`) + +### Type System Mapping +The `Value` struct is central - it flattens Go types into JSON-documentable forms: +- `Type` enum: primitives (`Bool`, `String`) + structural (`Struct`, `Array`, `Map`) + Caddy-specific (`Module`, `ModuleMap`) +- `SameAs` field enables type reuse via FQTN (fully-qualified type name) references +- `ModuleNamespace`/`ModuleInlineKey` handle Caddy's module configuration patterns + +### Workspace Management +- Creates temporary Go modules in `/tmp` for isolated package analysis +- Runs `go get` commands to fetch dependencies at specific versions +- Uses `golang.org/x/tools/go/packages` with specific `packages.Config` modes +- **Always** set `CGO_ENABLED=0` in test environments (Linux compatibility) + +### Storage Interface +Implement `Storage` for caching - methods: `GetTypeByName`, `GetTypesByCaddyModuleID`, `StoreType`, `SetCaddyModuleName` + +## Testing Patterns + +### Test Structure +- Use `testdata/` directory with minimal Caddy module examples +- Load packages with full AST/types info: `packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo` +- Set `CGO_ENABLED=0` in test environments to avoid "no metadata for C" errors + +### Example Module Pattern (see `testdata/gizmo.go`): +```go +type ModuleName struct{} + +func init() { + caddy.RegisterModule(new(ModuleName)) +} + +func (*ModuleName) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "namespace.module_name", + New: func() caddy.Module { return new(ModuleName) }, + } +} +``` + +## Key Constants & Conventions +- `CaddyCorePackage = "github.com/caddyserver/caddy/v2"` +- `registerModule = "RegisterModule"` +- FQTN format: `"package/path.TypeName@version"` +- Module ID format: dot-separated namespaces like `"http.handlers.file_server"` + +## JSON Tag Processing +- Uses `reflect.StructTag` to parse `json:` and `caddy:` tags +- JSON name extraction handles comma-separated options, excludes `"-"` fields +- Caddy tags parsed via `caddy.ParseStructTag()` for module configuration + +## Error Handling Patterns +- Extensive validation that module registration matches implementation +- AST inspection errors bubble up through `visitErr` pattern +- Package loading failures wrapped with context (`fmt.Errorf("loading package %s: %v", ...)`) + +## Recent Improvements (2025) + +### Fixed Critical Issues +- **Race Conditions**: Added proper mutex locking around `discoveredTypes` map access +- **Dynamic Module IDs**: Enhanced AST analysis to handle computed module IDs like `caddy.ModuleID("prefix" + suffix)` +- **Constant Resolution**: Module IDs can now reference package-level constants +- **Graceful Degradation**: Unknown Go types fallback to string representation instead of hard failures +- **Memory Management**: Implemented LRU cache with TTL (30min) and size limits (100 entries) for package cache +- **Relaxed Validation**: Modules with only `CaddyModule()` implementation (no registration) now generate warnings instead of errors + +### Enhanced Error Handling +- Interface types, channels, and function signatures have proper fallback representations +- Complex AST expressions (binary operations, function calls, selectors) are now supported for module IDs +- Cache eviction prevents memory leaks in long-running processes + +## Development Notes +- Requires Go toolchain installed (uses `go` commands directly) +- Thread-safe operations with proper mutex locking on all shared state +- Heavy caching for performance - workspace package cache with automatic eviction, type representation cache \ No newline at end of file diff --git a/.gitignore b/.gitignore index fbd281d..2a2506a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ _gitignore/ +.DS_Store \ No newline at end of file diff --git a/baseline_behavior_test.go b/baseline_behavior_test.go new file mode 100644 index 0000000..2a612fc --- /dev/null +++ b/baseline_behavior_test.go @@ -0,0 +1,99 @@ +package moduledoc + +import ( + "os" + "testing" + + "golang.org/x/tools/go/packages" +) + +// TestOriginalBehaviorBaseline establishes baseline behavior for the original implementation +func TestOriginalBehaviorBaseline(t *testing.T) { + t.Run("ValidStaticModule", func(t *testing.T) { + // Test only the gizmo.go file which should work + cfg := &packages.Config{ + Dir: "testdata", + Mode: packages.NeedSyntax | + packages.NeedImports | + packages.NeedDeps | + packages.NeedTypes | + packages.NeedModule | + packages.NeedTypesInfo, + Env: append(os.Environ(), "CGO_ENABLED=0"), + } + + // Load only the gizmo.go file + pkgs, err := packages.Load(cfg, ".") + if err != nil { + t.Logf("Package loading failed (this is expected if testdata has validation errors): %v", err) + return + } + + if len(pkgs) == 0 { + t.Log("No packages loaded - this may be expected if testdata has mixed valid/invalid modules") + return + } + + driver := New(nil) + pkg := pkgs[0] + + moduleIdents, err := driver.findCaddyModuleIdents(pkg) + if err != nil { + t.Logf("Module detection failed (this documents original behavior): %v", err) + t.Log("Original implementation fails hard on mixed valid/invalid modules") + return + } + + t.Logf("Found %d modules: %v", len(moduleIdents), moduleIdents) + + // If we get here, the original implementation found some modules + for _, moduleID := range moduleIdents { + t.Logf("✓ Detected module: %s", moduleID) + } + }) + + t.Run("StrictValidationBehavior", func(t *testing.T) { + t.Log("Original implementation behavior:") + t.Log("- Requires both caddy.RegisterModule() AND CaddyModule() method") + t.Log("- Fails hard if any module in package has missing registration/implementation") + t.Log("- Only accepts static string literals in ModuleInfo.ID") + t.Log("- Cannot resolve constants or computed expressions") + t.Log("- No graceful degradation for partial failures") + }) + + t.Run("CachingBehavior", func(t *testing.T) { + t.Log("Original caching limitations:") + t.Log("- discoveredTypes map has race conditions (TODO comment confirms this)") + t.Log("- No mutex protection on concurrent access") + t.Log("- Unbounded memory growth") + t.Log("- No TTL or eviction strategy") + }) + + t.Run("SynthesisBehavior", func(t *testing.T) { + t.Log("Original synthesis limitations:") + t.Log("- Hard failure on unknown types") + t.Log("- No fallback for interfaces, channels, functions") + t.Log("- No graceful degradation") + }) +} + +// TestOriginalLimitationsDocumented documents the specific limitations we found +func TestOriginalLimitationsDocumented(t *testing.T) { + t.Run("KnownIssues", func(t *testing.T) { + t.Log("Issues identified in original implementation:") + t.Log("1. Race condition in discoveredTypes (confirmed by TODO comment)") + t.Log("2. Dynamic module IDs skipped with warning") + t.Log("3. Constant module IDs skipped with warning") + t.Log("4. Hard failure on unknown Go types") + t.Log("5. No cache eviction causing memory leaks") + t.Log("6. Strict validation prevents mixed module packages") + }) + + t.Run("ExpectedBehaviors", func(t *testing.T) { + t.Log("What works in original implementation:") + t.Log("✓ Static string module IDs like 'app.namespace.gizmo'") + t.Log("✓ Basic type synthesis for primitives and structs") + t.Log("✓ JSON tag processing") + t.Log("✓ Package loading and AST analysis") + }) +} diff --git a/concurrency_baseline_test.go b/concurrency_baseline_test.go new file mode 100644 index 0000000..b580012 --- /dev/null +++ b/concurrency_baseline_test.go @@ -0,0 +1,65 @@ +package moduledoc + +import ( + "testing" +) + +// TestConcurrencyIssuesOriginal demonstrates the race conditions in the original implementation +func TestConcurrencyIssuesOriginal(t *testing.T) { + // t.Run("RaceConditionDemo", func(t *testing.T) { + // t.Log("This test demonstrates the race condition in discoveredTypes map") + // t.Log("Run with: go test -race -run TestConcurrencyIssuesOriginal") + + // driver := New(nil) + // var wg sync.WaitGroup + + // // Multiple goroutines accessing the map concurrently + // for i := 0; i < 10; i++ { + // wg.Add(1) + // go func(id int) { + // defer wg.Done() + + // // This is what the original code does - direct map access without locking + // // This should trigger race warnings when run with -race + // key := "test.type." + string(rune('A'+id)) + + // // Read operation + // if _, exists := driver.discoveredTypes[key]; !exists { + // // Write operation + // driver.discoveredTypes[key] = &Value{ + // Type: String, + // TypeName: key, + // } + // } + // }(i) + // } + + // wg.Wait() + + // t.Logf("Completed concurrent operations on discoveredTypes map") + // t.Logf("Map now contains %d entries", len(driver.discoveredTypes)) + // t.Log("NOTE: Race detector will show warnings for the above operations") + // }) +} + +// TestMemoryLeakDemo demonstrates the unbounded cache growth +func TestMemoryLeakDemo(t *testing.T) { + t.Run("UnboundedCacheGrowth", func(t *testing.T) { + driver := New(nil) + + // Simulate what happens with many package versions + for i := 0; i < 1000; i++ { + for version := 0; version < 5; version++ { + key := "test.package." + string(rune('A'+i)) + "@v1." + string(rune('0'+version)) + ".0" + driver.discoveredTypes[key] = &Value{ + Type: String, + TypeName: key, + } + } + } + + t.Logf("Cache grew to %d entries with no eviction", len(driver.discoveredTypes)) + t.Log("Original implementation has no size limits or TTL") + t.Log("This can cause memory exhaustion in long-running processes") + }) +} diff --git a/concurrency_edge_test.go b/concurrency_edge_test.go new file mode 100644 index 0000000..c58c393 --- /dev/null +++ b/concurrency_edge_test.go @@ -0,0 +1,163 @@ +package moduledoc + +import ( + "os" + "os/exec" + "sync" + "testing" + + "golang.org/x/tools/go/packages" +) + +// localWorkspace returns a workspace anchored at the repository root so +// the local testdata package can be loaded without running 'go get'. +func localWorkspace(t *testing.T, d *Driver) workspace { + t.Helper() + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("getting working directory: %v", err) + } + return workspace{ + mu: new(sync.RWMutex), + dir: cwd, + driver: d, + goGets: map[string]struct{}{"github.com/caddyserver/moduledoc": {}}, + packagePatterns: make(map[string][]string), + parsedPackages: make(map[string]*packages.Package), + } +} + +const testdataPackagePath = "github.com/caddyserver/moduledoc/testdata" + +// runIsolated re-runs the named test in a child process with childEnv set, +// so fatal runtime errors or race reports cannot take down the whole suite. +func runIsolated(t *testing.T, testName, childEnv, failureMsg string) { + t.Helper() + cmd := exec.Command(os.Args[0], "-test.run", "^"+testName+"$", "-test.v") + cmd.Env = append(os.Environ(), childEnv+"=1") + out, err := cmd.CombinedOutput() + if err != nil { + if len(out) > 4000 { + out = out[:4000] + } + t.Fatalf("%s: %v\n%s", failureMsg, err, out) + } +} + +func TestConcurrentDiscoveredTypesAccess(t *testing.T) { + if os.Getenv("MODULEDOC_TEST_DISCOVERED_TYPES_CHILD") == "1" { + db := newMemStorage() + d := New(db) + ws := localWorkspace(t, d) + + pkgs, err := ws.getPackages(testdataPackagePath, "") + if err != nil { + t.Fatalf("loading testdata package: %v", err) + } + obj := pkgs[0].Types.Scope().Lookup("Gizmo") + if obj == nil { + t.Fatal("Gizmo type not found in testdata package") + } + + // pre-store the type so every build takes the db-hit path, + // which writes to the driver's type cache on each call + db.StoreType(testdataPackagePath, "Gizmo", "", &Value{ + Type: Struct, + TypeName: testdataPackagePath + ".Gizmo", + }) + + start := make(chan struct{}) + var wg sync.WaitGroup + for i := 0; i < 8; i++ { + wg.Add(1) + go func() { + defer wg.Done() + <-start + rb := ws.representationBuilder() + if _, err := rb.buildRepresentation(obj.Type()); err != nil { + t.Errorf("building representation: %v", err) + } + }() + } + close(start) + wg.Wait() + return + } + + if testing.Short() { + t.Skip("requires the Go toolchain") + } + runIsolated(t, "TestConcurrentDiscoveredTypesAccess", "MODULEDOC_TEST_DISCOVERED_TYPES_CHILD", + "concurrent representation building on a shared Driver must be safe") +} + +func TestConcurrentModuleTypeLoading(t *testing.T) { + if os.Getenv("MODULEDOC_TEST_MODULE_LOADING_CHILD") == "1" { + db := newMemStorage() + db.types[storageKey("example.com/pkg", "Handler", "")] = &Value{ + Type: Struct, + TypeName: "example.com/pkg.Handler", + Doc: "handler doc", + StructFields: []*StructField{ + {Key: "root", Value: &Value{Type: String}, Doc: "root doc"}, + }, + } + db.modules["http.handlers.x"] = []*Value{ + {SameAs: "example.com/pkg.Handler", Doc: "usage doc"}, + } + d := New(db) + + start := make(chan struct{}) + var wg sync.WaitGroup + for i := 0; i < 8; i++ { + wg.Add(1) + go func() { + defer wg.Done() + <-start + if _, err := d.LoadTypesByModuleID("http.handlers.x"); err != nil { + t.Errorf("loading module types: %v", err) + } + }() + } + close(start) + wg.Wait() + return + } + + runIsolated(t, "TestConcurrentModuleTypeLoading", "MODULEDOC_TEST_MODULE_LOADING_CHILD", + "concurrent module type loading on a shared Driver must be safe") +} + +func TestConcurrentGetPackages(t *testing.T) { + if os.Getenv("MODULEDOC_TEST_GET_PACKAGES_CHILD") == "1" { + d := New(newMemStorage()) + ws := localWorkspace(t, d) + + start := make(chan struct{}) + var wg sync.WaitGroup + for i := 0; i < 4; i++ { + wg.Add(1) + go func() { + defer wg.Done() + <-start + pkgs, err := ws.getPackages(testdataPackagePath, "") + if err != nil { + t.Errorf("loading packages: %v", err) + return + } + if len(pkgs) != 1 { + t.Errorf("expected 1 package, got %d", len(pkgs)) + } + }() + } + close(start) + wg.Wait() + return + } + + if testing.Short() { + t.Skip("requires the Go toolchain") + } + runIsolated(t, "TestConcurrentGetPackages", "MODULEDOC_TEST_GET_PACKAGES_CHILD", + "concurrent package loading on a shared workspace must be safe") +} diff --git a/core_functionality_test.go b/core_functionality_test.go new file mode 100644 index 0000000..30aab4d --- /dev/null +++ b/core_functionality_test.go @@ -0,0 +1,318 @@ +package moduledoc + +import ( + "go/types" + "testing" + + "golang.org/x/tools/go/packages" +) + +// TestSynthesisOriginalBehavior tests the core synthesis.go functionality +func TestSynthesisOriginalBehavior(t *testing.T) { + t.Run("BuildRepresentationBasicTypes", func(t *testing.T) { + // Test that basic Go types are handled correctly + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + // Test basic types that should work + basicTypes := map[string]types.BasicKind{ + "bool": types.Bool, + "int": types.Int, + "string": types.String, + "float64": types.Float64, + "complex128": types.Complex128, + } + + for typeName, kind := range basicTypes { + basicType := types.Typ[kind] + rep, err := rb.buildRepresentation(basicType) + if err != nil { + t.Errorf("Failed to build representation for %s: %v", typeName, err) + continue + } + + if rep == nil { + t.Errorf("Got nil representation for %s", typeName) + continue + } + + t.Logf("✓ Successfully built representation for %s: %v", typeName, rep.Type) + } + }) + + t.Run("BuildRepresentationInterfaces", func(t *testing.T) { + // Test interface handling - original returns empty Value + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + // Create a simple interface type + interfaceType := types.NewInterfaceType(nil, nil) + rep, err := rb.buildRepresentation(interfaceType) + if err != nil { + t.Errorf("Interface handling should not error, but got: %v", err) + } + + // Original implementation returns new(Value) for interfaces + if rep == nil { + t.Error("Expected non-nil representation for interface") + } else { + t.Logf("✓ Interface representation: %+v", rep) + } + }) + + t.Run("BuildRepresentationUnknownType", func(t *testing.T) { + // Test the "unknown type" error case that causes hard failures + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + // Create a channel type (not handled by original implementation) + chanType := types.NewChan(types.SendRecv, types.Typ[types.String]) + rep, err := rb.buildRepresentation(chanType) + + // Original implementation should return an error for unknown types + if err == nil { + t.Error("Expected error for channel type (unknown type), but got none") + } else { + t.Logf("✓ Correctly failed on unknown type (channel): %v", err) + } + + if rep != nil { + t.Error("Expected nil representation for unknown type") + } + }) + + t.Run("GetDepVersionCaching", func(t *testing.T) { + // Test the version caching mechanism in getDepVersion + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + // Verify the version cache is initialized + if rb.versionCache == nil { + t.Error("Version cache not initialized in representationBuilder") + } else { + t.Log("✓ Version cache initialized correctly") + } + + // This tests the internal caching but we can't easily test without + // actual packages, so we document the behavior + t.Log("Original getDepVersion behavior:") + t.Log("- Caches module versions by package path") + t.Log("- Uses go list to determine version information") + t.Log("- No cache eviction or size limits") + t.Log("- Cache grows unbounded with package usage") + }) +} + +// TestWorkspaceOriginalBehavior tests workspace.go functionality +func TestWorkspaceOriginalBehavior(t *testing.T) { + t.Run("WorkspaceCreation", func(t *testing.T) { + driver := New(nil) + + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + + // Test workspace structure + if ws.dir == "" { + t.Error("Workspace directory not set") + } + + if ws.goGets == nil { + t.Error("goGets map not initialized") + } + + if ws.packagePatterns == nil { + t.Error("packagePatterns map not initialized") + } + + if ws.parsedPackages == nil { + t.Error("parsedPackages map not initialized") + } + + t.Logf("✓ Workspace created at: %s", ws.dir) + + // Test cleanup + err = ws.Close() + if err != nil { + t.Errorf("Failed to close workspace: %v", err) + } + t.Log("✓ Workspace cleaned up successfully") + }) + + t.Run("PackageCachingBehavior", func(t *testing.T) { + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + // Test the caching mechanism described in TODOs + t.Log("Original package caching behavior:") + t.Log("- parsedPackages map stores *packages.Package by key") + t.Log("- Keys include both versioned and non-versioned forms") + t.Log("- TODO comment: 'make this cache ephemeral... not enough memory for all versions'") + t.Log("- No eviction strategy implemented") + t.Log("- packages.Visit() caches all imported packages") + + // Demonstrate unbounded cache behavior + testPkg := &packages.Package{ + ID: "test/package", + Name: "testpkg", + } + + // Simulate what happens in the actual code + ws.mu.Lock() + ws.parsedPackages["test/package"] = testPkg + ws.parsedPackages["test/package@v1.0.0"] = testPkg + ws.parsedPackages["test/package@v1.1.0"] = testPkg + ws.mu.Unlock() + + if len(ws.parsedPackages) != 3 { + t.Errorf("Expected 3 cache entries, got %d", len(ws.parsedPackages)) + } + + t.Logf("✓ Cache grew to %d entries (demonstrates unbounded growth)", len(ws.parsedPackages)) + }) + + t.Run("ConcurrentAccessPatterns", func(t *testing.T) { + // Test the mutex usage in workspace + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + t.Log("Original workspace concurrency:") + t.Log("- Has sync.RWMutex for workspace operations") + t.Log("- Protects goGets map and package operations") + t.Log("- But Driver.discoveredTypes has no protection (race condition)") + + // The workspace itself is properly protected, but the driver cache is not + testKey := "concurrent/test" + + // This should be safe (workspace level) + ws.mu.Lock() + ws.goGets[testKey] = struct{}{} + ws.mu.Unlock() + + ws.mu.RLock() + _, exists := ws.goGets[testKey] + ws.mu.RUnlock() + + if !exists { + t.Error("Concurrent access to goGets failed") + } else { + t.Log("✓ Workspace-level synchronization works correctly") + } + }) +} + +// TestStorageOriginalBehavior tests storage.go functionality +func TestStorageOriginalBehavior(t *testing.T) { + t.Run("DereferenceFunction", func(t *testing.T) { + // Test the dereference functionality + driver := New(nil) + + // Test with no SameAs (should be no-op) + val := &Value{ + Type: String, + TypeName: "test", + } + + result, err := driver.dereference(val) + if err != nil { + t.Errorf("Dereference of non-SameAs value failed: %v", err) + } + + if result != val { + t.Error("Dereference should return same value when SameAs is empty") + } + + t.Log("✓ Dereference no-op case works correctly") + }) + + t.Run("DeepDereferenceRecursion", func(t *testing.T) { + // Test the recursive deep dereferencing + driver := New(nil) + + // Create a nested structure to test recursion + val := &Value{ + Type: Struct, + StructFields: []*StructField{ + { + Key: "field1", + Value: &Value{ + Type: String, + TypeName: "string", + }, + }, + }, + } + + result, err := driver.deepDereference(val) + if err != nil { + t.Errorf("DeepDereference failed: %v", err) + } + + if result == nil { + t.Error("DeepDereference returned nil") + } + + if len(result.StructFields) != 1 { + t.Errorf("Expected 1 struct field, got %d", len(result.StructFields)) + } + + t.Log("✓ DeepDereference recursion works correctly") + }) + + t.Run("SplitLastDotFunction", func(t *testing.T) { + // Test the utility function that splits fully qualified type names + testCases := []struct { + input string + expectedLeft string + expectedRight string + }{ + {"github.com/caddyserver/caddy/v2.Config", "github.com/caddyserver/caddy/v2", "Config"}, + {"http.handlers.file_server", "http.handlers", "file_server"}, + {"http", "", "http"}, + {"", "", ""}, + } + + for _, tc := range testCases { + left, right := SplitLastDot(tc.input) + if left != tc.expectedLeft || right != tc.expectedRight { + t.Errorf("SplitLastDot(%q) = (%q, %q), expected (%q, %q)", + tc.input, left, right, tc.expectedLeft, tc.expectedRight) + } else { + t.Logf("✓ SplitLastDot(%q) = (%q, %q)", tc.input, left, right) + } + } + }) +} diff --git a/go.mod b/go.mod index 859ab35..f30c806 100644 --- a/go.mod +++ b/go.mod @@ -1,46 +1,45 @@ module github.com/caddyserver/moduledoc -go 1.19 +go 1.25 require ( - github.com/caddyserver/caddy/v2 v2.6.0 - golang.org/x/tools v0.1.12 + github.com/caddyserver/caddy/v2 v2.10.0 + golang.org/x/tools v0.31.0 ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/caddyserver/certmagic v0.17.1 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect - github.com/golang/mock v1.6.0 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/klauspost/cpuid/v2 v2.1.1 // indirect - github.com/libdns/libdns v0.2.1 // indirect - github.com/lucas-clemente/quic-go v0.29.0 // indirect - github.com/marten-seemann/qpack v0.2.1 // indirect - github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect - github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/mholt/acmez v1.0.4 // indirect - github.com/miekg/dns v1.1.50 // indirect - github.com/nxadm/tail v1.4.8 // indirect - github.com/onsi/ginkgo v1.16.5 // indirect - github.com/prometheus/client_golang v1.13.0 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect - go.uber.org/atomic v1.10.0 // indirect - go.uber.org/multierr v1.8.0 // indirect - go.uber.org/zap v1.23.0 // indirect - golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 // indirect - golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b // indirect - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect - golang.org/x/net v0.0.0-20220920152717-4a395b0a80a1 // indirect - golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect - golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect - golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect - google.golang.org/protobuf v1.28.1 // indirect - gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + github.com/caddyserver/certmagic v0.23.0 // indirect + github.com/caddyserver/zerossl v0.1.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/google/pprof v0.0.0-20231212022811-ec68065c825e // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/libdns/libdns v1.0.0-beta.1 // indirect + github.com/mholt/acmez/v3 v3.1.2 // indirect + github.com/miekg/dns v1.1.63 // indirect + github.com/onsi/ginkgo/v2 v2.13.2 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.50.1 // indirect + github.com/zeebo/blake3 v0.2.4 // indirect + go.uber.org/mock v0.5.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + go.uber.org/zap/exp v0.3.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect ) diff --git a/go.sum b/go.sum index 29de51d..7ba38ca 100644 --- a/go.sum +++ b/go.sum @@ -1,588 +1,256 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/caddyserver/caddy/v2 v2.6.0 h1:lHDynvM+sTOi9Aq4Y15b4FtkqzPB36WbUrZvVdwzTCA= -github.com/caddyserver/caddy/v2 v2.6.0/go.mod h1:YFaWZkg/wx3xW8mCF1Ydazk8dWbBBu/tx7IagU+E9io= -github.com/caddyserver/certmagic v0.17.1 h1:VrWANhQAj3brK7jAUKyN6XBHg56WsyorI/84Ilq1tCQ= -github.com/caddyserver/certmagic v0.17.1/go.mod h1:pSS2aZcdKlrTZrb2DKuRafckx20o5Fz1EdDKEB8KOQM= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/caddyserver/caddy/v2 v2.10.0 h1:fonubSaQKF1YANl8TXqGcn4IbIRUDdfAkpcsfI/vX5U= +github.com/caddyserver/caddy/v2 v2.10.0/go.mod h1:q+dgBS3xtIJJGYI2H5Nyh9+4BvhQQ9yCGmECv4Ubdjo= +github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU= +github.com/caddyserver/certmagic v0.23.0/go.mod h1:9mEZIWqqWoI+Gf+4Trh04MOVPD0tGSxtqsxg87hAIH4= +github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= +github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/google/pprof v0.0.0-20231212022811-ec68065c825e h1:bwOy7hAFd0C91URzMIEBfr6BAz29yk7Qj0cy6S7DJlU= +github.com/google/pprof v0.0.0-20231212022811-ec68065c825e/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdydm0= -github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= -github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= -github.com/lucas-clemente/quic-go v0.29.0 h1:Vw0mGTfmWqGzh4jx/kMymsIkFK6rErFVmg+t9RLrnZE= -github.com/lucas-clemente/quic-go v0.29.0/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE= -github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs= -github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= -github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM= -github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= -github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU= -github.com/marten-seemann/qtls-go1-19 v0.1.0/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/libdns/libdns v1.0.0-beta.1 h1:KIf4wLfsrEpXpZ3vmc/poM8zCATXT2klbdPe6hyOBjQ= +github.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80= -github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY= -github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= -github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= +github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= +github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs= +github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= -github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q= +github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= -go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= -go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= +github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= +go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 h1:a5Yg6ylndHHYJqIPrdq0AhvR6KTvDTAvgBtaidhEevY= -golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b h1:SCE/18RnFsLrjydh/R/s5EVvHoZprqEQUuoxK8q2Pc4= -golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220920152717-4a395b0a80a1 h1:KPlMURVqlGj7IS5s1RR3RyiiiKAgGMrh3O4A0tpOQOg= -golang.org/x/net v0.0.0-20220920152717-4a395b0a80a1/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc= -golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w= -golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b h1:NXqSWXSRUSCaFuvitrWtU169I3876zRTalMRbfd6LL0= -golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/integration_baseline_test.go b/integration_baseline_test.go new file mode 100644 index 0000000..ccef586 --- /dev/null +++ b/integration_baseline_test.go @@ -0,0 +1,213 @@ +package moduledoc + +import ( + "testing" +) + +// TestIntegrationOriginalBehavior tests the integration between workspace, synthesis, and storage +func TestIntegrationOriginalBehavior(t *testing.T) { + t.Run("FullWorkflowWithTestdata", func(t *testing.T) { + // Test the complete workflow from package loading to module detection + driver := New(nil) + + // Test LoadModulesFromImportingPackage with testdata + // Note: testdata is not a valid module path, so this will fail + // but demonstrates the integration behavior + modules, err := driver.LoadModulesFromImportingPackage("./testdata", "") + if err != nil { + t.Errorf("LoadModulesFromImportingPackage failed: %v", err) + return + } + + t.Logf("Found %d modules in testdata", len(modules)) + + for i, module := range modules { + t.Logf("Module %d: Name=%s", i, module.Name) + if module.Representation != nil { + t.Logf(" Type: %s", module.Representation.Type) + t.Logf(" TypeName: %s", module.Representation.TypeName) + } + } + + // Verify we found the expected static module + foundGizmo := false + for _, module := range modules { + if module.Name == "app.namespace.gizmo" { + foundGizmo = true + break + } + } + + if !foundGizmo { + t.Error("Expected to find 'app.namespace.gizmo' module") + } else { + t.Log("✓ Successfully found expected module") + } + }) + + t.Run("WorkspacePackageCacheIntegration", func(t *testing.T) { + // Test the integration between workspace and package caching + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + // Test that cachedPackages returns nil for non-cached packages + cached := ws.cachedPackages("non.existent/package") + if cached != nil { + t.Error("Expected nil for non-cached package") + } else { + t.Log("✓ cachedPackages correctly returns nil for non-cached packages") + } + + // Test that the cache behavior matches the TODO comments + t.Log("Original caching behavior verification:") + t.Log("- Cache stores packages.Package by string key") + t.Log("- Uses both versioned and non-versioned keys") + t.Log("- No size limits (TODO: 'not enough memory for all versions')") + t.Log("- No TTL expiration (TODO: 'should probably expire')") + }) + + t.Run("SynthesisStorageIntegration", func(t *testing.T) { + // Test the integration between synthesis and storage via dereference + driver := New(nil) + + // Test dereference with SameAs reference and nil storage - this will panic + // because dereference calls ds.db.GetTypeByName() and ds.db is nil + valWithSameAs := &Value{ + SameAs: "example.com/test.TestType@v1.0.0", + ModuleNamespace: stringPtr("test.namespace"), + ModuleInlineKey: stringPtr("type"), + } + + // Test that dereference panics with nil storage when SameAs is set + func() { + defer func() { + if r := recover(); r != nil { + t.Logf("✓ Dereference panicked as expected with nil storage: %v", r) + } + }() + _, _ = driver.dereference(valWithSameAs) + t.Error("Expected dereference to panic with nil storage") + }() + + // Test dereference with empty SameAs - this should work fine + valNoSameAs := &Value{ + Type: String, + TypeName: "string", + ModuleNamespace: stringPtr("test.namespace"), + ModuleInlineKey: stringPtr("type"), + } + + result, err := driver.dereference(valNoSameAs) + if err != nil { + t.Errorf("dereference failed with empty SameAs: %v", err) + } else if result != valNoSameAs { + t.Error("dereference should return same value when SameAs is empty") + } else { + t.Log("✓ Dereference works correctly with empty SameAs") + } + + // Test deepDereference recursion with no SameAs references + structVal := &Value{ + Type: Struct, + StructFields: []*StructField{ + { + Key: "field1", + Value: &Value{ + Type: String, + TypeName: "string", + Doc: "Field documentation", + }, + }, + }, + } + + deepResult, err := driver.deepDereference(structVal) + if err != nil { + t.Errorf("deepDereference failed: %v", err) + } else if deepResult == nil { + t.Error("deepDereference returned nil") + } else { + t.Log("✓ deepDereference completed successfully") + if len(deepResult.StructFields) > 0 && deepResult.StructFields[0].Doc != "" { + t.Logf(" Field doc preserved: %q", deepResult.StructFields[0].Doc) + } + } + }) + + t.Run("BuildRepresentationCacheIntegration", func(t *testing.T) { + // Test that buildRepresentation uses discoveredTypes cache + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + // Test the caching behavior by examining the discoveredTypes map + initialCacheSize := len(driver.discoveredTypes) + t.Logf("Initial cache size: %d", initialCacheSize) + + // The cache is accessed without locking in the original implementation + // This is the race condition we documented + t.Log("Original cache access pattern:") + t.Log("- discoveredTypes map accessed without mutex protection") + t.Log("- Race condition confirmed by TODO comment") + t.Log("- Cache grows unbounded during synthesis") + + // Demonstrate direct cache access (original behavior) + testKey := "test.example/Type@v1.0.0" + driver.discoveredTypes[testKey] = &Value{Type: String, TypeName: testKey} + + if len(driver.discoveredTypes) != initialCacheSize+1 { + t.Error("Cache size should have increased") + } else { + t.Log("✓ Cache grows as expected (without synchronization)") + } + }) +} + +// TestModuleValidationIntegration tests the strict validation behavior +func TestModuleValidationIntegration(t *testing.T) { + t.Run("OriginalStrictValidation", func(t *testing.T) { + // Create a package with mixed valid/invalid modules to test validation + validSource := ` +package test + +import "github.com/caddyserver/caddy/v2" + +func init() { + caddy.RegisterModule(new(ValidModule)) +} + +type ValidModule struct{} + +func (*ValidModule) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "app.test.valid", + New: func() caddy.Module { return new(ValidModule) }, + } +} +` + + // Test that original validation requires perfect compliance + t.Log("Original validation behavior:") + t.Log("- findCaddyModuleIdents requires exact match between registration and implementation") + t.Log("- Missing registration causes hard error") + t.Log("- Missing implementation causes hard error") + t.Log("- Non-static module IDs are skipped with warning") + t.Log("- No graceful degradation for partial compliance") + + // We can't easily test this without creating temporary files, + // but the behavior is documented by the code structure + _ = validSource + }) +} + +// Helper function to create string pointers for testing +func stringPtr(s string) *string { + return &s +} diff --git a/original_baseline_test.go b/original_baseline_test.go new file mode 100644 index 0000000..a673af6 --- /dev/null +++ b/original_baseline_test.go @@ -0,0 +1,231 @@ +// Copyright 2019 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package moduledoc + +import ( + "os" + "testing" + + "golang.org/x/tools/go/packages" +) + +// TestOriginalImplementation tests the existing functionality to establish a baseline +func TestOriginalImplementation(t *testing.T) { + t.Run("BasicModuleDetection", func(t *testing.T) { + // Test that basic module detection works with static string literals + cfg := &packages.Config{ + Dir: "testdata", + Mode: packages.NeedSyntax | + packages.NeedImports | + packages.NeedDeps | + packages.NeedTypes | + packages.NeedModule | + packages.NeedTypesInfo, + Env: append(os.Environ(), "CGO_ENABLED=0"), + } + + pkgs, err := packages.Load(cfg, "testdata") + if err != nil { + t.Fatalf("Failed to load testdata: %v", err) + } + + if len(pkgs) == 0 { + t.Fatal("No packages loaded") + } + + driver := New(nil) + pkg := pkgs[0] + + // Test findCaddyModuleIdents with the original gizmo.go + moduleIdents, err := driver.findCaddyModuleIdents(pkg) + if err != nil { + t.Errorf("findCaddyModuleIdents failed: %v", err) + } + + // Should find the static module from gizmo.go + expectedModuleID := "app.namespace.gizmo" + found := false + for _, moduleID := range moduleIdents { + if moduleID == expectedModuleID { + found = true + break + } + } + + if !found { + t.Errorf("Expected to find module ID '%s' but got: %v", expectedModuleID, moduleIdents) + } + + t.Logf("Successfully found %d modules: %v", len(moduleIdents), moduleIdents) + }) + + // t.Run("RaceConditionExposure", func(t *testing.T) { + // // This test exposes the race condition in the original implementation + // // It should pass under normal conditions but may fail under race conditions + + // driver := New(nil) + // var wg sync.WaitGroup + // errors := make(chan error, 10) + + // // Simulate concurrent access to discoveredTypes map + // for i := 0; i < 10; i++ { + // wg.Add(1) + // go func(id int) { + // defer wg.Done() + // // Access the discoveredTypes map without proper locking (original behavior) + // key := "test.type" + string(rune(id)) + + // // This demonstrates the race condition - direct map access without locking + // if _, exists := driver.discoveredTypes[key]; !exists { + // driver.discoveredTypes[key] = &Value{Type: String, TypeName: key} + // } + // }(i) + // } + + // wg.Wait() + // close(errors) + + // // Check for any panics/errors (though they may not always occur) + // for err := range errors { + // t.Errorf("Race condition error: %v", err) + // } + + // t.Logf("Race condition test completed - current implementation has no mutex protection") + // }) +} + +// TestOriginalLimitations tests the known limitations of the original implementation +func TestOriginalLimitations(t *testing.T) { + t.Run("DynamicModuleIDHandling", func(t *testing.T) { + // Create a test file with dynamic module ID that should be skipped + dynamicModuleSource := ` +package test + +import "github.com/caddyserver/caddy/v2" + +const ModulePrefix = "app.test" + +type DynamicModule struct{} + +func init() { + caddy.RegisterModule(new(DynamicModule)) +} + +func (*DynamicModule) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: caddy.ModuleID(ModulePrefix + ".dynamic"), + New: func() caddy.Module { return new(DynamicModule) }, + } +} +` + + // Write to a temporary file in testdata + tempFile := "/tmp/dynamic_test.go" + err := os.WriteFile(tempFile, []byte(dynamicModuleSource), 0o644) + if err != nil { + t.Fatalf("Failed to write temp file: %v", err) + } + defer os.Remove(tempFile) + + // This should demonstrate that the original implementation skips dynamic module IDs + // and logs a warning (we can't easily test the log output, but we can verify the behavior) + + t.Log("Original implementation should skip dynamic module IDs with a warning") + t.Log("This limitation will be fixed in the enhanced version") + }) + + t.Run("ConstantResolutionLimitation", func(t *testing.T) { + // Test that constants in module names are not resolved + constantModuleSource := ` +package test + +import "github.com/caddyserver/caddy/v2" + +const ModuleName = "app.constant.module" + +type ConstantModule struct{} + +func init() { + caddy.RegisterModule(new(ConstantModule)) +} + +func (*ConstantModule) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: ModuleName, // This should fail in original implementation + New: func() caddy.Module { return new(ConstantModule) }, + } +} +` + + // Similar test - the original implementation cannot handle this + _ = constantModuleSource // Prevent unused variable error + t.Log("Original implementation cannot resolve constants in module IDs") + t.Log("ModuleInfo{ID: CONSTANT_NAME} will be skipped with warning") + }) + + t.Run("UnknownTypeHandling", func(t *testing.T) { + // Test that unknown types cause hard failures in original implementation + // We'll test this by examining the synthesis error handling + + t.Log("Original implementation fails hard on unknown Go types") + t.Log("No graceful degradation for unrecognized types") + }) +} + +// TestOriginalCachingBehavior tests the caching mechanisms in the original implementation +func TestOriginalCachingBehavior(t *testing.T) { + t.Run("UnboundedCacheGrowth", func(t *testing.T) { + // Test that demonstrates unbounded cache growth + driver := New(nil) + + // Simulate adding many entries to the cache + for i := 0; i < 1000; i++ { + key := "test.type." + string(rune(i)) + driver.discoveredTypes[key] = &Value{ + Type: String, + TypeName: key, + } + } + + // Original implementation has no size limits or eviction + if len(driver.discoveredTypes) != 1000 { + t.Errorf("Expected 1000 cache entries, got %d", len(driver.discoveredTypes)) + } + + t.Log("Original implementation has unbounded cache growth") + t.Log("No TTL or size-based eviction implemented") + }) + + t.Run("NoPackageCacheEviction", func(t *testing.T) { + // The workspace package cache also has no eviction in original implementation + t.Log("Original workspace package cache has no eviction strategy") + t.Log("Memory leaks possible with version-specific packages") + }) +} + +// TestOriginalErrorHandling tests error handling patterns in the original code +func TestOriginalErrorHandling(t *testing.T) { + t.Run("StrictModuleValidation", func(t *testing.T) { + // Test the strict validation that requires both registration AND implementation + t.Log("Original implementation requires both caddy.RegisterModule() call AND CaddyModule() method") + t.Log("Missing either one causes hard failure, no graceful degradation") + }) + + t.Run("HardFailureOnUnknownTypes", func(t *testing.T) { + // Original synthesis.go has: return nil, fmt.Errorf("unknown type %s: %#v", ...) + t.Log("Original synthesis fails immediately on unrecognized Go types") + t.Log("No fallback representation for interfaces, channels, or complex types") + }) +} diff --git a/sourcecode_edge_test.go b/sourcecode_edge_test.go new file mode 100644 index 0000000..eb658ad --- /dev/null +++ b/sourcecode_edge_test.go @@ -0,0 +1,120 @@ +package moduledoc + +import ( + "os" + "testing" + + "golang.org/x/tools/go/packages" +) + +// loadFixturePackage loads one local testdata package with full type info. +func loadFixturePackage(t *testing.T, pattern string) *packages.Package { + t.Helper() + if testing.Short() { + t.Skip("requires the Go toolchain") + } + cfg := &packages.Config{ + Mode: packages.NeedName | + packages.NeedSyntax | + packages.NeedTypes | + packages.NeedTypesInfo | + packages.NeedImports | + packages.NeedDeps, + Env: append(os.Environ(), "CGO_ENABLED=0"), + } + pkgs, err := packages.Load(cfg, pattern) + if err != nil { + t.Fatalf("loading %s: %v", pattern, err) + } + if len(pkgs) != 1 { + t.Fatalf("expected 1 package for %s, got %d", pattern, len(pkgs)) + } + for _, e := range pkgs[0].Errors { + t.Fatalf("fixture package %s has errors: %v", pattern, e) + } + return pkgs[0] +} + +func identsByTypeName(t *testing.T, d *Driver, pkg *packages.Package) map[string]string { + t.Helper() + idents, err := d.findCaddyModuleIdents(pkg) + if err != nil { + t.Fatalf("finding module idents: %v", err) + } + byName := make(map[string]string, len(idents)) + for ident, id := range idents { + byName[ident.Name] = id + } + return byName +} + +func TestFindModuleIdentsPointerAndValuePatterns(t *testing.T) { + d := New(newMemStorage()) + + // registration via new() with pointer receiver + pkg := loadFixturePackage(t, "./testdata") + got := identsByTypeName(t, d, pkg) + if got["Gizmo"] != "app.namespace.gizmo" { + t.Errorf("expected Gizmo module, got %#v", got) + } + + // registration via composite literal with value receiver + pkg = loadFixturePackage(t, "./testdata/valuerecv") + got = identsByTypeName(t, d, pkg) + if got["Sprocket"] != "app.namespace.sprocket" { + t.Errorf("expected Sprocket module, got %#v", got) + } +} + +func TestModuleIDFromConstant(t *testing.T) { + d := New(newMemStorage()) + pkg := loadFixturePackage(t, "./testdata/constid") + got := identsByTypeName(t, d, pkg) + if got["ConstWidget"] != "app.namespace.const_widget" { + t.Errorf("module with ID declared as a package constant should be discovered, got %#v", got) + } +} + +func TestModuleIDFromConcatenation(t *testing.T) { + d := New(newMemStorage()) + pkg := loadFixturePackage(t, "./testdata/exprid") + got := identsByTypeName(t, d, pkg) + if got["ExprWidget"] != "app.namespace.expr_widget" { + t.Errorf("module with ID built from constant string concatenation should be discovered, got %#v", got) + } +} + +func TestUnregisteredModuleFailsPackage(t *testing.T) { + d := New(newMemStorage()) + pkg := loadFixturePackage(t, "./testdata/unregistered") + // one unregistered module type fails the whole package, + // including its valid sibling module (current strict contract) + if _, err := d.findCaddyModuleIdents(pkg); err == nil { + t.Error("expected error for package containing an unregistered module type") + } +} + +func TestRegistrationWithoutLocalImplementation(t *testing.T) { + d := New(newMemStorage()) + pkg := loadFixturePackage(t, "./testdata/noimpl") + if _, err := d.findCaddyModuleIdents(pkg); err == nil { + t.Error("expected error for registration whose CaddyModule method is not declared in the package") + } +} + +func TestFindModuleIdentsUnusualAST(t *testing.T) { + d := New(newMemStorage()) + pkg := loadFixturePackage(t, "./testdata/crosspkg") + + defer func() { + if r := recover(); r != nil { + t.Fatalf("registration of a qualified type from another package must not panic: %v", r) + } + }() + // either outcome is acceptable as long as it does not panic: + // an error (no local implementation) or an empty result + idents, err := d.findCaddyModuleIdents(pkg) + if err == nil && len(idents) > 0 { + t.Logf("cross-package registration produced idents: %v", idents) + } +} diff --git a/specific_functions_test.go b/specific_functions_test.go new file mode 100644 index 0000000..2bb10e3 --- /dev/null +++ b/specific_functions_test.go @@ -0,0 +1,326 @@ +package moduledoc + +import ( + "go/types" + "os" + "reflect" + "strings" + "testing" + + "golang.org/x/tools/go/packages" +) + +// TestWorkspaceSpecificFunctions tests specific functions in workspace.go +func TestWorkspaceSpecificFunctions(t *testing.T) { + t.Run("PackageKeyFunction", func(t *testing.T) { + // Test the packageKey function that creates cache keys + testCases := []struct { + name string + pkg *packages.Package + expected string + }{ + { + name: "PackageWithVersion", + pkg: &packages.Package{ + ID: "example.com/test", + Module: &packages.Module{ + Version: "v1.0.0", + }, + }, + expected: "example.com/test@v1.0.0", + }, + { + name: "PackageWithoutVersion", + pkg: &packages.Package{ + ID: "example.com/test", + }, + expected: "example.com/test", + }, + { + name: "PackageWithEmptyVersion", + pkg: &packages.Package{ + ID: "example.com/test", + Module: &packages.Module{ + Version: "", + }, + }, + expected: "example.com/test", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := packageKey(tc.pkg) + if result != tc.expected { + t.Errorf("packageKey() = %q, expected %q", result, tc.expected) + } else { + t.Logf("✓ packageKey() = %q", result) + } + }) + } + }) + + t.Run("AlreadyGotModuleFunction", func(t *testing.T) { + // Test the alreadyGotModule function + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + // Add some modules to the goGets cache + ws.goGets["github.com/caddyserver/caddy"] = struct{}{} + ws.goGets["example.com/test"] = struct{}{} + + testCases := []struct { + packagePath string + expected bool + description string + }{ + {"github.com/caddyserver/caddy/v2", true, "should find parent module"}, + {"github.com/caddyserver/caddy/v2/modules/http", true, "should find ancestor module"}, + {"example.com/test/subpackage", true, "should find parent module"}, + {"other.com/package", false, "should not find unrelated module"}, + {"", false, "should handle empty path"}, + } + + for _, tc := range testCases { + result := ws.alreadyGotModule(tc.packagePath) + if result != tc.expected { + t.Errorf("alreadyGotModule(%q) = %v, expected %v (%s)", + tc.packagePath, result, tc.expected, tc.description) + } else { + t.Logf("✓ alreadyGotModule(%q) = %v (%s)", + tc.packagePath, result, tc.description) + } + } + }) + + t.Run("RepresentationBuilderCreation", func(t *testing.T) { + // Test representationBuilder creation + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + if rb.ws.dir != ws.dir { + t.Error("RepresentationBuilder workspace not set correctly") + } + + if rb.versionCache == nil { + t.Error("RepresentationBuilder versionCache not initialized") + } + + if len(rb.versionCache) != 0 { + t.Error("RepresentationBuilder versionCache should start empty") + } + + t.Log("✓ RepresentationBuilder created correctly") + }) +} + +// TestSynthesisSpecificFunctions tests specific functions in synthesis.go +func TestSynthesisSpecificFunctions(t *testing.T) { + t.Run("GetStructFieldGodocs", func(t *testing.T) { + // Test getStructFieldGodocs with real testdata + cfg := &packages.Config{ + Dir: "testdata", + Mode: packages.NeedSyntax | + packages.NeedImports | + packages.NeedDeps | + packages.NeedTypes | + packages.NeedModule | + packages.NeedTypesInfo, + Env: append(os.Environ(), "CGO_ENABLED=0"), + } + + pkgs, err := packages.Load(cfg, "testdata") + if err != nil { + t.Logf("Package loading failed (expected if testdata has issues): %v", err) + return + } + + if len(pkgs) == 0 { + t.Skip("No packages loaded, skipping getStructFieldGodocs test") + } + + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + pkg := pkgs[0] + + // Look for the Gizmo struct type + for _, obj := range pkg.TypesInfo.Defs { + if obj != nil && obj.Name() == "Gizmo" { + if typeName, ok := obj.(*types.TypeName); ok { + namedType := typeName.Type().(*types.Named) + + // Test getStructFieldGodocs + docs, err := rb.getStructFieldGodocs(namedType) + if err != nil { + t.Errorf("getStructFieldGodocs failed: %v", err) + } else { + t.Logf("✓ getStructFieldGodocs returned %d field docs", len(docs)) + for field, doc := range docs { + t.Logf(" Field %s: %q", field, strings.TrimSpace(doc)) + } + } + break + } + } + } + }) + + t.Run("GetGodocForType", func(t *testing.T) { + // Test getGodocForType with testdata + cfg := &packages.Config{ + Dir: "testdata", + Mode: packages.NeedSyntax | + packages.NeedImports | + packages.NeedDeps | + packages.NeedTypes | + packages.NeedModule | + packages.NeedTypesInfo, + Env: append(os.Environ(), "CGO_ENABLED=0"), + } + + pkgs, err := packages.Load(cfg, "testdata") + if err != nil { + t.Logf("Package loading failed (expected if testdata has issues): %v", err) + return + } + + if len(pkgs) == 0 { + t.Skip("No packages loaded, skipping getGodocForType test") + } + + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + pkg := pkgs[0] + + // Look for the Gizmo struct type + for _, obj := range pkg.TypesInfo.Defs { + if obj != nil && obj.Name() == "Gizmo" { + if typeName, ok := obj.(*types.TypeName); ok { + namedType := typeName.Type().(*types.Named) + + // Test getGodocForType + doc, err := rb.getGodocForType(namedType) + if err != nil { + t.Errorf("getGodocForType failed: %v", err) + } else { + t.Logf("✓ getGodocForType returned: %q", strings.TrimSpace(doc)) + } + break + } + } + } + }) +} + +// TestStorageSpecificFunctions tests specific functions in storage.go +func TestStorageSpecificFunctions(t *testing.T) { + t.Run("ConfigPathPartsFunction", func(t *testing.T) { + // Test ConfigPathParts function + testCases := []struct { + input string + expected []string + }{ + {"/path/to/config", []string{"path", "to", "config"}}, + {"path/to/config", []string{"path", "to", "config"}}, + {"path/to/config/", []string{"path", "to", "config"}}, + {"/path", []string{"path"}}, + {"path", []string{"path"}}, + {"/", []string{""}}, + {"", []string{""}}, + } + + for _, tc := range testCases { + result := ConfigPathParts(tc.input) + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("ConfigPathParts(%q) = %v, expected %v", tc.input, result, tc.expected) + } else { + t.Logf("✓ ConfigPathParts(%q) = %v", tc.input, result) + } + } + }) + + t.Run("JsonNameFromTagFunction", func(t *testing.T) { + // Test jsonNameFromTag function + testCases := []struct { + tagStr string + expected string + ok bool + }{ + {`json:"name"`, "name", true}, + {`json:"name,omitempty"`, "name", true}, + {`json:",omitempty"`, "", true}, + {`json:"-"`, "", false}, + {`json:"-,omitempty"`, "", false}, + {`json:"name,required"`, "name", true}, + {`other:"value" json:"field"`, "field", true}, + {`other:"value"`, "", true}, + {"", "", true}, + } + + for _, tc := range testCases { + result, ok := jsonNameFromTag(tc.tagStr) + if result != tc.expected || ok != tc.ok { + t.Errorf("jsonNameFromTag(%q) = (%q, %v), expected (%q, %v)", + tc.tagStr, result, ok, tc.expected, tc.ok) + } else { + t.Logf("✓ jsonNameFromTag(%q) = (%q, %v)", tc.tagStr, result, ok) + } + } + }) + + t.Run("CaddyTagFieldsFunction", func(t *testing.T) { + // Test caddyTagFields function + testCases := []struct { + tagStr string + hasError bool + }{ + {`caddy:"namespace=http.handlers"`, false}, + {`caddy:"inline_key=type"`, false}, + {`json:"name" caddy:"namespace=app"`, false}, + {`other:"value"`, false}, + {"", false}, + } + + for _, tc := range testCases { + result, err := caddyTagFields(tc.tagStr) + if tc.hasError && err == nil { + t.Errorf("caddyTagFields(%q) expected error but got none", tc.tagStr) + } else if !tc.hasError && err != nil { + t.Errorf("caddyTagFields(%q) unexpected error: %v", tc.tagStr, err) + } else { + t.Logf("✓ caddyTagFields(%q) = %v (err: %v)", tc.tagStr, result, err) + } + } + }) + + t.Run("TypeUtilityFunctions", func(t *testing.T) { + // Test the type utility functions with mock data + t.Log("Testing type utility functions:") + t.Log("- fullyQualifiedTypeName: creates FQTN from types.Type") + t.Log("- typePackageAndName: splits package and type name") + t.Log("- localTypeName: extracts just the type name") + t.Log("These functions work with types.Named objects from real packages") + }) +} diff --git a/storage_edge_test.go b/storage_edge_test.go new file mode 100644 index 0000000..9f3f9d8 --- /dev/null +++ b/storage_edge_test.go @@ -0,0 +1,244 @@ +package moduledoc + +import ( + "os" + "os/exec" + "strings" + "sync" + "testing" + + "golang.org/x/tools/go/packages" +) + +// memStorage is a minimal thread-safe in-memory Storage for unit tests. +type memStorage struct { + mu sync.Mutex + types map[string]*Value + modules map[string][]*Value +} + +func newMemStorage() *memStorage { + return &memStorage{ + types: make(map[string]*Value), + modules: make(map[string][]*Value), + } +} + +func storageKey(packagePath, name, version string) string { + return packagePath + "." + name + "@" + version +} + +func (m *memStorage) GetTypeByName(packagePath, name, version string) (*Value, error) { + m.mu.Lock() + defer m.mu.Unlock() + return m.types[storageKey(packagePath, name, version)], nil +} + +func (m *memStorage) GetTypesByCaddyModuleID(caddyModuleID string) ([]*Value, error) { + m.mu.Lock() + defer m.mu.Unlock() + // return a fresh slice, as a database-backed implementation would + return append([]*Value(nil), m.modules[caddyModuleID]...), nil +} + +func (m *memStorage) StoreType(packagePath, typeName, version string, rep *Value) error { + m.mu.Lock() + defer m.mu.Unlock() + m.types[storageKey(packagePath, typeName, version)] = rep + return nil +} + +func (m *memStorage) SetCaddyModuleName(pkg *packages.Package, typeName, modName string) error { + return nil +} + +func TestDereferenceNoOpWithoutSameAs(t *testing.T) { + d := New(newMemStorage()) + val := &Value{Type: String, Doc: "a doc"} + got, err := d.dereference(val) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != val { + t.Errorf("expected same value back when SameAs is empty") + } +} + +func TestDereferenceTypeNotFound(t *testing.T) { + d := New(newMemStorage()) + + _, err := d.dereference(&Value{SameAs: "example.com/pkg.Missing"}) + if err == nil { + t.Error("expected error for unknown referenced type, got nil") + } + + // malformed reference without any dot must error, not panic + _, err = d.dereference(&Value{SameAs: "nodot"}) + if err == nil { + t.Error("expected error for malformed type reference, got nil") + } +} + +func TestDereferenceVersionedReference(t *testing.T) { + db := newMemStorage() + db.types[storageKey("example.com/pkg", "T", "v1.2.3")] = &Value{Type: String, TypeName: "example.com/pkg.T"} + d := New(db) + + got, err := d.dereference(&Value{SameAs: "example.com/pkg.T@v1.2.3"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got.Type != String { + t.Errorf("expected resolved string type, got %#v", got) + } + + // same reference without the version must not resolve to the versioned entry + if _, err := d.dereference(&Value{SameAs: "example.com/pkg.T"}); err == nil { + t.Error("expected error for unversioned reference to a versioned-only type") + } +} + +func TestDereferenceTransfersModuleInfoToInnermostElem(t *testing.T) { + db := newMemStorage() + // stored shape: array of maps of modules + db.types[storageKey("example.com/pkg", "Handlers", "")] = &Value{ + Type: Array, + TypeName: "example.com/pkg.Handlers", + Elems: &Value{ + Type: Map, + Elems: &Value{Type: Module}, + }, + } + d := New(db) + + ns := "http.handlers" + ik := "handler" + got, err := d.dereference(&Value{ + SameAs: "example.com/pkg.Handlers", + ModuleNamespace: &ns, + ModuleInlineKey: &ik, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + inner := got + for inner.Elems != nil { + inner = inner.Elems + } + if inner.ModuleNamespace == nil || *inner.ModuleNamespace != ns { + t.Errorf("module namespace not transferred to innermost element: %#v", inner) + } + if inner.ModuleInlineKey == nil || *inner.ModuleInlineKey != ik { + t.Errorf("module inline key not transferred to innermost element: %#v", inner) + } + if got.ModuleNamespace != nil && got.Elems != nil { + t.Errorf("namespace should live on the innermost element only") + } +} + +func TestDereferenceDoesNotMutateStoredType(t *testing.T) { + db := newMemStorage() + db.types[storageKey("example.com/pkg", "T", "")] = &Value{ + Type: String, + TypeName: "example.com/pkg.T", + Doc: "type doc", + } + d := New(db) + + ns := "http.handlers" + first, err := d.dereference(&Value{SameAs: "example.com/pkg.T", Doc: "field doc", ModuleNamespace: &ns}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + wantDoc := "field doc\n\ntype doc" + if first.Doc != wantDoc { + t.Errorf("first dereference doc = %q; want %q", first.Doc, wantDoc) + } + + // dereferencing the same reference again from a different context + // must not accumulate docs or leak module info between contexts + second, err := d.dereference(&Value{SameAs: "example.com/pkg.T", Doc: "field doc"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if second.Doc != wantDoc { + t.Errorf("second dereference doc = %q; want %q (docs must not accumulate)", second.Doc, wantDoc) + } + if second.ModuleNamespace != nil { + t.Errorf("second dereference has namespace %q leaked from a prior dereference", *second.ModuleNamespace) + } + if first.ModuleNamespace == nil || *first.ModuleNamespace != ns { + t.Errorf("first result's namespace was clobbered by a later dereference: %#v", first.ModuleNamespace) + } +} + +func TestDeepDereferenceResolvesNestedReferences(t *testing.T) { + db := newMemStorage() + db.types[storageKey("example.com/pkg", "Leaf", "")] = &Value{ + Type: String, + TypeName: "example.com/pkg.Leaf", + Doc: "leaf type doc", + } + db.types[storageKey("example.com/pkg", "Root", "")] = &Value{ + Type: Struct, + TypeName: "example.com/pkg.Root", + StructFields: []*StructField{ + {Key: "leaf", Value: &Value{SameAs: "example.com/pkg.Leaf"}, Doc: "field doc"}, + }, + MapKeys: &Value{SameAs: "example.com/pkg.Leaf"}, + Elems: nil, + } + d := New(db) + + got, err := d.deepDereference(&Value{SameAs: "example.com/pkg.Root"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(got.StructFields) != 1 { + t.Fatalf("expected 1 struct field, got %d", len(got.StructFields)) + } + sf := got.StructFields[0] + if sf.Value.Type != String { + t.Errorf("struct field not dereferenced: %#v", sf.Value) + } + if !strings.Contains(sf.Doc, "field doc") || !strings.Contains(sf.Doc, "leaf type doc") { + t.Errorf("field doc should combine field and type docs, got %q", sf.Doc) + } + if !strings.HasPrefix(sf.Doc, "field doc") { + t.Errorf("field doc should lead with the more specific field doc, got %q", sf.Doc) + } + if got.MapKeys == nil || got.MapKeys.Type != String { + t.Errorf("map keys not dereferenced: %#v", got.MapKeys) + } +} + +func TestDeepDereferenceCycle(t *testing.T) { + // run in a child process because unbounded recursion kills the + // whole process and would take every other test down with it + if os.Getenv("MODULEDOC_TEST_CYCLE_CHILD") == "1" { + db := newMemStorage() + db.types[storageKey("example.com/pkg", "Node", "")] = &Value{ + Type: Struct, + TypeName: "example.com/pkg.Node", + StructFields: []*StructField{ + {Key: "next", Value: &Value{SameAs: "example.com/pkg.Node"}}, + }, + } + d := New(db) + if _, err := d.deepDereference(&Value{SameAs: "example.com/pkg.Node"}); err == nil { + t.Fatal("expected error for self-referential type, got nil") + } + return + } + + cmd := exec.Command(os.Args[0], "-test.run", "^TestDeepDereferenceCycle$") + cmd.Env = append(os.Environ(), "MODULEDOC_TEST_CYCLE_CHILD=1") + out, err := cmd.CombinedOutput() + if err != nil { + if len(out) > 2000 { + out = out[:2000] + } + t.Fatalf("deep dereference of a self-referential type must fail gracefully, not crash: %v\n%s", err, out) + } +} diff --git a/synthesis_baseline_test.go b/synthesis_baseline_test.go new file mode 100644 index 0000000..5201a09 --- /dev/null +++ b/synthesis_baseline_test.go @@ -0,0 +1,578 @@ +package moduledoc + +import ( + "go/types" + "strings" + "testing" +) + +// TestSynthesisFunctionsOriginalBehavior tests the core synthesis functions +func TestSynthesisFunctionsOriginalBehavior(t *testing.T) { + t.Run("GetStructFieldGodocs", func(t *testing.T) { + // Test getStructFieldGodocs function with real Go types + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + // Test behavior with invalid/nil types - should handle gracefully + t.Log("Testing getStructFieldGodocs behavior:") + t.Log("- Function expects a *types.Named struct type") + t.Log("- Returns map[string]string of field names to godocs") + t.Log("- Requires package loading and AST parsing") + t.Log("- Original implementation may fail on non-struct types") + + // We can't easily test this without a real struct type from packages, + // but we can document the expected behavior + var nilType types.Type + if nilType != nil { + _, err := rb.getStructFieldGodocs(nilType) + t.Logf("getStructFieldGodocs with nil type: %v", err) + } + }) + + t.Run("GetGodocForType", func(t *testing.T) { + // Test getGodocForType function + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + t.Log("Testing getGodocForType behavior:") + t.Log("- Extracts godoc from TypeSpec or GenDecl AST nodes") + t.Log("- Prefers TypeSpec doc over GenDecl doc (more specific)") + t.Log("- Returns empty string if no documentation found") + t.Log("- Requires package loading and AST traversal") + + // Test with nil - should handle gracefully + var nilType types.Type + if nilType != nil { + doc, err := rb.getGodocForType(nilType) + t.Logf("getGodocForType with nil type: doc=%q, err=%v", doc, err) + } + }) + + t.Run("BuildRepresentationBasicTypes", func(t *testing.T) { + // Test buildRepresentation with basic Go types + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + // Test basic types that should work without external dependencies + basicTypes := map[string]types.Type{ + "bool": types.Typ[types.Bool], + "int": types.Typ[types.Int], + "uint": types.Typ[types.Uint], + "float64": types.Typ[types.Float64], + "complex128": types.Typ[types.Complex128], + "string": types.Typ[types.String], + } + + for name, typ := range basicTypes { + t.Run(name, func(t *testing.T) { + rep, err := rb.buildRepresentation(typ) + if err != nil { + t.Errorf("buildRepresentation failed for %s: %v", name, err) + } else if rep == nil { + t.Errorf("buildRepresentation returned nil for %s", name) + } else { + t.Logf("✓ %s -> Type: %s", name, rep.Type) + } + }) + } + }) + + t.Run("BuildRepresentationInterfaces", func(t *testing.T) { + // Test buildRepresentation with interface types + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + // Create an interface type + interfaceType := types.NewInterfaceType(nil, nil) + + rep, err := rb.buildRepresentation(interfaceType) + if err != nil { + t.Errorf("buildRepresentation failed for interface: %v", err) + } else if rep == nil { + t.Log("✓ Interface type returns nil (original behavior)") + } else { + t.Logf("✓ Interface type -> %+v", rep) + } + + t.Log("Original interface handling:") + t.Log("- *types.Interface at top level returns new(Value)") + t.Log("- *types.Interface in switch default returns nil") + t.Log("- This inconsistency is part of original behavior") + }) + + t.Run("BuildRepresentationPointers", func(t *testing.T) { + // Test buildRepresentation with pointer types + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + // Create pointer to string + stringType := types.Typ[types.String] + ptrType := types.NewPointer(stringType) + + rep, err := rb.buildRepresentation(ptrType) + if err != nil { + t.Errorf("buildRepresentation failed for pointer: %v", err) + } else if rep == nil { + t.Error("buildRepresentation returned nil for pointer") + } else if rep.Type != String { + t.Errorf("Expected String type for *string, got %s", rep.Type) + } else { + t.Log("✓ Pointer types dereference to underlying type") + } + }) + + t.Run("BuildRepresentationSlices", func(t *testing.T) { + // Test buildRepresentation with slice types + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + // Create []string slice + stringType := types.Typ[types.String] + sliceType := types.NewSlice(stringType) + + rep, err := rb.buildRepresentation(sliceType) + if err != nil { + t.Errorf("buildRepresentation failed for slice: %v", err) + } else if rep == nil { + t.Error("buildRepresentation returned nil for slice") + } else if rep.Type != Array { + t.Errorf("Expected Array type for []string, got %s", rep.Type) + } else if rep.Elems == nil { + t.Error("Expected Elems to be set for array type") + } else if rep.Elems.Type != String { + t.Errorf("Expected String element type, got %s", rep.Elems.Type) + } else { + t.Log("✓ Slice types become Array with proper element type") + } + }) + + t.Run("BuildRepresentationMaps", func(t *testing.T) { + // Test buildRepresentation with map types + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + // Create map[string]int + stringType := types.Typ[types.String] + intType := types.Typ[types.Int] + mapType := types.NewMap(stringType, intType) + + rep, err := rb.buildRepresentation(mapType) + if err != nil { + t.Errorf("buildRepresentation failed for map: %v", err) + } else if rep == nil { + t.Error("buildRepresentation returned nil for map") + } else if rep.Type != Map { + t.Errorf("Expected Map type for map[string]int, got %s", rep.Type) + } else if rep.MapKeys == nil { + t.Error("Expected MapKeys to be set") + } else if rep.MapKeys.Type != String { + t.Errorf("Expected String key type, got %s", rep.MapKeys.Type) + } else if rep.Elems == nil { + t.Error("Expected Elems to be set") + } else if rep.Elems.Type != Int { + t.Errorf("Expected Int element type, got %s", rep.Elems.Type) + } else { + t.Log("✓ Map types have proper key and element types") + } + }) + + t.Run("ModuleMapDetection", func(t *testing.T) { + // Test special case: map[string]json.RawMessage -> ModuleMap + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + t.Log("Testing ModuleMap detection:") + t.Log("- map[string]Module should become ModuleMap type") + t.Log("- This requires json.RawMessage detection as Module type") + t.Log("- Original behavior depends on exact package path matching") + + // We can't easily create a json.RawMessage type without loading packages, + // but we can document the behavior + stringType := types.Typ[types.String] + intType := types.Typ[types.Int] + regularMap := types.NewMap(stringType, intType) + + rep, err := rb.buildRepresentation(regularMap) + if err != nil { + t.Errorf("buildRepresentation failed: %v", err) + } else if rep != nil && rep.Type == ModuleMap { + t.Log("✓ Detected ModuleMap (unexpected without json.RawMessage)") + } else { + t.Log("✓ Regular map detected (expected without Module element)") + } + }) + + t.Run("UnknownTypeHandling", func(t *testing.T) { + // Test handling of unknown/unsupported types + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + t.Log("Testing unknown type handling:") + t.Log("- Original implementation has hard failures for unknown types") + t.Log("- buildRepresentation returns error for unsupported Go types") + t.Log("- No graceful degradation or fallback handling") + + // Create a channel type (unsupported) + chanType := types.NewChan(types.SendRecv, types.Typ[types.String]) + + rep, err := rb.buildRepresentation(chanType) + if err != nil { + t.Logf("✓ buildRepresentation properly fails for unsupported channel type: %v", err) + } else { + t.Errorf("Expected error for channel type, got: %+v", rep) + } + + // Create a function type (unsupported) + sig := types.NewSignature(nil, nil, nil, false) + funcType := types.NewSignature(nil, types.NewTuple(), types.NewTuple(), false) + _ = funcType // Avoid unused variable + _ = sig // Avoid unused variable + + t.Log("✓ Original implementation fails hard on unsupported types (channels, funcs, etc.)") + }) +} + +// TestRepresentationBuilderOriginalBehavior tests the representationBuilder struct +func TestRepresentationBuilderOriginalBehavior(t *testing.T) { + t.Run("VersionCacheOriginalBehavior", func(t *testing.T) { + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + t.Log("Testing version cache behavior:") + t.Log("- representationBuilder has versionCache map[string]string") + t.Log("- Cache stores module versions by package path") + t.Log("- Caches parent package versions to avoid repeated 'go list' calls") + t.Log("- Original implementation has no size limits or TTL") + + // Test cache initialization + if rb.versionCache == nil { + t.Error("Expected versionCache to be initialized") + } else { + t.Logf("✓ versionCache initialized with %d entries", len(rb.versionCache)) + } + + // Test cache access pattern + initialSize := len(rb.versionCache) + testKey := "example.com/test" + testVersion := "v1.2.3" + rb.versionCache[testKey] = testVersion + + if len(rb.versionCache) != initialSize+1 { + t.Error("Cache size should have increased") + } else { + t.Log("✓ Version cache grows as expected") + } + + if cached := rb.versionCache[testKey]; cached != testVersion { + t.Errorf("Expected cached version %s, got %s", testVersion, cached) + } else { + t.Log("✓ Version cache retrieval works") + } + }) + + t.Run("GetDepVersionOriginalBehavior", func(t *testing.T) { + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + t.Log("Testing getDepVersion behavior:") + t.Log("- Extracts package path from *types.Named") + t.Log("- Checks version cache for parent packages (hierarchical lookup)") + t.Log("- Falls back to 'go list -json' command for version info") + t.Log("- Caches results for performance (shaves off *LOT* of time)") + t.Log("- Standard library types have empty version") + + // We can't test this easily without real types, but document behavior + t.Log("Original getDepVersion limitations:") + t.Log("- Requires *types.Named input (will panic on other types)") + t.Log("- Executes shell commands ('go list') synchronously") + t.Log("- No error recovery if 'go list' fails") + t.Log("- Cache grows unbounded during operation") + }) +} + +// TestRunGoListOriginalBehavior tests the runGoList utility function +func TestRunGoListOriginalBehavior(t *testing.T) { + t.Run("RunGoListBehavior", func(t *testing.T) { + t.Log("Testing runGoList behavior:") + t.Log("- Executes 'go list -json ' in workspace directory") + t.Log("- Parses JSON output into goListOutput struct") + t.Log("- Returns detailed module and package information") + t.Log("- Includes version, path, standard library flag, dependencies") + + // Test with a package that should fail + tempDir := "/tmp" + invalidPkg := "invalid.package.name.that.does.not.exist" + + output, err := runGoList(tempDir, invalidPkg) + if err != nil { + t.Logf("✓ runGoList properly fails for invalid package: %v", err) + // Check if error includes stderr output (original behavior) + if strings.Contains(err.Error(), ">>>>>>") { + t.Log("✓ Error includes stderr output as expected") + } + } else { + t.Errorf("Expected error for invalid package, got: %+v", output) + } + + t.Log("Original runGoList characteristics:") + t.Log("- Synchronous command execution (blocking)") + t.Log("- No timeout or cancellation support") + t.Log("- Stderr included in error messages with >>>>>> markers") + t.Log("- Direct JSON unmarshaling (fails on malformed output)") + }) + + t.Run("GoListOutputStructure", func(t *testing.T) { + t.Log("Testing goListOutput structure:") + t.Log("- Contains Dir, ImportPath, Name, Root fields") + t.Log("- Module field with Path, Version, Replace sub-fields") + t.Log("- Replace field handles module replacements") + t.Log("- Time field for module timestamps") + t.Log("- GoFiles, TestGoFiles for source file lists") + t.Log("- Imports, Deps for dependency tracking") + t.Log("- Standard, GoRoot flags for stdlib detection") + + // Create empty struct to verify initialization + var output goListOutput + if output.ImportPath == "" { + t.Log("✓ goListOutput initializes with empty strings") + } + if len(output.GoFiles) == 0 { + t.Log("✓ Slice fields initialize to nil/empty") + } + if !output.Stale { + t.Log("✓ Boolean fields initialize to false") + } + }) +} + +// TestSynthesisConstantsOriginalBehavior tests constants and package-level values +func TestSynthesisConstantsOriginalBehavior(t *testing.T) { + t.Run("CaddyCorePackagePath", func(t *testing.T) { + expectedPath := "github.com/caddyserver/caddy/v2" + if caddyCorePackagePath != expectedPath { + t.Errorf("Expected caddyCorePackagePath=%q, got %q", expectedPath, caddyCorePackagePath) + } else { + t.Log("✓ caddyCorePackagePath constant is correct") + } + + t.Log("Testing caddyCorePackagePath usage:") + t.Log("- Used for identifying Caddy core types and modules") + t.Log("- Version v2 indicates Caddy 2.x compatibility") + t.Log("- Critical for module detection and validation") + }) +} + +// TestGoListOutputOriginalBehavior tests the goListOutput struct behavior +func TestGoListOutputOriginalBehavior(t *testing.T) { + t.Run("GoListOutputFields", func(t *testing.T) { + var output goListOutput + + t.Log("Testing goListOutput struct fields:") + t.Log("- Dir: directory path of the package") + t.Log("- ImportPath: import path of the package") + t.Log("- Name: package name") + t.Log("- Root: path to root of package tree") + t.Log("- Module: detailed module information with version, replace, etc.") + t.Log("- Match: patterns matched (for wildcard queries)") + t.Log("- Stale: whether package needs rebuilding") + t.Log("- GoFiles: list of .go source files") + t.Log("- Imports/Deps: dependency information") + + // Test zero values + if output.Dir == "" { + t.Log("✓ String fields initialize to empty") + } + if len(output.GoFiles) == 0 { + t.Log("✓ Slice fields initialize to nil") + } + if !output.Stale { + t.Log("✓ Bool fields initialize to false") + } + + // Test nested struct access + if output.Module.Path == "" { + t.Log("✓ Nested Module struct initializes correctly") + } + if output.Module.Replace.Path == "" { + t.Log("✓ Nested Replace struct initializes correctly") + } + }) +} + +// TestTypeUtilitiesOriginalBehavior tests utility functions for type handling +func TestTypeUtilitiesOriginalBehavior(t *testing.T) { + t.Run("TypePackageAndName", func(t *testing.T) { + t.Log("Testing typePackageAndName behavior:") + t.Log("- Extracts package path and type name from types.Type") + t.Log("- Returns empty strings for invalid/unsupported types") + t.Log("- Used throughout synthesis for FQTN construction") + + // Test with basic type (should return empty package for builtins) + stringType := types.Typ[types.String] + pkg, name := typePackageAndName(stringType) + t.Logf("string type: package=%q, name=%q", pkg, name) + + if pkg != "" || name != "string" { + t.Logf("Basic type handling: pkg=%q, name=%q", pkg, name) + } else { + t.Log("✓ Basic types have empty package, correct name") + } + }) + + t.Run("FullyQualifiedTypeName", func(t *testing.T) { + t.Log("Testing fullyQualifiedTypeName behavior:") + t.Log("- Constructs FQTN for database storage") + t.Log("- Format: package/path.TypeName") + t.Log("- Used as key in discoveredTypes cache") + + // Test with string type + stringType := types.Typ[types.String] + fqtn := fullyQualifiedTypeName(stringType) + t.Logf("string FQTN: %q", fqtn) + + if fqtn != "" { + t.Logf("✓ FQTN generated: %s", fqtn) + } else { + t.Log("FQTN empty for basic type (expected behavior)") + } + }) +} + +// TestSynthesisIntegrationOriginalBehavior tests integration between synthesis components +func TestSynthesisIntegrationOriginalBehavior(t *testing.T) { + t.Run("RepresentationBuilderIntegration", func(t *testing.T) { + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + t.Log("Testing representationBuilder integration:") + t.Log("- Created through workspace.representationBuilder()") + t.Log("- Maintains reference to workspace and driver") + t.Log("- Has version cache for performance optimization") + t.Log("- Integrates with discoveredTypes cache in driver") + + // Test workspace reference + if rb.ws.driver != driver { + t.Error("representationBuilder should reference correct driver") + } else { + t.Log("✓ Workspace reference is correct") + } + + // Test version cache initialization + if rb.versionCache == nil { + t.Error("versionCache should be initialized") + } else { + t.Log("✓ versionCache is initialized") + } + + // Use rb to avoid unused variable error + _ = rb + }) + + t.Run("CacheIntegrationPatterns", func(t *testing.T) { + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + t.Log("Testing cache integration patterns:") + t.Log("- discoveredTypes cache in driver (type representations)") + t.Log("- versionCache in representationBuilder (module versions)") + t.Log("- parsedPackages cache in workspace (loaded packages)") + t.Log("- No synchronization between caches (race conditions possible)") + + initialDriverCache := len(driver.discoveredTypes) + initialVersionCache := len(rb.versionCache) + + t.Logf("Initial cache sizes: driver=%d, version=%d", initialDriverCache, initialVersionCache) + + // Add to caches to test integration + driver.discoveredTypes["test.type"] = &Value{Type: String} + rb.versionCache["test.package"] = "v1.0.0" + + if len(driver.discoveredTypes) != initialDriverCache+1 { + t.Error("Driver cache should have grown") + } else { + t.Log("✓ Driver cache grows independently") + } + + if len(rb.versionCache) != initialVersionCache+1 { + t.Error("Version cache should have grown") + } else { + t.Log("✓ Version cache grows independently") + } + }) +} diff --git a/synthesis_edge_test.go b/synthesis_edge_test.go new file mode 100644 index 0000000..302dc05 --- /dev/null +++ b/synthesis_edge_test.go @@ -0,0 +1,154 @@ +package moduledoc + +import ( + "go/types" + "os" + "strings" + "testing" +) + +const kindsPackagePath = "github.com/caddyserver/moduledoc/testdata/kinds" + +// kindsBuilder loads the kinds fixture package and returns a builder +// plus the named type, using an in-memory storage on the driver. +func kindsBuilder(t *testing.T, typeName string) (*memStorage, representationBuilder, types.Type) { + t.Helper() + if testing.Short() { + t.Skip("requires the Go toolchain") + } + db := newMemStorage() + d := New(db) + ws := localWorkspace(t, d) + pkgs, err := ws.getPackages(kindsPackagePath, "") + if err != nil { + t.Fatalf("loading kinds package: %v", err) + } + obj := pkgs[0].Types.Scope().Lookup(typeName) + if obj == nil { + t.Fatalf("type %s not found in kinds package", typeName) + } + return db, ws.representationBuilder(), obj.Type() +} + +func structField(rep *Value, key string) *StructField { + for _, sf := range rep.StructFields { + if sf.Key == key { + return sf + } + } + return nil +} + +func TestBuildRepresentationWidgetFields(t *testing.T) { + db, rb, typ := kindsBuilder(t, "Widget") + + ref, err := rb.buildRepresentation(typ) + if err != nil { + t.Fatalf("building representation: %v", err) + } + if ref.SameAs == "" { + t.Fatalf("expected reference to stored type, got %#v", ref) + } + rep, err := db.GetTypeByName(kindsPackagePath, "Widget", "") + if err != nil || rep == nil { + t.Fatalf("stored Widget type not found: %v", err) + } + + if rep.Type != Struct { + t.Fatalf("expected struct, got %#v", rep.Type) + } + if !strings.Contains(rep.Doc, "exercises the field kinds") { + t.Errorf("type godoc missing, got %q", rep.Doc) + } + + if sf := structField(rep, "name"); sf == nil || sf.Value.Type != String { + t.Errorf("expected string field 'name', got %#v", sf) + } else if !strings.Contains(sf.Doc, "Name is the widget name") { + t.Errorf("field godoc missing, got %q", sf.Doc) + } + + for _, absent := range []string{"Hidden", "-", "unexported"} { + if sf := structField(rep, absent); sf != nil { + t.Errorf("field %q should not be documented", absent) + } + } + + if sf := structField(rep, "double_ptr"); sf == nil || sf.Value.Type != Int { + t.Errorf("pointer-to-pointer field should unwrap to int, got %#v", sf) + } + if sf := structField(rep, "numbers"); sf == nil || sf.Value.Type != Array || sf.Value.Elems == nil || sf.Value.Elems.Type != Int { + t.Errorf("expected array of int, got %#v", sf) + } + if sf := structField(rep, "lookup"); sf == nil || sf.Value.Type != Map || + sf.Value.MapKeys == nil || sf.Value.MapKeys.Type != Int || + sf.Value.Elems == nil || sf.Value.Elems.Type != String { + t.Errorf("expected map with int keys and string values, got %#v", sf) + } + if sf := structField(rep, "anything"); sf == nil || sf.Value.Type != "" { + t.Errorf("interface field should have an empty representation, got %#v", sf) + } + + if sf := structField(rep, "raw"); sf == nil || sf.Value.Type != Module { + t.Errorf("json.RawMessage field should be a module, got %#v", sf) + } else { + if sf.Value.ModuleNamespace == nil || *sf.Value.ModuleNamespace != "widget.raw" { + t.Errorf("module namespace not applied, got %#v", sf.Value.ModuleNamespace) + } + if sf.Value.ModuleInlineKey == nil || *sf.Value.ModuleInlineKey != "kind" { + t.Errorf("module inline key not applied, got %#v", sf.Value.ModuleInlineKey) + } + } + if sf := structField(rep, "raw_map"); sf == nil || sf.Value.Type != ModuleMap { + t.Errorf("map of json.RawMessage should be a module map, got %#v", sf) + } else if sf.Value.ModuleNamespace == nil || *sf.Value.ModuleNamespace != "widget.rawmap" { + t.Errorf("module map namespace not applied, got %#v", sf.Value.ModuleNamespace) + } + + if sf := structField(rep, "extra"); sf == nil || sf.Value.Type != Bool { + t.Errorf("embedded struct fields should be promoted, got %#v", sf) + } + if sf := structField(rep, "Embedded"); sf != nil { + t.Error("embedded type itself should not appear as a field") + } + + if sf := structField(rep, "nested"); sf == nil || !strings.Contains(sf.Value.SameAs, "kinds.Nested") { + t.Errorf("named nested type should be stored by reference, got %#v", sf) + } + if nested, _ := db.GetTypeByName(kindsPackagePath, "Nested", ""); nested == nil { + t.Error("nested named type should have been stored") + } + + if sf := structField(rep, "inline"); sf == nil || sf.Value.Type != Struct || structField(sf.Value, "a") == nil { + t.Errorf("inline anonymous struct should be represented with its fields, got %#v", sf) + } +} + +func TestBuildRepresentationUnsupportedFieldTypes(t *testing.T) { + db, rb, typ := kindsBuilder(t, "Tricky") + + // function- and channel-typed fields cannot appear in JSON config, + // but they must not prevent documenting the rest of the type + if _, err := rb.buildRepresentation(typ); err != nil { + t.Fatalf("expected graceful handling of func/chan fields, got error: %v", err) + } + rep, err := db.GetTypeByName(kindsPackagePath, "Tricky", "") + if err != nil || rep == nil { + t.Fatalf("stored Tricky type not found: %v", err) + } +} + +func TestBuildRepresentationRecursiveType(t *testing.T) { + if os.Getenv("MODULEDOC_TEST_RECURSIVE_TYPE_CHILD") == "1" { + _, rb, typ := kindsBuilder(t, "Node") + // success or a clean error are both fine; a crash is not + if _, err := rb.buildRepresentation(typ); err != nil { + t.Logf("recursive type produced error: %v", err) + } + return + } + if testing.Short() { + t.Skip("requires the Go toolchain") + } + runIsolated(t, "TestBuildRepresentationRecursiveType", "MODULEDOC_TEST_RECURSIVE_TYPE_CHILD", + "a self-referential struct type must not crash representation building") +} diff --git a/synthesis_real_test.go b/synthesis_real_test.go new file mode 100644 index 0000000..0b10cd0 --- /dev/null +++ b/synthesis_real_test.go @@ -0,0 +1,372 @@ +package moduledoc + +import ( + "encoding/json" + "go/types" + "os" + "path/filepath" + "strings" + "testing" +) + +// TestSynthesisRealFunctionsOriginalBehavior tests synthesis functions with actual data +func TestSynthesisRealFunctionsOriginalBehavior(t *testing.T) { + t.Run("RunGoListWithRealPackage", func(t *testing.T) { + // Create a temporary workspace directory + tempDir, err := os.MkdirTemp("", "moduledoc-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create a basic go.mod file + goModContent := `module test.example +go 1.19 +` + goModPath := filepath.Join(tempDir, "go.mod") + if err := os.WriteFile(goModPath, []byte(goModContent), 0o644); err != nil { + t.Fatalf("Failed to write go.mod: %v", err) + } + + t.Log("Testing runGoList behavior:") + t.Log("- Executes 'go list -json ' in workspace") + t.Log("- Returns goListOutput struct with module information") + t.Log("- Handles standard library packages differently") + + // Test with standard library package + output, err := runGoList(tempDir, "fmt") + if err != nil { + t.Logf("runGoList failed for fmt package (expected in temp workspace): %v", err) + } else { + t.Logf("✓ fmt package info: ImportPath=%s, Standard=%v", output.ImportPath, output.Standard) + if output.ImportPath == "fmt" { + t.Log("✓ Standard library package detected correctly") + } + if output.Standard { + t.Log("✓ Standard flag set correctly") + } + } + + // Test with non-existent package + _, err = runGoList(tempDir, "non.existent.package/v999") + if err != nil { + t.Logf("✓ runGoList properly fails for non-existent package: %v", err) + if strings.Contains(err.Error(), ">>>>>>") { + t.Log("✓ Error formatting includes stderr markers") + } + } else { + t.Error("Expected error for non-existent package") + } + }) + + t.Run("GoListOutputStructValidation", func(t *testing.T) { + t.Log("Testing goListOutput struct fields and JSON unmarshaling:") + + // Test JSON unmarshaling with sample data + jsonData := `{ + "Dir": "/test/dir", + "ImportPath": "example.com/test", + "Name": "test", + "Module": { + "Path": "example.com/test", + "Version": "v1.0.0" + }, + "Standard": false, + "GoFiles": ["main.go", "test.go"] + }` + + var output goListOutput + if err := json.Unmarshal([]byte(jsonData), &output); err != nil { + t.Errorf("Failed to unmarshal test JSON: %v", err) + } else { + t.Log("✓ JSON unmarshaling works correctly") + if output.Dir == "/test/dir" { + t.Log("✓ Dir field parsed correctly") + } + if output.Module.Version == "v1.0.0" { + t.Log("✓ Nested Module.Version field parsed correctly") + } + if len(output.GoFiles) == 2 { + t.Log("✓ GoFiles slice parsed correctly") + } + } + }) + + t.Run("RepresentationBuilderVersionCache", func(t *testing.T) { + // Test the version cache behavior with real representationBuilder + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + t.Log("Testing representationBuilder version cache:") + t.Log("- versionCache map stores package path -> version") + t.Log("- Hierarchical lookup checks parent packages") + t.Log("- Cache avoids repeated 'go list' calls") + + // Test cache initialization + if rb.versionCache == nil { + t.Error("versionCache should be initialized") + } else { + initialSize := len(rb.versionCache) + t.Logf("✓ versionCache initialized with %d entries", initialSize) + + // Test manual cache entry + testPkg := "example.com/test/subpackage" + testVersion := "v1.2.3" + rb.versionCache[testPkg] = testVersion + + // Test hierarchical lookup behavior + parts := strings.Split(testPkg, "/") + for i := len(parts); i > 0; i-- { + parent := strings.Join(parts[:i], "/") + if version, ok := rb.versionCache[parent]; ok { + t.Logf("✓ Found cached version for parent %s: %s", parent, version) + break + } + } + + if len(rb.versionCache) != initialSize+1 { + t.Error("Cache should have grown by 1") + } else { + t.Log("✓ Cache grows as expected") + } + } + }) +} + +// TestBuildRepresentationWithRealTypes tests buildRepresentation with real Go types +func TestBuildRepresentationWithRealTypes(t *testing.T) { + t.Run("BasicTypeRepresentations", func(t *testing.T) { + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + t.Log("Testing buildRepresentation with basic Go types:") + + // Test all basic types + basicTests := []struct { + name string + typ types.Type + expected Type + }{ + {"bool", types.Typ[types.Bool], Bool}, + {"int", types.Typ[types.Int], Int}, + {"int8", types.Typ[types.Int8], Int}, + {"int16", types.Typ[types.Int16], Int}, + {"int32", types.Typ[types.Int32], Int}, + {"int64", types.Typ[types.Int64], Int}, + {"uint", types.Typ[types.Uint], Uint}, + {"uint8", types.Typ[types.Uint8], Uint}, + {"uint16", types.Typ[types.Uint16], Uint}, + {"uint32", types.Typ[types.Uint32], Uint}, + {"uint64", types.Typ[types.Uint64], Uint}, + {"uintptr", types.Typ[types.Uintptr], Uint}, + {"float32", types.Typ[types.Float32], Float}, + {"float64", types.Typ[types.Float64], Float}, + {"complex64", types.Typ[types.Complex64], Complex}, + {"complex128", types.Typ[types.Complex128], Complex}, + {"string", types.Typ[types.String], String}, + } + + for _, tt := range basicTests { + t.Run(tt.name, func(t *testing.T) { + rep, err := rb.buildRepresentation(tt.typ) + if err != nil { + t.Errorf("buildRepresentation failed for %s: %v", tt.name, err) + } else if rep == nil { + t.Errorf("buildRepresentation returned nil for %s", tt.name) + } else if rep.Type != tt.expected { + t.Errorf("Expected Type %s for %s, got %s", tt.expected, tt.name, rep.Type) + } else { + t.Logf("✓ %s -> %s", tt.name, rep.Type) + } + }) + } + }) + + t.Run("ComplexTypeRepresentations", func(t *testing.T) { + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + t.Log("Testing buildRepresentation with complex Go types:") + + // Test pointer to string + stringType := types.Typ[types.String] + ptrType := types.NewPointer(stringType) + rep, err := rb.buildRepresentation(ptrType) + if err != nil { + t.Errorf("Pointer representation failed: %v", err) + } else if rep.Type != String { + t.Errorf("Expected String for *string, got %s", rep.Type) + } else { + t.Log("✓ *string -> String (pointer dereferenced)") + } + + // Test []string slice + sliceType := types.NewSlice(stringType) + rep, err = rb.buildRepresentation(sliceType) + if err != nil { + t.Errorf("Slice representation failed: %v", err) + } else if rep.Type != Array { + t.Errorf("Expected Array for []string, got %s", rep.Type) + } else if rep.Elems == nil || rep.Elems.Type != String { + t.Error("Expected String element type for []string") + } else { + t.Log("✓ []string -> Array with String elements") + } + + // Test map[string]int + intType := types.Typ[types.Int] + mapType := types.NewMap(stringType, intType) + rep, err = rb.buildRepresentation(mapType) + if err != nil { + t.Errorf("Map representation failed: %v", err) + } else if rep.Type != Map { + t.Errorf("Expected Map for map[string]int, got %s", rep.Type) + } else if rep.MapKeys == nil || rep.MapKeys.Type != String { + t.Error("Expected String key type") + } else if rep.Elems == nil || rep.Elems.Type != Int { + t.Error("Expected Int element type") + } else { + t.Log("✓ map[string]int -> Map with String keys, Int elements") + } + + // Test interface{} + emptyInterface := types.NewInterfaceType(nil, nil) + rep, err = rb.buildRepresentation(emptyInterface) + if err != nil { + t.Errorf("Interface representation failed: %v", err) + } else if rep == nil { + t.Log("✓ interface{} -> nil (original behavior)") + } else { + t.Logf("Interface representation: %+v", rep) + } + }) + + t.Run("UnsupportedTypeHandling", func(t *testing.T) { + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + rb := ws.representationBuilder() + + t.Log("Testing buildRepresentation with unsupported types:") + + // Test channel type + stringType := types.Typ[types.String] + chanType := types.NewChan(types.SendRecv, stringType) + _, err = rb.buildRepresentation(chanType) + if err != nil { + t.Logf("✓ Channel type properly rejected: %v", err) + if strings.Contains(err.Error(), "unknown type") { + t.Log("✓ Error message indicates unknown type") + } + } else { + t.Error("Expected error for channel type") + } + + // Test function type + sig := types.NewSignature(nil, types.NewTuple(), types.NewTuple(), false) + _, err = rb.buildRepresentation(sig) + if err != nil { + t.Logf("✓ Function type properly rejected: %v", err) + } else { + t.Error("Expected error for function type") + } + }) +} + +// TestCaddyCorePackagePathConstant tests the constant usage +func TestCaddyCorePackagePathConstant(t *testing.T) { + t.Log("Testing caddyCorePackagePath constant:") + + expectedPath := "github.com/caddyserver/caddy/v2" + if caddyCorePackagePath != expectedPath { + t.Errorf("Expected caddyCorePackagePath=%q, got %q", expectedPath, caddyCorePackagePath) + } else { + t.Log("✓ caddyCorePackagePath constant correct") + } + + // Test that it's used consistently in the codebase + t.Log("- Used for identifying Caddy core modules") + t.Log("- Version /v2 indicates Caddy 2.x compatibility") + t.Log("- Critical for module validation and detection") +} + +// TestSynthesisErrorHandlingOriginalBehavior tests error conditions +func TestSynthesisErrorHandlingOriginalBehavior(t *testing.T) { + t.Run("GetStructFieldGodocsErrors", func(t *testing.T) { + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + t.Log("Testing getStructFieldGodocs error handling:") + t.Log("- Expects *types.Named input (panics on others)") + t.Log("- Returns error if package loading fails") + t.Log("- Returns error if struct type not found") + + // This would panic in original implementation with non-Named types + // We document the behavior without triggering the panic + t.Log("Original limitation: panics if typ is not *types.Named") + t.Log("Original limitation: no graceful handling of package load failures") + }) + + t.Run("GetGodocForTypeErrors", func(t *testing.T) { + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + t.Log("Testing getGodocForType error handling:") + t.Log("- Expects *types.Named input (panics on others)") + t.Log("- Returns error if type not found in package") + t.Log("- Returns empty string if no documentation found") + + // Document original behavior limitations + t.Log("Original limitation: panics if typ is not *types.Named") + t.Log("Original limitation: no fallback for missing documentation") + }) + + t.Run("GetDepVersionErrors", func(t *testing.T) { + driver := New(nil) + ws, err := driver.openWorkspace() + if err != nil { + t.Fatalf("Failed to create workspace: %v", err) + } + defer ws.Close() + + t.Log("Testing getDepVersion error handling:") + t.Log("- Returns error if package path cannot be determined") + t.Log("- Returns error if 'go list' command fails") + t.Log("- No timeout or cancellation for shell commands") + + // Document the TODO comment behavior + t.Log("Original TODO: 'we could probably ignore this error, but let's see...'") + t.Log("Original limitation: synchronous shell command execution") + t.Log("Original limitation: no retry logic for transient failures") + }) +} diff --git a/testdata/constid/module.go b/testdata/constid/module.go new file mode 100644 index 0000000..631535b --- /dev/null +++ b/testdata/constid/module.go @@ -0,0 +1,23 @@ +package constid + +import "github.com/caddyserver/caddy/v2" + +const widgetModuleID = "app.namespace.const_widget" + +func init() { + caddy.RegisterModule(new(ConstWidget)) +} + +type ConstWidget struct{} + +// CaddyModule implements caddy.Module +func (*ConstWidget) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: widgetModuleID, + New: func() caddy.Module { + return new(ConstWidget) + }, + } +} + +var _ caddy.Module = (*ConstWidget)(nil) diff --git a/testdata/crosspkg/module.go b/testdata/crosspkg/module.go new file mode 100644 index 0000000..28d1a57 --- /dev/null +++ b/testdata/crosspkg/module.go @@ -0,0 +1,10 @@ +package crosspkg + +import ( + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/moduledoc/testdata/valuerecv" +) + +func init() { + caddy.RegisterModule(valuerecv.Sprocket{}) +} diff --git a/testdata/exprid/module.go b/testdata/exprid/module.go new file mode 100644 index 0000000..10c82fd --- /dev/null +++ b/testdata/exprid/module.go @@ -0,0 +1,23 @@ +package exprid + +import "github.com/caddyserver/caddy/v2" + +const idPrefix = "app.namespace." + +func init() { + caddy.RegisterModule(new(ExprWidget)) +} + +type ExprWidget struct{} + +// CaddyModule implements caddy.Module +func (*ExprWidget) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: idPrefix + "expr_widget", + New: func() caddy.Module { + return new(ExprWidget) + }, + } +} + +var _ caddy.Module = (*ExprWidget)(nil) diff --git a/testdata/gizmo.go b/testdata/gizmo.go new file mode 100644 index 0000000..a24cb40 --- /dev/null +++ b/testdata/gizmo.go @@ -0,0 +1,21 @@ +package testdata + +import "github.com/caddyserver/caddy/v2" + +func init() { + caddy.RegisterModule(new(Gizmo)) +} + +type Gizmo struct{} + +// CaddyModule implements caddy.Module +func (*Gizmo) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "app.namespace.gizmo", + New: func() caddy.Module { + return new(Gizmo) + }, + } +} + +var _ caddy.Module = (*Gizmo)(nil) diff --git a/testdata/kinds/types.go b/testdata/kinds/types.go new file mode 100644 index 0000000..bfe54fd --- /dev/null +++ b/testdata/kinds/types.go @@ -0,0 +1,52 @@ +package kinds + +import "encoding/json" + +// Widget exercises the field kinds the doc system should handle. +type Widget struct { + // Name is the widget name. + Name string `json:"name"` + + // Hidden is excluded from JSON. + Hidden string `json:"-"` + + unexported string + + DoublePtr **int `json:"double_ptr,omitempty"` + Numbers []int `json:"numbers,omitempty"` + Lookup map[int]string `json:"lookup,omitempty"` + Anything interface{} `json:"anything,omitempty"` + + Raw json.RawMessage `json:"raw,omitempty" caddy:"namespace=widget.raw inline_key=kind"` + RawMap map[string]json.RawMessage `json:"raw_map,omitempty" caddy:"namespace=widget.rawmap"` + + Embedded + + Nested Nested `json:"nested,omitempty"` + + Inline struct { + A string `json:"a"` + } `json:"inline,omitempty"` +} + +// Embedded is a mixin whose fields are promoted. +type Embedded struct { + // Extra adds extra behavior. + Extra bool `json:"extra,omitempty"` +} + +// Nested is a named nested type. +type Nested struct { + B int `json:"b"` +} + +// Node is a self-referential type. +type Node struct { + Next *Node `json:"next,omitempty"` +} + +// Tricky has field kinds with no JSON representation. +type Tricky struct { + Notify func() `json:"notify,omitempty"` + Events chan int `json:"events,omitempty"` +} diff --git a/testdata/noimpl/module.go b/testdata/noimpl/module.go new file mode 100644 index 0000000..43f0f1f --- /dev/null +++ b/testdata/noimpl/module.go @@ -0,0 +1,16 @@ +package noimpl + +import ( + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/moduledoc/testdata" +) + +func init() { + caddy.RegisterModule(new(Inherited)) +} + +// Inherited satisfies caddy.Module through embedding, so this +// package has a registration but no local CaddyModule method. +type Inherited struct { + testdata.Gizmo +} diff --git a/testdata/unregistered/module.go b/testdata/unregistered/module.go new file mode 100644 index 0000000..9c0f4af --- /dev/null +++ b/testdata/unregistered/module.go @@ -0,0 +1,32 @@ +package unregistered + +import "github.com/caddyserver/caddy/v2" + +func init() { + caddy.RegisterModule(new(Good)) +} + +type Good struct{} + +// CaddyModule implements caddy.Module +func (*Good) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "app.namespace.good", + New: func() caddy.Module { + return new(Good) + }, + } +} + +// Orphan implements caddy.Module but is never registered. +type Orphan struct{} + +// CaddyModule implements caddy.Module +func (*Orphan) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "app.namespace.orphan", + New: func() caddy.Module { + return new(Orphan) + }, + } +} diff --git a/testdata/valuerecv/module.go b/testdata/valuerecv/module.go new file mode 100644 index 0000000..06b02d7 --- /dev/null +++ b/testdata/valuerecv/module.go @@ -0,0 +1,21 @@ +package valuerecv + +import "github.com/caddyserver/caddy/v2" + +func init() { + caddy.RegisterModule(Sprocket{}) +} + +type Sprocket struct{} + +// CaddyModule implements caddy.Module +func (Sprocket) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "app.namespace.sprocket", + New: func() caddy.Module { + return Sprocket{} + }, + } +} + +var _ caddy.Module = Sprocket{} diff --git a/traverse_edge_test.go b/traverse_edge_test.go new file mode 100644 index 0000000..54ef1a5 --- /dev/null +++ b/traverse_edge_test.go @@ -0,0 +1,175 @@ +package moduledoc + +import ( + "strings" + "testing" +) + +func newConfigTree() (*memStorage, *Value) { + db := newMemStorage() + + ns := "http.handlers" + ik := "handler" + + root := &Value{ + Type: Struct, + TypeName: "example.com/pkg.Config", + StructFields: []*StructField{ + { + Key: "listen", + Value: &Value{ + Type: String, + }, + Doc: "listen doc", + }, + { + Key: "servers", + Value: &Value{ + Type: Map, + MapKeys: &Value{Type: String}, + Elems: &Value{ + Type: Struct, + TypeName: "example.com/pkg.Server", + StructFields: []*StructField{ + {Key: "port", Value: &Value{Type: Int}}, + }, + }, + }, + }, + { + Key: "handlers", + Value: &Value{ + Type: Array, + Elems: &Value{ + Type: Module, + ModuleNamespace: &ns, + ModuleInlineKey: &ik, + }, + }, + }, + }, + } + + db.modules["http.handlers.file_server"] = []*Value{ + { + Type: Struct, + TypeName: "example.com/fileserver.FileServer", + StructFields: []*StructField{ + {Key: "root", Value: &Value{Type: String}}, + }, + }, + } + + return db, root +} + +func TestTraverseTypeStartValidation(t *testing.T) { + d := New(newMemStorage()) + if _, _, err := d.TraverseType("a/b", &Value{}); err == nil { + t.Error("expected error when starting from an untyped value") + } +} + +func TestTraverseTypeEmptyPathReturnsStart(t *testing.T) { + db, root := newConfigTree() + d := New(db) + val, nearest, err := d.TraverseType("", root) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val != root || nearest != root { + t.Error("empty path should return the start value as both results") + } +} + +func TestTraverseTypeStructField(t *testing.T) { + db, root := newConfigTree() + d := New(db) + + val, nearest, err := d.TraverseType("listen", root) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val.Type != String { + t.Errorf("expected string value at path, got %#v", val) + } + if !strings.Contains(val.Doc, "listen doc") { + t.Errorf("struct field doc should be included at target, got %q", val.Doc) + } + if nearest.TypeName != "example.com/pkg.Config" { + t.Errorf("nearest type should be the containing config type, got %q", nearest.TypeName) + } +} + +func TestTraverseTypeUnknownStructField(t *testing.T) { + db, root := newConfigTree() + d := New(db) + if _, _, err := d.TraverseType("nope", root); err == nil { + t.Error("expected error for unknown struct field") + } +} + +func TestTraverseTypeThroughMapContainer(t *testing.T) { + db, root := newConfigTree() + d := New(db) + + // maps and arrays are transparent containers: the next path + // part applies to the element type, not a key or index + val, nearest, err := d.TraverseType("servers/port", root) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val.Type != Int { + t.Errorf("expected int value at path, got %#v", val) + } + if nearest.TypeName != "example.com/pkg.Server" { + t.Errorf("nearest type should be the map's element type, got %q", nearest.TypeName) + } +} + +func TestTraverseTypeModuleLookup(t *testing.T) { + db, root := newConfigTree() + d := New(db) + + val, _, err := d.TraverseType("handlers/file_server", root) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val.TypeName != "example.com/fileserver.FileServer" { + t.Errorf("expected module type at path, got %#v", val) + } + if val.ModuleInlineKey == nil || *val.ModuleInlineKey != "handler" { + t.Errorf("inline key should be set on module resolved at the final path part, got %#v", val.ModuleInlineKey) + } + + // traversal into the module's own fields + val, _, err = d.TraverseType("handlers/file_server/root", root) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val.Type != String { + t.Errorf("expected string value inside module, got %#v", val) + } +} + +func TestTraverseTypeUnknownModuleID(t *testing.T) { + db, root := newConfigTree() + d := New(db) + + defer func() { + if r := recover(); r != nil { + t.Fatalf("traversal to an unknown module ID must return an error, not panic: %v", r) + } + }() + if _, _, err := d.TraverseType("handlers/does_not_exist", root); err == nil { + t.Error("expected error for unknown module ID") + } +} + +func TestTraverseTypeNotTraversable(t *testing.T) { + db, root := newConfigTree() + d := New(db) + if _, _, err := d.TraverseType("listen/deeper", root); err == nil { + t.Error("expected error when traversing into a primitive value") + } +} diff --git a/utils_edge_test.go b/utils_edge_test.go new file mode 100644 index 0000000..e84dc4e --- /dev/null +++ b/utils_edge_test.go @@ -0,0 +1,128 @@ +package moduledoc + +import ( + "go/types" + "reflect" + "testing" +) + +func TestSplitLastDotEdgeCases(t *testing.T) { + for _, tc := range []struct { + input, before, after string + }{ + {"github.com/caddyserver/caddy/v2.Config", "github.com/caddyserver/caddy/v2", "Config"}, + {"http.handlers.file_server", "http.handlers", "file_server"}, + {"http", "", "http"}, + {"", "", ""}, + {".", "", ""}, + {"trailing.", "trailing", ""}, + {".leading", "", "leading"}, + {"a.b.c.d", "a.b.c", "d"}, + {"..", ".", ""}, + } { + before, after := SplitLastDot(tc.input) + if before != tc.before || after != tc.after { + t.Errorf("SplitLastDot(%q) = (%q, %q); want (%q, %q)", + tc.input, before, after, tc.before, tc.after) + } + } +} + +func TestConfigPathPartsEdgeCases(t *testing.T) { + for _, tc := range []struct { + input string + want []string + }{ + {"apps/http/servers", []string{"apps", "http", "servers"}}, + {"/apps/http/", []string{"apps", "http"}}, + {"apps", []string{"apps"}}, + // degenerate inputs collapse to a single empty part + {"", []string{""}}, + {"/", []string{""}}, + {"//", []string{""}}, + // interior empty segments are preserved + {"a//b", []string{"a", "", "b"}}, + } { + got := ConfigPathParts(tc.input) + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("ConfigPathParts(%q) = %#v; want %#v", tc.input, got, tc.want) + } + } +} + +func TestJSONNameFromTagEdgeCases(t *testing.T) { + for _, tc := range []struct { + tag string + name string + include bool + }{ + {`json:"listen"`, "listen", true}, + {`json:"listen,omitempty"`, "listen", true}, + {`json:"listen, omitempty"`, "listen", true}, + {`json:"-"`, "", false}, + // "-," means the field is named "-", not excluded + {`json:"-,"`, "-", true}, + // empty name with options means no explicit JSON name + {`json:",omitempty"`, "", true}, + {`json:""`, "", true}, + {``, "", true}, + {`yaml:"listen"`, "", true}, + {`json:"a" caddy:"namespace=http"`, "a", true}, + } { + name, include := jsonNameFromTag(tc.tag) + if name != tc.name || include != tc.include { + t.Errorf("jsonNameFromTag(%q) = (%q, %v); want (%q, %v)", + tc.tag, name, include, tc.name, tc.include) + } + } +} + +func TestCaddyTagFieldsEdgeCases(t *testing.T) { + fields, err := caddyTagFields(`caddy:"namespace=http.handlers inline_key=handler"`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if fields["namespace"] != "http.handlers" || fields["inline_key"] != "handler" { + t.Errorf("unexpected fields: %#v", fields) + } + + fields, err = caddyTagFields(``) + if err != nil { + t.Fatalf("empty tag should not error: %v", err) + } + if len(fields) != 0 { + t.Errorf("empty tag should yield no fields, got %#v", fields) + } + + fields, err = caddyTagFields(`json:"a"`) + if err != nil { + t.Fatalf("tag without caddy key should not error: %v", err) + } + if len(fields) != 0 { + t.Errorf("expected no caddy fields, got %#v", fields) + } +} + +func TestTypeNameHelpersOnNonNamedTypes(t *testing.T) { + basic := types.Typ[types.String] + + if got := fullyQualifiedTypeName(basic); got != "string" { + t.Errorf("fullyQualifiedTypeName(string) = %q; want %q", got, "string") + } + if pkg, name := typePackageAndName(basic); pkg != "" || name != "" { + t.Errorf("typePackageAndName(string) = (%q, %q); want empty", pkg, name) + } + if got := localTypeName(basic); got != "" { + t.Errorf("localTypeName(string) = %q; want empty", got) + } + + ptr := types.NewPointer(basic) + if got := fullyQualifiedTypeName(ptr); got != "*string" { + t.Errorf("fullyQualifiedTypeName(*string) = %q; want %q", got, "*string") + } + + slice := types.NewSlice(basic) + if got := fullyQualifiedTypeName(slice); got != "[]string" { + t.Errorf("fullyQualifiedTypeName([]string) = %q; want %q", got, "[]string") + } +} diff --git a/validation_baseline_test.go b/validation_baseline_test.go new file mode 100644 index 0000000..065f36f --- /dev/null +++ b/validation_baseline_test.go @@ -0,0 +1,218 @@ +package moduledoc + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "testing" + + "golang.org/x/tools/go/packages" +) + +// TestModuleValidationOriginal tests the strict validation logic of the original implementation +func TestModuleValidationOriginal(t *testing.T) { + t.Run("ValidModulePattern", func(t *testing.T) { + // Test the valid pattern: registration + implementation + static ID + source := ` +package test + +import "github.com/caddyserver/caddy/v2" + +func init() { + caddy.RegisterModule(new(ValidModule)) +} + +type ValidModule struct{} + +func (*ValidModule) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "app.test.valid", + New: func() caddy.Module { return new(ValidModule) }, + } +} +` + + // This should work with the original implementation + testModuleSource(t, source, true, "Valid module should be detected") + }) + + t.Run("MissingRegistration", func(t *testing.T) { + // Test module with implementation but no registration + source := ` +package test + +import "github.com/caddyserver/caddy/v2" + +type UnregisteredModule struct{} + +func (*UnregisteredModule) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "app.test.unregistered", + New: func() caddy.Module { return new(UnregisteredModule) }, + } +} +` + + // This should fail in original implementation + testModuleSource(t, source, false, "Module without registration should fail") + }) + + t.Run("MissingImplementation", func(t *testing.T) { + // Test registration without implementation + source := ` +package test + +import "github.com/caddyserver/caddy/v2" + +func init() { + caddy.RegisterModule(new(IncompleteModule)) +} + +type IncompleteModule struct{} +// Missing CaddyModule() method +` + + // This should fail in original implementation + testModuleSource(t, source, false, "Module without implementation should fail") + }) + + t.Run("NonStaticModuleID", func(t *testing.T) { + // Test computed module ID that should be skipped + source := ` +package test + +import "github.com/caddyserver/caddy/v2" + +func init() { + caddy.RegisterModule(new(ComputedModule)) +} + +type ComputedModule struct{} + +func (*ComputedModule) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: caddy.ModuleID("computed." + "dynamic"), + New: func() caddy.Module { return new(ComputedModule) }, + } +} +` + + // This should be skipped (not fail, but not detected) in original implementation + testModuleSource(t, source, false, "Computed module ID should be skipped") + }) +} + +// testModuleSource is a helper that tests module detection on source code +func testModuleSource(t *testing.T, source string, expectSuccess bool, message string) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "test.go", source, parser.ParseComments) + if err != nil { + t.Fatalf("Failed to parse source: %v", err) + } + + // Create a minimal package for testing + pkg := &packages.Package{ + Name: "test", + Syntax: []*ast.File{file}, + TypesInfo: &types.Info{ + Uses: make(map[*ast.Ident]types.Object), + Defs: make(map[*ast.Ident]types.Object), + }, + } + + driver := New(nil) + + moduleIdents, err := driver.findCaddyModuleIdents(pkg) + + if expectSuccess { + if err != nil { + t.Errorf("%s: Expected success but got error: %v", message, err) + } + if len(moduleIdents) == 0 { + t.Errorf("%s: Expected to find modules but found none", message) + } + t.Logf("%s: ✓ Found %d modules", message, len(moduleIdents)) + } else { + if err == nil && len(moduleIdents) > 0 { + t.Errorf("%s: Expected failure/skip but found modules: %v", message, moduleIdents) + } + t.Logf("%s: ✓ Correctly failed or skipped as expected", message) + } +} + +// TestSynthesisOriginalLimitations tests the type synthesis limitations +func TestSynthesisOriginalLimitations(t *testing.T) { + t.Run("UnknownTypeFailure", func(t *testing.T) { + // Test that unknown types cause hard failures + // We'll simulate this by checking the synthesis logic + + t.Log("Original synthesis.go has hard failure on unknown types:") + t.Log("default: return nil, fmt.Errorf with unknown type pattern") + + // This documents the limitation - we can't easily test the actual failure + // without creating complex type scenarios, but we document the behavior + }) + + t.Run("NoGracefulDegradation", func(t *testing.T) { + t.Log("Original implementation has no fallback for:") + t.Log("- Interface types (returns nil, nil)") + t.Log("- Channel types (not handled)") + t.Log("- Function types (not handled)") + t.Log("- Complex generic types (not handled)") + }) +} + +// TestConcurrencyIssues attempts to expose race conditions in the original implementation +// func TestConcurrencyIssues(t *testing.T) { +// t.Run("ConcurrentCacheAccess", func(t *testing.T) { +// // This test may expose the race condition mentioned in the TODO comment +// driver := New(nil) + +// // Run with race detector: go test -race +// // The original implementation should show race warnings + +// done := make(chan bool) + +// // Writer goroutine +// go func() { +// for i := 0; i < 100; i++ { +// key := "test" + string(rune(i)) +// // Direct map access without locking (original behavior) +// driver.discoveredTypes[key] = &Value{Type: String} +// } +// done <- true +// }() + +// // Reader goroutine +// go func() { +// for i := 0; i < 100; i++ { +// key := "test" + string(rune(i)) +// // Direct map access without locking (original behavior) +// _ = driver.discoveredTypes[key] +// } +// done <- true +// }() + +// // Wait for both to complete +// <-done +// <-done + +// t.Log("Concurrent access test completed") +// t.Log("Run with 'go test -race' to see race condition warnings") +// }) +// } + +// TestWorkspaceMemoryLeak tests the memory leak issues in workspace caching +func TestWorkspaceMemoryLeak(t *testing.T) { + t.Run("UnboundedPackageCache", func(t *testing.T) { + // The workspace.go has TODO comments about memory issues: + // "TODO: make this cache ephemeral (workspace-scoped), there's just not enough memory for all the versions." + + t.Log("Original workspace caching issues:") + t.Log("- No cache size limits") + t.Log("- No TTL-based eviction") + t.Log("- No cleanup of old package versions") + t.Log("- Memory grows unbounded with version-specific packages") + }) +} diff --git a/workspace_edge_test.go b/workspace_edge_test.go new file mode 100644 index 0000000..072b001 --- /dev/null +++ b/workspace_edge_test.go @@ -0,0 +1,133 @@ +package moduledoc + +import ( + "os" + "path/filepath" + "sync" + "testing" + + "golang.org/x/tools/go/packages" +) + +func tempDocsysDirs(t *testing.T) map[string]bool { + t.Helper() + matches, err := filepath.Glob(filepath.Join(os.TempDir(), "caddy_docsys_*")) + if err != nil { + t.Fatalf("globbing temp dir: %v", err) + } + set := make(map[string]bool, len(matches)) + for _, m := range matches { + set[m] = true + } + return set +} + +func TestWorkspaceCloseIdempotent(t *testing.T) { + if testing.Short() { + t.Skip("requires the Go toolchain") + } + d := New(newMemStorage()) + ws, err := d.openWorkspace() + if err != nil { + t.Fatalf("opening workspace: %v", err) + } + if err := ws.Close(); err != nil { + t.Fatalf("closing workspace: %v", err) + } + if _, err := os.Stat(ws.dir); !os.IsNotExist(err) { + t.Errorf("workspace dir should be removed after Close: %v", err) + } + if err := ws.Close(); err != nil { + t.Errorf("closing an already-closed workspace should not error: %v", err) + } +} + +func TestOpenWorkspaceInitFailureCleansTempDir(t *testing.T) { + before := tempDocsysDirs(t) + + // an empty PATH makes the 'go mod init' exec fail after TempDir succeeds + t.Setenv("PATH", t.TempDir()) + + d := New(newMemStorage()) + if _, err := d.openWorkspace(); err == nil { + t.Fatal("expected error when the go binary is unavailable") + } + + after := tempDocsysDirs(t) + for dir := range after { + if !before[dir] { + t.Errorf("temp workspace dir leaked after failed init: %s", dir) + } + } +} + +func TestGetPackagesEmptyPattern(t *testing.T) { + ws := workspace{mu: new(sync.RWMutex)} + if _, err := ws.getPackages("", ""); err == nil { + t.Error("expected error for empty package pattern") + } +} + +func TestCachedPackagesPartialMiss(t *testing.T) { + pkgA := &packages.Package{ID: "example.com/mod/a"} + ws := workspace{ + mu: new(sync.RWMutex), + packagePatterns: map[string][]string{ + "example.com/mod/...": {"example.com/mod/a", "example.com/mod/b"}, + }, + parsedPackages: map[string]*packages.Package{ + "example.com/mod/a": pkgA, + }, + } + + // exact key hit + if got := ws.cachedPackages("example.com/mod/a"); len(got) != 1 || got[0] != pkgA { + t.Errorf("expected cached package for exact key, got %#v", got) + } + // pattern expansion with one member missing must miss entirely + if got := ws.cachedPackages("example.com/mod/..."); got != nil { + t.Errorf("expected nil for partially-cached pattern, got %#v", got) + } + // unknown pattern + if got := ws.cachedPackages("example.com/other/..."); got != nil { + t.Errorf("expected nil for unknown pattern, got %#v", got) + } +} + +func TestPackageKeyVersionSuffix(t *testing.T) { + pkg := &packages.Package{ID: "example.com/mod/a"} + if got := packageKey(pkg); got != "example.com/mod/a" { + t.Errorf("packageKey without module = %q", got) + } + pkg.Module = &packages.Module{Path: "example.com/mod"} + if got := packageKey(pkg); got != "example.com/mod/a" { + t.Errorf("packageKey with unversioned module = %q", got) + } + pkg.Module.Version = "v1.2.3" + if got := packageKey(pkg); got != "example.com/mod/a@v1.2.3" { + t.Errorf("packageKey with versioned module = %q", got) + } +} + +func TestAlreadyGotModuleHierarchy(t *testing.T) { + ws := workspace{ + goGets: map[string]struct{}{"github.com/foo/bar": {}}, + } + for _, tc := range []struct { + path string + want bool + }{ + {"github.com/foo/bar", true}, + {"github.com/foo/bar/baz", true}, + {"github.com/foo/bar/baz/deep", true}, + // sibling with a shared string prefix is a different module + {"github.com/foo/barbaz", false}, + {"github.com/foo", false}, + {"github.com", false}, + {"", false}, + } { + if got := ws.alreadyGotModule(tc.path); got != tc.want { + t.Errorf("alreadyGotModule(%q) = %v; want %v", tc.path, got, tc.want) + } + } +} From 412b9a86ea3fb33ae44280d1afc0a56d26c3bc1a Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Fri, 21 Aug 2026 23:58:36 +0300 Subject: [PATCH 02/12] ci: add test job Signed-off-by: Mohammed Al Sahaf --- .github/workflows/ci.yml | 86 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0e707de --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,86 @@ +# Used as inspiration: https://github.com/caddyserver/caddy/blob/master/.github/workflows/ci.yml + +name: Tests + +on: + push: + branches: + - master + pull_request: + branches: + - master + +env: + # https://github.com/actions/setup-go/issues/491 + GOTOOLCHAIN: local + +permissions: + contents: read + +jobs: + test: + strategy: + # Default is true, cancels jobs for other platforms in the matrix if one fails + fail-fast: false + matrix: + os: + - linux + go: + - '1.27' + + include: + # Set the minimum Go patch version for the given Go minor + # Usable via ${{ matrix.GO_SEMVER }} + - go: '1.27' + GO_SEMVER: '~1.27.0' + + # Set some variables per OS, usable via ${{ matrix.VAR }} + # OS_LABEL: the VM label from GitHub Actions + - os: linux + OS_LABEL: ubuntu-latest + + runs-on: ${{ matrix.OS_LABEL }} + permissions: + contents: read + pull-requests: read + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Install Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version: ${{ matrix.GO_SEMVER }} + check-latest: true + + - name: Print Go version and environment + shell: bash + run: | + printf "Using go at: $(which go)\n" + printf "Go version: $(go version)\n" + printf "\n\nGo environment:\n\n" + go env + printf "\n\nSystem environment:\n\n" + env + printf "Git version: $(git version)\n\n" + + - name: Get dependencies + run: | + go get -v -t -d ./... + + - name: Run vet + run: | + go vet ./... + + # The test suite loads packages with the Go toolchain and sets + # CGO_ENABLED=0 itself where needed; the race detector requires + # cgo, so CGO_ENABLED is not forced off here. + - name: Run tests + run: | + go test -short -race ./... From 5911954a89f25b61b114245a7e4ef41fccc3a718 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Sat, 22 Aug 2026 00:15:03 +0300 Subject: [PATCH 03/12] tests fixes Signed-off-by: Mohammed Al Sahaf --- .github/copilot-instructions.md | 31 +++++++++++++------------ core_functionality_test.go | 15 ++++++------- integration_baseline_test.go | 40 +++++++++++++++++++++++---------- original_baseline_test.go | 4 ++-- synthesis_baseline_test.go | 5 +++-- synthesis_real_test.go | 9 ++++---- 6 files changed, 62 insertions(+), 42 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7bf936f..9321800 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -40,9 +40,13 @@ Implement `Storage` for caching - methods: `GetTypeByName`, `GetTypesByCaddyModu ## Testing Patterns ### Test Structure -- Use `testdata/` directory with minimal Caddy module examples +- Two suites: baseline tests (pass on current behavior, document limitations) and the edge-case suite (assert correct behavior; intentionally RED until the tracked bug is fixed) +- Do not "fix" a red edge-case test by weakening its assertions; fix the source bug it tracks +- Use `testdata/` fixture packages (one per directory) with minimal Caddy module examples - Load packages with full AST/types info: `packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo` - Set `CGO_ENABLED=0` in test environments to avoid "no metadata for C" errors +- Toolchain-dependent tests skip under `go test -short`; crash-prone tests (stack overflow, fatal map races) re-run themselves in a child process via `runIsolated` +- A reusable thread-safe in-memory mock Storage (`memStorage`) lives in `storage_edge_test.go` ### Example Module Pattern (see `testdata/gizmo.go`): ```go @@ -78,20 +82,19 @@ func (*ModuleName) CaddyModule() caddy.ModuleInfo { ## Recent Improvements (2025) -### Fixed Critical Issues -- **Race Conditions**: Added proper mutex locking around `discoveredTypes` map access -- **Dynamic Module IDs**: Enhanced AST analysis to handle computed module IDs like `caddy.ModuleID("prefix" + suffix)` -- **Constant Resolution**: Module IDs can now reference package-level constants -- **Graceful Degradation**: Unknown Go types fallback to string representation instead of hard failures -- **Memory Management**: Implemented LRU cache with TTL (30min) and size limits (100 entries) for package cache -- **Relaxed Validation**: Modules with only `CaddyModule()` implementation (no registration) now generate warnings instead of errors +None. An earlier version of this document claimed fixes (mutex locking, LRU cache with TTL, dynamic module ID support, graceful type fallbacks, relaxed validation) that were never implemented. Do not assume any of them exist. -### Enhanced Error Handling -- Interface types, channels, and function signatures have proper fallback representations -- Complex AST expressions (binary operations, function calls, selectors) are now supported for module IDs -- Cache eviction prevents memory leaks in long-running processes +## Known Limitations & Open Bugs + +- **Not thread-safe**: `Driver.mu` is declared but never used; `discoveredTypes` is accessed without locking (confirmed data race). `dereference` mutates Storage-owned values in place, making concurrent `LoadTypesByModuleID` racy as well. +- **Static module IDs only**: IDs computed from constants or expressions (e.g. `caddy.ModuleID("prefix" + suffix)`) are silently skipped with a warning; only `*ast.BasicLit` string literals are supported. +- **Panics on edge inputs**: `TraverseType` panics on unknown module IDs (`vals[0]` without a length check); unchecked AST type assertions panic on inputs like `caddy.RegisterModule(otherpkg.Type{})`. +- **No recursion guards**: self-referential types cause stack overflows in both `buildRepresentation` and `deepDereference`. +- **Hard failures on unknown types**: chan/func/generic fields error out the whole containing type; no fallback representation. +- **Unbounded caches**: per-workspace package caches have no TTL, size limit, or eviction (memory-growth issue only; access is properly locked). +- **Strict validation**: one incomplete module (registration without implementation, or vice versa) fails the entire package load. ## Development Notes - Requires Go toolchain installed (uses `go` commands directly) -- Thread-safe operations with proper mutex locking on all shared state -- Heavy caching for performance - workspace package cache with automatic eviction, type representation cache \ No newline at end of file +- **NOT thread-safe** — do not share a Driver or workspace across goroutines until the tracked races are fixed +- Caching is per-workspace and unbounded (no eviction); the Driver's `discoveredTypes` type cache is unsynchronized \ No newline at end of file diff --git a/core_functionality_test.go b/core_functionality_test.go index 30aab4d..dfcd360 100644 --- a/core_functionality_test.go +++ b/core_functionality_test.go @@ -87,15 +87,14 @@ func TestSynthesisOriginalBehavior(t *testing.T) { chanType := types.NewChan(types.SendRecv, types.Typ[types.String]) rep, err := rb.buildRepresentation(chanType) - // Original implementation should return an error for unknown types - if err == nil { - t.Error("Expected error for channel type (unknown type), but got none") + // today this hard-fails; a graceful fallback representation would + // also be acceptable — only a nil result without error is wrong + if err != nil { + t.Logf("✓ Current behavior: hard failure on unknown type (channel): %v", err) + } else if rep != nil { + t.Logf("✓ Graceful fallback for channel type: %+v", rep) } else { - t.Logf("✓ Correctly failed on unknown type (channel): %v", err) - } - - if rep != nil { - t.Error("Expected nil representation for unknown type") + t.Error("buildRepresentation returned neither error nor representation for channel type") } }) diff --git a/integration_baseline_test.go b/integration_baseline_test.go index ccef586..a9b70be 100644 --- a/integration_baseline_test.go +++ b/integration_baseline_test.go @@ -7,16 +7,26 @@ import ( // TestIntegrationOriginalBehavior tests the integration between workspace, synthesis, and storage func TestIntegrationOriginalBehavior(t *testing.T) { t.Run("FullWorkflowWithTestdata", func(t *testing.T) { - // Test the complete workflow from package loading to module detection - driver := New(nil) + if testing.Short() { + t.Skip("requires the Go toolchain") + } + // the public API needs a fetchable module path, so run the same + // pipeline (load → find idents → build representations) against + // the local testdata package instead + driver := New(newMemStorage()) + ws := localWorkspace(t, driver) - // Test LoadModulesFromImportingPackage with testdata - // Note: testdata is not a valid module path, so this will fail - // but demonstrates the integration behavior - modules, err := driver.LoadModulesFromImportingPackage("./testdata", "") + pkgs, err := ws.getPackages(testdataPackagePath, "") if err != nil { - t.Errorf("LoadModulesFromImportingPackage failed: %v", err) - return + t.Fatalf("loading testdata package: %v", err) + } + if len(pkgs) != 1 { + t.Fatalf("expected 1 package, got %d", len(pkgs)) + } + + modules, err := ws.representationBuilder().loadModulesFromSinglePackage(pkgs[0]) + if err != nil { + t.Fatalf("loading modules from testdata package: %v", err) } t.Logf("Found %d modules in testdata", len(modules)) @@ -82,15 +92,21 @@ func TestIntegrationOriginalBehavior(t *testing.T) { ModuleInlineKey: stringPtr("type"), } - // Test that dereference panics with nil storage when SameAs is set + // A nil Storage is invalid usage; today dereference panics on it, + // a graceful error would also be acceptable — only require that it + // does not silently succeed func() { defer func() { if r := recover(); r != nil { - t.Logf("✓ Dereference panicked as expected with nil storage: %v", r) + t.Logf("✓ Dereference panicked with nil storage: %v", r) } }() - _, _ = driver.dereference(valWithSameAs) - t.Error("Expected dereference to panic with nil storage") + _, err := driver.dereference(valWithSameAs) + if err != nil { + t.Logf("✓ Dereference returned error with nil storage: %v", err) + } else { + t.Error("dereference with nil storage must not silently succeed") + } }() // Test dereference with empty SameAs - this should work fine diff --git a/original_baseline_test.go b/original_baseline_test.go index a673af6..4888d64 100644 --- a/original_baseline_test.go +++ b/original_baseline_test.go @@ -26,7 +26,7 @@ func TestOriginalImplementation(t *testing.T) { t.Run("BasicModuleDetection", func(t *testing.T) { // Test that basic module detection works with static string literals cfg := &packages.Config{ - Dir: "testdata", + Dir: ".", Mode: packages.NeedSyntax | packages.NeedImports | packages.NeedDeps | @@ -36,7 +36,7 @@ func TestOriginalImplementation(t *testing.T) { Env: append(os.Environ(), "CGO_ENABLED=0"), } - pkgs, err := packages.Load(cfg, "testdata") + pkgs, err := packages.Load(cfg, "./testdata") if err != nil { t.Fatalf("Failed to load testdata: %v", err) } diff --git a/synthesis_baseline_test.go b/synthesis_baseline_test.go index 5201a09..8f4a20d 100644 --- a/synthesis_baseline_test.go +++ b/synthesis_baseline_test.go @@ -271,9 +271,10 @@ func TestSynthesisFunctionsOriginalBehavior(t *testing.T) { rep, err := rb.buildRepresentation(chanType) if err != nil { - t.Logf("✓ buildRepresentation properly fails for unsupported channel type: %v", err) + t.Logf("✓ Current behavior: buildRepresentation fails for channel type: %v", err) } else { - t.Errorf("Expected error for channel type, got: %+v", rep) + // a graceful fallback is an acceptable future behavior + t.Logf("✓ Graceful fallback for channel type: %+v", rep) } // Create a function type (unsupported) diff --git a/synthesis_real_test.go b/synthesis_real_test.go index 0b10cd0..097a910 100644 --- a/synthesis_real_test.go +++ b/synthesis_real_test.go @@ -276,21 +276,22 @@ func TestBuildRepresentationWithRealTypes(t *testing.T) { chanType := types.NewChan(types.SendRecv, stringType) _, err = rb.buildRepresentation(chanType) if err != nil { - t.Logf("✓ Channel type properly rejected: %v", err) + t.Logf("✓ Current behavior: channel type rejected: %v", err) if strings.Contains(err.Error(), "unknown type") { t.Log("✓ Error message indicates unknown type") } } else { - t.Error("Expected error for channel type") + // a graceful fallback is an acceptable future behavior + t.Log("✓ Graceful fallback for channel type") } // Test function type sig := types.NewSignature(nil, types.NewTuple(), types.NewTuple(), false) _, err = rb.buildRepresentation(sig) if err != nil { - t.Logf("✓ Function type properly rejected: %v", err) + t.Logf("✓ Current behavior: function type rejected: %v", err) } else { - t.Error("Expected error for function type") + t.Log("✓ Graceful fallback for function type") } }) } From 3034714b2c722da3badc4bc51aef6a1377ba3658 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Sat, 22 Aug 2026 01:32:28 +0300 Subject: [PATCH 04/12] fix identified bugs Signed-off-by: Mohammed Al Sahaf --- driver.go | 27 +++++++++++++++-- sourcecode.go | 62 ++++++++++++++++++++++++++------------ specific_functions_test.go | 3 +- storage.go | 55 +++++++++++++++++++++++++++++++-- synthesis.go | 22 +++++++++++--- utils.go | 15 ++++----- 6 files changed, 147 insertions(+), 37 deletions(-) diff --git a/driver.go b/driver.go index b7f2f90..fbd0e6f 100644 --- a/driver.go +++ b/driver.go @@ -30,7 +30,7 @@ import ( type Driver struct { db Storage - // TODO: use this, there's DEFINITELY A CONFIRMED race on discoveredTypes + // protects discoveredTypes mu sync.RWMutex // a cache of type definitions we've processed, keyed @@ -38,6 +38,26 @@ type Driver struct { discoveredTypes map[string]*Value } +// discoveredType returns the cached type for key, if present. +func (d *Driver) discoveredType(key string) (*Value, bool) { + d.mu.RLock() + defer d.mu.RUnlock() + val, ok := d.discoveredTypes[key] + return val, ok +} + +func (d *Driver) setDiscoveredType(key string, val *Value) { + d.mu.Lock() + defer d.mu.Unlock() + d.discoveredTypes[key] = val +} + +func (d *Driver) deleteDiscoveredType(key string) { + d.mu.Lock() + defer d.mu.Unlock() + delete(d.discoveredTypes, key) +} + // New constructs a new documentation system. func New(database Storage) *Driver { return &Driver{ @@ -228,7 +248,10 @@ func (d *Driver) TraverseType(path string, start *Value) (val, nearestType *Valu if err != nil { return nil, nil, fmt.Errorf("loading type for module %s: %v", caddyModuleID, err) } - val = vals[0] // TODO: support multiple values (two modules with same ID)... how? if in the middle, maybe find the one that matches; if at end...? maybe return a slice of them? + if len(vals) == 0 { + return nil, nil, fmt.Errorf("no module found with ID: %s", caddyModuleID) + } + val = vals[0].clone() // TODO: support multiple values (two modules with same ID)... how? if in the middle, maybe find the one that matches; if at end...? maybe return a slice of them? val.ModuleInlineKey = moduleInlineKey case Map, Array: diff --git a/sourcecode.go b/sourcecode.go index 9075790..7500cd9 100644 --- a/sourcecode.go +++ b/sourcecode.go @@ -17,6 +17,7 @@ package moduledoc import ( "fmt" "go/ast" + "go/constant" "go/types" "log" "strings" @@ -102,24 +103,29 @@ func (ds *Driver) findCaddyModuleIdents(pkg *packages.Package) (map[*ast.Ident]s if !ok { continue } - if kv.Key.(*ast.Ident).Name == "ID" { - // TODO: configadapters.go in the main caddy module has an unexported helper type called - // adapterModule which implements CaddyModule interface, and its ID is computed, not static: - // `caddy.ModuleID("caddy.adapters." + am.name)` - this is obviously problematic here... - // but that's also a special case that real modules should not be having - kvValueBasicLiteral, ok := kv.Value.(*ast.BasicLit) - if !ok { - log.Printf("[WARNING] CaddyModule() method in %s returns ModuleInfo with unsupported ID value (must be a static literal value); skipping: %#v", file.Name, kv.Value) - delete(caddyModRegs, currentCaddyModuleFunc.Name) - delete(caddyModImpls, currentCaddyModuleFunc.Name) - currentCaddyModuleFunc = nil - return true + keyIdent, ok := kv.Key.(*ast.Ident) + if !ok { + continue + } + if keyIdent.Name == "ID" { + // the type checker evaluates constant expressions, so + // literals, constants, and constant concatenations all work; + // only truly runtime-computed IDs remain unsupported + if tv, ok := pkg.TypesInfo.Types[kv.Value]; ok && tv.Value != nil && tv.Value.Kind() == constant.String { + caddyModName = constant.StringVal(tv.Value) + break } - - // TODO: What if the module name is pulled out to a constant? do we need to evaluate it? - rawString := kvValueBasicLiteral.Value - caddyModName = strings.Trim(rawString, `"`) - break + // no usable type info (e.g. package has errors); still + // accept plain string literals + if lit, ok := kv.Value.(*ast.BasicLit); ok { + caddyModName = strings.Trim(lit.Value, `"`) + break + } + log.Printf("[WARNING] CaddyModule() method in %s returns ModuleInfo with unsupported ID value (must be constant); skipping: %#v", file.Name, kv.Value) + delete(caddyModRegs, currentCaddyModuleFunc.Name) + delete(caddyModImpls, currentCaddyModuleFunc.Name) + currentCaddyModuleFunc = nil + return true } } @@ -214,12 +220,24 @@ func (ds *Driver) findModuleRegistration(pkg *packages.Package, fnCall *ast.Call switch val := fnCall.Args[0].(type) { case *ast.CompositeLit: // happens with `caddy.RegisterModule(Gizmo{})` - caddyModuleIdent = val.Type.(*ast.Ident) + ident, ok := val.Type.(*ast.Ident) + if !ok { + // e.g. a qualified type from another package; it will be + // documented from its home package, so skip it here + log.Printf("[WARNING] %s() argument type is not declared in this package; skipping: %#v", registerModule, val.Type) + return nil, nil + } + caddyModuleIdent = ident case *ast.CallExpr: // happens with `caddy.RegisterModule(new(Gizmo))` if funIdent, ok := val.Fun.(*ast.Ident); ok && funIdent.Name == "new" { - caddyModuleIdent = val.Args[0].(*ast.Ident) + ident, ok := val.Args[0].(*ast.Ident) + if !ok { + log.Printf("[WARNING] %s() argument type is not declared in this package; skipping: %#v", registerModule, val.Args[0]) + return nil, nil + } + caddyModuleIdent = ident } else { return nil, fmt.Errorf("unknown function call in %s(): %#v - only support new()", registerModule, val.Fun) @@ -252,7 +270,11 @@ func (ds *Driver) findModuleImpl(fnDecl *ast.FuncDecl) (*ast.Ident, error) { case *ast.Ident: receiver = val case *ast.StarExpr: - receiver = val.X.(*ast.Ident) + ident, ok := val.X.(*ast.Ident) + if !ok { + return nil, fmt.Errorf("expected identifier for pointer receiver type, but got %#v", val.X) + } + receiver = ident default: return nil, fmt.Errorf("expected identifier or pointer for receiver type, but got %#v", fnDecl.Recv.List[0].Type) } diff --git a/specific_functions_test.go b/specific_functions_test.go index 2bb10e3..5b9b551 100644 --- a/specific_functions_test.go +++ b/specific_functions_test.go @@ -272,7 +272,8 @@ func TestStorageSpecificFunctions(t *testing.T) { {`json:"name,omitempty"`, "name", true}, {`json:",omitempty"`, "", true}, {`json:"-"`, "", false}, - {`json:"-,omitempty"`, "", false}, + // per encoding/json, only exactly "-" excludes; this names the field "-" + {`json:"-,omitempty"`, "-", true}, {`json:"name,required"`, "name", true}, {`other:"value" json:"field"`, "field", true}, {`other:"value"`, "", true}, diff --git a/storage.go b/storage.go index 619cbdd..e9c410a 100644 --- a/storage.go +++ b/storage.go @@ -65,6 +65,10 @@ func (ds *Driver) dereference(val *Value) (*Value, error) { return nil, fmt.Errorf("dereference failed, type not found: %s@%s", fqtn, version) } + // work on a copy so the Storage-owned value is never mutated; + // otherwise context-specific info would bleed between dereferences + typ = typ.clone() + // transfer over the module namespace and inline key, since that // information is specific to the context in which the type appears, // thus the normalized stored type will not have that information; @@ -91,6 +95,21 @@ func (ds *Driver) dereference(val *Value) (*Value, error) { // As a result, the returned value information is completely // dereferenced and filled out. func (ds *Driver) deepDereference(val *Value) (*Value, error) { + // clone so the caller's (or Storage's) value is never mutated + return ds.deepDereferenceRec(val.clone(), make(map[string]struct{})) +} + +// deepDereferenceRec does the work of deepDereference, tracking the +// references on the current resolution path to detect cycles. +func (ds *Driver) deepDereferenceRec(val *Value, path map[string]struct{}) (*Value, error) { + if val.SameAs != "" { + if _, ok := path[val.SameAs]; ok { + return nil, fmt.Errorf("circular type reference: %s", val.SameAs) + } + path[val.SameAs] = struct{}{} + defer delete(path, val.SameAs) + } + var err error val, err = ds.dereference(val) if err != nil { @@ -99,7 +118,7 @@ func (ds *Driver) deepDereference(val *Value) (*Value, error) { // dereference all struct fields for _, sf := range val.StructFields { - sf.Value, err = ds.deepDereference(sf.Value) + sf.Value, err = ds.deepDereferenceRec(sf.Value, path) if err != nil { return nil, err } @@ -128,7 +147,7 @@ func (ds *Driver) deepDereference(val *Value) (*Value, error) { // dereference all map keys if val.MapKeys != nil { - val.MapKeys, err = ds.deepDereference(val.MapKeys) + val.MapKeys, err = ds.deepDereferenceRec(val.MapKeys, path) if err != nil { return nil, err } @@ -136,7 +155,7 @@ func (ds *Driver) deepDereference(val *Value) (*Value, error) { // dereference all map or array elements if val.Elems != nil { - val.Elems, err = ds.deepDereference(val.Elems) + val.Elems, err = ds.deepDereferenceRec(val.Elems, path) if err != nil { return nil, err } @@ -151,3 +170,33 @@ func (ds *Driver) getTypeByFullName(fqtn, version string) (*Value, error) { pkgName, typeName := SplitLastDot(fqtn) return ds.db.GetTypeByName(pkgName, typeName, version) } + +// clone returns a deep copy of v, so callers can freely +// mutate the copy without affecting Storage-owned values. +func (v *Value) clone() *Value { + if v == nil { + return nil + } + c := *v + c.MapKeys = v.MapKeys.clone() + c.Elems = v.Elems.clone() + if v.StructFields != nil { + c.StructFields = make([]*StructField, len(v.StructFields)) + for i, sf := range v.StructFields { + c.StructFields[i] = &StructField{ + Key: sf.Key, + Value: sf.Value.clone(), + Doc: sf.Doc, + } + } + } + if v.ModuleNamespace != nil { + ns := *v.ModuleNamespace + c.ModuleNamespace = &ns + } + if v.ModuleInlineKey != nil { + ik := *v.ModuleInlineKey + c.ModuleInlineKey = &ik + } + return &c +} diff --git a/synthesis.go b/synthesis.go index 867bc64..06266a0 100644 --- a/synthesis.go +++ b/synthesis.go @@ -210,7 +210,7 @@ func (rb representationBuilder) buildRepresentation(caddyModuleType types.Type) if typeVersion != "" { sameAs += "@" + typeVersion } - if _, ok := rb.ws.driver.discoveredTypes[sameAs]; ok { + if _, ok := rb.ws.driver.discoveredType(sameAs); ok { return &Value{SameAs: sameAs}, nil } @@ -221,7 +221,7 @@ func (rb representationBuilder) buildRepresentation(caddyModuleType types.Type) return nil, err } if discoveredType != nil { - rb.ws.driver.discoveredTypes[sameAs] = discoveredType + rb.ws.driver.setDiscoveredType(sameAs, discoveredType) return &Value{SameAs: discoveredType.TypeName}, nil } @@ -230,6 +230,16 @@ func (rb representationBuilder) buildRepresentation(caddyModuleType types.Type) return &Value{Type: Module}, nil } + // pre-register the type so self-referential fields resolve to a + // reference instead of recursing forever; removed again on failure + rb.ws.driver.setDiscoveredType(sameAs, new(Value)) + stored := false + defer func() { + if !stored { + rb.ws.driver.deleteDiscoveredType(sameAs) + } + }() + // otherwise, if this type is new, store it in the DB switch utyp := typ.Underlying().(type) { case *types.Struct: @@ -312,7 +322,8 @@ func (rb representationBuilder) buildRepresentation(caddyModuleType types.Type) rep.TypeName = fullTypeName // remember this type so we don't have to re-assemble it all later - rb.ws.driver.discoveredTypes[sameAs] = rep + rb.ws.driver.setDiscoveredType(sameAs, rep) + stored = true err = rb.ws.driver.db.StoreType(packagePath, typeName, typeVersion, rep) if err != nil { return nil, err @@ -389,7 +400,10 @@ func (rb representationBuilder) buildRepresentation(caddyModuleType types.Type) return nil, nil default: - return nil, fmt.Errorf("unknown type %s: %#v", caddyModuleType.String(), caddyModuleType) + // types with no JSON representation (chan, func, generics, ...) + // must not fail the whole containing type; represent them like + // interfaces, as an empty value + return new(Value), nil } } diff --git a/utils.go b/utils.go index 85196db..57822d5 100644 --- a/utils.go +++ b/utils.go @@ -26,10 +26,9 @@ import ( // If there is no dot, then before will be empty string and after // will be the input. Examples: // -// "github.com/caddyserver/caddy/v2.Config" => ("github.com/caddyserver/caddy/v2", "Config") -// "http.handlers.file_server" => ("http.handlers", "file_server") -// "http" => ("", "http") -// +// "github.com/caddyserver/caddy/v2.Config" => ("github.com/caddyserver/caddy/v2", "Config") +// "http.handlers.file_server" => ("http.handlers", "file_server") +// "http" => ("", "http") func SplitLastDot(input string) (before, after string) { lastDot := strings.LastIndex(input, ".") if lastDot < 0 { @@ -52,12 +51,14 @@ func ConfigPathParts(configPath string) []string { // by the encoding/json package), then false is returned. func jsonNameFromTag(tagStr string) (string, bool) { jsonName := reflect.StructTag(tagStr).Get("json") - if commaIdx := strings.Index(jsonName, ","); commaIdx > 0 { - jsonName = strings.TrimSpace(jsonName[:commaIdx]) - } + // per encoding/json, only a tag of exactly "-" excludes the field; + // "-," names the field "-" if jsonName == "-" { return "", false } + if commaIdx := strings.Index(jsonName, ","); commaIdx >= 0 { + jsonName = strings.TrimSpace(jsonName[:commaIdx]) + } return jsonName, true } From 38e557ab66a6e169fff955b4982a977a9fb0a14e Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Sun, 6 Sep 2026 16:02:29 +0300 Subject: [PATCH 05/12] tighten copilot instructions Signed-off-by: Mohammed Al Sahaf --- .github/copilot-instructions.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9321800..05efdab 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -40,8 +40,8 @@ Implement `Storage` for caching - methods: `GetTypeByName`, `GetTypesByCaddyModu ## Testing Patterns ### Test Structure -- Two suites: baseline tests (pass on current behavior, document limitations) and the edge-case suite (assert correct behavior; intentionally RED until the tracked bug is fixed) -- Do not "fix" a red edge-case test by weakening its assertions; fix the source bug it tracks +- Two suites: baseline tests (document behavior and limitations) and the edge-case suite (assert correct behavior) +- Do not weaken an edge-case test's assertions to make it pass; fix the source bug instead - Use `testdata/` fixture packages (one per directory) with minimal Caddy module examples - Load packages with full AST/types info: `packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo` - Set `CGO_ENABLED=0` in test environments to avoid "no metadata for C" errors @@ -86,15 +86,14 @@ None. An earlier version of this document claimed fixes (mutex locking, LRU cach ## Known Limitations & Open Bugs -- **Not thread-safe**: `Driver.mu` is declared but never used; `discoveredTypes` is accessed without locking (confirmed data race). `dereference` mutates Storage-owned values in place, making concurrent `LoadTypesByModuleID` racy as well. -- **Static module IDs only**: IDs computed from constants or expressions (e.g. `caddy.ModuleID("prefix" + suffix)`) are silently skipped with a warning; only `*ast.BasicLit` string literals are supported. -- **Panics on edge inputs**: `TraverseType` panics on unknown module IDs (`vals[0]` without a length check); unchecked AST type assertions panic on inputs like `caddy.RegisterModule(otherpkg.Type{})`. -- **No recursion guards**: self-referential types cause stack overflows in both `buildRepresentation` and `deepDereference`. -- **Hard failures on unknown types**: chan/func/generic fields error out the whole containing type; no fallback representation. +Most bugs found by the 2026-08 edge-case suite were fixed on 2026-08-22 (mutex-protected `discoveredTypes`, no-panic `TraverseType`, constant module ID evaluation, checked AST assertions, cycle guards, non-mutating `dereference` via `Value.clone()`, empty-value fallback for chan/func/generic fields). Remaining limitations: + +- **Runtime-computed module IDs**: only constant expressions are supported (evaluated via `go/types`); IDs computed at runtime are skipped with a warning. +- **Recursive types in deep dereference**: `deepDereference` detects `SameAs` cycles and returns a `circular type reference` error rather than resolving them. - **Unbounded caches**: per-workspace package caches have no TTL, size limit, or eviction (memory-growth issue only; access is properly locked). - **Strict validation**: one incomplete module (registration without implementation, or vice versa) fails the entire package load. ## Development Notes - Requires Go toolchain installed (uses `go` commands directly) -- **NOT thread-safe** — do not share a Driver or workspace across goroutines until the tracked races are fixed -- Caching is per-workspace and unbounded (no eviction); the Driver's `discoveredTypes` type cache is unsynchronized \ No newline at end of file +- Driver is safe for concurrent use: `discoveredTypes` is mutex-protected and `dereference` works on deep copies (`Value.clone()`) +- Caching is per-workspace and unbounded (no eviction) \ No newline at end of file From d7633f9fd50abceed953a87362e5db1278ecfb0b Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Wed, 9 Sep 2026 16:22:51 +0300 Subject: [PATCH 06/12] correct tests to have real assertion of behavior Signed-off-by: Mohammed Al Sahaf --- baseline_behavior_test.go | 89 ++++----- concurrency_baseline_test.go | 49 +---- core_functionality_test.go | 40 ++-- integration_baseline_test.go | 49 ++--- original_baseline_test.go | 147 ++------------ specific_functions_test.go | 21 +- synthesis_baseline_test.go | 371 +++++------------------------------ synthesis_real_test.go | 120 +++++------ validation_baseline_test.go | 76 ------- 9 files changed, 213 insertions(+), 749 deletions(-) diff --git a/baseline_behavior_test.go b/baseline_behavior_test.go index 2a612fc..ba755e5 100644 --- a/baseline_behavior_test.go +++ b/baseline_behavior_test.go @@ -12,7 +12,7 @@ func TestOriginalBehaviorBaseline(t *testing.T) { t.Run("ValidStaticModule", func(t *testing.T) { // Test only the gizmo.go file which should work cfg := &packages.Config{ - Dir: "testdata", + Dir: ".", Mode: packages.NeedSyntax | packages.NeedImports | packages.NeedDeps | @@ -22,16 +22,12 @@ func TestOriginalBehaviorBaseline(t *testing.T) { Env: append(os.Environ(), "CGO_ENABLED=0"), } - // Load only the gizmo.go file - pkgs, err := packages.Load(cfg, ".") + pkgs, err := packages.Load(cfg, "./testdata") if err != nil { - t.Logf("Package loading failed (this is expected if testdata has validation errors): %v", err) - return + t.Fatalf("loading testdata package: %v", err) } - if len(pkgs) == 0 { - t.Log("No packages loaded - this may be expected if testdata has mixed valid/invalid modules") - return + t.Fatal("no packages loaded") } driver := New(nil) @@ -39,61 +35,50 @@ func TestOriginalBehaviorBaseline(t *testing.T) { moduleIdents, err := driver.findCaddyModuleIdents(pkg) if err != nil { - t.Logf("Module detection failed (this documents original behavior): %v", err) - t.Log("Original implementation fails hard on mixed valid/invalid modules") - return + t.Fatalf("finding module idents: %v", err) } - t.Logf("Found %d modules: %v", len(moduleIdents), moduleIdents) - - // If we get here, the original implementation found some modules + found := false for _, moduleID := range moduleIdents { - t.Logf("✓ Detected module: %s", moduleID) + if moduleID == "app.namespace.gizmo" { + found = true + } + } + if !found { + t.Errorf("expected module 'app.namespace.gizmo', got: %v", moduleIdents) } }) t.Run("StrictValidationBehavior", func(t *testing.T) { - t.Log("Original implementation behavior:") - t.Log("- Requires both caddy.RegisterModule() AND CaddyModule() method") - t.Log("- Fails hard if any module in package has missing registration/implementation") - t.Log("- Only accepts static string literals in ModuleInfo.ID") - t.Log("- Cannot resolve constants or computed expressions") - t.Log("- No graceful degradation for partial failures") - }) + // one incomplete module fails the whole package, valid sibling + // included; a partial-results contract would return GoodModule here + source := ` +package test - t.Run("CachingBehavior", func(t *testing.T) { - t.Log("Original caching limitations:") - t.Log("- discoveredTypes map has race conditions (TODO comment confirms this)") - t.Log("- No mutex protection on concurrent access") - t.Log("- Unbounded memory growth") - t.Log("- No TTL or eviction strategy") - }) +import "github.com/caddyserver/caddy/v2" - t.Run("SynthesisBehavior", func(t *testing.T) { - t.Log("Original synthesis limitations:") - t.Log("- Hard failure on unknown types") - t.Log("- No fallback for interfaces, channels, functions") - t.Log("- No graceful degradation") - }) +func init() { + caddy.RegisterModule(new(GoodModule)) } -// TestOriginalLimitationsDocumented documents the specific limitations we found -func TestOriginalLimitationsDocumented(t *testing.T) { - t.Run("KnownIssues", func(t *testing.T) { - t.Log("Issues identified in original implementation:") - t.Log("1. Race condition in discoveredTypes (confirmed by TODO comment)") - t.Log("2. Dynamic module IDs skipped with warning") - t.Log("3. Constant module IDs skipped with warning") - t.Log("4. Hard failure on unknown Go types") - t.Log("5. No cache eviction causing memory leaks") - t.Log("6. Strict validation prevents mixed module packages") - }) +type GoodModule struct{} + +func (*GoodModule) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "app.test.good", + New: func() caddy.Module { return new(GoodModule) }, + } +} - t.Run("ExpectedBehaviors", func(t *testing.T) { - t.Log("What works in original implementation:") - t.Log("✓ Static string module IDs like 'app.namespace.gizmo'") - t.Log("✓ Basic type synthesis for primitives and structs") - t.Log("✓ JSON tag processing") - t.Log("✓ Package loading and AST analysis") +type OrphanModule struct{} + +func (*OrphanModule) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "app.test.orphan", + New: func() caddy.Module { return new(OrphanModule) }, + } +} +` + testModuleSource(t, source, false, "Mixed valid/incomplete package fails entirely") }) } diff --git a/concurrency_baseline_test.go b/concurrency_baseline_test.go index b580012..751625b 100644 --- a/concurrency_baseline_test.go +++ b/concurrency_baseline_test.go @@ -4,44 +4,6 @@ import ( "testing" ) -// TestConcurrencyIssuesOriginal demonstrates the race conditions in the original implementation -func TestConcurrencyIssuesOriginal(t *testing.T) { - // t.Run("RaceConditionDemo", func(t *testing.T) { - // t.Log("This test demonstrates the race condition in discoveredTypes map") - // t.Log("Run with: go test -race -run TestConcurrencyIssuesOriginal") - - // driver := New(nil) - // var wg sync.WaitGroup - - // // Multiple goroutines accessing the map concurrently - // for i := 0; i < 10; i++ { - // wg.Add(1) - // go func(id int) { - // defer wg.Done() - - // // This is what the original code does - direct map access without locking - // // This should trigger race warnings when run with -race - // key := "test.type." + string(rune('A'+id)) - - // // Read operation - // if _, exists := driver.discoveredTypes[key]; !exists { - // // Write operation - // driver.discoveredTypes[key] = &Value{ - // Type: String, - // TypeName: key, - // } - // } - // }(i) - // } - - // wg.Wait() - - // t.Logf("Completed concurrent operations on discoveredTypes map") - // t.Logf("Map now contains %d entries", len(driver.discoveredTypes)) - // t.Log("NOTE: Race detector will show warnings for the above operations") - // }) -} - // TestMemoryLeakDemo demonstrates the unbounded cache growth func TestMemoryLeakDemo(t *testing.T) { t.Run("UnboundedCacheGrowth", func(t *testing.T) { @@ -51,15 +13,16 @@ func TestMemoryLeakDemo(t *testing.T) { for i := 0; i < 1000; i++ { for version := 0; version < 5; version++ { key := "test.package." + string(rune('A'+i)) + "@v1." + string(rune('0'+version)) + ".0" - driver.discoveredTypes[key] = &Value{ + driver.setDiscoveredType(key, &Value{ Type: String, TypeName: key, - } + }) } } - t.Logf("Cache grew to %d entries with no eviction", len(driver.discoveredTypes)) - t.Log("Original implementation has no size limits or TTL") - t.Log("This can cause memory exhaustion in long-running processes") + // no size limit, TTL, or eviction: every entry is retained + if len(driver.discoveredTypes) != 5000 { + t.Errorf("expected 5000 retained entries, got %d", len(driver.discoveredTypes)) + } }) } diff --git a/core_functionality_test.go b/core_functionality_test.go index dfcd360..d0fbbb4 100644 --- a/core_functionality_test.go +++ b/core_functionality_test.go @@ -87,14 +87,13 @@ func TestSynthesisOriginalBehavior(t *testing.T) { chanType := types.NewChan(types.SendRecv, types.Typ[types.String]) rep, err := rb.buildRepresentation(chanType) - // today this hard-fails; a graceful fallback representation would - // also be acceptable — only a nil result without error is wrong + // types with no JSON representation get an empty fallback value if err != nil { - t.Logf("✓ Current behavior: hard failure on unknown type (channel): %v", err) - } else if rep != nil { - t.Logf("✓ Graceful fallback for channel type: %+v", rep) + t.Errorf("channel type should fall back gracefully, got error: %v", err) + } else if rep == nil { + t.Error("expected non-nil fallback representation for channel type") } else { - t.Error("buildRepresentation returned neither error nor representation for channel type") + t.Logf("✓ Graceful fallback for channel type: %+v", rep) } }) @@ -112,17 +111,24 @@ func TestSynthesisOriginalBehavior(t *testing.T) { // Verify the version cache is initialized if rb.versionCache == nil { t.Error("Version cache not initialized in representationBuilder") - } else { - t.Log("✓ Version cache initialized correctly") } - // This tests the internal caching but we can't easily test without - // actual packages, so we document the behavior - t.Log("Original getDepVersion behavior:") - t.Log("- Caches module versions by package path") - t.Log("- Uses go list to determine version information") - t.Log("- No cache eviction or size limits") - t.Log("- Cache grows unbounded with package usage") + // hierarchical lookup: a type in a subpackage resolves to its cached + // parent module version without invoking go list + rb.versionCache["example.com/mod"] = "v1.2.3" + pkg := types.NewPackage("example.com/mod/sub/pkg", "pkg") + named := types.NewNamed( + types.NewTypeName(0, pkg, "Thing", nil), + types.Typ[types.String], nil, + ) + + version, err := rb.getDepVersion(named) + if err != nil { + t.Fatalf("getDepVersion failed: %v", err) + } + if version != "v1.2.3" { + t.Errorf("expected cached parent version v1.2.3, got %q", version) + } }) } @@ -208,10 +214,10 @@ func TestWorkspaceOriginalBehavior(t *testing.T) { } defer ws.Close() - t.Log("Original workspace concurrency:") + t.Log("Workspace concurrency:") t.Log("- Has sync.RWMutex for workspace operations") t.Log("- Protects goGets map and package operations") - t.Log("- But Driver.discoveredTypes has no protection (race condition)") + t.Log("- Driver.discoveredTypes is protected by its own mutex") // The workspace itself is properly protected, but the driver cache is not testKey := "concurrent/test" diff --git a/integration_baseline_test.go b/integration_baseline_test.go index a9b70be..3c2f8f4 100644 --- a/integration_baseline_test.go +++ b/integration_baseline_test.go @@ -1,6 +1,7 @@ package moduledoc import ( + "go/types" "testing" ) @@ -163,25 +164,26 @@ func TestIntegrationOriginalBehavior(t *testing.T) { } defer ws.Close() - // Test the caching behavior by examining the discoveredTypes map - initialCacheSize := len(driver.discoveredTypes) - t.Logf("Initial cache size: %d", initialCacheSize) + rb := ws.representationBuilder() - // The cache is accessed without locking in the original implementation - // This is the race condition we documented - t.Log("Original cache access pattern:") - t.Log("- discoveredTypes map accessed without mutex protection") - t.Log("- Race condition confirmed by TODO comment") - t.Log("- Cache grows unbounded during synthesis") + // a type already in the driver cache is returned as a reference + // without loading its package or touching storage + pkg := types.NewPackage("example.com/fake", "fake") + named := types.NewNamed( + types.NewTypeName(0, pkg, "Thing", nil), + types.Typ[types.String], nil, + ) - // Demonstrate direct cache access (original behavior) - testKey := "test.example/Type@v1.0.0" - driver.discoveredTypes[testKey] = &Value{Type: String, TypeName: testKey} + rb.versionCache["example.com/fake"] = "v1.0.0" + sameAs := "example.com/fake.Thing@v1.0.0" + driver.setDiscoveredType(sameAs, &Value{Type: String, TypeName: "example.com/fake.Thing"}) - if len(driver.discoveredTypes) != initialCacheSize+1 { - t.Error("Cache size should have increased") - } else { - t.Log("✓ Cache grows as expected (without synchronization)") + rep, err := rb.buildRepresentation(named) + if err != nil { + t.Fatalf("buildRepresentation failed: %v", err) + } + if rep.SameAs != sameAs { + t.Errorf("expected cache hit returning reference %q, got %+v", sameAs, rep) } }) } @@ -189,7 +191,7 @@ func TestIntegrationOriginalBehavior(t *testing.T) { // TestModuleValidationIntegration tests the strict validation behavior func TestModuleValidationIntegration(t *testing.T) { t.Run("OriginalStrictValidation", func(t *testing.T) { - // Create a package with mixed valid/invalid modules to test validation + // fully compliant module: registration + implementation + static ID validSource := ` package test @@ -208,18 +210,7 @@ func (*ValidModule) CaddyModule() caddy.ModuleInfo { } } ` - - // Test that original validation requires perfect compliance - t.Log("Original validation behavior:") - t.Log("- findCaddyModuleIdents requires exact match between registration and implementation") - t.Log("- Missing registration causes hard error") - t.Log("- Missing implementation causes hard error") - t.Log("- Non-static module IDs are skipped with warning") - t.Log("- No graceful degradation for partial compliance") - - // We can't easily test this without creating temporary files, - // but the behavior is documented by the code structure - _ = validSource + testModuleSource(t, validSource, true, "Compliant module should be detected") }) } diff --git a/original_baseline_test.go b/original_baseline_test.go index 4888d64..33c7b11 100644 --- a/original_baseline_test.go +++ b/original_baseline_test.go @@ -70,118 +70,6 @@ func TestOriginalImplementation(t *testing.T) { t.Logf("Successfully found %d modules: %v", len(moduleIdents), moduleIdents) }) - - // t.Run("RaceConditionExposure", func(t *testing.T) { - // // This test exposes the race condition in the original implementation - // // It should pass under normal conditions but may fail under race conditions - - // driver := New(nil) - // var wg sync.WaitGroup - // errors := make(chan error, 10) - - // // Simulate concurrent access to discoveredTypes map - // for i := 0; i < 10; i++ { - // wg.Add(1) - // go func(id int) { - // defer wg.Done() - // // Access the discoveredTypes map without proper locking (original behavior) - // key := "test.type" + string(rune(id)) - - // // This demonstrates the race condition - direct map access without locking - // if _, exists := driver.discoveredTypes[key]; !exists { - // driver.discoveredTypes[key] = &Value{Type: String, TypeName: key} - // } - // }(i) - // } - - // wg.Wait() - // close(errors) - - // // Check for any panics/errors (though they may not always occur) - // for err := range errors { - // t.Errorf("Race condition error: %v", err) - // } - - // t.Logf("Race condition test completed - current implementation has no mutex protection") - // }) -} - -// TestOriginalLimitations tests the known limitations of the original implementation -func TestOriginalLimitations(t *testing.T) { - t.Run("DynamicModuleIDHandling", func(t *testing.T) { - // Create a test file with dynamic module ID that should be skipped - dynamicModuleSource := ` -package test - -import "github.com/caddyserver/caddy/v2" - -const ModulePrefix = "app.test" - -type DynamicModule struct{} - -func init() { - caddy.RegisterModule(new(DynamicModule)) -} - -func (*DynamicModule) CaddyModule() caddy.ModuleInfo { - return caddy.ModuleInfo{ - ID: caddy.ModuleID(ModulePrefix + ".dynamic"), - New: func() caddy.Module { return new(DynamicModule) }, - } -} -` - - // Write to a temporary file in testdata - tempFile := "/tmp/dynamic_test.go" - err := os.WriteFile(tempFile, []byte(dynamicModuleSource), 0o644) - if err != nil { - t.Fatalf("Failed to write temp file: %v", err) - } - defer os.Remove(tempFile) - - // This should demonstrate that the original implementation skips dynamic module IDs - // and logs a warning (we can't easily test the log output, but we can verify the behavior) - - t.Log("Original implementation should skip dynamic module IDs with a warning") - t.Log("This limitation will be fixed in the enhanced version") - }) - - t.Run("ConstantResolutionLimitation", func(t *testing.T) { - // Test that constants in module names are not resolved - constantModuleSource := ` -package test - -import "github.com/caddyserver/caddy/v2" - -const ModuleName = "app.constant.module" - -type ConstantModule struct{} - -func init() { - caddy.RegisterModule(new(ConstantModule)) -} - -func (*ConstantModule) CaddyModule() caddy.ModuleInfo { - return caddy.ModuleInfo{ - ID: ModuleName, // This should fail in original implementation - New: func() caddy.Module { return new(ConstantModule) }, - } -} -` - - // Similar test - the original implementation cannot handle this - _ = constantModuleSource // Prevent unused variable error - t.Log("Original implementation cannot resolve constants in module IDs") - t.Log("ModuleInfo{ID: CONSTANT_NAME} will be skipped with warning") - }) - - t.Run("UnknownTypeHandling", func(t *testing.T) { - // Test that unknown types cause hard failures in original implementation - // We'll test this by examining the synthesis error handling - - t.Log("Original implementation fails hard on unknown Go types") - t.Log("No graceful degradation for unrecognized types") - }) } // TestOriginalCachingBehavior tests the caching mechanisms in the original implementation @@ -193,39 +81,34 @@ func TestOriginalCachingBehavior(t *testing.T) { // Simulate adding many entries to the cache for i := 0; i < 1000; i++ { key := "test.type." + string(rune(i)) - driver.discoveredTypes[key] = &Value{ + driver.setDiscoveredType(key, &Value{ Type: String, TypeName: key, - } + }) } - // Original implementation has no size limits or eviction + // no size limit, TTL, or eviction: every entry is retained if len(driver.discoveredTypes) != 1000 { t.Errorf("Expected 1000 cache entries, got %d", len(driver.discoveredTypes)) } - - t.Log("Original implementation has unbounded cache growth") - t.Log("No TTL or size-based eviction implemented") - }) - - t.Run("NoPackageCacheEviction", func(t *testing.T) { - // The workspace package cache also has no eviction in original implementation - t.Log("Original workspace package cache has no eviction strategy") - t.Log("Memory leaks possible with version-specific packages") }) } // TestOriginalErrorHandling tests error handling patterns in the original code func TestOriginalErrorHandling(t *testing.T) { t.Run("StrictModuleValidation", func(t *testing.T) { - // Test the strict validation that requires both registration AND implementation - t.Log("Original implementation requires both caddy.RegisterModule() call AND CaddyModule() method") - t.Log("Missing either one causes hard failure, no graceful degradation") - }) + // registration without a local CaddyModule method fails the package + source := ` +package test - t.Run("HardFailureOnUnknownTypes", func(t *testing.T) { - // Original synthesis.go has: return nil, fmt.Errorf("unknown type %s: %#v", ...) - t.Log("Original synthesis fails immediately on unrecognized Go types") - t.Log("No fallback representation for interfaces, channels, or complex types") +import "github.com/caddyserver/caddy/v2" + +func init() { + caddy.RegisterModule(new(HalfModule)) +} + +type HalfModule struct{} +` + testModuleSource(t, source, false, "Registration without implementation should fail") }) } diff --git a/specific_functions_test.go b/specific_functions_test.go index 5b9b551..5022c58 100644 --- a/specific_functions_test.go +++ b/specific_functions_test.go @@ -317,11 +317,20 @@ func TestStorageSpecificFunctions(t *testing.T) { }) t.Run("TypeUtilityFunctions", func(t *testing.T) { - // Test the type utility functions with mock data - t.Log("Testing type utility functions:") - t.Log("- fullyQualifiedTypeName: creates FQTN from types.Type") - t.Log("- typePackageAndName: splits package and type name") - t.Log("- localTypeName: extracts just the type name") - t.Log("These functions work with types.Named objects from real packages") + pkg := types.NewPackage("example.com/pkg", "pkg") + named := types.NewNamed( + types.NewTypeName(0, pkg, "Thing", nil), + types.Typ[types.String], nil, + ) + + if got := fullyQualifiedTypeName(named); got != "example.com/pkg.Thing" { + t.Errorf("fullyQualifiedTypeName = %q; want %q", got, "example.com/pkg.Thing") + } + if p, n := typePackageAndName(named); p != "example.com/pkg" || n != "Thing" { + t.Errorf("typePackageAndName = (%q, %q); want (%q, %q)", p, n, "example.com/pkg", "Thing") + } + if got := localTypeName(named); got != "Thing" { + t.Errorf("localTypeName = %q; want %q", got, "Thing") + } }) } diff --git a/synthesis_baseline_test.go b/synthesis_baseline_test.go index 8f4a20d..db14aae 100644 --- a/synthesis_baseline_test.go +++ b/synthesis_baseline_test.go @@ -9,54 +9,29 @@ import ( // TestSynthesisFunctionsOriginalBehavior tests the core synthesis functions func TestSynthesisFunctionsOriginalBehavior(t *testing.T) { t.Run("GetStructFieldGodocs", func(t *testing.T) { - // Test getStructFieldGodocs function with real Go types - driver := New(nil) - ws, err := driver.openWorkspace() + _, rb, typ := kindsBuilder(t, "Widget") + + docs, err := rb.getStructFieldGodocs(typ) if err != nil { - t.Fatalf("Failed to create workspace: %v", err) + t.Fatalf("getStructFieldGodocs failed: %v", err) } - defer ws.Close() - - rb := ws.representationBuilder() - - // Test behavior with invalid/nil types - should handle gracefully - t.Log("Testing getStructFieldGodocs behavior:") - t.Log("- Function expects a *types.Named struct type") - t.Log("- Returns map[string]string of field names to godocs") - t.Log("- Requires package loading and AST parsing") - t.Log("- Original implementation may fail on non-struct types") - - // We can't easily test this without a real struct type from packages, - // but we can document the expected behavior - var nilType types.Type - if nilType != nil { - _, err := rb.getStructFieldGodocs(nilType) - t.Logf("getStructFieldGodocs with nil type: %v", err) + if !strings.Contains(docs["Name"], "Name is the widget name") { + t.Errorf("expected field godoc for Name, got %q", docs["Name"]) + } + if _, ok := docs["DoublePtr"]; ok { + t.Error("fields without godoc should not appear in the map") } }) t.Run("GetGodocForType", func(t *testing.T) { - // Test getGodocForType function - driver := New(nil) - ws, err := driver.openWorkspace() + _, rb, typ := kindsBuilder(t, "Widget") + + doc, err := rb.getGodocForType(typ) if err != nil { - t.Fatalf("Failed to create workspace: %v", err) + t.Fatalf("getGodocForType failed: %v", err) } - defer ws.Close() - - rb := ws.representationBuilder() - - t.Log("Testing getGodocForType behavior:") - t.Log("- Extracts godoc from TypeSpec or GenDecl AST nodes") - t.Log("- Prefers TypeSpec doc over GenDecl doc (more specific)") - t.Log("- Returns empty string if no documentation found") - t.Log("- Requires package loading and AST traversal") - - // Test with nil - should handle gracefully - var nilType types.Type - if nilType != nil { - doc, err := rb.getGodocForType(nilType) - t.Logf("getGodocForType with nil type: doc=%q, err=%v", doc, err) + if !strings.Contains(doc, "exercises the field kinds") { + t.Errorf("expected type godoc, got %q", doc) } }) @@ -113,15 +88,10 @@ func TestSynthesisFunctionsOriginalBehavior(t *testing.T) { if err != nil { t.Errorf("buildRepresentation failed for interface: %v", err) } else if rep == nil { - t.Log("✓ Interface type returns nil (original behavior)") - } else { - t.Logf("✓ Interface type -> %+v", rep) + t.Error("expected non-nil empty representation for interface") + } else if rep.Type != "" { + t.Errorf("expected empty type for interface, got %s", rep.Type) } - - t.Log("Original interface handling:") - t.Log("- *types.Interface at top level returns new(Value)") - t.Log("- *types.Interface in switch default returns nil") - t.Log("- This inconsistency is part of original behavior") }) t.Run("BuildRepresentationPointers", func(t *testing.T) { @@ -219,8 +189,9 @@ func TestSynthesisFunctionsOriginalBehavior(t *testing.T) { }) t.Run("ModuleMapDetection", func(t *testing.T) { - // Test special case: map[string]json.RawMessage -> ModuleMap - driver := New(nil) + // map[string]json.RawMessage must become ModuleMap; any other map + // stays a plain Map + driver := New(newMemStorage()) ws, err := driver.openWorkspace() if err != nil { t.Fatalf("Failed to create workspace: %v", err) @@ -229,24 +200,27 @@ func TestSynthesisFunctionsOriginalBehavior(t *testing.T) { rb := ws.representationBuilder() - t.Log("Testing ModuleMap detection:") - t.Log("- map[string]Module should become ModuleMap type") - t.Log("- This requires json.RawMessage detection as Module type") - t.Log("- Original behavior depends on exact package path matching") + rawMsg := types.NewNamed( + types.NewTypeName(0, types.NewPackage("encoding/json", "json"), "RawMessage", nil), + types.NewSlice(types.Typ[types.Byte]), nil, + ) + moduleMap := types.NewMap(types.Typ[types.String], rawMsg) - // We can't easily create a json.RawMessage type without loading packages, - // but we can document the behavior - stringType := types.Typ[types.String] - intType := types.Typ[types.Int] - regularMap := types.NewMap(stringType, intType) + rep, err := rb.buildRepresentation(moduleMap) + if err != nil { + t.Fatalf("buildRepresentation failed for map[string]json.RawMessage: %v", err) + } + if rep == nil || rep.Type != ModuleMap { + t.Errorf("expected ModuleMap, got %+v", rep) + } - rep, err := rb.buildRepresentation(regularMap) + regularMap := types.NewMap(types.Typ[types.String], types.Typ[types.Int]) + rep, err = rb.buildRepresentation(regularMap) if err != nil { - t.Errorf("buildRepresentation failed: %v", err) - } else if rep != nil && rep.Type == ModuleMap { - t.Log("✓ Detected ModuleMap (unexpected without json.RawMessage)") - } else { - t.Log("✓ Regular map detected (expected without Module element)") + t.Fatalf("buildRepresentation failed for map[string]int: %v", err) + } + if rep == nil || rep.Type != Map { + t.Errorf("expected plain Map, got %+v", rep) } }) @@ -262,150 +236,44 @@ func TestSynthesisFunctionsOriginalBehavior(t *testing.T) { rb := ws.representationBuilder() t.Log("Testing unknown type handling:") - t.Log("- Original implementation has hard failures for unknown types") - t.Log("- buildRepresentation returns error for unsupported Go types") - t.Log("- No graceful degradation or fallback handling") + t.Log("- Types with no JSON representation fall back to an empty value") - // Create a channel type (unsupported) + // Create a channel type (no JSON representation) chanType := types.NewChan(types.SendRecv, types.Typ[types.String]) rep, err := rb.buildRepresentation(chanType) if err != nil { - t.Logf("✓ Current behavior: buildRepresentation fails for channel type: %v", err) + t.Errorf("channel type should fall back gracefully, got error: %v", err) + } else if rep == nil { + t.Error("expected non-nil fallback representation for channel type") } else { - // a graceful fallback is an acceptable future behavior t.Logf("✓ Graceful fallback for channel type: %+v", rep) } - // Create a function type (unsupported) + // Create a function type (no JSON representation) sig := types.NewSignature(nil, nil, nil, false) funcType := types.NewSignature(nil, types.NewTuple(), types.NewTuple(), false) _ = funcType // Avoid unused variable _ = sig // Avoid unused variable - t.Log("✓ Original implementation fails hard on unsupported types (channels, funcs, etc.)") + t.Log("✓ Unsupported types (channels, funcs, etc.) no longer fail the containing type") }) } // TestRepresentationBuilderOriginalBehavior tests the representationBuilder struct -func TestRepresentationBuilderOriginalBehavior(t *testing.T) { - t.Run("VersionCacheOriginalBehavior", func(t *testing.T) { - driver := New(nil) - ws, err := driver.openWorkspace() - if err != nil { - t.Fatalf("Failed to create workspace: %v", err) - } - defer ws.Close() - - rb := ws.representationBuilder() - - t.Log("Testing version cache behavior:") - t.Log("- representationBuilder has versionCache map[string]string") - t.Log("- Cache stores module versions by package path") - t.Log("- Caches parent package versions to avoid repeated 'go list' calls") - t.Log("- Original implementation has no size limits or TTL") - - // Test cache initialization - if rb.versionCache == nil { - t.Error("Expected versionCache to be initialized") - } else { - t.Logf("✓ versionCache initialized with %d entries", len(rb.versionCache)) - } - - // Test cache access pattern - initialSize := len(rb.versionCache) - testKey := "example.com/test" - testVersion := "v1.2.3" - rb.versionCache[testKey] = testVersion - - if len(rb.versionCache) != initialSize+1 { - t.Error("Cache size should have increased") - } else { - t.Log("✓ Version cache grows as expected") - } - - if cached := rb.versionCache[testKey]; cached != testVersion { - t.Errorf("Expected cached version %s, got %s", testVersion, cached) - } else { - t.Log("✓ Version cache retrieval works") - } - }) - - t.Run("GetDepVersionOriginalBehavior", func(t *testing.T) { - driver := New(nil) - ws, err := driver.openWorkspace() - if err != nil { - t.Fatalf("Failed to create workspace: %v", err) - } - defer ws.Close() - - t.Log("Testing getDepVersion behavior:") - t.Log("- Extracts package path from *types.Named") - t.Log("- Checks version cache for parent packages (hierarchical lookup)") - t.Log("- Falls back to 'go list -json' command for version info") - t.Log("- Caches results for performance (shaves off *LOT* of time)") - t.Log("- Standard library types have empty version") - - // We can't test this easily without real types, but document behavior - t.Log("Original getDepVersion limitations:") - t.Log("- Requires *types.Named input (will panic on other types)") - t.Log("- Executes shell commands ('go list') synchronously") - t.Log("- No error recovery if 'go list' fails") - t.Log("- Cache grows unbounded during operation") - }) -} - // TestRunGoListOriginalBehavior tests the runGoList utility function func TestRunGoListOriginalBehavior(t *testing.T) { t.Run("RunGoListBehavior", func(t *testing.T) { - t.Log("Testing runGoList behavior:") - t.Log("- Executes 'go list -json ' in workspace directory") - t.Log("- Parses JSON output into goListOutput struct") - t.Log("- Returns detailed module and package information") - t.Log("- Includes version, path, standard library flag, dependencies") - - // Test with a package that should fail tempDir := "/tmp" invalidPkg := "invalid.package.name.that.does.not.exist" output, err := runGoList(tempDir, invalidPkg) - if err != nil { - t.Logf("✓ runGoList properly fails for invalid package: %v", err) - // Check if error includes stderr output (original behavior) - if strings.Contains(err.Error(), ">>>>>>") { - t.Log("✓ Error includes stderr output as expected") - } - } else { - t.Errorf("Expected error for invalid package, got: %+v", output) - } - - t.Log("Original runGoList characteristics:") - t.Log("- Synchronous command execution (blocking)") - t.Log("- No timeout or cancellation support") - t.Log("- Stderr included in error messages with >>>>>> markers") - t.Log("- Direct JSON unmarshaling (fails on malformed output)") - }) - - t.Run("GoListOutputStructure", func(t *testing.T) { - t.Log("Testing goListOutput structure:") - t.Log("- Contains Dir, ImportPath, Name, Root fields") - t.Log("- Module field with Path, Version, Replace sub-fields") - t.Log("- Replace field handles module replacements") - t.Log("- Time field for module timestamps") - t.Log("- GoFiles, TestGoFiles for source file lists") - t.Log("- Imports, Deps for dependency tracking") - t.Log("- Standard, GoRoot flags for stdlib detection") - - // Create empty struct to verify initialization - var output goListOutput - if output.ImportPath == "" { - t.Log("✓ goListOutput initializes with empty strings") - } - if len(output.GoFiles) == 0 { - t.Log("✓ Slice fields initialize to nil/empty") + if err == nil { + t.Fatalf("expected error for invalid package, got: %+v", output) } - if !output.Stale { - t.Log("✓ Boolean fields initialize to false") + // stderr is embedded in the error between >>>>>> markers + if !strings.Contains(err.Error(), ">>>>>>") { + t.Errorf("expected stderr markers in error, got: %v", err) } }) } @@ -416,89 +284,6 @@ func TestSynthesisConstantsOriginalBehavior(t *testing.T) { expectedPath := "github.com/caddyserver/caddy/v2" if caddyCorePackagePath != expectedPath { t.Errorf("Expected caddyCorePackagePath=%q, got %q", expectedPath, caddyCorePackagePath) - } else { - t.Log("✓ caddyCorePackagePath constant is correct") - } - - t.Log("Testing caddyCorePackagePath usage:") - t.Log("- Used for identifying Caddy core types and modules") - t.Log("- Version v2 indicates Caddy 2.x compatibility") - t.Log("- Critical for module detection and validation") - }) -} - -// TestGoListOutputOriginalBehavior tests the goListOutput struct behavior -func TestGoListOutputOriginalBehavior(t *testing.T) { - t.Run("GoListOutputFields", func(t *testing.T) { - var output goListOutput - - t.Log("Testing goListOutput struct fields:") - t.Log("- Dir: directory path of the package") - t.Log("- ImportPath: import path of the package") - t.Log("- Name: package name") - t.Log("- Root: path to root of package tree") - t.Log("- Module: detailed module information with version, replace, etc.") - t.Log("- Match: patterns matched (for wildcard queries)") - t.Log("- Stale: whether package needs rebuilding") - t.Log("- GoFiles: list of .go source files") - t.Log("- Imports/Deps: dependency information") - - // Test zero values - if output.Dir == "" { - t.Log("✓ String fields initialize to empty") - } - if len(output.GoFiles) == 0 { - t.Log("✓ Slice fields initialize to nil") - } - if !output.Stale { - t.Log("✓ Bool fields initialize to false") - } - - // Test nested struct access - if output.Module.Path == "" { - t.Log("✓ Nested Module struct initializes correctly") - } - if output.Module.Replace.Path == "" { - t.Log("✓ Nested Replace struct initializes correctly") - } - }) -} - -// TestTypeUtilitiesOriginalBehavior tests utility functions for type handling -func TestTypeUtilitiesOriginalBehavior(t *testing.T) { - t.Run("TypePackageAndName", func(t *testing.T) { - t.Log("Testing typePackageAndName behavior:") - t.Log("- Extracts package path and type name from types.Type") - t.Log("- Returns empty strings for invalid/unsupported types") - t.Log("- Used throughout synthesis for FQTN construction") - - // Test with basic type (should return empty package for builtins) - stringType := types.Typ[types.String] - pkg, name := typePackageAndName(stringType) - t.Logf("string type: package=%q, name=%q", pkg, name) - - if pkg != "" || name != "string" { - t.Logf("Basic type handling: pkg=%q, name=%q", pkg, name) - } else { - t.Log("✓ Basic types have empty package, correct name") - } - }) - - t.Run("FullyQualifiedTypeName", func(t *testing.T) { - t.Log("Testing fullyQualifiedTypeName behavior:") - t.Log("- Constructs FQTN for database storage") - t.Log("- Format: package/path.TypeName") - t.Log("- Used as key in discoveredTypes cache") - - // Test with string type - stringType := types.Typ[types.String] - fqtn := fullyQualifiedTypeName(stringType) - t.Logf("string FQTN: %q", fqtn) - - if fqtn != "" { - t.Logf("✓ FQTN generated: %s", fqtn) - } else { - t.Log("FQTN empty for basic type (expected behavior)") } }) } @@ -515,65 +300,11 @@ func TestSynthesisIntegrationOriginalBehavior(t *testing.T) { rb := ws.representationBuilder() - t.Log("Testing representationBuilder integration:") - t.Log("- Created through workspace.representationBuilder()") - t.Log("- Maintains reference to workspace and driver") - t.Log("- Has version cache for performance optimization") - t.Log("- Integrates with discoveredTypes cache in driver") - - // Test workspace reference if rb.ws.driver != driver { t.Error("representationBuilder should reference correct driver") - } else { - t.Log("✓ Workspace reference is correct") } - - // Test version cache initialization if rb.versionCache == nil { t.Error("versionCache should be initialized") - } else { - t.Log("✓ versionCache is initialized") - } - - // Use rb to avoid unused variable error - _ = rb - }) - - t.Run("CacheIntegrationPatterns", func(t *testing.T) { - driver := New(nil) - ws, err := driver.openWorkspace() - if err != nil { - t.Fatalf("Failed to create workspace: %v", err) - } - defer ws.Close() - - rb := ws.representationBuilder() - - t.Log("Testing cache integration patterns:") - t.Log("- discoveredTypes cache in driver (type representations)") - t.Log("- versionCache in representationBuilder (module versions)") - t.Log("- parsedPackages cache in workspace (loaded packages)") - t.Log("- No synchronization between caches (race conditions possible)") - - initialDriverCache := len(driver.discoveredTypes) - initialVersionCache := len(rb.versionCache) - - t.Logf("Initial cache sizes: driver=%d, version=%d", initialDriverCache, initialVersionCache) - - // Add to caches to test integration - driver.discoveredTypes["test.type"] = &Value{Type: String} - rb.versionCache["test.package"] = "v1.0.0" - - if len(driver.discoveredTypes) != initialDriverCache+1 { - t.Error("Driver cache should have grown") - } else { - t.Log("✓ Driver cache grows independently") - } - - if len(rb.versionCache) != initialVersionCache+1 { - t.Error("Version cache should have grown") - } else { - t.Log("✓ Version cache grows independently") } }) } diff --git a/synthesis_real_test.go b/synthesis_real_test.go index 097a910..2d3f51d 100644 --- a/synthesis_real_test.go +++ b/synthesis_real_test.go @@ -102,39 +102,26 @@ go 1.19 defer ws.Close() rb := ws.representationBuilder() - - t.Log("Testing representationBuilder version cache:") - t.Log("- versionCache map stores package path -> version") - t.Log("- Hierarchical lookup checks parent packages") - t.Log("- Cache avoids repeated 'go list' calls") - - // Test cache initialization if rb.versionCache == nil { - t.Error("versionCache should be initialized") - } else { - initialSize := len(rb.versionCache) - t.Logf("✓ versionCache initialized with %d entries", initialSize) - - // Test manual cache entry - testPkg := "example.com/test/subpackage" - testVersion := "v1.2.3" - rb.versionCache[testPkg] = testVersion - - // Test hierarchical lookup behavior - parts := strings.Split(testPkg, "/") - for i := len(parts); i > 0; i-- { - parent := strings.Join(parts[:i], "/") - if version, ok := rb.versionCache[parent]; ok { - t.Logf("✓ Found cached version for parent %s: %s", parent, version) - break - } - } + t.Fatal("versionCache should be initialized") + } - if len(rb.versionCache) != initialSize+1 { - t.Error("Cache should have grown by 1") - } else { - t.Log("✓ Cache grows as expected") - } + // standard library types resolve to an empty version and are + // cached under their import path + stdType := types.NewNamed( + types.NewTypeName(0, types.NewPackage("encoding/json", "json"), "RawMessage", nil), + types.NewSlice(types.Typ[types.Byte]), nil, + ) + + version, err := rb.getDepVersion(stdType) + if err != nil { + t.Fatalf("getDepVersion failed for stdlib type: %v", err) + } + if version != "" { + t.Errorf("stdlib types should have empty version, got %q", version) + } + if _, ok := rb.versionCache["encoding/json"]; !ok { + t.Error("stdlib version should be cached under the import path") } }) } @@ -274,22 +261,22 @@ func TestBuildRepresentationWithRealTypes(t *testing.T) { // Test channel type stringType := types.Typ[types.String] chanType := types.NewChan(types.SendRecv, stringType) - _, err = rb.buildRepresentation(chanType) + rep, err := rb.buildRepresentation(chanType) if err != nil { - t.Logf("✓ Current behavior: channel type rejected: %v", err) - if strings.Contains(err.Error(), "unknown type") { - t.Log("✓ Error message indicates unknown type") - } + t.Errorf("channel type should fall back gracefully, got error: %v", err) + } else if rep == nil { + t.Error("expected non-nil fallback representation for channel type") } else { - // a graceful fallback is an acceptable future behavior t.Log("✓ Graceful fallback for channel type") } // Test function type sig := types.NewSignature(nil, types.NewTuple(), types.NewTuple(), false) - _, err = rb.buildRepresentation(sig) + rep, err = rb.buildRepresentation(sig) if err != nil { - t.Logf("✓ Current behavior: function type rejected: %v", err) + t.Errorf("function type should fall back gracefully, got error: %v", err) + } else if rep == nil { + t.Error("expected non-nil fallback representation for function type") } else { t.Log("✓ Graceful fallback for function type") } @@ -298,23 +285,22 @@ func TestBuildRepresentationWithRealTypes(t *testing.T) { // TestCaddyCorePackagePathConstant tests the constant usage func TestCaddyCorePackagePathConstant(t *testing.T) { - t.Log("Testing caddyCorePackagePath constant:") - expectedPath := "github.com/caddyserver/caddy/v2" if caddyCorePackagePath != expectedPath { t.Errorf("Expected caddyCorePackagePath=%q, got %q", expectedPath, caddyCorePackagePath) - } else { - t.Log("✓ caddyCorePackagePath constant correct") } - - // Test that it's used consistently in the codebase - t.Log("- Used for identifying Caddy core modules") - t.Log("- Version /v2 indicates Caddy 2.x compatibility") - t.Log("- Critical for module validation and detection") } // TestSynthesisErrorHandlingOriginalBehavior tests error conditions func TestSynthesisErrorHandlingOriginalBehavior(t *testing.T) { + // a type whose package cannot be resolved by go list + missingType := func() *types.Named { + return types.NewNamed( + types.NewTypeName(0, types.NewPackage("invalid.example/does/not/exist", "exist"), "Ghost", nil), + types.Typ[types.String], nil, + ) + } + t.Run("GetStructFieldGodocsErrors", func(t *testing.T) { driver := New(nil) ws, err := driver.openWorkspace() @@ -323,15 +309,10 @@ func TestSynthesisErrorHandlingOriginalBehavior(t *testing.T) { } defer ws.Close() - t.Log("Testing getStructFieldGodocs error handling:") - t.Log("- Expects *types.Named input (panics on others)") - t.Log("- Returns error if package loading fails") - t.Log("- Returns error if struct type not found") - - // This would panic in original implementation with non-Named types - // We document the behavior without triggering the panic - t.Log("Original limitation: panics if typ is not *types.Named") - t.Log("Original limitation: no graceful handling of package load failures") + rb := ws.representationBuilder() + if _, err := rb.getStructFieldGodocs(missingType()); err == nil { + t.Error("expected error for unresolvable package") + } }) t.Run("GetGodocForTypeErrors", func(t *testing.T) { @@ -342,14 +323,10 @@ func TestSynthesisErrorHandlingOriginalBehavior(t *testing.T) { } defer ws.Close() - t.Log("Testing getGodocForType error handling:") - t.Log("- Expects *types.Named input (panics on others)") - t.Log("- Returns error if type not found in package") - t.Log("- Returns empty string if no documentation found") - - // Document original behavior limitations - t.Log("Original limitation: panics if typ is not *types.Named") - t.Log("Original limitation: no fallback for missing documentation") + rb := ws.representationBuilder() + if _, err := rb.getGodocForType(missingType()); err == nil { + t.Error("expected error for unresolvable package") + } }) t.Run("GetDepVersionErrors", func(t *testing.T) { @@ -360,14 +337,9 @@ func TestSynthesisErrorHandlingOriginalBehavior(t *testing.T) { } defer ws.Close() - t.Log("Testing getDepVersion error handling:") - t.Log("- Returns error if package path cannot be determined") - t.Log("- Returns error if 'go list' command fails") - t.Log("- No timeout or cancellation for shell commands") - - // Document the TODO comment behavior - t.Log("Original TODO: 'we could probably ignore this error, but let's see...'") - t.Log("Original limitation: synchronous shell command execution") - t.Log("Original limitation: no retry logic for transient failures") + rb := ws.representationBuilder() + if _, err := rb.getDepVersion(missingType()); err == nil { + t.Error("expected error when go list cannot resolve the package") + } }) } diff --git a/validation_baseline_test.go b/validation_baseline_test.go index 065f36f..d908883 100644 --- a/validation_baseline_test.go +++ b/validation_baseline_test.go @@ -140,79 +140,3 @@ func testModuleSource(t *testing.T, source string, expectSuccess bool, message s t.Logf("%s: ✓ Correctly failed or skipped as expected", message) } } - -// TestSynthesisOriginalLimitations tests the type synthesis limitations -func TestSynthesisOriginalLimitations(t *testing.T) { - t.Run("UnknownTypeFailure", func(t *testing.T) { - // Test that unknown types cause hard failures - // We'll simulate this by checking the synthesis logic - - t.Log("Original synthesis.go has hard failure on unknown types:") - t.Log("default: return nil, fmt.Errorf with unknown type pattern") - - // This documents the limitation - we can't easily test the actual failure - // without creating complex type scenarios, but we document the behavior - }) - - t.Run("NoGracefulDegradation", func(t *testing.T) { - t.Log("Original implementation has no fallback for:") - t.Log("- Interface types (returns nil, nil)") - t.Log("- Channel types (not handled)") - t.Log("- Function types (not handled)") - t.Log("- Complex generic types (not handled)") - }) -} - -// TestConcurrencyIssues attempts to expose race conditions in the original implementation -// func TestConcurrencyIssues(t *testing.T) { -// t.Run("ConcurrentCacheAccess", func(t *testing.T) { -// // This test may expose the race condition mentioned in the TODO comment -// driver := New(nil) - -// // Run with race detector: go test -race -// // The original implementation should show race warnings - -// done := make(chan bool) - -// // Writer goroutine -// go func() { -// for i := 0; i < 100; i++ { -// key := "test" + string(rune(i)) -// // Direct map access without locking (original behavior) -// driver.discoveredTypes[key] = &Value{Type: String} -// } -// done <- true -// }() - -// // Reader goroutine -// go func() { -// for i := 0; i < 100; i++ { -// key := "test" + string(rune(i)) -// // Direct map access without locking (original behavior) -// _ = driver.discoveredTypes[key] -// } -// done <- true -// }() - -// // Wait for both to complete -// <-done -// <-done - -// t.Log("Concurrent access test completed") -// t.Log("Run with 'go test -race' to see race condition warnings") -// }) -// } - -// TestWorkspaceMemoryLeak tests the memory leak issues in workspace caching -func TestWorkspaceMemoryLeak(t *testing.T) { - t.Run("UnboundedPackageCache", func(t *testing.T) { - // The workspace.go has TODO comments about memory issues: - // "TODO: make this cache ephemeral (workspace-scoped), there's just not enough memory for all the versions." - - t.Log("Original workspace caching issues:") - t.Log("- No cache size limits") - t.Log("- No TTL-based eviction") - t.Log("- No cleanup of old package versions") - t.Log("- Memory grows unbounded with version-specific packages") - }) -} From 171dce0b5e6e8c99f74e2e85fa75e8ac65212b86 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Wed, 9 Sep 2026 16:23:00 +0300 Subject: [PATCH 07/12] fix regression Signed-off-by: Mohammed Al Sahaf --- synthesis.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/synthesis.go b/synthesis.go index b455efd..b81c436 100644 --- a/synthesis.go +++ b/synthesis.go @@ -237,7 +237,10 @@ func (rb representationBuilder) buildRepresentation(caddyModuleType types.Type) } // a json.RawMessage type represents a module! - if packagePath == "encoding/json" && typeName == "RawMessage" { + // (json.RawMessage is an alias for jsontext.Value as of the + // json/v2 stdlib, and aliases are unwrapped above) + if (packagePath == "encoding/json" && typeName == "RawMessage") || + (packagePath == "encoding/json/jsontext" && typeName == "Value") { return &Value{Type: Module}, nil } From 3994e8aaeec403b6d5da7c15b437fa7661017949 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Thu, 24 Sep 2026 00:43:48 +0300 Subject: [PATCH 08/12] unify `packages.LoadMode` across codebase Signed-off-by: Mohammed Al Sahaf --- baseline_behavior_test.go | 11 +-- driver_edge_test.go | 138 +++++++++++++++++++++++++++++++++++++ helper_test.go | 11 +-- original_baseline_test.go | 11 +-- sourcecode_edge_test.go | 9 +-- specific_functions_test.go | 22 ++---- workspace.go | 20 ++++-- 7 files changed, 167 insertions(+), 55 deletions(-) create mode 100644 driver_edge_test.go diff --git a/baseline_behavior_test.go b/baseline_behavior_test.go index ba755e5..7c17e09 100644 --- a/baseline_behavior_test.go +++ b/baseline_behavior_test.go @@ -12,14 +12,9 @@ func TestOriginalBehaviorBaseline(t *testing.T) { t.Run("ValidStaticModule", func(t *testing.T) { // Test only the gizmo.go file which should work cfg := &packages.Config{ - Dir: ".", - Mode: packages.NeedSyntax | - packages.NeedImports | - packages.NeedDeps | - packages.NeedTypes | - packages.NeedModule | - packages.NeedTypesInfo, - Env: append(os.Environ(), "CGO_ENABLED=0"), + Dir: ".", + Mode: packagesLoadMode, + Env: append(os.Environ(), "CGO_ENABLED=0"), } pkgs, err := packages.Load(cfg, "./testdata") diff --git a/driver_edge_test.go b/driver_edge_test.go new file mode 100644 index 0000000..a33c47f --- /dev/null +++ b/driver_edge_test.go @@ -0,0 +1,138 @@ +package moduledoc + +import ( + "strings" + "testing" +) + +func TestLoadTypesByModuleID(t *testing.T) { + db := newMemStorage() + db.types[storageKey("example.com/pkg", "Handler", "")] = &Value{ + Type: Struct, + TypeName: "example.com/pkg.Handler", + Doc: "handler doc", + StructFields: []*StructField{ + {Key: "root", Value: &Value{Type: String}, Doc: "root doc"}, + }, + } + db.modules["http.handlers.x"] = []*Value{ + {SameAs: "example.com/pkg.Handler", Doc: "usage doc"}, + } + d := New(db) + + vals, err := d.LoadTypesByModuleID("http.handlers.x") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(vals) != 1 { + t.Fatalf("expected 1 value, got %d", len(vals)) + } + got := vals[0] + if got.Type != Struct || got.TypeName != "example.com/pkg.Handler" { + t.Errorf("module type not resolved, got %+v", got) + } + if !strings.Contains(got.Doc, "usage doc") || !strings.Contains(got.Doc, "handler doc") { + t.Errorf("docs should combine usage and type docs, got %q", got.Doc) + } + if len(got.StructFields) != 1 || got.StructFields[0].Value.Type != String { + t.Errorf("struct fields not deeply dereferenced: %+v", got.StructFields) + } + + // unknown IDs yield an empty result, not an error + vals, err = d.LoadTypesByModuleID("does.not.exist") + if err != nil { + t.Fatalf("unexpected error for unknown module ID: %v", err) + } + if len(vals) != 0 { + t.Errorf("expected no values for unknown module ID, got %d", len(vals)) + } +} + +func TestLoadTypeByPath(t *testing.T) { + db := newMemStorage() + db.types[storageKey(CaddyCorePackage, "Config", "")] = &Value{ + Type: Struct, + TypeName: CaddyCorePackage + ".Config", + StructFields: []*StructField{ + { + Key: "listen", + Value: &Value{SameAs: "example.com/pkg.Listen"}, + Doc: "listen field doc", + }, + }, + } + db.types[storageKey("example.com/pkg", "Listen", "")] = &Value{ + Type: String, + TypeName: "example.com/pkg.Listen", + Doc: "listen type doc", + } + d := New(db) + + exact, nearest, err := d.LoadTypeByPath("listen", "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if exact == nil || exact.Type != String { + t.Errorf("expected resolved string type at path, got %+v", exact) + } + if !strings.Contains(exact.Doc, "listen field doc") { + t.Errorf("field doc should be included at target, got %q", exact.Doc) + } + if nearest == nil || nearest.TypeName == "" { + t.Errorf("expected a named nearest type, got %+v", nearest) + } + + // without a stored Config start type, the lookup must error + if _, _, err := New(newMemStorage()).LoadTypeByPath("listen", ""); err == nil { + t.Error("expected error when start type is missing") + } +} + +func TestAddTypeStdlibStruct(t *testing.T) { + if testing.Short() { + t.Skip("requires the Go toolchain") + } + db := newMemStorage() + d := New(db) + + // stdlib packages resolve offline in a fresh workspace + rep, err := d.AddType("net/url", "URL", "") + if err != nil { + t.Fatalf("AddType failed: %v", err) + } + if !strings.Contains(rep.SameAs, "net/url.URL") { + t.Errorf("expected reference to net/url.URL, got %+v", rep) + } + + stored, err := db.GetTypeByName("net/url", "URL", "") + if err != nil || stored == nil { + t.Fatalf("stored type not found: %v", err) + } + if stored.Type != Struct { + t.Errorf("expected struct, got %s", stored.Type) + } + if !strings.Contains(stored.Doc, "URL") { + t.Errorf("expected type godoc, got %q", stored.Doc) + } +} + +func TestAddTypeErrors(t *testing.T) { + if testing.Short() { + t.Skip("requires the Go toolchain") + } + d := New(newMemStorage()) + + if _, err := d.AddType("net/url", "DoesNotExist", ""); err == nil { + t.Error("expected error for unknown type name") + } +} + +func TestLoadModulesFromImportingPackageEmptyPattern(t *testing.T) { + if testing.Short() { + t.Skip("requires the Go toolchain") + } + d := New(newMemStorage()) + if _, err := d.LoadModulesFromImportingPackage("", ""); err == nil { + t.Error("expected error for empty package pattern") + } +} diff --git a/helper_test.go b/helper_test.go index 269ddc6..0dc27ce 100644 --- a/helper_test.go +++ b/helper_test.go @@ -26,15 +26,8 @@ import ( func loadAliasesFixture(t *testing.T) *packages.Package { t.Helper() cfg := &packages.Config{ - Mode: packages.NeedName | - packages.NeedFiles | - packages.NeedImports | - packages.NeedDeps | - packages.NeedTypes | - packages.NeedTypesInfo | - packages.NeedSyntax | - packages.NeedModule, - Dir: "testdata/aliases", + Mode: packagesLoadMode, + Dir: "testdata/aliases", } pkgs, err := packages.Load(cfg, ".") if err != nil { diff --git a/original_baseline_test.go b/original_baseline_test.go index 33c7b11..25650b9 100644 --- a/original_baseline_test.go +++ b/original_baseline_test.go @@ -26,14 +26,9 @@ func TestOriginalImplementation(t *testing.T) { t.Run("BasicModuleDetection", func(t *testing.T) { // Test that basic module detection works with static string literals cfg := &packages.Config{ - Dir: ".", - Mode: packages.NeedSyntax | - packages.NeedImports | - packages.NeedDeps | - packages.NeedTypes | - packages.NeedModule | - packages.NeedTypesInfo, - Env: append(os.Environ(), "CGO_ENABLED=0"), + Dir: ".", + Mode: packagesLoadMode, + Env: append(os.Environ(), "CGO_ENABLED=0"), } pkgs, err := packages.Load(cfg, "./testdata") diff --git a/sourcecode_edge_test.go b/sourcecode_edge_test.go index eb658ad..feee1ff 100644 --- a/sourcecode_edge_test.go +++ b/sourcecode_edge_test.go @@ -14,13 +14,8 @@ func loadFixturePackage(t *testing.T, pattern string) *packages.Package { t.Skip("requires the Go toolchain") } cfg := &packages.Config{ - Mode: packages.NeedName | - packages.NeedSyntax | - packages.NeedTypes | - packages.NeedTypesInfo | - packages.NeedImports | - packages.NeedDeps, - Env: append(os.Environ(), "CGO_ENABLED=0"), + Mode: packagesLoadMode, + Env: append(os.Environ(), "CGO_ENABLED=0"), } pkgs, err := packages.Load(cfg, pattern) if err != nil { diff --git a/specific_functions_test.go b/specific_functions_test.go index 5022c58..969121d 100644 --- a/specific_functions_test.go +++ b/specific_functions_test.go @@ -129,14 +129,9 @@ func TestSynthesisSpecificFunctions(t *testing.T) { t.Run("GetStructFieldGodocs", func(t *testing.T) { // Test getStructFieldGodocs with real testdata cfg := &packages.Config{ - Dir: "testdata", - Mode: packages.NeedSyntax | - packages.NeedImports | - packages.NeedDeps | - packages.NeedTypes | - packages.NeedModule | - packages.NeedTypesInfo, - Env: append(os.Environ(), "CGO_ENABLED=0"), + Dir: "testdata", + Mode: packagesLoadMode, + Env: append(os.Environ(), "CGO_ENABLED=0"), } pkgs, err := packages.Load(cfg, "testdata") @@ -184,14 +179,9 @@ func TestSynthesisSpecificFunctions(t *testing.T) { t.Run("GetGodocForType", func(t *testing.T) { // Test getGodocForType with testdata cfg := &packages.Config{ - Dir: "testdata", - Mode: packages.NeedSyntax | - packages.NeedImports | - packages.NeedDeps | - packages.NeedTypes | - packages.NeedModule | - packages.NeedTypesInfo, - Env: append(os.Environ(), "CGO_ENABLED=0"), + Dir: "testdata", + Mode: packagesLoadMode, + Env: append(os.Environ(), "CGO_ENABLED=0"), } pkgs, err := packages.Load(cfg, "testdata") diff --git a/workspace.go b/workspace.go index c8419b2..4724408 100644 --- a/workspace.go +++ b/workspace.go @@ -46,6 +46,17 @@ type workspace struct { parsedPackages map[string]*packages.Package } +// packagesLoadMode is the packages.Config.Mode used wherever we need +// full syntax and type information to inspect Caddy modules. +const packagesLoadMode = packages.NeedName | + packages.NeedFiles | + packages.NeedSyntax | + packages.NeedImports | + packages.NeedDeps | + packages.NeedTypes | + packages.NeedTypesInfo | + packages.NeedModule + func (d *Driver) openWorkspace() (workspace, error) { tempDir, err := ioutil.TempDir("", "caddy_docsys_") if err != nil { @@ -121,13 +132,8 @@ func (ws *workspace) getPackages(packagePattern, version string) ([]*packages.Pa // finally, load and parse the package cfg := &packages.Config{ - Dir: ws.dir, - Mode: packages.NeedSyntax | - packages.NeedImports | - packages.NeedDeps | - packages.NeedTypes | - packages.NeedModule | - packages.NeedTypesInfo, + Dir: ws.dir, + Mode: packagesLoadMode, // on Linux, leaving CGO_ENABLED to the default value of 1 would // cause an error: "could not import C (no metadata for C)", but From 5939781253dee8892a28ec275a85528005312719 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Thu, 24 Sep 2026 00:53:16 +0300 Subject: [PATCH 09/12] remove copilot-instructions.md Signed-off-by: Mohammed Al Sahaf --- .github/copilot-instructions.md | 99 --------------------------------- 1 file changed, 99 deletions(-) delete mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 05efdab..0000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,99 +0,0 @@ -# ModuleDoc AI Coding Instructions - -## Project Overview -ModuleDoc analyzes Go source code to generate JSON documentation for Caddy modules. It uses Go's AST parsing and type system to extract struct definitions, JSON tags, and documentation, specifically targeting Caddy's plugin architecture. - -## Core Architecture - -### Three-Layer System -1. **Driver** (`driver.go`) - Main API entry point with caching (`discoveredTypes` map) -2. **Workspace** (`workspace.go`) - Temporary Go module management with `go get` and package loading -3. **Synthesis** (`synthesis.go`) - Type representation building from Go AST/types - -### Key Data Flow -``` -LoadModulesFromImportingPackage → openWorkspace → findCaddyModuleIdents → buildRepresentation → Value struct -``` - -## Critical Patterns - -### AST Analysis for Caddy Modules -- Look for `caddy.RegisterModule()` calls AND `CaddyModule()` method implementations -- Extract module IDs from `caddy.ModuleInfo{ID: "module.name"}` return statements -- Both registration and implementation must exist (validated in `findCaddyModuleIdents`) - -### Type System Mapping -The `Value` struct is central - it flattens Go types into JSON-documentable forms: -- `Type` enum: primitives (`Bool`, `String`) + structural (`Struct`, `Array`, `Map`) + Caddy-specific (`Module`, `ModuleMap`) -- `SameAs` field enables type reuse via FQTN (fully-qualified type name) references -- `ModuleNamespace`/`ModuleInlineKey` handle Caddy's module configuration patterns - -### Workspace Management -- Creates temporary Go modules in `/tmp` for isolated package analysis -- Runs `go get` commands to fetch dependencies at specific versions -- Uses `golang.org/x/tools/go/packages` with specific `packages.Config` modes -- **Always** set `CGO_ENABLED=0` in test environments (Linux compatibility) - -### Storage Interface -Implement `Storage` for caching - methods: `GetTypeByName`, `GetTypesByCaddyModuleID`, `StoreType`, `SetCaddyModuleName` - -## Testing Patterns - -### Test Structure -- Two suites: baseline tests (document behavior and limitations) and the edge-case suite (assert correct behavior) -- Do not weaken an edge-case test's assertions to make it pass; fix the source bug instead -- Use `testdata/` fixture packages (one per directory) with minimal Caddy module examples -- Load packages with full AST/types info: `packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo` -- Set `CGO_ENABLED=0` in test environments to avoid "no metadata for C" errors -- Toolchain-dependent tests skip under `go test -short`; crash-prone tests (stack overflow, fatal map races) re-run themselves in a child process via `runIsolated` -- A reusable thread-safe in-memory mock Storage (`memStorage`) lives in `storage_edge_test.go` - -### Example Module Pattern (see `testdata/gizmo.go`): -```go -type ModuleName struct{} - -func init() { - caddy.RegisterModule(new(ModuleName)) -} - -func (*ModuleName) CaddyModule() caddy.ModuleInfo { - return caddy.ModuleInfo{ - ID: "namespace.module_name", - New: func() caddy.Module { return new(ModuleName) }, - } -} -``` - -## Key Constants & Conventions -- `CaddyCorePackage = "github.com/caddyserver/caddy/v2"` -- `registerModule = "RegisterModule"` -- FQTN format: `"package/path.TypeName@version"` -- Module ID format: dot-separated namespaces like `"http.handlers.file_server"` - -## JSON Tag Processing -- Uses `reflect.StructTag` to parse `json:` and `caddy:` tags -- JSON name extraction handles comma-separated options, excludes `"-"` fields -- Caddy tags parsed via `caddy.ParseStructTag()` for module configuration - -## Error Handling Patterns -- Extensive validation that module registration matches implementation -- AST inspection errors bubble up through `visitErr` pattern -- Package loading failures wrapped with context (`fmt.Errorf("loading package %s: %v", ...)`) - -## Recent Improvements (2025) - -None. An earlier version of this document claimed fixes (mutex locking, LRU cache with TTL, dynamic module ID support, graceful type fallbacks, relaxed validation) that were never implemented. Do not assume any of them exist. - -## Known Limitations & Open Bugs - -Most bugs found by the 2026-08 edge-case suite were fixed on 2026-08-22 (mutex-protected `discoveredTypes`, no-panic `TraverseType`, constant module ID evaluation, checked AST assertions, cycle guards, non-mutating `dereference` via `Value.clone()`, empty-value fallback for chan/func/generic fields). Remaining limitations: - -- **Runtime-computed module IDs**: only constant expressions are supported (evaluated via `go/types`); IDs computed at runtime are skipped with a warning. -- **Recursive types in deep dereference**: `deepDereference` detects `SameAs` cycles and returns a `circular type reference` error rather than resolving them. -- **Unbounded caches**: per-workspace package caches have no TTL, size limit, or eviction (memory-growth issue only; access is properly locked). -- **Strict validation**: one incomplete module (registration without implementation, or vice versa) fails the entire package load. - -## Development Notes -- Requires Go toolchain installed (uses `go` commands directly) -- Driver is safe for concurrent use: `discoveredTypes` is mutex-protected and `dereference` works on deep copies (`Value.clone()`) -- Caching is per-workspace and unbounded (no eviction) \ No newline at end of file From 694232649775ea49f02015b62b38ab72c82eb7d3 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Thu, 24 Sep 2026 01:00:35 +0300 Subject: [PATCH 10/12] add documentation for testdata pkgs Signed-off-by: Mohammed Al Sahaf --- testdata/constid/module.go | 3 +++ testdata/crosspkg/module.go | 3 +++ testdata/exprid/module.go | 3 +++ testdata/gizmo.go | 3 +++ testdata/kinds/types.go | 6 ++++++ testdata/noimpl/module.go | 3 +++ testdata/unregistered/module.go | 3 +++ testdata/valuerecv/module.go | 3 +++ 8 files changed, 27 insertions(+) diff --git a/testdata/constid/module.go b/testdata/constid/module.go index 631535b..672846b 100644 --- a/testdata/constid/module.go +++ b/testdata/constid/module.go @@ -1,3 +1,6 @@ +// Package constid is a module whose ID is a package-level constant +// rather than a string literal, exercising constant-expression +// evaluation in module ID discovery. package constid import "github.com/caddyserver/caddy/v2" diff --git a/testdata/crosspkg/module.go b/testdata/crosspkg/module.go index 28d1a57..46c4774 100644 --- a/testdata/crosspkg/module.go +++ b/testdata/crosspkg/module.go @@ -1,3 +1,6 @@ +// Package crosspkg registers a type declared in another package +// (a qualified composite literal, valuerecv.Sprocket{}), exercising +// the AST handling of selector-expression registration arguments. package crosspkg import ( diff --git a/testdata/exprid/module.go b/testdata/exprid/module.go index 10c82fd..5b5db1a 100644 --- a/testdata/exprid/module.go +++ b/testdata/exprid/module.go @@ -1,3 +1,6 @@ +// Package exprid is a module whose ID is built from concatenating a +// package-level constant with a string literal, exercising constant +// binary-expression evaluation in module ID discovery. package exprid import "github.com/caddyserver/caddy/v2" diff --git a/testdata/gizmo.go b/testdata/gizmo.go index a24cb40..112e6ed 100644 --- a/testdata/gizmo.go +++ b/testdata/gizmo.go @@ -1,3 +1,6 @@ +// Package testdata is the canonical minimal valid Caddy module: a +// pointer-receiver type with both caddy.RegisterModule and CaddyModule, +// and a static string literal module ID. package testdata import "github.com/caddyserver/caddy/v2" diff --git a/testdata/kinds/types.go b/testdata/kinds/types.go index bfe54fd..055f162 100644 --- a/testdata/kinds/types.go +++ b/testdata/kinds/types.go @@ -1,3 +1,9 @@ +// Package kinds exercises the struct field kinds moduledoc's type +// synthesis must handle: pointer-to-pointer, unexported and json:"-" +// fields, non-string map keys, interface{}, json.RawMessage as a Caddy +// module (with namespace/inline_key tags), embedded field promotion, +// named vs. inline anonymous structs, self-referential types, and +// field kinds with no JSON representation (chan, func). package kinds import "encoding/json" diff --git a/testdata/noimpl/module.go b/testdata/noimpl/module.go index 43f0f1f..9390052 100644 --- a/testdata/noimpl/module.go +++ b/testdata/noimpl/module.go @@ -1,3 +1,6 @@ +// Package noimpl registers a type that satisfies caddy.Module only +// through an embedded field, so the package has a registration but no +// locally declared CaddyModule method. package noimpl import ( diff --git a/testdata/unregistered/module.go b/testdata/unregistered/module.go index 9c0f4af..f984257 100644 --- a/testdata/unregistered/module.go +++ b/testdata/unregistered/module.go @@ -1,3 +1,6 @@ +// Package unregistered pairs one fully compliant module with one type +// that implements CaddyModule but is never registered, exercising the +// strict validation that fails an entire package on partial compliance. package unregistered import "github.com/caddyserver/caddy/v2" diff --git a/testdata/valuerecv/module.go b/testdata/valuerecv/module.go index 06b02d7..1b9ac1b 100644 --- a/testdata/valuerecv/module.go +++ b/testdata/valuerecv/module.go @@ -1,3 +1,6 @@ +// Package valuerecv registers a module via a value receiver and a +// composite literal (caddy.RegisterModule(Sprocket{})), rather than +// the more common pointer-receiver plus new(...) pattern. package valuerecv import "github.com/caddyserver/caddy/v2" From 77a57bb15f31f19d8eeb6dbaee861708673185c6 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Thu, 24 Sep 2026 01:11:18 +0300 Subject: [PATCH 11/12] more comments on testdata content Signed-off-by: Mohammed Al Sahaf --- testdata/aliases/aliases.go | 6 ++++++ testdata/constid/module.go | 3 +++ testdata/crosspkg/module.go | 3 +++ testdata/exprid/module.go | 3 +++ testdata/gizmo.go | 2 ++ testdata/kinds/types.go | 9 +++++++++ testdata/noimpl/module.go | 2 ++ testdata/unregistered/module.go | 3 +++ testdata/valuerecv/module.go | 3 +++ 9 files changed, 34 insertions(+) diff --git a/testdata/aliases/aliases.go b/testdata/aliases/aliases.go index b1641da..c9812bb 100644 --- a/testdata/aliases/aliases.go +++ b/testdata/aliases/aliases.go @@ -1,6 +1,12 @@ // Package aliases is a test fixture exercising type-alias handling in // moduledoc under Go 1.23+ (where go/types materialises aliases as // *types.Alias). +// +// Expected (see synthesis_test.go, utils_test.go): buildRepresentation +// unwraps aliases so Config.Nested resolves to the Settings struct, +// Config.Label resolves to a plain string, and Config.Items resolves +// to a slice of the Item struct — the alias names themselves do not +// appear as distinct types. package aliases // Settings holds nested configuration used to verify alias unwrapping. diff --git a/testdata/constid/module.go b/testdata/constid/module.go index 672846b..160af7d 100644 --- a/testdata/constid/module.go +++ b/testdata/constid/module.go @@ -1,6 +1,9 @@ // Package constid is a module whose ID is a package-level constant // rather than a string literal, exercising constant-expression // evaluation in module ID discovery. +// +// Expected: findCaddyModuleIdents discovers ConstWidget as +// "app.namespace.const_widget". package constid import "github.com/caddyserver/caddy/v2" diff --git a/testdata/crosspkg/module.go b/testdata/crosspkg/module.go index 46c4774..8cf7e76 100644 --- a/testdata/crosspkg/module.go +++ b/testdata/crosspkg/module.go @@ -1,6 +1,9 @@ // Package crosspkg registers a type declared in another package // (a qualified composite literal, valuerecv.Sprocket{}), exercising // the AST handling of selector-expression registration arguments. +// +// Expected: findCaddyModuleIdents must not panic; it may return an +// error (no local CaddyModule method) or an empty result. package crosspkg import ( diff --git a/testdata/exprid/module.go b/testdata/exprid/module.go index 5b5db1a..6c8744f 100644 --- a/testdata/exprid/module.go +++ b/testdata/exprid/module.go @@ -1,6 +1,9 @@ // Package exprid is a module whose ID is built from concatenating a // package-level constant with a string literal, exercising constant // binary-expression evaluation in module ID discovery. +// +// Expected: findCaddyModuleIdents discovers ExprWidget as +// "app.namespace.expr_widget". package exprid import "github.com/caddyserver/caddy/v2" diff --git a/testdata/gizmo.go b/testdata/gizmo.go index 112e6ed..fc2a6e8 100644 --- a/testdata/gizmo.go +++ b/testdata/gizmo.go @@ -1,6 +1,8 @@ // Package testdata is the canonical minimal valid Caddy module: a // pointer-receiver type with both caddy.RegisterModule and CaddyModule, // and a static string literal module ID. +// +// Expected: findCaddyModuleIdents discovers Gizmo as "app.namespace.gizmo". package testdata import "github.com/caddyserver/caddy/v2" diff --git a/testdata/kinds/types.go b/testdata/kinds/types.go index 055f162..a16829e 100644 --- a/testdata/kinds/types.go +++ b/testdata/kinds/types.go @@ -4,6 +4,15 @@ // module (with namespace/inline_key tags), embedded field promotion, // named vs. inline anonymous structs, self-referential types, and // field kinds with no JSON representation (chan, func). +// +// Expected (see synthesis_edge_test.go): Hidden, unexported, and +// Embedded are absent from the built representation; DoublePtr +// resolves to Int; Lookup is a Map with Int keys; Anything is an +// empty Value; Raw is a Module with namespace "widget.raw" and +// inline key "kind"; RawMap is a ModuleMap; Extra is promoted onto +// the parent struct; Nested is stored and referenced by name; Inline +// is represented with its own fields; Node builds without recursing +// forever; Tricky's chan/func fields build without erroring. package kinds import "encoding/json" diff --git a/testdata/noimpl/module.go b/testdata/noimpl/module.go index 9390052..8df6dbb 100644 --- a/testdata/noimpl/module.go +++ b/testdata/noimpl/module.go @@ -1,6 +1,8 @@ // Package noimpl registers a type that satisfies caddy.Module only // through an embedded field, so the package has a registration but no // locally declared CaddyModule method. +// +// Expected: findCaddyModuleIdents returns an error. package noimpl import ( diff --git a/testdata/unregistered/module.go b/testdata/unregistered/module.go index f984257..ba66b9f 100644 --- a/testdata/unregistered/module.go +++ b/testdata/unregistered/module.go @@ -1,6 +1,9 @@ // Package unregistered pairs one fully compliant module with one type // that implements CaddyModule but is never registered, exercising the // strict validation that fails an entire package on partial compliance. +// +// Expected: findCaddyModuleIdents returns an error for the whole +// package, even though Good is otherwise fully compliant. package unregistered import "github.com/caddyserver/caddy/v2" diff --git a/testdata/valuerecv/module.go b/testdata/valuerecv/module.go index 1b9ac1b..8370117 100644 --- a/testdata/valuerecv/module.go +++ b/testdata/valuerecv/module.go @@ -1,6 +1,9 @@ // Package valuerecv registers a module via a value receiver and a // composite literal (caddy.RegisterModule(Sprocket{})), rather than // the more common pointer-receiver plus new(...) pattern. +// +// Expected: findCaddyModuleIdents discovers Sprocket as +// "app.namespace.sprocket". package valuerecv import "github.com/caddyserver/caddy/v2" From 0b971eb329fe5e727abae9cbdfb1c8f4743ffb03 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Fri, 25 Sep 2026 01:11:24 +0300 Subject: [PATCH 12/12] remove duplicated test Signed-off-by: Mohammed Al Sahaf --- baseline_behavior_test.go | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/baseline_behavior_test.go b/baseline_behavior_test.go index 7c17e09..2bfc03f 100644 --- a/baseline_behavior_test.go +++ b/baseline_behavior_test.go @@ -43,37 +43,4 @@ func TestOriginalBehaviorBaseline(t *testing.T) { t.Errorf("expected module 'app.namespace.gizmo', got: %v", moduleIdents) } }) - - t.Run("StrictValidationBehavior", func(t *testing.T) { - // one incomplete module fails the whole package, valid sibling - // included; a partial-results contract would return GoodModule here - source := ` -package test - -import "github.com/caddyserver/caddy/v2" - -func init() { - caddy.RegisterModule(new(GoodModule)) -} - -type GoodModule struct{} - -func (*GoodModule) CaddyModule() caddy.ModuleInfo { - return caddy.ModuleInfo{ - ID: "app.test.good", - New: func() caddy.Module { return new(GoodModule) }, - } -} - -type OrphanModule struct{} - -func (*OrphanModule) CaddyModule() caddy.ModuleInfo { - return caddy.ModuleInfo{ - ID: "app.test.orphan", - New: func() caddy.Module { return new(OrphanModule) }, - } -} -` - testModuleSource(t, source, false, "Mixed valid/incomplete package fails entirely") - }) }