2020-03-20 15:17:58 +00:00
// 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.
//
2023-01-06 00:07:45 +00:00
//nolint:lll, goconst
2020-03-20 15:17:58 +00:00
package docs
import (
2021-10-19 22:21:39 +00:00
"fmt"
2021-11-13 02:37:17 +00:00
"testing"
2021-03-17 13:20:05 +00:00
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
2022-02-07 11:10:04 +00:00
"github.com/pulumi/pulumi/pkg/v3/codegen/testing/test"
2020-03-20 15:17:58 +00:00
"github.com/stretchr/testify/assert"
)
const (
unitTestTool = "Pulumi Resource Docs Unit Test"
providerPackage = "prov"
2020-05-08 23:25:28 +00:00
codeFence = "```"
2020-03-20 15:17:58 +00:00
)
2023-03-03 16:36:39 +00:00
var simpleProperties = map [ string ] schema . PropertySpec {
"stringProp" : {
Description : "A string prop." ,
TypeSpec : schema . TypeSpec {
Type : "string" ,
2020-03-20 15:17:58 +00:00
} ,
2023-03-03 16:36:39 +00:00
} ,
"boolProp" : {
Description : "A bool prop." ,
TypeSpec : schema . TypeSpec {
Type : "boolean" ,
2020-03-20 15:17:58 +00:00
} ,
2023-03-03 16:36:39 +00:00
} ,
}
2020-03-20 15:17:58 +00:00
2023-01-07 00:12:24 +00:00
// newTestPackageSpec returns a new fake package spec for a Provider used for testing.
func newTestPackageSpec ( ) schema . PackageSpec {
2021-07-13 23:41:40 +00:00
pythonMapCase := map [ string ] schema . RawMessage {
"python" : schema . RawMessage ( ` { "mapCase":false} ` ) ,
2020-03-20 15:17:58 +00:00
}
2023-01-07 00:12:24 +00:00
return schema . PackageSpec {
2020-03-20 15:17:58 +00:00
Name : providerPackage ,
2021-09-17 19:12:22 +00:00
Version : "0.0.1" ,
2020-03-20 15:17:58 +00:00
Description : "A fake provider package used for testing." ,
Meta : & schema . MetadataSpec {
ModuleFormat : "(.*)(?:/[^/]*)" ,
} ,
2020-09-16 20:47:40 +00:00
Types : map [ string ] schema . ComplexTypeSpec {
2020-03-20 15:17:58 +00:00
// Package-level types.
"prov:/getPackageResourceOptions:getPackageResourceOptions" : {
2020-09-16 20:47:40 +00:00
ObjectTypeSpec : schema . ObjectTypeSpec {
Description : "Options object for the package-level function getPackageResource." ,
Type : "object" ,
Properties : simpleProperties ,
} ,
2020-03-20 15:17:58 +00:00
} ,
// Module-level types.
"prov:module/getModuleResourceOptions:getModuleResourceOptions" : {
2020-09-16 20:47:40 +00:00
ObjectTypeSpec : schema . ObjectTypeSpec {
Description : "Options object for the module-level function getModuleResource." ,
Type : "object" ,
Properties : simpleProperties ,
} ,
2020-03-20 15:17:58 +00:00
} ,
"prov:module/ResourceOptions:ResourceOptions" : {
2020-09-16 20:47:40 +00:00
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" ,
} ,
2020-03-20 15:17:58 +00:00
} ,
2020-09-16 20:47:40 +00:00
"boolProp" : {
Description : "A bool prop." ,
Language : pythonMapCase ,
TypeSpec : schema . TypeSpec {
Type : "boolean" ,
} ,
2020-03-20 15:17:58 +00:00
} ,
2020-09-16 20:47:40 +00:00
"recursiveType" : {
Description : "I am a recursive type." ,
Language : pythonMapCase ,
TypeSpec : schema . TypeSpec {
Ref : "#/types/prov:module/ResourceOptions:ResourceOptions" ,
} ,
2020-04-22 23:49:57 +00:00
} ,
} ,
2020-03-20 15:17:58 +00:00
} ,
} ,
"prov:module/ResourceOptions2:ResourceOptions2" : {
2020-09-16 20:47:40 +00:00
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" ,
} ,
2020-03-20 15:17:58 +00:00
} ,
} ,
} ,
} ,
} ,
2021-10-19 22:21:39 +00:00
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" ,
} ,
} ,
} ,
} ,
2020-03-20 15:17:58 +00:00
Resources : map [ string ] schema . ResourceSpec {
2021-10-19 22:21:39 +00:00
"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" ,
} ,
} ,
} ,
} ,
2020-03-20 15:17:58 +00:00
"prov:module/resource:Resource" : {
2020-04-21 11:28:44 +00:00
ObjectTypeSpec : schema . ObjectTypeSpec {
2020-05-08 23:25:28 +00:00
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 % } }
2020-11-09 14:12:58 +00:00
2021-09-22 17:55:20 +00:00
# # Import
2020-11-09 14:12:58 +00:00
The import docs would be here
` + codeFence + ` sh
2021-09-22 17:55:20 +00:00
$ pulumi import prov : module / resource : Resource test test
2020-11-09 14:12:58 +00:00
` + codeFence + `
2020-05-08 23:25:28 +00:00
` ,
2020-04-21 11:28:44 +00:00
} ,
2020-03-20 15:17:58 +00:00
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" ,
} ,
} ,
2020-04-22 23:49:57 +00:00
"recursiveType" : {
Description : "I am a recursive type." ,
TypeSpec : schema . TypeSpec {
Ref : "#/types/prov:module/ResourceOptions:ResourceOptions" ,
} ,
} ,
2020-03-20 15:17:58 +00:00
} ,
} ,
2020-04-21 11:28:44 +00:00
"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" ,
} ,
} ,
} ,
} ,
2020-03-20 15:17:58 +00:00
} ,
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" ,
} ,
} ,
} ,
}
}
2020-04-21 11:28:44 +00:00
func getResourceFromModule ( resource string , mod * modContext ) * schema . Resource {
for _ , r := range mod . resources {
if resourceName ( r ) != resource {
continue
}
return r
}
return nil
}
2021-02-17 04:03:06 +00:00
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 ) {
2022-03-04 08:17:41 +00:00
t . Parallel ( )
2021-10-06 15:03:21 +00:00
dctx := newDocGenContext ( )
2023-01-07 00:12:24 +00:00
testPackageSpec := newTestPackageSpec ( )
2021-02-17 04:03:06 +00:00
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." ,
} ,
}
2021-10-06 15:03:21 +00:00
modules := dctx . generateModulesFromSchemaPackage ( unitTestTool , schemaPkg )
2021-02-17 04:03:06 +00:00
for _ , test := range tests {
2022-03-04 08:17:41 +00:00
test := test
2021-02-17 04:03:06 +00:00
t . Run ( test . FunctionName , func ( t * testing . T ) {
2022-03-04 08:17:41 +00:00
t . Parallel ( )
2021-02-17 04:03:06 +00:00
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 )
} )
}
}
2020-04-21 11:28:44 +00:00
func TestResourceDocHeader ( t * testing . T ) {
2022-03-04 08:17:41 +00:00
t . Parallel ( )
2021-10-06 15:03:21 +00:00
dctx := newDocGenContext ( )
2023-01-07 00:12:24 +00:00
testPackageSpec := newTestPackageSpec ( )
2020-04-21 11:28:44 +00:00
schemaPkg , err := schema . ImportSpec ( testPackageSpec , nil )
assert . NoError ( t , err , "importing spec" )
tests := [ ] struct {
Name string
ExpectedTitleTag string
ResourceName string
ModuleName string
2020-12-17 18:44:57 +00:00
ExpectedMetaDesc string
2020-04-21 11:28:44 +00:00
} {
{
Name : "PackageLevelResourceHeader" ,
ResourceName : "PackageLevelResource" ,
// Empty string indicates the package-level root module.
ModuleName : "" ,
2021-02-17 04:03:06 +00:00
ExpectedTitleTag : "prov.PackageLevelResource" ,
ExpectedMetaDesc : "Documentation for the prov.PackageLevelResource resource with examples, input properties, output properties, lookup functions, and supporting types." ,
2020-04-21 11:28:44 +00:00
} ,
{
Name : "ModuleLevelResourceHeader" ,
ResourceName : "Resource" ,
ModuleName : "module" ,
2020-12-08 23:04:36 +00:00
ExpectedTitleTag : "prov.module.Resource" ,
2020-12-17 18:44:57 +00:00
ExpectedMetaDesc : "Documentation for the prov.module.Resource resource with examples, input properties, output properties, lookup functions, and supporting types." ,
2020-04-21 11:28:44 +00:00
} ,
}
2021-10-06 15:03:21 +00:00
modules := dctx . generateModulesFromSchemaPackage ( unitTestTool , schemaPkg )
2020-04-21 11:28:44 +00:00
for _ , test := range tests {
2022-03-04 08:17:41 +00:00
test := test
2020-04-21 11:28:44 +00:00
t . Run ( test . Name , func ( t * testing . T ) {
2022-03-04 08:17:41 +00:00
t . Parallel ( )
2020-04-21 11:28:44 +00:00
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 )
2020-12-17 18:44:57 +00:00
assert . Equal ( t , test . ExpectedMetaDesc , h . MetaDesc )
2020-04-21 11:28:44 +00:00
} )
}
}
2020-05-08 23:25:28 +00:00
func TestExamplesProcessing ( t * testing . T ) {
2022-03-04 08:17:41 +00:00
t . Parallel ( )
2023-01-07 00:12:24 +00:00
testPackageSpec := newTestPackageSpec ( )
2021-10-06 15:03:21 +00:00
dctx := newDocGenContext ( )
2020-05-08 23:25:28 +00:00
description := testPackageSpec . Resources [ "prov:module/resource:Resource" ] . Description
2021-10-06 15:03:21 +00:00
docInfo := dctx . decomposeDocstring ( description )
2020-06-18 19:32:15 +00:00
examplesSection := docInfo . examples
2020-11-09 14:12:58 +00:00
importSection := docInfo . importDetails
assert . NotEmpty ( t , importSection )
2020-05-08 23:25:28 +00:00
// 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 )
}
}
}
2021-04-19 21:05:23 +00:00
func generatePackage ( tool string , pkg * schema . Package , extraFiles map [ string ] [ ] byte ) ( map [ string ] [ ] byte , error ) {
2021-10-06 15:03:21 +00:00
dctx := newDocGenContext ( )
dctx . initialize ( tool , pkg )
return dctx . generatePackage ( tool , pkg )
2021-04-19 21:05:23 +00:00
}
func TestGeneratePackage ( t * testing . T ) {
2022-03-04 08:17:41 +00:00
t . Parallel ( )
2021-09-22 17:55:20 +00:00
test . TestSDKCodegen ( t , & test . SDKCodegenOptions {
Language : "docs" ,
GenPackage : generatePackage ,
2022-02-07 11:10:04 +00:00
TestCases : test . PulumiPulumiSDKTests ,
2021-09-22 17:55:20 +00:00
} )
2021-04-19 21:05:23 +00:00
}
2022-10-04 01:06:38 +00:00
func TestDecomposeDocstring ( t * testing . T ) {
2022-10-04 01:07:48 +00:00
t . Parallel ( )
2022-10-04 01:06:38 +00:00
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 )
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" ,
2022-10-04 17:24:06 +00:00
"go" : "Coming soon!" ,
2022-10-04 01:06:38 +00:00
"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" ,
} ,
} ,
} ,
2023-03-03 16:36:39 +00:00
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" ,
} ,
2022-10-04 01:06:38 +00:00
info )
}