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

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

Issue 14757019: Add browser controller and allow it to be used under a flag. (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart/
Patch Set: Created 7 years, 7 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 | « pkg/unittest/lib/test_controller.js ('k') | tools/testing/dart/test_options.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:core";
8 import "dart:io";
9
10
11 /** Class describing the interface for communicating with browsers. */
12 class Browser {
13 // Browsers actually takes a while to cleanup after itself when closing
14 // Give it sufficient time to do that.
15 static final Duration killRepeatInternal = const Duration(seconds: 10);
16 static final int killRetries = 5;
17 StringBuffer _stdout = new StringBuffer();
18 StringBuffer _stderr = new StringBuffer();
19 StringBuffer _usageLog = new StringBuffer();
20 // This function is called when the process is closed.
21 // This is extracted to an external function so that we can do additional
22 // functionality when the process closes (cleanup and call onExit)
23 Function _processClosed;
24 // This is called after the process is closed, after _processClosed has
25 // been called, but before onExit. Subclasses can use this to cleanup
26 // any browser specific resources (temp directories, profiles, etc)
27 Function _cleanup;
kustermann 2013/05/14 09:05:24 We have a ton of callback in this code. In this ca
28
29 /** The version of the browser - normally set when starting a browser */
30 String version = "";
31 /**
32 * The underlying process - don't mess directly with this if you don't
33 * know what you are doing (this is an interactive process that needs
34 * special threatment to not leak).
35 */
36 Process process;
37
38 /**
39 * Id of the browser
40 */
41 String id;
42
43 /** Callback that will be executed when the browser has closed */
44 Function onClose;
45
46 /** Print everything (stdout, stderr, usageLog) whenever we add to it */
47 bool debugPrint = false;
48
49 void _logEvent(String event) {
50 String toLog = "$this ($id) - ${new DateTime.now()}: $event \n";
51 if (debugPrint) print("usageLog: $toLog");
52 _usageLog.write(toLog);
kustermann 2013/05/14 09:05:24 a) Is the 'debugPrint' still used? b) You don't wr
53 }
54
55 void _addStdout(String output) {
56 if (debugPrint) print("stdout: $output");
57 _stdout.write(output);
58 }
59
60 void _addStderr(String output) {
61 if (debugPrint) print("stderr: $output");
62 _stderr.write(output);
63 }
64
65 // Kill the underlying process using the supplied kill function
66 // If there is a alternativeKillFunction we will use that after trying
67 // the default killFunction.
68 Future _killIt(killFunction, retries, [alternativeKillFunction = null]) {
69 Completer<bool> completer = new Completer<bool>();
70
71 // To capture non successfull attempts we set up a timer that will
72 // trigger a retry (using the alternativeKillFunction if supplied).
73 Timer timer = new Timer(killRepeatInternal, () {
74 // Remove the handler, we will set this again in the call to killIt
75 // below
76 if (retries <= 0) {
77 _logEvent("Could not kill the process, not trying anymore");
78 // TODO(ricow): Should we crash the test script here and
79 // write out all our log. This is basically not a situation
80 // that we want to ignore. We could potentially have a handler we
81 // can call if this happens, which will shutdown the main process
82 // with info that people should contact [ricow,kustermann,?]
83 completer.complete(false);
kustermann 2013/05/14 09:05:24 What about calling utils.py:die("....")?
84 }
85 _logEvent("Could not kill the process, retrying");
86 var nextKillFunction = killFunction;
87 if (alternativeKillFunction != null) {
88 nextKillFunction = alternativeKillFunction;
89 }
90 _killIt(nextKillFunction, retries - 1).then((success) {
91 completer.complete(success);
92 });
93 });
94
95 // Make sure we intercept onExit calls and eliminate the timer.
96 _processClosed = () {
97 timer.cancel();
98 _logEvent("Proccess exited, cancel timer in kill loop");
99 _processClosed = null;
100 completer.complete(true);
kustermann 2013/05/14 09:05:24 You could set 'process = null' here.
101 };
102
103
104 _logEvent("calling kill function");
105 if (killFunction()) {
106 // We successfully sent the signal.
107 _logEvent("killing signal sent");
108 } else {
109 _logEvent("The process is already dead, kill signal could not be send");
110 completer.complete(true);
111 }
112 return completer.future;
113 }
114
115
116 /** Close the browser */
117 Future<bool> close() {
118 _logEvent("Close called on browser");
119 if (process == null) {
120 _logEvent("No process open, nothing to kill.");
121 return new Future.immediate(true);
122 }
123 var killFunction = process.kill;
124 // We use a SIGKILL signal if we don't kill the process in the first go.
125 var alternativeKillFunction =
126 () { return process.kill(ProcessSignal.SIGKILL);};
127 return _killIt(killFunction, killRetries, alternativeKillFunction);
128 }
129
130 /**
131 * Start the browser using the supplied argument.
132 * This sets up the error handling and usage logging.
133 */
134 Future<bool> startBrowser(String command, List<String> arguments) {
135 return Process.start(command, arguments).then((p) {
kustermann 2013/05/14 09:05:24 I have no strong opinion about this, but you told
136 process = p;
137 p.stdout.transform(new StringDecoder()).listen((data) {
138 _addStdout(data);
139 }, onError: (e) {
140 // This should _never_ happen, but we really want this in the log
141 // if it actually does due to dart:io or vm bug.
142 _usageLog.add("An error occured in the process stdout handling: $e");
143 });
144
145 p.stderr.transform(new StringDecoder()).listen((data) {
146 _addStderr(data);
147 }, onError: (e) {
148 // This should _never_ happen, but we really want this in the log
149 // if it actually does due to dart:io or vm bug.
150 _usageLog.add("An error occured in the process stderr handling: $e");
151 });
152
153 process.exitCode.then((exitCode) {
154 _logEvent("Browser closed with exitcode $exitCode");
155 if (_processClosed != null) _processClosed();
156 if (_cleanup != null) _cleanup();
157 if (onClose != null) onClose(exitCode);
158 });
kustermann 2013/05/14 09:05:24 Indentation.
159 return true;
160 }).catchError((e) {
161 _logEvent("Running $binary $arguments failed with $e");
162 return false;
163 });
164 }
165
166 /**
167 * Get any stdout that the browser wrote during execution.
168 */
169 String get stdout => _stdout.toString();
170 String get stderr => _stderr.toString();
171 String get usageLog => _usageLog.toString();
kustermann 2013/05/14 09:05:24 Where are these three variables used?
172
173 String toString();
174 /** Starts the browser loading the given url */
175 Future<bool> start(String url);
176 }
177
178
179 class Chrome extends Browser {
180 /**
181 * The binary used to run chrome - changing this can be nececcary for
182 * testing or using non standard chrome installation.
183 */
184 const String binary = "google-chrome";
185
186 Future<bool> start(String url) {
187 _logEvent("Starting chrome browser on: $url");
188 // Get the version and log that.
189 return Process.run(binary, ["--version"]).then((var versionResult) {
190 if (versionResult.exitCode != 0) {
191 _logEvent("Failed to chrome get version");
192 _logEvent("Make sure $binary is a valid program for running chrome");
193 return new Future.immediate(false);
194 }
195 version = versionResult.stdout;
196 _logEvent("Got version: $version");
197
198 return new Directory('').createTemp().then((userDir) {
199 _cleanup = () { userDir.delete(recursive: true); };
200 var args = ["--user-data-dir=${userDir.path}", url,
201 "--disable-extensions", "--disable-popup-blocking",
202 "--bwsi"];
203 return startBrowser(binary, args);
204
205 });
206 }).catchError((e) {
207 _logEvent("Running $binary --version failed with $e");
208 return false;
209 });
210 }
211
212 String toString() => "Chrome";
213 }
214
215 class Firefox extends Browser {
216 /**
217 * The binary used to run firefox - changing this can be nececcary for
218 * testing or using non standard firefox installation.
219 */
220 const String binary = "firefox";
221
222 const String enablePopUp =
223 "user_pref(\"dom.disable_open_during_load\", false);";
224
225 Future _createPreferenceFile(var path) {
226 var file = new File("${path.toString()}/user.js");
227 var randomFile = file.openSync(FileMode.WRITE);
228 randomFile.writeStringSync(enablePopUp);
229 randomFile.close();
230 }
231
232
233 Future<bool> start(String url) {
234 _logEvent("Starting firefox browser on: $url");
235 // Get the version and log that.
236 return Process.run(binary, ["--version"]).then((var versionResult) {
237 if (versionResult.exitCode != 0) {
238 _logEvent("Failed to firefox get version");
239 _logEvent("Make sure $binary is a valid program for running firefox");
240 return new Future.immediate(false);
241 }
242 version = versionResult.stdout;
243 _logEvent("Got version: $version");
244
245 return new Directory('').createTemp().then((userDir) {
246 _createPreferenceFile(userDir.path);
247 _cleanup = () { userDir.delete(recursive: true); };
248 var args = ["-profile", "${userDir.path}",
249 "-no-remote", "-new-instance", url];
250 return startBrowser(binary, args);
251
252 });
253 }).catchError((e) {
254 _logEvent("Running $binary --version failed with $e");
255 return false;
256 });
257 }
258
259 String toString() => "Firefox";
260 }
261
262
263 /**
264 * Describes the current state of a browser used for testing.
265 */
266 class BrowserTestingStatus {
267 // TODO(ricow): Add prefetching to the browsers. We spend a lot of time waiting
268 // for the next test. Handling timeouts is the hard part of this!
269
270
271 Browser browser;
272 BrowserTest currentlyRunning;
kustermann 2013/05/14 09:05:24 'currentlyRunning' feels like it's a boolean or so
273 // This is currently not used for anything except for error reporting.
274 // Given the usefulness of this in debugging issues this should not be
275 // removed even when we have really stable system.
276 BrowserTest last;
kustermann 2013/05/14 09:05:24 last -> lastTest
277 bool timeout = false;
278 BrowserTestingStatus(Browser this.browser);
279 }
280
281
282 /**
283 * Describes a single test to be run int the browser.
284 */
285 class BrowserTest {
286 // TODO(ricow): Add timeout callback instead of the string passing hack.
287 Function doneCallback;
kustermann 2013/05/14 09:05:24 Again, we could make it a completer and let the te
288 String url;
289 int timeout;
290 // We store this here for easy access when tests time out (instead of
291 // capturing this in a closure)
292 Timer timeoutTimer;
293
294 // Used for debugging, this is simply a unique identifier assigned to each
295 // test.
296 int id;
297 static int _idCounter = 0;
298
299 BrowserTest(this.url, this.doneCallback, this.timeout) {
300 id = _idCounter++;
301 }
302 }
303
304
305 /**
306 * Encapsulates all the functionality for running tests in browsers.
307 * The interface is rather simple. After starting the runner tests
308 * are simply added to the queue and a the supplied callbacks are called
309 * whenever a test completes.
310 */
311 class BrowserTestRunner {
312 int maxNumBrowsers;
313 String browserName;
314
315 bool underTermination = false;
316
317 List<BrowserTest> testQueue = new List<BrowserTest>();
318 Map<String, BrowserTestingStatus> browserStatus =
319 new Map<String, BrowserTestingStatus>();
kustermann 2013/05/14 09:05:24 The type of the variable should be inferred -- no
320 // This cache is used to guarantee that we never see double reporting.
321 // If we do we need to provide developers with this information.
322 // We don't add urls to the cache until we have run it.
323 Map<int, String> testCache = new Map<int, String>();
324 List<int> doubleReportingTests = new List<int>();
325
326 BrowserTestingServer testingServer;
327
328 BrowserTestRunner(String this.browserName, int this.maxNumBrowsers);
329
330 Future<bool> start() {
331 testingServer = new BrowserTestingServer();
332 return testingServer.start().then((_) {
333 testingServer.testDoneCallBack = handleResults;
334 testingServer.nextTestCallBack = getNextTest;
335 var futures = [];
336 for (int i = 0; i < maxNumBrowsers; i++) {
337 var browser = getInstance();
338 var id = "BROWSER$i";
339 // We store this in case we need to kill the browser.
340 browser.id = id;
341 var future =
342 browser.start(testingServer.getDriverUrl(id)).then((success) {
343 if (success) {
344 browserStatus[id] = new BrowserTestingStatus(browser);
345 }
346 return success;
347 });
348 futures.add(future);
349 }
350 return Future.wait(futures).then((values) {
351 return !values.contains(false);
352 });
353 });
354 }
355
356 var timedOut = [];
357
358 void handleResults(String browserId, String output, int testId) {
359 var status = browserStatus[browserId];
360 if (testCache.containsKey(testId)) {
361 doubleReportingTests.add(testId);
362 return;
363 }
364
365 if (status.timeout) {
366 // We don't do anything, this browser is currently being killed and
367 // replaced.
368 } else if (status.currentlyRunning != null) {
369 status.currentlyRunning.timeoutTimer.cancel();
370 if (status.currentlyRunning.id != testId) {
371 print("Expected test id ${status.currentlyRunning.id} for"
372 "${status.currentlyRunning.url}");
373 print("Got test id ${testId}");
374 print("Last test id was ${status.last.id} for "
375 "${status.currentlyRunning.url}");
376 throw("This should never happen, wrong test id");
377 }
378 testCache[testId] = status.currentlyRunning.url;
379 status.currentlyRunning.doneCallback(output);
380 status.last = status.currentlyRunning;
381 status.currentlyRunning = null;
382 } else {
383 print("\nThis is bad, should never happen, handleResult no test");
384 print("URL: ${status.last.url}");
385 print(output);
386 terminate().then((_) {
387 exit(1);
388 });
389 }
390 }
391
392 void handleTimeout(BrowserTestingStatus status) {
393 // We simply kill the browser and starts up a new one!
394 // We could be smarter here, but it does not seems like it is worth it.
395 status.timeout = true;
396 timedOut.add(status.currentlyRunning.url);
397 // Start the new browser first
398 var browser = getInstance();
kustermann 2013/05/14 09:05:24 The comment is outdated now + nove the 'getInstanc
399 var id = status.browser.id;
400 status.browser.close().then((closed) {
401 if (!closed) {
402 // Very bad, we could not kill the browser.
403 print("could not kill browser $id");
404 return;
405 }
406 browser.start(testingServer.getDriverUrl(id)).then((success) {
407 // We may have started terminating in the mean time.
408 if (underTermination) {
409 browser.close().then((success) {
kustermann 2013/05/14 09:05:24 If we're 'underTermination' then the 'browser.clos
410 // We should never hit this, print it out.
411 if (!success) {
412 print("Could not kill browser ($id) started due to timeout");
413 }
414 });
415 return;
kustermann 2013/05/14 09:05:24 indentation
416 }
417 if (success) {
418 browser.id = id;
419 status.browser = browser;
420 status.timeout = false;
421 } else {
422 // TODO(ricow): Handle this better.
423 print("This is bad, should never happen, could not start browser");
424 exit(1);
425 }
426 });
427 });
428
429 status.currentlyRunning.doneCallback("TIMEOUT");
430 status.currentlyRunning = null;
431 }
432
433 BrowserTest getNextTest(String browserId) {
434 if (testQueue.isEmpty) return null;
435 var status = browserStatus[browserId];
436 if (status == null) return null;
437 // We are currently terminating this browser, don't start a new test.
438 if (status.timeout) return null;
439 BrowserTest test = testQueue.removeLast();
440 if (status.currentlyRunning == null) {
441 status.currentlyRunning = test;
442 } else {
443 // TODO(ricow): Handle this better.
444 print("This is bad, should never happen, getNextTest all full");
445 print("Old test was: ${status.currentlyRunning.url}");
446 print("Timed out tests:");
447 for (var v in timedOut) {
448 print(" $v");
449 }
450 exit(1);
kustermann 2013/05/14 09:05:24 utils.py:die() ?
451 }
452 Timer timer = new Timer(new Duration(seconds: test.timeout),
453 () { handleTimeout(status); });
454 status.currentlyRunning.timeoutTimer = timer;
455 return test;
456 }
457
458 void queueTest(BrowserTest test) {
459 testQueue.add(test);
460 }
461
462 void printDoubleReportingTests() {
463 if (doubleReportingTests.length == 0) return;
464 // Currently we just report this here, we could have a callback to the
465 // encapsulating environment.
466 print("");
467 print("Double reporting tests");
468 for (var id in doubleReportingTests) {
469 print(" ${testCache[id]}");
470 }
kustermann 2013/05/14 09:05:24 Add a TODO to 'die()' in the future if we get doub
471 }
472
473 Future<bool> terminate() {
474 var futures = [];
475 underTermination = true;
476 testingServer.underTermination = true;
477 for (BrowserTestingStatus status in browserStatus.values) {
478 futures.add(status.browser.close());
479 }
480 return Future.wait(futures).then((values) {
481 testingServer.httpServer.close();
482 printDoubleReportingTests();
483 return !values.contains(false);
484 });
485 }
486
487 Browser getInstance() {
488 if (browserName == "chrome") {
489 return new Chrome();
490 } else if (browserName == "firefox") {
491 return new Firefox();
492 }
493 throw "Non supported browser for browser controller";
494 }
495 }
496
497 class BrowserTestingServer {
498 const String server = "127.0.0.1";
499
500 /// Interface of the testing server:
501 ///
502 /// /driver: This will get the driver page to fetch and run tests
503 /// /nextTestPath: This will get the next available test to run, returned
504 /// as a url and an id. If there are currently no available
505 /// tests the waitSignal is send back. If we are in the
506 /// process of terminating the terminateSignal is send back
507 /// and the browser will stop requesting new tasks.
508 /// /reportPath: Used for shipping back the result of running a test.
kustermann 2013/05/14 09:05:24 This is not the interface. Please document exactly
509
510
511 const String driverPath = "/driver";
512 const String nextTestPath = "/next_test";
513 const String reportPath = "/report";
514 const String waitSignal = "WAIT";
515 const String terminateSignal = "TERMINATE";
516
517 var testCount = 0;
518 var httpServer;
519 bool underTermination = false;
520
521 Function testDoneCallBack;
522 Function nextTestCallBack;
523
524 Future start() {
525 return HttpServer.bind(server, 0).then((createdServer) {
526 httpServer = createdServer;
527 void handler(HttpRequest request) {
528 if (request.uri.path.startsWith(reportPath)) {
529 var browserId = request.uri.path.substring(reportPath.length + 1);
530 var testId = int.parse(request.queryParameters["id"].split("=")[1]);
531
532 handleReport(request, browserId, testId);
533 // handleReport will asynchroniously fetch the data and will handle
534 // the closing of the streams.
535 return;
536 }
537 var textResponse = "";
538 if (request.uri.path.startsWith(driverPath)) {
539 var browserId = request.uri.path.substring(driverPath.length + 1);
540 textResponse = getDriverPage(browserId);
541 } else if (request.uri.path.startsWith(nextTestPath)) {
542 var browserId = request.uri.path.substring(nextTestPath.length + 1);
543 textResponse = getNextTest(browserId);
544 } else {
545 // We silently ignore other requests.
546 }
547 request.response.write(textResponse);
548 request.listen((_) {}, onDone: request.response.close);
549 request.response.done.catchError((error) {
550 if (!underTermination) {
551 print("URI ${request.uri}");
552 print("Textresponse $textResponse");
553 throw("Error returning content to browser: $error");
554 }
555 });
556 }
557 void errorHandler(e) {
558 if (!underTermination) print("Error occured in httpserver: $e");
559 };
560 httpServer.listen(handler, onError: errorHandler);
561 return true;
562 });
563 }
564
565 void handleReport(HttpRequest request, String browserId, var testId) {
566 StringBuffer buffer = new StringBuffer();
567 request.transform(new StringDecoder()).listen((data) {
568 buffer.write(data);
569 }, onDone: () {
570 String back = buffer.toString();
571 request.response.close();
572 testDoneCallBack(browserId, back, testId);
573 }, onError: (error) { print(error); });
574 }
575
576 String getNextTest(String browserId) {
577 var nextTest = nextTestCallBack(browserId);
578 if (underTermination) {
579 // Browsers will be killed shortly, send them a terminate signal so
580 // that they stop pulling.
581 return terminateSignal;
582 } else if (nextTest == null) {
583 // We don't currently have any tests ready for consumption, wait.
584 return waitSignal;
585 } else {
586 return "${nextTest.url}#id=${nextTest.id}";
587 }
588 }
589
590 String getDriverUrl(String browserId) {
591 if (httpServer == null) {
592 print("Bad browser testing server, you are not started yet. Can't "
593 "produce driver url");
594 exit(1);
595 // This should never happen - exit immediately;
596 }
597 return "http://$server:${httpServer.port}/driver/$browserId";
598 }
599
600
601 String getDriverPage(String browserId) {
602 String driverContent = """
603 <!DOCTYPE html><html>
604 <head>
605 <title>Driving page</title>
606 <script type='text/javascript'>
607 var numberOfTests = 0;
608 var currentId;
609 var testing_window;
610
611 function newTaskHandler() {
612 if (this.readyState == this.DONE) {
613 if (this.status == 200) {
614 if (this.responseText == '$waitSignal') {
615 setTimeout(getNextTask, 500);
616 } else if (this.responseText == 'TERMINATE') {
kustermann 2013/05/14 09:05:24 Use the constant above (i.e. " == '$terminateSigna
617 // Don't do anything, we will be killed shortly.
618 } else {
619 // TODO(ricow): Do something more clever here.
620 if (nextTask != undefined) alert('This is really bad');
621 // The task is send to us as:
622 // URL#ID
623 var split = this.responseText.split('#');
624 var nextTask = split[0];
625 id = split[1];
626 run(nextTask);
627 }
628 } else {
629 // We are basically in trouble - do something clever.
630 }
631 }
632 }
633
634 function getNextTask() {
635 var client = new XMLHttpRequest();
636 client.onreadystatechange = newTaskHandler;
637 client.open('GET', '$nextTestPath/$browserId');
638 client.send();
639 }
640
641 function run(url) {
642 numberOfTests++;
643 document.getElementById('number').innerHTML = numberOfTests;
644 if (testing_window == undefined) {
645 testing_window = window.open(url);
646 } else {
647 testing_window.location = url;
648 }
649 }
650
651 function reportMessage(msg) {
652 var client = new XMLHttpRequest();
653 function handleReady() {
654 if (this.readyState == this.DONE) {
655 getNextTask();
656 }
657 }
658 client.onreadystatechange = handleReady;
659 client.open('POST', '$reportPath/${browserId}?id=' + id);
660 client.setRequestHeader('Content-type',
661 'application/x-www-form-urlencoded');
662 client.send(msg);
663 // TODO(ricow) add error handling to somehow report the fact that
664 // we could not send back a result.
665 }
666
667 function messageHandler(e) {
668 var msg = e.data;
669 if (typeof msg != 'string') return;
670 reportMessage(msg);
671 }
672
673 window.addEventListener('message', messageHandler, false);
674 waitForDone = false;
675
676 getNextTask();
677
678 </script>
679 </head>
680 <body>
681 Dart test driver, number of tests: <div id="number"></div>
682 </body>
683 </html>
684 """;
685 return driverContent;
686 }
687 }
OLDNEW
« no previous file with comments | « pkg/unittest/lib/test_controller.js ('k') | tools/testing/dart/test_options.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698