diff --git a/packer_test/common/plugin.go b/packer_test/common/plugin.go index 90d29647abf..d6350b97b99 100644 --- a/packer_test/common/plugin.go +++ b/packer_test/common/plugin.go @@ -2,11 +2,13 @@ package common import ( "fmt" + "log" "os" "os/exec" "path/filepath" "runtime" "strings" + "sync" "testing" "github.com/hashicorp/go-version" @@ -130,6 +132,83 @@ 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 = struct { + Mutex *sync.Mutex + Jobs chan compilationJob + Running bool +}{ + Mutex: &sync.Mutex{}, + Jobs: make(chan compilationJob, 10), + Running: false, +} + +func processJobs() { + CompilationJobs.Mutex.Lock() + defer CompilationJobs.Mutex.Unlock() + + if CompilationJobs.Running { + return + } + + go func() { + for job := range CompilationJobs.Jobs { + 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) + } + CompilationJobs.Mutex.Lock() + defer CompilationJobs.Mutex.Unlock() + CompilationJobs.Running = false + }() +} + // CompilePlugin builds a tester plugin with the specified version. // // The plugin's code is contained in a subdirectory of this file, and lets us @@ -146,7 +225,29 @@ 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, customisations ...BuildCustomisation) { +// +// 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.Jobs <- compilationJob{ + versionString: versionString, + suite: ts, + customisations: customisations, + done: false, + resultCh: resultCh, + } + + processJobs() + + return 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 @@ -154,23 +255,21 @@ func (ts *PackerTestSuite) CompilePlugin(t *testing.T, versionString string, cus // 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 { - t.Fatalf("failed to prepare plugin workdir: %s", err) + return fmt.Errorf("failed to prepare plugin workdir: %s", err) } defer cleanup() } @@ -180,10 +279,11 @@ func (ts *PackerTestSuite) CompilePlugin(t *testing.T, versionString string, cus 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 { diff --git a/packer_test/common/suite.go b/packer_test/common/suite.go index 5c7477ce33f..dad9766e2c7 100644 --- a/packer_test/common/suite.go +++ b/packer_test/common/suite.go @@ -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