package modules_test import ( "fmt" "go/parser" "go/token" "io/fs" "path/filepath" "runtime" "strconv" "strings" "testing" ) const moduleImportPrefix = "gitea.maximumdirect.net/eric/notarius/internal/modules/" func TestProductionImportBoundaries(t *testing.T) { repositoryRoot := testRepositoryRoot(t) err := filepath.WalkDir(repositoryRoot, func(path string, entry fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } if entry.IsDir() { if entry.Name() == ".git" || entry.Name() == "testdata" || entry.Name() == "vendor" { return filepath.SkipDir } return nil } if filepath.Ext(path) != ".go" { return nil } return checkImportBoundaries(repositoryRoot, path) }) if err != nil { t.Fatal(err) } } func TestImportBoundaryFixtureIsRejected(t *testing.T) { repositoryRoot := testRepositoryRoot(t) fixture := filepath.Join(repositoryRoot, "internal", "modules", "generic", "testdata", "importboundaries", "imports_dnd.go") err := checkImportBoundaries(repositoryRoot, fixture) if err == nil { t.Fatal("fixture import was accepted, want generic-to-D&D violation") } if !strings.Contains(err.Error(), "generic packages must not import D&D packages") { t.Fatalf("fixture error = %q, want generic-to-D&D violation", err) } } func TestImportBoundaryRules(t *testing.T) { tests := []struct { name string filename string importPath string wantError bool }{ { name: "D&D implementation cannot import Seriatim", filename: "internal/modules/dnd/extract/example/extractor.go", importPath: moduleImportPrefix + "seriatim/input/transcript", wantError: true, }, { name: "Seriatim implementation cannot import D&D", filename: "internal/modules/seriatim/input/example/adapter.go", importPath: moduleImportPrefix + "dnd/shared", wantError: true, }, { name: "generic implementation cannot import D&D", filename: "internal/modules/generic/merge/example/merger.go", importPath: moduleImportPrefix + "dnd/shared", wantError: true, }, { name: "domain root cannot import child", filename: "internal/modules/dnd/types.go", importPath: moduleImportPrefix + "dnd/extract/spells", wantError: true, }, { name: "D&D implementation may import generic implementation", filename: "internal/modules/dnd/extract/example/extractor.go", importPath: moduleImportPrefix + "generic/normalize/noop", }, { name: "domain registrar may compose child packages", filename: "internal/modules/dnd/register/register.go", importPath: moduleImportPrefix + "dnd/extract/spells", }, { name: "CLI may compose registrars", filename: "internal/cli/catalog.go", importPath: moduleImportPrefix + "dnd/register", }, { name: "external integration test may compose domains", filename: "internal/modules/integration/example_test.go", importPath: moduleImportPrefix + "dnd/extract/spells", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateImport(tt.filename, tt.importPath) if tt.wantError && err == nil { t.Fatal("validateImport() error = nil, want boundary violation") } if !tt.wantError && err != nil { t.Fatalf("validateImport() error = %v, want nil", err) } }) } } func checkImportBoundaries(repositoryRoot string, filename string) error { parsed, err := parser.ParseFile(token.NewFileSet(), filename, nil, parser.ImportsOnly) if err != nil { return fmt.Errorf("parse %s: %w", filename, err) } relative, err := filepath.Rel(repositoryRoot, filename) if err != nil { return fmt.Errorf("resolve relative path for %s: %w", filename, err) } relative = filepath.ToSlash(relative) for _, imported := range parsed.Imports { importPath, err := strconv.Unquote(imported.Path.Value) if err != nil { return fmt.Errorf("parse import in %s: %w", relative, err) } if err := validateImport(relative, importPath); err != nil { return fmt.Errorf("%s imports %s: %w", relative, importPath, err) } } return nil } func validateImport(filename string, importPath string) error { if isExternalIntegrationTest(filename) { return nil } sourceDomain, sourceRoot := domainForFile(filename) targetDomain, targetChild := domainForImport(importPath) if sourceDomain == "" || targetDomain == "" { return nil } if sourceRoot && sourceDomain == targetDomain && targetChild { return fmt.Errorf("domain root packages must not import child implementations") } if sourceDomain == "generic" && targetDomain == "dnd" { return fmt.Errorf("generic packages must not import D&D packages") } if sourceDomain == "dnd" && targetDomain == "seriatim" { return fmt.Errorf("D&D packages must not import Seriatim packages") } if sourceDomain == "seriatim" && targetDomain == "dnd" { return fmt.Errorf("Seriatim packages must not import D&D packages") } return nil } func domainForFile(filename string) (domain string, root bool) { const prefix = "internal/modules/" if !strings.HasPrefix(filename, prefix) { return "", false } remainder := strings.TrimPrefix(filename, prefix) parts := strings.Split(remainder, "/") if len(parts) < 2 || !isDomain(parts[0]) { return "", false } return parts[0], len(parts) == 2 } func domainForImport(importPath string) (domain string, child bool) { if !strings.HasPrefix(importPath, moduleImportPrefix) { return "", false } remainder := strings.TrimPrefix(importPath, moduleImportPrefix) parts := strings.Split(remainder, "/") if len(parts) == 0 || !isDomain(parts[0]) { return "", false } return parts[0], len(parts) > 1 } func isDomain(name string) bool { return name == "dnd" || name == "generic" || name == "seriatim" } func isExternalIntegrationTest(filename string) bool { return strings.HasPrefix(filename, "internal/modules/integration/") && strings.HasSuffix(filename, "_test.go") } func testRepositoryRoot(t *testing.T) string { t.Helper() _, filename, _, ok := runtime.Caller(0) if !ok { t.Fatal("resolve import-boundary test location") } return filepath.Clean(filepath.Join(filepath.Dir(filename), "..", "..")) }