Repository: pulumi/pulumi-terraform-bridge
Pull request #535 (2022-06-02 13:22:16)

Clean up Makefile
* Remove the (basically) unused imported build/common.mk
* Remove unused targets and variables
* Rename targets for simplicity (test instead of test_all)
* Update GH Actions files for new Makefile targets

github-actions[bot]: Diff for [pulumi-azuread](https://github.com/pulumi-bot/pulumi-azuread/compare/aa1a4d2..afd2b42) with merge commit 60f3350e3828759eb5d1f2c84929d9290d726a17
github-actions[bot]: Diff for [pulumi-gcp](https://github.com/pulumi-bot/pulumi-gcp/compare/45296b953..7ba979ab4) with merge commit 60f3350e3828759eb5d1f2c84929d9290d726a17
github-actions[bot]: Diff for [pulumi-random](https://github.com/pulumi-bot/pulumi-random/compare/bba55c8..d2e2cc7) with merge commit 60f3350e3828759eb5d1f2c84929d9290d726a17

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b1bf00cd..9cba3b0d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -24,7 +24,7 @@ jobs:
       - name: Build
         run: make build
       - name: Test
-        run: make test_all
+        run: make test
 
   downstream-test:
     name: Test ${{ matrix.provider }} Downstream
diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml
index ebdcea64..be3bceb9 100644
--- a/.github/workflows/master.yml
+++ b/.github/workflows/master.yml
@@ -31,4 +31,4 @@ jobs:
       - name: Build
         run: make build
       - name: Test
-        run: make test_all
+        run: make test
diff --git a/Makefile b/Makefile
index 60e5530b..53133089 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,3 @@
-PROJECT_NAME := Pulumi Terraform Bridge
-include build/common.mk
-
-PACK             := terraform
-PACKDIR          := sdk
 PROJECT          := github.com/pulumi/pulumi-terraform-bridge
 TESTPARALLELISM  := 10
 
@@ -15,18 +10,5 @@ build::
 lint::
 	golangci-lint run
 
-test_fast:: build
-	$(GO_TEST) ./...
-
-test_all:: test_fast
-
-.PHONY: check_clean_worktree
-check_clean_worktree:
-	$$(go env GOPATH)/src/github.com/pulumi/scripts/ci/check-worktree-is-clean.sh
-
-# The travis_* targets are entrypoints for CI.
-.PHONY: travis_cron travis_push travis_pull_request travis_api
-travis_cron: all
-travis_push: all check_clean_worktree only_test
-travis_pull_request: all
-travis_api: all
+test::
+	go test -v -count=1 -cover -timeout 2h -parallel ${TESTPARALLELISM} ./...
diff --git a/build/common.mk b/build/common.mk
deleted file mode 100644
index 0a1cedda..00000000
--- a/build/common.mk
+++ /dev/null
@@ -1,222 +0,0 @@
-# Copyright 2016-2021, Pulumi Corporation.  All rights reserved.
-
-# common.mk provides most of the scalfholding for our build system. It
-# provides default targets for each project we want to build.
-#
-# The default targets we use are:
-#
-#  - ensure: restores and dependencies needed for the build from
-#            remote sources (e.g dep ensure or yarn install)
-#
-#  - build: builds a project but does not install it. In the case of
-#           go code, this usually means running go install (which
-#           would place them in `GOBIN`, but not `PULUMI_ROOT`
-#
-#  - install: copies the bits we plan to ship into a layout in
-#             `PULUMI_ROOT` that looks like what a customer would get
-#             when they download and install Pulumi. For JavaScript
-#             projects, installing also runs yarn link to register
-#             this package, so that other projects can depend on it.
-#
-#  - lint: runs relevent linters for the project
-#
-#  - test_fast: runs the fast tests for a project. These are often
-#               go unit tests or javascript unit tests, they should
-#               complete quickly, as we expect developers to run them
-#               fequently as part of their "inner loop" development.
-#
-#  - test_all: runs all of test_fast and then runs additional testing,
-#              which may take longer (some times a lot longer!). These
-#              are often integration tests which will use `pulumi` to
-#              deploy example Pulumi projects, creating cloud
-#              resources along the way.
-#
-# In addition, we have a few higher level targets that just depend on
-# these targets:
-#
-#  - only_build: this target runs build and install targets
-#
-#  - only_test: this target runs the list and test_all targets
-#               (test_all itself runs test_fast)
-#
-#  - default: this is the target that is run by default when no
-#             arguments are passed to make, it runs the build, lint,
-#             install and test_fast targets
-#
-#  - core: this target behaves like `default` except for the case
-#          where a project declares SUB_PROJECTS (see a discussion on
-#          that later). In that case, building `core` target does not
-#          build sub projects.
-#
-#  - all: this target runs build, lint, install and test_all (which
-#         itself runs test_fast).
-#
-# Before including this makefile, a project may define some values
-# that this makefile understands:
-#
-# - PROJECT_NAME: If set, make default and make all will print a banner
-#                 with the project name when they are built.
-#
-# - SUB_PROJECTS: If set, each item in the list is treated as a path
-#                 to another project (relative to the directory of the
-#                 main Makefile) which should be built as well. When
-#                 this happens, the default and all targets first
-#                 build the default or all target of each child
-#                 project. For each subproject we also create targets
-#                 with our standard names, prepended by the target
-#                 name and an underscore, which just calls Make for
-#                 that specific target. These can be handy targets to
-#                 build explicitly on the command line from time to
-#                 time.
-#
-# - NODE_MODULE_NAME: If set, an install target will be auto-generated
-#                     that installs the module to
-#                     $(PULUMI_ROOT)/node_modules/$(NODE_MODULE_NAME)
-#
-# This Makefile also provides some convience methods:
-#
-# STEP_MESSAGE is a macro that can be invoked with `$(call
-# STEP_MESSAGE)` and it will print the name of the current target (in
-# green text) to the console. All the targets provided by this makefile
-# do that by default.
-#
-# The ensure target also provides some default behavior, detecting if
-# there is a Gopkg.toml or package.json file in the current folder and
-# if so calling dep ensure -v or yarn install. This behavior means that
-# projects will not often need to augment the ensure target.
-#
-# Unlike the other leaf targets, ensure will call the ensure target on
-# any sub-projects.
-#
-# Importing common.mk should be the first thing your Makefile does, after
-# optionally setting SUB_PROJECTS, PROJECT_NAME and NODE_MODULE_NAME.
-SHELL       := /bin/bash
-.SHELLFLAGS := -ec
-
-STEP_MESSAGE = @echo -e "\033[0;32m$(shell echo '$@' | tr a-z A-Z | tr '_' ' '):\033[0m"
-
-# Our install targets place items item into $PULUMI_ROOT, if it's
-# unset, default to /opt/pulumi.
-ifeq ($(PULUMI_ROOT),)
-	PULUMI_ROOT:=/opt/pulumi
-endif
-
-PULUMI_BIN          := $(PULUMI_ROOT)/bin
-PULUMI_NODE_MODULES := $(PULUMI_ROOT)/node_modules
-
-GO_TEST_FAST = go test -short -v -count=1 -cover -timeout 2h -parallel ${TESTPARALLELISM}
-GO_TEST = go test -v -count=1 -cover -timeout 2h -parallel ${TESTPARALLELISM}
-
-.PHONY: default all ensure only_build only_test build lint install test_all core
-
-# ensure that `default` is the target that is run when no arguments are passed to make
-default::
-
-PYTHON ?= python3
-PIP ?= pip3
-
-# If there are sub projects, our default, all, and ensure targets will
-# recurse into them.
-ifneq ($(SUB_PROJECTS),)
-only_build:: $(SUB_PROJECTS:%=%_only_build)
-only_test:: $(SUB_PROJECTS:%=%_only_test)
-only_test_fast:: $(SUB_PROJECTS:%=%_only_test_fast)
-default:: $(SUB_PROJECTS:%=%_default)
-all:: $(SUB_PROJECTS:%=%_all)
-ensure:: $(SUB_PROJECTS:%=%_ensure)
-endif
-
-# `core` is like `default` except it does not build sub projects.
-core:: build lint install test_fast
-
-# If $(PROJECT_NAME) has been set, have our default and all targets
-# print a nice banner.
-ifneq ($(PROJECT_NAME),)
-default::
-	@echo -e "\033[1;37m$(shell echo '$(PROJECT_NAME)' | sed -e 's/./=/g')\033[1;37m"
-	@echo -e "\033[1;37m$(PROJECT_NAME)\033[1;37m"
-	@echo -e "\033[1;37m$(shell echo '$(PROJECT_NAME)' | sed -e 's/./=/g')\033[1;37m"
-all::
-	@echo -e "\033[1;37m$(shell echo '$(PROJECT_NAME)' | sed -e 's/./=/g')\033[1;37m"
-	@echo -e "\033[1;37m$(PROJECT_NAME)\033[1;37m"
-	@echo -e "\033[1;37m$(shell echo '$(PROJECT_NAME)' | sed -e 's/./=/g')\033[1;37m"
-endif
-
-default:: build install lint test_fast
-all:: build install lint test_all
-
-ensure::
-	$(call STEP_MESSAGE)
-	@echo "GO111MODULE=on go mod tidy"; GO111MODULE=on go mod tidy;
-	@echo "GO111MODULE=on go mod download"; GO111MODULE=on go mod download;
-	@if [ -e 'package.json' ]; then echo "yarn install"; yarn install; fi
-
-build::
-	$(call STEP_MESSAGE)
-
-lint::
-	$(call STEP_MESSAGE)
-
-test_fast::
-	$(call STEP_MESSAGE)
-
-install::
-	$(call STEP_MESSAGE)
-	@mkdir -p $(PULUMI_BIN)
-	@mkdir -p $(PULUMI_NODE_MODULES)
-
-test_all::
-	$(call STEP_MESSAGE)
-
-ifneq ($(NODE_MODULE_NAME),)
-install::
-	[ ! -e "$(PULUMI_NODE_MODULES)/$(NODE_MODULE_NAME)" ] || rm -rf "$(PULUMI_NODE_MODULES)/$(NODE_MODULE_NAME)"
-	mkdir -p "$(PULUMI_NODE_MODULES)/$(NODE_MODULE_NAME)"
-	cp -r bin/. "$(PULUMI_NODE_MODULES)/$(NODE_MODULE_NAME)"
-	cp package.json "$(PULUMI_NODE_MODULES)/$(NODE_MODULE_NAME)"
-	cp yarn.lock "$(PULUMI_NODE_MODULES)/$(NODE_MODULE_NAME)"
-	rm -rf "$(PULUMI_NODE_MODULES)/$(NODE_MODULE_NAME)/node_modules"
-	cd "$(PULUMI_NODE_MODULES)/$(NODE_MODULE_NAME)" && \
-	yarn install --offline --production && \
-	(yarn unlink > /dev/null 2>&1 || true) && \
-	yarn link
-endif
-
-only_build:: build install
-only_test:: lint test_all
-only_test_fast:: lint test_fast
-
-# Generate targets for each sub project. This project's default and
-# all targets will depend on the sub project's targets, and the
-# individual targets for sub projects are added as a convience when
-# invoking make from the command line
-ifneq ($(SUB_PROJECTS),)
-$(SUB_PROJECTS:%=%_default):
-	@$(MAKE) -C ./$(@:%_default=%) default
-$(SUB_PROJECTS:%=%_all):
-	@$(MAKE) -C ./$(@:%_all=%) all
-$(SUB_PROJECTS:%=%_ensure):
-	@$(MAKE) -C ./$(@:%_ensure=%) ensure
-$(SUB_PROJECTS:%=%_build):
-	@$(MAKE) -C ./$(@:%_build=%) build
-$(SUB_PROJECTS:%=%_lint):
-	@$(MAKE) -C ./$(@:%_lint=%) lint
-$(SUB_PROJECTS:%=%_test_fast):
-	@$(MAKE) -C ./$(@:%_test_fast=%) test_fast
-$(SUB_PROJECTS:%=%_install):
-	@$(MAKE) -C ./$(@:%_install=%) install
-$(SUB_PROJECTS:%=%_test_all):
-	@$(MAKE) -C ./$(@:%_test_all=%) test_all
-$(SUB_PROJECTS:%=%_only_build):
-	@$(MAKE) -C ./$(@:%_only_build=%) only_build
-$(SUB_PROJECTS:%=%_only_test):
-	@$(MAKE) -C ./$(@:%_only_test=%) only_test
-$(SUB_PROJECTS:%=%_only_test_fast):
-	@$(MAKE) -C ./$(@:%_only_test_fast=%) only_test_fast
-endif
-
-# As a convinece, we provide a format target that folks can build to
-# run go fmt over all the go code in their tree.
-.PHONY: format
-format:
-	find . -iname "*.go" -not -path "./vendor/*" | xargs gofmt -s -w
diff --git a/pkg/tfbridge/diff.go b/pkg/tfbridge/diff.go
index 5fa73ec6..55ee0b0d 100644
--- a/pkg/tfbridge/diff.go
+++ b/pkg/tfbridge/diff.go
@@ -196,7 +196,8 @@ func makePropertyDiff(name, path string, v resource.PropertyValue, tfDiff shim.I
 			// These diffs are typically changes to output properties that we don't care about.
 			if finalize {
 				if hasOtherDiff &&
-					(other.Kind == pulumirpc.PropertyDiff_ADD || other.Kind == pulumirpc.PropertyDiff_ADD_REPLACE) {
+					(other.Kind == pulumirpc.PropertyDiff_ADD || other.Kind == pulumirpc.PropertyDiff_ADD_REPLACE) &&
+					!d.RequiresNew {
 					delete(diff, path)
 				}
 				return false
diff --git a/pkg/tfbridge/info.go b/pkg/tfbridge/info.go
index c55eefbb..7740e1b1 100644
--- a/pkg/tfbridge/info.go
+++ b/pkg/tfbridge/info.go
@@ -148,14 +148,17 @@ type ResourceOrDataSourceInfo interface {
 // also give custom metadata for fields, using the SchemaInfo structure below.  Finally, a set of composite keys can be
 // given; this is used when Terraform needs more than just the ID to uniquely identify and query for a resource.
 type ResourceInfo struct {
-	Tok                 tokens.Type            // a type token to override the default; "" uses the default.
-	Fields              map[string]*SchemaInfo // a map of custom field names; if a type is missing, uses the default.
-	IDFields            []string               // an optional list of ID alias fields.
-	Docs                *DocInfo               // overrides for finding and mapping TF docs.
-	DeleteBeforeReplace bool                   // if true, Pulumi will delete before creating new replacement resources.
-	Aliases             []AliasInfo            // aliases for this resources, if any.
-	DeprecationMessage  string                 // message to use in deprecation warning
-	CSharpName          string                 // .NET-specific name
+	Tok      tokens.Type            // a type token to override the default; "" uses the default.
+	Fields   map[string]*SchemaInfo // a map of custom field names; if a type is missing, uses the default.
+	IDFields []string               // an optional list of ID alias fields.
+	// list of parameters that we can trust that any change will allow a createBeforeDelete
+	UniqueNameFields    []string
+	Docs                *DocInfo    // overrides for finding and mapping TF docs.
+	DeleteBeforeReplace bool        // if true, Pulumi will delete before creating new replacement resources.
+	Aliases             []AliasInfo // aliases for this resources, if any.
+	DeprecationMessage  string      // message to use in deprecation warning
+	CSharpName          string      // .NET-specific name
+
 }
 
 // GetTok returns a resource type token
diff --git a/pkg/tfbridge/provider.go b/pkg/tfbridge/provider.go
index cabfcc16..022bd793 100644
--- a/pkg/tfbridge/provider.go
+++ b/pkg/tfbridge/provider.go
@@ -805,7 +805,7 @@ func (p *Provider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*pulum
 	})
 
 	deleteBeforeReplace := len(replaces) > 0 &&
-		(res.Schema.DeleteBeforeReplace || nameRequiresDeleteBeforeReplace(news, res.TF.Schema(), res.Schema.Fields))
+		(res.Schema.DeleteBeforeReplace || nameRequiresDeleteBeforeReplace(news, olds, res.TF.Schema(), res.Schema))
 
 	return &pulumirpc.DiffResponse{
 		Changes:             changes,
diff --git a/pkg/tfbridge/schema.go b/pkg/tfbridge/schema.go
index 52fae2b6..053b4bc5 100644
--- a/pkg/tfbridge/schema.go
+++ b/pkg/tfbridge/schema.go
@@ -53,10 +53,11 @@ type AssetTable map[*SchemaInfo]resource.PropertyValue
 
 // nameRequiresDeleteBeforeReplace returns true if the given set of resource inputs includes an autonameable
 // property with a value that was not populated by the autonamer.
-func nameRequiresDeleteBeforeReplace(inputs resource.PropertyMap,
-	tfs shim.SchemaMap, ps map[string]*SchemaInfo) bool {
+func nameRequiresDeleteBeforeReplace(news resource.PropertyMap, olds resource.PropertyMap,
+	tfs shim.SchemaMap, resourceInfo *ResourceInfo) bool {
+	fields := resourceInfo.Fields
 
-	defaults, hasDefaults := inputs[defaultsKey]
+	defaults, hasDefaults := news[defaultsKey]
 	if !hasDefaults || !defaults.IsArray() {
 		// If there is no list of properties that were populated using defaults, consider the resource autonamed.
 		// This avoids setting delete-before-replace for resources that were created before the defaults list existed.
@@ -71,8 +72,31 @@ func nameRequiresDeleteBeforeReplace(inputs resource.PropertyMap,
 		hasDefault[resource.PropertyKey(key.StringValue())] = true
 	}
 
-	for key := range inputs {
-		_, _, psi := getInfoFromPulumiName(key, tfs, ps, false)
+	// These are a list of Pulumi named fields that we care about comparing to try
+	// and override the deleteBeforeReplace e.g. name or namePrefix
+	// if any of these values change then we can assume we can
+	if len(resourceInfo.UniqueNameFields) > 0 {
+		for _, name := range resourceInfo.UniqueNameFields {
+			key := resource.PropertyKey(name)
+			_, _, psi := getInfoFromPulumiName(key, tfs, fields, false)
+
+			oldVal := olds[key]
+			newVal := news[key]
+
+			if !oldVal.DeepEquals(newVal) {
+				return false
+			}
+
+			if psi != nil && psi.HasDefault() && psi.Default.AutoNamed && hasDefault[key] {
+				return false
+			}
+		}
+
+		return true
+	}
+
+	for key := range news {
+		_, _, psi := getInfoFromPulumiName(key, tfs, fields, false)
 		if psi != nil && psi.HasDefault() && psi.Default.AutoNamed && !hasDefault[key] {
 			return true
 		}


Test patch:
diff --git a/pkg/tfbridge/diff_test.go b/pkg/tfbridge/diff_test.go
index 3fb44b56..10b013c5 100644
--- a/pkg/tfbridge/diff_test.go
+++ b/pkg/tfbridge/diff_test.go
@@ -1,14 +1,17 @@
 package tfbridge
 
 import (
+	"context"
 	"testing"
 
 	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+	v2Schema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
 	pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
 	"github.com/stretchr/testify/assert"
 
 	shimv1 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/sdk-v1"
+	shimv2 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/sdk-v2"
 )
 
 type DiffKind = pulumirpc.PropertyDiff_Kind
@@ -24,6 +27,122 @@ const (
 
 var computedValue = resource.Computed{Element: resource.NewStringProperty("")}
 
+func TestCustomizeDiff(t *testing.T) {
+	inputsMap := resource.NewPropertyMapFromMap(map[string]interface{}{
+		"prop": "foo",
+	})
+	stateMap := resource.NewPropertyMapFromMap(map[string]interface{}{
+		"prop": "foo",
+		"outp": false,
+	})
+
+	// Pulumi Schema
+	info := map[string]*SchemaInfo{}
+	tfs := map[string]*v2Schema.Schema{
+		"prop": {Type: v2Schema.TypeString},
+		"outp": {Type: v2Schema.TypeBool, Computed: true},
+	}
+	// Terraform schema
+	sch := shimv2.NewSchemaMap(tfs)
+
+	// ignores
+	var ignores []string
+
+	t.Run("CustomDiffCausesAddReplace", func(t *testing.T) {
+		// expected diff
+		expected := map[string]DiffKind{
+			"outp": AR,
+		}
+
+		// Fake up a TF resource and a TF provider.
+		customDiffRes := &v2Schema.Resource{
+			Schema: tfs,
+			CustomizeDiff: func(_ context.Context, d *v2Schema.ResourceDiff, _ interface{}) error {
+				var err error
+				err = d.SetNew("outp", true)
+				if err != nil {
+					return err
+				}
+				err = d.ForceNew("outp")
+				if err != nil {
+					return err
+				}
+				return err
+			},
+		}
+		provider := shimv2.NewProvider(&v2Schema.Provider{
+			ResourcesMap: map[string]*v2Schema.Resource{
+				"resource": customDiffRes,
+			},
+		})
+
+		// Convert the inputs and state to TF config and resource attributes.
+		r := Resource{
+			TF:     shimv2.NewResource(customDiffRes),
+			Schema: &ResourceInfo{Fields: info},
+		}
+		tfState, err := MakeTerraformState(r, "id", stateMap)
+		assert.NoError(t, err)
+
+		config, _, err := MakeTerraformConfig(&Provider{tf: provider}, inputsMap, sch, info)
+		assert.NoError(t, err)
+
+		tfDiff, err := provider.Diff("resource", tfState, config)
+		assert.NoError(t, err)
+
+		// ProcessIgnoreChanges
+		doIgnoreChanges(sch, info, stateMap, inputsMap, ignores, tfDiff)
+
+		// Convert the diff to a detailed diff and check the result.
+		diff := makeDetailedDiff(sch, info, stateMap, inputsMap, tfDiff)
+		expectedDiff := map[string]*pulumirpc.PropertyDiff{}
+		for k, v := range expected {
+			expectedDiff[k] = &pulumirpc.PropertyDiff{Kind: v}
+		}
+		assert.Equal(t, expectedDiff, diff)
+	})
+
+	t.Run("NoCustomDiffCausesNoDiff", func(t *testing.T) {
+		// expected diff
+		expected := map[string]DiffKind{}
+
+		// Fake up a TF resource and a TF provider.
+		noCustomDiffRes := &v2Schema.Resource{
+			Schema: tfs,
+		}
+		provider := shimv2.NewProvider(&v2Schema.Provider{
+			ResourcesMap: map[string]*v2Schema.Resource{
+				"resource": noCustomDiffRes,
+			},
+		})
+
+		// Convert the inputs and state to TF config and resource attributes.
+		r := Resource{
+			TF:     shimv2.NewResource(noCustomDiffRes),
+			Schema: &ResourceInfo{Fields: info},
+		}
+		tfState, err := MakeTerraformState(r, "id", stateMap)
+		assert.NoError(t, err)
+
+		config, _, err := MakeTerraformConfig(&Provider{tf: provider}, inputsMap, sch, info)
+		assert.NoError(t, err)
+
+		tfDiff, err := provider.Diff("resource", tfState, config)
+		assert.NoError(t, err)
+
+		// ProcessIgnoreChanges
+		doIgnoreChanges(sch, info, stateMap, inputsMap, ignores, tfDiff)
+
+		// Convert the diff to a detailed diff and check the result.
+		diff := makeDetailedDiff(sch, info, stateMap, inputsMap, tfDiff)
+		expectedDiff := map[string]*pulumirpc.PropertyDiff{}
+		for k, v := range expected {
+			expectedDiff[k] = &pulumirpc.PropertyDiff{Kind: v}
+		}
+		assert.Equal(t, expectedDiff, diff)
+	})
+}
+
 func diffTest(t *testing.T, tfs map[string]*schema.Schema, info map[string]*SchemaInfo,
 	inputs, state map[string]interface{}, expected map[string]DiffKind, ignoreChanges ...string) {
 
@@ -80,6 +199,23 @@ func diffTest(t *testing.T, tfs map[string]*schema.Schema, info map[string]*Sche
 	assert.Equal(t, map[string]*pulumirpc.PropertyDiff{}, diff)
 }
 
+func TestCustomDiffProducesReplace(t *testing.T) {
+	diffTest(t,
+		map[string]*schema.Schema{
+			"prop": {Type: schema.TypeString},
+			"outp": {Type: schema.TypeString, Computed: true},
+		},
+		map[string]*SchemaInfo{},
+		map[string]interface{}{
+			"prop": "foo",
+		},
+		map[string]interface{}{
+			"prop": "foo",
+			"outp": "bar",
+		},
+		map[string]DiffKind{})
+}
+
 func TestEmptyDiff(t *testing.T) {
 	diffTest(t,
 		map[string]*schema.Schema{