Files
ReleaseKit/internal/apicheck/main_test.go

181 lines
5.2 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 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},
}
err := checkImportDirections(packages)
if err == nil || !strings.Contains(err.Error(), "internal package "+modulePath+"/internal/operations imports public or command package "+modulePath) {
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
}