| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 library browser; | |
| 5 | |
| 6 import "dart:async"; | |
| 7 import "dart:convert" show LineSplitter, UTF8, JSON; | |
| 8 import "dart:core"; | |
| 9 import "dart:io"; | |
| 10 | |
| 11 import 'android.dart'; | |
| 12 import 'http_server.dart'; | |
| 13 import 'path.dart'; | |
| 14 import 'utils.dart'; | |
| 15 | |
| 16 class BrowserOutput { | |
| 17 final StringBuffer stdout = new StringBuffer(); | |
| 18 final StringBuffer stderr = new StringBuffer(); | |
| 19 final StringBuffer eventLog = new StringBuffer(); | |
| 20 } | |
| 21 | |
| 22 /** Class describing the interface for communicating with browsers. */ | |
| 23 abstract class Browser { | |
| 24 BrowserOutput _allBrowserOutput = new BrowserOutput(); | |
| 25 BrowserOutput _testBrowserOutput = new BrowserOutput(); | |
| 26 | |
| 27 // This is called after the process is closed, before the done future | |
| 28 // is completed. | |
| 29 // Subclasses can use this to cleanup any browser specific resources | |
| 30 // (temp directories, profiles, etc). The function is expected to do | |
| 31 // it's work synchronously. | |
| 32 Function _cleanup; | |
| 33 | |
| 34 /** The version of the browser - normally set when starting a browser */ | |
| 35 String version = ""; | |
| 36 | |
| 37 // The path to the browser executable. | |
| 38 String _binary; | |
| 39 | |
| 40 /** | |
| 41 * The underlying process - don't mess directly with this if you don't | |
| 42 * know what you are doing (this is an interactive process that needs | |
| 43 * special treatment to not leak). | |
| 44 */ | |
| 45 Process process; | |
| 46 | |
| 47 Function logger; | |
| 48 | |
| 49 /** | |
| 50 * Id of the browser | |
| 51 */ | |
| 52 String id; | |
| 53 | |
| 54 /** | |
| 55 * Delete the browser specific caches on startup. | |
| 56 * Browser specific implementations are free to ignore this. | |
| 57 */ | |
| 58 static bool deleteCache = false; | |
| 59 | |
| 60 /** Print everything (stdout, stderr, usageLog) whenever we add to it */ | |
| 61 bool debugPrint = false; | |
| 62 | |
| 63 // This future returns when the process exits. It is also the return value | |
| 64 // of close() | |
| 65 Future done; | |
| 66 | |
| 67 Browser(); | |
| 68 | |
| 69 factory Browser.byName(String name, | |
| 70 String executablePath, | |
| 71 [bool checkedMode = false]) { | |
| 72 var browser; | |
| 73 if (name == 'firefox') { | |
| 74 browser = new Firefox(); | |
| 75 } else if (name == 'chrome') { | |
| 76 browser = new Chrome(); | |
| 77 } else if (name == 'dartium') { | |
| 78 browser = new Dartium(checkedMode); | |
| 79 } else if (name == 'safari') { | |
| 80 browser = new Safari(); | |
| 81 } else if (name == 'safarimobilesim') { | |
| 82 browser = new SafariMobileSimulator(); | |
| 83 } else if (name.startsWith('ie')) { | |
| 84 browser = new IE(); | |
| 85 } else { | |
| 86 throw "Non supported browser"; | |
| 87 } | |
| 88 browser._binary = executablePath; | |
| 89 return browser; | |
| 90 } | |
| 91 | |
| 92 static const List<String> SUPPORTED_BROWSERS = | |
| 93 const ['safari', 'ff', 'firefox', 'chrome', 'ie9', 'ie10', | |
| 94 'ie11', 'dartium']; | |
| 95 | |
| 96 static const List<String> BROWSERS_WITH_WINDOW_SUPPORT = | |
| 97 const ['ie11', 'ie10']; | |
| 98 | |
| 99 // TODO(kustermann): add standard support for chrome on android | |
| 100 static bool supportedBrowser(String name) { | |
| 101 return SUPPORTED_BROWSERS.contains(name); | |
| 102 } | |
| 103 | |
| 104 void _logEvent(String event) { | |
| 105 String toLog = "$this ($id) - $event \n"; | |
| 106 if (debugPrint) print("usageLog: $toLog"); | |
| 107 if (logger != null) logger(toLog); | |
| 108 | |
| 109 _allBrowserOutput.eventLog.write(toLog); | |
| 110 _testBrowserOutput.eventLog.write(toLog); | |
| 111 } | |
| 112 | |
| 113 void _addStdout(String output) { | |
| 114 if (debugPrint) print("stdout: $output"); | |
| 115 | |
| 116 _allBrowserOutput.stdout.write(output); | |
| 117 _testBrowserOutput.stdout.write(output); | |
| 118 } | |
| 119 | |
| 120 void _addStderr(String output) { | |
| 121 if (debugPrint) print("stderr: $output"); | |
| 122 | |
| 123 _allBrowserOutput.stderr.write(output); | |
| 124 _testBrowserOutput.stderr.write(output); | |
| 125 } | |
| 126 | |
| 127 Future close() { | |
| 128 _logEvent("Close called on browser"); | |
| 129 if (process != null) { | |
| 130 if (process.kill(ProcessSignal.SIGKILL)) { | |
| 131 _logEvent("Successfully sent kill signal to process."); | |
| 132 } else { | |
| 133 _logEvent("Sending kill signal failed."); | |
| 134 } | |
| 135 return done; | |
| 136 } else { | |
| 137 _logEvent("The process is already dead."); | |
| 138 return new Future.value(true); | |
| 139 } | |
| 140 } | |
| 141 | |
| 142 /** | |
| 143 * Start the browser using the supplied argument. | |
| 144 * This sets up the error handling and usage logging. | |
| 145 */ | |
| 146 Future<bool> startBrowser(String command, | |
| 147 List<String> arguments, | |
| 148 {Map<String,String> environment}) { | |
| 149 return Process.start(command, arguments, environment: environment) | |
| 150 .then((startedProcess) { | |
| 151 process = startedProcess; | |
| 152 // Used to notify when exiting, and as a return value on calls to | |
| 153 // close(). | |
| 154 var doneCompleter = new Completer(); | |
| 155 done = doneCompleter.future; | |
| 156 | |
| 157 Completer stdoutDone = new Completer(); | |
| 158 Completer stderrDone = new Completer(); | |
| 159 | |
| 160 bool stdoutIsDone = false; | |
| 161 bool stderrIsDone = false; | |
| 162 StreamSubscription stdoutSubscription; | |
| 163 StreamSubscription stderrSubscription; | |
| 164 | |
| 165 // This timer is used to close stdio to the subprocess once we got | |
| 166 // the exitCode. Sometimes descendants of the subprocess keep stdio | |
| 167 // handles alive even though the direct subprocess is dead. | |
| 168 Timer watchdogTimer; | |
| 169 | |
| 170 void closeStdout([_]){ | |
| 171 if (!stdoutIsDone) { | |
| 172 stdoutDone.complete(); | |
| 173 stdoutIsDone = true; | |
| 174 | |
| 175 if (stderrIsDone && watchdogTimer != null) { | |
| 176 watchdogTimer.cancel(); | |
| 177 } | |
| 178 } | |
| 179 } | |
| 180 | |
| 181 void closeStderr([_]) { | |
| 182 if (!stderrIsDone) { | |
| 183 stderrDone.complete(); | |
| 184 stderrIsDone = true; | |
| 185 | |
| 186 if (stdoutIsDone && watchdogTimer != null) { | |
| 187 watchdogTimer.cancel(); | |
| 188 } | |
| 189 } | |
| 190 } | |
| 191 | |
| 192 stdoutSubscription = | |
| 193 process.stdout.transform(UTF8.decoder).listen((data) { | |
| 194 _addStdout(data); | |
| 195 }, onError: (error) { | |
| 196 // This should _never_ happen, but we really want this in the log | |
| 197 // if it actually does due to dart:io or vm bug. | |
| 198 _logEvent("An error occured in the process stdout handling: $error"); | |
| 199 }, onDone: closeStdout); | |
| 200 | |
| 201 stderrSubscription = | |
| 202 process.stderr.transform(UTF8.decoder).listen((data) { | |
| 203 _addStderr(data); | |
| 204 }, onError: (error) { | |
| 205 // This should _never_ happen, but we really want this in the log | |
| 206 // if it actually does due to dart:io or vm bug. | |
| 207 _logEvent("An error occured in the process stderr handling: $error"); | |
| 208 }, onDone: closeStderr); | |
| 209 | |
| 210 process.exitCode.then((exitCode) { | |
| 211 _logEvent("Browser closed with exitcode $exitCode"); | |
| 212 | |
| 213 if (!stdoutIsDone || !stderrIsDone) { | |
| 214 watchdogTimer = new Timer(MAX_STDIO_DELAY, () { | |
| 215 DebugLogger.warning( | |
| 216 "$MAX_STDIO_DELAY_PASSED_MESSAGE (browser: $this)"); | |
| 217 watchdogTimer = null; | |
| 218 stdoutSubscription.cancel(); | |
| 219 stderrSubscription.cancel(); | |
| 220 closeStdout(); | |
| 221 closeStderr(); | |
| 222 }); | |
| 223 } | |
| 224 | |
| 225 Future.wait([stdoutDone.future, stderrDone.future]).then((_) { | |
| 226 process = null; | |
| 227 if (_cleanup != null) { | |
| 228 _cleanup(); | |
| 229 } | |
| 230 }).catchError((error) { | |
| 231 _logEvent("Error closing browsers: $error"); | |
| 232 }).whenComplete(() => doneCompleter.complete(true)); | |
| 233 }); | |
| 234 return true; | |
| 235 }).catchError((error) { | |
| 236 _logEvent("Running $command $arguments failed with $error"); | |
| 237 return false; | |
| 238 }); | |
| 239 } | |
| 240 | |
| 241 /** | |
| 242 * Get the output that was written so far to stdout/stderr/eventLog. | |
| 243 */ | |
| 244 BrowserOutput get allBrowserOutput => _allBrowserOutput; | |
| 245 BrowserOutput get testBrowserOutput => _testBrowserOutput; | |
| 246 | |
| 247 void resetTestBrowserOutput() { | |
| 248 _testBrowserOutput = new BrowserOutput(); | |
| 249 } | |
| 250 | |
| 251 /** | |
| 252 * Add useful info about the browser to the _testBrowserOutput.stdout, | |
| 253 * where it will be reported for failing tests. Used to report which | |
| 254 * android device a failing test is running on. | |
| 255 */ | |
| 256 void logBrowserInfoToTestBrowserOutput() { } | |
| 257 | |
| 258 String toString(); | |
| 259 | |
| 260 /** Starts the browser loading the given url */ | |
| 261 Future<bool> start(String url); | |
| 262 } | |
| 263 | |
| 264 class Safari extends Browser { | |
| 265 /** | |
| 266 * We get the safari version by parsing a version file | |
| 267 */ | |
| 268 static const String versionFile = | |
| 269 "/Applications/Safari.app/Contents/version.plist"; | |
| 270 | |
| 271 /** | |
| 272 * Directories where safari stores state. We delete these if the deleteCache | |
| 273 * is set | |
| 274 */ | |
| 275 static const List<String> CACHE_DIRECTORIES = | |
| 276 const ["Library/Caches/com.apple.Safari", | |
| 277 "Library/Safari", | |
| 278 "Library/Saved Application State/com.apple.Safari.savedState", | |
| 279 "Library/Caches/Metadata/Safari"]; | |
| 280 | |
| 281 | |
| 282 Future<bool> allowPopUps() { | |
| 283 var command = "defaults"; | |
| 284 var args = ["write", "com.apple.safari", | |
| 285 "com.apple.Safari.ContentPageGroupIdentifier." | |
| 286 "WebKit2JavaScriptCanOpenWindowsAutomatically", | |
| 287 "1"]; | |
| 288 return Process.run(command, args).then((result) { | |
| 289 if (result.exitCode != 0) { | |
| 290 _logEvent("Could not disable pop-up blocking for safari"); | |
| 291 return false; | |
| 292 } | |
| 293 return true; | |
| 294 }); | |
| 295 } | |
| 296 | |
| 297 Future<bool> deleteIfExists(Iterator<String> paths) { | |
| 298 if (!paths.moveNext()) return new Future.value(true); | |
| 299 Directory directory = new Directory(paths.current); | |
| 300 return directory.exists().then((exists) { | |
| 301 if (exists) { | |
| 302 _logEvent("Deleting ${paths.current}"); | |
| 303 return directory.delete(recursive: true) | |
| 304 .then((_) => deleteIfExists(paths)) | |
| 305 .catchError((error) { | |
| 306 _logEvent("Failure trying to delete ${paths.current}: $error"); | |
| 307 return false; | |
| 308 }); | |
| 309 } else { | |
| 310 _logEvent("${paths.current} is not present"); | |
| 311 return deleteIfExists(paths); | |
| 312 } | |
| 313 }); | |
| 314 } | |
| 315 | |
| 316 // Clears the cache if the static deleteCache flag is set. | |
| 317 // Returns false if the command to actually clear the cache did not complete. | |
| 318 Future<bool> clearCache() { | |
| 319 if (!Browser.deleteCache) return new Future.value(true); | |
| 320 var home = Platform.environment['HOME']; | |
| 321 Iterator iterator = CACHE_DIRECTORIES.map((s) => "$home/$s").iterator; | |
| 322 return deleteIfExists(iterator); | |
| 323 } | |
| 324 | |
| 325 Future<String> getVersion() { | |
| 326 /** | |
| 327 * Example of the file: | |
| 328 * <?xml version="1.0" encoding="UTF-8"?> | |
| 329 * <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.co
m/DTDs/PropertyList-1.0.dtd"> | |
| 330 * <plist version="1.0"> | |
| 331 * <dict> | |
| 332 * <key>BuildVersion</key> | |
| 333 * <string>2</string> | |
| 334 * <key>CFBundleShortVersionString</key> | |
| 335 * <string>6.0.4</string> | |
| 336 * <key>CFBundleVersion</key> | |
| 337 * <string>8536.29.13</string> | |
| 338 * <key>ProjectName</key> | |
| 339 * <string>WebBrowser</string> | |
| 340 * <key>SourceVersion</key> | |
| 341 * <string>7536029013000000</string> | |
| 342 * </dict> | |
| 343 * </plist> | |
| 344 */ | |
| 345 File f = new File(versionFile); | |
| 346 return f.readAsLines().then((content) { | |
| 347 bool versionOnNextLine = false; | |
| 348 for (var line in content) { | |
| 349 if (versionOnNextLine) return line; | |
| 350 if (line.contains("CFBundleShortVersionString")) { | |
| 351 versionOnNextLine = true; | |
| 352 } | |
| 353 } | |
| 354 return null; | |
| 355 }); | |
| 356 } | |
| 357 | |
| 358 void _createLaunchHTML(var path, var url) { | |
| 359 var file = new File("${path}/launch.html"); | |
| 360 var randomFile = file.openSync(mode: FileMode.WRITE); | |
| 361 var content = '<script language="JavaScript">location = "$url"</script>'; | |
| 362 randomFile.writeStringSync(content); | |
| 363 randomFile.close(); | |
| 364 } | |
| 365 | |
| 366 Future<bool> start(String url) { | |
| 367 _logEvent("Starting Safari browser on: $url"); | |
| 368 return allowPopUps().then((success) { | |
| 369 if (!success) { | |
| 370 return false; | |
| 371 } | |
| 372 return clearCache().then((cleared) { | |
| 373 if (!cleared) { | |
| 374 _logEvent("Could not clear cache"); | |
| 375 return false; | |
| 376 } | |
| 377 // Get the version and log that. | |
| 378 return getVersion().then((version) { | |
| 379 _logEvent("Got version: $version"); | |
| 380 return Directory.systemTemp.createTemp().then((userDir) { | |
| 381 _cleanup = () { userDir.deleteSync(recursive: true); }; | |
| 382 _createLaunchHTML(userDir.path, url); | |
| 383 var args = ["${userDir.path}/launch.html"]; | |
| 384 return startBrowser(_binary, args); | |
| 385 }); | |
| 386 }).catchError((error) { | |
| 387 _logEvent("Running $_binary --version failed with $error"); | |
| 388 return false; | |
| 389 }); | |
| 390 }); | |
| 391 }); | |
| 392 } | |
| 393 | |
| 394 String toString() => "Safari"; | |
| 395 } | |
| 396 | |
| 397 | |
| 398 class Chrome extends Browser { | |
| 399 String _version = "Version not found yet"; | |
| 400 | |
| 401 Map<String, String> _getEnvironment() => null; | |
| 402 | |
| 403 Future<bool> _getVersion() { | |
| 404 if (Platform.isWindows) { | |
| 405 // The version flag does not work on windows. | |
| 406 // See issue: | |
| 407 // https://code.google.com/p/chromium/issues/detail?id=158372 | |
| 408 // The registry hack does not seem to work. | |
| 409 _version = "Can't get version on windows"; | |
| 410 // We still validate that the binary exists so that we can give good | |
| 411 // feedback. | |
| 412 return new File(_binary).exists().then((exists) { | |
| 413 if (!exists) { | |
| 414 _logEvent("Chrome binary not available."); | |
| 415 _logEvent("Make sure $_binary is a valid program for running chrome"); | |
| 416 } | |
| 417 return exists; | |
| 418 }); | |
| 419 } | |
| 420 return Process.run(_binary, ["--version"]).then((var versionResult) { | |
| 421 if (versionResult.exitCode != 0) { | |
| 422 _logEvent("Failed to chrome get version"); | |
| 423 _logEvent("Make sure $_binary is a valid program for running chrome"); | |
| 424 return false; | |
| 425 } | |
| 426 _version = versionResult.stdout; | |
| 427 return true; | |
| 428 }); | |
| 429 } | |
| 430 | |
| 431 | |
| 432 Future<bool> start(String url) { | |
| 433 _logEvent("Starting chrome browser on: $url"); | |
| 434 // Get the version and log that. | |
| 435 return _getVersion().then((success) { | |
| 436 if (!success) return false; | |
| 437 _logEvent("Got version: $_version"); | |
| 438 | |
| 439 return Directory.systemTemp.createTemp().then((userDir) { | |
| 440 _cleanup = () { userDir.deleteSync(recursive: true); }; | |
| 441 var args = ["--user-data-dir=${userDir.path}", url, | |
| 442 "--disable-extensions", "--disable-popup-blocking", | |
| 443 "--bwsi", "--no-first-run"]; | |
| 444 return startBrowser(_binary, args, environment: _getEnvironment()); | |
| 445 }); | |
| 446 }).catchError((e) { | |
| 447 _logEvent("Running $_binary --version failed with $e"); | |
| 448 return false; | |
| 449 }); | |
| 450 } | |
| 451 | |
| 452 String toString() => "Chrome"; | |
| 453 } | |
| 454 | |
| 455 | |
| 456 class SafariMobileSimulator extends Safari { | |
| 457 /** | |
| 458 * Directories where safari simulator stores state. We delete these if the | |
| 459 * deleteCache is set | |
| 460 */ | |
| 461 static const List<String> CACHE_DIRECTORIES = | |
| 462 const ["Library/Application Support/iPhone Simulator/7.1/Applications"]; | |
| 463 | |
| 464 // Clears the cache if the static deleteCache flag is set. | |
| 465 // Returns false if the command to actually clear the cache did not complete. | |
| 466 Future<bool> clearCache() { | |
| 467 if (!Browser.deleteCache) return new Future.value(true); | |
| 468 var home = Platform.environment['HOME']; | |
| 469 Iterator iterator = CACHE_DIRECTORIES.map((s) => "$home/$s").iterator; | |
| 470 return deleteIfExists(iterator); | |
| 471 } | |
| 472 | |
| 473 Future<bool> start(String url) { | |
| 474 _logEvent("Starting safari mobile simulator browser on: $url"); | |
| 475 return clearCache().then((success) { | |
| 476 if (!success) { | |
| 477 _logEvent("Could not clear cache, exiting"); | |
| 478 return false; | |
| 479 } | |
| 480 var args = ["-SimulateApplication", | |
| 481 "/Applications/Xcode.app/Contents/Developer/Platforms/" | |
| 482 "iPhoneSimulator.platform/Developer/SDKs/" | |
| 483 "iPhoneSimulator7.1.sdk/Applications/MobileSafari.app/" | |
| 484 "MobileSafari", | |
| 485 "-u", url]; | |
| 486 return startBrowser(_binary, args) | |
| 487 .catchError((e) { | |
| 488 _logEvent("Running $_binary --version failed with $e"); | |
| 489 return false; | |
| 490 }); | |
| 491 }); | |
| 492 } | |
| 493 | |
| 494 String toString() => "SafariMobileSimulator"; | |
| 495 } | |
| 496 | |
| 497 | |
| 498 class Dartium extends Chrome { | |
| 499 final bool checkedMode; | |
| 500 | |
| 501 Dartium(this.checkedMode); | |
| 502 | |
| 503 Map<String, String> _getEnvironment() { | |
| 504 var environment = new Map<String,String>.from(Platform.environment); | |
| 505 // By setting this environment variable, dartium will forward "print()" | |
| 506 // calls in dart to the top-level javascript function "dartPrint()" if | |
| 507 // available. | |
| 508 environment['DART_FORWARDING_PRINT'] = '1'; | |
| 509 if (checkedMode) { | |
| 510 environment['DART_FLAGS'] = '--checked'; | |
| 511 } | |
| 512 return environment; | |
| 513 } | |
| 514 | |
| 515 String toString() => "Dartium"; | |
| 516 } | |
| 517 | |
| 518 class IE extends Browser { | |
| 519 Future<String> getVersion() { | |
| 520 var args = ["query", | |
| 521 "HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Internet Explorer", | |
| 522 "/v", | |
| 523 "svcVersion"]; | |
| 524 return Process.run("reg", args).then((result) { | |
| 525 if (result.exitCode == 0) { | |
| 526 // The string we get back looks like this: | |
| 527 // HKEY_LOCAL_MACHINE\Software\Microsoft\Internet Explorer | |
| 528 // version REG_SZ 9.0.8112.16421 | |
| 529 var findString = "REG_SZ"; | |
| 530 var index = result.stdout.indexOf(findString); | |
| 531 if (index > 0) { | |
| 532 return result.stdout.substring(index + findString.length).trim(); | |
| 533 } | |
| 534 } | |
| 535 return "Could not get the version of internet explorer"; | |
| 536 }); | |
| 537 } | |
| 538 | |
| 539 // Clears the recovery cache if the static deleteCache flag is set. | |
| 540 Future<bool> clearCache() { | |
| 541 if (!Browser.deleteCache) return new Future.value(true); | |
| 542 var localAppData = Platform.environment['LOCALAPPDATA']; | |
| 543 | |
| 544 Directory dir = new Directory("$localAppData\\Microsoft\\" | |
| 545 "Internet Explorer\\Recovery"); | |
| 546 return dir.delete(recursive: true) | |
| 547 .then((_) { return true; }) | |
| 548 .catchError((error) { | |
| 549 _logEvent("Deleting recovery dir failed with $error"); | |
| 550 return false; | |
| 551 }); | |
| 552 } | |
| 553 | |
| 554 Future<bool> start(String url) { | |
| 555 _logEvent("Starting ie browser on: $url"); | |
| 556 return clearCache().then((_) => getVersion()).then((version) { | |
| 557 _logEvent("Got version: $version"); | |
| 558 return startBrowser(_binary, [url]); | |
| 559 }); | |
| 560 } | |
| 561 String toString() => "IE"; | |
| 562 | |
| 563 } | |
| 564 | |
| 565 | |
| 566 class AndroidBrowserConfig { | |
| 567 final String name; | |
| 568 final String package; | |
| 569 final String activity; | |
| 570 final String action; | |
| 571 AndroidBrowserConfig(this.name, this.package, this.activity, this.action); | |
| 572 } | |
| 573 | |
| 574 | |
| 575 final contentShellOnAndroidConfig = new AndroidBrowserConfig( | |
| 576 'ContentShellOnAndroid', | |
| 577 'org.chromium.content_shell_apk', | |
| 578 '.ContentShellActivity', | |
| 579 'android.intent.action.VIEW'); | |
| 580 | |
| 581 | |
| 582 final dartiumOnAndroidConfig = new AndroidBrowserConfig( | |
| 583 'DartiumOnAndroid', | |
| 584 'com.google.android.apps.chrome', | |
| 585 '.Main', | |
| 586 'android.intent.action.VIEW'); | |
| 587 | |
| 588 | |
| 589 class AndroidBrowser extends Browser { | |
| 590 final bool checkedMode; | |
| 591 AdbDevice _adbDevice; | |
| 592 AndroidBrowserConfig _config; | |
| 593 | |
| 594 AndroidBrowser(this._adbDevice, this._config, this.checkedMode, apkPath) { | |
| 595 _binary = apkPath; | |
| 596 } | |
| 597 | |
| 598 Future<bool> start(String url) { | |
| 599 var intent = new Intent( | |
| 600 _config.action, _config.package, _config.activity, url); | |
| 601 return _adbDevice.waitForBootCompleted().then((_) { | |
| 602 return _adbDevice.forceStop(_config.package); | |
| 603 }).then((_) { | |
| 604 return _adbDevice.killAll(); | |
| 605 }).then((_) { | |
| 606 return _adbDevice.adbRoot(); | |
| 607 }).then((_) { | |
| 608 return _adbDevice.setProp("DART_FORWARDING_PRINT", "1"); | |
| 609 }).then((_) { | |
| 610 if (checkedMode) { | |
| 611 return _adbDevice.setProp("DART_FLAGS", "--checked"); | |
| 612 } else { | |
| 613 return _adbDevice.setProp("DART_FLAGS", ""); | |
| 614 } | |
| 615 }).then((_) { | |
| 616 return _adbDevice.installApk(new Path(_binary)); | |
| 617 }).then((_) { | |
| 618 return _adbDevice.startActivity(intent).then((_) => true); | |
| 619 }); | |
| 620 } | |
| 621 | |
| 622 Future<bool> close() { | |
| 623 if (_adbDevice != null) { | |
| 624 return _adbDevice.forceStop(_config.package).then((_) { | |
| 625 return _adbDevice.killAll().then((_) => true); | |
| 626 }); | |
| 627 } | |
| 628 return new Future.value(true); | |
| 629 } | |
| 630 | |
| 631 void logBrowserInfoToTestBrowserOutput() { | |
| 632 _testBrowserOutput.stdout.write( | |
| 633 'Android device id: ${_adbDevice.deviceId}\n'); | |
| 634 } | |
| 635 | |
| 636 String toString() => _config.name; | |
| 637 } | |
| 638 | |
| 639 | |
| 640 class AndroidChrome extends Browser { | |
| 641 static const String viewAction = 'android.intent.action.VIEW'; | |
| 642 static const String mainAction = 'android.intent.action.MAIN'; | |
| 643 static const String chromePackage = 'com.android.chrome'; | |
| 644 static const String browserPackage = 'com.android.browser'; | |
| 645 static const String firefoxPackage = 'org.mozilla.firefox'; | |
| 646 static const String turnScreenOnPackage = 'com.google.dart.turnscreenon'; | |
| 647 | |
| 648 AndroidEmulator _emulator; | |
| 649 AdbDevice _adbDevice; | |
| 650 | |
| 651 AndroidChrome(this._adbDevice); | |
| 652 | |
| 653 Future<bool> start(String url) { | |
| 654 var browserIntent = new Intent( | |
| 655 viewAction, browserPackage, '.BrowserActivity', url); | |
| 656 var chromeIntent = new Intent(viewAction, chromePackage, '.Main', url); | |
| 657 var firefoxIntent = new Intent(viewAction, firefoxPackage, '.App', url); | |
| 658 var turnScreenOnIntent = | |
| 659 new Intent(mainAction, turnScreenOnPackage, '.Main'); | |
| 660 | |
| 661 var testing_resources_dir = | |
| 662 new Path('third_party/android_testing_resources'); | |
| 663 if (!new Directory(testing_resources_dir.toNativePath()).existsSync()) { | |
| 664 DebugLogger.error("$testing_resources_dir doesn't exist. Exiting now."); | |
| 665 exit(1); | |
| 666 } | |
| 667 | |
| 668 var chromeAPK = testing_resources_dir.append('com.android.chrome-1.apk'); | |
| 669 var turnScreenOnAPK = testing_resources_dir.append('TurnScreenOn.apk'); | |
| 670 var chromeConfDir = testing_resources_dir.append('chrome_configuration'); | |
| 671 var chromeConfDirRemote = new Path('/data/user/0/com.android.chrome/'); | |
| 672 | |
| 673 return _adbDevice.waitForBootCompleted().then((_) { | |
| 674 return _adbDevice.forceStop(chromeIntent.package); | |
| 675 }).then((_) { | |
| 676 return _adbDevice.killAll(); | |
| 677 }).then((_) { | |
| 678 return _adbDevice.adbRoot(); | |
| 679 }).then((_) { | |
| 680 return _adbDevice.installApk(turnScreenOnAPK); | |
| 681 }).then((_) { | |
| 682 return _adbDevice.installApk(chromeAPK); | |
| 683 }).then((_) { | |
| 684 return _adbDevice.pushData(chromeConfDir, chromeConfDirRemote); | |
| 685 }).then((_) { | |
| 686 return _adbDevice.chmod('777', chromeConfDirRemote); | |
| 687 }).then((_) { | |
| 688 return _adbDevice.startActivity(turnScreenOnIntent).then((_) => true); | |
| 689 }).then((_) { | |
| 690 return _adbDevice.startActivity(chromeIntent).then((_) => true); | |
| 691 }); | |
| 692 } | |
| 693 | |
| 694 Future<bool> close() { | |
| 695 if (_adbDevice != null) { | |
| 696 return _adbDevice.forceStop(chromePackage).then((_) { | |
| 697 return _adbDevice.killAll().then((_) => true); | |
| 698 }); | |
| 699 } | |
| 700 return new Future.value(true); | |
| 701 } | |
| 702 | |
| 703 void logBrowserInfoToTestBrowserOutput() { | |
| 704 _testBrowserOutput.stdout.write( | |
| 705 'Android device id: ${_adbDevice.deviceId}\n'); | |
| 706 } | |
| 707 | |
| 708 String toString() => "chromeOnAndroid"; | |
| 709 } | |
| 710 | |
| 711 | |
| 712 class Firefox extends Browser { | |
| 713 static const String enablePopUp = | |
| 714 'user_pref("dom.disable_open_during_load", false);'; | |
| 715 static const String disableDefaultCheck = | |
| 716 'user_pref("browser.shell.checkDefaultBrowser", false);'; | |
| 717 static const String disableScriptTimeLimit = | |
| 718 'user_pref("dom.max_script_run_time", 0);'; | |
| 719 | |
| 720 void _createPreferenceFile(var path) { | |
| 721 var file = new File("${path.toString()}/user.js"); | |
| 722 var randomFile = file.openSync(mode: FileMode.WRITE); | |
| 723 randomFile.writeStringSync(enablePopUp); | |
| 724 randomFile.writeStringSync(disableDefaultCheck); | |
| 725 randomFile.writeStringSync(disableScriptTimeLimit); | |
| 726 randomFile.close(); | |
| 727 } | |
| 728 | |
| 729 Future<bool> start(String url) { | |
| 730 _logEvent("Starting firefox browser on: $url"); | |
| 731 // Get the version and log that. | |
| 732 return Process.run(_binary, ["--version"]).then((var versionResult) { | |
| 733 if (versionResult.exitCode != 0) { | |
| 734 _logEvent("Failed to firefox get version"); | |
| 735 _logEvent("Make sure $_binary is a valid program for running firefox"); | |
| 736 return new Future.value(false); | |
| 737 } | |
| 738 version = versionResult.stdout; | |
| 739 _logEvent("Got version: $version"); | |
| 740 | |
| 741 return Directory.systemTemp.createTemp().then((userDir) { | |
| 742 _createPreferenceFile(userDir.path); | |
| 743 _cleanup = () { userDir.deleteSync(recursive: true); }; | |
| 744 var args = ["-profile", "${userDir.path}", | |
| 745 "-no-remote", "-new-instance", url]; | |
| 746 return startBrowser(_binary, args); | |
| 747 | |
| 748 }); | |
| 749 }).catchError((e) { | |
| 750 _logEvent("Running $_binary --version failed with $e"); | |
| 751 return false; | |
| 752 }); | |
| 753 } | |
| 754 | |
| 755 String toString() => "Firefox"; | |
| 756 } | |
| 757 | |
| 758 | |
| 759 /** | |
| 760 * Describes the current state of a browser used for testing. | |
| 761 */ | |
| 762 class BrowserTestingStatus { | |
| 763 Browser browser; | |
| 764 BrowserTest currentTest; | |
| 765 | |
| 766 // This is currently not used for anything except for error reporting. | |
| 767 // Given the usefulness of this in debugging issues this should not be | |
| 768 // removed even when we have a really stable system. | |
| 769 BrowserTest lastTest; | |
| 770 bool timeout = false; | |
| 771 Timer nextTestTimeout; | |
| 772 Stopwatch timeSinceRestart = new Stopwatch(); | |
| 773 | |
| 774 BrowserTestingStatus(Browser this.browser); | |
| 775 } | |
| 776 | |
| 777 | |
| 778 /** | |
| 779 * Describes a single test to be run in the browser. | |
| 780 */ | |
| 781 class BrowserTest { | |
| 782 // TODO(ricow): Add timeout callback instead of the string passing hack. | |
| 783 Function doneCallback; | |
| 784 String url; | |
| 785 int timeout; | |
| 786 String lastKnownMessage = ''; | |
| 787 Stopwatch stopwatch; | |
| 788 | |
| 789 // This might be null | |
| 790 Duration delayUntilTestStarted; | |
| 791 | |
| 792 // We store this here for easy access when tests time out (instead of | |
| 793 // capturing this in a closure) | |
| 794 Timer timeoutTimer; | |
| 795 | |
| 796 // Used for debugging, this is simply a unique identifier assigned to each | |
| 797 // test. | |
| 798 int id; | |
| 799 static int _idCounter = 0; | |
| 800 | |
| 801 BrowserTest(this.url, this.doneCallback, this.timeout) { | |
| 802 id = _idCounter++; | |
| 803 } | |
| 804 | |
| 805 String toJSON() => JSON.encode({'url': url, | |
| 806 'id': id, | |
| 807 'isHtmlTest': false}); | |
| 808 } | |
| 809 | |
| 810 | |
| 811 /** | |
| 812 * Describes a test with a custom HTML page to be run in the browser. | |
| 813 */ | |
| 814 class HtmlTest extends BrowserTest { | |
| 815 List<String> expectedMessages; | |
| 816 | |
| 817 HtmlTest(url, doneCallback, timeout, this.expectedMessages) | |
| 818 : super(url, doneCallback, timeout) { } | |
| 819 | |
| 820 String toJSON() => JSON.encode({'url': url, | |
| 821 'id': id, | |
| 822 'isHtmlTest': true, | |
| 823 'expectedMessages': expectedMessages}); | |
| 824 } | |
| 825 | |
| 826 | |
| 827 /* Describes the output of running the test in a browser */ | |
| 828 class BrowserTestOutput { | |
| 829 final Duration delayUntilTestStarted; | |
| 830 final Duration duration; | |
| 831 | |
| 832 final String lastKnownMessage; | |
| 833 | |
| 834 final BrowserOutput browserOutput; | |
| 835 final bool didTimeout; | |
| 836 | |
| 837 BrowserTestOutput( | |
| 838 this.delayUntilTestStarted, this.duration, this.lastKnownMessage, | |
| 839 this.browserOutput, {this.didTimeout: false}); | |
| 840 } | |
| 841 | |
| 842 /** | |
| 843 * Encapsulates all the functionality for running tests in browsers. | |
| 844 * The interface is rather simple. After starting, the runner tests | |
| 845 * are simply added to the queue and a the supplied callbacks are called | |
| 846 * whenever a test completes. | |
| 847 */ | |
| 848 class BrowserTestRunner { | |
| 849 static const int MAX_NEXT_TEST_TIMEOUTS = 10; | |
| 850 static const Duration NEXT_TEST_TIMEOUT = const Duration(seconds: 60); | |
| 851 static const Duration RESTART_BROWSER_INTERVAL = const Duration(seconds: 60); | |
| 852 | |
| 853 final Map configuration; | |
| 854 | |
| 855 final String localIp; | |
| 856 String browserName; | |
| 857 final int maxNumBrowsers; | |
| 858 bool checkedMode; | |
| 859 // Used to send back logs from the browser (start, stop etc) | |
| 860 Function logger; | |
| 861 int browserIdCount = 0; | |
| 862 | |
| 863 bool underTermination = false; | |
| 864 int numBrowserGetTestTimeouts = 0; | |
| 865 | |
| 866 List<BrowserTest> testQueue = new List<BrowserTest>(); | |
| 867 Map<String, BrowserTestingStatus> browserStatus = | |
| 868 new Map<String, BrowserTestingStatus>(); | |
| 869 | |
| 870 var adbDeviceMapping = new Map<String, AdbDevice>(); | |
| 871 // This cache is used to guarantee that we never see double reporting. | |
| 872 // If we do we need to provide developers with this information. | |
| 873 // We don't add urls to the cache until we have run it. | |
| 874 Map<int, String> testCache = new Map<int, String>(); | |
| 875 Map<int, String> doubleReportingOutputs = new Map<int, String>(); | |
| 876 | |
| 877 BrowserTestingServer testingServer; | |
| 878 | |
| 879 /** | |
| 880 * The TestRunner takes the testingServer in as a constructor parameter in | |
| 881 * case we wish to have a testing server with different behavior (such as the | |
| 882 * case for performance testing. | |
| 883 */ | |
| 884 BrowserTestRunner(this.configuration, | |
| 885 this.localIp, | |
| 886 this.browserName, | |
| 887 this.maxNumBrowsers, | |
| 888 {BrowserTestingServer this.testingServer}) { | |
| 889 checkedMode = configuration['checked']; | |
| 890 } | |
| 891 | |
| 892 Future<bool> start() { | |
| 893 // If [browserName] doesn't support opening new windows, we use new iframes | |
| 894 // instead. | |
| 895 bool useIframe = | |
| 896 !Browser.BROWSERS_WITH_WINDOW_SUPPORT.contains(browserName); | |
| 897 if (testingServer == null) { | |
| 898 testingServer = new BrowserTestingServer( | |
| 899 configuration, localIp, useIframe); | |
| 900 } | |
| 901 return testingServer.start().then((_) { | |
| 902 testingServer.testDoneCallBack = handleResults; | |
| 903 testingServer.testStatusUpdateCallBack = handleStatusUpdate; | |
| 904 testingServer.testStartedCallBack = handleStarted; | |
| 905 testingServer.nextTestCallBack = getNextTest; | |
| 906 return getBrowsers().then((browsers) { | |
| 907 var futures = []; | |
| 908 for (var browser in browsers) { | |
| 909 var url = testingServer.getDriverUrl(browser.id); | |
| 910 var future = browser.start(url).then((success) { | |
| 911 if (success) { | |
| 912 var status = new BrowserTestingStatus(browser); | |
| 913 browserStatus[browser.id] = status; | |
| 914 status.nextTestTimeout = createNextTestTimer(status); | |
| 915 status.timeSinceRestart.start(); | |
| 916 } | |
| 917 return success; | |
| 918 }); | |
| 919 futures.add(future); | |
| 920 } | |
| 921 return Future.wait(futures).then((values) { | |
| 922 return !values.contains(false); | |
| 923 }); | |
| 924 }); | |
| 925 }); | |
| 926 } | |
| 927 | |
| 928 Future<List<Browser>> getBrowsers() { | |
| 929 // TODO(kustermann): This is a hackisch way to accomplish it and should | |
| 930 // be encapsulated | |
| 931 var browsersCompleter = new Completer(); | |
| 932 var androidBrowserCreationMapping = { | |
| 933 'chromeOnAndroid' : (AdbDevice device) => new AndroidChrome(device), | |
| 934 'ContentShellOnAndroid' : (AdbDevice device) => new AndroidBrowser( | |
| 935 device, | |
| 936 contentShellOnAndroidConfig, | |
| 937 checkedMode, | |
| 938 configuration['drt']), | |
| 939 'DartiumOnAndroid' : (AdbDevice device) => new AndroidBrowser( | |
| 940 device, | |
| 941 dartiumOnAndroidConfig, | |
| 942 checkedMode, | |
| 943 configuration['dartium']), | |
| 944 }; | |
| 945 if (androidBrowserCreationMapping.containsKey(browserName)) { | |
| 946 AdbHelper.listDevices().then((deviceIds) { | |
| 947 if (deviceIds.length > 0) { | |
| 948 var browsers = []; | |
| 949 for (int i = 0; i < deviceIds.length; i++) { | |
| 950 var id = "BROWSER$i"; | |
| 951 var device = new AdbDevice(deviceIds[i]); | |
| 952 adbDeviceMapping[id] = device; | |
| 953 var browser = androidBrowserCreationMapping[browserName](device); | |
| 954 browsers.add(browser); | |
| 955 // We store this in case we need to kill the browser. | |
| 956 browser.id = id; | |
| 957 } | |
| 958 browsersCompleter.complete(browsers); | |
| 959 } else { | |
| 960 throw new StateError("No android devices found."); | |
| 961 } | |
| 962 }); | |
| 963 } else { | |
| 964 var browsers = []; | |
| 965 for (int i = 0; i < maxNumBrowsers; i++) { | |
| 966 var id = "BROWSER$browserIdCount"; | |
| 967 browserIdCount++; | |
| 968 var browser = getInstance(); | |
| 969 browsers.add(browser); | |
| 970 // We store this in case we need to kill the browser. | |
| 971 browser.id = id; | |
| 972 } | |
| 973 browsersCompleter.complete(browsers); | |
| 974 } | |
| 975 return browsersCompleter.future; | |
| 976 } | |
| 977 | |
| 978 var timedOut = []; | |
| 979 | |
| 980 void handleResults(String browserId, String output, int testId) { | |
| 981 var status = browserStatus[browserId]; | |
| 982 if (testCache.containsKey(testId)) { | |
| 983 doubleReportingOutputs[testId] = output; | |
| 984 return; | |
| 985 } | |
| 986 | |
| 987 if (status == null || status.timeout) { | |
| 988 // We don't do anything, this browser is currently being killed and | |
| 989 // replaced. The browser here can be null if we decided to kill the | |
| 990 // browser. | |
| 991 } else if (status.currentTest != null) { | |
| 992 status.currentTest.timeoutTimer.cancel(); | |
| 993 status.currentTest.stopwatch.stop(); | |
| 994 | |
| 995 if (status.currentTest.id != testId) { | |
| 996 print("Expected test id ${status.currentTest.id} for" | |
| 997 "${status.currentTest.url}"); | |
| 998 print("Got test id ${testId}"); | |
| 999 print("Last test id was ${status.lastTest.id} for " | |
| 1000 "${status.currentTest.url}"); | |
| 1001 throw("This should never happen, wrong test id"); | |
| 1002 } | |
| 1003 testCache[testId] = status.currentTest.url; | |
| 1004 | |
| 1005 // Report that the test is finished now | |
| 1006 var browserTestOutput = new BrowserTestOutput( | |
| 1007 status.currentTest.delayUntilTestStarted, | |
| 1008 status.currentTest.stopwatch.elapsed, | |
| 1009 output, | |
| 1010 status.browser.testBrowserOutput); | |
| 1011 status.currentTest.doneCallback(browserTestOutput); | |
| 1012 | |
| 1013 status.lastTest = status.currentTest; | |
| 1014 status.currentTest = null; | |
| 1015 status.nextTestTimeout = createNextTestTimer(status); | |
| 1016 } else { | |
| 1017 print("\nThis is bad, should never happen, handleResult no test"); | |
| 1018 print("URL: ${status.lastTest.url}"); | |
| 1019 print(output); | |
| 1020 terminate().then((_) { | |
| 1021 exit(1); | |
| 1022 }); | |
| 1023 } | |
| 1024 } | |
| 1025 | |
| 1026 void handleStatusUpdate(String browserId, String output, int testId) { | |
| 1027 var status = browserStatus[browserId]; | |
| 1028 | |
| 1029 if (status == null || status.timeout) { | |
| 1030 // We don't do anything, this browser is currently being killed and | |
| 1031 // replaced. The browser here can be null if we decided to kill the | |
| 1032 // browser. | |
| 1033 } else if (status.currentTest != null && status.currentTest.id == testId) { | |
| 1034 status.currentTest.lastKnownMessage = output; | |
| 1035 } | |
| 1036 } | |
| 1037 | |
| 1038 void handleStarted(String browserId, String output, int testId) { | |
| 1039 var status = browserStatus[browserId]; | |
| 1040 | |
| 1041 if (status != null && !status.timeout && status.currentTest != null) { | |
| 1042 status.currentTest.timeoutTimer.cancel(); | |
| 1043 status.currentTest.timeoutTimer = | |
| 1044 createTimeoutTimer(status.currentTest, status); | |
| 1045 status.currentTest.delayUntilTestStarted = | |
| 1046 status.currentTest.stopwatch.elapsed; | |
| 1047 } | |
| 1048 } | |
| 1049 | |
| 1050 void handleTimeout(BrowserTestingStatus status) { | |
| 1051 // We simply kill the browser and starts up a new one! | |
| 1052 // We could be smarter here, but it does not seems like it is worth it. | |
| 1053 if (status.timeout) { | |
| 1054 DebugLogger.error( | |
| 1055 "Got test timeout for an already restarting browser"); | |
| 1056 return; | |
| 1057 } | |
| 1058 status.timeout = true; | |
| 1059 timedOut.add(status.currentTest.url); | |
| 1060 var id = status.browser.id; | |
| 1061 | |
| 1062 status.currentTest.stopwatch.stop(); | |
| 1063 status.browser.close().then((_) { | |
| 1064 var lastKnownMessage = | |
| 1065 'Dom could not be fetched, since the test timed out.'; | |
| 1066 if (status.currentTest.lastKnownMessage.length > 0) { | |
| 1067 lastKnownMessage = status.currentTest.lastKnownMessage; | |
| 1068 } | |
| 1069 // Wait until the browser is closed before reporting the test as timeout. | |
| 1070 // This will enable us to capture stdout/stderr from the browser | |
| 1071 // (which might provide us with information about what went wrong). | |
| 1072 var browserTestOutput = new BrowserTestOutput( | |
| 1073 status.currentTest.delayUntilTestStarted, | |
| 1074 status.currentTest.stopwatch.elapsed, | |
| 1075 lastKnownMessage, | |
| 1076 status.browser.testBrowserOutput, | |
| 1077 didTimeout: true); | |
| 1078 status.currentTest.doneCallback(browserTestOutput); | |
| 1079 status.lastTest = status.currentTest; | |
| 1080 status.currentTest = null; | |
| 1081 | |
| 1082 // We don't want to start a new browser if we are terminating. | |
| 1083 if (underTermination) return; | |
| 1084 restartBrowser(id); | |
| 1085 }); | |
| 1086 } | |
| 1087 | |
| 1088 void restartBrowser(String id) { | |
| 1089 var browser; | |
| 1090 var new_id = id; | |
| 1091 if (browserName == 'chromeOnAndroid') { | |
| 1092 browser = new AndroidChrome(adbDeviceMapping[id]); | |
| 1093 } else if (browserName == 'ContentShellOnAndroid') { | |
| 1094 browser = new AndroidBrowser(adbDeviceMapping[id], | |
| 1095 contentShellOnAndroidConfig, | |
| 1096 checkedMode, | |
| 1097 configuration['drt']); | |
| 1098 } else if (browserName == 'DartiumOnAndroid') { | |
| 1099 browser = new AndroidBrowser(adbDeviceMapping[id], | |
| 1100 dartiumOnAndroidConfig, | |
| 1101 checkedMode, | |
| 1102 configuration['dartium']); | |
| 1103 } else { | |
| 1104 browserStatus.remove(id); | |
| 1105 browser = getInstance(); | |
| 1106 new_id = "BROWSER$browserIdCount"; | |
| 1107 browserIdCount++; | |
| 1108 } | |
| 1109 browser.id = new_id; | |
| 1110 var status = new BrowserTestingStatus(browser); | |
| 1111 browserStatus[new_id] = status; | |
| 1112 status.nextTestTimeout = createNextTestTimer(status); | |
| 1113 status.timeSinceRestart.start(); | |
| 1114 browser.start(testingServer.getDriverUrl(new_id)).then((success) { | |
| 1115 // We may have started terminating in the mean time. | |
| 1116 if (underTermination) { | |
| 1117 if (status.nextTestTimeout != null) { | |
| 1118 status.nextTestTimeout.cancel(); | |
| 1119 status.nextTestTimeout = null; | |
| 1120 } | |
| 1121 browser.close().then((success) { | |
| 1122 // We should never hit this, print it out. | |
| 1123 if (!success) { | |
| 1124 print("Could not kill browser ($id) started due to timeout"); | |
| 1125 } | |
| 1126 }); | |
| 1127 return; | |
| 1128 } | |
| 1129 if (!success) { | |
| 1130 // TODO(ricow): Handle this better. | |
| 1131 print("This is bad, should never happen, could not start browser"); | |
| 1132 exit(1); | |
| 1133 } | |
| 1134 }); | |
| 1135 } | |
| 1136 | |
| 1137 BrowserTest getNextTest(String browserId) { | |
| 1138 var status = browserStatus[browserId]; | |
| 1139 if (status == null) return null; | |
| 1140 if (status.nextTestTimeout != null) { | |
| 1141 status.nextTestTimeout.cancel(); | |
| 1142 status.nextTestTimeout = null; | |
| 1143 } | |
| 1144 if (testQueue.isEmpty) return null; | |
| 1145 | |
| 1146 // We are currently terminating this browser, don't start a new test. | |
| 1147 if (status.timeout) return null; | |
| 1148 | |
| 1149 // Restart content_shell and dartium on Android if they have been | |
| 1150 // running for longer than RESTART_BROWSER_INTERVAL. The tests have | |
| 1151 // had flaky timeouts, and this may help. | |
| 1152 if ((browserName == 'ContentShellOnAndroid' || | |
| 1153 browserName == 'DartiumOnAndroid' ) && | |
| 1154 status.timeSinceRestart.elapsed > RESTART_BROWSER_INTERVAL) { | |
| 1155 var id = status.browser.id; | |
| 1156 // Reset stopwatch so we don't trigger again before restarting. | |
| 1157 status.timeout = true; | |
| 1158 status.browser.close().then((_) { | |
| 1159 // We don't want to start a new browser if we are terminating. | |
| 1160 if (underTermination) return; | |
| 1161 restartBrowser(id); | |
| 1162 }); | |
| 1163 // Don't send a test to the browser we are restarting. | |
| 1164 return null; | |
| 1165 } | |
| 1166 | |
| 1167 BrowserTest test = testQueue.removeLast(); | |
| 1168 if (status.currentTest == null) { | |
| 1169 status.currentTest = test; | |
| 1170 status.currentTest.lastKnownMessage = ''; | |
| 1171 } else { | |
| 1172 // TODO(ricow): Handle this better. | |
| 1173 print("Browser requested next test before reporting previous result"); | |
| 1174 print("This happened for browser $browserId"); | |
| 1175 print("Old test was: ${status.currentTest.url}"); | |
| 1176 print("The test before that was: ${status.lastTest.url}"); | |
| 1177 print("Timed out tests:"); | |
| 1178 for (var v in timedOut) { | |
| 1179 print(" $v"); | |
| 1180 } | |
| 1181 exit(1); | |
| 1182 } | |
| 1183 | |
| 1184 status.currentTest.timeoutTimer = createTimeoutTimer(test, status); | |
| 1185 status.currentTest.stopwatch = new Stopwatch()..start(); | |
| 1186 | |
| 1187 // Reset the test specific output information (stdout, stderr) on the | |
| 1188 // browser, since a new test is being started. | |
| 1189 status.browser.resetTestBrowserOutput(); | |
| 1190 status.browser.logBrowserInfoToTestBrowserOutput(); | |
| 1191 | |
| 1192 return test; | |
| 1193 } | |
| 1194 | |
| 1195 Timer createTimeoutTimer(BrowserTest test, BrowserTestingStatus status) { | |
| 1196 return new Timer(new Duration(seconds: test.timeout), | |
| 1197 () { handleTimeout(status); }); | |
| 1198 } | |
| 1199 | |
| 1200 Timer createNextTestTimer(BrowserTestingStatus status) { | |
| 1201 return new Timer(BrowserTestRunner.NEXT_TEST_TIMEOUT, | |
| 1202 () { handleNextTestTimeout(status); }); | |
| 1203 } | |
| 1204 | |
| 1205 void handleNextTestTimeout(status) { | |
| 1206 DebugLogger.warning( | |
| 1207 "Browser timed out before getting next test. Restarting"); | |
| 1208 if (status.timeout) return; | |
| 1209 numBrowserGetTestTimeouts++; | |
| 1210 if (numBrowserGetTestTimeouts >= MAX_NEXT_TEST_TIMEOUTS) { | |
| 1211 DebugLogger.error( | |
| 1212 "Too many browser timeouts before getting next test. Terminating"); | |
| 1213 terminate().then((_) => exit(1)); | |
| 1214 } else { | |
| 1215 status.timeout = true; | |
| 1216 status.browser.close().then((_) => restartBrowser(status.browser.id)); | |
| 1217 } | |
| 1218 } | |
| 1219 | |
| 1220 void queueTest(BrowserTest test) { | |
| 1221 testQueue.add(test); | |
| 1222 } | |
| 1223 | |
| 1224 void printDoubleReportingTests() { | |
| 1225 if (doubleReportingOutputs.length == 0) return; | |
| 1226 // TODO(ricow): die on double reporting. | |
| 1227 // Currently we just report this here, we could have a callback to the | |
| 1228 // encapsulating environment. | |
| 1229 print(""); | |
| 1230 print("Double reporting tests"); | |
| 1231 for (var id in doubleReportingOutputs.keys) { | |
| 1232 print(" ${testCache[id]}"); | |
| 1233 } | |
| 1234 | |
| 1235 DebugLogger.warning("Double reporting tests:"); | |
| 1236 for (var id in doubleReportingOutputs.keys) { | |
| 1237 DebugLogger.warning("${testCache[id]}, output: "); | |
| 1238 DebugLogger.warning("${doubleReportingOutputs[id]}"); | |
| 1239 DebugLogger.warning(""); | |
| 1240 DebugLogger.warning(""); | |
| 1241 } | |
| 1242 } | |
| 1243 | |
| 1244 Future<bool> terminate() { | |
| 1245 var browsers = []; | |
| 1246 underTermination = true; | |
| 1247 testingServer.underTermination = true; | |
| 1248 for (BrowserTestingStatus status in browserStatus.values) { | |
| 1249 browsers.add(status.browser); | |
| 1250 if (status.nextTestTimeout != null) { | |
| 1251 status.nextTestTimeout.cancel(); | |
| 1252 status.nextTestTimeout = null; | |
| 1253 } | |
| 1254 } | |
| 1255 // Success if all the browsers closed successfully. | |
| 1256 bool success = true; | |
| 1257 Future closeBrowser(Browser b) { | |
| 1258 return b.close().then((bool closeSucceeded) { | |
| 1259 if (!closeSucceeded) { | |
| 1260 success = false; | |
| 1261 } | |
| 1262 }); | |
| 1263 } | |
| 1264 return Future.forEach(browsers, closeBrowser).then((_) { | |
| 1265 testingServer.errorReportingServer.close(); | |
| 1266 printDoubleReportingTests(); | |
| 1267 return success; | |
| 1268 }); | |
| 1269 } | |
| 1270 | |
| 1271 Browser getInstance() { | |
| 1272 if (browserName == 'ff') browserName = 'firefox'; | |
| 1273 var path = Locations.getBrowserLocation(browserName, configuration); | |
| 1274 var browser = new Browser.byName(browserName, path, checkedMode); | |
| 1275 browser.logger = logger; | |
| 1276 return browser; | |
| 1277 } | |
| 1278 } | |
| 1279 | |
| 1280 class BrowserTestingServer { | |
| 1281 final Map configuration; | |
| 1282 /// Interface of the testing server: | |
| 1283 /// | |
| 1284 /// GET /driver/BROWSER_ID -- This will get the driver page to fetch | |
| 1285 /// and run tests ... | |
| 1286 /// GET /next_test/BROWSER_ID -- returns "WAIT" "TERMINATE" or "url#id" | |
| 1287 /// where url is the test to run, and id is the id of the test. | |
| 1288 /// If there are currently no available tests the waitSignal is send | |
| 1289 /// back. If we are in the process of terminating the terminateSignal | |
| 1290 /// is send back and the browser will stop requesting new tasks. | |
| 1291 /// POST /report/BROWSER_ID?id=NUM -- sends back the dom of the executed | |
| 1292 /// test | |
| 1293 | |
| 1294 final String localIp; | |
| 1295 | |
| 1296 static const String driverPath = "/driver"; | |
| 1297 static const String nextTestPath = "/next_test"; | |
| 1298 static const String reportPath = "/report"; | |
| 1299 static const String statusUpdatePath = "/status_update"; | |
| 1300 static const String startedPath = "/started"; | |
| 1301 static const String waitSignal = "WAIT"; | |
| 1302 static const String terminateSignal = "TERMINATE"; | |
| 1303 | |
| 1304 var testCount = 0; | |
| 1305 var errorReportingServer; | |
| 1306 bool underTermination = false; | |
| 1307 bool useIframe = false; | |
| 1308 | |
| 1309 Function testDoneCallBack; | |
| 1310 Function testStatusUpdateCallBack; | |
| 1311 Function testStartedCallBack; | |
| 1312 Function nextTestCallBack; | |
| 1313 | |
| 1314 BrowserTestingServer(this.configuration, this.localIp, this.useIframe); | |
| 1315 | |
| 1316 Future start() { | |
| 1317 var test_driver_error_port = configuration['test_driver_error_port']; | |
| 1318 return HttpServer.bind(localIp, test_driver_error_port) | |
| 1319 .then(setupErrorServer) | |
| 1320 .then(setupDispatchingServer); | |
| 1321 } | |
| 1322 | |
| 1323 void setupErrorServer(HttpServer server) { | |
| 1324 errorReportingServer = server; | |
| 1325 void errorReportingHandler(HttpRequest request) { | |
| 1326 StringBuffer buffer = new StringBuffer(); | |
| 1327 request.transform(UTF8.decoder).listen((data) { | |
| 1328 buffer.write(data); | |
| 1329 }, onDone: () { | |
| 1330 String back = buffer.toString(); | |
| 1331 request.response.headers.set("Access-Control-Allow-Origin", "*"); | |
| 1332 request.response.done.catchError((error) { | |
| 1333 DebugLogger.error("Error getting error from browser" | |
| 1334 "on uri ${request.uri.path}: $error"); | |
| 1335 }); | |
| 1336 request.response.close(); | |
| 1337 DebugLogger.error("Error from browser on : " | |
| 1338 "${request.uri.path}, data: $back"); | |
| 1339 }, onError: (error) { print(error); }); | |
| 1340 } | |
| 1341 void errorHandler(e) { | |
| 1342 if (!underTermination) print("Error occured in httpserver: $e"); | |
| 1343 } | |
| 1344 errorReportingServer.listen(errorReportingHandler, onError: errorHandler); | |
| 1345 } | |
| 1346 | |
| 1347 void setupDispatchingServer(_) { | |
| 1348 DispatchingServer server = configuration['_servers_'].server; | |
| 1349 void noCache(request) { | |
| 1350 request.response.headers.set("Cache-Control", | |
| 1351 "no-cache, no-store, must-revalidate"); | |
| 1352 } | |
| 1353 int testId(request) => | |
| 1354 int.parse(request.uri.queryParameters["id"]); | |
| 1355 String browserId(request, prefix) => | |
| 1356 request.uri.path.substring(prefix.length + 1); | |
| 1357 | |
| 1358 | |
| 1359 server.addHandler(reportPath, (HttpRequest request) { | |
| 1360 noCache(request); | |
| 1361 handleReport(request, browserId(request, reportPath), | |
| 1362 testId(request), isStatusUpdate: false); | |
| 1363 }); | |
| 1364 server.addHandler(statusUpdatePath, (HttpRequest request) { | |
| 1365 noCache(request); | |
| 1366 handleReport(request, browserId(request, statusUpdatePath), | |
| 1367 testId(request), isStatusUpdate: true); | |
| 1368 }); | |
| 1369 server.addHandler(startedPath, (HttpRequest request) { | |
| 1370 noCache(request); | |
| 1371 handleStarted(request, browserId(request, startedPath), | |
| 1372 testId(request)); | |
| 1373 }); | |
| 1374 | |
| 1375 makeSendPageHandler(String prefix) => (HttpRequest request) { | |
| 1376 noCache(request); | |
| 1377 var textResponse = ""; | |
| 1378 if (prefix == driverPath) { | |
| 1379 textResponse = getDriverPage(browserId(request, prefix)); | |
| 1380 request.response.headers.set('Content-Type', 'text/html'); | |
| 1381 } | |
| 1382 if (prefix == nextTestPath) { | |
| 1383 textResponse = getNextTest(browserId(request, prefix)); | |
| 1384 request.response.headers.set('Content-Type', 'text/plain'); | |
| 1385 } | |
| 1386 request.response.write(textResponse); | |
| 1387 request.listen((_) {}, onDone: request.response.close); | |
| 1388 request.response.done.catchError((error) { | |
| 1389 if (!underTermination) { | |
| 1390 print("URI ${request.uri}"); | |
| 1391 print("Textresponse $textResponse"); | |
| 1392 throw "Error returning content to browser: $error"; | |
| 1393 } | |
| 1394 }); | |
| 1395 }; | |
| 1396 server.addHandler(driverPath, makeSendPageHandler(driverPath)); | |
| 1397 server.addHandler(nextTestPath, makeSendPageHandler(nextTestPath)); | |
| 1398 } | |
| 1399 | |
| 1400 void handleReport(HttpRequest request, String browserId, var testId, | |
| 1401 {bool isStatusUpdate}) { | |
| 1402 StringBuffer buffer = new StringBuffer(); | |
| 1403 request.transform(UTF8.decoder).listen((data) { | |
| 1404 buffer.write(data); | |
| 1405 }, onDone: () { | |
| 1406 String back = buffer.toString(); | |
| 1407 request.response.close(); | |
| 1408 if (isStatusUpdate) { | |
| 1409 testStatusUpdateCallBack(browserId, back, testId); | |
| 1410 } else { | |
| 1411 testDoneCallBack(browserId, back, testId); | |
| 1412 } | |
| 1413 // TODO(ricow): We should do something smart if we get an error here. | |
| 1414 }, onError: (error) { DebugLogger.error("$error"); }); | |
| 1415 } | |
| 1416 | |
| 1417 void handleStarted(HttpRequest request, String browserId, var testId) { | |
| 1418 StringBuffer buffer = new StringBuffer(); | |
| 1419 // If an error occurs while receiving the data from the request stream, | |
| 1420 // we don't handle it specially. We can safely ignore it, since the started | |
| 1421 // events are not crucial. | |
| 1422 request.transform(UTF8.decoder).listen((data) { | |
| 1423 buffer.write(data); | |
| 1424 }, onDone: () { | |
| 1425 String back = buffer.toString(); | |
| 1426 request.response.close(); | |
| 1427 testStartedCallBack(browserId, back, testId); | |
| 1428 }, onError: (error) { DebugLogger.error("$error"); }); | |
| 1429 } | |
| 1430 | |
| 1431 String getNextTest(String browserId) { | |
| 1432 var nextTest = nextTestCallBack(browserId); | |
| 1433 if (underTermination) { | |
| 1434 // Browsers will be killed shortly, send them a terminate signal so | |
| 1435 // that they stop pulling. | |
| 1436 return terminateSignal; | |
| 1437 } | |
| 1438 return nextTest == null ? waitSignal : nextTest.toJSON(); | |
| 1439 } | |
| 1440 | |
| 1441 String getDriverUrl(String browserId) { | |
| 1442 if (errorReportingServer == null) { | |
| 1443 print("Bad browser testing server, you are not started yet. Can't " | |
| 1444 "produce driver url"); | |
| 1445 exit(1); | |
| 1446 // This should never happen - exit immediately; | |
| 1447 } | |
| 1448 var port = configuration['_servers_'].port; | |
| 1449 return "http://$localIp:$port/driver/$browserId"; | |
| 1450 } | |
| 1451 | |
| 1452 | |
| 1453 String getDriverPage(String browserId) { | |
| 1454 var errorReportingUrl = | |
| 1455 "http://$localIp:${errorReportingServer.port}/$browserId"; | |
| 1456 String driverContent = """ | |
| 1457 <!DOCTYPE html><html> | |
| 1458 <head> | |
| 1459 <style> | |
| 1460 body { | |
| 1461 margin: 0; | |
| 1462 } | |
| 1463 .box { | |
| 1464 overflow: hidden; | |
| 1465 overflow-y: auto; | |
| 1466 position: absolute; | |
| 1467 left: 0; | |
| 1468 right: 0; | |
| 1469 } | |
| 1470 .controller.box { | |
| 1471 height: 75px; | |
| 1472 top: 0; | |
| 1473 } | |
| 1474 .test.box { | |
| 1475 top: 75px; | |
| 1476 bottom: 0; | |
| 1477 } | |
| 1478 </style> | |
| 1479 <title>Driving page</title> | |
| 1480 <script type='text/javascript'> | |
| 1481 var STATUS_UPDATE_INTERVAL = 10000; | |
| 1482 | |
| 1483 function startTesting() { | |
| 1484 var number_of_tests = 0; | |
| 1485 var current_id; | |
| 1486 var next_id; | |
| 1487 | |
| 1488 // Has the test in the current iframe reported that it is done? | |
| 1489 var test_completed = true; | |
| 1490 // Has the test in the current iframe reported that it is started? | |
| 1491 var test_started = false; | |
| 1492 var testing_window; | |
| 1493 | |
| 1494 var embedded_iframe_div = document.getElementById('embedded_iframe_div'); | |
| 1495 var embedded_iframe = document.getElementById('embedded_iframe'); | |
| 1496 var number_div = document.getElementById('number'); | |
| 1497 var executing_div = document.getElementById('currently_executing'); | |
| 1498 var error_div = document.getElementById('unhandled_error'); | |
| 1499 var use_iframe = ${useIframe}; | |
| 1500 var start = new Date(); | |
| 1501 | |
| 1502 // Object that holds the state of an HTML test | |
| 1503 var html_test; | |
| 1504 | |
| 1505 function newTaskHandler() { | |
| 1506 if (this.readyState == this.DONE) { | |
| 1507 if (this.status == 200) { | |
| 1508 if (this.responseText == '$waitSignal') { | |
| 1509 setTimeout(getNextTask, 500); | |
| 1510 } else if (this.responseText == '$terminateSignal') { | |
| 1511 // Don't do anything, we will be killed shortly. | |
| 1512 } else { | |
| 1513 var elapsed = new Date() - start; | |
| 1514 var nextTask = JSON.parse(this.responseText); | |
| 1515 var url = nextTask.url; | |
| 1516 next_id = nextTask.id; | |
| 1517 if (nextTask.isHtmlTest) { | |
| 1518 html_test = { | |
| 1519 expected_messages: nextTask.expectedMessages, | |
| 1520 found_message_count: 0, | |
| 1521 double_received_messages: [], | |
| 1522 unexpected_messages: [], | |
| 1523 found_messages: {} | |
| 1524 }; | |
| 1525 for (var i = 0; i < html_test.expected_messages.length; ++i) { | |
| 1526 html_test.found_messages[html_test.expected_messages[i]] = 0; | |
| 1527 } | |
| 1528 } else { | |
| 1529 html_test = null; | |
| 1530 } | |
| 1531 run(url); | |
| 1532 } | |
| 1533 } else { | |
| 1534 reportError('Could not contact the server and get a new task'); | |
| 1535 } | |
| 1536 } | |
| 1537 } | |
| 1538 | |
| 1539 function contactBrowserController(method, | |
| 1540 path, | |
| 1541 callback, | |
| 1542 msg, | |
| 1543 isUrlEncoded) { | |
| 1544 var client = new XMLHttpRequest(); | |
| 1545 client.onreadystatechange = callback; | |
| 1546 client.open(method, path); | |
| 1547 if (isUrlEncoded) { | |
| 1548 client.setRequestHeader('Content-type', | |
| 1549 'application/x-www-form-urlencoded'); | |
| 1550 } | |
| 1551 client.send(msg); | |
| 1552 } | |
| 1553 | |
| 1554 function getNextTask() { | |
| 1555 // Until we have the next task we set the current_id to a specific | |
| 1556 // negative value. | |
| 1557 contactBrowserController( | |
| 1558 'GET', '$nextTestPath/$browserId', newTaskHandler, "", false); | |
| 1559 } | |
| 1560 | |
| 1561 function childError(message, filename, lineno, colno, error) { | |
| 1562 sendStatusUpdate(); | |
| 1563 if (error) { | |
| 1564 reportMessage('FAIL:' + filename + ':' + lineno + | |
| 1565 ':' + colno + ':' + message + '\\n' + error.stack, false, false); | |
| 1566 } else if (filename) { | |
| 1567 reportMessage('FAIL:' + filename + ':' + lineno + | |
| 1568 ':' + colno + ':' + message, false, false); | |
| 1569 } else { | |
| 1570 reportMessage('FAIL: ' + message, false, false); | |
| 1571 } | |
| 1572 return true; | |
| 1573 } | |
| 1574 | |
| 1575 function setChildHandlers(e) { | |
| 1576 embedded_iframe.contentWindow.addEventListener('message', | |
| 1577 childMessageHandler, | |
| 1578 false); | |
| 1579 embedded_iframe.contentWindow.onerror = childError; | |
| 1580 reportMessage("First message from html test", true, false); | |
| 1581 html_test.handlers_installed = true; | |
| 1582 sendRepeatingStatusUpdate(); | |
| 1583 } | |
| 1584 | |
| 1585 function checkChildHandlersInstalled() { | |
| 1586 if (!html_test.handlers_installed) { | |
| 1587 reportMessage("First message from html test", true, false); | |
| 1588 reportMessage( | |
| 1589 'FAIL: Html test did not call ' + | |
| 1590 'window.parent.dispatchEvent(new Event("detect_errors")) ' + | |
| 1591 'as its first action', false, false); | |
| 1592 } | |
| 1593 } | |
| 1594 | |
| 1595 function run(url) { | |
| 1596 number_of_tests++; | |
| 1597 number_div.innerHTML = number_of_tests; | |
| 1598 executing_div.innerHTML = url; | |
| 1599 if (use_iframe) { | |
| 1600 if (html_test) { | |
| 1601 window.addEventListener('detect_errors', setChildHandlers, false); | |
| 1602 embedded_iframe.onload = checkChildHandlersInstalled; | |
| 1603 } else { | |
| 1604 embedded_iframe.onload = null; | |
| 1605 } | |
| 1606 embedded_iframe_div.removeChild(embedded_iframe); | |
| 1607 embedded_iframe = document.createElement('iframe'); | |
| 1608 embedded_iframe.id = "embedded_iframe"; | |
| 1609 embedded_iframe.style="width:100%;height:100%"; | |
| 1610 embedded_iframe_div.appendChild(embedded_iframe); | |
| 1611 embedded_iframe.src = url; | |
| 1612 } else { | |
| 1613 if (typeof testing_window != 'undefined') { | |
| 1614 testing_window.close(); | |
| 1615 } | |
| 1616 testing_window = window.open(url); | |
| 1617 } | |
| 1618 test_started = false; | |
| 1619 test_completed = false; | |
| 1620 } | |
| 1621 | |
| 1622 window.onerror = function (message, url, lineNumber) { | |
| 1623 if (url) { | |
| 1624 reportError(url + ':' + lineNumber + ':' + message); | |
| 1625 } else { | |
| 1626 reportError(message); | |
| 1627 } | |
| 1628 } | |
| 1629 | |
| 1630 function reportError(msg) { | |
| 1631 function handleReady() { | |
| 1632 if (this.readyState == this.DONE && this.status != 200) { | |
| 1633 var error = 'Sending back error did not succeeed: ' + this.status; | |
| 1634 error = error + '. Failed to send msg: ' + msg; | |
| 1635 error_div.innerHTML = error; | |
| 1636 } | |
| 1637 } | |
| 1638 contactBrowserController( | |
| 1639 'POST', '$errorReportingUrl?test=1', handleReady, msg, true); | |
| 1640 } | |
| 1641 | |
| 1642 function reportMessage(msg, isFirstMessage, isStatusUpdate) { | |
| 1643 if (isFirstMessage) { | |
| 1644 if (test_started) { | |
| 1645 reportMessage( | |
| 1646 "FAIL: test started more than once (test reloads itself) " + | |
| 1647 msg, false, false); | |
| 1648 return; | |
| 1649 } | |
| 1650 current_id = next_id; | |
| 1651 test_started = true; | |
| 1652 contactBrowserController( | |
| 1653 'POST', '$startedPath/${browserId}?id=' + current_id, | |
| 1654 function () {}, msg, true); | |
| 1655 } else if (isStatusUpdate) { | |
| 1656 contactBrowserController( | |
| 1657 'POST', '$statusUpdatePath/${browserId}?id=' + current_id, | |
| 1658 function() {}, msg, true); | |
| 1659 } else { | |
| 1660 var is_double_report = test_completed; | |
| 1661 var retry = 0; | |
| 1662 test_completed = true; | |
| 1663 | |
| 1664 function reportDoneMessage() { | |
| 1665 contactBrowserController( | |
| 1666 'POST', '$reportPath/${browserId}?id=' + current_id, | |
| 1667 handleReady, msg, true); | |
| 1668 } | |
| 1669 | |
| 1670 function handleReady() { | |
| 1671 if (this.readyState == this.DONE) { | |
| 1672 if (this.status == 200) { | |
| 1673 if (!is_double_report) { | |
| 1674 getNextTask(); | |
| 1675 } | |
| 1676 } else { | |
| 1677 reportError('Error sending result to server. Status: ' + | |
| 1678 this.status + ' Retry: ' + retry); | |
| 1679 retry++; | |
| 1680 if (retry < 3) { | |
| 1681 setTimeout(reportDoneMessage, 1000); | |
| 1682 } | |
| 1683 } | |
| 1684 } | |
| 1685 } | |
| 1686 | |
| 1687 reportDoneMessage(); | |
| 1688 } | |
| 1689 } | |
| 1690 | |
| 1691 function parseResult(result) { | |
| 1692 var parsedData = null; | |
| 1693 try { | |
| 1694 parsedData = JSON.parse(result); | |
| 1695 } catch(error) { } | |
| 1696 return parsedData; | |
| 1697 } | |
| 1698 | |
| 1699 // Browser tests send JSON messages to the driver window, handled here. | |
| 1700 function messageHandler(e) { | |
| 1701 var msg = e.data; | |
| 1702 if (typeof msg != 'string') return; | |
| 1703 var expectedSource = | |
| 1704 use_iframe ? embedded_iframe.contentWindow : testing_window; | |
| 1705 if (e.source != expectedSource) { | |
| 1706 reportError("Message received from old test window: " + msg); | |
| 1707 return; | |
| 1708 } | |
| 1709 var parsedData = parseResult(msg); | |
| 1710 if (parsedData) { | |
| 1711 // Only if the JSON message contains all required parameters, | |
| 1712 // will we handle it and post it back to the test controller. | |
| 1713 if ('message' in parsedData && | |
| 1714 'is_first_message' in parsedData && | |
| 1715 'is_status_update' in parsedData && | |
| 1716 'is_done' in parsedData) { | |
| 1717 var message = parsedData['message']; | |
| 1718 var isFirstMessage = parsedData['is_first_message']; | |
| 1719 var isStatusUpdate = parsedData['is_status_update']; | |
| 1720 var isDone = parsedData['is_done']; | |
| 1721 if (!isFirstMessage && !isStatusUpdate) { | |
| 1722 if (!isDone) { | |
| 1723 alert("Bug in test_controller.js: " + | |
| 1724 "isFirstMessage/isStatusUpdate/isDone were all false"); | |
| 1725 } | |
| 1726 } | |
| 1727 reportMessage(message, isFirstMessage, isStatusUpdate); | |
| 1728 } | |
| 1729 } | |
| 1730 } | |
| 1731 | |
| 1732 function sendStatusUpdate () { | |
| 1733 var dom = | |
| 1734 embedded_iframe.contentWindow.document.documentElement.innerHTML; | |
| 1735 reportMessage('Status:\\n Messages received multiple times:\\n ' + | |
| 1736 html_test.double_received_messages + | |
| 1737 '\\n Unexpected messages:\\n ' + | |
| 1738 html_test.unexpected_messages + | |
| 1739 '\\n DOM:\\n ' + dom, false, true); | |
| 1740 } | |
| 1741 | |
| 1742 function sendRepeatingStatusUpdate() { | |
| 1743 sendStatusUpdate(); | |
| 1744 setTimeout(sendRepeatingStatusUpdate, STATUS_UPDATE_INTERVAL); | |
| 1745 } | |
| 1746 | |
| 1747 // HTML tests post messages to their own window, handled by this handler. | |
| 1748 // This handler is installed on the child window when it sends the | |
| 1749 // 'detect_errors' event. Every HTML test must send 'detect_errors' to | |
| 1750 // its parent window as its first action, so all errors will be caught. | |
| 1751 function childMessageHandler(e) { | |
| 1752 var msg = e.data; | |
| 1753 if (typeof msg != 'string') return; | |
| 1754 if (msg in html_test.found_messages) { | |
| 1755 html_test.found_messages[msg]++; | |
| 1756 if (html_test.found_messages[msg] == 1) { | |
| 1757 html_test.found_message_count++; | |
| 1758 } else { | |
| 1759 html_test.double_received_messages.push(msg); | |
| 1760 sendStatusUpdate(); | |
| 1761 } | |
| 1762 } else { | |
| 1763 html_test.unexpected_messages.push(msg); | |
| 1764 sendStatusUpdate(); | |
| 1765 } | |
| 1766 if (html_test.found_message_count == | |
| 1767 html_test.expected_messages.length) { | |
| 1768 reportMessage('Test done: PASS', false, false); | |
| 1769 } | |
| 1770 } | |
| 1771 | |
| 1772 if (!html_test) { | |
| 1773 window.addEventListener('message', messageHandler, false); | |
| 1774 waitForDone = false; | |
| 1775 } | |
| 1776 getNextTask(); | |
| 1777 } | |
| 1778 </script> | |
| 1779 </head> | |
| 1780 <body onload="startTesting()"> | |
| 1781 <div class="controller box"> | |
| 1782 Dart test driver, number of tests: <span id="number"></span><br> | |
| 1783 Currently executing: <span id="currently_executing"></span><br> | |
| 1784 Unhandled error: <span id="unhandled_error"></span> | |
| 1785 </div> | |
| 1786 <div id="embedded_iframe_div" class="test box"> | |
| 1787 <iframe style="width:100%;height:100%;" id="embedded_iframe"></iframe> | |
| 1788 </div> | |
| 1789 </body> | |
| 1790 </html> | |
| 1791 """; | |
| 1792 return driverContent; | |
| 1793 } | |
| 1794 } | |
| OLD | NEW |