OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 import "dart:async"; |
| 6 import "dart:io"; |
| 7 import "dart:isolate"; |
| 8 import "dart:convert" show JSON; |
| 9 import "package:path/path.dart" as p; |
| 10 import "package:expect/expect.dart"; |
| 11 import "package:async_helper/async_helper.dart"; |
| 12 |
| 13 main() async { |
| 14 asyncStart(); |
| 15 |
| 16 await test("file: no resolution", |
| 17 "%file/main.dart", |
| 18 file: {"main": testMain}, |
| 19 expect: {"foo.x": null}); |
| 20 |
| 21 // An HTTP script with no ".packages" file assumes a "packages" dir. |
| 22 await test("http: no resolution", "%http/main.dart", |
| 23 http: {"main": testMain}, |
| 24 expect: { |
| 25 // "foo": null, |
| 26 "foo/": "%http/packages/foo/", |
| 27 "foo/bar": "%http/packages/foo/bar", |
| 28 "foo.x": null, |
| 29 }); |
| 30 |
| 31 for (var scheme in ["file", "http"]) { |
| 32 |
| 33 testScheme(name, main, {expect, files, args, root, config}) { |
| 34 return test("$scheme: $name", main, expect: expect, |
| 35 root: root, config: config, args: args, |
| 36 file: scheme == "file" ? files : null, |
| 37 http: scheme == "http" ? files : null); |
| 38 } |
| 39 |
| 40 { |
| 41 var files = {"main": testMain, "packages": fooPackage}; |
| 42 // Expect implicitly detected package dir. |
| 43 await testScheme("implicit packages dir","%$scheme/main.dart", |
| 44 files: files, |
| 45 expect: { |
| 46 "iroot": "%$scheme/packages/", |
| 47 // "foo": null, |
| 48 "foo/": "%$scheme/packages/foo/", |
| 49 "foo/bar": "%$scheme/packages/foo/bar", |
| 50 }); |
| 51 } |
| 52 |
| 53 { |
| 54 var files = {"sub": {"main": testMain, "packages": fooPackage}, |
| 55 ".packages": ""}; |
| 56 // Expect implicitly detected package dir. |
| 57 await testScheme("implicit packages dir 2", "%$scheme/sub/main.dart", |
| 58 files: files, |
| 59 expect: { |
| 60 "iroot": "%$scheme/sub/packages/", |
| 61 // "foo": null, |
| 62 "foo/": "%$scheme/sub/packages/foo/", |
| 63 "foo/bar": "%$scheme/sub/packages/foo/bar", |
| 64 }); |
| 65 } |
| 66 |
| 67 { |
| 68 var files = {"main": testMain, |
| 69 ".packages": "foo:pkgs/foo/", |
| 70 "pkgs": fooPackage}; |
| 71 await testScheme("implicit .packages file", "%$scheme/main.dart", |
| 72 files: files, |
| 73 expect: { |
| 74 "iconf": "%$scheme/.packages", |
| 75 // "foo": null, |
| 76 "foo/": "%$scheme/pkgs/foo/", |
| 77 "foo/bar": "%$scheme/pkgs/foo/bar", |
| 78 }); |
| 79 } |
| 80 |
| 81 { |
| 82 var files = {"main": testMain, |
| 83 ".packages": "foo:packages/foo/", |
| 84 "packages": fooPackage, |
| 85 "pkgs": fooPackage}; |
| 86 await testScheme("explicit package root, no slash", "%$scheme/main.dart", |
| 87 files: files, |
| 88 root: "%$scheme/pkgs", |
| 89 expect: { |
| 90 "proot": "%$scheme/pkgs/", |
| 91 "iroot": "%$scheme/pkgs/", |
| 92 // "foo": null, |
| 93 "foo/": "%$scheme/pkgs/foo/", |
| 94 "foo/bar": "%$scheme/pkgs/foo/bar", |
| 95 }); |
| 96 } |
| 97 |
| 98 { |
| 99 var files = {"main": testMain, |
| 100 ".packages": "foo:packages/foo/", |
| 101 "packages": fooPackage, |
| 102 "pkgs": fooPackage}; |
| 103 await testScheme("explicit package root, slash", "%$scheme/main.dart", |
| 104 files: files, |
| 105 root: "%$scheme/pkgs", |
| 106 expect: { |
| 107 "proot": "%$scheme/pkgs/", |
| 108 "iroot": "%$scheme/pkgs/", |
| 109 // "foo": null, |
| 110 "foo/": "%$scheme/pkgs/foo/", |
| 111 "foo/bar": "%$scheme/pkgs/foo/bar", |
| 112 }); |
| 113 } |
| 114 |
| 115 { |
| 116 var files = {"main": testMain, |
| 117 ".packages": "foo:packages/foo/", |
| 118 "packages": fooPackage, |
| 119 ".pkgs": "foo:pkgs/foo/", |
| 120 "pkgs": fooPackage}; |
| 121 await testScheme("explicit package config file", "%$scheme/main.dart", |
| 122 files: files, |
| 123 config: "%$scheme/.pkgs", |
| 124 expect: { |
| 125 "pconf": "%$scheme/.pkgs", |
| 126 "iconf": "%$scheme/.pkgs", |
| 127 // "foo": null, |
| 128 "foo/": "%$scheme/pkgs/foo/", |
| 129 "foo/bar": "%$scheme/pkgs/foo/bar", |
| 130 }); |
| 131 } |
| 132 |
| 133 { |
| 134 var files = {"main": testMain, |
| 135 ".packages": "foo:packages/foo/", |
| 136 "packages": fooPackage, |
| 137 "pkgs": fooPackage}; |
| 138 var dataUri = "data:,foo:%$scheme/pkgs/foo/\n"; |
| 139 await testScheme("explicit data: config file", "%$scheme/main.dart", |
| 140 files: files, |
| 141 config: dataUri, |
| 142 expect: { |
| 143 "pconf": dataUri, |
| 144 "iconf": dataUri, |
| 145 // "foo": null, |
| 146 "foo/": "%$scheme/pkgs/foo/", |
| 147 "foo/bar": "%$scheme/pkgs/foo/bar", |
| 148 }); |
| 149 } |
| 150 } |
| 151 |
| 152 { |
| 153 // With a file: URI, the lookup checks for a .packages file in superdirs. |
| 154 var files = {"sub": { "main": testMain }, |
| 155 ".packages": "foo:pkgs/foo/", |
| 156 "pkgs": fooPackage}; |
| 157 await test("file: implicit .packages file in ..", "%file/sub/main.dart", |
| 158 file: files, |
| 159 expect: { |
| 160 "iconf": "%file/.packages", |
| 161 // "foo": null, |
| 162 "foo/": "%file/pkgs/foo/", |
| 163 "foo/bar": "%file/pkgs/foo/bar", |
| 164 }); |
| 165 } |
| 166 |
| 167 { |
| 168 // With a non-file: URI, the lookup assumes a packges/ dir. |
| 169 var files = {"sub": { "main": testMain }, |
| 170 ".packages": "foo:pkgs/foo/", |
| 171 "pkgs": fooPackage}; |
| 172 // Expect implicitly detected .package file. |
| 173 await test("http: implicit packages dir", "%http/sub/main.dart", |
| 174 http: files, |
| 175 expect: { |
| 176 "iroot": "%http/sub/packages/", |
| 177 // "foo": null, |
| 178 "foo/": "%http/sub/packages/foo/", |
| 179 "foo/bar": "%http/sub/packages/foo/bar", |
| 180 "foo.x": null, |
| 181 }); |
| 182 } |
| 183 |
| 184 |
| 185 if (failingTests.isNotEmpty) { |
| 186 print("Errors found in tests:\n ${failingTests.join("\n ")}\n"); |
| 187 exit(255); |
| 188 } |
| 189 asyncEnd(); |
| 190 } |
| 191 |
| 192 // --------------------------------------------------------- |
| 193 // Helper functionality. |
| 194 |
| 195 var failingTests = new Set(); |
| 196 |
| 197 var fileHttpRegexp = new RegExp(r"%(?:file|http)/"); |
| 198 |
| 199 Future test(String name, String main, |
| 200 {String root, String config, List<String> args, |
| 201 Map file, Map http, Map expect}) async { |
| 202 // Default values that are easily recognized in output. |
| 203 String fileRoot = "<no files configured>"; |
| 204 String httpRoot = "<not http server configured>"; |
| 205 |
| 206 /// Replaces markers `%file/` and `%http/` with the actual locations. |
| 207 /// |
| 208 /// Accepts a `null` [source] and returns `null` again. |
| 209 String fixPaths(String source) { |
| 210 if (source == null) return null; |
| 211 var result = source.replaceAllMapped(fileHttpRegexp, (match) { |
| 212 if (source.startsWith("file", match.start + 1)) return fileRoot; |
| 213 return httpRoot; |
| 214 }); |
| 215 return result; |
| 216 } |
| 217 |
| 218 // Set up temporary directory or HTTP server. |
| 219 Directory tmpDir; |
| 220 var https; |
| 221 if (file != null) { |
| 222 tmpDir = createTempDir(); |
| 223 fileRoot = new Uri.directory(tmpDir.path).toString(); |
| 224 } |
| 225 if (http != null) { |
| 226 https = await startServer(http, fixPaths); |
| 227 httpRoot = "http://${https.address.address}:${https.port}/"; |
| 228 } |
| 229 if (file != null) { |
| 230 // Create files after both roots are known, to allow file content |
| 231 // to refer to the them. |
| 232 createFiles(tmpDir, file, fixPaths); |
| 233 } |
| 234 |
| 235 try { |
| 236 var output = await runDart(fixPaths(main), |
| 237 root: fixPaths(root), |
| 238 config: fixPaths(config), |
| 239 scriptArgs: args?.map(fixPaths)); |
| 240 // These expectations are default. If not overridden the value will be |
| 241 // expected to be null. That is, you can't avoid testing the actual |
| 242 // value of these, you can only change what value to expect. |
| 243 // For values not included here (commented out), the result is not tested |
| 244 // unless a value (maybe null) is provided. |
| 245 var expects = { |
| 246 "pconf": null, |
| 247 "proot": null, |
| 248 "iconf": null, |
| 249 "iconf": null, |
| 250 // "foo": null, |
| 251 "foo/": null, |
| 252 "foo/bar": null, |
| 253 "foo.x": "qux", |
| 254 }..addAll(expect); |
| 255 match(JSON.decode(output), expects, fixPaths, name); |
| 256 } catch (e) { |
| 257 // Unexpected error calling runDart or parsing the result. |
| 258 // Report it and continue. |
| 259 print("ERROR running $name: $e\n$s"); |
| 260 failingTests.add(name); |
| 261 } finally { |
| 262 if (https != null) await https.close(); |
| 263 if (tmpDir != null) tmpDir.deleteSync(recursive: true); |
| 264 } |
| 265 } |
| 266 |
| 267 |
| 268 /// Test that the output of running testMain matches the expectations. |
| 269 /// |
| 270 /// The output is a string which is parse as a JSON literal. |
| 271 /// The resulting map is always mapping strings to strings, or possibly `null`. |
| 272 /// The expectations can have non-string values other than null, |
| 273 /// they are `toString`'ed before being compared (so the caller can use a URI |
| 274 /// or a File/Directory directly as an expectation). |
| 275 void match(Map actuals, Map expectations, String fixPaths(String expectation), |
| 276 String name) { |
| 277 for (var key in expectations.keys) { |
| 278 var expectation = fixPaths(expectations[key]?.toString()); |
| 279 var actual = actuals[key]; |
| 280 if (expectation != actual) { |
| 281 print("ERROR: $name: $key: Expected: <$expectation> Found: <$actual>"); |
| 282 failingTests.add(name); |
| 283 } |
| 284 } |
| 285 } |
| 286 |
| 287 /// Script that prints the current state and the result of resolving |
| 288 /// a few package URIs. This script will be invoked in different settings, |
| 289 /// and the result will be parsed and compared to the expectations. |
| 290 const String testMain = r""" |
| 291 import "dart:convert" show JSON; |
| 292 import "dart:io" show Platform, Directory; |
| 293 import "dart:isolate" show Isolate; |
| 294 import "package:foo/foo.dart" deferred as foo; |
| 295 main(_) async { |
| 296 String platformRoot = await Platform.packageRoot; |
| 297 String platformConfig = await Platform.packageConfig; |
| 298 Directory cwd = Directory.current; |
| 299 Uri script = Platform.script; |
| 300 Uri isolateRoot = await Isolate.packageRoot; |
| 301 Uri isolateConfig = await Isolate.packageConfig; |
| 302 Uri base = Uri.base; |
| 303 Uri res1 = await Isolate.resolvePackageUri(Uri.parse("package:foo")); |
| 304 Uri res2 = await Isolate.resolvePackageUri(Uri.parse("package:foo/")); |
| 305 Uri res3 = await Isolate.resolvePackageUri(Uri.parse("package:foo/bar")); |
| 306 String fooX = await foo |
| 307 .loadLibrary() |
| 308 .timeout(const Duration(seconds: 1)) |
| 309 .then((_) => foo.x, onError: (_) => null); |
| 310 print(JSON.encode({ |
| 311 "cwd": cwd.path, |
| 312 "base": base?.toString(), |
| 313 "script": script?.toString(), |
| 314 "proot": platformRoot, |
| 315 "pconf": platformConfig, |
| 316 "iroot" : isolateRoot?.toString(), |
| 317 "iconf" : isolateConfig?.toString(), |
| 318 "foo": res1?.toString(), |
| 319 "foo/": res2?.toString(), |
| 320 "foo/bar": res3?.toString(), |
| 321 "foo.x": fooX?.toString(), |
| 322 })); |
| 323 } |
| 324 """; |
| 325 |
| 326 /// Script that spawns a new Isolate using Isolate.spawnUri. |
| 327 /// |
| 328 /// Takes URI of target isolate, package config and package root as |
| 329 /// command line arguments. Any further arguments are forwarded to the |
| 330 /// spawned isolate. |
| 331 const String spawnUriMain = r""" |
| 332 import "dart:isolate"; |
| 333 main(args) async { |
| 334 Uri target = Uri.parse(args[0]); |
| 335 Uri conf = args.length > 1 && args[1].isNotEmpty ? Uri.parse(args[1]) : null; |
| 336 Uri root = args.length > 2 && args[2].isNotEmpty ? Uri.parse(args[2]) : null; |
| 337 var restArgs = args.skip(3).toList(); |
| 338 var isolate = await Isolate.spawnUri(target, restArgs, |
| 339 packageRoot: root, packageConfig: conf, paused: true); |
| 340 // Wait for isolate to exit before exiting the main isolate. |
| 341 var done = new RawReceivePort(); |
| 342 done.handler = (_) { done.close(); }; |
| 343 isolate.addExitHandler(done.sendPort); |
| 344 isolate.resume(isolate.pauseCapability); |
| 345 } |
| 346 """; |
| 347 |
| 348 /// Script that spawns a new Isolate using Isolate.spawn. |
| 349 const String spawnMain = r""" |
| 350 import "dart:isolate"; |
| 351 import "testmain.dart" as test; |
| 352 main() async { |
| 353 var isolate = await Isolate.spawn(test.main, [], paused: true); |
| 354 // Wait for isolate to exit before exiting the main isolate. |
| 355 var done = new RawReceivePort(); |
| 356 done.handler = (_) { done.close(); }; |
| 357 isolate.addExitHandler(done.sendPort); |
| 358 isolate.resume(isolate.pauseCapability); |
| 359 } |
| 360 """; |
| 361 |
| 362 /// A package directory containing only one package, "foo", with one file. |
| 363 const Map fooPackage = const { "foo": const { "foo": "var x = 'qux';" }}; |
| 364 |
| 365 /// Runs the Dart executable with the provided parameters. |
| 366 /// |
| 367 /// Captures and returns the output. |
| 368 Future<String> runDart(String script, |
| 369 {String root, String config, |
| 370 Iterable<String> scriptArgs}) async { |
| 371 // TODO: Find a way to change CWD before running script. |
| 372 var executable = Platform.executable; |
| 373 var args = []; |
| 374 if (root != null) args..add("-p")..add(root); |
| 375 if (config != null) args..add("--packages=$config"); |
| 376 args.add(script); |
| 377 if (scriptArgs != null) { |
| 378 args.addAll(scriptArgs); |
| 379 } |
| 380 return Process.run(executable, args).then((results) { |
| 381 if (results.exitCode != 0) { |
| 382 throw results.stderr; |
| 383 } |
| 384 return results.stdout; |
| 385 }); |
| 386 } |
| 387 |
| 388 /// Creates a number of files and subdirectories. |
| 389 /// |
| 390 /// The [content] is the content of the directory itself. The map keys are |
| 391 /// names and the values are either strings that represent Dart file contents |
| 392 /// or maps that represent subdirectories. |
| 393 /// Subdirectories may include a package directory. If [packageDir] |
| 394 /// is provided, a `.packages` file is created for the content of that |
| 395 /// directory. |
| 396 void createFiles(Directory tempDir, Map content, String fixPaths(String text), |
| 397 [String packageDir]) { |
| 398 Directory createDir(Directory base, String name) { |
| 399 Directory newDir = new Directory(p.join(base.path, name)); |
| 400 newDir.createSync(); |
| 401 return newDir; |
| 402 } |
| 403 |
| 404 void createTextFile(Directory base, String name, String content) { |
| 405 File newFile = new File(p.join(base.path, name)); |
| 406 newFile.writeAsStringSync(fixPaths(content)); |
| 407 } |
| 408 |
| 409 void createRecursive(Directory dir, Map map) { |
| 410 for (var name in map.keys) { |
| 411 var content = map[name]; |
| 412 if (content is String) { |
| 413 // If the name starts with "." it's a .packages file, otherwise it's |
| 414 // a dart file. Those are the only files we care about in this test. |
| 415 createTextFile(dir, |
| 416 name.startsWith(".") ? name : name + ".dart", |
| 417 content); |
| 418 } else { |
| 419 assert(content is Map); |
| 420 var subdir = createDir(dir, name); |
| 421 createRecursive(subdir, content); |
| 422 } |
| 423 } |
| 424 } |
| 425 |
| 426 createRecursive(tempDir, content); |
| 427 if (packageDir != null) { |
| 428 // Unused? |
| 429 Map packages = content[packageDir]; |
| 430 var entries = |
| 431 packages.keys.map((key) => "$key:$packageDir/$key").join("\n"); |
| 432 createTextFile(tempDir, ".packages", entries); |
| 433 } |
| 434 } |
| 435 |
| 436 /// Start an HTTP server which serves a directory/file structure. |
| 437 /// |
| 438 /// The directories and files are described by [files]. |
| 439 /// |
| 440 /// Each map key is an entry in a directory. A `Map` value is a sub-directory |
| 441 /// and a `String` value is a text file. |
| 442 /// The file contents are run through [fixPaths] to allow them to be self- |
| 443 /// referential. |
| 444 Future<HttpServer> startServer(Map files, String fixPaths(String text)) async { |
| 445 return (await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 0)) |
| 446 ..forEach((request) { |
| 447 var result = files; |
| 448 onFailure: { |
| 449 for (var part in request.uri.pathSegments) { |
| 450 if (part.endsWith(".dart")) { |
| 451 part = part.substring(0, part.length - 5); |
| 452 } |
| 453 if (result is Map) { |
| 454 result = result[part]; |
| 455 } else { |
| 456 break onFailure; |
| 457 } |
| 458 } |
| 459 if (result is String) { |
| 460 request.response..write(fixPaths(result)) |
| 461 ..close(); |
| 462 return; |
| 463 } |
| 464 } |
| 465 request.response..statusCode = HttpStatus.NOT_FOUND |
| 466 ..close(); |
| 467 }); |
| 468 } |
| 469 |
| 470 // Counter used to avoid reusing temporary directory names. |
| 471 // Some platforms are timer based, and creating two temp-dirs withing a short |
| 472 // duration may cause a collision. |
| 473 int tmpDirCounter = 0; |
| 474 |
| 475 Directory createTempDir() { |
| 476 return Directory.systemTemp.createTempSync("pftest-${tmpDirCounter++}-"); |
| 477 } |
OLD | NEW |