Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for bootstrap commands to use custom data for templates #1110

Merged
merged 1 commit into from
Jan 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5081,7 +5081,17 @@ will generate a file named `PACKAGE_suite_test.go` and
ginkgo generate <SUBJECT>
```

will generate a file named `SUBJECT_test.go` (or `PACKAGE_test.go` if `<SUBJECT>` is not provided). Both generators support custom templates using `--template`. Take a look at the [Ginkgo's CLI code](https://github.com/onsi/ginkgo/tree/master/ginkgo/ginkgo/generators) to see what's available in the template.
will generate a file named `SUBJECT_test.go` (or `PACKAGE_test.go` if `<SUBJECT>` is not provided). Both generators support custom templates using `--template`
and the option to provide extra custom data to be rendered into the template, besides the default values, using `--template-data`. The custom data should be a well structured JSON file. When loaded into the template the custom data will be available to access from the global key `.CustomData`. For example,
with a JSON file
```json
{ "suitename": "E2E",
"labels": ["fast", "parallel", "component"]}
```
The custom data can be accessed like so:
`{{ .CustomData.suitename }}` or `{{ range .CustomData.labels }} {{.}} {{ end }}`

Take a look at the [Ginkgo's CLI code](https://github.com/onsi/ginkgo/tree/master/ginkgo/ginkgo/generators) to see what's available in the template.

### Creating an Outline of Specs

Expand Down
24 changes: 22 additions & 2 deletions ginkgo/generators/bootstrap_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package generators

import (
"bytes"
"encoding/json"
"fmt"
"os"
"text/template"
Expand All @@ -25,6 +26,9 @@ func BuildBootstrapCommand() command.Command {
{Name: "template", KeyPath: "CustomTemplate",
UsageArgument: "template-file",
Usage: "If specified, generate will use the contents of the file passed as the bootstrap template"},
{Name: "template-data", KeyPath: "CustomTemplateData",
UsageArgument: "template-data-file",
Usage: "If specified, generate will use the contents of the file passed as data to be rendered in the bootstrap template"},
},
&conf,
types.GinkgoFlagSections{},
Expand Down Expand Up @@ -57,6 +61,7 @@ type bootstrapData struct {
GomegaImport string
GinkgoPackage string
GomegaPackage string
CustomData map[string]any
}

func generateBootstrap(conf GeneratorsConfig) {
Expand Down Expand Up @@ -95,17 +100,32 @@ func generateBootstrap(conf GeneratorsConfig) {
tpl, err := os.ReadFile(conf.CustomTemplate)
command.AbortIfError("Failed to read custom bootstrap file:", err)
templateText = string(tpl)
if conf.CustomTemplateData != "" {
var tplCustomDataMap map[string]any
tplCustomData, err := os.ReadFile(conf.CustomTemplateData)
command.AbortIfError("Failed to read custom boostrap data file:", err)
if !json.Valid([]byte(tplCustomData)) {
command.AbortWith("Invalid JSON object in custom data file.")
}
//create map from the custom template data
json.Unmarshal(tplCustomData, &tplCustomDataMap)
data.CustomData = tplCustomDataMap
}
} else if conf.Agouti {
templateText = agoutiBootstrapText
} else {
templateText = bootstrapText
}

bootstrapTemplate, err := template.New("bootstrap").Funcs(sprig.TxtFuncMap()).Parse(templateText)
//Setting the option to explicitly fail if template is rendered trying to access missing key
bootstrapTemplate, err := template.New("bootstrap").Funcs(sprig.TxtFuncMap()).Option("missingkey=error").Parse(templateText)
command.AbortIfError("Failed to parse bootstrap template:", err)

buf := &bytes.Buffer{}
bootstrapTemplate.Execute(buf, data)
//Being explicit about failing sooner during template rendering
//when accessing custom data rather than during the go fmt command
err = bootstrapTemplate.Execute(buf, data)
command.AbortIfError("Failed to render bootstrap template:", err)

buf.WriteTo(f)

Expand Down
24 changes: 22 additions & 2 deletions ginkgo/generators/generate_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package generators

import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
Expand All @@ -28,6 +29,9 @@ func BuildGenerateCommand() command.Command {
{Name: "template", KeyPath: "CustomTemplate",
UsageArgument: "template-file",
Usage: "If specified, generate will use the contents of the file passed as the test file template"},
{Name: "template-data", KeyPath: "CustomTemplateData",
UsageArgument: "template-data-file",
Usage: "If specified, generate will use the contents of the file passed as data to be rendered in the test file template"},
},
&conf,
types.GinkgoFlagSections{},
Expand Down Expand Up @@ -64,6 +68,7 @@ type specData struct {
GomegaImport string
GinkgoPackage string
GomegaPackage string
CustomData map[string]any
}

func generateTestFiles(conf GeneratorsConfig, args []string) {
Expand Down Expand Up @@ -122,16 +127,31 @@ func generateTestFileForSubject(subject string, conf GeneratorsConfig) {
tpl, err := os.ReadFile(conf.CustomTemplate)
command.AbortIfError("Failed to read custom template file:", err)
templateText = string(tpl)
if conf.CustomTemplateData != "" {
var tplCustomDataMap map[string]any
tplCustomData, err := os.ReadFile(conf.CustomTemplateData)
command.AbortIfError("Failed to read custom template data file:", err)
if !json.Valid([]byte(tplCustomData)) {
command.AbortWith("Invalid JSON object in custom data file.")
}
//create map from the custom template data
json.Unmarshal(tplCustomData, &tplCustomDataMap)
data.CustomData = tplCustomDataMap
}
} else if conf.Agouti {
templateText = agoutiSpecText
} else {
templateText = specText
}

specTemplate, err := template.New("spec").Funcs(sprig.TxtFuncMap()).Parse(templateText)
//Setting the option to explicitly fail if template is rendered trying to access missing key
specTemplate, err := template.New("spec").Funcs(sprig.TxtFuncMap()).Option("missingkey=error").Parse(templateText)
command.AbortIfError("Failed to read parse test template:", err)

specTemplate.Execute(f, data)
//Being explicit about failing sooner during template rendering
//when accessing custom data rather than during the go fmt command
err = specTemplate.Execute(f, data)
command.AbortIfError("Failed to render bootstrap template:", err)
internal.GoFmt(targetFile)
}

Expand Down
1 change: 1 addition & 0 deletions ginkgo/generators/generators_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
type GeneratorsConfig struct {
Agouti, NoDot, Internal bool
CustomTemplate string
CustomTemplateData string
}

func getPackageAndFormattedName() (string, string, string) {
Expand Down
150 changes: 150 additions & 0 deletions integration/subcommand_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,85 @@ var _ = Describe("Subcommand", func() {
Ω(content).Should(ContainSubstring(`"binary"`))
Ω(content).Should(ContainSubstring("// This is a foo_testfoo_testfoo_test test"))
})

It("should generate a bootstrap file using a template and custom template data when told to", func() {
fm.WriteFile(pkg, ".bootstrap", `package {{.Package}}

import (
{{.GinkgoImport}}
{{.GomegaImport}}

"testing"
"binary"
)

func Test{{.FormattedName}}(t *testing.T) {
// This is a {{.Package | repeat 3}} test
// This is a custom data {{.CustomData.suitename}} test
}`)
fm.WriteFile(pkg, "custom.json", `{"suitename": "integration"}`)
session := startGinkgo(fm.PathTo(pkg), "bootstrap", "--template", ".bootstrap", "--template-data", "custom.json")
Eventually(session).Should(gexec.Exit(0))
output := session.Out.Contents()

Ω(output).Should(ContainSubstring("foo_suite_test.go"))

content := fm.ContentOf(pkg, "foo_suite_test.go")
Ω(content).Should(ContainSubstring("package foo_test"))
Ω(content).Should(ContainSubstring(`. "github.com/onsi/ginkgo/v2"`))
Ω(content).Should(ContainSubstring(`. "github.com/onsi/gomega"`))
Ω(content).Should(ContainSubstring(`"binary"`))
Ω(content).Should(ContainSubstring("// This is a foo_testfoo_testfoo_test test"))
Ω(content).Should(ContainSubstring("// This is a custom data integration test"))
})

It("should fail to render a bootstrap file using a template and custom template data when accessing a missing key", func() {
fm.WriteFile(pkg, ".bootstrap", `package {{.Package}}

import (
{{.GinkgoImport}}
{{.GomegaImport}}

"testing"
"binary"
)

func Test{{.FormattedName}}(t *testing.T) {
// This is a {{.Package | repeat 3}} test
// This is a custom data {{.CustomData.component}} test
}`)
fm.WriteFile(pkg, "custom.json", `{"suitename": "integration"}`)
session := startGinkgo(fm.PathTo(pkg), "bootstrap", "--template", ".bootstrap", "--template-data", "custom.json")
Eventually(session).Should(gexec.Exit(1))
output := string(session.Err.Contents())

Ω(output).Should(ContainSubstring(`executing "bootstrap" at <.CustomData.component>: map has no entry for key "component"`))

})

It("should fail to render a bootstrap file using a template and custom template data when data is invalid JSON", func() {
fm.WriteFile(pkg, ".bootstrap", `package {{.Package}}

import (
{{.GinkgoImport}}
{{.GomegaImport}}

"testing"
"binary"
)

func Test{{.FormattedName}}(t *testing.T) {
// This is a {{.Package | repeat 3}} test
// This is a custom data {{.CustomData.component}} test
}`)
fm.WriteFile(pkg, "custom.json", `{'suitename': 'integration']`)
session := startGinkgo(fm.PathTo(pkg), "bootstrap", "--template", ".bootstrap", "--template-data", "custom.json")
Eventually(session).Should(gexec.Exit(1))
output := string(session.Err.Contents())

Ω(output).Should(ContainSubstring(`Invalid JSON object in custom data file.`))

})
})

Describe("ginkgo generate", func() {
Expand Down Expand Up @@ -230,6 +309,77 @@ var _ = Describe("Subcommand", func() {
Ω(content).Should(ContainSubstring(`/foo_bar"`))
Ω(content).Should(ContainSubstring("// This is a foo_bar_testfoo_bar_testfoo_bar_test test"))
})

It("should generate a test file using a template and custom data when told to", func() {
fm.WriteFile(pkg, ".generate", `package {{.Package}}
import (
{{.GinkgoImport}}
{{.GomegaImport}}

{{if .ImportPackage}}"{{.PackageImportPath}}"{{end}}
)

var _ = Describe("{{.Subject}}", Label("{{.CustomData.label}}"), func() {
// This is a {{.Package | repeat 3 }} test
})`)
fm.WriteFile(pkg, "custom_spec.json", `{"label": "integration"}`)
session := startGinkgo(fm.PathTo(pkg), "generate", "--template", ".generate", "--template-data", "custom_spec.json")
Eventually(session).Should(gexec.Exit(0))
output := session.Out.Contents()

Ω(output).Should(ContainSubstring("foo_bar_test.go"))

content := fm.ContentOf(pkg, "foo_bar_test.go")
Ω(content).Should(ContainSubstring("package foo_bar_test"))
Ω(content).Should(ContainSubstring(`. "github.com/onsi/ginkgo/v2"`))
Ω(content).Should(ContainSubstring(`. "github.com/onsi/gomega"`))
Ω(content).Should(ContainSubstring(`/foo_bar"`))
Ω(content).Should(ContainSubstring("// This is a foo_bar_testfoo_bar_testfoo_bar_test test"))
Ω(content).Should(ContainSubstring(`Label("integration")`))
})

It("should fail to render a test file using a template and custom template data when accessing a missing key", func() {
fm.WriteFile(pkg, ".generate", `package {{.Package}}
import (
{{.GinkgoImport}}
{{.GomegaImport}}

{{if .ImportPackage}}"{{.PackageImportPath}}"{{end}}
)

var _ = Describe("{{.Subject}}", Label("{{.CustomData.component}}"), func() {
// This is a {{.Package | repeat 3 }} test
})`)
fm.WriteFile(pkg, "custom.json", `{"label": "integration"}`)
session := startGinkgo(fm.PathTo(pkg), "generate", "--template", ".generate", "--template-data", "custom.json")
Eventually(session).Should(gexec.Exit(1))
output := string(session.Err.Contents())

Ω(output).Should(ContainSubstring(`executing "spec" at <.CustomData.component>: map has no entry for key "component"`))

})

It("should fail to render a test file using a template and custom template data when data is invalid JSON", func() {
fm.WriteFile(pkg, ".generate", `package {{.Package}}
import (
{{.GinkgoImport}}
{{.GomegaImport}}

{{if .ImportPackage}}"{{.PackageImportPath}}"{{end}}
)

var _ = Describe("{{.Subject}}", Label("{{.CustomData.label}}"), func() {
// This is a {{.Package | repeat 3 }} test
})`)
fm.WriteFile(pkg, "custom.json", `{'label': 'integration']`)
session := startGinkgo(fm.PathTo(pkg), "generate", "--template", ".generate", "--template-data", "custom.json")
Eventually(session).Should(gexec.Exit(1))
output := string(session.Err.Contents())

Ω(output).Should(ContainSubstring(`Invalid JSON object in custom data file.`))

})

})

Context("with an argument of the form: foo", func() {
Expand Down