github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/engine/lifecycletest/delete_before_replace_test.go (about)

     1  //nolint:goconst
     2  package lifecycletest
     3  
     4  import (
     5  	"testing"
     6  
     7  	"github.com/blang/semver"
     8  	"github.com/stretchr/testify/assert"
     9  
    10  	. "github.com/pulumi/pulumi/pkg/v3/engine"
    11  	"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
    12  	"github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest"
    13  	"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
    14  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
    15  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
    16  	"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
    17  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/result"
    18  	"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
    19  )
    20  
    21  type propertyDependencies map[resource.PropertyKey][]resource.URN
    22  
    23  var complexTestDependencyGraphNames = []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"}
    24  
    25  func generateComplexTestDependencyGraph(
    26  	t *testing.T, p *TestPlan) ([]resource.URN, *deploy.Snapshot, plugin.LanguageRuntime) {
    27  
    28  	resType := tokens.Type("pkgA:m:typA")
    29  
    30  	names := complexTestDependencyGraphNames
    31  
    32  	urnA := p.NewProviderURN("pkgA", names[0], "")
    33  	urnB := p.NewURN(resType, names[1], "")
    34  	urnC := p.NewProviderURN("pkgA", names[2], "")
    35  	urnD := p.NewProviderURN("pkgA", names[3], "")
    36  	urnE := p.NewURN(resType, names[4], "")
    37  	urnF := p.NewURN(resType, names[5], "")
    38  	urnG := p.NewURN(resType, names[6], "")
    39  	urnH := p.NewURN(resType, names[7], "")
    40  	urnI := p.NewURN(resType, names[8], "")
    41  	urnJ := p.NewURN(resType, names[9], "")
    42  	urnK := p.NewURN(resType, names[10], "")
    43  	urnL := p.NewURN(resType, names[11], "")
    44  
    45  	urns := []resource.URN{
    46  		urnA, urnB, urnC, urnD, urnE, urnF,
    47  		urnG, urnH, urnI, urnJ, urnK, urnL,
    48  	}
    49  
    50  	newResource := func(urn resource.URN, id resource.ID, provider string, dependencies []resource.URN,
    51  		propertyDeps propertyDependencies, outputs resource.PropertyMap) *resource.State {
    52  		return newResource(urn, "", id, provider, dependencies, propertyDeps, outputs, true)
    53  	}
    54  
    55  	old := &deploy.Snapshot{
    56  		Resources: []*resource.State{
    57  			newResource(urnA, "0", "", nil, nil, resource.PropertyMap{"A": resource.NewStringProperty("foo")}),
    58  			newResource(urnB, "1", string(urnA)+"::0", nil, nil, nil),
    59  			newResource(urnC, "2", "",
    60  				[]resource.URN{urnA},
    61  				propertyDependencies{"A": []resource.URN{urnA}},
    62  				resource.PropertyMap{"A": resource.NewStringProperty("bar")}),
    63  			newResource(urnD, "3", "",
    64  				[]resource.URN{urnA},
    65  				propertyDependencies{"B": []resource.URN{urnA}}, nil),
    66  			newResource(urnE, "4", string(urnC)+"::2", nil, nil, nil),
    67  			newResource(urnF, "5", "",
    68  				[]resource.URN{urnC},
    69  				propertyDependencies{"A": []resource.URN{urnC}}, nil),
    70  			newResource(urnG, "6", "",
    71  				[]resource.URN{urnC},
    72  				propertyDependencies{"B": []resource.URN{urnC}}, nil),
    73  			newResource(urnH, "4", string(urnD)+"::3", nil, nil, nil),
    74  			newResource(urnI, "5", "",
    75  				[]resource.URN{urnD},
    76  				propertyDependencies{"A": []resource.URN{urnD}}, nil),
    77  			newResource(urnJ, "6", "",
    78  				[]resource.URN{urnD},
    79  				propertyDependencies{"B": []resource.URN{urnD}}, nil),
    80  			newResource(urnK, "7", "",
    81  				[]resource.URN{urnF, urnG},
    82  				propertyDependencies{"A": []resource.URN{urnF, urnG}}, nil),
    83  			newResource(urnL, "8", "",
    84  				[]resource.URN{urnF, urnG},
    85  				propertyDependencies{"B": []resource.URN{urnF, urnG}}, nil),
    86  		},
    87  	}
    88  
    89  	program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
    90  		register := func(urn resource.URN, provider string, inputs resource.PropertyMap) resource.ID {
    91  			_, id, _, err := monitor.RegisterResource(urn.Type(), string(urn.Name()), true, deploytest.ResourceOptions{
    92  				Provider: provider,
    93  				Inputs:   inputs,
    94  			})
    95  			assert.NoError(t, err)
    96  			return id
    97  		}
    98  
    99  		idA := register(urnA, "", resource.PropertyMap{"A": resource.NewStringProperty("bar")})
   100  		register(urnB, string(urnA)+"::"+string(idA), nil)
   101  		idC := register(urnC, "", nil)
   102  		idD := register(urnD, "", nil)
   103  		register(urnE, string(urnC)+"::"+string(idC), nil)
   104  		register(urnF, "", nil)
   105  		register(urnG, "", nil)
   106  		register(urnH, string(urnD)+"::"+string(idD), nil)
   107  		register(urnI, "", nil)
   108  		register(urnJ, "", nil)
   109  		register(urnK, "", nil)
   110  		register(urnL, "", nil)
   111  
   112  		return nil
   113  	})
   114  
   115  	return urns, old, program
   116  }
   117  
   118  func TestDeleteBeforeReplace(t *testing.T) {
   119  	t.Parallel()
   120  
   121  	//             A
   122  	//    _________|_________
   123  	//    B        C        D
   124  	//          ___|___  ___|___
   125  	//          E  F  G  H  I  J
   126  	//             |__|
   127  	//             K  L
   128  	//
   129  	// For a given resource R in (A, C, D):
   130  	// - R will be the provider for its first dependent
   131  	// - A change to R will require that its second dependent be replaced
   132  	// - A change to R will not require that its third dependent be replaced
   133  	//
   134  	// In addition, K will have a requires-replacement property that depends on both F and G, and
   135  	// L will have a normal property that depends on both F and G.
   136  	//
   137  	// With that in mind, the following resources should require replacement: A, B, C, E, F, and K
   138  
   139  	p := &TestPlan{}
   140  
   141  	urns, old, program := generateComplexTestDependencyGraph(t, p)
   142  	names := complexTestDependencyGraphNames
   143  
   144  	loaders := []*deploytest.ProviderLoader{
   145  		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
   146  			return &deploytest.Provider{
   147  				DiffConfigF: func(urn resource.URN, olds, news resource.PropertyMap,
   148  					ignoreChanges []string) (plugin.DiffResult, error) {
   149  					if !olds["A"].DeepEquals(news["A"]) {
   150  						return plugin.DiffResult{
   151  							ReplaceKeys:         []resource.PropertyKey{"A"},
   152  							DeleteBeforeReplace: true,
   153  						}, nil
   154  					}
   155  					return plugin.DiffResult{}, nil
   156  				},
   157  				DiffF: func(urn resource.URN, id resource.ID,
   158  					olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
   159  
   160  					if !olds["A"].DeepEquals(news["A"]) {
   161  						return plugin.DiffResult{ReplaceKeys: []resource.PropertyKey{"A"}}, nil
   162  					}
   163  					return plugin.DiffResult{}, nil
   164  				},
   165  			}, nil
   166  		}),
   167  	}
   168  
   169  	p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...)
   170  
   171  	p.Steps = []TestStep{{
   172  		Op:            Update,
   173  		ExpectFailure: false,
   174  		SkipPreview:   true,
   175  		Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
   176  			evts []Event, res result.Result) result.Result {
   177  
   178  			assert.Nil(t, res)
   179  
   180  			replaced := make(map[resource.URN]bool)
   181  			for _, entry := range entries {
   182  				if entry.Step.Op() == deploy.OpReplace {
   183  					replaced[entry.Step.URN()] = true
   184  				}
   185  			}
   186  
   187  			assert.Equal(t, map[resource.URN]bool{
   188  				pickURN(t, urns, names, "A"): true,
   189  				pickURN(t, urns, names, "B"): true,
   190  				pickURN(t, urns, names, "C"): true,
   191  				pickURN(t, urns, names, "E"): true,
   192  				pickURN(t, urns, names, "F"): true,
   193  				pickURN(t, urns, names, "K"): true,
   194  			}, replaced)
   195  
   196  			return res
   197  		},
   198  	}}
   199  
   200  	p.Run(t, old)
   201  }
   202  
   203  func TestPropertyDependenciesAdapter(t *testing.T) {
   204  	t.Parallel()
   205  	// Ensure that the eval source properly shims in property dependencies if none were reported (and does not if
   206  	// any were reported).
   207  
   208  	type propertyDependencies map[resource.PropertyKey][]resource.URN
   209  
   210  	loaders := []*deploytest.ProviderLoader{
   211  		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
   212  			return &deploytest.Provider{}, nil
   213  		}),
   214  	}
   215  
   216  	const resType = "pkgA:m:typA"
   217  	var urnA, urnB, urnC, urnD resource.URN
   218  	program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
   219  
   220  		register := func(name string, inputs resource.PropertyMap, inputDeps propertyDependencies,
   221  			dependencies []resource.URN) resource.URN {
   222  
   223  			urn, _, _, err := monitor.RegisterResource(resType, name, true, deploytest.ResourceOptions{
   224  				Inputs:       inputs,
   225  				Dependencies: dependencies,
   226  				PropertyDeps: inputDeps,
   227  			})
   228  			assert.NoError(t, err)
   229  
   230  			return urn
   231  		}
   232  
   233  		urnA = register("A", nil, nil, nil)
   234  		urnB = register("B", nil, nil, nil)
   235  		urnC = register("C", resource.PropertyMap{
   236  			"A": resource.NewStringProperty("foo"),
   237  			"B": resource.NewStringProperty("bar"),
   238  		}, nil, []resource.URN{urnA, urnB})
   239  		urnD = register("D", resource.PropertyMap{
   240  			"A": resource.NewStringProperty("foo"),
   241  			"B": resource.NewStringProperty("bar"),
   242  		}, propertyDependencies{
   243  			"A": []resource.URN{urnB},
   244  			"B": []resource.URN{urnA, urnC},
   245  		}, []resource.URN{urnA, urnB, urnC})
   246  
   247  		return nil
   248  	})
   249  
   250  	host := deploytest.NewPluginHost(nil, nil, program, loaders...)
   251  	p := &TestPlan{
   252  		Options: UpdateOptions{Host: host},
   253  		Steps:   []TestStep{{Op: Update}},
   254  	}
   255  	snap := p.Run(t, nil)
   256  	for _, res := range snap.Resources {
   257  		switch res.URN {
   258  		case urnA, urnB:
   259  			assert.Empty(t, res.Dependencies)
   260  			assert.Empty(t, res.PropertyDependencies)
   261  		case urnC:
   262  			assert.Equal(t, []resource.URN{urnA, urnB}, res.Dependencies)
   263  			assert.EqualValues(t, propertyDependencies{
   264  				"A": res.Dependencies,
   265  				"B": res.Dependencies,
   266  			}, res.PropertyDependencies)
   267  		case urnD:
   268  			assert.Equal(t, []resource.URN{urnA, urnB, urnC}, res.Dependencies)
   269  			assert.EqualValues(t, propertyDependencies{
   270  				"A": []resource.URN{urnB},
   271  				"B": []resource.URN{urnA, urnC},
   272  			}, res.PropertyDependencies)
   273  		}
   274  	}
   275  }
   276  
   277  func TestExplicitDeleteBeforeReplace(t *testing.T) {
   278  	t.Parallel()
   279  
   280  	p := &TestPlan{}
   281  
   282  	dbrDiff := false
   283  	loaders := []*deploytest.ProviderLoader{
   284  		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
   285  			return &deploytest.Provider{
   286  				DiffF: func(urn resource.URN, id resource.ID,
   287  					olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
   288  
   289  					if !olds["A"].DeepEquals(news["A"]) {
   290  						return plugin.DiffResult{
   291  							ReplaceKeys:         []resource.PropertyKey{"A"},
   292  							DeleteBeforeReplace: dbrDiff,
   293  						}, nil
   294  					}
   295  					return plugin.DiffResult{}, nil
   296  				},
   297  			}, nil
   298  		}),
   299  	}
   300  
   301  	const resType = "pkgA:index:typ"
   302  
   303  	inputsA := resource.NewPropertyMapFromMap(map[string]interface{}{"A": "foo"})
   304  	dbrValue, dbrA := true, (*bool)(nil)
   305  	inputsB := resource.NewPropertyMapFromMap(map[string]interface{}{"A": "foo"})
   306  
   307  	var provURN, urnA, urnB resource.URN
   308  	var provID resource.ID
   309  	var err error
   310  	program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
   311  		provURN, provID, _, err = monitor.RegisterResource(providers.MakeProviderType("pkgA"), "provA", true)
   312  		assert.NoError(t, err)
   313  
   314  		if provID == "" {
   315  			provID = providers.UnknownID
   316  		}
   317  		provRef, err := providers.NewReference(provURN, provID)
   318  		assert.NoError(t, err)
   319  		provA := provRef.String()
   320  
   321  		urnA, _, _, err = monitor.RegisterResource(resType, "resA", true, deploytest.ResourceOptions{
   322  			Provider:            provA,
   323  			Inputs:              inputsA,
   324  			DeleteBeforeReplace: dbrA,
   325  		})
   326  		assert.NoError(t, err)
   327  
   328  		inputDepsB := map[resource.PropertyKey][]resource.URN{"A": {urnA}}
   329  		urnB, _, _, err = monitor.RegisterResource(resType, "resB", true, deploytest.ResourceOptions{
   330  			Provider:     provA,
   331  			Inputs:       inputsB,
   332  			Dependencies: []resource.URN{urnA},
   333  			PropertyDeps: inputDepsB,
   334  		})
   335  		assert.NoError(t, err)
   336  
   337  		return nil
   338  	})
   339  
   340  	p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...)
   341  	p.Steps = []TestStep{{Op: Update}}
   342  	snap := p.Run(t, nil)
   343  
   344  	// Change the value of resA.A. Only resA should be replaced, and the replacement should be create-before-delete.
   345  	inputsA["A"] = resource.NewStringProperty("bar")
   346  	p.Steps = []TestStep{{
   347  		Op: Update,
   348  
   349  		Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
   350  			evts []Event, res result.Result) result.Result {
   351  
   352  			assert.Nil(t, res)
   353  
   354  			AssertSameSteps(t, []StepSummary{
   355  				{Op: deploy.OpSame, URN: provURN},
   356  				{Op: deploy.OpCreateReplacement, URN: urnA},
   357  				{Op: deploy.OpReplace, URN: urnA},
   358  				{Op: deploy.OpSame, URN: urnB},
   359  				{Op: deploy.OpDeleteReplaced, URN: urnA},
   360  			}, SuccessfulSteps(entries))
   361  
   362  			return res
   363  		},
   364  	}}
   365  	snap = p.Run(t, snap)
   366  
   367  	// Change the registration of resA such that it requires delete-before-replace and change the value of resA.A. Both
   368  	// resA and resB should be replaced, and the replacements should be delete-before-replace.
   369  	dbrA, inputsA["A"] = &dbrValue, resource.NewStringProperty("baz")
   370  	p.Steps = []TestStep{{
   371  		Op: Update,
   372  
   373  		Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
   374  			evts []Event, res result.Result) result.Result {
   375  
   376  			assert.Nil(t, res)
   377  
   378  			AssertSameSteps(t, []StepSummary{
   379  				{Op: deploy.OpSame, URN: provURN},
   380  				{Op: deploy.OpDeleteReplaced, URN: urnB},
   381  				{Op: deploy.OpDeleteReplaced, URN: urnA},
   382  				{Op: deploy.OpReplace, URN: urnA},
   383  				{Op: deploy.OpCreateReplacement, URN: urnA},
   384  				{Op: deploy.OpReplace, URN: urnB},
   385  				{Op: deploy.OpCreateReplacement, URN: urnB},
   386  			}, SuccessfulSteps(entries))
   387  
   388  			return res
   389  		},
   390  	}}
   391  	snap = p.Run(t, snap)
   392  
   393  	// Change the value of resB.A. Only resB should be replaced, and the replacement should be create-before-delete.
   394  	inputsB["A"] = resource.NewStringProperty("qux")
   395  	p.Steps = []TestStep{{
   396  		Op: Update,
   397  
   398  		Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
   399  			evts []Event, res result.Result) result.Result {
   400  
   401  			assert.Nil(t, res)
   402  
   403  			AssertSameSteps(t, []StepSummary{
   404  				{Op: deploy.OpSame, URN: provURN},
   405  				{Op: deploy.OpSame, URN: urnA},
   406  				{Op: deploy.OpCreateReplacement, URN: urnB},
   407  				{Op: deploy.OpReplace, URN: urnB},
   408  				{Op: deploy.OpDeleteReplaced, URN: urnB},
   409  			}, SuccessfulSteps(entries))
   410  
   411  			return res
   412  		},
   413  	}}
   414  	snap = p.Run(t, snap)
   415  
   416  	// Change the registration of resA such that it no longer requires delete-before-replace and change the value of
   417  	// resA.A. Only resA should be replaced, and the replacement should be create-before-delete.
   418  	dbrA, inputsA["A"] = nil, resource.NewStringProperty("zam")
   419  	p.Steps = []TestStep{{
   420  		Op: Update,
   421  
   422  		Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
   423  			evts []Event, res result.Result) result.Result {
   424  
   425  			assert.Nil(t, res)
   426  
   427  			AssertSameSteps(t, []StepSummary{
   428  				{Op: deploy.OpSame, URN: provURN},
   429  				{Op: deploy.OpCreateReplacement, URN: urnA},
   430  				{Op: deploy.OpReplace, URN: urnA},
   431  				{Op: deploy.OpSame, URN: urnB},
   432  				{Op: deploy.OpDeleteReplaced, URN: urnA},
   433  			}, SuccessfulSteps(entries))
   434  
   435  			return res
   436  		},
   437  	}}
   438  	snap = p.Run(t, snap)
   439  
   440  	// Change the diff of resA such that it requires delete-before-replace and change the value of resA.A. Both
   441  	// resA and resB should be replaced, and the replacements should be delete-before-replace.
   442  	dbrDiff, inputsA["A"] = true, resource.NewStringProperty("foo")
   443  	p.Steps = []TestStep{{
   444  		Op: Update,
   445  
   446  		Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
   447  			evts []Event, res result.Result) result.Result {
   448  
   449  			assert.Nil(t, res)
   450  
   451  			AssertSameSteps(t, []StepSummary{
   452  				{Op: deploy.OpSame, URN: provURN},
   453  				{Op: deploy.OpDeleteReplaced, URN: urnB},
   454  				{Op: deploy.OpDeleteReplaced, URN: urnA},
   455  				{Op: deploy.OpReplace, URN: urnA},
   456  				{Op: deploy.OpCreateReplacement, URN: urnA},
   457  				{Op: deploy.OpReplace, URN: urnB},
   458  				{Op: deploy.OpCreateReplacement, URN: urnB},
   459  			}, SuccessfulSteps(entries))
   460  
   461  			return res
   462  		},
   463  	}}
   464  	snap = p.Run(t, snap)
   465  
   466  	// Change the registration of resA such that it disables delete-before-replace and change the value of
   467  	// resA.A. Only resA should be replaced, and the replacement should be create-before-delete.
   468  	dbrA, dbrValue, inputsA["A"] = &dbrValue, false, resource.NewStringProperty("bar")
   469  	p.Steps = []TestStep{{
   470  		Op: Update,
   471  
   472  		Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
   473  			evts []Event, res result.Result) result.Result {
   474  
   475  			assert.Nil(t, res)
   476  
   477  			AssertSameSteps(t, []StepSummary{
   478  				{Op: deploy.OpSame, URN: provURN},
   479  				{Op: deploy.OpCreateReplacement, URN: urnA},
   480  				{Op: deploy.OpReplace, URN: urnA},
   481  				{Op: deploy.OpSame, URN: urnB},
   482  				{Op: deploy.OpDeleteReplaced, URN: urnA},
   483  			}, SuccessfulSteps(entries))
   484  
   485  			return res
   486  		},
   487  	}}
   488  	p.Run(t, snap)
   489  }
   490  
   491  func TestDependencyChangeDBR(t *testing.T) {
   492  	t.Parallel()
   493  
   494  	p := &TestPlan{}
   495  
   496  	loaders := []*deploytest.ProviderLoader{
   497  		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
   498  			return &deploytest.Provider{
   499  				DiffF: func(urn resource.URN, id resource.ID,
   500  					olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
   501  
   502  					if !olds["A"].DeepEquals(news["A"]) {
   503  						return plugin.DiffResult{
   504  							ReplaceKeys:         []resource.PropertyKey{"A"},
   505  							DeleteBeforeReplace: true,
   506  						}, nil
   507  					}
   508  					if !olds["B"].DeepEquals(news["B"]) {
   509  						return plugin.DiffResult{
   510  							Changes: plugin.DiffSome,
   511  						}, nil
   512  					}
   513  					return plugin.DiffResult{}, nil
   514  				},
   515  				CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
   516  					preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
   517  
   518  					return "created-id", news, resource.StatusOK, nil
   519  				},
   520  			}, nil
   521  		}),
   522  	}
   523  
   524  	const resType = "pkgA:index:typ"
   525  
   526  	inputsA := resource.NewPropertyMapFromMap(map[string]interface{}{"A": "foo"})
   527  	inputsB := resource.NewPropertyMapFromMap(map[string]interface{}{"A": "foo"})
   528  
   529  	var urnA, urnB resource.URN
   530  	var err error
   531  	program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
   532  		urnA, _, _, err = monitor.RegisterResource(resType, "resA", true, deploytest.ResourceOptions{
   533  			Inputs: inputsA,
   534  		})
   535  		assert.NoError(t, err)
   536  
   537  		inputDepsB := map[resource.PropertyKey][]resource.URN{"A": {urnA}}
   538  		urnB, _, _, err = monitor.RegisterResource(resType, "resB", true, deploytest.ResourceOptions{
   539  			Inputs:       inputsB,
   540  			Dependencies: []resource.URN{urnA},
   541  			PropertyDeps: inputDepsB,
   542  		})
   543  		assert.NoError(t, err)
   544  
   545  		return nil
   546  	})
   547  
   548  	p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...)
   549  	p.Steps = []TestStep{{Op: Update}}
   550  	snap := p.Run(t, nil)
   551  
   552  	inputsA["A"] = resource.NewStringProperty("bar")
   553  	program = deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
   554  		urnB, _, _, err = monitor.RegisterResource(resType, "resB", true, deploytest.ResourceOptions{
   555  			Inputs: inputsB,
   556  		})
   557  		assert.NoError(t, err)
   558  
   559  		urnA, _, _, err = monitor.RegisterResource(resType, "resA", true, deploytest.ResourceOptions{
   560  			Inputs: inputsA,
   561  		})
   562  		assert.NoError(t, err)
   563  
   564  		return nil
   565  	})
   566  
   567  	p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...)
   568  	p.Steps = []TestStep{
   569  		{
   570  			Op: Update,
   571  			Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
   572  				evts []Event, res result.Result) result.Result {
   573  
   574  				assert.Nil(t, res)
   575  				assert.True(t, len(entries) > 0)
   576  
   577  				resBDeleted, resBSame := false, false
   578  				for _, entry := range entries {
   579  					if entry.Step.URN() == urnB {
   580  						switch entry.Step.Op() {
   581  						case deploy.OpDelete, deploy.OpDeleteReplaced:
   582  							resBDeleted = true
   583  						case deploy.OpSame:
   584  							resBSame = true
   585  						}
   586  					}
   587  				}
   588  				assert.True(t, resBSame)
   589  				assert.False(t, resBDeleted)
   590  
   591  				return res
   592  			},
   593  		},
   594  	}
   595  	p.Run(t, snap)
   596  }