Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(testing): Adds cached python venvs for integration tests #11532

Merged
merged 1 commit into from Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci-run-test.yml
Expand Up @@ -64,6 +64,7 @@ env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_PROD_ACCESS_TOKEN }}
# Release builds use the service, PR checks and snapshots will use the local backend if possible.
PULUMI_TEST_USE_SERVICE: ${{ !contains(inputs.version, '-') }}
PULUMI_TEST_PYTHON_SHARED_VENV: "true"
PYTHON: python
GO_TEST_PARALLELISM: 8
GO_TEST_PKG_PARALLELISM: 2
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
@@ -1,6 +1,9 @@
{
"go.buildTags": "all",
"go.testTimeout": "1h",
"go.testEnvVars": {
"PULUMI_TEST_PYTHON_SHARED_VENV": "true",
},
"gopls": {
// A couple of modules get copied as part of builds and this confuse gopls as it sees the module name twice, just ignore the copy in the build folders.
"build.directoryFilters": [
Expand Down
@@ -0,0 +1,4 @@
changes:
- type: fix
scope: pkg/testing
description: Optionally caches python venvs for testing
92 changes: 86 additions & 6 deletions pkg/testing/integration/program.go
Expand Up @@ -17,6 +17,7 @@ package integration
import (
"context"
cryptorand "crypto/rand"
sha256 "crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
Expand Down Expand Up @@ -292,6 +293,12 @@ type ProgramTestOptions struct {
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
Copy link
Contributor Author

@kpitzen kpitzen Dec 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't love that I have to use a pointer here, but it seems to be the only way to disambiguate between false and an unset value at initialization time. I've used a couple convenience functions to try to alleviate the ergonomics issue this introduces, but I'm open to other approaches if anyone has any.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a better answer here either, I expect we'll want to make this the default in the future because it'll speed up examples & templates tests, tests in providers.

Using a pointer for Maybe[bool] and an env var to set the default is a good approach for a testing harness, and we can change the default in the future after validating.

// 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
Expand All @@ -314,6 +321,13 @@ type ProgramTestOptions struct {
LocalProviders []LocalDependency
}

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

type LocalDependency struct {
Package string
Path string
Expand Down Expand Up @@ -375,6 +389,34 @@ func (opts *ProgramTestOptions) GetStackName() tokens.QName {
return tokens.QName(opts.StackName)
}

// 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 {
Expand Down Expand Up @@ -547,6 +589,12 @@ func (opts ProgramTestOptions) With(overrides ProgramTestOptions) ProgramTestOpt
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
}
Expand Down Expand Up @@ -700,6 +748,24 @@ func prepareProgram(t *testing.T, opts *ProgramTestOptions) {
}
}

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
Expand Down Expand Up @@ -1025,7 +1091,7 @@ func (pt *ProgramTester) runVirtualEnvCommand(name string, args []string, wd str
}()
}

virtualenvBinPath, err := getVirtualenvBinPath(wd, args[0])
virtualenvBinPath, err := getVirtualenvBinPath(wd, args[0], pt)
if err != nil {
return err
}
Expand Down Expand Up @@ -1985,11 +2051,21 @@ func (pt *ProgramTester) preparePythonProject(projinfo *engine.Projinfo) error {
return err
}
} else {
if err = pt.runPythonCommand("python-venv", []string{"-m", "venv", "venv"}, cwd); err != nil {
venvPath := "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", "venv")
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)
Expand Down Expand Up @@ -2077,10 +2153,14 @@ func (pt *ProgramTester) installPipPackageDeps(cwd string) error {
return nil
}

func getVirtualenvBinPath(cwd, bin string) (string, error) {
virtualenvBinPath := filepath.Join(cwd, "venv", "bin", bin)
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(cwd, "venv", "Scripts", fmt.Sprintf("%s.exe", bin))
virtualenvBinPath = filepath.Join(virtualEnvBasePath, "Scripts", fmt.Sprintf("%s.exe", bin))
}
if info, err := os.Stat(virtualenvBinPath); err != nil || info.IsDir() {
return "", fmt.Errorf("Expected %s to exist in virtual environment at %q", bin, virtualenvBinPath)
Expand Down
8 changes: 7 additions & 1 deletion tests/integration/integration_python_smoke_test.go
Expand Up @@ -31,6 +31,10 @@ import (
"github.com/pulumi/pulumi/sdk/v3/python"
)

func boolPointer(b bool) *bool {
return &b
}

// TestEmptyPython simply tests that we can run an empty Python project.
func TestEmptyPython(t *testing.T) {
integration.ProgramTest(t, &integration.ProgramTestOptions{
Expand Down Expand Up @@ -83,6 +87,7 @@ func TestDynamicPython(t *testing.T) {
assert.Equal(t, randomVal, stack.Outputs["random_val"].(string))
},
}},
UseSharedVirtualEnv: boolPointer(false),
})
}

Expand Down Expand Up @@ -144,7 +149,8 @@ func optsForConstructPython(t *testing.T, expectedResourceCount int, localProvid
Secrets: map[string]string{
"secret": "this super secret is encrypted",
},
Quick: true,
Quick: true,
UseSharedVirtualEnv: boolPointer(false),
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
assert.NotNil(t, stackInfo.Deployment)
if assert.Equal(t, expectedResourceCount, len(stackInfo.Deployment.Resources)) {
Expand Down