github.com/wiselike/revel-cmd@v1.2.1/revel/test.go (about)

     1  // Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
     2  // Revel Framework source code and usage is governed by a MIT style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"net/http"
    13  	"os"
    14  	"path/filepath"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/wiselike/revel-cmd/harness"
    19  	"github.com/wiselike/revel-cmd/model"
    20  	"github.com/wiselike/revel-cmd/tests"
    21  	"github.com/wiselike/revel-cmd/utils"
    22  )
    23  
    24  var cmdTest = &Command{
    25  	UsageLine: "test <import path> [<run mode> <suite.method>]",
    26  	Short:     "run all tests from the command-line",
    27  	Long: `
    28  Run all tests for the Revel app named by the given import path.
    29  
    30  For example, to run the booking sample application's tests:
    31  
    32      revel test github.com/revel/examples/booking dev
    33  
    34  The run mode is used to select which set of app.conf configuration should
    35  apply and may be used to determine logic in the application itself.
    36  
    37  Run mode defaults to "dev".
    38  
    39  You can run a specific suite (and function) by specifying a third parameter.
    40  For example, to run all of UserTest:
    41  
    42      revel test outspoken test UserTest
    43  
    44  or one of UserTest's methods:
    45  
    46      revel test outspoken test UserTest.Test1
    47  `,
    48  }
    49  
    50  func init() {
    51  	cmdTest.RunWith = testApp
    52  	cmdTest.UpdateConfig = updateTestConfig
    53  }
    54  
    55  // Called to update the config command with from the older stype.
    56  func updateTestConfig(c *model.CommandConfig, args []string) bool {
    57  	c.Index = model.TEST
    58  	if len(args) == 0 && c.Test.ImportPath != "" {
    59  		return true
    60  	}
    61  
    62  	// The full test runs
    63  	// revel test <import path> (run mode) (suite(.function))
    64  	if len(args) < 1 {
    65  		return false
    66  	}
    67  	c.Test.ImportPath = args[0]
    68  	if len(args) > 1 {
    69  		c.Test.Mode = args[1]
    70  	}
    71  	if len(args) > 2 {
    72  		c.Test.Function = args[2]
    73  	}
    74  	return true
    75  }
    76  
    77  // Called to test the application.
    78  func testApp(c *model.CommandConfig) (err error) {
    79  	mode := DefaultRunMode
    80  	if c.Test.Mode != "" {
    81  		mode = c.Test.Mode
    82  	}
    83  
    84  	// Find and parse app.conf
    85  	revelPath, err := model.NewRevelPaths(mode, c.ImportPath, c.AppPath, model.NewWrappedRevelCallback(nil, c.PackageResolver))
    86  	if err != nil {
    87  		return
    88  	}
    89  
    90  	// todo Ensure that the testrunner is loaded in this mode.
    91  
    92  	// Create a directory to hold the test result files.
    93  	resultPath := filepath.Join(revelPath.BasePath, "test-results")
    94  	if err = os.RemoveAll(resultPath); err != nil {
    95  		return utils.NewBuildError("Failed to remove test result directory ", "path", resultPath, "error", err)
    96  	}
    97  	if err = os.Mkdir(resultPath, 0777); err != nil {
    98  		return utils.NewBuildError("Failed to create test result directory ", "path", resultPath, "error", err)
    99  	}
   100  
   101  	// Direct all the output into a file in the test-results directory.
   102  	file, err := os.OpenFile(filepath.Join(resultPath, "app.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
   103  	if err != nil {
   104  		return utils.NewBuildError("Failed to create test result log file: ", "error", err)
   105  	}
   106  
   107  	app, reverr := harness.Build(c, revelPath)
   108  	if reverr != nil {
   109  		return utils.NewBuildIfError(reverr, "Error building: ")
   110  	}
   111  	var paths []byte
   112  	if len(app.PackagePathMap) > 0 {
   113  		paths, _ = json.Marshal(app.PackagePathMap)
   114  	}
   115  	runMode := fmt.Sprintf(`{"mode":"%s", "specialUseFlag":%v,"packagePathMap":%s}`, app.Paths.RunMode, c.GetVerbose(), string(paths))
   116  	if c.HistoricMode {
   117  		runMode = app.Paths.RunMode
   118  	}
   119  	cmd := app.Cmd(runMode)
   120  	cmd.Dir = c.AppPath
   121  
   122  	cmd.Stderr = io.MultiWriter(cmd.Stderr, file)
   123  	cmd.Stdout = io.MultiWriter(cmd.Stderr, file)
   124  
   125  	// Start the app...
   126  	if err := cmd.Start(c); err != nil {
   127  		return utils.NewBuildError("Unable to start server", "error", err)
   128  	}
   129  	defer cmd.Kill()
   130  
   131  	httpAddr := revelPath.HTTPAddr
   132  	if httpAddr == "" {
   133  		httpAddr = "localhost"
   134  	}
   135  
   136  	httpProto := "http"
   137  	if revelPath.HTTPSsl {
   138  		httpProto = "https"
   139  	}
   140  
   141  	// Get a list of tests
   142  	baseURL := fmt.Sprintf("%s://%s:%d", httpProto, httpAddr, revelPath.HTTPPort)
   143  
   144  	utils.Logger.Infof("Testing %s (%s) in %s mode URL %s \n", revelPath.AppName, revelPath.ImportPath, mode, baseURL)
   145  	testSuites, _ := getTestsList(baseURL)
   146  
   147  	// If a specific TestSuite[.Method] is specified, only run that suite/test
   148  	if c.Test.Function != "" {
   149  		testSuites = filterTestSuites(testSuites, c.Test.Function)
   150  	}
   151  
   152  	testSuiteCount := len(*testSuites)
   153  	fmt.Printf("\n%d test suite%s to run.\n", testSuiteCount, pluralize(testSuiteCount, "", "s"))
   154  	fmt.Println()
   155  
   156  	// Run each suite.
   157  	failedResults, overallSuccess := runTestSuites(revelPath, baseURL, resultPath, testSuites)
   158  
   159  	fmt.Println()
   160  	if overallSuccess {
   161  		writeResultFile(resultPath, "result.passed", "passed")
   162  		fmt.Println("All Tests Passed.")
   163  	} else {
   164  		for _, failedResult := range *failedResults {
   165  			fmt.Printf("Failures:\n")
   166  			for _, result := range failedResult.Results {
   167  				if !result.Passed {
   168  					fmt.Printf("%s.%s\n", failedResult.Name, result.Name)
   169  					fmt.Printf("%s\n\n", result.ErrorSummary)
   170  				}
   171  			}
   172  		}
   173  		writeResultFile(resultPath, "result.failed", "failed")
   174  		utils.Logger.Errorf("Some tests failed.  See file://%s for results.", resultPath)
   175  	}
   176  
   177  	return
   178  }
   179  
   180  // Outputs the results to a file.
   181  func writeResultFile(resultPath, name, content string) {
   182  	if err := ioutil.WriteFile(filepath.Join(resultPath, name), []byte(content), 0666); err != nil {
   183  		utils.Logger.Errorf("Failed to write result file %s: %s", filepath.Join(resultPath, name), err)
   184  	}
   185  }
   186  
   187  // Determines if response should be plural.
   188  func pluralize(num int, singular, plural string) string {
   189  	if num == 1 {
   190  		return singular
   191  	}
   192  	return plural
   193  }
   194  
   195  // Filters test suites and individual tests to match
   196  // the parsed command line parameter.
   197  func filterTestSuites(suites *[]tests.TestSuiteDesc, suiteArgument string) *[]tests.TestSuiteDesc {
   198  	var suiteName, testName string
   199  	argArray := strings.Split(suiteArgument, ".")
   200  	suiteName = argArray[0]
   201  	if suiteName == "" {
   202  		return suites
   203  	}
   204  	if len(argArray) == 2 {
   205  		testName = argArray[1]
   206  	}
   207  	for _, suite := range *suites {
   208  		if suite.Name != suiteName {
   209  			continue
   210  		}
   211  		if testName == "" {
   212  			return &[]tests.TestSuiteDesc{suite}
   213  		}
   214  		// Only run a particular test in a suite
   215  		for _, test := range suite.Tests {
   216  			if test.Name != testName {
   217  				continue
   218  			}
   219  			return &[]tests.TestSuiteDesc{
   220  				{
   221  					Name:  suite.Name,
   222  					Tests: []tests.TestDesc{test},
   223  				},
   224  			}
   225  		}
   226  		utils.Logger.Errorf("Couldn't find test %s in suite %s", testName, suiteName)
   227  	}
   228  	utils.Logger.Errorf("Couldn't find test suite %s", suiteName)
   229  	return nil
   230  }
   231  
   232  // Get a list of tests from server.
   233  // Since this is the first request to the server, retry/sleep a couple times
   234  // in case it hasn't finished starting up yet.
   235  func getTestsList(baseURL string) (*[]tests.TestSuiteDesc, error) {
   236  	var (
   237  		err        error
   238  		resp       *http.Response
   239  		testSuites []tests.TestSuiteDesc
   240  	)
   241  	for i := 0; ; i++ {
   242  		if resp, err = http.Get(baseURL + "/@tests.list"); err == nil {
   243  			if resp.StatusCode == http.StatusOK {
   244  				break
   245  			}
   246  		}
   247  		if i < 3 {
   248  			time.Sleep(3 * time.Second)
   249  			continue
   250  		}
   251  		if err != nil {
   252  			utils.Logger.Fatalf("Failed to request test list: %s %s", baseURL, err)
   253  		} else {
   254  			utils.Logger.Fatalf("Failed to request test list: non-200 response %s", baseURL)
   255  		}
   256  	}
   257  	defer func() {
   258  		_ = resp.Body.Close()
   259  	}()
   260  
   261  	err = json.NewDecoder(resp.Body).Decode(&testSuites)
   262  
   263  	return &testSuites, err
   264  }
   265  
   266  // Run the testsuites using the container.
   267  func runTestSuites(paths *model.RevelContainer, baseURL, resultPath string, testSuites *[]tests.TestSuiteDesc) (*[]tests.TestSuiteResult, bool) {
   268  	// We can determine the testsuite location by finding the test module and extracting the data from it
   269  	resultFilePath := filepath.Join(paths.ModulePathMap["testrunner"].Path, "app", "views", "TestRunner/SuiteResult.html")
   270  
   271  	var (
   272  		overallSuccess = true
   273  		failedResults  []tests.TestSuiteResult
   274  	)
   275  	for _, suite := range *testSuites {
   276  		// Print the name of the suite we're running.
   277  		name := suite.Name
   278  		if len(name) > 22 {
   279  			name = name[:19] + "..."
   280  		}
   281  		fmt.Printf("%-22s", name)
   282  
   283  		// Run every test.
   284  		startTime := time.Now()
   285  		suiteResult := tests.TestSuiteResult{Name: suite.Name, Passed: true}
   286  		for _, test := range suite.Tests {
   287  			testURL := baseURL + "/@tests/" + suite.Name + "/" + test.Name
   288  			resp, err := http.Get(testURL)
   289  			if err != nil {
   290  				utils.Logger.Errorf("Failed to fetch test result at url %s: %s", testURL, err)
   291  			}
   292  			defer func() {
   293  				_ = resp.Body.Close()
   294  			}()
   295  
   296  			var testResult tests.TestResult
   297  			err = json.NewDecoder(resp.Body).Decode(&testResult)
   298  			if err == nil && !testResult.Passed {
   299  				suiteResult.Passed = false
   300  				utils.Logger.Error("Test Failed", "suite", suite.Name, "test", test.Name)
   301  				fmt.Printf("   %s.%s : FAILED\n", suite.Name, test.Name)
   302  			} else {
   303  				fmt.Printf("   %s.%s : PASSED\n", suite.Name, test.Name)
   304  			}
   305  			suiteResult.Results = append(suiteResult.Results, testResult)
   306  		}
   307  		overallSuccess = overallSuccess && suiteResult.Passed
   308  
   309  		// Print result.  (Just PASSED or FAILED, and the time taken)
   310  		suiteResultStr, suiteAlert := "PASSED", ""
   311  		if !suiteResult.Passed {
   312  			suiteResultStr, suiteAlert = "FAILED", "!"
   313  			failedResults = append(failedResults, suiteResult)
   314  		}
   315  		fmt.Printf("%8s%3s%6ds\n", suiteResultStr, suiteAlert, int(time.Since(startTime).Seconds()))
   316  		// Create the result HTML file.
   317  		suiteResultFilename := filepath.Join(resultPath,
   318  			fmt.Sprintf("%s.%s.html", suite.Name, strings.ToLower(suiteResultStr)))
   319  		if err := utils.RenderTemplate(suiteResultFilename, resultFilePath, suiteResult); err != nil {
   320  			utils.Logger.Error("Failed to render template", "error", err)
   321  		}
   322  	}
   323  
   324  	return &failedResults, overallSuccess
   325  }