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, parser.ParseComments) if err != nil { panic(fmt.Sprintf("Error parsing test file '%s':\n%s\n", pathToFile, err.Error())) } addGinkgoImports(rootNode) removeTestingImport(rootNode) varUnderscoreBlock := createVarUnderscoreBlock() describeBlock := createDescribeBlock() varUnderscoreBlock.Values = []ast.Expr{describeBlock} for _, testFunc := range findTestFuncs(rootNode) { rewriteTestFuncAsItStatement(testFunc, rootNode, describeBlock) } underscoreDecl := &ast.GenDecl{ Tok: 85, // gah, magick numbers are needed to make this work TokPos: 14, // this tricks Go into writing "var _ = Describe" Specs: []ast.Spec{varUnderscoreBlock}, } imports := rootNode.Decls[0] tail := rootNode.Decls[1:] rootNode.Decls = append(append([]ast.Decl{imports}, underscoreDecl), tail...) 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)) } err = ioutil.WriteFile(pathToFile, buffer.Bytes(), fileInfo.Mode()) } /* * 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.CallExpr) { 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:]...) } /* * 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) } } }