diff --git a/pkg/internal/tests/cross-tests/diff_cross_test.go b/pkg/internal/tests/cross-tests/diff_cross_test.go index 8899bf56f..cbbb706a0 100644 --- a/pkg/internal/tests/cross-tests/diff_cross_test.go +++ b/pkg/internal/tests/cross-tests/diff_cross_test.go @@ -1042,7 +1042,7 @@ func findKeyInPulumiDetailedDiff(detailedDiff map[string]interface{}, key string } func TestNilVsEmptyNestedCollections(t *testing.T) { - // TODO: remove once accurate bridge previews are rolled out + // TODO[pulumi/pulumi-terraform-bridge#2517]: remove once accurate bridge previews are rolled out t.Setenv("PULUMI_TF_BRIDGE_ACCURATE_BRIDGE_PREVIEW", "true") for _, MaxItems := range []int{0, 1} { t.Run(fmt.Sprintf("MaxItems=%d", MaxItems), func(t *testing.T) { diff --git a/pkg/tests/schema_pulumi_test.go b/pkg/tests/schema_pulumi_test.go index 1666a5fa3..7f3e26350 100644 --- a/pkg/tests/schema_pulumi_test.go +++ b/pkg/tests/schema_pulumi_test.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" "time" @@ -731,6 +732,18 @@ outputs: } } +func trimDiff(t *testing.T, diff string) string { + urnLine := " [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test]" + resourcesSummaryLine := "Resources:\n" + require.Contains(t, diff, urnLine) + require.Contains(t, diff, resourcesSummaryLine) + + // trim the diff to only include the contents after the URN line and before the summary + urnIndex := strings.Index(diff, urnLine) + resourcesSummaryIndex := strings.Index(diff, resourcesSummaryLine) + return diff[urnIndex+len(urnLine) : resourcesSummaryIndex] +} + func TestUnknownBlocks(t *testing.T) { resMap := map[string]*schema.Resource{ "prov_test": { @@ -889,20 +902,14 @@ resources: tests: ${auxRes.auxes} `, provTestKnownProgram, - autogold.Expect(`Previewing update (test): -+ pulumi:pulumi:Stack: (create) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + prov:index/test:Test: (create) [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] tests : output -Resources: - + 3 to create `), - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] ~ prov:index/test:Test: (update) @@ -914,10 +921,6 @@ Resources: } ] + tests: output -Resources: - + 1 to create - ~ 1 to update - 2 changes. 1 unchanged `), }, { @@ -939,9 +942,7 @@ resources: `, provTestKnownProgram, - autogold.Expect(`Previewing update (test): -+ pulumi:pulumi:Stack: (create) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + prov:index/test:Test: (create) @@ -949,12 +950,8 @@ resources: tests : [ [0]: output ] -Resources: - + 3 to create `), - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] ~ prov:index/test:Test: (update) @@ -966,10 +963,6 @@ Resources: } + [0]: output ] -Resources: - + 1 to create - ~ 1 to update - 2 changes. 1 unchanged `), }, { @@ -992,9 +985,7 @@ resources: `, provTestKnownProgram, - autogold.Expect(`Previewing update (test): -+ pulumi:pulumi:Stack: (create) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + prov:index/test:Test: (create) @@ -1005,12 +996,8 @@ resources: testProp : "val" } ] -Resources: - + 3 to create `), - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] ~ prov:index/test:Test: (update) @@ -1025,10 +1012,6 @@ Resources: + testProp : "val" } ] -Resources: - + 1 to create - ~ 1 to update - 2 changes. 1 unchanged `), }, { @@ -1048,20 +1031,14 @@ resources: tests: ${auxRes.nestedAuxes} `, nestedProvTestKnownProgram, - autogold.Expect(`Previewing update (test): -+ pulumi:pulumi:Stack: (create) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + prov:index/nestedTest:NestedTest: (create) [urn=urn:pulumi:test::test::prov:index/nestedTest:NestedTest::mainRes] tests : output -Resources: - + 3 to create `), - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] ~ prov:index/nestedTest:NestedTest: (update) @@ -1079,10 +1056,6 @@ Resources: } ] + tests: output -Resources: - + 1 to create - ~ 1 to update - 2 changes. 1 unchanged `), }, { @@ -1103,9 +1076,7 @@ resources: - ${auxRes.nestedAuxes[0]} `, nestedProvTestKnownProgram, - autogold.Expect(`Previewing update (test): -+ pulumi:pulumi:Stack: (create) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + prov:index/nestedTest:NestedTest: (create) @@ -1113,12 +1084,8 @@ resources: tests : [ [0]: output ] -Resources: - + 3 to create `), - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] ~ prov:index/nestedTest:NestedTest: (update) @@ -1136,10 +1103,6 @@ Resources: } + [0]: output ] -Resources: - + 1 to create - ~ 1 to update - 2 changes. 1 unchanged `), }, { @@ -1160,9 +1123,7 @@ resources: - nestedProps: ${auxRes.nestedAuxes[0].nestedProps} `, nestedProvTestKnownProgram, - autogold.Expect(`Previewing update (test): -+ pulumi:pulumi:Stack: (create) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + prov:index/nestedTest:NestedTest: (create) @@ -1172,12 +1133,8 @@ resources: nestedProps: output } ] -Resources: - + 3 to create `), - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] ~ prov:index/nestedTest:NestedTest: (update) @@ -1195,10 +1152,6 @@ Resources: + nestedProps: output } ] -Resources: - + 1 to create - ~ 1 to update - 2 changes. 1 unchanged `), }, { @@ -1220,9 +1173,7 @@ resources: - ${auxRes.nestedAuxes[0].nestedProps[0]} `, nestedProvTestKnownProgram, - autogold.Expect(`Previewing update (test): -+ pulumi:pulumi:Stack: (create) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + prov:index/nestedTest:NestedTest: (create) @@ -1234,12 +1185,8 @@ resources: ] } ] -Resources: - + 3 to create `), - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] ~ prov:index/nestedTest:NestedTest: (update) @@ -1257,10 +1204,6 @@ Resources: ] } ] -Resources: - + 1 to create - ~ 1 to update - 2 changes. 1 unchanged `), }, { @@ -1282,9 +1225,7 @@ resources: - testProps: ${auxRes.nestedAuxes[0].nestedProps[0].testProps} `, nestedProvTestKnownProgram, - autogold.Expect(`Previewing update (test): -+ pulumi:pulumi:Stack: (create) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + prov:index/nestedTest:NestedTest: (create) @@ -1298,12 +1239,8 @@ resources: ] } ] -Resources: - + 3 to create `), - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] ~ prov:index/nestedTest:NestedTest: (update) @@ -1321,10 +1258,6 @@ Resources: ] } ] -Resources: - + 1 to create - ~ 1 to update - 2 changes. 1 unchanged `), }, { @@ -1347,9 +1280,7 @@ resources: - ${auxRes.nestedAuxes[0].nestedProps[0].testProps[0]} `, nestedProvTestKnownProgram, - autogold.Expect(`Previewing update (test): -+ pulumi:pulumi:Stack: (create) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + prov:index/nestedTest:NestedTest: (create) @@ -1365,12 +1296,8 @@ resources: ] } ] -Resources: - + 3 to create `), - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] ~ prov:index/nestedTest:NestedTest: (update) @@ -1387,10 +1314,6 @@ Resources: ] } ] -Resources: - + 1 to create - ~ 1 to update - 2 changes. 1 unchanged `), }, } { @@ -1402,7 +1325,7 @@ Resources: res := pt.Preview(t, optpreview.Diff()) t.Log(res.StdOut) - tc.expectedInitial.Equal(t, res.StdOut) + tc.expectedInitial.Equal(t, trimDiff(t, res.StdOut)) }) t.Run("update preview", func(t *testing.T) { @@ -1421,7 +1344,7 @@ Resources: res := pt.Preview(t, optpreview.Diff()) t.Log(res.StdOut) - tc.expectedUpdate.Equal(t, res.StdOut) + tc.expectedUpdate.Equal(t, trimDiff(t, res.StdOut)) }) t.Run("update preview with computed", func(t *testing.T) { @@ -1435,7 +1358,7 @@ Resources: res := pt.Preview(t, optpreview.Diff()) t.Log(res.StdOut) - tc.expectedUpdate.Equal(t, res.StdOut) + tc.expectedUpdate.Equal(t, trimDiff(t, res.StdOut)) }) }) } @@ -1878,7 +1801,7 @@ resources: } func TestDetailedDiffPlainTypes(t *testing.T) { - // TODO: Remove this once accurate bridge previews are rolled out + // TODO[pulumi/pulumi-terraform-bridge#2517]: Remove this once accurate bridge previews are rolled out t.Setenv("PULUMI_TF_BRIDGE_ACCURATE_BRIDGE_PREVIEW", "true") resMap := map[string]*schema.Resource{ "prov_test": { @@ -1964,88 +1887,58 @@ resources: "string unchanged", map[string]interface{}{"stringProp": "val"}, map[string]interface{}{"stringProp": "val"}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] -Resources: - 2 unchanged -`), + autogold.Expect("\n"), }, { "string added", map[string]interface{}{}, map[string]interface{}{"stringProp": "val"}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + stringProp: "val" -Resources: - ~ 1 to update - 1 unchanged `), }, { "string removed", map[string]interface{}{"stringProp": "val1"}, map[string]interface{}{}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - stringProp: "val1" -Resources: - ~ 1 to update - 1 unchanged `), }, { "string changed", map[string]interface{}{"stringProp": "val1"}, map[string]interface{}{"stringProp": "val2"}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ stringProp: "val1" => "val2" -Resources: - ~ 1 to update - 1 unchanged `), }, { "list unchanged", map[string]interface{}{"listProps": []interface{}{"val"}}, map[string]interface{}{"listProps": []interface{}{"val"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] -Resources: - 2 unchanged -`), + autogold.Expect("\n"), }, { "list added", map[string]interface{}{}, map[string]interface{}{"listProps": []interface{}{"val"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + listProps: [ + [0]: "val" ] -Resources: - ~ 1 to update - 1 unchanged `), }, // pulumi/pulumi-terraform-bridge#2233: This is the intended behavior @@ -2053,29 +1946,19 @@ Resources: "list added empty", map[string]interface{}{}, map[string]interface{}{"listProps": []interface{}{}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] -Resources: - 2 unchanged -`), + autogold.Expect("\n"), }, { "list removed", map[string]interface{}{"listProps": []interface{}{"val"}}, map[string]interface{}{}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - listProps: [ - [0]: "val" ] -Resources: - ~ 1 to update - 1 unchanged `), }, // pulumi/pulumi-terraform-bridge#2233: This is the intended behavior @@ -2083,20 +1966,13 @@ Resources: "list removed empty", map[string]interface{}{"listProps": []interface{}{}}, map[string]interface{}{}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] -Resources: - 2 unchanged -`), + autogold.Expect("\n"), }, { "list element added front", map[string]interface{}{"listProps": []interface{}{"val2", "val3"}}, map[string]interface{}{"listProps": []interface{}{"val1", "val2", "val3"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2105,36 +1981,26 @@ Resources: ~ [1]: "val3" => "val2" + [2]: "val3" ] -Resources: - ~ 1 to update - 1 unchanged `), }, { "list element added back", map[string]interface{}{"listProps": []interface{}{"val1", "val2"}}, map[string]interface{}{"listProps": []interface{}{"val1", "val2", "val3"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ listProps: [ + [2]: "val3" ] -Resources: - ~ 1 to update - 1 unchanged `), }, { "list element added middle", map[string]interface{}{"listProps": []interface{}{"val1", "val3"}}, map[string]interface{}{"listProps": []interface{}{"val1", "val2", "val3"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2142,18 +2008,13 @@ Resources: ~ [1]: "val3" => "val2" + [2]: "val3" ] -Resources: - ~ 1 to update - 1 unchanged `), }, { "list element removed front", map[string]interface{}{"listProps": []interface{}{"val1", "val2", "val3"}}, map[string]interface{}{"listProps": []interface{}{"val2", "val3"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2162,36 +2023,26 @@ Resources: ~ [1]: "val2" => "val3" - [2]: "val3" ] -Resources: - ~ 1 to update - 1 unchanged `), }, { "list element removed back", map[string]interface{}{"listProps": []interface{}{"val1", "val2", "val3"}}, map[string]interface{}{"listProps": []interface{}{"val1", "val2"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ listProps: [ - [2]: "val3" ] -Resources: - ~ 1 to update - 1 unchanged `), }, { "list element removed middle", map[string]interface{}{"listProps": []interface{}{"val1", "val2", "val3"}}, map[string]interface{}{"listProps": []interface{}{"val1", "val3"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2199,56 +2050,38 @@ Resources: ~ [1]: "val2" => "val3" - [2]: "val3" ] -Resources: - ~ 1 to update - 1 unchanged `), }, { "list element changed", map[string]interface{}{"listProps": []interface{}{"val1"}}, map[string]interface{}{"listProps": []interface{}{"val2"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ listProps: [ ~ [0]: "val1" => "val2" ] -Resources: - ~ 1 to update - 1 unchanged `), }, { "set unchanged", map[string]interface{}{"setProps": []interface{}{"val"}}, map[string]interface{}{"setProps": []interface{}{"val"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] -Resources: - 2 unchanged -`), + autogold.Expect("\n"), }, { "set added", map[string]interface{}{}, map[string]interface{}{"setProps": []interface{}{"val"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + setProps: [ + [0]: "val" ] -Resources: - ~ 1 to update - 1 unchanged `), }, // pulumi/pulumi-terraform-bridge#2233: This is the intended behavior @@ -2256,29 +2089,19 @@ Resources: "set added empty", map[string]interface{}{}, map[string]interface{}{"setProps": []interface{}{}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] -Resources: - 2 unchanged -`), + autogold.Expect("\n"), }, { "set removed", map[string]interface{}{"setProps": []interface{}{"val"}}, map[string]interface{}{}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - setProps: [ - [0]: "val" ] -Resources: - ~ 1 to update - 1 unchanged `), }, // pulumi/pulumi-terraform-bridge#2233: This is the intended behavior @@ -2286,172 +2109,116 @@ Resources: "set removed empty", map[string]interface{}{"setProps": []interface{}{}}, map[string]interface{}{}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] -Resources: - 2 unchanged -`), + autogold.Expect("\n"), }, { "set element added front", map[string]interface{}{"setProps": []interface{}{"val2", "val3"}}, map[string]interface{}{"setProps": []interface{}{"val1", "val2", "val3"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setProps: [ - ~ [0]: "val2" => "val1" - ~ [1]: "val3" => "val2" - + [2]: "val3" + + [0]: "val1" ] -Resources: - ~ 1 to update - 1 unchanged `), }, { "set element added back", map[string]interface{}{"setProps": []interface{}{"val1", "val2"}}, map[string]interface{}{"setProps": []interface{}{"val1", "val2", "val3"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setProps: [ + [2]: "val3" ] -Resources: - ~ 1 to update - 1 unchanged `), }, { "set element added middle", map[string]interface{}{"setProps": []interface{}{"val1", "val3"}}, map[string]interface{}{"setProps": []interface{}{"val1", "val2", "val3"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setProps: [ - ~ [1]: "val3" => "val2" - + [2]: "val3" + + [1]: "val2" ] -Resources: - ~ 1 to update - 1 unchanged `), }, { "set element removed front", map[string]interface{}{"setProps": []interface{}{"val1", "val2", "val3"}}, map[string]interface{}{"setProps": []interface{}{"val2", "val3"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setProps: [ - ~ [0]: "val1" => "val2" - ~ [1]: "val2" => "val3" - - [2]: "val3" + - [0]: "val1" ] -Resources: - ~ 1 to update - 1 unchanged `), }, { "set element removed back", map[string]interface{}{"setProps": []interface{}{"val1", "val2", "val3"}}, map[string]interface{}{"setProps": []interface{}{"val1", "val2"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setProps: [ - [2]: "val3" ] -Resources: - ~ 1 to update - 1 unchanged `), }, { "set element removed middle", map[string]interface{}{"setProps": []interface{}{"val1", "val2", "val3"}}, map[string]interface{}{"setProps": []interface{}{"val1", "val3"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setProps: [ - ~ [1]: "val2" => "val3" - - [2]: "val3" + - [1]: "val2" ] -Resources: - ~ 1 to update - 1 unchanged `), }, { "set element changed", map[string]interface{}{"setProps": []interface{}{"val1"}}, map[string]interface{}{"setProps": []interface{}{"val2"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setProps: [ ~ [0]: "val1" => "val2" ] -Resources: - ~ 1 to update - 1 unchanged `), }, { "map unchanged", map[string]interface{}{"mapProp": map[string]interface{}{"key": "val"}}, map[string]interface{}{"mapProp": map[string]interface{}{"key": "val"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] -Resources: - 2 unchanged -`), + autogold.Expect("\n"), }, { "map added", map[string]interface{}{}, map[string]interface{}{"mapProp": map[string]interface{}{"key": "val"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + mapProp: { + key: "val" } -Resources: - ~ 1 to update - 1 unchanged `), }, // pulumi/pulumi-terraform-bridge#2233: This is the intended behavior @@ -2459,29 +2226,19 @@ Resources: "map added empty", map[string]interface{}{}, map[string]interface{}{"mapProp": map[string]interface{}{}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] -Resources: - 2 unchanged -`), + autogold.Expect("\n"), }, { "map removed", map[string]interface{}{"mapProp": map[string]interface{}{"key": "val"}}, map[string]interface{}{}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - mapProp: { - key: "val" } -Resources: - ~ 1 to update - 1 unchanged `), }, // pulumi/pulumi-terraform-bridge#2233: This is the intended behavior @@ -2489,74 +2246,52 @@ Resources: "map removed empty", map[string]interface{}{"mapProp": map[string]interface{}{}}, map[string]interface{}{}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] -Resources: - 2 unchanged -`), + autogold.Expect("\n"), }, { "map element added", map[string]interface{}{"mapProp": map[string]interface{}{}}, map[string]interface{}{"mapProp": map[string]interface{}{"key": "val"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + mapProp: { + key: "val" } -Resources: - ~ 1 to update - 1 unchanged `), }, { "map element removed", map[string]interface{}{"mapProp": map[string]interface{}{"key": "val"}}, map[string]interface{}{"mapProp": map[string]interface{}{}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ mapProp: { - key: "val" } -Resources: - ~ 1 to update - 1 unchanged `), }, { "map value changed", map[string]interface{}{"mapProp": map[string]interface{}{"key": "val1"}}, map[string]interface{}{"mapProp": map[string]interface{}{"key": "val2"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ mapProp: { ~ key: "val1" => "val2" } -Resources: - ~ 1 to update - 1 unchanged `), }, { "map key changed", map[string]interface{}{"mapProp": map[string]interface{}{"key1": "val"}}, map[string]interface{}{"mapProp": map[string]interface{}{"key2": "val"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2564,29 +2299,19 @@ Resources: - key1: "val" + key2: "val" } -Resources: - ~ 1 to update - 1 unchanged `), }, { "list block unchanged", map[string]interface{}{"listBlocks": []interface{}{map[string]interface{}{"prop": "val"}}}, map[string]interface{}{"listBlocks": []interface{}{map[string]interface{}{"prop": "val"}}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] -Resources: - 2 unchanged -`), + autogold.Expect("\n"), }, { "list block added", map[string]interface{}{}, map[string]interface{}{"listBlocks": []interface{}{map[string]interface{}{"prop": "val"}}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2595,9 +2320,6 @@ Resources: + prop : "val" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, // This is expected to be a no-op because blocks can not be nil in TF @@ -2605,20 +2327,13 @@ Resources: "list block added empty", map[string]interface{}{}, map[string]interface{}{"listBlocks": []interface{}{}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] -Resources: - 2 unchanged -`), + autogold.Expect("\n"), }, { "list block added empty object", map[string]interface{}{}, map[string]interface{}{"listBlocks": []interface{}{map[string]interface{}{}}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2626,18 +2341,13 @@ Resources: + [0]: { } ] -Resources: - ~ 1 to update - 1 unchanged `), }, { "list block removed", map[string]interface{}{"listBlocks": []interface{}{map[string]interface{}{"prop": "val"}}}, map[string]interface{}{}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2646,9 +2356,6 @@ Resources: - prop: "val" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, // This is expected to be a no-op because blocks can not be nil in TF @@ -2656,20 +2363,13 @@ Resources: "list block removed empty", map[string]interface{}{"listBlocks": []interface{}{}}, map[string]interface{}{}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] -Resources: - 2 unchanged -`), + autogold.Expect("\n"), }, { "list block removed empty object", map[string]interface{}{"listBlocks": []interface{}{map[string]interface{}{}}}, map[string]interface{}{}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2678,9 +2378,6 @@ Resources: - prop: } ] -Resources: - ~ 1 to update - 1 unchanged `), }, { @@ -2694,9 +2391,7 @@ Resources: map[string]interface{}{"prop": "val2"}, map[string]interface{}{"prop": "val3"}, }}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2711,9 +2406,6 @@ Resources: + prop : "val3" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, { @@ -2727,9 +2419,7 @@ Resources: map[string]interface{}{"prop": "val2"}, map[string]interface{}{"prop": "val3"}, }}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2738,9 +2428,6 @@ Resources: + prop : "val3" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, { @@ -2754,9 +2441,7 @@ Resources: map[string]interface{}{"prop": "val2"}, map[string]interface{}{"prop": "val3"}, }}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2768,9 +2453,6 @@ Resources: + prop : "val3" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, { @@ -2784,9 +2466,7 @@ Resources: map[string]interface{}{"prop": "val2"}, map[string]interface{}{"prop": "val3"}, }}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2801,9 +2481,6 @@ Resources: - prop: "val3" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, { @@ -2817,9 +2494,7 @@ Resources: map[string]interface{}{"prop": "val1"}, map[string]interface{}{"prop": "val2"}, }}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2828,9 +2503,6 @@ Resources: - prop: "val3" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, { @@ -2844,9 +2516,7 @@ Resources: map[string]interface{}{"prop": "val1"}, map[string]interface{}{"prop": "val3"}, }}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2858,9 +2528,6 @@ Resources: - prop: "val3" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, { @@ -2871,9 +2538,7 @@ Resources: map[string]interface{}{"listBlocks": []interface{}{ map[string]interface{}{"prop": "val2"}, }}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2882,29 +2547,19 @@ Resources: ~ prop: "val1" => "val2" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, { "set block unchanged", map[string]interface{}{"setBlocks": []interface{}{map[string]interface{}{"prop": "val"}}}, map[string]interface{}{"setBlocks": []interface{}{map[string]interface{}{"prop": "val"}}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] -Resources: - 2 unchanged -`), + autogold.Expect("\n"), }, { "set block added", map[string]interface{}{}, map[string]interface{}{"setBlocks": []interface{}{map[string]interface{}{"prop": "val"}}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2913,9 +2568,6 @@ Resources: + prop : "val" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, // This is expected to be a no-op because blocks can not be nil in TF @@ -2923,20 +2575,13 @@ Resources: "set block added empty", map[string]interface{}{}, map[string]interface{}{"setBlocks": []interface{}{}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] -Resources: - 2 unchanged -`), + autogold.Expect("\n"), }, { "set block added empty object", map[string]interface{}{}, map[string]interface{}{"setBlocks": []interface{}{map[string]interface{}{}}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2944,18 +2589,13 @@ Resources: + [0]: { } ] -Resources: - ~ 1 to update - 1 unchanged `), }, { "set block removed", map[string]interface{}{"setBlocks": []interface{}{map[string]interface{}{"prop": "val"}}}, map[string]interface{}{}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2964,9 +2604,6 @@ Resources: - prop: "val" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, // This is expected to be a no-op because blocks can not be nil in TF @@ -2974,21 +2611,14 @@ Resources: "set block removed empty", map[string]interface{}{"setBlocks": []interface{}{}}, map[string]interface{}{}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] -Resources: - 2 unchanged -`), + autogold.Expect("\n"), }, // TODO[pulumi/pulumi-terraform-bridge#2399] nested prop diff { "set block removed empty object", map[string]interface{}{"setBlocks": []interface{}{map[string]interface{}{}}}, map[string]interface{}{}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -2997,9 +2627,6 @@ Resources: - prop: "" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, { @@ -3013,26 +2640,15 @@ Resources: map[string]interface{}{"prop": "val2"}, map[string]interface{}{"prop": "val3"}, }}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setBlocks: [ - ~ [0]: { - ~ prop: "val2" => "val1" - } - ~ [1]: { - ~ prop: "val3" => "val2" - } - + [2]: { - + prop : "val3" + + [0]: { + + prop : "val1" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, { @@ -3046,9 +2662,7 @@ Resources: map[string]interface{}{"prop": "val2"}, map[string]interface{}{"prop": "val3"}, }}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -3057,9 +2671,6 @@ Resources: + prop : "val3" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, { @@ -3073,23 +2684,15 @@ Resources: map[string]interface{}{"prop": "val2"}, map[string]interface{}{"prop": "val3"}, }}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setBlocks: [ - ~ [1]: { - ~ prop: "val3" => "val2" - } - + [2]: { - + prop : "val3" + + [1]: { + + prop : "val2" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, { @@ -3103,26 +2706,15 @@ Resources: map[string]interface{}{"prop": "val2"}, map[string]interface{}{"prop": "val3"}, }}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setBlocks: [ - ~ [0]: { - ~ prop: "val1" => "val2" - } - ~ [1]: { - ~ prop: "val2" => "val3" - } - - [2]: { - - prop: "val3" + - [0]: { + - prop: "val1" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, { @@ -3136,9 +2728,7 @@ Resources: map[string]interface{}{"prop": "val1"}, map[string]interface{}{"prop": "val2"}, }}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -3147,9 +2737,6 @@ Resources: - prop: "val3" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, { @@ -3163,23 +2750,15 @@ Resources: map[string]interface{}{"prop": "val1"}, map[string]interface{}{"prop": "val3"}, }}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setBlocks: [ - ~ [1]: { - ~ prop: "val2" => "val3" - } - - [2]: { - - prop: "val3" + - [1]: { + - prop: "val2" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, { @@ -3190,9 +2769,7 @@ Resources: map[string]interface{}{"setBlocks": []interface{}{ map[string]interface{}{"prop": "val2"}, }}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] @@ -3201,73 +2778,50 @@ Resources: ~ prop: "val1" => "val2" } ] -Resources: - ~ 1 to update - 1 unchanged `), }, { "maxItemsOne block unchanged", map[string]interface{}{"maxItemsOneBlock": map[string]interface{}{"prop": "val"}}, map[string]interface{}{"maxItemsOneBlock": map[string]interface{}{"prop": "val"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] -Resources: - 2 unchanged -`), + autogold.Expect("\n"), }, { "maxItemsOne block added", map[string]interface{}{}, map[string]interface{}{"maxItemsOneBlock": map[string]interface{}{"prop": "val"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + maxItemsOneBlock: { + prop : "val" } -Resources: - ~ 1 to update - 1 unchanged `), }, { "maxItemsOne block added empty", map[string]interface{}{}, map[string]interface{}{"maxItemsOneBlock": map[string]interface{}{}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + maxItemsOneBlock: { } -Resources: - ~ 1 to update - 1 unchanged `), }, { "maxItemsOne block removed", map[string]interface{}{"maxItemsOneBlock": map[string]interface{}{"prop": "val"}}, map[string]interface{}{}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - maxItemsOneBlock: { - prop: "val" } -Resources: - ~ 1 to update - 1 unchanged `), }, // TODO[pulumi/pulumi-terraform-bridge#2399] nested prop diff @@ -3275,36 +2829,26 @@ Resources: "maxItemsOne block removed empty", map[string]interface{}{"maxItemsOneBlock": map[string]interface{}{}}, map[string]interface{}{}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - maxItemsOneBlock: { - prop: } -Resources: - ~ 1 to update - 1 unchanged `), }, { "maxItemsOne block changed", map[string]interface{}{"maxItemsOneBlock": map[string]interface{}{"prop": "val1"}}, map[string]interface{}{"maxItemsOneBlock": map[string]interface{}{"prop": "val2"}}, - autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + autogold.Expect(` ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ maxItemsOneBlock: { ~ prop: "val1" => "val2" } -Resources: - ~ 1 to update - 1 unchanged `), }, } { @@ -3327,7 +2871,7 @@ Resources: res := pt.Preview(t, optpreview.Diff()) t.Log(res.StdOut) - tc.expected.Equal(t, res.StdOut) + tc.expected.Equal(t, trimDiff(t, res.StdOut)) }) } } @@ -3980,130 +3524,6 @@ outputs: assert.Equal(t, "", out.Outputs["emptyValue"].Value) } -func TestUnknownSetElementDiff(t *testing.T) { - resMap := map[string]*schema.Resource{ - "prov_test": { - Schema: map[string]*schema.Schema{ - "test": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - }, - }, - "prov_aux": { - Schema: map[string]*schema.Schema{ - "aux": { - Type: schema.TypeString, - Computed: true, - Optional: true, - }, - }, - CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - d.SetId("aux") - err := d.Set("aux", "aux") - require.NoError(t, err) - return nil - }, - }, - } - tfp := &schema.Provider{ResourcesMap: resMap} - - runTest := func(t *testing.T, PRC bool, expectedOutput autogold.Value) { - opts := []pulcheck.BridgedProviderOpt{} - if !PRC { - opts = append(opts, pulcheck.DisablePlanResourceChange()) - } - bridgedProvider := pulcheck.BridgedProvider(t, "prov", tfp, opts...) - originalProgram := ` -name: test -runtime: yaml -resources: - mainRes: - type: prov:index:Test -outputs: - testOut: ${mainRes.tests} - ` - - programWithUnknown := ` -name: test -runtime: yaml -resources: - auxRes: - type: prov:index:Aux - mainRes: - type: prov:index:Test - properties: - tests: - - ${auxRes.aux} -outputs: - testOut: ${mainRes.tests} -` - pt := pulcheck.PulCheck(t, bridgedProvider, originalProgram) - pt.Up(t) - pulumiYamlPath := filepath.Join(pt.CurrentStack().Workspace().WorkDir(), "Pulumi.yaml") - - err := os.WriteFile(pulumiYamlPath, []byte(programWithUnknown), 0o600) - require.NoError(t, err) - - res := pt.Preview(t, optpreview.Diff()) - // Test that the test property is unknown at preview time - expectedOutput.Equal(t, res.StdOut) - resUp := pt.Up(t) - // assert that the property gets resolved - require.Equal(t, - []interface{}{"aux"}, - resUp.Outputs["testOut"].Value, - ) - } - - t.Run("PRC enabled", func(t *testing.T) { - // TODO: Remove this once accurate bridge previews are rolled out - t.Setenv("PULUMI_TF_BRIDGE_ACCURATE_BRIDGE_PREVIEW", "true") - runTest(t, true, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] - + prov:index/aux:Aux: (create) - [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] - ~ prov:index/test:Test: (update) - [id=newid] - [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - + tests: [ - + [0]: output - ] - --outputs:-- - + testOut: output -Resources: - + 1 to create - ~ 1 to update - 2 changes. 1 unchanged -`)) - }) - - t.Run("PRC disabled", func(t *testing.T) { - runTest(t, false, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] - + prov:index/aux:Aux: (create) - [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] - ~ prov:index/test:Test: (update) - [id=newid] - [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - + tests: [ - + [0]: output - ] - --outputs:-- - + testOut: output -Resources: - + 1 to create - ~ 1 to update - 2 changes. 1 unchanged -`)) - }) -} - func TestMakeTerraformResultNilVsEmptyMap(t *testing.T) { // Nil and empty maps are not equal nilMap := resource.NewObjectProperty(nil) @@ -4161,8 +3581,2510 @@ func TestMakeTerraformResultNilVsEmptyMap(t *testing.T) { }) } +func runDetailedDiffTest( + t *testing.T, resMap map[string]*schema.Resource, program1, program2 string, +) (string, map[string]interface{}) { + tfp := &schema.Provider{ResourcesMap: resMap} + bridgedProvider := pulcheck.BridgedProvider(t, "prov", tfp) + pt := pulcheck.PulCheck(t, bridgedProvider, program1) + pt.Up(t) + pulumiYamlPath := filepath.Join(pt.CurrentStack().Workspace().WorkDir(), "Pulumi.yaml") + + err := os.WriteFile(pulumiYamlPath, []byte(program2), 0o600) + require.NoError(t, err) + + pt.ClearGrpcLog(t) + res := pt.Preview(t, optpreview.Diff()) + t.Log(res.StdOut) + + diffResponse := struct { + DetailedDiff map[string]interface{} `json:"detailedDiff"` + }{} + + for _, entry := range pt.GrpcLog(t).Entries { + if entry.Method == "/pulumirpc.ResourceProvider/Diff" { + err := json.Unmarshal(entry.Response, &diffResponse) + require.NoError(t, err) + } + } + + return res.StdOut, diffResponse.DetailedDiff +} + +func TestDetailedDiffSet(t *testing.T) { + // TODO[pulumi/pulumi-terraform-bridge#2517]: Remove this once accurate bridge previews are rolled out + t.Setenv("PULUMI_TF_BRIDGE_ACCURATE_BRIDGE_PREVIEW", "true") + runTest := func(t *testing.T, resMap map[string]*schema.Resource, props1, props2 interface{}, + expected, expectedDetailedDiff autogold.Value, + ) { + program := ` +name: test +runtime: yaml +resources: + mainRes: + type: prov:index:Test + properties: + tests: %s +` + props1JSON, err := json.Marshal(props1) + require.NoError(t, err) + program1 := fmt.Sprintf(program, string(props1JSON)) + props2JSON, err := json.Marshal(props2) + require.NoError(t, err) + program2 := fmt.Sprintf(program, string(props2JSON)) + out, detailedDiff := runDetailedDiffTest(t, resMap, program1, program2) + + expected.Equal(t, trimDiff(t, out)) + expectedDetailedDiff.Equal(t, detailedDiff) + } + + // The following test cases use the same inputs (props1 and props2) to test a few variations: + // - The diff when the schema is a set of strings + // - The diff when the schema is a set of strings with ForceNew + // - The diff when the schema is a set of structs with a nested string + // - The diff when the schema is a set of structs with a nested string and ForceNew + // For each of these variations, we record both the detailed diff output sent to the engine + // and the output that we expect to see in the Pulumi console. + type setDetailedDiffTestCase struct { + name string + props1 []string + props2 []string + expectedAttrDetailedDiff autogold.Value + expectedAttr autogold.Value + expectedAttrForceNewDetailedDiff autogold.Value + expectedAttrForceNew autogold.Value + expectedBlockDetailedDiff autogold.Value + expectedBlock autogold.Value + expectedBlockForceNewDetailedDiff autogold.Value + expectedBlockForceNew autogold.Value + } + + testCases := []setDetailedDiffTestCase{ + { + name: "unchanged", + props1: []string{"val1"}, + props2: []string{"val1"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedAttr: autogold.Expect("\n"), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedAttrForceNew: autogold.Expect("\n"), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedBlock: autogold.Expect("\n"), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedBlockForceNew: autogold.Expect("\n"), + }, + { + name: "changed non-empty", + props1: []string{"val1"}, + props2: []string{"val2"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "UPDATE"}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [0]: "val1" => "val2" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "UPDATE_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [0]: "val1" => "val2" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0].nested": map[string]interface{}{"kind": "UPDATE"}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [0]: { + ~ nested: "val1" => "val2" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0].nested": map[string]interface{}{"kind": "UPDATE_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [0]: { + ~ nested: "val1" => "val2" + } + ] +`), + }, + { + name: "changed from empty", + props1: []string{}, + props2: []string{"val1"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + + tests: [ + + [0]: "val1" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + + tests: [ + + [0]: "val1" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + + tests: [ + + [0]: { + + nested : "val1" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + + tests: [ + + [0]: { + + nested : "val1" + } + ] +`), + }, + { + name: "changed to empty", + props1: []string{"val1"}, + props2: []string{}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "DELETE"}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + - tests: [ + - [0]: "val1" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + - tests: [ + - [0]: "val1" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "DELETE"}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + - tests: [ + - [0]: { + - nested: "val1" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + - tests: [ + - [0]: { + - nested: "val1" + } + ] +`), + }, + { + name: "removed front", + props1: []string{"val1", "val2", "val3"}, + props2: []string{"val2", "val3"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE"}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: "val1" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: "val1" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE"}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: { + - nested: "val1" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: { + - nested: "val1" + } + ] +`), + }, + { + name: "removed front unordered", + props1: []string{"val2", "val1", "val3"}, + props2: []string{"val1", "val3"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE"}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [1]: "val2" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [1]: "val2" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE"}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [1]: { + - nested: "val2" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [1]: { + - nested: "val2" + } + ] +`), + }, + { + name: "removed middle", + props1: []string{"val1", "val2", "val3"}, + props2: []string{"val1", "val3"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE"}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [1]: "val2" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [1]: "val2" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE"}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [1]: { + - nested: "val2" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [1]: { + - nested: "val2" + } + ] +`), + }, + { + name: "removed middle unordered", + props1: []string{"val2", "val3", "val1"}, + props2: []string{"val2", "val1"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE"}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: "val3" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: "val3" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE"}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: { + - nested: "val3" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: { + - nested: "val3" + } + ] +`), + }, + { + name: "removed end", + props1: []string{"val1", "val2", "val3"}, + props2: []string{"val1", "val2"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE"}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: "val3" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: "val3" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE"}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: { + - nested: "val3" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: { + - nested: "val3" + } + ] +`), + }, + { + name: "removed end unordered", + props1: []string{"val2", "val3", "val1"}, + props2: []string{"val2", "val3"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE"}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: "val1" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: "val1" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE"}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: { + - nested: "val1" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: { + - nested: "val1" + } + ] +`), + }, + { + name: "added front", + props1: []string{"val2", "val3"}, + props2: []string{"val1", "val2", "val3"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: "val1" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: "val1" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: { + + nested : "val1" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: { + + nested : "val1" + } + ] +`), + }, + { + name: "added front unordered", + props1: []string{"val3", "val1"}, + props2: []string{"val2", "val2", "val1"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "UPDATE"}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [1]: "val3" => "val2" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "UPDATE_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [1]: "val3" => "val2" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1].nested": map[string]interface{}{"kind": "UPDATE"}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [1]: { + ~ nested: "val3" => "val2" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1].nested": map[string]interface{}{"kind": "UPDATE_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [1]: { + ~ nested: "val3" => "val2" + } + ] +`), + }, + { + name: "added middle", + props1: []string{"val1", "val3"}, + props2: []string{"val1", "val2", "val3"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val2" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val2" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val2" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val2" + } + ] +`), + }, + { + name: "added middle unordered", + props1: []string{"val2", "val1"}, + props2: []string{"val2", "val3", "val1"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val3" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val3" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val3" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val3" + } + ] +`), + }, + { + name: "added end", + props1: []string{"val1", "val2"}, + props2: []string{"val1", "val2", "val3"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: "val3" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: "val3" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: { + + nested : "val3" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: { + + nested : "val3" + } + ] +`), + }, + { + name: "added end unordered", + props1: []string{"val2", "val3"}, + props2: []string{"val2", "val3", "val1"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: "val1" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: "val1" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: { + + nested : "val1" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: { + + nested : "val1" + } + ] +`), + }, + { + name: "same element updated", + props1: []string{"val1", "val2", "val3"}, + props2: []string{"val1", "val4", "val3"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "UPDATE"}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [1]: "val2" => "val4" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "UPDATE_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [1]: "val2" => "val4" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1].nested": map[string]interface{}{"kind": "UPDATE"}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [1]: { + ~ nested: "val2" => "val4" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1].nested": map[string]interface{}{"kind": "UPDATE_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [1]: { + ~ nested: "val2" => "val4" + } + ] +`), + }, + { + name: "same element updated unordered", + props1: []string{"val2", "val3", "val1"}, + props2: []string{"val2", "val4", "val1"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{ + "tests[1]": map[string]interface{}{}, + "tests[2]": map[string]interface{}{"kind": "DELETE"}, + }), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val4" + - [2]: "val3" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{ + "tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + }), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val4" + - [2]: "val3" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{ + "tests[1]": map[string]interface{}{}, + "tests[2]": map[string]interface{}{"kind": "DELETE"}, + }), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val4" + } + - [2]: { + - nested: "val3" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{ + "tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + }), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val4" + } + - [2]: { + - nested: "val3" + } + ] +`), + }, + { + name: "shuffled", + props1: []string{"val1", "val2", "val3"}, + props2: []string{"val3", "val1", "val2"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedAttr: autogold.Expect("\n"), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedAttrForceNew: autogold.Expect("\n"), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedBlock: autogold.Expect("\n"), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedBlockForceNew: autogold.Expect("\n"), + }, + { + name: "shuffled unordered", + props1: []string{"val2", "val3", "val1"}, + props2: []string{"val3", "val1", "val2"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedAttr: autogold.Expect("\n"), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedAttrForceNew: autogold.Expect("\n"), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedBlock: autogold.Expect("\n"), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedBlockForceNew: autogold.Expect("\n"), + }, + { + name: "shuffled with duplicates", + props1: []string{"val1", "val2", "val3"}, + props2: []string{"val3", "val1", "val2", "val3"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedAttr: autogold.Expect("\n"), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedAttrForceNew: autogold.Expect("\n"), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedBlock: autogold.Expect("\n"), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedBlockForceNew: autogold.Expect("\n"), + }, + { + name: "shuffled with duplicates unordered", + props1: []string{"val2", "val3", "val1"}, + props2: []string{"val3", "val1", "val2", "val3"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedAttr: autogold.Expect("\n"), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedAttrForceNew: autogold.Expect("\n"), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedBlock: autogold.Expect("\n"), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{}), + expectedBlockForceNew: autogold.Expect("\n"), + }, + { + name: "shuffled added front", + props1: []string{"val2", "val3"}, + props2: []string{"val1", "val3", "val2"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: "val1" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: "val1" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: { + + nested : "val1" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: { + + nested : "val1" + } + ] +`), + }, + { + name: "shuffled added front unordered", + props1: []string{"val3", "val1"}, + props2: []string{"val2", "val1", "val3"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: "val2" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: "val2" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: { + + nested : "val2" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: { + + nested : "val2" + } + ] +`), + }, + { + name: "shuffled added middle", + props1: []string{"val1", "val3"}, + props2: []string{"val3", "val2", "val1"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val2" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val2" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val2" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val2" + } + ] +`), + }, + { + name: "shuffled added middle unordered", + props1: []string{"val2", "val1"}, + props2: []string{"val1", "val3", "val2"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val3" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val3" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val3" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val3" + } + ] +`), + }, + { + name: "shuffled added end", + props1: []string{"val1", "val2"}, + props2: []string{"val2", "val1", "val3"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: "val3" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: "val3" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: { + + nested : "val3" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: { + + nested : "val3" + } + ] +`), + }, + { + name: "shuffled removed front", + props1: []string{"val1", "val2", "val3"}, + props2: []string{"val3", "val2"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE"}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: "val1" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: "val1" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE"}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: { + - nested: "val1" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: { + - nested: "val1" + } + ] +`), + }, + { + name: "shuffled removed middle", + props1: []string{"val1", "val2", "val3"}, + props2: []string{"val3", "val1"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE"}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [1]: "val2" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [1]: "val2" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE"}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [1]: { + - nested: "val2" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [1]: { + - nested: "val2" + } + ] +`), + }, + { + name: "shuffled removed end", + props1: []string{"val1", "val2", "val3"}, + props2: []string{"val2", "val1"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE"}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: "val3" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: "val3" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE"}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: { + - nested: "val3" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: { + - nested: "val3" + } + ] +`), + }, + { + name: "two added", + props1: []string{"val1", "val2"}, + props2: []string{"val1", "val2", "val3", "val4"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{}, "tests[3]": map[string]interface{}{}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: "val3" + + [3]: "val4" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "ADD_REPLACE"}, "tests[3]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: "val3" + + [3]: "val4" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{}, "tests[3]": map[string]interface{}{}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: { + + nested : "val3" + } + + [3]: { + + nested : "val4" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "ADD_REPLACE"}, "tests[3]": map[string]interface{}{"kind": "ADD_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: { + + nested : "val3" + } + + [3]: { + + nested : "val4" + } + ] +`), + }, + { + name: "two removed", + props1: []string{"val1", "val2", "val3", "val4"}, + props2: []string{"val1", "val2"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE"}, "tests[3]": map[string]interface{}{"kind": "DELETE"}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: "val3" + - [3]: "val4" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}, "tests[3]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: "val3" + - [3]: "val4" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE"}, "tests[3]": map[string]interface{}{"kind": "DELETE"}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: { + - nested: "val3" + } + - [3]: { + - nested: "val4" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}, "tests[3]": map[string]interface{}{"kind": "DELETE_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: { + - nested: "val3" + } + - [3]: { + - nested: "val4" + } + ] +`), + }, + { + name: "two added and two removed", + props1: []string{"val1", "val2", "val3", "val4"}, + props2: []string{"val1", "val2", "val5", "val6"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "UPDATE"}, "tests[3]": map[string]interface{}{"kind": "UPDATE"}}), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [2]: "val3" => "val5" + ~ [3]: "val4" => "val6" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "UPDATE_REPLACE"}, "tests[3]": map[string]interface{}{"kind": "UPDATE_REPLACE"}}), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [2]: "val3" => "val5" + ~ [3]: "val4" => "val6" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2].nested": map[string]interface{}{"kind": "UPDATE"}, "tests[3].nested": map[string]interface{}{"kind": "UPDATE"}}), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [2]: { + ~ nested: "val3" => "val5" + } + ~ [3]: { + ~ nested: "val4" => "val6" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{"tests[2].nested": map[string]interface{}{"kind": "UPDATE_REPLACE"}, "tests[3].nested": map[string]interface{}{"kind": "UPDATE_REPLACE"}}), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [2]: { + ~ nested: "val3" => "val5" + } + ~ [3]: { + ~ nested: "val4" => "val6" + } + ] +`), + }, + { + name: "two added and two removed shuffled, one overlaps", + props1: []string{"val1", "val2", "val3", "val4"}, + props2: []string{"val1", "val5", "val6", "val2"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{ + "tests[1]": map[string]interface{}{}, + "tests[2]": map[string]interface{}{"kind": "UPDATE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE"}, + }), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val5" + ~ [2]: "val3" => "val6" + - [3]: "val4" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{ + "tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[2]": map[string]interface{}{"kind": "UPDATE_REPLACE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + }), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val5" + ~ [2]: "val3" => "val6" + - [3]: "val4" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{ + "tests[1]": map[string]interface{}{}, + "tests[2].nested": map[string]interface{}{"kind": "UPDATE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE"}, + }), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val5" + } + ~ [2]: { + ~ nested: "val3" => "val6" + } + - [3]: { + - nested: "val4" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{ + "tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[2].nested": map[string]interface{}{"kind": "UPDATE_REPLACE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + }), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val5" + } + ~ [2]: { + ~ nested: "val3" => "val6" + } + - [3]: { + - nested: "val4" + } + ] +`), + }, + { + name: "two added and two removed shuffled, no overlaps", + props1: []string{"val1", "val2", "val3", "val4"}, + props2: []string{"val5", "val6", "val1", "val2"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{ + "tests[0]": map[string]interface{}{}, + "tests[1]": map[string]interface{}{}, + "tests[2]": map[string]interface{}{"kind": "DELETE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE"}, + }), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: "val5" + + [1]: "val6" + - [2]: "val3" + - [3]: "val4" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{ + "tests[0]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + }), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: "val5" + + [1]: "val6" + - [2]: "val3" + - [3]: "val4" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{ + "tests[0]": map[string]interface{}{}, + "tests[1]": map[string]interface{}{}, + "tests[2]": map[string]interface{}{"kind": "DELETE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE"}, + }), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: { + + nested : "val5" + } + + [1]: { + + nested : "val6" + } + - [2]: { + - nested: "val3" + } + - [3]: { + - nested: "val4" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{ + "tests[0]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + }), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: { + + nested : "val5" + } + + [1]: { + + nested : "val6" + } + - [2]: { + - nested: "val3" + } + - [3]: { + - nested: "val4" + } + ] +`), + }, + { + name: "two added and two removed shuffled, with duplicates", + props1: []string{"val1", "val2", "val3", "val4"}, + props2: []string{"val1", "val5", "val6", "val2", "val1", "val2"}, + expectedAttrDetailedDiff: autogold.Expect(map[string]interface{}{ + "tests[1]": map[string]interface{}{}, + "tests[2]": map[string]interface{}{"kind": "UPDATE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE"}, + }), + expectedAttr: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val5" + ~ [2]: "val3" => "val6" + - [3]: "val4" + ] +`), + expectedAttrForceNewDetailedDiff: autogold.Expect(map[string]interface{}{ + "tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[2]": map[string]interface{}{"kind": "UPDATE_REPLACE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + }), + expectedAttrForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val5" + ~ [2]: "val3" => "val6" + - [3]: "val4" + ] +`), + expectedBlockDetailedDiff: autogold.Expect(map[string]interface{}{ + "tests[1]": map[string]interface{}{}, + "tests[2].nested": map[string]interface{}{"kind": "UPDATE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE"}, + }), + expectedBlock: autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val5" + } + ~ [2]: { + ~ nested: "val3" => "val6" + } + - [3]: { + - nested: "val4" + } + ] +`), + expectedBlockForceNewDetailedDiff: autogold.Expect(map[string]interface{}{ + "tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[2].nested": map[string]interface{}{"kind": "UPDATE_REPLACE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + }), + expectedBlockForceNew: autogold.Expect(` + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val5" + } + ~ [2]: { + ~ nested: "val3" => "val6" + } + - [3]: { + - nested: "val4" + } + ] +`), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + for _, forceNew := range []bool{false, true} { + t.Run(fmt.Sprintf("ForceNew=%v", forceNew), func(t *testing.T) { + expected := tc.expectedAttr + if forceNew { + expected = tc.expectedAttrForceNew + } + + expectedDetailedDiff := tc.expectedAttrDetailedDiff + if forceNew { + expectedDetailedDiff = tc.expectedAttrForceNewDetailedDiff + } + t.Run("Attribute", func(t *testing.T) { + res := &schema.Resource{ + Schema: map[string]*schema.Schema{ + "test": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + ForceNew: forceNew, + }, + }, + } + runTest(t, map[string]*schema.Resource{"prov_test": res}, tc.props1, tc.props2, expected, expectedDetailedDiff) + }) + + expected = tc.expectedBlock + if forceNew { + expected = tc.expectedBlockForceNew + } + expectedDetailedDiff = tc.expectedBlockDetailedDiff + if forceNew { + expectedDetailedDiff = tc.expectedBlockForceNewDetailedDiff + } + + t.Run("Block", func(t *testing.T) { + res := &schema.Resource{ + Schema: map[string]*schema.Schema{ + "test": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "nested": { + Type: schema.TypeString, + Optional: true, + ForceNew: forceNew, + }, + }, + }, + }, + }, + } + + props1 := make([]interface{}, len(tc.props1)) + for i, v := range tc.props1 { + props1[i] = map[string]interface{}{"nested": v} + } + + props2 := make([]interface{}, len(tc.props2)) + for i, v := range tc.props2 { + props2[i] = map[string]interface{}{"nested": v} + } + + runTest(t, map[string]*schema.Resource{"prov_test": res}, props1, props2, expected, expectedDetailedDiff) + }) + }) + } + }) + } +} + +// "UNKNOWN" for unknown values +func testDetailedDiffWithUnknowns(t *testing.T, resMap map[string]*schema.Resource, unknownString string, props1, props2 interface{}, expected, expectedDetailedDiff autogold.Value) { + originalProgram := ` +name: test +runtime: yaml +resources: + mainRes: + type: prov:index:Test + properties: + tests: %s +outputs: + testOut: ${mainRes.tests} + ` + props1JSON, err := json.Marshal(props1) + require.NoError(t, err) + program1 := fmt.Sprintf(originalProgram, string(props1JSON)) + + programWithUnknown := ` +name: test +runtime: yaml +resources: + auxRes: + type: prov:index:Aux + mainRes: + type: prov:index:Test + properties: + tests: %s +outputs: + testOut: ${mainRes.tests} +` + props2JSON, err := json.Marshal(props2) + require.NoError(t, err) + program2 := fmt.Sprintf(programWithUnknown, string(props2JSON)) + program2 = strings.ReplaceAll(program2, "UNKNOWN", unknownString) + + out, detailedDiff := runDetailedDiffTest(t, resMap, program1, program2) + expected.Equal(t, trimDiff(t, out)) + expectedDetailedDiff.Equal(t, detailedDiff) +} + +func TestDetailedDiffUnknownSetAttributeElement(t *testing.T) { + // TODO[pulumi/pulumi-terraform-bridge#2517]: Remove this once accurate bridge previews are rolled out + t.Setenv("PULUMI_TF_BRIDGE_ACCURATE_BRIDGE_PREVIEW", "true") + resMap := map[string]*schema.Resource{ + "prov_test": { + Schema: map[string]*schema.Schema{ + "test": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + "prov_aux": { + Schema: map[string]*schema.Schema{ + "aux": { + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + }, + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("aux") + err := d.Set("aux", "aux") + require.NoError(t, err) + return nil + }, + }, + } + + t.Run("empty to unknown element", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.aux}", + []interface{}{}, + []interface{}{"UNKNOWN"}, + autogold.Expect(` + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + + tests: [ + + [0]: output + ] + --outputs:-- + + testOut: output +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{}})) + }) + + t.Run("non-empty to unknown element", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.aux}", + []interface{}{"val1"}, + []interface{}{"UNKNOWN"}, + autogold.Expect(` + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [0]: "val1" => output + ] +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "UPDATE"}})) + }) + + t.Run("unknown element added front", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.aux}", + []interface{}{"val2", "val3"}, + []interface{}{"UNKNOWN", "val2", "val3"}, + autogold.Expect(` + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [0]: "val2" => output + ~ [1]: "val3" => "val2" + + [2]: "val3" + ] +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "UPDATE"}}), + ) + }) + + t.Run("unknown element added middle", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.aux}", + []interface{}{"val1", "val3"}, + []interface{}{"val1", "UNKNOWN", "val3"}, + autogold.Expect(` + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + [0]: "val1" + ~ [1]: "val3" => output + + [2]: "val3" + ] +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "UPDATE"}}), + ) + }) + + t.Run("unknown element added end", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.aux}", + []interface{}{"val1", "val2"}, + []interface{}{"val1", "val2", "UNKNOWN"}, + autogold.Expect(` + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + [0]: "val1" + [1]: "val2" + + [2]: output + ] +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "UPDATE"}}), + ) + }) + + t.Run("element updated to unknown", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.aux}", + []interface{}{"val1", "val2", "val3"}, + []interface{}{"val1", "UNKNOWN", "val3"}, + autogold.Expect(` + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + [0]: "val1" + ~ [1]: "val2" => output + [2]: "val3" + ] +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "UPDATE"}}), + ) + }) + + t.Run("shuffled unknown added front", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.aux}", + []interface{}{"val2", "val3"}, + []interface{}{"UNKNOWN", "val3", "val2"}, + autogold.Expect(` + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [0]: "val2" => output + [1]: "val3" + + [2]: "val2" + ] +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "UPDATE"}}), + ) + }) + + t.Run("shuffled unknown added middle", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.aux}", + []interface{}{"val1", "val3"}, + []interface{}{"val3", "UNKNOWN", "val1"}, + autogold.Expect(` + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [0]: "val1" => "val3" + ~ [1]: "val3" => output + + [2]: "val1" + ] +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "UPDATE"}}), + ) + }) + + t.Run("shuffled unknown added end", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.aux}", + []interface{}{"val1", "val2"}, + []interface{}{"val2", "val1", "UNKNOWN"}, + autogold.Expect(` + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [0]: "val1" => "val2" + ~ [1]: "val2" => "val1" + + [2]: output + ] +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "UPDATE"}}), + ) + }) +} + +func TestUnknownSetAttributeDiff(t *testing.T) { + // TODO[pulumi/pulumi-terraform-bridge#2517]: Remove this once accurate bridge previews are rolled out + t.Setenv("PULUMI_TF_BRIDGE_ACCURATE_BRIDGE_PREVIEW", "true") + resMap := map[string]*schema.Resource{ + "prov_test": { + Schema: map[string]*schema.Schema{ + "test": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + "prov_aux": { + Schema: map[string]*schema.Schema{ + "aux": { + Type: schema.TypeSet, + Computed: true, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("aux") + err := d.Set("aux", []interface{}{"aux"}) + require.NoError(t, err) + return nil + }, + }, + } + + t.Run("empty to unknown set", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.auxes}", + []interface{}{}, + "UNKNOWN", + autogold.Expect(` + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + + tests: output + --outputs:-- + + testOut: output +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{}}), + ) + }) + + t.Run("non-empty to unknown set", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.auxes}", + []interface{}{"val"}, + "UNKNOWN", + autogold.Expect(` + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + - tests: [ + - [0]: "val" + ] + + tests: output +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "UPDATE"}}), + ) + }) +} + +func TestDetailedDiffSetDuplicates(t *testing.T) { + // TODO[pulumi/pulumi-terraform-bridge#2517]: Remove this once accurate bridge previews are rolled out + t.Setenv("PULUMI_TF_BRIDGE_ACCURATE_BRIDGE_PREVIEW", "true") + resMap := map[string]*schema.Resource{ + "prov_test": { + Schema: map[string]*schema.Schema{ + "test": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + } + tfp := &schema.Provider{ResourcesMap: resMap} + bridgedProvider := pulcheck.BridgedProvider(t, "prov", tfp) + + program := ` +name: test +runtime: yaml +resources: + mainRes: + type: prov:index:Test + properties: + tests: %s` + + t.Run("pulumi", func(t *testing.T) { + pt := pulcheck.PulCheck(t, bridgedProvider, fmt.Sprintf(program, `["a", "a"]`)) + pt.Up(t) + + pt.WritePulumiYaml(t, fmt.Sprintf(program, `["b", "b", "a", "a", "c"]`)) + + res := pt.Preview(t, optpreview.Diff()) + + autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "b" + + [4]: "c" + ] +`).Equal(t, trimDiff(t, res.StdOut)) + }) + + t.Run("terraform", func(t *testing.T) { + tfdriver := tfcheck.NewTfDriver(t, t.TempDir(), "prov", tfp) + tfdriver.Write(t, ` +resource "prov_test" "mainRes" { + test = ["a", "a"] +}`) + + plan, err := tfdriver.Plan(t) + require.NoError(t, err) + err = tfdriver.Apply(t, plan) + require.NoError(t, err) + + tfdriver.Write(t, ` +resource "prov_test" "mainRes" { + test = ["b", "b", "a", "a", "c"] +}`) + + plan, err = tfdriver.Plan(t) + require.NoError(t, err) + + autogold.Expect(` +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # prov_test.mainRes will be updated in-place + ~ resource "prov_test" "mainRes" { + id = "newid" + ~ test = [ + + "b", + + "c", + # (1 unchanged element hidden) + ] + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +`).Equal(t, plan.StdOut) + }) +} + +func TestDetailedDiffSetNestedAttributeUpdated(t *testing.T) { + // TODO[pulumi/pulumi-terraform-bridge#2517]: Remove this once accurate bridge previews are rolled out + t.Setenv("PULUMI_TF_BRIDGE_ACCURATE_BRIDGE_PREVIEW", "true") + + resMap := map[string]*schema.Resource{ + "prov_test": { + Schema: map[string]*schema.Schema{ + "test": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "nested": { + Type: schema.TypeString, + Optional: true, + }, + "nested2": { + Type: schema.TypeString, + Optional: true, + }, + "nested3": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + } + + tfp := &schema.Provider{ResourcesMap: resMap} + + bridgedProvider := pulcheck.BridgedProvider(t, "prov", tfp) + + program := ` +name: test +runtime: yaml +resources: + mainRes: + type: prov:index:Test + properties: + tests: %s` + + t.Run("pulumi", func(t *testing.T) { + props1 := []map[string]string{ + {"nested": "b", "nested2": "b", "nested3": "b"}, + {"nested": "a", "nested2": "a", "nested3": "a"}, + {"nested": "c", "nested2": "c", "nested3": "c"}, + } + props2 := []map[string]string{ + {"nested": "b", "nested2": "b", "nested3": "b"}, + {"nested": "d", "nested2": "a", "nested3": "a"}, + {"nested": "c", "nested2": "c", "nested3": "c"}, + } + + props1JSON, err := json.Marshal(props1) + require.NoError(t, err) + + pt := pulcheck.PulCheck(t, bridgedProvider, fmt.Sprintf(program, string(props1JSON))) + pt.Up(t) + + props2JSON, err := json.Marshal(props2) + require.NoError(t, err) + + pt.WritePulumiYaml(t, fmt.Sprintf(program, string(props2JSON))) + + res := pt.Preview(t, optpreview.Diff()) + + autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: { + - nested : "a" + - nested2: "a" + - nested3: "a" + } + + [1]: { + + nested : "d" + + nested2 : "a" + + nested3 : "a" + } + ] +`).Equal(t, trimDiff(t, res.StdOut)) + }) + + t.Run("terraform", func(t *testing.T) { + tfdriver := tfcheck.NewTfDriver(t, t.TempDir(), "prov", tfp) + tfdriver.Write(t, ` +resource "prov_test" "mainRes" { + test { + nested = "b" + nested2 = "b" + nested3 = "b" + } + test { + nested = "a" + nested2 = "a" + nested3 = "a" + } + test { + nested = "c" + nested2 = "c" + nested3 = "c" + } +}`) + + plan, err := tfdriver.Plan(t) + require.NoError(t, err) + err = tfdriver.Apply(t, plan) + require.NoError(t, err) + + tfdriver.Write(t, ` +resource "prov_test" "mainRes" { + test { + nested = "b" + nested2 = "b" + nested3 = "b" + } + test { + nested = "d" + nested2 = "a" + nested3 = "a" + } + test { + nested = "c" + nested2 = "c" + nested3 = "c" + } +}`) + + plan, err = tfdriver.Plan(t) + require.NoError(t, err) + + autogold.Expect(` +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # prov_test.mainRes will be updated in-place + ~ resource "prov_test" "mainRes" { + id = "newid" + + - test { + - nested = "a" -> null + - nested2 = "a" -> null + - nested3 = "a" -> null + } + + test { + + nested = "d" + + nested2 = "a" + + nested3 = "a" + } + + # (2 unchanged blocks hidden) + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +`).Equal(t, plan.StdOut) + }) +} + +func TestDetailedDiffSetComputedNestedAttribute(t *testing.T) { + // TODO[pulumi/pulumi-terraform-bridge#2517]: Remove this once accurate bridge previews are rolled out + t.Setenv("PULUMI_TF_BRIDGE_ACCURATE_BRIDGE_PREVIEW", "true") + + resCount := 0 + setComputedProp := func(t *testing.T, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + testSet := d.Get("test").(*schema.Set) + testVals := testSet.List() + newTestVals := make([]interface{}, len(testVals)) + for i, v := range testVals { + val := v.(map[string]interface{}) + if val["computed"] == nil || val["computed"] == "" { + val["computed"] = fmt.Sprint(resCount) + resCount++ + } + newTestVals[i] = val + } + + err := d.Set("test", schema.NewSet(testSet.F, newTestVals)) + require.NoError(t, err) + return nil + } + + resMap := map[string]*schema.Resource{ + "prov_test": { + Schema: map[string]*schema.Schema{ + "test": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "nested": { + Type: schema.TypeString, + Optional: true, + }, + "computed": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + CreateContext: func(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { + d.SetId("id") + return setComputedProp(t, d, i) + }, + UpdateContext: func(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { + return setComputedProp(t, d, i) + }, + }, + } + + tfp := &schema.Provider{ResourcesMap: resMap} + bridgedProvider := pulcheck.BridgedProvider(t, "prov", tfp) + + program := ` +name: test +runtime: yaml +resources: + mainRes: + type: prov:index:Test + properties: + tests: %s` + + t.Run("pulumi", func(t *testing.T) { + props1 := []map[string]string{ + {"nested": "a", "computed": "b"}, + } + props1JSON, err := json.Marshal(props1) + require.NoError(t, err) + + pt := pulcheck.PulCheck(t, bridgedProvider, fmt.Sprintf(program, string(props1JSON))) + pt.Up(t) + + props2 := []map[string]string{ + {"nested": "a"}, + {"nested": "a", "computed": "b"}, + } + props2JSON, err := json.Marshal(props2) + require.NoError(t, err) + + pt.WritePulumiYaml(t, fmt.Sprintf(program, string(props2JSON))) + res := pt.Preview(t, optpreview.Diff()) + + // TODO[pulumi/pulumi-terraform-bridge#2528]: The preview is wrong here because of the computed property + autogold.Expect(` + ~ prov:index/test:Test: (update) + [id=id] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + computed : "b" + + nested : "a" + } + ] +`).Equal(t, trimDiff(t, res.StdOut)) + }) + + t.Run("terraform", func(t *testing.T) { + resCount = 0 + tfdriver := tfcheck.NewTfDriver(t, t.TempDir(), "prov", tfp) + tfdriver.Write(t, ` +resource "prov_test" "mainRes" { + test { + nested = "a" + computed = "b" + } +}`) + + plan, err := tfdriver.Plan(t) + require.NoError(t, err) + err = tfdriver.Apply(t, plan) + require.NoError(t, err) + + tfdriver.Write(t, ` +resource "prov_test" "mainRes" { + test { + nested = "a" + computed = "b" + } + test { + nested = "a" + } +}`) + plan, err = tfdriver.Plan(t) + require.NoError(t, err) + + autogold.Expect(` +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # prov_test.mainRes will be updated in-place + ~ resource "prov_test" "mainRes" { + id = "id" + + + test { + + computed = (known after apply) + + nested = "a" + } + + # (1 unchanged block hidden) + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +`).Equal(t, plan.StdOut) + }) +} + func TestUnknownCollectionForceNewDetailedDiff(t *testing.T) { - // TODO: Remove this once accurate bridge previews are rolled out + // TODO[pulumi/pulumi-terraform-bridge#2517]: Remove this once accurate bridge previews are rolled out t.Setenv("PULUMI_TF_BRIDGE_ACCURATE_BRIDGE_PREVIEW", "true") collectionForceNewResource := func(typ schema.ValueType) *schema.Resource { @@ -4259,7 +6181,7 @@ func TestUnknownCollectionForceNewDetailedDiff(t *testing.T) { res := pt.Preview(t, optpreview.Diff()) - expectedOutput.Equal(t, res.StdOut) + expectedOutput.Equal(t, trimDiff(t, res.StdOut)) } t.Run("list force new", func(t *testing.T) { @@ -4276,9 +6198,7 @@ func TestUnknownCollectionForceNewDetailedDiff(t *testing.T) { t.Run("unknown plain property", func(t *testing.T) { program2 := fmt.Sprintf(program, "[{prop: \"${auxRes.auxes[0].prop}\"}]") - runTest(t, program2, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + runTest(t, program2, autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] ~ prov:index/test:Test: (update) @@ -4289,18 +6209,12 @@ func TestUnknownCollectionForceNewDetailedDiff(t *testing.T) { ~ prop: "value" => output } ] -Resources: - + 1 to create - ~ 1 to update - 2 changes. 1 unchanged `)) }) t.Run("unknown object", func(t *testing.T) { program2 := fmt.Sprintf(program, "[\"${auxRes.auxes[0]}\"]") - runTest(t, program2, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + runTest(t, program2, autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] +-prov:index/test:Test: (replace) @@ -4312,18 +6226,12 @@ Resources: } + [0]: output ] -Resources: - + 1 to create - +-1 to replace - 2 changes. 1 unchanged `)) }) t.Run("unknown collection", func(t *testing.T) { program2 := fmt.Sprintf(program, "\"${auxRes.auxes}\"") - runTest(t, program2, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + runTest(t, program2, autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] +-prov:index/test:Test: (replace) @@ -4335,10 +6243,6 @@ Resources: } ] + tests: output -Resources: - + 1 to create - +-1 to replace - 2 changes. 1 unchanged `)) }) }) @@ -4357,9 +6261,7 @@ Resources: t.Run("unknown plain property", func(t *testing.T) { program2 := fmt.Sprintf(program, "[{prop: \"${auxRes.auxes[0].prop}\"}]") - runTest(t, program2, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + runTest(t, program2, autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] +-prov:index/test:Test: (replace) @@ -4370,18 +6272,12 @@ Resources: ~ prop: "value" => output } ] -Resources: - + 1 to create - +-1 to replace - 2 changes. 1 unchanged `)) }) t.Run("unknown object", func(t *testing.T) { program2 := fmt.Sprintf(program, "[\"${auxRes.auxes[0]}\"]") - runTest(t, program2, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + runTest(t, program2, autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] +-prov:index/test:Test: (replace) @@ -4393,18 +6289,12 @@ Resources: } + [0]: output ] -Resources: - + 1 to create - +-1 to replace - 2 changes. 1 unchanged `)) }) t.Run("unknown collection", func(t *testing.T) { program2 := fmt.Sprintf(program, "\"${auxRes.auxes}\"") - runTest(t, program2, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + runTest(t, program2, autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] +-prov:index/test:Test: (replace) @@ -4416,10 +6306,6 @@ Resources: } ] + tests: output -Resources: - + 1 to create - +-1 to replace - 2 changes. 1 unchanged `)) }) }) @@ -4438,9 +6324,7 @@ Resources: t.Run("unknown plain property", func(t *testing.T) { program2 := fmt.Sprintf(program, "[{prop: \"${auxRes.auxes[0].prop}\"}]") - runTest(t, program2, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + runTest(t, program2, autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] ~ prov:index/test:Test: (update) @@ -4451,18 +6335,12 @@ Resources: ~ prop: "value" => output } ] -Resources: - + 1 to create - ~ 1 to update - 2 changes. 1 unchanged `)) }) t.Run("unknown object", func(t *testing.T) { program2 := fmt.Sprintf(program, "[\"${auxRes.auxes[0]}\"]") - runTest(t, program2, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + runTest(t, program2, autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] +-prov:index/test:Test: (replace) @@ -4474,18 +6352,12 @@ Resources: } + [0]: output ] -Resources: - + 1 to create - +-1 to replace - 2 changes. 1 unchanged `)) }) t.Run("unknown collection", func(t *testing.T) { program2 := fmt.Sprintf(program, "\"${auxRes.auxes}\"") - runTest(t, program2, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + runTest(t, program2, autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] +-prov:index/test:Test: (replace) @@ -4497,10 +6369,6 @@ Resources: } ] + tests: output -Resources: - + 1 to create - +-1 to replace - 2 changes. 1 unchanged `)) }) }) @@ -4519,9 +6387,7 @@ Resources: t.Run("unknown plain property", func(t *testing.T) { program2 := fmt.Sprintf(program, "[{prop: \"${auxRes.auxes[0].prop}\"}]") - runTest(t, program2, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + runTest(t, program2, autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] +-prov:index/test:Test: (replace) @@ -4532,18 +6398,12 @@ Resources: ~ prop: "value" => output } ] -Resources: - + 1 to create - +-1 to replace - 2 changes. 1 unchanged `)) }) t.Run("unknown object", func(t *testing.T) { program2 := fmt.Sprintf(program, "[\"${auxRes.auxes[0]}\"]") - runTest(t, program2, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + runTest(t, program2, autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] +-prov:index/test:Test: (replace) @@ -4555,18 +6415,12 @@ Resources: } + [0]: output ] -Resources: - + 1 to create - +-1 to replace - 2 changes. 1 unchanged `)) }) t.Run("unknown collection", func(t *testing.T) { program2 := fmt.Sprintf(program, "\"${auxRes.auxes}\"") - runTest(t, program2, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + runTest(t, program2, autogold.Expect(` + prov:index/aux:Aux: (create) [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] +-prov:index/test:Test: (replace) @@ -4578,10 +6432,6 @@ Resources: } ] + tests: output -Resources: - + 1 to create - +-1 to replace - 2 changes. 1 unchanged `)) }) }) diff --git a/pkg/tests/tfcheck/tfcheck.go b/pkg/tests/tfcheck/tfcheck.go index 08a93049f..bc4dd8104 100644 --- a/pkg/tests/tfcheck/tfcheck.go +++ b/pkg/tests/tfcheck/tfcheck.go @@ -32,6 +32,7 @@ type TfDriver struct { } type TfPlan struct { + StdOut string PlanFile string RawPlan any } @@ -123,13 +124,15 @@ func (d *TfDriver) Plan(t pulcheck.T) (*TfPlan, error) { planFile := filepath.Join(d.cwd, "test.tfplan") env := []string{d.formatReattachEnvVar()} tfCmd := getTFCommand() - _, err := execCmd(t, d.cwd, env, tfCmd, "plan", "-refresh=false", "-out", planFile) + cm, err := execCmd(t, d.cwd, env, tfCmd, "plan", "-refresh=false", "-out", planFile, "-no-color") if err != nil { return nil, err } + planStdout := cm.Stdout.(*bytes.Buffer).String() + planStdout = strings.Split(planStdout, "───")[0] // trim unstable output about the plan file cmd, err := execCmd(t, d.cwd, env, tfCmd, "show", "-json", planFile) require.NoError(t, err) - tp := TfPlan{PlanFile: planFile} + tp := TfPlan{PlanFile: planFile, StdOut: planStdout} err = json.Unmarshal(cmd.Stdout.(*bytes.Buffer).Bytes(), &tp.RawPlan) require.NoErrorf(t, err, "failed to unmarshal terraform plan") return &tp, nil diff --git a/pkg/tfbridge/detailed_diff.go b/pkg/tfbridge/detailed_diff.go index f58b7ec13..315ab7db2 100644 --- a/pkg/tfbridge/detailed_diff.go +++ b/pkg/tfbridge/detailed_diff.go @@ -3,6 +3,7 @@ package tfbridge import ( "cmp" "context" + "fmt" "slices" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" @@ -126,8 +127,11 @@ func makeBaseDiff(old, new resource.PropertyValue) baseDiff { type detailedDiffKey string type detailedDiffer struct { + ctx context.Context tfs shim.SchemaMap ps map[string]*SchemaInfo + // These are used to convert set indices back to something the engine can reference. + newInputs resource.PropertyMap } func (differ detailedDiffer) propertyPathToSchemaPath(path propertyPath) walk.SchemaPath { @@ -153,6 +157,54 @@ func (differ detailedDiffer) getEffectiveType(path walk.SchemaPath) shim.ValueTy return tfs.Type() } +type ( + setHash int + arrayIndex int +) + +type hashIndexMap map[setHash]arrayIndex + +func (differ detailedDiffer) calculateSetHashIndexMap( + path propertyPath, listVal []resource.PropertyValue, +) hashIndexMap { + identities := make(hashIndexMap) + + tfs, ps, err := lookupSchemas(path, differ.tfs, differ.ps) + if err != nil { + return nil + } + + convertedVal, err := makeSingleTerraformInput( + differ.ctx, path.String(), resource.NewArrayProperty(listVal), tfs, ps) + if err != nil { + return nil + } + + if convertedVal == nil { + return nil + } + + convertedListVal, ok := convertedVal.([]interface{}) + contract.Assertf(ok, "converted value should be a list") + + // Calculate the identity of each element. Note that the SetHash function can panic + // in the case of custom SetHash functions which get unexpected inputs. + for i, newElem := range convertedListVal { + elementHash := func() int { + defer func() { + if r := recover(); r != nil { + GetLogger(differ.ctx).Warn(fmt.Sprintf( + "Failed to calculate preview for element in %s: %v", + path.String(), r)) + } + }() + return tfs.SetHash(newElem) + }() + identities[setHash(elementHash)] = arrayIndex(i) + } + return identities +} + // makePlainPropDiff is used for plain properties and ones with an unknown schema. // It does not access the TF schema, so it does not know about the type of the property. func (differ detailedDiffer) makePlainPropDiff( @@ -228,8 +280,7 @@ func (differ detailedDiffer) makePropDiff( case shim.TypeList: return differ.makeListDiff(path, old, new) case shim.TypeSet: - // TODO[pulumi/pulumi-terraform-bridge#2200]: Implement set diffing - return differ.makeListDiff(path, old, new) + return differ.makeSetDiff(path, old, new) case shim.TypeMap: // Note that TF objects are represented as maps when returned by LookupSchemas return differ.makeMapDiff(path, old, new) @@ -267,6 +318,117 @@ func (differ detailedDiffer) makeListDiff( return diff } +func computeSetHashChanges( + oldIdentities, newIdentities hashIndexMap, +) (removed, added hashIndexMap) { + removed = hashIndexMap{} + added = hashIndexMap{} + + for elementHash := range oldIdentities { + if _, ok := newIdentities[elementHash]; !ok { + removed[elementHash] = oldIdentities[elementHash] + } + } + + for elementHash := range newIdentities { + if _, ok := oldIdentities[elementHash]; !ok { + added[elementHash] = newIdentities[elementHash] + } + } + + return +} + +func (differ detailedDiffer) matchNewIndicesToInputs( + path propertyPath, changedIdentities hashIndexMap, +) hashIndexMap { + matched := hashIndexMap{} + + newInputsList := []resource.PropertyValue{} + + newInputs, newInputsOk := path.GetFromMap(differ.newInputs) + if newInputsOk && isPresent(newInputs) && newInputs.IsArray() { + newInputsList = newInputs.ArrayValue() + } + + inputIdentities := hashIndexMap{} + + if !pathContainsComputed(path, differ.tfs, differ.ps) { + // The inputs are only safe to hash if the schema has no computed properties + inputIdentities = differ.calculateSetHashIndexMap(path, newInputsList) + } + + for elementHash, newStateIndex := range changedIdentities { + if inputIndex, ok := inputIdentities[elementHash]; ok { + matched[elementHash] = inputIndex + } else { + GetLogger(differ.ctx).Warn(fmt.Sprintf( + "Additional changes detected in %s, the displayed diff might be inaccurate", + path.String())) + matched[elementHash] = newStateIndex + } + } + + return matched +} + +type hashPair struct { + oldHash setHash + newHash setHash +} + +func buildChangesIndexMap(added, removed hashIndexMap) map[arrayIndex]hashPair { + changes := map[arrayIndex]hashPair{} + for hash, index := range added { + changes[index] = hashPair{oldHash: -1, newHash: hash} + } + for hash, index := range removed { + if el, ok := changes[index]; !ok { + changes[index] = hashPair{oldHash: hash, newHash: -1} + } else { + el.oldHash = hash + changes[index] = el + } + } + return changes +} + +func (differ detailedDiffer) makeSetDiff( + path propertyPath, old, new resource.PropertyValue, +) map[detailedDiffKey]*pulumirpc.PropertyDiff { + diff := make(map[detailedDiffKey]*pulumirpc.PropertyDiff) + oldList := old.ArrayValue() + newList := new.ArrayValue() + + oldIdentities := differ.calculateSetHashIndexMap(path, oldList) + newIdentities := differ.calculateSetHashIndexMap(path, newList) + + removed, added := computeSetHashChanges(oldIdentities, newIdentities) + + // We need to match the new indices to the inputs to ensure that the identity of the + // elements is preserved - this is necessary since the planning process can reorder + // the elements. + addedInputs := differ.matchNewIndicesToInputs(path, added) + + changes := buildChangesIndexMap(addedInputs, removed) + for index, hashes := range changes { + oldVal := resource.NewNullProperty() + if removedIndex, ok := removed[hashes.oldHash]; ok { + oldVal = oldList[removedIndex] + } + newVal := resource.NewNullProperty() + if addedIndex, ok := added[hashes.newHash]; ok { + newVal = newList[addedIndex] + } + + elemDiff := differ.makePropDiff(path.Index(int(index)), oldVal, newVal) + for subKey, subDiff := range elemDiff { + diff[subKey] = subDiff + } + } + return diff +} + func (differ detailedDiffer) makeMapDiff( path propertyPath, old, new resource.PropertyValue, ) map[detailedDiffKey]*pulumirpc.PropertyDiff { @@ -322,6 +484,7 @@ func makeDetailedDiffV2( diff shim.InstanceDiff, assets AssetTable, supportsSecrets bool, + newInputs resource.PropertyMap, ) (map[string]*pulumirpc.PropertyDiff, error) { // We need to compare the new and olds after all transformations have been applied. // ex. state upgrades, implementation-specific normalizations etc. @@ -343,6 +506,6 @@ func makeDetailedDiffV2( return nil, err } - differ := detailedDiffer{tfs: tfs, ps: ps} + differ := detailedDiffer{ctx: ctx, tfs: tfs, ps: ps, newInputs: newInputs} return differ.makeDetailedDiffPropertyMap(priorProps, props), nil } diff --git a/pkg/tfbridge/detailed_diff_test.go b/pkg/tfbridge/detailed_diff_test.go index 2dbda14ef..858946183 100644 --- a/pkg/tfbridge/detailed_diff_test.go +++ b/pkg/tfbridge/detailed_diff_test.go @@ -1,6 +1,9 @@ package tfbridge import ( + "bytes" + "context" + "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -13,6 +16,7 @@ import ( shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" shimschema "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/schema" shimv2 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/sdk-v2" + "github.com/pulumi/pulumi-terraform-bridge/v3/unstable/logging" ) func TestDiffPair(t *testing.T) { @@ -288,7 +292,7 @@ func runDetailedDiffTest( expected map[string]*pulumirpc.PropertyDiff, ) { t.Helper() - differ := detailedDiffer{tfs: tfs, ps: ps} + differ := detailedDiffer{tfs: tfs, ps: ps, newInputs: new} actual := differ.makeDetailedDiffPropertyMap(old, new) require.Equal(t, expected, actual) @@ -957,7 +961,7 @@ func TestDetailedDiffTFForceNewAttributeCollection(t *testing.T) { value1: []interface{}{"val1"}, value2: []interface{}{"val2"}, computedCollection: ComputedVal, - computedElem: []interface{}{ComputedVal}, + computedElem: nil, }, { name: "map", @@ -998,6 +1002,7 @@ func TestDetailedDiffTFForceNewAttributeCollection(t *testing.T) { "prop": tt.computedCollection, }, ) + propertyMapComputedElem := resource.NewPropertyMapFromMap( map[string]interface{}{ "prop": tt.computedElem, @@ -1033,23 +1038,26 @@ func TestDetailedDiffTFForceNewAttributeCollection(t *testing.T) { }) }) - t.Run("changed to computed elem", func(t *testing.T) { - runDetailedDiffTest(t, propertyMapListVal1, propertyMapComputedElem, tfs, ps, map[string]*pulumirpc.PropertyDiff{ - tt.elementIndex: {Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, - }) - }) - t.Run("changed from empty to computed collection", func(t *testing.T) { runDetailedDiffTest(t, propertyMapEmpty, propertyMapComputedCollection, tfs, ps, map[string]*pulumirpc.PropertyDiff{ "prop": {Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, }) }) - t.Run("changed from empty to computed elem", func(t *testing.T) { - runDetailedDiffTest(t, propertyMapEmpty, propertyMapComputedElem, tfs, ps, map[string]*pulumirpc.PropertyDiff{ - "prop": {Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + if tt.computedElem != nil { + + t.Run("changed to computed elem", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapListVal1, propertyMapComputedElem, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + tt.elementIndex: {Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, + }) }) - }) + + t.Run("changed from empty to computed elem", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapComputedElem, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "prop": {Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + }) + }) + } }) } } @@ -1748,6 +1756,576 @@ func TestDetailedDiffPulumiSchemaOverride(t *testing.T) { }) } +func TestDetailedDiffSetAttribute(t *testing.T) { + sdkv2Schema := map[string]*schema.Schema{ + "foo": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + } + ps, tfs := map[string]*info.Schema{}, shimv2.NewSchemaMap(sdkv2Schema) + + propertyMapElems := func(elems ...interface{}) resource.PropertyMap { + return resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": elems, + }, + ) + } + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapElems("val1"), propertyMapElems("val1"), tfs, ps, + map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1"), + propertyMapElems("val2"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems(), + propertyMapElems("val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("changed to empty", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1"), + propertyMapElems(), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("removed front", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val2", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("removed middle", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val1", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("removed end", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val1", "val2"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[2]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("added front", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val2", "val3"), + propertyMapElems("val1", "val2", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("added middle", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val3"), + propertyMapElems("val1", "val2", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("added end", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapElems("val1", "val2"), + propertyMapElems("val1", "val2", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[2]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("same element updated", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val1", "val4", "val3"), tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }, + ) + }) + + t.Run("shuffled", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val3", "val2", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("shuffled with duplicates", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val3", "val2", "val1", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("shuffled added front", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val2", "val3"), + propertyMapElems("val1", "val3", "val2"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("shuffled added middle", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val3"), + propertyMapElems("val3", "val2", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("shuffled added end", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2"), + propertyMapElems("val2", "val1", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[2]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("shuffled removed front", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val3", "val2"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("shuffled removed middle", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val3", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("shuffled removed end", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val2", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[2]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("computed", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1"), + resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": computedValue, + }, + ), + tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }, + ) + }) + + t.Run("nil to computed", func(t *testing.T) { + runDetailedDiffTest(t, + resource.NewPropertyMapFromMap( + map[string]interface{}{}, + ), + resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": computedValue, + }, + ), + tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_ADD}, + }, + ) + }) + + t.Run("empty to computed", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems(), + resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": computedValue, + }, + ), + tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }, + ) + }) + + t.Run("two added, two removed", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2"), + propertyMapElems("val3", "val4"), + tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_UPDATE}, + "foo[1]": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }, + ) + }) + + t.Run("two added, two removed, shuffled", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("stable1", "stable2", "val1", "val2"), + propertyMapElems("val4", "val3", "stable1", "stable2"), + tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_ADD}, + "foo[1]": {Kind: pulumirpc.PropertyDiff_ADD}, + "foo[2]": {Kind: pulumirpc.PropertyDiff_DELETE}, + "foo[3]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }, + ) + }) +} + +func TestDetailedDiffSetBlock(t *testing.T) { + propertyMapElems := func(elems ...string) resource.PropertyMap { + var elemMaps []map[string]interface{} + for _, elem := range elems { + elemMaps = append(elemMaps, map[string]interface{}{"bar": elem}) + } + return resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": elemMaps, + }, + ) + } + + for _, forceNew := range []bool{false, true} { + sdkv2Schema := map[string]*schema.Schema{ + "foo": { + Type: schema.TypeSet, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "bar": { + Type: schema.TypeString, + Optional: true, + ForceNew: forceNew, + }, + }, + }, + }, + } + ps, tfs := map[string]*info.Schema{}, shimv2.NewSchemaMap(sdkv2Schema) + t.Run(fmt.Sprintf("forceNew=%v", forceNew), func(t *testing.T) { + update := pulumirpc.PropertyDiff_UPDATE + add := pulumirpc.PropertyDiff_ADD + delete := pulumirpc.PropertyDiff_DELETE + if forceNew { + update = pulumirpc.PropertyDiff_UPDATE_REPLACE + add = pulumirpc.PropertyDiff_ADD_REPLACE + delete = pulumirpc.PropertyDiff_DELETE_REPLACE + } + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapElems("val1"), propertyMapElems("val1"), tfs, ps, + map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1"), + propertyMapElems("val2"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0].bar": {Kind: update}, + }, + ) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems(), + propertyMapElems("val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: add}, + }, + ) + }) + + t.Run("changed to empty", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1"), + propertyMapElems(), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: delete}, + }, + ) + }) + + t.Run("removed front", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val2", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: delete}, + }, + ) + }) + + t.Run("removed middle", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val1", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: delete}, + }, + ) + }) + + t.Run("removed end", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val1", "val2"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[2]": {Kind: delete}, + }, + ) + }) + + t.Run("added front", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val2", "val3"), + propertyMapElems("val1", "val2", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: add}, + }, + ) + }) + + t.Run("added middle", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val3"), + propertyMapElems("val1", "val2", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: add}, + }, + ) + }) + + t.Run("added end", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2"), + propertyMapElems("val1", "val2", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[2]": {Kind: add}, + }, + ) + }) + + t.Run("same element updated", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val1", "val4", "val3"), tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo[1].bar": {Kind: update}, + }, + ) + }) + + t.Run("shuffled", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val3", "val2", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("shuffled with duplicates", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val3", "val2", "val1", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("shuffled added front", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val2", "val3"), + propertyMapElems("val1", "val3", "val2"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: add}, + }, + ) + }) + + t.Run("shuffled added middle", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val3"), + propertyMapElems("val3", "val2", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: add}, + }, + ) + }) + + t.Run("shuffled added end", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2"), + propertyMapElems("val2", "val1", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[2]": {Kind: add}, + }, + ) + }) + + t.Run("shuffled removed front", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val3", "val2"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: delete}, + }, + ) + }) + + t.Run("shuffled removed middle", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val3", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: delete}, + }, + ) + }) + + t.Run("shuffled removed end", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val2", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[2]": {Kind: delete}, + }, + ) + }) + + t.Run("computed", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1"), + resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": computedValue, + }, + ), + tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: update}, + }, + ) + }) + + t.Run("nil to computed", func(t *testing.T) { + runDetailedDiffTest(t, + resource.NewPropertyMapFromMap( + map[string]interface{}{}, + ), + resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": computedValue, + }, + ), + tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_ADD}, + }, + ) + }) + + t.Run("empty to computed", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems(), + resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": computedValue, + }, + ), + tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }, + ) + }) + }) + } +} + +func TestDetailedDiffSetBlockNestedMaxItemsOne(t *testing.T) { + customResponseSchema := func() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "custom_response_body_key": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + } + } + blockConfigSchema := func() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "custom_response": customResponseSchema(), + }, + }, + } + } + ruleElement := &schema.Resource{ + Schema: map[string]*schema.Schema{ + "action": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "block": blockConfigSchema(), + }, + }, + }, + }, + } + + schMap := map[string]*schema.Schema{ + "rule": { + Type: schema.TypeSet, + Optional: true, + Elem: ruleElement, + }, + } + + ps, tfs := map[string]*info.Schema{}, shimv2.NewSchemaMap(schMap) + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, resource.NewPropertyMapFromMap(map[string]interface{}{ + "rule": []map[string]interface{}{ + { + "action": map[string]interface{}{ + "block": map[string]interface{}{ + "custom_response": map[string]interface{}{ + "custom_response_body_key": "val1", + }, + }, + }, + }, + }, + }), resource.NewPropertyMapFromMap(map[string]interface{}{ + "rule": []map[string]interface{}{ + { + "action": map[string]interface{}{ + "block": map[string]interface{}{ + "custom_response": map[string]interface{}{ + "custom_response_body_key": "val1", + }, + }, + }, + }, + }, + }), tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) +} + func TestDetailedDiffMismatchedSchemas(t *testing.T) { stringSchema := map[string]*schema.Schema{ "foo": { @@ -1860,3 +2438,174 @@ func TestDetailedDiffMismatchedSchemas(t *testing.T) { }) }) } + +func TestDetailedDiffSetHashChanges(t *testing.T) { + runTest := func(old, new hashIndexMap, expectedRemoved, expectedAdded hashIndexMap) { + t.Helper() + removed, added := computeSetHashChanges(old, new) + + require.Equal(t, removed, expectedRemoved) + require.Equal(t, added, expectedAdded) + } + + runTest(hashIndexMap{}, hashIndexMap{}, hashIndexMap{}, hashIndexMap{}) + runTest(hashIndexMap{1: 1}, hashIndexMap{1: 1}, hashIndexMap{}, hashIndexMap{}) + runTest(hashIndexMap{1: 1}, hashIndexMap{}, hashIndexMap{1: 1}, hashIndexMap{}) + runTest(hashIndexMap{1: 1}, hashIndexMap{2: 2}, hashIndexMap{1: 1}, hashIndexMap{2: 2}) +} + +func TestDetailedDiffMatchNewIndicesToInputs(t *testing.T) { + tfs := shimv2.NewSchemaMap(map[string]*schema.Schema{ + "foo": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }) + + getHash := func(element string) setHash { + return setHash(tfs.Get("foo").SetHash(element)) + } + + runTest := func( + newInputs []resource.PropertyValue, changes hashIndexMap, expected hashIndexMap, logBuf *bytes.Buffer, + ) { + t.Helper() + ctx := logging.InitLogging(context.Background(), logging.LogOptions{ + LogSink: &testLogSink{buf: logBuf}, + }) + inputs := resource.NewPropertyMapFromMap(map[string]interface{}{ + "foo": newInputs, + }) + differ := detailedDiffer{ + ctx: ctx, + tfs: tfs, + ps: nil, + newInputs: inputs, + } + matched := differ.matchNewIndicesToInputs(newPropertyPath("foo"), changes) + require.Equal(t, matched, expected) + } + + t.Run("single element", func(t *testing.T) { + logBuf := &bytes.Buffer{} + runTest( + []resource.PropertyValue{resource.NewStringProperty("val1")}, + hashIndexMap{getHash("val1"): 0}, + hashIndexMap{getHash("val1"): 0}, + logBuf, + ) + require.Empty(t, logBuf.String()) + }) + + t.Run("single element, doesn't match", func(t *testing.T) { + logBuf := &bytes.Buffer{} + runTest( + []resource.PropertyValue{resource.NewStringProperty("val1")}, + hashIndexMap{getHash("val2"): 0}, + hashIndexMap{getHash("val2"): 0}, + logBuf, + ) + require.Contains(t, logBuf.String(), "Additional changes detected in foo") + }) + + t.Run("two elements, one changed", func(t *testing.T) { + logBuf := &bytes.Buffer{} + runTest( + []resource.PropertyValue{resource.NewStringProperty("val1"), resource.NewStringProperty("val2")}, + hashIndexMap{getHash("val2"): 1}, + hashIndexMap{getHash("val2"): 1}, + logBuf, + ) + require.Empty(t, logBuf.String()) + }) + + t.Run("two elements, both changed", func(t *testing.T) { + logBuf := &bytes.Buffer{} + runTest( + []resource.PropertyValue{resource.NewStringProperty("val1"), resource.NewStringProperty("val2")}, + hashIndexMap{getHash("val1"): 0, getHash("val2"): 1}, + hashIndexMap{getHash("val1"): 0, getHash("val2"): 1}, + logBuf, + ) + require.Empty(t, logBuf.String()) + }) + + t.Run("two elements, one changed, one doesn't match", func(t *testing.T) { + logBuf := &bytes.Buffer{} + runTest( + []resource.PropertyValue{resource.NewStringProperty("val1"), resource.NewStringProperty("val2")}, + hashIndexMap{getHash("val1"): 0, getHash("val3"): 1}, + hashIndexMap{getHash("val1"): 0, getHash("val3"): 1}, + logBuf, + ) + require.Contains(t, logBuf.String(), "Additional changes detected in foo") + }) +} + +func TestDetailedDiffBuildChangesIndexMap(t *testing.T) { + runTest := func(added, removed hashIndexMap, expected map[arrayIndex]hashPair) { + t.Helper() + changes := buildChangesIndexMap(added, removed) + require.Equal(t, expected, changes) + } + + t.Run("empty", func(t *testing.T) { + runTest(hashIndexMap{}, hashIndexMap{}, map[arrayIndex]hashPair{}) + }) + t.Run("one added", func(t *testing.T) { + runTest(hashIndexMap{1: 0}, hashIndexMap{}, map[arrayIndex]hashPair{ + 0: {oldHash: -1, newHash: 1}, + }) + }) + t.Run("one removed", func(t *testing.T) { + runTest(hashIndexMap{}, hashIndexMap{1: 0}, map[arrayIndex]hashPair{ + 0: {oldHash: 1, newHash: -1}, + }) + }) + t.Run("one added, one removed, different indices", func(t *testing.T) { + runTest(hashIndexMap{1: 0}, hashIndexMap{2: 1}, map[arrayIndex]hashPair{ + 0: {oldHash: -1, newHash: 1}, + 1: {oldHash: 2, newHash: -1}, + }) + }) + + t.Run("one added, one removed, same indices", func(t *testing.T) { + runTest(hashIndexMap{1: 0}, hashIndexMap{2: 0}, map[arrayIndex]hashPair{ + 0: {oldHash: 2, newHash: 1}, + }) + }) +} + +func TestDetailedDiffSetHashPanicCaught(t *testing.T) { + tfs := shimv2.NewSchemaMap(map[string]*schema.Schema{ + "foo": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Set: func(v interface{}) int { + panic("test") + }, + }, + }) + + buf := &bytes.Buffer{} + ctx := logging.InitLogging(context.Background(), logging.LogOptions{ + LogSink: &testLogSink{buf: buf}, + }) + + differ := detailedDiffer{ + ctx: ctx, + tfs: tfs, + ps: nil, + } + + differ.calculateSetHashIndexMap( + newPropertyPath("foo"), + []resource.PropertyValue{resource.NewStringProperty("val1")}, + ) + + require.Contains(t, buf.String(), "Failed to calculate preview for element in foo") +} diff --git a/pkg/tfbridge/property_path.go b/pkg/tfbridge/property_path.go index 4c88323c9..c3d7943a9 100644 --- a/pkg/tfbridge/property_path.go +++ b/pkg/tfbridge/property_path.go @@ -6,6 +6,7 @@ import ( "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info" shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/walk" "github.com/pulumi/pulumi-terraform-bridge/v3/unstable/propertyvalue" ) @@ -45,6 +46,11 @@ func (k propertyPath) IsReservedKey() bool { return leaf == "__meta" || leaf == "__defaults" } +func (k propertyPath) GetFromMap(v resource.PropertyMap) (resource.PropertyValue, bool) { + path := resource.PropertyPath(k) + return path.Get(resource.NewProperty(v)) +} + func lookupSchemas( path propertyPath, tfs shim.SchemaMap, ps map[string]*info.Schema, ) (shim.Schema, *info.Schema, error) { @@ -105,3 +111,23 @@ func propertyValueTriggersReplacement( return replacement } + +// pathContainsComputed returns true if the schema contains a Computed property at a path prefixed by path. +func pathContainsComputed( + path propertyPath, rootTFSchema shim.SchemaMap, rootPulumiSchema map[string]*info.Schema, +) bool { + tfs, _, err := lookupSchemas(path, rootTFSchema, rootPulumiSchema) + if err != nil { + return false + } + + computed := false + visitor := func(path walk.SchemaPath, tfs shim.Schema) { + if tfs.Computed() { + computed = true + } + } + walk.VisitSchema(tfs, visitor) + + return computed +} diff --git a/pkg/tfbridge/provider.go b/pkg/tfbridge/provider.go index 5ed4eecef..a9d50155f 100644 --- a/pkg/tfbridge/provider.go +++ b/pkg/tfbridge/provider.go @@ -1169,7 +1169,8 @@ func (p *Provider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*pulum changes = pulumirpc.DiffResponse_DIFF_SOME } - detailedDiff, err = makeDetailedDiffV2(ctx, schema, fields, res.TF, p.tf, state, diff, assets, p.supportsSecrets) + detailedDiff, err = makeDetailedDiffV2( + ctx, schema, fields, res.TF, p.tf, state, diff, assets, p.supportsSecrets, news) if err != nil { return nil, err } diff --git a/pkg/tfbridge/schema.go b/pkg/tfbridge/schema.go index 4018c0b12..2f3738f46 100644 --- a/pkg/tfbridge/schema.go +++ b/pkg/tfbridge/schema.go @@ -341,6 +341,24 @@ func MakeTerraformInputs( return makeTerraformInputsWithOptions(ctx, instance, config, olds, news, tfs, ps, makeTerraformInputsOptions{}) } +// makeSingleTerraformInput converts a single Pulumi property value into a plain go value suitable for use by Terraform. +// makeSingleTerraformInput does not apply any defaults or other transformations. +func makeSingleTerraformInput( + ctx context.Context, name string, val resource.PropertyValue, tfs shim.Schema, ps *SchemaInfo, +) (interface{}, error) { + cctx := &conversionContext{ + Ctx: ctx, + ComputeDefaultOptions: ComputeDefaultOptions{}, + ProviderConfig: nil, + ApplyDefaults: false, + ApplyTFDefaults: false, + Assets: AssetTable{}, + UnknownCollectionsSupported: false, + } + + return cctx.makeTerraformInput(name, resource.NewNullProperty(), val, tfs, ps) +} + // makeTerraformInput takes a single property plus custom schema info and does whatever is necessary // to prepare it for use by Terraform. Note that this function may have side effects, for instance // if it is necessary to spill an asset to disk in order to create a name out of it. Please take diff --git a/pkg/tfbridge/schema_test.go b/pkg/tfbridge/schema_test.go index 9cc780c2f..1146706af 100644 --- a/pkg/tfbridge/schema_test.go +++ b/pkg/tfbridge/schema_test.go @@ -3759,3 +3759,128 @@ func TestExtractInputsFromOutputsSdkv2(t *testing.T) { } } + +func TestMakeSingleTerraformInput(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + prop resource.PropertyValue + schema *schemav2.Schema + expected interface{} + } + + testCases := []testCase{ + { + name: "bool", + prop: resource.NewBoolProperty(true), + schema: &schemav2.Schema{ + Type: schemav2.TypeBool, + Optional: true, + }, + expected: true, + }, + { + name: "number", + prop: resource.NewNumberProperty(42), + schema: &schemav2.Schema{ + Type: schemav2.TypeInt, + Optional: true, + }, + expected: 42, + }, + { + name: "string", + prop: resource.NewStringProperty("foo"), + schema: &schemav2.Schema{ + Type: schemav2.TypeString, + Optional: true, + }, + expected: "foo", + }, + { + name: "array", + prop: resource.NewArrayProperty([]resource.PropertyValue{ + resource.NewStringProperty("foo"), + }), + schema: &schemav2.Schema{ + Type: schemav2.TypeList, + Optional: true, + Elem: &schema.Schema{Type: shim.TypeString}, + }, + expected: []interface{}{"foo"}, + }, + { + name: "map", + prop: resource.NewObjectProperty(resource.PropertyMap{ + "foo": resource.NewStringProperty("bar"), + }), + schema: &schemav2.Schema{ + Type: schemav2.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: shim.TypeString}, + }, + expected: map[string]interface{}{"foo": "bar"}, + }, + { + name: "object", + prop: resource.NewObjectProperty(resource.PropertyMap{ + "foo": resource.NewStringProperty("bar"), + }), + schema: &schemav2.Schema{ + Type: schemav2.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: schema.SchemaMap{ + "foo": (&schema.Schema{Type: shim.TypeString, Optional: true}).Shim(), + }, + }, + }, + expected: []interface{}{map[string]interface{}{"foo": "bar"}}, + }, + { + name: "nested object", + prop: resource.NewObjectProperty(resource.PropertyMap{ + "foo": resource.NewObjectProperty(resource.PropertyMap{ + "bar": resource.NewStringProperty("baz"), + }), + }), + schema: &schemav2.Schema{ + Type: schemav2.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schemav2.Resource{ + Schema: map[string]*schemav2.Schema{ + "foo": { + Type: schemav2.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schemav2.Resource{ + Schema: map[string]*schemav2.Schema{ + "bar": {Type: schemav2.TypeString, Optional: true}, + }, + }, + }, + }, + }, + }, + expected: []interface{}{map[string]interface{}{ + "foo": []interface{}{map[string]interface{}{ + "bar": "baz", + }}, + }}, + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + result, err := makeSingleTerraformInput(context.Background(), "name", tc.prop, shimv2.NewSchema(tc.schema), nil) + require.NoError(t, err) + assert.Equal(t, tc.expected, result) + }) + } +}