github.com/jd-ly/cmd@v1.0.10/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/jd-ly/cmd/harness" 19 "github.com/jd-ly/cmd/model" 20 "github.com/jd-ly/cmd/tests" 21 "github.com/jd-ly/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 revel_path, 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(revel_path.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, revel_path) 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.Verbose, 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 var httpAddr = revel_path.HTTPAddr 132 if httpAddr == "" { 133 httpAddr = "localhost" 134 } 135 136 var httpProto = "http" 137 if revel_path.HTTPSsl { 138 httpProto = "https" 139 } 140 141 // Get a list of tests 142 var baseURL = fmt.Sprintf("%s://%s:%d", httpProto, httpAddr, revel_path.HTTPPort) 143 144 utils.Logger.Infof("Testing %s (%s) in %s mode URL %s \n", revel_path.AppName, revel_path.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(revel_path, 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 269 // We can determine the testsuite location by finding the test module and extracting the data from it 270 resultFilePath := filepath.Join(paths.ModulePathMap["testrunner"].Path, "app", "views", "TestRunner/SuiteResult.html") 271 272 var ( 273 overallSuccess = true 274 failedResults []tests.TestSuiteResult 275 ) 276 for _, suite := range *testSuites { 277 // Print the name of the suite we're running. 278 name := suite.Name 279 if len(name) > 22 { 280 name = name[:19] + "..." 281 } 282 fmt.Printf("%-22s", name) 283 284 // Run every test. 285 startTime := time.Now() 286 suiteResult := tests.TestSuiteResult{Name: suite.Name, Passed: true} 287 for _, test := range suite.Tests { 288 testURL := baseURL + "/@tests/" + suite.Name + "/" + test.Name 289 resp, err := http.Get(testURL) 290 if err != nil { 291 utils.Logger.Errorf("Failed to fetch test result at url %s: %s", testURL, err) 292 } 293 defer func() { 294 _ = resp.Body.Close() 295 }() 296 297 var testResult tests.TestResult 298 err = json.NewDecoder(resp.Body).Decode(&testResult) 299 if err == nil && !testResult.Passed { 300 suiteResult.Passed = false 301 utils.Logger.Error("Test Failed", "suite", suite.Name, "test", test.Name) 302 fmt.Printf(" %s.%s : FAILED\n", suite.Name, test.Name) 303 } else { 304 fmt.Printf(" %s.%s : PASSED\n", suite.Name, test.Name) 305 } 306 suiteResult.Results = append(suiteResult.Results, testResult) 307 } 308 overallSuccess = overallSuccess && suiteResult.Passed 309 310 // Print result. (Just PASSED or FAILED, and the time taken) 311 suiteResultStr, suiteAlert := "PASSED", "" 312 if !suiteResult.Passed { 313 suiteResultStr, suiteAlert = "FAILED", "!" 314 failedResults = append(failedResults, suiteResult) 315 } 316 fmt.Printf("%8s%3s%6ds\n", suiteResultStr, suiteAlert, int(time.Since(startTime).Seconds())) 317 // Create the result HTML file. 318 suiteResultFilename := filepath.Join(resultPath, 319 fmt.Sprintf("%s.%s.html", suite.Name, strings.ToLower(suiteResultStr))) 320 if err := utils.RenderTemplate(suiteResultFilename, resultFilePath, suiteResult); err != nil { 321 utils.Logger.Error("Failed to render template", "error", err) 322 } 323 } 324 325 return &failedResults, overallSuccess 326 }