mirror of
https://github.com/kevin-DL/revel-cmd.git
synced 2026-01-11 18:54:31 +00:00
Initial commit
This commit is contained in:
4
README.md
Normal file
4
README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Revel command line tools
|
||||
|
||||
Provides the `revel` command, used to create and run Revel apps.
|
||||
|
||||
112
revel/build.go
Normal file
112
revel/build.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/revel/revel"
|
||||
"github.com/revel/revel/harness"
|
||||
)
|
||||
|
||||
var cmdBuild = &Command{
|
||||
UsageLine: "build [import path] [target path]",
|
||||
Short: "build a Revel application (e.g. for deployment)",
|
||||
Long: `
|
||||
Build the Revel web application named by the given import path.
|
||||
This allows it to be deployed and run on a machine that lacks a Go installation.
|
||||
|
||||
WARNING: The target path will be completely deleted, if it already exists!
|
||||
|
||||
For example:
|
||||
|
||||
revel build github.com/revel/revel/samples/chat /tmp/chat
|
||||
`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmdBuild.Run = buildApp
|
||||
}
|
||||
|
||||
func buildApp(args []string) {
|
||||
if len(args) != 2 {
|
||||
fmt.Fprintf(os.Stderr, "%s\n%s", cmdBuild.UsageLine, cmdBuild.Long)
|
||||
return
|
||||
}
|
||||
|
||||
appImportPath, destPath := args[0], args[1]
|
||||
if !revel.Initialized {
|
||||
revel.Init("", appImportPath, "")
|
||||
}
|
||||
|
||||
// First, verify that it is either already empty or looks like a previous
|
||||
// build (to avoid clobbering anything)
|
||||
if exists(destPath) && !empty(destPath) && !exists(path.Join(destPath, "run.sh")) {
|
||||
errorf("Abort: %s exists and does not look like a build directory.", destPath)
|
||||
}
|
||||
|
||||
os.RemoveAll(destPath)
|
||||
os.MkdirAll(destPath, 0777)
|
||||
|
||||
app, reverr := harness.Build()
|
||||
panicOnError(reverr, "Failed to build")
|
||||
|
||||
// Included are:
|
||||
// - run scripts
|
||||
// - binary
|
||||
// - revel
|
||||
// - app
|
||||
|
||||
// Revel and the app are in a directory structure mirroring import path
|
||||
srcPath := path.Join(destPath, "src")
|
||||
destBinaryPath := path.Join(destPath, filepath.Base(app.BinaryPath))
|
||||
tmpRevelPath := path.Join(srcPath, filepath.FromSlash(revel.REVEL_IMPORT_PATH))
|
||||
mustCopyFile(destBinaryPath, app.BinaryPath)
|
||||
mustChmod(destBinaryPath, 0755)
|
||||
mustCopyDir(path.Join(tmpRevelPath, "conf"), path.Join(revel.RevelPath, "conf"), nil)
|
||||
mustCopyDir(path.Join(tmpRevelPath, "templates"), path.Join(revel.RevelPath, "templates"), nil)
|
||||
mustCopyDir(path.Join(srcPath, filepath.FromSlash(appImportPath)), revel.BasePath, nil)
|
||||
|
||||
// Find all the modules used and copy them over.
|
||||
config := revel.Config.Raw()
|
||||
modulePaths := make(map[string]string) // import path => filesystem path
|
||||
for _, section := range config.Sections() {
|
||||
options, _ := config.SectionOptions(section)
|
||||
for _, key := range options {
|
||||
if !strings.HasPrefix(key, "module.") {
|
||||
continue
|
||||
}
|
||||
moduleImportPath, _ := config.String(section, key)
|
||||
if moduleImportPath == "" {
|
||||
continue
|
||||
}
|
||||
modulePath, err := revel.ResolveImportPath(moduleImportPath)
|
||||
if err != nil {
|
||||
revel.ERROR.Fatalln("Failed to load module %s: %s", key[len("module."):], err)
|
||||
}
|
||||
modulePaths[moduleImportPath] = modulePath
|
||||
}
|
||||
}
|
||||
for importPath, fsPath := range modulePaths {
|
||||
mustCopyDir(path.Join(srcPath, importPath), fsPath, nil)
|
||||
}
|
||||
|
||||
tmplData, runShPath := map[string]interface{}{
|
||||
"BinName": filepath.Base(app.BinaryPath),
|
||||
"ImportPath": appImportPath,
|
||||
}, path.Join(destPath, "run.sh")
|
||||
|
||||
mustRenderTemplate(
|
||||
runShPath,
|
||||
path.Join(revel.RevelPath, "revel", "package_run.sh.template"),
|
||||
tmplData)
|
||||
|
||||
mustChmod(runShPath, 0755)
|
||||
|
||||
mustRenderTemplate(
|
||||
path.Join(destPath, "run.bat"),
|
||||
path.Join(revel.RevelPath, "revel", "package_run.bat.template"),
|
||||
tmplData)
|
||||
}
|
||||
48
revel/clean.go
Normal file
48
revel/clean.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/build"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
var cmdClean = &Command{
|
||||
UsageLine: "clean [import path]",
|
||||
Short: "clean a Revel application's temp files",
|
||||
Long: `
|
||||
Clean the Revel web application named by the given import path.
|
||||
|
||||
For example:
|
||||
|
||||
revel clean github.com/revel/revel/samples/chat
|
||||
|
||||
It removes the app/tmp directory.
|
||||
`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmdClean.Run = cleanApp
|
||||
}
|
||||
|
||||
func cleanApp(args []string) {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintf(os.Stderr, cmdClean.Long)
|
||||
return
|
||||
}
|
||||
|
||||
appPkg, err := build.Import(args[0], "", build.FindOnly)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Abort: Failed to find import path:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove the app/tmp directory.
|
||||
tmpDir := path.Join(appPkg.Dir, "app", "tmp")
|
||||
fmt.Println("Removing:", tmpDir)
|
||||
err = os.RemoveAll(tmpDir)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Abort:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
186
revel/new.go
Normal file
186
revel/new.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
var cmdNew = &Command{
|
||||
UsageLine: "new [path] [skeleton]",
|
||||
Short: "create a skeleton Revel application",
|
||||
Long: `
|
||||
New creates a few files to get a new Revel application running quickly.
|
||||
|
||||
It puts all of the files in the given import path, taking the final element in
|
||||
the path to be the app name.
|
||||
|
||||
Skeleton is an optional argument, provided as an import path
|
||||
|
||||
For example:
|
||||
|
||||
revel new import/path/helloworld
|
||||
|
||||
revel new import/path/helloworld import/path/skeleton
|
||||
`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmdNew.Run = newApp
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
// go related paths
|
||||
gopath string
|
||||
gocmd string
|
||||
srcRoot string
|
||||
|
||||
// revel related paths
|
||||
revelPkg *build.Package
|
||||
appPath string
|
||||
appName string
|
||||
basePath string
|
||||
importPath string
|
||||
skeletonPath string
|
||||
)
|
||||
|
||||
func newApp(args []string) {
|
||||
// check for proper args by count
|
||||
if len(args) == 0 {
|
||||
errorf("No import path given.\nRun 'revel help new' for usage.\n")
|
||||
}
|
||||
if len(args) > 2 {
|
||||
errorf("Too many arguments provided.\nRun 'revel help new' for usage.\n")
|
||||
}
|
||||
|
||||
// checking and setting go paths
|
||||
initGoPaths()
|
||||
|
||||
// checking and setting application
|
||||
setApplicationPath(args)
|
||||
|
||||
// checking and setting skeleton
|
||||
setSkeletonPath(args)
|
||||
|
||||
// copy files to new app directory
|
||||
copyNewAppFiles()
|
||||
|
||||
// goodbye world
|
||||
fmt.Fprintln(os.Stdout, "Your application is ready:\n ", appPath)
|
||||
fmt.Fprintln(os.Stdout, "\nYou can run it with:\n revel run", importPath)
|
||||
}
|
||||
|
||||
const alphaNumeric = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
func generateSecret() string {
|
||||
chars := make([]byte, 64)
|
||||
for i := 0; i < 64; i++ {
|
||||
chars[i] = alphaNumeric[rand.Intn(len(alphaNumeric))]
|
||||
}
|
||||
return string(chars)
|
||||
}
|
||||
|
||||
// lookup and set Go related variables
|
||||
func initGoPaths() {
|
||||
// lookup go path
|
||||
gopath = build.Default.GOPATH
|
||||
if gopath == "" {
|
||||
errorf("Abort: GOPATH environment variable is not set. " +
|
||||
"Please refer to http://golang.org/doc/code.html to configure your Go environment.")
|
||||
}
|
||||
|
||||
// set go src path
|
||||
srcRoot = filepath.Join(filepath.SplitList(gopath)[0], "src")
|
||||
|
||||
// check for go executable
|
||||
var err error
|
||||
gocmd, err = exec.LookPath("go")
|
||||
if err != nil {
|
||||
errorf("Go executable not found in PATH.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func setApplicationPath(args []string) {
|
||||
var err error
|
||||
importPath = args[0]
|
||||
if filepath.IsAbs(importPath) {
|
||||
errorf("Abort: '%s' looks like a directory. Please provide a Go import path instead.",
|
||||
importPath)
|
||||
}
|
||||
|
||||
_, err = build.Import(importPath, "", build.FindOnly)
|
||||
if err == nil {
|
||||
errorf("Abort: Import path %s already exists.\n", importPath)
|
||||
}
|
||||
|
||||
revelPkg, err = build.Import(revel.REVEL_IMPORT_PATH, "", build.FindOnly)
|
||||
if err != nil {
|
||||
errorf("Abort: Could not find Revel source code: %s\n", err)
|
||||
}
|
||||
|
||||
appPath = filepath.Join(srcRoot, filepath.FromSlash(importPath))
|
||||
appName = filepath.Base(appPath)
|
||||
basePath = filepath.ToSlash(filepath.Dir(importPath))
|
||||
|
||||
if basePath == "." {
|
||||
// we need to remove the a single '.' when
|
||||
// the app is in the $GOROOT/src directory
|
||||
basePath = ""
|
||||
} else {
|
||||
// we need to append a '/' when the app is
|
||||
// is a subdirectory such as $GOROOT/src/path/to/revelapp
|
||||
basePath += "/"
|
||||
}
|
||||
}
|
||||
|
||||
func setSkeletonPath(args []string) {
|
||||
var err error
|
||||
if len(args) == 2 { // user specified
|
||||
skeletonName := args[1]
|
||||
_, err = build.Import(skeletonName, "", build.FindOnly)
|
||||
if err != nil {
|
||||
// Execute "go get <pkg>"
|
||||
getCmd := exec.Command(gocmd, "get", "-d", skeletonName)
|
||||
fmt.Println("Exec:", getCmd.Args)
|
||||
getOutput, err := getCmd.CombinedOutput()
|
||||
|
||||
// check getOutput for no buildible string
|
||||
bpos := bytes.Index(getOutput, []byte("no buildable Go source files in"))
|
||||
if err != nil && bpos == -1 {
|
||||
errorf("Abort: Could not find or 'go get' Skeleton source code: %s\n%s\n", getOutput, skeletonName)
|
||||
}
|
||||
}
|
||||
// use the
|
||||
skeletonPath = filepath.Join(srcRoot, skeletonName)
|
||||
|
||||
} else {
|
||||
// use the revel default
|
||||
skeletonPath = filepath.Join(revelPkg.Dir, "skeleton")
|
||||
}
|
||||
}
|
||||
|
||||
func copyNewAppFiles() {
|
||||
var err error
|
||||
err = os.MkdirAll(appPath, 0777)
|
||||
panicOnError(err, "Failed to create directory "+appPath)
|
||||
|
||||
mustCopyDir(appPath, skeletonPath, map[string]interface{}{
|
||||
// app.conf
|
||||
"AppName": appName,
|
||||
"BasePath": basePath,
|
||||
"Secret": generateSecret(),
|
||||
})
|
||||
|
||||
// Dotfiles are skipped by mustCopyDir, so we have to explicitly copy the .gitignore.
|
||||
gitignore := ".gitignore"
|
||||
mustCopyFile(filepath.Join(appPath, gitignore), filepath.Join(skeletonPath, gitignore))
|
||||
|
||||
}
|
||||
51
revel/package.go
Normal file
51
revel/package.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/revel/revel"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var cmdPackage = &Command{
|
||||
UsageLine: "package [import path]",
|
||||
Short: "package a Revel application (e.g. for deployment)",
|
||||
Long: `
|
||||
Package the Revel web application named by the given import path.
|
||||
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
|
||||
`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmdPackage.Run = packageApp
|
||||
}
|
||||
|
||||
func packageApp(args []string) {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprint(os.Stderr, cmdPackage.Long)
|
||||
return
|
||||
}
|
||||
|
||||
appImportPath := args[0]
|
||||
revel.Init("", appImportPath, "")
|
||||
|
||||
// Remove the archive if it already exists.
|
||||
destFile := filepath.Base(revel.BasePath) + ".tar.gz"
|
||||
os.Remove(destFile)
|
||||
|
||||
// Collect stuff in a temp directory.
|
||||
tmpDir, err := ioutil.TempDir("", filepath.Base(revel.BasePath))
|
||||
panicOnError(err, "Failed to get temp dir")
|
||||
|
||||
buildApp([]string{args[0], tmpDir})
|
||||
|
||||
// Create the zip file.
|
||||
archiveName := mustTarGzDir(destFile, tmpDir)
|
||||
|
||||
fmt.Println("Your archive is ready:", archiveName)
|
||||
}
|
||||
2
revel/package_run.bat.template
Normal file
2
revel/package_run.bat.template
Normal file
@@ -0,0 +1,2 @@
|
||||
@echo off
|
||||
{{.BinName}} -importPath {{.ImportPath}} -srcPath %CD%\src -runMode prod
|
||||
3
revel/package_run.sh.template
Normal file
3
revel/package_run.sh.template
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
SCRIPTPATH=$(cd "$(dirname "$0")"; pwd)
|
||||
"$SCRIPTPATH/{{.BinName}}" -importPath {{.ImportPath}} -srcPath "$SCRIPTPATH/src" -runMode prod
|
||||
129
revel/rev.go
Normal file
129
revel/rev.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// The command line tool for running Revel apps.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/agtorre/gocolorize"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cribbed from the genius organization of the "go" command.
|
||||
type Command struct {
|
||||
Run func(args []string)
|
||||
UsageLine, Short, Long string
|
||||
}
|
||||
|
||||
func (cmd *Command) Name() string {
|
||||
name := cmd.UsageLine
|
||||
i := strings.Index(name, " ")
|
||||
if i >= 0 {
|
||||
name = name[:i]
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
var commands = []*Command{
|
||||
cmdNew,
|
||||
cmdRun,
|
||||
cmdBuild,
|
||||
cmdPackage,
|
||||
cmdClean,
|
||||
cmdTest,
|
||||
}
|
||||
|
||||
func main() {
|
||||
if runtime.GOOS == "windows" {
|
||||
gocolorize.SetPlain(true)
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, gocolorize.NewColor("blue").Paint(header))
|
||||
flag.Usage = func() { usage(1) }
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
|
||||
if len(args) < 1 || args[0] == "help" {
|
||||
if len(args) == 1 {
|
||||
usage(0)
|
||||
}
|
||||
if len(args) > 1 {
|
||||
for _, cmd := range commands {
|
||||
if cmd.Name() == args[1] {
|
||||
tmpl(os.Stdout, helpTemplate, cmd)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
usage(2)
|
||||
}
|
||||
|
||||
// Commands use panic to abort execution when something goes wrong.
|
||||
// Panics are logged at the point of error. Ignore those.
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
if _, ok := err.(LoggedError); !ok {
|
||||
// This panic was not expected / logged.
|
||||
panic(err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, cmd := range commands {
|
||||
if cmd.Name() == args[0] {
|
||||
cmd.Run(args[1:])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
errorf("unknown command %q\nRun 'revel help' for usage.\n", args[0])
|
||||
}
|
||||
|
||||
func errorf(format string, args ...interface{}) {
|
||||
// Ensure the user's command prompt starts on the next line.
|
||||
if !strings.HasSuffix(format, "\n") {
|
||||
format += "\n"
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, format, args...)
|
||||
panic(LoggedError{}) // Panic instead of os.Exit so that deferred will run.
|
||||
}
|
||||
|
||||
const header = `~
|
||||
~ revel! http://revel.github.com
|
||||
~
|
||||
`
|
||||
|
||||
const usageTemplate = `usage: revel command [arguments]
|
||||
|
||||
The commands are:
|
||||
{{range .}}
|
||||
{{.Name | printf "%-11s"}} {{.Short}}{{end}}
|
||||
|
||||
Use "revel help [command]" for more information.
|
||||
`
|
||||
|
||||
var helpTemplate = `usage: revel {{.UsageLine}}
|
||||
{{.Long}}
|
||||
`
|
||||
|
||||
func usage(exitCode int) {
|
||||
tmpl(os.Stderr, usageTemplate, commands)
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
func tmpl(w io.Writer, text string, data interface{}) {
|
||||
t := template.New("top")
|
||||
template.Must(t.Parse(text))
|
||||
if err := t.Execute(w, data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
73
revel/run.go
Normal file
73
revel/run.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
"github.com/revel/revel/harness"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var cmdRun = &Command{
|
||||
UsageLine: "run [import path] [run mode] [port]",
|
||||
Short: "run a Revel application",
|
||||
Long: `
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmdRun.Run = runApp
|
||||
}
|
||||
|
||||
func runApp(args []string) {
|
||||
if len(args) == 0 {
|
||||
errorf("No import path given.\nRun 'revel help run' for usage.\n")
|
||||
}
|
||||
|
||||
// Determine the run mode.
|
||||
mode := "dev"
|
||||
if len(args) >= 2 {
|
||||
mode = args[1]
|
||||
}
|
||||
|
||||
// Find and parse app.conf
|
||||
revel.Init(mode, args[0], "")
|
||||
revel.LoadMimeConfig()
|
||||
|
||||
// Determine the override port, if any.
|
||||
port := revel.HttpPort
|
||||
if len(args) == 3 {
|
||||
var err error
|
||||
if port, err = strconv.Atoi(args[2]); err != nil {
|
||||
errorf("Failed to parse port as integer: %s", args[2])
|
||||
}
|
||||
}
|
||||
|
||||
revel.INFO.Printf("Running %s (%s) in %s mode\n", revel.AppName, revel.ImportPath, mode)
|
||||
revel.TRACE.Println("Base path:", revel.BasePath)
|
||||
|
||||
// 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.HttpPort = port
|
||||
harness.NewHarness().Run() // Never returns.
|
||||
}
|
||||
|
||||
// Else, just build and run the app.
|
||||
app, err := harness.Build()
|
||||
if err != nil {
|
||||
errorf("Failed to build app: %s", err)
|
||||
}
|
||||
app.Port = port
|
||||
app.Cmd().Run()
|
||||
}
|
||||
268
revel/test.go
Normal file
268
revel/test.go
Normal file
@@ -0,0 +1,268 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/revel/revel"
|
||||
"github.com/revel/revel/harness"
|
||||
"github.com/revel/revel/modules/testrunner/app/controllers"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var cmdTest = &Command{
|
||||
UsageLine: "test [import path] [run mode] [suite.method]",
|
||||
Short: "run all tests from the command-line",
|
||||
Long: `
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
Run mode defaults to "dev".
|
||||
|
||||
You can run a specific suite (and function) by specifying a third parameter.
|
||||
For example, to run all of UserTest:
|
||||
|
||||
revel test outspoken test UserTest
|
||||
|
||||
or one of UserTest's methods:
|
||||
|
||||
revel test outspoken test UserTest.Test1
|
||||
`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmdTest.Run = testApp
|
||||
}
|
||||
|
||||
func testApp(args []string) {
|
||||
var err error
|
||||
if len(args) == 0 {
|
||||
errorf("No import path given.\nRun 'revel help test' for usage.\n")
|
||||
}
|
||||
|
||||
mode := "dev"
|
||||
if len(args) >= 2 {
|
||||
mode = args[1]
|
||||
}
|
||||
|
||||
// Find and parse app.conf
|
||||
revel.Init(mode, args[0], "")
|
||||
|
||||
// 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" {
|
||||
testRunnerFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !testRunnerFound {
|
||||
errorf(`Error: The testrunner module is not running.
|
||||
|
||||
You can add it to a run mode configuration with the following line:
|
||||
|
||||
module.testrunner = github.com/revel/revel/modules/testrunner
|
||||
|
||||
`)
|
||||
}
|
||||
|
||||
// Create a directory to hold the test result files.
|
||||
resultPath := path.Join(revel.BasePath, "test-results")
|
||||
if err = os.RemoveAll(resultPath); err != nil {
|
||||
errorf("Failed to remove test result directory %s: %s", resultPath, err)
|
||||
}
|
||||
if err = os.Mkdir(resultPath, 0777); err != nil {
|
||||
errorf("Failed to create test result directory %s: %s", resultPath, err)
|
||||
}
|
||||
|
||||
// Direct all the output into a file in the test-results directory.
|
||||
file, err := os.OpenFile(path.Join(resultPath, "app.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
errorf("Failed to create log file: %s", err)
|
||||
}
|
||||
|
||||
app, reverr := harness.Build()
|
||||
if reverr != nil {
|
||||
errorf("Error building: %s", reverr)
|
||||
}
|
||||
cmd := app.Cmd()
|
||||
cmd.Stderr = io.MultiWriter(cmd.Stderr, file)
|
||||
cmd.Stdout = io.MultiWriter(cmd.Stderr, file)
|
||||
|
||||
// Start the app...
|
||||
if err := cmd.Start(); err != nil {
|
||||
errorf("%s", err)
|
||||
}
|
||||
defer cmd.Kill()
|
||||
revel.INFO.Printf("Testing %s (%s) in %s mode\n", revel.AppName, revel.ImportPath, mode)
|
||||
|
||||
// Get a list of tests.
|
||||
// Since this is the first request to the server, retry/sleep a couple times
|
||||
// in case it hasn't finished starting up yet.
|
||||
var (
|
||||
testSuites []controllers.TestSuiteDesc
|
||||
resp *http.Response
|
||||
baseUrl = fmt.Sprintf("http://127.0.0.1:%d", revel.HttpPort)
|
||||
)
|
||||
for i := 0; ; i++ {
|
||||
if resp, err = http.Get(baseUrl + "/@tests.list"); err == nil {
|
||||
break
|
||||
}
|
||||
if i < 3 {
|
||||
time.Sleep(3 * time.Second)
|
||||
continue
|
||||
}
|
||||
errorf("Failed to request test list: %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
json.NewDecoder(resp.Body).Decode(&testSuites)
|
||||
|
||||
// If a specific TestSuite[.Method] is specified, only run that suite/test
|
||||
if len(args) == 3 {
|
||||
testSuites = filterTestSuites(testSuites, args[2])
|
||||
}
|
||||
fmt.Printf("\n%d test suite%s to run.\n", len(testSuites), pluralize(len(testSuites), "", "s"))
|
||||
fmt.Println()
|
||||
|
||||
// Load the result template, which we execute for each suite.
|
||||
module, _ := revel.ModuleByName("testrunner")
|
||||
TemplateLoader := revel.NewTemplateLoader([]string{path.Join(module.Path, "app", "views")})
|
||||
if err := TemplateLoader.Refresh(); err != nil {
|
||||
errorf("Failed to compile templates: %s", err)
|
||||
}
|
||||
resultTemplate, err := TemplateLoader.Template("TestRunner/SuiteResult.html")
|
||||
if err != nil {
|
||||
errorf("Failed to load suite result template: %s", err)
|
||||
}
|
||||
|
||||
// Run each suite.
|
||||
var (
|
||||
overallSuccess = true
|
||||
failedResults []controllers.TestSuiteResult
|
||||
)
|
||||
for _, suite := range testSuites {
|
||||
// Print the name of the suite we're running.
|
||||
name := suite.Name
|
||||
if len(name) > 22 {
|
||||
name = name[:19] + "..."
|
||||
}
|
||||
fmt.Printf("%-22s", name)
|
||||
|
||||
// Run every test.
|
||||
startTime := time.Now()
|
||||
suiteResult := controllers.TestSuiteResult{Name: suite.Name, Passed: true}
|
||||
for _, test := range suite.Tests {
|
||||
testUrl := baseUrl + "/@tests/" + suite.Name + "/" + test.Name
|
||||
resp, err := http.Get(testUrl)
|
||||
if err != nil {
|
||||
errorf("Failed to fetch test result at url %s: %s", testUrl, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var testResult controllers.TestResult
|
||||
json.NewDecoder(resp.Body).Decode(&testResult)
|
||||
if !testResult.Passed {
|
||||
suiteResult.Passed = false
|
||||
}
|
||||
suiteResult.Results = append(suiteResult.Results, testResult)
|
||||
}
|
||||
overallSuccess = overallSuccess && suiteResult.Passed
|
||||
|
||||
// Print result. (Just PASSED or FAILED, and the time taken)
|
||||
suiteResultStr, suiteAlert := "PASSED", ""
|
||||
if !suiteResult.Passed {
|
||||
suiteResultStr, suiteAlert = "FAILED", "!"
|
||||
failedResults = append(failedResults, suiteResult)
|
||||
}
|
||||
fmt.Printf("%8s%3s%6ds\n", suiteResultStr, suiteAlert, int(time.Since(startTime).Seconds()))
|
||||
// Create the result HTML file.
|
||||
suiteResultFilename := path.Join(resultPath,
|
||||
fmt.Sprintf("%s.%s.html", suite.Name, strings.ToLower(suiteResultStr)))
|
||||
suiteResultFile, err := os.Create(suiteResultFilename)
|
||||
if err != nil {
|
||||
errorf("Failed to create result file %s: %s", suiteResultFilename, err)
|
||||
}
|
||||
if err = resultTemplate.Render(suiteResultFile, suiteResult); err != nil {
|
||||
errorf("Failed to render result template: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
if overallSuccess {
|
||||
writeResultFile(resultPath, "result.passed", "passed")
|
||||
fmt.Println("All Tests Passed.")
|
||||
} else {
|
||||
for _, failedResult := range failedResults {
|
||||
fmt.Printf("Failures:\n")
|
||||
for _, result := range failedResult.Results {
|
||||
if !result.Passed {
|
||||
fmt.Printf("%s.%s\n", failedResult.Name, result.Name)
|
||||
fmt.Printf("%s\n\n", result.ErrorSummary)
|
||||
}
|
||||
}
|
||||
}
|
||||
writeResultFile(resultPath, "result.failed", "failed")
|
||||
errorf("Some tests failed. See file://%s for results.", resultPath)
|
||||
}
|
||||
}
|
||||
|
||||
func writeResultFile(resultPath, name, content string) {
|
||||
if err := ioutil.WriteFile(path.Join(resultPath, name), []byte(content), 0666); err != nil {
|
||||
errorf("Failed to write result file %s: %s", path.Join(resultPath, name), err)
|
||||
}
|
||||
}
|
||||
|
||||
func pluralize(num int, singular, plural string) string {
|
||||
if num == 1 {
|
||||
return singular
|
||||
}
|
||||
return plural
|
||||
}
|
||||
|
||||
// Filters test suites and individual tests to match
|
||||
// the parsed command line parameter
|
||||
func filterTestSuites(suites []controllers.TestSuiteDesc, suiteArgument string) []controllers.TestSuiteDesc {
|
||||
var suiteName, testName string
|
||||
argArray := strings.Split(suiteArgument, ".")
|
||||
suiteName = argArray[0]
|
||||
if suiteName == "" {
|
||||
return suites
|
||||
}
|
||||
if len(argArray) == 2 {
|
||||
testName = argArray[1]
|
||||
}
|
||||
for _, suite := range suites {
|
||||
if suite.Name != suiteName {
|
||||
continue
|
||||
}
|
||||
if testName == "" {
|
||||
return []controllers.TestSuiteDesc{suite}
|
||||
}
|
||||
// Only run a particular test in a suite
|
||||
for _, test := range suite.Tests {
|
||||
if test.Name != testName {
|
||||
continue
|
||||
}
|
||||
return []controllers.TestSuiteDesc{
|
||||
controllers.TestSuiteDesc{
|
||||
Name: suite.Name,
|
||||
Tests: []controllers.TestDesc{test},
|
||||
},
|
||||
}
|
||||
}
|
||||
errorf("Couldn't find test %s in suite %s", testName, suiteName)
|
||||
}
|
||||
errorf("Couldn't find test suite %s", suiteName)
|
||||
return nil
|
||||
}
|
||||
156
revel/util.go
Normal file
156
revel/util.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
// Use a wrapper to differentiate logged panics from unexpected ones.
|
||||
type LoggedError struct{ error }
|
||||
|
||||
func panicOnError(err error, msg string) {
|
||||
if revErr, ok := err.(*revel.Error); (ok && revErr != nil) || (!ok && err != nil) {
|
||||
fmt.Fprintf(os.Stderr, "Abort: %s: %s\n", msg, err)
|
||||
panic(LoggedError{err})
|
||||
}
|
||||
}
|
||||
|
||||
func mustCopyFile(destFilename, srcFilename string) {
|
||||
destFile, err := os.Create(destFilename)
|
||||
panicOnError(err, "Failed to create file "+destFilename)
|
||||
|
||||
srcFile, err := os.Open(srcFilename)
|
||||
panicOnError(err, "Failed to open file "+srcFilename)
|
||||
|
||||
_, err = io.Copy(destFile, srcFile)
|
||||
panicOnError(err,
|
||||
fmt.Sprintf("Failed to copy data from %s to %s", srcFile.Name(), destFile.Name()))
|
||||
|
||||
err = destFile.Close()
|
||||
panicOnError(err, "Failed to close file "+destFile.Name())
|
||||
|
||||
err = srcFile.Close()
|
||||
panicOnError(err, "Failed to close file "+srcFile.Name())
|
||||
}
|
||||
|
||||
func mustRenderTemplate(destPath, srcPath string, data map[string]interface{}) {
|
||||
tmpl, err := template.ParseFiles(srcPath)
|
||||
panicOnError(err, "Failed to parse template "+srcPath)
|
||||
|
||||
f, err := os.Create(destPath)
|
||||
panicOnError(err, "Failed to create "+destPath)
|
||||
|
||||
err = tmpl.Execute(f, data)
|
||||
panicOnError(err, "Failed to render template "+srcPath)
|
||||
|
||||
err = f.Close()
|
||||
panicOnError(err, "Failed to close "+f.Name())
|
||||
}
|
||||
|
||||
func mustChmod(filename string, mode os.FileMode) {
|
||||
err := os.Chmod(filename, mode)
|
||||
panicOnError(err, fmt.Sprintf("Failed to chmod %d %q", mode, filename))
|
||||
}
|
||||
|
||||
// copyDir copies a directory tree over to a new directory. Any files ending in
|
||||
// ".template" are treated as a Go template and rendered using the given data.
|
||||
// Additionally, the trailing ".template" is stripped from the file name.
|
||||
// Also, dot files and dot directories are skipped.
|
||||
func mustCopyDir(destDir, srcDir string, data map[string]interface{}) error {
|
||||
return filepath.Walk(srcDir, func(srcPath string, info os.FileInfo, err error) error {
|
||||
// Get the relative path from the source base, and the corresponding path in
|
||||
// the dest directory.
|
||||
relSrcPath := strings.TrimLeft(srcPath[len(srcDir):], string(os.PathSeparator))
|
||||
destPath := path.Join(destDir, relSrcPath)
|
||||
|
||||
// Skip dot files and dot directories.
|
||||
if strings.HasPrefix(relSrcPath, ".") {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a subdirectory if necessary.
|
||||
if info.IsDir() {
|
||||
err := os.MkdirAll(path.Join(destDir, relSrcPath), 0777)
|
||||
if !os.IsExist(err) {
|
||||
panicOnError(err, "Failed to create directory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// If this file ends in ".template", render it as a template.
|
||||
if strings.HasSuffix(relSrcPath, ".template") {
|
||||
mustRenderTemplate(destPath[:len(destPath)-len(".template")], srcPath, data)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Else, just copy it over.
|
||||
mustCopyFile(destPath, srcPath)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func mustTarGzDir(destFilename, srcDir string) string {
|
||||
zipFile, err := os.Create(destFilename)
|
||||
panicOnError(err, "Failed to create archive")
|
||||
defer zipFile.Close()
|
||||
|
||||
gzipWriter := gzip.NewWriter(zipFile)
|
||||
defer gzipWriter.Close()
|
||||
|
||||
tarWriter := tar.NewWriter(gzipWriter)
|
||||
defer tarWriter.Close()
|
||||
|
||||
filepath.Walk(srcDir, func(srcPath string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
srcFile, err := os.Open(srcPath)
|
||||
panicOnError(err, "Failed to read source file")
|
||||
defer srcFile.Close()
|
||||
|
||||
err = tarWriter.WriteHeader(&tar.Header{
|
||||
Name: strings.TrimLeft(srcPath[len(srcDir):], string(os.PathSeparator)),
|
||||
Size: info.Size(),
|
||||
Mode: int64(info.Mode()),
|
||||
ModTime: info.ModTime(),
|
||||
})
|
||||
panicOnError(err, "Failed to write tar entry header")
|
||||
|
||||
_, err = io.Copy(tarWriter, srcFile)
|
||||
panicOnError(err, "Failed to copy")
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return zipFile.Name()
|
||||
}
|
||||
|
||||
func exists(filename string) bool {
|
||||
_, err := os.Stat(filename)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// empty returns true if the given directory is empty.
|
||||
// the directory must exist.
|
||||
func empty(dirname string) bool {
|
||||
dir, err := os.Open(dirname)
|
||||
if err != nil {
|
||||
errorf("error opening directory: %s", err)
|
||||
}
|
||||
defer dir.Close()
|
||||
results, _ := dir.Readdir(1)
|
||||
return len(results) == 0
|
||||
}
|
||||
Reference in New Issue
Block a user