Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test add compilation queue and customisations #13230

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 152 additions & 6 deletions packer_test/common/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package common

import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -62,6 +63,59 @@ func ExpectedInstalledName(versionStr string) string {
runtime.GOOS, runtime.GOARCH, ext)
}

// BuildCustomisation is a function that allows you to change things on a plugin's
// local files, with a way to rollback those changes after the fact.
//
// The function is meant to take a path parameter to the directory for the plugin,
// and returns a function that unravels those changes once the build process is done.
type BuildCustomisation func(string) (error, func())

const SDKModule = "github.com/hashicorp/packer-plugin-sdk"

// UseDependency invokes go get and go mod tidy to update a package required
// by the plugin, and use it to build the plugin with that change.
func UseDependency(remoteModule, ref string) BuildCustomisation {
return func(path string) (error, func()) {
modPath := filepath.Join(path, "go.mod")

stat, err := os.Stat(modPath)
if err != nil {
return fmt.Errorf("cannot stat mod file %q: %s", modPath, err), nil
}

// Save old go.mod file from dir
oldGoMod, err := os.ReadFile(modPath)
if err != nil {
return fmt.Errorf("failed to read current mod file %q: %s", modPath, err), nil
}

modSpec := fmt.Sprintf("%s@%s", remoteModule, ref)
cmd := exec.Command("go", "get", modSpec)
cmd.Dir = path
err = cmd.Run()
if err != nil {
return fmt.Errorf("failed to run go get %s: %s", modSpec, err), nil
}

cmd = exec.Command("go", "mod", "tidy")
cmd.Dir = path
err = cmd.Run()
if err != nil {
return fmt.Errorf("failed to run go mod tidy: %s", err), nil
}

return nil, func() {
err = os.WriteFile(modPath, oldGoMod, stat.Mode())
if err != nil {
fmt.Fprintf(os.Stderr, "failed to reset modfile %q: %s; manual cleanup may be needed", modPath, err)
}
cmd := exec.Command("go", "mod", "tidy")
cmd.Dir = path
_ = cmd.Run()
}
}
}

// GetPluginPath gets the path for a pre-compiled plugin in the current test suite.
//
// The version only is needed, as the path to a compiled version of the tester
Expand All @@ -77,6 +131,49 @@ func (ts *PackerTestSuite) GetPluginPath(t *testing.T, version string) string {
return path.(string)
}

type CompilationResult struct {
Error error
Version string
}

// Ready processes a series of CompilationResults, as returned by CompilePlugin
//
// If any of the jobs requested failed, the test will fail also.
func Ready(t *testing.T, results []chan CompilationResult) {
for _, res := range results {
jobErr := <-res
empty := CompilationResult{}
if jobErr != empty {
t.Errorf("failed to compile plugin at version %s: %s", jobErr.Version, jobErr.Error)
}
}

if t.Failed() {
t.Fatalf("some plugins failed to be compiled, see logs for more info")
}
}

type compilationJob struct {
versionString string
suite *PackerTestSuite
done bool
resultCh chan CompilationResult
customisations []BuildCustomisation
}

// CompilationJobs keeps a queue of compilation jobs for plugins
//
// This approach allows us to avoid conflicts between compilation jobs.
// Typically building the plugin with different ldflags is safe to perform
// in parallel on the same file set, however customisations tend to be more
// conflictual, as two concurrent compilation jobs may end-up compiling the
// wrong plugin, which may cause some tests to misbehave, or even compilation
// jobs to fail.
//
// The solution to this approach is to have a global queue for every plugin
// compilation to be performed safely.
var CompilationJobs = make(chan compilationJob, 10)

// CompilePlugin builds a tester plugin with the specified version.
//
// The plugin's code is contained in a subdirectory of this file, and lets us
Expand All @@ -93,36 +190,85 @@ func (ts *PackerTestSuite) GetPluginPath(t *testing.T, version string) string {
// Note: each tester plugin may only be compiled once for a specific version in
// a test suite. The version may include core (mandatory), pre-release and
// metadata. Unlike Packer core, metadata does matter for the version being built.
func (ts *PackerTestSuite) CompilePlugin(t *testing.T, versionString string) {
//
// Note: the compilation will process asynchronously, and should be waited upon
// before tests that use this plugin may proceed. Refer to the `Ready` function
// for doing that.
func (ts *PackerTestSuite) CompilePlugin(versionString string, customisations ...BuildCustomisation) chan CompilationResult {
resultCh := make(chan CompilationResult)

CompilationJobs <- compilationJob{
versionString: versionString,
suite: ts,
customisations: customisations,
done: false,
resultCh: resultCh,
}

return resultCh
}

func init() {
// Run a processor coroutine for the duration of the test.
//
// It's simpler to have this occurring on the side at all times, without
// trying to manage its lifecycle based on the current amount of queued
// tasks, since this is linked to the test lifecycle, and as it's a single
// coroutine, we can leave it run until the process exits.
go func() {
for job := range CompilationJobs {
log.Printf("compiling plugin on version %s", job.versionString)
err := compilePlugin(job.suite, job.versionString, job.customisations...)
if err != nil {
job.resultCh <- CompilationResult{
Error: err,
Version: job.versionString,
}
}
close(job.resultCh)
}
}()
}

// compilePlugin performs the actual compilation procedure for the plugin, and
// registers it to the test suite instance passed as a parameter.
func compilePlugin(ts *PackerTestSuite, versionString string, customisations ...BuildCustomisation) error {
// Fail to build plugin if already built.
//
// Especially with customisations being a thing, relying on cache to get and
// build a plugin at once means that the function is not idempotent anymore,
// and therefore we cannot rely on it being called twice and producing the
// same result, so we forbid it.
if _, ok := ts.compiledPlugins.Load(versionString); ok {
t.Fatalf("plugin version %q was already built, use GetTestPlugin instead", versionString)
return fmt.Errorf("plugin version %q was already built, use GetTestPlugin instead", versionString)
}

v := version.Must(version.NewSemver(versionString))

t.Logf("Building tester plugin in version %v", v)

testDir, err := currentDir()
if err != nil {
t.Fatalf("failed to compile plugin binary: %s", err)
return fmt.Errorf("failed to compile plugin binary: %s", err)
}

testerPluginDir := filepath.Join(testDir, "plugin_tester")
for _, custom := range customisations {
err, cleanup := custom(testerPluginDir)
if err != nil {
return fmt.Errorf("failed to prepare plugin workdir: %s", err)
}
defer cleanup()
}

outBin := filepath.Join(ts.pluginsDirectory, BinaryName(v))

compileCommand := exec.Command("go", "build", "-C", testerPluginDir, "-o", outBin, "-ldflags", LDFlags(v), ".")
logs, err := compileCommand.CombinedOutput()
if err != nil {
t.Fatalf("failed to compile plugin binary: %s\ncompiler logs: %s", err, logs)
return fmt.Errorf("failed to compile plugin binary: %s\ncompiler logs: %s", err, logs)
}

ts.compiledPlugins.Store(v.String(), outBin)
return nil
}

type PluginDirSpec struct {
Expand Down
16 changes: 4 additions & 12 deletions packer_test/common/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,14 @@ type PackerTestSuite struct {
compiledPlugins sync.Map
}

func (ts *PackerTestSuite) buildPluginVersion(waitgroup *sync.WaitGroup, versionString string, t *testing.T) {
waitgroup.Add(1)
go func() {
defer waitgroup.Done()
ts.CompilePlugin(t, versionString)
}()
}

// CompileTestPluginVersions batch compiles a series of plugins
func (ts *PackerTestSuite) CompileTestPluginVersions(t *testing.T, versions ...string) {
wg := &sync.WaitGroup{}

results := []chan CompilationResult{}
for _, ver := range versions {
ts.buildPluginVersion(wg, ver, t)
results = append(results, ts.CompilePlugin(ver))
}

wg.Wait()
Ready(t, results)
}

// SkipNoAcc is a pre-condition that skips the test if the PACKER_ACC environment
Expand Down
Loading