blob: 27cd1cbeadbcb7977c3ba520f0214340a7fbe70a [file] [log] [blame]
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// The following set of variables should be set by the caller that
// #sources this file.
/** Whether to include elapsed time. */
part of test_controller;
bool includeTime;
/** Path to DRT executable. */
String drt;
/** Whether to regenerate layout test files. */
bool regenerate;
/** Whether to output test summary. */
bool summarize;
/** Whether to print results immediately as they come in. */
bool immediate;
/** Format strings to use for test result messages. */
String passFormat, failFormat, errorFormat, listFormat;
/** Location of the running test file. */
String sourceDir;
/** Path of the running test file. */
String testfile;
/** URL of the child test file. */
String baseUrl;
/** The print function to use. */
Function tprint;
/** A callback function to notify the caller we are done. */
Function notifyDone;
// Variable below here are local to this file.
var passCount = 0, failCount = 0, errorCount = 0;
DateTime start;
class Macros {
static const String testTime = '<TIME>';
static const String testfile = '<FILENAME>';
static const String testGroup = '<GROUPNAME>';
static const String testDescription = '<TESTNAME>';
static const String testMessage = '<MESSAGE>';
static const String testStacktrace = '<STACK>';
}
String formatMessage(filename, groupname,
[testname = '', testTime = '', result = '',
message = '', stack = '']) {
var format = errorFormat;
if (result == 'pass') format = passFormat;
else if (result == 'fail') format = failFormat;
return format.
replaceAll(Macros.testTime, testTime).
replaceAll(Macros.testfile, filename).
replaceAll(Macros.testGroup, groupname).
replaceAll(Macros.testDescription, testname).
replaceAll(Macros.testMessage, message).
replaceAll(Macros.testStacktrace, stack);
}
void outputResult(start, label, result, [message = '']) {
var idx = label.lastIndexOf('###');
var group = '', test = '';
if (idx >= 0) {
group = '${label.substring(0, idx).replaceAll("###", " ")} ';
test = '${label.substring(idx+3)} ';
} else {
test = '$label ';
}
var elapsed = '';
if (includeTime) {
var end = new DateTime.now();
double duration = (end.difference(start)).inMilliseconds.toDouble();
duration /= 1000;
elapsed = '${duration.toStringAsFixed(3)}s ';
}
tprint(formatMessage('$testfile ', group, test, elapsed, result, message));
}
pass(start, label) {
++passCount;
outputResult(start, label, 'pass');
}
fail(start, label, message) {
++failCount;
outputResult(start, label, 'fail', message);
}
error(start, label, message) {
++errorCount;
outputResult(start, label, 'error', message);
}
void printSummary(String testFile, int passed, int failed, int errors,
[String uncaughtError = '']) {
tprint('');
if (passed == 0 && failed == 0 && errors == 0) {
tprint('$testFile: No tests found.');
} else if (failed == 0 && errors == 0 && uncaughtError == null) {
tprint('$testFile: All $passed tests passed.');
} else {
if (uncaughtError != null) {
tprint('$testFile: Top-level uncaught error: $uncaughtError');
}
tprint('$testFile: $passed PASSED, $failed FAILED, $errors ERRORS');
}
}
complete() {
if (summarize) {
printSummary(testfile, passCount, failCount, errorCount);
}
notifyDone(failCount > 0 ? -1 : 0);
}
/*
* Run an external process [cmd] with command line arguments [args].
* [timeout] can be used to forcefully terminate the process after
* some number of seconds. This is used by runCommand and startProcess.
* If [procId] is non-zero (i.e. called from startProcess) then a reference
* to the [Process] will be put in a map with key [procId]; in this case
* the process can be terminated later by calling [stopProcess] and
* passing in the [procId].
* [outputMonitor] is an optional function that will be called back with each
* line of output from the process.
* Returns a [Future] for when the process terminates.
*/
Future _processHelper(String command, List<String> args,
List stdout, List stderr,
int timeout, int procId, Function outputMonitor, bool raw) {
var timer = null;
return Process.start(command, args).then((process) {
timer = new Timer(new Duration(seconds: timeout), () {
timer = null;
process.kill();
});
if (raw) {
process.stdout.listen((c) { stdout.addAll(c); });
} else {
_pipeStream(process.stdout, stdout, outputMonitor);
}
_pipeStream(process.stderr, stderr, outputMonitor);
return process.exitCode;
}).then((exitCode) {
if (timer != null) {
timer.cancel();
}
return exitCode;
})
.catchError((e) {
stderr.add("#Error starting process $command: ${e.error}");
});
}
void _pipeStream(Stream stream, List<String> destination,
Function outputMonitor) {
stream
.transform(new StringDecoder())
.transform(new LineTransformer())
.listen((String line) {
if (outputMonitor != null) {
outputMonitor(line);
}
destination.add(line);
});
}
/**
* Run an external process [cmd] with command line arguments [args].
* [timeout] can be used to forcefully terminate the process after
* some number of seconds.
* Returns a [Future] for when the process terminates.
*/
Future runCommand(String command, List<String> args,
List stdout, List stderr,
{int timeout: 300, Function outputMonitor,
bool raw: false}) {
return _processHelper(command, args, stdout, stderr,
timeout, 0, outputMonitor, raw);
}
String parseLabel(String line) {
if (line.startsWith('CONSOLE MESSAGE')) {
var idx = line.indexOf('#TEST ');
if (idx > 0) {
return line.substring(idx + 6);
}
}
return null;
}
runTextLayoutTest(testNum) {
var url = '$baseUrl?test=$testNum';
var stdout = new List();
var stderr = new List();
start = new DateTime.now();
runCommand(drt, [url], stdout, stderr).then((e) {
if (stdout.length > 0 && stdout[stdout.length-1].startsWith('#EOF')) {
stdout.removeLast();
}
var done = false;
var i = 0;
var label = null;
var contentMarker = 'layer at ';
while (i < stdout.length) {
if (label == null && (label = parseLabel(stdout[i])) != null) {
if (label == 'NONEXISTENT') {
complete();
return;
}
} else if (stdout[i].startsWith(contentMarker)) {
if (label == null) {
complete();
return;
}
var expectedFileName =
'$sourceDir${Platform.pathSeparator}'
'${label.replaceAll("###", "_")
.replaceAll(new RegExp("[^A-Za-z0-9]"),"_")}.txt';
var expected = new File(expectedFileName);
if (regenerate) {
var osink = expected.openWrite();
while (i < stdout.length) {
osink.write(stdout[i]);
osink.write('\n');
i++;
}
osink.close();
pass(start, label);
} else if (!expected.existsSync()) {
fail(start, label, 'No expectation file');
} else {
var lines = expected.readAsLinesSync();
var actualLength = stdout.length - i;
var compareCount = min(lines.length, actualLength);
var match = true;
for (var j = 0; j < compareCount; j++) {
if (lines[j] != stdout[i + j]) {
fail(start, label, 'Expectation differs at line ${j + 1}');
match = false;
break;
}
}
if (match) {
if (lines.length != actualLength) {
fail(start, label, 'Expectation file has wrong length');
} else {
pass(start, label);
}
}
}
done = true;
break;
}
i++;
}
if (label != null) {
if (!done) error(start, label, 'Failed to parse output');
runTextLayoutTest(testNum + 1);
}
});
}
runPixelLayoutTest(int testNum) {
var url = '$baseUrl?test=$testNum';
var stdout = new List();
var stderr = new List();
start = new DateTime.now();
runCommand(drt, ["$url'-p"], stdout, stderr, raw:true).then((exitCode) {
var contentMarker = 'Content-Length: ';
var eol = '\n'.codeUnitAt(0);
var pos = 0;
var label = null;
var done = false;
while(pos < stdout.length) {
StringBuffer sb = new StringBuffer();
while (pos < stdout.length && stdout[pos] != eol) {
sb.writeCharCode(stdout[pos++]);
}
if (++pos >= stdout.length && line == '') break;
var line = sb.toString();
if (label == null && (label = parseLabel(line)) != null) {
if (label == 'NONEXISTENT') {
complete();
}
} else if (line.startsWith(contentMarker)) {
if (label == null) {
complete();
}
var len = int.parse(line.substring(contentMarker.length));
var expectedFileName =
'$sourceDir${Platform.pathSeparator}'
'${label.replaceAll("###","_").
replaceAll(new RegExp("[^A-Za-z0-9]"),"_")}.png';
var expected = new File(expectedFileName);
if (regenerate) {
var osink = expected.openWrite();
stdout.removeRange(0, pos);
stdout.length = len;
osink.add(stdout);
osink.close();
pass(start, label);
} else if (!expected.existsSync()) {
fail(start, label, 'No expectation file');
} else {
var bytes = expected.readAsBytesSync();
if (bytes.length != len) {
fail(start, label, 'Expectation file has wrong length');
} else {
var match = true;
for (var j = 0; j < len; j++) {
if (bytes[j] != stdout[pos + j]) {
fail(start, label, 'Expectation differs at byte ${j + 1}');
match = false;
break;
}
}
if (match) pass(start, label);
}
}
done = true;
break;
}
}
if (label != null) {
if (!done) error(start, label, 'Failed to parse output');
runPixelLayoutTest(testNum + 1);
}
});
}
void init() {
// Get the name of the directory that has the expectation files
// (by stripping .dart suffix from test file path).
// Create it if it does not exist.
sourceDir = testfile.substring(0, testfile.length - 5);
if (regenerate) {
var d = new Directory(sourceDir);
if (!d.existsSync()) {
d.createSync();
}
}
}
void runPixelLayoutTests() {
init();
runPixelLayoutTest(0);
}
void runTextLayoutTests() {
init();
runTextLayoutTest(0);
}