Skip to content

Commit

Permalink
Support for bootstrap commands to use custom data for templates (#1110)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dannyb48 committed Jan 5, 2023
1 parent 62fff95 commit 7a2b242
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 5 deletions.
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

0 comments on commit 7a2b242

Please sign in to comment.