Index: utils/tests/pub/test_pub.dart |
diff --git a/utils/tests/pub/test_pub.dart b/utils/tests/pub/test_pub.dart |
index bf573fd9d364eb01dad5eef02a78f3e27abd4bbe..afff31e63515c8d95b9cf73892a60d81f1e02da7 100644 |
--- a/utils/tests/pub/test_pub.dart |
+++ b/utils/tests/pub/test_pub.dart |
@@ -19,8 +19,11 @@ import 'dart:utf'; |
import '../../../pkg/http/lib/testing.dart'; |
import '../../../pkg/oauth2/lib/oauth2.dart' as oauth2; |
import '../../../pkg/pathos/lib/path.dart' as path; |
-import '../../../pkg/unittest/lib/unittest.dart'; |
+import '../../../pkg/scheduled_test/lib/scheduled_process.dart'; |
+import '../../../pkg/scheduled_test/lib/scheduled_server.dart'; |
+import '../../../pkg/scheduled_test/lib/scheduled_test.dart'; |
import '../../../pkg/yaml/lib/yaml.dart'; |
+ |
import '../../lib/file_system.dart' as fs; |
import '../../pub/entrypoint.dart'; |
// TODO(rnystrom): Using "gitlib" as the prefix here is ugly, but "git" collides |
@@ -37,6 +40,7 @@ import '../../pub/system_cache.dart'; |
import '../../pub/utils.dart'; |
import '../../pub/validator.dart'; |
import 'command_line_config.dart'; |
+import 'descriptor.dart' as d; |
/// This should be called at the top of a test file to set up an appropriate |
/// test configuration for the machine running the tests. |
@@ -47,33 +51,6 @@ initConfig() { |
} |
} |
-/// Creates a new [FileDescriptor] with [name] and [contents]. |
-FileDescriptor file(Pattern name, String contents) => |
- new FileDescriptor(name, contents); |
- |
-/// Creates a new [FileDescriptor] with [name] and [contents]. |
-FileDescriptor binaryFile(Pattern name, List<int> contents) => |
- new FileDescriptor.bytes(name, contents); |
- |
-/// Creates a new [DirectoryDescriptor] with [name] and [contents]. |
-DirectoryDescriptor dir(Pattern name, [List<Descriptor> contents]) => |
- new DirectoryDescriptor(name, contents); |
- |
-/// Creates a new [FutureDescriptor] wrapping [future]. |
-FutureDescriptor async(Future<Descriptor> future) => |
- new FutureDescriptor(future); |
- |
-/// Creates a new [GitRepoDescriptor] with [name] and [contents]. |
-GitRepoDescriptor git(Pattern name, [List<Descriptor> contents]) => |
- new GitRepoDescriptor(name, contents); |
- |
-/// Creates a new [TarFileDescriptor] with [name] and [contents]. |
-TarFileDescriptor tar(Pattern name, [List<Descriptor> contents]) => |
- new TarFileDescriptor(name, contents); |
- |
-/// Creates a new [NothingDescriptor] with [name]. |
-NothingDescriptor nothing(String name) => new NothingDescriptor(name); |
- |
/// The current [HttpServer] created using [serve]. |
var _server; |
@@ -84,9 +61,9 @@ Completer<int> _portCompleterCache; |
Completer<int> get _portCompleter { |
if (_portCompleterCache != null) return _portCompleterCache; |
_portCompleterCache = new Completer<int>(); |
- _scheduleCleanup((_) { |
+ currentSchedule.onComplete.schedule(() { |
_portCompleterCache = null; |
- }); |
+ }, 'clearing the port completer'); |
return _portCompleterCache; |
} |
@@ -97,45 +74,43 @@ Future<int> get port => _portCompleter.future; |
/// exist only for the duration of the pub run. |
/// |
/// Subsequent calls to [serve] will replace the previous server. |
-void serve([List<Descriptor> contents]) { |
- var baseDir = dir("serve-dir", contents); |
+void serve([List<d.Descriptor> contents]) { |
+ var baseDir = d.dir("serve-dir", contents); |
- _schedule((_) { |
+ schedule(() { |
return _closeServer().then((_) { |
return SafeHttpServer.bind("127.0.0.1", 0).then((server) { |
_server = server; |
server.listen((request) { |
var response = request.response; |
- var path = request.uri.path.replaceFirst("/", "").split("/"); |
- response.persistentConnection = false; |
- var stream; |
try { |
- stream = baseDir.load(path); |
+ var path = request.uri.path.replaceFirst("/", ""); |
+ response.persistentConnection = false; |
+ var stream = baseDir.load(path); |
+ |
+ new ByteStream(stream).toBytes().then((data) { |
+ response.statusCode = 200; |
+ response.contentLength = data.length; |
+ response.writeBytes(data); |
+ response.close(); |
+ }).catchError((e) { |
+ response.statusCode = 404; |
+ response.contentLength = 0; |
+ response.close(); |
+ }); |
} catch (e) { |
- response.statusCode = 404; |
- response.contentLength = 0; |
+ currentSchedule.signalError(e); |
+ response.statusCode = 500; |
response.close(); |
return; |
} |
- |
- stream.toBytes().then((data) { |
- response.statusCode = 200; |
- response.contentLength = data.length; |
- response.writeBytes(data); |
- response.close(); |
- }).catchError((e) { |
- print("Exception while handling ${request.uri}: $e"); |
- response.statusCode = 500; |
- response.reasonPhrase = e.message; |
- response.close(); |
- }); |
}); |
_portCompleter.complete(_server.port); |
- _scheduleCleanup((_) => _closeServer()); |
+ currentSchedule.onComplete.schedule(_closeServer); |
return null; |
}); |
}); |
- }); |
+ }, 'starting a server serving:\n${baseDir.describe()}'); |
} |
/// Closes [_server]. Returns a [Future] that will complete after the [_server] |
@@ -151,10 +126,10 @@ Future _closeServer() { |
return sleep(10); |
} |
-/// The [DirectoryDescriptor] describing the server layout of packages that are |
-/// being served via [servePackages]. This is `null` if [servePackages] has not |
-/// yet been called for this test. |
-DirectoryDescriptor _servedPackageDir; |
+/// The [d.DirectoryDescriptor] describing the server layout of packages that |
+/// are being served via [servePackages]. This is `null` if [servePackages] has |
+/// not yet been called for this test. |
+d.DirectoryDescriptor _servedPackageDir; |
/// A map from package names to version numbers to YAML-serialized pubspecs for |
/// those packages. This represents the packages currently being served by |
@@ -171,17 +146,17 @@ Map<String, Map<String, String>> _servedPackages; |
void servePackages(List<Map> pubspecs) { |
if (_servedPackages == null || _servedPackageDir == null) { |
_servedPackages = <String, Map<String, String>>{}; |
- _servedPackageDir = dir('packages', []); |
+ _servedPackageDir = d.dir('packages', []); |
serve([_servedPackageDir]); |
- _scheduleCleanup((_) { |
+ currentSchedule.onComplete.schedule(() { |
_servedPackages = null; |
_servedPackageDir = null; |
- }); |
+ }, 'cleaning up served packages'); |
} |
- _schedule((_) { |
- return _awaitObject(pubspecs).then((resolvedPubspecs) { |
+ schedule(() { |
+ return awaitObject(pubspecs).then((resolvedPubspecs) { |
for (var spec in resolvedPubspecs) { |
var name = spec['name']; |
var version = spec['version']; |
@@ -194,15 +169,14 @@ void servePackages(List<Map> pubspecs) { |
for (var name in _servedPackages.keys) { |
var versions = _servedPackages[name].keys.toList(); |
_servedPackageDir.contents.addAll([ |
- file('$name.json', |
- json.stringify({'versions': versions})), |
- dir(name, [ |
- dir('versions', flatten(versions.map((version) { |
+ d.file('$name.json', json.stringify({'versions': versions})), |
+ d.dir(name, [ |
+ d.dir('versions', flatten(versions.map((version) { |
return [ |
- file('$version.yaml', _servedPackages[name][version]), |
- tar('$version.tar.gz', [ |
- file('pubspec.yaml', _servedPackages[name][version]), |
- libDir(name, '$name $version') |
+ d.file('$version.yaml', _servedPackages[name][version]), |
+ d.tar('$version.tar.gz', [ |
+ d.file('pubspec.yaml', _servedPackages[name][version]), |
+ d.libDir(name, '$name $version') |
]) |
]; |
}))) |
@@ -210,237 +184,12 @@ void servePackages(List<Map> pubspecs) { |
]); |
} |
}); |
- }); |
+ }, 'initializing the package server'); |
} |
/// Converts [value] into a YAML string. |
String yaml(value) => json.stringify(value); |
-/// Describes a package that passes all validation. |
-Descriptor get normalPackage => dir(appPath, [ |
- libPubspec("test_pkg", "1.0.0"), |
- file("LICENSE", "Eh, do what you want."), |
- dir("lib", [ |
- file("test_pkg.dart", "int i = 1;") |
- ]) |
-]); |
- |
-/// Describes a file named `pubspec.yaml` with the given YAML-serialized |
-/// [contents], which should be a serializable object. |
-/// |
-/// [contents] may contain [Future]s that resolve to serializable objects, |
-/// which may in turn contain [Future]s recursively. |
-Descriptor pubspec(Map contents) { |
- return async(_awaitObject(contents).then((resolvedContents) => |
- file("pubspec.yaml", yaml(resolvedContents)))); |
-} |
- |
-/// Describes a file named `pubspec.yaml` for an application package with the |
-/// given [dependencies]. |
-Descriptor appPubspec(List dependencies) { |
- return pubspec({ |
- "name": "myapp", |
- "dependencies": _dependencyListToMap(dependencies) |
- }); |
-} |
- |
-/// Describes a file named `pubspec.yaml` for a library package with the given |
-/// [name], [version], and [deps]. If "sdk" is given, then it adds an SDK |
-/// constraint on that version. |
-Descriptor libPubspec(String name, String version, {List deps, String sdk}) { |
- var map = package(name, version, deps); |
- |
- if (sdk != null) { |
- map["environment"] = { |
- "sdk": sdk |
- }; |
- } |
- |
- return pubspec(map); |
-} |
- |
-/// Describes a directory named `lib` containing a single dart file named |
-/// `<name>.dart` that contains a line of Dart code. |
-Descriptor libDir(String name, [String code]) { |
- // Default to printing the name if no other code was given. |
- if (code == null) { |
- code = name; |
- } |
- |
- return dir("lib", [ |
- file("$name.dart", 'main() => "$code";') |
- ]); |
-} |
- |
-/// Describes a map representing a library package with the given [name], |
-/// [version], and [dependencies]. |
-Map package(String name, String version, [List dependencies]) { |
- var package = { |
- "name": name, |
- "version": version, |
- "author": "Nathan Weizenbaum <nweiz@google.com>", |
- "homepage": "http://pub.dartlang.org", |
- "description": "A package, I guess." |
- }; |
- if (dependencies != null) { |
- package["dependencies"] = _dependencyListToMap(dependencies); |
- } |
- return package; |
-} |
- |
-/// Describes a map representing a dependency on a package in the package |
-/// repository. |
-Map dependency(String name, [String versionConstraint]) { |
- var url = port.then((p) => "http://localhost:$p"); |
- var dependency = {"hosted": {"name": name, "url": url}}; |
- if (versionConstraint != null) dependency["version"] = versionConstraint; |
- return dependency; |
-} |
- |
-/// Describes a directory for a package installed from the mock package server. |
-/// This directory is of the form found in the global package cache. |
-DirectoryDescriptor packageCacheDir(String name, String version) { |
- return dir("$name-$version", [ |
- libDir(name, '$name $version') |
- ]); |
-} |
- |
-/// Describes a directory for a Git package. This directory is of the form |
-/// found in the revision cache of the global package cache. |
-DirectoryDescriptor gitPackageRevisionCacheDir(String name, [int modifier]) { |
- var value = name; |
- if (modifier != null) value = "$name $modifier"; |
- return dir(new RegExp("$name${r'-[a-f0-9]+'}"), [ |
- libDir(name, value) |
- ]); |
-} |
- |
-/// Describes a directory for a Git package. This directory is of the form |
-/// found in the repo cache of the global package cache. |
-DirectoryDescriptor gitPackageRepoCacheDir(String name) { |
- return dir(new RegExp("$name${r'-[a-f0-9]+'}"), [ |
- dir('hooks'), |
- dir('info'), |
- dir('objects'), |
- dir('refs') |
- ]); |
-} |
- |
-/// Describes the `packages/` directory containing all the given [packages], |
-/// which should be name/version pairs. The packages will be validated against |
-/// the format produced by the mock package server. |
-/// |
-/// A package with a null version should not be installed. |
-DirectoryDescriptor packagesDir(Map<String, String> packages) { |
- var contents = <Descriptor>[]; |
- packages.forEach((name, version) { |
- if (version == null) { |
- contents.add(nothing(name)); |
- } else { |
- contents.add(dir(name, [ |
- file("$name.dart", 'main() => "$name $version";') |
- ])); |
- } |
- }); |
- return dir(packagesPath, contents); |
-} |
- |
-/// Describes the global package cache directory containing all the given |
-/// [packages], which should be name/version pairs. The packages will be |
-/// validated against the format produced by the mock package server. |
-/// |
-/// A package's value may also be a list of versions, in which case all |
-/// versions are expected to be installed. |
-DirectoryDescriptor cacheDir(Map packages) { |
- var contents = <Descriptor>[]; |
- packages.forEach((name, versions) { |
- if (versions is! List) versions = [versions]; |
- for (var version in versions) { |
- contents.add(packageCacheDir(name, version)); |
- } |
- }); |
- return dir(cachePath, [ |
- dir('hosted', [ |
- async(port.then((p) => dir('localhost%58$p', contents))) |
- ]) |
- ]); |
-} |
- |
-/// Describes the file in the system cache that contains the client's OAuth2 |
-/// credentials. The URL "/token" on [server] will be used as the token |
-/// endpoint for refreshing the access token. |
-Descriptor credentialsFile( |
- ScheduledServer server, |
- String accessToken, |
- {String refreshToken, |
- DateTime expiration}) { |
- return async(server.url.then((url) { |
- return dir(cachePath, [ |
- file('credentials.json', new oauth2.Credentials( |
- accessToken, |
- refreshToken, |
- url.resolve('/token'), |
- ['https://www.googleapis.com/auth/userinfo.email'], |
- expiration).toJson()) |
- ]); |
- })); |
-} |
- |
-/// Describes the application directory, containing only a pubspec specifying |
-/// the given [dependencies]. |
-DirectoryDescriptor appDir(List dependencies) => |
- dir(appPath, [appPubspec(dependencies)]); |
- |
-/// Converts a list of dependencies as passed to [package] into a hash as used |
-/// in a pubspec. |
-Future<Map> _dependencyListToMap(List<Map> dependencies) { |
- return _awaitObject(dependencies).then((resolvedDependencies) { |
- var result = <String, Map>{}; |
- for (var dependency in resolvedDependencies) { |
- var keys = dependency.keys.where((key) => key != "version"); |
- var sourceName = only(keys); |
- var source; |
- switch (sourceName) { |
- case "git": |
- source = new GitSource(); |
- break; |
- case "hosted": |
- source = new HostedSource(); |
- break; |
- case "path": |
- source = new PathSource(); |
- break; |
- default: |
- throw 'Unknown source "$sourceName"'; |
- } |
- |
- result[_packageName(sourceName, dependency[sourceName])] = dependency; |
- } |
- return result; |
- }); |
-} |
- |
-/// Return the name for the package described by [description] and from |
-/// [sourceName]. |
-String _packageName(String sourceName, description) { |
- switch (sourceName) { |
- case "git": |
- var url = description is String ? description : description['url']; |
- // TODO(rnystrom): Using path.basename on a URL is hacky. If we add URL |
- // support to pkg/pathos, should use an explicit builder for that. |
- return path.basename(url.replaceFirst(new RegExp(r"(\.git)?/?$"), "")); |
- case "hosted": |
- if (description is String) return description; |
- return description['name']; |
- case "path": |
- return path.basename(description); |
- case "sdk": |
- return description; |
- default: |
- return description; |
- } |
-} |
- |
/// The full path to the created sandbox directory for an integration test. |
String get sandboxDir => _sandboxDir; |
String _sandboxDir; |
@@ -461,21 +210,6 @@ final String appPath = "myapp"; |
/// to the sandbox directory. |
final String packagesPath = "$appPath/packages"; |
-/// The type for callbacks that will be fired during [schedulePub]. Takes the |
-/// sandbox directory as a parameter. |
-typedef Future _ScheduledEvent(String parentDir); |
- |
-/// The list of events that are scheduled to run as part of the test case. |
-Queue<_ScheduledEvent> _scheduled; |
- |
-/// The list of events that are scheduled to run after the test case, even if |
-/// it failed. |
-Queue<_ScheduledEvent> _scheduledCleanup; |
- |
-/// The list of events that are scheduled to run after the test case only if it |
-/// failed. |
-Queue<_ScheduledEvent> _scheduledOnException; |
- |
/// Set to true when the current batch of scheduled events should be aborted. |
bool _abortScheduled = false; |
@@ -495,47 +229,17 @@ void solo_integration(String description, void body()) => |
void _integration(String description, void body(), [Function testFn]) { |
testFn(description, () { |
// Ensure the SDK version is always available. |
- dir(sdkPath, [ |
- file('version', '0.1.2.3') |
- ]).scheduleCreate(); |
+ d.dir(sdkPath, [ |
+ d.file('version', '0.1.2.3') |
+ ]).create(); |
_sandboxDir = createTempDir(); |
+ d.defaultRoot = sandboxDir; |
+ currentSchedule.onComplete.schedule(() => deleteDir(_sandboxDir), |
+ 'deleting the sandbox directory'); |
// Schedule the test. |
body(); |
- |
- // Run all of the scheduled tasks. If an error occurs, it will propagate |
- // through the futures back up to here where we can hand it off to unittest. |
- var asyncDone = expectAsync0(() {}); |
- return timeout(_runScheduled(_scheduled), |
- _TIMEOUT, 'waiting for a test to complete').catchError((e) { |
- return _runScheduled(_scheduledOnException).then((_) { |
- // Rethrow the original error so it keeps propagating. |
- throw e; |
- }); |
- }).whenComplete(() { |
- // Clean up after ourselves. Do this first before reporting back to |
- // unittest because it will advance to the next test immediately. |
- return _runScheduled(_scheduledCleanup).then((_) { |
- _scheduled = null; |
- _scheduledCleanup = null; |
- _scheduledOnException = null; |
- if (_sandboxDir != null) { |
- var dir = _sandboxDir; |
- _sandboxDir = null; |
- return deleteDir(dir); |
- } |
- }); |
- }).then((_) { |
- // If we got here, the test completed successfully so tell unittest so. |
- asyncDone(); |
- }).catchError((e) { |
- // If we got here, an error occurred. We will register it with unittest |
- // directly so that the error message isn't wrapped in any matcher stuff. |
- // We do this call last because it will cause unittest to *synchronously* |
- // advance to the next test and run it. |
- registerException(e.error, e.stackTrace); |
- }); |
}); |
} |
@@ -551,60 +255,39 @@ String get testDirectory { |
/// Schedules renaming (moving) the directory at [from] to [to], both of which |
/// are assumed to be relative to [sandboxDir]. |
void scheduleRename(String from, String to) { |
- _schedule((sandboxDir) { |
- return renameDir(path.join(sandboxDir, from), path.join(sandboxDir, to)); |
- }); |
+ schedule( |
+ () => renameDir( |
+ path.join(sandboxDir, from), |
+ path.join(sandboxDir, to)), |
+ 'renaming $from to $to'); |
} |
- |
/// Schedules creating a symlink at path [symlink] that points to [target], |
/// both of which are assumed to be relative to [sandboxDir]. |
void scheduleSymlink(String target, String symlink) { |
- _schedule((sandboxDir) { |
- return createSymlink(path.join(sandboxDir, target), |
- path.join(sandboxDir, symlink)); |
- }); |
+ schedule( |
+ () => createSymlink( |
+ path.join(sandboxDir, target), |
+ path.join(sandboxDir, symlink)), |
+ 'symlinking $target to $symlink'); |
} |
/// Schedules a call to the Pub command-line utility. Runs Pub with [args] and |
/// validates that its results match [output], [error], and [exitCode]. |
void schedulePub({List args, Pattern output, Pattern error, |
Future<Uri> tokenEndpoint, int exitCode: 0}) { |
- _schedule((sandboxDir) { |
- return _doPub(runProcess, sandboxDir, args, tokenEndpoint).then((result) { |
- var failures = []; |
- |
- _validateOutput(failures, 'stdout', output, result.stdout); |
- _validateOutput(failures, 'stderr', error, result.stderr); |
+ var pub = startPub(args: args, tokenEndpoint: tokenEndpoint); |
+ pub.shouldExit(exitCode); |
- if (result.exitCode != exitCode) { |
- failures.add( |
- 'Pub returned exit code ${result.exitCode}, expected $exitCode.'); |
- } |
- |
- if (failures.length > 0) { |
- if (error == null) { |
- // If we aren't validating the error, still show it on failure. |
- failures.add('Pub stderr:'); |
- failures.addAll(result.stderr.map((line) => '| $line')); |
- } |
- |
- throw new TestFailure(failures.join('\n')); |
- } |
- |
- return null; |
- }); |
- }); |
-} |
- |
-/// Starts a Pub process and returns a [ScheduledProcess] that supports |
-/// interaction with that process. |
-/// |
-/// Any futures in [args] will be resolved before the process is started. |
-ScheduledProcess startPub({List args, Future<Uri> tokenEndpoint}) { |
- var process = _scheduleValue((sandboxDir) => |
- _doPub(startProcess, sandboxDir, args, tokenEndpoint)); |
- return new ScheduledProcess("pub", process); |
+ expect(Future.wait([ |
+ pub.remainingStdout(), |
+ pub.remainingStderr() |
+ ]).then((results) { |
+ var failures = []; |
+ _validateOutput(failures, 'stdout', output, results[0].split('\n')); |
+ _validateOutput(failures, 'stderr', error, results[1].split('\n')); |
+ if (!failures.isEmpty) throw new TestFailure(failures.join('\n')); |
+ }), completes); |
} |
/// Like [startPub], but runs `pub lish` in particular with [server] used both |
@@ -612,7 +295,7 @@ ScheduledProcess startPub({List args, Future<Uri> tokenEndpoint}) { |
/// package server. |
/// |
/// Any futures in [args] will be resolved before the process is started. |
-ScheduledProcess startPubLish(ScheduledServer server, {List args}) { |
+ScheduledProcess startPublish(ScheduledServer server, {List args}) { |
var tokenEndpoint = server.url.then((url) => |
url.resolve('/token').toString()); |
if (args == null) args = []; |
@@ -626,61 +309,60 @@ ScheduledProcess startPubLish(ScheduledServer server, {List args}) { |
void confirmPublish(ScheduledProcess pub) { |
// TODO(rnystrom): This is overly specific and inflexible regarding different |
// test packages. Should validate this a little more loosely. |
- expectLater(pub.nextLine(), equals('Publishing "test_pkg" 1.0.0:')); |
- expectLater(pub.nextLine(), equals("|-- LICENSE")); |
- expectLater(pub.nextLine(), equals("|-- lib")); |
- expectLater(pub.nextLine(), equals("| '-- test_pkg.dart")); |
- expectLater(pub.nextLine(), equals("'-- pubspec.yaml")); |
- expectLater(pub.nextLine(), equals("")); |
+ expect(pub.nextLine(), completion(equals('Publishing "test_pkg" 1.0.0:'))); |
+ expect(pub.nextLine(), completion(equals("|-- LICENSE"))); |
+ expect(pub.nextLine(), completion(equals("|-- lib"))); |
+ expect(pub.nextLine(), completion(equals("| '-- test_pkg.dart"))); |
+ expect(pub.nextLine(), completion(equals("'-- pubspec.yaml"))); |
+ expect(pub.nextLine(), completion(equals(""))); |
pub.writeLine("y"); |
} |
-/// Calls [fn] with appropriately modified arguments to run a pub process. [fn] |
-/// should have the same signature as [startProcess], except that the returned |
-/// [Future] may have a type other than [Process]. |
-Future _doPub(Function fn, sandboxDir, List args, Future<Uri> tokenEndpoint) { |
+/// Starts a Pub process and returns a [ScheduledProcess] that supports |
+/// interaction with that process. |
+/// |
+/// Any futures in [args] will be resolved before the process is started. |
+ScheduledProcess startPub({List args, Future<Uri> tokenEndpoint}) { |
String pathInSandbox(String relPath) { |
return path.join(path.absolute(sandboxDir), relPath); |
} |
- return defer(() { |
- ensureDir(pathInSandbox(appPath)); |
- return Future.wait([ |
- _awaitObject(args), |
- tokenEndpoint == null ? new Future.immediate(null) : tokenEndpoint |
- ]); |
- }).then((results) { |
- var args = results[0]; |
- var tokenEndpoint = results[1]; |
- // Find a Dart executable we can use to spawn. Use the same one that was |
- // used to run this script itself. |
- var dartBin = new Options().executable; |
- |
- // If the executable looks like a path, get its full path. That way we |
- // can still find it when we spawn it with a different working directory. |
- if (dartBin.contains(Platform.pathSeparator)) { |
- dartBin = new File(dartBin).fullPathSync(); |
- } |
+ ensureDir(pathInSandbox(appPath)); |
+ |
+ // Find a Dart executable we can use to spawn. Use the same one that was |
+ // used to run this script itself. |
+ var dartBin = new Options().executable; |
- // Find the main pub entrypoint. |
- var pubPath = fs.joinPaths(testDirectory, '../../pub/pub.dart'); |
+ // If the executable looks like a path, get its full path. That way we |
+ // can still find it when we spawn it with a different working directory. |
+ if (dartBin.contains(Platform.pathSeparator)) { |
+ dartBin = new File(dartBin).fullPathSync(); |
+ } |
+ |
+ // Find the main pub entrypoint. |
+ var pubPath = fs.joinPaths(testDirectory, '../../pub/pub.dart'); |
- var dartArgs = ['--checked', pubPath, '--trace']; |
- dartArgs.addAll(args); |
+ var dartArgs = ['--checked', pubPath, '--trace']; |
+ dartArgs.addAll(args); |
- var environment = { |
+ if (tokenEndpoint == null) tokenEndpoint = new Future.immediate(null); |
+ var optionsFuture = tokenEndpoint.then((tokenEndpoint) { |
+ var options = new ProcessOptions(); |
+ options.workingDirectory = pathInSandbox(appPath); |
+ options.environment = { |
'PUB_CACHE': pathInSandbox(cachePath), |
'DART_SDK': pathInSandbox(sdkPath) |
}; |
- |
if (tokenEndpoint != null) { |
- environment['_PUB_TEST_TOKEN_ENDPOINT'] = tokenEndpoint.toString(); |
+ options.environment['_PUB_TEST_TOKEN_ENDPOINT'] = |
+ tokenEndpoint.toString(); |
} |
- |
- return fn(dartBin, dartArgs, workingDir: pathInSandbox(appPath), |
- environment: environment); |
+ return options; |
}); |
+ |
+ return new ScheduledProcess.start(dartBin, dartArgs, options: optionsFuture, |
+ description: args.isEmpty ? 'pub' : 'pub ${args.first}'); |
} |
/// Skips the current test if Git is not installed. This validates that the |
@@ -690,15 +372,13 @@ Future _doPub(Function fn, sandboxDir, List args, Future<Uri> tokenEndpoint) { |
/// have git installed to run the tests locally (unless they actually care |
/// about the pub git tests). |
void ensureGit() { |
- _schedule((_) { |
+ schedule(() { |
return gitlib.isInstalled.then((installed) { |
- if (!installed && |
- !Platform.environment.containsKey('BUILDBOT_BUILDERNAME')) { |
- _abortScheduled = true; |
- } |
- return null; |
+ if (installed) return; |
+ if (Platform.environment.containsKey('BUILDBOT_BUILDERNAME')) return; |
+ currentSchedule.abort(); |
}); |
- }); |
+ }, 'ensuring that Git is installed'); |
} |
/// Use [client] as the mock HTTP client for this test. |
@@ -708,29 +388,84 @@ void ensureGit() { |
void useMockClient(MockClient client) { |
var oldInnerClient = httpClient.inner; |
httpClient.inner = client; |
- _scheduleCleanup((_) { |
+ currentSchedule.onComplete.schedule(() { |
httpClient.inner = oldInnerClient; |
- }); |
+ }, 'de-activating the mock client'); |
} |
-Future _runScheduled(Queue<_ScheduledEvent> scheduled) { |
- if (scheduled == null) return new Future.immediate(null); |
+/// Describes a map representing a library package with the given [name], |
+/// [version], and [dependencies]. |
+Map packageMap(String name, String version, [List dependencies]) { |
+ var package = { |
+ "name": name, |
+ "version": version, |
+ "author": "Nathan Weizenbaum <nweiz@google.com>", |
+ "homepage": "http://pub.dartlang.org", |
+ "description": "A package, I guess." |
+ }; |
+ if (dependencies != null) { |
+ package["dependencies"] = dependencyListToMap(dependencies); |
+ } |
+ return package; |
+} |
- Future runNextEvent(_) { |
- if (_abortScheduled || scheduled.isEmpty) { |
- _abortScheduled = false; |
- return new Future.immediate(null); |
- } |
+/// Describes a map representing a dependency on a package in the package |
+/// repository. |
+Map dependencyMap(String name, [String versionConstraint]) { |
+ var url = port.then((p) => "http://localhost:$p"); |
+ var dependency = {"hosted": {"name": name, "url": url}}; |
+ if (versionConstraint != null) dependency["version"] = versionConstraint; |
+ return dependency; |
+} |
- var future = scheduled.removeFirst()(_sandboxDir); |
- if (future != null) { |
- return future.then(runNextEvent); |
- } else { |
- return runNextEvent(null); |
+/// Converts a list of dependencies as passed to [package] into a hash as used |
+/// in a pubspec. |
+Future<Map> dependencyListToMap(List<Map> dependencies) { |
+ return awaitObject(dependencies).then((resolvedDependencies) { |
+ var result = <String, Map>{}; |
+ for (var dependency in resolvedDependencies) { |
+ var keys = dependency.keys.where((key) => key != "version"); |
+ var sourceName = only(keys); |
+ var source; |
+ switch (sourceName) { |
+ case "git": |
+ source = new GitSource(); |
+ break; |
+ case "hosted": |
+ source = new HostedSource(); |
+ break; |
+ case "path": |
+ source = new PathSource(); |
+ break; |
+ default: |
+ throw 'Unknown source "$sourceName"'; |
+ } |
+ |
+ result[_packageName(sourceName, dependency[sourceName])] = dependency; |
} |
- } |
+ return result; |
+ }); |
+} |
- return runNextEvent(null); |
+/// Return the name for the package described by [description] and from |
+/// [sourceName]. |
+String _packageName(String sourceName, description) { |
+ switch (sourceName) { |
+ case "git": |
+ var url = description is String ? description : description['url']; |
+ // TODO(rnystrom): Using path.basename on a URL is hacky. If we add URL |
+ // support to pkg/pathos, should use an explicit builder for that. |
+ return path.basename(url.replaceFirst(new RegExp(r"(\.git)?/?$"), "")); |
+ case "hosted": |
+ if (description is String) return description; |
+ return description['name']; |
+ case "path": |
+ return path.basename(description); |
+ case "sdk": |
+ return description; |
+ default: |
+ return description; |
+ } |
} |
/// Compares the [actual] output from running pub with [expected]. For [String] |
@@ -810,392 +545,6 @@ void _validateOutputString(List<String> failures, String pipe, |
} |
} |
-/// Base class for [FileDescriptor] and [DirectoryDescriptor] so that a |
-/// directory can contain a heterogeneous collection of files and |
-/// subdirectories. |
-abstract class Descriptor { |
- /// The name of this file or directory. This must be a [String] if the file |
- /// or directory is going to be created. |
- final Pattern name; |
- |
- Descriptor(this.name); |
- |
- /// Creates the file or directory within [dir]. Returns a [Future] that is |
- /// completed after the creation is done. |
- Future create(dir); |
- |
- /// Validates that this descriptor correctly matches the corresponding file |
- /// system entry within [dir]. Returns a [Future] that completes to `null` if |
- /// the entry is valid, or throws an error if it failed. |
- Future validate(String dir); |
- |
- /// Deletes the file or directory within [dir]. Returns a [Future] that is |
- /// completed after the deletion is done. |
- Future delete(String dir); |
- |
- /// Loads the file at [path] from within this descriptor. If [path] is empty, |
- /// loads the contents of the descriptor itself. |
- ByteStream load(List<String> path); |
- |
- /// Schedules the directory to be created before Pub is run with |
- /// [schedulePub]. The directory will be created relative to the sandbox |
- /// directory. |
- // TODO(nweiz): Use implicit closurization once issue 2984 is fixed. |
- void scheduleCreate() => _schedule((dir) => this.create(dir)); |
- |
- /// Schedules the file or directory to be deleted recursively. |
- void scheduleDelete() => _schedule((dir) => this.delete(dir)); |
- |
- /// Schedules the directory to be validated after Pub is run with |
- /// [schedulePub]. The directory will be validated relative to the sandbox |
- /// directory. |
- void scheduleValidate() => _schedule((parentDir) => validate(parentDir)); |
- |
- /// Asserts that the name of the descriptor is a [String] and returns it. |
- String get _stringName { |
- if (name is String) return name; |
- throw 'Pattern $name must be a string.'; |
- } |
- |
- /// Validates that at least one file in [dir] matching [name] is valid |
- /// according to [validate]. [validate] should throw or complete to an |
- /// exception if the input path is invalid. |
- Future _validateOneMatch(String dir, Future validate(String entry)) { |
- // Special-case strings to support multi-level names like "myapp/packages". |
- if (name is String) { |
- var entry = path.join(dir, name); |
- return defer(() { |
- if (!entryExists(entry)) { |
- throw new TestFailure('Entry $entry not found.'); |
- } |
- return validate(entry); |
- }); |
- } |
- |
- // TODO(nweiz): remove this when issue 4061 is fixed. |
- var stackTrace; |
- try { |
- throw ""; |
- } catch (_, localStackTrace) { |
- stackTrace = localStackTrace; |
- } |
- |
- return listDir(dir).then((files) { |
- var matches = files.where((file) => endsWithPattern(file, name)).toList(); |
- if (matches.isEmpty) { |
- throw new TestFailure('No files in $dir match pattern $name.'); |
- } |
- if (matches.length == 1) return validate(matches[0]); |
- |
- var failures = []; |
- var successes = 0; |
- var completer = new Completer(); |
- checkComplete() { |
- if (failures.length + successes != matches.length) return; |
- if (successes > 0) { |
- completer.complete(); |
- return; |
- } |
- |
- var error = new StringBuffer(); |
- error.write("No files named $name in $dir were valid:\n"); |
- for (var failure in failures) { |
- error.write(" $failure\n"); |
- } |
- completer.completeError( |
- new TestFailure(error.toString()), stackTrace); |
- } |
- |
- for (var match in matches) { |
- var future = validate(match).then((_) { |
- successes++; |
- checkComplete(); |
- }).catchError((e) { |
- failures.add(e); |
- checkComplete(); |
- }); |
- } |
- return completer.future; |
- }); |
- } |
-} |
- |
-/// Describes a file. These are used both for setting up an expected directory |
-/// tree before running a test, and for validating that the file system matches |
-/// some expectations after running it. |
-class FileDescriptor extends Descriptor { |
- /// The contents of the file, in bytes. |
- final List<int> contents; |
- |
- String get textContents => new String.fromCharCodes(contents); |
- |
- FileDescriptor.bytes(Pattern name, this.contents) : super(name); |
- |
- FileDescriptor(Pattern name, String contents) : |
- this.bytes(name, encodeUtf8(contents)); |
- |
- /// Creates the file within [dir]. Returns a [Future] that is completed after |
- /// the creation is done. |
- Future<String> create(dir) => |
- defer(() => writeBinaryFile(path.join(dir, _stringName), contents)); |
- |
- /// Deletes the file within [dir]. Returns a [Future] that is completed after |
- /// the deletion is done. |
- Future delete(dir) => |
- defer(() => deleteFile(path.join(dir, _stringName))); |
- |
- /// Validates that this file correctly matches the actual file at [path]. |
- Future validate(String path) { |
- return _validateOneMatch(path, (file) { |
- var text = readTextFile(file); |
- if (text == textContents) return null; |
- |
- throw new TestFailure( |
- 'File $file should contain:\n\n$textContents\n\n' |
- 'but contained:\n\n$text'); |
- }); |
- } |
- |
- /// Loads the contents of the file. |
- ByteStream load(List<String> path) { |
- if (!path.isEmpty) { |
- throw "Can't load ${path.join('/')} from within $name: not a directory."; |
- } |
- |
- return new ByteStream.fromBytes(contents); |
- } |
-} |
- |
-/// Describes a directory and its contents. These are used both for setting up |
-/// an expected directory tree before running a test, and for validating that |
-/// the file system matches some expectations after running it. |
-class DirectoryDescriptor extends Descriptor { |
- /// The files and directories contained in this directory. |
- final List<Descriptor> contents; |
- |
- DirectoryDescriptor(Pattern name, List<Descriptor> contents) |
- : this.contents = contents == null ? <Descriptor>[] : contents, |
- super(name); |
- |
- /// Creates the file within [dir]. Returns a [Future] that is completed after |
- /// the creation is done. |
- Future<String> create(parentDir) { |
- return defer(() { |
- // Create the directory. |
- var dir = ensureDir(path.join(parentDir, _stringName)); |
- if (contents == null) return dir; |
- |
- // Recursively create all of its children. |
- var childFutures = contents.map((child) => child.create(dir)).toList(); |
- // Only complete once all of the children have been created too. |
- return Future.wait(childFutures).then((_) => dir); |
- }); |
- } |
- |
- /// Deletes the directory within [dir]. Returns a [Future] that is completed |
- /// after the deletion is done. |
- Future delete(dir) { |
- return deleteDir(path.join(dir, _stringName)); |
- } |
- |
- /// Validates that the directory at [path] contains all of the expected |
- /// contents in this descriptor. Note that this does *not* check that the |
- /// directory doesn't contain other unexpected stuff, just that it *does* |
- /// contain the stuff we do expect. |
- Future validate(String path) { |
- return _validateOneMatch(path, (dir) { |
- // Validate each of the items in this directory. |
- final entryFutures = |
- contents.map((entry) => entry.validate(dir)).toList(); |
- |
- // If they are all valid, the directory is valid. |
- return Future.wait(entryFutures).then((entries) => null); |
- }); |
- } |
- |
- /// Loads [path] from within this directory. |
- ByteStream load(List<String> path) { |
- if (path.isEmpty) { |
- throw "Can't load the contents of $name: is a directory."; |
- } |
- |
- for (var descriptor in contents) { |
- if (descriptor.name == path[0]) { |
- return descriptor.load(path.sublist(1)); |
- } |
- } |
- |
- throw "Directory $name doesn't contain ${path.join('/')}."; |
- } |
-} |
- |
-/// Wraps a [Future] that will complete to a [Descriptor] and makes it behave |
-/// like a concrete [Descriptor]. This is necessary when the contents of the |
-/// descriptor depends on information that's not available until part of the |
-/// test run is completed. |
-class FutureDescriptor extends Descriptor { |
- Future<Descriptor> _future; |
- |
- FutureDescriptor(this._future) : super('<unknown>'); |
- |
- Future create(dir) => _future.then((desc) => desc.create(dir)); |
- |
- Future validate(dir) => _future.then((desc) => desc.validate(dir)); |
- |
- Future delete(dir) => _future.then((desc) => desc.delete(dir)); |
- |
- ByteStream load(List<String> path) { |
- var controller = new StreamController<List<int>>(); |
- _future.then((desc) => store(desc.load(path), controller)); |
- return new ByteStream(controller.stream); |
- } |
-} |
- |
-/// Describes a Git repository and its contents. |
-class GitRepoDescriptor extends DirectoryDescriptor { |
- GitRepoDescriptor(Pattern name, List<Descriptor> contents) |
- : super(name, contents); |
- |
- /// Creates the Git repository and commits the contents. |
- Future create(parentDir) { |
- return _runGitCommands(parentDir, [ |
- ['init'], |
- ['add', '.'], |
- ['commit', '-m', 'initial commit'] |
- ]); |
- } |
- |
- /// Commits any changes to the Git repository. |
- Future commit(parentDir) { |
- return _runGitCommands(parentDir, [ |
- ['add', '.'], |
- ['commit', '-m', 'update'] |
- ]); |
- } |
- |
- /// Schedules changes to be committed to the Git repository. |
- void scheduleCommit() => _schedule((dir) => this.commit(dir)); |
- |
- /// Return a Future that completes to the commit in the git repository |
- /// referred to by [ref] at the current point in the scheduled test run. |
- Future<String> revParse(String ref) { |
- return _scheduleValue((parentDir) { |
- return super.create(parentDir).then((rootDir) { |
- return _runGit(['rev-parse', ref], rootDir); |
- }).then((output) => output[0]); |
- }); |
- } |
- |
- /// Schedule a Git command to run in this repository. |
- void scheduleGit(List<String> args) { |
- _schedule((parentDir) => _runGit(args, path.join(parentDir, name))); |
- } |
- |
- Future _runGitCommands(parentDir, List<List<String>> commands) { |
- var workingDir; |
- |
- Future runGitStep(_) { |
- if (commands.isEmpty) return new Future.immediate(workingDir); |
- var command = commands.removeAt(0); |
- return _runGit(command, workingDir).then(runGitStep); |
- } |
- |
- return super.create(parentDir).then((rootDir) { |
- workingDir = rootDir; |
- return runGitStep(null); |
- }); |
- } |
- |
- Future<List<String>> _runGit(List<String> args, String workingDir) { |
- // Explicitly specify the committer information. Git needs this to commit |
- // and we don't want to rely on the buildbots having this already set up. |
- var environment = { |
- 'GIT_AUTHOR_NAME': 'Pub Test', |
- 'GIT_AUTHOR_EMAIL': 'pub@dartlang.org', |
- 'GIT_COMMITTER_NAME': 'Pub Test', |
- 'GIT_COMMITTER_EMAIL': 'pub@dartlang.org' |
- }; |
- |
- return gitlib.run(args, workingDir: workingDir, environment: environment); |
- } |
-} |
- |
-/// Describes a gzipped tar file and its contents. |
-class TarFileDescriptor extends Descriptor { |
- final List<Descriptor> contents; |
- |
- TarFileDescriptor(Pattern name, this.contents) |
- : super(name); |
- |
- /// Creates the files and directories within this tar file, then archives |
- /// them, compresses them, and saves the result to [parentDir]. |
- Future<String> create(parentDir) { |
- return withTempDir((tempDir) { |
- return Future.wait(contents.map((child) => child.create(tempDir))) |
- .then((createdContents) { |
- return createTarGz(createdContents, baseDir: tempDir).toBytes(); |
- }).then((bytes) { |
- var file = path.join(parentDir, _stringName); |
- writeBinaryFile(file, bytes); |
- return file; |
- }); |
- }); |
- } |
- |
- /// Validates that the `.tar.gz` file at [path] contains the expected |
- /// contents. |
- Future validate(String path) { |
- throw "TODO(nweiz): implement this"; |
- } |
- |
- Future delete(dir) { |
- throw new UnsupportedError(''); |
- } |
- |
- /// Loads the contents of this tar file. |
- ByteStream load(List<String> path) { |
- if (!path.isEmpty) { |
- throw "Can't load ${path.join('/')} from within $name: not a directory."; |
- } |
- |
- var controller = new StreamController<List<int>>(); |
- // TODO(nweiz): propagate any errors to the return value. See issue 3657. |
- withTempDir((tempDir) { |
- return create(tempDir).then((tar) { |
- var sourceStream = new File(tar).openRead(); |
- return store(sourceStream, controller); |
- }); |
- }); |
- return new ByteStream(controller.stream); |
- } |
-} |
- |
-/// A descriptor that validates that no file or directory exists with the given |
-/// name. |
-class NothingDescriptor extends Descriptor { |
- NothingDescriptor(String name) : super(name); |
- |
- Future create(dir) => new Future.immediate(null); |
- Future delete(dir) => new Future.immediate(null); |
- |
- Future validate(String dir) { |
- return defer(() { |
- if (entryExists(path.join(dir, name))) { |
- throw new TestFailure('Entry $name in $dir should not exist.'); |
- } |
- }); |
- } |
- |
- ByteStream load(List<String> path) { |
- if (path.isEmpty) { |
- throw "Can't load the contents of $name: it doesn't exist."; |
- } else { |
- throw "Can't load ${path.join('/')} from within $name: $name doesn't " |
- "exist."; |
- } |
- } |
-} |
- |
/// A function that creates a [Validator] subclass. |
typedef Validator ValidatorCreator(Entrypoint entrypoint); |
@@ -1203,7 +552,7 @@ typedef Validator ValidatorCreator(Entrypoint entrypoint); |
/// Future that contains the errors and warnings produced by that validator. |
Future<Pair<List<String>, List<String>>> schedulePackageValidation( |
ValidatorCreator fn) { |
- return _scheduleValue((sandboxDir) { |
+ return schedule(() { |
var cache = new SystemCache.withSources(path.join(sandboxDir, cachePath)); |
return defer(() { |
@@ -1212,7 +561,7 @@ Future<Pair<List<String>, List<String>>> schedulePackageValidation( |
return new Pair(validator.errors, validator.warnings); |
}); |
}); |
- }); |
+ }, "validating package"); |
} |
/// A matcher that matches a Pair. |
@@ -1235,406 +584,3 @@ class _PairMatcher extends BaseMatcher { |
description.addAll("(", ", ", ")", [_firstMatcher, _lastMatcher]); |
} |
} |
- |
-/// The time (in milliseconds) to wait for scheduled events that could run |
-/// forever. |
-const _SCHEDULE_TIMEOUT = 10000; |
- |
-/// A class representing a [Process] that is scheduled to run in the course of |
-/// the test. This class allows actions on the process to be scheduled |
-/// synchronously. All operations on this class are scheduled. |
-/// |
-/// Before running the test, either [shouldExit] or [kill] must be called on |
-/// this to ensure that the process terminates when expected. |
-/// |
-/// If the test fails, this will automatically print out any remaining stdout |
-/// and stderr from the process to aid debugging. |
-class ScheduledProcess { |
- /// The name of the process. Used for error reporting. |
- final String name; |
- |
- /// The process future that's scheduled to run. |
- Future<PubProcess> _processFuture; |
- |
- /// The process that's scheduled to run. It may be null. |
- PubProcess _process; |
- |
- /// The exit code of the scheduled program. It may be null. |
- int _exitCode; |
- |
- /// A future that will complete to a list of all the lines emitted on the |
- /// process's standard output stream. This is independent of what data is read |
- /// from [_stdout]. |
- Future<List<String>> _stdoutLines; |
- |
- /// A [Stream] of stdout lines emitted by the process that's scheduled to run. |
- /// It may be null. |
- Stream<String> _stdout; |
- |
- /// A [Future] that will resolve to [_stdout] once it's available. |
- Future get _stdoutFuture => _processFuture.then((_) => _stdout); |
- |
- /// A [StreamSubscription] that controls [_stdout]. |
- StreamSubscription _stdoutSubscription; |
- |
- /// A future that will complete to a list of all the lines emitted on the |
- /// process's standard error stream. This is independent of what data is read |
- /// from [_stderr]. |
- Future<List<String>> _stderrLines; |
- |
- /// A [Stream] of stderr lines emitted by the process that's scheduled to run. |
- /// It may be null. |
- Stream<String> _stderr; |
- |
- /// A [Future] that will resolve to [_stderr] once it's available. |
- Future get _stderrFuture => _processFuture.then((_) => _stderr); |
- |
- /// A [StreamSubscription] that controls [_stderr]. |
- StreamSubscription _stderrSubscription; |
- |
- /// The exit code of the process that's scheduled to run. This will naturally |
- /// only complete once the process has terminated. |
- Future<int> get _exitCodeFuture => _exitCodeCompleter.future; |
- |
- /// The completer for [_exitCode]. |
- final Completer<int> _exitCodeCompleter = new Completer(); |
- |
- /// Whether the user has scheduled the end of this process by calling either |
- /// [shouldExit] or [kill]. |
- bool _endScheduled = false; |
- |
- /// Whether the process is expected to terminate at this point. |
- bool _endExpected = false; |
- |
- /// Wraps a [Process] [Future] in a scheduled process. |
- ScheduledProcess(this.name, Future<PubProcess> process) |
- : _processFuture = process { |
- var pairFuture = process.then((p) { |
- _process = p; |
- |
- byteStreamToLines(stream) { |
- return streamToLines(new ByteStream(stream.handleError((e) { |
- registerException(e.error, e.stackTrace); |
- })).toStringStream()); |
- } |
- |
- var stdoutTee = tee(byteStreamToLines(p.stdout)); |
- var stdoutPair = streamWithSubscription(stdoutTee.last); |
- _stdout = stdoutPair.first; |
- _stdoutSubscription = stdoutPair.last; |
- |
- var stderrTee = tee(byteStreamToLines(p.stderr)); |
- var stderrPair = streamWithSubscription(stderrTee.last); |
- _stderr = stderrPair.first; |
- _stderrSubscription = stderrPair.last; |
- |
- return new Pair(stdoutTee.first, stderrTee.first); |
- }); |
- |
- _stdoutLines = pairFuture.then((pair) => pair.first.toList()); |
- _stderrLines = pairFuture.then((pair) => pair.last.toList()); |
- |
- _schedule((_) { |
- if (!_endScheduled) { |
- throw new StateError("Scheduled process $name must have shouldExit() " |
- "or kill() called before the test is run."); |
- } |
- |
- process.then((p) => p.exitCode).then((exitCode) { |
- if (_endExpected) { |
- _exitCode = exitCode; |
- _exitCodeCompleter.complete(exitCode); |
- return; |
- } |
- |
- // Sleep for half a second in case _endExpected is set in the next |
- // scheduled event. |
- return sleep(500).then((_) { |
- if (_endExpected) { |
- _exitCodeCompleter.complete(exitCode); |
- return; |
- } |
- |
- return _printStreams(); |
- }).then((_) { |
- registerException(new TestFailure("Process $name ended " |
- "earlier than scheduled with exit code $exitCode")); |
- }); |
- }).catchError((e) => registerException(e.error, e.stackTrace)); |
- }); |
- |
- _scheduleOnException((_) { |
- if (_process == null) return; |
- |
- if (_exitCode == null) { |
- print("\nKilling process $name prematurely."); |
- _endExpected = true; |
- _process.kill(); |
- } |
- |
- return _printStreams(); |
- }); |
- |
- _scheduleCleanup((_) { |
- if (_process == null) return; |
- // Ensure that the process is dead and we aren't waiting on any IO. |
- _process.kill(); |
- _stdoutSubscription.cancel(); |
- _stderrSubscription.cancel(); |
- }); |
- } |
- |
- /// Reads the next line of stdout from the process. |
- Future<String> nextLine() { |
- return _scheduleValue((_) { |
- return timeout(_stdoutFuture.then((stream) => streamFirst(stream)), |
- _SCHEDULE_TIMEOUT, |
- "waiting for the next stdout line from process $name"); |
- }); |
- } |
- |
- /// Reads the next line of stderr from the process. |
- Future<String> nextErrLine() { |
- return _scheduleValue((_) { |
- return timeout(_stderrFuture.then((stream) => streamFirst(stream)), |
- _SCHEDULE_TIMEOUT, |
- "waiting for the next stderr line from process $name"); |
- }); |
- } |
- |
- /// Reads the remaining stdout from the process. This should only be called |
- /// after kill() or shouldExit(). |
- Future<String> remainingStdout() { |
- if (!_endScheduled) { |
- throw new StateError("remainingStdout() should only be called after " |
- "kill() or shouldExit()."); |
- } |
- |
- return _scheduleValue((_) { |
- return timeout(_stdoutFuture.then((stream) => stream.toList()) |
- .then((lines) => lines.join("\n")), |
- _SCHEDULE_TIMEOUT, |
- "waiting for the last stdout line from process $name"); |
- }); |
- } |
- |
- /// Reads the remaining stderr from the process. This should only be called |
- /// after kill() or shouldExit(). |
- Future<String> remainingStderr() { |
- if (!_endScheduled) { |
- throw new StateError("remainingStderr() should only be called after " |
- "kill() or shouldExit()."); |
- } |
- |
- return _scheduleValue((_) { |
- return timeout(_stderrFuture.then((stream) => stream.toList()) |
- .then((lines) => lines.join("\n")), |
- _SCHEDULE_TIMEOUT, |
- "waiting for the last stderr line from process $name"); |
- }); |
- } |
- |
- /// Writes [line] to the process as stdin. |
- void writeLine(String line) { |
- _schedule((_) => _processFuture.then( |
- (p) => p.stdin.add(encodeUtf8('$line\n')))); |
- } |
- |
- /// Kills the process, and waits until it's dead. |
- void kill() { |
- _endScheduled = true; |
- _schedule((_) { |
- _endExpected = true; |
- _process.kill(); |
- timeout(_exitCodeFuture, _SCHEDULE_TIMEOUT, |
- "waiting for process $name to die"); |
- }); |
- } |
- |
- /// Waits for the process to exit, and verifies that the exit code matches |
- /// [expectedExitCode] (if given). |
- void shouldExit([int expectedExitCode]) { |
- _endScheduled = true; |
- _schedule((_) { |
- _endExpected = true; |
- return timeout(_exitCodeFuture, _SCHEDULE_TIMEOUT, |
- "waiting for process $name to exit").then((exitCode) { |
- if (expectedExitCode != null) { |
- expect(exitCode, equals(expectedExitCode)); |
- } |
- }); |
- }); |
- } |
- |
- /// Prints the remaining data in the process's stdout and stderr streams. |
- /// Prints nothing if the streams are empty. |
- Future _printStreams() { |
- void printStream(String streamName, List<String> lines) { |
- if (lines.isEmpty) return; |
- |
- print('\nProcess $name $streamName:'); |
- for (var line in lines) { |
- print('| $line'); |
- } |
- } |
- |
- return _stdoutLines.then((stdoutLines) { |
- printStream('stdout', stdoutLines); |
- return _stderrLines.then((stderrLines) { |
- printStream('stderr', stderrLines); |
- }); |
- }); |
- } |
-} |
- |
-/// A class representing an [HttpServer] that's scheduled to run in the course |
-/// of the test. This class allows the server's request handling to be |
-/// scheduled synchronously. All operations on this class are scheduled. |
-class ScheduledServer { |
- /// The wrapped server. |
- final Future<HttpServer> _server; |
- |
- /// The queue of handlers to run for upcoming requests. |
- final _handlers = new Queue<Future>(); |
- |
- /// The requests to be ignored. |
- final _ignored = new Set<Pair<String, String>>(); |
- |
- ScheduledServer._(this._server); |
- |
- /// Creates a new server listening on an automatically-allocated port on |
- /// localhost. |
- factory ScheduledServer() { |
- var scheduledServer; |
- scheduledServer = new ScheduledServer._(_scheduleValue((_) { |
- return SafeHttpServer.bind("127.0.0.1", 0).then((server) { |
- server.listen(scheduledServer._awaitHandle); |
- _scheduleCleanup((_) => server.close()); |
- return server; |
- }); |
- })); |
- return scheduledServer; |
- } |
- |
- /// The port on which the server is listening. |
- Future<int> get port => _server.then((s) => s.port); |
- |
- /// The base URL of the server, including its port. |
- Future<Uri> get url => |
- port.then((p) => Uri.parse("http://localhost:$p")); |
- |
- /// Assert that the next request has the given [method] and [path], and pass |
- /// it to [handler] to handle. If [handler] returns a [Future], wait until |
- /// it's completed to continue the schedule. |
- void handle(String method, String path, |
- Future handler(HttpRequest request, HttpResponse response)) { |
- var handlerCompleter = new Completer<Function>(); |
- _scheduleValue((_) { |
- var requestCompleteCompleter = new Completer(); |
- handlerCompleter.complete((request, response) { |
- expect(request.method, equals(method)); |
- expect(request.uri.path, equals(path)); |
- |
- var future = handler(request, response); |
- if (future == null) future = new Future.immediate(null); |
- chainToCompleter(future, requestCompleteCompleter); |
- }); |
- return timeout(requestCompleteCompleter.future, |
- _SCHEDULE_TIMEOUT, "waiting for $method $path"); |
- }); |
- _handlers.add(handlerCompleter.future); |
- } |
- |
- /// Ignore all requests with the given [method] and [path]. If one is |
- /// received, don't respond to it. |
- void ignore(String method, String path) => |
- _ignored.add(new Pair(method, path)); |
- |
- /// Raises an error complaining of an unexpected request. |
- void _awaitHandle(HttpRequest request) { |
- HttpResponse response = request.response; |
- if (_ignored.contains(new Pair(request.method, request.uri.path))) return; |
- var future = timeout(defer(() { |
- if (_handlers.isEmpty) { |
- fail('Unexpected ${request.method} request to ${request.uri.path}.'); |
- } |
- return _handlers.removeFirst(); |
- }).then((handler) { |
- handler(request, response); |
- }), _SCHEDULE_TIMEOUT, "waiting for a handler for ${request.method} " |
- "${request.uri.path}"); |
- expect(future, completes); |
- } |
-} |
- |
-/// Takes a simple data structure (composed of [Map]s, [List]s, scalar objects, |
-/// and [Future]s) and recursively resolves all the [Future]s contained within. |
-/// Completes with the fully resolved structure. |
-Future _awaitObject(object) { |
- // Unroll nested futures. |
- if (object is Future) return object.then(_awaitObject); |
- if (object is Collection) { |
- return Future.wait(object.map(_awaitObject).toList()); |
- } |
- if (object is! Map) return new Future.immediate(object); |
- |
- var pairs = <Future<Pair>>[]; |
- object.forEach((key, value) { |
- pairs.add(_awaitObject(value) |
- .then((resolved) => new Pair(key, resolved))); |
- }); |
- return Future.wait(pairs).then((resolvedPairs) { |
- var map = {}; |
- for (var pair in resolvedPairs) { |
- map[pair.first] = pair.last; |
- } |
- return map; |
- }); |
-} |
- |
-/// Schedules a callback to be called as part of the test case. |
-void _schedule(_ScheduledEvent event) { |
- if (_scheduled == null) _scheduled = new Queue(); |
- _scheduled.addLast(event); |
-} |
- |
-/// Like [_schedule], but pipes the return value of [event] to a returned |
-/// [Future]. |
-Future _scheduleValue(_ScheduledEvent event) { |
- var completer = new Completer(); |
- _schedule((parentDir) { |
- chainToCompleter(event(parentDir), completer); |
- return completer.future; |
- }); |
- return completer.future; |
-} |
- |
-/// Schedules a callback to be called after the test case has completed, even |
-/// if it failed. |
-void _scheduleCleanup(_ScheduledEvent event) { |
- if (_scheduledCleanup == null) _scheduledCleanup = new Queue(); |
- _scheduledCleanup.addLast(event); |
-} |
- |
-/// Schedules a callback to be called after the test case has completed, but |
-/// only if it failed. |
-void _scheduleOnException(_ScheduledEvent event) { |
- if (_scheduledOnException == null) _scheduledOnException = new Queue(); |
- _scheduledOnException.addLast(event); |
-} |
- |
-/// Like [expect], but for [Future]s that complete as part of the scheduled |
-/// test. This is necessary to ensure that the exception thrown by the |
-/// expectation failing is handled by the scheduler. |
-/// |
-/// Note that [matcher] matches against the completed value of [actual], so |
-/// calling [completion] is unnecessary. |
-void expectLater(Future actual, matcher, {String reason, |
- FailureHandler failureHandler, bool verbose: false}) { |
- _schedule((_) { |
- return actual.then((value) { |
- expect(value, matcher, reason: reason, failureHandler: failureHandler, |
- verbose: false); |
- }); |
- }); |
-} |