OLD | NEW |
| (Empty) |
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 | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /// Test infrastructure for testing pub. | |
6 /// | |
7 /// Unlike typical unit tests, most pub tests are integration tests that stage | |
8 /// some stuff on the file system, run pub, and then validate the results. This | |
9 /// library provides an API to build tests like that. | |
10 library test_pub; | |
11 | |
12 import 'dart:async'; | |
13 import 'dart:convert'; | |
14 import 'dart:io'; | |
15 import 'dart:math'; | |
16 | |
17 import 'package:http/testing.dart'; | |
18 import 'package:path/path.dart' as p; | |
19 import 'package:pub_semver/pub_semver.dart'; | |
20 import 'package:scheduled_test/scheduled_process.dart'; | |
21 import 'package:scheduled_test/scheduled_server.dart'; | |
22 import 'package:scheduled_test/scheduled_stream.dart'; | |
23 import 'package:scheduled_test/scheduled_test.dart' hide fail; | |
24 import 'package:shelf/shelf.dart' as shelf; | |
25 import 'package:shelf/shelf_io.dart' as shelf_io; | |
26 import 'package:unittest/compact_vm_config.dart'; | |
27 import 'package:yaml/yaml.dart'; | |
28 | |
29 import '../lib/src/entrypoint.dart'; | |
30 import '../lib/src/exit_codes.dart' as exit_codes; | |
31 // TODO(rnystrom): Using "gitlib" as the prefix here is ugly, but "git" collides | |
32 // with the git descriptor method. Maybe we should try to clean up the top level | |
33 // scope a bit? | |
34 import '../lib/src/git.dart' as gitlib; | |
35 import '../lib/src/http.dart'; | |
36 import '../lib/src/io.dart'; | |
37 import '../lib/src/lock_file.dart'; | |
38 import '../lib/src/log.dart' as log; | |
39 import '../lib/src/package.dart'; | |
40 import '../lib/src/pubspec.dart'; | |
41 import '../lib/src/source/hosted.dart'; | |
42 import '../lib/src/source/path.dart'; | |
43 import '../lib/src/source_registry.dart'; | |
44 import '../lib/src/system_cache.dart'; | |
45 import '../lib/src/utils.dart'; | |
46 import '../lib/src/validator.dart'; | |
47 import 'descriptor.dart' as d; | |
48 import 'serve_packages.dart'; | |
49 | |
50 export 'serve_packages.dart'; | |
51 | |
52 /// This should be called at the top of a test file to set up an appropriate | |
53 /// test configuration for the machine running the tests. | |
54 initConfig() { | |
55 useCompactVMConfiguration(); | |
56 filterStacks = true; | |
57 unittestConfiguration.timeout = null; | |
58 } | |
59 | |
60 /// The current [HttpServer] created using [serve]. | |
61 var _server; | |
62 | |
63 /// The list of paths that have been requested from the server since the last | |
64 /// call to [getRequestedPaths]. | |
65 final _requestedPaths = <String>[]; | |
66 | |
67 /// The cached value for [_portCompleter]. | |
68 Completer<int> _portCompleterCache; | |
69 | |
70 /// A [Matcher] that matches JavaScript generated by dart2js with minification | |
71 /// enabled. | |
72 Matcher isMinifiedDart2JSOutput = | |
73 isNot(contains("// The code supports the following hooks")); | |
74 | |
75 /// A [Matcher] that matches JavaScript generated by dart2js with minification | |
76 /// disabled. | |
77 Matcher isUnminifiedDart2JSOutput = | |
78 contains("// The code supports the following hooks"); | |
79 | |
80 /// A map from package names to paths from which those packages should be loaded | |
81 /// for [createLockFile]. | |
82 /// | |
83 /// This allows older versions of dependencies than those that exist in the repo | |
84 /// to be used when testing pub. | |
85 Map<String, String> _packageOverrides; | |
86 | |
87 /// A map from barback versions to the paths of directories in the repo | |
88 /// containing them. | |
89 /// | |
90 /// This includes the latest version of barback from pkg as well as all old | |
91 /// versions of barback in third_party. | |
92 final _barbackVersions = _findBarbackVersions(); | |
93 | |
94 /// Some older barback versions require older versions of barback's dependencies | |
95 /// than those that are in the repo. | |
96 /// | |
97 /// This is a map from barback version ranges to the dependencies for those | |
98 /// barback versions. Each dependency version listed here should be included in | |
99 /// third_party/pkg. | |
100 final _barbackDeps = { | |
101 new VersionConstraint.parse("<0.15.0"): { | |
102 "source_maps": "0.9.4" | |
103 } | |
104 }; | |
105 | |
106 /// Populates [_barbackVersions]. | |
107 Map<Version, String> _findBarbackVersions() { | |
108 var versions = {}; | |
109 var currentBarback = p.join(repoRoot, 'third_party', 'pkg', 'barback'); | |
110 versions[new Pubspec.load(currentBarback, new SourceRegistry()).version] = | |
111 currentBarback; | |
112 | |
113 for (var dir in listDir(p.join(repoRoot, 'third_party', 'pkg'))) { | |
114 var basename = p.basename(dir); | |
115 if (!basename.startsWith('barback-')) continue; | |
116 versions[new Version.parse(split1(basename, '-').last)] = dir; | |
117 } | |
118 | |
119 return versions; | |
120 } | |
121 | |
122 /// Runs the tests in [callback] against all versions of barback in the repo | |
123 /// that match [versionConstraint]. | |
124 /// | |
125 /// This is used to test that pub doesn't accidentally break older versions of | |
126 /// barback that it's committed to supporting. Only versions `0.13.0` and later | |
127 /// will be tested. | |
128 void withBarbackVersions(String versionConstraint, void callback()) { | |
129 var constraint = new VersionConstraint.parse(versionConstraint); | |
130 | |
131 var validVersions = _barbackVersions.keys.where(constraint.allows); | |
132 if (validVersions.isEmpty) { | |
133 throw new ArgumentError( | |
134 'No available barback version matches "$versionConstraint".'); | |
135 } | |
136 | |
137 for (var version in validVersions) { | |
138 group("with barback $version", () { | |
139 setUp(() { | |
140 _packageOverrides = {}; | |
141 _packageOverrides['barback'] = _barbackVersions[version]; | |
142 _barbackDeps.forEach((constraint, deps) { | |
143 if (!constraint.allows(version)) return; | |
144 deps.forEach((packageName, version) { | |
145 _packageOverrides[packageName] = | |
146 p.join(repoRoot, 'third_party', 'pkg', '$packageName-$version'); | |
147 }); | |
148 }); | |
149 | |
150 currentSchedule.onComplete.schedule(() { | |
151 _packageOverrides = null; | |
152 }); | |
153 }); | |
154 | |
155 callback(); | |
156 }); | |
157 } | |
158 } | |
159 | |
160 /// The completer for [port]. | |
161 Completer<int> get _portCompleter { | |
162 if (_portCompleterCache != null) return _portCompleterCache; | |
163 _portCompleterCache = new Completer<int>(); | |
164 currentSchedule.onComplete.schedule(() { | |
165 _portCompleterCache = null; | |
166 }, 'clearing the port completer'); | |
167 return _portCompleterCache; | |
168 } | |
169 | |
170 /// A future that will complete to the port used for the current server. | |
171 Future<int> get port => _portCompleter.future; | |
172 | |
173 /// Gets the list of paths that have been requested from the server since the | |
174 /// last time this was called (or since the server was first spun up). | |
175 Future<List<String>> getRequestedPaths() { | |
176 return schedule(() { | |
177 var paths = _requestedPaths.toList(); | |
178 _requestedPaths.clear(); | |
179 return paths; | |
180 }, "get previous network requests"); | |
181 } | |
182 | |
183 /// Creates an HTTP server to serve [contents] as static files. | |
184 /// | |
185 /// This server will exist only for the duration of the pub run. Subsequent | |
186 /// calls to [serve] replace the previous server. | |
187 void serve([List<d.Descriptor> contents]) { | |
188 var baseDir = d.dir("serve-dir", contents); | |
189 | |
190 _hasServer = true; | |
191 | |
192 schedule(() { | |
193 return _closeServer().then((_) { | |
194 return shelf_io.serve((request) { | |
195 currentSchedule.heartbeat(); | |
196 var path = p.posix.fromUri(request.url.path.replaceFirst("/", "")); | |
197 _requestedPaths.add(path); | |
198 | |
199 return validateStream( | |
200 baseDir.load( | |
201 path)).then((stream) => new shelf.Response.ok(stream)).catchErro
r((error) { | |
202 return new shelf.Response.notFound('File "$path" not found.'); | |
203 }); | |
204 }, 'localhost', 0).then((server) { | |
205 _server = server; | |
206 _portCompleter.complete(_server.port); | |
207 currentSchedule.onComplete.schedule(_closeServer); | |
208 }); | |
209 }); | |
210 }, 'starting a server serving:\n${baseDir.describe()}'); | |
211 } | |
212 | |
213 /// Closes [_server]. | |
214 /// | |
215 /// Returns a [Future] that completes after the [_server] is closed. | |
216 Future _closeServer() { | |
217 if (_server == null) return new Future.value(); | |
218 var future = _server.close(); | |
219 _server = null; | |
220 _hasServer = false; | |
221 _portCompleterCache = null; | |
222 return future; | |
223 } | |
224 | |
225 /// `true` if the current test spins up an HTTP server. | |
226 bool _hasServer = false; | |
227 | |
228 /// Converts [value] into a YAML string. | |
229 String yaml(value) => JSON.encode(value); | |
230 | |
231 /// The full path to the created sandbox directory for an integration test. | |
232 String get sandboxDir => _sandboxDir; | |
233 String _sandboxDir; | |
234 | |
235 /// The path of the package cache directory used for tests, relative to the | |
236 /// sandbox directory. | |
237 final String cachePath = "cache"; | |
238 | |
239 /// The path of the mock app directory used for tests, relative to the sandbox | |
240 /// directory. | |
241 final String appPath = "myapp"; | |
242 | |
243 /// The path of the packages directory in the mock app used for tests, relative | |
244 /// to the sandbox directory. | |
245 final String packagesPath = "$appPath/packages"; | |
246 | |
247 /// Set to true when the current batch of scheduled events should be aborted. | |
248 bool _abortScheduled = false; | |
249 | |
250 /// Enum identifying a pub command that can be run with a well-defined success | |
251 /// output. | |
252 class RunCommand { | |
253 static final get = new RunCommand( | |
254 'get', | |
255 new RegExp(r'Got dependencies!|Changed \d+ dependenc(y|ies)!')); | |
256 static final upgrade = new RunCommand( | |
257 'upgrade', | |
258 new RegExp(r'(No dependencies changed\.|Changed \d+ dependenc(y|ies)!)$'))
; | |
259 static final downgrade = new RunCommand( | |
260 'downgrade', | |
261 new RegExp(r'(No dependencies changed\.|Changed \d+ dependenc(y|ies)!)$'))
; | |
262 | |
263 final String name; | |
264 final RegExp success; | |
265 RunCommand(this.name, this.success); | |
266 } | |
267 | |
268 /// Runs the tests defined within [callback] using both pub get and pub upgrade. | |
269 /// | |
270 /// Many tests validate behavior that is the same between pub get and | |
271 /// upgrade have the same behavior. Instead of duplicating those tests, this | |
272 /// takes a callback that defines get/upgrade agnostic tests and runs them | |
273 /// with both commands. | |
274 void forBothPubGetAndUpgrade(void callback(RunCommand command)) { | |
275 group(RunCommand.get.name, () => callback(RunCommand.get)); | |
276 group(RunCommand.upgrade.name, () => callback(RunCommand.upgrade)); | |
277 } | |
278 | |
279 /// Schedules an invocation of pub [command] and validates that it completes | |
280 /// in an expected way. | |
281 /// | |
282 /// By default, this validates that the command completes successfully and | |
283 /// understands the normal output of a successful pub command. If [warning] is | |
284 /// given, it expects the command to complete successfully *and* print | |
285 /// [warning] to stderr. If [error] is given, it expects the command to *only* | |
286 /// print [error] to stderr. [output], [error], and [warning] may be strings, | |
287 /// [RegExp]s, or [Matcher]s. | |
288 /// | |
289 /// If [exitCode] is given, expects the command to exit with that code. | |
290 // TODO(rnystrom): Clean up other tests to call this when possible. | |
291 void pubCommand(RunCommand command, {Iterable<String> args, output, error, | |
292 warning, int exitCode}) { | |
293 if (error != null && warning != null) { | |
294 throw new ArgumentError("Cannot pass both 'error' and 'warning'."); | |
295 } | |
296 | |
297 var allArgs = [command.name]; | |
298 if (args != null) allArgs.addAll(args); | |
299 | |
300 if (output == null) output = command.success; | |
301 | |
302 if (error != null && exitCode == null) exitCode = 1; | |
303 | |
304 // No success output on an error. | |
305 if (error != null) output = null; | |
306 if (warning != null) error = warning; | |
307 | |
308 schedulePub(args: allArgs, output: output, error: error, exitCode: exitCode); | |
309 } | |
310 | |
311 void pubGet({Iterable<String> args, output, error, warning, int exitCode}) { | |
312 pubCommand( | |
313 RunCommand.get, | |
314 args: args, | |
315 output: output, | |
316 error: error, | |
317 warning: warning, | |
318 exitCode: exitCode); | |
319 } | |
320 | |
321 void pubUpgrade({Iterable<String> args, output, error, warning, int exitCode}) { | |
322 pubCommand( | |
323 RunCommand.upgrade, | |
324 args: args, | |
325 output: output, | |
326 error: error, | |
327 warning: warning, | |
328 exitCode: exitCode); | |
329 } | |
330 | |
331 void pubDowngrade({Iterable<String> args, output, error, warning, int exitCode}) | |
332 { | |
333 pubCommand( | |
334 RunCommand.downgrade, | |
335 args: args, | |
336 output: output, | |
337 error: error, | |
338 warning: warning, | |
339 exitCode: exitCode); | |
340 } | |
341 | |
342 /// Schedules starting the "pub [global] run" process and validates the | |
343 /// expected startup output. | |
344 /// | |
345 /// If [global] is `true`, this invokes "pub global run", otherwise it does | |
346 /// "pub run". | |
347 /// | |
348 /// Returns the `pub run` process. | |
349 ScheduledProcess pubRun({bool global: false, Iterable<String> args}) { | |
350 var pubArgs = global ? ["global", "run"] : ["run"]; | |
351 pubArgs.addAll(args); | |
352 var pub = startPub(args: pubArgs); | |
353 | |
354 // Loading sources and transformers isn't normally printed, but the pub test | |
355 // infrastructure runs pub in verbose mode, which enables this. | |
356 pub.stdout.expect(consumeWhile(startsWith("Loading"))); | |
357 | |
358 return pub; | |
359 } | |
360 | |
361 /// Defines an integration test. | |
362 /// | |
363 /// The [body] should schedule a series of operations which will be run | |
364 /// asynchronously. | |
365 void integration(String description, void body()) => | |
366 _integration(description, body, test); | |
367 | |
368 /// Like [integration], but causes only this test to run. | |
369 void solo_integration(String description, void body()) => | |
370 _integration(description, body, solo_test); | |
371 | |
372 void _integration(String description, void body(), [Function testFn]) { | |
373 testFn(description, () { | |
374 // TODO(nweiz): remove this when issue 15362 is fixed. | |
375 currentSchedule.timeout *= 2; | |
376 | |
377 // The windows bots are very slow, so we increase the default timeout. | |
378 if (Platform.operatingSystem == "windows") { | |
379 currentSchedule.timeout *= 2; | |
380 } | |
381 | |
382 _sandboxDir = createSystemTempDir(); | |
383 d.defaultRoot = sandboxDir; | |
384 currentSchedule.onComplete.schedule( | |
385 () => deleteEntry(_sandboxDir), | |
386 'deleting the sandbox directory'); | |
387 | |
388 // Schedule the test. | |
389 body(); | |
390 }); | |
391 } | |
392 | |
393 /// Get the path to the root "pub/test" directory containing the pub | |
394 /// tests. | |
395 String get testDirectory => p.absolute(p.dirname(libraryPath('test_pub'))); | |
396 | |
397 /// Schedules renaming (moving) the directory at [from] to [to], both of which | |
398 /// are assumed to be relative to [sandboxDir]. | |
399 void scheduleRename(String from, String to) { | |
400 schedule( | |
401 () => renameDir(p.join(sandboxDir, from), p.join(sandboxDir, to)), | |
402 'renaming $from to $to'); | |
403 } | |
404 | |
405 /// Schedules creating a symlink at path [symlink] that points to [target], | |
406 /// both of which are assumed to be relative to [sandboxDir]. | |
407 void scheduleSymlink(String target, String symlink) { | |
408 schedule( | |
409 () => createSymlink(p.join(sandboxDir, target), p.join(sandboxDir, symlink
)), | |
410 'symlinking $target to $symlink'); | |
411 } | |
412 | |
413 /// Schedules a call to the Pub command-line utility. | |
414 /// | |
415 /// Runs Pub with [args] and validates that its results match [output] (or | |
416 /// [outputJson]), [error], and [exitCode]. | |
417 /// | |
418 /// [output] and [error] can be [String]s, [RegExp]s, or [Matcher]s. | |
419 /// | |
420 /// If [outputJson] is given, validates that pub outputs stringified JSON | |
421 /// matching that object, which can be a literal JSON object or any other | |
422 /// [Matcher]. | |
423 /// | |
424 /// If [environment] is given, any keys in it will override the environment | |
425 /// variables passed to the spawned process. | |
426 void schedulePub({List args, output, error, outputJson, int exitCode: | |
427 exit_codes.SUCCESS, Map<String, String> environment}) { | |
428 // Cannot pass both output and outputJson. | |
429 assert(output == null || outputJson == null); | |
430 | |
431 var pub = startPub(args: args, environment: environment); | |
432 pub.shouldExit(exitCode); | |
433 | |
434 var failures = []; | |
435 var stderr; | |
436 | |
437 expect( | |
438 Future.wait( | |
439 [pub.stdoutStream().toList(), pub.stderrStream().toList()]).then((resu
lts) { | |
440 var stdout = results[0].join("\n"); | |
441 stderr = results[1].join("\n"); | |
442 | |
443 if (outputJson == null) { | |
444 _validateOutput(failures, 'stdout', output, stdout); | |
445 return null; | |
446 } | |
447 | |
448 // Allow the expected JSON to contain futures. | |
449 return awaitObject(outputJson).then((resolved) { | |
450 _validateOutputJson(failures, 'stdout', resolved, stdout); | |
451 }); | |
452 }).then((_) { | |
453 _validateOutput(failures, 'stderr', error, stderr); | |
454 | |
455 if (!failures.isEmpty) throw new TestFailure(failures.join('\n')); | |
456 }), completes); | |
457 } | |
458 | |
459 /// Like [startPub], but runs `pub lish` in particular with [server] used both | |
460 /// as the OAuth2 server (with "/token" as the token endpoint) and as the | |
461 /// package server. | |
462 /// | |
463 /// Any futures in [args] will be resolved before the process is started. | |
464 ScheduledProcess startPublish(ScheduledServer server, {List args}) { | |
465 var tokenEndpoint = | |
466 server.url.then((url) => url.resolve('/token').toString()); | |
467 if (args == null) args = []; | |
468 args = flatten(['lish', '--server', tokenEndpoint, args]); | |
469 return startPub(args: args, tokenEndpoint: tokenEndpoint); | |
470 } | |
471 | |
472 /// Handles the beginning confirmation process for uploading a packages. | |
473 /// | |
474 /// Ensures that the right output is shown and then enters "y" to confirm the | |
475 /// upload. | |
476 void confirmPublish(ScheduledProcess pub) { | |
477 // TODO(rnystrom): This is overly specific and inflexible regarding different | |
478 // test packages. Should validate this a little more loosely. | |
479 pub.stdout.expect(startsWith('Publishing test_pkg 1.0.0 to ')); | |
480 pub.stdout.expect( | |
481 emitsLines( | |
482 "|-- LICENSE\n" "|-- lib\n" "| '-- test_pkg.dart\n" "'-- pubspec.yam
l\n" "\n" | |
483 "Looks great! Are you ready to upload your package (y/n)?")); | |
484 pub.writeLine("y"); | |
485 } | |
486 | |
487 /// Gets the absolute path to [relPath], which is a relative path in the test | |
488 /// sandbox. | |
489 String _pathInSandbox(String relPath) { | |
490 return p.join(p.absolute(sandboxDir), relPath); | |
491 } | |
492 | |
493 /// Gets the environment variables used to run pub in a test context. | |
494 Future<Map> getPubTestEnvironment([String tokenEndpoint]) { | |
495 final completer0 = new Completer(); | |
496 scheduleMicrotask(() { | |
497 try { | |
498 var environment = {}; | |
499 environment['_PUB_TESTING'] = 'true'; | |
500 environment['PUB_CACHE'] = _pathInSandbox(cachePath); | |
501 environment['_PUB_TEST_SDK_VERSION'] = "0.1.2+3"; | |
502 join0() { | |
503 join1() { | |
504 completer0.complete(environment); | |
505 } | |
506 if (_hasServer) { | |
507 completer0.complete(port.then(((p) { | |
508 environment['PUB_HOSTED_URL'] = "http://localhost:$p"; | |
509 return environment; | |
510 }))); | |
511 } else { | |
512 join1(); | |
513 } | |
514 } | |
515 if (tokenEndpoint != null) { | |
516 environment['_PUB_TEST_TOKEN_ENDPOINT'] = tokenEndpoint.toString(); | |
517 join0(); | |
518 } else { | |
519 join0(); | |
520 } | |
521 } catch (e, s) { | |
522 completer0.completeError(e, s); | |
523 } | |
524 }); | |
525 return completer0.future; | |
526 } | |
527 | |
528 /// Starts a Pub process and returns a [ScheduledProcess] that supports | |
529 /// interaction with that process. | |
530 /// | |
531 /// Any futures in [args] will be resolved before the process is started. | |
532 /// | |
533 /// If [environment] is given, any keys in it will override the environment | |
534 /// variables passed to the spawned process. | |
535 ScheduledProcess startPub({List args, Future<String> tokenEndpoint, Map<String, | |
536 String> environment}) { | |
537 ensureDir(_pathInSandbox(appPath)); | |
538 | |
539 // Find a Dart executable we can use to spawn. Use the same one that was | |
540 // used to run this script itself. | |
541 var dartBin = Platform.executable; | |
542 | |
543 // If the executable looks like a path, get its full path. That way we | |
544 // can still find it when we spawn it with a different working directory. | |
545 if (dartBin.contains(Platform.pathSeparator)) { | |
546 dartBin = p.absolute(dartBin); | |
547 } | |
548 | |
549 // Always run pub from a snapshot. Since we require the SDK to be built, the | |
550 // snapshot should be there. Note that this *does* mean that the snapshot has | |
551 // to be manually updated when changing code before running the tests. | |
552 // Otherwise, you will test against stale data. | |
553 // | |
554 // Using the snapshot makes running the tests much faster, which is why we | |
555 // make this trade-off. | |
556 var pubPath = p.join(p.dirname(dartBin), 'snapshots/pub.dart.snapshot'); | |
557 var dartArgs = [pubPath, '--verbose']; | |
558 dartArgs.addAll(args); | |
559 | |
560 if (tokenEndpoint == null) tokenEndpoint = new Future.value(); | |
561 var environmentFuture = tokenEndpoint.then( | |
562 (tokenEndpoint) => getPubTestEnvironment(tokenEndpoint)).then((pubEnvironm
ent) { | |
563 if (environment != null) pubEnvironment.addAll(environment); | |
564 return pubEnvironment; | |
565 }); | |
566 | |
567 return new PubProcess.start( | |
568 dartBin, | |
569 dartArgs, | |
570 environment: environmentFuture, | |
571 workingDirectory: _pathInSandbox(appPath), | |
572 description: args.isEmpty ? 'pub' : 'pub ${args.first}'); | |
573 } | |
574 | |
575 /// A subclass of [ScheduledProcess] that parses pub's verbose logging output | |
576 /// and makes [stdout] and [stderr] work as though pub weren't running in | |
577 /// verbose mode. | |
578 class PubProcess extends ScheduledProcess { | |
579 Stream<Pair<log.Level, String>> _log; | |
580 Stream<String> _stdout; | |
581 Stream<String> _stderr; | |
582 | |
583 PubProcess.start(executable, arguments, {workingDirectory, environment, | |
584 String description, Encoding encoding: UTF8}) | |
585 : super.start( | |
586 executable, | |
587 arguments, | |
588 workingDirectory: workingDirectory, | |
589 environment: environment, | |
590 description: description, | |
591 encoding: encoding); | |
592 | |
593 Stream<Pair<log.Level, String>> _logStream() { | |
594 if (_log == null) { | |
595 _log = mergeStreams( | |
596 _outputToLog(super.stdoutStream(), log.Level.MESSAGE), | |
597 _outputToLog(super.stderrStream(), log.Level.ERROR)); | |
598 } | |
599 | |
600 var pair = tee(_log); | |
601 _log = pair.first; | |
602 return pair.last; | |
603 } | |
604 | |
605 final _logLineRegExp = new RegExp(r"^([A-Z ]{4})[:|] (.*)$"); | |
606 final _logLevels = [ | |
607 log.Level.ERROR, | |
608 log.Level.WARNING, | |
609 log.Level.MESSAGE, | |
610 log.Level.IO, | |
611 log.Level.SOLVER, | |
612 log.Level.FINE].fold(<String, log.Level>{}, (levels, level) { | |
613 levels[level.name] = level; | |
614 return levels; | |
615 }); | |
616 | |
617 Stream<Pair<log.Level, String>> _outputToLog(Stream<String> stream, | |
618 log.Level defaultLevel) { | |
619 var lastLevel; | |
620 return stream.map((line) { | |
621 var match = _logLineRegExp.firstMatch(line); | |
622 if (match == null) return new Pair<log.Level, String>(defaultLevel, line); | |
623 | |
624 var level = _logLevels[match[1]]; | |
625 if (level == null) level = lastLevel; | |
626 lastLevel = level; | |
627 return new Pair<log.Level, String>(level, match[2]); | |
628 }); | |
629 } | |
630 | |
631 Stream<String> stdoutStream() { | |
632 if (_stdout == null) { | |
633 _stdout = _logStream().expand((entry) { | |
634 if (entry.first != log.Level.MESSAGE) return []; | |
635 return [entry.last]; | |
636 }); | |
637 } | |
638 | |
639 var pair = tee(_stdout); | |
640 _stdout = pair.first; | |
641 return pair.last; | |
642 } | |
643 | |
644 Stream<String> stderrStream() { | |
645 if (_stderr == null) { | |
646 _stderr = _logStream().expand((entry) { | |
647 if (entry.first != log.Level.ERROR && | |
648 entry.first != log.Level.WARNING) { | |
649 return []; | |
650 } | |
651 return [entry.last]; | |
652 }); | |
653 } | |
654 | |
655 var pair = tee(_stderr); | |
656 _stderr = pair.first; | |
657 return pair.last; | |
658 } | |
659 } | |
660 | |
661 /// The path to the `packages` directory from which pub loads its dependencies. | |
662 String get _packageRoot => p.absolute(Platform.packageRoot); | |
663 | |
664 /// Fails the current test if Git is not installed. | |
665 /// | |
666 /// We require machines running these tests to have git installed. This | |
667 /// validation gives an easier-to-understand error when that requirement isn't | |
668 /// met than just failing in the middle of a test when pub invokes git. | |
669 /// | |
670 /// This also increases the [Schedule] timeout to 30 seconds on Windows, | |
671 /// where Git runs really slowly. | |
672 void ensureGit() { | |
673 if (Platform.operatingSystem == "windows") { | |
674 currentSchedule.timeout = new Duration(seconds: 30); | |
675 } | |
676 | |
677 if (!gitlib.isInstalled) { | |
678 throw new Exception("Git must be installed to run this test."); | |
679 } | |
680 } | |
681 | |
682 /// Schedules activating a global package [package] without running | |
683 /// "pub global activate". | |
684 /// | |
685 /// This is useful because global packages must be hosted, but the test hosted | |
686 /// server doesn't serve barback. The other parameters here follow | |
687 /// [createLockFile]. | |
688 void makeGlobalPackage(String package, String version, | |
689 Iterable<d.Descriptor> contents, {Iterable<String> pkg, Map<String, | |
690 String> hosted}) { | |
691 // Start the server so we know what port to use in the cache directory name. | |
692 serveNoPackages(); | |
693 | |
694 // Create the package in the hosted cache. | |
695 d.hostedCache([d.dir("$package-$version", contents)]).create(); | |
696 | |
697 var lockFile = _createLockFile(pkg: pkg, hosted: hosted); | |
698 | |
699 // Add the root package to the lockfile. | |
700 var id = | |
701 new PackageId(package, "hosted", new Version.parse(version), package); | |
702 lockFile.packages[package] = id; | |
703 | |
704 // Write the lockfile to the global cache. | |
705 var sources = new SourceRegistry(); | |
706 sources.register(new HostedSource()); | |
707 sources.register(new PathSource()); | |
708 | |
709 d.dir( | |
710 cachePath, | |
711 [ | |
712 d.dir( | |
713 "global_packages", | |
714 [d.file("$package.lock", lockFile.serialize(null, sources))])]).cr
eate(); | |
715 } | |
716 | |
717 /// Creates a lock file for [package] without running `pub get`. | |
718 /// | |
719 /// [sandbox] is a list of path dependencies to be found in the sandbox | |
720 /// directory. [pkg] is a list of packages in the Dart repo's "pkg" directory; | |
721 /// each package listed here and all its dependencies will be linked to the | |
722 /// version in the Dart repo. | |
723 /// | |
724 /// [hosted] is a list of package names to version strings for dependencies on | |
725 /// hosted packages. | |
726 void createLockFile(String package, {Iterable<String> sandbox, | |
727 Iterable<String> pkg, Map<String, String> hosted}) { | |
728 var lockFile = _createLockFile(sandbox: sandbox, pkg: pkg, hosted: hosted); | |
729 | |
730 var sources = new SourceRegistry(); | |
731 sources.register(new HostedSource()); | |
732 sources.register(new PathSource()); | |
733 | |
734 d.file( | |
735 p.join(package, 'pubspec.lock'), | |
736 lockFile.serialize(null, sources)).create(); | |
737 } | |
738 | |
739 /// Creates a lock file for [package] without running `pub get`. | |
740 /// | |
741 /// [sandbox] is a list of path dependencies to be found in the sandbox | |
742 /// directory. [pkg] is a list of packages in the Dart repo's "pkg" directory; | |
743 /// each package listed here and all its dependencies will be linked to the | |
744 /// version in the Dart repo. | |
745 /// | |
746 /// [hosted] is a list of package names to version strings for dependencies on | |
747 /// hosted packages. | |
748 LockFile _createLockFile({Iterable<String> sandbox, Iterable<String> pkg, | |
749 Map<String, String> hosted}) { | |
750 var dependencies = {}; | |
751 | |
752 if (sandbox != null) { | |
753 for (var package in sandbox) { | |
754 dependencies[package] = '../$package'; | |
755 } | |
756 } | |
757 | |
758 if (pkg != null) { | |
759 _addPackage(String package) { | |
760 if (dependencies.containsKey(package)) return; | |
761 | |
762 var path; | |
763 if (package == 'barback' && _packageOverrides == null) { | |
764 throw new StateError( | |
765 "createLockFile() can only create a lock file " | |
766 "with a barback dependency within a withBarbackVersions() " "blo
ck."); | |
767 } | |
768 | |
769 if (_packageOverrides.containsKey(package)) { | |
770 path = _packageOverrides[package]; | |
771 } else { | |
772 path = packagePath(package); | |
773 } | |
774 | |
775 dependencies[package] = path; | |
776 var pubspec = loadYaml(readTextFile(p.join(path, 'pubspec.yaml'))); | |
777 var packageDeps = pubspec['dependencies']; | |
778 if (packageDeps == null) return; | |
779 packageDeps.keys.forEach(_addPackage); | |
780 } | |
781 | |
782 pkg.forEach(_addPackage); | |
783 } | |
784 | |
785 var lockFile = new LockFile.empty(); | |
786 dependencies.forEach((name, dependencyPath) { | |
787 var id = new PackageId(name, 'path', new Version(0, 0, 0), { | |
788 'path': dependencyPath, | |
789 'relative': p.isRelative(dependencyPath) | |
790 }); | |
791 lockFile.packages[name] = id; | |
792 }); | |
793 | |
794 if (hosted != null) { | |
795 hosted.forEach((name, version) { | |
796 var id = new PackageId(name, 'hosted', new Version.parse(version), name); | |
797 lockFile.packages[name] = id; | |
798 }); | |
799 } | |
800 | |
801 return lockFile; | |
802 } | |
803 | |
804 /// Returns the path to [package] within the repo. | |
805 String packagePath(String package) => | |
806 dirExists(p.join(repoRoot, 'pkg', package)) ? | |
807 p.join(repoRoot, 'pkg', package) : | |
808 p.join(repoRoot, 'third_party', 'pkg', package); | |
809 | |
810 /// Uses [client] as the mock HTTP client for this test. | |
811 /// | |
812 /// Note that this will only affect HTTP requests made via http.dart in the | |
813 /// parent process. | |
814 void useMockClient(MockClient client) { | |
815 var oldInnerClient = innerHttpClient; | |
816 innerHttpClient = client; | |
817 currentSchedule.onComplete.schedule(() { | |
818 innerHttpClient = oldInnerClient; | |
819 }, 'de-activating the mock client'); | |
820 } | |
821 | |
822 /// Describes a map representing a library package with the given [name], | |
823 /// [version], and [dependencies]. | |
824 Map packageMap(String name, String version, [Map dependencies]) { | |
825 var package = { | |
826 "name": name, | |
827 "version": version, | |
828 "author": "Natalie Weizenbaum <nweiz@google.com>", | |
829 "homepage": "http://pub.dartlang.org", | |
830 "description": "A package, I guess." | |
831 }; | |
832 | |
833 if (dependencies != null) package["dependencies"] = dependencies; | |
834 | |
835 return package; | |
836 } | |
837 | |
838 /// Resolves [target] relative to the path to pub's `test/asset` directory. | |
839 String testAssetPath(String target) { | |
840 var libPath = libraryPath('test_pub'); | |
841 | |
842 // We are running from the generated directory, but non-dart assets are only | |
843 // in the canonical directory. | |
844 // TODO(rnystrom): Remove this when #104 is fixed. | |
845 libPath = libPath.replaceAll('pub_generated', 'pub'); | |
846 | |
847 return p.join(p.dirname(libPath), 'asset', target); | |
848 } | |
849 | |
850 /// Returns a Map in the format used by the pub.dartlang.org API to represent a | |
851 /// package version. | |
852 /// | |
853 /// [pubspec] is the parsed pubspec of the package version. If [full] is true, | |
854 /// this returns the complete map, including metadata that's only included when | |
855 /// requesting the package version directly. | |
856 Map packageVersionApiMap(Map pubspec, {bool full: false}) { | |
857 var name = pubspec['name']; | |
858 var version = pubspec['version']; | |
859 var map = { | |
860 'pubspec': pubspec, | |
861 'version': version, | |
862 'url': '/api/packages/$name/versions/$version', | |
863 'archive_url': '/packages/$name/versions/$version.tar.gz', | |
864 'new_dartdoc_url': '/api/packages/$name/versions/$version' '/new_dartdoc', | |
865 'package_url': '/api/packages/$name' | |
866 }; | |
867 | |
868 if (full) { | |
869 map.addAll({ | |
870 'downloads': 0, | |
871 'created': '2012-09-25T18:38:28.685260', | |
872 'libraries': ['$name.dart'], | |
873 'uploader': ['nweiz@google.com'] | |
874 }); | |
875 } | |
876 | |
877 return map; | |
878 } | |
879 | |
880 /// Returns the name of the shell script for a binstub named [name]. | |
881 /// | |
882 /// Adds a ".bat" extension on Windows. | |
883 String binStubName(String name) => Platform.isWindows ? '$name.bat' : name; | |
884 | |
885 /// Compares the [actual] output from running pub with [expected]. | |
886 /// | |
887 /// If [expected] is a [String], ignores leading and trailing whitespace | |
888 /// differences and tries to report the offending difference in a nice way. | |
889 /// | |
890 /// If it's a [RegExp] or [Matcher], just reports whether the output matches. | |
891 void _validateOutput(List<String> failures, String pipe, expected, | |
892 String actual) { | |
893 if (expected == null) return; | |
894 | |
895 if (expected is String) { | |
896 _validateOutputString(failures, pipe, expected, actual); | |
897 } else { | |
898 if (expected is RegExp) expected = matches(expected); | |
899 expect(actual, expected); | |
900 } | |
901 } | |
902 | |
903 void _validateOutputString(List<String> failures, String pipe, String expected, | |
904 String actual) { | |
905 var actualLines = actual.split("\n"); | |
906 var expectedLines = expected.split("\n"); | |
907 | |
908 // Strip off the last line. This lets us have expected multiline strings | |
909 // where the closing ''' is on its own line. It also fixes '' expected output | |
910 // to expect zero lines of output, not a single empty line. | |
911 if (expectedLines.last.trim() == '') { | |
912 expectedLines.removeLast(); | |
913 } | |
914 | |
915 var results = []; | |
916 var failed = false; | |
917 | |
918 // Compare them line by line to see which ones match. | |
919 var length = max(expectedLines.length, actualLines.length); | |
920 for (var i = 0; i < length; i++) { | |
921 if (i >= actualLines.length) { | |
922 // Missing output. | |
923 failed = true; | |
924 results.add('? ${expectedLines[i]}'); | |
925 } else if (i >= expectedLines.length) { | |
926 // Unexpected extra output. | |
927 failed = true; | |
928 results.add('X ${actualLines[i]}'); | |
929 } else { | |
930 var expectedLine = expectedLines[i].trim(); | |
931 var actualLine = actualLines[i].trim(); | |
932 | |
933 if (expectedLine != actualLine) { | |
934 // Mismatched lines. | |
935 failed = true; | |
936 results.add('X ${actualLines[i]}'); | |
937 } else { | |
938 // Output is OK, but include it in case other lines are wrong. | |
939 results.add('| ${actualLines[i]}'); | |
940 } | |
941 } | |
942 } | |
943 | |
944 // If any lines mismatched, show the expected and actual. | |
945 if (failed) { | |
946 failures.add('Expected $pipe:'); | |
947 failures.addAll(expectedLines.map((line) => '| $line')); | |
948 failures.add('Got:'); | |
949 failures.addAll(results); | |
950 } | |
951 } | |
952 | |
953 /// Validates that [actualText] is a string of JSON that matches [expected], | |
954 /// which may be a literal JSON object, or any other [Matcher]. | |
955 void _validateOutputJson(List<String> failures, String pipe, expected, | |
956 String actualText) { | |
957 var actual; | |
958 try { | |
959 actual = JSON.decode(actualText); | |
960 } on FormatException catch (error) { | |
961 failures.add('Expected $pipe JSON:'); | |
962 failures.add(expected); | |
963 failures.add('Got invalid JSON:'); | |
964 failures.add(actualText); | |
965 } | |
966 | |
967 // Match against the expectation. | |
968 expect(actual, expected); | |
969 } | |
970 | |
971 /// A function that creates a [Validator] subclass. | |
972 typedef Validator ValidatorCreator(Entrypoint entrypoint); | |
973 | |
974 /// Schedules a single [Validator] to run on the [appPath]. | |
975 /// | |
976 /// Returns a scheduled Future that contains the errors and warnings produced | |
977 /// by that validator. | |
978 Future<Pair<List<String>, List<String>>> | |
979 schedulePackageValidation(ValidatorCreator fn) { | |
980 return schedule(() { | |
981 var cache = | |
982 new SystemCache.withSources(rootDir: p.join(sandboxDir, cachePath)); | |
983 | |
984 return new Future.sync(() { | |
985 var validator = fn(new Entrypoint(p.join(sandboxDir, appPath), cache)); | |
986 return validator.validate().then((_) { | |
987 return new Pair(validator.errors, validator.warnings); | |
988 }); | |
989 }); | |
990 }, "validating package"); | |
991 } | |
992 | |
993 /// A matcher that matches a Pair. | |
994 Matcher pairOf(Matcher firstMatcher, Matcher lastMatcher) => | |
995 new _PairMatcher(firstMatcher, lastMatcher); | |
996 | |
997 class _PairMatcher extends Matcher { | |
998 final Matcher _firstMatcher; | |
999 final Matcher _lastMatcher; | |
1000 | |
1001 _PairMatcher(this._firstMatcher, this._lastMatcher); | |
1002 | |
1003 bool matches(item, Map matchState) { | |
1004 if (item is! Pair) return false; | |
1005 return _firstMatcher.matches(item.first, matchState) && | |
1006 _lastMatcher.matches(item.last, matchState); | |
1007 } | |
1008 | |
1009 Description describe(Description description) { | |
1010 return description.addAll("(", ", ", ")", [_firstMatcher, _lastMatcher]); | |
1011 } | |
1012 } | |
1013 | |
1014 /// A [StreamMatcher] that matches multiple lines of output. | |
1015 StreamMatcher emitsLines(String output) => inOrder(output.split("\n")); | |
OLD | NEW |