Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1161)

Side by Side Diff: tools/testing/dart/browser_controller.dart

Issue 841193003: cleanup to tools/testing/dart (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: one last bit Created 5 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « tools/testing/dart/android.dart ('k') | tools/testing/dart/browser_test.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 }
OLDNEW
« no previous file with comments | « tools/testing/dart/android.dart ('k') | tools/testing/dart/browser_test.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698