blob: e716ec97b39f894bbb305e47e5549a08ec95fcfe [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package acctest
import (
"context"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
"testing"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/provisioner/file"
shellprovisioner "github.com/hashicorp/packer/provisioner/shell"
)
// TestEnvVar must be set to a non-empty value for acceptance tests to run.
const TestEnvVar = "PACKER_ACC"
// TestCase is a single set of tests to run for a backend. A TestCase
// should generally map 1:1 to each test method for your acceptance
// tests.
type TestCase struct {
// Precheck, if non-nil, will be called once before the test case
// runs at all. This can be used for some validation prior to the
// test running.
PreCheck func()
// Builder is the Builder that will be tested. It will be available
// as the "test" builder in the template.
Builder packersdk.Builder
// Template is the template contents to use.
Template string
// Check is called after this step is executed in order to test that
// the step executed successfully. If this is not set, then the next
// step will be called
Check TestCheckFunc
// Teardown will be called before the test case is over regardless
// of if the test succeeded or failed. This should return an error
// in the case that the test can't guarantee all resources were
// properly cleaned up.
Teardown TestTeardownFunc
// If SkipArtifactTeardown is true, we will not attempt to destroy the
// artifact created in this test run.
SkipArtifactTeardown bool
// If set, overrides the default provisioner store with custom provisioners.
// This can be useful for running acceptance tests for a particular
// provisioner using a specific builder.
// Default provisioner store:
// ProvisionerStore: packersdk.MapOfProvisioner{
// "shell": func() (packersdk.Provisioner, error) { return &shellprovisioner.Provisioner{}, nil },
// "file": func() (packersdk.Provisioner, error) { return &file.Provisioner{}, nil },
// },
ProvisionerStore packersdk.MapOfProvisioner
}
// TestCheckFunc is the callback used for Check in TestStep.
type TestCheckFunc func([]packersdk.Artifact) error
// TestTeardownFunc is the callback used for Teardown in TestCase.
type TestTeardownFunc func() error
// TestT is the interface used to handle the test lifecycle of a test.
//
// Users should just use a *testing.T object, which implements this.
type TestT interface {
Error(args ...interface{})
Fatal(args ...interface{})
Skip(args ...interface{})
}
type TestBuilderSet struct {
packer.BuilderSet
StartFn func(name string) (packersdk.Builder, error)
}
func (tbs TestBuilderSet) Start(name string) (packersdk.Builder, error) { return tbs.StartFn(name) }
// Test performs an acceptance test on a backend with the given test case.
//
// Tests are not run unless an environmental variable "PACKER_ACC" is
// set to some non-empty value. This is to avoid test cases surprising
// a user by creating real resources.
//
// Tests will fail unless the verbose flag (`go test -v`, or explicitly
// the "-test.v" flag) is set. Because some acceptance tests take quite
// long, we require the verbose flag so users are able to see progress
// output.
func Test(t TestT, c TestCase) {
// We only run acceptance tests if an env var is set because they're
// slow and generally require some outside configuration.
if os.Getenv(TestEnvVar) == "" {
t.Skip(fmt.Sprintf(
"Acceptance tests skipped unless env '%s' set",
TestEnvVar))
return
}
// We require verbose mode so that the user knows what is going on.
if !testTesting && !testing.Verbose() {
t.Fatal("Acceptance tests must be run with the -v flag on tests")
return
}
// Run the PreCheck if we have it
if c.PreCheck != nil {
c.PreCheck()
}
// Parse the template
log.Printf("[DEBUG] Parsing template...")
tpl, err := template.Parse(strings.NewReader(c.Template))
if err != nil {
t.Fatal(fmt.Sprintf("Failed to parse template: %s", err))
return
}
if c.ProvisionerStore == nil {
c.ProvisionerStore = packersdk.MapOfProvisioner{
"shell": func() (packersdk.Provisioner, error) { return &shellprovisioner.Provisioner{}, nil },
"file": func() (packersdk.Provisioner, error) { return &file.Provisioner{}, nil },
}
}
// Build the core
log.Printf("[DEBUG] Initializing core...")
core := packer.NewCore(&packer.CoreConfig{
Components: packer.ComponentFinder{
PluginConfig: &packer.PluginConfig{
Builders: TestBuilderSet{
StartFn: func(n string) (packersdk.Builder, error) {
if n == "test" {
return c.Builder, nil
}
return nil, nil
},
},
Provisioners: c.ProvisionerStore,
},
},
Template: tpl,
})
diags := core.Initialize(packer.InitializeOptions{})
if diags.HasErrors() {
t.Fatal(fmt.Sprintf("Failed to init core: %s", err))
return
}
// Get the build
log.Printf("[DEBUG] Retrieving 'test' build")
build, err := core.Build("test")
if err != nil {
t.Fatal(fmt.Sprintf("Failed to get 'test' build: %s", err))
return
}
// Prepare it
log.Printf("[DEBUG] Preparing 'test' build")
warnings, err := build.Prepare()
if err != nil {
t.Fatal(fmt.Sprintf("Prepare error: %s", err))
return
}
if len(warnings) > 0 {
t.Fatal(fmt.Sprintf(
"Prepare warnings:\n\n%s",
strings.Join(warnings, "\n")))
return
}
// Run it! We use a temporary directory for caching and discard
// any UI output. We discard since it shows up in logs anyways.
log.Printf("[DEBUG] Running 'test' build")
ui := &packersdk.BasicUi{
Reader: os.Stdin,
Writer: ioutil.Discard,
ErrorWriter: ioutil.Discard,
PB: &packersdk.NoopProgressTracker{},
}
artifacts, err := build.Run(context.Background(), ui)
if err != nil {
t.Fatal(fmt.Sprintf("Run error:\n\n%s", err))
goto TEARDOWN
}
// Check function
if c.Check != nil {
log.Printf("[DEBUG] Running check function")
if err := c.Check(artifacts); err != nil {
t.Fatal(fmt.Sprintf("Check error:\n\n%s", err))
goto TEARDOWN
}
}
TEARDOWN:
if !c.SkipArtifactTeardown {
// Delete all artifacts
for _, a := range artifacts {
if err := a.Destroy(); err != nil {
t.Error(fmt.Sprintf(
"!!! ERROR REMOVING ARTIFACT '%s': %s !!!",
a.String(), err))
}
}
}
// Teardown
if c.Teardown != nil {
log.Printf("[DEBUG] Running teardown function")
if err := c.Teardown(); err != nil {
t.Fatal(fmt.Sprintf("Teardown failure:\n\n%s", err))
return
}
}
}
// This is for unit tests of this package.
var testTesting = false