-
Notifications
You must be signed in to change notification settings - Fork 9.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
stackeval: Improve coverage of namedPromiseReporter impls
The "namedPromiseReporter" interface is a helper we use to collect user-friendly names for promises when we need to report a promise self-reference error to the user. We collect names for promises reactively on error, rather than proactively in the normal course of work, because proactively maintaining a suitable data structure for this cross-cutting concern would be both expensive and complicated due to it being a cross-cutting concern. This compromise therefore allows us to pay the cost of walking over all of the promise-owning objects only in the exceptional case where we need to report a self-reference error, but it does come at the expense of it being possible for us to forget to implement this interface for certain types and thus be unable to name a subset of the affected promises. This commit implements reportNamedPromises for all promise-owning types that are reachable from a stackeval.Main, although there is a notable gap here where we can't actually report promises from the individual instances of a multi-instance object (e.g. a component) because the promising API doesn't let us peek into a promising.Once to see if it's resolved without blocking until it becomes resolved. There are FIXME comments for that limitation in all of the places where it's relevant, and so hopefully in a future commit we'll devise a suitable way to perform a fallible non-blocking peek of a promise so that we can report promises nested in its result only if they are already present.
- Loading branch information
1 parent
a4d19a8
commit 9448845
Showing
18 changed files
with
394 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
179 changes: 179 additions & 0 deletions
179
internal/stacks/stackruntime/internal/stackeval/diagnostics_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
package stackeval | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
"github.com/zclconf/go-cty/cty" | ||
|
||
"github.com/hashicorp/terraform/internal/addrs" | ||
"github.com/hashicorp/terraform/internal/collections" | ||
"github.com/hashicorp/terraform/internal/configs/configschema" | ||
"github.com/hashicorp/terraform/internal/plans" | ||
"github.com/hashicorp/terraform/internal/promising" | ||
"github.com/hashicorp/terraform/internal/providers" | ||
providertest "github.com/hashicorp/terraform/internal/providers/testing" | ||
"github.com/hashicorp/terraform/internal/stacks/stackaddrs" | ||
"github.com/hashicorp/terraform/internal/stacks/stackstate" | ||
"github.com/hashicorp/terraform/internal/tfdiags" | ||
) | ||
|
||
func TestNamedPromisesPlan(t *testing.T) { | ||
// The goal of this test is to make sure we retain namedPromiseReporter | ||
// coverage over various important object types, so that we don't | ||
// accidentally regress the quality of self-reference ("dependency cycle") | ||
// errors under future maintenence. | ||
// | ||
// It isn't totally comprehensive over all implementations of | ||
// namedPromiseReporter, but we do aim to cover the main cases that a | ||
// typical stack configuration might hit. | ||
// | ||
// This is intentionally a test of the namedPromiseReporter implementations | ||
// directly, rather than of the dependency-message-building logic built | ||
// in terms of it, because the goal is for namedPromiseReporter to return | ||
// everything and then the diagnostic reporter to cherry-pick only the | ||
// subset of names it needs, and because this way we can get more test | ||
// coverage without needing fixtures for every possible combination of | ||
// self-references. | ||
|
||
cfg := testStackConfig(t, "planning", "named_promises") | ||
|
||
main := NewForPlanning(cfg, stackstate.NewState(), PlanOpts{ | ||
PlanningMode: plans.NormalMode, | ||
InputVariableValues: map[stackaddrs.InputVariable]ExternalInputValue{ | ||
{Name: "in"}: ExternalInputValue{ | ||
Value: cty.StringVal("hello"), | ||
}, | ||
}, | ||
ProviderFactories: ProviderFactories{ | ||
addrs.MustParseProviderSourceString("example.com/test/happycloud"): providers.FactoryFixed( | ||
&providertest.MockProvider{ | ||
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ | ||
Provider: providers.Schema{ | ||
Block: &configschema.Block{}, | ||
}, | ||
ResourceTypes: map[string]providers.Schema{ | ||
"happycloud_thingy": providers.Schema{ | ||
Block: &configschema.Block{}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
), | ||
}, | ||
}) | ||
|
||
// We don't actually really care about the plan here. We just want the | ||
// side-effect of getting a bunch of promises created inside "main", which | ||
// we'll then ask about below. | ||
_, diags := testPlan(t, main) | ||
assertNoDiagnostics(t, diags) | ||
|
||
wantNames := collections.NewSetCmp[string]( | ||
// Component-related | ||
`component.foo`, | ||
`component.foo modules`, | ||
`component.foo for_each`, | ||
`component.foo instances`, | ||
|
||
// Nested-stack-related | ||
`stack.child collected outputs`, | ||
`stack.child inputs`, | ||
`stack.child for_each`, | ||
`stack.child instances`, | ||
|
||
// Provider-related | ||
`example.com/test/happycloud schema`, | ||
`provider["example.com/test/happycloud"].main`, | ||
`provider["example.com/test/happycloud"].main for_each`, | ||
`provider["example.com/test/happycloud"].main instances`, | ||
|
||
// Output-value-related | ||
`output.out value`, | ||
`stack.child.output.out value`, | ||
`output.out`, | ||
`stack.child.output.out`, | ||
|
||
// Input-variable-related | ||
`var.in`, | ||
`stack.child.var.in`, | ||
) | ||
gotNames := collections.NewSetCmp[string]() | ||
ids := map[string]promising.PromiseID{} | ||
main.reportNamedPromises(func(id promising.PromiseID, name string) { | ||
gotNames.Add(name) | ||
// We'll also remember the id associated with each name so that | ||
// we can test the diagnostic message rendering below. | ||
ids[name] = id | ||
// NOTE: Some of the names get reused across both a config object | ||
// and its associated dynamic object when there are no dynamic | ||
// instance keys involved, and for those it's unspecified which | ||
// promise ID will "win", but that's fine for our purposes here | ||
// because we're only testing that some specific names get | ||
// included into the error messages and so it doesn't matter which | ||
// of the promise ids we use to achieve that. | ||
}) | ||
|
||
if diff := cmp.Diff(wantNames, gotNames, collections.CmpOptions); diff != "" { | ||
// If you're here because you've seen a failure where some of the | ||
// wanted names seem to have vanished, and you weren't intentionally | ||
// trying to remove them, check to make sure that the type that was | ||
// supposed to report that name is still reachable indirectly from the | ||
// Main.reportNamedPromises implementation. | ||
t.Errorf("wrong promise names\n%s", diff) | ||
} | ||
|
||
// Since we're now holding all of the information required, let's also | ||
// test that we can render some self-dependency diagnostic messages. | ||
t.Run("diagnostics", func(t *testing.T) { | ||
// For this we need to choose some specific promise ids to report. | ||
// It doesn't matter which ones we use but we can only proceed if | ||
// they were ones detected by the reportNamedPromises call earlier. | ||
providerSchemaPromise := ids[`example.com/test/happycloud schema`] | ||
stackCallInstancesPromise := ids[`stack.child instances`] | ||
if providerSchemaPromise == promising.NoPromise || stackCallInstancesPromise == promising.NoPromise { | ||
t.Fatalf("don't have the promise ids required to test diagnostic rendering") | ||
} | ||
|
||
t.Run("just one", func(t *testing.T) { | ||
err := promising.ErrSelfDependent{stackCallInstancesPromise} | ||
diag := taskSelfDependencyDiagnostic{ | ||
err: err, | ||
root: main, | ||
} | ||
got := diag.Description() | ||
want := tfdiags.Description{ | ||
Summary: `Self-dependent item in configuration`, | ||
Detail: `The item "stack.child instances" depends on its own results, so there is no correct order of operations.`, | ||
} | ||
if diff := cmp.Diff(want, got); diff != "" { | ||
t.Errorf("wrong diagnostic description\n%s", diff) | ||
} | ||
}) | ||
t.Run("multiple", func(t *testing.T) { | ||
err := promising.ErrSelfDependent{ | ||
providerSchemaPromise, | ||
stackCallInstancesPromise, | ||
} | ||
diag := taskSelfDependencyDiagnostic{ | ||
err: err, | ||
root: main, | ||
} | ||
got := diag.Description() | ||
want := tfdiags.Description{ | ||
Summary: `Self-dependent items in configuration`, | ||
Detail: `The following items in your configuration form a circular dependency chain through their references: | ||
- example.com/test/happycloud schema | ||
- stack.child instances | ||
Terraform uses references to decide a suitable order for performing operations, so configuration items may not refer to their own results either directly or indirectly.`, | ||
} | ||
if diff := cmp.Diff(want, got); diff != "" { | ||
t.Errorf("wrong diagnostic description\n%s", diff) | ||
} | ||
}) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.