github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/codegen/docs/gen_test.go (about) 1 // Copyright 2016-2020, Pulumi Corporation. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Pulling out some of the repeated strings tokens into constants would harm readability, so we just ignore the 16 // goconst linter's warning. 17 // 18 // nolint: lll, goconst 19 package docs 20 21 import ( 22 "fmt" 23 "testing" 24 25 "github.com/pulumi/pulumi/pkg/v3/codegen/schema" 26 "github.com/pulumi/pulumi/pkg/v3/codegen/testing/test" 27 "github.com/stretchr/testify/assert" 28 ) 29 30 const ( 31 unitTestTool = "Pulumi Resource Docs Unit Test" 32 providerPackage = "prov" 33 codeFence = "```" 34 ) 35 36 var ( 37 simpleProperties = map[string]schema.PropertySpec{ 38 "stringProp": { 39 Description: "A string prop.", 40 TypeSpec: schema.TypeSpec{ 41 Type: "string", 42 }, 43 }, 44 "boolProp": { 45 Description: "A bool prop.", 46 TypeSpec: schema.TypeSpec{ 47 Type: "boolean", 48 }, 49 }, 50 } 51 52 // testPackageSpec represents a fake package spec for a Provider used for testing. 53 testPackageSpec schema.PackageSpec 54 ) 55 56 func initTestPackageSpec(t *testing.T) { 57 t.Helper() 58 59 pythonMapCase := map[string]schema.RawMessage{ 60 "python": schema.RawMessage(`{"mapCase":false}`), 61 } 62 testPackageSpec = schema.PackageSpec{ 63 Name: providerPackage, 64 Version: "0.0.1", 65 Description: "A fake provider package used for testing.", 66 Meta: &schema.MetadataSpec{ 67 ModuleFormat: "(.*)(?:/[^/]*)", 68 }, 69 Types: map[string]schema.ComplexTypeSpec{ 70 // Package-level types. 71 "prov:/getPackageResourceOptions:getPackageResourceOptions": { 72 ObjectTypeSpec: schema.ObjectTypeSpec{ 73 Description: "Options object for the package-level function getPackageResource.", 74 Type: "object", 75 Properties: simpleProperties, 76 }, 77 }, 78 79 // Module-level types. 80 "prov:module/getModuleResourceOptions:getModuleResourceOptions": { 81 ObjectTypeSpec: schema.ObjectTypeSpec{ 82 Description: "Options object for the module-level function getModuleResource.", 83 Type: "object", 84 Properties: simpleProperties, 85 }, 86 }, 87 "prov:module/ResourceOptions:ResourceOptions": { 88 ObjectTypeSpec: schema.ObjectTypeSpec{ 89 Description: "The resource options object.", 90 Type: "object", 91 Properties: map[string]schema.PropertySpec{ 92 "stringProp": { 93 Description: "A string prop.", 94 Language: pythonMapCase, 95 TypeSpec: schema.TypeSpec{ 96 Type: "string", 97 }, 98 }, 99 "boolProp": { 100 Description: "A bool prop.", 101 Language: pythonMapCase, 102 TypeSpec: schema.TypeSpec{ 103 Type: "boolean", 104 }, 105 }, 106 "recursiveType": { 107 Description: "I am a recursive type.", 108 Language: pythonMapCase, 109 TypeSpec: schema.TypeSpec{ 110 Ref: "#/types/prov:module/ResourceOptions:ResourceOptions", 111 }, 112 }, 113 }, 114 }, 115 }, 116 "prov:module/ResourceOptions2:ResourceOptions2": { 117 ObjectTypeSpec: schema.ObjectTypeSpec{ 118 Description: "The resource options object.", 119 Type: "object", 120 Properties: map[string]schema.PropertySpec{ 121 "uniqueProp": { 122 Description: "This is a property unique to this type.", 123 Language: pythonMapCase, 124 TypeSpec: schema.TypeSpec{ 125 Type: "number", 126 }, 127 }, 128 }, 129 }, 130 }, 131 }, 132 Provider: schema.ResourceSpec{ 133 ObjectTypeSpec: schema.ObjectTypeSpec{ 134 Description: fmt.Sprintf("The provider type for the %s package.", providerPackage), 135 Type: "object", 136 }, 137 InputProperties: map[string]schema.PropertySpec{ 138 "stringProp": { 139 Description: "A stringProp for the provider resource.", 140 TypeSpec: schema.TypeSpec{ 141 Type: "string", 142 }, 143 }, 144 }, 145 }, 146 Resources: map[string]schema.ResourceSpec{ 147 "prov:module2/resource2:Resource2": { 148 ObjectTypeSpec: schema.ObjectTypeSpec{ 149 Description: `This is a module-level resource called Resource. 150 {{% examples %}} 151 ## Example Usage 152 153 {{% example %}} 154 ### Basic Example 155 156 ` + codeFence + `typescript 157 // Some TypeScript code. 158 ` + codeFence + ` 159 ` + codeFence + `python 160 # Some Python code. 161 ` + codeFence + ` 162 {{% /example %}} 163 {{% example %}} 164 ### Custom Sub-Domain Example 165 166 ` + codeFence + `typescript 167 // Some typescript code 168 ` + codeFence + ` 169 ` + codeFence + `python 170 # Some Python code. 171 ` + codeFence + ` 172 {{% /example %}} 173 {{% /examples %}} 174 175 ## Import 176 177 The import docs would be here 178 179 ` + codeFence + `sh 180 $ pulumi import prov:module/resource:Resource test test 181 ` + codeFence + ` 182 `, 183 }, 184 InputProperties: map[string]schema.PropertySpec{ 185 "integerProp": { 186 Description: "This is integerProp's description.", 187 TypeSpec: schema.TypeSpec{ 188 Type: "integer", 189 }, 190 }, 191 "stringProp": { 192 Description: "This is stringProp's description.", 193 TypeSpec: schema.TypeSpec{ 194 Type: "string", 195 }, 196 }, 197 "boolProp": { 198 Description: "A bool prop.", 199 TypeSpec: schema.TypeSpec{ 200 Type: "boolean", 201 }, 202 }, 203 "optionsProp": { 204 TypeSpec: schema.TypeSpec{ 205 Ref: "#/types/prov:module/ResourceOptions:ResourceOptions", 206 }, 207 }, 208 "options2Prop": { 209 TypeSpec: schema.TypeSpec{ 210 Ref: "#/types/prov:module/ResourceOptions2:ResourceOptions2", 211 }, 212 }, 213 "recursiveType": { 214 Description: "I am a recursive type.", 215 TypeSpec: schema.TypeSpec{ 216 Ref: "#/types/prov:module/ResourceOptions:ResourceOptions", 217 }, 218 }, 219 }, 220 }, 221 "prov:module/resource:Resource": { 222 ObjectTypeSpec: schema.ObjectTypeSpec{ 223 Description: `This is a module-level resource called Resource. 224 {{% examples %}} 225 ## Example Usage 226 227 {{% example %}} 228 ### Basic Example 229 230 ` + codeFence + `typescript 231 // Some TypeScript code. 232 ` + codeFence + ` 233 ` + codeFence + `python 234 # Some Python code. 235 ` + codeFence + ` 236 {{% /example %}} 237 {{% example %}} 238 ### Custom Sub-Domain Example 239 240 ` + codeFence + `typescript 241 // Some typescript code 242 ` + codeFence + ` 243 ` + codeFence + `python 244 # Some Python code. 245 ` + codeFence + ` 246 {{% /example %}} 247 {{% /examples %}} 248 249 ## Import 250 251 The import docs would be here 252 253 ` + codeFence + `sh 254 $ pulumi import prov:module/resource:Resource test test 255 ` + codeFence + ` 256 `, 257 }, 258 InputProperties: map[string]schema.PropertySpec{ 259 "integerProp": { 260 Description: "This is integerProp's description.", 261 TypeSpec: schema.TypeSpec{ 262 Type: "integer", 263 }, 264 }, 265 "stringProp": { 266 Description: "This is stringProp's description.", 267 TypeSpec: schema.TypeSpec{ 268 Type: "string", 269 }, 270 }, 271 "boolProp": { 272 Description: "A bool prop.", 273 TypeSpec: schema.TypeSpec{ 274 Type: "boolean", 275 }, 276 }, 277 "optionsProp": { 278 TypeSpec: schema.TypeSpec{ 279 Ref: "#/types/prov:module/ResourceOptions:ResourceOptions", 280 }, 281 }, 282 "options2Prop": { 283 TypeSpec: schema.TypeSpec{ 284 Ref: "#/types/prov:module/ResourceOptions2:ResourceOptions2", 285 }, 286 }, 287 "recursiveType": { 288 Description: "I am a recursive type.", 289 TypeSpec: schema.TypeSpec{ 290 Ref: "#/types/prov:module/ResourceOptions:ResourceOptions", 291 }, 292 }, 293 }, 294 }, 295 "prov:/packageLevelResource:PackageLevelResource": { 296 ObjectTypeSpec: schema.ObjectTypeSpec{ 297 Description: "This is a package-level resource.", 298 }, 299 InputProperties: map[string]schema.PropertySpec{ 300 "prop": { 301 Description: "An input property.", 302 TypeSpec: schema.TypeSpec{ 303 Type: "string", 304 }, 305 }, 306 }, 307 }, 308 }, 309 Functions: map[string]schema.FunctionSpec{ 310 // Package-level Functions. 311 "prov:/getPackageResource:getPackageResource": { 312 Description: "A package-level function.", 313 Inputs: &schema.ObjectTypeSpec{ 314 Description: "Inputs for getPackageResource.", 315 Type: "object", 316 Properties: map[string]schema.PropertySpec{ 317 "options": { 318 TypeSpec: schema.TypeSpec{ 319 Ref: "#/types/prov:/getPackageResourceOptions:getPackageResourceOptions", 320 }, 321 }, 322 }, 323 }, 324 Outputs: &schema.ObjectTypeSpec{ 325 Description: "Outputs for getPackageResource.", 326 Properties: simpleProperties, 327 Type: "object", 328 }, 329 }, 330 331 // Module-level Functions. 332 "prov:module/getModuleResource:getModuleResource": { 333 Description: "A module-level function.", 334 Inputs: &schema.ObjectTypeSpec{ 335 Description: "Inputs for getModuleResource.", 336 Type: "object", 337 Properties: map[string]schema.PropertySpec{ 338 "options": { 339 TypeSpec: schema.TypeSpec{ 340 Ref: "#/types/prov:module/getModuleResource:getModuleResource", 341 }, 342 }, 343 }, 344 }, 345 Outputs: &schema.ObjectTypeSpec{ 346 Description: "Outputs for getModuleResource.", 347 Properties: simpleProperties, 348 Type: "object", 349 }, 350 }, 351 }, 352 } 353 } 354 355 func getResourceFromModule(resource string, mod *modContext) *schema.Resource { 356 for _, r := range mod.resources { 357 if resourceName(r) != resource { 358 continue 359 } 360 return r 361 } 362 return nil 363 } 364 365 func getFunctionFromModule(function string, mod *modContext) *schema.Function { 366 for _, f := range mod.functions { 367 if tokenToName(f.Token) != function { 368 continue 369 } 370 return f 371 } 372 return nil 373 } 374 375 func TestFunctionHeaders(t *testing.T) { 376 t.Parallel() 377 378 dctx := newDocGenContext() 379 initTestPackageSpec(t) 380 381 schemaPkg, err := schema.ImportSpec(testPackageSpec, nil) 382 assert.NoError(t, err, "importing spec") 383 384 tests := []struct { 385 ExpectedTitleTag string 386 FunctionName string 387 ModuleName string 388 ExpectedMetaDesc string 389 }{ 390 { 391 FunctionName: "getPackageResource", 392 // Empty string indicates the package-level root module. 393 ModuleName: "", 394 ExpectedTitleTag: "prov.getPackageResource", 395 ExpectedMetaDesc: "Documentation for the prov.getPackageResource function with examples, input properties, output properties, and supporting types.", 396 }, 397 { 398 FunctionName: "getModuleResource", 399 ModuleName: "module", 400 ExpectedTitleTag: "prov.module.getModuleResource", 401 ExpectedMetaDesc: "Documentation for the prov.module.getModuleResource function with examples, input properties, output properties, and supporting types.", 402 }, 403 } 404 405 modules := dctx.generateModulesFromSchemaPackage(unitTestTool, schemaPkg) 406 for _, test := range tests { 407 test := test 408 t.Run(test.FunctionName, func(t *testing.T) { 409 t.Parallel() 410 411 mod, ok := modules[test.ModuleName] 412 if !ok { 413 t.Fatalf("could not find the module %s in modules map", test.ModuleName) 414 } 415 416 f := getFunctionFromModule(test.FunctionName, mod) 417 if f == nil { 418 t.Fatalf("could not find %s in modules", test.FunctionName) 419 } 420 h := mod.genFunctionHeader(f) 421 assert.Equal(t, test.ExpectedTitleTag, h.TitleTag) 422 assert.Equal(t, test.ExpectedMetaDesc, h.MetaDesc) 423 }) 424 } 425 } 426 427 func TestResourceDocHeader(t *testing.T) { 428 t.Parallel() 429 430 dctx := newDocGenContext() 431 initTestPackageSpec(t) 432 433 schemaPkg, err := schema.ImportSpec(testPackageSpec, nil) 434 assert.NoError(t, err, "importing spec") 435 436 tests := []struct { 437 Name string 438 ExpectedTitleTag string 439 ResourceName string 440 ModuleName string 441 ExpectedMetaDesc string 442 }{ 443 { 444 Name: "PackageLevelResourceHeader", 445 ResourceName: "PackageLevelResource", 446 // Empty string indicates the package-level root module. 447 ModuleName: "", 448 ExpectedTitleTag: "prov.PackageLevelResource", 449 ExpectedMetaDesc: "Documentation for the prov.PackageLevelResource resource with examples, input properties, output properties, lookup functions, and supporting types.", 450 }, 451 { 452 Name: "ModuleLevelResourceHeader", 453 ResourceName: "Resource", 454 ModuleName: "module", 455 ExpectedTitleTag: "prov.module.Resource", 456 ExpectedMetaDesc: "Documentation for the prov.module.Resource resource with examples, input properties, output properties, lookup functions, and supporting types.", 457 }, 458 } 459 460 modules := dctx.generateModulesFromSchemaPackage(unitTestTool, schemaPkg) 461 for _, test := range tests { 462 test := test 463 t.Run(test.Name, func(t *testing.T) { 464 t.Parallel() 465 466 mod, ok := modules[test.ModuleName] 467 if !ok { 468 t.Fatalf("could not find the module %s in modules map", test.ModuleName) 469 } 470 471 r := getResourceFromModule(test.ResourceName, mod) 472 if r == nil { 473 t.Fatalf("could not find %s in modules", test.ResourceName) 474 } 475 h := mod.genResourceHeader(r) 476 assert.Equal(t, test.ExpectedTitleTag, h.TitleTag) 477 assert.Equal(t, test.ExpectedMetaDesc, h.MetaDesc) 478 }) 479 } 480 } 481 482 func TestExamplesProcessing(t *testing.T) { 483 t.Parallel() 484 485 initTestPackageSpec(t) 486 dctx := newDocGenContext() 487 488 description := testPackageSpec.Resources["prov:module/resource:Resource"].Description 489 docInfo := dctx.decomposeDocstring(description) 490 examplesSection := docInfo.examples 491 importSection := docInfo.importDetails 492 493 assert.NotEmpty(t, importSection) 494 495 // The resource under test has two examples and both have TS and Python examples. 496 assert.Equal(t, 2, len(examplesSection)) 497 assert.Equal(t, "### Basic Example", examplesSection[0].Title) 498 assert.Equal(t, "### Custom Sub-Domain Example", examplesSection[1].Title) 499 expectedLangSnippets := []string{"typescript", "python"} 500 otherLangSnippets := []string{"csharp", "go"} 501 for _, e := range examplesSection { 502 for _, lang := range expectedLangSnippets { 503 _, ok := e.Snippets[lang] 504 assert.True(t, ok, "Could not find %s snippet", lang) 505 } 506 for _, lang := range otherLangSnippets { 507 snippet, ok := e.Snippets[lang] 508 assert.True(t, ok, "Expected to find default placeholders for other languages") 509 assert.Contains(t, "Coming soon!", snippet) 510 } 511 } 512 } 513 514 func generatePackage(tool string, pkg *schema.Package, extraFiles map[string][]byte) (map[string][]byte, error) { 515 dctx := newDocGenContext() 516 dctx.initialize(tool, pkg) 517 return dctx.generatePackage(tool, pkg) 518 } 519 520 func TestGeneratePackage(t *testing.T) { 521 t.Parallel() 522 523 test.TestSDKCodegen(t, &test.SDKCodegenOptions{ 524 Language: "docs", 525 GenPackage: generatePackage, 526 TestCases: test.PulumiPulumiSDKTests, 527 }) 528 } 529 530 func TestDecomposeDocstring(t *testing.T) { 531 t.Parallel() 532 awsVpcDocs := "Provides a VPC resource.\n" + 533 "\n" + 534 "{{% examples %}}\n" + 535 "## Example Usage\n" + 536 "{{% example %}}\n" + 537 "\n" + 538 "Basic usage:\n" + 539 "\n" + 540 "```typescript\n" + 541 "Basic usage: typescript\n" + 542 "```\n" + 543 "```python\n" + 544 "Basic usage: python\n" + 545 "```\n" + 546 "```csharp\n" + 547 "Basic usage: csharp\n" + 548 "```\n" + 549 "```go\n" + 550 "Basic usage: go\n" + 551 "```\n" + 552 "```java\n" + 553 "Basic usage: java\n" + 554 "```\n" + 555 "```yaml\n" + 556 "Basic usage: yaml\n" + 557 "```\n" + 558 "\n" + 559 "Basic usage with tags:\n" + 560 "\n" + 561 "```typescript\n" + 562 "Basic usage with tags: typescript\n" + 563 "```\n" + 564 "```python\n" + 565 "Basic usage with tags: python\n" + 566 "```\n" + 567 "```csharp\n" + 568 "Basic usage with tags: csharp\n" + 569 "```\n" + 570 "```go\n" + 571 "Basic usage with tags: go\n" + 572 "```\n" + 573 "```java\n" + 574 "Basic usage with tags: java\n" + 575 "```\n" + 576 "```yaml\n" + 577 "Basic usage with tags: yaml\n" + 578 "```\n" + 579 "\n" + 580 "VPC with CIDR from AWS IPAM:\n" + 581 "\n" + 582 "```typescript\n" + 583 "VPC with CIDR from AWS IPAM: typescript\n" + 584 "```\n" + 585 "```python\n" + 586 "VPC with CIDR from AWS IPAM: python\n" + 587 "```\n" + 588 "```csharp\n" + 589 "VPC with CIDR from AWS IPAM: csharp\n" + 590 "```\n" + 591 "```java\n" + 592 "VPC with CIDR from AWS IPAM: java\n" + 593 "```\n" + 594 "```yaml\n" + 595 "VPC with CIDR from AWS IPAM: yaml\n" + 596 "```\n" + 597 "{{% /example %}}\n" + 598 "{{% /examples %}}\n" + 599 "\n" + 600 "## Import\n" + 601 "\n" + 602 "VPCs can be imported using the `vpc id`, e.g.,\n" + 603 "\n" + 604 "```sh\n" + 605 " $ pulumi import aws:ec2/vpc:Vpc test_vpc vpc-a01106c2\n" + 606 "```\n" + 607 "\n" + 608 " " 609 dctx := newDocGenContext() 610 611 info := dctx.decomposeDocstring(awsVpcDocs) 612 assert.Equal(t, docInfo{ 613 description: "Provides a VPC resource.\n", 614 examples: []exampleSection{ 615 { 616 Title: "Basic usage", 617 Snippets: map[string]string{ 618 "csharp": "```csharp\nBasic usage: csharp\n```\n", 619 "go": "```go\nBasic usage: go\n```\n", 620 "java": "```java\nBasic usage: java\n```\n", 621 "python": "```python\nBasic usage: python\n```\n", 622 "typescript": "\n```typescript\nBasic usage: typescript\n```\n", 623 "yaml": "```yaml\nBasic usage: yaml\n```\n", 624 }, 625 }, 626 { 627 Title: "Basic usage with tags", 628 Snippets: map[string]string{ 629 "csharp": "```csharp\nBasic usage with tags: csharp\n```\n", 630 "go": "```go\nBasic usage with tags: go\n```\n", 631 "java": "```java\nBasic usage with tags: java\n```\n", 632 "python": "```python\nBasic usage with tags: python\n```\n", 633 "typescript": "\n```typescript\nBasic usage with tags: typescript\n```\n", 634 "yaml": "```yaml\nBasic usage with tags: yaml\n```\n", 635 }, 636 }, 637 { 638 Title: "VPC with CIDR from AWS IPAM", 639 Snippets: map[string]string{ 640 "csharp": "```csharp\nVPC with CIDR from AWS IPAM: csharp\n```\n", 641 "go": "Coming soon!", 642 "java": "```java\nVPC with CIDR from AWS IPAM: java\n```\n", 643 "python": "```python\nVPC with CIDR from AWS IPAM: python\n```\n", 644 "typescript": "\n```typescript\nVPC with CIDR from AWS IPAM: typescript\n```\n", 645 "yaml": "```yaml\nVPC with CIDR from AWS IPAM: yaml\n```\n", 646 }, 647 }, 648 }, 649 importDetails: "\n\nVPCs can be imported using the `vpc id`, e.g.,\n\n```sh\n $ pulumi import aws:ec2/vpc:Vpc test_vpc vpc-a01106c2\n```\n"}, 650 info) 651 }