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-concrete violation") } if !strings.Contains(err.Error(), "generic family must not import concrete family") { t.Fatalf("fixture error = %q, want generic-to-concrete violation", err) } } func TestImportBoundaryRules(t *testing.T) { tests := []struct { name string filename string sourcePackage string importPath string wantError bool }{ { name: "concrete implementation cannot import peer concrete family", filename: "internal/modules/dnd/extract/example/extractor.go", sourcePackage: "example", importPath: moduleImportPrefix + "seriatim/input/transcript", wantError: true, }, { name: "future concrete family cannot import current concrete family", filename: "internal/modules/almanac/extract/example/extractor.go", sourcePackage: "example", importPath: moduleImportPrefix + "dnd/shared", wantError: true, }, { name: "current concrete family cannot import future concrete family", filename: "internal/modules/seriatim/input/example/adapter.go", sourcePackage: "example", importPath: moduleImportPrefix + "almanac/shared", wantError: true, }, { name: "generic implementation cannot import current concrete family", filename: "internal/modules/generic/merge/example/merger.go", sourcePackage: "example", importPath: moduleImportPrefix + "dnd/shared", wantError: true, }, { name: "generic implementation cannot import future concrete family", filename: "internal/modules/generic/merge/example/merger.go", sourcePackage: "example", importPath: moduleImportPrefix + "almanac/shared", wantError: true, }, { name: "concrete implementation cannot import generic implementation", filename: "internal/modules/dnd/extract/example/extractor.go", sourcePackage: "example", importPath: moduleImportPrefix + "generic/normalize/noop", wantError: true, }, { name: "concrete white-box test follows production rules", filename: "internal/modules/dnd/extract/example/extractor_test.go", sourcePackage: "example", importPath: moduleImportPrefix + "generic/normalize/noop", wantError: true, }, { name: "concrete registrar may compose generic implementation", filename: "internal/modules/almanac/register/register.go", sourcePackage: "register", importPath: moduleImportPrefix + "generic/normalize/noop", }, { name: "family root cannot import child implementation", filename: "internal/modules/almanac/types.go", sourcePackage: "almanac", importPath: moduleImportPrefix + "almanac/extract/events", wantError: true, }, { name: "family root cannot import registrar", filename: "internal/modules/almanac/types.go", sourcePackage: "almanac", importPath: moduleImportPrefix + "almanac/register", wantError: true, }, { name: "child may import family root", filename: "internal/modules/almanac/extract/events/extractor.go", sourcePackage: "events", importPath: moduleImportPrefix + "almanac", }, { name: "child may import same-family sibling", filename: "internal/modules/almanac/extract/events/extractor.go", sourcePackage: "events", importPath: moduleImportPrefix + "almanac/shared", }, { name: "concrete registrar may compose own child", filename: "internal/modules/almanac/register/register.go", sourcePackage: "register", importPath: moduleImportPrefix + "almanac/extract/events", }, { name: "generic registrar may compose generic child", filename: "internal/modules/generic/register/register.go", sourcePackage: "register", importPath: moduleImportPrefix + "generic/output/json", }, { name: "application composition root may compose registrars", filename: "internal/cli/catalog.go", sourcePackage: "cli", importPath: moduleImportPrefix + "almanac/register", }, { name: "black-box integration test may compose families", filename: "internal/modules/integration/example_test.go", sourcePackage: "integration_test", importPath: moduleImportPrefix + "almanac/extract/events", }, { name: "white-box integration test is not exempt", filename: "internal/modules/integration/example_test.go", sourcePackage: "integration", importPath: moduleImportPrefix + "almanac/extract/events", wantError: true, }, { name: "integration production file is not exempt", filename: "internal/modules/integration/example.go", sourcePackage: "integration", importPath: moduleImportPrefix + "almanac/extract/events", wantError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateImport(tt.filename, tt.sourcePackage, 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, parsed.Name.Name, importPath); err != nil { return fmt.Errorf("%s imports %s: %w", relative, importPath, err) } } return nil } func validateImport(filename string, sourcePackage string, importPath string) error { if isBlackBoxIntegrationTest(filename, sourcePackage) { return nil } targetFamily, targetChild := moduleFamilyForImport(importPath) if targetFamily == "" { return nil } if isIntegrationFile(filename) { return fmt.Errorf("module integration composition is allowed only in black-box tests") } sourceFamily, sourceRoot, sourceRegistrar := moduleFamilyForFile(filename) if sourceFamily == "" { return nil } if sourceRoot && sourceFamily == targetFamily && targetChild { return fmt.Errorf("family root must not import child packages") } if sourceFamily == targetFamily { return nil } if sourceFamily == "generic" { return fmt.Errorf("generic family must not import concrete family %q", targetFamily) } if targetFamily == "generic" { if sourceRegistrar { return nil } return fmt.Errorf("concrete family %q may import generic implementations only from its registrar", sourceFamily) } return fmt.Errorf("concrete family %q must not import concrete family %q", sourceFamily, targetFamily) } func moduleFamilyForFile(filename string) (family string, root bool, registrar bool) { const prefix = "internal/modules/" if !strings.HasPrefix(filename, prefix) { return "", false, false } remainder := strings.TrimPrefix(filename, prefix) parts := strings.Split(remainder, "/") if len(parts) < 2 || parts[0] == "integration" { return "", false, false } return parts[0], len(parts) == 2, len(parts) > 2 && parts[1] == "register" } func moduleFamilyForImport(importPath string) (family string, child bool) { if !strings.HasPrefix(importPath, moduleImportPrefix) { return "", false } remainder := strings.TrimPrefix(importPath, moduleImportPrefix) parts := strings.Split(remainder, "/") if len(parts) == 0 || parts[0] == "" || parts[0] == "integration" { return "", false } return parts[0], len(parts) > 1 } func isIntegrationFile(filename string) bool { return strings.HasPrefix(filename, "internal/modules/integration/") } func isBlackBoxIntegrationTest(filename string, sourcePackage string) bool { return isIntegrationFile(filename) && strings.HasSuffix(filename, "_test.go") && sourcePackage == "integration_test" } 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), "..", "..")) }