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/" const internalImportPrefix = "gitea.maximumdirect.net/eric/notarius/internal/" 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: "production CLI cannot import family root", filename: "internal/cli/catalog.go", sourcePackage: "cli", importPath: moduleImportPrefix + "almanac", wantError: true, }, { name: "production CLI cannot import concrete leaf", filename: "internal/cli/catalog.go", sourcePackage: "cli", importPath: moduleImportPrefix + "almanac/extract/events", wantError: true, }, { name: "production CLI cannot import generic leaf", filename: "internal/cli/catalog.go", sourcePackage: "cli", importPath: moduleImportPrefix + "generic/output/json", wantError: true, }, { name: "production CLI cannot import non-registrar package", filename: "internal/cli/catalog.go", sourcePackage: "cli", importPath: moduleImportPrefix + "almanac/register/helpers", wantError: true, }, { name: "CLI test may import concrete leaf", filename: "internal/cli/compatibility_test.go", sourcePackage: "cli", importPath: moduleImportPrefix + "almanac/extract/events", }, { name: "command production cannot import registrar", filename: "cmd/notarius/main.go", sourcePackage: "main", importPath: moduleImportPrefix + "almanac/register", wantError: true, }, { name: "command production cannot import concrete leaf", filename: "cmd/notarius/main.go", sourcePackage: "main", importPath: moduleImportPrefix + "almanac/extract/events", wantError: true, }, { name: "unknown production package cannot import concrete leaf", filename: "internal/application/bootstrap.go", sourcePackage: "application", importPath: moduleImportPrefix + "almanac/extract/events", wantError: true, }, { name: "unknown production package cannot import registrar", filename: "internal/application/bootstrap.go", sourcePackage: "application", importPath: moduleImportPrefix + "almanac/register", wantError: true, }, { name: "unknown test package is not a compatibility root", filename: "internal/application/bootstrap_test.go", sourcePackage: "application", importPath: moduleImportPrefix + "almanac/extract/events", wantError: true, }, { name: "framework production cannot import concrete module", filename: "internal/framework/pipeline/runner.go", sourcePackage: "pipeline", importPath: moduleImportPrefix + "almanac/extract/events", wantError: true, }, { name: "framework production cannot import generic module", filename: "internal/framework/pipeline/runner.go", sourcePackage: "pipeline", importPath: moduleImportPrefix + "generic/normalize/noop", wantError: true, }, { name: "core production cannot import concrete module", filename: "internal/core/source/source.go", sourcePackage: "source", importPath: moduleImportPrefix + "almanac", wantError: true, }, { name: "core production cannot import generic module", filename: "internal/core/source/source.go", sourcePackage: "source", importPath: moduleImportPrefix + "generic/chunk/units", wantError: true, }, { name: "source production may import another core package", filename: "internal/core/source/source.go", sourcePackage: "source", importPath: internalImportPrefix + "artifacts", }, { name: "source production cannot import framework", filename: "internal/core/source/source.go", sourcePackage: "source", importPath: internalImportPrefix + "framework/contracts", wantError: true, }, { name: "source production cannot import modules", filename: "internal/core/source/source.go", sourcePackage: "source", importPath: moduleImportPrefix + "dnd", wantError: true, }, { name: "chunkplan production may import framework", filename: "internal/framework/chunkplan/store.go", sourcePackage: "chunkplan", importPath: internalImportPrefix + "framework/contracts", }, { name: "chunkplan production cannot import modules", filename: "internal/framework/chunkplan/store.go", sourcePackage: "chunkplan", importPath: moduleImportPrefix + "dnd", wantError: true, }, { name: "framework test may import module implementation", filename: "internal/framework/pipeline/compatibility_test.go", sourcePackage: "pipeline", importPath: moduleImportPrefix + "generic/chunk/units", }, { name: "core test may import module implementation", filename: "internal/core/source/compatibility_test.go", sourcePackage: "source", importPath: moduleImportPrefix + "almanac", }, { name: "module production cannot import integration infrastructure", filename: "internal/modules/almanac/register/register.go", sourcePackage: "register", importPath: moduleImportPrefix + "integration", wantError: true, }, { name: "non-module production cannot import integration infrastructure", filename: "cmd/notarius/main.go", sourcePackage: "main", importPath: moduleImportPrefix + "integration/helpers", wantError: true, }, { name: "test may import integration infrastructure", filename: "internal/cli/compatibility_test.go", sourcePackage: "cli", importPath: moduleImportPrefix + "integration", }, { 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 && (!strings.Contains(err.Error(), tt.filename) || !strings.Contains(err.Error(), tt.importPath)) { t.Fatalf("validateImport() error = %q, want importing file and import target", err) } 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 err } } return nil } func validateImport(filename string, sourcePackage string, importPath string) error { if !strings.HasSuffix(filename, "_test.go") && strings.HasPrefix(filename, "internal/core/source/") && strings.HasPrefix(importPath, internalImportPrefix+"framework/") { return importBoundaryViolation(filename, importPath, "core/source production code must not import framework or module implementations") } target, ok := moduleTargetForImport(importPath) if !ok { return nil } isTest := strings.HasSuffix(filename, "_test.go") if target.integration && !isTest { return importBoundaryViolation(filename, importPath, "module integration infrastructure is not a production dependency target") } if isIntegrationFile(filename) { if isBlackBoxIntegrationTest(filename, sourcePackage) { return nil } return importBoundaryViolation(filename, importPath, "module integration composition is allowed only in black-box tests") } sourceFamily, sourceRoot, sourceRegistrar := moduleFamilyForFile(filename) if sourceFamily != "" { if sourceRoot && sourceFamily == target.family && target.child { return importBoundaryViolation(filename, importPath, "family root must not import child packages") } if sourceFamily == target.family { return nil } if sourceFamily == "generic" { return importBoundaryViolation(filename, importPath, fmt.Sprintf("generic family must not import concrete family %q", target.family)) } if target.family == "generic" { if sourceRegistrar { return nil } return importBoundaryViolation(filename, importPath, fmt.Sprintf("concrete family %q may import generic implementations only from its registrar", sourceFamily)) } return importBoundaryViolation(filename, importPath, fmt.Sprintf("concrete family %q must not import concrete family %q", sourceFamily, target.family)) } if isTest { if isCompatibilityTestFile(filename) { return nil } return importBoundaryViolation(filename, importPath, "direct module imports from non-module tests are allowed only in CLI, core, and framework compatibility-test roots") } if strings.HasPrefix(filename, "internal/core/source/") && strings.HasPrefix(importPath, moduleImportPrefix) { return importBoundaryViolation(filename, importPath, "core/source production code must not import framework or module implementations") } if strings.HasPrefix(filename, "internal/framework/chunkplan/") && strings.HasPrefix(importPath, moduleImportPrefix) { return importBoundaryViolation(filename, importPath, "framework/chunkplan production code must not import module implementations") } if strings.HasPrefix(filename, "internal/framework/") || strings.HasPrefix(filename, "internal/core/") { return importBoundaryViolation(filename, importPath, "core and framework production code must not import module implementations") } if strings.HasPrefix(filename, "internal/cli/") { if target.registrar { return nil } return importBoundaryViolation(filename, importPath, "CLI production code may import only exact module family registrar packages") } return importBoundaryViolation(filename, importPath, "production code outside module families may import modules only from the CLI composition root through exact registrar packages") } type moduleImportTarget struct { family string child bool registrar bool integration bool } func moduleTargetForImport(importPath string) (moduleImportTarget, bool) { if !strings.HasPrefix(importPath, moduleImportPrefix) { return moduleImportTarget{}, false } remainder := strings.TrimPrefix(importPath, moduleImportPrefix) parts := strings.Split(remainder, "/") if len(parts) == 0 || parts[0] == "" { return moduleImportTarget{}, false } return moduleImportTarget{ family: parts[0], child: len(parts) > 1, registrar: len(parts) == 2 && parts[1] == "register", integration: parts[0] == "integration", }, true } func importBoundaryViolation(filename string, importPath string, rule string) error { return fmt.Errorf("import boundary violation: %s imports %s: %s", filename, importPath, rule) } 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 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 isCompatibilityTestFile(filename string) bool { if !strings.HasSuffix(filename, "_test.go") { return false } return strings.HasPrefix(filename, "internal/cli/") || strings.HasPrefix(filename, "internal/core/") || strings.HasPrefix(filename, "internal/framework/") } 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), "..", "..")) }