Skip to content

Commit

Permalink
fix(testing): Adds cached python venvs for integration tests
Browse files Browse the repository at this point in the history
The cache key is based on hashing the contents of "requirements.txt" for each
python test.  This should result in unchanged test results among tests
sharing venvs, and maintains environment isolation across requirement sets
  • Loading branch information
Kyle Pitzen authored and Kyle Pitzen committed Dec 5, 2022
1 parent beaf07d commit 411c9b9
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 7 deletions.
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
// 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
7 changes: 6 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 @@ -144,7 +148,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

0 comments on commit 411c9b9

Please sign in to comment.