mirror of https://github.com/pulumi/pulumi.git
414 lines
11 KiB
Go
414 lines
11 KiB
Go
// Copyright 2016-2021, 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 python
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen/testing/test"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/testing/iotest"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
|
"github.com/pulumi/pulumi/sdk/v3/python/toolchain"
|
|
)
|
|
|
|
const venvRelDir = "venv"
|
|
|
|
var pathTests = []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{".", "."},
|
|
{"", "."},
|
|
{"../", ".."},
|
|
{"../..", "..."},
|
|
{"../../..", "...."},
|
|
{"something", ".something"},
|
|
{"../parent", "..parent"},
|
|
{"../../module", "...module"},
|
|
}
|
|
|
|
func TestRelPathToRelImport(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tt := range pathTests {
|
|
tt := tt
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
result := relPathToRelImport(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("expected \"%s\"; got \"%s\"", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGeneratePackage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var virtualEnvLock sync.Mutex
|
|
// If we are running without checks, we mark the env as already built so we don't
|
|
// build it again.
|
|
virtualEnvBuilt := test.NoSDKCodegenChecks()
|
|
|
|
// To speed up these tests, we will generate one common virtual environment for all of
|
|
// them to run in, rather than having one per test. We want to make sure that we only
|
|
// build the virtual env if we are going to run one of the tests. We thus build the
|
|
// environment lazily
|
|
needsEnv := func(testFn test.CodegenCheck) test.CodegenCheck {
|
|
return func(t *testing.T, codedir string) {
|
|
func() {
|
|
virtualEnvLock.Lock()
|
|
defer virtualEnvLock.Unlock()
|
|
if !virtualEnvBuilt {
|
|
err := buildVirtualEnv(context.Background())
|
|
if err != nil {
|
|
t.Error(err)
|
|
t.FailNow()
|
|
}
|
|
virtualEnvBuilt = true
|
|
}
|
|
}()
|
|
testFn(t, codedir)
|
|
}
|
|
}
|
|
|
|
test.TestSDKCodegen(t, &test.SDKCodegenOptions{
|
|
Language: "python",
|
|
GenPackage: GeneratePackage,
|
|
Checks: map[string]test.CodegenCheck{
|
|
"python/py_compile": needsEnv(test.CompilePython),
|
|
"python/test": needsEnv(pyTestCheck),
|
|
},
|
|
TestCases: test.PulumiPulumiSDKTests,
|
|
})
|
|
}
|
|
|
|
func absTestsPath() (string, error) {
|
|
hereDir, err := filepath.Abs(".")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return hereDir, nil
|
|
}
|
|
|
|
func virtualEnvPath() (string, error) {
|
|
hereDir, err := absTestsPath()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(hereDir, venvRelDir), nil
|
|
}
|
|
|
|
// To serialize shared `venv` operations; without the lock running
|
|
// tests with `-parallel` causes sproadic failure.
|
|
var venvMutex = &sync.Mutex{}
|
|
|
|
func buildVirtualEnv(ctx context.Context) error {
|
|
hereDir, err := absTestsPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
venvDir, err := virtualEnvPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
gotVenv, err := test.PathExists(venvDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if gotVenv {
|
|
err := os.RemoveAll(venvDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
tc, err := toolchain.ResolveToolchain(toolchain.PythonOptions{
|
|
Toolchain: toolchain.Pip,
|
|
Root: hereDir,
|
|
Virtualenv: venvRelDir,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = tc.InstallDependencies(ctx, hereDir, false, /*useLanguageVersionTools */
|
|
false /*showOutput*/, os.Stdout, os.Stderr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sdkDir, err := filepath.Abs(filepath.Join("..", "..", "..", "sdk", "python", "env", "src"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
gotSdk, err := test.PathExists(sdkDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !gotSdk {
|
|
return errors.New("This test requires Python SDK to be built; please `cd sdk/python && make ensure build install`")
|
|
}
|
|
|
|
// install Pulumi Python SDK from the current source tree, -e means no-copy, ref directly
|
|
pyCmd, err := tc.ModuleCommand(ctx, "pip", "install", "-e", sdkDir)
|
|
if err != nil {
|
|
contract.Failf("failed to create pip install command: %v", err)
|
|
}
|
|
pyCmd.Dir = hereDir
|
|
output, err := pyCmd.CombinedOutput()
|
|
if err != nil {
|
|
contract.Failf("failed to link venv against in-source pulumi: %v\nstdout/stderr:\n%s",
|
|
err, output)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func pyTestCheck(t *testing.T, codeDir string) {
|
|
extraDir := filepath.Join(filepath.Dir(codeDir), "python-extras")
|
|
if _, err := os.Stat(extraDir); os.IsNotExist(err) {
|
|
// We won't run any tests since no extra tests were included.
|
|
return
|
|
}
|
|
hereDir, err := absTestsPath()
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
|
|
moduleCmd := func(module string, args ...string) error {
|
|
t.Logf("cd %s && %s", codeDir, strings.Join(append([]string{module}, args...), " "))
|
|
tc, err := toolchain.ResolveToolchain(toolchain.PythonOptions{
|
|
Toolchain: toolchain.Pip,
|
|
Root: hereDir,
|
|
Virtualenv: venvRelDir,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd, err := tc.ModuleCommand(context.Background(), module, args...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd.Dir = codeDir
|
|
|
|
outw := iotest.LogWriter(t)
|
|
cmd.Stderr = outw
|
|
cmd.Stdout = outw
|
|
return cmd.Run()
|
|
}
|
|
|
|
installPackage := func() error {
|
|
venvMutex.Lock()
|
|
defer venvMutex.Unlock()
|
|
return moduleCmd("pip", "install", "-e", ".")
|
|
}
|
|
|
|
if err = installPackage(); err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
|
|
if err = moduleCmd("pytest", "."); err != nil {
|
|
exitError, isExitError := err.(*exec.ExitError)
|
|
if isExitError && exitError.ExitCode() == 5 {
|
|
t.Logf("Could not find any pytest tests in %s", codeDir)
|
|
} else {
|
|
t.Error(err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
func TestGenerateTypeNames(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
test.TestTypeNameCodegen(t, "python", func(pkg *schema.Package) test.TypeNameGeneratorFunc {
|
|
// Decode python-specific info
|
|
err := pkg.ImportLanguages(map[string]schema.Language{"python": Importer})
|
|
require.NoError(t, err)
|
|
|
|
info, _ := pkg.Language["python"].(PackageInfo)
|
|
|
|
modules, err := generateModuleContextMap("test", pkg, info, nil)
|
|
require.NoError(t, err)
|
|
|
|
root, ok := modules[""]
|
|
require.True(t, ok)
|
|
|
|
return func(t schema.Type) string {
|
|
return root.typeString(t, false, false, false)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestEscapeDocString(t *testing.T) {
|
|
t.Parallel()
|
|
lines := []string{
|
|
`Active directory email address. Example: xyz@contoso.com or Contoso\xyz`,
|
|
`Triple quotes """ are all escaped`,
|
|
`But just quotes " are not`,
|
|
`This \N should be escaped`,
|
|
`Here \\N slashes should be escaped but not N`,
|
|
}
|
|
source := strings.Join(lines, "\n")
|
|
expected := `"""
|
|
Active directory email address. Example: xyz@contoso.com or Contoso\\xyz
|
|
Triple quotes \"\"\" are all escaped
|
|
But just quotes " are not
|
|
This \\N should be escaped
|
|
Here \\\\N slashes should be escaped but not N
|
|
"""
|
|
`
|
|
w := &bytes.Buffer{}
|
|
printComment(w, source, "")
|
|
assert.Equal(t, expected, w.String())
|
|
}
|
|
|
|
// This test evaluates the calculateDeps function, which takes a list of
|
|
// dependencies, and generates a slice of order pairs, where the first
|
|
// item is the name of the dep and the second item is the version constraint.
|
|
func TestCalculateDeps(t *testing.T) {
|
|
t.Parallel()
|
|
type TestCase struct {
|
|
// This is the input to the calculate deps function, a list of
|
|
// deps provided in the schema.
|
|
inputDeps map[string]string
|
|
// This is the set of ordered pairs.
|
|
expected [][2]string
|
|
// calculateDeps can error if the Pulumi version provided is
|
|
// invalid. This field is used to check that condition.
|
|
expectedErr error
|
|
}
|
|
cases := []TestCase{{
|
|
// Test 1: Give no explicit deps.
|
|
inputDeps: map[string]string{},
|
|
expected: [][2]string{
|
|
// We expect three alphabetized deps,
|
|
// with semver and parver formatted differently from Pulumi.
|
|
// Pulumi should not have a version.
|
|
{"parver>=0.2.1", ""},
|
|
{"pulumi", ">=3.136.0,<4.0.0"},
|
|
{"semver>=2.8.1"},
|
|
},
|
|
}, {
|
|
// Test 2: If you only one dep, we expect Pulumi to have a narrower
|
|
// constraint than if you had provided no deps.
|
|
inputDeps: map[string]string{
|
|
"foobar": "7.10.8",
|
|
},
|
|
expected: [][2]string{
|
|
{"foobar", "7.10.8"},
|
|
{"parver>=0.2.1", ""},
|
|
{"pulumi", ">=3.136.0,<4.0.0"},
|
|
{"semver>=2.8.1"},
|
|
},
|
|
}, {
|
|
// Test 3: If you provide pulumi, we expect the constraint to
|
|
// be respected.
|
|
inputDeps: map[string]string{
|
|
"pulumi": ">=3.0.0,<3.50.0",
|
|
},
|
|
expected: [][2]string{
|
|
// We expect three alphabetized deps,
|
|
// with semver and parver formatted differently from Pulumi.
|
|
{"parver>=0.2.1", ""},
|
|
{"pulumi", ">=3.0.0,<3.50.0"},
|
|
{"semver>=2.8.1"},
|
|
},
|
|
}, {
|
|
// Test 4: If you provide an illegal pulumi version, we expect an error.
|
|
inputDeps: map[string]string{
|
|
"pulumi": ">=0.16.0,<4.0.0",
|
|
},
|
|
expectedErr: fmt.Errorf("lower version bound must be at least %v", oldestAllowedPulumi),
|
|
}}
|
|
|
|
for i, tc := range cases {
|
|
tc := tc
|
|
name := fmt.Sprintf("CalculateDeps #%d", i+1)
|
|
t.Run(name, func(tt *testing.T) {
|
|
tt.Parallel()
|
|
observedDeps, err := calculateDeps(false, tc.inputDeps)
|
|
assert.Equal(tt, tc.expectedErr, err)
|
|
for index := range observedDeps {
|
|
observedDep := observedDeps[index]
|
|
expectedDep := tc.expected[index]
|
|
assert.ElementsMatch(tt, expectedDep, observedDep)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// This function tests that setPythonRequires correctly sets the minimum
|
|
// Python version when generating pyproject metadata.
|
|
func TestPythonRequiresSuccessful(t *testing.T) {
|
|
t.Parallel()
|
|
expected := "3.1"
|
|
pkg := schema.Package{
|
|
Language: map[string]interface{}{
|
|
"python": PackageInfo{
|
|
PythonRequires: expected,
|
|
},
|
|
},
|
|
}
|
|
schema := new(PyprojectSchema)
|
|
schema.Project = new(Project)
|
|
|
|
setPythonRequires(schema, &pkg)
|
|
observed := *schema.Project.RequiresPython
|
|
assert.Equal(t, expected, observed, "Expected version %s but observed version %s", expected, observed)
|
|
}
|
|
|
|
// This function tests that setPythonRequires correctly selects the default
|
|
// Python version when generating pyproject metadata.
|
|
func TestPythonRequiresNotProvided(t *testing.T) {
|
|
t.Parallel()
|
|
expected := defaultMinPythonVersion
|
|
pkg := schema.Package{
|
|
Language: map[string]interface{}{
|
|
"python": PackageInfo{
|
|
// Don't set PythonRequires
|
|
},
|
|
},
|
|
}
|
|
schema := new(PyprojectSchema)
|
|
schema.Project = new(Project)
|
|
|
|
setPythonRequires(schema, &pkg)
|
|
observed := *schema.Project.RequiresPython
|
|
assert.Equal(t, expected, observed, "Expected version %s but observed version %s", expected, observed)
|
|
}
|