2017-07-12 22:46:08 +00:00
// 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." )
2017-08-31 16:37:43 +00:00
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." )
2017-07-12 22:46:08 +00:00
flag . StringVar ( & gobin , "gobin" , "go" , "The path to the 'go' binary (default: search on the PATH)." )
2017-08-31 16:37:43 +00:00
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." )
2017-07-12 22:46:08 +00:00
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)." )
2017-08-31 16:37:43 +00:00
flag . StringVar ( & watchedSuffixes , "watchedSuffixes" , ".go" , "A comma separated list of file suffixes to watch for modifications." )
2017-07-12 22:46:08 +00:00
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"
)