Skip to content

Commit 7a2b242

Browse files
authoredJan 5, 2023
Support for bootstrap commands to use custom data for templates (#1110)
1 parent 62fff95 commit 7a2b242

File tree

5 files changed

+206
-5
lines changed

5 files changed

+206
-5
lines changed
 

‎docs/index.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -5081,7 +5081,17 @@ will generate a file named `PACKAGE_suite_test.go` and
50815081
ginkgo generate <SUBJECT>
50825082
```
50835083
5084-
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.
5084+
will generate a file named `SUBJECT_test.go` (or `PACKAGE_test.go` if `<SUBJECT>` is not provided). Both generators support custom templates using `--template`
5085+
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,
5086+
with a JSON file
5087+
```json
5088+
{ "suitename": "E2E",
5089+
"labels": ["fast", "parallel", "component"]}
5090+
```
5091+
The custom data can be accessed like so:
5092+
`{{ .CustomData.suitename }}` or `{{ range .CustomData.labels }} {{.}} {{ end }}`
5093+
5094+
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.
50855095
50865096
### Creating an Outline of Specs
50875097

‎ginkgo/generators/bootstrap_command.go

+22-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package generators
22

33
import (
44
"bytes"
5+
"encoding/json"
56
"fmt"
67
"os"
78
"text/template"
@@ -25,6 +26,9 @@ func BuildBootstrapCommand() command.Command {
2526
{Name: "template", KeyPath: "CustomTemplate",
2627
UsageArgument: "template-file",
2728
Usage: "If specified, generate will use the contents of the file passed as the bootstrap template"},
29+
{Name: "template-data", KeyPath: "CustomTemplateData",
30+
UsageArgument: "template-data-file",
31+
Usage: "If specified, generate will use the contents of the file passed as data to be rendered in the bootstrap template"},
2832
},
2933
&conf,
3034
types.GinkgoFlagSections{},
@@ -57,6 +61,7 @@ type bootstrapData struct {
5761
GomegaImport string
5862
GinkgoPackage string
5963
GomegaPackage string
64+
CustomData map[string]any
6065
}
6166

6267
func generateBootstrap(conf GeneratorsConfig) {
@@ -95,17 +100,32 @@ func generateBootstrap(conf GeneratorsConfig) {
95100
tpl, err := os.ReadFile(conf.CustomTemplate)
96101
command.AbortIfError("Failed to read custom bootstrap file:", err)
97102
templateText = string(tpl)
103+
if conf.CustomTemplateData != "" {
104+
var tplCustomDataMap map[string]any
105+
tplCustomData, err := os.ReadFile(conf.CustomTemplateData)
106+
command.AbortIfError("Failed to read custom boostrap data file:", err)
107+
if !json.Valid([]byte(tplCustomData)) {
108+
command.AbortWith("Invalid JSON object in custom data file.")
109+
}
110+
//create map from the custom template data
111+
json.Unmarshal(tplCustomData, &tplCustomDataMap)
112+
data.CustomData = tplCustomDataMap
113+
}
98114
} else if conf.Agouti {
99115
templateText = agoutiBootstrapText
100116
} else {
101117
templateText = bootstrapText
102118
}
103119

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

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

110130
buf.WriteTo(f)
111131

‎ginkgo/generators/generate_command.go

+22-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package generators
22

33
import (
44
"bytes"
5+
"encoding/json"
56
"fmt"
67
"os"
78
"path/filepath"
@@ -28,6 +29,9 @@ func BuildGenerateCommand() command.Command {
2829
{Name: "template", KeyPath: "CustomTemplate",
2930
UsageArgument: "template-file",
3031
Usage: "If specified, generate will use the contents of the file passed as the test file template"},
32+
{Name: "template-data", KeyPath: "CustomTemplateData",
33+
UsageArgument: "template-data-file",
34+
Usage: "If specified, generate will use the contents of the file passed as data to be rendered in the test file template"},
3135
},
3236
&conf,
3337
types.GinkgoFlagSections{},
@@ -64,6 +68,7 @@ type specData struct {
6468
GomegaImport string
6569
GinkgoPackage string
6670
GomegaPackage string
71+
CustomData map[string]any
6772
}
6873

6974
func generateTestFiles(conf GeneratorsConfig, args []string) {
@@ -122,16 +127,31 @@ func generateTestFileForSubject(subject string, conf GeneratorsConfig) {
122127
tpl, err := os.ReadFile(conf.CustomTemplate)
123128
command.AbortIfError("Failed to read custom template file:", err)
124129
templateText = string(tpl)
130+
if conf.CustomTemplateData != "" {
131+
var tplCustomDataMap map[string]any
132+
tplCustomData, err := os.ReadFile(conf.CustomTemplateData)
133+
command.AbortIfError("Failed to read custom template data file:", err)
134+
if !json.Valid([]byte(tplCustomData)) {
135+
command.AbortWith("Invalid JSON object in custom data file.")
136+
}
137+
//create map from the custom template data
138+
json.Unmarshal(tplCustomData, &tplCustomDataMap)
139+
data.CustomData = tplCustomDataMap
140+
}
125141
} else if conf.Agouti {
126142
templateText = agoutiSpecText
127143
} else {
128144
templateText = specText
129145
}
130146

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

134-
specTemplate.Execute(f, data)
151+
//Being explicit about failing sooner during template rendering
152+
//when accessing custom data rather than during the go fmt command
153+
err = specTemplate.Execute(f, data)
154+
command.AbortIfError("Failed to render bootstrap template:", err)
135155
internal.GoFmt(targetFile)
136156
}
137157

‎ginkgo/generators/generators_common.go

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
type GeneratorsConfig struct {
1414
Agouti, NoDot, Internal bool
1515
CustomTemplate string
16+
CustomTemplateData string
1617
}
1718

1819
func getPackageAndFormattedName() (string, string, string) {

‎integration/subcommand_test.go

+150
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,85 @@ var _ = Describe("Subcommand", func() {
133133
Ω(content).Should(ContainSubstring(`"binary"`))
134134
Ω(content).Should(ContainSubstring("// This is a foo_testfoo_testfoo_test test"))
135135
})
136+
137+
It("should generate a bootstrap file using a template and custom template data when told to", func() {
138+
fm.WriteFile(pkg, ".bootstrap", `package {{.Package}}
139+
140+
import (
141+
{{.GinkgoImport}}
142+
{{.GomegaImport}}
143+
144+
"testing"
145+
"binary"
146+
)
147+
148+
func Test{{.FormattedName}}(t *testing.T) {
149+
// This is a {{.Package | repeat 3}} test
150+
// This is a custom data {{.CustomData.suitename}} test
151+
}`)
152+
fm.WriteFile(pkg, "custom.json", `{"suitename": "integration"}`)
153+
session := startGinkgo(fm.PathTo(pkg), "bootstrap", "--template", ".bootstrap", "--template-data", "custom.json")
154+
Eventually(session).Should(gexec.Exit(0))
155+
output := session.Out.Contents()
156+
157+
Ω(output).Should(ContainSubstring("foo_suite_test.go"))
158+
159+
content := fm.ContentOf(pkg, "foo_suite_test.go")
160+
Ω(content).Should(ContainSubstring("package foo_test"))
161+
Ω(content).Should(ContainSubstring(`. "github.com/onsi/ginkgo/v2"`))
162+
Ω(content).Should(ContainSubstring(`. "github.com/onsi/gomega"`))
163+
Ω(content).Should(ContainSubstring(`"binary"`))
164+
Ω(content).Should(ContainSubstring("// This is a foo_testfoo_testfoo_test test"))
165+
Ω(content).Should(ContainSubstring("// This is a custom data integration test"))
166+
})
167+
168+
It("should fail to render a bootstrap file using a template and custom template data when accessing a missing key", func() {
169+
fm.WriteFile(pkg, ".bootstrap", `package {{.Package}}
170+
171+
import (
172+
{{.GinkgoImport}}
173+
{{.GomegaImport}}
174+
175+
"testing"
176+
"binary"
177+
)
178+
179+
func Test{{.FormattedName}}(t *testing.T) {
180+
// This is a {{.Package | repeat 3}} test
181+
// This is a custom data {{.CustomData.component}} test
182+
}`)
183+
fm.WriteFile(pkg, "custom.json", `{"suitename": "integration"}`)
184+
session := startGinkgo(fm.PathTo(pkg), "bootstrap", "--template", ".bootstrap", "--template-data", "custom.json")
185+
Eventually(session).Should(gexec.Exit(1))
186+
output := string(session.Err.Contents())
187+
188+
Ω(output).Should(ContainSubstring(`executing "bootstrap" at <.CustomData.component>: map has no entry for key "component"`))
189+
190+
})
191+
192+
It("should fail to render a bootstrap file using a template and custom template data when data is invalid JSON", func() {
193+
fm.WriteFile(pkg, ".bootstrap", `package {{.Package}}
194+
195+
import (
196+
{{.GinkgoImport}}
197+
{{.GomegaImport}}
198+
199+
"testing"
200+
"binary"
201+
)
202+
203+
func Test{{.FormattedName}}(t *testing.T) {
204+
// This is a {{.Package | repeat 3}} test
205+
// This is a custom data {{.CustomData.component}} test
206+
}`)
207+
fm.WriteFile(pkg, "custom.json", `{'suitename': 'integration']`)
208+
session := startGinkgo(fm.PathTo(pkg), "bootstrap", "--template", ".bootstrap", "--template-data", "custom.json")
209+
Eventually(session).Should(gexec.Exit(1))
210+
output := string(session.Err.Contents())
211+
212+
Ω(output).Should(ContainSubstring(`Invalid JSON object in custom data file.`))
213+
214+
})
136215
})
137216

138217
Describe("ginkgo generate", func() {
@@ -230,6 +309,77 @@ var _ = Describe("Subcommand", func() {
230309
Ω(content).Should(ContainSubstring(`/foo_bar"`))
231310
Ω(content).Should(ContainSubstring("// This is a foo_bar_testfoo_bar_testfoo_bar_test test"))
232311
})
312+
313+
It("should generate a test file using a template and custom data when told to", func() {
314+
fm.WriteFile(pkg, ".generate", `package {{.Package}}
315+
import (
316+
{{.GinkgoImport}}
317+
{{.GomegaImport}}
318+
319+
{{if .ImportPackage}}"{{.PackageImportPath}}"{{end}}
320+
)
321+
322+
var _ = Describe("{{.Subject}}", Label("{{.CustomData.label}}"), func() {
323+
// This is a {{.Package | repeat 3 }} test
324+
})`)
325+
fm.WriteFile(pkg, "custom_spec.json", `{"label": "integration"}`)
326+
session := startGinkgo(fm.PathTo(pkg), "generate", "--template", ".generate", "--template-data", "custom_spec.json")
327+
Eventually(session).Should(gexec.Exit(0))
328+
output := session.Out.Contents()
329+
330+
Ω(output).Should(ContainSubstring("foo_bar_test.go"))
331+
332+
content := fm.ContentOf(pkg, "foo_bar_test.go")
333+
Ω(content).Should(ContainSubstring("package foo_bar_test"))
334+
Ω(content).Should(ContainSubstring(`. "github.com/onsi/ginkgo/v2"`))
335+
Ω(content).Should(ContainSubstring(`. "github.com/onsi/gomega"`))
336+
Ω(content).Should(ContainSubstring(`/foo_bar"`))
337+
Ω(content).Should(ContainSubstring("// This is a foo_bar_testfoo_bar_testfoo_bar_test test"))
338+
Ω(content).Should(ContainSubstring(`Label("integration")`))
339+
})
340+
341+
It("should fail to render a test file using a template and custom template data when accessing a missing key", func() {
342+
fm.WriteFile(pkg, ".generate", `package {{.Package}}
343+
import (
344+
{{.GinkgoImport}}
345+
{{.GomegaImport}}
346+
347+
{{if .ImportPackage}}"{{.PackageImportPath}}"{{end}}
348+
)
349+
350+
var _ = Describe("{{.Subject}}", Label("{{.CustomData.component}}"), func() {
351+
// This is a {{.Package | repeat 3 }} test
352+
})`)
353+
fm.WriteFile(pkg, "custom.json", `{"label": "integration"}`)
354+
session := startGinkgo(fm.PathTo(pkg), "generate", "--template", ".generate", "--template-data", "custom.json")
355+
Eventually(session).Should(gexec.Exit(1))
356+
output := string(session.Err.Contents())
357+
358+
Ω(output).Should(ContainSubstring(`executing "spec" at <.CustomData.component>: map has no entry for key "component"`))
359+
360+
})
361+
362+
It("should fail to render a test file using a template and custom template data when data is invalid JSON", func() {
363+
fm.WriteFile(pkg, ".generate", `package {{.Package}}
364+
import (
365+
{{.GinkgoImport}}
366+
{{.GomegaImport}}
367+
368+
{{if .ImportPackage}}"{{.PackageImportPath}}"{{end}}
369+
)
370+
371+
var _ = Describe("{{.Subject}}", Label("{{.CustomData.label}}"), func() {
372+
// This is a {{.Package | repeat 3 }} test
373+
})`)
374+
fm.WriteFile(pkg, "custom.json", `{'label': 'integration']`)
375+
session := startGinkgo(fm.PathTo(pkg), "generate", "--template", ".generate", "--template-data", "custom.json")
376+
Eventually(session).Should(gexec.Exit(1))
377+
output := string(session.Err.Contents())
378+
379+
Ω(output).Should(ContainSubstring(`Invalid JSON object in custom data file.`))
380+
381+
})
382+
233383
})
234384

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

0 commit comments

Comments
 (0)
Please sign in to comment.