// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package integration

import (
	"context"
	cryptorand "crypto/rand"
	sha256 "crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"hash/fnv"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"testing"
	"time"

	multierror "github.com/hashicorp/go-multierror"
	"golang.org/x/mod/modfile"
	"golang.org/x/mod/module"
	"gopkg.in/yaml.v3"

	"github.com/pulumi/pulumi/pkg/v3/engine"
	"github.com/pulumi/pulumi/pkg/v3/operations"
	"github.com/pulumi/pulumi/pkg/v3/resource/stack"
	"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
	"github.com/pulumi/pulumi/sdk/v3/go/common/env"
	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
	ptesting "github.com/pulumi/pulumi/sdk/v3/go/common/testing"
	"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
	"github.com/pulumi/pulumi/sdk/v3/go/common/tools"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/fsutil"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/retry"
	"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
	"github.com/pulumi/pulumi/sdk/v3/nodejs/npm"
	"github.com/stretchr/testify/assert"
	user "github.com/tweekmonster/luser"
)

const (
	PythonRuntime = "python"
	NodeJSRuntime = "nodejs"
	GoRuntime     = "go"
	DotNetRuntime = "dotnet"
	YAMLRuntime   = "yaml"
	JavaRuntime   = "java"
)

const windowsOS = "windows"

var ErrTestFailed = errors.New("test failed")

// RuntimeValidationStackInfo contains details related to the stack that runtime validation logic may want to use.
type RuntimeValidationStackInfo struct {
	StackName    tokens.QName
	Deployment   *apitype.DeploymentV3
	RootResource apitype.ResourceV3
	Outputs      map[string]interface{}
	Events       []apitype.EngineEvent
}

// EditDir is an optional edit to apply to the example, as subsequent deployments.
type EditDir struct {
	Dir                    string
	ExtraRuntimeValidation func(t *testing.T, stack RuntimeValidationStackInfo)

	// Additive is true if Dir should be copied *on top* of the test directory.
	// Otherwise Dir *replaces* the test directory, except we keep .pulumi/ and Pulumi.yaml and Pulumi.<stack>.yaml.
	Additive bool

	// ExpectFailure is true if we expect this test to fail.  This is very coarse grained, and will essentially
	// tolerate *any* failure in the program (IDEA: in the future, offer a way to narrow this down more).
	ExpectFailure bool

	// ExpectNoChanges is true if the edit is expected to not propose any changes.
	ExpectNoChanges bool

	// Stdout is the writer to use for all stdout messages.
	Stdout io.Writer
	// Stderr is the writer to use for all stderr messages.
	Stderr io.Writer
	// Verbose may be set to true to print messages as they occur, rather than buffering and showing upon failure.
	Verbose bool

	// Run program directory in query mode.
	QueryMode bool
}

// TestCommandStats is a collection of data related to running a single command during a test.
type TestCommandStats struct {
	// StartTime is the time at which the command was started
	StartTime string `json:"startTime"`
	// EndTime is the time at which the command exited
	EndTime string `json:"endTime"`
	// ElapsedSeconds is the time at which the command exited
	ElapsedSeconds float64 `json:"elapsedSeconds"`
	// StackName is the name of the stack
	StackName string `json:"stackName"`
	// TestId is the unique ID of the test run
	TestID string `json:"testId"`
	// StepName is the command line which was invoked
	StepName string `json:"stepName"`
	// CommandLine is the command line which was invoked
	CommandLine string `json:"commandLine"`
	// TestName is the name of the directory in which the test was executed
	TestName string `json:"testName"`
	// IsError is true if the command failed
	IsError bool `json:"isError"`
	// The Cloud that the test was run against, or empty for local deployments
	CloudURL string `json:"cloudURL"`
}

// TestStatsReporter reports results and metadata from a test run.
type TestStatsReporter interface {
	ReportCommand(stats TestCommandStats)
}

// Environment is used to create environments for use by test programs.
type Environment struct {
	// The name of the environment.
	Name string
	// The definition of the environment.
	Definition map[string]any
}

// ConfigValue is used to provide config values to a test program.
type ConfigValue struct {
	// The config key to pass to `pulumi config`.
	Key string
	// The config value to pass to `pulumi config`.
	Value string
	// Secret indicates that the `--secret` flag should be specified when calling `pulumi config`.
	Secret bool
	// Path indicates that the `--path` flag should be specified when calling `pulumi config`.
	Path bool
}

// ProgramTestOptions provides options for ProgramTest
type ProgramTestOptions struct {
	// Dir is the program directory to test.
	Dir string
	// Array of NPM packages which must be `yarn linked` (e.g. {"pulumi", "@pulumi/aws"})
	Dependencies []string
	// Map of package names to versions. The test will use the specified versions of these packages instead of what
	// is declared in `package.json`.
	Overrides map[string]string
	// Automatically use the latest dev version of pulumi SDKs if available.
	InstallDevReleases bool
	// List of environments to create in order.
	CreateEnvironments []Environment
	// List of environments to use.
	Environments []string
	// Map of config keys and values to set (e.g. {"aws:region": "us-east-2"}).
	Config map[string]string
	// Map of secure config keys and values to set (e.g. {"aws:region": "us-east-2"}).
	Secrets map[string]string
	// List of config keys and values to set in order, including Secret and Path options.
	OrderedConfig []ConfigValue
	// SecretsProvider is the optional custom secrets provider to use instead of the default.
	SecretsProvider string
	// EditDirs is an optional list of edits to apply to the example, as subsequent deployments.
	EditDirs []EditDir
	// ExtraRuntimeValidation is an optional callback for additional validation, called before applying edits.
	ExtraRuntimeValidation func(t *testing.T, stack RuntimeValidationStackInfo)
	// RelativeWorkDir is an optional path relative to `Dir` which should be used as working directory during tests.
	RelativeWorkDir string
	// AllowEmptyPreviewChanges is true if we expect that this test's no-op preview may propose changes (e.g.
	// because the test is sensitive to the exact contents of its working directory and those contents change
	// incidentally between the initial update and the empty update).
	AllowEmptyPreviewChanges bool
	// AllowEmptyUpdateChanges is true if we expect that this test's no-op update may perform changes (e.g.
	// because the test is sensitive to the exact contents of its working directory and those contents change
	// incidentally between the initial update and the empty update).
	AllowEmptyUpdateChanges bool
	// ExpectFailure is true if we expect this test to fail.  This is very coarse grained, and will essentially
	// tolerate *any* failure in the program (IDEA: in the future, offer a way to narrow this down more).
	ExpectFailure bool
	// ExpectRefreshChanges may be set to true if a test is expected to have changes yielded by an immediate refresh.
	// This could occur, for example, is a resource's state is constantly changing outside of Pulumi (e.g., timestamps).
	ExpectRefreshChanges bool
	// RetryFailedSteps indicates that failed updates, refreshes, and destroys should be retried after a brief
	// intermission. A maximum of 3 retries will be attempted.
	RetryFailedSteps bool
	// SkipRefresh indicates that the refresh step should be skipped entirely.
	SkipRefresh bool
	// Require a preview after refresh to be a no-op (expect no changes). Has no effect if SkipRefresh is true.
	RequireEmptyPreviewAfterRefresh bool
	// SkipPreview indicates that the preview step should be skipped entirely.
	SkipPreview bool
	// SkipUpdate indicates that the update step should be skipped entirely.
	SkipUpdate bool
	// SkipExportImport skips testing that exporting and importing the stack works properly.
	SkipExportImport bool
	// SkipEmptyPreviewUpdate skips the no-change preview/update that is performed that validates
	// that no changes happen.
	SkipEmptyPreviewUpdate bool
	// SkipStackRemoval indicates that the stack should not be removed. (And so the test's results could be inspected
	// in the Pulumi Service after the test has completed.)
	SkipStackRemoval bool
	// Destroy on cleanup defers stack destruction until the test cleanup step, rather than after
	// program test execution. This is useful for more realistic stack reference testing, allowing one
	// project and stack to be stood up and a second to be run before the first is destroyed.
	//
	// Implies NoParallel because we expect that another caller to ProgramTest will set that
	DestroyOnCleanup bool
	// DestroyExcludeProtected indicates that when the test stack is destroyed,
	// protected resources should be excluded from the destroy operation.
	DestroyExcludeProtected bool
	// Quick implies SkipPreview, SkipExportImport and SkipEmptyPreviewUpdate
	Quick bool
	// RequireService indicates that the test must be run against the Pulumi Service
	RequireService bool
	// PreviewCommandlineFlags specifies flags to add to the `pulumi preview` command line (e.g. "--color=raw")
	PreviewCommandlineFlags []string
	// UpdateCommandlineFlags specifies flags to add to the `pulumi up` command line (e.g. "--color=raw")
	UpdateCommandlineFlags []string
	// QueryCommandlineFlags specifies flags to add to the `pulumi query` command line (e.g. "--color=raw")
	QueryCommandlineFlags []string
	// RunBuild indicates that the build step should be run (e.g. run `yarn build` for `nodejs` programs)
	RunBuild bool
	// RunUpdateTest will ensure that updates to the package version can test for spurious diffs
	RunUpdateTest bool
	// DecryptSecretsInOutput will ensure that stack output is passed `--show-secrets` parameter
	// Used in conjunction with ExtraRuntimeValidation
	DecryptSecretsInOutput bool

	// CloudURL is an optional URL to override the default Pulumi Service API (https://api.pulumi-staging.io). The
	// PULUMI_ACCESS_TOKEN environment variable must also be set to a valid access token for the target cloud.
	CloudURL string

	// StackName allows the stack name to be explicitly provided instead of computed from the
	// environment during tests.
	StackName string

	// If non-empty, specifies the value of the `--tracing` flag to pass
	// to Pulumi CLI, which may be a Zipkin endpoint or a
	// `file:./local.trace` style url for AppDash tracing.
	//
	// Template `{command}` syntax will be expanded to the current
	// command name such as `pulumi-stack-rm`. This is useful for
	// file-based tracing since `ProgramTest` performs multiple
	// CLI invocations that can inadvertently overwrite the trace
	// file.
	Tracing string

	// NoParallel will opt the test out of being ran in parallel.
	NoParallel bool

	// PrePulumiCommand specifies a callback that will be executed before each `pulumi` invocation. This callback may
	// optionally return another callback to be invoked after the `pulumi` invocation completes.
	PrePulumiCommand func(verb string) (func(err error) error, error)

	// ReportStats optionally specifies how to report results from the test for external collection.
	ReportStats TestStatsReporter

	// Stdout is the writer to use for all stdout messages.
	Stdout io.Writer
	// Stderr is the writer to use for all stderr messages.
	Stderr io.Writer
	// Verbose may be set to true to print messages as they occur, rather than buffering and showing upon failure.
	Verbose bool

	// DebugLogging may be set to anything >0 to enable excessively verbose debug logging from `pulumi`. This
	// is equivalent to `--logflow --logtostderr -v=N`, where N is the value of DebugLogLevel. This may also
	// be enabled by setting the environment variable PULUMI_TEST_DEBUG_LOG_LEVEL.
	DebugLogLevel int
	// DebugUpdates may be set to true to enable debug logging from `pulumi preview`, `pulumi up`, and
	// `pulumi destroy`.  This may also be enabled by setting the environment variable PULUMI_TEST_DEBUG_UPDATES.
	DebugUpdates bool

	// Bin is a location of a `pulumi` executable to be run.  Taken from the $PATH if missing.
	Bin string
	// YarnBin is a location of a `yarn` executable to be run.  Taken from the $PATH if missing.
	YarnBin string
	// GoBin is a location of a `go` executable to be run.  Taken from the $PATH if missing.
	GoBin string
	// PythonBin is a location of a `python` executable to be run.  Taken from the $PATH if missing.
	PythonBin string
	// PipenvBin is a location of a `pipenv` executable to run.  Taken from the $PATH if missing.
	PipenvBin string
	// DotNetBin is a location of a `dotnet` executable to be run.  Taken from the $PATH if missing.
	DotNetBin string

	// Additional environment variables to pass for each command we run.
	Env []string

	// Automatically create and use a virtual environment, rather than using the Pipenv tool. This is now the default
	// behavior, so this option no longer has any affect. To go back to the old behavior use the `UsePipenv` option.
	UseAutomaticVirtualEnv bool
	// Use the Pipenv tool to manage the virtual environment.
	UsePipenv bool
	// Use a shared virtual environment for tests based on the contents of the requirements file. Defaults to false.
	UseSharedVirtualEnv *bool
	// Shared venv path when UseSharedVirtualEnv is true. Defaults to $HOME/.pulumi-test-venvs.
	SharedVirtualEnvPath string
	// Refers to the shared venv directory when UseSharedVirtualEnv is true. Otherwise defaults to venv
	virtualEnvDir string

	// If set, this hook is called after the `pulumi preview` command has completed.
	PreviewCompletedHook func(dir string) error

	// JSONOutput indicates that the `--json` flag should be passed to `up`, `preview`,
	// `refresh` and `destroy` commands.
	JSONOutput bool

	// If set, this hook is called after `pulumi stack export` on the exported file. If `SkipExportImport` is set, this
	// hook is ignored.
	ExportStateValidator func(t *testing.T, stack []byte)

	// If not nil, specifies the logic of preparing a project by
	// ensuring dependencies. If left as nil, runs default
	// preparation logic by dispatching on whether the project
	// uses Node, Python, .NET or Go.
	PrepareProject func(*engine.Projinfo) error

	// If not nil, will be run after the project has been prepared.
	PostPrepareProject func(*engine.Projinfo) error

	// Array of provider plugin dependencies which come from local packages.
	LocalProviders []LocalDependency
}

func (opts *ProgramTestOptions) GetUseSharedVirtualEnv() bool {
	if opts.UseSharedVirtualEnv != nil {
		return *opts.UseSharedVirtualEnv
	}
	return false
}

type LocalDependency struct {
	Package string
	Path    string
}

func (opts *ProgramTestOptions) GetDebugLogLevel() int {
	if opts.DebugLogLevel > 0 {
		return opts.DebugLogLevel
	}
	if du := os.Getenv("PULUMI_TEST_DEBUG_LOG_LEVEL"); du != "" {
		if n, e := strconv.Atoi(du); e != nil {
			panic(e)
		} else if n > 0 {
			return n
		}
	}
	return 0
}

func (opts *ProgramTestOptions) GetDebugUpdates() bool {
	return opts.DebugUpdates || os.Getenv("PULUMI_TEST_DEBUG_UPDATES") != ""
}

// GetStackName returns a stack name to use for this test.
func (opts *ProgramTestOptions) GetStackName() tokens.QName {
	if opts.StackName == "" {
		// Fetch the host and test dir names, cleaned so to contain just [a-zA-Z0-9-_] chars.
		hostname, err := os.Hostname()
		contract.AssertNoErrorf(err, "failure to fetch hostname for stack prefix")
		var host string
		for _, c := range hostname {
			if len(host) >= 10 {
				break
			}
			if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
				(c >= '0' && c <= '9') || c == '-' || c == '_' {
				host += string(c)
			}
		}

		var test string
		for _, c := range filepath.Base(opts.Dir) {
			if len(test) >= 10 {
				break
			}
			if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
				(c >= '0' && c <= '9') || c == '-' || c == '_' {
				test += string(c)
			}
		}

		b := make([]byte, 4)
		_, err = cryptorand.Read(b)
		contract.AssertNoErrorf(err, "failure to generate random stack suffix")

		opts.StackName = strings.ToLower("p-it-" + host + "-" + test + "-" + hex.EncodeToString(b))
	}

	return tokens.QName(opts.StackName)
}

// getEnvName returns the uniquified name for the given environment. The name is made unique by appending the FNV hash
// of the associated stack's name. This ensures that the name is both unique and deterministic. The name must be
// deterministic because it is computed by both LifeCycleInitialize and TestLifeCycleDestroy.
func (opts *ProgramTestOptions) getEnvName(name string) string {
	h := fnv.New32()
	_, err := h.Write([]byte(opts.GetStackName()))
	contract.IgnoreError(err)

	suffix := hex.EncodeToString(h.Sum(nil))
	return fmt.Sprintf("%v-%v", name, suffix)
}

func (opts *ProgramTestOptions) getEnvNameWithOwner(name string) string {
	owner := os.Getenv("PULUMI_TEST_OWNER")
	if opts.RequireService && owner != "" {
		return fmt.Sprintf("%v/%v", owner, opts.getEnvName(name))
	}
	return opts.getEnvName(name)
}

// Returns the md5 hash of the file at the given path as a string
func hashFile(path string) (string, error) {
	file, err := os.Open(path)
	if err != nil {
		return "", err
	}
	defer file.Close()
	buf := make([]byte, 32*1024)
	hash := sha256.New()
	for {
		n, err := file.Read(buf)
		if n > 0 {
			_, err := hash.Write(buf[:n])
			if err != nil {
				return "", err
			}
		}
		if err == io.EOF {
			break
		}
		if err != nil {
			return "", err
		}
	}
	sum := string(hash.Sum(nil))
	return sum, nil
}

// GetStackNameWithOwner gets the name of the stack prepended with an owner, if PULUMI_TEST_OWNER is set.
// We use this in CI to create test stacks in an organization that all developers have access to, for debugging.
func (opts *ProgramTestOptions) GetStackNameWithOwner() tokens.QName {
	owner := os.Getenv("PULUMI_TEST_OWNER")

	if opts.RequireService && owner != "" {
		return tokens.QName(fmt.Sprintf("%s/%s", owner, opts.GetStackName()))
	}

	return opts.GetStackName()
}

// With combines a source set of options with a set of overrides.
func (opts ProgramTestOptions) With(overrides ProgramTestOptions) ProgramTestOptions {
	if overrides.Dir != "" {
		opts.Dir = overrides.Dir
	}
	if overrides.Dependencies != nil {
		opts.Dependencies = overrides.Dependencies
	}
	if overrides.Overrides != nil {
		opts.Overrides = overrides.Overrides
	}
	if overrides.InstallDevReleases {
		opts.InstallDevReleases = overrides.InstallDevReleases
	}
	if len(overrides.CreateEnvironments) != 0 {
		opts.CreateEnvironments = append(opts.CreateEnvironments, overrides.CreateEnvironments...)
	}
	if len(overrides.Environments) != 0 {
		opts.Environments = append(opts.Environments, overrides.Environments...)
	}
	for k, v := range overrides.Config {
		if opts.Config == nil {
			opts.Config = make(map[string]string)
		}
		opts.Config[k] = v
	}
	for k, v := range overrides.Secrets {
		if opts.Secrets == nil {
			opts.Secrets = make(map[string]string)
		}
		opts.Secrets[k] = v
	}
	if overrides.OrderedConfig != nil {
		opts.OrderedConfig = append(opts.OrderedConfig, overrides.OrderedConfig...)
	}
	if overrides.SecretsProvider != "" {
		opts.SecretsProvider = overrides.SecretsProvider
	}
	if overrides.EditDirs != nil {
		opts.EditDirs = overrides.EditDirs
	}
	if overrides.ExtraRuntimeValidation != nil {
		opts.ExtraRuntimeValidation = overrides.ExtraRuntimeValidation
	}
	if overrides.RelativeWorkDir != "" {
		opts.RelativeWorkDir = overrides.RelativeWorkDir
	}
	if overrides.AllowEmptyPreviewChanges {
		opts.AllowEmptyPreviewChanges = overrides.AllowEmptyPreviewChanges
	}
	if overrides.AllowEmptyUpdateChanges {
		opts.AllowEmptyUpdateChanges = overrides.AllowEmptyUpdateChanges
	}
	if overrides.ExpectFailure {
		opts.ExpectFailure = overrides.ExpectFailure
	}
	if overrides.ExpectRefreshChanges {
		opts.ExpectRefreshChanges = overrides.ExpectRefreshChanges
	}
	if overrides.RetryFailedSteps {
		opts.RetryFailedSteps = overrides.RetryFailedSteps
	}
	if overrides.SkipRefresh {
		opts.SkipRefresh = overrides.SkipRefresh
	}
	if overrides.RequireEmptyPreviewAfterRefresh {
		opts.RequireEmptyPreviewAfterRefresh = overrides.RequireEmptyPreviewAfterRefresh
	}
	if overrides.SkipPreview {
		opts.SkipPreview = overrides.SkipPreview
	}
	if overrides.SkipUpdate {
		opts.SkipUpdate = overrides.SkipUpdate
	}
	if overrides.SkipExportImport {
		opts.SkipExportImport = overrides.SkipExportImport
	}
	if overrides.SkipEmptyPreviewUpdate {
		opts.SkipEmptyPreviewUpdate = overrides.SkipEmptyPreviewUpdate
	}
	if overrides.SkipStackRemoval {
		opts.SkipStackRemoval = overrides.SkipStackRemoval
	}
	if overrides.DestroyOnCleanup {
		opts.DestroyOnCleanup = overrides.DestroyOnCleanup
	}
	if overrides.DestroyExcludeProtected {
		opts.DestroyExcludeProtected = overrides.DestroyExcludeProtected
	}
	if overrides.Quick {
		opts.Quick = overrides.Quick
	}
	if overrides.RequireService {
		opts.RequireService = overrides.RequireService
	}
	if overrides.PreviewCommandlineFlags != nil {
		opts.PreviewCommandlineFlags = append(opts.PreviewCommandlineFlags, overrides.PreviewCommandlineFlags...)
	}
	if overrides.UpdateCommandlineFlags != nil {
		opts.UpdateCommandlineFlags = append(opts.UpdateCommandlineFlags, overrides.UpdateCommandlineFlags...)
	}
	if overrides.QueryCommandlineFlags != nil {
		opts.QueryCommandlineFlags = append(opts.QueryCommandlineFlags, overrides.QueryCommandlineFlags...)
	}
	if overrides.RunBuild {
		opts.RunBuild = overrides.RunBuild
	}
	if overrides.RunUpdateTest {
		opts.RunUpdateTest = overrides.RunUpdateTest
	}
	if overrides.DecryptSecretsInOutput {
		opts.DecryptSecretsInOutput = overrides.DecryptSecretsInOutput
	}
	if overrides.CloudURL != "" {
		opts.CloudURL = overrides.CloudURL
	}
	if overrides.StackName != "" {
		opts.StackName = overrides.StackName
	}
	if overrides.Tracing != "" {
		opts.Tracing = overrides.Tracing
	}
	if overrides.NoParallel {
		opts.NoParallel = overrides.NoParallel
	}
	if overrides.PrePulumiCommand != nil {
		opts.PrePulumiCommand = overrides.PrePulumiCommand
	}
	if overrides.ReportStats != nil {
		opts.ReportStats = overrides.ReportStats
	}
	if overrides.Stdout != nil {
		opts.Stdout = overrides.Stdout
	}
	if overrides.Stderr != nil {
		opts.Stderr = overrides.Stderr
	}
	if overrides.Verbose {
		opts.Verbose = overrides.Verbose
	}
	if overrides.DebugLogLevel != 0 {
		opts.DebugLogLevel = overrides.DebugLogLevel
	}
	if overrides.DebugUpdates {
		opts.DebugUpdates = overrides.DebugUpdates
	}
	if overrides.Bin != "" {
		opts.Bin = overrides.Bin
	}
	if overrides.YarnBin != "" {
		opts.YarnBin = overrides.YarnBin
	}
	if overrides.GoBin != "" {
		opts.GoBin = overrides.GoBin
	}
	if overrides.PipenvBin != "" {
		opts.PipenvBin = overrides.PipenvBin
	}
	if overrides.DotNetBin != "" {
		opts.DotNetBin = overrides.DotNetBin
	}
	if overrides.Env != nil {
		opts.Env = append(opts.Env, overrides.Env...)
	}
	if overrides.UseAutomaticVirtualEnv {
		opts.UseAutomaticVirtualEnv = overrides.UseAutomaticVirtualEnv
	}
	if overrides.UsePipenv {
		opts.UsePipenv = overrides.UsePipenv
	}
	if overrides.UseSharedVirtualEnv != nil {
		opts.UseSharedVirtualEnv = overrides.UseSharedVirtualEnv
	}
	if overrides.SharedVirtualEnvPath != "" {
		opts.SharedVirtualEnvPath = overrides.SharedVirtualEnvPath
	}
	if overrides.PreviewCompletedHook != nil {
		opts.PreviewCompletedHook = overrides.PreviewCompletedHook
	}
	if overrides.JSONOutput {
		opts.JSONOutput = overrides.JSONOutput
	}
	if overrides.ExportStateValidator != nil {
		opts.ExportStateValidator = overrides.ExportStateValidator
	}
	if overrides.PrepareProject != nil {
		opts.PrepareProject = overrides.PrepareProject
	}
	if overrides.PostPrepareProject != nil {
		opts.PostPrepareProject = overrides.PostPrepareProject
	}
	if overrides.LocalProviders != nil {
		opts.LocalProviders = append(opts.LocalProviders, overrides.LocalProviders...)
	}
	return opts
}

type regexFlag struct {
	re *regexp.Regexp
}

func (rf *regexFlag) String() string {
	if rf.re == nil {
		return ""
	}
	return rf.re.String()
}

func (rf *regexFlag) Set(v string) error {
	r, err := regexp.Compile(v)
	if err != nil {
		return err
	}
	rf.re = r
	return nil
}

var (
	directoryMatcher regexFlag
	listDirs         bool
	pipMutex         *fsutil.FileMutex
)

func init() {
	flag.Var(&directoryMatcher, "dirs", "optional list of regexes to use to select integration tests to run")
	flag.BoolVar(&listDirs, "list-dirs", false, "list available integration tests without running them")

	mutexPath := filepath.Join(os.TempDir(), "pip-mutex.lock")
	pipMutex = fsutil.NewFileMutex(mutexPath)
}

// GetLogs retrieves the logs for a given stack in a particular region making the query provided.
//
// [provider] should be one of "aws" or "azure"
func GetLogs(
	t *testing.T,
	provider, region string,
	stackInfo RuntimeValidationStackInfo,
	query operations.LogQuery,
) *[]operations.LogEntry {
	snap, err := stack.DeserializeDeploymentV3(
		context.Background(),
		*stackInfo.Deployment,
		stack.DefaultSecretsProvider)
	assert.NoError(t, err)

	tree := operations.NewResourceTree(snap.Resources)
	if !assert.NotNil(t, tree) {
		return nil
	}

	cfg := map[config.Key]string{
		config.MustMakeKey(provider, "region"): region,
	}
	ops := tree.OperationsProvider(cfg)

	// Validate logs from example
	logs, err := ops.GetLogs(query)
	if !assert.NoError(t, err) {
		return nil
	}

	return logs
}

func prepareProgram(t *testing.T, opts *ProgramTestOptions) {
	// If we're just listing tests, simply print this test's directory.
	if listDirs {
		fmt.Printf("%s\n", opts.Dir)
	}

	// If we have a matcher, ensure that this test matches its pattern.
	if directoryMatcher.re != nil && !directoryMatcher.re.Match([]byte(opts.Dir)) {
		t.Skipf("Skipping: '%v' does not match '%v'", opts.Dir, directoryMatcher.re)
	}

	// Disable stack backups for tests to avoid filling up ~/.pulumi/backups with unnecessary
	// backups of test stacks.
	disableCheckpointBackups := env.DIYBackendDisableCheckpointBackups.Var().Name()
	opts.Env = append(opts.Env, disableCheckpointBackups+"=1")

	// We want tests to default into being ran in parallel, hence the odd double negative.
	if !opts.NoParallel && !opts.DestroyOnCleanup {
		t.Parallel()
	}

	if os.Getenv("PULUMI_TEST_USE_SERVICE") == "true" {
		opts.RequireService = true
	}
	if opts.RequireService {
		// This token is set in CI jobs, so this escape hatch is here to enable a smooth local dev
		// experience, i.e.: running "make" and not seeing many failures due to a missing token.
		if os.Getenv("PULUMI_ACCESS_TOKEN") == "" {
			t.Skipf("Skipping: PULUMI_ACCESS_TOKEN is not set")
		}
	} else if opts.CloudURL == "" {
		opts.CloudURL = MakeTempBackend(t)
	}

	// If the test panics, recover and log instead of letting the panic escape the test. Even though *this* test will
	// have run deferred functions and cleaned up, if the panic reaches toplevel it will kill the process and prevent
	// other tests running in parallel from cleaning up.
	defer func() {
		if failure := recover(); failure != nil {
			t.Errorf("panic testing %v: %v", opts.Dir, failure)
		}
	}()

	// Set up some default values for sending test reports and tracing data. We use environment varaiables to
	// control these globally and set reasonable values for our own use in CI.
	if opts.ReportStats == nil {
		if v := os.Getenv("PULUMI_TEST_REPORT_CONFIG"); v != "" {
			splits := strings.Split(v, ":")
			if len(splits) != 3 {
				t.Errorf("report config should be set to a value of the form: <aws-region>:<bucket-name>:<keyPrefix>")
			}

			opts.ReportStats = NewS3Reporter(splits[0], splits[1], splits[2])
		}
	}

	if opts.Tracing == "" {
		opts.Tracing = os.Getenv("PULUMI_TEST_TRACE_ENDPOINT")
	}

	if opts.UseSharedVirtualEnv == nil {
		if sharedVenv := os.Getenv("PULUMI_TEST_PYTHON_SHARED_VENV"); sharedVenv != "" {
			useSharedVenvBool := sharedVenv == "true"
			opts.UseSharedVirtualEnv = &useSharedVenvBool
		}
	}

	if opts.virtualEnvDir == "" && !opts.GetUseSharedVirtualEnv() {
		opts.virtualEnvDir = "venv"
	}

	if opts.SharedVirtualEnvPath == "" {
		opts.SharedVirtualEnvPath = filepath.Join(os.Getenv("HOME"), ".pulumi-test-venvs")
		if sharedVenvPath := os.Getenv("PULUMI_TEST_PYTHON_SHARED_VENV_PATH"); sharedVenvPath != "" {
			opts.SharedVirtualEnvPath = sharedVenvPath
		}
	}

	if opts.Quick {
		opts.SkipPreview = true
		opts.SkipExportImport = true
		opts.SkipEmptyPreviewUpdate = true
	}
}

// ProgramTest runs a lifecycle of Pulumi commands in a program working directory, using the `pulumi` and `yarn`
// binaries available on PATH.  It essentially executes the following workflow:
//
//	yarn install
//	yarn link <each opts.Depencies>
//	(+) yarn run build
//	pulumi init
//	(*) pulumi login
//	pulumi stack init integrationtesting
//	pulumi config set <each opts.Config>
//	pulumi config set --secret <each opts.Secrets>
//	pulumi preview
//	pulumi up
//	pulumi stack export --file stack.json
//	pulumi stack import --file stack.json
//	pulumi preview (expected to be empty)
//	pulumi up (expected to be empty)
//	pulumi destroy --yes
//	pulumi stack rm --yes integrationtesting
//
//	(*) Only if PULUMI_ACCESS_TOKEN is set.
//	(+) Only if `opts.RunBuild` is true.
//
// All commands must return success return codes for the test to succeed, unless ExpectFailure is true.
func ProgramTest(t *testing.T, opts *ProgramTestOptions) {
	prepareProgram(t, opts)
	pt := newProgramTester(t, opts)
	err := pt.TestLifeCycleInitAndDestroy()
	if !errors.Is(err, ErrTestFailed) {
		assert.NoError(t, err)
	}
}

// ProgramTestManualLifeCycle returns a ProgramTester than must be manually controlled in terms of its lifecycle
func ProgramTestManualLifeCycle(t *testing.T, opts *ProgramTestOptions) *ProgramTester {
	prepareProgram(t, opts)
	pt := newProgramTester(t, opts)
	return pt
}

// ProgramTester contains state associated with running a single test pass.
type ProgramTester struct {
	t              *testing.T          // the Go tester for this run.
	opts           *ProgramTestOptions // options that control this test run.
	bin            string              // the `pulumi` binary we are using.
	yarnBin        string              // the `yarn` binary we are using.
	goBin          string              // the `go` binary we are using.
	pythonBin      string              // the `python` binary we are using.
	pipenvBin      string              // The `pipenv` binary we are using.
	dotNetBin      string              // the `dotnet` binary we are using.
	updateEventLog string              // The path to the engine event log for `pulumi up` in this test.
	maxStepTries   int                 // The maximum number of times to retry a failed pulumi step.
	tmpdir         string              // the temporary directory we use for our test environment
	projdir        string              // the project directory we use for this run
	TestFinished   bool                // whether or not the test if finished
	pulumiHome     string              // The directory PULUMI_HOME will be set to
}

func newProgramTester(t *testing.T, opts *ProgramTestOptions) *ProgramTester {
	stackName := opts.GetStackName()
	maxStepTries := 1
	if opts.RetryFailedSteps {
		maxStepTries = 3
	}
	home, err := os.MkdirTemp("", "test-env-home")
	assert.NoError(t, err, "creating temp PULUMI_HOME directory")
	return &ProgramTester{
		t:              t,
		opts:           opts,
		updateEventLog: filepath.Join(os.TempDir(), string(stackName)+"-events.json"),
		maxStepTries:   maxStepTries,
		pulumiHome:     home,
	}
}

// MakeTempBackend creates a temporary backend directory which will clean up on test exit.
func MakeTempBackend(t *testing.T) string {
	tempDir := t.TempDir()
	return "file://" + filepath.ToSlash(tempDir)
}

func (pt *ProgramTester) getBin() (string, error) {
	return getCmdBin(&pt.bin, "pulumi", pt.opts.Bin)
}

func (pt *ProgramTester) getYarnBin() (string, error) {
	return getCmdBin(&pt.yarnBin, "yarn", pt.opts.YarnBin)
}

func (pt *ProgramTester) getGoBin() (string, error) {
	return getCmdBin(&pt.goBin, "go", pt.opts.GoBin)
}

// getPythonBin returns a path to the currently-installed `python` binary, or an error if it could not be found.
func (pt *ProgramTester) getPythonBin() (string, error) {
	if pt.pythonBin == "" {
		pt.pythonBin = pt.opts.PythonBin
		if pt.opts.PythonBin == "" {
			var err error
			// Look for `python3` by default, but fallback to `python` if not found, except on Windows
			// where we look for these in the reverse order because the default python.org Windows
			// installation does not include a `python3` binary, and the existence of a `python3.exe`
			// symlink to `python.exe` on some systems does not work correctly with the Python `venv`
			// module.
			pythonCmds := []string{"python3", "python"}
			if runtime.GOOS == windowsOS {
				pythonCmds = []string{"python", "python3"}
			}
			for _, bin := range pythonCmds {
				pt.pythonBin, err = exec.LookPath(bin)
				// Break on the first cmd we find on the path (if any).
				if err == nil {
					break
				}
			}
			if err != nil {
				return "", fmt.Errorf("Expected to find one of %q on $PATH: %w", pythonCmds, err)
			}
		}
	}
	return pt.pythonBin, nil
}

// getPipenvBin returns a path to the currently-installed Pipenv tool, or an error if the tool could not be found.
func (pt *ProgramTester) getPipenvBin() (string, error) {
	return getCmdBin(&pt.pipenvBin, "pipenv", pt.opts.PipenvBin)
}

func (pt *ProgramTester) getDotNetBin() (string, error) {
	return getCmdBin(&pt.dotNetBin, "dotnet", pt.opts.DotNetBin)
}

func (pt *ProgramTester) pulumiCmd(name string, args []string) ([]string, error) {
	bin, err := pt.getBin()
	if err != nil {
		return nil, err
	}
	cmd := []string{bin}
	if du := pt.opts.GetDebugLogLevel(); du > 0 {
		cmd = append(cmd, "--logflow", "--logtostderr", "-v="+strconv.Itoa(du))
	}
	cmd = append(cmd, args...)
	if tracing := pt.opts.Tracing; tracing != "" {
		cmd = append(cmd, "--tracing", strings.ReplaceAll(tracing, "{command}", name))
	}
	return cmd, nil
}

func (pt *ProgramTester) yarnCmd(args []string) ([]string, error) {
	bin, err := pt.getYarnBin()
	if err != nil {
		return nil, err
	}
	result := []string{bin}
	result = append(result, args...)
	return withOptionalYarnFlags(result), nil
}

func (pt *ProgramTester) pythonCmd(args []string) ([]string, error) {
	bin, err := pt.getPythonBin()
	if err != nil {
		return nil, err
	}

	cmd := []string{bin}
	return append(cmd, args...), nil
}

func (pt *ProgramTester) pipenvCmd(args []string) ([]string, error) {
	bin, err := pt.getPipenvBin()
	if err != nil {
		return nil, err
	}

	cmd := []string{bin}
	return append(cmd, args...), nil
}

func (pt *ProgramTester) runCommand(name string, args []string, wd string) error {
	return RunCommandPulumiHome(pt.t, name, args, wd, pt.opts, pt.pulumiHome)
}

// RunPulumiCommand runs a Pulumi command in the project directory.
// For example:
//
//	pt.RunPulumiCommand("preview", "--stack", "dev")
func (pt *ProgramTester) RunPulumiCommand(name string, args ...string) error {
	// pt.runPulumiCommand uses 'name' for logging only.
	// We want it to be part of the actual command.
	args = append([]string{name}, args...)
	return pt.runPulumiCommand(name, args, pt.projdir, false /* expectFailure */)
}

func (pt *ProgramTester) runPulumiCommand(name string, args []string, wd string, expectFailure bool) error {
	cmd, err := pt.pulumiCmd(name, args)
	if err != nil {
		return err
	}

	var postFn func(error) error
	if pt.opts.PrePulumiCommand != nil {
		postFn, err = pt.opts.PrePulumiCommand(args[0])
		if err != nil {
			return err
		}
	}

	isUpdate := args[0] == "preview" || args[0] == "up" || args[0] == "destroy" || args[0] == "refresh"

	// If we're doing a preview or an update and this project is a Python project, we need to run
	// the command in the context of the virtual environment that Pipenv created in order to pick up
	// the correct version of Python.  We also need to do this for destroy and refresh so that
	// dynamic providers are run in the right virtual environment.
	// This is only necessary when not using automatic virtual environment support.
	if pt.opts.UsePipenv && isUpdate {
		projinfo, err := pt.getProjinfo(wd)
		if err != nil {
			return nil
		}

		if projinfo.Proj.Runtime.Name() == "python" {
			pipenvBin, err := pt.getPipenvBin()
			if err != nil {
				return err
			}

			// "pipenv run" activates the current virtual environment and runs the remainder of the arguments as if it
			// were a command.
			cmd = append([]string{pipenvBin, "run"}, cmd...)
		}
	}

	_, _, err = retry.Until(context.Background(), retry.Acceptor{
		Accept: func(try int, nextRetryTime time.Duration) (bool, interface{}, error) {
			runerr := pt.runCommand(name, cmd, wd)
			if runerr == nil {
				return true, nil, nil
			} else if _, ok := runerr.(*exec.ExitError); ok && isUpdate && !expectFailure {
				// the update command failed, let's try again, assuming we haven't failed a few times.
				if try+1 >= pt.maxStepTries {
					return false, nil, fmt.Errorf("%v did not succeed after %v tries", cmd, try+1)
				}

				pt.t.Logf("%v failed: %v; retrying...", cmd, runerr)
				return false, nil, nil
			}

			// some other error, fail
			return false, nil, runerr
		},
	})
	if postFn != nil {
		if postErr := postFn(err); postErr != nil {
			return multierror.Append(err, postErr)
		}
	}
	return err
}

func (pt *ProgramTester) runYarnCommand(name string, args []string, wd string) error {
	// Yarn will time out if multiple processes are trying to install packages at the same time.
	ptesting.YarnInstallMutex.Lock()
	defer ptesting.YarnInstallMutex.Unlock()
	pt.t.Log("acquired yarn install lock")
	defer pt.t.Log("released yarn install lock")

	cmd, err := pt.yarnCmd(args)
	if err != nil {
		return err
	}

	_, _, err = retry.Until(context.Background(), retry.Acceptor{
		Accept: func(try int, nextRetryTime time.Duration) (bool, interface{}, error) {
			runerr := pt.runCommand(name, cmd, wd)
			if runerr == nil {
				return true, nil, nil
			} else if _, ok := runerr.(*exec.ExitError); ok {
				// yarn failed, let's try again, assuming we haven't failed a few times.
				if try+1 >= 3 {
					return false, nil, fmt.Errorf("%v did not complete after %v tries", cmd, try+1)
				}

				return false, nil, nil
			}

			// someother error, fail
			return false, nil, runerr
		},
	})
	return err
}

func (pt *ProgramTester) runPythonCommand(name string, args []string, wd string) error {
	cmd, err := pt.pythonCmd(args)
	if err != nil {
		return err
	}

	return pt.runCommand(name, cmd, wd)
}

func (pt *ProgramTester) runVirtualEnvCommand(name string, args []string, wd string) error {
	// When installing with `pip install -e`, a PKG-INFO file is created. If two packages are being installed
	// this way simultaneously (which happens often, when running tests), both installations will be writing the
	// same file simultaneously. If one process catches "PKG-INFO" in a half-written state, the one process that
	// observed the torn write will fail to install the package.
	//
	// To avoid this problem, we use pipMutex to explicitly serialize installation operations. Doing so avoids
	// the problem of multiple processes stomping on the same files in the source tree. Note that pipMutex is a
	// file mutex, so this strategy works even if the go test runner chooses to split up text execution across
	// multiple processes. (Furthermore, each test gets an instance of ProgramTester and thus the mutex, so we'd
	// need to be sharing the mutex globally in each test process if we weren't using the file system to lock.)
	if name == "virtualenv-pip-install-package" {
		if err := pipMutex.Lock(); err != nil {
			panic(err)
		}

		if pt.opts.Verbose {
			pt.t.Log("acquired pip install lock")
			defer pt.t.Log("released pip install lock")
		}
		defer func() {
			if err := pipMutex.Unlock(); err != nil {
				panic(err)
			}
		}()
	}

	virtualenvBinPath, err := getVirtualenvBinPath(wd, args[0], pt)
	if err != nil {
		return err
	}

	cmd := append([]string{virtualenvBinPath}, args[1:]...)
	return pt.runCommand(name, cmd, wd)
}

func (pt *ProgramTester) runPipenvCommand(name string, args []string, wd string) error {
	// Pipenv uses setuptools to install and uninstall packages. Setuptools has an installation mode called "develop"
	// that we use to install the package being tested, since it is 1) lightweight and 2) not doing so has its own set
	// of annoying problems.
	//
	// Setuptools develop does three things:
	//   1. It invokes the "egg_info" command in the target package,
	//   2. It creates a special `.egg-link` sentinel file in the current site-packages folder, pointing to the package
	//      being installed's path on disk
	//   3. It updates easy-install.pth in site-packages so that pip understand that this package has been installed.
	//
	// Steps 2 and 3 operate entirely within the context of a virtualenv. The state that they mutate is fully contained
	// within the current virtualenv. However, step 1 operates in the context of the package's source tree. Egg info
	// is responsible for producing a minimal "egg" for a particular package, and its largest responsibility is creating
	// a PKG-INFO file for a package. PKG-INFO contains, among other things, the version of the package being installed.
	//
	// If two packages are being installed in "develop" mode simultaneously (which happens often, when running tests),
	// both installations will run "egg_info" on the source tree and both processes will be writing the same files
	// simultaneously. If one process catches "PKG-INFO" in a half-written state, the one process that observed the
	// torn write will fail to install the package (setuptools crashes).
	//
	// To avoid this problem, we use pipMutex to explicitly serialize installation operations. Doing so avoids the
	// problem of multiple processes stomping on the same files in the source tree. Note that pipMutex is a file
	// mutex, so this strategy works even if the go test runner chooses to split up text execution across multiple
	// processes. (Furthermore, each test gets an instance of ProgramTester and thus the mutex, so we'd need to be
	// sharing the mutex globally in each test process if we weren't using the file system to lock.)
	if name == "pipenv-install-package" {
		if err := pipMutex.Lock(); err != nil {
			panic(err)
		}

		if pt.opts.Verbose {
			pt.t.Log("acquired pip install lock")
			defer pt.t.Log("released pip install lock")
		}
		defer func() {
			if err := pipMutex.Unlock(); err != nil {
				panic(err)
			}
		}()
	}

	cmd, err := pt.pipenvCmd(args)
	if err != nil {
		return err
	}

	return pt.runCommand(name, cmd, wd)
}

// TestLifeCyclePrepare prepares a test by creating a temporary directory
func (pt *ProgramTester) TestLifeCyclePrepare() error {
	tmpdir, projdir, err := pt.copyTestToTemporaryDirectory()
	pt.tmpdir = tmpdir
	pt.projdir = projdir
	return err
}

func (pt *ProgramTester) checkTestFailure() error {
	if pt.t.Failed() {
		pt.t.Logf("Canceling further steps due to test failure")
		return ErrTestFailed
	}
	return nil
}

// TestCleanUp cleans up the temporary directory that a test used
func (pt *ProgramTester) TestCleanUp() {
	testFinished := pt.TestFinished
	if pt.tmpdir != "" {
		if !testFinished || pt.t.Failed() {
			// Test aborted or failed. Maybe copy to "failed tests" directory.
			failedTestsDir := os.Getenv("PULUMI_FAILED_TESTS_DIR")
			if failedTestsDir != "" {
				dest := filepath.Join(failedTestsDir, pt.t.Name()+uniqueSuffix())
				contract.IgnoreError(fsutil.CopyFile(dest, pt.tmpdir, nil))
			}
		} else {
			contract.IgnoreError(os.RemoveAll(pt.tmpdir))
		}
	} else {
		// When tmpdir is empty, we ran "in tree", which means we wrote output
		// to the "command-output" folder in the projdir, and we should clean
		// it up if the test passed
		if testFinished && !pt.t.Failed() {
			contract.IgnoreError(os.RemoveAll(filepath.Join(pt.projdir, commandOutputFolderName)))
		}
	}
}

// TestLifeCycleInitAndDestroy executes the test and cleans up
func (pt *ProgramTester) TestLifeCycleInitAndDestroy() error {
	err := pt.TestLifeCyclePrepare()
	if err != nil {
		return fmt.Errorf("copying test to temp dir %s: %w", pt.tmpdir, err)
	}

	pt.TestFinished = false
	if pt.opts.DestroyOnCleanup {
		pt.t.Cleanup(pt.TestCleanUp)
	} else {
		defer pt.TestCleanUp()
	}

	err = pt.TestLifeCycleInitialize()
	if err != nil {
		return fmt.Errorf("initializing test project: %w", err)
	}

	destroyStack := func() {
		destroyErr := pt.TestLifeCycleDestroy()
		assert.NoError(pt.t, destroyErr)
	}
	if pt.opts.DestroyOnCleanup {
		// Allow other tests to refer to this stack until the test is complete.
		pt.t.Cleanup(destroyStack)
	} else {
		// Ensure that before we exit, we attempt to destroy and remove the stack.
		defer destroyStack()
	}

	if err = pt.TestPreviewUpdateAndEdits(); err != nil {
		return fmt.Errorf("running test preview, update, and edits: %w", err)
	}

	if pt.opts.RunUpdateTest {
		err = upgradeProjectDeps(pt.projdir, pt)
		if err != nil {
			return fmt.Errorf("upgrading project dependencies: %w", err)
		}

		if err = pt.TestPreviewUpdateAndEdits(); err != nil {
			return fmt.Errorf("running test preview, update, and edits (updateTest): %w", err)
		}
	}

	pt.TestFinished = true
	return nil
}

func upgradeProjectDeps(projectDir string, pt *ProgramTester) error {
	projInfo, err := pt.getProjinfo(projectDir)
	if err != nil {
		return fmt.Errorf("getting project info: %w", err)
	}

	switch rt := projInfo.Proj.Runtime.Name(); rt {
	case NodeJSRuntime:
		if err = pt.yarnLinkPackageDeps(projectDir); err != nil {
			return err
		}
	case PythonRuntime:
		if err = pt.installPipPackageDeps(projectDir); err != nil {
			return err
		}
	default:
		return fmt.Errorf("unrecognized project runtime: %s", rt)
	}

	return nil
}

// TestLifeCycleInitialize initializes the project directory and stack along with any configuration
func (pt *ProgramTester) TestLifeCycleInitialize() error {
	dir := pt.projdir
	stackName := pt.opts.GetStackName()

	// Set the default target Pulumi API if not overridden in options.
	if pt.opts.CloudURL == "" {
		pulumiAPI := os.Getenv("PULUMI_API")
		if pulumiAPI != "" {
			pt.opts.CloudURL = pulumiAPI
		}
	}

	// Ensure all links are present, the stack is created, and all configs are applied.
	pt.t.Logf("Initializing project (dir %s; stack %s)", dir, stackName)

	// Login as needed.
	stackInitName := string(pt.opts.GetStackNameWithOwner())

	if os.Getenv("PULUMI_ACCESS_TOKEN") == "" && pt.opts.CloudURL == "" {
		fmt.Printf("Using existing logged in user for tests.  Set PULUMI_ACCESS_TOKEN and/or PULUMI_API to override.\n")
	} else {
		// Set PulumiCredentialsPathEnvVar to our CWD, so we use credentials specific to just this
		// test.
		pt.opts.Env = append(pt.opts.Env, fmt.Sprintf("%s=%s", workspace.PulumiCredentialsPathEnvVar, dir))

		loginArgs := []string{"login"}
		loginArgs = addFlagIfNonNil(loginArgs, "--cloud-url", pt.opts.CloudURL)

		// If this is a local OR cloud login, then don't attach the owner to the stack-name.
		if pt.opts.CloudURL != "" {
			stackInitName = string(pt.opts.GetStackName())
		}

		if err := pt.runPulumiCommand("pulumi-login", loginArgs, dir, false); err != nil {
			return err
		}
	}

	// Stack init
	stackInitArgs := []string{"stack", "init", stackInitName}
	if pt.opts.SecretsProvider != "" {
		stackInitArgs = append(stackInitArgs, "--secrets-provider", pt.opts.SecretsProvider)
	}
	if err := pt.runPulumiCommand("pulumi-stack-init", stackInitArgs, dir, false); err != nil {
		return err
	}

	if len(pt.opts.Config)+len(pt.opts.Secrets) > 0 {
		setAllArgs := []string{"config", "set-all"}

		for key, value := range pt.opts.Config {
			setAllArgs = append(setAllArgs, "--plaintext", fmt.Sprintf("%s=%s", key, value))
		}
		for key, value := range pt.opts.Secrets {
			setAllArgs = append(setAllArgs, "--secret", fmt.Sprintf("%s=%s", key, value))
		}

		if err := pt.runPulumiCommand("pulumi-config", setAllArgs, dir, false); err != nil {
			return err
		}
	}

	for _, cv := range pt.opts.OrderedConfig {
		configArgs := []string{"config", "set", cv.Key, cv.Value}
		if cv.Secret {
			configArgs = append(configArgs, "--secret")
		}
		if cv.Path {
			configArgs = append(configArgs, "--path")
		}
		if err := pt.runPulumiCommand("pulumi-config", configArgs, dir, false); err != nil {
			return err
		}
	}

	// Environments
	for _, env := range pt.opts.CreateEnvironments {
		name := pt.opts.getEnvNameWithOwner(env.Name)

		envFile, err := func() (string, error) {
			temp, err := os.CreateTemp(pt.t.TempDir(), fmt.Sprintf("pulumi-env-%v-*", env.Name))
			if err != nil {
				return "", err
			}
			defer contract.IgnoreClose(temp)

			enc := yaml.NewEncoder(temp)
			enc.SetIndent(2)
			if err = enc.Encode(env.Definition); err != nil {
				return "", err
			}
			return temp.Name(), nil
		}()
		if err != nil {
			return err
		}

		initArgs := []string{"env", "init", name, "-f", envFile}
		if err := pt.runPulumiCommand("pulumi-env-init", initArgs, dir, false); err != nil {
			return err
		}
	}

	if len(pt.opts.Environments) != 0 {
		envs := make([]string, len(pt.opts.Environments))
		for i, e := range pt.opts.Environments {
			envs[i] = pt.opts.getEnvName(e)
		}

		stackFile := filepath.Join(dir, fmt.Sprintf("Pulumi.%v.yaml", stackName))
		bytes, err := os.ReadFile(stackFile)
		if err != nil && !os.IsNotExist(err) {
			return err
		}

		var stack workspace.ProjectStack
		if err := yaml.Unmarshal(bytes, &stack); err != nil {
			return err
		}
		stack.Environment = workspace.NewEnvironment(envs)

		bytes, err = yaml.Marshal(stack)
		if err != nil {
			return err
		}

		if err = os.WriteFile(stackFile, bytes, 0o600); err != nil {
			return err
		}
	}

	return nil
}

// TestLifeCycleDestroy destroys a stack and removes it
func (pt *ProgramTester) TestLifeCycleDestroy() error {
	if pt.projdir != "" {
		// Destroy and remove the stack.
		pt.t.Log("Destroying stack")
		destroy := []string{"destroy", "--non-interactive", "--yes", "--skip-preview"}
		if pt.opts.GetDebugUpdates() {
			destroy = append(destroy, "-d")
		}
		if pt.opts.JSONOutput {
			destroy = append(destroy, "--json")
		}
		if pt.opts.DestroyExcludeProtected {
			destroy = append(destroy, "--exclude-protected")
		}
		if err := pt.runPulumiCommand("pulumi-destroy", destroy, pt.projdir, false); err != nil {
			return err
		}

		if pt.t.Failed() {
			pt.t.Logf("Test failed, retaining stack '%s'", pt.opts.GetStackNameWithOwner())
			return nil
		}

		if !pt.opts.SkipStackRemoval {
			err := pt.runPulumiCommand("pulumi-stack-rm", []string{"stack", "rm", "--yes"}, pt.projdir, false)
			if err != nil {
				return err
			}
		}

		for _, env := range pt.opts.CreateEnvironments {
			name := pt.opts.getEnvNameWithOwner(env.Name)
			err := pt.runPulumiCommand("pulumi-env-rm", []string{"env", "rm", "--yes", name}, pt.projdir, false)
			if err != nil {
				return err
			}
		}
	}
	return nil
}

// TestPreviewUpdateAndEdits runs the preview, update, and any relevant edits
func (pt *ProgramTester) TestPreviewUpdateAndEdits() error {
	dir := pt.projdir
	// Now preview and update the real changes.
	pt.t.Log("Performing primary preview and update")
	initErr := pt.PreviewAndUpdate(dir, "initial", pt.opts.ExpectFailure, false, false)

	// If the initial preview/update failed, just exit without trying the rest (but make sure to destroy).
	if initErr != nil {
		return fmt.Errorf("initial failure: %w", initErr)
	}

	// Perform an empty preview and update; nothing is expected to happen here.
	if !pt.opts.SkipExportImport {
		pt.t.Log("Roundtripping checkpoint via stack export and stack import")

		if err := pt.exportImport(dir); err != nil {
			return fmt.Errorf("empty preview + update: %w", err)
		}
	}

	if !pt.opts.SkipEmptyPreviewUpdate {
		msg := ""
		if !pt.opts.AllowEmptyUpdateChanges {
			msg = "(no changes expected)"
		}
		pt.t.Logf("Performing empty preview and update%s", msg)
		if err := pt.PreviewAndUpdate(dir, "empty", pt.opts.ExpectFailure,
			!pt.opts.AllowEmptyPreviewChanges, !pt.opts.AllowEmptyUpdateChanges); err != nil {
			return fmt.Errorf("empty preview: %w", err)
		}
	}

	// Run additional validation provided by the test options, passing in the checkpoint info.
	if err := pt.performExtraRuntimeValidation(pt.opts.ExtraRuntimeValidation, dir); err != nil {
		return err
	}

	if !pt.opts.SkipRefresh {
		// Perform a refresh and ensure it doesn't yield changes.
		refresh := []string{"refresh", "--non-interactive", "--yes", "--skip-preview"}
		if pt.opts.GetDebugUpdates() {
			refresh = append(refresh, "-d")
		}
		if pt.opts.JSONOutput {
			refresh = append(refresh, "--json")
		}
		if !pt.opts.ExpectRefreshChanges {
			refresh = append(refresh, "--expect-no-changes")
		}
		if err := pt.runPulumiCommand("pulumi-refresh", refresh, dir, false); err != nil {
			return err
		}

		// Perform another preview and expect no changes in it.
		if pt.opts.RequireEmptyPreviewAfterRefresh {
			preview := []string{"preview", "--non-interactive", "--expect-no-changes"}
			if pt.opts.GetDebugUpdates() {
				preview = append(preview, "-d")
			}
			if pt.opts.JSONOutput {
				preview = append(preview, "--json")
			}
			if err := pt.runPulumiCommand("pulumi-preview-after-refresh", preview, dir, false); err != nil {
				return err
			}
		}
	}

	// If there are any edits, apply them and run a preview and update for each one.
	return pt.testEdits(dir)
}

func (pt *ProgramTester) exportImport(dir string) error {
	exportCmd := []string{"stack", "export", "--file", "stack.json"}
	importCmd := []string{"stack", "import", "--file", "stack.json"}

	defer func() {
		contract.IgnoreError(os.Remove(filepath.Join(dir, "stack.json")))
	}()

	if err := pt.runPulumiCommand("pulumi-stack-export", exportCmd, dir, false); err != nil {
		return err
	}

	if f := pt.opts.ExportStateValidator; f != nil {
		bytes, err := os.ReadFile(filepath.Join(dir, "stack.json"))
		if err != nil {
			pt.t.Logf("Failed to read stack.json: %s", err)
			return err
		}
		pt.t.Logf("Calling ExportStateValidator")
		f(pt.t, bytes)

		if err := pt.checkTestFailure(); err != nil {
			return err
		}
	}

	return pt.runPulumiCommand("pulumi-stack-import", importCmd, dir, false)
}

// PreviewAndUpdate runs pulumi preview followed by pulumi up
func (pt *ProgramTester) PreviewAndUpdate(dir string, name string, shouldFail, expectNopPreview,
	expectNopUpdate bool,
) error {
	preview := []string{"preview", "--non-interactive", "--diff"}
	update := []string{"up", "--non-interactive", "--yes", "--skip-preview", "--event-log", pt.updateEventLog}
	if pt.opts.GetDebugUpdates() {
		preview = append(preview, "-d")
		update = append(update, "-d")
	}
	if pt.opts.JSONOutput {
		preview = append(preview, "--json")
		update = append(update, "--json")
	}
	if expectNopPreview {
		preview = append(preview, "--expect-no-changes")
	}
	if expectNopUpdate {
		update = append(update, "--expect-no-changes")
	}
	if pt.opts.PreviewCommandlineFlags != nil {
		preview = append(preview, pt.opts.PreviewCommandlineFlags...)
	}
	if pt.opts.UpdateCommandlineFlags != nil {
		update = append(update, pt.opts.UpdateCommandlineFlags...)
	}

	// If not in quick mode, run an explicit preview.
	if !pt.opts.SkipPreview {
		if err := pt.runPulumiCommand("pulumi-preview-"+name, preview, dir, shouldFail); err != nil {
			if shouldFail {
				pt.t.Log("Permitting failure (ExpectFailure=true for this preview)")
				return nil
			}
			return err
		}
		if pt.opts.PreviewCompletedHook != nil {
			if err := pt.opts.PreviewCompletedHook(dir); err != nil {
				return err
			}
		}
	}

	// Now run an update.
	if !pt.opts.SkipUpdate {
		if err := pt.runPulumiCommand("pulumi-update-"+name, update, dir, shouldFail); err != nil {
			if shouldFail {
				pt.t.Log("Permitting failure (ExpectFailure=true for this update)")
				return nil
			}
			return err
		}
	}

	// If we expected a failure, but none occurred, return an error.
	if shouldFail {
		return errors.New("expected this step to fail, but it succeeded")
	}

	return nil
}

func (pt *ProgramTester) query(dir string, name string, shouldFail bool) error {
	query := []string{"query", "--non-interactive"}
	if pt.opts.GetDebugUpdates() {
		query = append(query, "-d")
	}
	if pt.opts.QueryCommandlineFlags != nil {
		query = append(query, pt.opts.QueryCommandlineFlags...)
	}

	// Now run a query.
	if err := pt.runPulumiCommand("pulumi-query-"+name, query, dir, shouldFail); err != nil {
		if shouldFail {
			pt.t.Log("Permitting failure (ExpectFailure=true for this update)")
			return nil
		}
		return err
	}

	// If we expected a failure, but none occurred, return an error.
	if shouldFail {
		return errors.New("expected this step to fail, but it succeeded")
	}

	return nil
}

func (pt *ProgramTester) testEdits(dir string) error {
	for i, edit := range pt.opts.EditDirs {
		var err error
		if err = pt.testEdit(dir, i, edit); err != nil {
			return err
		}
	}
	return nil
}

func (pt *ProgramTester) testEdit(dir string, i int, edit EditDir) error {
	pt.t.Logf("Applying edit '%v' and rerunning preview and update", edit.Dir)

	if edit.Additive {
		// Just copy new files into dir
		if err := fsutil.CopyFile(dir, edit.Dir, nil); err != nil {
			return fmt.Errorf("Couldn't copy %v into %v: %w", edit.Dir, dir, err)
		}
	} else {
		// Create a new temporary directory
		newDir, err := os.MkdirTemp("", pt.opts.StackName+"-")
		if err != nil {
			return fmt.Errorf("Couldn't create new temporary directory: %w", err)
		}

		// Delete whichever copy of the test is unused when we return
		dirToDelete := newDir
		defer func() {
			contract.IgnoreError(os.RemoveAll(dirToDelete))
		}()

		// Copy everything except Pulumi.yaml, Pulumi.<stack-name>.yaml, and .pulumi from source into new directory
		exclusions := make(map[string]bool)
		projectYaml := workspace.ProjectFile + ".yaml"
		configYaml := workspace.ProjectFile + "." + pt.opts.StackName + ".yaml"
		exclusions[workspace.BookkeepingDir] = true
		exclusions[projectYaml] = true
		exclusions[configYaml] = true

		if err := fsutil.CopyFile(newDir, edit.Dir, exclusions); err != nil {
			return fmt.Errorf("Couldn't copy %v into %v: %w", edit.Dir, newDir, err)
		}

		// Copy Pulumi.yaml, Pulumi.<stack-name>.yaml, and .pulumi from old directory to new directory
		oldProjectYaml := filepath.Join(dir, projectYaml)
		newProjectYaml := filepath.Join(newDir, projectYaml)

		oldConfigYaml := filepath.Join(dir, configYaml)
		newConfigYaml := filepath.Join(newDir, configYaml)

		oldProjectDir := filepath.Join(dir, workspace.BookkeepingDir)
		newProjectDir := filepath.Join(newDir, workspace.BookkeepingDir)

		if err := fsutil.CopyFile(newProjectYaml, oldProjectYaml, nil); err != nil {
			return fmt.Errorf("Couldn't copy Pulumi.yaml: %w", err)
		}

		// Copy the config file over if it exists.
		//
		// Pulumi is not required to write a config file if there is no config, so
		// it might not.
		if _, err := os.Stat(oldConfigYaml); !os.IsNotExist(err) {
			if err := fsutil.CopyFile(newConfigYaml, oldConfigYaml, nil); err != nil {
				return fmt.Errorf("Couldn't copy Pulumi.%s.yaml: %w", pt.opts.StackName, err)
			}
		}

		// Likewise, pulumi is not required to write a book-keeping (.pulumi) file.
		if _, err := os.Stat(oldProjectDir); !os.IsNotExist(err) {
			if err := fsutil.CopyFile(newProjectDir, oldProjectDir, nil); err != nil {
				return fmt.Errorf("Couldn't copy .pulumi: %w", err)
			}
		}

		// Finally, replace our current temp directory with the new one.
		dirOld := dir + ".old"
		if err := os.Rename(dir, dirOld); err != nil {
			return fmt.Errorf("Couldn't rename %v to %v: %w", dir, dirOld, err)
		}

		// There's a brief window here where the old temp dir name could be taken from us.

		if err := os.Rename(newDir, dir); err != nil {
			return fmt.Errorf("Couldn't rename %v to %v: %w", newDir, dir, err)
		}

		// Keep dir, delete oldDir
		dirToDelete = dirOld
	}

	err := pt.prepareProjectDir(dir)
	if err != nil {
		return fmt.Errorf("Couldn't prepare project in %v: %w", dir, err)
	}

	oldStdOut := pt.opts.Stdout
	oldStderr := pt.opts.Stderr
	oldVerbose := pt.opts.Verbose
	if edit.Stdout != nil {
		pt.opts.Stdout = edit.Stdout
	}
	if edit.Stderr != nil {
		pt.opts.Stderr = edit.Stderr
	}
	if edit.Verbose {
		pt.opts.Verbose = true
	}

	defer func() {
		pt.opts.Stdout = oldStdOut
		pt.opts.Stderr = oldStderr
		pt.opts.Verbose = oldVerbose
	}()

	if !edit.QueryMode {
		if err = pt.PreviewAndUpdate(dir, fmt.Sprintf("edit-%d", i),
			edit.ExpectFailure, edit.ExpectNoChanges, edit.ExpectNoChanges); err != nil {
			return err
		}
	} else {
		if err = pt.query(dir, fmt.Sprintf("query-%d", i), edit.ExpectFailure); err != nil {
			return err
		}
	}
	return pt.performExtraRuntimeValidation(edit.ExtraRuntimeValidation, dir)
}

func (pt *ProgramTester) performExtraRuntimeValidation(
	extraRuntimeValidation func(t *testing.T, stack RuntimeValidationStackInfo), dir string,
) error {
	if extraRuntimeValidation == nil {
		return nil
	}

	stackName := pt.opts.GetStackName()

	// Create a temporary file name for the stack export
	tempDir, err := os.MkdirTemp("", string(stackName))
	if err != nil {
		return err
	}
	fileName := filepath.Join(tempDir, "stack.json")

	// Invoke `pulumi stack export`
	// There are situations where we want to get access to the secrets in the validation
	// this will allow us to get access to them as part of running ExtraRuntimeValidation
	var pulumiCommand []string
	if pt.opts.DecryptSecretsInOutput {
		pulumiCommand = append(pulumiCommand, "stack", "export", "--show-secrets", "--file", fileName)
	} else {
		pulumiCommand = append(pulumiCommand, "stack", "export", "--file", fileName)
	}
	if err = pt.runPulumiCommand("pulumi-export",
		pulumiCommand, dir, false); err != nil {
		return fmt.Errorf("expected to export stack to file: %s: %w", fileName, err)
	}

	// Open the exported JSON file
	f, err := os.Open(fileName)
	if err != nil {
		return fmt.Errorf("expected to be able to open file with stack exports: %s: %w", fileName, err)
	}
	defer func() {
		contract.IgnoreClose(f)
		contract.IgnoreError(os.RemoveAll(tempDir))
	}()

	// Unmarshal the Deployment
	var untypedDeployment apitype.UntypedDeployment
	if err = json.NewDecoder(f).Decode(&untypedDeployment); err != nil {
		return err
	}
	var deployment apitype.DeploymentV3
	if err = json.Unmarshal(untypedDeployment.Deployment, &deployment); err != nil {
		return err
	}

	// Get the root resource and outputs from the deployment
	var rootResource apitype.ResourceV3
	var outputs map[string]interface{}
	for _, res := range deployment.Resources {
		if res.Type == resource.RootStackType && res.Parent == "" {
			rootResource = res
			outputs = res.Outputs
		}
	}

	events, err := pt.readUpdateEventLog()
	if err != nil {
		return err
	}

	// Populate stack info object with all of this data to pass to the validation function
	stackInfo := RuntimeValidationStackInfo{
		StackName:    pt.opts.GetStackName(),
		Deployment:   &deployment,
		RootResource: rootResource,
		Outputs:      outputs,
		Events:       events,
	}

	pt.t.Log("Performing extra runtime validation.")
	extraRuntimeValidation(pt.t, stackInfo)
	pt.t.Log("Extra runtime validation complete.")

	return pt.checkTestFailure()
}

func (pt *ProgramTester) readUpdateEventLog() ([]apitype.EngineEvent, error) {
	events := []apitype.EngineEvent{}
	eventsFile, err := os.Open(pt.updateEventLog)
	if err != nil {
		if os.IsNotExist(err) {
			return events, nil
		}
		return events, fmt.Errorf("expected to be able to open event log file %s: %w",
			pt.updateEventLog, err)
	}

	defer contract.IgnoreClose(eventsFile)

	decoder := json.NewDecoder(eventsFile)
	for {
		var event apitype.EngineEvent
		if err = decoder.Decode(&event); err != nil {
			if err == io.EOF {
				break
			}
			return events, fmt.Errorf("failed decoding engine event from log file %s: %w",
				pt.updateEventLog, err)
		}
		events = append(events, event)
	}

	return events, nil
}

// copyTestToTemporaryDirectory creates a temporary directory to run the test in and copies the test to it.
func (pt *ProgramTester) copyTestToTemporaryDirectory() (string, string, error) {
	// Get the source dir and project info.
	sourceDir := pt.opts.Dir
	projSourceDir := sourceDir
	if wd := pt.opts.RelativeWorkDir; wd != "" {
		projSourceDir = filepath.Join(projSourceDir, wd)
	}
	projinfo, err := pt.getProjinfo(projSourceDir)
	if err != nil {
		return "", "", fmt.Errorf("could not get project info from source: %w", err)
	}

	if pt.opts.Stdout == nil {
		pt.opts.Stdout = os.Stdout
	}
	if pt.opts.Stderr == nil {
		pt.opts.Stderr = os.Stderr
	}

	pt.t.Logf("sample: %v", sourceDir)
	bin, err := pt.getBin()
	if err != nil {
		return "", "", err
	}
	pt.t.Logf("pulumi: %v\n", bin)

	stackName := string(pt.opts.GetStackName())

	// For most projects, we will copy to a temporary directory.  For Go projects, however, we must create
	// a folder structure that adheres to GOPATH requirements
	var tmpdir, projdir string
	if projinfo.Proj.Runtime.Name() == "go" {
		targetDir, err := tools.CreateTemporaryGoFolder("stackName")
		if err != nil {
			return "", "", fmt.Errorf("Couldn't create temporary directory: %w", err)
		}
		tmpdir = targetDir
		projdir = targetDir
	} else {
		targetDir, tempErr := os.MkdirTemp("", stackName+"-")
		if tempErr != nil {
			return "", "", fmt.Errorf("Couldn't create temporary directory: %w", tempErr)
		}
		tmpdir = targetDir
		projdir = targetDir
	}
	if wd := pt.opts.RelativeWorkDir; wd != "" {
		projdir = filepath.Join(projdir, wd)
	}
	// Copy the source project.
	if copyErr := fsutil.CopyFile(tmpdir, sourceDir, nil); copyErr != nil {
		return "", "", copyErr
	}
	// Reload the projinfo before making mutating changes (workspace.LoadProject caches the in-memory Project by path)
	projinfo, err = pt.getProjinfo(projdir)
	if err != nil {
		return "", "", fmt.Errorf("could not get project info: %w", err)
	}

	// Add dynamic plugin paths from ProgramTester
	if (projinfo.Proj.Plugins == nil || projinfo.Proj.Plugins.Providers == nil) && pt.opts.LocalProviders != nil {
		projinfo.Proj.Plugins = &workspace.Plugins{
			Providers: make([]workspace.PluginOptions, 0),
		}
	}

	if pt.opts.LocalProviders != nil {
		for _, provider := range pt.opts.LocalProviders {
			// LocalProviders are relative to the working directory when running tests, NOT relative to the
			// Pulumi.yaml. This is a bit odd, but makes it easier to construct the required paths in each
			// test.
			absPath, err := filepath.Abs(provider.Path)
			if err != nil {
				return "", "", fmt.Errorf("could not get absolute path for plugin %s: %w", provider.Path, err)
			}

			projinfo.Proj.Plugins.Providers = append(projinfo.Proj.Plugins.Providers, workspace.PluginOptions{
				Name: provider.Package,
				Path: absPath,
			})
		}
	}

	// Absolute path of the source directory, for fixupPath to use below
	absSource, err := filepath.Abs(sourceDir)
	if err != nil {
		return "", "", fmt.Errorf("could not get absolute path for source directory %s: %w", sourceDir, err)
	}

	// Return a fixed up path if it's relative to sourceDir but not beneath it, else just returns the input
	fixupPath := func(path string) (string, error) {
		if filepath.IsAbs(path) {
			return path, nil
		}
		absPlugin := filepath.Join(absSource, path)
		if !strings.HasPrefix(absPlugin, absSource+string(filepath.Separator)) {
			return absPlugin, nil
		}
		return path, nil
	}

	if projinfo.Proj.Plugins != nil {
		optionSets := [][]workspace.PluginOptions{
			projinfo.Proj.Plugins.Providers,
			projinfo.Proj.Plugins.Languages,
			projinfo.Proj.Plugins.Analyzers,
		}
		for _, options := range optionSets {
			for i, opt := range options {
				path, err := fixupPath(opt.Path)
				if err != nil {
					return "", "", fmt.Errorf("could not get fixed path for plugin %s: %w", opt.Path, err)
				}
				options[i].Path = path
			}
		}
	}
	projfile := filepath.Join(projdir, workspace.ProjectFile+".yaml")
	bytes, err := yaml.Marshal(projinfo.Proj)
	if err != nil {
		return "", "", fmt.Errorf("error marshalling project %q: %w", projfile, err)
	}

	if err := os.WriteFile(projfile, bytes, 0o600); err != nil {
		return "", "", fmt.Errorf("error writing project: %w", err)
	}

	err = pt.prepareProject(projinfo)
	if err != nil {
		return "", "", fmt.Errorf("Failed to prepare %v: %w", projdir, err)
	}

	if pt.opts.PostPrepareProject != nil {
		err = pt.opts.PostPrepareProject(projinfo)
		if err != nil {
			return "", "", fmt.Errorf("Failed to post-prepare %v: %w", projdir, err)
		}
	}

	// TODO[pulumi/pulumi#5455]: Dynamic providers fail to load when used from multi-lang components.
	// Until that's been fixed, this environment variable can be set by a test, which results in
	// a package.json being emitted in the project directory and `yarn install && yarn link @pulumi/pulumi`
	// being run.
	// When the underlying issue has been fixed, the use of this environment variable should be removed.
	var yarnLinkPulumi bool
	for _, env := range pt.opts.Env {
		if env == "PULUMI_TEST_YARN_LINK_PULUMI=true" {
			yarnLinkPulumi = true
			break
		}
	}
	if yarnLinkPulumi {
		const packageJSON = `{
			"name": "test",
			"peerDependencies": {
				"@pulumi/pulumi": "latest"
			}
		}`
		if err := os.WriteFile(filepath.Join(projdir, "package.json"), []byte(packageJSON), 0o600); err != nil {
			return "", "", err
		}
		if err := pt.runYarnCommand("yarn-link", []string{"link", "@pulumi/pulumi"}, projdir); err != nil {
			return "", "", err
		}
		if err = pt.runYarnCommand("yarn-install", []string{"install"}, projdir); err != nil {
			return "", "", err
		}
	}

	pt.t.Logf("projdir: %v", projdir)
	return tmpdir, projdir, nil
}

func (pt *ProgramTester) getProjinfo(projectDir string) (*engine.Projinfo, error) {
	// Load up the package so we know things like what language the project is.
	projfile := filepath.Join(projectDir, workspace.ProjectFile+".yaml")
	proj, err := workspace.LoadProject(projfile)
	if err != nil {
		return nil, err
	}
	return &engine.Projinfo{Proj: proj, Root: projectDir}, nil
}

// prepareProject runs setup necessary to get the project ready for `pulumi` commands.
func (pt *ProgramTester) prepareProject(projinfo *engine.Projinfo) error {
	if pt.opts.PrepareProject != nil {
		return pt.opts.PrepareProject(projinfo)
	}
	return pt.defaultPrepareProject(projinfo)
}

// prepareProjectDir runs setup necessary to get the project ready for `pulumi` commands.
func (pt *ProgramTester) prepareProjectDir(projectDir string) error {
	projinfo, err := pt.getProjinfo(projectDir)
	if err != nil {
		return err
	}
	return pt.prepareProject(projinfo)
}

// prepareNodeJSProject runs setup necessary to get a Node.js project ready for `pulumi` commands.
func (pt *ProgramTester) prepareNodeJSProject(projinfo *engine.Projinfo) error {
	if err := ptesting.WriteYarnRCForTest(projinfo.Root); err != nil {
		return err
	}

	// Get the correct pwd to run Yarn in.
	cwd, _, err := projinfo.GetPwdMain()
	if err != nil {
		return err
	}

	workspaceRoot, err := npm.FindWorkspaceRoot(cwd)
	if err != nil {
		if !errors.Is(err, npm.ErrNotInWorkspace) {
			return err
		}
		// Not in a workspace, don't updated cwd.
	} else {
		pt.t.Logf("detected yarn/npm workspace root at %s", workspaceRoot)
		cwd = workspaceRoot
	}

	// If dev versions were requested, we need to update the
	// package.json to use them.  Note that Overrides take
	// priority over installing dev versions.
	if pt.opts.InstallDevReleases {
		err := pt.runYarnCommand("yarn-add", []string{"add", "@pulumi/pulumi@dev"}, cwd)
		if err != nil {
			return err
		}
	}

	// If the test requested some packages to be overridden, we do two things. First, if the package is listed as a
	// direct dependency of the project, we change the version constraint in the package.json. For transitive
	// dependencies, we use yarn's "resolutions" feature to force them to a specific version.
	if len(pt.opts.Overrides) > 0 {
		packageJSON, err := readPackageJSON(cwd)
		if err != nil {
			return err
		}

		resolutions := make(map[string]interface{})

		for packageName, packageVersion := range pt.opts.Overrides {
			for _, section := range []string{"dependencies", "devDependencies"} {
				if _, has := packageJSON[section]; has {
					entry := packageJSON[section].(map[string]interface{})

					if _, has := entry[packageName]; has {
						entry[packageName] = packageVersion
					}

				}
			}

			pt.t.Logf("adding resolution for %s to version %s", packageName, packageVersion)
			resolutions["**/"+packageName] = packageVersion
		}

		// Wack any existing resolutions section with our newly computed one.
		packageJSON["resolutions"] = resolutions

		if err := writePackageJSON(cwd, packageJSON); err != nil {
			return err
		}
	}

	// Now ensure dependencies are present.
	if err = pt.runYarnCommand("yarn-install", []string{"install"}, cwd); err != nil {
		return err
	}

	if !pt.opts.RunUpdateTest {
		if err = pt.yarnLinkPackageDeps(cwd); err != nil {
			return err
		}
	}

	if pt.opts.RunBuild {
		// And finally compile it using whatever build steps are in the package.json file.
		if err = pt.runYarnCommand("yarn-build", []string{"run", "build"}, cwd); err != nil {
			return err
		}
	}

	return nil
}

// readPackageJSON unmarshals the package.json file located in pathToPackage.
func readPackageJSON(pathToPackage string) (map[string]interface{}, error) {
	f, err := os.Open(filepath.Join(pathToPackage, "package.json"))
	if err != nil {
		return nil, fmt.Errorf("opening package.json: %w", err)
	}
	defer contract.IgnoreClose(f)

	var ret map[string]interface{}
	if err := json.NewDecoder(f).Decode(&ret); err != nil {
		return nil, fmt.Errorf("decoding package.json: %w", err)
	}

	return ret, nil
}

func writePackageJSON(pathToPackage string, metadata map[string]interface{}) error {
	// os.Create truncates the already existing file.
	f, err := os.Create(filepath.Join(pathToPackage, "package.json"))
	if err != nil {
		return fmt.Errorf("opening package.json: %w", err)
	}
	defer contract.IgnoreClose(f)

	encoder := json.NewEncoder(f)
	encoder.SetEscapeHTML(false)
	encoder.SetIndent("", "  ")
	if err := encoder.Encode(metadata); err != nil {
		return fmt.Errorf("writing package.json: %w", err)
	}
	return nil
}

// preparePythonProject runs setup necessary to get a Python project ready for `pulumi` commands.
func (pt *ProgramTester) preparePythonProject(projinfo *engine.Projinfo) error {
	cwd, _, err := projinfo.GetPwdMain()
	if err != nil {
		return err
	}

	if pt.opts.UsePipenv {
		if err = pt.preparePythonProjectWithPipenv(cwd); err != nil {
			return err
		}
	} else {
		venvPath := "venv"
		if cwd != projinfo.Root {
			venvPath = filepath.Join(cwd, "venv")
		}

		if pt.opts.GetUseSharedVirtualEnv() {
			requirementsPath := filepath.Join(cwd, "requirements.txt")
			requirementsmd5, err := hashFile(requirementsPath)
			if err != nil {
				return err
			}
			pt.opts.virtualEnvDir = fmt.Sprintf("pulumi-venv-%x", requirementsmd5)
			venvPath = filepath.Join(pt.opts.SharedVirtualEnvPath, pt.opts.virtualEnvDir)
		}
		if err = pt.runPythonCommand("python-venv", []string{"-m", "venv", venvPath}, cwd); err != nil {
			return err
		}

		projinfo.Proj.Runtime.SetOption("virtualenv", venvPath)
		projfile := filepath.Join(projinfo.Root, workspace.ProjectFile+".yaml")
		if err = projinfo.Proj.Save(projfile); err != nil {
			return fmt.Errorf("saving project: %w", err)
		}

		if pt.opts.InstallDevReleases {
			command := []string{"python", "-m", "pip", "install", "--pre", "pulumi"}
			if err := pt.runVirtualEnvCommand("virtualenv-pip-install", command, cwd); err != nil {
				return err
			}
		}
		command := []string{"python", "-m", "pip", "install", "-r", "requirements.txt"}
		if err := pt.runVirtualEnvCommand("virtualenv-pip-install", command, cwd); err != nil {
			return err
		}
	}

	if !pt.opts.RunUpdateTest {
		if err = pt.installPipPackageDeps(cwd); err != nil {
			return err
		}
	}

	return nil
}

func (pt *ProgramTester) preparePythonProjectWithPipenv(cwd string) error {
	// Allow ENV var based overload of desired Python version for
	// the Pipenv environment. This is useful in CI scenarios that
	// need to pin a specific version such as 3.9.x vs 3.10.x.
	pythonVersion := os.Getenv("PYTHON_VERSION")
	if pythonVersion == "" {
		pythonVersion = "3"
	}

	// Create a new Pipenv environment. This bootstraps a new virtual environment containing the version of Python that
	// we requested. Note that this version of Python is sourced from the machine, so you must first install the version
	// of Python that you are requesting on the host machine before building a virtualenv for it.

	if err := pt.runPipenvCommand("pipenv-new", []string{"--python", pythonVersion}, cwd); err != nil {
		return err
	}

	// Install the package's dependencies. We do this by running `pip` inside the virtualenv that `pipenv` has created.
	// We don't use `pipenv install` because we don't want a lock file and prefer the similar model of `pip install`
	// which matches what our customers do
	command := []string{"run", "pip", "install", "-r", "requirements.txt"}
	if pt.opts.InstallDevReleases {
		command = []string{"run", "pip", "install", "--pre", "-r", "requirements.txt"}
	}
	err := pt.runPipenvCommand("pipenv-install", command, cwd)
	if err != nil {
		return err
	}
	return nil
}

// YarnLinkPackageDeps bring in package dependencies via yarn
func (pt *ProgramTester) yarnLinkPackageDeps(cwd string) error {
	for _, dependency := range pt.opts.Dependencies {
		if err := pt.runYarnCommand("yarn-link", []string{"link", dependency}, cwd); err != nil {
			return err
		}
	}

	return nil
}

// InstallPipPackageDeps brings in package dependencies via pip install
func (pt *ProgramTester) installPipPackageDeps(cwd string) error {
	var err error
	for _, dep := range pt.opts.Dependencies {
		// If the given filepath isn't absolute, make it absolute. We're about to pass it to pipenv and pipenv is
		// operating inside of a random folder in /tmp.
		if !filepath.IsAbs(dep) {
			dep, err = filepath.Abs(dep)
			if err != nil {
				return err
			}
		}

		if pt.opts.UsePipenv {
			if err := pt.runPipenvCommand("pipenv-install-package",
				[]string{"run", "pip", "install", "-e", dep}, cwd); err != nil {
				return err
			}
		} else {
			if err := pt.runVirtualEnvCommand("virtualenv-pip-install-package",
				[]string{"python", "-m", "pip", "install", "-e", dep}, cwd); err != nil {
				return err
			}
		}
	}

	return nil
}

func getVirtualenvBinPath(cwd, bin string, pt *ProgramTester) (string, error) {
	virtualEnvBasePath := filepath.Join(cwd, pt.opts.virtualEnvDir)
	if pt.opts.GetUseSharedVirtualEnv() {
		virtualEnvBasePath = filepath.Join(pt.opts.SharedVirtualEnvPath, pt.opts.virtualEnvDir)
	}
	virtualenvBinPath := filepath.Join(virtualEnvBasePath, "bin", bin)
	if runtime.GOOS == windowsOS {
		virtualenvBinPath = filepath.Join(virtualEnvBasePath, "Scripts", bin+".exe")
	}
	if info, err := os.Stat(virtualenvBinPath); err != nil || info.IsDir() {
		return "", fmt.Errorf("Expected %s to exist in virtual environment at %q", bin, virtualenvBinPath)
	}
	return virtualenvBinPath, nil
}

// getSanitizedPkg strips the version string from a go dep
// Note: most of the pulumi modules don't use major version subdirectories for modules
func getSanitizedModulePath(pkg string) string {
	re := regexp.MustCompile(`v\d`)
	v := re.FindString(pkg)
	if v != "" {
		return strings.TrimSuffix(strings.ReplaceAll(pkg, v, ""), "/")
	}
	return pkg
}

func getRewritePath(pkg string, gopath string, depRoot string) string {
	var depParts []string
	sanitizedPkg := getSanitizedModulePath(pkg)

	splitPkg := strings.Split(sanitizedPkg, "/")

	if depRoot != "" {
		// Get the package name
		// This is the value after "github.com/foo/bar"
		repoName := splitPkg[2]
		basePath := splitPkg[len(splitPkg)-1]
		if basePath == repoName {
			depParts = []string{depRoot, repoName}
		} else {
			depParts = []string{depRoot, repoName, basePath}
		}
		return filepath.Join(depParts...)
	}
	depParts = append([]string{gopath, "src"}, splitPkg...)
	return filepath.Join(depParts...)
}

// Fetchs the GOPATH
func GoPath() (string, error) {
	gopath := os.Getenv("GOPATH")
	if gopath == "" {
		usr, userErr := user.Current()
		if userErr != nil {
			return "", userErr
		}
		gopath = filepath.Join(usr.HomeDir, "go")
	}
	return gopath, nil
}

// prepareGoProject runs setup necessary to get a Go project ready for `pulumi` commands.
func (pt *ProgramTester) prepareGoProject(projinfo *engine.Projinfo) error {
	// Go programs are compiled, so we will compile the project first.
	goBin, err := pt.getGoBin()
	if err != nil {
		return fmt.Errorf("locating `go` binary: %w", err)
	}

	depRoot := os.Getenv("PULUMI_GO_DEP_ROOT")
	gopath, userError := GoPath()
	if userError != nil {
		return userError
	}

	cwd, _, err := projinfo.GetPwdMain()
	if err != nil {
		return err
	}

	// initialize a go.mod for dependency resolution if one doesn't exist
	_, err = os.Stat(filepath.Join(cwd, "go.mod"))
	if err != nil {
		err = pt.runCommand("go-mod-init", []string{goBin, "mod", "init"}, cwd)
		if err != nil {
			return err
		}
	}

	// install dev dependencies if requested
	if pt.opts.InstallDevReleases {
		// We're currently only installing pulumi/pulumi dependencies, which always have
		// "master" as the default branch.
		defaultBranch := "master"
		err = pt.runCommand("go-get-dev-deps", []string{
			goBin, "get", "-u", "github.com/pulumi/pulumi/sdk/v3@" + defaultBranch,
		}, cwd)
		if err != nil {
			return err
		}
	}

	// link local dependencies
	for _, dep := range pt.opts.Dependencies {
		editStr, err := getEditStr(dep, gopath, depRoot)
		if err != nil {
			return fmt.Errorf("error generating go mod replacement for dep %q: %w", dep, err)
		}
		err = pt.runCommand("go-mod-edit", []string{goBin, "mod", "edit", "-replace", editStr}, cwd)
		if err != nil {
			return err
		}
	}

	// tidy to resolve all transitive dependencies including from local dependencies above.
	err = pt.runCommand("go-mod-tidy", []string{goBin, "mod", "tidy"}, cwd)
	if err != nil {
		return err
	}

	if pt.opts.RunBuild {
		outBin := filepath.Join(gopath, "bin", string(projinfo.Proj.Name))
		if runtime.GOOS == windowsOS {
			outBin = outBin + ".exe"
		}
		err = pt.runCommand("go-build", []string{goBin, "build", "-o", outBin, "."}, cwd)
		if err != nil {
			return err
		}

		_, err = os.Stat(outBin)
		if err != nil {
			return fmt.Errorf("error finding built application artifact: %w", err)
		}
	}

	return nil
}

func getEditStr(dep string, gopath string, depRoot string) (string, error) {
	checkModName := true
	var err error
	var replacedModName string
	var targetModDir string
	if strings.ContainsRune(dep, '=') {
		parts := strings.Split(dep, "=")
		replacedModName = parts[0]
		targetModDir = parts[1]
	} else if !modfile.IsDirectoryPath(dep) {
		replacedModName = dep
		targetModDir = getRewritePath(dep, gopath, depRoot)
	} else {
		targetModDir = dep
		replacedModName, err = getModName(targetModDir)
		if err != nil {
			return "", err
		}
		// We've read the package name from the go.mod file, skip redundant check below.
		checkModName = false
	}

	targetModDir, err = filepath.Abs(targetModDir)
	if err != nil {
		return "", err
	}

	if checkModName {
		targetModName, err := getModName(targetModDir)
		if err != nil {
			return "", fmt.Errorf("no go.mod at directory, set the path to the module explicitly or place "+
				"the dependency in the path specified by PULUMI_GO_DEP_ROOT or the default GOPATH: %w", err)
		}
		targetPrefix, _, ok := module.SplitPathVersion(targetModName)
		if !ok {
			return "", fmt.Errorf("invalid module path for target module %q", targetModName)
		}
		replacedPrefix, _, ok := module.SplitPathVersion(replacedModName)
		if !ok {
			return "", fmt.Errorf("invalid module path for replaced module %q", replacedModName)
		}
		if targetPrefix != replacedPrefix {
			return "", fmt.Errorf("found module path with prefix %s, expected %s", targetPrefix, replacedPrefix)
		}
	}

	editStr := fmt.Sprintf("%s=%s", replacedModName, targetModDir)
	return editStr, nil
}

func getModName(dir string) (string, error) {
	pkgModPath := filepath.Join(dir, "go.mod")
	pkgModData, err := os.ReadFile(pkgModPath)
	if err != nil {
		return "", fmt.Errorf("error reading go.mod at %s: %w", dir, err)
	}
	pkgMod, err := modfile.Parse(pkgModPath, pkgModData, nil)
	if err != nil {
		return "", fmt.Errorf("error parsing go.mod at %s: %w", dir, err)
	}

	return pkgMod.Module.Mod.Path, nil
}

// prepareDotNetProject runs setup necessary to get a .NET project ready for `pulumi` commands.
func (pt *ProgramTester) prepareDotNetProject(projinfo *engine.Projinfo) error {
	dotNetBin, err := pt.getDotNetBin()
	if err != nil {
		return fmt.Errorf("locating `dotnet` binary: %w", err)
	}

	cwd, _, err := projinfo.GetPwdMain()
	if err != nil {
		return err
	}

	localNuget := os.Getenv("PULUMI_LOCAL_NUGET")
	if localNuget == "" {
		home := os.Getenv("HOME")
		localNuget = filepath.Join(home, ".pulumi-dev", "nuget")
	}

	if pt.opts.InstallDevReleases {
		err = pt.runCommand("dotnet-add-package",
			[]string{
				dotNetBin, "add", "package", "Pulumi",
				"--prerelease",
			},
			cwd)
		if err != nil {
			return err
		}
	}

	for _, dep := range pt.opts.Dependencies {

		// dotnet add package requires a specific version in case of a pre-release, so we have to look it up.
		globPattern := filepath.Join(localNuget, dep+".?.*.nupkg")
		matches, err := filepath.Glob(globPattern)
		if err != nil {
			return fmt.Errorf("failed to find a local Pulumi NuGet package: %w", err)
		}
		if len(matches) != 1 {
			return fmt.Errorf("attempting to find a local NuGet package %s by searching %s yielded %d results: %v",
				dep,
				globPattern,
				len(matches),
				matches)
		}
		file := filepath.Base(matches[0])
		r := strings.NewReplacer(dep+".", "", ".nupkg", "")
		version := r.Replace(file)

		// We don't restore because the program might depend on external
		// packages which cannot be found in our local nuget source. A restore
		// will happen automatically as part of the `pulumi up`.
		err = pt.runCommand("dotnet-add-package",
			[]string{
				dotNetBin, "add", "package", dep,
				"-v", version,
				"-s", localNuget,
				"--no-restore",
			},
			cwd)
		if err != nil {
			return fmt.Errorf("failed to add dependency on %s: %w", dep, err)
		}
	}

	return nil
}

func (pt *ProgramTester) prepareYAMLProject(projinfo *engine.Projinfo) error {
	// YAML doesn't need any system setup, and should auto-install required plugins
	return nil
}

func (pt *ProgramTester) prepareJavaProject(projinfo *engine.Projinfo) error {
	// Java doesn't need any system setup, and should auto-install required plugins
	return nil
}

func (pt *ProgramTester) defaultPrepareProject(projinfo *engine.Projinfo) error {
	// Based on the language, invoke the right routine to prepare the target directory.
	switch rt := projinfo.Proj.Runtime.Name(); rt {
	case NodeJSRuntime:
		return pt.prepareNodeJSProject(projinfo)
	case PythonRuntime:
		return pt.preparePythonProject(projinfo)
	case GoRuntime:
		return pt.prepareGoProject(projinfo)
	case DotNetRuntime:
		return pt.prepareDotNetProject(projinfo)
	case YAMLRuntime:
		return pt.prepareYAMLProject(projinfo)
	case JavaRuntime:
		return pt.prepareJavaProject(projinfo)
	default:
		return fmt.Errorf("unrecognized project runtime: %s", rt)
	}
}