Files
avo/tests/thirdparty/config.go

233 lines
5.7 KiB
Go
Raw Normal View History

// Package thirdparty executes integration tests based on third-party packages that use avo.
package thirdparty
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
)
// GithubRepository specifies a repository on github.
type GithubRepository struct {
Owner string `json:"owner"`
Name string `json:"name"`
}
func (r GithubRepository) String() string {
return path.Join(r.Owner, r.Name)
}
// CloneURL returns the git clone URL.
func (r GithubRepository) CloneURL() string {
return fmt.Sprintf("https://github.com/%s.git", r)
}
// Step represents a set of commands to run as part of the testing plan for a
// third-party package.
type Step struct {
Name string `json:"name"`
WorkingDirectory string `json:"dir"`
Commands []string `json:"commands"`
}
// Validate step parameters.
func (s *Step) Validate() error {
if s.Name == "" {
return errors.New("missing name")
}
if len(s.Commands) == 0 {
return errors.New("missing commands")
}
return nil
}
// Package defines an integration test based on a third-party package using avo.
type Package struct {
// Repository the package belongs to. At the moment, all packages are
// available on github.
Repository GithubRepository `json:"repository"`
// Version as a git sha, tag or branch.
Version string `json:"version"`
// Sub-package within the repository under test. All file path references
// will be relative to this directory. If empty the root of the repository
// is used.
SubPackage string `json:"pkg"`
// Path to the module file for the avo generator package. This is necessary
// so the integration test can insert replace directives to point at the avo
// version under test.
Module string `json:"module"`
// Setup steps. These run prior to the insertion of avo replace directives,
// therefore should be used if it's necessary to initialize new go modules
// within the repository.
Setup []*Step `json:"setup"`
// Steps to run the avo code generator.
Generate []*Step `json:"generate"` // generate commands to run
// Test steps. If empty, defaults to "go test ./...".
Test []*Step `json:"test"`
}
// ID returns an identifier for the package.
func (p *Package) ID() string {
pkgpath := path.Join(p.Repository.String(), p.SubPackage)
return strings.ReplaceAll(pkgpath, "/", "-")
}
// setdefaults fills in missing parameters to help make the input package
// descriptions less verbose.
func (p *Package) setdefaults() {
for _, stage := range []struct {
Steps []*Step
DefaultName string
}{
{p.Setup, "Setup"},
{p.Generate, "Generate"},
{p.Test, "Test"},
} {
if len(stage.Steps) == 1 && stage.Steps[0].Name == "" {
stage.Steps[0].Name = stage.DefaultName
}
}
}
// Validate package definition.
func (p *Package) Validate() error {
if p.Version == "" {
return errors.New("missing version")
}
if p.Module == "" {
return errors.New("missing module")
}
if len(p.Generate) == 0 {
return errors.New("no generate commands")
}
stages := map[string][]*Step{
"setup": p.Setup,
"generate": p.Generate,
"test": p.Test,
}
for name, steps := range stages {
for _, s := range steps {
if err := s.Validate(); err != nil {
return fmt.Errorf("%s step: %w", name, err)
}
}
}
return nil
}
// Context specifies execution environment parameters for a third-party test.
type Context struct {
// Path to the avo version under test.
AvoDirectory string
// Path to the checked out third-party repository.
RepositoryDirectory string
}
// Steps generates the list of steps required to execute the integration test
// for this package. Context specifies execution environment parameters.
func (p *Package) Steps(c *Context) []*Step {
var steps []*Step
// Optional setup.
steps = append(steps, p.Setup...)
// Replace avo dependency.
const invalid = "v0.0.0-00010101000000-000000000000"
moddir := filepath.Dir(p.Module)
modfile := filepath.Base(p.Module)
steps = append(steps, &Step{
Name: "Avo Module Replacement",
WorkingDirectory: moddir,
Commands: []string{
"go mod edit -modfile=" + modfile + " -require=github.com/mmcloughlin/avo@" + invalid,
"go mod edit -modfile=" + modfile + " -replace=github.com/mmcloughlin/avo=" + c.AvoDirectory,
"go mod tidy -modfile=" + modfile,
},
})
// Run generation.
steps = append(steps, p.Generate...)
// Display changes.
steps = append(steps, &Step{
Name: "Diff",
Commands: []string{"git diff"},
})
// Tests.
if len(p.Test) > 0 {
steps = append(steps, p.Test...)
} else {
steps = append(steps, &Step{
Name: "Test",
Commands: []string{
"go test ./...",
},
})
}
// Prepend sub-directory to every step.
if p.SubPackage != "" {
for _, s := range steps {
s.WorkingDirectory = filepath.Join(p.SubPackage, s.WorkingDirectory)
}
}
return steps
}
// Packages is a collection of third-party integration tests.
type Packages []*Package
func (p Packages) setdefaults() {
for _, pkg := range p {
pkg.setdefaults()
}
}
// Validate the package collection.
func (p Packages) Validate() error {
for _, pkg := range p {
if err := pkg.Validate(); err != nil {
return fmt.Errorf("package %s: %w", pkg.ID(), err)
}
}
return nil
}
// LoadPackages loads a list of package configurations from JSON format.
func LoadPackages(r io.Reader) (Packages, error) {
var pkgs Packages
d := json.NewDecoder(r)
d.DisallowUnknownFields()
if err := d.Decode(&pkgs); err != nil {
return nil, err
}
pkgs.setdefaults()
return pkgs, nil
}
// LoadPackagesFile loads a list of package configurations from a JSON file.
func LoadPackagesFile(filename string) (Packages, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
return LoadPackages(f)
}