mirror of
https://github.com/kevin-DL/revel-cmd.git
synced 2026-01-22 06:55:20 +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