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 }