diff --git a/docs/index.md b/docs/index.md index 3b090a6f5..350765a4e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5081,7 +5081,17 @@ will generate a file named `PACKAGE_suite_test.go` and ginkgo generate ``` -will generate a file named `SUBJECT_test.go` (or `PACKAGE_test.go` if `` 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 `` 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 diff --git a/ginkgo/generators/bootstrap_command.go b/ginkgo/generators/bootstrap_command.go index 0273abe9c..73aff0b7a 100644 --- a/ginkgo/generators/bootstrap_command.go +++ b/ginkgo/generators/bootstrap_command.go @@ -2,6 +2,7 @@ package generators import ( "bytes" + "encoding/json" "fmt" "os" "text/template" @@ -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{}, @@ -57,6 +61,7 @@ type bootstrapData struct { GomegaImport string GinkgoPackage string GomegaPackage string + CustomData map[string]any } func generateBootstrap(conf GeneratorsConfig) { @@ -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) diff --git a/ginkgo/generators/generate_command.go b/ginkgo/generators/generate_command.go index 93b0b4b25..48d23f919 100644 --- a/ginkgo/generators/generate_command.go +++ b/ginkgo/generators/generate_command.go @@ -2,6 +2,7 @@ package generators import ( "bytes" + "encoding/json" "fmt" "os" "path/filepath" @@ -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{}, @@ -64,6 +68,7 @@ type specData struct { GomegaImport string GinkgoPackage string GomegaPackage string + CustomData map[string]any } func generateTestFiles(conf GeneratorsConfig, args []string) { @@ -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) } diff --git a/ginkgo/generators/generators_common.go b/ginkgo/generators/generators_common.go index 3086e6056..3046a4487 100644 --- a/ginkgo/generators/generators_common.go +++ b/ginkgo/generators/generators_common.go @@ -13,6 +13,7 @@ import ( type GeneratorsConfig struct { Agouti, NoDot, Internal bool CustomTemplate string + CustomTemplateData string } func getPackageAndFormattedName() (string, string, string) { diff --git a/integration/subcommand_test.go b/integration/subcommand_test.go index f64a1f622..19ee15d94 100644 --- a/integration/subcommand_test.go +++ b/integration/subcommand_test.go @@ -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() { @@ -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() {