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; |
} |