github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/codegen/testing/test/sdk_driver.go (about)

     1  package test
     2  
     3  import (
     4  	"flag"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"runtime"
     9  	"sort"
    10  	"strconv"
    11  	"strings"
    12  	"sync"
    13  	"testing"
    14  
    15  	"github.com/stretchr/testify/require"
    16  
    17  	"github.com/pulumi/pulumi/pkg/v3/codegen"
    18  )
    19  
    20  // Defines an extra check logic that accepts the directory with the
    21  // generated code, typically `$TestDir/$test.Directory/$language`.
    22  type CodegenCheck func(t *testing.T, codedir string)
    23  
    24  type SDKTest struct {
    25  	Directory   string
    26  	Description string
    27  
    28  	// Extra checks for this test. They keys of this map
    29  	// are of the form "$language/$check" such as "go/compile".
    30  	Checks map[string]CodegenCheck
    31  
    32  	// Skip checks, identified by "$language/$check".
    33  	// "$language/any" is special, skipping generating the
    34  	// code as well as all tests.
    35  	Skip codegen.StringSet
    36  
    37  	// Do not compile the generated code for the languages in this set.
    38  	// This is a helper form of `Skip`.
    39  	SkipCompileCheck codegen.StringSet
    40  
    41  	// Mutex to ensure only a single test operates on directory at a time
    42  	Mutex sync.Mutex
    43  }
    44  
    45  // ShouldSkipTest indicates if a given test for a given language should be run.
    46  func (tt *SDKTest) ShouldSkipTest(language, test string) bool {
    47  
    48  	// Only language-specific checks.
    49  	if !strings.HasPrefix(test, language+"/") {
    50  		return true
    51  	}
    52  
    53  	// Obey SkipCompileCheck to skip compile and test targets.
    54  	if tt.SkipCompileCheck != nil &&
    55  		tt.SkipCompileCheck.Has(language) &&
    56  		(test == fmt.Sprintf("%s/compile", language) ||
    57  			test == fmt.Sprintf("%s/test", language)) {
    58  		return true
    59  	}
    60  
    61  	// Obey Skip.
    62  	if tt.Skip != nil && tt.Skip.Has(test) {
    63  		return true
    64  	}
    65  
    66  	return false
    67  }
    68  
    69  // ShouldSkipCodegen determines if codegen should be run. ShouldSkipCodegen=true
    70  // further implies no other tests will be run.
    71  func (tt *SDKTest) ShouldSkipCodegen(language string) bool {
    72  	return tt.Skip.Has(language + "/any")
    73  }
    74  
    75  const (
    76  	python = "python"
    77  	nodejs = "nodejs"
    78  	dotnet = "dotnet"
    79  	golang = "go"
    80  )
    81  
    82  var allLanguages = codegen.NewStringSet("python/any", "nodejs/any", "dotnet/any", "go/any", "docs/any")
    83  
    84  var PulumiPulumiSDKTests = []*SDKTest{
    85  	{
    86  		Directory:   "naming-collisions",
    87  		Description: "Schema with types that could potentially produce collisions (go).",
    88  	},
    89  	{
    90  		Directory:   "dash-named-schema",
    91  		Description: "Simple schema with a two part name (foo-bar)",
    92  	},
    93  	{
    94  		Directory:        "external-resource-schema",
    95  		Description:      "External resource schema",
    96  		SkipCompileCheck: codegen.NewStringSet(golang),
    97  	},
    98  	{
    99  		Directory:        "nested-module",
   100  		Description:      "Nested module",
   101  		SkipCompileCheck: codegen.NewStringSet(dotnet),
   102  	},
   103  	{
   104  		Directory:        "nested-module-thirdparty",
   105  		Description:      "Third-party nested module",
   106  		SkipCompileCheck: codegen.NewStringSet(dotnet),
   107  	},
   108  	{
   109  		Directory:   "plain-schema-gh6957",
   110  		Description: "Repro for #6957",
   111  	},
   112  	{
   113  		Directory:   "resource-args-python-case-insensitive",
   114  		Description: "Resource args with same named resource and type case insensitive",
   115  	},
   116  	{
   117  		Directory:   "resource-args-python",
   118  		Description: "Resource args with same named resource and type",
   119  	},
   120  	{
   121  		Directory:   "simple-enum-schema",
   122  		Description: "Simple schema with enum types",
   123  	},
   124  	{
   125  		Directory:   "simple-plain-schema",
   126  		Description: "Simple schema with plain properties",
   127  	},
   128  	{
   129  		Directory:   "simple-plain-schema-with-root-package",
   130  		Description: "Simple schema with root package set",
   131  	},
   132  	{
   133  		Directory:   "simple-resource-schema",
   134  		Description: "Simple schema with local resource properties",
   135  	},
   136  	{
   137  		Directory:   "simple-resource-schema-custom-pypackage-name",
   138  		Description: "Simple schema with local resource properties and custom Python package name",
   139  	},
   140  	{
   141  		Directory:        "simple-methods-schema",
   142  		Description:      "Simple schema with methods",
   143  		SkipCompileCheck: codegen.NewStringSet(nodejs, golang),
   144  	},
   145  	{
   146  		Directory:   "simple-methods-schema-single-value-returns",
   147  		Description: "Simple schema with methods that return single values",
   148  	},
   149  	{
   150  		Directory:   "simple-yaml-schema",
   151  		Description: "Simple schema encoded using YAML",
   152  	},
   153  	{
   154  		Directory:        "provider-config-schema",
   155  		Description:      "Simple provider config schema",
   156  		SkipCompileCheck: codegen.NewStringSet(dotnet),
   157  	},
   158  	{
   159  		Directory:   "replace-on-change",
   160  		Description: "Simple use of replaceOnChange in schema",
   161  	},
   162  	{
   163  		Directory:        "resource-property-overlap",
   164  		Description:      "A resource with the same name as its property",
   165  		SkipCompileCheck: codegen.NewStringSet(dotnet, nodejs),
   166  	},
   167  	{
   168  		Directory:   "type-references-resource",
   169  		Description: "An instance where a type references a resource",
   170  		Skip:        allLanguages.Except("nodejs/any"),
   171  		// SkipCompileCheck: codegen.NewStringSet(dotnet, golang, python),
   172  	},
   173  	{
   174  		Directory:   "hyphen-url",
   175  		Description: "A resource url with a hyphen in its path",
   176  		Skip:        codegen.NewStringSet("go/any"),
   177  	},
   178  	{
   179  		Directory:   "output-funcs",
   180  		Description: "Tests targeting the $fn_output helper code generation feature",
   181  	},
   182  	{
   183  		Directory:        "output-funcs-edgeorder",
   184  		Description:      "Regresses Node compilation issues on a subset of azure-native",
   185  		SkipCompileCheck: codegen.NewStringSet(golang, python),
   186  		Skip:             codegen.NewStringSet("nodejs/test"),
   187  	},
   188  	{
   189  		Directory:        "output-funcs-tfbridge20",
   190  		Description:      "Similar to output-funcs, but with compatibility: tfbridge20, to simulate pulumi-aws use case",
   191  		SkipCompileCheck: codegen.NewStringSet(python),
   192  	},
   193  	{
   194  		Directory:   "cyclic-types",
   195  		Description: "Cyclic object types",
   196  	},
   197  	{
   198  		Directory:   "regress-node-8110",
   199  		Description: "Test the fix for pulumi/pulumi#8110 nodejs compilation error",
   200  		Skip:        codegen.NewStringSet("go/test", "dotnet/test"),
   201  	},
   202  	{
   203  		Directory:   "dashed-import-schema",
   204  		Description: "Ensure that we handle all valid go import paths",
   205  		Skip:        codegen.NewStringSet("go/test", "dotnet/test"),
   206  	},
   207  	{
   208  		Directory:   "plain-and-default",
   209  		Description: "Ensure that a resource with a plain default property works correctly",
   210  	},
   211  	{
   212  		Directory:   "plain-object-defaults",
   213  		Description: "Ensure that object defaults are generated (repro #8132)",
   214  	},
   215  	{
   216  		Directory:   "plain-object-disable-defaults",
   217  		Description: "Ensure that we can still compile safely when defaults are disabled",
   218  	},
   219  	{
   220  		Directory:        "regress-8403",
   221  		Description:      "Regress pulumi/pulumi#8403",
   222  		SkipCompileCheck: codegen.NewStringSet(python),
   223  	},
   224  	{
   225  		Directory:   "different-package-name-conflict",
   226  		Description: "different packages with the same resource",
   227  		Skip:        allLanguages,
   228  	},
   229  	{
   230  		Directory:   "different-enum",
   231  		Description: "An enum in a different package namespace",
   232  		Skip:        codegen.NewStringSet("dotnet/compile"),
   233  	},
   234  	{
   235  		Directory:   "azure-native-nested-types",
   236  		Description: "Condensed example of nested collection types from Azure Native",
   237  		Skip:        codegen.NewStringSet("go/any"),
   238  	},
   239  	{
   240  		Directory:   "regress-go-8664",
   241  		Description: "Regress pulumi/pulumi#8664 affecting Go",
   242  		Skip:        allLanguages.Except("go/any"),
   243  	},
   244  	{
   245  		Directory:   "regress-go-10527",
   246  		Description: "Regress pulumi/pulumi#10527 affecting Go",
   247  		Skip:        allLanguages.Except("go/any"),
   248  	},
   249  	{
   250  		Directory:   "other-owned",
   251  		Description: "CSharp rootNamespaces",
   252  		// We only test in dotnet, because we are testing a change in a dotnet
   253  		// language property. Other tests should pass, but do not put the
   254  		// relevant feature under test. To save time, we skip them.
   255  		//
   256  		// We need to see dotnet changes (paths) in the docs too.
   257  		Skip: allLanguages.Except("dotnet/any").Except("docs/any"),
   258  	},
   259  	{
   260  		Directory: "external-node-compatibility",
   261  		// In this case, this test's schema has kubernetes20 set, but is referencing a type from Google Native
   262  		// which doesn't have any compatibility modes set, so the referenced type should be `AuditConfigArgs`
   263  		// (with the `Args` suffix) and not `AuditConfig`.
   264  		Description: "Ensure external package compatibility modes are used when referencing external types",
   265  		Skip:        allLanguages.Except("nodejs/any"),
   266  	},
   267  	{
   268  		Directory: "external-go-import-aliases",
   269  		// Google Native has its own import aliases, so those should be respected, unless there are local aliases.
   270  		// AWS Classic doesn't have any import aliases, so none should be used, unless there are local aliases.
   271  		Description: "Ensure external import aliases are honored, and any local import aliases override them",
   272  		Skip:        allLanguages.Except("go/any"),
   273  	},
   274  	{
   275  		Directory:   "external-python-same-module-name",
   276  		Description: "Ensure referencing external types/resources with the same module name are referenced correctly",
   277  		Skip:        allLanguages.Except("python/any"),
   278  	},
   279  	{
   280  		Directory:   "enum-reference",
   281  		Description: "Ensure referencing external types/resources with referenced enums import correctly",
   282  	},
   283  	{
   284  		Directory:   "external-enum",
   285  		Description: "Ensure we generate valid tokens for external enums",
   286  		Skip:        codegen.NewStringSet("dotnet/any"),
   287  	},
   288  	{
   289  		Directory:   "internal-dependencies-go",
   290  		Description: "Emit Go internal dependencies",
   291  		Skip:        allLanguages.Except("go/any"),
   292  	},
   293  	{
   294  		Directory:   "go-plain-ref-repro",
   295  		Description: "Generate a resource that accepts a plain input type",
   296  		Skip:        allLanguages.Except("go/any"),
   297  	},
   298  	{
   299  		Directory:   "go-nested-collections",
   300  		Description: "Generate a resource that outputs [][][]Foo",
   301  		Skip:        allLanguages.Except("go/any"),
   302  	},
   303  	{
   304  		Directory: "functions-secrets",
   305  		// Secret properties for non-Output<T> returning functions cannot be secret because they are plain.
   306  		Description: "functions that have properties that are secrets in the schema",
   307  	},
   308  	{
   309  		Directory:        "secrets",
   310  		Description:      "Generate a resource with secret properties",
   311  		SkipCompileCheck: codegen.NewStringSet(dotnet),
   312  	},
   313  	{
   314  		Directory:   "regress-py-tfbridge-611",
   315  		Description: "Regresses pulumi/pulumi-terraform-bridge#611",
   316  		Skip:        allLanguages.Except("python/any").Union(codegen.NewStringSet("python/test", "python/py_compile")),
   317  	},
   318  	{
   319  		Directory:   "hyphenated-symbols",
   320  		Description: "Test that types can have names with hyphens in them",
   321  		Skip:        allLanguages.Except("go/any").Except("python/any"),
   322  	},
   323  }
   324  
   325  var genSDKOnly bool
   326  
   327  func NoSDKCodegenChecks() bool {
   328  	return genSDKOnly
   329  }
   330  
   331  func init() {
   332  	noChecks := false
   333  	if env, ok := os.LookupEnv("PULUMI_TEST_SDK_NO_CHECKS"); ok {
   334  		noChecks, _ = strconv.ParseBool(env)
   335  	}
   336  	flag.BoolVar(&genSDKOnly, "sdk.no-checks", noChecks, "when set, skips all post-SDK-generation checks")
   337  
   338  	// NOTE: the testing package will call flag.Parse.
   339  }
   340  
   341  // SDKCodegenOptions describes the set of codegen tests for a language.
   342  type SDKCodegenOptions struct {
   343  	// Name of the programming language.
   344  	Language string
   345  
   346  	// Language-aware code generator; such as `GeneratePackage`.
   347  	// from `codegen/dotnet`.
   348  	GenPackage GenPkgSignature
   349  
   350  	// Extra checks for all the tests. They keys of this map are
   351  	// of the form "$language/$check" such as "go/compile".
   352  	Checks map[string]CodegenCheck
   353  
   354  	// The tests to run. A testcase `tt` are assumed to be located at
   355  	// ../testing/test/testdata/${tt.Directory}
   356  	TestCases []*SDKTest
   357  }
   358  
   359  // TestSDKCodegen runs the complete set of SDK code generation tests
   360  // against a particular language's code generator. It also verifies
   361  // that the generated code is structurally sound.
   362  //
   363  // The test files live in `pkg/codegen/testing/test/testdata` and
   364  // are registered in `var sdkTests` in `sdk_driver.go`.
   365  //
   366  // An SDK code generation test files consists of a schema and a set of
   367  // expected outputs for each language. Each test is structured as a
   368  // directory that contains that information:
   369  //
   370  //	testdata/
   371  //	    my-simple-schema/   # i.e. `simple-enum-schema`
   372  //	        schema.(json|yaml)
   373  //	        go/
   374  //	        python/
   375  //	        nodejs/
   376  //	        dotnet/
   377  //	        ...
   378  //
   379  // The schema is the only piece that *must* be manually authored.
   380  //
   381  // Once the schema has been written, the actual codegen outputs can be
   382  // generated by running the following in `pkg/codegen` directory:
   383  //
   384  //	PULUMI_ACCEPT=true go test ./...
   385  //
   386  // This will rebuild subfolders such as `go/` from scratch and store
   387  // the set of code-generated file names in `go/codegen-manifest.json`.
   388  // If these outputs look correct, they need to be checked into git and
   389  // will then serve as the expected values for the normal test runs:
   390  //
   391  //	go test ./...
   392  //
   393  // That is, the normal test runs will fail if changes to codegen or
   394  // schema lead to a diff in the generated file set. If the diff is
   395  // intentional, it can be accepted again via `PULUMI_ACCEPT=true`.
   396  //
   397  // To support running unit tests over the generated code, the tests
   398  // also support mixing in manually written `$lang-extras` files into
   399  // the generated tree. For example, given the following input:
   400  //
   401  //	testdata/
   402  //	    my-simple-schema/
   403  //	        schema.json
   404  //	        go/
   405  //	        go-extras/
   406  //	            tests/
   407  //	                go_test.go
   408  //
   409  // The system will copy `go-extras/tests/go_test.go` into
   410  // `go/tests/go_test.go` before performing compilation and unit test
   411  // checks over the project generated in `go`.
   412  func TestSDKCodegen(t *testing.T, opts *SDKCodegenOptions) { // revive:disable-line
   413  	if runtime.GOOS == "windows" {
   414  		t.Skip("TestSDKCodegen is skipped on Windows")
   415  	}
   416  
   417  	testDir := filepath.Join("..", "testing", "test", "testdata")
   418  
   419  	require.NotNil(t, opts.TestCases, "No test cases were provided. This was probably a mistake")
   420  	for _, tt := range opts.TestCases {
   421  		tt := tt // avoid capturing loop variable `sdkTest` in the closure
   422  
   423  		t.Run(tt.Directory, func(t *testing.T) {
   424  			t.Parallel()
   425  
   426  			tt.Mutex.Lock()
   427  			t.Cleanup(tt.Mutex.Unlock)
   428  
   429  			t.Log(tt.Description)
   430  
   431  			dirPath := filepath.Join(testDir, filepath.FromSlash(tt.Directory))
   432  
   433  			schemaPath := filepath.Join(dirPath, "schema.json")
   434  			if _, err := os.Stat(schemaPath); err != nil && os.IsNotExist(err) {
   435  				schemaPath = filepath.Join(dirPath, "schema.yaml")
   436  			}
   437  
   438  			if tt.ShouldSkipCodegen(opts.Language) {
   439  				t.Logf("Skipping generation + tests for %s", tt.Directory)
   440  				return
   441  			}
   442  
   443  			files, err := GeneratePackageFilesFromSchema(schemaPath, opts.GenPackage)
   444  			require.NoError(t, err)
   445  
   446  			if !RewriteFilesWhenPulumiAccept(t, dirPath, opts.Language, files) {
   447  				expectedFiles, err := LoadBaseline(dirPath, opts.Language)
   448  				require.NoError(t, err)
   449  
   450  				if !ValidateFileEquality(t, files, expectedFiles) {
   451  					t.Fail()
   452  				}
   453  			}
   454  
   455  			if genSDKOnly {
   456  				return
   457  			}
   458  
   459  			CopyExtraFiles(t, dirPath, opts.Language)
   460  
   461  			// Merge language-specific global and
   462  			// test-specific checks, with test-specific
   463  			// having precedence.
   464  			allChecks := make(map[string]CodegenCheck)
   465  			for k, v := range opts.Checks {
   466  				allChecks[k] = v
   467  			}
   468  			for k, v := range tt.Checks {
   469  				allChecks[k] = v
   470  			}
   471  
   472  			// Sort the checks in alphabetical order.
   473  			var checkOrder []string
   474  			for check := range allChecks {
   475  				checkOrder = append(checkOrder, check)
   476  			}
   477  			sort.Strings(checkOrder)
   478  
   479  			codeDir := filepath.Join(dirPath, opts.Language)
   480  
   481  			// Perform the checks.
   482  			//nolint:paralleltest // test functions are ordered
   483  			for _, check := range checkOrder {
   484  				check := check
   485  				t.Run(check, func(t *testing.T) {
   486  					if tt.ShouldSkipTest(opts.Language, check) {
   487  						t.Skip()
   488  					}
   489  					checkFun := allChecks[check]
   490  					checkFun(t, codeDir)
   491  				})
   492  			}
   493  		})
   494  	}
   495  }