Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, 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 /** | 5 /** |
| 6 * Test infrastructure for testing pub. Unlike typical unit tests, most pub | 6 * Test infrastructure for testing pub. Unlike typical unit tests, most pub |
| 7 * tests are integration tests that stage some stuff on the file system, run | 7 * tests are integration tests that stage some stuff on the file system, run |
| 8 * pub, and then validate the results. This library provides an API to build | 8 * pub, and then validate the results. This library provides an API to build |
| 9 * tests like that. | 9 * tests like that. |
| 10 */ | 10 */ |
| 11 #library('test_pub'); | 11 #library('test_pub'); |
| 12 | 12 |
| 13 #import('dart:io'); | 13 #import('dart:io'); |
| 14 #import('dart:uri'); | 14 #import('dart:uri'); |
| 15 | 15 |
| 16 #import('../../../lib/unittest/unittest.dart'); | 16 #import('../../../lib/unittest/unittest.dart'); |
| 17 #import('../../lib/file_system.dart', prefix: 'fs'); | 17 #import('../../lib/file_system.dart', prefix: 'fs'); |
| 18 #import('../../pub/io.dart'); | 18 #import('../../pub/io.dart'); |
| 19 #import('../../pub/utils.dart'); | |
| 19 #import('../../pub/yaml/yaml.dart'); | 20 #import('../../pub/yaml/yaml.dart'); |
| 20 | 21 |
| 21 /** | 22 /** |
| 22 * Creates a new [FileDescriptor] with [name] and [contents]. | 23 * Creates a new [FileDescriptor] with [name] and [contents]. |
| 23 */ | 24 */ |
| 24 FileDescriptor file(String name, String contents) => | 25 FileDescriptor file(Pattern name, String contents) => |
| 25 new FileDescriptor(name, contents); | 26 new FileDescriptor(name, contents); |
| 26 | 27 |
| 27 /** | 28 /** |
| 28 * Creates a new [DirectoryDescriptor] with [name] and [contents]. | 29 * Creates a new [DirectoryDescriptor] with [name] and [contents]. |
| 29 */ | 30 */ |
| 30 DirectoryDescriptor dir(String name, [List<Descriptor> contents]) => | 31 DirectoryDescriptor dir(Pattern name, [List<Descriptor> contents]) => |
| 31 new DirectoryDescriptor(name, contents); | 32 new DirectoryDescriptor(name, contents); |
| 32 | 33 |
| 33 /** | 34 /** |
| 34 * Creates a new [GitRepoDescriptor] with [name] and [contents]. | 35 * Creates a new [GitRepoDescriptor] with [name] and [contents]. |
| 35 */ | 36 */ |
| 36 DirectoryDescriptor git(String name, [List<Descriptor> contents]) => | 37 DirectoryDescriptor git(Pattern name, [List<Descriptor> contents]) => |
| 37 new GitRepoDescriptor(name, contents); | 38 new GitRepoDescriptor(name, contents); |
| 38 | 39 |
| 39 /** | 40 /** |
| 40 * Creates a new [TarFileDescriptor] with [name] and [contents]. | 41 * Creates a new [TarFileDescriptor] with [name] and [contents]. |
| 41 */ | 42 */ |
| 42 TarFileDescriptor tar(String name, [List<Descriptor> contents]) => | 43 TarFileDescriptor tar(Pattern name, [List<Descriptor> contents]) => |
| 43 new TarFileDescriptor(name, contents); | 44 new TarFileDescriptor(name, contents); |
| 44 | 45 |
| 45 /** | 46 /** |
| 46 * Creates an HTTP server to serve [contents] as static files. This server will | 47 * Creates an HTTP server to serve [contents] as static files. This server will |
| 47 * exist only for the duration of the pub run. | 48 * exist only for the duration of the pub run. |
| 48 */ | 49 */ |
| 49 void serve(String host, int port, [List<Descriptor> contents]) { | 50 void serve(String host, int port, [List<Descriptor> contents]) { |
| 50 var baseDir = dir("serve-dir", contents); | 51 var baseDir = dir("serve-dir", contents); |
| 51 if (host == 'localhost') { | 52 if (host == 'localhost') { |
| 52 host = '127.0.0.1'; | 53 host = '127.0.0.1'; |
| 53 } | 54 } |
| 54 | 55 |
| 55 _scheduleBeforePub((_) { | 56 _schedule((_) { |
| 56 var server = new HttpServer(); | 57 var server = new HttpServer(); |
| 57 server.defaultRequestHandler = (request, response) { | 58 server.defaultRequestHandler = (request, response) { |
| 58 var path = request.uri.replaceFirst("/", "").split("/"); | 59 var path = request.uri.replaceFirst("/", "").split("/"); |
| 59 var stream = baseDir.load(path); | 60 var stream = baseDir.load(path); |
| 60 response.persistentConnection = false; | 61 response.persistentConnection = false; |
| 61 if (stream == null) { | 62 if (stream == null) { |
| 62 response.statusCode = 404; | 63 response.statusCode = 404; |
| 63 response.outputStream.close(); | 64 response.outputStream.close(); |
| 64 return; | 65 return; |
| 65 } | 66 } |
| (...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 138 */ | 139 */ |
| 139 final String packagesPath = "$appPath/packages"; | 140 final String packagesPath = "$appPath/packages"; |
| 140 | 141 |
| 141 /** | 142 /** |
| 142 * The type for callbacks that will be fired during [runPub]. Takes the sandbox | 143 * The type for callbacks that will be fired during [runPub]. Takes the sandbox |
| 143 * directory as a parameter. | 144 * directory as a parameter. |
| 144 */ | 145 */ |
| 145 typedef Future _ScheduledEvent(Directory parentDir); | 146 typedef Future _ScheduledEvent(Directory parentDir); |
| 146 | 147 |
| 147 /** | 148 /** |
| 148 * The list of events that are scheduled to run after the sandbox directory has | 149 * The list of events that are scheduled to run as part of the test case. |
| 149 * been created but before Pub is run. | |
| 150 */ | 150 */ |
| 151 List<_ScheduledEvent> _scheduledBeforePub; | 151 List<_ScheduledEvent> _scheduled; |
| 152 | 152 |
| 153 /** | 153 /** |
| 154 * The list of events that are scheduled to run after Pub has been run. | 154 * The list of events that are scheduled to run after the test case, even if it |
| 155 */ | 155 * failed. |
| 156 List<_ScheduledEvent> _scheduledAfterPub; | |
| 157 | |
| 158 /** | |
| 159 * The list of events that are scheduled to run after Pub has been run, even if | |
| 160 * it failed. | |
| 161 */ | 156 */ |
| 162 List<_ScheduledEvent> _scheduledCleanup; | 157 List<_ScheduledEvent> _scheduledCleanup; |
| 163 | 158 |
| 164 void runPub([List<String> args, Pattern output, Pattern error, | 159 /** |
| 165 int exitCode = 0]) { | 160 * Runs all the scheduled events for a test case. This should only be called |
| 161 * once per test case. | |
| 162 */ | |
| 163 void run() { | |
| 166 var createdSandboxDir; | 164 var createdSandboxDir; |
| 167 | 165 |
| 168 var asyncDone = expectAsync0(() {}); | 166 var asyncDone = expectAsync0(() {}); |
| 169 | 167 |
| 170 Future cleanup() { | 168 Future cleanup() { |
| 171 return _runScheduled(createdSandboxDir, _scheduledCleanup).chain((_) { | 169 return _runScheduled(createdSandboxDir, _scheduledCleanup).chain((_) { |
| 172 _scheduledBeforePub = null; | 170 _scheduled = null; |
| 173 _scheduledAfterPub = null; | |
| 174 if (createdSandboxDir != null) return deleteDir(createdSandboxDir); | 171 if (createdSandboxDir != null) return deleteDir(createdSandboxDir); |
| 175 return new Future.immediate(null); | 172 return new Future.immediate(null); |
| 176 }); | 173 }); |
| 177 } | 174 } |
| 178 | 175 |
| 179 String pathInSandbox(path) => join(getFullPath(createdSandboxDir), path); | |
| 180 | |
| 181 final future = _setUpSandbox().chain((sandboxDir) { | 176 final future = _setUpSandbox().chain((sandboxDir) { |
| 182 createdSandboxDir = sandboxDir; | 177 createdSandboxDir = sandboxDir; |
| 183 return _runScheduled(sandboxDir, _scheduledBeforePub); | 178 return _runScheduled(sandboxDir, _scheduled); |
| 184 }).chain((_) { | |
| 185 return ensureDir(pathInSandbox(appPath)); | |
| 186 }).chain((_) { | |
| 187 // TODO(rnystrom): Hack in the cache directory path. Should pass this | |
| 188 // in using environment var once #752 is done. | |
| 189 args.add('--cachedir=${pathInSandbox(cachePath)}'); | |
| 190 | |
| 191 // TODO(rnystrom): Hack in the SDK path. Should pass this in using | |
| 192 // environment var once #752 is done. | |
| 193 args.add('--sdkdir=${pathInSandbox(sdkPath)}'); | |
| 194 | |
| 195 return _runPub(args, pathInSandbox(appPath), pipeStdout: output == null, | |
| 196 pipeStderr: error == null); | |
| 197 }).chain((result) { | |
| 198 _validateOutput(output, result.stdout); | |
| 199 _validateOutput(error, result.stderr); | |
| 200 | |
| 201 Expect.equals(result.exitCode, exitCode, | |
| 202 'Pub returned exit code ${result.exitCode}, expected $exitCode.'); | |
| 203 | |
| 204 return _runScheduled(createdSandboxDir, _scheduledAfterPub); | |
| 205 }); | 179 }); |
| 206 | 180 |
| 207 future.chain((_) => cleanup()).then((_) => asyncDone()); | |
| 208 | |
| 209 future.handleException((error) { | 181 future.handleException((error) { |
| 210 // If an error occurs during testing, delete the sandbox, throw the error so | 182 // If an error occurs during testing, delete the sandbox, throw the error so |
| 211 // that the test framework sees it, then finally call asyncDone so that the | 183 // that the test framework sees it, then finally call asyncDone so that the |
| 212 // test framework knows we're done doing asynchronous stuff. | 184 // test framework knows we're done doing asynchronous stuff. |
| 213 cleanup().then((_) { | 185 cleanup().then((_) { |
| 214 guardAsync(() { throw error; }, asyncDone); | 186 guardAsync(() { throw error; }, asyncDone); |
| 215 }); | 187 }); |
| 216 return true; | 188 return true; |
| 217 }); | 189 }); |
| 190 | |
| 191 future.chain((_) => cleanup()).then((_) => asyncDone()); | |
| 218 } | 192 } |
| 219 | 193 |
| 194 /** | |
| 195 * Schedules a call to the Pub command-line utility. Runs Pub with [args] and | |
| 196 * validates that its results match [output], [error], and [exitCode]. | |
| 197 */ | |
| 198 void schedulePub([List<String> args, Pattern output, Pattern error, | |
| 199 int exitCode = 0]) { | |
| 200 _schedule((sandboxDir) { | |
| 201 String pathInSandbox(path) => join(getFullPath(sandboxDir), path); | |
| 202 | |
| 203 return ensureDir(pathInSandbox(appPath)).chain((_) { | |
| 204 // TODO(rnystrom): Hack in the cache directory path. Should pass this in | |
| 205 // using environment var once #752 is done. | |
| 206 args.add('--cachedir=${pathInSandbox(cachePath)}'); | |
| 207 | |
| 208 // TODO(rnystrom): Hack in the SDK path. Should pass this in using | |
| 209 // environment var once #752 is done. | |
| 210 args.add('--sdkdir=${pathInSandbox(sdkPath)}'); | |
| 211 | |
| 212 return _runPub(args, pathInSandbox(appPath), pipeStdout: output == null, | |
| 213 pipeStderr: error == null); | |
| 214 }).transform((result) { | |
| 215 _validateOutput(output, result.stdout); | |
| 216 _validateOutput(error, result.stderr); | |
| 217 | |
| 218 Expect.equals(result.exitCode, exitCode, | |
| 219 'Pub returned exit code ${result.exitCode}, expected $exitCode.'); | |
| 220 | |
| 221 return null; | |
| 222 }); | |
| 223 }); | |
| 224 } | |
| 225 | |
| 226 /** | |
| 227 * A shorthand for [schedulePub] and [run] when no validation needs to be done | |
| 228 * after Pub has been run. | |
| 229 */ | |
| 230 void runPub([List<String> args, Pattern output, Pattern error, | |
| 231 int exitCode = 0]) { | |
| 232 schedulePub(args, output, error, exitCode); | |
| 233 run(); | |
| 234 } | |
| 220 | 235 |
| 221 /** | 236 /** |
| 222 * Wraps a test that needs git in order to run. This validates that the test is | 237 * Wraps a test that needs git in order to run. This validates that the test is |
| 223 * running on a builbot in which case we expect git to be installed. If we are | 238 * running on a builbot in which case we expect git to be installed. If we are |
| 224 * not running on the buildbot, we will instead see if git is installed and | 239 * not running on the buildbot, we will instead see if git is installed and |
| 225 * skip the test if not. This way, users don't need to have git installed to | 240 * skip the test if not. This way, users don't need to have git installed to |
| 226 * run the tests locally (unless they actually care about the pub git tests). | 241 * run the tests locally (unless they actually care about the pub git tests). |
| 227 */ | 242 */ |
| 228 void withGit(void callback()) { | 243 void withGit(void callback()) { |
| 229 isGitInstalled.then(expectAsync1((installed) { | 244 isGitInstalled.then(expectAsync1((installed) { |
| 230 if (installed || Platform.environment.containsKey('BUILDBOT_BUILDERNAME')) { | 245 if (installed || Platform.environment.containsKey('BUILDBOT_BUILDERNAME')) { |
| 231 callback(); | 246 callback(); |
| 232 } | 247 } |
| 233 })); | 248 })); |
| 234 } | 249 } |
| 235 | 250 |
| 236 Future<Directory> _setUpSandbox() { | 251 Future<Directory> _setUpSandbox() { |
| 237 return createTempDir('pub-test-sandbox-'); | 252 return createTempDir('pub-test-sandbox-'); |
| 238 } | 253 } |
| 239 | 254 |
| 240 _runScheduled(Directory parentDir, List<_ScheduledEvent> scheduled) { | 255 Future _runScheduled(Directory parentDir, List<_ScheduledEvent> scheduled) { |
| 241 if (scheduled == null) return new Future.immediate(null); | 256 if (scheduled == null) return new Future.immediate(null); |
| 242 var future = Futures.wait(scheduled.map((event) { | 257 var iterator = scheduled.iterator(); |
| 243 var subFuture = event(parentDir); | 258 |
| 244 return subFuture == null ? new Future.immediate(null) : subFuture; | 259 Future runNextEvent([_]) { |
| 245 })); | 260 if (!iterator.hasNext()) { |
| 246 scheduled.clear(); | 261 scheduled.clear(); |
| 247 return future; | 262 return new Future.immediate(null); |
| 263 } | |
| 264 | |
| 265 var future = iterator.next()(parentDir); | |
| 266 if (future != null) { | |
| 267 return future.chain(runNextEvent); | |
| 268 } else { | |
| 269 return runNextEvent(); | |
| 270 } | |
| 271 } | |
| 272 | |
| 273 return runNextEvent(); | |
| 248 } | 274 } |
| 249 | 275 |
| 250 Future<ProcessResult> _runPub(List<String> pubArgs, String workingDir, | 276 Future<ProcessResult> _runPub(List<String> pubArgs, String workingDir, |
| 251 [bool pipeStdout=false, bool pipeStderr=false]) { | 277 [bool pipeStdout=false, bool pipeStderr=false]) { |
| 252 // Find a dart executable we can use to run pub. Uses the one that the | 278 // Find a dart executable we can use to run pub. Uses the one that the |
| 253 // test infrastructure uses. We are not using new Options.executable here | 279 // test infrastructure uses. We are not using new Options.executable here |
| 254 // because that gets confused if you invoked Dart through a shell script. | 280 // because that gets confused if you invoked Dart through a shell script. |
| 255 final scriptDir = new File(new Options().script).directorySync().path; | 281 final scriptDir = new File(new Options().script).directorySync().path; |
| 256 final platform = Platform.operatingSystem; | 282 final platform = Platform.operatingSystem; |
| 257 final dartBin = join(scriptDir, '../../../tools/testing/bin/$platform/dart'); | 283 final dartBin = join(scriptDir, '../../../tools/testing/bin/$platform/dart'); |
| (...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 319 } | 345 } |
| 320 } | 346 } |
| 321 | 347 |
| 322 /** | 348 /** |
| 323 * Base class for [FileDescriptor] and [DirectoryDescriptor] so that a | 349 * Base class for [FileDescriptor] and [DirectoryDescriptor] so that a |
| 324 * directory can contain a heterogeneous collection of files and | 350 * directory can contain a heterogeneous collection of files and |
| 325 * subdirectories. | 351 * subdirectories. |
| 326 */ | 352 */ |
| 327 class Descriptor { | 353 class Descriptor { |
| 328 /** | 354 /** |
| 329 * The short name of this file or directory. | 355 * The name of this file or directory. This must be a [String] if the fiel or |
| 356 * directory is going to be created. | |
| 330 */ | 357 */ |
| 331 final String name; | 358 final Pattern name; |
| 332 | 359 |
| 333 Descriptor(this.name); | 360 Descriptor(this.name); |
| 334 | 361 |
| 335 /** | 362 /** |
| 336 * Creates the file or directory within [dir]. Returns a [Future] that is | 363 * Creates the file or directory within [dir]. Returns a [Future] that is |
| 337 * completed after the creation is done. | 364 * completed after the creation is done. |
| 338 */ | 365 */ |
| 339 abstract Future create(dir); | 366 abstract Future create(dir); |
| 340 | 367 |
| 341 /** | 368 /** |
| 342 * Validates that this descriptor correctly matches the corresponding file | 369 * Validates that this descriptor correctly matches the corresponding file |
| 343 * system entry within [dir]. Returns a [Future] that completes to `null` if | 370 * system entry within [dir]. Returns a [Future] that completes to `null` if |
| 344 * the entry is valid, or throws an error if it failed. | 371 * the entry is valid, or throws an error if it failed. |
| 345 */ | 372 */ |
| 346 abstract Future validate(String dir); | 373 abstract Future validate(String dir); |
| 347 | 374 |
| 348 /** | 375 /** |
| 376 * Deletes the file or directory within [dir]. Returns a [Future] that is | |
| 377 * completed after the deletion is done. | |
| 378 */ | |
| 379 abstract Future delete(String dir); | |
| 380 | |
| 381 /** | |
| 349 * Loads the file at [path] from within this descriptor. If [path] is empty, | 382 * Loads the file at [path] from within this descriptor. If [path] is empty, |
| 350 * loads the contents of the descriptor itself. | 383 * loads the contents of the descriptor itself. |
| 351 */ | 384 */ |
| 352 abstract InputStream load(List<String> path); | 385 abstract InputStream load(List<String> path); |
| 353 | 386 |
| 354 /** | 387 /** |
| 355 * Schedules the directory to be created before Pub is run with [runPub]. The | 388 * Schedules the directory to be created before Pub is run with [runPub]. The |
| 356 * directory will be created relative to the sandbox directory. | 389 * directory will be created relative to the sandbox directory. |
| 357 */ | 390 */ |
| 358 // TODO(nweiz): Use implicit closurization once issue 2984 is fixed. | 391 // TODO(nweiz): Use implicit closurization once issue 2984 is fixed. |
| 359 void scheduleCreate() => _scheduleBeforePub((dir) => this.create(dir)); | 392 void scheduleCreate() => _schedule((dir) => this.create(dir)); |
| 393 | |
| 394 /** | |
| 395 * Schedules the file or directory to be deleted recursively. | |
| 396 */ | |
| 397 void scheduleDelete() => _schedule((dir) => this.delete(dir)); | |
| 360 | 398 |
| 361 /** | 399 /** |
| 362 * Schedules the directory to be validated after Pub is run with [runPub]. The | 400 * Schedules the directory to be validated after Pub is run with [runPub]. The |
| 363 * directory will be validated relative to the sandbox directory. | 401 * directory will be validated relative to the sandbox directory. |
| 364 */ | 402 */ |
| 365 void scheduleValidate() => | 403 void scheduleValidate() => _schedule((parentDir) => validate(parentDir.path)); |
| 366 _scheduleAfterPub((parentDir) => validate(parentDir.path)); | 404 |
| 405 /** | |
| 406 * Asserts that the name of the descriptor is a [String] and returns it. | |
| 407 */ | |
| 408 String get _stringName() { | |
| 409 if (name is String) return name; | |
| 410 throw 'Pattern $name must be a string.'; | |
| 411 } | |
| 412 | |
| 413 /** | |
| 414 * Validates that at least one file in [dir] matching [name] is valid | |
| 415 * according to [validate]. [validate] should complete to an exception if the | |
| 416 * input path is invalid. | |
| 417 */ | |
| 418 Future _validateOneMatch(String dir, Future validate(String path)) { | |
| 419 // Special-case strings to support multi-level names like "myapp/packages". | |
| 420 if (name is String) { | |
| 421 var path = join(dir, name); | |
| 422 return exists(path).chain((exists) { | |
| 423 if (!exists) Expect.fail('File $name in $dir not found.'); | |
| 424 return validate(path); | |
| 425 }); | |
| 426 } | |
| 427 | |
| 428 return listDir(dir).chain((files) { | |
| 429 var matches = files.filter((file) => endsWithPattern(file, name)); | |
| 430 if (matches.length == 0) { | |
| 431 Expect.fail('No files in $dir match pattern $name.'); | |
| 432 } | |
| 433 if (matches.length == 1) return validate(matches[0]); | |
| 434 | |
| 435 var failures = []; | |
| 436 var completer = new Completer(); | |
| 437 for (var match in matches) { | |
| 438 var future = validate(match); | |
| 439 | |
| 440 future.handleException((e) { | |
| 441 failures.add(e); | |
| 442 if (failures.length != matches.length) return true; | |
| 443 | |
| 444 var error = new StringBuffer(); | |
| 445 error.add("No files named $name in $dir were valid:\n"); | |
| 446 for (var failure in failures) { | |
| 447 error.add(" ").add(failure).add("\n"); | |
| 448 } | |
| 449 completer.completeException(new ExpectException(error.toString())); | |
| 450 return true; | |
| 451 }); | |
| 452 | |
| 453 future.then(completer.complete); | |
| 454 } | |
| 455 return completer.future; | |
| 456 }); | |
| 457 } | |
| 367 } | 458 } |
| 368 | 459 |
| 369 /** | 460 /** |
| 370 * Describes a file. These are used both for setting up an expected directory | 461 * Describes a file. These are used both for setting up an expected directory |
| 371 * tree before running a test, and for validating that the file system matches | 462 * tree before running a test, and for validating that the file system matches |
| 372 * some expectations after running it. | 463 * some expectations after running it. |
| 373 */ | 464 */ |
| 374 class FileDescriptor extends Descriptor { | 465 class FileDescriptor extends Descriptor { |
| 375 /** | 466 /** |
| 376 * The text contents of the file. | 467 * The text contents of the file. |
| 377 */ | 468 */ |
| 378 final String contents; | 469 final String contents; |
| 379 | 470 |
| 380 FileDescriptor(String name, this.contents) : super(name); | 471 FileDescriptor(Pattern name, this.contents) : super(name); |
| 381 | 472 |
| 382 /** | 473 /** |
| 383 * Creates the file within [dir]. Returns a [Future] that is completed after | 474 * Creates the file within [dir]. Returns a [Future] that is completed after |
| 384 * the creation is done. | 475 * the creation is done. |
| 385 */ | 476 */ |
| 386 Future<File> create(dir) { | 477 Future<File> create(dir) { |
| 387 return writeTextFile(join(dir, name), contents); | 478 return writeTextFile(join(dir, _stringName), contents); |
| 479 } | |
| 480 | |
| 481 /** | |
| 482 * Deletes the file within [dir]. Returns a [Future] that is completed after | |
| 483 * the deletion is done. | |
| 484 */ | |
| 485 Future delete(dir) { | |
| 486 return deleteFile(join(dir, _stringName)); | |
| 388 } | 487 } |
| 389 | 488 |
| 390 /** | 489 /** |
| 391 * Validates that this file correctly matches the actual file at [path]. | 490 * Validates that this file correctly matches the actual file at [path]. |
| 392 */ | 491 */ |
| 393 Future validate(String path) { | 492 Future validate(String path) { |
| 394 path = join(path, name); | 493 return _validateOneMatch(path, (file) { |
| 395 return fileExists(path).chain((exists) { | 494 return readTextFile(file).transform((text) { |
| 396 if (!exists) Expect.fail('Expected file $path does not exist.'); | |
| 397 | |
| 398 return readTextFile(path).transform((text) { | |
| 399 if (text == contents) return null; | 495 if (text == contents) return null; |
| 400 | 496 |
| 401 Expect.fail('File $path should contain:\n\n$contents\n\n' | 497 Expect.fail('File $file should contain:\n\n$contents\n\n' |
| 402 'but contained:\n\n$text'); | 498 'but contained:\n\n$text'); |
| 403 }); | 499 }); |
| 404 }); | 500 }); |
| 405 } | 501 } |
| 406 | 502 |
| 407 /** | 503 /** |
| 408 * Loads the contents of the file. | 504 * Loads the contents of the file. |
| 409 */ | 505 */ |
| 410 InputStream load(List<String> path) { | 506 InputStream load(List<String> path) { |
| 411 if (!path.isEmpty()) { | 507 if (!path.isEmpty()) { |
| (...skipping 11 matching lines...) Expand all Loading... | |
| 423 * Describes a directory and its contents. These are used both for setting up | 519 * Describes a directory and its contents. These are used both for setting up |
| 424 * an expected directory tree before running a test, and for validating that | 520 * an expected directory tree before running a test, and for validating that |
| 425 * the file system matches some expectations after running it. | 521 * the file system matches some expectations after running it. |
| 426 */ | 522 */ |
| 427 class DirectoryDescriptor extends Descriptor { | 523 class DirectoryDescriptor extends Descriptor { |
| 428 /** | 524 /** |
| 429 * The files and directories contained in this directory. | 525 * The files and directories contained in this directory. |
| 430 */ | 526 */ |
| 431 final List<Descriptor> contents; | 527 final List<Descriptor> contents; |
| 432 | 528 |
| 433 DirectoryDescriptor(String name, this.contents) : super(name); | 529 DirectoryDescriptor(Pattern name, this.contents) : super(name); |
| 434 | 530 |
| 435 /** | 531 /** |
| 436 * Creates the file within [dir]. Returns a [Future] that is completed after | 532 * Creates the file within [dir]. Returns a [Future] that is completed after |
| 437 * the creation is done. | 533 * the creation is done. |
| 438 */ | 534 */ |
| 439 Future<Directory> create(parentDir) { | 535 Future<Directory> create(parentDir) { |
| 440 final completer = new Completer<Directory>(); | 536 final completer = new Completer<Directory>(); |
| 441 | 537 |
| 442 // Create the directory. | 538 // Create the directory. |
| 443 createDir(join(parentDir, name)).then((dir) { | 539 ensureDir(join(parentDir, _stringName)).then((dir) { |
| 444 if (contents == null) { | 540 if (contents == null) { |
| 445 completer.complete(dir); | 541 completer.complete(dir); |
| 446 } else { | 542 } else { |
| 447 // Recursively create all of its children. | 543 // Recursively create all of its children. |
| 448 final childFutures = contents.map((child) => child.create(dir)); | 544 final childFutures = contents.map((child) => child.create(dir)); |
| 449 Futures.wait(childFutures).then((_) { | 545 Futures.wait(childFutures).then((_) { |
| 450 // Only complete once all of the children have been created too. | 546 // Only complete once all of the children have been created too. |
| 451 completer.complete(dir); | 547 completer.complete(dir); |
| 452 }); | 548 }); |
| 453 } | 549 } |
| 454 }); | 550 }); |
| 455 | 551 |
| 456 return completer.future; | 552 return completer.future; |
| 457 } | 553 } |
| 458 | 554 |
| 459 /** | 555 /** |
| 556 * Deletes the directory within [dir]. Returns a [Future] that is completed | |
| 557 * after the deletion is done. | |
| 558 */ | |
| 559 Future delete(dir) { | |
| 560 return deleteDir(join(dir, _stringName)); | |
| 561 } | |
| 562 | |
| 563 /** | |
| 460 * Validates that the directory at [path] contains all of the expected | 564 * Validates that the directory at [path] contains all of the expected |
| 461 * contents in this descriptor. Note that this does *not* check that the | 565 * contents in this descriptor. Note that this does *not* check that the |
| 462 * directory doesn't contain other unexpected stuff, just that it *does* | 566 * directory doesn't contain other unexpected stuff, just that it *does* |
| 463 * contain the stuff we do expect. | 567 * contain the stuff we do expect. |
| 464 */ | 568 */ |
| 465 Future validate(String path) { | 569 Future validate(String path) { |
| 466 // Validate each of the items in this directory. | 570 return _validateOneMatch(path, (dir) { |
| 467 final entryFutures = contents.map( | 571 // Validate each of the items in this directory. |
| 468 (entry) => entry.validate(join(path, name))); | 572 final entryFutures = contents.map((entry) => entry.validate(dir)); |
| 469 | 573 |
| 470 // If they are all valid, the directory is valid. | 574 // If they are all valid, the directory is valid. |
| 471 return Futures.wait(entryFutures).transform((entries) => null); | 575 return Futures.wait(entryFutures).transform((entries) => null); |
| 576 }); | |
| 472 } | 577 } |
| 473 | 578 |
| 474 /** | 579 /** |
| 475 * Loads [path] from within this directory. | 580 * Loads [path] from within this directory. |
| 476 */ | 581 */ |
| 477 InputStream load(List<String> path) { | 582 InputStream load(List<String> path) { |
| 478 if (path.isEmpty()) { | 583 if (path.isEmpty()) { |
| 479 throw "Can't load the contents of $name: is a directory."; | 584 throw "Can't load the contents of $name: is a directory."; |
| 480 } | 585 } |
| 481 | 586 |
| 482 for (var descriptor in contents) { | 587 for (var descriptor in contents) { |
| 483 if (descriptor.name == path[0]) { | 588 if (descriptor.name == path[0]) { |
| 484 return descriptor.load(path.getRange(1, path.length - 1)); | 589 return descriptor.load(path.getRange(1, path.length - 1)); |
| 485 } | 590 } |
| 486 } | 591 } |
| 487 | 592 |
| 488 throw "Directory $name doesn't contain ${Strings.join('/', path)}."; | 593 throw "Directory $name doesn't contain ${Strings.join('/', path)}."; |
| 489 } | 594 } |
| 490 } | 595 } |
| 491 | 596 |
| 492 /** | 597 /** |
| 493 * Describes a Git repository and its contents. | 598 * Describes a Git repository and its contents. |
| 494 */ | 599 */ |
| 495 class GitRepoDescriptor extends DirectoryDescriptor { | 600 class GitRepoDescriptor extends DirectoryDescriptor { |
| 496 GitRepoDescriptor(String name, List<Descriptor> contents) | 601 GitRepoDescriptor(Pattern name, List<Descriptor> contents) |
| 497 : super(name, contents); | 602 : super(name, contents); |
| 498 | 603 |
| 499 /** | 604 /** |
| 500 * Creates the Git repository and commits the contents. | 605 * Creates the Git repository and commits the contents. |
| 501 */ | 606 */ |
| 502 Future<Directory> create(parentDir) { | 607 Future<Directory> create(parentDir) { |
| 503 var workingDir; | 608 var workingDir; |
| 504 Future runGit(List<String> args) { | 609 Future runGit(List<String> args) => _runGit(args, workingDir); |
| 505 return runProcess('git', args, workingDir: workingDir.path). | |
| 506 transform((result) { | |
| 507 if (!result.success) throw "Error running git: ${result.stderr}"; | |
| 508 return null; | |
| 509 }); | |
| 510 } | |
| 511 | 610 |
| 512 return super.create(parentDir).chain((rootDir) { | 611 return super.create(parentDir).chain((rootDir) { |
| 513 workingDir = rootDir; | 612 workingDir = rootDir; |
| 514 return runGit(['init']); | 613 return runGit(['init']); |
| 515 }).chain((_) => runGit(['add', '.'])) | 614 }).chain((_) => runGit(['add', '.'])) |
| 516 .chain((_) => runGit(['commit', '-m', 'initial commit'])) | 615 .chain((_) => runGit(['commit', '-m', 'initial commit'])) |
| 517 .transform((_) => workingDir); | 616 .transform((_) => workingDir); |
| 518 } | 617 } |
| 618 | |
| 619 /** | |
| 620 * Commits any changes to the Git repository. | |
| 621 */ | |
| 622 Future commit(parentDir) { | |
|
Jennifer Messerly
2012/07/03 02:03:10
anyone using this yet?
nweiz
2012/07/03 18:04:49
No, all these changes are going to be used in a fu
| |
| 623 var workingDir; | |
| 624 Future runGit(List<String> args) => _runGit(args, workingDir); | |
| 625 | |
| 626 return super.create(parentDir).chain((rootDir) { | |
| 627 workingDir = rootDir; | |
| 628 return runGit(['add', '.']); | |
| 629 }).chain((_) => runGit(['commit', '-m', 'update'])); | |
| 630 } | |
| 631 | |
| 632 /** | |
| 633 * Schedules changes to be committed to the Git repository. | |
| 634 */ | |
| 635 void scheduleCommit() => _schedule((dir) => this.commit(dir)); | |
| 636 | |
| 637 Future _runGit(List<String> args, Directory workingDir) { | |
| 638 return runProcess('git', args, workingDir: workingDir.path). | |
| 639 transform((result) { | |
| 640 if (!result.success) throw "Error running git: ${result.stderr}"; | |
| 641 return null; | |
| 642 }); | |
| 643 } | |
| 519 } | 644 } |
| 520 | 645 |
| 521 /** | 646 /** |
| 522 * Describes a gzipped tar file and its contents. | 647 * Describes a gzipped tar file and its contents. |
| 523 */ | 648 */ |
| 524 class TarFileDescriptor extends Descriptor { | 649 class TarFileDescriptor extends Descriptor { |
| 525 final List<Descriptor> contents; | 650 final List<Descriptor> contents; |
| 526 | 651 |
| 527 TarFileDescriptor(String name, this.contents) | 652 TarFileDescriptor(Pattern name, this.contents) |
| 528 : super(name); | 653 : super(name); |
| 529 | 654 |
| 530 /** | 655 /** |
| 531 * Creates the files and directories within this tar file, then archives them, | 656 * Creates the files and directories within this tar file, then archives them, |
| 532 * compresses them, and saves the result to [parentDir]. | 657 * compresses them, and saves the result to [parentDir]. |
| 533 */ | 658 */ |
| 534 Future<File> create(parentDir) { | 659 Future<File> create(parentDir) { |
| 535 var tempDir; | 660 var tempDir; |
| 536 return parentDir.createTemp().chain((_tempDir) { | 661 return parentDir.createTemp().chain((_tempDir) { |
| 537 tempDir = _tempDir; | 662 tempDir = _tempDir; |
| 538 return Futures.wait(contents.map((child) => child.create(tempDir))); | 663 return Futures.wait(contents.map((child) => child.create(tempDir))); |
| 539 }).chain((_) { | 664 }).chain((_) { |
| 540 var args = ["--directory", tempDir.path, "--create", "--gzip", "--file", | 665 var args = ["--directory", tempDir.path, "--create", "--gzip", "--file", |
| 541 join(parentDir, name)]; | 666 join(parentDir, _stringName)]; |
| 542 args.addAll(contents.map((child) => child.name)); | 667 args.addAll(contents.map((child) => child.name)); |
| 543 return runProcess("tar", args); | 668 return runProcess("tar", args); |
| 544 }).chain((result) { | 669 }).chain((result) { |
| 545 if (!result.success) { | 670 if (!result.success) { |
| 546 throw "Failed to create tar file $name.\n" | 671 throw "Failed to create tar file $name.\n" |
| 547 "STDERR: ${Strings.join(result.stderr, "\n")}"; | 672 "STDERR: ${Strings.join(result.stderr, "\n")}"; |
| 548 } | 673 } |
| 549 return deleteDir(tempDir); | 674 return deleteDir(tempDir); |
| 550 }).transform((_) { | 675 }).transform((_) { |
| 551 return new File(join(parentDir, name)); | 676 return new File(join(parentDir, _stringName)); |
| 552 }); | 677 }); |
| 553 } | 678 } |
| 554 | 679 |
| 555 /** | 680 /** |
| 556 * Validates that the `.tar.gz` file at [path] contains the expected contents. | 681 * Validates that the `.tar.gz` file at [path] contains the expected contents. |
| 557 */ | 682 */ |
| 558 Future validate(String path) { | 683 Future validate(String path) { |
| 559 throw "TODO(nweiz): implement this"; | 684 throw "TODO(nweiz): implement this"; |
| 560 } | 685 } |
| 561 | 686 |
| (...skipping 15 matching lines...) Expand all Loading... | |
| 577 }).then((tar) { | 702 }).then((tar) { |
| 578 var sourceStream = tar.openInputStream(); | 703 var sourceStream = tar.openInputStream(); |
| 579 pipeInputToInput( | 704 pipeInputToInput( |
| 580 sourceStream, sinkStream, onClosed: tempDir.deleteRecursively); | 705 sourceStream, sinkStream, onClosed: tempDir.deleteRecursively); |
| 581 }); | 706 }); |
| 582 return sinkStream; | 707 return sinkStream; |
| 583 } | 708 } |
| 584 } | 709 } |
| 585 | 710 |
| 586 /** | 711 /** |
| 587 * Schedules a callback to be called before Pub is run with [runPub]. | 712 * Schedules a callback to be called as part of the test case. |
| 588 */ | 713 */ |
| 589 void _scheduleBeforePub(_ScheduledEvent event) { | 714 void _schedule(_ScheduledEvent event) { |
| 590 if (_scheduledBeforePub == null) _scheduledBeforePub = []; | 715 if (_scheduled == null) _scheduled = []; |
| 591 _scheduledBeforePub.add(event); | 716 _scheduled.add(event); |
| 592 } | 717 } |
| 593 | 718 |
| 594 /** | 719 /** |
| 595 * Schedules a callback to be called after Pub is run with [runPub]. | |
| 596 */ | |
| 597 void _scheduleAfterPub(_ScheduledEvent event) { | |
| 598 if (_scheduledAfterPub == null) _scheduledAfterPub = []; | |
| 599 _scheduledAfterPub.add(event); | |
| 600 } | |
| 601 | |
| 602 /** | |
| 603 * Schedules a callback to be called after Pub is run with [runPub], even if it | 720 * Schedules a callback to be called after Pub is run with [runPub], even if it |
| 604 * fails. | 721 * fails. |
| 605 */ | 722 */ |
| 606 void _scheduleCleanup(_ScheduledEvent event) { | 723 void _scheduleCleanup(_ScheduledEvent event) { |
| 607 if (_scheduledCleanup == null) _scheduledCleanup = []; | 724 if (_scheduledCleanup == null) _scheduledCleanup = []; |
| 608 _scheduledCleanup.add(event); | 725 _scheduledCleanup.add(event); |
| 609 } | 726 } |
| OLD | NEW |