diff --git a/docs/DeveloperGenerateTest.md b/docs/DeveloperGenerateTest.md index ff80cee30..bda3641c1 100644 --- a/docs/DeveloperGenerateTest.md +++ b/docs/DeveloperGenerateTest.md @@ -174,6 +174,16 @@ Before generating anything make sure you have Ginkgo in place. To install Ginkgo `go install -mod=mod github.com/onsi/ginkgo/v2/ginkgo` +## Team specific template +Teams can create their own templates according to their needs. +These should utilize the provided `specs.tmpl` by including the following line wherever they want their specs to be. (do not forget the dot!) + +`{{ template "specs" . }}` + +Everything needed to get started is in the [templates/default](../templates/default) directory. + +Please see the provided [recommended](../templates/default/recommended.tmpl) and [barebones](../templates/default/barebones.tmpl) templates. +Copy them and make your own. ## Usage @@ -218,21 +228,33 @@ I0622 22:19:41.865837 20923 textspec.go:67] successfully written to /tmp/outli ``` - ### Generating the ginkgo spec file from an existing text outline file - This will generate the Ginkgo spec in a subdirectory within our tests directory + ### Generating a generic Ginkgo spec file from an existing text outline file + This will generate a generic Ginkgo spec in a subdirectory within our tests directory - `./mage GenerateGinkoSpecFromTextOutline // /.go` + `./mage GenerateGinkgoSpecFromTextOutline // /.go` ```bash $ ./mage GenerateGinkoSpecFromTextOutline dummy_test.outline books/books.go I0622 22:14:22.140583 20356 testcasemapper.go:58] Mapping outline from a text file, dummy_test.outline I0622 22:14:22.140673 20356 testcasemapper.go:47] Mapping outline to a Ginkgo test file, books/books.go I0622 22:14:22.140841 20356 ginkgosspec.go:242] Creating new test package directory and spec file tests/books/books.go. - - ``` As noted above, this command will create a new package under the `tests/` directory and a test spec file `.go` for you. It will contain some basic imports but more importantly it will generate a basic structured Ginkgo spec skeleton that you can code against. +### Generating a team specific Ginkgo spec file from an existing text outline file +This will generate the Ginkgo spec in a subdirectory within our tests directory using a team specific template provided by user. Please see the [Team specific template](#team-specific-template) section. + +Feel free to use the provided [testOutline](../templates/default/testOutline) file for testing. + +`./mage GenerateTeamSpecificGinkgoSpecFromTextOutline // // /.go` + +```bash +➜ ./mage GenerateTeamSpecificGinkgoSpecFromTextOutline templates/default/testOutline templates/default/recommended.tmpl tests/template_poc/template_poc.go +I0219 15:42:17.808595 351210 magefile.go:755] Mapping outline from a text file, templates/default/testOutline +I0219 15:42:17.808685 351210 magefile.go:762] Mapping outline to a Ginkgo spec file, tests/template_poc/template_poc.go +I0219 15:42:17.809210 351210 ginkgosspec.go:144] Creating new test package directory and spec file /home/tnevrlka/Work/e2e-tests/tests/template_poc/template_poc.go. +``` + ### Printing a text outline in JSON format of an existing ginkgo spec file This will generate the outline and output to your terminal in JSON format. This is the format we use when rendering the template. You can pipe this output to tools like `jq` for formatting and filtering. This would only be useful for troubleshooting purposes @@ -298,4 +320,4 @@ $ ./mage GenerateTestSuiteFile chaos I0623 12:48:13.761038 31196 magefile.go:467] Creating new test suite file cmd/chaos_test.go. cmd/chaos_test.go -``` \ No newline at end of file +``` diff --git a/magefiles/magefile.go b/magefiles/magefile.go index 481c36c0f..974fb92fb 100644 --- a/magefiles/magefile.go +++ b/magefiles/magefile.go @@ -24,12 +24,12 @@ import ( gh "github.com/google/go-github/v44/github" "github.com/magefile/mage/sh" "github.com/redhat-appstudio/e2e-tests/magefiles/installation" - "github.com/redhat-appstudio/e2e-tests/magefiles/testspecs" "github.com/redhat-appstudio/e2e-tests/magefiles/upgrade" "github.com/redhat-appstudio/e2e-tests/pkg/clients/github" "github.com/redhat-appstudio/e2e-tests/pkg/clients/slack" "github.com/redhat-appstudio/e2e-tests/pkg/clients/sprayproxy" "github.com/redhat-appstudio/e2e-tests/pkg/constants" + "github.com/redhat-appstudio/e2e-tests/pkg/testspecs" "github.com/redhat-appstudio/e2e-tests/pkg/utils" "github.com/redhat-appstudio/e2e-tests/pkg/utils/tekton" "github.com/redhat-appstudio/image-controller/pkg/quay" @@ -758,26 +758,31 @@ func GenerateTextOutlineFromGinkgoSpec(source string, destination string) error } // Generate a Ginkgo Spec file from a Text Outline file -func GenerateGinkoSpecFromTextOutline(source string, destination string) error { +func GenerateGinkgoSpecFromTextOutline(source string, destination string) error { + return GenerateTeamSpecificGinkgoSpecFromTextOutline(source, testspecs.TestFilePath, destination) +} +// Generate a team specific file using specs in templates/specs.tmpl file and a provided team specific template +func GenerateTeamSpecificGinkgoSpecFromTextOutline(outlinePath, teamTmplPath, destinationPath string) error { gs := testspecs.NewGinkgoSpecTranslator() ts := testspecs.NewTextSpecTranslator() - klog.Infof("Mapping outline from a text file, %s", source) - outline, err := ts.FromFile(source) + klog.Infof("Mapping outline from a text file, %s", outlinePath) + outline, err := ts.FromFile(outlinePath) if err != nil { klog.Error("Failed to map text outline file") return err } - klog.Infof("Mapping outline to a Ginkgo spec file, %s", destination) - err = gs.ToFile(destination, outline) + klog.Infof("Mapping outline to a Ginkgo spec file, %s", destinationPath) + err = gs.ToFile(destinationPath, teamTmplPath, outline) if err != nil { klog.Error("Failed to map Ginkgo spec file") return err } return err + } // Print the outline of the Ginkgo spec diff --git a/magefiles/testspecs/astginkgos.go b/pkg/testspecs/astginkgos.go similarity index 100% rename from magefiles/testspecs/astginkgos.go rename to pkg/testspecs/astginkgos.go diff --git a/pkg/testspecs/const.go b/pkg/testspecs/const.go new file mode 100644 index 000000000..d453e1470 --- /dev/null +++ b/pkg/testspecs/const.go @@ -0,0 +1,8 @@ +package testspecs + +const ( + TestFilePath = "templates/test_output_spec.tmpl" + FrameworkDescribePath = "templates/framework_describe_func.tmpl" + + SpecsPath = "templates/specs.tmpl" +) diff --git a/magefiles/testspecs/ginkgosspec.go b/pkg/testspecs/ginkgosspec.go similarity index 79% rename from magefiles/testspecs/ginkgosspec.go rename to pkg/testspecs/ginkgosspec.go index db94efebc..f050d0b69 100644 --- a/magefiles/testspecs/ginkgosspec.go +++ b/pkg/testspecs/ginkgosspec.go @@ -3,8 +3,9 @@ package testspecs import ( "encoding/json" "fmt" - + "io" "os" + "path" "path/filepath" "reflect" "strings" @@ -54,7 +55,7 @@ func (gst *GinkgosSpecTranslator) FromFile(file string) (TestOutline, error) { } // ToFile generates a Ginkgo test file from a TestOutline -func (gst *GinkgosSpecTranslator) ToFile(destination string, outline TestOutline) error { +func (gst *GinkgosSpecTranslator) ToFile(destination, teamTmplPath string, outline TestOutline) error { e2ePath, err := os.Getwd() if err != nil { @@ -71,7 +72,7 @@ func (gst *GinkgosSpecTranslator) ToFile(destination string, outline TestOutline return err } - return generateGinkgoSpec(e2ePath, testFilePath, dataFile) + return generateGinkgoSpec(e2ePath, teamTmplPath, testFilePath, dataFile) } @@ -112,10 +113,19 @@ func excludeSetupTeardownNodes(nodes TestOutline) TestOutline { // generateGinkgoSpec will call the ginkgo generate command // to generate the ginkgo data json file we created and // the template located in out templates directory -func generateGinkgoSpec(cwd string, destination string, dataFile string) error { +func generateGinkgoSpec(cwd, teamTmplPath, destination string, dataFile string) error { var err error + if teamTmplPath != TestFilePath { + tmplFile, err := mergeTemplates(teamTmplPath, SpecsPath) + if err != nil { + return err + } + defer os.Remove(tmplFile.Name()) + teamTmplPath = tmplFile.Name() + } + // Note I change into the directory and rename things because ginkgo // by default generates the test file name as _test.go. // Since that is not a semantic we follow I perform this action. @@ -134,15 +144,9 @@ func generateGinkgoSpec(cwd string, destination string, dataFile string) error { // Doing this to avoid errcheck flagging this in a defer. // Refer to https://github.com/kisielk/errcheck // issues 101, 77, 55 - tmpl, err := GetTemplate("test-file") - if err != nil { - return err - } - - fullTemplatePath := fmt.Sprintf("%s/%s", cwd, tmpl) klog.Infof("Creating new test package directory and spec file %s.\n", destination) - _, err = ginkgoGenerateSpecCmd("--template", fullTemplatePath, "--template-data", dataFile) + _, err = ginkgoGenerateSpecCmd("--template", teamTmplPath, "--template-data", dataFile) if err != nil { err = os.Remove(ginkgoFileName) if err != nil { @@ -170,6 +174,47 @@ func generateGinkgoSpec(cwd string, destination string, dataFile string) error { return err } +// mergeTemplates creates a new template file from files provided in the argument +func mergeTemplates(paths ...string) (*os.File, error) { + cwd, err := os.Getwd() + if err != nil { + return nil, err + } + + tempFile, err := os.CreateTemp(cwd, "merged-tmpl") + if err != nil { + return nil, err + } + defer tempFile.Close() + + for _, templatePath := range paths { + // Avoid possible memory leak caused by defer by wrapping in a function + appendToTempPath := func() error { + tmplFile, err := os.Open(path.Clean(templatePath)) + if err != nil { + return err + } + defer tmplFile.Close() + + _, err = io.Copy(tempFile, tmplFile) + if err != nil { + return err + } + + _, err = tempFile.Write([]byte{'\n', '\n'}) + if err != nil { + return err + } + return nil + } + err = appendToTempPath() + if err != nil { + return nil, fmt.Errorf("error during appending to temp templatePath: %+v", err) + } + } + return tempFile, nil +} + // createTestPath will create the full test path in the tests // directory if it doesn't exit func createTestPath(cwd string, destination string) (string, error) { diff --git a/pkg/testspecs/ginkgosspec_test.go b/pkg/testspecs/ginkgosspec_test.go new file mode 100644 index 000000000..6addd79ca --- /dev/null +++ b/pkg/testspecs/ginkgosspec_test.go @@ -0,0 +1,69 @@ +package testspecs + +import ( + "fmt" + "os" + "testing" +) + +func TestMergeTemplates(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + return + } + + tempDir, err := os.MkdirTemp(cwd, "test-merge-templates") + if err != nil { + t.Fatal(err) + return + } + defer os.RemoveAll(tempDir) + + err = os.Chdir(tempDir) + if err != nil { + t.Fatal(err) + return + } + + var fileNames []string + var expectedString string + + for i := 0; i < 10; i++ { + file, err := os.CreateTemp(tempDir, "tempFile") + if err != nil { + t.Fatal(err) + return + } + lineContent := fmt.Sprintf("This should be line number '%d' from '%s'", i, file.Name()) + _, err = file.WriteString(lineContent) + if err != nil { + t.Fatal(err) + return + } + expectedString += lineContent + "\n\n" + fileNames = append(fileNames, file.Name()) + } + + mergedFile, err := mergeTemplates(fileNames...) + if err != nil { + t.Errorf("failed to merge templates: %+v", err) + return + } + + mergedFile, err = os.Open(mergedFile.Name()) + if err != nil { + t.Error(err) + return + } + mergedFileBytes, err := os.ReadFile(mergedFile.Name()) + if err != nil { + t.Error(err) + return + } + + mergedFileContent := string(mergedFileBytes) + if mergedFileContent != expectedString { + t.Errorf("content of merged file does not match the expected content") + } +} diff --git a/magefiles/testspecs/templates.go b/pkg/testspecs/templates.go similarity index 87% rename from magefiles/testspecs/templates.go rename to pkg/testspecs/templates.go index 3ef3ee0ed..076752edd 100644 --- a/magefiles/testspecs/templates.go +++ b/pkg/testspecs/templates.go @@ -13,11 +13,6 @@ import ( "k8s.io/klog/v2" ) -var templates = map[string]string{ - "test-file": "templates/test_output_spec.tmpl", - "framework-describe": "templates/framework_describe_func.tmpl", -} - func NewTemplateData(specOutline TestOutline, destination string) *TemplateData { // This regex will find all the words that start with capital letter @@ -47,22 +42,10 @@ func NewTemplateData(specOutline TestOutline, destination string) *TemplateData return &TemplateData{Outline: specOutline, PackageName: dirName, FrameworkDescribeString: newSpecName} } -func GetTemplate(name string) (string, error) { - if s, ok := templates[name]; ok { - return s, nil - } - return "", fmt.Errorf("no Template found for %q", name) -} - func RenderFrameworkDescribeGoFile(t TemplateData) error { - - templatePath, err := GetTemplate("framework-describe") - if err != nil { - return err - } var describeFile = "pkg/framework/describe.go" - err = renderTemplate(describeFile, templatePath, t, true) + err := renderTemplate(describeFile, FrameworkDescribePath, t, true) if err != nil { klog.Errorf("failed to append to pkg/framework/describe.go with : %s", err) return err diff --git a/magefiles/testspecs/textspec.go b/pkg/testspecs/textspec.go similarity index 100% rename from magefiles/testspecs/textspec.go rename to pkg/testspecs/textspec.go diff --git a/magefiles/testspecs/types.go b/pkg/testspecs/types.go similarity index 100% rename from magefiles/testspecs/types.go rename to pkg/testspecs/types.go diff --git a/templates/default/recommended.tmpl b/templates/default/recommended.tmpl new file mode 100644 index 000000000..5db1255dc --- /dev/null +++ b/templates/default/recommended.tmpl @@ -0,0 +1,45 @@ +package {{ .CustomData.PackageName }} + +/* This was generated from a template file. Please feel free to update as necessary! + a couple things to note: + - Remember to implement specific logic of the service/domain you are trying to test if it not already there in the pkg/ + + - To include the tests as part of the E2E Test suite: + - Update the pkg/framework/describe.go to include the `Describe func` of this new test suite, If you haven't already done so. + - Import this new package into the cmd/e2e_test.go +*/ + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "fmt" + "strings" + "time" + "encoding/json" + "context" + + + "github.com/redhat-appstudio/e2e-tests/pkg/framework" + //framework imports edit as required + "github.com/redhat-appstudio/e2e-tests/pkg/constants" + "github.com/redhat-appstudio/e2e-tests/pkg/utils" + +) + +{{ range .CustomData.Outline }} +var _ = framework.{{ .Name }}("{{ .Text }}", {{range .Labels }}Label("{{.}}"), {{ end }} func() { + defer GinkgoRecover() + var err error + var f *framework.Framework + // use 'f' to access common controllers or the specific service controllers within the framework + BeforeAll(func() { + // Initialize the tests controllers + f, err = framework.NewFramework() + Expect(err).NotTo(HaveOccurred()) + }) + + // Generated specs: + {{ template "specs" . }} +}) +{{ end }} \ No newline at end of file diff --git a/templates/default/testOutline b/templates/default/testOutline new file mode 100644 index 000000000..771906c22 --- /dev/null +++ b/templates/default/testOutline @@ -0,0 +1,32 @@ +BuildSuiteDescribe: Build service E2E tests @build, @HACBS + Describe: test PaC component build @github-webhook, @pac-build, @pipeline + When: a new component without specified branch is created @pac-custom-default-branch + It: correctly targets the default branch (that is not named 'main') with PaC + It: triggers a PipelineRun + It: a related PipelineRun and Github webhook should be deleted after deleting the component + It: PR branch should not exists in the repo + When: a new component with specified custom branch branch is created + It: triggers a PipelineRun + It: should lead to a PaC init PR creation + It: the PipelineRun should eventually finish successfully + It: eventually leads to a creation of a PR comment with the PipelineRun status report + When: the PaC init branch is updated + It: eventually leads to triggering another PipelineRun + It: PipelineRun should eventually finish + It: eventually leads to another update of a PR with a comment about the PipelineRun status report + When: the PaC init branch is merged + It: eventually leads to triggering another PipelineRun + It: pipelineRun should eventually finish + When: the component is removed and recreated (with the same name in the same namespace) + It: should no longer lead to a creation of a PaC PR + + Describe: Creating component with container image source + It: should not trigger a PipelineRun + + Describe: PLNSRVCE-799 - test pipeline selector @pipeline-selector + It: a specific Pipeline bundle should be used and additional pipeline params should be added to the PipelineRun if all WhenConditions match + It: default Pipeline bundle should be used and no additional Pipeline params should be added to the PipelineRun if one of the WhenConditions does not match + + Describe: A secret with dummy quay.io credentials is created in the testing namespace + It: should override the shared secret + It: should not be possible to push to quay.io repo (PipelineRun should fail) diff --git a/templates/specs.tmpl b/templates/specs.tmpl new file mode 100644 index 000000000..acb02b4a4 --- /dev/null +++ b/templates/specs.tmpl @@ -0,0 +1,121 @@ +{{ define "specs" }} + {{ range .Nodes }} + {{ if eq .Name "DescribeTable" }} + {{ .Name }}("{{ .Text }} is table", + func() { + {{range .Nodes }} + {{ if eq .Name "By" }} + {{ .Name }}("{{ .Text }}") + {{ end -}} + {{ end -}} + }, + {{range .Nodes }} + {{ if eq .Name "Entry" }} + {{ .Name }}("{{ .Text }}",), + {{ end -}} + {{ end -}} + ) + {{ end -}} + {{ if ne .Name "DescribeTable" }} + {{ .Name }}("{{ .Text }}", {{range .Labels }}Label("{{.}}"), {{ end }}func() { + // Declare variables here. + {{range .Nodes }} + {{ if eq .Name "DescribeTable" }} + {{ .Name }}("{{ .Text }}", + func() { + {{range .Nodes }} + {{ if eq .Name "By" }} + {{ .Name }}("{{ .Text }}") + {{ end -}} + {{ end -}} + }, + {{range .Nodes }} + {{ if eq .Name "Entry" }} + {{ .Name }}("{{ .Text }}",), + {{ end -}} + {{ end -}} + ) + {{ end -}} + {{ if eq .Name "By" }} + {{ .Name }}("{{ .Text }}") + {{ continue }} + {{ end -}} + {{ if eq .Name "Entry" }} + {{ continue }} + {{ end -}} + {{ if ne .Name "DescribeTable" -}} + {{ .Name }}("{{ .Text }}", {{range .Labels }}Label("{{.}}"), {{ end }} func() { + {{ if eq .Name "It" -}} + // Implement test and assertions here + {{ end -}} + {{ range .Nodes -}} + {{ if eq .Name "DescribeTable" -}} + {{ .Name }}("{{ .Text }}", + func() { + {{range .Nodes -}} + {{ if eq .Name "By" -}} + {{ .Name }}("{{ .Text }}") + {{ end -}} + {{ end -}} + }, + {{range .Nodes -}} + {{ if eq .Name "Entry" -}} + {{ .Name }}("{{ .Text }}",), + {{ end -}} + {{ end -}} + ) + {{ end }} + {{ if eq .Name "By" -}} + {{ .Name }}("{{ .Text }}") + {{ continue }} + {{ end -}} + {{ if eq .Name "Entry" -}} + {{ continue }} + {{ end -}} + {{ if ne .Name "DescribeTable" -}} + {{ .Name }}("{{ .Text }}", {{range .Labels }}Label("{{.}}"), {{ end }} func() { + {{ if eq .Name "It" -}} + // Implement test and assertions here + {{ end -}} + {{ range .Nodes -}} + {{ if eq .Name "DescribeTable" -}} + {{ .Name }}("{{ .Text }}", + func() { + {{range .Nodes }} + {{ if eq .Name "By" }} + {{ .Name }}("{{ .Text }}") + {{ end -}} + {{ end -}} + }, + {{range .Nodes }} + {{ if eq .Name "Entry" }} + {{ .Name }}("{{ .Text }}",), + {{ end -}} + {{ end -}} + ) + {{ end -}} + {{ if eq .Name "By" }} + {{ .Name }}("{{ .Text }}") + {{ continue }} + {{ end -}} + {{ if eq .Name "Entry" }} + {{ continue }} + {{ end -}} + {{ if ne .Name "DescribeTable" }} + {{ .Name }}("{{ .Text }}", {{range .Labels }}Label("{{.}}"), {{ end }} func() { + {{ if eq .Name "It" -}} + // Implement test and assertions here + {{ end }} + }) + {{ end -}} + {{ end }} + }) + {{ end -}} + {{ end }} + }) + {{ end -}} + {{ end }} + }) + {{ end -}} + {{ end -}} +{{ end }} \ No newline at end of file