mirror of
https://github.com/dart-lang/sdk
synced 2024-11-05 18:22:09 +00:00
87b5112d5b
Change-Id: I42e0c02b3aac7f952c5346bbcddd50293bf2b5b8 Reviewed-on: https://dart-review.googlesource.com/58620 Commit-Queue: Zach Anderson <zra@google.com> Reviewed-by: Leaf Petersen <leafp@google.com>
1008 lines
33 KiB
Dart
1008 lines
33 KiB
Dart
// Copyright (c) 2016, 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.
|
|
|
|
import "dart:async";
|
|
import "dart:io";
|
|
import "dart:convert" show json;
|
|
import "package:path/path.dart" as p;
|
|
import "package:async_helper/async_helper.dart";
|
|
|
|
/// Root directory of generated files.
|
|
/// Path contains trailing slash.
|
|
/// Each configuration gets its own sub-directory.
|
|
Directory fileRoot;
|
|
|
|
/// Shared HTTP server serving the files in [httpFiles].
|
|
/// Each configuration gets its own "sub-dir" entry in `httpFiles`.
|
|
HttpServer httpServer;
|
|
|
|
/// Directory structure served by HTTP server.
|
|
Map<String, dynamic> httpFiles = {};
|
|
|
|
/// List of configurations.
|
|
List<Configuration> configurations = [];
|
|
|
|
/// Collection of failing tests and their failure messages.
|
|
///
|
|
/// Each test may fail in more than one way.
|
|
var failingTests = <String, List<String>>{};
|
|
|
|
main() async {
|
|
asyncStart();
|
|
await setUp();
|
|
|
|
await runTests(); // //# 01: ok
|
|
await runTests([spawn]); // //# 02: ok
|
|
await runTests([spawn, spawn]); // //# 03: ok
|
|
await runTests([spawnUriInherit]); // //# 04: ok
|
|
await runTests([spawnUriInherit, spawn]); // //# 05: ok
|
|
await runTests([spawn, spawnUriInherit]); // //# 06: ok
|
|
|
|
// Test that spawning a new VM with file paths instead of URIs as arguments
|
|
// gives the same URIs in the internal values.
|
|
await runTests([asPath]); // //# 07: ok
|
|
|
|
// Test that spawnUri can reproduce the behavior of VM command line parameters
|
|
// exactly.
|
|
// (Don't run all configuration combinations in the same test, so
|
|
// unroll the configurations into multiple groups and run each group
|
|
// as its own multitest.
|
|
{
|
|
var groupCount = 8;
|
|
var groups = new List.generate(8, (_) => []);
|
|
for (int i = 0; i < configurations.length; i++) {
|
|
groups[i % groupCount].add(configurations[i]);
|
|
}
|
|
var group = -1;
|
|
group = 0; // //# 10: ok
|
|
group = 1; // //# 11: ok
|
|
group = 2; // //# 12: ok
|
|
group = 3; // //# 13: ok
|
|
group = 4; // //# 14: ok
|
|
group = 5; // //# 15: ok
|
|
group = 6; // //# 16: ok
|
|
group = 7; // //# 17: ok
|
|
if (group >= 0) {
|
|
for (var other in groups[group]) {
|
|
await runTests([spawnUriOther(other)]);
|
|
}
|
|
}
|
|
}
|
|
|
|
await tearDown();
|
|
|
|
if (failingTests.isNotEmpty) {
|
|
print("Errors found in tests:");
|
|
failingTests.forEach((test, actual) {
|
|
print("$test:\n ${actual.join("\n ")}");
|
|
});
|
|
exit(255);
|
|
}
|
|
|
|
asyncEnd();
|
|
}
|
|
|
|
/// Test running the test of the configuration through [Isolate.spawn].
|
|
///
|
|
/// This should not change the expected results compared to running it
|
|
/// directly.
|
|
Configuration spawn(Configuration conf) {
|
|
return conf.update(
|
|
description: conf.description + "/spawn",
|
|
main: "spawnMain",
|
|
newArgs: [conf.mainType],
|
|
expect: null);
|
|
}
|
|
|
|
/// Tests running a spawnUri on top of the configuration before testing.
|
|
///
|
|
/// The `spawnUri` call has no explicit root or config parameter, and
|
|
/// shouldn't search for one, so it implicitly inherits the current isolate's
|
|
/// actual root or configuration.
|
|
Configuration spawnUriInherit(Configuration conf) {
|
|
if (conf.expect["iroot"] == null &&
|
|
conf.expect["iconf"] == null &&
|
|
conf.expect["pconf"] != null) {
|
|
// This means that the specified configuration file didn't exist.
|
|
// spawning a new URI to "inherit" that will actually do an automatic
|
|
// package resolution search with results that are unpredictable.
|
|
// That behavior will be tested in a setting where we have more control over
|
|
// the files around the spawned URI.
|
|
return null;
|
|
}
|
|
return conf.update(
|
|
description: conf.description + "/spawnUri-inherit",
|
|
main: "spawnUriMain",
|
|
// encode null parameters as "-". Windows fails if using empty string.
|
|
newArgs: [
|
|
conf.mainFile,
|
|
"-",
|
|
"-",
|
|
"false"
|
|
],
|
|
expect: {
|
|
"proot": conf.expect["iroot"],
|
|
"pconf": conf.expect["iconf"],
|
|
});
|
|
}
|
|
|
|
/// Tests running a spawnUri with an explicit configuration different
|
|
/// from the original configuration.
|
|
///
|
|
/// Duplicates the explicit parameters as arguments to the spawned isolate.
|
|
ConfigurationTransformer spawnUriOther(Configuration other) {
|
|
return (Configuration conf) {
|
|
bool search = (other.config == null) && (other.root == null);
|
|
return conf.update(
|
|
description: "${conf.description} -spawnUri-> ${other.description}",
|
|
main: "spawnUriMain",
|
|
newArgs: [
|
|
other.mainFile,
|
|
other.config ?? "-",
|
|
other.root ?? "-",
|
|
"$search"
|
|
],
|
|
expect: other.expect);
|
|
};
|
|
}
|
|
|
|
/// Convert command line parameters to file paths.
|
|
///
|
|
/// This only works on the command line, not with `spawnUri`.
|
|
Configuration asPath(Configuration conf) {
|
|
bool change = false;
|
|
|
|
String toPath(String string) {
|
|
if (string == null) return null;
|
|
if (string.startsWith("file:")) {
|
|
change = true;
|
|
return new File.fromUri(Uri.parse(string)).path;
|
|
}
|
|
return string;
|
|
}
|
|
|
|
var mainFile = toPath(conf.mainFile);
|
|
var root = toPath(conf.root);
|
|
var config = toPath(conf.config);
|
|
if (!change) return null;
|
|
return conf.update(
|
|
description: conf.description + "/as path",
|
|
mainFile: mainFile,
|
|
root: root,
|
|
config: config);
|
|
}
|
|
|
|
/// --------------------------------------------------------------
|
|
|
|
Future setUp() async {
|
|
fileRoot = createTempDir();
|
|
// print("FILES: $fileRoot");
|
|
httpServer = await startServer(httpFiles);
|
|
// print("HTTPS: ${httpServer.address.address}:${httpServer.port}");
|
|
createConfigurations();
|
|
}
|
|
|
|
Future tearDown() async {
|
|
fileRoot.deleteSync(recursive: true);
|
|
await httpServer.close();
|
|
}
|
|
|
|
typedef Configuration ConfigurationTransformer(Configuration conf);
|
|
|
|
Future runTests([List<ConfigurationTransformer> transformations]) async {
|
|
outer:
|
|
for (var config in configurations) {
|
|
if (transformations != null) {
|
|
for (int i = transformations.length - 1; i >= 0; i--) {
|
|
config = transformations[i](config);
|
|
if (config == null) {
|
|
continue outer; // Can be used to skip some tests.
|
|
}
|
|
}
|
|
}
|
|
await testConfiguration(config);
|
|
}
|
|
}
|
|
|
|
// Creates a combination of configurations for running the Dart VM.
|
|
//
|
|
// The combinations covers most configurations of implicit and explicit
|
|
// package configurations over both file: and http: file sources.
|
|
// It also specifies the expected values of the following for a VM
|
|
// run in that configuration.
|
|
//
|
|
// * `Process.packageRoot`
|
|
// * `Process.packageConfig`
|
|
// * `Isolate.packageRoot`
|
|
// * `Isolate.packageRoot`
|
|
// * `Isolate.resolvePackageUri` of various inputs.
|
|
// * A variable defined in a library loaded using a `package:` URI.
|
|
//
|
|
// The configurations all have URIs as `root`, `config` and `mainFile` strings,
|
|
// have empty argument lists and `mainFile` points to the `main.dart` file.
|
|
void createConfigurations() {
|
|
add(String description, String mainDir,
|
|
{String root, String config, Map file, Map http, Map expect}) {
|
|
var id = freshName("conf");
|
|
|
|
file ??= {};
|
|
http ??= {};
|
|
|
|
// Fix-up paths.
|
|
String fileUri = fileRoot.uri.resolve("$id/").toString();
|
|
String httpUri =
|
|
"http://${httpServer.address.address}:${httpServer.port}/$id/";
|
|
|
|
String fixPath(String path) {
|
|
return path?.replaceAllMapped(fileHttpRegexp, (match) {
|
|
if (path.startsWith("%file/", match.start)) return fileUri;
|
|
return httpUri;
|
|
});
|
|
}
|
|
|
|
void fixPaths(Map dirs) {
|
|
for (var name in dirs.keys) {
|
|
var value = dirs[name];
|
|
if (value is Map) {
|
|
Map subDir = value;
|
|
fixPaths(subDir);
|
|
} else {
|
|
var newValue = fixPath(value);
|
|
if (newValue != value) dirs[name] = newValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!mainDir.endsWith("/")) mainDir += "/";
|
|
// Insert main files into the main-dir map.
|
|
Map mainDirMap;
|
|
{
|
|
if (mainDir.startsWith("%file/")) {
|
|
mainDirMap = file;
|
|
} else {
|
|
mainDirMap = http;
|
|
}
|
|
var parts = mainDir.split('/');
|
|
for (int i = 1; i < parts.length - 1; i++) {
|
|
var dirName = parts[i];
|
|
mainDirMap = mainDirMap[dirName] ?? (mainDirMap[dirName] = {});
|
|
}
|
|
}
|
|
|
|
mainDirMap["main"] = testMain;
|
|
mainDirMap["spawnMain"] = spawnMain.replaceAll("%mainDir/", mainDir);
|
|
mainDirMap["spawnUriMain"] = spawnUriMain;
|
|
|
|
mainDir = fixPath(mainDir);
|
|
root = fixPath(root);
|
|
config = fixPath(config);
|
|
fixPaths(file);
|
|
fixPaths(http);
|
|
// These expectations are default. If not overridden the value will be
|
|
// expected to be null. That is, you can't avoid testing the actual
|
|
// value of these, you can only change what value to expect.
|
|
// For values not included here (commented out), the result is not tested
|
|
// unless a value (maybe null) is provided.
|
|
fixPaths(expect);
|
|
|
|
expect = {
|
|
"pconf": null,
|
|
"proot": null,
|
|
"iconf": null,
|
|
"iroot": null,
|
|
"foo": null,
|
|
"foo/": null,
|
|
"foo/bar": null,
|
|
"foo.x": "qux",
|
|
"bar/bar": null,
|
|
"relative": "relative/path",
|
|
"nonpkg": "http://example.org/file"
|
|
}..addAll(expect ?? const {});
|
|
|
|
// Add http files to the http server.
|
|
if (http.isNotEmpty) {
|
|
httpFiles[id] = http;
|
|
}
|
|
// Add file files to the file system.
|
|
if (file.isNotEmpty) {
|
|
createFiles(fileRoot, id, file);
|
|
}
|
|
|
|
configurations.add(new Configuration(
|
|
description: description,
|
|
root: root,
|
|
config: config,
|
|
mainFile: mainDir + "main.dart",
|
|
args: const [],
|
|
expect: expect));
|
|
}
|
|
|
|
// The `test` function can generate file or http resources.
|
|
// It replaces "%file/" with URI of the root directory of generated files and
|
|
// "%http/" with the URI of the HTTP server's root in appropriate contexts
|
|
// (all file contents and parameters).
|
|
|
|
// Tests that only use one scheme to access files.
|
|
for (var scheme in ["file", "http"]) {
|
|
/// Run a test in the current scheme.
|
|
///
|
|
/// The files are served either through HTTP or in a local directory.
|
|
/// Use "%$scheme/" to refer to the root of the served files.
|
|
addScheme(description, main, {expect, files, args, root, config}) {
|
|
add("$scheme/$description", main,
|
|
expect: expect,
|
|
root: root,
|
|
config: config,
|
|
file: (scheme == "file") ? files : null,
|
|
http: (scheme == "http") ? files : null);
|
|
}
|
|
|
|
{
|
|
// No parameters, no .packages files or packages/ dir.
|
|
// A "file:" source realizes there is no configuration and can't resolve
|
|
// any packages, but a "http:" source assumes a "packages/" directory.
|
|
addScheme("no resolution", "%$scheme/",
|
|
files: {},
|
|
expect: (scheme == "file")
|
|
? {"foo.x": null}
|
|
: {
|
|
"iroot": "%http/packages/",
|
|
"foo": "%http/packages/foo",
|
|
"foo/": "%http/packages/foo/",
|
|
"foo/bar": "%http/packages/foo/bar",
|
|
"foo.x": null,
|
|
"bar/bar": "%http/packages/bar/bar",
|
|
});
|
|
}
|
|
|
|
{
|
|
// No parameters, no .packages files,
|
|
// packages/ dir exists and is detected.
|
|
var files = {"packages": fooPackage};
|
|
addScheme("implicit packages dir", "%$scheme/", files: files, expect: {
|
|
"iroot": "%$scheme/packages/",
|
|
"foo": "%$scheme/packages/foo",
|
|
"foo/": "%$scheme/packages/foo/",
|
|
"foo/bar": "%$scheme/packages/foo/bar",
|
|
"bar/bar": "%$scheme/packages/bar/bar",
|
|
});
|
|
}
|
|
|
|
{
|
|
// No parameters, no .packages files in current dir, but one in parent,
|
|
// packages/ dir exists and is used.
|
|
//
|
|
// Should not detect the .packages file in parent directory.
|
|
// That file is empty, so if it is used, the system cannot resolve "foo".
|
|
var files = {
|
|
"sub": {"packages": fooPackage},
|
|
".packages": ""
|
|
};
|
|
addScheme(
|
|
"implicit packages dir overrides parent .packages", "%$scheme/sub/",
|
|
files: files,
|
|
expect: {
|
|
"iroot": "%$scheme/sub/packages/",
|
|
"foo": "%$scheme/sub/packages/foo",
|
|
"foo/": "%$scheme/sub/packages/foo/",
|
|
"foo/bar": "%$scheme/sub/packages/foo/bar",
|
|
// "foo.x": "qux", // Blocked by issue http://dartbug.com/26482
|
|
"bar/bar": "%$scheme/sub/packages/bar/bar",
|
|
});
|
|
}
|
|
|
|
{
|
|
// No parameters, a .packages file next to entry is found and used.
|
|
// A packages/ directory is ignored.
|
|
var files = {
|
|
".packages": "foo:pkgs/foo/",
|
|
"packages": {},
|
|
"pkgs": fooPackage
|
|
};
|
|
addScheme("implicit .packages file", "%$scheme/", files: files, expect: {
|
|
"iconf": "%$scheme/.packages",
|
|
"foo/": "%$scheme/pkgs/foo/",
|
|
"foo/bar": "%$scheme/pkgs/foo/bar",
|
|
});
|
|
}
|
|
|
|
{
|
|
// No parameters, a .packages file in parent dir, no packages/ dir.
|
|
// With a file: URI, find the .packages file.
|
|
// WIth a http: URI, assume a packages/ dir.
|
|
var files = {"sub": {}, ".packages": "foo:pkgs/foo/", "pkgs": fooPackage};
|
|
addScheme(".packages file in parent", "%$scheme/sub/",
|
|
files: files,
|
|
expect: (scheme == "file")
|
|
? {
|
|
"iconf": "%file/.packages",
|
|
"foo/": "%file/pkgs/foo/",
|
|
"foo/bar": "%file/pkgs/foo/bar",
|
|
}
|
|
: {
|
|
"iroot": "%http/sub/packages/",
|
|
"foo": "%http/sub/packages/foo",
|
|
"foo/": "%http/sub/packages/foo/",
|
|
"foo/bar": "%http/sub/packages/foo/bar",
|
|
"foo.x": null,
|
|
"bar/bar": "%http/sub/packages/bar/bar",
|
|
});
|
|
}
|
|
|
|
{
|
|
// Specified package root that doesn't exist.
|
|
// Ignores existing .packages file and packages/ dir.
|
|
addScheme("explicit root not there", "%$scheme/",
|
|
files: {
|
|
"packages": fooPackage,
|
|
".packages": "foo:%$scheme/packages/"
|
|
},
|
|
root: "%$scheme/notthere/",
|
|
expect: {
|
|
"proot": "%$scheme/notthere/",
|
|
"iroot": "%$scheme/notthere/",
|
|
"foo": "%$scheme/notthere/foo",
|
|
"foo/": "%$scheme/notthere/foo/",
|
|
"foo/bar": "%$scheme/notthere/foo/bar",
|
|
"foo.x": null,
|
|
"bar/bar": "%$scheme/notthere/bar/bar",
|
|
});
|
|
}
|
|
|
|
{
|
|
// Specified package config that doesn't exist.
|
|
// Ignores existing .packages file and packages/ dir.
|
|
addScheme("explicit config not there", "%$scheme/",
|
|
files: {".packages": "foo:packages/foo/", "packages": fooPackage},
|
|
config: "%$scheme/.notthere",
|
|
expect: {
|
|
"pconf": "%$scheme/.notthere",
|
|
"iconf": null, // <- Only there if actually loaded (unspecified).
|
|
"foo/": null,
|
|
"foo/bar": null,
|
|
"foo.x": null,
|
|
});
|
|
}
|
|
|
|
{
|
|
// Specified package root with no trailing slash.
|
|
// The Platform.packageRoot and Isolate.packageRoot has a trailing slash.
|
|
var files = {
|
|
".packages": "foo:packages/foo/",
|
|
"packages": {},
|
|
"pkgs": fooPackage
|
|
};
|
|
addScheme("explicit package root, no slash", "%$scheme/",
|
|
files: files,
|
|
root: "%$scheme/pkgs",
|
|
expect: {
|
|
"proot": "%$scheme/pkgs/",
|
|
"iroot": "%$scheme/pkgs/",
|
|
"foo": "%$scheme/pkgs/foo",
|
|
"foo/": "%$scheme/pkgs/foo/",
|
|
"foo/bar": "%$scheme/pkgs/foo/bar",
|
|
"bar/bar": "%$scheme/pkgs/bar/bar",
|
|
});
|
|
}
|
|
|
|
{
|
|
// Specified package root with trailing slash.
|
|
var files = {
|
|
".packages": "foo:packages/foo/",
|
|
"packages": {},
|
|
"pkgs": fooPackage
|
|
};
|
|
addScheme("explicit package root, slash", "%$scheme/",
|
|
files: files,
|
|
root: "%$scheme/pkgs",
|
|
expect: {
|
|
"proot": "%$scheme/pkgs/",
|
|
"iroot": "%$scheme/pkgs/",
|
|
"foo": "%$scheme/pkgs/foo",
|
|
"foo/": "%$scheme/pkgs/foo/",
|
|
"foo/bar": "%$scheme/pkgs/foo/bar",
|
|
"bar/bar": "%$scheme/pkgs/bar/bar",
|
|
});
|
|
}
|
|
|
|
{
|
|
// Specified package config.
|
|
var files = {
|
|
".packages": "foo:packages/foo/",
|
|
"packages": {},
|
|
".pkgs": "foo:pkgs/foo/",
|
|
"pkgs": fooPackage
|
|
};
|
|
addScheme("explicit package config file", "%$scheme/",
|
|
files: files,
|
|
config: "%$scheme/.pkgs",
|
|
expect: {
|
|
"pconf": "%$scheme/.pkgs",
|
|
"iconf": "%$scheme/.pkgs",
|
|
"foo/": "%$scheme/pkgs/foo/",
|
|
"foo/bar": "%$scheme/pkgs/foo/bar",
|
|
});
|
|
}
|
|
|
|
{
|
|
// Specified package config as data: URI.
|
|
// The package config can be specified as a data: URI.
|
|
// (In that case, relative URI references in the config file won't work).
|
|
var files = {
|
|
".packages": "foo:packages/foo/",
|
|
"packages": {},
|
|
"pkgs": fooPackage
|
|
};
|
|
var dataUri = "data:,foo:%$scheme/pkgs/foo/\n";
|
|
addScheme("explicit data: config file", "%$scheme/",
|
|
files: files,
|
|
config: dataUri,
|
|
expect: {
|
|
"pconf": dataUri,
|
|
"iconf": dataUri,
|
|
"foo/": "%$scheme/pkgs/foo/",
|
|
"foo/bar": "%$scheme/pkgs/foo/bar",
|
|
});
|
|
}
|
|
}
|
|
|
|
// Tests where there are files on both http: and file: sources.
|
|
|
|
for (var entryScheme in const ["file", "http"]) {
|
|
for (var pkgScheme in const ["file", "http"]) {
|
|
// Package root.
|
|
if (entryScheme != pkgScheme) {
|
|
// Package dir and entry point on different schemes.
|
|
var files = {};
|
|
var https = {};
|
|
(entryScheme == "file" ? files : https)["main"] = testMain;
|
|
(pkgScheme == "file" ? files : https)["pkgs"] = fooPackage;
|
|
add("$pkgScheme pkg/$entryScheme main", "%$entryScheme/",
|
|
file: files,
|
|
http: https,
|
|
root: "%$pkgScheme/pkgs/",
|
|
expect: {
|
|
"proot": "%$pkgScheme/pkgs/",
|
|
"iroot": "%$pkgScheme/pkgs/",
|
|
"foo": "%$pkgScheme/pkgs/foo",
|
|
"foo/": "%$pkgScheme/pkgs/foo/",
|
|
"foo/bar": "%$pkgScheme/pkgs/foo/bar",
|
|
"bar/bar": "%$pkgScheme/pkgs/bar/bar",
|
|
"foo.x": "qux",
|
|
});
|
|
}
|
|
// Package config. The configuration file may also be on either source.
|
|
for (var configScheme in const ["file", "http"]) {
|
|
// Don't do the boring stuff!
|
|
if (entryScheme == configScheme && entryScheme == pkgScheme) continue;
|
|
// Package config, packages and entry point not all on same scheme.
|
|
var files = {};
|
|
var https = {};
|
|
(entryScheme == "file" ? files : https)["main"] = testMain;
|
|
(configScheme == "file" ? files : https)[".pkgs"] =
|
|
"foo:%$pkgScheme/pkgs/foo/\n";
|
|
(pkgScheme == "file" ? files : https)["pkgs"] = fooPackage;
|
|
add("$pkgScheme pkg/$configScheme config/$entryScheme main",
|
|
"%$entryScheme/",
|
|
file: files,
|
|
http: https,
|
|
config: "%$configScheme/.pkgs",
|
|
expect: {
|
|
"pconf": "%$configScheme/.pkgs",
|
|
"iconf": "%$configScheme/.pkgs",
|
|
"foo/": "%$pkgScheme/pkgs/foo/",
|
|
"foo/bar": "%$pkgScheme/pkgs/foo/bar",
|
|
"foo.x": "qux",
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// Helper functionality.
|
|
|
|
var fileHttpRegexp = new RegExp(r"%(?:file|http)/");
|
|
|
|
// Executes a test in a configuration.
|
|
//
|
|
// The test must specify which main file to use
|
|
// (`main`, `spawnMain` or `spawnUriMain`)
|
|
// and any arguments which will be used by `spawnMain` and `spawnUriMain`.
|
|
//
|
|
// The [expect] map may be used to override the expectations of the
|
|
// configuration on a value-by-value basis. Passing, e.g., `{"pconf": null}`
|
|
// will override only the `pconf` (`Platform.packageConfig`) expectation.
|
|
Future testConfiguration(Configuration conf) async {
|
|
print("-- ${conf.description}");
|
|
var description = conf.description;
|
|
try {
|
|
var output = await execDart(conf.mainFile,
|
|
root: conf.root, config: conf.config, scriptArgs: conf.args);
|
|
match(json.decode(output), conf.expect, description, output);
|
|
} catch (e, s) {
|
|
// Unexpected error calling execDart or parsing the result.
|
|
// Report it and continue.
|
|
print("ERROR running $description: $e\n$s");
|
|
failingTests.putIfAbsent(description, () => []).add("$e");
|
|
}
|
|
}
|
|
|
|
/// Test that the output of running testMain matches the expectations.
|
|
///
|
|
/// The output is a string which is parse as a JSON literal.
|
|
/// The resulting map is always mapping strings to strings, or possibly `null`.
|
|
/// The expectations can have non-string values other than null,
|
|
/// they are `toString`'ed before being compared (so the caller can use a URI
|
|
/// or a File/Directory directly as an expectation).
|
|
void match(Map actuals, Map expectations, String desc, String actualJson) {
|
|
for (var key in expectations.keys) {
|
|
var expectation = expectations[key]?.toString();
|
|
var actual = actuals[key];
|
|
if (expectation != actual) {
|
|
print("ERROR: $desc: $key: Expected: <$expectation> Found: <$actual>");
|
|
failingTests
|
|
.putIfAbsent(desc, () => [])
|
|
.add("$key: $expectation != $actual");
|
|
}
|
|
}
|
|
}
|
|
|
|
const String improt = "import"; // Avoid multitest import rewriting.
|
|
|
|
/// Script that prints the current state and the result of resolving
|
|
/// a few package URIs. This script will be invoked in different settings,
|
|
/// and the result will be parsed and compared to the expectations.
|
|
const String testMain = """
|
|
$improt "dart:convert" show json;
|
|
$improt "dart:io" show Platform, Directory;
|
|
$improt "dart:isolate" show Isolate;
|
|
$improt "package:foo/foo.dart" deferred as foo;
|
|
main(_) async {
|
|
String platformRoot = await Platform.packageRoot;
|
|
String platformConfig = await Platform.packageConfig;
|
|
Directory cwd = Directory.current;
|
|
Uri script = Platform.script;
|
|
Uri isolateRoot = await Isolate.packageRoot;
|
|
Uri isolateConfig = await Isolate.packageConfig;
|
|
Uri base = Uri.base;
|
|
Uri res1 = await Isolate.resolvePackageUri(Uri.parse("package:foo"));
|
|
Uri res2 = await Isolate.resolvePackageUri(Uri.parse("package:foo/"));
|
|
Uri res3 = await Isolate.resolvePackageUri(Uri.parse("package:foo/bar"));
|
|
Uri res4 = await Isolate.resolvePackageUri(Uri.parse("package:bar/bar"));
|
|
Uri res5 = await Isolate.resolvePackageUri(Uri.parse("relative/path"));
|
|
Uri res6 = await Isolate.resolvePackageUri(
|
|
Uri.parse("http://example.org/file"));
|
|
String fooX = await foo
|
|
.loadLibrary()
|
|
.timeout(const Duration(seconds: 1))
|
|
.then((_) => foo.x, onError: (_) => null);
|
|
print(json.encode({
|
|
"cwd": cwd.path,
|
|
"base": base?.toString(),
|
|
"script": script?.toString(),
|
|
"proot": platformRoot,
|
|
"pconf": platformConfig,
|
|
"iroot" : isolateRoot?.toString(),
|
|
"iconf" : isolateConfig?.toString(),
|
|
"foo": res1?.toString(),
|
|
"foo/": res2?.toString(),
|
|
"foo/bar": res3?.toString(),
|
|
"foo.x": fooX?.toString(),
|
|
"bar/bar": res4?.toString(),
|
|
"relative": res5?.toString(),
|
|
"nonpkg": res6?.toString(),
|
|
}));
|
|
}
|
|
""";
|
|
|
|
/// Script that spawns a new Isolate using Isolate.spawnUri.
|
|
///
|
|
/// Takes URI of target isolate, package config, package root and
|
|
/// automatic package resolution-flag parameters as command line arguments.
|
|
/// Any further arguments are forwarded to the spawned isolate.
|
|
const String spawnUriMain = """
|
|
$improt "dart:isolate";
|
|
$improt "dart:async";
|
|
main(args) async {
|
|
Uri target = Uri.parse(args[0]);
|
|
Uri config = (args[1] == "-") ? null : Uri.parse(args[1]);
|
|
Uri root = (args[2] == "-") ? null : Uri.parse(args[2]);
|
|
bool search = args[3] == "true";
|
|
var restArgs = args.skip(4).toList();
|
|
// Port keeps isolate alive until spawned isolate terminates.
|
|
var port = new RawReceivePort();
|
|
port.handler = (res) async {
|
|
port.close(); // Close on exit or first error.
|
|
if (res != null) {
|
|
await new Future.error(res[0], new StackTrace.fromString(res[1]));
|
|
}
|
|
};
|
|
Isolate.spawnUri(target, restArgs, null,
|
|
packageRoot: root, packageConfig: config,
|
|
automaticPackageResolution: search,
|
|
onError: port.sendPort, onExit: port.sendPort);
|
|
}
|
|
""";
|
|
|
|
/// Script that spawns a new Isolate using Isolate.spawn.
|
|
///
|
|
/// Uses the first argument to select which target to spawn.
|
|
/// Should be either "test", "uri" or "spawn".
|
|
const String spawnMain = """
|
|
$improt "dart:async";
|
|
$improt "dart:isolate";
|
|
$improt "%mainDir/main.dart" as test;
|
|
$improt "%mainDir/spawnUriMain.dart" as spawnUri;
|
|
main(List<String> args) async {
|
|
// Port keeps isolate alive until spawned isolate terminates.
|
|
var port = new RawReceivePort();
|
|
port.handler = (res) async {
|
|
port.close(); // Close on exit or first error.
|
|
if (res != null) {
|
|
await new Future.error(res[0], new StackTrace.fromString(res[1]));
|
|
}
|
|
};
|
|
var arg = args.first;
|
|
var rest = args.skip(1).toList();
|
|
var target;
|
|
if (arg == "main") {
|
|
target = test.main;
|
|
} else if (arg == "spawnUriMain") {
|
|
target = spawnUri.main;
|
|
} else {
|
|
target = main;
|
|
}
|
|
Isolate.spawn(target, rest, onError: port.sendPort, onExit: port.sendPort);
|
|
}
|
|
""";
|
|
|
|
/// A package directory containing only one package, "foo", with one file.
|
|
const Map fooPackage = const {
|
|
"foo": const {"foo": "var x = 'qux';"}
|
|
};
|
|
|
|
/// Runs the Dart executable with the provided parameters.
|
|
///
|
|
/// Captures and returns the output.
|
|
Future<String> execDart(String script,
|
|
{String root, String config, Iterable<String> scriptArgs}) async {
|
|
var checked = false;
|
|
assert((checked = true));
|
|
// TODO: Find a way to change CWD before running script.
|
|
var executable = Platform.executable;
|
|
var args = [];
|
|
if (checked) args.add("--checked");
|
|
if (root != null) args.add("--package-root=$root");
|
|
if (config != null) args.add("--packages=$config");
|
|
args.add(script);
|
|
if (scriptArgs != null) {
|
|
args.addAll(scriptArgs);
|
|
}
|
|
return Process.run(executable, args).then((results) {
|
|
if (results.exitCode != 0 || results.stderr.isNotEmpty) {
|
|
throw results.stderr;
|
|
}
|
|
return results.stdout;
|
|
});
|
|
}
|
|
|
|
/// Creates a number of files and subdirectories.
|
|
///
|
|
/// The [content] is the content of the directory itself. The map keys are
|
|
/// names and the values are either strings that represent Dart file contents
|
|
/// or maps that represent subdirectories.
|
|
void createFiles(Directory tempDir, String subDir, Map content) {
|
|
Directory createDir(Directory base, String name) {
|
|
Directory newDir = new Directory(p.join(base.path, name));
|
|
newDir.createSync();
|
|
return newDir;
|
|
}
|
|
|
|
void createTextFile(Directory base, String name, String content) {
|
|
File newFile = new File(p.join(base.path, name));
|
|
newFile.writeAsStringSync(content);
|
|
}
|
|
|
|
void createRecursive(Directory dir, Map map) {
|
|
for (var name in map.keys) {
|
|
var content = map[name];
|
|
if (content is String) {
|
|
// If the name starts with "." it's a .packages file, otherwise it's
|
|
// a dart file. Those are the only files we care about in this test.
|
|
createTextFile(
|
|
dir, name.startsWith(".") ? name : name + ".dart", content);
|
|
} else {
|
|
assert(content is Map);
|
|
var subdir = createDir(dir, name);
|
|
createRecursive(subdir, content);
|
|
}
|
|
}
|
|
}
|
|
|
|
createRecursive(createDir(tempDir, subDir), content);
|
|
}
|
|
|
|
/// Start an HTTP server which serves a directory/file structure.
|
|
///
|
|
/// The directories and files are described by [files].
|
|
///
|
|
/// Each map key is an entry in a directory. A `Map` value is a sub-directory
|
|
/// and a `String` value is a text file.
|
|
/// The file contents are run through [fixPaths] to allow them to be self-
|
|
/// referential.
|
|
Future<HttpServer> startServer(Map files) async {
|
|
return (await HttpServer.bind(InternetAddress.loopbackIPv4, 0))
|
|
..forEach((request) {
|
|
var result = files;
|
|
onFailure:
|
|
{
|
|
for (var part in request.uri.pathSegments) {
|
|
if (part.endsWith(".dart")) {
|
|
part = part.substring(0, part.length - 5);
|
|
}
|
|
if (result is Map) {
|
|
result = result[part];
|
|
} else {
|
|
break onFailure;
|
|
}
|
|
}
|
|
if (result is String) {
|
|
request.response
|
|
..write(result)
|
|
..close();
|
|
return;
|
|
}
|
|
}
|
|
request.response
|
|
..statusCode = HttpStatus.notFound
|
|
..close();
|
|
});
|
|
}
|
|
|
|
// Counter used to avoid reusing temporary file or directory names.
|
|
//
|
|
// Used when adding extra files to an existing directory structure,
|
|
// and when creating temporary directories.
|
|
//
|
|
// Some platform temporary-directory implementations are timer based,
|
|
// and creating two temp-dirs withing a short duration may cause a collision.
|
|
int tmpNameCounter = 0;
|
|
|
|
// Fresh file name.
|
|
String freshName([String base = "tmp"]) => "$base${tmpNameCounter++}";
|
|
|
|
Directory createTempDir() {
|
|
return Directory.systemTemp.createTempSync(freshName("pftest-"));
|
|
}
|
|
|
|
typedef void ConfigUpdate(Configuration configuration);
|
|
|
|
/// The configuration for a single test.
|
|
class Configuration {
|
|
/// The "description" of the test - a description of the set-up.
|
|
final String description;
|
|
|
|
/// The package root parameter passed to the Dart isolate.
|
|
///
|
|
/// At most one of [root] and [config] should be supplied. If both are
|
|
/// omitted, a VM will search for a packages file or dir.
|
|
final String root;
|
|
|
|
/// The package configuration file location passed to the Dart isolate.
|
|
final String config;
|
|
|
|
/// Path to the main file to run.
|
|
final String mainFile;
|
|
|
|
/// List of arguments to pass to the main function.
|
|
final List<String> args;
|
|
|
|
/// The expected values for `Platform.package{Root,Config}`,
|
|
/// `Isolate.package{Root,Config}` and resolution of package URIs
|
|
/// in a `foo` package.
|
|
///
|
|
/// The results are found by running the `main.dart` file inside [mainDir].
|
|
/// The tests can run this file after doing other `spawn` or `spawnUri` calls.
|
|
final Map expect;
|
|
|
|
Configuration(
|
|
{this.description,
|
|
this.root,
|
|
this.config,
|
|
this.mainFile,
|
|
this.args,
|
|
this.expect});
|
|
|
|
// Gets the type of main file, one of `main`, `spawnMain` or `spawnUriMain`.
|
|
String get mainType {
|
|
var lastSlash = mainFile.lastIndexOf("/");
|
|
if (lastSlash < 0) {
|
|
// Assume it's a Windows path.
|
|
lastSlash = mainFile.lastIndexOf(r"\");
|
|
}
|
|
var name = mainFile.substring(lastSlash + 1, mainFile.length - 5);
|
|
assert(name == "main" || name == "spawnMain" || name == "spawnUriMain");
|
|
return name;
|
|
}
|
|
|
|
String get mainPath {
|
|
var lastSlash = mainFile.lastIndexOf("/");
|
|
if (lastSlash < 0) {
|
|
// Assume it's a Windows path.
|
|
lastSlash = mainFile.lastIndexOf(r"\");
|
|
}
|
|
return mainFile.substring(0, lastSlash + 1);
|
|
}
|
|
|
|
/// Create a new configuration from the old one.
|
|
///
|
|
/// [description] is new description.
|
|
///
|
|
/// [main] is one of `main`, `spawnMain` or `spawnUriMain`, and changes
|
|
/// the [Configuration.mainFile] to a different file in the same directory.
|
|
///
|
|
/// [mainFile] overrides [Configuration.mainFile] completely, and ignores
|
|
/// [main].
|
|
///
|
|
/// [newArgs] are prepended to the existing [Configuration.args].
|
|
///
|
|
/// [args] overrides [Configuration.args] completely and ignores [newArgs].
|
|
///
|
|
/// [expect] overrides individual expectations.
|
|
///
|
|
/// [root] and [config] overrides the existing values.
|
|
Configuration update(
|
|
{String description,
|
|
String main,
|
|
String mainFile,
|
|
String root,
|
|
String config,
|
|
List<String> args,
|
|
List<String> newArgs,
|
|
Map expect}) {
|
|
return new Configuration(
|
|
description: description ?? this.description,
|
|
root: root ?? this.root,
|
|
config: config ?? this.config,
|
|
mainFile: mainFile ??
|
|
((main == null) ? this.mainFile : "${this.mainPath}$main.dart"),
|
|
args: args ??
|
|
(<String>[]
|
|
..addAll(newArgs ?? const <String>[])
|
|
..addAll(this.args)),
|
|
expect: expect == null ? this.expect : new Map.from(this.expect)
|
|
..addAll(expect ?? const {}));
|
|
}
|
|
|
|
// For debugging.
|
|
String toString() {
|
|
return "Configuration($description\n"
|
|
" root : $root\n"
|
|
" config: $config\n"
|
|
" main : $mainFile\n"
|
|
" args : ${args.map((x) => '"$x"').join(" ")}\n"
|
|
") : expect {\n${expect.keys.map((k) =>
|
|
' "$k"'.padRight(6) + ":${json.encode(expect[k])}\n").join()}"
|
|
"}";
|
|
}
|
|
}
|
|
|
|
// Inserts the file with generalized [name] at [path] with [content].
|
|
//
|
|
// The [path] is a directory where the file is created. It must start with
|
|
// either '%file/' or '%http/' to select the structure to put it into.
|
|
//
|
|
// The [name] should not have a trailing ".dart" for Dart files. Any file
|
|
// not starting with "." is assumed to be a ".dart" file.
|
|
void insertFileAt(
|
|
Map file, Map http, String path, String name, String content) {
|
|
var parts = path.split('/').toList();
|
|
var dir = (parts[0] == "%file") ? file : http;
|
|
for (var i = 1; i < parts.length - 1; i++) {
|
|
var entry = parts[i];
|
|
dir = dir[entry] ?? (dir[entry] = {});
|
|
}
|
|
dir[name] = content;
|
|
}
|