From 07b1962edc0228a1a76e070ebb31d1f4c77ed83c Mon Sep 17 00:00:00 2001 From: "nweiz@google.com" <nweiz@google.com> Date: Wed, 20 Aug 2014 20:06:20 +0000 Subject: [PATCH] Precompile dependencies' executables for use with "pub run". R=rnystrom@google.com BUG=20482 Review URL: https://codereview.chromium.org//482053002 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge@39424 260f80e4-7a28-3924-810f-c04153c831b5 --- lib/src/entrypoint.dart | 128 ++++++++++++++++++ lib/src/executable.dart | 49 ++++++- lib/src/package.dart | 14 +- lib/src/package_graph.dart | 22 +++ lib/src/utils.dart | 26 ++++ ...for_immediate_and_transitive_dep_test.dart | 56 ++++++++ test/snapshot/creates_a_snapshot_test.dart | 53 ++++++++ ...napshot_an_entrypoint_dependency_test.dart | 32 +++++ .../doesnt_snapshot_path_dependency_test.dart | 29 ++++ ...snapshot_transitive_dependencies_test.dart | 30 ++++ ...ints_errors_for_broken_snapshots_test.dart | 49 +++++++ ...mpiles_if_the_sdk_is_out_of_date_test.dart | 44 ++++++ .../snapshots_transformed_code_test.dart | 61 +++++++++ ...upgrades_snapshot_for_dependency_test.dart | 55 ++++++++ test/snapshot/upgrades_snapshot_test.dart | 46 +++++++ test/test_pub.dart | 35 ++++- 16 files changed, 719 insertions(+), 10 deletions(-) create mode 100644 test/snapshot/creates_a_snapshot_for_immediate_and_transitive_dep_test.dart create mode 100644 test/snapshot/creates_a_snapshot_test.dart create mode 100644 test/snapshot/doesnt_snapshot_an_entrypoint_dependency_test.dart create mode 100644 test/snapshot/doesnt_snapshot_path_dependency_test.dart create mode 100644 test/snapshot/doesnt_snapshot_transitive_dependencies_test.dart create mode 100644 test/snapshot/prints_errors_for_broken_snapshots_test.dart create mode 100644 test/snapshot/recompiles_if_the_sdk_is_out_of_date_test.dart create mode 100644 test/snapshot/snapshots_transformed_code_test.dart create mode 100644 test/snapshot/upgrades_snapshot_for_dependency_test.dart create mode 100644 test/snapshot/upgrades_snapshot_test.dart diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index 9d2f815b..16b19a59 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart @@ -5,14 +5,19 @@ library pub.entrypoint; import 'dart:async'; +import 'dart:io'; import 'package:path/path.dart' as path; +import 'package:barback/barback.dart'; +import 'barback/asset_environment.dart'; +import 'exceptions.dart'; import 'io.dart'; import 'lock_file.dart'; import 'log.dart' as log; import 'package.dart'; import 'package_graph.dart'; +import 'sdk.dart' as sdk; import 'solver/version_solver.dart'; import 'source/cached.dart'; import 'system_cache.dart'; @@ -133,10 +138,133 @@ class Entrypoint { _linkOrDeleteSecondaryPackageDirs(); result.summarizeChanges(type, dryRun: dryRun); + + // TODO(nweiz): we've already parsed all the pubspecs and we know the + // lockfile is up to date; there's got to be a way to re-use that + // information here. + // + // Also, don't precompile stuff when the transitive dependencies + // haven't changed. + return precompileExecutables().catchError((error, stackTrace) { + // Just log exceptions here. Since the method is just about acquiring + // dependencies, it shouldn't fail unless that fails. + log.exception(error, stackTrace); + }); }); }); } + /// Precompiles all executables from dependencies that don't transitively + /// depend on [this] or on a path dependency. + Future precompileExecutables() { + return loadPackageGraph().then((graph) { + var executables = new Map.fromIterable(root.immediateDependencies, + key: (dep) => dep.name, + value: (dep) => _executablesForPackage(graph, dep.name)); + + for (var package in executables.keys.toList()) { + if (executables[package].isEmpty) executables.remove(package); + } + + var binDir = path.join('.pub', 'bin'); + deleteEntry(binDir); + if (executables.isEmpty) return null; + + return log.progress("Precompiling executables", () { + // TODO(nweiz): Only add assets touchable by the executables we're + // precompiling. + ensureDir(binDir); + + // Make sure there's a trailing newline so our version file matches the + // SDK's. + writeTextFile(path.join(binDir, 'sdk-version'), "${sdk.version}\n"); + return AssetEnvironment.create(this, BarbackMode.RELEASE, + WatcherType.NONE, useDart2JS: false).then((environment) { + environment.barback.errors.listen((error) { + log.error(log.red("Build error:\n$error")); + }); + + return waitAndPrintErrors(executables.keys.map((package) { + return _precompileExecutablesForPackage( + environment, package, executables[package]); + })); + }); + }); + }); + } + + /// Returns the list of all executable assets for [packageName] that should be + /// precompiled. + /// + /// If [changed] isn't `null`, executables for [packageName] will only be + /// compiled if they might depend on a package in [changed]. + List<AssetId> _executablesForPackage(PackageGraph graph, String packageName) { + var package = graph.packages[packageName]; + var binDir = path.join(package.dir, 'bin'); + if (!dirExists(binDir)) return []; + + // If the lockfile has a dependency on the entrypoint or on a path + // dependency, its executables could change at any point, so we + // shouldn't precompile them. + var hasUncachedDependency = graph.transitiveDependencies(packageName) + .any((package) { + var source = cache.sources[ + graph.lockFile.packages[package.name].source]; + return source is! CachedSource; + }); + if (hasUncachedDependency) return []; + + return ordered(package.listFiles(beneath: binDir, recursive: false)) + .where((executable) => path.extension(executable) == '.dart') + .map((executable) { + return new AssetId( + package.name, + path.toUri(path.relative(executable, from: package.dir)) + .toString()); + }).toList(); + } + + /// Precompiles all [executables] for [package]. + /// + /// [executables] is assumed to be a list of Dart executables in [package]'s + /// bin directory. + Future _precompileExecutablesForPackage( + AssetEnvironment environment, String package, List<AssetId> executables) { + var cacheDir = path.join('.pub', 'bin', package); + ensureDir(cacheDir); + + // TODO(nweiz): Unserve this directory when we're done with it. + return environment.servePackageBinDirectory(package).then((server) { + return waitAndPrintErrors(executables.map((id) { + var basename = path.url.basename(id.path); + var snapshotPath = path.join(cacheDir, "$basename.snapshot"); + return runProcess(Platform.executable, [ + '--snapshot=$snapshotPath', + server.url.resolve(basename).toString() + ]).then((result) { + if (result.success) { + log.message("Precompiled ${_executableName(id)}."); + } else { + // TODO(nweiz): Stop manually deleting this when issue 20504 is + // fixed. + deleteEntry(snapshotPath); + throw new ApplicationException( + log.yellow("Failed to precompile " + "${_executableName(id)}:\n") + + result.stderr.join('\n')); + } + }); + })); + }); + } + + /// Returns the executable name for [id]. + /// + /// [id] is assumed to be an executable in a bin directory. The return value + /// is intended for log output and may contain formatting. + String _executableName(AssetId id) => + log.bold("${id.package}:${path.basenameWithoutExtension(id.path)}"); + /// Makes sure the package at [id] is locally available. /// /// This automatically downloads the package to the system-wide cache as well diff --git a/lib/src/executable.dart b/lib/src/executable.dart index 9d62188b..2551bae8 100644 --- a/lib/src/executable.dart +++ b/lib/src/executable.dart @@ -15,7 +15,9 @@ import 'barback/asset_environment.dart'; import 'command.dart'; import 'entrypoint.dart'; import 'exit_codes.dart' as exit_codes; +import 'io.dart'; import 'log.dart' as log; +import 'sdk.dart' as sdk; import 'utils.dart'; /// Runs [executable] from [package] reachable from [entrypoint]. @@ -31,6 +33,18 @@ import 'utils.dart'; Future<int> runExecutable(PubCommand command, Entrypoint entrypoint, String package, String executable, Iterable<String> args, {bool isGlobal: false}) { + // Unless the user overrides the verbosity, we want to filter out the + // normal pub output shown while loading the environment. + if (log.verbosity == log.Verbosity.NORMAL) { + log.verbosity = log.Verbosity.WARNING; + } + + var snapshotPath = p.join(".pub", "bin", package, + "$executable.dart.snapshot"); + if (!isGlobal && fileExists(snapshotPath)) { + return _runCachedExecutable(entrypoint, snapshotPath, args); + } + // If the command has a path separator, then it's a path relative to the // root of the package. Otherwise, it's implicitly understood to be in // "bin". @@ -50,12 +64,6 @@ Future<int> runExecutable(PubCommand command, Entrypoint entrypoint, executable = p.join("bin", executable); } - // Unless the user overrides the verbosity, we want to filter out the - // normal pub output shown while loading the environment. - if (log.verbosity == log.Verbosity.NORMAL) { - log.verbosity = log.Verbosity.WARNING; - } - var environment; return AssetEnvironment.create(entrypoint, BarbackMode.RELEASE, WatcherType.NONE, useDart2JS: false).then((_environment) { @@ -128,3 +136,32 @@ Future<int> runExecutable(PubCommand command, Entrypoint entrypoint, }); }); } + +/// Runs the executable snapshot at [snapshotPath]. +Future _runCachedExecutable(Entrypoint entrypoint, String snapshotPath, + List<String> args) { + return syncFuture(() { + // If the snapshot was compiled with a different SDK version, we need to + // recompile it. + var sdkVersionPath = p.join(".pub", "bin", "sdk-version"); + if (fileExists(sdkVersionPath) && + readTextFile(sdkVersionPath) == "${sdk.version}\n") { + return null; + } + + log.fine("Precompiled executables are out of date."); + return entrypoint.precompileExecutables(); + }).then((_) { + var vmArgs = ["--checked", snapshotPath]..addAll(args); + + return Process.start(Platform.executable, vmArgs).then((process) { + // Note: we're not using process.std___.pipe(std___) here because + // that prevents pub from also writing to the output streams. + process.stderr.listen(stderr.add); + process.stdout.listen(stdout.add); + stdin.listen(process.stdin.add); + + return process.exitCode; + }); + }); +} diff --git a/lib/src/package.dart b/lib/src/package.dart index 43336484..438c34b2 100644 --- a/lib/src/package.dart +++ b/lib/src/package.dart @@ -124,8 +124,10 @@ class Package { /// If this is a Git repository, this will respect .gitignore; otherwise, it /// will return all non-hidden, non-blacklisted files. /// - /// If [beneath] is passed, this will only return files beneath that path. - List<String> listFiles({String beneath}) { + /// If [beneath] is passed, this will only return files beneath that path. If + /// [recursive] is true, this will return all files beneath that path; + /// otherwise, it will only return files one level beneath it. + List<String> listFiles({String beneath, recursive: true}) { if (beneath == null) beneath = dir; // This is used in some performance-sensitive paths and can list many, many @@ -145,6 +147,12 @@ class Package { ["ls-files", "--cached", "--others", "--exclude-standard", relativeBeneath], workingDir: dir); + + // If we're not listing recursively, strip out paths that contain + // separators. Since git always prints forward slashes, we always detect + // them. + if (!recursive) files = files.where((file) => !file.contains('/')); + // Git always prints files relative to the repository root, but we want // them relative to the working directory. It also prints forward slashes // on Windows which we normalize away for easier testing. @@ -156,7 +164,7 @@ class Package { return fileExists(file); }); } else { - files = listDir(beneath, recursive: true, includeDirs: false, + files = listDir(beneath, recursive: recursive, includeDirs: false, whitelist: _WHITELISTED_FILES); } diff --git a/lib/src/package_graph.dart b/lib/src/package_graph.dart index 14913787..4470e51b 100644 --- a/lib/src/package_graph.dart +++ b/lib/src/package_graph.dart @@ -7,6 +7,7 @@ library pub.package_graph; import 'entrypoint.dart'; import 'lock_file.dart'; import 'package.dart'; +import 'utils.dart'; /// A holistic view of the entire transitive dependency graph for an entrypoint. /// @@ -24,5 +25,26 @@ class PackageGraph { /// All transitive dependencies of the entrypoint (including itself). final Map<String, Package> packages; + /// A map of transitive dependencies for each package. + Map<String, Set<Package>> _transitiveDependencies; + PackageGraph(this.entrypoint, this.lockFile, this.packages); + + /// Returns all transitive dependencies of [package]. + /// + /// For the entrypoint this returns all packages in [packages], which includes + /// dev and override. For any other package, it ignores dev and override + /// dependencies. + Set<Package> transitiveDependencies(String package) { + if (package == entrypoint.root.name) return packages.values.toSet(); + + if (_transitiveDependencies == null) { + var closure = transitiveClosure(mapMap(packages, + value: (_, package) => package.dependencies.map((dep) => dep.name))); + _transitiveDependencies = mapMap(closure, + value: (_, names) => names.map((name) => packages[name]).toSet()); + } + + return _transitiveDependencies[package]; + } } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 4e1a369b..3abeefdb 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -331,6 +331,32 @@ Future<Map> mapFromIterableAsync(Iterable iter, {key(element), })).then((_) => map); } +/// Returns the transitive closure of [graph]. +/// +/// This assumes [graph] represents a graph with a vertex for each key and an +/// edge betweek each key and the values for that key. +Map<dynamic, Set> transitiveClosure(Map<dynamic, Iterable> graph) { + // This uses the Floyd-Warshall algorithm + // (https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm). + var result = {}; + graph.forEach((vertex, edges) { + result[vertex] = new Set.from(edges)..add(vertex); + }); + + for (var vertex1 in graph.keys) { + for (var vertex2 in graph.keys) { + for (var vertex3 in graph.keys) { + if (result[vertex2].contains(vertex1) && + result[vertex1].contains(vertex3)) { + result[vertex2].add(vertex3); + } + } + } + } + + return result; +} + /// Given a list of filenames, returns a set of patterns that can be used to /// filter for those filenames. /// diff --git a/test/snapshot/creates_a_snapshot_for_immediate_and_transitive_dep_test.dart b/test/snapshot/creates_a_snapshot_for_immediate_and_transitive_dep_test.dart new file mode 100644 index 00000000..a084aa79 --- /dev/null +++ b/test/snapshot/creates_a_snapshot_for_immediate_and_transitive_dep_test.dart @@ -0,0 +1,56 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library pub_tests; + +import 'package:path/path.dart' as p; +import 'package:scheduled_test/scheduled_test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +main() { + initConfig(); + integration("creates a snapshot for an immediate dependency that's also a " + "transitive dependency", () { + servePackages([ + packageMap("foo", "1.2.3"), + packageMap("bar", "1.2.3", {"foo": "1.2.3"}) + ], contents: [ + d.dir("bin", [ + d.file("hello.dart", "void main() => print('hello!');"), + d.file("goodbye.dart", "void main() => print('goodbye!');"), + d.file("shell.sh", "echo shell"), + d.dir("subdir", [ + d.file("sub.dart", "void main() => print('sub!');") + ]) + ]) + ]); + + d.appDir({"foo": "1.2.3"}).create(); + + pubGet(output: allOf([ + contains("Precompiled foo:hello."), + contains("Precompiled foo:goodbye.") + ])); + + d.dir(p.join(appPath, '.pub', 'bin'), [ + d.file('sdk-version', '0.1.2+3\n'), + d.dir('foo', [ + d.matcherFile('hello.dart.snapshot', contains('hello!')), + d.matcherFile('goodbye.dart.snapshot', contains('goodbye!')), + d.nothing('shell.sh.snapshot'), + d.nothing('subdir') + ]) + ]).validate(); + + var process = pubRun(args: ['foo:hello']); + process.stdout.expect("hello!"); + process.shouldExit(); + + process = pubRun(args: ['foo:goodbye']); + process.stdout.expect("goodbye!"); + process.shouldExit(); + }); +} diff --git a/test/snapshot/creates_a_snapshot_test.dart b/test/snapshot/creates_a_snapshot_test.dart new file mode 100644 index 00000000..27411dc1 --- /dev/null +++ b/test/snapshot/creates_a_snapshot_test.dart @@ -0,0 +1,53 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library pub_tests; + +import 'package:path/path.dart' as p; +import 'package:scheduled_test/scheduled_test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +main() { + initConfig(); + integration("creates a snapshot for an immediate dependency's executables", + () { + servePackages([packageMap("foo", "1.2.3")], contents: [ + d.dir("bin", [ + d.file("hello.dart", "void main() => print('hello!');"), + d.file("goodbye.dart", "void main() => print('goodbye!');"), + d.file("shell.sh", "echo shell"), + d.dir("subdir", [ + d.file("sub.dart", "void main() => print('sub!');") + ]) + ]) + ]); + + d.appDir({"foo": "1.2.3"}).create(); + + pubGet(output: allOf([ + contains("Precompiled foo:hello."), + contains("Precompiled foo:goodbye.") + ])); + + d.dir(p.join(appPath, '.pub', 'bin'), [ + d.file('sdk-version', '0.1.2+3\n'), + d.dir('foo', [ + d.matcherFile('hello.dart.snapshot', contains('hello!')), + d.matcherFile('goodbye.dart.snapshot', contains('goodbye!')), + d.nothing('shell.sh.snapshot'), + d.nothing('subdir') + ]) + ]).validate(); + + var process = pubRun(args: ['foo:hello']); + process.stdout.expect("hello!"); + process.shouldExit(); + + process = pubRun(args: ['foo:goodbye']); + process.stdout.expect("goodbye!"); + process.shouldExit(); + }); +} diff --git a/test/snapshot/doesnt_snapshot_an_entrypoint_dependency_test.dart b/test/snapshot/doesnt_snapshot_an_entrypoint_dependency_test.dart new file mode 100644 index 00000000..164344ce --- /dev/null +++ b/test/snapshot/doesnt_snapshot_an_entrypoint_dependency_test.dart @@ -0,0 +1,32 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library pub_tests; + +import 'package:path/path.dart' as p; +import 'package:scheduled_test/scheduled_test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +main() { + initConfig(); + integration("doesn't create a snapshot for a package that depends on the " + "entrypoint", () { + servePackages([ + packageMap("foo", "1.2.3", {'bar': '1.2.3'}), + packageMap("bar", "1.2.3", {'myapp': 'any'}) + ], contents: [ + d.dir("bin", [d.file("hello.dart", "void main() => print('hello!');")]) + ]); + + d.appDir({"foo": "1.2.3"}).create(); + + pubGet(); + + // No local cache should be created, since all dependencies transitively + // depend on the entrypoint. + d.nothing(p.join(appPath, '.pub', 'bin')).validate(); + }); +} diff --git a/test/snapshot/doesnt_snapshot_path_dependency_test.dart b/test/snapshot/doesnt_snapshot_path_dependency_test.dart new file mode 100644 index 00000000..5c1b1c48 --- /dev/null +++ b/test/snapshot/doesnt_snapshot_path_dependency_test.dart @@ -0,0 +1,29 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library pub_tests; + +import 'package:path/path.dart' as p; +import 'package:scheduled_test/scheduled_test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +main() { + initConfig(); + integration("doesn't create a snapshot for a path dependency", () { + d.dir("foo", [ + d.libPubspec("foo", "1.2.3"), + d.dir("bin", [ + d.dir("bin", [d.file("hello.dart", "void main() => print('hello!');")]) + ]) + ]).create(); + + d.appDir({"foo": {"path": "../foo"}}).create(); + + pubGet(); + + d.nothing(p.join(appPath, '.pub', 'bin')).validate(); + }); +} diff --git a/test/snapshot/doesnt_snapshot_transitive_dependencies_test.dart b/test/snapshot/doesnt_snapshot_transitive_dependencies_test.dart new file mode 100644 index 00000000..9b4512e7 --- /dev/null +++ b/test/snapshot/doesnt_snapshot_transitive_dependencies_test.dart @@ -0,0 +1,30 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library pub_tests; + +import 'package:path/path.dart' as p; +import 'package:scheduled_test/scheduled_test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +main() { + initConfig(); + integration("doesn't create a snapshot for transitive dependencies' " + "executables", () { + servePackages([ + packageMap("foo", "1.2.3", {'bar': '1.2.3'}), + packageMap("bar", "1.2.3") + ], contents: [ + d.dir("bin", [d.file("hello.dart", "void main() => print('hello!');")]) + ]); + + d.appDir({"foo": "1.2.3"}).create(); + + pubGet(); + + d.nothing(p.join(appPath, '.pub', 'bin', 'bar')).validate(); + }); +} diff --git a/test/snapshot/prints_errors_for_broken_snapshots_test.dart b/test/snapshot/prints_errors_for_broken_snapshots_test.dart new file mode 100644 index 00000000..1c8af8f1 --- /dev/null +++ b/test/snapshot/prints_errors_for_broken_snapshots_test.dart @@ -0,0 +1,49 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library pub_tests; + +import 'package:path/path.dart' as p; +import 'package:scheduled_test/scheduled_test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +main() { + initConfig(); + integration("prints errors for broken snapshot compilation", () { + servePackages([ + packageMap("foo", "1.2.3"), + packageMap("bar", "1.2.3") + ], contents: [ + d.dir("bin", [ + d.file("hello.dart", "void main() { no closing brace"), + d.file("goodbye.dart", "void main() { no closing brace"), + ]) + ]); + + d.appDir({"foo": "1.2.3", "bar": "1.2.3"}).create(); + + // This should still have a 0 exit code, since installation succeeded even + // if precompilation didn't. + pubGet(error: allOf([ + contains("Failed to precompile foo:hello"), + contains("Failed to precompile foo:goodbye"), + contains("Failed to precompile bar:hello"), + contains("Failed to precompile bar:goodbye") + ]), exitCode: 0); + + d.dir(p.join(appPath, '.pub', 'bin'), [ + d.file('sdk-version', '0.1.2+3\n'), + d.dir('foo', [ + d.nothing('hello.dart.snapshot'), + d.nothing('goodbye.dart.snapshot') + ]), + d.dir('bar', [ + d.nothing('hello.dart.snapshot'), + d.nothing('goodbye.dart.snapshot') + ]) + ]).validate(); + }); +} diff --git a/test/snapshot/recompiles_if_the_sdk_is_out_of_date_test.dart b/test/snapshot/recompiles_if_the_sdk_is_out_of_date_test.dart new file mode 100644 index 00000000..7712798d --- /dev/null +++ b/test/snapshot/recompiles_if_the_sdk_is_out_of_date_test.dart @@ -0,0 +1,44 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library pub_tests; + +import 'package:path/path.dart' as p; +import 'package:scheduled_test/scheduled_test.dart'; +import 'package:scheduled_test/scheduled_stream.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +main() { + initConfig(); + integration("creates a snapshot for an immediate dependency's executables", + () { + servePackages([packageMap("foo", "5.6.7")], contents: [ + d.dir("bin", [d.file("hello.dart", "void main() => print('hello!');")]) + ]); + + d.appDir({"foo": "5.6.7"}).create(); + + pubGet(output: contains("Precompiled foo:hello.")); + + d.dir(p.join(appPath, '.pub', 'bin'), [ + d.file('sdk-version', '0.0.1'), + d.dir('foo', [d.file('hello.dart.snapshot', 'junk')]) + ]).create(); + + var process = pubRun(args: ['foo:hello']); + + // In the real world this would just print "hello!", but since we collect + // all output we see the precompilation messages as well. + process.stdout.expect("Precompiling executables..."); + process.stdout.expect(consumeThrough("hello!")); + process.shouldExit(); + + d.dir(p.join(appPath, '.pub', 'bin'), [ + d.file('sdk-version', '0.1.2+3'), + d.dir('foo', [d.matcherFile('hello.dart.snapshot', contains('hello!'))]) + ]).create(); + }); +} diff --git a/test/snapshot/snapshots_transformed_code_test.dart b/test/snapshot/snapshots_transformed_code_test.dart new file mode 100644 index 00000000..38018ced --- /dev/null +++ b/test/snapshot/snapshots_transformed_code_test.dart @@ -0,0 +1,61 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library pub_tests; + +import 'package:path/path.dart' as p; +import 'package:scheduled_test/scheduled_test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +const REPLACE_TRANSFORMER = """ +import 'dart:async'; + +import 'package:barback/barback.dart'; + +class ReplaceTransformer extends Transformer { + ReplaceTransformer.asPlugin(); + + String get allowedExtensions => '.dart'; + + Future apply(Transform transform) { + return transform.primaryInput.readAsString().then((contents) { + transform.addOutput(new Asset.fromString(transform.primaryInput.id, + contents.replaceAll("REPLACE ME", "hello!"))); + }); + } +} +"""; + +main() { + initConfig(); + integration("snapshots the transformed version of an executable", () { + servePackages([ + packageMap("foo", "1.2.3", {"barback": "any"}) + ..addAll({'transformers': ['foo']}) + ], contents: [ + d.dir("lib", [d.file("foo.dart", REPLACE_TRANSFORMER)]), + d.dir("bin", [ + d.file("hello.dart", """ +final message = 'REPLACE ME'; + +void main() => print(message); +"""), + ]) + ], serveBarback: true); + + d.appDir({"foo": "1.2.3"}).create(); + + pubGet(output: contains("Precompiled foo:hello.")); + + d.dir(p.join(appPath, '.pub', 'bin'), [ + d.dir('foo', [d.matcherFile('hello.dart.snapshot', contains('hello!'))]) + ]).validate(); + + var process = pubRun(args: ['foo:hello']); + process.stdout.expect("hello!"); + process.shouldExit(); + }); +} diff --git a/test/snapshot/upgrades_snapshot_for_dependency_test.dart b/test/snapshot/upgrades_snapshot_for_dependency_test.dart new file mode 100644 index 00000000..f25d4768 --- /dev/null +++ b/test/snapshot/upgrades_snapshot_for_dependency_test.dart @@ -0,0 +1,55 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library pub_tests; + +import 'package:path/path.dart' as p; +import 'package:scheduled_test/scheduled_test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +main() { + initConfig(); + integration("upgrades a snapshot when a dependency is upgraded", () { + servePackages([ + packageMap("foo", "1.2.3", {"bar": "any"}), + packageMap("bar", "1.2.3") + ], contents: [ + d.dir("lib", [d.file("bar.dart", "final message = 'hello!';")]), + d.dir("bin", [ + d.file("hello.dart", """ +import 'package:bar/bar.dart'; + +void main() => print(message); +""") + ]) + ]); + + d.appDir({"foo": "any"}).create(); + + pubGet(output: contains("Precompiled foo:hello.")); + + d.dir(p.join(appPath, '.pub', 'bin', 'foo'), [ + d.matcherFile('hello.dart.snapshot', contains('hello!')) + ]).validate(); + + servePackages([ + packageMap("foo", "1.2.3", {"bar": "any"}), + packageMap("bar", "1.2.4") + ], contents: [ + d.dir("lib", [d.file("bar.dart", "final message = 'hello 2!';")]), + ], replace: true); + + pubUpgrade(output: contains("Precompiled foo:hello.")); + + d.dir(p.join(appPath, '.pub', 'bin', 'foo'), [ + d.matcherFile('hello.dart.snapshot', contains('hello 2!')) + ]).validate(); + + var process = pubRun(args: ['foo:hello']); + process.stdout.expect("hello 2!"); + process.shouldExit(); + }); +} diff --git a/test/snapshot/upgrades_snapshot_test.dart b/test/snapshot/upgrades_snapshot_test.dart new file mode 100644 index 00000000..35316821 --- /dev/null +++ b/test/snapshot/upgrades_snapshot_test.dart @@ -0,0 +1,46 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library pub_tests; + +import 'package:path/path.dart' as p; +import 'package:scheduled_test/scheduled_test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +main() { + initConfig(); + integration("upgrades a snapshot when its package is upgraded", () { + servePackages([packageMap("foo", "1.2.3")], contents: [ + d.dir("bin", [ + d.file("hello.dart", "void main() => print('hello!');") + ]) + ]); + + d.appDir({"foo": "any"}).create(); + + pubGet(output: contains("Precompiled foo:hello.")); + + d.dir(p.join(appPath, '.pub', 'bin', 'foo'), [ + d.matcherFile('hello.dart.snapshot', contains('hello!')) + ]).validate(); + + servePackages([packageMap("foo", "1.2.4")], contents: [ + d.dir("bin", [ + d.file("hello.dart", "void main() => print('hello 2!');") + ]) + ]); + + pubUpgrade(output: contains("Precompiled foo:hello.")); + + d.dir(p.join(appPath, '.pub', 'bin', 'foo'), [ + d.matcherFile('hello.dart.snapshot', contains('hello 2!')) + ]).validate(); + + var process = pubRun(args: ['foo:hello']); + process.stdout.expect("hello 2!"); + process.shouldExit(); + }); +} diff --git a/test/test_pub.dart b/test/test_pub.dart index 54e30dec..ff5d9f9d 100644 --- a/test/test_pub.dart +++ b/test/test_pub.dart @@ -255,8 +255,11 @@ Map<String, List<Map>> _servedPackages; /// /// If [contents] is given, its contents are added to every served /// package. +/// +/// If [serveBarback] is true, the repo versions of barback and its dependencies +/// will be served as well. void servePackages(List<Map> pubspecs, {bool replace: false, - Iterable<d.Descriptor> contents}) { + Iterable<d.Descriptor> contents, bool serveBarback: false}) { if (_servedPackages == null || _servedPackageDir == null) { _servedPackages = <String, List<Map>>{}; _servedApiPackageDir = d.dir('packages', []); @@ -284,6 +287,28 @@ void servePackages(List<Map> pubspecs, {bool replace: false, versions.add(pubspec); } + var repoPackages = new Set(); + if (serveBarback) { + _addPackage(name) { + if (_servedPackages.containsKey(name)) return; + repoPackages.add(name); + + var pubspec = new Map.from(loadYaml( + readTextFile(path.join(repoRoot, 'pkg', name, 'pubspec.yaml')))); + + // Remove any SDK constraints since we don't have a valid SDK version + // while testing. + pubspec.remove('environment'); + + _servedPackages[name] = [pubspec]; + if (pubspec.containsKey('dependencies')) { + pubspec['dependencies'].keys.forEach(_addPackage); + } + } + + _addPackage('barback'); + } + _servedApiPackageDir.contents.clear(); _servedPackageDir.contents.clear(); for (var name in _servedPackages.keys) { @@ -305,6 +330,14 @@ void servePackages(List<Map> pubspecs, {bool replace: false, d.dir('versions', _servedPackages[name].map((pubspec) { var version = pubspec['version']; + if (repoPackages.contains(name)) { + return d.tar('$version.tar.gz', [ + d.file('pubspec.yaml', JSON.encode(pubspec)), + new d.DirectoryDescriptor.fromFilesystem('lib', + path.join(repoRoot, 'pkg', name, 'lib')) + ]); + } + var archiveContents = [ d.file('pubspec.yaml', JSON.encode(pubspec)), d.libDir(name, '$name $version') -- GitLab