blob: 26f93461a9918df3c8f9cdbc8d2f451e0738d62d [file] [log] [blame]
// 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.
// @dart = 2.9
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;
}