Files
revel-cmd/revel/test.go

313 lines
8.8 KiB
Go

// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package main
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/revel/cmd/harness"
"github.com/revel/cmd/model"
"github.com/revel/cmd/tests"
"github.com/revel/cmd/utils"
)
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/examples/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.RunWith = testApp
cmdTest.UpdateConfig = updateTestConfig
}
// Called to update the config command with from the older stype
func updateTestConfig(c *model.CommandConfig, args []string) bool {
c.Index = TEST
// The full test runs
// revel test <import path> (run mode) (suite(.function))
if len(args) < 1 {
return false
}
c.Test.ImportPath = args[0]
if len(args) > 1 {
c.Test.Mode = args[1]
}
if len(args) > 2 {
c.Test.Function = args[2]
}
return true
}
// Called to test the application
func testApp(c *model.CommandConfig) {
var err error
mode := DefaultRunMode
if c.Test.Mode != "" {
mode = c.Test.Mode
}
// Find and parse app.conf
revel_path := model.NewRevelPaths(mode, c.Test.ImportPath, "", model.DoNothingRevelCallback)
// Ensure that the testrunner is loaded in this mode.
// todo checkTestRunner()
// Create a directory to hold the test result files.
resultPath := filepath.Join(revel_path.BasePath, "test-results")
if err = os.RemoveAll(resultPath); err != nil {
utils.Logger.Errorf("Failed to remove test result directory %s: %s", resultPath, err)
}
if err = os.Mkdir(resultPath, 0777); err != nil {
utils.Logger.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(filepath.Join(resultPath, "app.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
utils.Logger.Errorf("Failed to create test result log file: %s", err)
}
app, reverr := harness.Build(c, revel_path)
if reverr != nil {
utils.Logger.Errorf("Error building: %s", reverr)
}
runMode := fmt.Sprintf(`{"mode":"%s","testModeFlag":true, "specialUseFlag":%v}`, app.Paths.RunMode, c.Verbose)
if c.HistoricMode {
runMode = app.Paths.RunMode
}
cmd := app.Cmd(runMode)
cmd.Stderr = io.MultiWriter(cmd.Stderr, file)
cmd.Stdout = io.MultiWriter(cmd.Stderr, file)
// Start the app...
if err := cmd.Start(c); err != nil {
utils.Logger.Errorf("%s", err)
}
defer cmd.Kill()
var httpAddr = revel_path.HTTPAddr
if httpAddr == "" {
httpAddr = "localhost"
}
var httpProto = "http"
if revel_path.HTTPSsl {
httpProto = "https"
}
// Get a list of tests
var baseURL = fmt.Sprintf("%s://%s:%d", httpProto, httpAddr, revel_path.HTTPPort)
utils.Logger.Infof("Testing %s (%s) in %s mode URL %s \n", revel_path.AppName, revel_path.ImportPath, mode, baseURL)
testSuites, _ := getTestsList(baseURL)
// If a specific TestSuite[.Method] is specified, only run that suite/test
if c.Test.Function != "" {
testSuites = filterTestSuites(testSuites, c.Test.Function)
}
testSuiteCount := len(*testSuites)
fmt.Printf("\n%d test suite%s to run.\n", testSuiteCount, pluralize(testSuiteCount, "", "s"))
fmt.Println()
// Run each suite.
failedResults, overallSuccess := runTestSuites(revel_path, baseURL, resultPath, testSuites)
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")
utils.Logger.Errorf("Some tests failed. See file://%s for results.", resultPath)
}
}
// Outputs the results to a file
func writeResultFile(resultPath, name, content string) {
if err := ioutil.WriteFile(filepath.Join(resultPath, name), []byte(content), 0666); err != nil {
utils.Logger.Errorf("Failed to write result file %s: %s", filepath.Join(resultPath, name), err)
}
}
// Determines if response should be plural
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 *[]tests.TestSuiteDesc, suiteArgument string) *[]tests.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 &[]tests.TestSuiteDesc{suite}
}
// Only run a particular test in a suite
for _, test := range suite.Tests {
if test.Name != testName {
continue
}
return &[]tests.TestSuiteDesc{
{
Name: suite.Name,
Tests: []tests.TestDesc{test},
},
}
}
utils.Logger.Errorf("Couldn't find test %s in suite %s", testName, suiteName)
}
utils.Logger.Errorf("Couldn't find test suite %s", suiteName)
return nil
}
// Get a list of tests from server.
// Since this is the first request to the server, retry/sleep a couple times
// in case it hasn't finished starting up yet.
func getTestsList(baseURL string) (*[]tests.TestSuiteDesc, error) {
var (
err error
resp *http.Response
testSuites []tests.TestSuiteDesc
)
for i := 0; ; i++ {
if resp, err = http.Get(baseURL + "/@tests.list"); err == nil {
if resp.StatusCode == http.StatusOK {
break
}
}
if i < 3 {
time.Sleep(3 * time.Second)
continue
}
if err != nil {
utils.Logger.Fatalf("Failed to request test list: %s %s", baseURL, err)
} else {
utils.Logger.Fatalf("Failed to request test list: non-200 response %s", baseURL)
}
}
defer func() {
_ = resp.Body.Close()
}()
err = json.NewDecoder(resp.Body).Decode(&testSuites)
return &testSuites, err
}
// Run the testsuites using the container
func runTestSuites(paths *model.RevelContainer, baseURL, resultPath string, testSuites *[]tests.TestSuiteDesc) (*[]tests.TestSuiteResult, bool) {
// We can determine the testsuite location by finding the test module and extracting the data from it
resultFilePath := filepath.Join(paths.ModulePathMap["testrunner"], "app", "views", "TestRunner/SuiteResult.html")
var (
overallSuccess = true
failedResults []tests.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 := tests.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 {
utils.Logger.Errorf("Failed to fetch test result at url %s: %s", testURL, err)
}
defer func() {
_ = resp.Body.Close()
}()
var testResult tests.TestResult
err = json.NewDecoder(resp.Body).Decode(&testResult)
if err == nil && !testResult.Passed {
suiteResult.Passed = false
fmt.Printf(" %s.%s : FAILED\n", suite.Name, test.Name)
} else {
fmt.Printf(" %s.%s : PASSED\n", suite.Name, test.Name)
}
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 := filepath.Join(resultPath,
fmt.Sprintf("%s.%s.html", suite.Name, strings.ToLower(suiteResultStr)))
utils.MustRenderTemplate(suiteResultFilename, resultFilePath, suiteResult)
}
return &failedResults, overallSuccess
}