238 lines
7.9 KiB
Go
238 lines
7.9 KiB
Go
package main
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/importer"
|
|
"go/parser"
|
|
"go/token"
|
|
"go/types"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestExportedContractBoundaries(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
source string
|
|
wantError string
|
|
}{
|
|
{
|
|
name: "leaked implementation type",
|
|
source: `package facade
|
|
import "example.com/implementation"
|
|
func New() *implementation.State { return nil }
|
|
`,
|
|
wantError: "exposes named type example.com/implementation.State",
|
|
},
|
|
{
|
|
name: "alias to implementation type",
|
|
source: `package facade
|
|
import "example.com/implementation"
|
|
type Alias = implementation.State
|
|
`,
|
|
wantError: "exposes named type example.com/implementation.State",
|
|
},
|
|
{
|
|
name: "callback leaks implementation type",
|
|
source: `package facade
|
|
import "example.com/implementation"
|
|
type Callback func() *implementation.State
|
|
`,
|
|
wantError: "exposes named type example.com/implementation.State",
|
|
},
|
|
{
|
|
name: "generic constraint leaks implementation type",
|
|
source: `package facade
|
|
import "example.com/implementation"
|
|
type Box[T interface{ Value() *implementation.State }] struct { value T }
|
|
`,
|
|
wantError: "exposes named type example.com/implementation.State",
|
|
},
|
|
{
|
|
name: "generic instantiation leaks implementation type",
|
|
source: `package facade
|
|
import "example.com/implementation"
|
|
type Box[T any] struct { value T }
|
|
func New() Box[implementation.State] { return Box[implementation.State]{} }
|
|
`,
|
|
wantError: "exposes named type example.com/implementation.State",
|
|
},
|
|
{
|
|
name: "promoted implementation method",
|
|
source: `package facade
|
|
import "example.com/implementation"
|
|
type state = implementation.State
|
|
type Handle struct { state }
|
|
`,
|
|
wantError: "promotes method Promoted from example.com/implementation",
|
|
},
|
|
{
|
|
name: "promoted pointer implementation method",
|
|
source: `package facade
|
|
import "example.com/implementation"
|
|
type state = implementation.State
|
|
type Handle struct { state }
|
|
`,
|
|
wantError: "promotes method PointerPromoted from example.com/implementation",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
violations := checkFixture(t, test.source)
|
|
if !strings.Contains(strings.Join(violations, "\n"), test.wantError) {
|
|
t.Fatalf("checkExports() = %v, want an error containing %q", violations, test.wantError)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPrivateImplementationFieldAndSelectedStandardTypes(t *testing.T) {
|
|
source := `package facade
|
|
import (
|
|
"context"
|
|
"example.com/implementation"
|
|
)
|
|
type Handle struct { state *implementation.State }
|
|
func Open(context.Context) *Handle { return nil }
|
|
`
|
|
if violations := checkFixture(t, source); len(violations) != 0 {
|
|
t.Fatalf("private implementation field and selected standard type were rejected: %v", violations)
|
|
}
|
|
}
|
|
|
|
func TestValidationDiagnosticTypesBelongToFacade(t *testing.T) {
|
|
valid := `package facade
|
|
type ValidationCheck string
|
|
type ValidationFinding struct{}
|
|
type FileSizeMeasurement struct{}
|
|
type ValidationError struct {
|
|
Check ValidationCheck
|
|
Findings []ValidationFinding
|
|
Measurement *FileSizeMeasurement
|
|
Cause error
|
|
}
|
|
`
|
|
pkg := typecheck(t, token.NewFileSet(), modulePath, valid)
|
|
if violations := checkValidationTypes(pkg); len(violations) != 0 {
|
|
t.Fatalf("checkValidationTypes() = %v, want facade-owned diagnostics", violations)
|
|
}
|
|
|
|
implementation := typecheck(t, token.NewFileSet(), "example.com/implementation", `package implementation
|
|
type State string
|
|
`)
|
|
fset := token.NewFileSet()
|
|
fixtureImporter := singlePackageImporter{path: "example.com/implementation", pkg: implementation}
|
|
invalid := strings.Replace(valid, "type ValidationCheck string", "import \"example.com/implementation\"\ntype ValidationCheck = implementation.State", 1)
|
|
pkg = typecheckWithImporter(t, fset, modulePath, invalid, fixtureImporter)
|
|
if violations := checkValidationTypes(pkg); !strings.Contains(strings.Join(violations, "\n"), "ValidationCheck is not a facade-owned named type") {
|
|
t.Fatalf("checkValidationTypes() = %v, want ownership violation", violations)
|
|
}
|
|
}
|
|
|
|
func TestActualRepositoryBoundaries(t *testing.T) {
|
|
if err := checkRepository(t.Context(), "../.."); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestInternalPackagesCannotImportPublicFacades(t *testing.T) {
|
|
packages := []listedPackage{
|
|
{ImportPath: modulePath + "/internal/operations", Imports: []string{modulePath}},
|
|
{ImportPath: modulePath + "/cmd/releasekit/internal/cli", Imports: []string{modulePath + "/buildinfo"}},
|
|
{ImportPath: modulePath},
|
|
{ImportPath: modulePath + "/buildinfo"},
|
|
}
|
|
err := checkImportDirections(packages)
|
|
if err == nil ||
|
|
!strings.Contains(err.Error(), "internal package "+modulePath+"/internal/operations imports public or command package "+modulePath) ||
|
|
!strings.Contains(err.Error(), "internal package "+modulePath+"/cmd/releasekit/internal/cli imports public or command package "+modulePath+"/buildinfo") {
|
|
t.Fatalf("checkImportDirections() error = %v, want internal-to-facade violation", err)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeFacadeCannotDependOnExternalPackages(t *testing.T) {
|
|
packages := []listedPackage{
|
|
{ImportPath: modulePath + "/buildinfo", Imports: []string{"example.com/runtime"}},
|
|
{ImportPath: "example.com/runtime"},
|
|
}
|
|
err := checkImportDirections(packages)
|
|
if err == nil || !strings.Contains(err.Error(), "buildinfo depends on non-runtime package example.com/runtime") {
|
|
t.Fatalf("checkImportDirections() error = %v, want runtime dependency violation", err)
|
|
}
|
|
}
|
|
|
|
func checkFixture(t *testing.T, facadeSource string) []string {
|
|
t.Helper()
|
|
fset := token.NewFileSet()
|
|
implementation := typecheck(t, fset, "example.com/implementation", `package implementation
|
|
type State struct{}
|
|
func (State) Promoted() {}
|
|
func (*State) PointerPromoted() {}
|
|
`)
|
|
fixtureImporter := singlePackageImporter{path: "example.com/implementation", pkg: implementation}
|
|
facade := typecheckWithImporter(t, fset, "example.com/facade", facadeSource, fixtureImporter)
|
|
return checkExports(facade)
|
|
}
|
|
|
|
type singlePackageImporter struct {
|
|
path string
|
|
pkg *types.Package
|
|
}
|
|
|
|
func (i singlePackageImporter) Import(path string) (*types.Package, error) {
|
|
if path == i.path {
|
|
return i.pkg, nil
|
|
}
|
|
return importer.Default().Import(path)
|
|
}
|
|
|
|
func typecheck(t *testing.T, fset *token.FileSet, path, source string) *types.Package {
|
|
t.Helper()
|
|
return typecheckWithImporter(t, fset, path, source, importer.Default())
|
|
}
|
|
|
|
func typecheckWithImporter(t *testing.T, fset *token.FileSet, path, source string, imports types.Importer) *types.Package {
|
|
t.Helper()
|
|
file, err := parser.ParseFile(fset, path+".go", source, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var errors []error
|
|
config := types.Config{
|
|
Importer: imports,
|
|
Error: func(err error) {
|
|
errors = append(errors, err)
|
|
},
|
|
}
|
|
pkg, err := config.Check(path, fset, []*ast.File{file}, nil)
|
|
if err != nil {
|
|
t.Fatalf("typecheck %s: %v (%v)", path, err, errors)
|
|
}
|
|
return pkg
|
|
}
|
|
|
|
func TestCommandInternalCompositionPreservesLibraryBoundaries(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name, from, to string
|
|
reject bool
|
|
}{
|
|
{"command helpers", "/cmd/releasekit/internal/cli/parser", "/cmd/releasekit/internal/cli/settings", false},
|
|
{"other executable helpers", "/cmd/future/internal/parser", "/cmd/future/internal/settings", false},
|
|
{"helper root", "/cmd/releasekit/internal", "/cmd/releasekit/internal/cli", false},
|
|
{"controller uses facade", "/cmd/releasekit", "", false},
|
|
{"library imports controller", "/internal/build", "/cmd/releasekit", true},
|
|
{"library imports helper", "/internal/build", "/cmd/releasekit/internal/cli", true},
|
|
{"helper imports facade", "/cmd/releasekit/internal/cli", "", true},
|
|
{"helper imports runtime facade", "/cmd/releasekit/internal/cli", "/buildinfo", true},
|
|
{"helper imports controller", "/cmd/releasekit/internal/cli", "/cmd/releasekit", true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
err := checkImportDirections([]listedPackage{{ImportPath: modulePath + test.from, Imports: []string{modulePath + test.to}}})
|
|
if (err != nil) != test.reject {
|
|
t.Fatalf("dependency %s -> %s: %v", test.from, test.to, err)
|
|
}
|
|
})
|
|
}
|
|
}
|