| // This executable provides an HTTP server that watches for file system changes |
| // to .go files within the working directory (and all nested go packages). |
| // Navigating to the configured host and port in a web browser will display the |
| // latest results of running `go test` in each go package. |
| package main |
| |
| import ( |
| "flag" |
| "fmt" |
| "log" |
| "net" |
| "net/http" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "runtime" |
| "strings" |
| "time" |
| |
| "go/build" |
| |
| "github.com/smartystreets/goconvey/web/server/api" |
| "github.com/smartystreets/goconvey/web/server/contract" |
| "github.com/smartystreets/goconvey/web/server/executor" |
| "github.com/smartystreets/goconvey/web/server/messaging" |
| "github.com/smartystreets/goconvey/web/server/parser" |
| "github.com/smartystreets/goconvey/web/server/system" |
| "github.com/smartystreets/goconvey/web/server/watch" |
| ) |
| |
| func init() { |
| flags() |
| folders() |
| } |
| func flags() { |
| flag.IntVar(&port, "port", 8080, "The port at which to serve http.") |
| flag.StringVar(&host, "host", "127.0.0.1", "The host at which to serve http.") |
| flag.DurationVar(&nap, "poll", quarterSecond, "The interval to wait between polling the file system for changes.") |
| flag.IntVar(&packages, "packages", 10, "The number of packages to test in parallel. Higher == faster but more costly in terms of computing.") |
| flag.StringVar(&gobin, "gobin", "go", "The path to the 'go' binary (default: search on the PATH).") |
| flag.BoolVar(&cover, "cover", true, "Enable package-level coverage statistics. Requires Go 1.2+ and the go cover tool.") |
| flag.IntVar(&depth, "depth", -1, "The directory scanning depth. If -1, scan infinitely deep directory structures. 0: scan working directory. 1+: Scan into nested directories, limited to value.") |
| flag.StringVar(&timeout, "timeout", "0", "The test execution timeout if none is specified in the *.goconvey file (default is '0', which is the same as not providing this option).") |
| flag.StringVar(&watchedSuffixes, "watchedSuffixes", ".go", "A comma separated list of file suffixes to watch for modifications.") |
| flag.StringVar(&excludedDirs, "excludedDirs", "vendor,node_modules", "A comma separated list of directories that will be excluded from being watched") |
| flag.StringVar(&workDir, "workDir", "", "set goconvey working directory (default current directory)") |
| flag.BoolVar(&autoLaunchBrowser, "launchBrowser", true, "toggle auto launching of browser (default: true)") |
| |
| log.SetOutput(os.Stdout) |
| log.SetFlags(log.LstdFlags | log.Lshortfile) |
| } |
| func folders() { |
| _, file, _, _ := runtime.Caller(0) |
| here := filepath.Dir(file) |
| static = filepath.Join(here, "/web/client") |
| reports = filepath.Join(static, "reports") |
| } |
| |
| func main() { |
| flag.Parse() |
| log.Printf(initialConfiguration, host, port, nap, cover) |
| |
| working := getWorkDir() |
| cover = coverageEnabled(cover, reports) |
| shell := system.NewShell(gobin, reports, cover, timeout) |
| |
| watcherInput := make(chan messaging.WatcherCommand) |
| watcherOutput := make(chan messaging.Folders) |
| excludedDirItems := strings.Split(excludedDirs, `,`) |
| watcher := watch.NewWatcher(working, depth, nap, watcherInput, watcherOutput, watchedSuffixes, excludedDirItems) |
| |
| parser := parser.NewParser(parser.ParsePackageResults) |
| tester := executor.NewConcurrentTester(shell) |
| tester.SetBatchSize(packages) |
| |
| longpollChan := make(chan chan string) |
| executor := executor.NewExecutor(tester, parser, longpollChan) |
| server := api.NewHTTPServer(working, watcherInput, executor, longpollChan) |
| listener := createListener() |
| go runTestOnUpdates(watcherOutput, executor, server) |
| go watcher.Listen() |
| if autoLaunchBrowser { |
| go launchBrowser(listener.Addr().String()) |
| } |
| serveHTTP(server, listener) |
| } |
| |
| func browserCmd() (string, bool) { |
| browser := map[string]string{ |
| "darwin": "open", |
| "linux": "xdg-open", |
| "win32": "start", |
| } |
| cmd, ok := browser[runtime.GOOS] |
| return cmd, ok |
| } |
| |
| func launchBrowser(addr string) { |
| browser, ok := browserCmd() |
| if !ok { |
| log.Printf("Skipped launching browser for this OS: %s", runtime.GOOS) |
| return |
| } |
| |
| log.Printf("Launching browser on %s", addr) |
| url := fmt.Sprintf("http://%s", addr) |
| cmd := exec.Command(browser, url) |
| |
| output, err := cmd.CombinedOutput() |
| if err != nil { |
| log.Println(err) |
| } |
| log.Println(string(output)) |
| } |
| |
| func runTestOnUpdates(queue chan messaging.Folders, executor contract.Executor, server contract.Server) { |
| for update := range queue { |
| log.Println("Received request from watcher to execute tests...") |
| packages := extractPackages(update) |
| output := executor.ExecuteTests(packages) |
| root := extractRoot(update, packages) |
| server.ReceiveUpdate(root, output) |
| } |
| } |
| |
| func extractPackages(folderList messaging.Folders) []*contract.Package { |
| packageList := []*contract.Package{} |
| for _, folder := range folderList { |
| hasImportCycle := testFilesImportTheirOwnPackage(folder.Path) |
| packageList = append(packageList, contract.NewPackage(folder, hasImportCycle)) |
| } |
| return packageList |
| } |
| |
| func extractRoot(folderList messaging.Folders, packageList []*contract.Package) string { |
| path := packageList[0].Path |
| folder := folderList[path] |
| return folder.Root |
| } |
| |
| // This method exists because of a bug in the go cover tool that |
| // causes an infinite loop when you try to run `go test -cover` |
| // on a package that has an import cycle defined in one of it's |
| // test files. Yuck. |
| func testFilesImportTheirOwnPackage(packagePath string) bool { |
| meta, err := build.ImportDir(packagePath, build.AllowBinary) |
| if err != nil { |
| return false |
| } |
| |
| for _, dependency := range meta.TestImports { |
| if dependency == meta.ImportPath { |
| return true |
| } |
| } |
| return false |
| } |
| |
| func createListener() net.Listener { |
| l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, port)) |
| if err != nil { |
| log.Println(err) |
| } |
| if l == nil { |
| os.Exit(1) |
| } |
| return l |
| } |
| |
| func serveHTTP(server contract.Server, listener net.Listener) { |
| serveStaticResources() |
| serveAjaxMethods(server) |
| activateServer(listener) |
| } |
| |
| func serveStaticResources() { |
| http.Handle("/", http.FileServer(http.Dir(static))) |
| } |
| |
| func serveAjaxMethods(server contract.Server) { |
| http.HandleFunc("/watch", server.Watch) |
| http.HandleFunc("/ignore", server.Ignore) |
| http.HandleFunc("/reinstate", server.Reinstate) |
| http.HandleFunc("/latest", server.Results) |
| http.HandleFunc("/execute", server.Execute) |
| http.HandleFunc("/status", server.Status) |
| http.HandleFunc("/status/poll", server.LongPollStatus) |
| http.HandleFunc("/pause", server.TogglePause) |
| } |
| |
| func activateServer(listener net.Listener) { |
| log.Printf("Serving HTTP at: http://%s\n", listener.Addr()) |
| err := http.Serve(listener, nil) |
| if err != nil { |
| log.Println(err) |
| } |
| } |
| |
| func coverageEnabled(cover bool, reports string) bool { |
| return (cover && |
| goVersion_1_2_orGreater() && |
| coverToolInstalled() && |
| ensureReportDirectoryExists(reports)) |
| } |
| func goVersion_1_2_orGreater() bool { |
| version := runtime.Version() // 'go1.2....' |
| major, minor := version[2], version[4] |
| version_1_2 := major >= byte('1') && minor >= byte('2') |
| if !version_1_2 { |
| log.Printf(pleaseUpgradeGoVersion, version) |
| return false |
| } |
| return true |
| } |
| func coverToolInstalled() bool { |
| working := getWorkDir() |
| command := system.NewCommand(working, "go", "tool", "cover").Execute() |
| installed := strings.Contains(command.Output, "Usage of 'go tool cover':") |
| if !installed { |
| log.Print(coverToolMissing) |
| return false |
| } |
| return true |
| } |
| func ensureReportDirectoryExists(reports string) bool { |
| result, err := exists(reports) |
| if err != nil { |
| log.Fatal(err) |
| } |
| if result { |
| return true |
| } |
| |
| if err := os.Mkdir(reports, 0755); err == nil { |
| return true |
| } |
| |
| log.Printf(reportDirectoryUnavailable, reports) |
| return false |
| } |
| func exists(path string) (bool, error) { |
| _, err := os.Stat(path) |
| if err == nil { |
| return true, nil |
| } |
| if os.IsNotExist(err) { |
| return false, nil |
| } |
| return false, err |
| } |
| func getWorkDir() string { |
| working := "" |
| var err error |
| if workDir != "" { |
| working = workDir |
| } else { |
| working, err = os.Getwd() |
| if err != nil { |
| log.Fatal(err) |
| } |
| } |
| result, err := exists(working) |
| if err != nil { |
| log.Fatal(err) |
| } |
| if !result { |
| log.Fatalf("Path:%s does not exists", working) |
| } |
| return working |
| } |
| |
| var ( |
| port int |
| host string |
| gobin string |
| nap time.Duration |
| packages int |
| cover bool |
| depth int |
| timeout string |
| watchedSuffixes string |
| excludedDirs string |
| autoLaunchBrowser bool |
| |
| static string |
| reports string |
| |
| quarterSecond = time.Millisecond * 250 |
| workDir string |
| ) |
| |
| const ( |
| initialConfiguration = "Initial configuration: [host: %s] [port: %d] [poll: %v] [cover: %v]\n" |
| pleaseUpgradeGoVersion = "Go version is less that 1.2 (%s), please upgrade to the latest stable version to enable coverage reporting.\n" |
| coverToolMissing = "Go cover tool is not installed or not accessible: for Go < 1.5 run`go get golang.org/x/tools/cmd/cover`\n For >= Go 1.5 run `go install $GOROOT/src/cmd/cover`\n" |
| reportDirectoryUnavailable = "Could not find or create the coverage report directory (at: '%s'). You probably won't see any coverage statistics...\n" |
| ) |