mirror of https://github.com/pulumi/pulumi.git
829 lines
26 KiB
Go
829 lines
26 KiB
Go
// Copyright 2021-2024, 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 test
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
|
|
|
|
"github.com/blang/semver"
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen"
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax"
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen/pcl"
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen/testing/utils"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
|
)
|
|
|
|
const (
|
|
transpiledExamplesDir = "transpiled_examples"
|
|
)
|
|
|
|
func transpiled(dir string) string {
|
|
return filepath.Join(transpiledExamplesDir, dir)
|
|
}
|
|
|
|
var allProgLanguages = codegen.NewStringSet(TestDotnet, TestPython, TestGo, TestNodeJS)
|
|
|
|
type ProgramTest struct {
|
|
Directory string
|
|
Description string
|
|
Skip codegen.StringSet
|
|
ExpectNYIDiags codegen.StringSet
|
|
SkipCompile codegen.StringSet
|
|
BindOptions []pcl.BindOption
|
|
MockPluginVersions map[string]string
|
|
PluginHost plugin.Host
|
|
}
|
|
|
|
var testdataPath = filepath.Join("..", "testing", "test", "testdata")
|
|
|
|
// Get batch number k (base-1 indexed) of tests out of n batches total.
|
|
func ProgramTestBatch(k, n int) []ProgramTest {
|
|
start := ((k - 1) * len(PulumiPulumiProgramTests)) / n
|
|
end := ((k) * len(PulumiPulumiProgramTests)) / n
|
|
return PulumiPulumiProgramTests[start:end]
|
|
}
|
|
|
|
// Useful when debugging a single test case.
|
|
func SingleTestCase(directoryName string) []ProgramTest {
|
|
output := make([]ProgramTest, 0)
|
|
for _, t := range PulumiPulumiProgramTests {
|
|
if t.Directory == directoryName {
|
|
output = append(output, t)
|
|
}
|
|
}
|
|
return output
|
|
}
|
|
|
|
var PulumiPulumiProgramTests = []ProgramTest{
|
|
{
|
|
Directory: "assets-archives",
|
|
Description: "Assets and archives",
|
|
},
|
|
{
|
|
Directory: "synthetic-resource-properties",
|
|
Description: "Synthetic resource properties",
|
|
SkipCompile: codegen.NewStringSet(TestNodeJS, TestDotnet, TestGo), // not a real package
|
|
},
|
|
{
|
|
Directory: "aws-s3-folder",
|
|
Description: "AWS S3 Folder",
|
|
SkipCompile: codegen.NewStringSet(TestGo),
|
|
// Blocked on go:
|
|
// TODO[pulumi/pulumi#8064]
|
|
// TODO[pulumi/pulumi#8065]
|
|
},
|
|
{
|
|
Directory: "aws-eks",
|
|
Description: "AWS EKS",
|
|
},
|
|
{
|
|
Directory: "csharp-invoke-options",
|
|
Description: "A program that uses InvokeOptions in C#",
|
|
// Test only C# because the other languages have conformance tests
|
|
Skip: allProgLanguages.Except(TestDotnet),
|
|
},
|
|
{
|
|
Directory: "aws-fargate",
|
|
Description: "AWS Fargate",
|
|
},
|
|
{
|
|
Directory: "aws-static-website",
|
|
Description: "an example resource from AWS static website multi-language component",
|
|
// TODO: blocked on resolving imports (python) / using statements (C#) for types from external packages
|
|
SkipCompile: codegen.NewStringSet(TestDotnet, TestPython),
|
|
},
|
|
{
|
|
Directory: "aws-fargate-output-versioned",
|
|
Description: "AWS Fargate Using Output-versioned invokes for python and typescript",
|
|
Skip: codegen.NewStringSet(TestGo, TestDotnet),
|
|
BindOptions: []pcl.BindOption{pcl.PreferOutputVersionedInvokes},
|
|
},
|
|
{
|
|
Directory: "aws-s3-logging",
|
|
Description: "AWS S3 with logging",
|
|
SkipCompile: codegen.NewStringSet(TestGo),
|
|
// Blocked on nodejs: TODO[pulumi/pulumi#8068]
|
|
// Flaky in go: TODO[pulumi/pulumi#8123]
|
|
},
|
|
{
|
|
Directory: "aws-iam-policy",
|
|
Description: "AWS IAM Policy",
|
|
},
|
|
{
|
|
Directory: "read-file-func",
|
|
Description: "ReadFile function translation works",
|
|
},
|
|
{
|
|
Directory: "python-regress-10914",
|
|
Description: "Python regression test for #10914",
|
|
Skip: allProgLanguages.Except(TestPython),
|
|
},
|
|
{
|
|
Directory: "simplified-invokes",
|
|
Description: "Simplified invokes",
|
|
Skip: codegen.NewStringSet(TestPython, TestGo),
|
|
SkipCompile: codegen.NewStringSet(TestDotnet, TestNodeJS),
|
|
},
|
|
{
|
|
Directory: "aws-optionals",
|
|
Description: "AWS get invoke with nested object constructor that takes an optional string",
|
|
// Testing Go behavior exclusively:
|
|
Skip: allProgLanguages.Except(TestGo),
|
|
},
|
|
{
|
|
Directory: "aws-webserver",
|
|
Description: "AWS Webserver",
|
|
},
|
|
{
|
|
Directory: "simple-range",
|
|
Description: "Simple range as int expression translation",
|
|
},
|
|
{
|
|
Directory: "azure-native",
|
|
Description: "Azure Native",
|
|
SkipCompile: codegen.NewStringSet(TestGo, TestDotnet),
|
|
// Blocked on go:
|
|
// TODO[pulumi/pulumi#8073]
|
|
// TODO[pulumi/pulumi#8074]
|
|
},
|
|
{
|
|
Directory: "azure-native-v2-eventgrid",
|
|
Description: "Azure Native V2 basic example to ensure that importPathPatten works",
|
|
// Specifically use a simplified azure-native v2.x schema when testing this program
|
|
// this schema only contains content from the eventgrid module which is sufficient to test with
|
|
PluginHost: utils.NewHostWithProviders(testdataPath,
|
|
utils.NewSchemaProvider("azure-native", "2.41.0")),
|
|
},
|
|
{
|
|
Directory: "azure-sa",
|
|
Description: "Azure SA",
|
|
},
|
|
{
|
|
Directory: "string-enum-union-list",
|
|
Description: "Contains resource which has a property of type List<Union<String, Enum>>",
|
|
// skipping compiling on Go because it doesn't know to handle unions in lists
|
|
// and instead generates pulumi.StringArray
|
|
SkipCompile: codegen.NewStringSet(TestGo),
|
|
},
|
|
{
|
|
Directory: "using-object-as-input-for-any",
|
|
Description: "Tests using object as input for a property of type 'any'",
|
|
},
|
|
{
|
|
Directory: "kubernetes-operator",
|
|
Description: "K8s Operator",
|
|
},
|
|
{
|
|
Directory: "kubernetes-pod",
|
|
Description: "K8s Pod",
|
|
SkipCompile: codegen.NewStringSet(TestGo),
|
|
// Blocked on go:
|
|
// TODO[pulumi/pulumi#8073]
|
|
// TODO[pulumi/pulumi#8074]
|
|
},
|
|
{
|
|
Directory: "kubernetes-template",
|
|
Description: "K8s Template",
|
|
},
|
|
{
|
|
Directory: "kubernetes-template-quoted",
|
|
Description: "K8s Template with quoted string property keys to ensure that resource binding works here",
|
|
},
|
|
{
|
|
Directory: "random-pet",
|
|
Description: "Random Pet",
|
|
},
|
|
{
|
|
Directory: "aws-secret",
|
|
Description: "Secret",
|
|
},
|
|
{
|
|
Directory: "functions",
|
|
Description: "Functions",
|
|
},
|
|
{
|
|
Directory: "output-funcs-aws",
|
|
Description: "Output Versioned Functions",
|
|
},
|
|
{
|
|
Directory: "third-party-package",
|
|
Description: "Ensuring correct imports for third party packages",
|
|
// compiling and type checking involves downloading the real package to
|
|
// check against. Because we are checking against the "other" package
|
|
// (which doesn't exist), this does not work.
|
|
SkipCompile: codegen.NewStringSet(TestNodeJS, TestDotnet, TestGo),
|
|
},
|
|
{
|
|
Directory: "invalid-go-sprintf",
|
|
Description: "Regress invalid Go",
|
|
Skip: codegen.NewStringSet(TestPython, TestNodeJS, TestDotnet),
|
|
},
|
|
{
|
|
Directory: "typed-enum",
|
|
Description: "Supply strongly typed enums",
|
|
Skip: codegen.NewStringSet(TestGo),
|
|
},
|
|
{
|
|
Directory: "pulumi-stack-reference",
|
|
Description: "StackReference as resource",
|
|
},
|
|
{
|
|
Directory: "python-resource-names",
|
|
Description: "Repro for #9357",
|
|
Skip: codegen.NewStringSet(TestGo, TestNodeJS, TestDotnet),
|
|
},
|
|
{
|
|
Directory: "logical-name",
|
|
Description: "Logical names",
|
|
},
|
|
{
|
|
Directory: "aws-lambda",
|
|
Description: "AWS Lambdas",
|
|
// We have special testing for this case because lambda is a python keyword.
|
|
Skip: codegen.NewStringSet(TestGo, TestNodeJS, TestDotnet),
|
|
},
|
|
{
|
|
Directory: "basic-unions",
|
|
Description: "Tests program generation of fields of type union",
|
|
SkipCompile: allProgLanguages, // because the schema is synthetic
|
|
},
|
|
{
|
|
Directory: "traverse-union-repro",
|
|
Description: `Handling the error "cannot traverse value of type union(...)"`,
|
|
BindOptions: []pcl.BindOption{
|
|
pcl.SkipResourceTypechecking,
|
|
pcl.AllowMissingVariables,
|
|
pcl.AllowMissingProperties,
|
|
},
|
|
// The example is known to be invalid. Specifically it hands a
|
|
// `[aws_subnet.test1.id]` to a `string` attribute, where `aws_subnet` is not in
|
|
// scope.
|
|
//
|
|
// The example is invalid in two ways:
|
|
// 1. `aws_subnet` is a missing variable.
|
|
// 2. `[...]` is a tuple, which can never be a string.
|
|
//
|
|
// Even though the generated code will not type check, it should still be
|
|
// directionally correct.
|
|
SkipCompile: allProgLanguages,
|
|
},
|
|
{
|
|
Directory: "components",
|
|
Description: "Components",
|
|
SkipCompile: codegen.NewStringSet(TestGo),
|
|
},
|
|
{
|
|
Directory: "entries-function",
|
|
Description: "Using the entries function",
|
|
// go and dotnet do fully not support GenForExpression yet
|
|
// Todo: https://github.com/pulumi/pulumi/issues/12606
|
|
SkipCompile: allProgLanguages.Except(TestNodeJS).Except(TestPython),
|
|
},
|
|
{
|
|
Directory: "retain-on-delete",
|
|
Description: "Generate RetainOnDelete option",
|
|
},
|
|
{
|
|
Directory: "depends-on-array",
|
|
Description: "Using DependsOn resource option with an array of resources",
|
|
},
|
|
{
|
|
Directory: "multiline-string",
|
|
Description: "Multiline string literals",
|
|
},
|
|
{
|
|
Directory: "config-variables",
|
|
Description: "Basic program with a bunch of config variables",
|
|
// TODO[https://github.com/pulumi/pulumi/issues/14957] - object config variables are broken here
|
|
SkipCompile: codegen.NewStringSet(TestGo, TestDotnet),
|
|
},
|
|
{
|
|
Directory: "regress-11176",
|
|
Description: "Regression test for https://github.com/pulumi/pulumi/issues/11176",
|
|
Skip: allProgLanguages.Except(TestGo),
|
|
},
|
|
{
|
|
Directory: "throw-not-implemented",
|
|
Description: "Function notImplemented is compiled to a runtime error at call-site",
|
|
},
|
|
{
|
|
Directory: "python-reserved",
|
|
Description: "Test python reserved words aren't used",
|
|
Skip: allProgLanguages.Except(TestPython),
|
|
},
|
|
{
|
|
Directory: "iterating-optional-range-expressions",
|
|
Description: "Test that we can iterate over range expression that are option(iterator)",
|
|
// TODO: dotnet and go
|
|
Skip: allProgLanguages.Except(TestNodeJS).Except(TestPython),
|
|
// We are using a synthetic schema defined in range-1.0.0.json so we can't compile all the way
|
|
SkipCompile: allProgLanguages,
|
|
},
|
|
{
|
|
Directory: "output-literals",
|
|
Description: "Tests that we can return various literal values via stack outputs",
|
|
},
|
|
{
|
|
Directory: "dynamic-entries",
|
|
Description: "Testing iteration of dynamic entries in TypeScript",
|
|
Skip: allProgLanguages.Except(TestNodeJS),
|
|
SkipCompile: allProgLanguages,
|
|
},
|
|
{
|
|
Directory: "single-or-none",
|
|
Description: "Tests using the singleOrNone function",
|
|
},
|
|
{
|
|
Directory: "simple-splat",
|
|
Description: "An example that shows we can compile splat expressions from array of objects",
|
|
// Skip compiling because we are using a test schema without a corresponding real package
|
|
SkipCompile: allProgLanguages,
|
|
},
|
|
{
|
|
Directory: "invoke-inside-conditional-range",
|
|
Description: "Using the result of an invoke inside a conditional range expression of a resource",
|
|
Skip: allProgLanguages.Except(TestNodeJS).Except(TestDotnet),
|
|
SkipCompile: allProgLanguages,
|
|
},
|
|
{
|
|
Directory: "output-name-conflict",
|
|
Description: "Tests whether we are able to generate programs where output variables have same id as config var",
|
|
SkipCompile: codegen.NewStringSet(TestGo),
|
|
},
|
|
{
|
|
Directory: "snowflake-python-12998",
|
|
Description: "Tests regression for issue https://github.com/pulumi/pulumi/issues/12998",
|
|
Skip: allProgLanguages.Except(TestPython),
|
|
SkipCompile: allProgLanguages,
|
|
BindOptions: []pcl.BindOption{pcl.AllowMissingVariables, pcl.AllowMissingProperties},
|
|
},
|
|
{
|
|
Directory: "unknown-resource",
|
|
Description: "Tests generating code for unknown resources when skipping resource type-checking",
|
|
SkipCompile: allProgLanguages,
|
|
BindOptions: []pcl.BindOption{pcl.SkipResourceTypechecking},
|
|
},
|
|
{
|
|
Directory: "using-dashes",
|
|
Description: "Test program generation on packages with a dash in the name",
|
|
SkipCompile: allProgLanguages, // since we are using a synthetic schema
|
|
},
|
|
{
|
|
Directory: "unknown-invoke",
|
|
Description: "Tests generating code for unknown invokes when skipping invoke type checking",
|
|
SkipCompile: allProgLanguages,
|
|
BindOptions: []pcl.BindOption{pcl.SkipInvokeTypechecking},
|
|
},
|
|
{
|
|
Directory: "optional-complex-config",
|
|
Description: "Tests generating code for optional and complex config values",
|
|
Skip: allProgLanguages.Except(TestNodeJS).Except(TestDotnet),
|
|
SkipCompile: allProgLanguages.Except(TestNodeJS).Except(TestDotnet),
|
|
},
|
|
{
|
|
Directory: "interpolated-string-keys",
|
|
Description: "Tests that interpolated string keys are supported in maps. ",
|
|
Skip: allProgLanguages.Except(TestNodeJS).Except(TestPython),
|
|
},
|
|
{
|
|
Directory: "regress-node-12507",
|
|
Description: "Regression test for https://github.com/pulumi/pulumi/issues/12507",
|
|
Skip: allProgLanguages.Except(TestNodeJS),
|
|
BindOptions: []pcl.BindOption{pcl.PreferOutputVersionedInvokes},
|
|
},
|
|
{
|
|
Directory: "csharp-plain-lists",
|
|
Description: "Tests that plain lists are supported in C#",
|
|
Skip: allProgLanguages.Except(TestDotnet),
|
|
},
|
|
{
|
|
Directory: "csharp-typed-for-expressions",
|
|
Description: "Testing for expressions with typed target expressions in csharp",
|
|
Skip: allProgLanguages.Except(TestDotnet),
|
|
},
|
|
{
|
|
Directory: "empty-list-property",
|
|
Description: "Tests compiling empty list expressions of object properties",
|
|
},
|
|
{
|
|
Directory: "python-regress-14037",
|
|
Description: "Regression test for rewriting qoutes in python",
|
|
Skip: allProgLanguages.Except(TestPython),
|
|
},
|
|
{
|
|
Directory: "inline-invokes",
|
|
Description: "Tests whether using inline invoke expressions works",
|
|
SkipCompile: codegen.NewStringSet(TestGo),
|
|
},
|
|
}
|
|
|
|
var PulumiPulumiYAMLProgramTests = []ProgramTest{
|
|
// PCL files from pulumi/yaml transpiled examples
|
|
{
|
|
Directory: transpiled("aws-eks"),
|
|
Description: "AWS EKS",
|
|
Skip: codegen.NewStringSet(TestGo, TestNodeJS, TestDotnet),
|
|
},
|
|
{
|
|
Directory: transpiled("aws-static-website"),
|
|
Description: "AWS static website",
|
|
Skip: codegen.NewStringSet(TestGo, TestNodeJS, TestDotnet),
|
|
BindOptions: []pcl.BindOption{pcl.SkipResourceTypechecking},
|
|
},
|
|
{
|
|
Directory: transpiled("awsx-fargate"),
|
|
Description: "AWSx Fargate",
|
|
Skip: codegen.NewStringSet(TestDotnet, TestNodeJS, TestGo),
|
|
},
|
|
{
|
|
Directory: transpiled("azure-app-service"),
|
|
Description: "Azure App Service",
|
|
Skip: codegen.NewStringSet(TestGo, TestDotnet),
|
|
},
|
|
{
|
|
Directory: transpiled("azure-container-apps"),
|
|
Description: "Azure Container Apps",
|
|
Skip: codegen.NewStringSet(TestGo, TestNodeJS, TestDotnet, TestPython),
|
|
},
|
|
{
|
|
Directory: transpiled("azure-static-website"),
|
|
Description: "Azure static website",
|
|
Skip: codegen.NewStringSet(TestGo, TestNodeJS, TestDotnet, TestPython),
|
|
},
|
|
{
|
|
Directory: transpiled("cue-eks"),
|
|
Description: "Cue EKS",
|
|
Skip: codegen.NewStringSet(TestGo, TestNodeJS, TestDotnet),
|
|
},
|
|
{
|
|
Directory: transpiled("cue-random"),
|
|
Description: "Cue random",
|
|
},
|
|
{
|
|
Directory: transpiled("getting-started"),
|
|
Description: "Getting started",
|
|
},
|
|
{
|
|
Directory: transpiled("kubernetes"),
|
|
Description: "Kubernetes",
|
|
Skip: codegen.NewStringSet(TestGo),
|
|
},
|
|
{
|
|
Directory: transpiled("pulumi-variable"),
|
|
Description: "Pulumi variable",
|
|
Skip: codegen.NewStringSet(TestGo, TestNodeJS, TestDotnet),
|
|
},
|
|
{
|
|
Directory: transpiled("random"),
|
|
Description: "Random",
|
|
Skip: codegen.NewStringSet(TestNodeJS),
|
|
},
|
|
{
|
|
Directory: transpiled("readme"),
|
|
Description: "README",
|
|
Skip: codegen.NewStringSet(TestGo, TestDotnet),
|
|
},
|
|
{
|
|
Directory: transpiled("stackreference-consumer"),
|
|
Description: "Stack reference consumer",
|
|
Skip: codegen.NewStringSet(TestGo, TestNodeJS, TestDotnet),
|
|
},
|
|
{
|
|
Directory: transpiled("stackreference-producer"),
|
|
Description: "Stack reference producer",
|
|
Skip: codegen.NewStringSet(TestGo, TestDotnet),
|
|
},
|
|
{
|
|
Directory: transpiled("webserver-json"),
|
|
Description: "Webserver JSON",
|
|
Skip: codegen.NewStringSet(TestGo, TestDotnet, TestPython),
|
|
},
|
|
{
|
|
Directory: transpiled("webserver"),
|
|
Description: "Webserver",
|
|
Skip: codegen.NewStringSet(TestGo, TestDotnet, TestPython),
|
|
},
|
|
}
|
|
|
|
// Checks that a generated program is correct
|
|
//
|
|
// The arguments are to be read:
|
|
// (Testing environment, path to generated code, set of dependencies)
|
|
type CheckProgramOutput = func(*testing.T, string, codegen.StringSet)
|
|
|
|
// Generates a program from a pcl.Program
|
|
type GenProgram = func(program *pcl.Program) (map[string][]byte, hcl.Diagnostics, error)
|
|
|
|
// Generates a project from a pcl.Program
|
|
type GenProject = func(
|
|
directory string, project workspace.Project,
|
|
program *pcl.Program, localDependencies map[string]string,
|
|
) error
|
|
|
|
type ProgramCodegenOptions struct {
|
|
Language string
|
|
Extension string
|
|
OutputFile string
|
|
Check CheckProgramOutput
|
|
GenProgram GenProgram
|
|
TestCases []ProgramTest
|
|
|
|
// For generating a full project
|
|
IsGenProject bool
|
|
GenProject GenProject
|
|
// Maps a test file (i.e. "aws-resource-options") to a struct containing a package
|
|
// (i.e. "github.com/pulumi/pulumi-aws/sdk/v5", "pulumi-aws) and its
|
|
// version prefixed by an operator (i.e. " v5.11.0", ==5.11.0")
|
|
ExpectedVersion map[string]PkgVersionInfo
|
|
DependencyFile string
|
|
}
|
|
|
|
type PkgVersionInfo struct {
|
|
Pkg string
|
|
OpAndVersion string
|
|
}
|
|
|
|
// TestProgramCodegen runs the complete set of program code generation tests against a particular
|
|
// language's code generator.
|
|
//
|
|
// A program code generation test consists of a PCL file (.pp extension) and a set of expected outputs
|
|
// for each language.
|
|
//
|
|
// The PCL file is the only piece that must be manually authored. Once the schema has been written, the expected outputs
|
|
// can be generated by running `PULUMI_ACCEPT=true go test ./..." from the `pkg/codegen` directory.
|
|
//
|
|
//nolint:revive
|
|
func TestProgramCodegen(
|
|
t *testing.T,
|
|
testcase ProgramCodegenOptions,
|
|
) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("TestProgramCodegen is skipped on Windows")
|
|
}
|
|
|
|
assert.NotNil(t, testcase.TestCases, "Caller must provide test cases")
|
|
pulumiAccept := cmdutil.IsTruthy(os.Getenv("PULUMI_ACCEPT"))
|
|
skipCompile := cmdutil.IsTruthy(os.Getenv("PULUMI_SKIP_COMPILE_TEST"))
|
|
|
|
for _, tt := range testcase.TestCases {
|
|
tt := tt // avoid capturing loop variable
|
|
t.Run(tt.Directory, func(t *testing.T) {
|
|
// These tests should not run in parallel.
|
|
// They take up a fair bit of memory
|
|
// and can OOM in CI with too many running.
|
|
|
|
var err error
|
|
if tt.Skip.Has(testcase.Language) {
|
|
t.Skip()
|
|
return
|
|
}
|
|
|
|
expectNYIDiags := tt.ExpectNYIDiags.Has(testcase.Language)
|
|
|
|
testDir := filepath.Join(testdataPath, tt.Directory+"-pp")
|
|
pclFile := filepath.Join(testDir, tt.Directory+".pp")
|
|
if strings.HasPrefix(tt.Directory, transpiledExamplesDir) {
|
|
pclFile = filepath.Join(testDir, filepath.Base(tt.Directory)+".pp")
|
|
}
|
|
testDir = filepath.Join(testDir, testcase.Language)
|
|
err = os.MkdirAll(testDir, 0o700)
|
|
if err != nil && !os.IsExist(err) {
|
|
t.Fatalf("Failed to create %q: %s", testDir, err)
|
|
}
|
|
|
|
contents, err := os.ReadFile(pclFile)
|
|
if err != nil {
|
|
t.Fatalf("could not read %v: %v", pclFile, err)
|
|
}
|
|
|
|
expectedFile := filepath.Join(testDir, tt.Directory+"."+testcase.Extension)
|
|
if strings.HasPrefix(tt.Directory, transpiledExamplesDir) {
|
|
expectedFile = filepath.Join(testDir, filepath.Base(tt.Directory)+"."+testcase.Extension)
|
|
}
|
|
expected, err := os.ReadFile(expectedFile)
|
|
if err != nil && !pulumiAccept {
|
|
t.Fatalf("could not read %v: %v", expectedFile, err)
|
|
}
|
|
|
|
parser := syntax.NewParser()
|
|
err = parser.ParseFile(bytes.NewReader(contents), tt.Directory+".pp")
|
|
if err != nil {
|
|
t.Fatalf("could not read %v: %v", pclFile, err)
|
|
}
|
|
if parser.Diagnostics.HasErrors() {
|
|
t.Fatalf("failed to parse files: %v", parser.Diagnostics)
|
|
}
|
|
|
|
hclFiles := map[string]*hcl.File{
|
|
tt.Directory + ".pp": {Body: parser.Files[0].Body, Bytes: parser.Files[0].Bytes},
|
|
}
|
|
var pluginHost plugin.Host
|
|
if tt.PluginHost != nil {
|
|
pluginHost = tt.PluginHost
|
|
} else {
|
|
pluginHost = utils.NewHost(testdataPath)
|
|
}
|
|
|
|
opts := append(tt.BindOptions, pcl.PluginHost(pluginHost))
|
|
rootProgramPath := filepath.Join(testdataPath, tt.Directory+"-pp")
|
|
absoluteProgramPath, err := filepath.Abs(rootProgramPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to bind program: unable to find the absolute path of %v", rootProgramPath)
|
|
}
|
|
opts = append(opts, pcl.DirPath(absoluteProgramPath))
|
|
opts = append(opts, pcl.ComponentBinder(pcl.ComponentProgramBinderFromFileSystem()))
|
|
|
|
program, diags, err := pcl.BindProgram(parser.Files, opts...)
|
|
if err != nil {
|
|
t.Fatalf("could not bind program: %v", err)
|
|
}
|
|
bindDiags := new(bytes.Buffer)
|
|
if len(diags) > 0 {
|
|
require.NoError(t, hcl.NewDiagnosticTextWriter(bindDiags, hclFiles, 80, false).WriteDiagnostics(diags))
|
|
if diags.HasErrors() {
|
|
t.Fatalf("failed to bind program:\n%s", bindDiags)
|
|
}
|
|
t.Logf("bind diags:\n%s", bindDiags)
|
|
}
|
|
var files map[string][]byte
|
|
// generate a full project and check expected package versions
|
|
if testcase.IsGenProject {
|
|
project := workspace.Project{
|
|
Name: "test",
|
|
Runtime: workspace.NewProjectRuntimeInfo(testcase.Language, nil),
|
|
}
|
|
err = testcase.GenProject(testDir, project, program, nil /*localDependencies*/)
|
|
assert.NoError(t, err)
|
|
|
|
depFilePath := filepath.Join(testDir, testcase.DependencyFile)
|
|
outfilePath := filepath.Join(testDir, testcase.OutputFile)
|
|
CheckVersion(t, tt.Directory, depFilePath, testcase.ExpectedVersion)
|
|
GenProjectCleanUp(t, testDir, depFilePath, outfilePath)
|
|
}
|
|
files, diags, err = testcase.GenProgram(program)
|
|
assert.NoError(t, err)
|
|
if expectNYIDiags {
|
|
var tmpDiags hcl.Diagnostics
|
|
for _, d := range diags {
|
|
if !strings.HasPrefix(d.Summary, "not yet implemented") {
|
|
tmpDiags = append(tmpDiags, d)
|
|
}
|
|
}
|
|
diags = tmpDiags
|
|
}
|
|
if diags.HasErrors() {
|
|
buf := new(bytes.Buffer)
|
|
|
|
err := hcl.NewDiagnosticTextWriter(buf, hclFiles, 80, false).WriteDiagnostics(diags)
|
|
require.NoError(t, err, "Failed to write diag")
|
|
|
|
t.Fatalf("failed to generate program:\n%s", buf)
|
|
}
|
|
|
|
if pulumiAccept {
|
|
err := os.WriteFile(expectedFile, files[testcase.OutputFile], 0o600)
|
|
require.NoError(t, err)
|
|
// generate the rest of the files
|
|
for fileName, content := range files {
|
|
if fileName != testcase.OutputFile {
|
|
outputPath := filepath.Join(testDir, fileName)
|
|
err := os.WriteFile(outputPath, content, 0o600)
|
|
require.NoError(t, err, "Failed to write file %s", outputPath)
|
|
}
|
|
}
|
|
} else {
|
|
assert.Equal(t, string(expected), string(files[testcase.OutputFile]))
|
|
// assert that the content is correct for the rest of the files
|
|
for fileName, content := range files {
|
|
if fileName != testcase.OutputFile {
|
|
outputPath := filepath.Join(testDir, fileName)
|
|
outputContent, err := os.ReadFile(outputPath)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, string(outputContent), string(content))
|
|
}
|
|
}
|
|
}
|
|
if !skipCompile && testcase.Check != nil && !tt.SkipCompile.Has(testcase.Language) {
|
|
extraPulumiPackages := codegen.NewStringSet()
|
|
collectExtraPulumiPackages(program, extraPulumiPackages)
|
|
testcase.Check(t, expectedFile, extraPulumiPackages)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func collectExtraPulumiPackages(program *pcl.Program, extraPulumiPackages codegen.StringSet) {
|
|
for _, n := range program.Nodes {
|
|
if r, isResource := n.(*pcl.Resource); isResource {
|
|
pkg, _, _, _ := r.DecomposeToken()
|
|
if pkg != "pulumi" {
|
|
extraPulumiPackages.Add(pkg)
|
|
}
|
|
}
|
|
|
|
if component, isComponent := n.(*pcl.Component); isComponent {
|
|
collectExtraPulumiPackages(component.Program, extraPulumiPackages)
|
|
}
|
|
}
|
|
}
|
|
|
|
// CheckVersion checks for an expected package version
|
|
// Todo: support checking multiple package expected versions
|
|
func CheckVersion(t *testing.T, dir, depFilePath string, expectedVersionMap map[string]PkgVersionInfo) {
|
|
depFile, err := os.Open(depFilePath)
|
|
require.NoError(t, err)
|
|
defer depFile.Close()
|
|
|
|
// Splits on newlines by default.
|
|
scanner := bufio.NewScanner(depFile)
|
|
|
|
match := false
|
|
expectedPkg, expectedVersion := strings.TrimSpace(expectedVersionMap[dir].Pkg),
|
|
strings.TrimSpace(expectedVersionMap[dir].OpAndVersion)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if strings.Contains(line, expectedPkg) {
|
|
line = strings.TrimSpace(line)
|
|
actualVersion := strings.TrimPrefix(line, expectedPkg)
|
|
actualVersion = strings.TrimSpace(actualVersion)
|
|
expectedVersion = strings.Trim(expectedVersion, "v:^/> ")
|
|
actualVersion = strings.Trim(actualVersion, "v:^/> ")
|
|
if expectedVersion == actualVersion {
|
|
match = true
|
|
break
|
|
}
|
|
actualSemver, err := semver.Make(actualVersion)
|
|
if err == nil {
|
|
continue
|
|
}
|
|
expectedSemver, _ := semver.Make(expectedVersion)
|
|
if actualSemver.Compare(expectedSemver) >= 0 {
|
|
match = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
require.Truef(t, match, "Did not find expected package version pair (%q,%q). Searched in:\n%s",
|
|
expectedPkg, expectedVersion, newLazyStringer(func() string {
|
|
// Reset the read on the file
|
|
_, err := depFile.Seek(0, io.SeekStart)
|
|
require.NoError(t, err)
|
|
buf := new(strings.Builder)
|
|
_, err = io.Copy(buf, depFile)
|
|
require.NoError(t, err)
|
|
return buf.String()
|
|
}).String())
|
|
}
|
|
|
|
func GenProjectCleanUp(t *testing.T, dir, depFilePath, outfilePath string) {
|
|
os.Remove(depFilePath)
|
|
os.Remove(outfilePath)
|
|
os.Remove(dir + "/.gitignore")
|
|
os.Remove(dir + "/Pulumi.yaml")
|
|
}
|
|
|
|
type lazyStringer struct {
|
|
cache string
|
|
f func() string
|
|
}
|
|
|
|
func (l lazyStringer) String() string {
|
|
if l.cache == "" {
|
|
l.cache = l.f()
|
|
}
|
|
return l.cache
|
|
}
|
|
|
|
// The `fmt` `%s` calls .String() if the object is not a string itself. We can delay
|
|
// computing expensive display logic until and unless we actually will use it.
|
|
func newLazyStringer(f func() string) fmt.Stringer {
|
|
return lazyStringer{f: f}
|
|
}
|