blob: 43faad8f81f6a395dffcd5d1670a7e9bc6ac04dd [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.
/**
* Classes and methods for enumerating and preparing tests.
*
* This library includes:
*
* - Creating tests by listing all the Dart files in certain directories,
* and creating [TestCase]s for those files that meet the relevant criteria.
* - Preparing tests, including copying files and frameworks to temporary
* directories, and computing the command line and arguments to be run.
*/
#library("test_suite");
#import("dart:io");
#import("dart:builtin");
#import("dart:isolate");
#import("status_file_parser.dart");
#import("test_runner.dart");
#import("multitest.dart");
#import("drt_updater.dart");
#source("browser_test.dart");
// TODO(rnystrom): Add to dart:core?
/**
* A simple function that tests [arg] and returns `true` or `false`.
*/
typedef bool Predicate<T>(T arg);
/**
* A TestSuite represents a collection of tests. It creates a [TestCase]
* object for each test to be run, and passes the test cases to a callback.
*
* Most TestSuites represent a directory or directory tree containing tests,
* and a status file containing the expected results when these tests are run.
*/
interface TestSuite {
/**
* Call the callback function onTest with a [TestCase] argument for each
* test in the suite. When all tests have been processed, call [onDone].
*
* The [testCache] argument provides a persistent store that can be used to
* cache information about the test suite, so that directories do not need
* to be listed each time. If the tests require a temporary directory for
* their files, they can get one from [globalTempDir].
*/
void forEachTest(Function onTest, Map testCache, String globalTempDir(),
[Function onDone]);
}
class CCTestListerIsolate extends Isolate {
CCTestListerIsolate() : super.heavy();
void main() {
port.receive((String runnerPath, SendPort replyTo) {
var p = new Process.start(runnerPath, ["--list"]);
StringInputStream stdoutStream = new StringInputStream(p.stdout);
List<String> tests = new List<String>();
stdoutStream.onLine = () {
String line = stdoutStream.readLine();
while (line != null) {
tests.add(line);
line = stdoutStream.readLine();
}
};
p.onError = (error) {
print("Failed to list tests: $runnerPath --list");
replyTo.send("");
};
p.onExit = (code) {
if (code < 0) {
print("Failed to list tests: $runnerPath --list");
replyTo.send("");
}
for (String test in tests) {
replyTo.send(test);
}
replyTo.send("");
};
port.close();
});
}
}
/**
* A specialized [TestSuite] that runs tests written in C to unit test
* the Dart virtual machine and its API.
*
* The tests are compiled into a monolithic executable by the build step.
* The executable lists its tests when run with the --list command line flag.
* Individual tests are run by specifying them on the command line.
*/
class CCTestSuite implements TestSuite {
Map configuration;
final String suiteName;
String runnerPath;
final String dartDir;
List<String> statusFilePaths;
Function doTest;
Function doDone;
ReceivePort receiveTestName;
TestExpectations testExpectations;
CCTestSuite(Map this.configuration,
String this.suiteName,
String runnerName,
List<String> this.statusFilePaths)
: dartDir = TestUtils.dartDir() {
runnerPath = TestUtils.buildDir(configuration) + '/' + runnerName;
}
void testNameHandler(String testName, ignore) {
if (testName == "") {
receiveTestName.close();
doDone(true);
} else {
// Only run the tests that match the pattern. Use the name
// "suiteName/testName" for cc tests.
RegExp pattern = configuration['selectors'][suiteName];
String constructedName = '$suiteName/$testName';
if (!pattern.hasMatch(constructedName)) return;
var expectations = testExpectations.expectations(testName);
if (configuration["report"]) {
SummaryReport.add(expectations);
}
if (expectations.contains(SKIP)) return;
// The cc test runner takes options after the name of the test
// to run.
var args = [testName];
args.addAll(TestUtils.standardOptions(configuration));
doTest(new TestCase('$suiteName/$testName',
[new Command(runnerPath, args)],
configuration,
completeHandler,
expectations));
}
}
void forEachTest(Function onTest, Map testCache, String globalTempDir(),
[Function onDone]) {
doTest = onTest;
doDone = (ignore) => (onDone != null) ? onDone() : null;
var filesRead = 0;
void statusFileRead() {
filesRead++;
if (filesRead == statusFilePaths.length) {
receiveTestName = new ReceivePort();
new CCTestListerIsolate().spawn().then((port) {
port.send(runnerPath, receiveTestName.toSendPort());
receiveTestName.receive(testNameHandler);
});
}
}
testExpectations = new TestExpectations();
for (var statusFilePath in statusFilePaths) {
ReadTestExpectationsInto(testExpectations,
'$dartDir/$statusFilePath',
configuration,
statusFileRead);
}
}
void completeHandler(TestCase testCase) {
}
}
class TestInformation {
String filename;
Map optionsFromFile;
bool isNegative;
bool isNegativeIfChecked;
bool hasFatalTypeErrors;
bool hasRuntimeErrors;
Set<String> multitestOutcome;
TestInformation(this.filename, this.optionsFromFile, this.isNegative,
this.isNegativeIfChecked, this.hasFatalTypeErrors,
this.hasRuntimeErrors, this.multitestOutcome);
}
/**
* A standard [TestSuite] implementation that searches for tests in a
* directory, and creates [TestCase]s that compile and/or run them.
*/
class StandardTestSuite implements TestSuite {
Map configuration;
String suiteName;
String directoryPath;
List<String> statusFilePaths;
Function doTest;
Function doDone;
int activeTestGenerators = 0;
bool listingDone = false;
TestExpectations testExpectations;
List<TestInformation> cachedTests;
final String dartDir;
Function globalTemporaryDirectory;
Predicate<String> isTestFilePredicate;
StandardTestSuite(Map this.configuration,
String this.suiteName,
String this.directoryPath,
List<String> this.statusFilePaths,
[Predicate<String> this.isTestFilePredicate])
: dartDir = TestUtils.dartDir();
/**
* Creates a test suite whose file organization matches an expected structure.
* To use this, your suite should look like:
*
* dart/
* path/
* to/
* mytestsuite/
* mytestsuite.status
* example1_tests.dart
* example2_tests.dart
* example3_tests.dart
*
* The important parts:
*
* * The leaf directory name is the name of your test suite.
* * The status file uses the same name.
* * Test files are directly in that directory and end in "_tests.dart".
*
* If you follow that convention, then you can construct one of these like:
*
* new DirectoryTestSuite(configuration, 'path/to/mytestsuite');
*
* instead of having to create a custom [StandardTestSuite] subclass. In
* particular, if you add 'path/to/mytestsuite' to [TEST_SUITE_DIRECTORIES]
* in test.dart, this will all be set up for you.
*/
factory StandardTestSuite.forDirectory(
Map configuration, String directory) {
final name = directory.substring(directory.lastIndexOf('/') + 1);
return new StandardTestSuite(configuration,
name, directory, ['$directory/$name.status'],
(filename) => filename.endsWith('_tests.dart'));
}
/**
* The default implementation assumes a file is a test if
* it ends in "Test.dart".
*/
bool isTestFile(String filename) {
// Use the specified predicate, if provided.
if (isTestFilePredicate != null) return isTestFilePredicate(filename);
return filename.endsWith("Test.dart");
}
bool listRecursively() => false;
String shellPath() => TestUtils.dartShellFileName(configuration);
List<String> additionalOptions(String filename) => [];
void forEachTest(Function onTest, Map testCache, String globalTempDir(),
[Function onDone = null]) {
// If DumpRenderTree/Dartium is required, and not yet updated,
// wait for update.
var updater = runtimeUpdater(configuration['runtime']);
if (updater !== null && !updater.updated) {
Expect.isTrue(updater.isActive);
updater.onUpdated.add(() {
forEachTest(onTest, testCache, globalTempDir, onDone);
});
return;
}
doTest = onTest;
doDone = (onDone != null) ? onDone : (() => null);
globalTemporaryDirectory = globalTempDir;
var filesRead = 0;
void statusFileRead() {
filesRead++;
if (filesRead == statusFilePaths.length) {
// Checked if we have already found and generated the tests for
// this suite.
if (!testCache.containsKey(suiteName)) {
cachedTests = testCache[suiteName] = [];
processDirectory();
} else {
// We rely on enqueueing completing asynchronously so use a
// timer to make it so.
void enqueueCachedTests(Timer ignore) {
for (var info in testCache[suiteName]) {
enqueueTestCaseFromTestInformation(info);
}
doDone();
}
new Timer(0, enqueueCachedTests);
}
}
}
// Read test expectations from status files.
testExpectations = new TestExpectations();
for (var statusFilePath in statusFilePaths) {
ReadTestExpectationsInto(testExpectations,
'$dartDir/$statusFilePath',
configuration,
statusFileRead);
}
}
void processDirectory() {
directoryPath = '$dartDir/$directoryPath';
Directory dir = new Directory(directoryPath);
dir.onError = (s) {
throw s;
};
dir.exists((bool exists) {
if (!exists) {
print('Directory containing tests not found: $directoryPath');
directoryListingDone(false);
} else {
dir.onFile = processFile;
dir.onDone = directoryListingDone;
dir.list(recursive: listRecursively());
}
});
}
void enqueueTestCaseFromTestInformation(TestInformation info) {
var filename = info.filename;
var optionsFromFile = info.optionsFromFile;
var isNegative = info.isNegative;
// Look up expectations in status files using a modified file path.
String testName;
filename = filename.replaceAll('\\', '/');
// See if there's a 'src' directory inside the 'tests' one.
int testsStart = filename.lastIndexOf('tests/');
int start = filename.lastIndexOf('src/');
if (start > testsStart) {
testName = filename.substring(start + 4, filename.length - 5);
} else if (optionsFromFile['isMultitest']) {
start = filename.lastIndexOf('/');
int middle = filename.lastIndexOf('_');
testName = filename.substring(start + 1, middle) + '/' +
filename.substring(middle + 1, filename.length - 5);
} else {
// This case is hit by the dartc client compilation
// tests. These tests are pretty broken compared to the
// rest. They use the .dart suffix in the status files. They
// find tests in weird ways (testing that they contain "#").
// They need to be redone.
// TODO(1058): This does not work on Windows.
start = filename.indexOf(directoryPath);
if (start != -1) {
testName = filename.substring(start + directoryPath.length + 1);
} else {
testName = filename;
}
if (configuration['compiler'] != 'dartc') {
if (testName.endsWith('.dart')) {
testName = testName.substring(0, testName.length - 5);
}
}
}
int shards = configuration['shards'];
if (shards > 1) {
int shard = configuration['shard'];
if (testName.hashCode() % shards != shard - 1) {
return;
}
}
Set<String> expectations = testExpectations.expectations(testName);
if (configuration['report']) {
// Tests with multiple VMOptions are counted more than once.
for (var dummy in optionsFromFile["vmOptions"]) {
SummaryReport.add(expectations);
}
}
if (expectations.contains(SKIP)) return;
if (TestUtils.isBrowserRuntime(configuration['runtime'])) {
enqueueBrowserTest(info, testName, expectations);
} else {
enqueueStandardTest(info, testName, expectations);
}
}
void enqueueStandardTest(TestInformation info,
String testName,
Set<String> expectations) {
bool isNegative = info.isNegative ||
(configuration['checked'] && info.isNegativeIfChecked);
if (configuration['compiler'] == 'dartc') {
// dartc can detect static type warnings by the
// format of the error line
if (info.hasFatalTypeErrors) {
isNegative = true;
} else if (info.hasRuntimeErrors) {
isNegative = false;
}
}
var argumentLists = argumentListsFromFile(info.filename,
info.optionsFromFile);
for (var args in argumentLists) {
doTest(new TestCase('$suiteName/$testName',
[new Command(shellPath(), args)],
configuration,
completeHandler,
expectations,
isNegative,
info));
}
}
Function makeTestCaseCreator(Map optionsFromFile) {
return (String filename,
bool isNegative,
[bool isNegativeIfChecked = false,
bool hasFatalTypeErrors = false,
bool hasRuntimeErrors = false,
Set<String> multitestOutcome = null]) {
// Cache the test information for each test case.
var info = new TestInformation(filename,
optionsFromFile,
isNegative,
isNegativeIfChecked,
hasFatalTypeErrors,
hasRuntimeErrors,
multitestOutcome);
cachedTests.add(info);
enqueueTestCaseFromTestInformation(info);
};
}
void processFile(String filename) {
if (!isTestFile(filename)) return;
// Only run the tests that match the pattern.
RegExp pattern = configuration['selectors'][suiteName];
if (!pattern.hasMatch(filename)) return;
if (filename.endsWith('test_config.dart')) return;
var optionsFromFile = readOptionsFromFile(filename);
Function createTestCase = makeTestCaseCreator(optionsFromFile);
if (optionsFromFile['isMultitest']) {
testGeneratorStarted();
DoMultitest(filename,
TestUtils.buildDir(configuration),
directoryPath,
createTestCase,
testGeneratorDone);
} else {
createTestCase(filename, optionsFromFile['isNegative']);
}
}
/**
* The [StandardTestSuite] has support for tests that
* compile a test from Dart to Javascript, and then run the resulting
* Javascript. This function creates a working directory to hold the
* Javascript version of the test, and copies the appropriate framework
* files to that directory. It creates a [BrowserTestCase], which has
* two sequential steps to be run by the [ProcessQueue when] the test is
* executed: a compilation
* step and an execution step, both with the appropriate executable and
* arguments.
*/
void enqueueBrowserTest(TestInformation info,
String testName,
Set<String> expectations) {
Map optionsFromFile = info.optionsFromFile;
String filename = info.filename;
if (optionsFromFile['isMultitest']) return;
bool isWebTest = optionsFromFile['containsDomImport'];
bool isLibraryDefinition = optionsFromFile['isLibraryDefinition'];
if (!isLibraryDefinition && optionsFromFile['containsSourceOrImport']) {
print('Warning for $filename: Browser tests require #library ' +
'in any file that uses #import, #source, or #resource');
}
final String compiler = configuration['compiler'];
final String runtime = configuration['runtime'];
final String testPath =
new File(filename).fullPathSync().replaceAll('\\', '/');
for (var vmOptions in optionsFromFile['vmOptions']) {
// Create a unique temporary directory for each set of vmOptions.
// TODO(dart:429): Replace separate replaceAlls with a RegExp when
// replaceAll(RegExp, String) is implemented.
String optionsName = '';
if (optionsFromFile['vmOptions'].length > 1) {
optionsName = Strings.join(vmOptions, '-').replaceAll('-','')
.replaceAll('=','')
.replaceAll('/','');
}
Directory tempDir = createOutputDirectory(testPath, optionsName);
String dartWrapperFilename = '${tempDir.path}/test.dart';
String compiledDartWrapperFilename = '${tempDir.path}/test.js';
String domLibraryImport = 'dart:dom';
String htmlPath = '${tempDir.path}/test.html';
if (!isWebTest) {
// test.dart will import the dart test directly, if it is a library,
// or indirectly through test_as_library.dart, if it is not.
String dartLibraryFilename;
if (isLibraryDefinition) {
dartLibraryFilename = testPath;
} else {
dartLibraryFilename = 'test_as_library.dart';
File file = new File('${tempDir.path}/$dartLibraryFilename');
RandomAccessFile dartLibrary = file.openSync(FileMode.WRITE);
dartLibrary.writeStringSync(WrapDartTestInLibrary(testPath));
dartLibrary.closeSync();
}
File file = new File(dartWrapperFilename);
RandomAccessFile dartWrapper = file.openSync(FileMode.WRITE);
dartWrapper.writeStringSync(DartTestWrapper(
domLibraryImport,
'$dartDir/tests/isolate/src/TestFramework.dart',
dartLibraryFilename));
dartWrapper.closeSync();
} else {
dartWrapperFilename = testPath;
// TODO(whesse): Once test.py is retired, adjust the relative path in
// the client/samples/dartcombat test to its css file, remove the
// "../../" from this path, and move this out of the isWebTest guard.
// Also remove getHtmlName, and just use test.html.
// TODO(efortuna): this shortening of htmlFilename is a band-aid until
// the above TODO gets fixed. Windows cannot have paths that are longer
// than 260 characters, and without this hack, we were running past the
// the limit.
String htmlFilename = getHtmlName(filename);
while ('${tempDir.path}/../$htmlFilename'.length >= 260) {
htmlFilename = htmlFilename.substring(htmlFilename.length~/2);
}
htmlPath = '${tempDir.path}/../$htmlFilename';
}
final String scriptPath = (compiler == 'none') ?
dartWrapperFilename : compiledDartWrapperFilename;
// Create the HTML file for the test.
RandomAccessFile htmlTest = new File(htmlPath).openSync(FileMode.WRITE);
String filePrefix = '';
if (Platform.operatingSystem() == 'windows') {
// Firefox on Windows does not like absolute file path names that start
// with 'C:' adding 'file:///' solves the problem.
filePrefix = 'file:///';
}
htmlTest.writeStringSync(GetHtmlContents(
filename,
'$filePrefix$dartDir/lib/unittest/test_controller.js',
scriptType,
filePrefix + scriptPath));
htmlTest.closeSync();
// Construct the command(s) that compile all the inputs needed by the
// browser test. For running Dart in DRT, this will be noop commands.
List<Command> commands = [];
if (compiler != 'none') {
commands.add(_compileCommand(
dartWrapperFilename, compiledDartWrapperFilename,
compiler, tempDir.path, vmOptions));
// some tests require compiling multiple input scripts.
List<String> otherScripts = optionsFromFile['otherScripts'];
for (String name in otherScripts) {
int end = filename.lastIndexOf('/');
if (end == -1) {
print('Warning: error processing "OtherScripts" of $filename.');
print('Skipping test ($testName).');
return;
}
String dir = filename.substring(0, end);
end = name.lastIndexOf('.dart');
if (end == -1) {
print('Warning: error processing "OtherScripts" in $filename.');
print('Skipping test ($testName).');
return;
}
String compiledName = '${name.substring(0, end)}.js';
commands.add(_compileCommand(
'$dir/$name', '${tempDir.path}/$compiledName',
compiler, tempDir.path, vmOptions));
}
}
// Construct the command that executes the browser test
List<String> args;
if (runtime == 'ie' || runtime == 'ff' || runtime == 'chrome' ||
runtime == 'safari' || runtime == 'opera' || runtime == 'dartium') {
args = ['$dartDir/tools/testing/run_selenium.py',
'--browser=$runtime',
'--timeout=${configuration["timeout"] - 2}',
'--out=$htmlPath'];
if (runtime == 'dartium') {
args.add('--executable=$dartiumFilename');
}
} else {
args = [
'$dartDir/tools/testing/drt-trampoline.py',
dumpRenderTreeFilename,
'--no-timeout'
];
if (runtime == 'drt' && compiler == 'none') {
var dartFlags = ['--ignore-unrecognized-flags'];
if (configuration["checked"]) {
dartFlags.add('--enable_asserts');
dartFlags.add("--enable_type_checks");
}
dartFlags.addAll(vmOptions);
args.add('--dart-flags=${Strings.join(dartFlags, " ")}');
}
args.add(htmlPath);
}
commands.add(new Command('python', args));
// Create BrowserTestCase and queue it.
var testCase = new BrowserTestCase(testName, commands, configuration,
completeHandler, expectations, optionsFromFile['isNegative']);
doTest(testCase);
}
}
/** Helper to create a compilation command for a single input file. */
Command _compileCommand(String inputFile, String outputFile,
String compiler, String dir, var vmOptions) {
String executable = TestUtils.compilerPath(configuration);
List<String> args = TestUtils.standardOptions(configuration);
switch (compiler) {
case 'frog':
case 'dart2js':
String libdir = configuration['froglib'];
if (libdir == '') {
libdir = '$dartDir/frog/lib';
}
args.addAll(['--libdir=$libdir',
'--compile-only',
'--out=$outputFile']);
args.addAll(vmOptions);
args.add(inputFile);
break;
default:
Expect.fail('unimplemented compiler $compiler');
}
if (executable.endsWith('.dart')) {
// Run the compiler script via the Dart VM.
args.insertRange(0, 1, executable);
executable = TestUtils.dartShellFileName(configuration);
}
return new Command(executable, args);
}
bool get requiresCleanTemporaryDirectory() =>
configuration['compiler'] == 'dartc';
/**
* Create a directory for the generated test. If a Dart language test
* needs to be run in a browser, the Dart test needs to be embedded in
* an HTML page, with a testing framework based on scripting and DOM events.
* These scripts and pages are written to a generated_test directory,
* usually inside the build directory of the checkout.
*
* Some tests, such as those using the dartc compiler, need to be run
* with an empty directory as the compiler's work directory. These
* tests are copied to a subdirectory of a system-provided temporary
* directory, which is deleted at the end of the test run unless the
* --keep-temporary-files flag is given.
*
* Those tests which are already HTML web applications (web tests), with
* resources including CSS files and HTML files, need to be compiled into
* a work directory where the relative URLS to the resources work.
* We use a subdirectory of the build directory that is the same number
* of levels down in the checkout as the original path of the web test.
*/
Directory createOutputDirectory(String testPath, String optionsName) {
String testUniqueName =
testPath.substring(dartDir.length + 1, testPath.length - 5);
testUniqueName = testUniqueName.replaceAll('/', '_');
testUniqueName += '-$optionsName';
// Create '[build dir]/generated_tests/$compiler-$runtime/$testUniqueName',
// including any intermediate directories that don't exist.
var generatedTestPath = ['generated_tests',
configuration['compiler'] + '-' +
configuration['runtime'],
testUniqueName];
String tempDirPath = TestUtils.buildDir(configuration);
if (requiresCleanTemporaryDirectory) {
tempDirPath = globalTemporaryDirectory();
String debugMode =
(configuration['mode'] == 'debug') ? 'Debug_' : 'Release_';
var temp = ['${debugMode}_${configuration["arch"]}'];
temp.addAll(generatedTestPath);
generatedTestPath = temp;
}
Directory tempDir = new Directory(tempDirPath);
if (!tempDir.existsSync()) {
// Dartium tests can be run with no build step, with no output directory.
// This special case builds the build directory that should be there.
var buildPath = tempDirPath.split('/');
tempDirPath = buildPath[0];
if (tempDirPath == '') {
throw new Exception(
'Non-relative path to build directory in test_suite.dart');
}
if (buildPath.length > 1) {
buildPath.removeRange(0, 1);
if (buildPath.last() == '') buildPath.removeLast();
buildPath.addAll(generatedTestPath);
generatedTestPath = buildPath;
}
tempDir = new Directory(tempDirPath);
if (!tempDir.existsSync()) {
tempDir.createSync();
}
}
tempDirPath = new File(tempDirPath).fullPathSync().replaceAll('\\', '/');
return TestUtils.mkdirRecursive(tempDirPath,
Strings.join(generatedTestPath, '/'));
}
String get scriptType() {
switch (configuration['compiler']) {
case 'none':
return 'application/dart';
case 'frog':
case 'dart2js':
case 'dartc':
return 'text/javascript';
default:
Expect.fail('Non-web runtime, so no scriptType for: ' +
'${configuration["compiler"]}');
return null;
}
}
bool get hasRuntime() {
switch(configuration['runtime']) {
case null:
Expect.fail("configuration['runtime'] is not set");
case 'none':
return false;
default:
return true;
}
}
String getHtmlName(String filename) {
return filename.replaceAll('/', '_').replaceAll(':', '_')
.replaceAll('\\', '_') + configuration['compiler'] + '-' +
configuration['runtime'] + '.html';
}
String get dumpRenderTreeFilename() {
if (configuration['drt'] != '') {
return configuration['drt'];
}
if (Platform.operatingSystem() == 'macos') {
return '$dartDir/client/tests/drt/DumpRenderTree.app/Contents/'
'MacOS/DumpRenderTree';
}
return '$dartDir/client/tests/drt/DumpRenderTree';
}
String get dartiumFilename() {
if (configuration['dartium'] != '') {
return configuration['dartium'];
}
if (Platform.operatingSystem() == 'macos') {
return '$dartDir/client/tests/dartium/Chromium.app/Contents/'
'MacOS/Chromium';
}
return '$dartDir/client/tests/dartium/chrome';
}
void testGeneratorStarted() {
++activeTestGenerators;
}
void testGeneratorDone() {
--activeTestGenerators;
if (activeTestGenerators == 0 && listingDone) {
doDone();
}
}
void directoryListingDone(ignore) {
listingDone = true;
if (activeTestGenerators == 0) {
doDone();
}
}
void completeHandler(TestCase testCase) {
}
List<List<String>> argumentListsFromFile(String filename,
Map optionsFromFile) {
List args = TestUtils.standardOptions(configuration);
args.addAll(additionalOptions(filename));
if (configuration['compiler'] == 'dartc') {
args.add('--error_format');
args.add('machine');
}
if ((configuration['compiler'] == 'frog'
|| configuration['compiler'] == 'dart2js')
&& (configuration['runtime'] == 'none')) {
args.add('--compile-only');
}
bool isMultitest = optionsFromFile["isMultitest"];
List<String> dartOptions = optionsFromFile["dartOptions"];
List<List<String>> vmOptionsList = optionsFromFile["vmOptions"];
Expect.isTrue(!isMultitest || dartOptions == null);
if (dartOptions == null) {
args.add(filename);
} else {
var executable_name = dartOptions[0];
// TODO(ager): Get rid of this hack when the runtime checkout goes away.
var file = new File(executable_name);
if (!file.existsSync()) {
executable_name = '../$executable_name';
Expect.isTrue(new File(executable_name).existsSync());
dartOptions[0] = executable_name;
}
args.addAll(dartOptions);
}
var result = new List<List<String>>();
Expect.isFalse(vmOptionsList.isEmpty(), "empty vmOptionsList");
for (var vmOptions in vmOptionsList) {
var options = new List<String>.from(vmOptions);
options.addAll(args);
result.add(options);
}
return result;
}
Map readOptionsFromFile(String filename) {
RegExp testOptionsRegExp = const RegExp(@"// VMOptions=(.*)");
RegExp dartOptionsRegExp = const RegExp(@"// DartOptions=(.*)");
RegExp otherScriptsRegExp = const RegExp(@"// OtherScripts=(.*)");
RegExp multiTestRegExp = const RegExp(@"/// [0-9][0-9]:(.*)");
RegExp staticTypeRegExp =
const RegExp(@"/// ([0-9][0-9]:){0,1}\s*static type warning");
RegExp compileTimeRegExp =
const RegExp(@"/// ([0-9][0-9]:){0,1}\s*compile-time error");
RegExp staticCleanRegExp = const RegExp(@"// @static-clean");
RegExp leadingHashRegExp = const RegExp(@"^#", multiLine: true);
RegExp isolateStubsRegExp = const RegExp(@"// IsolateStubs=(.*)");
RegExp domImportRegExp =
const RegExp(@"^#import.*(dart:(dom|html)|html\.dart).*\)",
multiLine: true);
RegExp libraryDefinitionRegExp =
const RegExp(@"^#library\(", multiLine: true);
RegExp sourceOrImportRegExp =
const RegExp(@"^#(source|import|resource)\(", multiLine: true);
// Read the entire file into a byte buffer and transform it to a
// String. This will treat the file as ascii but the only parts
// we are interested in will be ascii in any case.
RandomAccessFile file = new File(filename).openSync(FileMode.READ);
List chars = new List(file.lengthSync());
var offset = 0;
while (offset != chars.length) {
offset += file.readListSync(chars, offset, chars.length - offset);
}
file.closeSync();
String contents = new String.fromCharCodes(chars);
chars = null;
// Find the options in the file.
List<List> result = new List<List>();
List<String> dartOptions;
bool isNegative = false;
bool isStaticClean = false;
Iterable<Match> matches = testOptionsRegExp.allMatches(contents);
for (var match in matches) {
result.add(match[1].split(' ').filter((e) => e != ''));
}
if (result.isEmpty()) result.add([]);
matches = dartOptionsRegExp.allMatches(contents);
for (var match in matches) {
if (dartOptions != null) {
throw new Exception(
'More than one "// DartOptions=" line in test $filename');
}
dartOptions = match[1].split(' ').filter((e) => e != '');
}
matches = staticCleanRegExp.allMatches(contents);
for (var match in matches) {
if (isStaticClean) {
throw new Exception(
'More than one "// @static-clean=" line in test $filename');
}
isStaticClean = true;
}
List<String> otherScripts = new List<String>();
matches = otherScriptsRegExp.allMatches(contents);
for (var match in matches) {
otherScripts.addAll(match[1].split(' ').filter((e) => e != ''));
}
if (contents.contains("@compile-error")) {
isNegative = true;
}
if (contents.contains("@runtime-error") && hasRuntime) {
isNegative = true;
}
bool isMultitest = multiTestRegExp.hasMatch(contents);
bool containsLeadingHash = leadingHashRegExp.hasMatch(contents);
Match isolateMatch = isolateStubsRegExp.firstMatch(contents);
String isolateStubs = isolateMatch != null ? isolateMatch[1] : '';
bool containsDomImport = domImportRegExp.hasMatch(contents);
bool isLibraryDefinition = libraryDefinitionRegExp.hasMatch(contents);
bool containsSourceOrImport = sourceOrImportRegExp.hasMatch(contents);
int numStaticTypeAnnotations = 0;
for (var i in staticTypeRegExp.allMatches(contents)) {
numStaticTypeAnnotations++;
}
int numCompileTimeAnnotations = 0;
for (var i in compileTimeRegExp.allMatches(contents)) {
numCompileTimeAnnotations++;
}
return { "vmOptions": result,
"dartOptions": dartOptions,
"isNegative": isNegative,
"isStaticClean" : isStaticClean,
"otherScripts": otherScripts,
"isMultitest": isMultitest,
"containsLeadingHash": containsLeadingHash,
"isolateStubs": isolateStubs,
"containsDomImport": containsDomImport,
"isLibraryDefinition": isLibraryDefinition,
"containsSourceOrImport": containsSourceOrImport,
"numStaticTypeAnnotations": numStaticTypeAnnotations,
"numCompileTimeAnnotations": numCompileTimeAnnotations};
}
}
class DartcCompilationTestSuite extends StandardTestSuite {
List<String> _testDirs;
int activityCount = 0;
DartcCompilationTestSuite(Map configuration,
String suiteName,
String directoryPath,
List<String> this._testDirs,
List<String> expectations)
: super(configuration,
suiteName,
directoryPath,
expectations);
void activityStarted() { ++activityCount; }
void activityCompleted() {
if (--activityCount == 0) {
directoryListingDone(true);
}
}
String shellPath() => TestUtils.compilerPath(configuration);
List<String> additionalOptions(String filename) {
return ['--fatal-warnings', '--fatal-type-errors'];
}
void processDirectory() {
directoryPath = '$dartDir/$directoryPath';
// Enqueueing the directory listers is an activity.
activityStarted();
for (String testDir in _testDirs) {
Directory dir = new Directory("$directoryPath/$testDir");
if (dir.existsSync()) {
activityStarted();
dir.onError = (s) {
throw s;
};
dir.onFile = processFile;
dir.onDone = (ignore) => activityCompleted();
dir.list(recursive: listRecursively());
}
}
// Completed the enqueueing of listers.
activityCompleted();
}
}
/**
* A standard test suite whose file organization matches an expected structure.
* To use this, your suite should look like:
*
* dart/
* path/
* to/
* mytestsuite/
* mytestsuite.status
* example1_tests.dart
* example2_tests.dart
* example3_tests.dart
*
* The important parts:
*
* * The leaf directory name is the name of your test suite.
* * The status file uses the same name.
* * Test files are directly in that directory and end in "_tests.dart".
*
* If you follow that convention, then you can construct one of these like:
*
* new DirectoryTestSuite(configuration, 'path/to/mytestsuite');
*
* instead of having to create a custom [StandardTestSuite] subclass. In
* particular, if you add 'path/to/mytestsuite' to [TEST_SUITE_DIRECTORIES] in
* test.dart, this will all be set up for you.
*/
class DirectoryTestSuite extends StandardTestSuite {
factory DirectoryTestSuite(Map configuration, String directory) {
final name = directory.substring(directory.lastIndexOf('/') + 1);
print(name);
return new DirectoryTestSuite._internal(configuration,
name, directory, ['$directory/$name.status']);
}
DirectoryTestSuite._internal(Map configuration,
String suiteName,
String directoryPath,
List<String> statusFilePaths)
: super(configuration, suiteName, directoryPath, statusFilePaths);
bool isTestFile(String filename) => filename.endsWith('_tests.dart');
}
class JUnitTestSuite implements TestSuite {
Map configuration;
String suiteName;
String directoryPath;
String statusFilePath;
final String dartDir;
String buildDir;
String classPath;
List<String> testClasses;
Function doTest;
Function doDone;
TestExpectations testExpectations;
JUnitTestSuite(Map this.configuration,
String this.suiteName,
String this.directoryPath,
String this.statusFilePath)
: dartDir = TestUtils.dartDir();
bool isTestFile(String filename) => filename.endsWith("Tests.java") &&
!filename.contains('com/google/dart/compiler/vm') &&
!filename.contains('com/google/dart/corelib/SharedTests.java');
void forEachTest(Function onTest,
Map testCacheIgnored,
String globalTempDir(),
[Function onDone = null]) {
doTest = onTest;
doDone = (onDone != null) ? onDone : (() => null);
if (configuration['compiler'] != 'dartc') {
// Do nothing. Asynchronously report that the suite is enqueued.
new Timer(0, (timerUnused){ doDone(); });
return;
}
RegExp pattern = configuration['selectors']['dartc'];
if (!pattern.hasMatch('junit_tests')) {
new Timer(0, (timerUnused){ doDone(); });
return;
}
buildDir = TestUtils.buildDir(configuration);
computeClassPath();
testClasses = <String>[];
// Do not read the status file.
// All exclusions are hardcoded in this script, as they are in testcfg.py.
processDirectory();
}
void processDirectory() {
directoryPath = '$dartDir/$directoryPath';
Directory dir = new Directory(directoryPath);
dir.onError = (s) {
throw s;
};
dir.onFile = processFile;
dir.onDone = createTest;
dir.list(recursive: true);
}
void processFile(String filename) {
if (!isTestFile(filename)) return;
int index = filename.indexOf('compiler/javatests/com/google/dart');
if (index != -1) {
String testRelativePath =
filename.substring(index + 'compiler/javatests/'.length,
filename.length - '.java'.length);
String testClass = testRelativePath.replaceAll('/', '.');
testClasses.add(testClass);
}
}
void createTest(successIgnored) {
var sdkDir = "$buildDir/dart-sdk".trim();
List<String> args = <String>[
'-ea',
'-classpath', classPath,
'-Dcom.google.dart.sdk=$sdkDir',
'-Dcom.google.dart.corelib.SharedTests.test_py=' +
dartDir + '/tools/test.py',
'org.junit.runner.JUnitCore'];
args.addAll(testClasses);
// Lengthen the timeout for JUnit tests. It is normal for them
// to run for a few minutes.
Map updatedConfiguration = new Map();
configuration.forEach((key, value) {
updatedConfiguration[key] = value;
});
updatedConfiguration['timeout'] *= 2;
doTest(new TestCase(suiteName,
[new Command('java', args)],
updatedConfiguration,
completeHandler,
new Set<String>.from([PASS])));
doDone();
}
void completeHandler(TestCase testCase) {
}
void computeClassPath() {
classPath = Strings.join(
['$buildDir/compiler/lib/dartc.jar',
'$buildDir/compiler-tests.jar',
'$buildDir/closure_out/compiler.jar',
// Third party libraries.
'$dartDir/third_party/args4j/2.0.12/args4j-2.0.12.jar',
'$dartDir/third_party/guava/r09/guava-r09.jar',
'$dartDir/third_party/rhino/1_7R3/js.jar',
'$dartDir/third_party/hamcrest/v1_3/hamcrest-core-1.3.0RC2.jar',
'$dartDir/third_party/hamcrest/v1_3/hamcrest-generator-1.3.0RC2.jar',
'$dartDir/third_party/hamcrest/v1_3/hamcrest-integration-1.3.0RC2.jar',
'$dartDir/third_party/hamcrest/v1_3/hamcrest-library-1.3.0RC2.jar',
'$dartDir/third_party/junit/v4_8_2/junit.jar'],
':'); // Path separator.
}
}
class TestUtils {
/**
* Creates a directory using a [relativePath] to an existing
* [base] directory if that [relativePath] does not already exist.
*/
static Directory mkdirRecursive(String base, String relativePath) {
Directory baseDir = new Directory(base);
Expect.isTrue(baseDir.existsSync(),
"Expected ${base} to already exist");
var tempDir = new Directory(base);
for (String dir in relativePath.split('/')) {
base = "$base/$dir";
tempDir = new Directory(base);
if (!tempDir.existsSync()) {
tempDir.createSync();
}
Expect.isTrue(tempDir.existsSync(), "Failed to create ${tempDir.path}");
}
return tempDir;
}
/**
* Copy a [source] file to a new place.
* Assumes that the directory for [dest] already exists.
*/
static void copyFile(File source, File dest) {
List contents = source.readAsBytesSync();
RandomAccessFile handle = dest.openSync(FileMode.WRITE);
handle.writeListSync(contents, 0, contents.length);
handle.closeSync();
}
static String executableSuffix(String executable) {
if (Platform.operatingSystem() == 'windows') {
if (executable == 'd8' || executable == 'vm' || executable == 'none') {
return '.exe';
} else {
return '.bat';
}
}
return '';
}
static String executableName(Map configuration) {
String suffix = executableSuffix(configuration['compiler']);
switch (configuration['compiler']) {
case 'none':
return 'dart$suffix';
case 'dartc':
return 'compiler/bin/dartc$suffix';
case 'frog':
case 'dart2js':
return 'frog/bin/frog$suffix';
default:
throw "Unknown executable for: ${configuration['compiler']}";
}
}
static String compilerName(Map configuration) {
String suffix = executableSuffix(configuration['compiler']);
switch (configuration['compiler']) {
case 'dartc':
return 'compiler/bin/dartc$suffix';
case 'frog':
case 'dart2js':
return 'frog/bin/frog$suffix';
default:
throw "Unknown compiler for: ${configuration['compiler']}";
}
}
static String dartShellFileName(Map configuration) {
var name = configuration['dart'];
if (name == '') {
name = '${buildDir(configuration)}/${executableName(configuration)}';
}
if (!(new File(name)).existsSync() && !configuration['list']) {
throw "Executable '$name' does not exist";
}
return name;
}
static String compilerPath(Map configuration) {
if (configuration['compiler'] == 'none') {
return null; // No separate compiler for dartium tests.
}
var name = configuration['frog'];
if (name == '') {
name = '${buildDir(configuration)}/${compilerName(configuration)}';
}
if (!(new File(name)).existsSync() && !configuration['list']) {
throw "Executable '$name' does not exist";
}
return name;
}
static String outputDir(Map configuration) {
var result = '';
var system = configuration['system'];
if (system == 'linux') {
result = 'out/';
} else if (system == 'macos') {
result = 'xcodebuild/';
}
return result;
}
static String buildDir(Map configuration) {
var result = outputDir(configuration);
result += (configuration['mode'] == 'debug') ? 'Debug_' : 'Release_';
result += configuration['arch'];
return result;
}
static String dartDir() {
String scriptPath = new Options().script.replaceAll('\\', '/');
String toolsDir = scriptPath.substring(0, scriptPath.lastIndexOf('/'));
return new File('$toolsDir/..').fullPathSync().replaceAll('\\', '/');
}
static List<String> standardOptions(Map configuration) {
List args = ["--ignore-unrecognized-flags"];
if (configuration["checked"]) {
args.add('--enable_asserts');
args.add("--enable_type_checks");
}
if (configuration["compiler"] == "dart2js") {
args.add("--verbose");
args.add("--leg");
if (configuration["host_checked"]) {
args.add("--vm_flags=--enable_asserts --enable_type_checks");
}
if (configuration['runtime'] != 'drt') {
args.add("--allow-mock-compilation");
}
}
return args;
}
static bool isBrowserRuntime(String runtime) =>
const <String>['drt',
'dartium',
'ie',
'safari',
'opera',
'chrome',
'ff'].some((x) => x == runtime);
}
class SummaryReport {
static int total = 0;
static int skipped = 0;
static int noCrash = 0;
static int pass = 0;
static int failOk = 0;
static int fail = 0;
static int crash = 0;
static int timeout = 0;
static void add(Set<String> expectations) {
++total;
if (expectations.contains(SKIP)) {
++skipped;
} else {
if (expectations.contains(PASS) && expectations.contains(FAIL) &&
!expectations.contains(CRASH) && !expectations.contains(OK)) {
++noCrash;
}
if (expectations.contains(PASS) && expectations.length == 1) {
++pass;
}
if (expectations.containsAll([FAIL, OK]) && expectations.length == 2) {
++failOk;
}
if (expectations.contains(FAIL) && expectations.length == 1) {
++fail;
}
if (expectations.contains(CRASH) && expectations.length == 1) {
++crash;
}
if (expectations.contains(TIMEOUT)) {
++timeout;
}
}
}
static void printReport() {
if (total == 0) return;
String report = """Total: $total tests
* $skipped tests will be skipped
* $noCrash tests are expected to be flaky but not crash
* $pass tests are expected to pass
* $failOk tests are expected to fail that we won't fix
* $fail tests are expected to fail that we should fix
* $crash tests are expected to crash that we should fix
* $timeout tests are allowed to timeout
""";
print(report);
}
}