| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package stackruntime | |
| import ( | |
| "context" | |
| "crypto/sha256" | |
| "encoding/json" | |
| "fmt" | |
| "sort" | |
| "strings" | |
| "testing" | |
| "time" | |
| "github.com/google/go-cmp/cmp" | |
| "github.com/hashicorp/go-slug/sourceaddrs" | |
| "github.com/hashicorp/go-slug/sourcebundle" | |
| "github.com/zclconf/go-cty/cty" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/configs/configschema" | |
| "github.com/hashicorp/terraform/internal/depsfile" | |
| "github.com/hashicorp/terraform/internal/getproviders/providerreqs" | |
| "github.com/hashicorp/terraform/internal/plans" | |
| "github.com/hashicorp/terraform/internal/providers" | |
| "github.com/hashicorp/terraform/internal/stacks/stackaddrs" | |
| "github.com/hashicorp/terraform/internal/stacks/stackconfig" | |
| "github.com/hashicorp/terraform/internal/stacks/stackplan" | |
| "github.com/hashicorp/terraform/internal/stacks/stackstate" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| ) | |
| // This file has helper functions used by other tests. It doesn't contain any | |
| // test cases of its own. | |
| // TestContext contains all the information shared across multiple operations | |
| // in a single test. | |
| type TestContext struct { | |
| // timestamp is the timestamp that should be applied for this test. | |
| timestamp *time.Time | |
| // config is the config to use for this test. | |
| config *stackconfig.Config | |
| // providers are the providers that should be available within this test. | |
| providers map[addrs.Provider]providers.Factory | |
| // dependencyLocks is the locks file that should be used for this test. | |
| dependencyLocks depsfile.Locks | |
| } | |
| // TestCycle defines a single plan / apply cycle that should be performed within | |
| // a test. | |
| type TestCycle struct { | |
| // Validate options | |
| wantValidateDiags tfdiags.Diagnostics | |
| // Plan options | |
| planMode plans.Mode | |
| planInputs map[string]cty.Value | |
| wantPlannedChanges []stackplan.PlannedChange | |
| wantPlannedHooks *ExpectedHooks | |
| wantPlannedDiags tfdiags.Diagnostics | |
| // Apply options | |
| applyInputs map[string]cty.Value | |
| wantAppliedChanges []stackstate.AppliedChange | |
| wantAppliedHooks *ExpectedHooks | |
| wantAppliedDiags tfdiags.Diagnostics | |
| } | |
| func (tc TestContext) Validate(t *testing.T, ctx context.Context, cycle TestCycle) { | |
| t.Helper() | |
| gotDiags := Validate(ctx, &ValidateRequest{ | |
| Config: tc.config, | |
| ProviderFactories: tc.providers, | |
| DependencyLocks: tc.dependencyLocks, | |
| ExperimentsAllowed: true, | |
| }) | |
| validateDiags(t, cycle.wantValidateDiags, gotDiags) | |
| } | |
| func (tc TestContext) Plan(t *testing.T, ctx context.Context, state *stackstate.State, cycle TestCycle) *stackplan.Plan { | |
| request := PlanRequest{ | |
| PlanMode: cycle.planMode, | |
| Config: tc.config, | |
| PrevState: state, | |
| InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue { | |
| inputs := make(map[stackaddrs.InputVariable]ExternalInputValue, len(cycle.planInputs)) | |
| for k, v := range cycle.planInputs { | |
| inputs[stackaddrs.InputVariable{Name: k}] = ExternalInputValue{Value: v} | |
| } | |
| return inputs | |
| }(), | |
| ProviderFactories: tc.providers, | |
| DependencyLocks: tc.dependencyLocks, | |
| ForcePlanTimestamp: tc.timestamp, | |
| ExperimentsAllowed: true, | |
| } | |
| changesCh := make(chan stackplan.PlannedChange) | |
| diagsCh := make(chan tfdiags.Diagnostic) | |
| response := PlanResponse{ | |
| PlannedChanges: changesCh, | |
| Diagnostics: diagsCh, | |
| } | |
| capturedHooks := NewCapturedHooks(true) | |
| go Plan(ContextWithHooks(ctx, capturedHooks.captureHooks()), &request, &response) | |
| changes, diags := collectPlanOutput(changesCh, diagsCh) | |
| validateDiags(t, cycle.wantPlannedDiags, diags) | |
| if cycle.wantPlannedChanges != nil { | |
| var filteredChanges []stackplan.PlannedChange | |
| for _, change := range changes { | |
| if _, ok := change.(*stackplan.PlannedChangePriorStateElement); ok { | |
| // Remove the prior state elements from the analysis for tests | |
| // using this framework. They're really difficult to properly | |
| // compare as they use the raw state and raw key, and they're | |
| // actually ignored by most of the stacks runtime and so aren't | |
| // useful to be included in these kind of tests. | |
| continue | |
| } | |
| filteredChanges = append(filteredChanges, change) | |
| } | |
| // if this is nil (as opposed to empty) then we don't validate the | |
| // returned changes. | |
| sort.SliceStable(filteredChanges, func(i, j int) bool { | |
| return plannedChangeSortKey(filteredChanges[i]) < plannedChangeSortKey(filteredChanges[j]) | |
| }) | |
| if diff := cmp.Diff(cycle.wantPlannedChanges, filteredChanges, changesCmpOpts); len(diff) > 0 { | |
| t.Errorf("wrong planned changes\n%s", diff) | |
| } | |
| } | |
| if cycle.wantPlannedHooks != nil { | |
| cycle.wantPlannedHooks.Validate(t, &capturedHooks.ExpectedHooks) | |
| } | |
| planLoader := stackplan.NewLoader() | |
| for _, change := range changes { | |
| proto, err := change.PlannedChangeProto() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| for _, rawMsg := range proto.Raw { | |
| err = planLoader.AddRaw(rawMsg) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| } | |
| } | |
| plan, err := planLoader.Plan() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| return plan | |
| } | |
| func (tc TestContext) Apply(t *testing.T, ctx context.Context, plan *stackplan.Plan, cycle TestCycle) *stackstate.State { | |
| request := ApplyRequest{ | |
| Config: tc.config, | |
| Plan: plan, | |
| InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue { | |
| inputs := make(map[stackaddrs.InputVariable]ExternalInputValue, len(cycle.applyInputs)) | |
| for k, v := range cycle.applyInputs { | |
| inputs[stackaddrs.InputVariable{Name: k}] = ExternalInputValue{Value: v} | |
| } | |
| return inputs | |
| }(), | |
| ProviderFactories: tc.providers, | |
| ExperimentsAllowed: true, | |
| DependencyLocks: tc.dependencyLocks, | |
| } | |
| changesCh := make(chan stackstate.AppliedChange) | |
| diagsCh := make(chan tfdiags.Diagnostic) | |
| response := ApplyResponse{ | |
| AppliedChanges: changesCh, | |
| Diagnostics: diagsCh, | |
| } | |
| capturedHooks := NewCapturedHooks(false) | |
| go Apply(ContextWithHooks(ctx, capturedHooks.captureHooks()), &request, &response) | |
| changes, diags := collectApplyOutput(changesCh, diagsCh) | |
| validateDiags(t, cycle.wantAppliedDiags, diags) | |
| if cycle.wantAppliedChanges != nil { | |
| // nil indicates skip this check, empty slice indicates no changes expected. | |
| sort.SliceStable(changes, func(i, j int) bool { | |
| return appliedChangeSortKey(changes[i]) < appliedChangeSortKey(changes[j]) | |
| }) | |
| if diff := cmp.Diff(cycle.wantAppliedChanges, changes, changesCmpOpts); diff != "" { | |
| t.Errorf("wrong applied changes\n%s", diff) | |
| } | |
| } | |
| if cycle.wantAppliedHooks != nil { | |
| cycle.wantAppliedHooks.Validate(t, &capturedHooks.ExpectedHooks) | |
| } | |
| stateLoader := stackstate.NewLoader() | |
| for _, change := range changes { | |
| proto, err := change.AppliedChangeProto() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| for _, rawMsg := range proto.Raw { | |
| if rawMsg.Value == nil { | |
| // This is a removal notice, so we don't need to add it to the | |
| // state. | |
| continue | |
| } | |
| err = stateLoader.AddRaw(rawMsg.Key, rawMsg.Value) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| } | |
| } | |
| return stateLoader.State() | |
| } | |
| func initDiags(cb func(diags tfdiags.Diagnostics) tfdiags.Diagnostics) tfdiags.Diagnostics { | |
| var diags tfdiags.Diagnostics | |
| return cb(diags) | |
| } | |
| func validateDiags(t *testing.T, wantDiags, gotDiags tfdiags.Diagnostics) { | |
| t.Helper() | |
| sort.SliceStable(gotDiags, diagnosticSortFunc(gotDiags)) | |
| sort.SliceStable(wantDiags, diagnosticSortFunc(wantDiags)) | |
| gotDiags = gotDiags.ForRPC() | |
| wantDiags = wantDiags.ForRPC() | |
| if diff := cmp.Diff(wantDiags, gotDiags); len(diff) > 0 { | |
| t.Errorf("wrong diagnostics\n%s", diff) | |
| } | |
| } | |
| // loadConfigForTest is a test helper that tries to open bundleRoot as a | |
| // source bundle, and then if successful tries to load the given source address | |
| // from it as a stack configuration. If any part of the operation fails then | |
| // it halts execution of the test and doesn't return. | |
| func loadConfigForTest(t *testing.T, bundleRoot string, configSourceAddr string) *stackconfig.Config { | |
| t.Helper() | |
| sources, err := sourcebundle.OpenDir(bundleRoot) | |
| if err != nil { | |
| t.Fatalf("cannot load source bundle: %s", err) | |
| } | |
| // We force using remote source addresses here because that avoids | |
| // us having to deal with the extra version constraints argument | |
| // that registry sources require. Exactly what source address type | |
| // we use isn't relevant for tests in this package, since it's | |
| // the sourcebundle package's responsibility to make sure its | |
| // abstraction works for all of the source types. | |
| sourceAddr, err := sourceaddrs.ParseRemoteSource(configSourceAddr) | |
| if err != nil { | |
| t.Fatalf("invalid config source address: %s", err) | |
| } | |
| cfg, diags := stackconfig.LoadConfigDir(sourceAddr, sources) | |
| reportDiagnosticsForTest(t, diags) | |
| return cfg | |
| } | |
| func mainBundleSourceAddrStr(dirName string) string { | |
| return "git::https://example.com/test.git//" + dirName | |
| } | |
| // loadMainBundleConfigForTest is a convenience wrapper around | |
| // loadConfigForTest that knows the location and package address of our | |
| // "main" source bundle, in ./testdata/mainbundle, so that we can use that | |
| // conveniently without duplicating its location and synthetic package address | |
| // in every single test function. | |
| // | |
| // dirName should begin with the name of a subdirectory that's present in | |
| // ./testdata/mainbundle/test . It can optionally refer to subdirectories | |
| // thereof, using forward slashes as the path separator just as we'd do | |
| // in the subdirectory portion of a remote source address (which is exactly | |
| // what we're using this as.) | |
| func loadMainBundleConfigForTest(t *testing.T, dirName string) *stackconfig.Config { | |
| t.Helper() | |
| fullSourceAddr := mainBundleSourceAddrStr(dirName) | |
| return loadConfigForTest(t, "./testdata/mainbundle", fullSourceAddr) | |
| } | |
| type expectedDiagnostic struct { | |
| severity tfdiags.Severity | |
| summary string | |
| detail string | |
| } | |
| func expectDiagnostic(severity tfdiags.Severity, summary, detail string) expectedDiagnostic { | |
| return expectedDiagnostic{ | |
| severity: severity, | |
| summary: summary, | |
| detail: detail, | |
| } | |
| } | |
| func expectDiagnosticsForTest(t *testing.T, actual tfdiags.Diagnostics, expected ...expectedDiagnostic) { | |
| t.Helper() | |
| max := len(expected) | |
| if len(actual) > max { | |
| max = len(actual) | |
| } | |
| for ix := 0; ix < max; ix++ { | |
| if ix >= len(expected) { | |
| t.Errorf("unexpected diagnostic [%d]: %s - %s", ix, actual[ix].Description().Summary, actual[ix].Description().Detail) | |
| continue | |
| } | |
| if ix >= len(actual) { | |
| t.Errorf("missing diagnostic [%d]: %s - %s", ix, expected[ix].summary, expected[ix].detail) | |
| continue | |
| } | |
| if actual[ix].Severity() != expected[ix].severity { | |
| t.Errorf("diagnostic [%d] has wrong severity: %s (expected %s)", ix, actual[ix].Severity(), expected[ix].severity) | |
| } | |
| if diff := cmp.Diff(actual[ix].Description().Summary, expected[ix].summary); len(diff) > 0 { | |
| t.Errorf("diagnostic [%d] has wrong summary: %s", ix, diff) | |
| } | |
| if diff := cmp.Diff(actual[ix].Description().Detail, expected[ix].detail); len(diff) > 0 { | |
| t.Errorf("diagnostic [%d] has wrong detail: %s", ix, diff) | |
| } | |
| } | |
| } | |
| // reportDiagnosticsForTest creates a test log entry for every diagnostic in | |
| // the given diags, and halts the test if any of them are error diagnostics. | |
| func reportDiagnosticsForTest(t *testing.T, diags tfdiags.Diagnostics) { | |
| t.Helper() | |
| for _, diag := range diags { | |
| var b strings.Builder | |
| desc := diag.Description() | |
| locs := diag.Source() | |
| switch sev := diag.Severity(); sev { | |
| case tfdiags.Error: | |
| b.WriteString("Error: ") | |
| case tfdiags.Warning: | |
| b.WriteString("Warning: ") | |
| default: | |
| t.Errorf("unsupported diagnostic type %s", sev) | |
| } | |
| b.WriteString(desc.Summary) | |
| if desc.Address != "" { | |
| b.WriteString("\nwith ") | |
| b.WriteString(desc.Summary) | |
| } | |
| if locs.Subject != nil { | |
| b.WriteString("\nat ") | |
| b.WriteString(locs.Subject.StartString()) | |
| } | |
| if desc.Detail != "" { | |
| b.WriteString("\n\n") | |
| b.WriteString(desc.Detail) | |
| } | |
| t.Log(b.String()) | |
| } | |
| if diags.HasErrors() { | |
| t.FailNow() | |
| } | |
| } | |
| // appliedChangeSortKey returns a string that can be used to sort applied | |
| // changes in a predictable order for testing purposes. This is used to | |
| // ensure that we can compare applied changes in a consistent way across | |
| // different test runs. | |
| func appliedChangeSortKey(change stackstate.AppliedChange) string { | |
| switch change := change.(type) { | |
| case *stackstate.AppliedChangeResourceInstanceObject: | |
| return change.ResourceInstanceObjectAddr.String() | |
| case *stackstate.AppliedChangeComponentInstance: | |
| return change.ComponentInstanceAddr.String() | |
| case *stackstate.AppliedChangeComponentInstanceRemoved: | |
| return change.ComponentInstanceAddr.String() | |
| case *stackstate.AppliedChangeOutputValue: | |
| return change.Addr.String() | |
| case *stackstate.AppliedChangeInputVariable: | |
| return change.Addr.String() | |
| case *stackstate.AppliedChangeDiscardKeys: | |
| // There should only be a single discard keys in a plan, so we can just | |
| // return a static string here. | |
| return "discard" | |
| default: | |
| // This is only going to happen during tests, so we can panic here. | |
| panic(fmt.Errorf("unrecognized applied change type: %T", change)) | |
| } | |
| } | |
| // plannedChangeSortKey returns a string that can be used to sort planned | |
| // changes in a predictable order for testing purposes. This is used to | |
| // ensure that we can compare planned changes in a consistent way across | |
| // different test runs. | |
| func plannedChangeSortKey(change stackplan.PlannedChange) string { | |
| switch change := change.(type) { | |
| case *stackplan.PlannedChangeRootInputValue: | |
| return change.Addr.String() | |
| case *stackplan.PlannedChangeComponentInstance: | |
| return change.Addr.String() | |
| case *stackplan.PlannedChangeComponentInstanceRemoved: | |
| return change.Addr.String() | |
| case *stackplan.PlannedChangeResourceInstancePlanned: | |
| return change.ResourceInstanceObjectAddr.String() | |
| case *stackplan.PlannedChangeDeferredResourceInstancePlanned: | |
| return change.ResourceInstancePlanned.ResourceInstanceObjectAddr.String() | |
| case *stackplan.PlannedChangeOutputValue: | |
| return change.Addr.String() | |
| case *stackplan.PlannedChangeHeader: | |
| // There should only be a single header in a plan, so we can just return | |
| // a static string here. | |
| return "header" | |
| case *stackplan.PlannedChangeApplyable: | |
| // There should only be a single applyable marker in a plan, so we can | |
| // just return a static string here. | |
| return "applyable" | |
| case *stackplan.PlannedChangePlannedTimestamp: | |
| // There should only be a single timestamp in a plan, so we can | |
| // just return a static string here. | |
| return "planned-timestamp" | |
| case *stackplan.PlannedChangeProviderFunctionResults: | |
| // There should only be a single timestamp in a plan, so we can just | |
| // return a simple string. | |
| return "function-results" | |
| case *stackplan.PlannedChangeActionInvocationInstancePlanned: | |
| return change.ActionInvocationAddr.String() | |
| case *stackplan.PlannedChangeDeferredActionInvocation: | |
| return change.ActionInvocationPlanned.ActionInvocationAddr.String() | |
| default: | |
| // This is only going to happen during tests, so we can panic here. | |
| panic(fmt.Errorf("unrecognized planned change type: %T", change)) | |
| } | |
| } | |
| func diagnosticSortFunc(diags tfdiags.Diagnostics) func(i, j int) bool { | |
| sortDescription := func(i, j tfdiags.Description) bool { | |
| if i.Summary != j.Summary { | |
| return i.Summary < j.Summary | |
| } | |
| return i.Detail < j.Detail | |
| } | |
| sortPos := func(i, j tfdiags.SourcePos) bool { | |
| if i.Line != j.Line { | |
| return i.Line < j.Line | |
| } | |
| return i.Column < j.Column | |
| } | |
| sortRange := func(i, j *tfdiags.SourceRange) bool { | |
| if i.Filename != j.Filename { | |
| return i.Filename < j.Filename | |
| } | |
| if !cmp.Equal(i.Start, j.Start) { | |
| return sortPos(i.Start, j.Start) | |
| } | |
| return sortPos(i.End, j.End) | |
| } | |
| return func(i, j int) bool { | |
| id, jd := diags[i], diags[j] | |
| if id.Severity() != jd.Severity() { | |
| return id.Severity() == tfdiags.Error | |
| } | |
| if !cmp.Equal(id.Description(), jd.Description()) { | |
| return sortDescription(id.Description(), jd.Description()) | |
| } | |
| if id.Source().Subject != nil && jd.Source().Subject != nil { | |
| return sortRange(id.Source().Subject, jd.Source().Subject) | |
| } | |
| return false | |
| } | |
| } | |
| func mustDefaultRootProvider(provider string) addrs.AbsProviderConfig { | |
| return addrs.AbsProviderConfig{ | |
| Module: addrs.RootModule, | |
| Provider: addrs.NewDefaultProvider(provider), | |
| } | |
| } | |
| func mustAbsResourceInstance(addr string) addrs.AbsResourceInstance { | |
| ret, diags := addrs.ParseAbsResourceInstanceStr(addr) | |
| if len(diags) > 0 { | |
| panic(fmt.Sprintf("failed to parse resource instance address %q: %s", addr, diags)) | |
| } | |
| return ret | |
| } | |
| func mustAbsResourceInstanceObject(addr string) stackaddrs.AbsResourceInstanceObject { | |
| ret, diags := stackaddrs.ParseAbsResourceInstanceObjectStr(addr) | |
| if len(diags) > 0 { | |
| panic(fmt.Sprintf("failed to parse resource instance object address %q: %s", addr, diags)) | |
| } | |
| return ret | |
| } | |
| func mustAbsResourceInstanceObjectPtr(addr string) *stackaddrs.AbsResourceInstanceObject { | |
| ret := mustAbsResourceInstanceObject(addr) | |
| return &ret | |
| } | |
| func mustAbsComponentInstance(addr string) stackaddrs.AbsComponentInstance { | |
| ret, diags := stackaddrs.ParsePartialComponentInstanceStr(addr) | |
| if len(diags) > 0 { | |
| panic(fmt.Sprintf("failed to parse component instance address %q: %s", addr, diags)) | |
| } | |
| return ret | |
| } | |
| func mustAbsActionInvocationInstance(addr string) stackaddrs.AbsActionInvocationInstance { | |
| ret, diags := stackaddrs.ParseActionInvocationInstanceStr(addr) | |
| if len(diags) > 0 { | |
| panic(fmt.Sprintf("failed to parse action invocation instance address %q: %s", addr, diags)) | |
| } | |
| return ret | |
| } | |
| func mustAbsComponent(addr string) stackaddrs.AbsComponent { | |
| ret, diags := stackaddrs.ParsePartialComponentInstanceStr(addr) | |
| if len(diags) > 0 { | |
| panic(fmt.Sprintf("failed to parse component instance address %q: %s", addr, diags)) | |
| } | |
| return stackaddrs.AbsComponent{ | |
| Stack: ret.Stack, | |
| Item: ret.Item.Component, | |
| } | |
| } | |
| // mustPlanDynamicValue is a helper function that constructs a | |
| // plans.DynamicValue from the given cty.Value, panicking if the construction | |
| // fails. | |
| func mustPlanDynamicValue(v cty.Value) plans.DynamicValue { | |
| ret, err := plans.NewDynamicValue(v, v.Type()) | |
| if err != nil { | |
| panic(err) | |
| } | |
| return ret | |
| } | |
| // mustPlanDynamicValueDynamicType is a helper function that constructs a | |
| // plans.DynamicValue from the given cty.Value, using cty.DynamicPseudoType as | |
| // the type, and panicking if the construction fails. | |
| func mustPlanDynamicValueDynamicType(v cty.Value) plans.DynamicValue { | |
| ret, err := plans.NewDynamicValue(v, cty.DynamicPseudoType) | |
| if err != nil { | |
| panic(err) | |
| } | |
| return ret | |
| } | |
| // mustPlanDynamicValueSchema is a helper function that constructs a | |
| // plans.DynamicValue from the given cty.Value and configschema.Block, panicking | |
| // if the construction fails. | |
| func mustPlanDynamicValueSchema(v cty.Value, block *configschema.Block) plans.DynamicValue { | |
| ty := block.ImpliedType() | |
| ret, err := plans.NewDynamicValue(v, ty) | |
| if err != nil { | |
| panic(err) | |
| } | |
| return ret | |
| } | |
| func mustInputVariable(name string) addrs.InputVariable { | |
| return addrs.InputVariable{Name: name} | |
| } | |
| func mustStackInputVariable(name string) stackaddrs.InputVariable { | |
| return stackaddrs.InputVariable{Name: name} | |
| } | |
| func mustStackOutputValue(name string) stackaddrs.OutputValue { | |
| return stackaddrs.OutputValue{Name: name} | |
| } | |
| func mustMarshalJSONAttrs(attrs map[string]interface{}) []byte { | |
| jsonAttrs, err := json.Marshal(attrs) | |
| if err != nil { | |
| panic(err) | |
| } | |
| return jsonAttrs | |
| } | |
| func providerFunctionHashArgs(provider addrs.Provider, name string, args ...cty.Value) []byte { | |
| sum := sha256.New() | |
| sum.Write([]byte(provider.String())) | |
| sum.Write([]byte("|")) | |
| sum.Write([]byte(name)) | |
| for _, arg := range args { | |
| sum.Write([]byte("|")) | |
| sum.Write([]byte(arg.GoString())) | |
| } | |
| return sum.Sum(nil) | |
| } | |
| func providerFunctionHashResult(value cty.Value) []byte { | |
| bytes := sha256.Sum256([]byte(value.GoString())) | |
| return bytes[:] | |
| } | |
| // buildVersionMismatchLock returns a Locks value with provider "testing" | |
| // locked at 0.2.0, used to exercise version-mismatch diagnostics in tests. | |
| func buildVersionMismatchLock() depsfile.Locks { | |
| lock := depsfile.NewLocks() | |
| lock.SetProvider( | |
| addrs.NewDefaultProvider("testing"), | |
| providerreqs.MustParseVersion("0.2.0"), | |
| providerreqs.MustParseVersionConstraints("0.2.0"), | |
| providerreqs.PreferredHashes([]providerreqs.Hash{}), | |
| ) | |
| return *lock | |
| } | |
| // hasDiagSummary reports whether any diagnostic in diags has the given summary. | |
| func hasDiagSummary(diags tfdiags.Diagnostics, summary string) bool { | |
| for _, diag := range diags { | |
| if diag.Description().Summary == summary { | |
| return true | |
| } | |
| } | |
| return false | |
| } |