Browse files

Add `ginkgo convert command`

Usage: `ginkgo convert path/to/package`

Converts all of the *_test.go files in the package's dir recursively to
Ginkgo. Also runs ginkgo bootstrap for each package converted.
  • Loading branch information...
1 parent a13e218 commit 37e49ffce132af16236d64d86709d5cc54732571 @tjarratt tjarratt committed with Onsi Fakhouri Feb 4, 2014
View
3 .gitignore
@@ -1,2 +1,3 @@
.DS_Store
-TODO
+TODO
+tmp/**/*
View
13 convert-fixtures/extra_functions_test.go
@@ -0,0 +1,13 @@
+package tmp
+
+import (
+ "testing"
+)
+
+func TestSomethingLessImportant(t *testing.T) {
+ somethingImportant(t, &"hello!")
+}
+
+func somethingImportant(t *testing.T, message *string) {
+ t.Log("Something important happened in a test: " + *message)
+}
View
20 convert-fixtures/ginkgo_test_goldmaster.go
@@ -0,0 +1,20 @@
+package tmp
+
+import (
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+type UselessStruct struct {
+ ImportantField string
+}
+
+func init() {
+ Describe("Testing with ginkgo", func() {
+ It("TestSomethingImportant", func() {
+
+ whatever := &UselessStruct{}
+ t.Fail(whatever.ImportantField != "SECRET_PASSWORD")
+ })
+ })
+}
View
10 convert-fixtures/nested/nested_test.go
@@ -0,0 +1,10 @@
+package nested
+
+import (
+ "testing"
+)
+
+func TestSomethingLessImportant(t *testing.T) {
+ whatever := &UselessStruct{}
+ t.Fail(whatever.ImportantField != "SECRET_PASSWORD")
+}
View
14 convert-fixtures/outside_package_test.go
@@ -0,0 +1,14 @@
+package tmp_test
+
+import (
+ "testing"
+)
+
+type UselessStruct struct {
+ ImportantField string
+}
+
+func TestSomethingImportant(t *testing.T) {
+ whatever := &UselessStruct{}
+ t.Fail(whatever.ImportantField != "SECRET_PASSWORD")
+}
View
27 convert-fixtures/xunit_test.go
@@ -0,0 +1,27 @@
+package tmp
+
+import (
+ "testing"
+)
+
+type UselessStruct struct {
+ ImportantField string
+ T *testing.T
+}
+
+var testFunc = func(t *testing.T, arg *string) { }
+
+func TestSomethingImportant(t *testing.T) {
+ whatever := &UselessStruct{
+ T: t,
+ ImportantField: "twisty maze of passages",
+ }
+ app := "string value"
+ something := &UselessStruct{ImportantField: app}
+
+ t.Fail(whatever.ImportantField != "SECRET_PASSWORD")
+ assert.Equal(t, whatever.ImportantField, "SECRET_PASSWORD")
+ var foo = func(t *testing.T) {}
+ foo()
+ testFunc(t, "something")
+}
View
16 convert-goldmasters/extra_functions_test.go
@@ -0,0 +1,16 @@
+package tmp
+
+import (
+ . "github.com/onsi/ginkgo"
+)
+
+func somethingImportant(t GinkgoTestingT, message *string) {
+ t.Log("Something important happened in a test: " + *message)
+}
+func init() {
+ Describe("Testing with ginkgo", func() {
+ It("TestSomethingLessImportant", func() {
+ somethingImportant(GinkgoT(), &"hello!")
+ })
+ })
+}
View
13 convert-goldmasters/fixtures_suite_test.go
@@ -0,0 +1,13 @@
+package tmp
+
+import (
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+
+ "testing"
+)
+
+func TestTmp(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "Tmp Suite")
+}
View
13 convert-goldmasters/nested_suite_test.go
@@ -0,0 +1,13 @@
+package nested_test
+
+import (
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+
+ "testing"
+)
+
+func TestNested(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "Nested Suite")
+}
View
15 convert-goldmasters/nested_test.go
@@ -0,0 +1,15 @@
+package nested
+
+import (
+ . "github.com/onsi/ginkgo"
+)
+
+func init() {
+ Describe("Testing with ginkgo", func() {
+ It("TestSomethingLessImportant", func() {
+
+ whatever := &UselessStruct{}
+ GinkgoT().Fail(whatever.ImportantField != "SECRET_PASSWORD")
+ })
+ })
+}
View
19 convert-goldmasters/outside_package_test.go
@@ -0,0 +1,19 @@
+package tmp_test
+
+import (
+ . "github.com/onsi/ginkgo"
+)
+
+type UselessStruct struct {
+ ImportantField string
+}
+
+func init() {
+ Describe("Testing with ginkgo", func() {
+ It("TestSomethingImportant", func() {
+
+ whatever := &UselessStruct{}
+ GinkgoT().Fail(whatever.ImportantField != "SECRET_PASSWORD")
+ })
+ })
+}
View
13 convert-goldmasters/suite_test.go
@@ -0,0 +1,13 @@
+package tmp_test
+
+import (
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+
+ "testing"
+)
+
+func TestTmp(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "Tmp Suite")
+}
View
31 convert-goldmasters/xunit_test.go
@@ -0,0 +1,31 @@
+package tmp
+
+import (
+ . "github.com/onsi/ginkgo"
+)
+
+type UselessStruct struct {
+ ImportantField string
+ T GinkgoTestingT
+}
+
+var testFunc = func(t GinkgoTestingT, arg *string) {}
+
+func init() {
+ Describe("Testing with ginkgo", func() {
+ It("TestSomethingImportant", func() {
+
+ whatever := &UselessStruct{
+ T: GinkgoT(),
+ ImportantField: "twisty maze of passages",
+ }
+ app := "string value"
+ something := &UselessStruct{ImportantField: app}
+ GinkgoT().Fail(whatever.ImportantField != "SECRET_PASSWORD")
+ assert.Equal(GinkgoT(), whatever.ImportantField, "SECRET_PASSWORD")
+ var foo = func(t GinkgoTestingT) {}
+ foo()
+ testFunc(GinkgoT(), "something")
+ })
+ })
+}
View
185 convert_test.go
@@ -0,0 +1,185 @@
+package ginkgo
+
+import (
+ . "github.com/onsi/gomega"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+)
+
+func init() {
+ Describe("using ginkgo convert", func() {
+ BeforeEach(deleteFilesInTmp)
+ BeforeEach(buildGinkgo)
+
+ It("rewrites xunit tests as ginkgo tests", func() {
+ withTempDir(func(tempDir string) {
+ runGinkgoConvert()
+
+ convertedFile := readConvertedFileNamed(tempDir, "xunit_test.go")
+ goldmaster := readGoldMasterNamed("xunit_test.go")
+ Expect(convertedFile).To(Equal(goldmaster))
+ })
+ })
+
+ It("rewrites all usages of *testing.T as mr.T()", func() {
+ withTempDir(func(tempDir string) {
+ runGinkgoConvert()
+
+ convertedFile := readConvertedFileNamed(tempDir, "extra_functions_test.go")
+ goldmaster := readGoldMasterNamed("extra_functions_test.go")
+ Expect(convertedFile).To(Equal(goldmaster))
+ })
+ })
+
+ It("rewrites tests in the package dir that belong to other packages", func() {
+ withTempDir(func(tempDir string) {
+ runGinkgoConvert()
+
+ convertedFile := readConvertedFileNamed(tempDir, "outside_package_test.go")
+ goldMaster := readGoldMasterNamed("outside_package_test.go")
+ Expect(convertedFile).To(Equal(goldMaster))
+ })
+ })
+
+ It("rewrites tests in nested packages", func() {
+ withTempDir(func(dir string) {
+ runGinkgoConvert()
+
+ convertedFile := readConvertedFileNamed(dir, filepath.Join("nested", "nested_test.go"))
+ goldMaster := readGoldMasterNamed("nested_test.go")
+ Expect(convertedFile).To(Equal(goldMaster))
+ })
+ })
+
+ Context("ginkgo test suite files", func() {
+ It("creates a ginkgo test suite file for the package you specified", func() {
+ withTempDir(func(dir string) {
+ runGinkgoConvert()
+
+ testsuite := readConvertedFileNamed(dir, "tmp_suite_test.go")
+ goldmaster := readGoldMasterNamed("suite_test.go")
+ Expect(testsuite).To(Equal(goldmaster))
+ })
+ })
+
+ It("creates ginkgo test suites for all nested packages", func() {
+ withTempDir(func(dir string) {
+ runGinkgoConvert()
+
+ testsuite := readConvertedFileNamed(dir, "nested", "nested_suite_test.go")
+ goldmaster := readGoldMasterNamed("nested_suite_test.go")
+ Expect(testsuite).To(Equal(goldmaster))
+ })
+ })
+ })
+
+ It("gracefully handles existing test suite files", func() {
+ withTempDir(func(dir string) {
+ cwd, err := os.Getwd()
+ bytes, err := ioutil.ReadFile(filepath.Join(cwd, "convert-goldmasters", "fixtures_suite_test.go"))
+ Expect(err).NotTo(HaveOccurred())
+ err = ioutil.WriteFile(filepath.Join(cwd, "tmp", "tmp_suite_test.go"), bytes, 0600)
+ Expect(err).NotTo(HaveOccurred())
+
+ runGinkgoConvert()
+ })
+ })
+ })
+}
+
+func withTempDir(cb func(tempDir string)) {
+ cwd, err := os.Getwd()
+ Expect(err).NotTo(HaveOccurred())
+
+ tempDir := filepath.Join(cwd, "tmp")
+ err = os.MkdirAll(tempDir, os.ModeDir|os.ModeTemporary|os.ModePerm)
+ Expect(err).NotTo(HaveOccurred())
+
+ copyFixturesIntoTempDir("convert-fixtures", tempDir)
+
+ cb(tempDir)
+}
+
+func copyFixturesIntoTempDir(relativePathToFixtures, tempDir string) {
+ _, err := os.Stat(relativePathToFixtures)
+ if err != nil {
+ os.Mkdir(relativePathToFixtures, os.ModeDir|os.ModeTemporary|os.ModePerm)
+ }
+
+ cwd, err := os.Getwd()
+ Expect(err).NotTo(HaveOccurred())
+
+ files, err := ioutil.ReadDir(filepath.Join(cwd, relativePathToFixtures))
+ Expect(err).NotTo(HaveOccurred())
+
+ for _, f := range files {
+ if f.IsDir() {
+ nestedFixturesDir := filepath.Join(relativePathToFixtures, f.Name())
+ nestedTempDir := filepath.Join(tempDir, f.Name())
+ err = os.MkdirAll(nestedTempDir, os.ModeDir|os.ModeTemporary|os.ModePerm)
+ Expect(err).NotTo(HaveOccurred())
+
+ copyFixturesIntoTempDir(nestedFixturesDir, nestedTempDir)
+ continue
+ }
+
+ bytes, err := ioutil.ReadFile(filepath.Join(cwd, relativePathToFixtures, f.Name()))
+ Expect(err).NotTo(HaveOccurred())
+
+ err = ioutil.WriteFile(filepath.Join(tempDir, f.Name()), bytes, 0600)
+ Expect(err).NotTo(HaveOccurred())
+ }
+}
+
+func runGinkgoConvert() {
+ cwd, err := os.Getwd()
+ Expect(err).NotTo(HaveOccurred())
+
+ pathToExecutable := filepath.Join(cwd, "tmp", "ginkgo")
+ err = exec.Command(pathToExecutable, "convert", "github.com/onsi/ginkgo/tmp").Run()
+ Expect(err).NotTo(HaveOccurred())
+}
+
+func readGoldMasterNamed(filename string) string {
+ cwd, err := os.Getwd()
+ Expect(err).NotTo(HaveOccurred())
+
+ bytes, err := ioutil.ReadFile(filepath.Join(cwd, "convert-goldmasters", filename))
+ Expect(err).NotTo(HaveOccurred())
+
+ return string(bytes)
+}
+
+func readConvertedFileNamed(pathComponents ...string) string {
+ pathToFile := filepath.Join(pathComponents...)
+ bytes, err := ioutil.ReadFile(pathToFile)
+ Expect(err).NotTo(HaveOccurred())
+
+ return string(bytes)
+}
+
+func deleteFilesInTmp() {
+ cwd, err := os.Getwd()
+ Expect(err).NotTo(HaveOccurred())
+ tempDir := filepath.Join(cwd, "tmp")
+
+ err = os.RemoveAll(tempDir)
+ Expect(err).NotTo(HaveOccurred())
+}
+
+func buildGinkgo() {
+ cwd, err := os.Getwd()
+ Expect(err).NotTo(HaveOccurred())
+
+ defer func() {
+ os.Chdir(cwd)
+ }()
+
+ err = os.Chdir(filepath.Join(cwd, "ginkgo"))
+ Expect(err).NotTo(HaveOccurred())
+
+ err = exec.Command("go", "build", "-o", "../tmp/ginkgo").Run()
+ Expect(err).NotTo(HaveOccurred())
+}
View
31 ginkgo/convert.go
@@ -0,0 +1,31 @@
+package main
+
+import (
+ "fmt"
+ "github.com/onsi/ginkgo/ginkgo/convert"
+ "os"
+)
+
+func convertPackage() {
+ if len(os.Args) != 3 {
+ println(fmt.Sprintf("usage: %s convert /path/to/your/package", os.Args[2]))
+ os.Exit(1)
+ }
+
+ defer func() {
+ err := recover()
+ if err != nil {
+ switch err := err.(type) {
+ case error:
+ println(err.Error())
+ case string:
+ println(err)
+ default:
+ println(fmt.Sprintf("unexpected error: %#v", err))
+ }
+ os.Exit(1)
+ }
+ }()
+
+ convert.RewritePackage(os.Args[2])
+}
View
82 ginkgo/convert/ginkgo_ast_nodes.go
@@ -0,0 +1,82 @@
+package convert
+
+import (
+ "fmt"
+ "go/ast"
+)
+
+/*
+ * Creates a func init() node
+ */
+func createInitBlock() *ast.FuncDecl {
+ blockStatement := &ast.BlockStmt{List: []ast.Stmt{}}
+ fieldList := &ast.FieldList{}
+ funcType := &ast.FuncType{Params: fieldList}
+ ident := &ast.Ident{Name: "init"}
+
+ return &ast.FuncDecl{Name: ident, Type: funcType, Body: blockStatement}
+}
+
+/*
+ * Creates a Describe("Testing with ginkgo", func() { }) node
+ */
+func createDescribeBlock() *ast.ExprStmt {
+ blockStatement := &ast.BlockStmt{List: []ast.Stmt{}}
+
+ fieldList := &ast.FieldList{}
+ funcType := &ast.FuncType{Params: fieldList}
+ funcLit := &ast.FuncLit{Type: funcType, Body: blockStatement}
+ basicLit := &ast.BasicLit{Kind: 9, Value: "\"Testing with ginkgo\""}
+ describeIdent := &ast.Ident{Name: "Describe"}
+ callExpr := &ast.CallExpr{Fun: describeIdent, Args: []ast.Expr{basicLit, funcLit}}
+
+ return &ast.ExprStmt{X: callExpr}
+}
+
+/*
+ * Convenience function to return the name of the *testing.T param
+ * for a Test function that will be rewritten. This is useful because
+ * we will want to replace the usage of this named *testing.T inside the
+ * body of the function with a GinktoT.
+ */
+func namedTestingTArg(node *ast.FuncDecl) string {
+ return node.Type.Params.List[0].Names[0].Name // *exhale*
+}
+
+/*
+ * Convenience function to return the block statement node for a Describe statement
+ */
+func blockStatementFromDescribe(desc *ast.ExprStmt) *ast.BlockStmt {
+ var funcLit *ast.FuncLit
+ var found = false
+
+ for _, node := range desc.X.(*ast.CallExpr).Args {
+ switch node := node.(type) {
+ case *ast.FuncLit:
+ found = true
+ funcLit = node
+ break
+ }
+ }
+
+ if !found {
+ panic("Error finding ast.FuncLit inside describe statement. Somebody done goofed.")
+ }
+
+ return funcLit.Body
+}
+
+/* convenience function for creating an It("TestNameHere")
+ * with all the body of the test function inside the anonymous
+ * func passed to It()
+ */
+func createItStatementForTestFunc(testFunc *ast.FuncDecl) *ast.ExprStmt {
+ blockStatement := &ast.BlockStmt{List: testFunc.Body.List}
+ fieldList := &ast.FieldList{}
+ funcType := &ast.FuncType{Params: fieldList}
+ funcLit := &ast.FuncLit{Type: funcType, Body: blockStatement}
+ basicLit := &ast.BasicLit{Kind: 9, Value: fmt.Sprintf("\"%s\"", testFunc.Name.Name)}
+ itBlockIdent := &ast.Ident{Name: "It"}
+ callExpr := &ast.CallExpr{Fun: itBlockIdent, Args: []ast.Expr{basicLit, funcLit}}
+ return &ast.ExprStmt{X: callExpr}
+}
View
17 ginkgo/convert/ginkgo_t_helper.go
@@ -0,0 +1,17 @@
+package convert
+
+import (
+ "go/ast"
+)
+
+func newGinkgoTFromIdent(ident *ast.Ident) *ast.CallExpr {
+ return &ast.CallExpr{
+ Lparen: ident.NamePos + 1,
+ Rparen: ident.NamePos + 2,
+ Fun: &ast.Ident{Name: "GinkgoT"},
+ }
+}
+
+func newGinkgoTestingT() *ast.Ident {
+ return &ast.Ident{Name: "GinkgoTestingT"}
+}
View
91 ginkgo/convert/import.go
@@ -0,0 +1,91 @@
+package convert
+
+import (
+ "errors"
+ "fmt"
+ "go/ast"
+)
+
+/*
+ * Given the root node of an AST, returns the node containing the
+ * import statements for the file.
+ */
+func importsForRootNode(rootNode *ast.File) (imports *ast.GenDecl, err error) {
+ for _, declaration := range rootNode.Decls {
+ decl, ok := declaration.(*ast.GenDecl)
+ if !ok || len(decl.Specs) == 0 {
+ continue
+ }
+
+ _, ok = decl.Specs[0].(*ast.ImportSpec)
+ if ok {
+ imports = decl
+ return
+ }
+ }
+
+ err = errors.New(fmt.Sprintf("Could not find imports for root node:\n\t%#v\n", rootNode))
+ return
+}
+
+/*
+ * Removes "testing" import, if present
+ */
+func removeTestingImport(rootNode *ast.File) {
+ importDecl, err := importsForRootNode(rootNode)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ var index int
+ for i, importSpec := range importDecl.Specs {
+ importSpec := importSpec.(*ast.ImportSpec)
+ if importSpec.Path.Value == "\"testing\"" {
+ index = i
+ break
+ }
+ }
+
+ importDecl.Specs = append(importDecl.Specs[:index], importDecl.Specs[index+1:]...)
+}
+
+/*
+ * Adds import statements for onsi/ginkgo, if missing
+ */
+func addGinkgoImports(rootNode *ast.File) {
+ importDecl, err := importsForRootNode(rootNode)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ if len(importDecl.Specs) == 0 {
+ // TODO: might need to create a import decl here
+ panic("unimplemented : expected to find an imports block")
+ }
+
+ needsGinkgo := true
+ for _, importSpec := range importDecl.Specs {
+ importSpec, ok := importSpec.(*ast.ImportSpec)
+ if !ok {
+ continue
+ }
+
+ if importSpec.Path.Value == "\"github.com/onsi/ginkgo\"" {
+ needsGinkgo = false
+ }
+ }
+
+ if needsGinkgo {
+ importDecl.Specs = append(importDecl.Specs, createImport(".", "\"github.com/onsi/ginkgo\""))
+ }
+}
+
+/*
+ * convenience function to create an import statement
+ */
+func createImport(name, path string) *ast.ImportSpec {
+ return &ast.ImportSpec{
+ Name: &ast.Ident{Name: name},
+ Path: &ast.BasicLit{Kind: 9, Value: path},
+ }
+}
View
105 ginkgo/convert/package_rewriter.go
@@ -0,0 +1,105 @@
+package convert
+
+import (
+ "fmt"
+ "go/build"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+)
+
+/*
+ * RewritePackage takes a name (eg: my-package/tools), finds its test files using
+ * Go's build package, and then rewrites them. A ginkgo test suite file will
+ * also be added for this package, and all of its child packages.
+ */
+func RewritePackage(packageName string) {
+ pkg, err := build.Default.Import(packageName, ".", build.ImportMode(0))
+ if err != nil {
+ panic(fmt.Sprintf("unexpected error reading package: '%s'\n%s\n", os.Args[1], err.Error()))
+ }
+
+ for _, filename := range findTestsInPackage(pkg) {
+ rewriteTestsInFile(filename)
+ }
+ return
+}
+
+/*
+ * Given a package, findTestsInPackage reads the test files in the directory,
+ * and then recurses on each child package, returning a slice of all test files
+ * found in this process.
+ */
+func findTestsInPackage(pkg *build.Package) (testfiles []string) {
+ for _, file := range append(pkg.TestGoFiles, pkg.XTestGoFiles...) {
+ testfiles = append(testfiles, filepath.Join(pkg.Dir, file))
+ }
+
+ dirFiles, err := ioutil.ReadDir(pkg.Dir)
+ if err != nil {
+ panic(fmt.Sprintf("unexpected error reading dir: '%s'\n%s\n", pkg.Dir, err.Error()))
+ }
+
+ for _, file := range dirFiles {
+ if !file.IsDir() {
+ continue
+ }
+
+ packageName := filepath.Join(pkg.ImportPath, file.Name())
+ subPackage, err := build.Default.Import(packageName, ".", build.ImportMode(0))
+ if err != nil {
+ panic(fmt.Sprintf("unexpected error reading package: '%s'\n%s\n", packageName, err.Error()))
+ }
+
+ testfiles = append(testfiles, findTestsInPackage(subPackage)...)
+ }
+
+ addGinkgoSuiteForPackage(pkg)
+ goFmtPackage(pkg)
+ return
+}
+
+/*
+ * Shells out to `ginkgo bootstrap` to create a test suite file
+ */
+func addGinkgoSuiteForPackage(pkg *build.Package) {
+ originalDir, err := os.Getwd()
+ if err != nil {
+ panic(err)
+ }
+
+ suite_test_file := filepath.Join(pkg.Dir, pkg.Name+"_suite_test.go")
+ _, err = os.Stat(suite_test_file)
+ if err == nil {
+ return // test file already exists, this should be a no-op
+ }
+
+ err = os.Chdir(pkg.Dir)
+ if err != nil {
+ panic(err)
+ }
+
+ output, err := exec.Command("ginkgo", "bootstrap").Output()
+
+ if err != nil {
+ panic(fmt.Sprintf("error running 'ginkgo bootstrap'.\nstdout: %s\n%s\n", output, err.Error()))
+ }
+
+ err = os.Chdir(originalDir)
+ if err != nil {
+ panic(err)
+ }
+}
+
+
+/*
+ * Shells out to `go fmt` to format the package
+ */
+func goFmtPackage(pkg *build.Package) {
+ output, err := exec.Command("go", "fmt", pkg.ImportPath).Output()
+
+ if err != nil {
+ panic(fmt.Sprintf("Error running 'go fmt %s'.\nstdout: %s\n%s\n", pkg.ImportPath, output, err.Error()))
+ }
+}
View
56 ginkgo/convert/test_finder.go
@@ -0,0 +1,56 @@
+package convert
+
+import (
+ "go/ast"
+ "regexp"
+)
+
+/*
+ * Given a root node, walks its top level statements and returns
+ * points to function nodes to rewrite as It statements.
+ * These functions, according to Go testing convention, must be named
+ * TestWithCamelCasedName and receive a single *testing.T argument.
+ */
+func findTestFuncs(rootNode *ast.File) (testsToRewrite []*ast.FuncDecl) {
+ testNameRegexp := regexp.MustCompile("^Test[A-Z].+")
+
+ ast.Inspect(rootNode, func(node ast.Node) bool {
+ if node == nil {
+ return false
+ }
+
+ switch node := node.(type) {
+ case *ast.FuncDecl:
+ matches := testNameRegexp.MatchString(node.Name.Name)
+
+ if matches && receivesTestingT(node) {
+ testsToRewrite = append(testsToRewrite, node)
+ }
+ }
+
+ return true
+ })
+
+ return
+}
+
+/*
+ * convenience function that looks at args to a function and determines if its
+ * params include an argument of type *testing.T
+ */
+func receivesTestingT(node *ast.FuncDecl) bool {
+ if len(node.Type.Params.List) != 1 {
+ return false
+ }
+
+ base, ok := node.Type.Params.List[0].Type.(*ast.StarExpr)
+ if !ok {
+ return false
+ }
+
+ intermediate := base.X.(*ast.SelectorExpr)
+ isTestingPackage := intermediate.X.(*ast.Ident).Name == "testing"
+ isTestingT := intermediate.Sel.Name == "T"
+
+ return isTestingPackage && isTestingT
+}
View
155 ginkgo/convert/testfile_rewriter.go
@@ -0,0 +1,155 @@
+package convert
+
+import (
+ "bytes"
+ "fmt"
+ "go/ast"
+ "go/format"
+ "go/parser"
+ "go/token"
+ "io/ioutil"
+ "os"
+)
+
+/*
+ * Given a file path, rewrites any tests in the Ginkgo format.
+ * First, we parse the AST, and update the imports declaration.
+ * Then, we walk the first child elements in the file, returning tests to rewrite.
+ * A top level init func is declared, with a single Describe func inside.
+ * Then the test functions to rewrite are inserted as It statements inside the Describe.
+ * Finally we walk the rest of the file, replacing other usages of *testing.T
+ * Once that is complete, we write the AST back out again to its file.
+ */
+func rewriteTestsInFile(pathToFile string) {
+ fileSet := token.NewFileSet()
+ rootNode, err := parser.ParseFile(fileSet, pathToFile, nil, 0)
+ if err != nil {
+ panic(fmt.Sprintf("Error parsing test file '%s':\n%s\n", pathToFile, err.Error()))
+ }
+
+ addGinkgoImports(rootNode)
+ removeTestingImport(rootNode)
+
+ topLevelInitFunc := createInitBlock()
+ describeBlock := createDescribeBlock()
+ topLevelInitFunc.Body.List = append(topLevelInitFunc.Body.List, describeBlock)
+
+ for _, testFunc := range findTestFuncs(rootNode) {
+ rewriteTestFuncAsItStatement(testFunc, rootNode, describeBlock)
+ }
+
+ rootNode.Decls = append(rootNode.Decls, topLevelInitFunc)
+ rewriteOtherFuncsToUseGinkgoT(rootNode.Decls)
+ walkNodesInRootNodeReplacingTestingT(rootNode)
+
+ var buffer bytes.Buffer
+ if err = format.Node(&buffer, fileSet, rootNode); err != nil {
+ panic(fmt.Sprintf("Error formatting ast node after rewriting tests.\n%s\n", err.Error()))
+ }
+
+ fileInfo, err := os.Stat(pathToFile)
+ if err != nil {
+ panic(fmt.Sprintf("Error stat'ing file: %s\n", pathToFile))
+ }
+
+ ioutil.WriteFile(pathToFile, buffer.Bytes(), fileInfo.Mode())
+ return
+}
+
+/*
+ * Given a test func named TestDoesSomethingNeat, rewrites it as
+ * It("does something neat", func() { __test_body_here__ }) and adds it
+ * to the Describe's list of statements
+ */
+func rewriteTestFuncAsItStatement(testFunc *ast.FuncDecl, rootNode *ast.File, describe *ast.ExprStmt) {
+ var funcIndex int = -1
+ for index, child := range rootNode.Decls {
+ if child == testFunc {
+ funcIndex = index
+ break
+ }
+ }
+
+ if funcIndex < 0 {
+ panic(fmt.Sprintf("Assert failed: Error finding index for test node %s\n", testFunc.Name.Name))
+ }
+
+ var block *ast.BlockStmt = blockStatementFromDescribe(describe)
+ block.List = append(block.List, createItStatementForTestFunc(testFunc))
+ replaceTestingTsWithGinkgoT(block, namedTestingTArg(testFunc))
+
+ // remove the old test func from the root node's declarations
+ rootNode.Decls = append(rootNode.Decls[:funcIndex], rootNode.Decls[funcIndex+1:]...)
+ return
+}
+
+/*
+ * walks nodes inside of a test func's statements and replaces the usage of
+ * it's named *testing.T param with GinkgoT's
+ */
+func replaceTestingTsWithGinkgoT(statementsBlock *ast.BlockStmt, testingT string) {
+ ast.Inspect(statementsBlock, func(node ast.Node) bool {
+ if node == nil {
+ return false
+ }
+
+ keyValueExpr, ok := node.(*ast.KeyValueExpr)
+ if ok {
+ replaceNamedTestingTsInKeyValueExpression(keyValueExpr, testingT)
+ return true
+ }
+
+ funcLiteral, ok := node.(*ast.FuncLit)
+ if ok {
+ replaceTypeDeclTestingTsInFuncLiteral(funcLiteral)
+ return true
+ }
+
+ callExpr, ok := node.(*ast.CallExpr)
+ if !ok {
+ return true
+ }
+ replaceTestingTsInArgsLists(callExpr, testingT)
+
+ funCall, ok := callExpr.Fun.(*ast.SelectorExpr)
+ if ok {
+ replaceTestingTsMethodCalls(funCall, testingT)
+ }
+
+ return true
+ })
+}
+
+/*
+ * rewrite t.Fail() or any other *testing.T method by replacing with T().Fail()
+ * This function receives a selector expression (eg: t.Fail()) and
+ * the name of the *testing.T param from the function declaration. Rewrites the
+ * selector expression in place if the target was a *testing.T
+ */
+func replaceTestingTsMethodCalls(selectorExpr *ast.SelectorExpr, testingT string) {
+ ident, ok := selectorExpr.X.(*ast.Ident)
+ if !ok {
+ return
+ }
+
+ if ident.Name == testingT {
+ selectorExpr.X = newGinkgoTFromIdent(ident)
+ }
+}
+
+/*
+ * replaces usages of a named *testing.T param inside of a call expression
+ * with a new GinkgoT object
+ */
+func replaceTestingTsInArgsLists(callExpr *ast.CallExpr, testingT string) {
+ for index, arg := range callExpr.Args {
+ ident, ok := arg.(*ast.Ident)
+ if !ok {
+ continue
+ }
+
+ if ident.Name == testingT {
+ callExpr.Args[index] = newGinkgoTFromIdent(ident)
+ }
+ }
+}
View
130 ginkgo/convert/testing_t_rewriter.go
@@ -0,0 +1,130 @@
+package convert
+
+import (
+ "go/ast"
+)
+
+/*
+ * Rewrites any other top level funcs that receive a *testing.T param
+ */
+func rewriteOtherFuncsToUseGinkgoT(declarations []ast.Decl) {
+ for _, decl := range declarations {
+ decl, ok := decl.(*ast.FuncDecl)
+ if !ok {
+ continue
+ }
+
+ for _, param := range decl.Type.Params.List {
+ starExpr, ok := param.Type.(*ast.StarExpr)
+ if !ok {
+ continue
+ }
+
+ selectorExpr, ok := starExpr.X.(*ast.SelectorExpr)
+ if !ok {
+ continue
+ }
+
+ xIdent, ok := selectorExpr.X.(*ast.Ident)
+ if !ok || xIdent.Name != "testing" {
+ continue
+ }
+
+ if selectorExpr.Sel.Name != "T" {
+ continue
+ }
+
+ param.Type = newGinkgoTestingT()
+ }
+ }
+}
+
+/*
+ * Walks all of the nodes in the file, replacing *testing.T in struct
+ * and func literal nodes. eg:
+ * type foo struct { *testing.T }
+ * var bar = func(t *testing.T) { }
+ */
+func walkNodesInRootNodeReplacingTestingT(rootNode *ast.File) {
+ ast.Inspect(rootNode, func(node ast.Node) bool {
+ if node == nil {
+ return false
+ }
+
+ switch node := node.(type) {
+ case *ast.StructType:
+ replaceTestingTsInStructType(node)
+ case *ast.FuncLit:
+ replaceTypeDeclTestingTsInFuncLiteral(node)
+ }
+
+ return true
+ })
+}
+
+/*
+ * replaces named *testing.T inside a composite literal
+ */
+func replaceNamedTestingTsInKeyValueExpression(kve *ast.KeyValueExpr, testingT string) {
+ ident, ok := kve.Value.(*ast.Ident)
+ if !ok {
+ return
+ }
+
+ if ident.Name == testingT {
+ kve.Value = newGinkgoTFromIdent(ident)
+ }
+}
+
+/*
+ * replaces *testing.T params in a func literal with GinkgoT
+ */
+func replaceTypeDeclTestingTsInFuncLiteral(functionLiteral *ast.FuncLit) {
+ for _, arg := range functionLiteral.Type.Params.List {
+ starExpr, ok := arg.Type.(*ast.StarExpr)
+ if !ok {
+ continue
+ }
+
+ selectorExpr, ok := starExpr.X.(*ast.SelectorExpr)
+ if !ok {
+ continue
+ }
+
+ target, ok := selectorExpr.X.(*ast.Ident)
+ if !ok {
+ continue
+ }
+
+ if target.Name == "testing" && selectorExpr.Sel.Name == "T" {
+ arg.Type = newGinkgoTestingT()
+ }
+ }
+}
+
+/*
+ * Replaces *testing.T types inside of a struct declaration with a GinkgoT
+ * eg: type foo struct { *testing.T }
+ */
+func replaceTestingTsInStructType(structType *ast.StructType) {
+ for _, field := range structType.Fields.List {
+ starExpr, ok := field.Type.(*ast.StarExpr)
+ if !ok {
+ continue
+ }
+
+ selectorExpr, ok := starExpr.X.(*ast.SelectorExpr)
+ if !ok {
+ continue
+ }
+
+ xIdent, ok := selectorExpr.X.(*ast.Ident)
+ if !ok {
+ continue
+ }
+
+ if xIdent.Name == "testing" && selectorExpr.Sel.Name == "T" {
+ field.Type = newGinkgoTestingT()
+ }
+ }
+}
View
2 ginkgo/main.go
@@ -161,6 +161,8 @@ func handleSubcommands(args []string) bool {
switch args[0] {
case "bootstrap":
generateBootstrap()
+ case "convert":
+ convertPackage()
case "generate":
subject := ""
if len(args) > 1 {

0 comments on commit 37e49ff

Please sign in to comment.