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  }