backend: Replace ShowDiagnostics with view.Diagnostics

This commit is contained in:
Alisdair McDiarmid 2021-02-25 10:02:23 -05:00
parent 3737ee5081
commit 7c0ec0107f
15 changed files with 208 additions and 299 deletions

View File

@ -216,9 +216,6 @@ type Operation struct {
UIIn terraform.UIInput UIIn terraform.UIInput
UIOut terraform.UIOutput UIOut terraform.UIOutput
// ShowDiagnostics prints diagnostic messages to the UI.
ShowDiagnostics func(vals ...interface{})
// StateLocker is used to lock the state while providing UI feedback to the // StateLocker is used to lock the state while providing UI feedback to the
// user. This will be replaced by the Backend to update the context. // user. This will be replaced by the Backend to update the context.
// //
@ -253,7 +250,7 @@ func (o *Operation) Config() (*configs.Config, tfdiags.Diagnostics) {
// //
// If the given diagnostics contains errors then the operation's result // If the given diagnostics contains errors then the operation's result
// will be set to backend.OperationFailure. It will be set to // will be set to backend.OperationFailure. It will be set to
// backend.OperationSuccess otherwise. It will then use b.ShowDiagnostics // backend.OperationSuccess otherwise. It will then use o.View.Diagnostics
// to show the given diagnostics before returning. // to show the given diagnostics before returning.
// //
// Callers should feel free to do each of these operations separately in // Callers should feel free to do each of these operations separately in
@ -266,14 +263,14 @@ func (o *Operation) ReportResult(op *RunningOperation, diags tfdiags.Diagnostics
} else { } else {
op.Result = OperationSuccess op.Result = OperationSuccess
} }
if o.ShowDiagnostics != nil { if o.View != nil {
o.ShowDiagnostics(diags) o.View.Diagnostics(diags)
} else { } else {
// Shouldn't generally happen, but if it does then we'll at least // Shouldn't generally happen, but if it does then we'll at least
// make some noise in the logs to help us spot it. // make some noise in the logs to help us spot it.
if len(diags) != 0 { if len(diags) != 0 {
log.Printf( log.Printf(
"[ERROR] Backend needs to report diagnostics but ShowDiagnostics is not set:\n%s", "[ERROR] Backend needs to report diagnostics but View is not set:\n%s",
diags.ErrWithWarnings(), diags.ErrWithWarnings(),
) )
} }

View File

@ -56,10 +56,6 @@ type CLIOpts struct {
// for tailoring the output to fit the attached terminal, for example. // for tailoring the output to fit the attached terminal, for example.
Streams *terminal.Streams Streams *terminal.Streams
// ShowDiagnostics is a function that will format and print diagnostic
// messages to the UI.
ShowDiagnostics func(vals ...interface{})
// StatePath is the local path where state is read from. // StatePath is the local path where state is read from.
// //
// StateOutPath is the local path where the state will be written. // StateOutPath is the local path where the state will be written.

View File

@ -53,7 +53,7 @@ func (b *Local) opApply(
defer func() { defer func() {
diags := op.StateLocker.Unlock() diags := op.StateLocker.Unlock()
if diags.HasErrors() { if diags.HasErrors() {
op.ShowDiagnostics(diags) op.View.Diagnostics(diags)
runningOp.Result = backend.OperationFailure runningOp.Result = backend.OperationFailure
} }
}() }()
@ -101,7 +101,7 @@ func (b *Local) opApply(
// We'll show any accumulated warnings before we display the prompt, // We'll show any accumulated warnings before we display the prompt,
// so the user can consider them when deciding how to answer. // so the user can consider them when deciding how to answer.
if len(diags) > 0 { if len(diags) > 0 {
op.ShowDiagnostics(diags) op.View.Diagnostics(diags)
diags = nil // reset so we won't show the same diagnostics again later diags = nil // reset so we won't show the same diagnostics again later
} }
@ -168,7 +168,7 @@ func (b *Local) opApply(
// If we've accumulated any warnings along the way then we'll show them // If we've accumulated any warnings along the way then we'll show them
// here just before we show the summary and next steps. If we encountered // here just before we show the summary and next steps. If we encountered
// errors then we would've returned early at some other point above. // errors then we would've returned early at some other point above.
op.ShowDiagnostics(diags) op.View.Diagnostics(diags)
} }
// backupStateForError is called in a scenario where we're unable to persist the // backupStateForError is called in a scenario where we're unable to persist the

View File

@ -101,8 +101,8 @@ func TestLocal_applyEmptyDir(t *testing.T) {
// the backend should be unlocked after a run // the backend should be unlocked after a run
assertBackendStateUnlocked(t, b) assertBackendStateUnlocked(t, b)
if errOutput := done(t).Stderr(); errOutput != "" { if got, want := done(t).Stderr(), "Error: No configuration files"; !strings.Contains(got, want) {
t.Fatalf("unexpected error output:\n%s", errOutput) t.Fatalf("unexpected error output:\n%s\nwant: %s", got, want)
} }
} }
@ -165,7 +165,7 @@ func TestLocal_applyError(t *testing.T) {
ami := r.Config.GetAttr("ami").AsString() ami := r.Config.GetAttr("ami").AsString()
if !errored && ami == "error" { if !errored && ami == "error" {
errored = true errored = true
diags = diags.Append(errors.New("error")) diags = diags.Append(errors.New("ami error"))
return providers.ApplyResourceChangeResponse{ return providers.ApplyResourceChangeResponse{
Diagnostics: diags, Diagnostics: diags,
} }
@ -201,8 +201,8 @@ test_instance.foo:
// the backend should be unlocked after a run // the backend should be unlocked after a run
assertBackendStateUnlocked(t, b) assertBackendStateUnlocked(t, b)
if errOutput := done(t).Stderr(); errOutput != "" { if got, want := done(t).Stderr(), "Error: ami error"; !strings.Contains(got, want) {
t.Fatalf("unexpected error output:\n%s", errOutput) t.Fatalf("unexpected error output:\n%s\nwant: %s", got, want)
} }
} }
@ -229,9 +229,6 @@ func TestLocal_applyBackendFail(t *testing.T) {
op, configCleanup, done := testOperationApply(t, wd+"/testdata/apply") op, configCleanup, done := testOperationApply(t, wd+"/testdata/apply")
defer configCleanup() defer configCleanup()
record, playback := testRecordDiagnostics(t)
op.ShowDiagnostics = record
b.Backend = &backendWithFailingState{} b.Backend = &backendWithFailingState{}
run, err := b.Operation(context.Background(), op) run, err := b.Operation(context.Background(), op)
@ -239,11 +236,14 @@ func TestLocal_applyBackendFail(t *testing.T) {
t.Fatalf("bad: %s", err) t.Fatalf("bad: %s", err)
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatalf("apply succeeded; want error") t.Fatalf("apply succeeded; want error")
} }
diagErr := playback().Err().Error() diagErr := output.Stderr()
if !strings.Contains(diagErr, "Error saving state: fake failure") { if !strings.Contains(diagErr, "Error saving state: fake failure") {
t.Fatalf("missing \"fake failure\" message in diags:\n%s", diagErr) t.Fatalf("missing \"fake failure\" message in diags:\n%s", diagErr)
} }
@ -259,10 +259,6 @@ test_instance.foo:
// the backend should be unlocked after a run // the backend should be unlocked after a run
assertBackendStateUnlocked(t, b) assertBackendStateUnlocked(t, b)
if errOutput := done(t).Stderr(); errOutput != "" {
t.Fatalf("unexpected error output:\n%s", errOutput)
}
} }
func TestLocal_applyRefreshFalse(t *testing.T) { func TestLocal_applyRefreshFalse(t *testing.T) {
@ -323,7 +319,6 @@ func testOperationApply(t *testing.T, configDir string) (*backend.Operation, fun
Type: backend.OperationTypeApply, Type: backend.OperationTypeApply,
ConfigDir: configDir, ConfigDir: configDir,
ConfigLoader: configLoader, ConfigLoader: configLoader,
ShowDiagnostics: testLogDiagnostics(t),
StateLocker: clistate.NewNoopLocker(), StateLocker: clistate.NewNoopLocker(),
View: view, View: view,
}, configCleanup, done }, configCleanup, done

View File

@ -64,7 +64,7 @@ func (b *Local) opPlan(
defer func() { defer func() {
diags := op.StateLocker.Unlock() diags := op.StateLocker.Unlock()
if diags.HasErrors() { if diags.HasErrors() {
op.ShowDiagnostics(diags) op.View.Diagnostics(diags)
runningOp.Result = backend.OperationFailure runningOp.Result = backend.OperationFailure
} }
}() }()
@ -135,7 +135,7 @@ func (b *Local) opPlan(
op.View.PlanNoChanges() op.View.PlanNoChanges()
// Even if there are no changes, there still could be some warnings // Even if there are no changes, there still could be some warnings
op.ShowDiagnostics(diags) op.View.Diagnostics(diags)
return return
} }
@ -145,7 +145,7 @@ func (b *Local) opPlan(
// If we've accumulated any warnings along the way then we'll show them // If we've accumulated any warnings along the way then we'll show them
// here just before we show the summary and next steps. If we encountered // here just before we show the summary and next steps. If we encountered
// errors then we would've returned early at some other point above. // errors then we would've returned early at some other point above.
op.ShowDiagnostics(diags) op.View.Diagnostics(diags)
op.View.PlanNextStep(op.PlanOutPath) op.View.PlanNextStep(op.PlanOutPath)
} }

View File

@ -90,8 +90,6 @@ func TestLocal_planNoConfig(t *testing.T) {
TestLocalProvider(t, b, "test", &terraform.ProviderSchema{}) TestLocalProvider(t, b, "test", &terraform.ProviderSchema{})
op, configCleanup, done := testOperationPlan(t, "./testdata/empty") op, configCleanup, done := testOperationPlan(t, "./testdata/empty")
record, playback := testRecordDiagnostics(t)
op.ShowDiagnostics = record
defer configCleanup() defer configCleanup()
op.PlanRefresh = true op.PlanRefresh = true
@ -101,21 +99,18 @@ func TestLocal_planNoConfig(t *testing.T) {
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("plan operation succeeded; want failure") t.Fatal("plan operation succeeded; want failure")
} }
output := playback().Err().Error() if stderr := output.Stderr(); !strings.Contains(stderr, "No configuration files") {
if !strings.Contains(output, "No configuration files") { t.Fatalf("bad: %s", stderr)
t.Fatalf("bad: %s", err)
} }
// the backend should be unlocked after a run // the backend should be unlocked after a run
assertBackendStateUnlocked(t, b) assertBackendStateUnlocked(t, b)
if errOutput := done(t).Stderr(); errOutput != "" {
t.Fatalf("unexpected error output:\n%s", errOutput)
}
} }
// This test validates the state lacking behavior when the inner call to // This test validates the state lacking behavior when the inner call to
@ -141,8 +136,8 @@ func TestLocal_plan_context_error(t *testing.T) {
// the backend should be unlocked after a run // the backend should be unlocked after a run
assertBackendStateUnlocked(t, b) assertBackendStateUnlocked(t, b)
if errOutput := done(t).Stderr(); errOutput != "" { if got, want := done(t).Stderr(), "Error: Could not load plugin"; !strings.Contains(got, want) {
t.Fatalf("unexpected error output:\n%s", errOutput) t.Fatalf("unexpected error output:\n%s\nwant: %s", got, want)
} }
} }
@ -726,7 +721,6 @@ func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func
Type: backend.OperationTypePlan, Type: backend.OperationTypePlan,
ConfigDir: configDir, ConfigDir: configDir,
ConfigLoader: configLoader, ConfigLoader: configLoader,
ShowDiagnostics: testLogDiagnostics(t),
StateLocker: clistate.NewNoopLocker(), StateLocker: clistate.NewNoopLocker(),
View: view, View: view,
}, configCleanup, done }, configCleanup, done

View File

@ -57,7 +57,7 @@ func (b *Local) opRefresh(
defer func() { defer func() {
diags := op.StateLocker.Unlock() diags := op.StateLocker.Unlock()
if diags.HasErrors() { if diags.HasErrors() {
op.ShowDiagnostics(diags) op.View.Diagnostics(diags)
runningOp.Result = backend.OperationFailure runningOp.Result = backend.OperationFailure
} }
}() }()

View File

@ -238,10 +238,6 @@ func TestLocal_refreshEmptyState(t *testing.T) {
op, configCleanup, done := testOperationRefresh(t, "./testdata/refresh") op, configCleanup, done := testOperationRefresh(t, "./testdata/refresh")
defer configCleanup() defer configCleanup()
defer done(t)
record, playback := testRecordDiagnostics(t)
op.ShowDiagnostics = record
run, err := b.Operation(context.Background(), op) run, err := b.Operation(context.Background(), op)
if err != nil { if err != nil {
@ -249,11 +245,12 @@ func TestLocal_refreshEmptyState(t *testing.T) {
} }
<-run.Done() <-run.Done()
diags := playback() output := done(t)
if diags.HasErrors() {
t.Fatalf("expected only warning diags, got errors: %s", diags.Err()) if stderr := output.Stderr(); stderr != "" {
t.Fatalf("expected only warning diags, got errors: %s", stderr)
} }
if got, want := diags.ErrWithWarnings().Error(), "Empty or non-existent state"; !strings.Contains(got, want) { if got, want := output.Stdout(), "Warning: Empty or non-existent state"; !strings.Contains(got, want) {
t.Errorf("wrong diags\n got: %s\nwant: %s", got, want) t.Errorf("wrong diags\n got: %s\nwant: %s", got, want)
} }
@ -273,7 +270,6 @@ func testOperationRefresh(t *testing.T, configDir string) (*backend.Operation, f
Type: backend.OperationTypeRefresh, Type: backend.OperationTypeRefresh,
ConfigDir: configDir, ConfigDir: configDir,
ConfigLoader: configLoader, ConfigLoader: configLoader,
ShowDiagnostics: testLogDiagnostics(t),
StateLocker: clistate.NewNoopLocker(), StateLocker: clistate.NewNoopLocker(),
View: view, View: view,
}, configCleanup, done }, configCleanup, done

View File

@ -15,7 +15,6 @@ import (
"github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
) )
// TestLocal returns a configured Local struct with temporary paths and // TestLocal returns a configured Local struct with temporary paths and
@ -245,43 +244,3 @@ func assertBackendStateLocked(t *testing.T, b *Local) bool {
t.Error("unexpected success locking state") t.Error("unexpected success locking state")
return true return true
} }
// testRecordDiagnostics allows tests to record and later inspect diagnostics
// emitted during an Operation. It returns a record function which can be set
// as the ShowDiagnostics value of an Operation, and a playback function which
// returns the recorded diagnostics for inspection.
func testRecordDiagnostics(t *testing.T) (record func(vals ...interface{}), playback func() tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
record = func(vals ...interface{}) {
diags = diags.Append(vals...)
}
playback = func() tfdiags.Diagnostics {
diags.Sort()
return diags
}
return
}
// testLogDiagnostics returns a function which can be used as the
// ShowDiagnostics value for an Operation, in order to help debugging during
// tests. Any calls to this function result in test logs.
func testLogDiagnostics(t *testing.T) func(vals ...interface{}) {
return func(vals ...interface{}) {
var diags tfdiags.Diagnostics
diags = diags.Append(vals...)
diags.Sort()
for _, diag := range diags {
// NOTE: Since the caller here is not directly the TestLocal
// function, t.Helper doesn't apply and so the log source
// isn't correctly shown in the test log output. This seems
// unavoidable as long as this is happening so indirectly.
desc := diag.Description()
if desc.Detail != "" {
t.Logf("%s: %s", desc.Summary, desc.Detail)
} else {
t.Log(desc.Summary)
}
}
}
}

View File

@ -22,53 +22,44 @@ import (
"github.com/hashicorp/terraform/plans/planfile" "github.com/hashicorp/terraform/plans/planfile"
"github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
tfversion "github.com/hashicorp/terraform/version" tfversion "github.com/hashicorp/terraform/version"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func()) { func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
t.Helper() t.Helper()
return testOperationApplyWithTimeout(t, configDir, 0) return testOperationApplyWithTimeout(t, configDir, 0)
} }
func testOperationApplyWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func()) { func testOperationApplyWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
t.Helper() t.Helper()
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
streams, _ := terminal.StreamsForTesting(t) streams, done := terminal.StreamsForTesting(t)
view := views.NewStateLocker(arguments.ViewHuman, views.NewView(streams)) view := views.NewView(streams)
stateLockerView := views.NewStateLocker(arguments.ViewHuman, view)
operationView := views.NewOperation(arguments.ViewHuman, false, view)
return &backend.Operation{ return &backend.Operation{
ConfigDir: configDir, ConfigDir: configDir,
ConfigLoader: configLoader, ConfigLoader: configLoader,
Parallelism: defaultParallelism, Parallelism: defaultParallelism,
PlanRefresh: true, PlanRefresh: true,
ShowDiagnostics: testLogDiagnostics(t), StateLocker: clistate.NewLocker(timeout, stateLockerView),
StateLocker: clistate.NewLocker(timeout, view),
Type: backend.OperationTypeApply, Type: backend.OperationTypeApply,
}, configCleanup View: operationView,
} }, configCleanup, done
func testOperationApplyWithDiagnostics(t *testing.T, configDir string) (*backend.Operation, func(), func() tfdiags.Diagnostics) {
t.Helper()
op, cleanup := testOperationApply(t, configDir)
record, playback := testRecordDiagnostics(t)
op.ShowDiagnostics = record
return op, cleanup, playback
} }
func TestRemote_applyBasic(t *testing.T) { func TestRemote_applyBasic(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationApply(t, "./testdata/apply") op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup() defer configCleanup()
defer done(t)
input := testInput(t, map[string]string{ input := testInput(t, map[string]string{
"approve": "yes", "approve": "yes",
@ -117,8 +108,9 @@ func TestRemote_applyCanceled(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationApply(t, "./testdata/apply") op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup() defer configCleanup()
defer done(t)
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -158,7 +150,7 @@ func TestRemote_applyWithoutPermissions(t *testing.T) {
} }
w.Permissions.CanQueueApply = false w.Permissions.CanQueueApply = false
op, configCleanup, playback := testOperationApplyWithDiagnostics(t, "./testdata/apply") op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup() defer configCleanup()
op.UIOut = b.CLI op.UIOut = b.CLI
@ -170,11 +162,12 @@ func TestRemote_applyWithoutPermissions(t *testing.T) {
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected apply operation to fail") t.Fatal("expected apply operation to fail")
} }
errOutput := playback().Err().Error() errOutput := output.Stderr()
if !strings.Contains(errOutput, "Insufficient rights to apply changes") { if !strings.Contains(errOutput, "Insufficient rights to apply changes") {
t.Fatalf("expected a permissions error, got: %v", errOutput) t.Fatalf("expected a permissions error, got: %v", errOutput)
} }
@ -197,7 +190,7 @@ func TestRemote_applyWithVCS(t *testing.T) {
t.Fatalf("error creating named workspace: %v", err) t.Fatalf("error creating named workspace: %v", err)
} }
op, configCleanup, playback := testOperationApplyWithDiagnostics(t, "./testdata/apply") op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup() defer configCleanup()
op.Workspace = "prod" op.Workspace = "prod"
@ -208,6 +201,7 @@ func TestRemote_applyWithVCS(t *testing.T) {
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected apply operation to fail") t.Fatal("expected apply operation to fail")
} }
@ -215,7 +209,7 @@ func TestRemote_applyWithVCS(t *testing.T) {
t.Fatalf("expected plan to be empty") t.Fatalf("expected plan to be empty")
} }
errOutput := playback().Err().Error() errOutput := output.Stderr()
if !strings.Contains(errOutput, "not allowed for workspaces with a VCS") { if !strings.Contains(errOutput, "not allowed for workspaces with a VCS") {
t.Fatalf("expected a VCS error, got: %v", errOutput) t.Fatalf("expected a VCS error, got: %v", errOutput)
} }
@ -225,7 +219,7 @@ func TestRemote_applyWithParallelism(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup, playback := testOperationApplyWithDiagnostics(t, "./testdata/apply") op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup() defer configCleanup()
op.Parallelism = 3 op.Parallelism = 3
@ -237,11 +231,12 @@ func TestRemote_applyWithParallelism(t *testing.T) {
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected apply operation to fail") t.Fatal("expected apply operation to fail")
} }
errOutput := playback().Err().Error() errOutput := output.Stderr()
if !strings.Contains(errOutput, "parallelism values are currently not supported") { if !strings.Contains(errOutput, "parallelism values are currently not supported") {
t.Fatalf("expected a parallelism error, got: %v", errOutput) t.Fatalf("expected a parallelism error, got: %v", errOutput)
} }
@ -251,7 +246,7 @@ func TestRemote_applyWithPlan(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup, playback := testOperationApplyWithDiagnostics(t, "./testdata/apply") op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup() defer configCleanup()
op.PlanFile = &planfile.Reader{} op.PlanFile = &planfile.Reader{}
@ -263,6 +258,7 @@ func TestRemote_applyWithPlan(t *testing.T) {
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected apply operation to fail") t.Fatal("expected apply operation to fail")
} }
@ -270,7 +266,7 @@ func TestRemote_applyWithPlan(t *testing.T) {
t.Fatalf("expected plan to be empty") t.Fatalf("expected plan to be empty")
} }
errOutput := playback().Err().Error() errOutput := output.Stderr()
if !strings.Contains(errOutput, "saved plan is currently not supported") { if !strings.Contains(errOutput, "saved plan is currently not supported") {
t.Fatalf("expected a saved plan error, got: %v", errOutput) t.Fatalf("expected a saved plan error, got: %v", errOutput)
} }
@ -280,7 +276,7 @@ func TestRemote_applyWithoutRefresh(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup, playback := testOperationApplyWithDiagnostics(t, "./testdata/apply") op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup() defer configCleanup()
op.PlanRefresh = false op.PlanRefresh = false
@ -292,11 +288,12 @@ func TestRemote_applyWithoutRefresh(t *testing.T) {
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected apply operation to fail") t.Fatal("expected apply operation to fail")
} }
errOutput := playback().Err().Error() errOutput := output.Stderr()
if !strings.Contains(errOutput, "refresh is currently not supported") { if !strings.Contains(errOutput, "refresh is currently not supported") {
t.Fatalf("expected a refresh error, got: %v", errOutput) t.Fatalf("expected a refresh error, got: %v", errOutput)
} }
@ -306,8 +303,9 @@ func TestRemote_applyWithTarget(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationApply(t, "./testdata/apply") op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup() defer configCleanup()
defer done(t)
addr, _ := addrs.ParseAbsResourceStr("null_resource.foo") addr, _ := addrs.ParseAbsResourceStr("null_resource.foo")
@ -344,7 +342,7 @@ func TestRemote_applyWithTargetIncompatibleAPIVersion(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup, playback := testOperationPlanWithDiagnostics(t, "./testdata/plan") op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup() defer configCleanup()
// Set the tfe client's RemoteAPIVersion to an empty string, to mimic // Set the tfe client's RemoteAPIVersion to an empty string, to mimic
@ -362,6 +360,7 @@ func TestRemote_applyWithTargetIncompatibleAPIVersion(t *testing.T) {
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected apply operation to fail") t.Fatal("expected apply operation to fail")
} }
@ -369,7 +368,7 @@ func TestRemote_applyWithTargetIncompatibleAPIVersion(t *testing.T) {
t.Fatalf("expected plan to be empty") t.Fatalf("expected plan to be empty")
} }
errOutput := playback().Err().Error() errOutput := output.Stderr()
if !strings.Contains(errOutput, "Resource targeting is not supported") { if !strings.Contains(errOutput, "Resource targeting is not supported") {
t.Fatalf("expected a targeting error, got: %v", errOutput) t.Fatalf("expected a targeting error, got: %v", errOutput)
} }
@ -379,7 +378,7 @@ func TestRemote_applyWithVariables(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup, playback := testOperationApplyWithDiagnostics(t, "./testdata/apply-variables") op, configCleanup, done := testOperationApply(t, "./testdata/apply-variables")
defer configCleanup() defer configCleanup()
op.Variables = testVariables(terraform.ValueFromNamedFile, "foo", "bar") op.Variables = testVariables(terraform.ValueFromNamedFile, "foo", "bar")
@ -391,11 +390,12 @@ func TestRemote_applyWithVariables(t *testing.T) {
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected apply operation to fail") t.Fatal("expected apply operation to fail")
} }
errOutput := playback().Err().Error() errOutput := output.Stderr()
if !strings.Contains(errOutput, "variables are currently not supported") { if !strings.Contains(errOutput, "variables are currently not supported") {
t.Fatalf("expected a variables error, got: %v", errOutput) t.Fatalf("expected a variables error, got: %v", errOutput)
} }
@ -405,7 +405,7 @@ func TestRemote_applyNoConfig(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup, playback := testOperationApplyWithDiagnostics(t, "./testdata/empty") op, configCleanup, done := testOperationApply(t, "./testdata/empty")
defer configCleanup() defer configCleanup()
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -416,6 +416,7 @@ func TestRemote_applyNoConfig(t *testing.T) {
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected apply operation to fail") t.Fatal("expected apply operation to fail")
} }
@ -423,7 +424,7 @@ func TestRemote_applyNoConfig(t *testing.T) {
t.Fatalf("expected plan to be empty") t.Fatalf("expected plan to be empty")
} }
errOutput := playback().Err().Error() errOutput := output.Stderr()
if !strings.Contains(errOutput, "configuration files found") { if !strings.Contains(errOutput, "configuration files found") {
t.Fatalf("expected configuration files error, got: %v", errOutput) t.Fatalf("expected configuration files error, got: %v", errOutput)
} }
@ -439,8 +440,9 @@ func TestRemote_applyNoChanges(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationApply(t, "./testdata/apply-no-changes") op, configCleanup, done := testOperationApply(t, "./testdata/apply-no-changes")
defer configCleanup() defer configCleanup()
defer done(t)
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -470,7 +472,7 @@ func TestRemote_applyNoApprove(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup, playback := testOperationApplyWithDiagnostics(t, "./testdata/apply") op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup() defer configCleanup()
input := testInput(t, map[string]string{ input := testInput(t, map[string]string{
@ -487,6 +489,7 @@ func TestRemote_applyNoApprove(t *testing.T) {
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected apply operation to fail") t.Fatal("expected apply operation to fail")
} }
@ -498,7 +501,7 @@ func TestRemote_applyNoApprove(t *testing.T) {
t.Fatalf("expected no unused answers, got: %v", input.answers) t.Fatalf("expected no unused answers, got: %v", input.answers)
} }
errOutput := playback().Err().Error() errOutput := output.Stderr()
if !strings.Contains(errOutput, "Apply discarded") { if !strings.Contains(errOutput, "Apply discarded") {
t.Fatalf("expected an apply discarded error, got: %v", errOutput) t.Fatalf("expected an apply discarded error, got: %v", errOutput)
} }
@ -508,8 +511,9 @@ func TestRemote_applyAutoApprove(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationApply(t, "./testdata/apply") op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup() defer configCleanup()
defer done(t)
input := testInput(t, map[string]string{ input := testInput(t, map[string]string{
"approve": "no", "approve": "no",
@ -553,8 +557,9 @@ func TestRemote_applyApprovedExternally(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationApply(t, "./testdata/apply") op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup() defer configCleanup()
defer done(t)
input := testInput(t, map[string]string{ input := testInput(t, map[string]string{
"approve": "wait-for-external-update", "approve": "wait-for-external-update",
@ -628,8 +633,9 @@ func TestRemote_applyDiscardedExternally(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationApply(t, "./testdata/apply") op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup() defer configCleanup()
defer done(t)
input := testInput(t, map[string]string{ input := testInput(t, map[string]string{
"approve": "wait-for-external-update", "approve": "wait-for-external-update",
@ -716,8 +722,9 @@ func TestRemote_applyWithAutoApply(t *testing.T) {
t.Fatalf("error creating named workspace: %v", err) t.Fatalf("error creating named workspace: %v", err)
} }
op, configCleanup := testOperationApply(t, "./testdata/apply") op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup() defer configCleanup()
defer done(t)
input := testInput(t, map[string]string{ input := testInput(t, map[string]string{
"approve": "yes", "approve": "yes",
@ -767,8 +774,9 @@ func TestRemote_applyForceLocal(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationApply(t, "./testdata/apply") op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup() defer configCleanup()
defer done(t)
input := testInput(t, map[string]string{ input := testInput(t, map[string]string{
"approve": "yes", "approve": "yes",
@ -829,8 +837,9 @@ func TestRemote_applyWorkspaceWithoutOperations(t *testing.T) {
t.Fatalf("error creating named workspace: %v", err) t.Fatalf("error creating named workspace: %v", err)
} }
op, configCleanup := testOperationApply(t, "./testdata/apply") op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup() defer configCleanup()
defer done(t)
input := testInput(t, map[string]string{ input := testInput(t, map[string]string{
"approve": "yes", "approve": "yes",
@ -900,8 +909,9 @@ func TestRemote_applyLockTimeout(t *testing.T) {
t.Fatalf("error creating pending run: %v", err) t.Fatalf("error creating pending run: %v", err)
} }
op, configCleanup := testOperationApplyWithTimeout(t, "./testdata/apply", 50*time.Millisecond) op, configCleanup, done := testOperationApplyWithTimeout(t, "./testdata/apply", 50*time.Millisecond)
defer configCleanup() defer configCleanup()
defer done(t)
input := testInput(t, map[string]string{ input := testInput(t, map[string]string{
"cancel": "yes", "cancel": "yes",
@ -950,8 +960,9 @@ func TestRemote_applyDestroy(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationApply(t, "./testdata/apply-destroy") op, configCleanup, done := testOperationApply(t, "./testdata/apply-destroy")
defer configCleanup() defer configCleanup()
defer done(t)
input := testInput(t, map[string]string{ input := testInput(t, map[string]string{
"approve": "yes", "approve": "yes",
@ -999,8 +1010,9 @@ func TestRemote_applyDestroyNoConfig(t *testing.T) {
"approve": "yes", "approve": "yes",
}) })
op, configCleanup := testOperationApply(t, "./testdata/empty") op, configCleanup, done := testOperationApply(t, "./testdata/empty")
defer configCleanup() defer configCleanup()
defer done(t)
op.Destroy = true op.Destroy = true
op.UIIn = input op.UIIn = input
@ -1029,8 +1041,9 @@ func TestRemote_applyPolicyPass(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationApply(t, "./testdata/apply-policy-passed") op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-passed")
defer configCleanup() defer configCleanup()
defer done(t)
input := testInput(t, map[string]string{ input := testInput(t, map[string]string{
"approve": "yes", "approve": "yes",
@ -1076,7 +1089,7 @@ func TestRemote_applyPolicyHardFail(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup, playback := testOperationApplyWithDiagnostics(t, "./testdata/apply-policy-hard-failed") op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-hard-failed")
defer configCleanup() defer configCleanup()
input := testInput(t, map[string]string{ input := testInput(t, map[string]string{
@ -1093,6 +1106,7 @@ func TestRemote_applyPolicyHardFail(t *testing.T) {
} }
<-run.Done() <-run.Done()
viewOutput := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected apply operation to fail") t.Fatal("expected apply operation to fail")
} }
@ -1104,7 +1118,7 @@ func TestRemote_applyPolicyHardFail(t *testing.T) {
t.Fatalf("expected an unused answers, got: %v", input.answers) t.Fatalf("expected an unused answers, got: %v", input.answers)
} }
errOutput := playback().Err().Error() errOutput := viewOutput.Stderr()
if !strings.Contains(errOutput, "hard failed") { if !strings.Contains(errOutput, "hard failed") {
t.Fatalf("expected a policy check error, got: %v", errOutput) t.Fatalf("expected a policy check error, got: %v", errOutput)
} }
@ -1128,8 +1142,9 @@ func TestRemote_applyPolicySoftFail(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationApply(t, "./testdata/apply-policy-soft-failed") op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-soft-failed")
defer configCleanup() defer configCleanup()
defer done(t)
input := testInput(t, map[string]string{ input := testInput(t, map[string]string{
"override": "override", "override": "override",
@ -1176,7 +1191,7 @@ func TestRemote_applyPolicySoftFailAutoApprove(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup, playback := testOperationApplyWithDiagnostics(t, "./testdata/apply-policy-soft-failed") op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-soft-failed")
defer configCleanup() defer configCleanup()
input := testInput(t, map[string]string{ input := testInput(t, map[string]string{
@ -1194,6 +1209,7 @@ func TestRemote_applyPolicySoftFailAutoApprove(t *testing.T) {
} }
<-run.Done() <-run.Done()
viewOutput := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected apply operation to fail") t.Fatal("expected apply operation to fail")
} }
@ -1205,7 +1221,7 @@ func TestRemote_applyPolicySoftFailAutoApprove(t *testing.T) {
t.Fatalf("expected an unused answers, got: %v", input.answers) t.Fatalf("expected an unused answers, got: %v", input.answers)
} }
errOutput := playback().Err().Error() errOutput := viewOutput.Stderr()
if !strings.Contains(errOutput, "soft failed") { if !strings.Contains(errOutput, "soft failed") {
t.Fatalf("expected a policy check error, got: %v", errOutput) t.Fatalf("expected a policy check error, got: %v", errOutput)
} }
@ -1242,8 +1258,9 @@ func TestRemote_applyPolicySoftFailAutoApply(t *testing.T) {
t.Fatalf("error creating named workspace: %v", err) t.Fatalf("error creating named workspace: %v", err)
} }
op, configCleanup := testOperationApply(t, "./testdata/apply-policy-soft-failed") op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-soft-failed")
defer configCleanup() defer configCleanup()
defer done(t)
input := testInput(t, map[string]string{ input := testInput(t, map[string]string{
"override": "override", "override": "override",
@ -1290,8 +1307,9 @@ func TestRemote_applyWithRemoteError(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationApply(t, "./testdata/apply-with-error") op, configCleanup, done := testOperationApply(t, "./testdata/apply-with-error")
defer configCleanup() defer configCleanup()
defer done(t)
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -1393,13 +1411,12 @@ func TestRemote_applyVersionCheck(t *testing.T) {
} }
// RUN: prepare the apply operation and run it // RUN: prepare the apply operation and run it
op, configCleanup, playback := testOperationApplyWithDiagnostics(t, "./testdata/apply") op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup() defer configCleanup()
streams, done := terminal.StreamsForTesting(t) streams, done := terminal.StreamsForTesting(t)
view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
op.View = view op.View = view
defer done(t)
input := testInput(t, map[string]string{ input := testInput(t, map[string]string{
"approve": "yes", "approve": "yes",
@ -1416,6 +1433,7 @@ func TestRemote_applyVersionCheck(t *testing.T) {
// RUN: wait for completion // RUN: wait for completion
<-run.Done() <-run.Done()
output := done(t)
if tc.wantErr != "" { if tc.wantErr != "" {
// ASSERT: if the test case wants an error, check for failure // ASSERT: if the test case wants an error, check for failure
@ -1423,7 +1441,7 @@ func TestRemote_applyVersionCheck(t *testing.T) {
if run.Result != backend.OperationFailure { if run.Result != backend.OperationFailure {
t.Fatalf("expected run to fail, but result was %#v", run.Result) t.Fatalf("expected run to fail, but result was %#v", run.Result)
} }
errOutput := playback().Err().Error() errOutput := output.Stderr()
if !strings.Contains(errOutput, tc.wantErr) { if !strings.Contains(errOutput, tc.wantErr) {
t.Fatalf("missing error %q\noutput: %s", tc.wantErr, errOutput) t.Fatalf("missing error %q\noutput: %s", tc.wantErr, errOutput)
} }

View File

@ -21,52 +21,43 @@ import (
"github.com/hashicorp/terraform/plans/planfile" "github.com/hashicorp/terraform/plans/planfile"
"github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func()) { func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
t.Helper() t.Helper()
return testOperationPlanWithTimeout(t, configDir, 0) return testOperationPlanWithTimeout(t, configDir, 0)
} }
func testOperationPlanWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func()) { func testOperationPlanWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
t.Helper() t.Helper()
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
streams, _ := terminal.StreamsForTesting(t) streams, done := terminal.StreamsForTesting(t)
view := views.NewStateLocker(arguments.ViewHuman, views.NewView(streams)) view := views.NewView(streams)
stateLockerView := views.NewStateLocker(arguments.ViewHuman, view)
operationView := views.NewOperation(arguments.ViewHuman, false, view)
return &backend.Operation{ return &backend.Operation{
ConfigDir: configDir, ConfigDir: configDir,
ConfigLoader: configLoader, ConfigLoader: configLoader,
Parallelism: defaultParallelism, Parallelism: defaultParallelism,
PlanRefresh: true, PlanRefresh: true,
ShowDiagnostics: testLogDiagnostics(t), StateLocker: clistate.NewLocker(timeout, stateLockerView),
StateLocker: clistate.NewLocker(timeout, view),
Type: backend.OperationTypePlan, Type: backend.OperationTypePlan,
}, configCleanup View: operationView,
} }, configCleanup, done
func testOperationPlanWithDiagnostics(t *testing.T, configDir string) (*backend.Operation, func(), func() tfdiags.Diagnostics) {
t.Helper()
op, cleanup := testOperationPlan(t, configDir)
record, playback := testRecordDiagnostics(t)
op.ShowDiagnostics = record
return op, cleanup, playback
} }
func TestRemote_planBasic(t *testing.T) { func TestRemote_planBasic(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationPlan(t, "./testdata/plan") op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup() defer configCleanup()
defer done(t)
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -102,8 +93,9 @@ func TestRemote_planCanceled(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationPlan(t, "./testdata/plan") op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup() defer configCleanup()
defer done(t)
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -131,8 +123,9 @@ func TestRemote_planLongLine(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationPlan(t, "./testdata/plan-long-line") op, configCleanup, done := testOperationPlan(t, "./testdata/plan-long-line")
defer configCleanup() defer configCleanup()
defer done(t)
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -175,7 +168,7 @@ func TestRemote_planWithoutPermissions(t *testing.T) {
} }
w.Permissions.CanQueueRun = false w.Permissions.CanQueueRun = false
op, configCleanup, playback := testOperationPlanWithDiagnostics(t, "./testdata/plan") op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup() defer configCleanup()
op.Workspace = "prod" op.Workspace = "prod"
@ -186,11 +179,12 @@ func TestRemote_planWithoutPermissions(t *testing.T) {
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected plan operation to fail") t.Fatal("expected plan operation to fail")
} }
errOutput := playback().Err().Error() errOutput := output.Stderr()
if !strings.Contains(errOutput, "Insufficient rights to generate a plan") { if !strings.Contains(errOutput, "Insufficient rights to generate a plan") {
t.Fatalf("expected a permissions error, got: %v", errOutput) t.Fatalf("expected a permissions error, got: %v", errOutput)
} }
@ -200,7 +194,7 @@ func TestRemote_planWithParallelism(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup, playback := testOperationPlanWithDiagnostics(t, "./testdata/plan") op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup() defer configCleanup()
op.Parallelism = 3 op.Parallelism = 3
@ -212,11 +206,12 @@ func TestRemote_planWithParallelism(t *testing.T) {
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected plan operation to fail") t.Fatal("expected plan operation to fail")
} }
errOutput := playback().Err().Error() errOutput := output.Stderr()
if !strings.Contains(errOutput, "parallelism values are currently not supported") { if !strings.Contains(errOutput, "parallelism values are currently not supported") {
t.Fatalf("expected a parallelism error, got: %v", errOutput) t.Fatalf("expected a parallelism error, got: %v", errOutput)
} }
@ -226,7 +221,7 @@ func TestRemote_planWithPlan(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup, playback := testOperationPlanWithDiagnostics(t, "./testdata/plan") op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup() defer configCleanup()
op.PlanFile = &planfile.Reader{} op.PlanFile = &planfile.Reader{}
@ -238,6 +233,7 @@ func TestRemote_planWithPlan(t *testing.T) {
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected plan operation to fail") t.Fatal("expected plan operation to fail")
} }
@ -245,7 +241,7 @@ func TestRemote_planWithPlan(t *testing.T) {
t.Fatalf("expected plan to be empty") t.Fatalf("expected plan to be empty")
} }
errOutput := playback().Err().Error() errOutput := output.Stderr()
if !strings.Contains(errOutput, "saved plan is currently not supported") { if !strings.Contains(errOutput, "saved plan is currently not supported") {
t.Fatalf("expected a saved plan error, got: %v", errOutput) t.Fatalf("expected a saved plan error, got: %v", errOutput)
} }
@ -255,7 +251,7 @@ func TestRemote_planWithPath(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup, playback := testOperationPlanWithDiagnostics(t, "./testdata/plan") op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup() defer configCleanup()
op.PlanOutPath = "./testdata/plan" op.PlanOutPath = "./testdata/plan"
@ -267,6 +263,7 @@ func TestRemote_planWithPath(t *testing.T) {
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected plan operation to fail") t.Fatal("expected plan operation to fail")
} }
@ -274,7 +271,7 @@ func TestRemote_planWithPath(t *testing.T) {
t.Fatalf("expected plan to be empty") t.Fatalf("expected plan to be empty")
} }
errOutput := playback().Err().Error() errOutput := output.Stderr()
if !strings.Contains(errOutput, "generated plan is currently not supported") { if !strings.Contains(errOutput, "generated plan is currently not supported") {
t.Fatalf("expected a generated plan error, got: %v", errOutput) t.Fatalf("expected a generated plan error, got: %v", errOutput)
} }
@ -284,7 +281,7 @@ func TestRemote_planWithoutRefresh(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup, playback := testOperationPlanWithDiagnostics(t, "./testdata/plan") op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup() defer configCleanup()
op.PlanRefresh = false op.PlanRefresh = false
@ -296,11 +293,12 @@ func TestRemote_planWithoutRefresh(t *testing.T) {
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected plan operation to fail") t.Fatal("expected plan operation to fail")
} }
errOutput := playback().Err().Error() errOutput := output.Stderr()
if !strings.Contains(errOutput, "refresh is currently not supported") { if !strings.Contains(errOutput, "refresh is currently not supported") {
t.Fatalf("expected a refresh error, got: %v", errOutput) t.Fatalf("expected a refresh error, got: %v", errOutput)
} }
@ -333,8 +331,9 @@ func TestRemote_planWithTarget(t *testing.T) {
} }
} }
op, configCleanup := testOperationPlan(t, "./testdata/plan") op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup() defer configCleanup()
defer done(t)
addr, _ := addrs.ParseAbsResourceStr("null_resource.foo") addr, _ := addrs.ParseAbsResourceStr("null_resource.foo")
@ -382,7 +381,7 @@ func TestRemote_planWithTargetIncompatibleAPIVersion(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup, playback := testOperationPlanWithDiagnostics(t, "./testdata/plan") op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup() defer configCleanup()
// Set the tfe client's RemoteAPIVersion to an empty string, to mimic // Set the tfe client's RemoteAPIVersion to an empty string, to mimic
@ -400,6 +399,7 @@ func TestRemote_planWithTargetIncompatibleAPIVersion(t *testing.T) {
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected plan operation to fail") t.Fatal("expected plan operation to fail")
} }
@ -407,7 +407,7 @@ func TestRemote_planWithTargetIncompatibleAPIVersion(t *testing.T) {
t.Fatalf("expected plan to be empty") t.Fatalf("expected plan to be empty")
} }
errOutput := playback().Err().Error() errOutput := output.Stderr()
if !strings.Contains(errOutput, "Resource targeting is not supported") { if !strings.Contains(errOutput, "Resource targeting is not supported") {
t.Fatalf("expected a targeting error, got: %v", errOutput) t.Fatalf("expected a targeting error, got: %v", errOutput)
} }
@ -417,7 +417,7 @@ func TestRemote_planWithVariables(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup, playback := testOperationPlanWithDiagnostics(t, "./testdata/plan-variables") op, configCleanup, done := testOperationPlan(t, "./testdata/plan-variables")
defer configCleanup() defer configCleanup()
op.Variables = testVariables(terraform.ValueFromCLIArg, "foo", "bar") op.Variables = testVariables(terraform.ValueFromCLIArg, "foo", "bar")
@ -429,11 +429,12 @@ func TestRemote_planWithVariables(t *testing.T) {
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected plan operation to fail") t.Fatal("expected plan operation to fail")
} }
errOutput := playback().Err().Error() errOutput := output.Stderr()
if !strings.Contains(errOutput, "variables are currently not supported") { if !strings.Contains(errOutput, "variables are currently not supported") {
t.Fatalf("expected a variables error, got: %v", errOutput) t.Fatalf("expected a variables error, got: %v", errOutput)
} }
@ -443,7 +444,7 @@ func TestRemote_planNoConfig(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup, playback := testOperationPlanWithDiagnostics(t, "./testdata/empty") op, configCleanup, done := testOperationPlan(t, "./testdata/empty")
defer configCleanup() defer configCleanup()
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -454,6 +455,7 @@ func TestRemote_planNoConfig(t *testing.T) {
} }
<-run.Done() <-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected plan operation to fail") t.Fatal("expected plan operation to fail")
} }
@ -461,7 +463,7 @@ func TestRemote_planNoConfig(t *testing.T) {
t.Fatalf("expected plan to be empty") t.Fatalf("expected plan to be empty")
} }
errOutput := playback().Err().Error() errOutput := output.Stderr()
if !strings.Contains(errOutput, "configuration files found") { if !strings.Contains(errOutput, "configuration files found") {
t.Fatalf("expected configuration files error, got: %v", errOutput) t.Fatalf("expected configuration files error, got: %v", errOutput)
} }
@ -471,8 +473,9 @@ func TestRemote_planNoChanges(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationPlan(t, "./testdata/plan-no-changes") op, configCleanup, done := testOperationPlan(t, "./testdata/plan-no-changes")
defer configCleanup() defer configCleanup()
defer done(t)
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -509,8 +512,9 @@ func TestRemote_planForceLocal(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationPlan(t, "./testdata/plan") op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup() defer configCleanup()
defer done(t)
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -544,8 +548,9 @@ func TestRemote_planWithoutOperationsEntitlement(t *testing.T) {
b, bCleanup := testBackendNoOperations(t) b, bCleanup := testBackendNoOperations(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationPlan(t, "./testdata/plan") op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup() defer configCleanup()
defer done(t)
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -593,8 +598,9 @@ func TestRemote_planWorkspaceWithoutOperations(t *testing.T) {
t.Fatalf("error creating named workspace: %v", err) t.Fatalf("error creating named workspace: %v", err)
} }
op, configCleanup := testOperationPlan(t, "./testdata/plan") op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup() defer configCleanup()
defer done(t)
op.Workspace = "no-operations" op.Workspace = "no-operations"
@ -651,8 +657,9 @@ func TestRemote_planLockTimeout(t *testing.T) {
t.Fatalf("error creating pending run: %v", err) t.Fatalf("error creating pending run: %v", err)
} }
op, configCleanup := testOperationPlanWithTimeout(t, "./testdata/plan", 50) op, configCleanup, done := testOperationPlanWithTimeout(t, "./testdata/plan", 50)
defer configCleanup() defer configCleanup()
defer done(t)
input := testInput(t, map[string]string{ input := testInput(t, map[string]string{
"cancel": "yes", "cancel": "yes",
@ -698,8 +705,9 @@ func TestRemote_planDestroy(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationPlan(t, "./testdata/plan") op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup() defer configCleanup()
defer done(t)
op.Destroy = true op.Destroy = true
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -722,8 +730,9 @@ func TestRemote_planDestroyNoConfig(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationPlan(t, "./testdata/empty") op, configCleanup, done := testOperationPlan(t, "./testdata/empty")
defer configCleanup() defer configCleanup()
defer done(t)
op.Destroy = true op.Destroy = true
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -756,8 +765,9 @@ func TestRemote_planWithWorkingDirectory(t *testing.T) {
t.Fatalf("error configuring working directory: %v", err) t.Fatalf("error configuring working directory: %v", err)
} }
op, configCleanup := testOperationPlan(t, "./testdata/plan-with-working-directory/terraform") op, configCleanup, done := testOperationPlan(t, "./testdata/plan-with-working-directory/terraform")
defer configCleanup() defer configCleanup()
defer done(t)
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -814,8 +824,9 @@ func TestRemote_planWithWorkingDirectoryFromCurrentPath(t *testing.T) {
// For this test we need to give our current directory instead of the // For this test we need to give our current directory instead of the
// full path to the configuration as we already changed directories. // full path to the configuration as we already changed directories.
op, configCleanup := testOperationPlan(t, ".") op, configCleanup, done := testOperationPlan(t, ".")
defer configCleanup() defer configCleanup()
defer done(t)
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -845,8 +856,9 @@ func TestRemote_planCostEstimation(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationPlan(t, "./testdata/plan-cost-estimation") op, configCleanup, done := testOperationPlan(t, "./testdata/plan-cost-estimation")
defer configCleanup() defer configCleanup()
defer done(t)
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -879,8 +891,9 @@ func TestRemote_planPolicyPass(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationPlan(t, "./testdata/plan-policy-passed") op, configCleanup, done := testOperationPlan(t, "./testdata/plan-policy-passed")
defer configCleanup() defer configCleanup()
defer done(t)
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -913,7 +926,7 @@ func TestRemote_planPolicyHardFail(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup, playback := testOperationPlanWithDiagnostics(t, "./testdata/plan-policy-hard-failed") op, configCleanup, done := testOperationPlan(t, "./testdata/plan-policy-hard-failed")
defer configCleanup() defer configCleanup()
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -924,6 +937,7 @@ func TestRemote_planPolicyHardFail(t *testing.T) {
} }
<-run.Done() <-run.Done()
viewOutput := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected plan operation to fail") t.Fatal("expected plan operation to fail")
} }
@ -931,7 +945,7 @@ func TestRemote_planPolicyHardFail(t *testing.T) {
t.Fatalf("expected plan to be empty") t.Fatalf("expected plan to be empty")
} }
errOutput := playback().Err().Error() errOutput := viewOutput.Stderr()
if !strings.Contains(errOutput, "hard failed") { if !strings.Contains(errOutput, "hard failed") {
t.Fatalf("expected a policy check error, got: %v", errOutput) t.Fatalf("expected a policy check error, got: %v", errOutput)
} }
@ -952,7 +966,7 @@ func TestRemote_planPolicySoftFail(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup, playback := testOperationPlanWithDiagnostics(t, "./testdata/plan-policy-soft-failed") op, configCleanup, done := testOperationPlan(t, "./testdata/plan-policy-soft-failed")
defer configCleanup() defer configCleanup()
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -963,6 +977,7 @@ func TestRemote_planPolicySoftFail(t *testing.T) {
} }
<-run.Done() <-run.Done()
viewOutput := done(t)
if run.Result == backend.OperationSuccess { if run.Result == backend.OperationSuccess {
t.Fatal("expected plan operation to fail") t.Fatal("expected plan operation to fail")
} }
@ -970,7 +985,7 @@ func TestRemote_planPolicySoftFail(t *testing.T) {
t.Fatalf("expected plan to be empty") t.Fatalf("expected plan to be empty")
} }
errOutput := playback().Err().Error() errOutput := viewOutput.Stderr()
if !strings.Contains(errOutput, "soft failed") { if !strings.Contains(errOutput, "soft failed") {
t.Fatalf("expected a policy check error, got: %v", errOutput) t.Fatalf("expected a policy check error, got: %v", errOutput)
} }
@ -991,8 +1006,9 @@ func TestRemote_planWithRemoteError(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationPlan(t, "./testdata/plan-with-error") op, configCleanup, done := testOperationPlan(t, "./testdata/plan-with-error")
defer configCleanup() defer configCleanup()
defer done(t)
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -1022,8 +1038,9 @@ func TestRemote_planOtherError(t *testing.T) {
b, bCleanup := testBackendDefault(t) b, bCleanup := testBackendDefault(t)
defer bCleanup() defer bCleanup()
op, configCleanup := testOperationPlan(t, "./testdata/plan") op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup() defer configCleanup()
defer done(t)
op.Workspace = "network-error" // custom error response in backend_mock.go op.Workspace = "network-error" // custom error response in backend_mock.go

View File

@ -297,43 +297,3 @@ func testVariables(s terraform.ValueSourceType, vs ...string) map[string]backend
} }
return vars return vars
} }
// testRecordDiagnostics allows tests to record and later inspect diagnostics
// emitted during an Operation. It returns a record function which can be set
// as the ShowDiagnostics value of an Operation, and a playback function which
// returns the recorded diagnostics for inspection.
func testRecordDiagnostics(t *testing.T) (record func(vals ...interface{}), playback func() tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
record = func(vals ...interface{}) {
diags = diags.Append(vals...)
}
playback = func() tfdiags.Diagnostics {
diags.Sort()
return diags
}
return
}
// testLogDiagnostics returns a function which can be used as the
// ShowDiagnostics value for an Operation, in order to help debugging during
// tests. Any calls to this function result in test logs.
func testLogDiagnostics(t *testing.T) func(vals ...interface{}) {
return func(vals ...interface{}) {
var diags tfdiags.Diagnostics
diags = diags.Append(vals...)
diags.Sort()
for _, diag := range diags {
// NOTE: Since the caller here is not directly the TestLocal
// function, t.Helper doesn't apply and so the log source
// isn't correctly shown in the test log output. This seems
// unavoidable as long as this is happening so indirectly.
desc := diag.Description()
if desc.Detail != "" {
t.Logf("%s: %s", desc.Summary, desc.Detail)
} else {
t.Log(desc.Summary)
}
}
}
}

View File

@ -252,17 +252,6 @@ func (c *ApplyCommand) OperationRequest(
opReq.Type = backend.OperationTypeApply opReq.Type = backend.OperationTypeApply
opReq.View = view.Operation() opReq.View = view.Operation()
// FIXME: To allow errors to be easily rendered, the showDiagnostics method
// accepts ...interface{}. The backend no longer needs this, as only
// tfdiags.Diagnostics values are used. Once we have migrated plan and refresh
// to use views, we can remove ShowDiagnostics and update the backend code to
// call view.Diagnostics() instead.
opReq.ShowDiagnostics = func(vals ...interface{}) {
var diags tfdiags.Diagnostics
diags = diags.Append(vals...)
view.Diagnostics(diags)
}
var err error var err error
opReq.ConfigLoader, err = c.initConfigLoader() opReq.ConfigLoader, err = c.initConfigLoader()
if err != nil { if err != nil {

View File

@ -147,12 +147,6 @@ func (c *PlanCommand) OperationRequest(
opReq.Targets = args.Targets opReq.Targets = args.Targets
opReq.Type = backend.OperationTypePlan opReq.Type = backend.OperationTypePlan
opReq.View = view.Operation() opReq.View = view.Operation()
// FIXME: this shim is needed until the remote backend is migrated to views
opReq.ShowDiagnostics = func(vals ...interface{}) {
var diags tfdiags.Diagnostics
diags = diags.Append(vals...)
view.Diagnostics(diags)
}
var err error var err error
opReq.ConfigLoader, err = c.initConfigLoader() opReq.ConfigLoader, err = c.initConfigLoader()

View File

@ -135,12 +135,6 @@ func (c *RefreshCommand) OperationRequest(be backend.Enhanced, view views.Refres
opReq.Targets = args.Targets opReq.Targets = args.Targets
opReq.Type = backend.OperationTypeRefresh opReq.Type = backend.OperationTypeRefresh
opReq.View = view.Operation() opReq.View = view.Operation()
// FIXME: this shim is needed until the remote backend is migrated to views
opReq.ShowDiagnostics = func(vals ...interface{}) {
var diags tfdiags.Diagnostics
diags = diags.Append(vals...)
view.Diagnostics(diags)
}
var err error var err error
opReq.ConfigLoader, err = c.initConfigLoader() opReq.ConfigLoader, err = c.initConfigLoader()