diff --git a/harness/app.go b/harness/app.go index f36a42a2..e287c1a5 100644 --- a/harness/app.go +++ b/harness/app.go @@ -73,7 +73,7 @@ func (cmd AppCmd) Start() error { case <-cmd.waitChan(): return errors.New("revel/harness: app died") - case <-time.After(30 * time.Second): + case <-time.After(90 * time.Second): cmd.Kill() return errors.New("revel/harness: app timed out") diff --git a/harness/import_cache.go b/harness/import_cache.go new file mode 100644 index 00000000..de4f13e1 --- /dev/null +++ b/harness/import_cache.go @@ -0,0 +1,123 @@ +package harness + +import ( + "go/ast" + "go/build" + "go/token" + "strings" + + "github.com/revel/revel" +) + +type ImportCache map[string]string + +func (ip ImportCache) processPackage(fset *token.FileSet, pkgImportPath, pkgPath string, pkg *ast.Package) *SourceInfo { + var ( + structSpecs []*TypeInfo + initImportPaths []string + + methodSpecs = make(methodMap) + validationKeys = make(map[string]map[int]string) + scanControllers = strings.HasSuffix(pkgImportPath, "/controllers") || + strings.Contains(pkgImportPath, "/controllers/") + scanTests = strings.HasSuffix(pkgImportPath, "/tests") || + strings.Contains(pkgImportPath, "/tests/") + ) + + // For each source file in the package... + for _, file := range pkg.Files { + + // Imports maps the package key to the full import path. + // e.g. import "sample/app/models" => "models": "sample/app/models" + imports := map[string]string{} + + // For each declaration in the source file... + for _, decl := range file.Decls { + ip.addImports(imports, decl, pkgPath) + + if scanControllers { + // Match and add both structs and methods + structSpecs = appendStruct(structSpecs, pkgImportPath, pkg, decl, imports, fset) + appendAction(fset, methodSpecs, decl, pkgImportPath, pkg.Name, imports) + } else if scanTests { + structSpecs = appendStruct(structSpecs, pkgImportPath, pkg, decl, imports, fset) + } + + // If this is a func... + if funcDecl, ok := decl.(*ast.FuncDecl); ok { + // Scan it for validation calls + lineKeys := getValidationKeys(fset, funcDecl, imports) + if len(lineKeys) > 0 { + validationKeys[pkgImportPath+"."+getFuncName(funcDecl)] = lineKeys + } + + // Check if it's an init function. + if funcDecl.Name.Name == "init" { + initImportPaths = []string{pkgImportPath} + } + } + } + } + + // Add the method specs to the struct specs. + for _, spec := range structSpecs { + spec.MethodSpecs = methodSpecs[spec.StructName] + } + + return &SourceInfo{ + StructSpecs: structSpecs, + ValidationKeys: validationKeys, + InitImportPaths: initImportPaths, + } +} + +func (ip ImportCache) addImports(imports map[string]string, decl ast.Decl, srcDir string) { + genDecl, ok := decl.(*ast.GenDecl) + if !ok { + return + } + + if genDecl.Tok != token.IMPORT { + return + } + + for _, spec := range genDecl.Specs { + importSpec := spec.(*ast.ImportSpec) + var pkgAlias string + if importSpec.Name != nil { + pkgAlias = importSpec.Name.Name + if pkgAlias == "_" { + continue + } + } + quotedPath := importSpec.Path.Value // e.g. "\"sample/app/models\"" + fullPath := quotedPath[1 : len(quotedPath)-1] // Remove the quotes + + if pkgAlias == "" { + pkgAlias = ip[fullPath] + } + + // If the package was not aliased (common case), we have to import it + // to see what the package name is. + // TODO: Can improve performance here a lot: + // 1. Do not import everything over and over again. Keep a cache. + // 2. Exempt the standard library; their directories always match the package name. + // 3. Can use build.FindOnly and then use parser.ParseDir with mode PackageClauseOnly + + if pkgAlias == "" { + pkg, err := build.Import(fullPath, srcDir, 0) + if err != nil { + // We expect this to happen for apps using reverse routing (since we + // have not yet generated the routes). Don't log that. + if !strings.HasSuffix(fullPath, "/app/routes") { + revel.TRACE.Println("Could not find import:", fullPath) + } + continue + } + pkgAlias = pkg.Name + ip[fullPath] = pkgAlias + } + + imports[pkgAlias] = fullPath + } +} diff --git a/harness/reflect.go b/harness/reflect.go index 77d60077..b9b522ad 100644 --- a/harness/reflect.go +++ b/harness/reflect.go @@ -94,6 +94,7 @@ func ProcessSource(roots []string) (*SourceInfo, *revel.Error) { srcInfo *SourceInfo compileError *revel.Error ) + ip := ImportCache{} for _, root := range roots { rootImportPath := importPathFromPath(root) @@ -102,8 +103,7 @@ func ProcessSource(roots []string) (*SourceInfo, *revel.Error) { continue } - // Start walking the directory tree. - _ = revel.Walk(root, func(path string, info os.FileInfo, err error) error { + stroll := func(path string, info os.FileInfo, err error) error { if err != nil { log.Println("Error scanning app source:", err) return nil @@ -162,7 +162,16 @@ func ProcessSource(roots []string) (*SourceInfo, *revel.Error) { // There should be only one package in this directory. if len(pkgs) > 1 { - log.Println("Most unexpected! Multiple packages in a single directory:", pkgs) + log.Println("Most unexpected! Multiple packages in a single directory, removing *_test packages:", pkgs) + // filter out test packages + for k := range pkgs { + if strings.HasSuffix(k, "_test") { + delete(pkgs, k) + } + } + if len(pkgs) > 1 { + log.Fatalf("more than one non test package found in dir: %s", path) + } } var pkg *ast.Package @@ -170,9 +179,12 @@ func ProcessSource(roots []string) (*SourceInfo, *revel.Error) { pkg = v } - srcInfo = appendSourceInfo(srcInfo, processPackage(fset, pkgImportPath, path, pkg)) + srcInfo = appendSourceInfo(srcInfo, ip.processPackage(fset, pkgImportPath, path, pkg)) return nil - }) + } + + // Start walking the directory tree. + _ = revel.Walk(root, stroll) } return srcInfo, compileError @@ -195,66 +207,6 @@ func appendSourceInfo(srcInfo1, srcInfo2 *SourceInfo) *SourceInfo { return srcInfo1 } -func processPackage(fset *token.FileSet, pkgImportPath, pkgPath string, pkg *ast.Package) *SourceInfo { - var ( - structSpecs []*TypeInfo - initImportPaths []string - - methodSpecs = make(methodMap) - validationKeys = make(map[string]map[int]string) - scanControllers = strings.HasSuffix(pkgImportPath, "/controllers") || - strings.Contains(pkgImportPath, "/controllers/") - scanTests = strings.HasSuffix(pkgImportPath, "/tests") || - strings.Contains(pkgImportPath, "/tests/") - ) - - // For each source file in the package... - for _, file := range pkg.Files { - - // Imports maps the package key to the full import path. - // e.g. import "sample/app/models" => "models": "sample/app/models" - imports := map[string]string{} - - // For each declaration in the source file... - for _, decl := range file.Decls { - addImports(imports, decl, pkgPath) - - if scanControllers { - // Match and add both structs and methods - structSpecs = appendStruct(structSpecs, pkgImportPath, pkg, decl, imports, fset) - appendAction(fset, methodSpecs, decl, pkgImportPath, pkg.Name, imports) - } else if scanTests { - structSpecs = appendStruct(structSpecs, pkgImportPath, pkg, decl, imports, fset) - } - - // If this is a func... - if funcDecl, ok := decl.(*ast.FuncDecl); ok { - // Scan it for validation calls - lineKeys := getValidationKeys(fset, funcDecl, imports) - if len(lineKeys) > 0 { - validationKeys[pkgImportPath+"."+getFuncName(funcDecl)] = lineKeys - } - - // Check if it's an init function. - if funcDecl.Name.Name == "init" { - initImportPaths = []string{pkgImportPath} - } - } - } - } - - // Add the method specs to the struct specs. - for _, spec := range structSpecs { - spec.MethodSpecs = methodSpecs[spec.StructName] - } - - return &SourceInfo{ - StructSpecs: structSpecs, - ValidationKeys: validationKeys, - InitImportPaths: initImportPaths, - } -} - // getFuncName returns a name for this func or method declaration. // e.g. "(*Application).SayHello" for a method, "SayHello" for a func. func getFuncName(funcDecl *ast.FuncDecl) string { @@ -271,51 +223,6 @@ func getFuncName(funcDecl *ast.FuncDecl) string { return prefix + funcDecl.Name.Name } -func addImports(imports map[string]string, decl ast.Decl, srcDir string) { - genDecl, ok := decl.(*ast.GenDecl) - if !ok { - return - } - - if genDecl.Tok != token.IMPORT { - return - } - - for _, spec := range genDecl.Specs { - importSpec := spec.(*ast.ImportSpec) - var pkgAlias string - if importSpec.Name != nil { - pkgAlias = importSpec.Name.Name - if pkgAlias == "_" { - continue - } - } - quotedPath := importSpec.Path.Value // e.g. "\"sample/app/models\"" - fullPath := quotedPath[1 : len(quotedPath)-1] // Remove the quotes - - // If the package was not aliased (common case), we have to import it - // to see what the package name is. - // TODO: Can improve performance here a lot: - // 1. Do not import everything over and over again. Keep a cache. - // 2. Exempt the standard library; their directories always match the package name. - // 3. Can use build.FindOnly and then use parser.ParseDir with mode PackageClauseOnly - if pkgAlias == "" { - pkg, err := build.Import(fullPath, srcDir, 0) - if err != nil { - // We expect this to happen for apps using reverse routing (since we - // have not yet generated the routes). Don't log that. - if !strings.HasSuffix(fullPath, "/app/routes") { - revel.TRACE.Println("Could not find import:", fullPath) - } - continue - } - pkgAlias = pkg.Name - } - - imports[pkgAlias] = fullPath - } -} - // If this Decl is a struct type definition, it is summarized and added to specs. // Else, specs is returned unchanged. func appendStruct(specs []*TypeInfo, pkgImportPath string, pkg *ast.Package, decl ast.Decl, imports map[string]string, fset *token.FileSet) []*TypeInfo { diff --git a/revel/test.go b/revel/test.go index e7135a2d..a3f90542 100644 --- a/revel/test.go +++ b/revel/test.go @@ -13,6 +13,7 @@ import ( "os" "path/filepath" "strings" + "sync" "time" "github.com/revel/cmd/harness" @@ -226,8 +227,11 @@ func getTestsList(baseURL string) (*[]controllers.TestSuiteDesc, error) { break } } - if i < 3 { - time.Sleep(3 * time.Second) + if resp.Body != nil { + resp.Body.Close() + } + if i < 20 { + time.Sleep(time.Millisecond * 500) continue } if err != nil { @@ -257,6 +261,84 @@ func runTestSuites(baseURL, resultPath string, testSuites *[]controllers.TestSui errorf("Failed to load suite result template: %s", err) } + failedResults := make(chan controllers.TestSuiteResult, len(*testSuites)) + + var wg sync.WaitGroup + // TODO make this configurable and default to 1 + parallelism := make(chan struct{}, 50) + for _, s := range *testSuites { + wg.Add(1) + parallelism <- struct{}{} + go func(suite controllers.TestSuiteDesc) { + defer wg.Done() + defer func() { <-parallelism }() + // Print the name of the suite we're running. + name := suite.Name + if len(name) > 22 { + name = name[:19] + "..." + } + fmt.Printf("%-22s", name) + + // Run every test. + startTime := time.Now() + suiteResult := controllers.TestSuiteResult{Name: suite.Name, Passed: true} + for _, test := range suite.Tests { + testURL := baseURL + "/@tests/" + suite.Name + "/" + test.Name + resp, err := http.Get(testURL) + if err != nil { + errorf("Failed to fetch test result at url %s: %s", testURL, err) + } + + var testResult controllers.TestResult + err = json.NewDecoder(resp.Body).Decode(&testResult) + resp.Body.Close() + if err == nil && !testResult.Passed { + suiteResult.Passed = false + } + suiteResult.Results = append(suiteResult.Results, testResult) + } + + suiteResultStr, suiteAlert := "PASSED", "" + if !suiteResult.Passed { + suiteResultStr, suiteAlert = "FAILED", "!" + failedResults <- suiteResult + } + + // Create the result HTML file. + suiteResultFilename := filepath.Join(resultPath, fmt.Sprintf("%s.%s.html", suite.Name, strings.ToLower(suiteResultStr))) + suiteResultFile, err := os.Create(suiteResultFilename) + if err != nil { + errorf("Failed to create result file %s: %s", suiteResultFilename, err) + } + if err = resultTemplate.Render(suiteResultFile, suiteResult); err != nil { + errorf("Failed to render result template: %s", err) + } + + // Print result. (Just PASSED or FAILED, and the time taken) + fmt.Printf("%8s%3s%6ds\n", suiteResultStr, suiteAlert, int(time.Since(startTime).Seconds())) + }(s) + } + wg.Wait() + close(failedResults) + failedResultsSlice := make([]controllers.TestSuiteResult, len(failedResults)) + for fr := range failedResults { + failedResultsSlice = append(failedResultsSlice, fr) + } + return &failedResultsSlice, len(failedResultsSlice) == 0 +} + +func runTestSuitesSerial(baseURL, resultPath string, testSuites *[]controllers.TestSuiteDesc) (*[]controllers.TestSuiteResult, bool) { + // Load the result template, which we execute for each suite. + module, _ := revel.ModuleByName("testrunner") + TemplateLoader := revel.NewTemplateLoader([]string{filepath.Join(module.Path, "app", "views")}) + if err := TemplateLoader.Refresh(); err != nil { + errorf("Failed to compile templates: %s", err) + } + resultTemplate, err := TemplateLoader.Template("TestRunner/SuiteResult.html") + if err != nil { + errorf("Failed to load suite result template: %s", err) + } + var ( overallSuccess = true failedResults []controllers.TestSuiteResult