// Copyright 2016-2020, 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. // Pulling out some of the repeated strings tokens into constants would harm readability, so we just ignore the // goconst linter's warning. // //nolint:lll, goconst package docs import ( "fmt" "testing" "github.com/pulumi/pulumi/pkg/v3/codegen/schema" "github.com/pulumi/pulumi/pkg/v3/codegen/testing/test" "github.com/stretchr/testify/assert" ) const ( unitTestTool = "Pulumi Resource Docs Unit Test" providerPackage = "prov" codeFence = "```" ) var simpleProperties = map[string]schema.PropertySpec{ "stringProp": { Description: "A string prop.", TypeSpec: schema.TypeSpec{ Type: "string", }, }, "boolProp": { Description: "A bool prop.", TypeSpec: schema.TypeSpec{ Type: "boolean", }, }, } // newTestPackageSpec returns a new fake package spec for a Provider used for testing. func newTestPackageSpec() schema.PackageSpec { pythonMapCase := map[string]schema.RawMessage{ "python": schema.RawMessage(`{"mapCase":false}`), } return schema.PackageSpec{ Name: providerPackage, Version: "0.0.1", Description: "A fake provider package used for testing.", Meta: &schema.MetadataSpec{ ModuleFormat: "(.*)(?:/[^/]*)", }, Types: map[string]schema.ComplexTypeSpec{ // Package-level types. "prov:/getPackageResourceOptions:getPackageResourceOptions": { ObjectTypeSpec: schema.ObjectTypeSpec{ Description: "Options object for the package-level function getPackageResource.", Type: "object", Properties: simpleProperties, }, }, // Module-level types. "prov:module/getModuleResourceOptions:getModuleResourceOptions": { ObjectTypeSpec: schema.ObjectTypeSpec{ Description: "Options object for the module-level function getModuleResource.", Type: "object", Properties: simpleProperties, }, }, "prov:module/ResourceOptions:ResourceOptions": { ObjectTypeSpec: schema.ObjectTypeSpec{ Description: "The resource options object.", Type: "object", Properties: map[string]schema.PropertySpec{ "stringProp": { Description: "A string prop.", Language: pythonMapCase, TypeSpec: schema.TypeSpec{ Type: "string", }, }, "boolProp": { Description: "A bool prop.", Language: pythonMapCase, TypeSpec: schema.TypeSpec{ Type: "boolean", }, }, "recursiveType": { Description: "I am a recursive type.", Language: pythonMapCase, TypeSpec: schema.TypeSpec{ Ref: "#/types/prov:module/ResourceOptions:ResourceOptions", }, }, }, }, }, "prov:module/ResourceOptions2:ResourceOptions2": { ObjectTypeSpec: schema.ObjectTypeSpec{ Description: "The resource options object.", Type: "object", Properties: map[string]schema.PropertySpec{ "uniqueProp": { Description: "This is a property unique to this type.", Language: pythonMapCase, TypeSpec: schema.TypeSpec{ Type: "number", }, }, }, }, }, }, Provider: schema.ResourceSpec{ ObjectTypeSpec: schema.ObjectTypeSpec{ Description: fmt.Sprintf("The provider type for the %s package.", providerPackage), Type: "object", }, InputProperties: map[string]schema.PropertySpec{ "stringProp": { Description: "A stringProp for the provider resource.", TypeSpec: schema.TypeSpec{ Type: "string", }, }, }, }, Resources: map[string]schema.ResourceSpec{ "prov:module2/resource2:Resource2": { ObjectTypeSpec: schema.ObjectTypeSpec{ Description: `This is a module-level resource called Resource. {{% examples %}} ## Example Usage {{% example %}} ### Basic Example ` + codeFence + `typescript // Some TypeScript code. ` + codeFence + ` ` + codeFence + `python # Some Python code. ` + codeFence + ` {{% /example %}} {{% example %}} ### Custom Sub-Domain Example ` + codeFence + `typescript // Some typescript code ` + codeFence + ` ` + codeFence + `python # Some Python code. ` + codeFence + ` {{% /example %}} {{% /examples %}} ## Import The import docs would be here ` + codeFence + `sh $ pulumi import prov:module/resource:Resource test test ` + codeFence + ` `, }, InputProperties: map[string]schema.PropertySpec{ "integerProp": { Description: "This is integerProp's description.", TypeSpec: schema.TypeSpec{ Type: "integer", }, }, "stringProp": { Description: "This is stringProp's description.", TypeSpec: schema.TypeSpec{ Type: "string", }, }, "boolProp": { Description: "A bool prop.", TypeSpec: schema.TypeSpec{ Type: "boolean", }, }, "optionsProp": { TypeSpec: schema.TypeSpec{ Ref: "#/types/prov:module/ResourceOptions:ResourceOptions", }, }, "options2Prop": { TypeSpec: schema.TypeSpec{ Ref: "#/types/prov:module/ResourceOptions2:ResourceOptions2", }, }, "recursiveType": { Description: "I am a recursive type.", TypeSpec: schema.TypeSpec{ Ref: "#/types/prov:module/ResourceOptions:ResourceOptions", }, }, }, }, "prov:module/resource:Resource": { ObjectTypeSpec: schema.ObjectTypeSpec{ Description: `This is a module-level resource called Resource. {{% examples %}} ## Example Usage {{% example %}} ### Basic Example ` + codeFence + `typescript // Some TypeScript code. ` + codeFence + ` ` + codeFence + `python # Some Python code. ` + codeFence + ` {{% /example %}} {{% example %}} ### Custom Sub-Domain Example ` + codeFence + `typescript // Some typescript code ` + codeFence + ` ` + codeFence + `python # Some Python code. ` + codeFence + ` {{% /example %}} {{% /examples %}} ## Import The import docs would be here ` + codeFence + `sh $ pulumi import prov:module/resource:Resource test test ` + codeFence + ` `, }, InputProperties: map[string]schema.PropertySpec{ "integerProp": { Description: "This is integerProp's description.", TypeSpec: schema.TypeSpec{ Type: "integer", }, }, "stringProp": { Description: "This is stringProp's description.", TypeSpec: schema.TypeSpec{ Type: "string", }, }, "boolProp": { Description: "A bool prop.", TypeSpec: schema.TypeSpec{ Type: "boolean", }, }, "optionsProp": { TypeSpec: schema.TypeSpec{ Ref: "#/types/prov:module/ResourceOptions:ResourceOptions", }, }, "options2Prop": { TypeSpec: schema.TypeSpec{ Ref: "#/types/prov:module/ResourceOptions2:ResourceOptions2", }, }, "recursiveType": { Description: "I am a recursive type.", TypeSpec: schema.TypeSpec{ Ref: "#/types/prov:module/ResourceOptions:ResourceOptions", }, }, }, }, "prov:/packageLevelResource:PackageLevelResource": { ObjectTypeSpec: schema.ObjectTypeSpec{ Description: "This is a package-level resource.", }, InputProperties: map[string]schema.PropertySpec{ "prop": { Description: "An input property.", TypeSpec: schema.TypeSpec{ Type: "string", }, }, }, }, }, Functions: map[string]schema.FunctionSpec{ // Package-level Functions. "prov:/getPackageResource:getPackageResource": { Description: "A package-level function.", Inputs: &schema.ObjectTypeSpec{ Description: "Inputs for getPackageResource.", Type: "object", Properties: map[string]schema.PropertySpec{ "options": { TypeSpec: schema.TypeSpec{ Ref: "#/types/prov:/getPackageResourceOptions:getPackageResourceOptions", }, }, }, }, Outputs: &schema.ObjectTypeSpec{ Description: "Outputs for getPackageResource.", Properties: simpleProperties, Type: "object", }, }, // Module-level Functions. "prov:module/getModuleResource:getModuleResource": { Description: "A module-level function.", Inputs: &schema.ObjectTypeSpec{ Description: "Inputs for getModuleResource.", Type: "object", Properties: map[string]schema.PropertySpec{ "options": { TypeSpec: schema.TypeSpec{ Ref: "#/types/prov:module/getModuleResource:getModuleResource", }, }, }, }, Outputs: &schema.ObjectTypeSpec{ Description: "Outputs for getModuleResource.", Properties: simpleProperties, Type: "object", }, }, }, } } func getResourceFromModule(resource string, mod *modContext) *schema.Resource { for _, r := range mod.resources { if resourceName(r) != resource { continue } return r } return nil } func getFunctionFromModule(function string, mod *modContext) *schema.Function { for _, f := range mod.functions { if tokenToName(f.Token) != function { continue } return f } return nil } func TestFunctionHeaders(t *testing.T) { t.Parallel() dctx := newDocGenContext() testPackageSpec := newTestPackageSpec() schemaPkg, err := schema.ImportSpec(testPackageSpec, nil) assert.NoError(t, err, "importing spec") tests := []struct { ExpectedTitleTag string FunctionName string ModuleName string ExpectedMetaDesc string }{ { FunctionName: "getPackageResource", // Empty string indicates the package-level root module. ModuleName: "", ExpectedTitleTag: "prov.getPackageResource", ExpectedMetaDesc: "Documentation for the prov.getPackageResource function with examples, input properties, output properties, and supporting types.", }, { FunctionName: "getModuleResource", ModuleName: "module", ExpectedTitleTag: "prov.module.getModuleResource", ExpectedMetaDesc: "Documentation for the prov.module.getModuleResource function with examples, input properties, output properties, and supporting types.", }, } modules := dctx.generateModulesFromSchemaPackage(unitTestTool, schemaPkg) for _, test := range tests { test := test t.Run(test.FunctionName, func(t *testing.T) { t.Parallel() mod, ok := modules[test.ModuleName] if !ok { t.Fatalf("could not find the module %s in modules map", test.ModuleName) } f := getFunctionFromModule(test.FunctionName, mod) if f == nil { t.Fatalf("could not find %s in modules", test.FunctionName) } h := mod.genFunctionHeader(f) assert.Equal(t, test.ExpectedTitleTag, h.TitleTag) assert.Equal(t, test.ExpectedMetaDesc, h.MetaDesc) }) } } func TestResourceDocHeader(t *testing.T) { t.Parallel() dctx := newDocGenContext() testPackageSpec := newTestPackageSpec() schemaPkg, err := schema.ImportSpec(testPackageSpec, nil) assert.NoError(t, err, "importing spec") tests := []struct { Name string ExpectedTitleTag string ResourceName string ModuleName string ExpectedMetaDesc string }{ { Name: "PackageLevelResourceHeader", ResourceName: "PackageLevelResource", // Empty string indicates the package-level root module. ModuleName: "", ExpectedTitleTag: "prov.PackageLevelResource", ExpectedMetaDesc: "Documentation for the prov.PackageLevelResource resource with examples, input properties, output properties, lookup functions, and supporting types.", }, { Name: "ModuleLevelResourceHeader", ResourceName: "Resource", ModuleName: "module", ExpectedTitleTag: "prov.module.Resource", ExpectedMetaDesc: "Documentation for the prov.module.Resource resource with examples, input properties, output properties, lookup functions, and supporting types.", }, } modules := dctx.generateModulesFromSchemaPackage(unitTestTool, schemaPkg) for _, test := range tests { test := test t.Run(test.Name, func(t *testing.T) { t.Parallel() mod, ok := modules[test.ModuleName] if !ok { t.Fatalf("could not find the module %s in modules map", test.ModuleName) } r := getResourceFromModule(test.ResourceName, mod) if r == nil { t.Fatalf("could not find %s in modules", test.ResourceName) } h := mod.genResourceHeader(r) assert.Equal(t, test.ExpectedTitleTag, h.TitleTag) assert.Equal(t, test.ExpectedMetaDesc, h.MetaDesc) }) } } func TestExamplesProcessing(t *testing.T) { t.Parallel() testPackageSpec := newTestPackageSpec() dctx := newDocGenContext() description := testPackageSpec.Resources["prov:module/resource:Resource"].Description docInfo := dctx.decomposeDocstring(description, dctx.getSupportedSnippetLanguages(false, nil)) examplesSection := docInfo.examples importSection := docInfo.importDetails assert.NotEmpty(t, importSection) // The resource under test has two examples and both have TS and Python examples. assert.Equal(t, 2, len(examplesSection)) assert.Equal(t, "### Basic Example", examplesSection[0].Title) assert.Equal(t, "### Custom Sub-Domain Example", examplesSection[1].Title) expectedLangSnippets := []string{"typescript", "python"} otherLangSnippets := []string{"csharp", "go"} for _, e := range examplesSection { for _, lang := range expectedLangSnippets { _, ok := e.Snippets[lang] assert.True(t, ok, "Could not find %s snippet", lang) } for _, lang := range otherLangSnippets { snippet, ok := e.Snippets[lang] assert.True(t, ok, "Expected to find default placeholders for other languages") assert.Contains(t, "Coming soon!", snippet) } } } func generatePackage(tool string, pkg *schema.Package, extraFiles map[string][]byte) (map[string][]byte, error) { dctx := newDocGenContext() dctx.initialize(tool, pkg) return dctx.generatePackage(tool, pkg) } func TestGeneratePackage(t *testing.T) { t.Parallel() test.TestSDKCodegen(t, &test.SDKCodegenOptions{ Language: "docs", GenPackage: generatePackage, TestCases: test.PulumiPulumiSDKTests, }) } func TestDecomposeDocstring(t *testing.T) { t.Parallel() awsVpcDocs := "Provides a VPC resource.\n" + "\n" + "{{% examples %}}\n" + "## Example Usage\n" + "{{% example %}}\n" + "\n" + "Basic usage:\n" + "\n" + "```typescript\n" + "Basic usage: typescript\n" + "```\n" + "```python\n" + "Basic usage: python\n" + "```\n" + "```csharp\n" + "Basic usage: csharp\n" + "```\n" + "```go\n" + "Basic usage: go\n" + "```\n" + "```java\n" + "Basic usage: java\n" + "```\n" + "```yaml\n" + "Basic usage: yaml\n" + "```\n" + "\n" + "Basic usage with tags:\n" + "\n" + "```typescript\n" + "Basic usage with tags: typescript\n" + "```\n" + "```python\n" + "Basic usage with tags: python\n" + "```\n" + "```csharp\n" + "Basic usage with tags: csharp\n" + "```\n" + "```go\n" + "Basic usage with tags: go\n" + "```\n" + "```java\n" + "Basic usage with tags: java\n" + "```\n" + "```yaml\n" + "Basic usage with tags: yaml\n" + "```\n" + "\n" + "VPC with CIDR from AWS IPAM:\n" + "\n" + "```typescript\n" + "VPC with CIDR from AWS IPAM: typescript\n" + "```\n" + "```python\n" + "VPC with CIDR from AWS IPAM: python\n" + "```\n" + "```csharp\n" + "VPC with CIDR from AWS IPAM: csharp\n" + "```\n" + "```java\n" + "VPC with CIDR from AWS IPAM: java\n" + "```\n" + "```yaml\n" + "VPC with CIDR from AWS IPAM: yaml\n" + "```\n" + "{{% /example %}}\n" + "{{% /examples %}}\n" + "\n" + "## Import\n" + "\n" + "VPCs 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" + "\n" + " " dctx := newDocGenContext() info := dctx.decomposeDocstring(awsVpcDocs, dctx.getSupportedSnippetLanguages(false, nil)) assert.Equal(t, docInfo{ description: "Provides a VPC resource.\n", examples: []exampleSection{ { Title: "Basic usage", Snippets: map[string]string{ "csharp": "```csharp\nBasic usage: csharp\n```\n", "go": "```go\nBasic usage: go\n```\n", "java": "```java\nBasic usage: java\n```\n", "python": "```python\nBasic usage: python\n```\n", "typescript": "\n```typescript\nBasic usage: typescript\n```\n", "yaml": "```yaml\nBasic usage: yaml\n```\n", }, }, { Title: "Basic usage with tags", Snippets: map[string]string{ "csharp": "```csharp\nBasic usage with tags: csharp\n```\n", "go": "```go\nBasic usage with tags: go\n```\n", "java": "```java\nBasic usage with tags: java\n```\n", "python": "```python\nBasic usage with tags: python\n```\n", "typescript": "\n```typescript\nBasic usage with tags: typescript\n```\n", "yaml": "```yaml\nBasic usage with tags: yaml\n```\n", }, }, { Title: "VPC with CIDR from AWS IPAM", Snippets: map[string]string{ "csharp": "```csharp\nVPC with CIDR from AWS IPAM: csharp\n```\n", "go": "Coming soon!", "java": "```java\nVPC with CIDR from AWS IPAM: java\n```\n", "python": "```python\nVPC with CIDR from AWS IPAM: python\n```\n", "typescript": "\n```typescript\nVPC with CIDR from AWS IPAM: typescript\n```\n", "yaml": "```yaml\nVPC with CIDR from AWS IPAM: yaml\n```\n", }, }, }, 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", }, info) } func TestGenOverlayResource(t *testing.T) { t.Parallel() getSchemaPackage := func() *schema.Package { testPackageSpec := newTestPackageSpec() testPackageSpec.Resources["prov:module/overlayResource:OverlayResource"] = schema.ResourceSpec{ ObjectTypeSpec: schema.ObjectTypeSpec{ Description: "This is a module-level resource called OverlayResource.", IsOverlay: true, }, InputProperties: map[string]schema.PropertySpec{ "prop": { Description: "An input property.", TypeSpec: schema.TypeSpec{ Type: "string", }, }, }, } testPackageSpec.Resources["prov:module/overlayResourceConstrainedLanguages:OverlayResourceConstrainedLanguages"] = schema.ResourceSpec{ ObjectTypeSpec: schema.ObjectTypeSpec{ Description: "This is a module-level resource called OverlayResourceConstrainedLanguages.", IsOverlay: true, OverlaySupportedLanguages: []string{"python", "go", "nodejs"}, }, InputProperties: map[string]schema.PropertySpec{ "prop": { Description: "An input property.", TypeSpec: schema.TypeSpec{ Type: "string", }, }, }, } testPackageSpec.Resources["prov:module/overlayResourceWrongLanguage:OverlayResourceWrongLanguage"] = schema.ResourceSpec{ ObjectTypeSpec: schema.ObjectTypeSpec{ Description: "This is a module-level resource called OverlayResourceWrongLanguage.", IsOverlay: true, OverlaySupportedLanguages: []string{"python", "go", "nodejs", "smalltalk"}, }, InputProperties: map[string]schema.PropertySpec{ "prop": { Description: "An input property.", TypeSpec: schema.TypeSpec{ Type: "string", }, }, }, } schemaPkg, err := schema.ImportSpec(testPackageSpec, nil) assert.NoError(t, err, "importing spec") return schemaPkg } tests := []struct { ResourceName string ExpectedLangChooserLanguages string }{ { // regular resource, should support all languages (i.e. ExpectedLangChooserLanguages should be empty) ResourceName: "Resource", ExpectedLangChooserLanguages: "csharp,go,typescript,python,yaml,java", }, { // regular overlay resource, should support all languages (i.e. ExpectedLangChooserLanguages should be empty) ResourceName: "OverlayResource", ExpectedLangChooserLanguages: "csharp,go,typescript,python,yaml,java", }, { // overlay resource with a constrained list of supported languages // should support only the languages specified in OverlaySupportedLanguages ResourceName: "OverlayResourceConstrainedLanguages", ExpectedLangChooserLanguages: "python,go,typescript", }, { // overlay resource with a wrong language in OverlaySupportedLanguages // should filter out the wrong language ResourceName: "OverlayResourceWrongLanguage", ExpectedLangChooserLanguages: "python,go,typescript", }, } for _, test := range tests { test := test t.Run(test.ResourceName, func(t *testing.T) { t.Parallel() dctx := newDocGenContext() schemaPkg := getSchemaPackage() modules := dctx.generateModulesFromSchemaPackage(unitTestTool, schemaPkg) dctx.initialize(unitTestTool, schemaPkg) mod, ok := modules["module"] if !ok { t.Fatalf("could not find the module 'module' in modules map") } r := getResourceFromModule(test.ResourceName, mod) if r == nil { t.Fatalf("could not find %s in modules", test.ResourceName) } resourceDocs := mod.genResource(r) assert.Equal(t, test.ExpectedLangChooserLanguages, resourceDocs.LangChooserLanguages) }) } } func TestGenOverlayFunction(t *testing.T) { t.Parallel() getSchemaPackage := func() *schema.Package { testPackageSpec := newTestPackageSpec() testPackageSpec.Functions["prov:module/overlayFunction:overlayFunction"] = schema.FunctionSpec{ Description: "A module-level function.", IsOverlay: true, Inputs: &schema.ObjectTypeSpec{ Description: "Inputs for getModuleResource.", Type: "object", Properties: map[string]schema.PropertySpec{ "options": { TypeSpec: schema.TypeSpec{ Ref: "#/types/prov:module/getModuleResource:getModuleResource", }, }, }, }, Outputs: &schema.ObjectTypeSpec{ Description: "Outputs for getModuleResource.", Properties: simpleProperties, Type: "object", }, } testPackageSpec.Functions["prov:module/overlayFunctionConstrainedLanguages:overlayFunctionConstrainedLanguages"] = schema.FunctionSpec{ Description: "A module-level function.", IsOverlay: true, OverlaySupportedLanguages: []string{"python", "go", "nodejs"}, Inputs: &schema.ObjectTypeSpec{ Description: "Inputs for getModuleResource.", Type: "object", Properties: map[string]schema.PropertySpec{ "options": { TypeSpec: schema.TypeSpec{ Ref: "#/types/prov:module/getModuleResource:getModuleResource", }, }, }, }, Outputs: &schema.ObjectTypeSpec{ Description: "Outputs for getModuleResource.", Properties: simpleProperties, Type: "object", }, } testPackageSpec.Functions["prov:module/overlayFunctionWrongLanguage:overlayFunctionWrongLanguage"] = schema.FunctionSpec{ Description: "A module-level function.", IsOverlay: true, OverlaySupportedLanguages: []string{"python", "go", "nodejs", "smalltalk"}, Inputs: &schema.ObjectTypeSpec{ Description: "Inputs for getModuleResource.", Type: "object", Properties: map[string]schema.PropertySpec{ "options": { TypeSpec: schema.TypeSpec{ Ref: "#/types/prov:module/getModuleResource:getModuleResource", }, }, }, }, Outputs: &schema.ObjectTypeSpec{ Description: "Outputs for getModuleResource.", Properties: simpleProperties, Type: "object", }, } schemaPkg, err := schema.ImportSpec(testPackageSpec, nil) assert.NoError(t, err, "importing spec") return schemaPkg } tests := []struct { FunctionName string ExpectedLangChooserLanguages string }{ { // regular function, should support all languages FunctionName: "getModuleResource", ExpectedLangChooserLanguages: "csharp,go,typescript,python,yaml,java", }, { // regular overlay function, should support all languages FunctionName: "overlayFunction", ExpectedLangChooserLanguages: "csharp,go,typescript,python,yaml,java", }, { // overlay function with a constrained list of supported languages // should support only the languages specified in OverlaySupportedLanguages FunctionName: "overlayFunctionConstrainedLanguages", ExpectedLangChooserLanguages: "python,go,typescript", }, { // overlay function with a wrong language in OverlaySupportedLanguages // should filter out the wrong language FunctionName: "overlayFunctionWrongLanguage", ExpectedLangChooserLanguages: "python,go,typescript", }, } for _, test := range tests { test := test t.Run(test.FunctionName, func(t *testing.T) { t.Parallel() dctx := newDocGenContext() schemaPkg := getSchemaPackage() modules := dctx.generateModulesFromSchemaPackage(unitTestTool, schemaPkg) dctx.initialize(unitTestTool, schemaPkg) mod, ok := modules["module"] if !ok { t.Fatalf("could not find the module 'module' in modules map") } f := getFunctionFromModule(test.FunctionName, mod) if f == nil { t.Fatalf("could not find %s in modules", test.FunctionName) } resourceDocs := mod.genFunction(f) assert.Equal(t, test.ExpectedLangChooserLanguages, resourceDocs.LangChooserLanguages) }) } }