123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- // 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"
- "regexp"
- "runtime"
- "strconv"
- "strings"
- "time"
- "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(¶llelPackages, "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(parallelPackages)
- 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",
- "windows": "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)
- packageName := resolvePackageName(folder.Path)
- packageList = append(
- packageList,
- contract.NewPackage(folder, packageName, hasImportCycle),
- )
- }
- return packageList
- }
- func extractRoot(folderList messaging.Folders, packageList []*contract.Package) string {
- path := packageList[0].Path
- folder := folderList[path]
- return folder.Root
- }
- 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 &&
- goMinVersion(1, 2) &&
- coverToolInstalled() &&
- ensureReportDirectoryExists(reports))
- }
- func goMinVersion(wanted ...int) bool {
- version := runtime.Version() // 'go1.2....'
- s := regexp.MustCompile(`go([\d]+)\.([\d]+)\.?([\d]+)?`).FindAllStringSubmatch(version, 1)
- if len(s) == 0 {
- log.Printf("Cannot determine if newer than go1.2, disabling coverage.")
- return false
- }
- for idx, str := range s[0][1:] {
- if len(wanted) == idx {
- break
- }
- if v, _ := strconv.Atoi(str); v < wanted[idx] {
- 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
- parallelPackages 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"
- separator = string(filepath.Separator)
- endGoPath = separator + "src" + separator
- )
|