// 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. // VMOptions=--enable-isolate-groups --experimental-enable-isolate-groups-jit // VMOptions=--no-enable-isolate-groups 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 httpFiles = {}; /// List of configurations. List configurations = []; /// Collection of failing tests and their failure messages. /// /// Each test may fail in more than one way. var failingTests = >{}; 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 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 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 execDart(String script, {String root, String config, Iterable 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 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 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 args, List 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 ?? ([] ..addAll(newArgs ?? const []) ..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; }