github.com/terhitormanen/cmd@v1.1.4/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/terhitormanen/cmd/harness" 19 "github.com/terhitormanen/cmd/model" 20 "github.com/terhitormanen/cmd/tests" 21 "github.com/terhitormanen/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 }