| Index: tests/standalone/packages_file_test.dart
|
| diff --git a/tests/standalone/packages_file_test.dart b/tests/standalone/packages_file_test.dart
|
| index 4d695eae5283ac4768547a75595358dcbbd799a0..719d3d7fb3b37ccfa7b1dad5aab232706b9cf02b 100644
|
| --- a/tests/standalone/packages_file_test.dart
|
| +++ b/tests/standalone/packages_file_test.dart
|
| @@ -8,280 +8,604 @@ 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) {
|
| + // TODO(26555): Clean up when fixed.
|
| + // TEMPORARY FIX FOR ISSUE #26555 (http://dartbug.com/26555)
|
| + if (conf.expect["iroot"] == null &&
|
| + conf.expect["iconf"] == null &&
|
| + conf.expect["pconf"] != null) {
|
| + // The spawned isolate will do a search for a package file or root,
|
| + // which is not what the original did. Skip test for now.
|
| + return null;
|
| + }
|
| + // REMOVE WHEN ISSUE FIXED!
|
| + return conf.update(
|
| + description: conf.description + "/spawn",
|
| + main: "spawnMain",
|
| + newArgs: [conf.mainType],
|
| + // TEMPORARY FIX FOR ISSUE #26555 (http://dartbug.com/26555)
|
| + expect: {
|
| + "proot": conf.expect["iroot"],
|
| + "pconf": conf.expect["iconf"],
|
| + }
|
| + // REMOVE WHEN ISSUE FIXED!
|
| + );
|
| +}
|
| +
|
| +/// 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.resolvePacakgeUri` 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 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",
|
| + }..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).
|
|
|
| - // With no specified resolutiuon and no implicit .packages or packages/
|
| - // available, nothing can be resolved and the package can't be imported.
|
| - await test("file: no resolution",
|
| - "%file/main.dart",
|
| - file: {"main": testMain},
|
| - expect: {"foo.x": null});
|
| -
|
| - // An HTTP script with no ".packages" file assumes a "packages" dir.
|
| - // All packages are resolved relative to that dir, whether it exists or not.
|
| - await test("http: no resolution", "%http/main.dart",
|
| - http: {"main": testMain},
|
| - expect: {
|
| - "iroot": "%http/packages/",
|
| - // "foo": null,
|
| - "foo/": "%http/packages/foo/",
|
| - "foo/bar": "%http/packages/foo/bar",
|
| - "foo.x": null,
|
| - });
|
| -
|
| - // A number of tests which behave similarly whether run as local files or
|
| - // over HTTP.
|
| + // 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.
|
| - testScheme(name, main, {expect, files, args, root, config}) {
|
| - return test("$scheme: $name", main, expect: expect,
|
| - root: root, config: config, args: args,
|
| - file: scheme == "file" ? files : null,
|
| - http: scheme == "http" ? files : null);
|
| + 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);
|
| }
|
|
|
| {
|
| - var files = {"main": testMain, "packages": fooPackage};
|
| - // Expect implicitly detected package dir.
|
| - await testScheme("implicit packages dir","%$scheme/main.dart",
|
| + // 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/bar": "%http/packages/foo/bar",
|
| + "foo.x": null,
|
| + });
|
| + }
|
| +
|
| + {
|
| + // 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": null,
|
| "foo/": "%$scheme/packages/foo/",
|
| "foo/bar": "%$scheme/packages/foo/bar",
|
| });
|
| }
|
|
|
| {
|
| - var files = {"sub": {"main": testMain, "packages": fooPackage},
|
| - ".packages": ""};
|
| - // Expect implicitly detected package dir.
|
| + // 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".
|
| - await testScheme("implicit packages dir 2", "%$scheme/sub/main.dart",
|
| + var files = {"sub": {"packages": fooPackage},
|
| + ".packages": ""};
|
| + addScheme("implicit packages dir overrides parent .packages",
|
| + "%$scheme/sub/",
|
| files: files,
|
| expect: {
|
| "iroot": "%$scheme/sub/packages/",
|
| - // "foo": null,
|
| "foo/": "%$scheme/sub/packages/foo/",
|
| "foo/bar": "%$scheme/sub/packages/foo/bar",
|
| + // "foo.x": "qux", // Blocked by issue http://dartbug.com/26482
|
| });
|
| }
|
|
|
| {
|
| - var files = {"main": testMain,
|
| - ".packages": "foo:pkgs/foo/",
|
| + // 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};
|
| - await testScheme("implicit .packages file", "%$scheme/main.dart",
|
| + addScheme("implicit .packages file", "%$scheme/",
|
| files: files,
|
| expect: {
|
| "iconf": "%$scheme/.packages",
|
| - // "foo": null,
|
| "foo/": "%$scheme/pkgs/foo/",
|
| "foo/bar": "%$scheme/pkgs/foo/bar",
|
| });
|
| }
|
|
|
| {
|
| - var files = {"main": testMain,
|
| - ".packages": "foo:packages/foo/",
|
| - "packages": fooPackage,
|
| + // 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};
|
| - await testScheme("explicit package root, no slash", "%$scheme/main.dart",
|
| + 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/bar": "%http/sub/packages/foo/bar",
|
| + "foo.x": null,
|
| + });
|
| + }
|
| +
|
| + {
|
| + // 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/bar": "%$scheme/notthere/foo/bar",
|
| + "foo.x": null,
|
| + });
|
| + }
|
| +
|
| + {
|
| + // 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": null,
|
| "foo/": "%$scheme/pkgs/foo/",
|
| "foo/bar": "%$scheme/pkgs/foo/bar",
|
| });
|
| }
|
|
|
| {
|
| - var files = {"main": testMain,
|
| - ".packages": "foo:packages/foo/",
|
| - "packages": fooPackage,
|
| + // Specified package root with trailing slash.
|
| + var files = {".packages": "foo:packages/foo/",
|
| + "packages": {},
|
| "pkgs": fooPackage};
|
| - await testScheme("explicit package root, slash", "%$scheme/main.dart",
|
| + addScheme("explicit package root, slash", "%$scheme/",
|
| files: files,
|
| root: "%$scheme/pkgs",
|
| expect: {
|
| "proot": "%$scheme/pkgs/",
|
| "iroot": "%$scheme/pkgs/",
|
| - // "foo": null,
|
| "foo/": "%$scheme/pkgs/foo/",
|
| "foo/bar": "%$scheme/pkgs/foo/bar",
|
| });
|
| }
|
|
|
| {
|
| - var files = {"main": testMain,
|
| - ".packages": "foo:packages/foo/",
|
| - "packages": fooPackage,
|
| + // Specified package config.
|
| + var files = {".packages": "foo:packages/foo/",
|
| + "packages": {},
|
| ".pkgs": "foo:pkgs/foo/",
|
| "pkgs": fooPackage};
|
| - await testScheme("explicit package config file", "%$scheme/main.dart",
|
| + addScheme("explicit package config file", "%$scheme/",
|
| files: files,
|
| config: "%$scheme/.pkgs",
|
| expect: {
|
| "pconf": "%$scheme/.pkgs",
|
| "iconf": "%$scheme/.pkgs",
|
| - // "foo": null,
|
| "foo/": "%$scheme/pkgs/foo/",
|
| "foo/bar": "%$scheme/pkgs/foo/bar",
|
| });
|
| }
|
|
|
| {
|
| - /// 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 = {"main": testMain,
|
| - ".packages": "foo:packages/foo/",
|
| - "packages": fooPackage,
|
| + // 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";
|
| - await testScheme("explicit data: config file", "%$scheme/main.dart",
|
| + addScheme("explicit data: config file", "%$scheme/",
|
| files: files,
|
| config: dataUri,
|
| expect: {
|
| "pconf": dataUri,
|
| "iconf": dataUri,
|
| - // "foo": null,
|
| "foo/": "%$scheme/pkgs/foo/",
|
| "foo/bar": "%$scheme/pkgs/foo/bar",
|
| });
|
| }
|
| }
|
|
|
| - {
|
| - // With a file: URI, the lookup checks for a .packages file in superdirs
|
| - // when it fails to find a ,packages file or packages/ directory next to
|
| - // the entry point.
|
| - var files = {"sub": { "main": testMain },
|
| - ".packages": "foo:pkgs/foo/",
|
| - "pkgs": fooPackage};
|
| - await test("file: implicit .packages file in ..", "%file/sub/main.dart",
|
| - file: files,
|
| - expect: {
|
| - "iconf": "%file/.packages",
|
| - // "foo": null,
|
| - "foo/": "%file/pkgs/foo/",
|
| - "foo/bar": "%file/pkgs/foo/bar",
|
| - });
|
| - }
|
| -
|
| - {
|
| - // With a non-file: URI, the lookup assumes a packges/ dir.
|
| - // The absence of a .packages file next to the entry point means
|
| - // that the resolution assumes a packages directory, whether it exists or
|
| - // not. It should not find the .packages file in the parent directory.
|
| - var files = {"sub": { "main": testMain },
|
| - ".packages": "foo:pkgs/foo/",
|
| - "pkgs": fooPackage};
|
| - await test("http: implicit packages dir", "%http/sub/main.dart",
|
| - http: files,
|
| - expect: {
|
| - "iroot": "%http/sub/packages/",
|
| - // "foo": null,
|
| - "foo/": "%http/sub/packages/foo/",
|
| - "foo/bar": "%http/sub/packages/foo/bar",
|
| - "foo.x": null,
|
| - });
|
| - }
|
| -
|
| + // Tests where there are files on both http: and file: sources.
|
|
|
| - if (failingTests.isNotEmpty) {
|
| - print("Errors found in tests:\n ${failingTests.join("\n ")}\n");
|
| - exit(255);
|
| + 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/bar": "%$pkgScheme/pkgs/foo/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",
|
| + });
|
| + }
|
| + }
|
| }
|
| - asyncEnd();
|
| }
|
|
|
| +
|
| // ---------------------------------------------------------
|
| // Helper functionality.
|
|
|
| -var failingTests = new Set();
|
| -
|
| var fileHttpRegexp = new RegExp(r"%(?:file|http)/");
|
|
|
| -Future test(String name, String main,
|
| - {String root, String config, List<String> args,
|
| - Map file, Map http, Map expect}) async {
|
| - // Default values that are easily recognized in output.
|
| - String fileRoot = "<no files configured>";
|
| - String httpRoot = "<no http server configured>";
|
| -
|
| - /// Replaces markers `%file/` and `%http/` with the actual locations.
|
| - ///
|
| - /// Accepts a `null` [source] and returns `null` again.
|
| - String fixPaths(String source) {
|
| - if (source == null) return null;
|
| - var result = source.replaceAllMapped(fileHttpRegexp, (match) {
|
| - if (source.startsWith("file", match.start + 1)) return fileRoot;
|
| - return httpRoot;
|
| - });
|
| - return result;
|
| - }
|
| -
|
| - // Set up temporary directory or HTTP server.
|
| - Directory tmpDir;
|
| - var https;
|
| - if (file != null) {
|
| - tmpDir = createTempDir();
|
| - fileRoot = new Uri.directory(tmpDir.path).toString();
|
| - }
|
| - if (http != null) {
|
| - https = await startServer(http, fixPaths);
|
| - httpRoot = "http://${https.address.address}:${https.port}/";
|
| - }
|
| - if (file != null) {
|
| - // Create files after both roots are known, to allow file content
|
| - // to refer to the them.
|
| - createFiles(tmpDir, file, fixPaths);
|
| - }
|
| -
|
| +// 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 runDart(fixPaths(main),
|
| - root: fixPaths(root),
|
| - config: fixPaths(config),
|
| - scriptArgs: args?.map(fixPaths));
|
| - // 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.
|
| - var expects = {
|
| - "pconf": null,
|
| - "proot": null,
|
| - "iconf": null,
|
| - "iroot": null,
|
| - // "foo": null,
|
| - "foo/": null,
|
| - "foo/bar": null,
|
| - "foo.x": "qux",
|
| - }..addAll(expect);
|
| - match(JSON.decode(output), expects, fixPaths, name);
|
| + 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 runDart or parsing the result.
|
| + // Unexpected error calling execDart or parsing the result.
|
| // Report it and continue.
|
| - print("ERROR running $name: $e\n$s");
|
| - failingTests.add(name);
|
| - } finally {
|
| - if (https != null) await https.close();
|
| - if (tmpDir != null) tmpDir.deleteSync(recursive: true);
|
| + print("ERROR running $description: $e\n$s");
|
| + failingTests.putIfAbsent(description, () => []).add("$e");
|
| }
|
| }
|
|
|
| @@ -293,26 +617,28 @@ Future test(String name, String main,
|
| /// 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 fixPaths(String expectation),
|
| - String name) {
|
| +void match(Map actuals, Map expectations, String desc, String actualJson) {
|
| for (var key in expectations.keys) {
|
| - var expectation = fixPaths(expectations[key]?.toString());
|
| + var expectation = expectations[key]?.toString();
|
| var actual = actuals[key];
|
| if (expectation != actual) {
|
| - print("ERROR: $name: $key: Expected: <$expectation> Found: <$actual>");
|
| - failingTests.add(name);
|
| + 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 = r"""
|
| -import "dart:convert" show JSON;
|
| -import "dart:io" show Platform, Directory;
|
| -import "dart:isolate" show Isolate;
|
| -import "package:foo/foo.dart" deferred as foo;
|
| +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;
|
| @@ -346,60 +672,89 @@ main(_) async {
|
|
|
| /// Script that spawns a new Isolate using Isolate.spawnUri.
|
| ///
|
| -/// Takes URI of target isolate, package config and package root as
|
| -/// command line arguments. Any further arguments are forwarded to the
|
| -/// spawned isolate.
|
| -const String spawnUriMain = r"""
|
| -import "dart:isolate";
|
| +/// 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 conf = args.length > 1 && args[1].isNotEmpty ? Uri.parse(args[1]) : null;
|
| - Uri root = args.length > 2 && args[2].isNotEmpty ? Uri.parse(args[2]) : null;
|
| - var restArgs = args.skip(3).toList();
|
| - var isolate = await Isolate.spawnUri(target, restArgs,
|
| - packageRoot: root, packageConfig: conf, paused: true);
|
| - // Wait for isolate to exit before exiting the main isolate.
|
| - var done = new RawReceivePort();
|
| - done.handler = (_) { done.close(); };
|
| - isolate.addExitHandler(done.sendPort);
|
| - isolate.resume(isolate.pauseCapability);
|
| + 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.
|
| -const String spawnMain = r"""
|
| -import "dart:isolate";
|
| -import "testmain.dart" as test;
|
| -main() async {
|
| - var isolate = await Isolate.spawn(test.main, [], paused: true);
|
| - // Wait for isolate to exit before exiting the main isolate.
|
| - var done = new RawReceivePort();
|
| - done.handler = (_) { done.close(); };
|
| - isolate.addExitHandler(done.sendPort);
|
| - isolate.resume(isolate.pauseCapability);
|
| +///
|
| +/// 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> runDart(String script,
|
| - {String root, String config,
|
| - Iterable<String> scriptArgs}) async {
|
| +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 (root != null) args..add("-p")..add(root);
|
| - if (config != null) args..add("--packages=$config");
|
| + 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) {
|
| + if (results.exitCode != 0 || results.stderr.isNotEmpty) {
|
| throw results.stderr;
|
| }
|
| return results.stdout;
|
| @@ -411,11 +766,7 @@ Future<String> runDart(String script,
|
| /// 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.
|
| -/// Subdirectories may include a package directory. If [packageDir]
|
| -/// is provided, a `.packages` file is created for the content of that
|
| -/// directory.
|
| -void createFiles(Directory tempDir, Map content, String fixPaths(String text),
|
| - [String packageDir]) {
|
| +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();
|
| @@ -424,7 +775,7 @@ void createFiles(Directory tempDir, Map content, String fixPaths(String text),
|
|
|
| void createTextFile(Directory base, String name, String content) {
|
| File newFile = new File(p.join(base.path, name));
|
| - newFile.writeAsStringSync(fixPaths(content));
|
| + newFile.writeAsStringSync(content);
|
| }
|
|
|
| void createRecursive(Directory dir, Map map) {
|
| @@ -444,14 +795,7 @@ void createFiles(Directory tempDir, Map content, String fixPaths(String text),
|
| }
|
| }
|
|
|
| - createRecursive(tempDir, content);
|
| - if (packageDir != null) {
|
| - // Unused?
|
| - Map packages = content[packageDir];
|
| - var entries =
|
| - packages.keys.map((key) => "$key:$packageDir/$key").join("\n");
|
| - createTextFile(tempDir, ".packages", entries);
|
| - }
|
| + createRecursive(createDir(tempDir, subDir), content);
|
| }
|
|
|
| /// Start an HTTP server which serves a directory/file structure.
|
| @@ -462,7 +806,7 @@ void createFiles(Directory tempDir, Map content, String fixPaths(String text),
|
| /// 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, String fixPaths(String text)) async {
|
| +Future<HttpServer> startServer(Map files) async {
|
| return (await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 0))
|
| ..forEach((request) {
|
| var result = files;
|
| @@ -478,7 +822,7 @@ Future<HttpServer> startServer(Map files, String fixPaths(String text)) async {
|
| }
|
| }
|
| if (result is String) {
|
| - request.response..write(fixPaths(result))
|
| + request.response..write(result)
|
| ..close();
|
| return;
|
| }
|
| @@ -488,11 +832,144 @@ Future<HttpServer> startServer(Map files, String fixPaths(String text)) async {
|
| });
|
| }
|
|
|
| -// Counter used to avoid reusing temporary directory names.
|
| -// Some platforms are timer based, and creating two temp-dirs withing a short
|
| -// duration may cause a collision.
|
| -int tmpDirCounter = 0;
|
| +// 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("pftest-${tmpDirCounter++}-");
|
| + 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;
|
| }
|
|
|