OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 /// Test infrastructure for testing pub. Unlike typical unit tests, most pub | 5 /// Test infrastructure for testing pub. Unlike typical unit tests, most pub |
6 /// tests are integration tests that stage some stuff on the file system, run | 6 /// tests are integration tests that stage some stuff on the file system, run |
7 /// pub, and then validate the results. This library provides an API to build | 7 /// pub, and then validate the results. This library provides an API to build |
8 /// tests like that. | 8 /// tests like that. |
9 library test_pub; | 9 library test_pub; |
10 | 10 |
11 import 'dart:async'; | 11 import 'dart:async'; |
12 import 'dart:collection' show Queue; | 12 import 'dart:collection' show Queue; |
13 import 'dart:io'; | 13 import 'dart:io'; |
14 import 'dart:json' as json; | 14 import 'dart:json' as json; |
15 import 'dart:math'; | 15 import 'dart:math'; |
16 import 'dart:uri'; | 16 import 'dart:uri'; |
17 import 'dart:utf'; | 17 import 'dart:utf'; |
18 | 18 |
19 import '../../../pkg/http/lib/testing.dart'; | 19 import '../../../pkg/http/lib/testing.dart'; |
20 import '../../../pkg/oauth2/lib/oauth2.dart' as oauth2; | 20 import '../../../pkg/oauth2/lib/oauth2.dart' as oauth2; |
21 import '../../../pkg/pathos/lib/path.dart' as path; | 21 import '../../../pkg/pathos/lib/path.dart' as path; |
22 import '../../../pkg/scheduled_test/lib/scheduled_process.dart'; | 22 import '../../../pkg/unittest/lib/unittest.dart'; |
23 import '../../../pkg/scheduled_test/lib/scheduled_server.dart'; | |
24 import '../../../pkg/scheduled_test/lib/scheduled_test.dart'; | |
25 import '../../../pkg/yaml/lib/yaml.dart'; | 23 import '../../../pkg/yaml/lib/yaml.dart'; |
26 | |
27 import '../../lib/file_system.dart' as fs; | 24 import '../../lib/file_system.dart' as fs; |
28 import '../../pub/entrypoint.dart'; | 25 import '../../pub/entrypoint.dart'; |
29 // TODO(rnystrom): Using "gitlib" as the prefix here is ugly, but "git" collides | 26 // TODO(rnystrom): Using "gitlib" as the prefix here is ugly, but "git" collides |
30 // with the git descriptor method. Maybe we should try to clean up the top level | 27 // with the git descriptor method. Maybe we should try to clean up the top level |
31 // scope a bit? | 28 // scope a bit? |
32 import '../../pub/git.dart' as gitlib; | 29 import '../../pub/git.dart' as gitlib; |
33 import '../../pub/git_source.dart'; | 30 import '../../pub/git_source.dart'; |
34 import '../../pub/hosted_source.dart'; | 31 import '../../pub/hosted_source.dart'; |
35 import '../../pub/http.dart'; | 32 import '../../pub/http.dart'; |
36 import '../../pub/io.dart'; | 33 import '../../pub/io.dart'; |
37 import '../../pub/path_source.dart'; | 34 import '../../pub/path_source.dart'; |
38 import '../../pub/safe_http_server.dart'; | 35 import '../../pub/safe_http_server.dart'; |
39 import '../../pub/system_cache.dart'; | 36 import '../../pub/system_cache.dart'; |
40 import '../../pub/utils.dart'; | 37 import '../../pub/utils.dart'; |
41 import '../../pub/validator.dart'; | 38 import '../../pub/validator.dart'; |
42 import 'command_line_config.dart'; | 39 import 'command_line_config.dart'; |
43 import 'descriptor.dart' as d; | |
44 | 40 |
45 /// This should be called at the top of a test file to set up an appropriate | 41 /// This should be called at the top of a test file to set up an appropriate |
46 /// test configuration for the machine running the tests. | 42 /// test configuration for the machine running the tests. |
47 initConfig() { | 43 initConfig() { |
48 // If we aren't running on the bots, use the human-friendly config. | 44 // If we aren't running on the bots, use the human-friendly config. |
49 if (new Options().arguments.contains('--human')) { | 45 if (new Options().arguments.contains('--human')) { |
50 configure(new CommandLineConfiguration()); | 46 configure(new CommandLineConfiguration()); |
51 } | 47 } |
52 } | 48 } |
53 | 49 |
| 50 /// Creates a new [FileDescriptor] with [name] and [contents]. |
| 51 FileDescriptor file(Pattern name, String contents) => |
| 52 new FileDescriptor(name, contents); |
| 53 |
| 54 /// Creates a new [FileDescriptor] with [name] and [contents]. |
| 55 FileDescriptor binaryFile(Pattern name, List<int> contents) => |
| 56 new FileDescriptor.bytes(name, contents); |
| 57 |
| 58 /// Creates a new [DirectoryDescriptor] with [name] and [contents]. |
| 59 DirectoryDescriptor dir(Pattern name, [List<Descriptor> contents]) => |
| 60 new DirectoryDescriptor(name, contents); |
| 61 |
| 62 /// Creates a new [FutureDescriptor] wrapping [future]. |
| 63 FutureDescriptor async(Future<Descriptor> future) => |
| 64 new FutureDescriptor(future); |
| 65 |
| 66 /// Creates a new [GitRepoDescriptor] with [name] and [contents]. |
| 67 GitRepoDescriptor git(Pattern name, [List<Descriptor> contents]) => |
| 68 new GitRepoDescriptor(name, contents); |
| 69 |
| 70 /// Creates a new [TarFileDescriptor] with [name] and [contents]. |
| 71 TarFileDescriptor tar(Pattern name, [List<Descriptor> contents]) => |
| 72 new TarFileDescriptor(name, contents); |
| 73 |
| 74 /// Creates a new [NothingDescriptor] with [name]. |
| 75 NothingDescriptor nothing(String name) => new NothingDescriptor(name); |
| 76 |
54 /// The current [HttpServer] created using [serve]. | 77 /// The current [HttpServer] created using [serve]. |
55 var _server; | 78 var _server; |
56 | 79 |
57 /// The cached value for [_portCompleter]. | 80 /// The cached value for [_portCompleter]. |
58 Completer<int> _portCompleterCache; | 81 Completer<int> _portCompleterCache; |
59 | 82 |
60 /// The completer for [port]. | 83 /// The completer for [port]. |
61 Completer<int> get _portCompleter { | 84 Completer<int> get _portCompleter { |
62 if (_portCompleterCache != null) return _portCompleterCache; | 85 if (_portCompleterCache != null) return _portCompleterCache; |
63 _portCompleterCache = new Completer<int>(); | 86 _portCompleterCache = new Completer<int>(); |
64 currentSchedule.onComplete.schedule(() { | 87 _scheduleCleanup((_) { |
65 _portCompleterCache = null; | 88 _portCompleterCache = null; |
66 }, 'clearing the port completer'); | 89 }); |
67 return _portCompleterCache; | 90 return _portCompleterCache; |
68 } | 91 } |
69 | 92 |
70 /// A future that will complete to the port used for the current server. | 93 /// A future that will complete to the port used for the current server. |
71 Future<int> get port => _portCompleter.future; | 94 Future<int> get port => _portCompleter.future; |
72 | 95 |
73 /// Creates an HTTP server to serve [contents] as static files. This server will | 96 /// Creates an HTTP server to serve [contents] as static files. This server will |
74 /// exist only for the duration of the pub run. | 97 /// exist only for the duration of the pub run. |
75 /// | 98 /// |
76 /// Subsequent calls to [serve] will replace the previous server. | 99 /// Subsequent calls to [serve] will replace the previous server. |
77 void serve([List<d.Descriptor> contents]) { | 100 void serve([List<Descriptor> contents]) { |
78 var baseDir = d.dir("serve-dir", contents); | 101 var baseDir = dir("serve-dir", contents); |
79 | 102 |
80 schedule(() { | 103 _schedule((_) { |
81 return _closeServer().then((_) { | 104 return _closeServer().then((_) { |
82 return SafeHttpServer.bind("127.0.0.1", 0).then((server) { | 105 return SafeHttpServer.bind("127.0.0.1", 0).then((server) { |
83 _server = server; | 106 _server = server; |
84 server.listen((request) { | 107 server.listen((request) { |
85 var response = request.response; | 108 var response = request.response; |
| 109 var path = request.uri.path.replaceFirst("/", "").split("/"); |
| 110 response.persistentConnection = false; |
| 111 var stream; |
86 try { | 112 try { |
87 var path = request.uri.path.replaceFirst("/", ""); | 113 stream = baseDir.load(path); |
88 response.persistentConnection = false; | |
89 var stream = baseDir.load(path); | |
90 | |
91 new ByteStream(stream).toBytes().then((data) { | |
92 response.statusCode = 200; | |
93 response.contentLength = data.length; | |
94 response.writeBytes(data); | |
95 response.close(); | |
96 }).catchError((e) { | |
97 response.statusCode = 404; | |
98 response.contentLength = 0; | |
99 response.close(); | |
100 }); | |
101 } catch (e) { | 114 } catch (e) { |
102 currentSchedule.signalError(e); | 115 response.statusCode = 404; |
103 response.statusCode = 500; | 116 response.contentLength = 0; |
104 response.close(); | 117 response.close(); |
105 return; | 118 return; |
106 } | 119 } |
| 120 |
| 121 stream.toBytes().then((data) { |
| 122 response.statusCode = 200; |
| 123 response.contentLength = data.length; |
| 124 response.writeBytes(data); |
| 125 response.close(); |
| 126 }).catchError((e) { |
| 127 print("Exception while handling ${request.uri}: $e"); |
| 128 response.statusCode = 500; |
| 129 response.reasonPhrase = e.message; |
| 130 response.close(); |
| 131 }); |
107 }); | 132 }); |
108 _portCompleter.complete(_server.port); | 133 _portCompleter.complete(_server.port); |
109 currentSchedule.onComplete.schedule(_closeServer); | 134 _scheduleCleanup((_) => _closeServer()); |
110 return null; | 135 return null; |
111 }); | 136 }); |
112 }); | 137 }); |
113 }, 'starting a server serving:\n${baseDir.describe()}'); | 138 }); |
114 } | 139 } |
115 | 140 |
116 /// Closes [_server]. Returns a [Future] that will complete after the [_server] | 141 /// Closes [_server]. Returns a [Future] that will complete after the [_server] |
117 /// is closed. | 142 /// is closed. |
118 Future _closeServer() { | 143 Future _closeServer() { |
119 if (_server == null) return new Future.immediate(null); | 144 if (_server == null) return new Future.immediate(null); |
120 _server.close(); | 145 _server.close(); |
121 _server = null; | 146 _server = null; |
122 _portCompleterCache = null; | 147 _portCompleterCache = null; |
123 // TODO(nweiz): Remove this once issue 4155 is fixed. Pumping the event loop | 148 // TODO(nweiz): Remove this once issue 4155 is fixed. Pumping the event loop |
124 // *seems* to be enough to ensure that the server is actually closed, but I'm | 149 // *seems* to be enough to ensure that the server is actually closed, but I'm |
125 // putting this at 10ms to be safe. | 150 // putting this at 10ms to be safe. |
126 return sleep(10); | 151 return sleep(10); |
127 } | 152 } |
128 | 153 |
129 /// The [d.DirectoryDescriptor] describing the server layout of packages that | 154 /// The [DirectoryDescriptor] describing the server layout of packages that are |
130 /// are being served via [servePackages]. This is `null` if [servePackages] has | 155 /// being served via [servePackages]. This is `null` if [servePackages] has not |
131 /// not yet been called for this test. | 156 /// yet been called for this test. |
132 d.DirectoryDescriptor _servedPackageDir; | 157 DirectoryDescriptor _servedPackageDir; |
133 | 158 |
134 /// A map from package names to version numbers to YAML-serialized pubspecs for | 159 /// A map from package names to version numbers to YAML-serialized pubspecs for |
135 /// those packages. This represents the packages currently being served by | 160 /// those packages. This represents the packages currently being served by |
136 /// [servePackages], and is `null` if [servePackages] has not yet been called | 161 /// [servePackages], and is `null` if [servePackages] has not yet been called |
137 /// for this test. | 162 /// for this test. |
138 Map<String, Map<String, String>> _servedPackages; | 163 Map<String, Map<String, String>> _servedPackages; |
139 | 164 |
140 /// Creates an HTTP server that replicates the structure of pub.dartlang.org. | 165 /// Creates an HTTP server that replicates the structure of pub.dartlang.org. |
141 /// [pubspecs] is a list of unserialized pubspecs representing the packages to | 166 /// [pubspecs] is a list of unserialized pubspecs representing the packages to |
142 /// serve. | 167 /// serve. |
143 /// | 168 /// |
144 /// Subsequent calls to [servePackages] will add to the set of packages that | 169 /// Subsequent calls to [servePackages] will add to the set of packages that |
145 /// are being served. Previous packages will continue to be served. | 170 /// are being served. Previous packages will continue to be served. |
146 void servePackages(List<Map> pubspecs) { | 171 void servePackages(List<Map> pubspecs) { |
147 if (_servedPackages == null || _servedPackageDir == null) { | 172 if (_servedPackages == null || _servedPackageDir == null) { |
148 _servedPackages = <String, Map<String, String>>{}; | 173 _servedPackages = <String, Map<String, String>>{}; |
149 _servedPackageDir = d.dir('packages', []); | 174 _servedPackageDir = dir('packages', []); |
150 serve([_servedPackageDir]); | 175 serve([_servedPackageDir]); |
151 | 176 |
152 currentSchedule.onComplete.schedule(() { | 177 _scheduleCleanup((_) { |
153 _servedPackages = null; | 178 _servedPackages = null; |
154 _servedPackageDir = null; | 179 _servedPackageDir = null; |
155 }, 'cleaning up served packages'); | 180 }); |
156 } | 181 } |
157 | 182 |
158 schedule(() { | 183 _schedule((_) { |
159 return awaitObject(pubspecs).then((resolvedPubspecs) { | 184 return _awaitObject(pubspecs).then((resolvedPubspecs) { |
160 for (var spec in resolvedPubspecs) { | 185 for (var spec in resolvedPubspecs) { |
161 var name = spec['name']; | 186 var name = spec['name']; |
162 var version = spec['version']; | 187 var version = spec['version']; |
163 var versions = _servedPackages.putIfAbsent( | 188 var versions = _servedPackages.putIfAbsent( |
164 name, () => <String, String>{}); | 189 name, () => <String, String>{}); |
165 versions[version] = yaml(spec); | 190 versions[version] = yaml(spec); |
166 } | 191 } |
167 | 192 |
168 _servedPackageDir.contents.clear(); | 193 _servedPackageDir.contents.clear(); |
169 for (var name in _servedPackages.keys) { | 194 for (var name in _servedPackages.keys) { |
170 var versions = _servedPackages[name].keys.toList(); | 195 var versions = _servedPackages[name].keys.toList(); |
171 _servedPackageDir.contents.addAll([ | 196 _servedPackageDir.contents.addAll([ |
172 d.file('$name.json', json.stringify({'versions': versions})), | 197 file('$name.json', |
173 d.dir(name, [ | 198 json.stringify({'versions': versions})), |
174 d.dir('versions', flatten(versions.map((version) { | 199 dir(name, [ |
| 200 dir('versions', flatten(versions.map((version) { |
175 return [ | 201 return [ |
176 d.file('$version.yaml', _servedPackages[name][version]), | 202 file('$version.yaml', _servedPackages[name][version]), |
177 d.tar('$version.tar.gz', [ | 203 tar('$version.tar.gz', [ |
178 d.file('pubspec.yaml', _servedPackages[name][version]), | 204 file('pubspec.yaml', _servedPackages[name][version]), |
179 d.libDir(name, '$name $version') | 205 libDir(name, '$name $version') |
180 ]) | 206 ]) |
181 ]; | 207 ]; |
182 }))) | 208 }))) |
183 ]) | 209 ]) |
184 ]); | 210 ]); |
185 } | 211 } |
186 }); | 212 }); |
187 }, 'initializing the package server'); | 213 }); |
188 } | 214 } |
189 | 215 |
190 /// Converts [value] into a YAML string. | 216 /// Converts [value] into a YAML string. |
191 String yaml(value) => json.stringify(value); | 217 String yaml(value) => json.stringify(value); |
192 | 218 |
| 219 /// Describes a package that passes all validation. |
| 220 Descriptor get normalPackage => dir(appPath, [ |
| 221 libPubspec("test_pkg", "1.0.0"), |
| 222 file("LICENSE", "Eh, do what you want."), |
| 223 dir("lib", [ |
| 224 file("test_pkg.dart", "int i = 1;") |
| 225 ]) |
| 226 ]); |
| 227 |
| 228 /// Describes a file named `pubspec.yaml` with the given YAML-serialized |
| 229 /// [contents], which should be a serializable object. |
| 230 /// |
| 231 /// [contents] may contain [Future]s that resolve to serializable objects, |
| 232 /// which may in turn contain [Future]s recursively. |
| 233 Descriptor pubspec(Map contents) { |
| 234 return async(_awaitObject(contents).then((resolvedContents) => |
| 235 file("pubspec.yaml", yaml(resolvedContents)))); |
| 236 } |
| 237 |
| 238 /// Describes a file named `pubspec.yaml` for an application package with the |
| 239 /// given [dependencies]. |
| 240 Descriptor appPubspec(List dependencies) { |
| 241 return pubspec({ |
| 242 "name": "myapp", |
| 243 "dependencies": _dependencyListToMap(dependencies) |
| 244 }); |
| 245 } |
| 246 |
| 247 /// Describes a file named `pubspec.yaml` for a library package with the given |
| 248 /// [name], [version], and [deps]. If "sdk" is given, then it adds an SDK |
| 249 /// constraint on that version. |
| 250 Descriptor libPubspec(String name, String version, {List deps, String sdk}) { |
| 251 var map = package(name, version, deps); |
| 252 |
| 253 if (sdk != null) { |
| 254 map["environment"] = { |
| 255 "sdk": sdk |
| 256 }; |
| 257 } |
| 258 |
| 259 return pubspec(map); |
| 260 } |
| 261 |
| 262 /// Describes a directory named `lib` containing a single dart file named |
| 263 /// `<name>.dart` that contains a line of Dart code. |
| 264 Descriptor libDir(String name, [String code]) { |
| 265 // Default to printing the name if no other code was given. |
| 266 if (code == null) { |
| 267 code = name; |
| 268 } |
| 269 |
| 270 return dir("lib", [ |
| 271 file("$name.dart", 'main() => "$code";') |
| 272 ]); |
| 273 } |
| 274 |
| 275 /// Describes a map representing a library package with the given [name], |
| 276 /// [version], and [dependencies]. |
| 277 Map package(String name, String version, [List dependencies]) { |
| 278 var package = { |
| 279 "name": name, |
| 280 "version": version, |
| 281 "author": "Nathan Weizenbaum <nweiz@google.com>", |
| 282 "homepage": "http://pub.dartlang.org", |
| 283 "description": "A package, I guess." |
| 284 }; |
| 285 if (dependencies != null) { |
| 286 package["dependencies"] = _dependencyListToMap(dependencies); |
| 287 } |
| 288 return package; |
| 289 } |
| 290 |
| 291 /// Describes a map representing a dependency on a package in the package |
| 292 /// repository. |
| 293 Map dependency(String name, [String versionConstraint]) { |
| 294 var url = port.then((p) => "http://localhost:$p"); |
| 295 var dependency = {"hosted": {"name": name, "url": url}}; |
| 296 if (versionConstraint != null) dependency["version"] = versionConstraint; |
| 297 return dependency; |
| 298 } |
| 299 |
| 300 /// Describes a directory for a package installed from the mock package server. |
| 301 /// This directory is of the form found in the global package cache. |
| 302 DirectoryDescriptor packageCacheDir(String name, String version) { |
| 303 return dir("$name-$version", [ |
| 304 libDir(name, '$name $version') |
| 305 ]); |
| 306 } |
| 307 |
| 308 /// Describes a directory for a Git package. This directory is of the form |
| 309 /// found in the revision cache of the global package cache. |
| 310 DirectoryDescriptor gitPackageRevisionCacheDir(String name, [int modifier]) { |
| 311 var value = name; |
| 312 if (modifier != null) value = "$name $modifier"; |
| 313 return dir(new RegExp("$name${r'-[a-f0-9]+'}"), [ |
| 314 libDir(name, value) |
| 315 ]); |
| 316 } |
| 317 |
| 318 /// Describes a directory for a Git package. This directory is of the form |
| 319 /// found in the repo cache of the global package cache. |
| 320 DirectoryDescriptor gitPackageRepoCacheDir(String name) { |
| 321 return dir(new RegExp("$name${r'-[a-f0-9]+'}"), [ |
| 322 dir('hooks'), |
| 323 dir('info'), |
| 324 dir('objects'), |
| 325 dir('refs') |
| 326 ]); |
| 327 } |
| 328 |
| 329 /// Describes the `packages/` directory containing all the given [packages], |
| 330 /// which should be name/version pairs. The packages will be validated against |
| 331 /// the format produced by the mock package server. |
| 332 /// |
| 333 /// A package with a null version should not be installed. |
| 334 DirectoryDescriptor packagesDir(Map<String, String> packages) { |
| 335 var contents = <Descriptor>[]; |
| 336 packages.forEach((name, version) { |
| 337 if (version == null) { |
| 338 contents.add(nothing(name)); |
| 339 } else { |
| 340 contents.add(dir(name, [ |
| 341 file("$name.dart", 'main() => "$name $version";') |
| 342 ])); |
| 343 } |
| 344 }); |
| 345 return dir(packagesPath, contents); |
| 346 } |
| 347 |
| 348 /// Describes the global package cache directory containing all the given |
| 349 /// [packages], which should be name/version pairs. The packages will be |
| 350 /// validated against the format produced by the mock package server. |
| 351 /// |
| 352 /// A package's value may also be a list of versions, in which case all |
| 353 /// versions are expected to be installed. |
| 354 DirectoryDescriptor cacheDir(Map packages) { |
| 355 var contents = <Descriptor>[]; |
| 356 packages.forEach((name, versions) { |
| 357 if (versions is! List) versions = [versions]; |
| 358 for (var version in versions) { |
| 359 contents.add(packageCacheDir(name, version)); |
| 360 } |
| 361 }); |
| 362 return dir(cachePath, [ |
| 363 dir('hosted', [ |
| 364 async(port.then((p) => dir('localhost%58$p', contents))) |
| 365 ]) |
| 366 ]); |
| 367 } |
| 368 |
| 369 /// Describes the file in the system cache that contains the client's OAuth2 |
| 370 /// credentials. The URL "/token" on [server] will be used as the token |
| 371 /// endpoint for refreshing the access token. |
| 372 Descriptor credentialsFile( |
| 373 ScheduledServer server, |
| 374 String accessToken, |
| 375 {String refreshToken, |
| 376 DateTime expiration}) { |
| 377 return async(server.url.then((url) { |
| 378 return dir(cachePath, [ |
| 379 file('credentials.json', new oauth2.Credentials( |
| 380 accessToken, |
| 381 refreshToken, |
| 382 url.resolve('/token'), |
| 383 ['https://www.googleapis.com/auth/userinfo.email'], |
| 384 expiration).toJson()) |
| 385 ]); |
| 386 })); |
| 387 } |
| 388 |
| 389 /// Describes the application directory, containing only a pubspec specifying |
| 390 /// the given [dependencies]. |
| 391 DirectoryDescriptor appDir(List dependencies) => |
| 392 dir(appPath, [appPubspec(dependencies)]); |
| 393 |
| 394 /// Converts a list of dependencies as passed to [package] into a hash as used |
| 395 /// in a pubspec. |
| 396 Future<Map> _dependencyListToMap(List<Map> dependencies) { |
| 397 return _awaitObject(dependencies).then((resolvedDependencies) { |
| 398 var result = <String, Map>{}; |
| 399 for (var dependency in resolvedDependencies) { |
| 400 var keys = dependency.keys.where((key) => key != "version"); |
| 401 var sourceName = only(keys); |
| 402 var source; |
| 403 switch (sourceName) { |
| 404 case "git": |
| 405 source = new GitSource(); |
| 406 break; |
| 407 case "hosted": |
| 408 source = new HostedSource(); |
| 409 break; |
| 410 case "path": |
| 411 source = new PathSource(); |
| 412 break; |
| 413 default: |
| 414 throw 'Unknown source "$sourceName"'; |
| 415 } |
| 416 |
| 417 result[_packageName(sourceName, dependency[sourceName])] = dependency; |
| 418 } |
| 419 return result; |
| 420 }); |
| 421 } |
| 422 |
| 423 /// Return the name for the package described by [description] and from |
| 424 /// [sourceName]. |
| 425 String _packageName(String sourceName, description) { |
| 426 switch (sourceName) { |
| 427 case "git": |
| 428 var url = description is String ? description : description['url']; |
| 429 // TODO(rnystrom): Using path.basename on a URL is hacky. If we add URL |
| 430 // support to pkg/pathos, should use an explicit builder for that. |
| 431 return path.basename(url.replaceFirst(new RegExp(r"(\.git)?/?$"), "")); |
| 432 case "hosted": |
| 433 if (description is String) return description; |
| 434 return description['name']; |
| 435 case "path": |
| 436 return path.basename(description); |
| 437 case "sdk": |
| 438 return description; |
| 439 default: |
| 440 return description; |
| 441 } |
| 442 } |
| 443 |
193 /// The full path to the created sandbox directory for an integration test. | 444 /// The full path to the created sandbox directory for an integration test. |
194 String get sandboxDir => _sandboxDir; | 445 String get sandboxDir => _sandboxDir; |
195 String _sandboxDir; | 446 String _sandboxDir; |
196 | 447 |
197 /// The path of the package cache directory used for tests. Relative to the | 448 /// The path of the package cache directory used for tests. Relative to the |
198 /// sandbox directory. | 449 /// sandbox directory. |
199 final String cachePath = "cache"; | 450 final String cachePath = "cache"; |
200 | 451 |
201 /// The path of the mock SDK directory used for tests. Relative to the sandbox | 452 /// The path of the mock SDK directory used for tests. Relative to the sandbox |
202 /// directory. | 453 /// directory. |
203 final String sdkPath = "sdk"; | 454 final String sdkPath = "sdk"; |
204 | 455 |
205 /// The path of the mock app directory used for tests. Relative to the sandbox | 456 /// The path of the mock app directory used for tests. Relative to the sandbox |
206 /// directory. | 457 /// directory. |
207 final String appPath = "myapp"; | 458 final String appPath = "myapp"; |
208 | 459 |
209 /// The path of the packages directory in the mock app used for tests. Relative | 460 /// The path of the packages directory in the mock app used for tests. Relative |
210 /// to the sandbox directory. | 461 /// to the sandbox directory. |
211 final String packagesPath = "$appPath/packages"; | 462 final String packagesPath = "$appPath/packages"; |
212 | 463 |
| 464 /// The type for callbacks that will be fired during [schedulePub]. Takes the |
| 465 /// sandbox directory as a parameter. |
| 466 typedef Future _ScheduledEvent(String parentDir); |
| 467 |
| 468 /// The list of events that are scheduled to run as part of the test case. |
| 469 Queue<_ScheduledEvent> _scheduled; |
| 470 |
| 471 /// The list of events that are scheduled to run after the test case, even if |
| 472 /// it failed. |
| 473 Queue<_ScheduledEvent> _scheduledCleanup; |
| 474 |
| 475 /// The list of events that are scheduled to run after the test case only if it |
| 476 /// failed. |
| 477 Queue<_ScheduledEvent> _scheduledOnException; |
| 478 |
213 /// Set to true when the current batch of scheduled events should be aborted. | 479 /// Set to true when the current batch of scheduled events should be aborted. |
214 bool _abortScheduled = false; | 480 bool _abortScheduled = false; |
215 | 481 |
216 /// The time (in milliseconds) to wait for the entire scheduled test to | 482 /// The time (in milliseconds) to wait for the entire scheduled test to |
217 /// complete. | 483 /// complete. |
218 final _TIMEOUT = 30000; | 484 final _TIMEOUT = 30000; |
219 | 485 |
220 /// Defines an integration test. The [body] should schedule a series of | 486 /// Defines an integration test. The [body] should schedule a series of |
221 /// operations which will be run asynchronously. | 487 /// operations which will be run asynchronously. |
222 void integration(String description, void body()) => | 488 void integration(String description, void body()) => |
223 _integration(description, body, test); | 489 _integration(description, body, test); |
224 | 490 |
225 /// Like [integration], but causes only this test to run. | 491 /// Like [integration], but causes only this test to run. |
226 void solo_integration(String description, void body()) => | 492 void solo_integration(String description, void body()) => |
227 _integration(description, body, solo_test); | 493 _integration(description, body, solo_test); |
228 | 494 |
229 void _integration(String description, void body(), [Function testFn]) { | 495 void _integration(String description, void body(), [Function testFn]) { |
230 testFn(description, () { | 496 testFn(description, () { |
231 // Ensure the SDK version is always available. | 497 // Ensure the SDK version is always available. |
232 d.dir(sdkPath, [ | 498 dir(sdkPath, [ |
233 d.file('version', '0.1.2.3') | 499 file('version', '0.1.2.3') |
234 ]).create(); | 500 ]).scheduleCreate(); |
235 | 501 |
236 _sandboxDir = createTempDir(); | 502 _sandboxDir = createTempDir(); |
237 d.defaultRoot = sandboxDir; | |
238 currentSchedule.onComplete.schedule(() => deleteDir(_sandboxDir), | |
239 'deleting the sandbox directory'); | |
240 | 503 |
241 // Schedule the test. | 504 // Schedule the test. |
242 body(); | 505 body(); |
| 506 |
| 507 // Run all of the scheduled tasks. If an error occurs, it will propagate |
| 508 // through the futures back up to here where we can hand it off to unittest. |
| 509 var asyncDone = expectAsync0(() {}); |
| 510 return timeout(_runScheduled(_scheduled), |
| 511 _TIMEOUT, 'waiting for a test to complete').catchError((e) { |
| 512 return _runScheduled(_scheduledOnException).then((_) { |
| 513 // Rethrow the original error so it keeps propagating. |
| 514 throw e; |
| 515 }); |
| 516 }).whenComplete(() { |
| 517 // Clean up after ourselves. Do this first before reporting back to |
| 518 // unittest because it will advance to the next test immediately. |
| 519 return _runScheduled(_scheduledCleanup).then((_) { |
| 520 _scheduled = null; |
| 521 _scheduledCleanup = null; |
| 522 _scheduledOnException = null; |
| 523 if (_sandboxDir != null) { |
| 524 var dir = _sandboxDir; |
| 525 _sandboxDir = null; |
| 526 return deleteDir(dir); |
| 527 } |
| 528 }); |
| 529 }).then((_) { |
| 530 // If we got here, the test completed successfully so tell unittest so. |
| 531 asyncDone(); |
| 532 }).catchError((e) { |
| 533 // If we got here, an error occurred. We will register it with unittest |
| 534 // directly so that the error message isn't wrapped in any matcher stuff. |
| 535 // We do this call last because it will cause unittest to *synchronously* |
| 536 // advance to the next test and run it. |
| 537 registerException(e.error, e.stackTrace); |
| 538 }); |
243 }); | 539 }); |
244 } | 540 } |
245 | 541 |
246 /// Get the path to the root "util/test/pub" directory containing the pub | 542 /// Get the path to the root "util/test/pub" directory containing the pub |
247 /// tests. | 543 /// tests. |
248 String get testDirectory { | 544 String get testDirectory { |
249 var dir = new Options().script; | 545 var dir = new Options().script; |
250 while (path.basename(dir) != 'pub') dir = path.dirname(dir); | 546 while (path.basename(dir) != 'pub') dir = path.dirname(dir); |
251 | 547 |
252 return path.absolute(dir); | 548 return path.absolute(dir); |
253 } | 549 } |
254 | 550 |
255 /// Schedules renaming (moving) the directory at [from] to [to], both of which | 551 /// Schedules renaming (moving) the directory at [from] to [to], both of which |
256 /// are assumed to be relative to [sandboxDir]. | 552 /// are assumed to be relative to [sandboxDir]. |
257 void scheduleRename(String from, String to) { | 553 void scheduleRename(String from, String to) { |
258 schedule( | 554 _schedule((sandboxDir) { |
259 () => renameDir( | 555 return renameDir(path.join(sandboxDir, from), path.join(sandboxDir, to)); |
260 path.join(sandboxDir, from), | 556 }); |
261 path.join(sandboxDir, to)), | |
262 'renaming $from to $to'); | |
263 } | 557 } |
264 | 558 |
| 559 |
265 /// Schedules creating a symlink at path [symlink] that points to [target], | 560 /// Schedules creating a symlink at path [symlink] that points to [target], |
266 /// both of which are assumed to be relative to [sandboxDir]. | 561 /// both of which are assumed to be relative to [sandboxDir]. |
267 void scheduleSymlink(String target, String symlink) { | 562 void scheduleSymlink(String target, String symlink) { |
268 schedule( | 563 _schedule((sandboxDir) { |
269 () => createSymlink( | 564 return createSymlink(path.join(sandboxDir, target), |
270 path.join(sandboxDir, target), | 565 path.join(sandboxDir, symlink)); |
271 path.join(sandboxDir, symlink)), | 566 }); |
272 'symlinking $target to $symlink'); | |
273 } | 567 } |
274 | 568 |
275 /// Schedules a call to the Pub command-line utility. Runs Pub with [args] and | 569 /// Schedules a call to the Pub command-line utility. Runs Pub with [args] and |
276 /// validates that its results match [output], [error], and [exitCode]. | 570 /// validates that its results match [output], [error], and [exitCode]. |
277 void schedulePub({List args, Pattern output, Pattern error, | 571 void schedulePub({List args, Pattern output, Pattern error, |
278 Future<Uri> tokenEndpoint, int exitCode: 0}) { | 572 Future<Uri> tokenEndpoint, int exitCode: 0}) { |
279 var pub = startPub(args: args, tokenEndpoint: tokenEndpoint); | 573 _schedule((sandboxDir) { |
280 pub.shouldExit(exitCode); | 574 return _doPub(runProcess, sandboxDir, args, tokenEndpoint).then((result) { |
| 575 var failures = []; |
281 | 576 |
282 expect(Future.wait([ | 577 _validateOutput(failures, 'stdout', output, result.stdout); |
283 pub.remainingStdout(), | 578 _validateOutput(failures, 'stderr', error, result.stderr); |
284 pub.remainingStderr() | 579 |
285 ]).then((results) { | 580 if (result.exitCode != exitCode) { |
286 var failures = []; | 581 failures.add( |
287 _validateOutput(failures, 'stdout', output, results[0].split('\n')); | 582 'Pub returned exit code ${result.exitCode}, expected $exitCode.'); |
288 _validateOutput(failures, 'stderr', error, results[1].split('\n')); | 583 } |
289 if (!failures.isEmpty) throw new TestFailure(failures.join('\n')); | 584 |
290 }), completes); | 585 if (failures.length > 0) { |
| 586 if (error == null) { |
| 587 // If we aren't validating the error, still show it on failure. |
| 588 failures.add('Pub stderr:'); |
| 589 failures.addAll(result.stderr.map((line) => '| $line')); |
| 590 } |
| 591 |
| 592 throw new TestFailure(failures.join('\n')); |
| 593 } |
| 594 |
| 595 return null; |
| 596 }); |
| 597 }); |
| 598 } |
| 599 |
| 600 /// Starts a Pub process and returns a [ScheduledProcess] that supports |
| 601 /// interaction with that process. |
| 602 /// |
| 603 /// Any futures in [args] will be resolved before the process is started. |
| 604 ScheduledProcess startPub({List args, Future<Uri> tokenEndpoint}) { |
| 605 var process = _scheduleValue((sandboxDir) => |
| 606 _doPub(startProcess, sandboxDir, args, tokenEndpoint)); |
| 607 return new ScheduledProcess("pub", process); |
291 } | 608 } |
292 | 609 |
293 /// Like [startPub], but runs `pub lish` in particular with [server] used both | 610 /// Like [startPub], but runs `pub lish` in particular with [server] used both |
294 /// as the OAuth2 server (with "/token" as the token endpoint) and as the | 611 /// as the OAuth2 server (with "/token" as the token endpoint) and as the |
295 /// package server. | 612 /// package server. |
296 /// | 613 /// |
297 /// Any futures in [args] will be resolved before the process is started. | 614 /// Any futures in [args] will be resolved before the process is started. |
298 ScheduledProcess startPublish(ScheduledServer server, {List args}) { | 615 ScheduledProcess startPubLish(ScheduledServer server, {List args}) { |
299 var tokenEndpoint = server.url.then((url) => | 616 var tokenEndpoint = server.url.then((url) => |
300 url.resolve('/token').toString()); | 617 url.resolve('/token').toString()); |
301 if (args == null) args = []; | 618 if (args == null) args = []; |
302 args = flatten(['lish', '--server', tokenEndpoint, args]); | 619 args = flatten(['lish', '--server', tokenEndpoint, args]); |
303 return startPub(args: args, tokenEndpoint: tokenEndpoint); | 620 return startPub(args: args, tokenEndpoint: tokenEndpoint); |
304 } | 621 } |
305 | 622 |
306 /// Handles the beginning confirmation process for uploading a packages. | 623 /// Handles the beginning confirmation process for uploading a packages. |
307 /// Ensures that the right output is shown and then enters "y" to confirm the | 624 /// Ensures that the right output is shown and then enters "y" to confirm the |
308 /// upload. | 625 /// upload. |
309 void confirmPublish(ScheduledProcess pub) { | 626 void confirmPublish(ScheduledProcess pub) { |
310 // TODO(rnystrom): This is overly specific and inflexible regarding different | 627 // TODO(rnystrom): This is overly specific and inflexible regarding different |
311 // test packages. Should validate this a little more loosely. | 628 // test packages. Should validate this a little more loosely. |
312 expect(pub.nextLine(), completion(equals('Publishing "test_pkg" 1.0.0:'))); | 629 expectLater(pub.nextLine(), equals('Publishing "test_pkg" 1.0.0:')); |
313 expect(pub.nextLine(), completion(equals("|-- LICENSE"))); | 630 expectLater(pub.nextLine(), equals("|-- LICENSE")); |
314 expect(pub.nextLine(), completion(equals("|-- lib"))); | 631 expectLater(pub.nextLine(), equals("|-- lib")); |
315 expect(pub.nextLine(), completion(equals("| '-- test_pkg.dart"))); | 632 expectLater(pub.nextLine(), equals("| '-- test_pkg.dart")); |
316 expect(pub.nextLine(), completion(equals("'-- pubspec.yaml"))); | 633 expectLater(pub.nextLine(), equals("'-- pubspec.yaml")); |
317 expect(pub.nextLine(), completion(equals(""))); | 634 expectLater(pub.nextLine(), equals("")); |
318 | 635 |
319 pub.writeLine("y"); | 636 pub.writeLine("y"); |
320 } | 637 } |
321 | 638 |
322 /// Starts a Pub process and returns a [ScheduledProcess] that supports | 639 /// Calls [fn] with appropriately modified arguments to run a pub process. [fn] |
323 /// interaction with that process. | 640 /// should have the same signature as [startProcess], except that the returned |
324 /// | 641 /// [Future] may have a type other than [Process]. |
325 /// Any futures in [args] will be resolved before the process is started. | 642 Future _doPub(Function fn, sandboxDir, List args, Future<Uri> tokenEndpoint) { |
326 ScheduledProcess startPub({List args, Future<Uri> tokenEndpoint}) { | |
327 String pathInSandbox(String relPath) { | 643 String pathInSandbox(String relPath) { |
328 return path.join(path.absolute(sandboxDir), relPath); | 644 return path.join(path.absolute(sandboxDir), relPath); |
329 } | 645 } |
330 | 646 |
331 ensureDir(pathInSandbox(appPath)); | 647 return defer(() { |
| 648 ensureDir(pathInSandbox(appPath)); |
| 649 return Future.wait([ |
| 650 _awaitObject(args), |
| 651 tokenEndpoint == null ? new Future.immediate(null) : tokenEndpoint |
| 652 ]); |
| 653 }).then((results) { |
| 654 var args = results[0]; |
| 655 var tokenEndpoint = results[1]; |
| 656 // Find a Dart executable we can use to spawn. Use the same one that was |
| 657 // used to run this script itself. |
| 658 var dartBin = new Options().executable; |
332 | 659 |
333 // Find a Dart executable we can use to spawn. Use the same one that was | 660 // If the executable looks like a path, get its full path. That way we |
334 // used to run this script itself. | 661 // can still find it when we spawn it with a different working directory. |
335 var dartBin = new Options().executable; | 662 if (dartBin.contains(Platform.pathSeparator)) { |
| 663 dartBin = new File(dartBin).fullPathSync(); |
| 664 } |
336 | 665 |
337 // If the executable looks like a path, get its full path. That way we | 666 // Find the main pub entrypoint. |
338 // can still find it when we spawn it with a different working directory. | 667 var pubPath = fs.joinPaths(testDirectory, '../../pub/pub.dart'); |
339 if (dartBin.contains(Platform.pathSeparator)) { | |
340 dartBin = new File(dartBin).fullPathSync(); | |
341 } | |
342 | 668 |
343 // Find the main pub entrypoint. | 669 var dartArgs = ['--checked', pubPath, '--trace']; |
344 var pubPath = fs.joinPaths(testDirectory, '../../pub/pub.dart'); | 670 dartArgs.addAll(args); |
345 | 671 |
346 var dartArgs = ['--checked', pubPath, '--trace']; | 672 var environment = { |
347 dartArgs.addAll(args); | |
348 | |
349 if (tokenEndpoint == null) tokenEndpoint = new Future.immediate(null); | |
350 var optionsFuture = tokenEndpoint.then((tokenEndpoint) { | |
351 var options = new ProcessOptions(); | |
352 options.workingDirectory = pathInSandbox(appPath); | |
353 options.environment = { | |
354 'PUB_CACHE': pathInSandbox(cachePath), | 673 'PUB_CACHE': pathInSandbox(cachePath), |
355 'DART_SDK': pathInSandbox(sdkPath) | 674 'DART_SDK': pathInSandbox(sdkPath) |
356 }; | 675 }; |
| 676 |
357 if (tokenEndpoint != null) { | 677 if (tokenEndpoint != null) { |
358 options.environment['_PUB_TEST_TOKEN_ENDPOINT'] = | 678 environment['_PUB_TEST_TOKEN_ENDPOINT'] = tokenEndpoint.toString(); |
359 tokenEndpoint.toString(); | |
360 } | 679 } |
361 return options; | 680 |
| 681 return fn(dartBin, dartArgs, workingDir: pathInSandbox(appPath), |
| 682 environment: environment); |
362 }); | 683 }); |
363 | |
364 return new ScheduledProcess.start(dartBin, dartArgs, options: optionsFuture, | |
365 description: args.isEmpty ? 'pub' : 'pub ${args.first}'); | |
366 } | 684 } |
367 | 685 |
368 /// Skips the current test if Git is not installed. This validates that the | 686 /// Skips the current test if Git is not installed. This validates that the |
369 /// current test is running on a buildbot in which case we expect git to be | 687 /// current test is running on a buildbot in which case we expect git to be |
370 /// installed. If we are not running on the buildbot, we will instead see if | 688 /// installed. If we are not running on the buildbot, we will instead see if |
371 /// git is installed and skip the test if not. This way, users don't need to | 689 /// git is installed and skip the test if not. This way, users don't need to |
372 /// have git installed to run the tests locally (unless they actually care | 690 /// have git installed to run the tests locally (unless they actually care |
373 /// about the pub git tests). | 691 /// about the pub git tests). |
374 void ensureGit() { | 692 void ensureGit() { |
375 schedule(() { | 693 _schedule((_) { |
376 return gitlib.isInstalled.then((installed) { | 694 return gitlib.isInstalled.then((installed) { |
377 if (installed) return; | 695 if (!installed && |
378 if (Platform.environment.containsKey('BUILDBOT_BUILDERNAME')) return; | 696 !Platform.environment.containsKey('BUILDBOT_BUILDERNAME')) { |
379 currentSchedule.abort(); | 697 _abortScheduled = true; |
| 698 } |
| 699 return null; |
380 }); | 700 }); |
381 }, 'ensuring that Git is installed'); | 701 }); |
382 } | 702 } |
383 | 703 |
384 /// Use [client] as the mock HTTP client for this test. | 704 /// Use [client] as the mock HTTP client for this test. |
385 /// | 705 /// |
386 /// Note that this will only affect HTTP requests made via http.dart in the | 706 /// Note that this will only affect HTTP requests made via http.dart in the |
387 /// parent process. | 707 /// parent process. |
388 void useMockClient(MockClient client) { | 708 void useMockClient(MockClient client) { |
389 var oldInnerClient = httpClient.inner; | 709 var oldInnerClient = httpClient.inner; |
390 httpClient.inner = client; | 710 httpClient.inner = client; |
391 currentSchedule.onComplete.schedule(() { | 711 _scheduleCleanup((_) { |
392 httpClient.inner = oldInnerClient; | 712 httpClient.inner = oldInnerClient; |
393 }, 'de-activating the mock client'); | |
394 } | |
395 | |
396 /// Describes a map representing a library package with the given [name], | |
397 /// [version], and [dependencies]. | |
398 Map packageMap(String name, String version, [List dependencies]) { | |
399 var package = { | |
400 "name": name, | |
401 "version": version, | |
402 "author": "Nathan Weizenbaum <nweiz@google.com>", | |
403 "homepage": "http://pub.dartlang.org", | |
404 "description": "A package, I guess." | |
405 }; | |
406 if (dependencies != null) { | |
407 package["dependencies"] = dependencyListToMap(dependencies); | |
408 } | |
409 return package; | |
410 } | |
411 | |
412 /// Describes a map representing a dependency on a package in the package | |
413 /// repository. | |
414 Map dependencyMap(String name, [String versionConstraint]) { | |
415 var url = port.then((p) => "http://localhost:$p"); | |
416 var dependency = {"hosted": {"name": name, "url": url}}; | |
417 if (versionConstraint != null) dependency["version"] = versionConstraint; | |
418 return dependency; | |
419 } | |
420 | |
421 /// Converts a list of dependencies as passed to [package] into a hash as used | |
422 /// in a pubspec. | |
423 Future<Map> dependencyListToMap(List<Map> dependencies) { | |
424 return awaitObject(dependencies).then((resolvedDependencies) { | |
425 var result = <String, Map>{}; | |
426 for (var dependency in resolvedDependencies) { | |
427 var keys = dependency.keys.where((key) => key != "version"); | |
428 var sourceName = only(keys); | |
429 var source; | |
430 switch (sourceName) { | |
431 case "git": | |
432 source = new GitSource(); | |
433 break; | |
434 case "hosted": | |
435 source = new HostedSource(); | |
436 break; | |
437 case "path": | |
438 source = new PathSource(); | |
439 break; | |
440 default: | |
441 throw 'Unknown source "$sourceName"'; | |
442 } | |
443 | |
444 result[_packageName(sourceName, dependency[sourceName])] = dependency; | |
445 } | |
446 return result; | |
447 }); | 713 }); |
448 } | 714 } |
449 | 715 |
450 /// Return the name for the package described by [description] and from | 716 Future _runScheduled(Queue<_ScheduledEvent> scheduled) { |
451 /// [sourceName]. | 717 if (scheduled == null) return new Future.immediate(null); |
452 String _packageName(String sourceName, description) { | 718 |
453 switch (sourceName) { | 719 Future runNextEvent(_) { |
454 case "git": | 720 if (_abortScheduled || scheduled.isEmpty) { |
455 var url = description is String ? description : description['url']; | 721 _abortScheduled = false; |
456 // TODO(rnystrom): Using path.basename on a URL is hacky. If we add URL | 722 return new Future.immediate(null); |
457 // support to pkg/pathos, should use an explicit builder for that. | 723 } |
458 return path.basename(url.replaceFirst(new RegExp(r"(\.git)?/?$"), "")); | 724 |
459 case "hosted": | 725 var future = scheduled.removeFirst()(_sandboxDir); |
460 if (description is String) return description; | 726 if (future != null) { |
461 return description['name']; | 727 return future.then(runNextEvent); |
462 case "path": | 728 } else { |
463 return path.basename(description); | 729 return runNextEvent(null); |
464 case "sdk": | 730 } |
465 return description; | |
466 default: | |
467 return description; | |
468 } | 731 } |
| 732 |
| 733 return runNextEvent(null); |
469 } | 734 } |
470 | 735 |
471 /// Compares the [actual] output from running pub with [expected]. For [String] | 736 /// Compares the [actual] output from running pub with [expected]. For [String] |
472 /// patterns, ignores leading and trailing whitespace differences and tries to | 737 /// patterns, ignores leading and trailing whitespace differences and tries to |
473 /// report the offending difference in a nice way. For other [Pattern]s, just | 738 /// report the offending difference in a nice way. For other [Pattern]s, just |
474 /// reports whether the output contained the pattern. | 739 /// reports whether the output contained the pattern. |
475 void _validateOutput(List<String> failures, String pipe, Pattern expected, | 740 void _validateOutput(List<String> failures, String pipe, Pattern expected, |
476 List<String> actual) { | 741 List<String> actual) { |
477 if (expected == null) return; | 742 if (expected == null) return; |
478 | 743 |
(...skipping 59 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
538 | 803 |
539 // If any lines mismatched, show the expected and actual. | 804 // If any lines mismatched, show the expected and actual. |
540 if (failed) { | 805 if (failed) { |
541 failures.add('Expected $pipe:'); | 806 failures.add('Expected $pipe:'); |
542 failures.addAll(expected.map((line) => '| $line')); | 807 failures.addAll(expected.map((line) => '| $line')); |
543 failures.add('Got:'); | 808 failures.add('Got:'); |
544 failures.addAll(results); | 809 failures.addAll(results); |
545 } | 810 } |
546 } | 811 } |
547 | 812 |
| 813 /// Base class for [FileDescriptor] and [DirectoryDescriptor] so that a |
| 814 /// directory can contain a heterogeneous collection of files and |
| 815 /// subdirectories. |
| 816 abstract class Descriptor { |
| 817 /// The name of this file or directory. This must be a [String] if the file |
| 818 /// or directory is going to be created. |
| 819 final Pattern name; |
| 820 |
| 821 Descriptor(this.name); |
| 822 |
| 823 /// Creates the file or directory within [dir]. Returns a [Future] that is |
| 824 /// completed after the creation is done. |
| 825 Future create(dir); |
| 826 |
| 827 /// Validates that this descriptor correctly matches the corresponding file |
| 828 /// system entry within [dir]. Returns a [Future] that completes to `null` if |
| 829 /// the entry is valid, or throws an error if it failed. |
| 830 Future validate(String dir); |
| 831 |
| 832 /// Deletes the file or directory within [dir]. Returns a [Future] that is |
| 833 /// completed after the deletion is done. |
| 834 Future delete(String dir); |
| 835 |
| 836 /// Loads the file at [path] from within this descriptor. If [path] is empty, |
| 837 /// loads the contents of the descriptor itself. |
| 838 ByteStream load(List<String> path); |
| 839 |
| 840 /// Schedules the directory to be created before Pub is run with |
| 841 /// [schedulePub]. The directory will be created relative to the sandbox |
| 842 /// directory. |
| 843 // TODO(nweiz): Use implicit closurization once issue 2984 is fixed. |
| 844 void scheduleCreate() => _schedule((dir) => this.create(dir)); |
| 845 |
| 846 /// Schedules the file or directory to be deleted recursively. |
| 847 void scheduleDelete() => _schedule((dir) => this.delete(dir)); |
| 848 |
| 849 /// Schedules the directory to be validated after Pub is run with |
| 850 /// [schedulePub]. The directory will be validated relative to the sandbox |
| 851 /// directory. |
| 852 void scheduleValidate() => _schedule((parentDir) => validate(parentDir)); |
| 853 |
| 854 /// Asserts that the name of the descriptor is a [String] and returns it. |
| 855 String get _stringName { |
| 856 if (name is String) return name; |
| 857 throw 'Pattern $name must be a string.'; |
| 858 } |
| 859 |
| 860 /// Validates that at least one file in [dir] matching [name] is valid |
| 861 /// according to [validate]. [validate] should throw or complete to an |
| 862 /// exception if the input path is invalid. |
| 863 Future _validateOneMatch(String dir, Future validate(String entry)) { |
| 864 // Special-case strings to support multi-level names like "myapp/packages". |
| 865 if (name is String) { |
| 866 var entry = path.join(dir, name); |
| 867 return defer(() { |
| 868 if (!entryExists(entry)) { |
| 869 throw new TestFailure('Entry $entry not found.'); |
| 870 } |
| 871 return validate(entry); |
| 872 }); |
| 873 } |
| 874 |
| 875 // TODO(nweiz): remove this when issue 4061 is fixed. |
| 876 var stackTrace; |
| 877 try { |
| 878 throw ""; |
| 879 } catch (_, localStackTrace) { |
| 880 stackTrace = localStackTrace; |
| 881 } |
| 882 |
| 883 return listDir(dir).then((files) { |
| 884 var matches = files.where((file) => endsWithPattern(file, name)).toList(); |
| 885 if (matches.isEmpty) { |
| 886 throw new TestFailure('No files in $dir match pattern $name.'); |
| 887 } |
| 888 if (matches.length == 1) return validate(matches[0]); |
| 889 |
| 890 var failures = []; |
| 891 var successes = 0; |
| 892 var completer = new Completer(); |
| 893 checkComplete() { |
| 894 if (failures.length + successes != matches.length) return; |
| 895 if (successes > 0) { |
| 896 completer.complete(); |
| 897 return; |
| 898 } |
| 899 |
| 900 var error = new StringBuffer(); |
| 901 error.write("No files named $name in $dir were valid:\n"); |
| 902 for (var failure in failures) { |
| 903 error.write(" $failure\n"); |
| 904 } |
| 905 completer.completeError( |
| 906 new TestFailure(error.toString()), stackTrace); |
| 907 } |
| 908 |
| 909 for (var match in matches) { |
| 910 var future = validate(match).then((_) { |
| 911 successes++; |
| 912 checkComplete(); |
| 913 }).catchError((e) { |
| 914 failures.add(e); |
| 915 checkComplete(); |
| 916 }); |
| 917 } |
| 918 return completer.future; |
| 919 }); |
| 920 } |
| 921 } |
| 922 |
| 923 /// Describes a file. These are used both for setting up an expected directory |
| 924 /// tree before running a test, and for validating that the file system matches |
| 925 /// some expectations after running it. |
| 926 class FileDescriptor extends Descriptor { |
| 927 /// The contents of the file, in bytes. |
| 928 final List<int> contents; |
| 929 |
| 930 String get textContents => new String.fromCharCodes(contents); |
| 931 |
| 932 FileDescriptor.bytes(Pattern name, this.contents) : super(name); |
| 933 |
| 934 FileDescriptor(Pattern name, String contents) : |
| 935 this.bytes(name, encodeUtf8(contents)); |
| 936 |
| 937 /// Creates the file within [dir]. Returns a [Future] that is completed after |
| 938 /// the creation is done. |
| 939 Future<String> create(dir) => |
| 940 defer(() => writeBinaryFile(path.join(dir, _stringName), contents)); |
| 941 |
| 942 /// Deletes the file within [dir]. Returns a [Future] that is completed after |
| 943 /// the deletion is done. |
| 944 Future delete(dir) => |
| 945 defer(() => deleteFile(path.join(dir, _stringName))); |
| 946 |
| 947 /// Validates that this file correctly matches the actual file at [path]. |
| 948 Future validate(String path) { |
| 949 return _validateOneMatch(path, (file) { |
| 950 var text = readTextFile(file); |
| 951 if (text == textContents) return null; |
| 952 |
| 953 throw new TestFailure( |
| 954 'File $file should contain:\n\n$textContents\n\n' |
| 955 'but contained:\n\n$text'); |
| 956 }); |
| 957 } |
| 958 |
| 959 /// Loads the contents of the file. |
| 960 ByteStream load(List<String> path) { |
| 961 if (!path.isEmpty) { |
| 962 throw "Can't load ${path.join('/')} from within $name: not a directory."; |
| 963 } |
| 964 |
| 965 return new ByteStream.fromBytes(contents); |
| 966 } |
| 967 } |
| 968 |
| 969 /// Describes a directory and its contents. These are used both for setting up |
| 970 /// an expected directory tree before running a test, and for validating that |
| 971 /// the file system matches some expectations after running it. |
| 972 class DirectoryDescriptor extends Descriptor { |
| 973 /// The files and directories contained in this directory. |
| 974 final List<Descriptor> contents; |
| 975 |
| 976 DirectoryDescriptor(Pattern name, List<Descriptor> contents) |
| 977 : this.contents = contents == null ? <Descriptor>[] : contents, |
| 978 super(name); |
| 979 |
| 980 /// Creates the file within [dir]. Returns a [Future] that is completed after |
| 981 /// the creation is done. |
| 982 Future<String> create(parentDir) { |
| 983 return defer(() { |
| 984 // Create the directory. |
| 985 var dir = ensureDir(path.join(parentDir, _stringName)); |
| 986 if (contents == null) return dir; |
| 987 |
| 988 // Recursively create all of its children. |
| 989 var childFutures = contents.map((child) => child.create(dir)).toList(); |
| 990 // Only complete once all of the children have been created too. |
| 991 return Future.wait(childFutures).then((_) => dir); |
| 992 }); |
| 993 } |
| 994 |
| 995 /// Deletes the directory within [dir]. Returns a [Future] that is completed |
| 996 /// after the deletion is done. |
| 997 Future delete(dir) { |
| 998 return deleteDir(path.join(dir, _stringName)); |
| 999 } |
| 1000 |
| 1001 /// Validates that the directory at [path] contains all of the expected |
| 1002 /// contents in this descriptor. Note that this does *not* check that the |
| 1003 /// directory doesn't contain other unexpected stuff, just that it *does* |
| 1004 /// contain the stuff we do expect. |
| 1005 Future validate(String path) { |
| 1006 return _validateOneMatch(path, (dir) { |
| 1007 // Validate each of the items in this directory. |
| 1008 final entryFutures = |
| 1009 contents.map((entry) => entry.validate(dir)).toList(); |
| 1010 |
| 1011 // If they are all valid, the directory is valid. |
| 1012 return Future.wait(entryFutures).then((entries) => null); |
| 1013 }); |
| 1014 } |
| 1015 |
| 1016 /// Loads [path] from within this directory. |
| 1017 ByteStream load(List<String> path) { |
| 1018 if (path.isEmpty) { |
| 1019 throw "Can't load the contents of $name: is a directory."; |
| 1020 } |
| 1021 |
| 1022 for (var descriptor in contents) { |
| 1023 if (descriptor.name == path[0]) { |
| 1024 return descriptor.load(path.sublist(1)); |
| 1025 } |
| 1026 } |
| 1027 |
| 1028 throw "Directory $name doesn't contain ${path.join('/')}."; |
| 1029 } |
| 1030 } |
| 1031 |
| 1032 /// Wraps a [Future] that will complete to a [Descriptor] and makes it behave |
| 1033 /// like a concrete [Descriptor]. This is necessary when the contents of the |
| 1034 /// descriptor depends on information that's not available until part of the |
| 1035 /// test run is completed. |
| 1036 class FutureDescriptor extends Descriptor { |
| 1037 Future<Descriptor> _future; |
| 1038 |
| 1039 FutureDescriptor(this._future) : super('<unknown>'); |
| 1040 |
| 1041 Future create(dir) => _future.then((desc) => desc.create(dir)); |
| 1042 |
| 1043 Future validate(dir) => _future.then((desc) => desc.validate(dir)); |
| 1044 |
| 1045 Future delete(dir) => _future.then((desc) => desc.delete(dir)); |
| 1046 |
| 1047 ByteStream load(List<String> path) { |
| 1048 var controller = new StreamController<List<int>>(); |
| 1049 _future.then((desc) => store(desc.load(path), controller)); |
| 1050 return new ByteStream(controller.stream); |
| 1051 } |
| 1052 } |
| 1053 |
| 1054 /// Describes a Git repository and its contents. |
| 1055 class GitRepoDescriptor extends DirectoryDescriptor { |
| 1056 GitRepoDescriptor(Pattern name, List<Descriptor> contents) |
| 1057 : super(name, contents); |
| 1058 |
| 1059 /// Creates the Git repository and commits the contents. |
| 1060 Future create(parentDir) { |
| 1061 return _runGitCommands(parentDir, [ |
| 1062 ['init'], |
| 1063 ['add', '.'], |
| 1064 ['commit', '-m', 'initial commit'] |
| 1065 ]); |
| 1066 } |
| 1067 |
| 1068 /// Commits any changes to the Git repository. |
| 1069 Future commit(parentDir) { |
| 1070 return _runGitCommands(parentDir, [ |
| 1071 ['add', '.'], |
| 1072 ['commit', '-m', 'update'] |
| 1073 ]); |
| 1074 } |
| 1075 |
| 1076 /// Schedules changes to be committed to the Git repository. |
| 1077 void scheduleCommit() => _schedule((dir) => this.commit(dir)); |
| 1078 |
| 1079 /// Return a Future that completes to the commit in the git repository |
| 1080 /// referred to by [ref] at the current point in the scheduled test run. |
| 1081 Future<String> revParse(String ref) { |
| 1082 return _scheduleValue((parentDir) { |
| 1083 return super.create(parentDir).then((rootDir) { |
| 1084 return _runGit(['rev-parse', ref], rootDir); |
| 1085 }).then((output) => output[0]); |
| 1086 }); |
| 1087 } |
| 1088 |
| 1089 /// Schedule a Git command to run in this repository. |
| 1090 void scheduleGit(List<String> args) { |
| 1091 _schedule((parentDir) => _runGit(args, path.join(parentDir, name))); |
| 1092 } |
| 1093 |
| 1094 Future _runGitCommands(parentDir, List<List<String>> commands) { |
| 1095 var workingDir; |
| 1096 |
| 1097 Future runGitStep(_) { |
| 1098 if (commands.isEmpty) return new Future.immediate(workingDir); |
| 1099 var command = commands.removeAt(0); |
| 1100 return _runGit(command, workingDir).then(runGitStep); |
| 1101 } |
| 1102 |
| 1103 return super.create(parentDir).then((rootDir) { |
| 1104 workingDir = rootDir; |
| 1105 return runGitStep(null); |
| 1106 }); |
| 1107 } |
| 1108 |
| 1109 Future<List<String>> _runGit(List<String> args, String workingDir) { |
| 1110 // Explicitly specify the committer information. Git needs this to commit |
| 1111 // and we don't want to rely on the buildbots having this already set up. |
| 1112 var environment = { |
| 1113 'GIT_AUTHOR_NAME': 'Pub Test', |
| 1114 'GIT_AUTHOR_EMAIL': 'pub@dartlang.org', |
| 1115 'GIT_COMMITTER_NAME': 'Pub Test', |
| 1116 'GIT_COMMITTER_EMAIL': 'pub@dartlang.org' |
| 1117 }; |
| 1118 |
| 1119 return gitlib.run(args, workingDir: workingDir, environment: environment); |
| 1120 } |
| 1121 } |
| 1122 |
| 1123 /// Describes a gzipped tar file and its contents. |
| 1124 class TarFileDescriptor extends Descriptor { |
| 1125 final List<Descriptor> contents; |
| 1126 |
| 1127 TarFileDescriptor(Pattern name, this.contents) |
| 1128 : super(name); |
| 1129 |
| 1130 /// Creates the files and directories within this tar file, then archives |
| 1131 /// them, compresses them, and saves the result to [parentDir]. |
| 1132 Future<String> create(parentDir) { |
| 1133 return withTempDir((tempDir) { |
| 1134 return Future.wait(contents.map((child) => child.create(tempDir))) |
| 1135 .then((createdContents) { |
| 1136 return createTarGz(createdContents, baseDir: tempDir).toBytes(); |
| 1137 }).then((bytes) { |
| 1138 var file = path.join(parentDir, _stringName); |
| 1139 writeBinaryFile(file, bytes); |
| 1140 return file; |
| 1141 }); |
| 1142 }); |
| 1143 } |
| 1144 |
| 1145 /// Validates that the `.tar.gz` file at [path] contains the expected |
| 1146 /// contents. |
| 1147 Future validate(String path) { |
| 1148 throw "TODO(nweiz): implement this"; |
| 1149 } |
| 1150 |
| 1151 Future delete(dir) { |
| 1152 throw new UnsupportedError(''); |
| 1153 } |
| 1154 |
| 1155 /// Loads the contents of this tar file. |
| 1156 ByteStream load(List<String> path) { |
| 1157 if (!path.isEmpty) { |
| 1158 throw "Can't load ${path.join('/')} from within $name: not a directory."; |
| 1159 } |
| 1160 |
| 1161 var controller = new StreamController<List<int>>(); |
| 1162 // TODO(nweiz): propagate any errors to the return value. See issue 3657. |
| 1163 withTempDir((tempDir) { |
| 1164 return create(tempDir).then((tar) { |
| 1165 var sourceStream = new File(tar).openRead(); |
| 1166 return store(sourceStream, controller); |
| 1167 }); |
| 1168 }); |
| 1169 return new ByteStream(controller.stream); |
| 1170 } |
| 1171 } |
| 1172 |
| 1173 /// A descriptor that validates that no file or directory exists with the given |
| 1174 /// name. |
| 1175 class NothingDescriptor extends Descriptor { |
| 1176 NothingDescriptor(String name) : super(name); |
| 1177 |
| 1178 Future create(dir) => new Future.immediate(null); |
| 1179 Future delete(dir) => new Future.immediate(null); |
| 1180 |
| 1181 Future validate(String dir) { |
| 1182 return defer(() { |
| 1183 if (entryExists(path.join(dir, name))) { |
| 1184 throw new TestFailure('Entry $name in $dir should not exist.'); |
| 1185 } |
| 1186 }); |
| 1187 } |
| 1188 |
| 1189 ByteStream load(List<String> path) { |
| 1190 if (path.isEmpty) { |
| 1191 throw "Can't load the contents of $name: it doesn't exist."; |
| 1192 } else { |
| 1193 throw "Can't load ${path.join('/')} from within $name: $name doesn't " |
| 1194 "exist."; |
| 1195 } |
| 1196 } |
| 1197 } |
| 1198 |
548 /// A function that creates a [Validator] subclass. | 1199 /// A function that creates a [Validator] subclass. |
549 typedef Validator ValidatorCreator(Entrypoint entrypoint); | 1200 typedef Validator ValidatorCreator(Entrypoint entrypoint); |
550 | 1201 |
551 /// Schedules a single [Validator] to run on the [appPath]. Returns a scheduled | 1202 /// Schedules a single [Validator] to run on the [appPath]. Returns a scheduled |
552 /// Future that contains the errors and warnings produced by that validator. | 1203 /// Future that contains the errors and warnings produced by that validator. |
553 Future<Pair<List<String>, List<String>>> schedulePackageValidation( | 1204 Future<Pair<List<String>, List<String>>> schedulePackageValidation( |
554 ValidatorCreator fn) { | 1205 ValidatorCreator fn) { |
555 return schedule(() { | 1206 return _scheduleValue((sandboxDir) { |
556 var cache = new SystemCache.withSources(path.join(sandboxDir, cachePath)); | 1207 var cache = new SystemCache.withSources(path.join(sandboxDir, cachePath)); |
557 | 1208 |
558 return defer(() { | 1209 return defer(() { |
559 var validator = fn(new Entrypoint(path.join(sandboxDir, appPath), cache)); | 1210 var validator = fn(new Entrypoint(path.join(sandboxDir, appPath), cache)); |
560 return validator.validate().then((_) { | 1211 return validator.validate().then((_) { |
561 return new Pair(validator.errors, validator.warnings); | 1212 return new Pair(validator.errors, validator.warnings); |
562 }); | 1213 }); |
563 }); | 1214 }); |
564 }, "validating package"); | 1215 }); |
565 } | 1216 } |
566 | 1217 |
567 /// A matcher that matches a Pair. | 1218 /// A matcher that matches a Pair. |
568 Matcher pairOf(Matcher firstMatcher, Matcher lastMatcher) => | 1219 Matcher pairOf(Matcher firstMatcher, Matcher lastMatcher) => |
569 new _PairMatcher(firstMatcher, lastMatcher); | 1220 new _PairMatcher(firstMatcher, lastMatcher); |
570 | 1221 |
571 class _PairMatcher extends BaseMatcher { | 1222 class _PairMatcher extends BaseMatcher { |
572 final Matcher _firstMatcher; | 1223 final Matcher _firstMatcher; |
573 final Matcher _lastMatcher; | 1224 final Matcher _lastMatcher; |
574 | 1225 |
575 _PairMatcher(this._firstMatcher, this._lastMatcher); | 1226 _PairMatcher(this._firstMatcher, this._lastMatcher); |
576 | 1227 |
577 bool matches(item, MatchState matchState) { | 1228 bool matches(item, MatchState matchState) { |
578 if (item is! Pair) return false; | 1229 if (item is! Pair) return false; |
579 return _firstMatcher.matches(item.first, matchState) && | 1230 return _firstMatcher.matches(item.first, matchState) && |
580 _lastMatcher.matches(item.last, matchState); | 1231 _lastMatcher.matches(item.last, matchState); |
581 } | 1232 } |
582 | 1233 |
583 Description describe(Description description) { | 1234 Description describe(Description description) { |
584 description.addAll("(", ", ", ")", [_firstMatcher, _lastMatcher]); | 1235 description.addAll("(", ", ", ")", [_firstMatcher, _lastMatcher]); |
585 } | 1236 } |
586 } | 1237 } |
| 1238 |
| 1239 /// The time (in milliseconds) to wait for scheduled events that could run |
| 1240 /// forever. |
| 1241 const _SCHEDULE_TIMEOUT = 10000; |
| 1242 |
| 1243 /// A class representing a [Process] that is scheduled to run in the course of |
| 1244 /// the test. This class allows actions on the process to be scheduled |
| 1245 /// synchronously. All operations on this class are scheduled. |
| 1246 /// |
| 1247 /// Before running the test, either [shouldExit] or [kill] must be called on |
| 1248 /// this to ensure that the process terminates when expected. |
| 1249 /// |
| 1250 /// If the test fails, this will automatically print out any remaining stdout |
| 1251 /// and stderr from the process to aid debugging. |
| 1252 class ScheduledProcess { |
| 1253 /// The name of the process. Used for error reporting. |
| 1254 final String name; |
| 1255 |
| 1256 /// The process future that's scheduled to run. |
| 1257 Future<PubProcess> _processFuture; |
| 1258 |
| 1259 /// The process that's scheduled to run. It may be null. |
| 1260 PubProcess _process; |
| 1261 |
| 1262 /// The exit code of the scheduled program. It may be null. |
| 1263 int _exitCode; |
| 1264 |
| 1265 /// A future that will complete to a list of all the lines emitted on the |
| 1266 /// process's standard output stream. This is independent of what data is read |
| 1267 /// from [_stdout]. |
| 1268 Future<List<String>> _stdoutLines; |
| 1269 |
| 1270 /// A [Stream] of stdout lines emitted by the process that's scheduled to run. |
| 1271 /// It may be null. |
| 1272 Stream<String> _stdout; |
| 1273 |
| 1274 /// A [Future] that will resolve to [_stdout] once it's available. |
| 1275 Future get _stdoutFuture => _processFuture.then((_) => _stdout); |
| 1276 |
| 1277 /// A [StreamSubscription] that controls [_stdout]. |
| 1278 StreamSubscription _stdoutSubscription; |
| 1279 |
| 1280 /// A future that will complete to a list of all the lines emitted on the |
| 1281 /// process's standard error stream. This is independent of what data is read |
| 1282 /// from [_stderr]. |
| 1283 Future<List<String>> _stderrLines; |
| 1284 |
| 1285 /// A [Stream] of stderr lines emitted by the process that's scheduled to run. |
| 1286 /// It may be null. |
| 1287 Stream<String> _stderr; |
| 1288 |
| 1289 /// A [Future] that will resolve to [_stderr] once it's available. |
| 1290 Future get _stderrFuture => _processFuture.then((_) => _stderr); |
| 1291 |
| 1292 /// A [StreamSubscription] that controls [_stderr]. |
| 1293 StreamSubscription _stderrSubscription; |
| 1294 |
| 1295 /// The exit code of the process that's scheduled to run. This will naturally |
| 1296 /// only complete once the process has terminated. |
| 1297 Future<int> get _exitCodeFuture => _exitCodeCompleter.future; |
| 1298 |
| 1299 /// The completer for [_exitCode]. |
| 1300 final Completer<int> _exitCodeCompleter = new Completer(); |
| 1301 |
| 1302 /// Whether the user has scheduled the end of this process by calling either |
| 1303 /// [shouldExit] or [kill]. |
| 1304 bool _endScheduled = false; |
| 1305 |
| 1306 /// Whether the process is expected to terminate at this point. |
| 1307 bool _endExpected = false; |
| 1308 |
| 1309 /// Wraps a [Process] [Future] in a scheduled process. |
| 1310 ScheduledProcess(this.name, Future<PubProcess> process) |
| 1311 : _processFuture = process { |
| 1312 var pairFuture = process.then((p) { |
| 1313 _process = p; |
| 1314 |
| 1315 byteStreamToLines(stream) { |
| 1316 return streamToLines(new ByteStream(stream.handleError((e) { |
| 1317 registerException(e.error, e.stackTrace); |
| 1318 })).toStringStream()); |
| 1319 } |
| 1320 |
| 1321 var stdoutTee = tee(byteStreamToLines(p.stdout)); |
| 1322 var stdoutPair = streamWithSubscription(stdoutTee.last); |
| 1323 _stdout = stdoutPair.first; |
| 1324 _stdoutSubscription = stdoutPair.last; |
| 1325 |
| 1326 var stderrTee = tee(byteStreamToLines(p.stderr)); |
| 1327 var stderrPair = streamWithSubscription(stderrTee.last); |
| 1328 _stderr = stderrPair.first; |
| 1329 _stderrSubscription = stderrPair.last; |
| 1330 |
| 1331 return new Pair(stdoutTee.first, stderrTee.first); |
| 1332 }); |
| 1333 |
| 1334 _stdoutLines = pairFuture.then((pair) => pair.first.toList()); |
| 1335 _stderrLines = pairFuture.then((pair) => pair.last.toList()); |
| 1336 |
| 1337 _schedule((_) { |
| 1338 if (!_endScheduled) { |
| 1339 throw new StateError("Scheduled process $name must have shouldExit() " |
| 1340 "or kill() called before the test is run."); |
| 1341 } |
| 1342 |
| 1343 process.then((p) => p.exitCode).then((exitCode) { |
| 1344 if (_endExpected) { |
| 1345 _exitCode = exitCode; |
| 1346 _exitCodeCompleter.complete(exitCode); |
| 1347 return; |
| 1348 } |
| 1349 |
| 1350 // Sleep for half a second in case _endExpected is set in the next |
| 1351 // scheduled event. |
| 1352 return sleep(500).then((_) { |
| 1353 if (_endExpected) { |
| 1354 _exitCodeCompleter.complete(exitCode); |
| 1355 return; |
| 1356 } |
| 1357 |
| 1358 return _printStreams(); |
| 1359 }).then((_) { |
| 1360 registerException(new TestFailure("Process $name ended " |
| 1361 "earlier than scheduled with exit code $exitCode")); |
| 1362 }); |
| 1363 }).catchError((e) => registerException(e.error, e.stackTrace)); |
| 1364 }); |
| 1365 |
| 1366 _scheduleOnException((_) { |
| 1367 if (_process == null) return; |
| 1368 |
| 1369 if (_exitCode == null) { |
| 1370 print("\nKilling process $name prematurely."); |
| 1371 _endExpected = true; |
| 1372 _process.kill(); |
| 1373 } |
| 1374 |
| 1375 return _printStreams(); |
| 1376 }); |
| 1377 |
| 1378 _scheduleCleanup((_) { |
| 1379 if (_process == null) return; |
| 1380 // Ensure that the process is dead and we aren't waiting on any IO. |
| 1381 _process.kill(); |
| 1382 _stdoutSubscription.cancel(); |
| 1383 _stderrSubscription.cancel(); |
| 1384 }); |
| 1385 } |
| 1386 |
| 1387 /// Reads the next line of stdout from the process. |
| 1388 Future<String> nextLine() { |
| 1389 return _scheduleValue((_) { |
| 1390 return timeout(_stdoutFuture.then((stream) => streamFirst(stream)), |
| 1391 _SCHEDULE_TIMEOUT, |
| 1392 "waiting for the next stdout line from process $name"); |
| 1393 }); |
| 1394 } |
| 1395 |
| 1396 /// Reads the next line of stderr from the process. |
| 1397 Future<String> nextErrLine() { |
| 1398 return _scheduleValue((_) { |
| 1399 return timeout(_stderrFuture.then((stream) => streamFirst(stream)), |
| 1400 _SCHEDULE_TIMEOUT, |
| 1401 "waiting for the next stderr line from process $name"); |
| 1402 }); |
| 1403 } |
| 1404 |
| 1405 /// Reads the remaining stdout from the process. This should only be called |
| 1406 /// after kill() or shouldExit(). |
| 1407 Future<String> remainingStdout() { |
| 1408 if (!_endScheduled) { |
| 1409 throw new StateError("remainingStdout() should only be called after " |
| 1410 "kill() or shouldExit()."); |
| 1411 } |
| 1412 |
| 1413 return _scheduleValue((_) { |
| 1414 return timeout(_stdoutFuture.then((stream) => stream.toList()) |
| 1415 .then((lines) => lines.join("\n")), |
| 1416 _SCHEDULE_TIMEOUT, |
| 1417 "waiting for the last stdout line from process $name"); |
| 1418 }); |
| 1419 } |
| 1420 |
| 1421 /// Reads the remaining stderr from the process. This should only be called |
| 1422 /// after kill() or shouldExit(). |
| 1423 Future<String> remainingStderr() { |
| 1424 if (!_endScheduled) { |
| 1425 throw new StateError("remainingStderr() should only be called after " |
| 1426 "kill() or shouldExit()."); |
| 1427 } |
| 1428 |
| 1429 return _scheduleValue((_) { |
| 1430 return timeout(_stderrFuture.then((stream) => stream.toList()) |
| 1431 .then((lines) => lines.join("\n")), |
| 1432 _SCHEDULE_TIMEOUT, |
| 1433 "waiting for the last stderr line from process $name"); |
| 1434 }); |
| 1435 } |
| 1436 |
| 1437 /// Writes [line] to the process as stdin. |
| 1438 void writeLine(String line) { |
| 1439 _schedule((_) => _processFuture.then( |
| 1440 (p) => p.stdin.add(encodeUtf8('$line\n')))); |
| 1441 } |
| 1442 |
| 1443 /// Kills the process, and waits until it's dead. |
| 1444 void kill() { |
| 1445 _endScheduled = true; |
| 1446 _schedule((_) { |
| 1447 _endExpected = true; |
| 1448 _process.kill(); |
| 1449 timeout(_exitCodeFuture, _SCHEDULE_TIMEOUT, |
| 1450 "waiting for process $name to die"); |
| 1451 }); |
| 1452 } |
| 1453 |
| 1454 /// Waits for the process to exit, and verifies that the exit code matches |
| 1455 /// [expectedExitCode] (if given). |
| 1456 void shouldExit([int expectedExitCode]) { |
| 1457 _endScheduled = true; |
| 1458 _schedule((_) { |
| 1459 _endExpected = true; |
| 1460 return timeout(_exitCodeFuture, _SCHEDULE_TIMEOUT, |
| 1461 "waiting for process $name to exit").then((exitCode) { |
| 1462 if (expectedExitCode != null) { |
| 1463 expect(exitCode, equals(expectedExitCode)); |
| 1464 } |
| 1465 }); |
| 1466 }); |
| 1467 } |
| 1468 |
| 1469 /// Prints the remaining data in the process's stdout and stderr streams. |
| 1470 /// Prints nothing if the streams are empty. |
| 1471 Future _printStreams() { |
| 1472 void printStream(String streamName, List<String> lines) { |
| 1473 if (lines.isEmpty) return; |
| 1474 |
| 1475 print('\nProcess $name $streamName:'); |
| 1476 for (var line in lines) { |
| 1477 print('| $line'); |
| 1478 } |
| 1479 } |
| 1480 |
| 1481 return _stdoutLines.then((stdoutLines) { |
| 1482 printStream('stdout', stdoutLines); |
| 1483 return _stderrLines.then((stderrLines) { |
| 1484 printStream('stderr', stderrLines); |
| 1485 }); |
| 1486 }); |
| 1487 } |
| 1488 } |
| 1489 |
| 1490 /// A class representing an [HttpServer] that's scheduled to run in the course |
| 1491 /// of the test. This class allows the server's request handling to be |
| 1492 /// scheduled synchronously. All operations on this class are scheduled. |
| 1493 class ScheduledServer { |
| 1494 /// The wrapped server. |
| 1495 final Future<HttpServer> _server; |
| 1496 |
| 1497 /// The queue of handlers to run for upcoming requests. |
| 1498 final _handlers = new Queue<Future>(); |
| 1499 |
| 1500 /// The requests to be ignored. |
| 1501 final _ignored = new Set<Pair<String, String>>(); |
| 1502 |
| 1503 ScheduledServer._(this._server); |
| 1504 |
| 1505 /// Creates a new server listening on an automatically-allocated port on |
| 1506 /// localhost. |
| 1507 factory ScheduledServer() { |
| 1508 var scheduledServer; |
| 1509 scheduledServer = new ScheduledServer._(_scheduleValue((_) { |
| 1510 return SafeHttpServer.bind("127.0.0.1", 0).then((server) { |
| 1511 server.listen(scheduledServer._awaitHandle); |
| 1512 _scheduleCleanup((_) => server.close()); |
| 1513 return server; |
| 1514 }); |
| 1515 })); |
| 1516 return scheduledServer; |
| 1517 } |
| 1518 |
| 1519 /// The port on which the server is listening. |
| 1520 Future<int> get port => _server.then((s) => s.port); |
| 1521 |
| 1522 /// The base URL of the server, including its port. |
| 1523 Future<Uri> get url => |
| 1524 port.then((p) => Uri.parse("http://localhost:$p")); |
| 1525 |
| 1526 /// Assert that the next request has the given [method] and [path], and pass |
| 1527 /// it to [handler] to handle. If [handler] returns a [Future], wait until |
| 1528 /// it's completed to continue the schedule. |
| 1529 void handle(String method, String path, |
| 1530 Future handler(HttpRequest request, HttpResponse response)) { |
| 1531 var handlerCompleter = new Completer<Function>(); |
| 1532 _scheduleValue((_) { |
| 1533 var requestCompleteCompleter = new Completer(); |
| 1534 handlerCompleter.complete((request, response) { |
| 1535 expect(request.method, equals(method)); |
| 1536 expect(request.uri.path, equals(path)); |
| 1537 |
| 1538 var future = handler(request, response); |
| 1539 if (future == null) future = new Future.immediate(null); |
| 1540 chainToCompleter(future, requestCompleteCompleter); |
| 1541 }); |
| 1542 return timeout(requestCompleteCompleter.future, |
| 1543 _SCHEDULE_TIMEOUT, "waiting for $method $path"); |
| 1544 }); |
| 1545 _handlers.add(handlerCompleter.future); |
| 1546 } |
| 1547 |
| 1548 /// Ignore all requests with the given [method] and [path]. If one is |
| 1549 /// received, don't respond to it. |
| 1550 void ignore(String method, String path) => |
| 1551 _ignored.add(new Pair(method, path)); |
| 1552 |
| 1553 /// Raises an error complaining of an unexpected request. |
| 1554 void _awaitHandle(HttpRequest request) { |
| 1555 HttpResponse response = request.response; |
| 1556 if (_ignored.contains(new Pair(request.method, request.uri.path))) return; |
| 1557 var future = timeout(defer(() { |
| 1558 if (_handlers.isEmpty) { |
| 1559 fail('Unexpected ${request.method} request to ${request.uri.path}.'); |
| 1560 } |
| 1561 return _handlers.removeFirst(); |
| 1562 }).then((handler) { |
| 1563 handler(request, response); |
| 1564 }), _SCHEDULE_TIMEOUT, "waiting for a handler for ${request.method} " |
| 1565 "${request.uri.path}"); |
| 1566 expect(future, completes); |
| 1567 } |
| 1568 } |
| 1569 |
| 1570 /// Takes a simple data structure (composed of [Map]s, [List]s, scalar objects, |
| 1571 /// and [Future]s) and recursively resolves all the [Future]s contained within. |
| 1572 /// Completes with the fully resolved structure. |
| 1573 Future _awaitObject(object) { |
| 1574 // Unroll nested futures. |
| 1575 if (object is Future) return object.then(_awaitObject); |
| 1576 if (object is Collection) { |
| 1577 return Future.wait(object.map(_awaitObject).toList()); |
| 1578 } |
| 1579 if (object is! Map) return new Future.immediate(object); |
| 1580 |
| 1581 var pairs = <Future<Pair>>[]; |
| 1582 object.forEach((key, value) { |
| 1583 pairs.add(_awaitObject(value) |
| 1584 .then((resolved) => new Pair(key, resolved))); |
| 1585 }); |
| 1586 return Future.wait(pairs).then((resolvedPairs) { |
| 1587 var map = {}; |
| 1588 for (var pair in resolvedPairs) { |
| 1589 map[pair.first] = pair.last; |
| 1590 } |
| 1591 return map; |
| 1592 }); |
| 1593 } |
| 1594 |
| 1595 /// Schedules a callback to be called as part of the test case. |
| 1596 void _schedule(_ScheduledEvent event) { |
| 1597 if (_scheduled == null) _scheduled = new Queue(); |
| 1598 _scheduled.addLast(event); |
| 1599 } |
| 1600 |
| 1601 /// Like [_schedule], but pipes the return value of [event] to a returned |
| 1602 /// [Future]. |
| 1603 Future _scheduleValue(_ScheduledEvent event) { |
| 1604 var completer = new Completer(); |
| 1605 _schedule((parentDir) { |
| 1606 chainToCompleter(event(parentDir), completer); |
| 1607 return completer.future; |
| 1608 }); |
| 1609 return completer.future; |
| 1610 } |
| 1611 |
| 1612 /// Schedules a callback to be called after the test case has completed, even |
| 1613 /// if it failed. |
| 1614 void _scheduleCleanup(_ScheduledEvent event) { |
| 1615 if (_scheduledCleanup == null) _scheduledCleanup = new Queue(); |
| 1616 _scheduledCleanup.addLast(event); |
| 1617 } |
| 1618 |
| 1619 /// Schedules a callback to be called after the test case has completed, but |
| 1620 /// only if it failed. |
| 1621 void _scheduleOnException(_ScheduledEvent event) { |
| 1622 if (_scheduledOnException == null) _scheduledOnException = new Queue(); |
| 1623 _scheduledOnException.addLast(event); |
| 1624 } |
| 1625 |
| 1626 /// Like [expect], but for [Future]s that complete as part of the scheduled |
| 1627 /// test. This is necessary to ensure that the exception thrown by the |
| 1628 /// expectation failing is handled by the scheduler. |
| 1629 /// |
| 1630 /// Note that [matcher] matches against the completed value of [actual], so |
| 1631 /// calling [completion] is unnecessary. |
| 1632 void expectLater(Future actual, matcher, {String reason, |
| 1633 FailureHandler failureHandler, bool verbose: false}) { |
| 1634 _schedule((_) { |
| 1635 return actual.then((value) { |
| 1636 expect(value, matcher, reason: reason, failureHandler: failureHandler, |
| 1637 verbose: false); |
| 1638 }); |
| 1639 }); |
| 1640 } |
OLD | NEW |