From 9046edff96009d33cace32fba7d5a0d6b3370b27 Mon Sep 17 00:00:00 2001 From: Brenden Soares Date: Tue, 28 Oct 2014 22:22:43 -0700 Subject: [PATCH 1/9] Adds harness from 'revel/revel' and updates imports --- harness/app.go | 118 ++++++ harness/build.go | 390 ++++++++++++++++++++ harness/harness.go | 240 +++++++++++++ harness/reflect.go | 777 ++++++++++++++++++++++++++++++++++++++++ harness/reflect_test.go | 190 ++++++++++ revel/build.go | 2 +- revel/run.go | 4 +- revel/test.go | 2 +- 8 files changed, 1720 insertions(+), 3 deletions(-) create mode 100644 harness/app.go create mode 100755 harness/build.go create mode 100644 harness/harness.go create mode 100644 harness/reflect.go create mode 100644 harness/reflect_test.go 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" From eaa1f8b19e2d4b86e65aaf88ebe9ad8902244519 Mon Sep 17 00:00:00 2001 From: Brenden Soares Date: Tue, 28 Oct 2014 23:41:39 -0700 Subject: [PATCH 2/9] Updates references to module import paths --- revel/test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/revel/test.go b/revel/test.go index 33dfe62..856bb8c 100644 --- a/revel/test.go +++ b/revel/test.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/revel/revel" "github.com/revel/revel/harness" - "github.com/revel/revel/modules/testrunner/app/controllers" + "github.com/revel/modules/testrunner/app/controllers" "io" "io/ioutil" "net/http" @@ -62,7 +62,7 @@ func testApp(args []string) { // Ensure that the testrunner is loaded in this mode. testRunnerFound := false for _, module := range revel.Modules { - if module.ImportPath == "github.com/revel/revel/modules/testrunner" { + if module.ImportPath == "github.com/revel/modules/testrunner" { testRunnerFound = true break } @@ -72,7 +72,7 @@ func testApp(args []string) { You can add it to a run mode configuration with the following line: - module.testrunner = github.com/revel/revel/modules/testrunner + module.testrunner = github.com/revel/modules/testrunner `) } From 176c4077ded7a99c58d052ace92e19f0d9e83965 Mon Sep 17 00:00:00 2001 From: anonx Date: Wed, 29 Oct 2014 23:52:27 +0600 Subject: [PATCH 3/9] Get testrunner's path from config --- revel/test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/revel/test.go b/revel/test.go index 856bb8c..eefcaa7 100644 --- a/revel/test.go +++ b/revel/test.go @@ -62,7 +62,7 @@ func testApp(args []string) { // Ensure that the testrunner is loaded in this mode. testRunnerFound := false for _, module := range revel.Modules { - if module.ImportPath == "github.com/revel/modules/testrunner" { + if module.ImportPath == revel.Config.StringDefault("module.testrunner", "github.com/revel/modules/testrunner") { testRunnerFound = true break } From a13856d4d03bf2168f95a9b710bf0964f4ed253c Mon Sep 17 00:00:00 2001 From: Justin Li Date: Thu, 30 Oct 2014 11:22:53 -0400 Subject: [PATCH 4/9] Update samples import paths --- harness/reflect.go | 4 ++-- harness/reflect_test.go | 6 +++--- revel/build.go | 2 +- revel/clean.go | 2 +- revel/package.go | 2 +- revel/run.go | 4 ++-- revel/test.go | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/harness/reflect.go b/harness/reflect.go index f64630c..cf7aa28 100644 --- a/harness/reflect.go +++ b/harness/reflect.go @@ -25,7 +25,7 @@ type SourceInfo struct { 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" + // e.g. "github.com/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. @@ -44,7 +44,7 @@ type SourceInfo struct { // 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" + ImportPath string // e.g. "github.com/revel/samples/chat/app/controllers" PackageName string // e.g. "controllers" MethodSpecs []*MethodSpec diff --git a/harness/reflect_test.go b/harness/reflect_test.go index a704d74..f45364e 100644 --- a/harness/reflect_test.go +++ b/harness/reflect_test.go @@ -139,13 +139,13 @@ func TestTypeExpr(t *testing.T) { } func TestProcessBookingSource(t *testing.T) { - revel.Init("prod", "github.com/revel/revel/samples/booking", "") + revel.Init("prod", "github.com/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" + CONTROLLER_PKG := "github.com/revel/samples/booking/app/controllers" expectedControllerSpecs := []*TypeInfo{ {"GorpController", CONTROLLER_PKG, "controllers", nil, nil}, {"Application", CONTROLLER_PKG, "controllers", nil, nil}, @@ -177,7 +177,7 @@ NEXT_TEST: } func BenchmarkProcessBookingSource(b *testing.B) { - revel.Init("", "github.com/revel/revel/samples/booking", "") + revel.Init("", "github.com/revel/samples/booking", "") revel.TRACE = log.New(ioutil.Discard, "", 0) b.ResetTimer() diff --git a/revel/build.go b/revel/build.go index 8fb3e9d..45e7d07 100644 --- a/revel/build.go +++ b/revel/build.go @@ -22,7 +22,7 @@ WARNING: The target path will be completely deleted, if it already exists! For example: - revel build github.com/revel/revel/samples/chat /tmp/chat + revel build github.com/revel/samples/chat /tmp/chat `, } diff --git a/revel/clean.go b/revel/clean.go index 15a43d3..1af29bf 100644 --- a/revel/clean.go +++ b/revel/clean.go @@ -15,7 +15,7 @@ Clean the Revel web application named by the given import path. For example: - revel clean github.com/revel/revel/samples/chat + revel clean github.com/revel/samples/chat It removes the app/tmp directory. `, diff --git a/revel/package.go b/revel/package.go index b909901..fe1e4c3 100644 --- a/revel/package.go +++ b/revel/package.go @@ -17,7 +17,7 @@ This allows it to be deployed and run on a machine that lacks a Go installation. For example: - revel package github.com/revel/revel/samples/chat + revel package github.com/revel/samples/chat `, } diff --git a/revel/run.go b/revel/run.go index d1f8f7f..891415b 100644 --- a/revel/run.go +++ b/revel/run.go @@ -14,7 +14,7 @@ Run the Revel web application named by the given import path. For example, to run the chat room sample application: - revel run github.com/revel/revel/samples/chat dev + revel run github.com/revel/samples/chat dev The run mode is used to select which set of app.conf configuration should apply and may be used to determine logic in the application itself. @@ -23,7 +23,7 @@ Run mode defaults to "dev". You can set a port as an optional third parameter. For example: - revel run github.com/revel/revel/samples/chat prod 8080`, + revel run github.com/revel/samples/chat prod 8080`, } func init() { diff --git a/revel/test.go b/revel/test.go index 0276f91..3128cd6 100644 --- a/revel/test.go +++ b/revel/test.go @@ -23,7 +23,7 @@ Run all tests for the Revel app named by the given import path. For example, to run the booking sample application's tests: - revel test github.com/revel/revel/samples/booking dev + revel test github.com/revel/samples/booking dev The run mode is used to select which set of app.conf configuration should apply and may be used to determine logic in the application itself. From 09c730bceae4319ca74d7edadc7c01feae0e52eb Mon Sep 17 00:00:00 2001 From: anonx Date: Tue, 25 Nov 2014 16:17:20 +0600 Subject: [PATCH 5/9] Reflect the latest changes in revel related to testsuite --- harness/build.go | 3 ++- harness/reflect.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/harness/build.go b/harness/build.go index f89a28a..bee1fc2 100755 --- a/harness/build.go +++ b/harness/build.go @@ -322,6 +322,7 @@ import ( "reflect" "github.com/revel/revel"{{range $k, $v := $.ImportPaths}} {{$v}} "{{$k}}"{{end}} + "github.com/revel/revel/testing" ) var ( @@ -360,7 +361,7 @@ func main() { {{$line}}: "{{$key}}",{{end}} },{{end}} } - revel.TestSuites = []interface{}{ {{range .TestSuites}} + testing.TestSuites = []interface{}{ {{range .TestSuites}} (*{{index $.ImportPaths .ImportPath}}.{{.StructName}})(nil),{{end}} } diff --git a/harness/reflect.go b/harness/reflect.go index cf7aa28..f4ff4e6 100644 --- a/harness/reflect.go +++ b/harness/reflect.go @@ -681,7 +681,7 @@ func (s *SourceInfo) ControllerSpecs() []*TypeInfo { func (s *SourceInfo) TestSuites() []*TypeInfo { if s.testSuites == nil { - s.testSuites = s.TypesThatEmbed(revel.REVEL_IMPORT_PATH + ".TestSuite") + s.testSuites = s.TypesThatEmbed(revel.REVEL_IMPORT_PATH + "/testing.TestSuite") } return s.testSuites } From cb83616943e04514d6c2b97254d0601dc637094e Mon Sep 17 00:00:00 2001 From: anonx Date: Sun, 4 Jan 2015 08:42:12 +0600 Subject: [PATCH 6/9] Do not override binaries in $GOPATH/bin (fix #819) --- harness/build.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/harness/build.go b/harness/build.go index bee1fc2..74fa0ec 100755 --- a/harness/build.go +++ b/harness/build.go @@ -61,7 +61,7 @@ func Build(buildFlags ...string) (app *App, compileError *revel.Error) { if err != nil { revel.ERROR.Fatalln("Failure importing", revel.ImportPath) } - binName := path.Join(pkg.BinDir, path.Base(revel.BasePath)) + binName := path.Join(pkg.BinDir, "revel.d", path.Base(revel.BasePath)) // Change binary path for Windows build goos := runtime.GOOS From 9066d60a5e314025b9c228fe0f9a034383908ea1 Mon Sep 17 00:00:00 2001 From: Eliseo Date: Sun, 18 Jan 2015 02:41:29 +0600 Subject: [PATCH 7/9] Using full import paths for revel binaries --- harness/build.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/harness/build.go b/harness/build.go index 74fa0ec..9470286 100755 --- a/harness/build.go +++ b/harness/build.go @@ -61,7 +61,9 @@ func Build(buildFlags ...string) (app *App, compileError *revel.Error) { if err != nil { revel.ERROR.Fatalln("Failure importing", revel.ImportPath) } - binName := path.Join(pkg.BinDir, "revel.d", path.Base(revel.BasePath)) + + // Binary path is a combination of $GOBIN/revel.d directory, app's import path and its name. + binName := path.Join(pkg.BinDir, "revel.d", revel.ImportPath, path.Base(revel.BasePath)) // Change binary path for Windows build goos := runtime.GOOS From bc88881c559ce1dd429d599e0ff7276a9a49c767 Mon Sep 17 00:00:00 2001 From: Brenden Soares Date: Mon, 23 Feb 2015 21:47:11 -0800 Subject: [PATCH 8/9] Overwrites generated app files instead of deleting directory --- harness/build.go | 45 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/harness/build.go b/harness/build.go index bee1fc2..a4ac1ea 100755 --- a/harness/build.go +++ b/harness/build.go @@ -162,14 +162,44 @@ func getAppVersion() string { func cleanSource(dirs ...string) { for _, dir := range dirs { - tmpPath := path.Join(revel.AppPath, dir) - err := os.RemoveAll(tmpPath) + cleanDir(dir) if err != nil { revel.ERROR.Println("Failed to remove dir:", err) } } } +func cleanDir(dir string) { + revel.INFO.Println("Cleaning dir " + dir) + tmpPath := path.Join(revel.AppPath, dir) + f, err := os.Open(tmpPath) + if err != nil { + revel.ERROR.Println("Failed to clean dir:", err) + } else { + defer f.Close() + infos, err := f.Readdir(0) + if err != nil { + revel.ERROR.Println("Failed to clean dir:", err) + } else { + for _, info := range infos { + path := path.Join(tmpPath, info.Name()) + if info.IsDir() { + err := os.RemoveAll(path) + if err != nil { + revel.ERROR.Println("Failed to remove dir:", err) + } + } else { + err := os.Remove(path) + if err != nil { + revel.ERROR.Println("Failed to remove file:", 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{}) { @@ -178,14 +208,11 @@ func genSource(dir, filename, templateSource string, args map[string]interface{} args) // Create a fresh dir. + cleanSource(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) + err := os.Mkdir(tmpPath, 0777) + if err != nil && !os.IsExist(err) { + revel.ERROR.Fatalf("Failed to make '%v' directory: %v", dir, err) } // Create the file From 1a30b3cc7aca6d3956b4376531255d0d80176484 Mon Sep 17 00:00:00 2001 From: Brenden Soares Date: Mon, 23 Feb 2015 21:52:40 -0800 Subject: [PATCH 9/9] Fixes orphaned code --- harness/build.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/harness/build.go b/harness/build.go index a4ac1ea..759b570 100755 --- a/harness/build.go +++ b/harness/build.go @@ -163,9 +163,6 @@ func getAppVersion() string { func cleanSource(dirs ...string) { for _, dir := range dirs { cleanDir(dir) - if err != nil { - revel.ERROR.Println("Failed to remove dir:", err) - } } }