// 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" ) 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(pyCompileCheck), "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, "venv"), 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 } } err = python.InstallDependencies(ctx, hereDir, venvDir, false /*showOutput*/) 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 := python.VirtualEnvCommand(venvDir, "python", "-m", "pip", "install", "-e", sdkDir) 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 } venvDir, err := virtualEnvPath() if err != nil { t.Error(err) return } cmd := func(name string, args ...string) error { t.Logf("cd %s && %s %s", codeDir, name, strings.Join(args, " ")) cmd := python.VirtualEnvCommand(venvDir, name, args...) 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 cmd("python", "-m", "pip", "install", "-e", ".") } if err = installPackage(); err != nil { t.Error(err) return } if err = cmd("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) } }) } 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", ""}, {"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.0.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(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) }