Improve the heuristics for "pub get" is needed.
Instead of relying only on modification times, this does a more complex
compatibility check if the modification times of the pubspec, lockfile,
and .packages file aren't ordered properly. If they are up-to-date, it
updates the modification times accordingly.
This also fixes a case where "pub run" wasn't properly checking that the
dependencies were up-to-date.
Closes #1322
R=rnystrom@google.com
Review URL: https://codereview.chromium.org//1307853004 .
diff --git a/lib/src/command/deps.dart b/lib/src/command/deps.dart
index 25bfa91..32cb0cd 100644
--- a/lib/src/command/deps.dart
+++ b/lib/src/command/deps.dart
@@ -74,7 +74,7 @@
_buffer.writeln();
_buffer.writeln("$section:");
for (var name in ordered(names)) {
- var package = entrypoint.packageGraph.packages[name];
+ var package = _getPackage(name);
_buffer.write("- ${_labelPackage(package)}");
if (package.dependencies.isEmpty) {
@@ -115,7 +115,7 @@
_buffer.writeln("$name:");
for (var name in deps) {
- var package = entrypoint.packageGraph.packages[name];
+ var package = _getPackage(name);
_buffer.writeln("- ${_labelPackage(package)}");
for (var dep in package.dependencies) {
@@ -141,8 +141,7 @@
// Start with the root dependencies.
var packageTree = {};
for (var dep in entrypoint.root.immediateDependencies) {
- toWalk.add(
- new Pair(entrypoint.packageGraph.packages[dep.name], packageTree));
+ toWalk.add(new Pair(_getPackage(dep.name), packageTree));
}
// Do a breadth-first walk to the dependency graph.
@@ -163,8 +162,7 @@
map[_labelPackage(package)] = childMap;
for (var dep in package.dependencies) {
- toWalk.add(
- new Pair(entrypoint.packageGraph.packages[dep.name], childMap));
+ toWalk.add(new Pair(_getPackage(dep.name), childMap));
}
}
@@ -184,4 +182,17 @@
transitive.removeAll(root.dependencyOverrides.map((dep) => dep.name));
return transitive;
}
+
+ /// Get the package named [name], or throw a [DataError] if it's not
+ /// available.
+ ///
+ /// It's very unlikely that the lockfile won't be up-to-date with the pubspec,
+ /// but it's possible, since [Entrypoint.assertUpToDate]'s modification time
+ /// check can return a false negative. This fails gracefully if that happens.
+ Package _getPackage(String name) {
+ var package = entrypoint.packageGraph.packages[name];
+ if (package != null) return package;
+ dataError('The pubspec.yaml file has changed since the pubspec.lock file '
+ 'was generated, please run "pub get" again.');
+ }
}
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 3c2fa25..fd88b8d 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -7,7 +7,8 @@
import 'dart:async';
import 'dart:io';
-import 'package:path/path.dart' as path;
+import 'package:package_config/packages_file.dart' as packages_file;
+import 'package:path/path.dart' as p;
import 'package:barback/barback.dart';
import 'barback/asset_environment.dart';
@@ -201,13 +202,13 @@
// Just precompile the debug version of a package. We're mostly interested
// in improving speed for development iteration loops, which usually use
// debug mode.
- var depsDir = path.join('.pub', 'deps', 'debug');
+ var depsDir = p.join('.pub', 'deps', 'debug');
var dependenciesToPrecompile = packageGraph.packages.values
.where((package) {
if (package.pubspec.transformers.isEmpty) return false;
if (packageGraph.isPackageMutable(package.name)) return false;
- if (!dirExists(path.join(depsDir, package.name))) return true;
+ if (!dirExists(p.join(depsDir, package.name))) return true;
if (changed == null) return true;
/// Only recompile [package] if any of its transitive dependencies have
@@ -222,12 +223,12 @@
if (dirExists(depsDir)) {
// Delete any cached dependencies that are going to be recached.
for (var package in dependenciesToPrecompile) {
- deleteEntry(path.join(depsDir, package));
+ deleteEntry(p.join(depsDir, package));
}
// Also delete any cached dependencies that should no longer be cached.
for (var subdir in listDir(depsDir)) {
- var package = packageGraph.packages[path.basename(subdir)];
+ var package = packageGraph.packages[p.basename(subdir)];
if (package == null || package.pubspec.transformers.isEmpty ||
packageGraph.isPackageMutable(package.name)) {
deleteEntry(subdir);
@@ -257,9 +258,9 @@
await waitAndPrintErrors(assets.map((asset) async {
if (!dependenciesToPrecompile.contains(asset.id.package)) return;
- var destPath = path.join(
- depsDir, asset.id.package, path.fromUri(asset.id.path));
- ensureDir(path.dirname(destPath));
+ var destPath = p.join(
+ depsDir, asset.id.package, p.fromUri(asset.id.path));
+ ensureDir(p.dirname(destPath));
await createFileFromStream(asset.read(), destPath);
}));
@@ -271,7 +272,7 @@
// assets (issue 19491), catch and handle compilation errors on a
// per-package basis.
for (var package in dependenciesToPrecompile) {
- deleteEntry(path.join(depsDir, package));
+ deleteEntry(p.join(depsDir, package));
}
rethrow;
}
@@ -282,8 +283,8 @@
Future precompileExecutables({Iterable<String> changed}) async {
if (changed != null) changed = changed.toSet();
- var binDir = path.join('.pub', 'bin');
- var sdkVersionPath = path.join(binDir, 'sdk-version');
+ var binDir = p.join('.pub', 'bin');
+ var sdkVersionPath = p.join(binDir, 'sdk-version');
// If the existing executable was compiled with a different SDK, we need to
// recompile regardless of what changed.
@@ -297,7 +298,7 @@
for (var entry in listDir(binDir)) {
if (!dirExists(entry)) continue;
- var package = path.basename(entry);
+ var package = p.basename(entry);
if (!packageGraph.packages.containsKey(package) ||
packageGraph.isPackageMutable(package)) {
deleteEntry(entry);
@@ -337,7 +338,7 @@
});
await waitAndPrintErrors(executables.keys.map((package) async {
- var dir = path.join(binDir, package);
+ var dir = p.join(binDir, package);
cleanDir(dir);
await environment.precompileExecutables(package, dir,
executableIds: executables[package]);
@@ -373,8 +374,8 @@
// changed. Since we delete the bin directory before recompiling, we need to
// recompile all executables.
var executablesExist = executables.every((executable) =>
- fileExists(path.join('.pub', 'bin', packageName,
- "${path.url.basename(executable.path)}.snapshot")));
+ fileExists(p.join('.pub', 'bin', packageName,
+ "${p.url.basename(executable.path)}.snapshot")));
if (!executablesExist) return executables;
// Otherwise, we don't need to recompile.
@@ -396,7 +397,7 @@
return source.downloadToSystemCache(id);
}
- var packageDir = path.join(packagesDir, id.name);
+ var packageDir = p.join(packagesDir, id.name);
if (entryExists(packageDir)) deleteEntry(packageDir);
return source.get(id, packageDir);
}).then((_) => source.resolveId(id));
@@ -415,20 +416,127 @@
dataError('No .packages file found, please run "pub get" first.');
}
- var packagesModified = new File(packagesFile).lastModifiedSync();
var pubspecModified = new File(pubspecPath).lastModifiedSync();
- if (packagesModified.isBefore(pubspecModified)) {
- dataError('The pubspec.yaml file has changed since the .packages file '
- 'was generated, please run "pub get" again.');
+ var lockFileModified = new File(lockFilePath).lastModifiedSync();
+
+ var touchedLockFile = false;
+ if (lockFileModified.isBefore(pubspecModified)) {
+ if (_isLockFileUpToDate() && _arePackagesAvailable()) {
+ touchedLockFile = true;
+ touch(lockFilePath);
+ } else {
+ dataError('The pubspec.yaml file has changed since the pubspec.lock '
+ 'file was generated, please run "pub get" again.');
+ }
}
- var lockFileModified = new File(lockFilePath).lastModifiedSync();
+ var packagesModified = new File(packagesFile).lastModifiedSync();
if (packagesModified.isBefore(lockFileModified)) {
- dataError('The pubspec.lock file has changed since the .packages file '
- 'was generated, please run "pub get" again.');
+ if (_isPackagesFileUpToDate()) {
+ touch(packagesFile);
+ } else {
+ dataError('The pubspec.lock file has changed since the .packages file '
+ 'was generated, please run "pub get" again.');
+ }
+ } else if (touchedLockFile) {
+ touch(packagesFile);
}
}
+ /// Determines whether or not the lockfile is out of date with respect to the
+ /// pubspec.
+ ///
+ /// This will be `false` if the pubspec contains dependencies that are not in
+ /// the lockfile or that don't match what's in there.
+ bool _isLockFileUpToDate() {
+ return root.immediateDependencies.every((package) {
+ var locked = lockFile.packages[package.name];
+ if (locked == null) return false;
+
+ if (package.source != locked.source) return false;
+
+ if (!package.constraint.allows(locked.version)) return false;
+
+ var source = cache.sources[package.source];
+ if (source == null) return false;
+
+ return source.descriptionsEqual(package.description, locked.description);
+ });
+ }
+
+ /// Determines whether all of the packages in the lockfile are already
+ /// installed and available.
+ ///
+ /// Note: this assumes [_isLockFileUpToDate] has already been called and
+ /// returned `true`.
+ bool _arePackagesAvailable() {
+ return lockFile.packages.values.every((package) {
+ var source = cache.sources[package.source];
+
+ // This should only be called after [_isLockFileUpToDate] has returned
+ // `true`, which ensures all of the sources in the lock file are valid.
+ assert(source != null);
+
+ // We only care about cached sources. Uncached sources aren't "installed".
+ // If one of those is missing, we want to show the user the file not
+ // found error later since installing won't accomplish anything.
+ if (source is! CachedSource) return true;
+
+ // Get the directory.
+ var dir = source.getDirectory(package);
+ // See if the directory is there and looks like a package.
+ return dirExists(dir) && fileExists(p.join(dir, "pubspec.yaml"));
+ });
+ }
+
+ /// Determines whether or not the `.packages` file is out of date with respect
+ /// to the lockfile.
+ ///
+ /// This will be `false` if the packages file contains dependencies that are
+ /// not in the lockfile or that don't match what's in there.
+ bool _isPackagesFileUpToDate() {
+ var packages = packages_file.parse(
+ new File(packagesFile).readAsBytesSync(),
+ p.toUri(packagesFile));
+
+ return lockFile.packages.values.every((lockFileId) {
+ var source = cache.sources[lockFileId.source];
+
+ // It's very unlikely that the lockfile is invalid here, but it's not
+ // impossible—for example, the user may have a very old application
+ // package with a checked-in lockfile that's newer than the pubspec, but
+ // that contains sdk dependencies.
+ if (source == null) return false;
+
+ var packagesFileUri = packages[lockFileId.name];
+ if (packagesFileUri == null) return false;
+
+ // Pub only generates "file:" and relative URIs.
+ if (packagesFileUri.scheme != 'file' &&
+ packagesFileUri.scheme.isNotEmpty) {
+ return false;
+ }
+
+ // Get the dirname of the .packages path, since it's pointing to lib/.
+ var packagesFilePath = p.dirname(
+ p.join(root.dir, p.fromUri(packagesFileUri)));
+ var lockFilePath = p.join(root.dir, source.getDirectory(lockFileId));
+
+ // For cached sources, make sure the directory exists and looks like a
+ // package. This is also done by [_arePackagesAvailable] but that may not
+ // be run if the lockfile is newer than the pubspec.
+ if (source is CachedSource &&
+ !dirExists(packagesFilePath) ||
+ !fileExists(p.join(packagesFilePath, "pubspec.yaml"))) {
+ return false;
+ }
+
+ // Make sure that the packages file agrees with the lock file about the
+ // path to the package.
+ return p.normalize(packagesFilePath) == p.normalize(lockFilePath);
+ });
+ }
+
/// Saves a list of concrete package versions to the `pubspec.lock` file.
void _saveLockFile(List<PackageId> packageIds) {
_lockFile = new LockFile(packageIds, cache.sources);
@@ -439,7 +547,7 @@
/// Creates a self-referential symlink in the `packages` directory that allows
/// a package to import its own files using `package:`.
void _linkSelf() {
- var linkPath = path.join(packagesDir, root.name);
+ var linkPath = p.join(packagesDir, root.name);
// Create the symlink if it doesn't exist.
if (entryExists(linkPath)) return;
ensureDir(packagesDir);
@@ -482,7 +590,7 @@
/// files and `package` files.
List<String> _listDirWithoutPackages(dir) {
return flatten(listDir(dir).map((file) {
- if (path.basename(file) == 'packages') return [];
+ if (p.basename(file) == 'packages') return [];
if (!dirExists(file)) return [];
var fileAndSubfiles = [file];
fileAndSubfiles.addAll(_listDirWithoutPackages(file));
@@ -495,7 +603,7 @@
///
/// Otherwise, deletes a "packages" directories in [dir] if one exists.
void _linkOrDeleteSecondaryPackageDir(String dir) {
- var symlink = path.join(dir, 'packages');
+ var symlink = p.join(dir, 'packages');
if (entryExists(symlink)) deleteEntry(symlink);
if (_packageSymlinks) createSymlink(packagesDir, symlink, relative: true);
}
diff --git a/lib/src/executable.dart b/lib/src/executable.dart
index 65b276e..bd21b4d 100644
--- a/lib/src/executable.dart
+++ b/lib/src/executable.dart
@@ -82,6 +82,10 @@
// default mode for them to run. We can't run them in a different mode
// using the snapshot.
mode == BarbackMode.RELEASE) {
+ // Since we don't access the package graph, this doesn't happen
+ // automatically.
+ entrypoint.assertUpToDate();
+
return _runCachedExecutable(entrypoint, localSnapshotPath, args,
checked: checked);
}
diff --git a/lib/src/io.dart b/lib/src/io.dart
index 8b7fc17..952751c 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -831,6 +831,15 @@
environment: environment);
}
+/// Updates [path]'s modification time.
+void touch(String path) {
+ var file = new File(path).openSync(mode: FileMode.APPEND);
+ var originalLength = file.lengthSync();
+ file.writeByteSync(0);
+ file.truncateSync(originalLength);
+ file.closeSync();
+}
+
/// Creates a temporary directory and passes its path to [fn].
///
/// Once the [Future] returned by [fn] completes, the temporary directory and
diff --git a/test/must_pub_get_test.dart b/test/must_pub_get_test.dart
index 5a10d82..981d9ff 100644
--- a/test/must_pub_get_test.dart
+++ b/test/must_pub_get_test.dart
@@ -4,39 +4,42 @@
import 'dart:async';
import 'dart:convert';
+import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:pub/src/exit_codes.dart' as exit_codes;
import 'package:pub/src/io.dart';
+import 'package:scheduled_test/scheduled_stream.dart';
import 'package:scheduled_test/scheduled_test.dart';
import 'descriptor.dart' as d;
import 'test_pub.dart';
main() {
- group("requires the user to run pub get first if", () {
- setUp(() {
- d.dir(appPath, [
- d.appPubspec(),
- d.dir("web", []),
- d.dir("bin", [
- d.file("script.dart", "main() => print('hello!');")
- ])
- ]).create();
-
- pubGet();
-
- // Delay a bit to make sure the modification times are noticeably
- // different. 1s seems to be the finest granularity that dart:io reports.
- schedule(() => new Future.delayed(new Duration(seconds: 1)));
+ setUp(() {
+ servePackages((builder) {
+ builder.serve("foo", "1.0.0");
+ builder.serve("foo", "2.0.0");
});
+ d.dir(appPath, [
+ d.appPubspec(),
+ d.dir("web", []),
+ d.dir("bin", [
+ d.file("script.dart", "main() => print('hello!');")
+ ])
+ ]).create();
+
+ pubGet();
+ });
+
+ group("requires the user to run pub get first if", () {
group("there's no lockfile", () {
setUp(() {
schedule(() => deleteEntry(p.join(sandboxDir, "myapp/pubspec.lock")));
});
- _forEveryCommand(
+ _requiresPubGet(
'No pubspec.lock file found, please run "pub get" first.');
});
@@ -45,55 +48,335 @@
schedule(() => deleteEntry(p.join(sandboxDir, "myapp/.packages")));
});
- _forEveryCommand('No .packages file found, please run "pub get" first.');
+ _requiresPubGet('No .packages file found, please run "pub get" first.');
});
- group("the pubspec is newer than the package spec", () {
+ group("the pubspec has a new dependency", () {
setUp(() {
- schedule(() => _touch("pubspec.yaml"));
+ d.dir("foo", [
+ d.libPubspec("foo", "1.0.0")
+ ]).create();
+
+ d.dir(appPath, [
+ d.appPubspec({"foo": {"path": "../foo"}})
+ ]).create();
+
+ // Ensure that the pubspec looks newer than the lockfile.
+ _touch("pubspec.yaml");
});
- _forEveryCommand('The pubspec.yaml file has changed since the .packages '
+ _requiresPubGet('The pubspec.yaml file has changed since the '
+ 'pubspec.lock file was generated, please run "pub get" again.');
+ });
+
+ group("the lockfile has a dependency from the wrong source", () {
+ setUp(() {
+ d.dir(appPath, [
+ d.appPubspec({"foo": "1.0.0"})
+ ]).create();
+
+ pubGet();
+
+ createLockFile(appPath, sandbox: ["foo"]);
+
+ // Ensure that the pubspec looks newer than the lockfile.
+ _touch("pubspec.yaml");
+ });
+
+ _requiresPubGet('The pubspec.yaml file has changed since the '
+ 'pubspec.lock file was generated, please run "pub get" again.');
+ });
+
+ group("the lockfile has a dependency from an unknown source", () {
+ setUp(() {
+ d.dir(appPath, [
+ d.appPubspec({"foo": "1.0.0"})
+ ]).create();
+
+ pubGet();
+
+ d.dir(appPath, [
+ d.file("pubspec.lock", yaml({
+ "packages": {
+ "foo": {
+ "description": "foo",
+ "version": "1.0.0",
+ "source": "sdk"
+ }
+ }
+ }))
+ ]).create();
+
+ // Ensure that the pubspec looks newer than the lockfile.
+ _touch("pubspec.yaml");
+ });
+
+ _requiresPubGet('The pubspec.yaml file has changed since the '
+ 'pubspec.lock file was generated, please run "pub get" again.');
+ });
+
+ group("the lockfile has a dependency with the wrong description", () {
+ setUp(() {
+ d.dir("bar", [
+ d.libPubspec("foo", "1.0.0")
+ ]).create();
+
+ d.dir(appPath, [
+ d.appPubspec({"foo": {"path": "../bar"}})
+ ]).create();
+
+ pubGet();
+
+ createLockFile(appPath, sandbox: ["foo"]);
+
+ // Ensure that the pubspec looks newer than the lockfile.
+ _touch("pubspec.yaml");
+ });
+
+ _requiresPubGet('The pubspec.yaml file has changed since the '
+ 'pubspec.lock file was generated, please run "pub get" again.');
+ });
+
+ group("the pubspec has an incompatible version of a dependency", () {
+ setUp(() {
+ d.dir(appPath, [
+ d.appPubspec({"foo": "1.0.0"})
+ ]).create();
+
+ pubGet();
+
+ d.dir(appPath, [
+ d.appPubspec({"foo": "2.0.0"})
+ ]).create();
+
+ // Ensure that the pubspec looks newer than the lockfile.
+ _touch("pubspec.yaml");
+ });
+
+ _requiresPubGet('The pubspec.yaml file has changed since the '
+ 'pubspec.lock file was generated, please run "pub get" again.');
+ });
+
+ group("the lockfile is pointing to an unavailable package with a newer "
+ "pubspec", () {
+ setUp(() {
+ d.dir(appPath, [
+ d.appPubspec({"foo": "1.0.0"})
+ ]).create();
+
+ pubGet();
+
+ schedule(() => deleteEntry(p.join(sandboxDir, cachePath)));
+
+ // Ensure that the pubspec looks newer than the lockfile.
+ _touch("pubspec.yaml");
+ });
+
+ _requiresPubGet('The pubspec.yaml file has changed since the '
+ 'pubspec.lock file was generated, please run "pub get" again.');
+ });
+
+ group("the lockfile is pointing to an unavailable package with an older "
+ ".packages", () {
+ setUp(() {
+ d.dir(appPath, [
+ d.appPubspec({"foo": "1.0.0"})
+ ]).create();
+
+ pubGet();
+
+ schedule(() => deleteEntry(p.join(sandboxDir, cachePath)));
+
+ // Ensure that the lockfile looks newer than the .packages file.
+ _touch("pubspec.lock");
+ });
+
+ _requiresPubGet('The pubspec.lock file has changed since the .packages '
'file was generated, please run "pub get" again.');
});
- group("the lockfile is newer than the package spec", () {
+ group("the lockfile has a package that the .packages file doesn't", () {
setUp(() {
- schedule(() => _touch("pubspec.lock"));
+ d.dir("foo", [
+ d.libPubspec("foo", "1.0.0")
+ ]).create();
+
+ d.dir(appPath, [
+ d.appPubspec({"foo": {"path": "../foo"}})
+ ]).create();
+
+ pubGet();
+
+ createPackagesFile(appPath);
+
+ // Ensure that the pubspec looks newer than the lockfile.
+ _touch("pubspec.lock");
});
- _forEveryCommand('The pubspec.lock file has changed since the .packages '
+ _requiresPubGet('The pubspec.lock file has changed since the .packages '
'file was generated, please run "pub get" again.');
});
+
+ group("the .packages file has a package with a non-file URI", () {
+ setUp(() {
+ d.dir("foo", [
+ d.libPubspec("foo", "1.0.0")
+ ]).create();
+
+ d.dir(appPath, [
+ d.appPubspec({"foo": {"path": "../foo"}})
+ ]).create();
+
+ pubGet();
+
+ d.dir(appPath, [
+ d.file(".packages", """
+myapp:lib
+foo:http://example.com/
+""")
+ ]).create();
+
+ // Ensure that the pubspec looks newer than the lockfile.
+ _touch("pubspec.lock");
+ });
+
+ _requiresPubGet('The pubspec.lock file has changed since the .packages '
+ 'file was generated, please run "pub get" again.');
+ });
+
+ group("the .packages file points to the wrong place", () {
+ setUp(() {
+ d.dir("bar", [
+ d.libPubspec("foo", "1.0.0")
+ ]).create();
+
+ d.dir(appPath, [
+ d.appPubspec({"foo": {"path": "../bar"}})
+ ]).create();
+
+ pubGet();
+
+ createPackagesFile(appPath, sandbox: ["foo"]);
+
+ // Ensure that the pubspec looks newer than the lockfile.
+ _touch("pubspec.lock");
+ });
+
+ _requiresPubGet('The pubspec.lock file has changed since the .packages '
+ 'file was generated, please run "pub get" again.');
+ });
+ });
+
+ group("doesn't require the user to run pub get first if", () {
+ group("the pubspec is older than the lockfile which is older than the "
+ "packages file, even if the contents are wrong", () {
+ setUp(() {
+ d.dir(appPath, [
+ d.appPubspec({"foo": "1.0.0"})
+ ]).create();
+
+ _touch("pubspec.lock");
+ _touch(".packages");
+ });
+
+ _runsSuccessfully(runDeps: false);
+ });
+
+ group("the pubspec is newer than the lockfile, but they're up-to-date", () {
+ setUp(() {
+ d.dir(appPath, [
+ d.appPubspec({"foo": "1.0.0"})
+ ]).create();
+
+ pubGet();
+
+ _touch("pubspec.yaml");
+ });
+
+ _runsSuccessfully();
+ });
+
+ group("the lockfile is newer than .packages, but they're up-to-date", () {
+ setUp(() {
+ d.dir(appPath, [
+ d.appPubspec({"foo": "1.0.0"})
+ ]).create();
+
+ pubGet();
+
+ _touch("pubspec.lock");
+ });
+
+ _runsSuccessfully();
+ });
});
}
/// Runs every command that care about the world being up-to-date, and asserts
/// that it prints [message] as part of its error.
-void _forEveryCommand(String message) {
+void _requiresPubGet(String message) {
for (var command in ["build", "serve", "run", "deps"]) {
integration("for pub $command", () {
var args = [command];
if (command == "run") args.add("script");
- var output;
- var error;
- if (command == "list-package-dirs") {
- output = contains(JSON.encode(message));
- } else {
- error = contains(message);
- }
-
schedulePub(
args: args,
- output: output,
- error: error,
+ error: contains(message),
exitCode: exit_codes.DATA);
});
}
}
+/// Ensures that pub doesn't require "pub get" for the current package.
+///
+/// If [runDeps] is false, `pub deps` isn't included in the test. This is
+/// sometimes not desirable, since it uses slightly stronger checks for pubspec
+/// and lockfile consistency.
+void _runsSuccessfully({bool runDeps: true}) {
+ var commands = ["build", "serve", "run"];
+ if (runDeps) commands.add("deps");
+
+ for (var command in commands) {
+ integration("for pub $command", () {
+ var args = [command];
+ if (command == "run") args.add("bin/script.dart");
+ if (command == "serve") ;
+
+ if (command != "serve") {
+ schedulePub(args: args);
+ } else {
+ var pub = startPub(args: ["serve", "--port=0"]);
+ pub.stdout.expect(consumeThrough(startsWith("Serving myapp web")));
+ pub.kill();
+ }
+
+ schedule(() {
+ // If pub determines that everything is up-to-date, it should set the
+ // mtimes to indicate that.
+ var pubspecModified = new File(p.join(sandboxDir, "myapp/pubspec.yaml"))
+ .lastModifiedSync();
+ var lockFileModified =
+ new File(p.join(sandboxDir, "myapp/pubspec.lock"))
+ .lastModifiedSync();
+ var packagesModified = new File(p.join(sandboxDir, "myapp/.packages"))
+ .lastModifiedSync();
+
+ expect(!pubspecModified.isAfter(lockFileModified), isTrue);
+ expect(!lockFileModified.isAfter(packagesModified), isTrue);
+ }, "testing last-modified times");
+ });
+ }
+}
+
+/// Schedules a non-semantic modification to [path].
void _touch(String path) {
- path = p.join(sandboxDir, "myapp", path);
- writeTextFile(path, readTextFile(path) + " ");
+ schedule(() async {
+ // Delay a bit to make sure the modification times are noticeably different.
+ // 1s seems to be the finest granularity that dart:io reports.
+ await new Future.delayed(new Duration(seconds: 1));
+
+ path = p.join(sandboxDir, "myapp", path);
+ touch(path);
+ }, "touching $path");
}
diff --git a/test/test_pub.dart b/test/test_pub.dart
index 2c7af92..5427625 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -714,16 +714,35 @@
/// hosted packages.
void createLockFile(String package, {Iterable<String> sandbox,
Iterable<String> pkg, Map<String, String> hosted}) {
- var cache = new SystemCache.withSources(
- rootDir: p.join(sandboxDir, cachePath));
+ schedule(() async {
+ var cache = new SystemCache.withSources(
+ rootDir: p.join(sandboxDir, cachePath));
- var lockFile = _createLockFile(cache.sources,
- sandbox: sandbox, pkg: pkg, hosted: hosted);
+ var lockFile = _createLockFile(cache.sources,
+ sandbox: sandbox, pkg: pkg, hosted: hosted);
- d.dir(package, [
- d.file('pubspec.lock', lockFile.serialize(null)),
- d.file('.packages', lockFile.packagesFile(package))
- ]).create();
+ await d.dir(package, [
+ d.file('pubspec.lock', lockFile.serialize(null)),
+ d.file('.packages', lockFile.packagesFile(package))
+ ]).create();
+ }, "creating lockfile for $package");
+}
+
+/// Like [createLockFile], but creates only a `.packages` file without a
+/// lockfile.
+void createPackagesFile(String package, {Iterable<String> sandbox,
+ Iterable<String> pkg, Map<String, String> hosted}) {
+ schedule(() async {
+ var cache = new SystemCache.withSources(
+ rootDir: p.join(sandboxDir, cachePath));
+
+ var lockFile = _createLockFile(cache.sources,
+ sandbox: sandbox, pkg: pkg, hosted: hosted);
+
+ await d.dir(package, [
+ d.file('.packages', lockFile.packagesFile(package))
+ ]).create();
+ }, "creating .packages for $package");
}
/// Creates a lock file for [package] without running `pub get`.