package system import ( "log" "os/exec" "path/filepath" "regexp" "strings" ) /////////////////////////////////////////////////////////////////////////////// // Integration: /////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// type Shell struct { coverage bool gobin string reportsPath string defaultTimeout string } func NewShell(gobin, reportsPath string, coverage bool, defaultTimeout string) *Shell { return &Shell{ coverage: coverage, gobin: gobin, reportsPath: reportsPath, defaultTimeout: defaultTimeout, } } func (self *Shell) GoTest(directory, packageName string, tags, arguments []string) (output string, err error) { reportFilename := strings.Replace(packageName, "/", "-", -1) reportPath := filepath.Join(self.reportsPath, reportFilename) reportData := reportPath + ".txt" reportHTML := reportPath + ".html" tagsArg := "-tags=" + strings.Join(tags, ",") goconvey := findGoConvey(directory, self.gobin, packageName, tagsArg).Execute() compilation := compile(directory, self.gobin, tagsArg).Execute() withCoverage := runWithCoverage(compilation, goconvey, self.coverage, reportData, directory, self.gobin, self.defaultTimeout, tagsArg, arguments).Execute() final := runWithoutCoverage(compilation, withCoverage, goconvey, directory, self.gobin, self.defaultTimeout, tagsArg, arguments).Execute() go generateReports(final, self.coverage, directory, self.gobin, reportData, reportHTML).Execute() return final.Output, final.Error } /////////////////////////////////////////////////////////////////////////////// // Functional Core://////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// func findGoConvey(directory, gobin, packageName, tagsArg string) Command { return NewCommand(directory, gobin, "list", "-f", "'{{.TestImports}}{{.XTestImports}}'", tagsArg, packageName) } func compile(directory, gobin, tagsArg string) Command { return NewCommand(directory, gobin, "test", "-i", tagsArg) } func runWithCoverage(compile, goconvey Command, coverage bool, reportPath, directory, gobin, defaultTimeout, tagsArg string, customArguments []string) Command { if compile.Error != nil || goconvey.Error != nil { return compile } if !coverage { return compile } arguments := []string{"test", "-v", "-coverprofile=" + reportPath, tagsArg} customArgsText := strings.Join(customArguments, "\t") if !strings.Contains(customArgsText, "-covermode=") { arguments = append(arguments, "-covermode=set") } if !strings.Contains(customArgsText, "-timeout=") { arguments = append(arguments, "-timeout="+defaultTimeout) } if strings.Contains(goconvey.Output, goconveyDSLImport) { arguments = append(arguments, "-convey-json") } arguments = append(arguments, customArguments...) return NewCommand(directory, gobin, arguments...) } func runWithoutCoverage(compile, withCoverage, goconvey Command, directory, gobin, defaultTimeout, tagsArg string, customArguments []string) Command { if compile.Error != nil { return compile } if goconvey.Error != nil { log.Println(gopathProblem, goconvey.Output, goconvey.Error) return goconvey } if coverageStatementRE.MatchString(withCoverage.Output) { return withCoverage } log.Printf("Coverage output: %v", withCoverage.Output) log.Print("Run without coverage") arguments := []string{"test", "-v", tagsArg} customArgsText := strings.Join(customArguments, "\t") if !strings.Contains(customArgsText, "-timeout=") { arguments = append(arguments, "-timeout="+defaultTimeout) } if strings.Contains(goconvey.Output, goconveyDSLImport) { arguments = append(arguments, "-convey-json") } arguments = append(arguments, customArguments...) return NewCommand(directory, gobin, arguments...) } func generateReports(previous Command, coverage bool, directory, gobin, reportData, reportHTML string) Command { if previous.Error != nil { return previous } if !coverage { return previous } return NewCommand(directory, gobin, "tool", "cover", "-html="+reportData, "-o", reportHTML) } /////////////////////////////////////////////////////////////////////////////// // Imperative Shell: ////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// type Command struct { directory string executable string arguments []string Output string Error error } func NewCommand(directory, executable string, arguments ...string) Command { return Command{ directory: directory, executable: executable, arguments: arguments, } } func (this Command) Execute() Command { if len(this.executable) == 0 { return this } if len(this.Output) > 0 || this.Error != nil { return this } command := exec.Command(this.executable, this.arguments...) command.Dir = this.directory var rawOutput []byte rawOutput, this.Error = command.CombinedOutput() this.Output = string(rawOutput) return this } /////////////////////////////////////////////////////////////////////////////// const goconveyDSLImport = "github.com/smartystreets/goconvey/convey " // note the trailing space: we don't want to target packages nested in the /convey package. const gopathProblem = "Please run goconvey from within $GOPATH/src (also, symlinks might be problematic). Output and Error: " var coverageStatementRE = regexp.MustCompile(`(?m)^coverage: \d+\.\d% of statements(.*)$|^panic: test timed out after `)