diff --git a/harness/app.go b/harness/app.go new file mode 100644 index 0000000..a4ec244 --- /dev/null +++ b/harness/app.go @@ -0,0 +1,118 @@ +package harness + +import ( + "bytes" + "errors" + "fmt" + "github.com/revel/revel" + "io" + "os" + "os/exec" + "time" +) + +// App contains the configuration for running a Revel app. (Not for the app itself) +// Its only purpose is constructing the command to execute. +type App struct { + BinaryPath string // Path to the app executable + Port int // Port to pass as a command line argument. + cmd AppCmd // The last cmd returned. +} + +func NewApp(binPath string) *App { + return &App{BinaryPath: binPath} +} + +// Return a command to run the app server using the current configuration. +func (a *App) Cmd() AppCmd { + a.cmd = NewAppCmd(a.BinaryPath, a.Port) + return a.cmd +} + +// Kill the last app command returned. +func (a *App) Kill() { + a.cmd.Kill() +} + +// AppCmd manages the running of a Revel app server. +// It requires revel.Init to have been called previously. +type AppCmd struct { + *exec.Cmd +} + +func NewAppCmd(binPath string, port int) AppCmd { + cmd := exec.Command(binPath, + fmt.Sprintf("-port=%d", port), + fmt.Sprintf("-importPath=%s", revel.ImportPath), + fmt.Sprintf("-runMode=%s", revel.RunMode)) + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + return AppCmd{cmd} +} + +// Start the app server, and wait until it is ready to serve requests. +func (cmd AppCmd) Start() error { + listeningWriter := startupListeningWriter{os.Stdout, make(chan bool)} + cmd.Stdout = listeningWriter + revel.TRACE.Println("Exec app:", cmd.Path, cmd.Args) + if err := cmd.Cmd.Start(); err != nil { + revel.ERROR.Fatalln("Error running:", err) + } + + select { + case <-cmd.waitChan(): + return errors.New("revel/harness: app died") + + case <-time.After(30 * time.Second): + cmd.Kill() + return errors.New("revel/harness: app timed out") + + case <-listeningWriter.notifyReady: + return nil + } + panic("Impossible") +} + +// Run the app server inline. Never returns. +func (cmd AppCmd) Run() { + revel.TRACE.Println("Exec app:", cmd.Path, cmd.Args) + if err := cmd.Cmd.Run(); err != nil { + revel.ERROR.Fatalln("Error running:", err) + } +} + +// Terminate the app server if it's running. +func (cmd AppCmd) Kill() { + if cmd.Cmd != nil && (cmd.ProcessState == nil || !cmd.ProcessState.Exited()) { + revel.TRACE.Println("Killing revel server pid", cmd.Process.Pid) + err := cmd.Process.Kill() + if err != nil { + revel.ERROR.Fatalln("Failed to kill revel server:", err) + } + } +} + +// Return a channel that is notified when Wait() returns. +func (cmd AppCmd) waitChan() <-chan struct{} { + ch := make(chan struct{}, 1) + go func() { + cmd.Wait() + ch <- struct{}{} + }() + return ch +} + +// A io.Writer that copies to the destination, and listens for "Listening on.." +// in the stream. (Which tells us when the revel server has finished starting up) +// This is super ghetto, but by far the simplest thing that should work. +type startupListeningWriter struct { + dest io.Writer + notifyReady chan bool +} + +func (w startupListeningWriter) Write(p []byte) (n int, err error) { + if w.notifyReady != nil && bytes.Contains(p, []byte("Listening")) { + w.notifyReady <- true + w.notifyReady = nil + } + return w.dest.Write(p) +} diff --git a/harness/build.go b/harness/build.go new file mode 100755 index 0000000..f89a28a --- /dev/null +++ b/harness/build.go @@ -0,0 +1,390 @@ +package harness + +import ( + "fmt" + "go/build" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + "text/template" + + "github.com/revel/revel" +) + +var importErrorPattern = regexp.MustCompile("cannot find package \"([^\"]+)\"") + +// Build the app: +// 1. Generate the the main.go file. +// 2. Run the appropriate "go build" command. +// Requires that revel.Init has been called previously. +// Returns the path to the built binary, and an error if there was a problem building it. +func Build(buildFlags ...string) (app *App, compileError *revel.Error) { + // First, clear the generated files (to avoid them messing with ProcessSource). + cleanSource("tmp", "routes") + + sourceInfo, compileError := ProcessSource(revel.CodePaths) + if compileError != nil { + return nil, compileError + } + + // Add the db.import to the import paths. + if dbImportPath, found := revel.Config.String("db.import"); found { + sourceInfo.InitImportPaths = append(sourceInfo.InitImportPaths, dbImportPath) + } + + // Generate two source files. + templateArgs := map[string]interface{}{ + "Controllers": sourceInfo.ControllerSpecs(), + "ValidationKeys": sourceInfo.ValidationKeys, + "ImportPaths": calcImportAliases(sourceInfo), + "TestSuites": sourceInfo.TestSuites(), + } + genSource("tmp", "main.go", MAIN, templateArgs) + genSource("routes", "routes.go", ROUTES, templateArgs) + + // Read build config. + buildTags := revel.Config.StringDefault("build.tags", "") + + // Build the user program (all code under app). + // It relies on the user having "go" installed. + goPath, err := exec.LookPath("go") + if err != nil { + revel.ERROR.Fatalf("Go executable not found in PATH.") + } + + pkg, err := build.Default.Import(revel.ImportPath, "", build.FindOnly) + if err != nil { + revel.ERROR.Fatalln("Failure importing", revel.ImportPath) + } + binName := path.Join(pkg.BinDir, path.Base(revel.BasePath)) + + // Change binary path for Windows build + goos := runtime.GOOS + if goosEnv := os.Getenv("GOOS"); goosEnv != "" { + goos = goosEnv + } + if goos == "windows" { + binName += ".exe" + } + + gotten := make(map[string]struct{}) + for { + appVersion := getAppVersion() + versionLinkerFlags := fmt.Sprintf("-X %s/app.APP_VERSION \"%s\"", revel.ImportPath, appVersion) + flags := []string{ + "build", + "-ldflags", versionLinkerFlags, + "-tags", buildTags, + "-o", binName} + + // Add in build flags + flags = append(flags, buildFlags...) + + // The main path + flags = append(flags, path.Join(revel.ImportPath, "app", "tmp")) + + buildCmd := exec.Command(goPath, flags...) + revel.TRACE.Println("Exec:", buildCmd.Args) + output, err := buildCmd.CombinedOutput() + + // If the build succeeded, we're done. + if err == nil { + return NewApp(binName), nil + } + revel.ERROR.Println(string(output)) + + // See if it was an import error that we can go get. + matches := importErrorPattern.FindStringSubmatch(string(output)) + if matches == nil { + return nil, newCompileError(output) + } + + // Ensure we haven't already tried to go get it. + pkgName := matches[1] + if _, alreadyTried := gotten[pkgName]; alreadyTried { + return nil, newCompileError(output) + } + gotten[pkgName] = struct{}{} + + // Execute "go get " + getCmd := exec.Command(goPath, "get", pkgName) + revel.TRACE.Println("Exec:", getCmd.Args) + getOutput, err := getCmd.CombinedOutput() + if err != nil { + revel.ERROR.Println(string(getOutput)) + return nil, newCompileError(output) + } + + // Success getting the import, attempt to build again. + } + revel.ERROR.Fatalf("Not reachable") + return nil, nil +} + +// Try to define a version string for the compiled app +// The following is tried (first match returns): +// - Read a version explicitly specified in the APP_VERSION environment +// variable +// - Read the output of "git describe" if the source is in a git repository +// If no version can be determined, an empty string is returned. +func getAppVersion() string { + if version := os.Getenv("APP_VERSION"); version != "" { + return version + } + + // Check for the git binary + if gitPath, err := exec.LookPath("git"); err == nil { + // Check for the .git directory + gitDir := path.Join(revel.BasePath, ".git") + info, err := os.Stat(gitDir) + if (err != nil && os.IsNotExist(err)) || !info.IsDir() { + return "" + } + gitCmd := exec.Command(gitPath, "--git-dir="+gitDir, "describe", "--always", "--dirty") + revel.TRACE.Println("Exec:", gitCmd.Args) + output, err := gitCmd.Output() + + if err != nil { + revel.WARN.Println("Cannot determine git repository version:", err) + return "" + } + + return "git-" + strings.TrimSpace(string(output)) + } + + return "" +} + +func cleanSource(dirs ...string) { + for _, dir := range dirs { + tmpPath := path.Join(revel.AppPath, dir) + err := os.RemoveAll(tmpPath) + if err != nil { + revel.ERROR.Println("Failed to remove dir:", err) + } + } +} + +// genSource renders the given template to produce source code, which it writes +// to the given directory and file. +func genSource(dir, filename, templateSource string, args map[string]interface{}) { + sourceCode := revel.ExecuteTemplate( + template.Must(template.New("").Parse(templateSource)), + args) + + // Create a fresh dir. + tmpPath := path.Join(revel.AppPath, dir) + err := os.RemoveAll(tmpPath) + if err != nil { + revel.ERROR.Println("Failed to remove dir:", err) + } + err = os.Mkdir(tmpPath, 0777) + if err != nil { + revel.ERROR.Fatalf("Failed to make tmp directory: %v", err) + } + + // Create the file + file, err := os.Create(path.Join(tmpPath, filename)) + defer file.Close() + if err != nil { + revel.ERROR.Fatalf("Failed to create file: %v", err) + } + _, err = file.WriteString(sourceCode) + if err != nil { + revel.ERROR.Fatalf("Failed to write to file: %v", err) + } +} + +// Looks through all the method args and returns a set of unique import paths +// that cover all the method arg types. +// Additionally, assign package aliases when necessary to resolve ambiguity. +func calcImportAliases(src *SourceInfo) map[string]string { + aliases := make(map[string]string) + typeArrays := [][]*TypeInfo{src.ControllerSpecs(), src.TestSuites()} + for _, specs := range typeArrays { + for _, spec := range specs { + addAlias(aliases, spec.ImportPath, spec.PackageName) + + for _, methSpec := range spec.MethodSpecs { + for _, methArg := range methSpec.Args { + if methArg.ImportPath == "" { + continue + } + + addAlias(aliases, methArg.ImportPath, methArg.TypeExpr.PkgName) + } + } + } + } + + // Add the "InitImportPaths", with alias "_" + for _, importPath := range src.InitImportPaths { + if _, ok := aliases[importPath]; !ok { + aliases[importPath] = "_" + } + } + + return aliases +} + +func addAlias(aliases map[string]string, importPath, pkgName string) { + alias, ok := aliases[importPath] + if ok { + return + } + alias = makePackageAlias(aliases, pkgName) + aliases[importPath] = alias +} + +func makePackageAlias(aliases map[string]string, pkgName string) string { + i := 0 + alias := pkgName + for containsValue(aliases, alias) { + alias = fmt.Sprintf("%s%d", pkgName, i) + i++ + } + return alias +} + +func containsValue(m map[string]string, val string) bool { + for _, v := range m { + if v == val { + return true + } + } + return false +} + +// Parse the output of the "go build" command. +// Return a detailed Error. +func newCompileError(output []byte) *revel.Error { + errorMatch := regexp.MustCompile(`(?m)^([^:#]+):(\d+):(\d+:)? (.*)$`). + FindSubmatch(output) + if errorMatch == nil { + errorMatch = regexp.MustCompile(`(?m)^(.*?)\:(\d+)\:\s(.*?)$`).FindSubmatch(output) + + if errorMatch == nil { + revel.ERROR.Println("Failed to parse build errors:\n", string(output)) + return &revel.Error{ + SourceType: "Go code", + Title: "Go Compilation Error", + Description: "See console for build error.", + } + } + + errorMatch = append(errorMatch, errorMatch[3]) + + revel.ERROR.Println("Build errors:\n", string(output)) + } + + // Read the source for the offending file. + var ( + relFilename = string(errorMatch[1]) // e.g. "src/revel/sample/app/controllers/app.go" + absFilename, _ = filepath.Abs(relFilename) + line, _ = strconv.Atoi(string(errorMatch[2])) + description = string(errorMatch[4]) + compileError = &revel.Error{ + SourceType: "Go code", + Title: "Go Compilation Error", + Path: relFilename, + Description: description, + Line: line, + } + ) + + errorLink := revel.Config.StringDefault("error.link", "") + + if errorLink != "" { + compileError.SetLink(errorLink) + } + + fileStr, err := revel.ReadLines(absFilename) + if err != nil { + compileError.MetaError = absFilename + ": " + err.Error() + revel.ERROR.Println(compileError.MetaError) + return compileError + } + + compileError.SourceLines = fileStr + return compileError +} + +const MAIN = `// GENERATED CODE - DO NOT EDIT +package main + +import ( + "flag" + "reflect" + "github.com/revel/revel"{{range $k, $v := $.ImportPaths}} + {{$v}} "{{$k}}"{{end}} +) + +var ( + runMode *string = flag.String("runMode", "", "Run mode.") + port *int = flag.Int("port", 0, "By default, read from app.conf") + importPath *string = flag.String("importPath", "", "Go Import Path for the app.") + srcPath *string = flag.String("srcPath", "", "Path to the source root.") + + // So compiler won't complain if the generated code doesn't reference reflect package... + _ = reflect.Invalid +) + +func main() { + flag.Parse() + revel.Init(*runMode, *importPath, *srcPath) + revel.INFO.Println("Running revel server") + {{range $i, $c := .Controllers}} + revel.RegisterController((*{{index $.ImportPaths .ImportPath}}.{{.StructName}})(nil), + []*revel.MethodType{ + {{range .MethodSpecs}}&revel.MethodType{ + Name: "{{.Name}}", + Args: []*revel.MethodArg{ {{range .Args}} + &revel.MethodArg{Name: "{{.Name}}", Type: reflect.TypeOf((*{{index $.ImportPaths .ImportPath | .TypeExpr.TypeName}})(nil)) },{{end}} + }, + RenderArgNames: map[int][]string{ {{range .RenderCalls}} + {{.Line}}: []string{ {{range .Names}} + "{{.}}",{{end}} + },{{end}} + }, + }, + {{end}} + }) + {{end}} + revel.DefaultValidationKeys = map[string]map[int]string{ {{range $path, $lines := .ValidationKeys}} + "{{$path}}": { {{range $line, $key := $lines}} + {{$line}}: "{{$key}}",{{end}} + },{{end}} + } + revel.TestSuites = []interface{}{ {{range .TestSuites}} + (*{{index $.ImportPaths .ImportPath}}.{{.StructName}})(nil),{{end}} + } + + revel.Run(*port) +} +` +const ROUTES = `// GENERATED CODE - DO NOT EDIT +package routes + +import "github.com/revel/revel" + +{{range $i, $c := .Controllers}} +type t{{.StructName}} struct {} +var {{.StructName}} t{{.StructName}} + +{{range .MethodSpecs}} +func (_ t{{$c.StructName}}) {{.Name}}({{range .Args}} + {{.Name}} {{if .ImportPath}}interface{}{{else}}{{.TypeExpr.TypeName ""}}{{end}},{{end}} + ) string { + args := make(map[string]string) + {{range .Args}} + revel.Unbind(args, "{{.Name}}", {{.Name}}){{end}} + return revel.MainRouter.Reverse("{{$c.StructName}}.{{.Name}}", args).Url +} +{{end}} +{{end}} +` diff --git a/harness/harness.go b/harness/harness.go new file mode 100644 index 0000000..df9f740 --- /dev/null +++ b/harness/harness.go @@ -0,0 +1,240 @@ +// The Harness for a Revel program. +// +// It has a couple responsibilities: +// 1. Parse the user program, generating a main.go file that registers +// controller classes and starts the user's server. +// 2. Build and run the user program. Show compile errors. +// 3. Monitor the user source and re-build / restart the program when necessary. +// +// Source files are generated in the app/tmp directory. + +package harness + +import ( + "crypto/tls" + "fmt" + "github.com/revel/revel" + "go/build" + "io" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + "os/signal" + "path" + "path/filepath" + "strings" + "sync/atomic" +) + +var ( + watcher *revel.Watcher + doNotWatch = []string{"tmp", "views", "routes"} + + lastRequestHadError int32 +) + +// Harness reverse proxies requests to the application server. +// It builds / runs / rebuilds / restarts the server when code is changed. +type Harness struct { + app *App + serverHost string + port int + proxy *httputil.ReverseProxy +} + +func renderError(w http.ResponseWriter, r *http.Request, err error) { + req, resp := revel.NewRequest(r), revel.NewResponse(w) + c := revel.NewController(req, resp) + c.RenderError(err).Apply(req, resp) +} + +// ServeHTTP handles all requests. +// It checks for changes to app, rebuilds if necessary, and forwards the request. +func (hp *Harness) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Don't rebuild the app for favicon requests. + if lastRequestHadError > 0 && r.URL.Path == "/favicon.ico" { + return + } + + // Flush any change events and rebuild app if necessary. + // Render an error page if the rebuild / restart failed. + err := watcher.Notify() + if err != nil { + atomic.CompareAndSwapInt32(&lastRequestHadError, 0, 1) + renderError(w, r, err) + return + } + atomic.CompareAndSwapInt32(&lastRequestHadError, 1, 0) + + // Reverse proxy the request. + // (Need special code for websockets, courtesy of bradfitz) + if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") { + proxyWebsocket(w, r, hp.serverHost) + } else { + hp.proxy.ServeHTTP(w, r) + } +} + +// Return a reverse proxy that forwards requests to the given port. +func NewHarness() *Harness { + // Get a template loader to render errors. + // Prefer the app's views/errors directory, and fall back to the stock error pages. + revel.MainTemplateLoader = revel.NewTemplateLoader( + []string{path.Join(revel.RevelPath, "templates")}) + revel.MainTemplateLoader.Refresh() + + addr := revel.HttpAddr + port := revel.Config.IntDefault("harness.port", 0) + scheme := "http" + if revel.HttpSsl { + scheme = "https" + } + + // If the server is running on the wildcard address, use "localhost" + if addr == "" { + addr = "localhost" + } + + if port == 0 { + port = getFreePort() + } + + serverUrl, _ := url.ParseRequestURI(fmt.Sprintf(scheme+"://%s:%d", addr, port)) + + harness := &Harness{ + port: port, + serverHost: serverUrl.String()[len(scheme+"://"):], + proxy: httputil.NewSingleHostReverseProxy(serverUrl), + } + + if revel.HttpSsl { + harness.proxy.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } + return harness +} + +// Rebuild the Revel application and run it on the given port. +func (h *Harness) Refresh() (err *revel.Error) { + if h.app != nil { + h.app.Kill() + } + + revel.TRACE.Println("Rebuild") + h.app, err = Build() + if err != nil { + return + } + + h.app.Port = h.port + if err2 := h.app.Cmd().Start(); err2 != nil { + return &revel.Error{ + Title: "App failed to start up", + Description: err2.Error(), + } + } + + return +} + +func (h *Harness) WatchDir(info os.FileInfo) bool { + return !revel.ContainsString(doNotWatch, info.Name()) +} + +func (h *Harness) WatchFile(filename string) bool { + return strings.HasSuffix(filename, ".go") +} + +// Run the harness, which listens for requests and proxies them to the app +// server, which it runs and rebuilds as necessary. +func (h *Harness) Run() { + var paths []string + if revel.Config.BoolDefault("watch.gopath", false) { + gopaths := filepath.SplitList(build.Default.GOPATH) + paths = append(paths, gopaths...) + } + paths = append(paths, revel.CodePaths...) + watcher = revel.NewWatcher() + watcher.Listen(h, paths...) + + go func() { + addr := fmt.Sprintf("%s:%d", revel.HttpAddr, revel.HttpPort) + revel.INFO.Printf("Listening on %s", addr) + + var err error + if revel.HttpSsl { + err = http.ListenAndServeTLS(addr, revel.HttpSslCert, + revel.HttpSslKey, h) + } else { + err = http.ListenAndServe(addr, h) + } + if err != nil { + revel.ERROR.Fatalln("Failed to start reverse proxy:", err) + } + }() + + // Kill the app on signal. + ch := make(chan os.Signal) + signal.Notify(ch, os.Interrupt, os.Kill) + <-ch + if h.app != nil { + h.app.Kill() + } + os.Exit(1) +} + +// Find an unused port +func getFreePort() (port int) { + conn, err := net.Listen("tcp", ":0") + if err != nil { + revel.ERROR.Fatal(err) + } + + port = conn.Addr().(*net.TCPAddr).Port + err = conn.Close() + if err != nil { + revel.ERROR.Fatal(err) + } + return port +} + +// proxyWebsocket copies data between websocket client and server until one side +// closes the connection. (ReverseProxy doesn't work with websocket requests.) +func proxyWebsocket(w http.ResponseWriter, r *http.Request, host string) { + d, err := net.Dial("tcp", host) + if err != nil { + http.Error(w, "Error contacting backend server.", 500) + revel.ERROR.Printf("Error dialing websocket backend %s: %v", host, err) + return + } + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "Not a hijacker?", 500) + return + } + nc, _, err := hj.Hijack() + if err != nil { + revel.ERROR.Printf("Hijack error: %v", err) + return + } + defer nc.Close() + defer d.Close() + + err = r.Write(d) + if err != nil { + revel.ERROR.Printf("Error copying request to target: %v", err) + return + } + + errc := make(chan error, 2) + cp := func(dst io.Writer, src io.Reader) { + _, err := io.Copy(dst, src) + errc <- err + } + go cp(d, nc) + go cp(nc, d) + <-errc +} diff --git a/harness/reflect.go b/harness/reflect.go new file mode 100644 index 0000000..f64630c --- /dev/null +++ b/harness/reflect.go @@ -0,0 +1,777 @@ +package harness + +// This file handles the app code introspection. +// It catalogs the controllers, their methods, and their arguments. + +import ( + "go/ast" + "go/build" + "go/parser" + "go/scanner" + "go/token" + "log" + "os" + "path/filepath" + "strings" + + "github.com/revel/revel" +) + +// SourceInfo is the top-level struct containing all extracted information +// about the app source code, used to generate main.go. +type SourceInfo struct { + // StructSpecs lists type info for all structs found under the code paths. + // They may be queried to determine which ones (transitively) embed certain types. + StructSpecs []*TypeInfo + // ValidationKeys provides a two-level lookup. The keys are: + // 1. The fully-qualified function name, + // e.g. "github.com/revel/revel/samples/chat/app/controllers.(*Application).Action" + // 2. Within that func's file, the line number of the (overall) expression statement. + // e.g. the line returned from runtime.Caller() + // The result of the lookup the name of variable being validated. + ValidationKeys map[string]map[int]string + // A list of import paths. + // Revel notices files with an init() function and imports that package. + InitImportPaths []string + + // controllerSpecs lists type info for all structs found under + // app/controllers/... that embed (directly or indirectly) revel.Controller + controllerSpecs []*TypeInfo + // testSuites list the types that constitute the set of application tests. + testSuites []*TypeInfo +} + +// TypeInfo summarizes information about a struct type in the app source code. +type TypeInfo struct { + StructName string // e.g. "Application" + ImportPath string // e.g. "github.com/revel/revel/samples/chat/app/controllers" + PackageName string // e.g. "controllers" + MethodSpecs []*MethodSpec + + // Used internally to identify controllers that indirectly embed *revel.Controller. + embeddedTypes []*embeddedTypeName +} + +// methodCall describes a call to c.Render(..) +// It documents the argument names used, in order to propagate them to RenderArgs. +type methodCall struct { + Path string // e.g. "myapp/app/controllers.(*Application).Action" + Line int + Names []string +} + +type MethodSpec struct { + Name string // Name of the method, e.g. "Index" + Args []*MethodArg // Argument descriptors + RenderCalls []*methodCall // Descriptions of Render() invocations from this Method. +} + +type MethodArg struct { + Name string // Name of the argument. + TypeExpr TypeExpr // The name of the type, e.g. "int", "*pkg.UserType" + ImportPath string // If the arg is of an imported type, this is the import path. +} + +type embeddedTypeName struct { + ImportPath, StructName string +} + +// Maps a controller simple name (e.g. "Login") to the methods for which it is a +// receiver. +type methodMap map[string][]*MethodSpec + +// Parse the app controllers directory and return a list of the controller types found. +// Returns a CompileError if the parsing fails. +func ProcessSource(roots []string) (*SourceInfo, *revel.Error) { + var ( + srcInfo *SourceInfo + compileError *revel.Error + ) + + for _, root := range roots { + rootImportPath := importPathFromPath(root) + if rootImportPath == "" { + revel.WARN.Println("Skipping code path", root) + continue + } + + // Start walking the directory tree. + filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + log.Println("Error scanning app source:", err) + return nil + } + + if !info.IsDir() || info.Name() == "tmp" { + return nil + } + + // Get the import path of the package. + pkgImportPath := rootImportPath + if root != path { + pkgImportPath = rootImportPath + "/" + filepath.ToSlash(path[len(root)+1:]) + } + + // Parse files within the path. + var pkgs map[string]*ast.Package + fset := token.NewFileSet() + pkgs, err = parser.ParseDir(fset, path, func(f os.FileInfo) bool { + return !f.IsDir() && !strings.HasPrefix(f.Name(), ".") && strings.HasSuffix(f.Name(), ".go") + }, 0) + if err != nil { + if errList, ok := err.(scanner.ErrorList); ok { + var pos token.Position = errList[0].Pos + compileError = &revel.Error{ + SourceType: ".go source", + Title: "Go Compilation Error", + Path: pos.Filename, + Description: errList[0].Msg, + Line: pos.Line, + Column: pos.Column, + SourceLines: revel.MustReadLines(pos.Filename), + } + + errorLink := revel.Config.StringDefault("error.link", "") + + if errorLink != "" { + compileError.SetLink(errorLink) + } + + return compileError + } + ast.Print(nil, err) + log.Fatalf("Failed to parse dir: %s", err) + } + + // Skip "main" packages. + delete(pkgs, "main") + + // If there is no code in this directory, skip it. + if len(pkgs) == 0 { + return nil + } + + // There should be only one package in this directory. + if len(pkgs) > 1 { + log.Println("Most unexpected! Multiple packages in a single directory:", pkgs) + } + + var pkg *ast.Package + for _, v := range pkgs { + pkg = v + } + + srcInfo = appendSourceInfo(srcInfo, processPackage(fset, pkgImportPath, path, pkg)) + return nil + }) + } + + return srcInfo, compileError +} + +func appendSourceInfo(srcInfo1, srcInfo2 *SourceInfo) *SourceInfo { + if srcInfo1 == nil { + return srcInfo2 + } + + srcInfo1.StructSpecs = append(srcInfo1.StructSpecs, srcInfo2.StructSpecs...) + srcInfo1.InitImportPaths = append(srcInfo1.InitImportPaths, srcInfo2.InitImportPaths...) + for k, v := range srcInfo2.ValidationKeys { + if _, ok := srcInfo1.ValidationKeys[k]; ok { + log.Println("Key conflict when scanning validation calls:", k) + continue + } + srcInfo1.ValidationKeys[k] = v + } + 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 { + prefix := "" + if funcDecl.Recv != nil { + recvType := funcDecl.Recv.List[0].Type + if recvStarType, ok := recvType.(*ast.StarExpr); ok { + prefix = "(*" + recvStarType.X.(*ast.Ident).Name + ")" + } else { + prefix = recvType.(*ast.Ident).Name + } + prefix += "." + } + 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 { + // Filter out non-Struct type declarations. + spec, found := getStructTypeDecl(decl, fset) + if !found { + return specs + } + structType := spec.Type.(*ast.StructType) + + // At this point we know it's a type declaration for a struct. + // Fill in the rest of the info by diving into the fields. + // Add it provisionally to the Controller list -- it's later filtered using field info. + controllerSpec := &TypeInfo{ + StructName: spec.Name.Name, + ImportPath: pkgImportPath, + PackageName: pkg.Name, + } + + for _, field := range structType.Fields.List { + // If field.Names is set, it's not an embedded type. + if field.Names != nil { + continue + } + + // A direct "sub-type" has an ast.Field as either: + // Ident { "AppController" } + // SelectorExpr { "rev", "Controller" } + // Additionally, that can be wrapped by StarExprs. + fieldType := field.Type + pkgName, typeName := func() (string, string) { + // Drill through any StarExprs. + for { + if starExpr, ok := fieldType.(*ast.StarExpr); ok { + fieldType = starExpr.X + continue + } + break + } + + // If the embedded type is in the same package, it's an Ident. + if ident, ok := fieldType.(*ast.Ident); ok { + return "", ident.Name + } + + if selectorExpr, ok := fieldType.(*ast.SelectorExpr); ok { + if pkgIdent, ok := selectorExpr.X.(*ast.Ident); ok { + return pkgIdent.Name, selectorExpr.Sel.Name + } + } + return "", "" + }() + + // If a typename wasn't found, skip it. + if typeName == "" { + continue + } + + // Find the import path for this type. + // If it was referenced without a package name, use the current package import path. + // Else, look up the package's import path by name. + var importPath string + if pkgName == "" { + importPath = pkgImportPath + } else { + var ok bool + if importPath, ok = imports[pkgName]; !ok { + log.Print("Failed to find import path for ", pkgName, ".", typeName) + continue + } + } + + controllerSpec.embeddedTypes = append(controllerSpec.embeddedTypes, &embeddedTypeName{ + ImportPath: importPath, + StructName: typeName, + }) + } + + return append(specs, controllerSpec) +} + +// If decl is a Method declaration, it is summarized and added to the array +// underneath its receiver type. +// e.g. "Login" => {MethodSpec, MethodSpec, ..} +func appendAction(fset *token.FileSet, mm methodMap, decl ast.Decl, pkgImportPath, pkgName string, imports map[string]string) { + // Func declaration? + funcDecl, ok := decl.(*ast.FuncDecl) + if !ok { + return + } + + // Have a receiver? + if funcDecl.Recv == nil { + return + } + + // Is it public? + if !funcDecl.Name.IsExported() { + return + } + + // Does it return a Result? + if funcDecl.Type.Results == nil || len(funcDecl.Type.Results.List) != 1 { + return + } + selExpr, ok := funcDecl.Type.Results.List[0].Type.(*ast.SelectorExpr) + if !ok { + return + } + if selExpr.Sel.Name != "Result" { + return + } + if pkgIdent, ok := selExpr.X.(*ast.Ident); !ok || imports[pkgIdent.Name] != revel.REVEL_IMPORT_PATH { + return + } + + method := &MethodSpec{ + Name: funcDecl.Name.Name, + } + + // Add a description of the arguments to the method. + for _, field := range funcDecl.Type.Params.List { + for _, name := range field.Names { + var importPath string + typeExpr := NewTypeExpr(pkgName, field.Type) + if !typeExpr.Valid { + return // We didn't understand one of the args. Ignore this action. (Already logged) + } + if typeExpr.PkgName != "" { + var ok bool + if importPath, ok = imports[typeExpr.PkgName]; !ok { + log.Println("Failed to find import for arg of type:", typeExpr.TypeName("")) + } + } + method.Args = append(method.Args, &MethodArg{ + Name: name.Name, + TypeExpr: typeExpr, + ImportPath: importPath, + }) + } + } + + // Add a description of the calls to Render from the method. + // Inspect every node (e.g. always return true). + method.RenderCalls = []*methodCall{} + ast.Inspect(funcDecl.Body, func(node ast.Node) bool { + // Is it a function call? + callExpr, ok := node.(*ast.CallExpr) + if !ok { + return true + } + + // Is it calling (*Controller).Render? + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + + // The type of the receiver is not easily available, so just store every + // call to any method called Render. + if selExpr.Sel.Name != "Render" { + return true + } + + // Add this call's args to the renderArgs. + pos := fset.Position(callExpr.Rparen) + methodCall := &methodCall{ + Line: pos.Line, + Names: []string{}, + } + for _, arg := range callExpr.Args { + argIdent, ok := arg.(*ast.Ident) + if !ok { + continue + } + methodCall.Names = append(methodCall.Names, argIdent.Name) + } + method.RenderCalls = append(method.RenderCalls, methodCall) + return true + }) + + var recvTypeName string + var recvType ast.Expr = funcDecl.Recv.List[0].Type + if recvStarType, ok := recvType.(*ast.StarExpr); ok { + recvTypeName = recvStarType.X.(*ast.Ident).Name + } else { + recvTypeName = recvType.(*ast.Ident).Name + } + + mm[recvTypeName] = append(mm[recvTypeName], method) +} + +// Scan app source code for calls to X.Y(), where X is of type *Validation. +// +// Recognize these scenarios: +// - "Y" = "Validation" and is a member of the receiver. +// (The common case for inline validation) +// - "X" is passed in to the func as a parameter. +// (For structs implementing Validated) +// +// The line number to which a validation call is attributed is that of the +// surrounding ExprStmt. This is so that it matches what runtime.Callers() +// reports. +// +// The end result is that we can set the default validation key for each call to +// be the same as the local variable. +func getValidationKeys(fset *token.FileSet, funcDecl *ast.FuncDecl, imports map[string]string) map[int]string { + var ( + lineKeys = make(map[int]string) + + // Check the func parameters and the receiver's members for the *revel.Validation type. + validationParam = getValidationParameter(funcDecl, imports) + ) + + ast.Inspect(funcDecl.Body, func(node ast.Node) bool { + // e.g. c.Validation.Required(arg) or v.Required(arg) + callExpr, ok := node.(*ast.CallExpr) + if !ok { + return true + } + + // e.g. c.Validation.Required or v.Required + funcSelector, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + + switch x := funcSelector.X.(type) { + case *ast.SelectorExpr: // e.g. c.Validation + if x.Sel.Name != "Validation" { + return true + } + + case *ast.Ident: // e.g. v + if validationParam == nil || x.Obj != validationParam { + return true + } + + default: + return true + } + + if len(callExpr.Args) == 0 { + return true + } + + // Given the validation expression, extract the key. + key := callExpr.Args[0] + switch expr := key.(type) { + case *ast.BinaryExpr: + // If the argument is a binary expression, take the first expression. + // (e.g. c.Validation.Required(myName != "")) + key = expr.X + case *ast.UnaryExpr: + // If the argument is a unary expression, drill in. + // (e.g. c.Validation.Required(!myBool) + key = expr.X + case *ast.BasicLit: + // If it's a literal, skip it. + return true + } + + if typeExpr := NewTypeExpr("", key); typeExpr.Valid { + lineKeys[fset.Position(callExpr.End()).Line] = typeExpr.TypeName("") + } + return true + }) + + return lineKeys +} + +// Check to see if there is a *revel.Validation as an argument. +func getValidationParameter(funcDecl *ast.FuncDecl, imports map[string]string) *ast.Object { + for _, field := range funcDecl.Type.Params.List { + starExpr, ok := field.Type.(*ast.StarExpr) // e.g. *revel.Validation + if !ok { + continue + } + + selExpr, ok := starExpr.X.(*ast.SelectorExpr) // e.g. revel.Validation + if !ok { + continue + } + + xIdent, ok := selExpr.X.(*ast.Ident) // e.g. rev + if !ok { + continue + } + + if selExpr.Sel.Name == "Validation" && imports[xIdent.Name] == revel.REVEL_IMPORT_PATH { + return field.Names[0].Obj + } + } + return nil +} + +func (s *TypeInfo) String() string { + return s.ImportPath + "." + s.StructName +} + +func (s *embeddedTypeName) String() string { + return s.ImportPath + "." + s.StructName +} + +// getStructTypeDecl checks if the given decl is a type declaration for a +// struct. If so, the TypeSpec is returned. +func getStructTypeDecl(decl ast.Decl, fset *token.FileSet) (spec *ast.TypeSpec, found bool) { + genDecl, ok := decl.(*ast.GenDecl) + if !ok { + return + } + + if genDecl.Tok != token.TYPE { + return + } + + if len(genDecl.Specs) == 0 { + revel.WARN.Printf("Surprising: %s:%d Decl contains no specifications", fset.Position(decl.Pos()).Filename, fset.Position(decl.Pos()).Line) + return + } + + spec = genDecl.Specs[0].(*ast.TypeSpec) + _, found = spec.Type.(*ast.StructType) + + return +} + +// TypesThatEmbed returns all types that (directly or indirectly) embed the +// target type, which must be a fully qualified type name, +// e.g. "github.com/revel/revel.Controller" +func (s *SourceInfo) TypesThatEmbed(targetType string) (filtered []*TypeInfo) { + // Do a search in the "embedded type graph", starting with the target type. + var ( + nodeQueue = []string{targetType} + processed []string + ) + for len(nodeQueue) > 0 { + controllerSimpleName := nodeQueue[0] + nodeQueue = nodeQueue[1:] + processed = append(processed, controllerSimpleName) + + // Look through all known structs. + for _, spec := range s.StructSpecs { + // If this one has been processed or is already in nodeQueue, then skip it. + if revel.ContainsString(processed, spec.String()) || + revel.ContainsString(nodeQueue, spec.String()) { + continue + } + + // Look through the embedded types to see if the current type is among them. + for _, embeddedType := range spec.embeddedTypes { + + // If so, add this type's simple name to the nodeQueue, and its spec to + // the filtered list. + if controllerSimpleName == embeddedType.String() { + nodeQueue = append(nodeQueue, spec.String()) + filtered = append(filtered, spec) + break + } + } + } + } + return +} + +func (s *SourceInfo) ControllerSpecs() []*TypeInfo { + if s.controllerSpecs == nil { + s.controllerSpecs = s.TypesThatEmbed(revel.REVEL_IMPORT_PATH + ".Controller") + } + return s.controllerSpecs +} + +func (s *SourceInfo) TestSuites() []*TypeInfo { + if s.testSuites == nil { + s.testSuites = s.TypesThatEmbed(revel.REVEL_IMPORT_PATH + ".TestSuite") + } + return s.testSuites +} + +// TypeExpr provides a type name that may be rewritten to use a package name. +type TypeExpr struct { + Expr string // The unqualified type expression, e.g. "[]*MyType" + PkgName string // The default package idenifier + pkgIndex int // The index where the package identifier should be inserted. + Valid bool +} + +// TypeName returns the fully-qualified type name for this expression. +// The caller may optionally specify a package name to override the default. +func (e TypeExpr) TypeName(pkgOverride string) string { + pkgName := revel.FirstNonEmpty(pkgOverride, e.PkgName) + if pkgName == "" { + return e.Expr + } + return e.Expr[:e.pkgIndex] + pkgName + "." + e.Expr[e.pkgIndex:] +} + +// This returns the syntactic expression for referencing this type in Go. +func NewTypeExpr(pkgName string, expr ast.Expr) TypeExpr { + switch t := expr.(type) { + case *ast.Ident: + if IsBuiltinType(t.Name) { + pkgName = "" + } + return TypeExpr{t.Name, pkgName, 0, true} + case *ast.SelectorExpr: + e := NewTypeExpr(pkgName, t.X) + return TypeExpr{t.Sel.Name, e.Expr, 0, e.Valid} + case *ast.StarExpr: + e := NewTypeExpr(pkgName, t.X) + return TypeExpr{"*" + e.Expr, e.PkgName, e.pkgIndex + 1, e.Valid} + case *ast.ArrayType: + e := NewTypeExpr(pkgName, t.Elt) + return TypeExpr{"[]" + e.Expr, e.PkgName, e.pkgIndex + 2, e.Valid} + case *ast.Ellipsis: + e := NewTypeExpr(pkgName, t.Elt) + return TypeExpr{"[]" + e.Expr, e.PkgName, e.pkgIndex + 2, e.Valid} + default: + log.Println("Failed to generate name for field. Make sure the field name is valid.") + } + return TypeExpr{Valid: false} +} + +var _BUILTIN_TYPES = map[string]struct{}{ + "bool": struct{}{}, + "byte": struct{}{}, + "complex128": struct{}{}, + "complex64": struct{}{}, + "error": struct{}{}, + "float32": struct{}{}, + "float64": struct{}{}, + "int": struct{}{}, + "int16": struct{}{}, + "int32": struct{}{}, + "int64": struct{}{}, + "int8": struct{}{}, + "rune": struct{}{}, + "string": struct{}{}, + "uint": struct{}{}, + "uint16": struct{}{}, + "uint32": struct{}{}, + "uint64": struct{}{}, + "uint8": struct{}{}, + "uintptr": struct{}{}, +} + +func IsBuiltinType(name string) bool { + _, ok := _BUILTIN_TYPES[name] + return ok +} + +func importPathFromPath(root string) string { + for _, gopath := range filepath.SplitList(build.Default.GOPATH) { + srcPath := filepath.Join(gopath, "src") + if strings.HasPrefix(root, srcPath) { + return filepath.ToSlash(root[len(srcPath)+1:]) + } + } + + srcPath := filepath.Join(build.Default.GOROOT, "src", "pkg") + if strings.HasPrefix(root, srcPath) { + revel.WARN.Println("Code path should be in GOPATH, but is in GOROOT:", root) + return filepath.ToSlash(root[len(srcPath)+1:]) + } + + revel.ERROR.Println("Unexpected! Code path is not in GOPATH:", root) + return "" +} diff --git a/harness/reflect_test.go b/harness/reflect_test.go new file mode 100644 index 0000000..a704d74 --- /dev/null +++ b/harness/reflect_test.go @@ -0,0 +1,190 @@ +package harness + +import ( + "github.com/revel/revel" + "go/ast" + "go/parser" + "go/token" + "io/ioutil" + "log" + "reflect" + "strings" + "testing" +) + +const validationKeysSource = ` +package test + +func (c *Application) testFunc(a, b int, user models.User) revel.Result { + // Line 5 + c.Validation.Required(a) + c.Validation.Required(a).Message("Error message") + c.Validation.Required(a). + Message("Error message") + + // Line 11 + c.Validation.Required(user.Name) + c.Validation.Required(user.Name).Message("Error message") + + // Line 15 + c.Validation.MinSize(b, 12) + c.Validation.MinSize(b, 12).Message("Error message") + c.Validation.MinSize(b, + 12) + + // Line 21 + c.Validation.Required(b == 5) +} + +func (m Model) Validate(v *revel.Validation) { + // Line 26 + v.Required(m.name) + v.Required(m.name == "something"). + Message("Error Message") + v.Required(!m.bool) +} +` + +var expectedValidationKeys = []map[int]string{ + { + 6: "a", + 7: "a", + 8: "a", + 12: "user.Name", + 13: "user.Name", + 16: "b", + 17: "b", + 19: "b", + 22: "b", + }, { + 27: "m.name", + 28: "m.name", + 30: "m.bool", + }, +} + +// This tests the recording of line number to validation key of the preceeding +// example source. +func TestGetValidationKeys(t *testing.T) { + fset := token.NewFileSet() + + file, err := parser.ParseFile(fset, "validationKeysSource", validationKeysSource, 0) + if err != nil { + t.Fatal(err) + } + if len(file.Decls) != 2 { + t.Fatal("Expected 2 decl in the source, found", len(file.Decls)) + } + + for i, decl := range file.Decls { + lineKeys := getValidationKeys(fset, decl.(*ast.FuncDecl), map[string]string{"revel": revel.REVEL_IMPORT_PATH}) + for k, v := range expectedValidationKeys[i] { + if lineKeys[k] != v { + t.Errorf("Not found - %d: %v - Actual Map: %v", k, v, lineKeys) + } + } + + if len(lineKeys) != len(expectedValidationKeys[i]) { + t.Error("Validation key map not the same size as expected:", lineKeys) + } + } +} + +var TypeExprs = map[string]TypeExpr{ + "int": TypeExpr{"int", "", 0, true}, + "*int": TypeExpr{"*int", "", 1, true}, + "[]int": TypeExpr{"[]int", "", 2, true}, + "...int": TypeExpr{"[]int", "", 2, true}, + "[]*int": TypeExpr{"[]*int", "", 3, true}, + "...*int": TypeExpr{"[]*int", "", 3, true}, + "MyType": TypeExpr{"MyType", "pkg", 0, true}, + "*MyType": TypeExpr{"*MyType", "pkg", 1, true}, + "[]MyType": TypeExpr{"[]MyType", "pkg", 2, true}, + "...MyType": TypeExpr{"[]MyType", "pkg", 2, true}, + "[]*MyType": TypeExpr{"[]*MyType", "pkg", 3, true}, + "...*MyType": TypeExpr{"[]*MyType", "pkg", 3, true}, +} + +func TestTypeExpr(t *testing.T) { + for typeStr, expected := range TypeExprs { + // Handle arrays and ... myself, since ParseExpr() does not. + array := strings.HasPrefix(typeStr, "[]") + if array { + typeStr = typeStr[2:] + } + + ellipsis := strings.HasPrefix(typeStr, "...") + if ellipsis { + typeStr = typeStr[3:] + } + + expr, err := parser.ParseExpr(typeStr) + if err != nil { + t.Error("Failed to parse test expr:", typeStr) + continue + } + + if array { + expr = &ast.ArrayType{expr.Pos(), nil, expr} + } + if ellipsis { + expr = &ast.Ellipsis{expr.Pos(), expr} + } + + actual := NewTypeExpr("pkg", expr) + if !reflect.DeepEqual(expected, actual) { + t.Error("Fail, expected", expected, ", was", actual) + } + } +} + +func TestProcessBookingSource(t *testing.T) { + revel.Init("prod", "github.com/revel/revel/samples/booking", "") + sourceInfo, err := ProcessSource([]string{revel.AppPath}) + if err != nil { + t.Fatal("Failed to process booking source with error:", err) + } + + CONTROLLER_PKG := "github.com/revel/revel/samples/booking/app/controllers" + expectedControllerSpecs := []*TypeInfo{ + {"GorpController", CONTROLLER_PKG, "controllers", nil, nil}, + {"Application", CONTROLLER_PKG, "controllers", nil, nil}, + {"Hotels", CONTROLLER_PKG, "controllers", nil, nil}, + } + if len(sourceInfo.ControllerSpecs()) != len(expectedControllerSpecs) { + t.Errorf("Unexpected number of controllers found. Expected %d, Found %d", + len(expectedControllerSpecs), len(sourceInfo.ControllerSpecs())) + } + +NEXT_TEST: + for _, expected := range expectedControllerSpecs { + for _, actual := range sourceInfo.ControllerSpecs() { + if actual.StructName == expected.StructName { + if actual.ImportPath != expected.ImportPath { + t.Errorf("%s expected to have import path %s, actual %s", + actual.StructName, expected.ImportPath, actual.ImportPath) + } + if actual.PackageName != expected.PackageName { + t.Errorf("%s expected to have package name %s, actual %s", + actual.StructName, expected.PackageName, actual.PackageName) + } + continue NEXT_TEST + } + } + t.Errorf("Expected to find controller %s, but did not. Actuals: %s", + expected.StructName, sourceInfo.ControllerSpecs()) + } +} + +func BenchmarkProcessBookingSource(b *testing.B) { + revel.Init("", "github.com/revel/revel/samples/booking", "") + revel.TRACE = log.New(ioutil.Discard, "", 0) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := ProcessSource(revel.CodePaths) + if err != nil { + b.Error("Unexpected error:", err) + } + } +} diff --git a/revel/build.go b/revel/build.go index ef98976..3d40d2e 100644 --- a/revel/build.go +++ b/revel/build.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/revel/revel" - "github.com/revel/revel/harness" + "github.com/revel/cmd/harness" ) var cmdBuild = &Command{ diff --git a/revel/run.go b/revel/run.go index 0edc673..d1f8f7f 100644 --- a/revel/run.go +++ b/revel/run.go @@ -2,7 +2,7 @@ package main import ( "github.com/revel/revel" - "github.com/revel/revel/harness" + "github.com/revel/cmd/harness" "strconv" ) @@ -59,11 +59,13 @@ func runApp(args []string) { // If the app is run in "watched" mode, use the harness to run it. if revel.Config.BoolDefault("watch", true) && revel.Config.BoolDefault("watch.code", true) { + revel.TRACE.Println("Running in watched mode.") revel.HttpPort = port harness.NewHarness().Run() // Never returns. } // Else, just build and run the app. + revel.TRACE.Println("Running in live build mode.") app, err := harness.Build() if err != nil { errorf("Failed to build app: %s", err) diff --git a/revel/test.go b/revel/test.go index 33dfe62..2e51931 100644 --- a/revel/test.go +++ b/revel/test.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" "github.com/revel/revel" - "github.com/revel/revel/harness" + "github.com/revel/cmd/harness" "github.com/revel/revel/modules/testrunner/app/controllers" "io" "io/ioutil"