utils.dart 22.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:io';
8
import 'dart:math' as math;
9 10 11

import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
12
import 'package:process/process.dart';
13 14
import 'package:stack_trace/stack_trace.dart';

15 16
import 'framework.dart';

17 18 19
/// Virtual current working directory, which affect functions, such as [exec].
String cwd = Directory.current.path;

20
/// The local engine to use for [flutter] and [evalFlutter], if any.
21 22 23 24 25 26 27 28
String get localEngine {
  // Use two distinct `defaultValue`s to determine whether a 'localEngine'
  // declaration exists in the environment.
  const bool isDefined =
      String.fromEnvironment('localEngine', defaultValue: 'a') ==
          String.fromEnvironment('localEngine', defaultValue: 'b');
  return isDefined ? const String.fromEnvironment('localEngine') : null;
}
29 30 31

/// The local engine source path to use if a local engine is used for [flutter]
/// and [evalFlutter].
32 33 34 35 36 37 38 39
String get localEngineSrcPath {
  // Use two distinct `defaultValue`s to determine whether a
  // 'localEngineSrcPath' declaration exists in the environment.
  const bool isDefined =
      String.fromEnvironment('localEngineSrcPath', defaultValue: 'a') ==
          String.fromEnvironment('localEngineSrcPath', defaultValue: 'b');
  return isDefined ? const String.fromEnvironment('localEngineSrcPath') : null;
}
40

41
List<ProcessInfo> _runningProcesses = <ProcessInfo>[];
42
ProcessManager _processManager = const LocalProcessManager();
43 44 45 46

class ProcessInfo {
  ProcessInfo(this.command, this.process);

47
  final DateTime startTime = DateTime.now();
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
  final String command;
  final Process process;

  @override
  String toString() {
    return '''
  command : $command
  started : $startTime
  pid     : ${process.pid}
'''
        .trim();
  }
}

/// Result of a health check for a specific parameter.
class HealthCheckResult {
  HealthCheckResult.success([this.details]) : succeeded = true;
  HealthCheckResult.failure(this.details) : succeeded = false;
  HealthCheckResult.error(dynamic error, dynamic stackTrace)
      : succeeded = false,
        details = 'ERROR: $error${'\n$stackTrace' ?? ''}';

  final bool succeeded;
  final String details;

  @override
  String toString() {
75
    final StringBuffer buf = StringBuffer(succeeded ? 'succeeded' : 'failed');
76 77 78
    if (details != null && details.trim().isNotEmpty) {
      buf.writeln();
      // Indent details by 4 spaces
79
      for (final String line in details.trim().split('\n')) {
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
        buf.writeln('    $line');
      }
    }
    return '$buf';
  }
}

class BuildFailedError extends Error {
  BuildFailedError(this.message);

  final String message;

  @override
  String toString() => message;
}

void fail(String message) {
97
  throw BuildFailedError(message);
98 99
}

100 101 102 103 104 105 106 107 108 109 110 111
// Remove the given file or directory.
void rm(FileSystemEntity entity, { bool recursive = false}) {
  if (entity.existsSync()) {
    // This should not be necessary, but it turns out that
    // on Windows it's common for deletions to fail due to
    // bogus (we think) "access denied" errors.
    try {
      entity.deleteSync(recursive: recursive);
    } on FileSystemException catch (error) {
      print('Failed to delete ${entity.path}: $error');
    }
  }
112 113 114 115
}

/// Remove recursively.
void rmTree(FileSystemEntity entity) {
116
  rm(entity, recursive: true);
117 118 119 120
}

List<FileSystemEntity> ls(Directory directory) => directory.listSync();

121
Directory dir(String path) => Directory(path);
122

123
File file(String path) => File(path);
124 125

void copy(File sourceFile, Directory targetDirectory, {String name}) {
126
  final File target = file(
127 128 129 130
      path.join(targetDirectory.path, name ?? path.basename(sourceFile.path)));
  target.writeAsBytesSync(sourceFile.readAsBytesSync());
}

131 132 133 134
void recursiveCopy(Directory source, Directory target) {
  if (!target.existsSync())
    target.createSync();

135
  for (final FileSystemEntity entity in source.listSync(followLinks: false)) {
136
    final String name = path.basename(entity.path);
137
    if (entity is Directory && !entity.path.contains('.dart_tool'))
138
      recursiveCopy(entity, Directory(path.join(target.path, name)));
139
    else if (entity is File) {
140
      final File dest = File(path.join(target.path, name));
141
      dest.writeAsBytesSync(entity.readAsBytesSync());
142 143 144 145 146
      // Preserve executable bit
      final String modes = entity.statSync().modeString();
      if (modes != null && modes.contains('x')) {
        makeExecutable(dest);
      }
147 148 149 150
    }
  }
}

151 152 153 154 155 156
FileSystemEntity move(FileSystemEntity whatToMove,
    {Directory to, String name}) {
  return whatToMove
      .renameSync(path.join(to.path, name ?? path.basename(whatToMove.path)));
}

157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
/// Equivalent of `chmod a+x file`
void makeExecutable(File file) {
  // Windows files do not have an executable bit
  if (Platform.isWindows) {
    return;
  }
  final ProcessResult result = _processManager.runSync(<String>[
    'chmod',
    'a+x',
    file.path,
  ]);

  if (result.exitCode != 0) {
    throw FileSystemException(
      'Error making ${file.path} executable.\n'
      '${result.stderr}',
      file.path,
    );
  }
}

178 179 180 181 182 183 184 185 186 187 188 189 190
/// Equivalent of `mkdir directory`.
void mkdir(Directory directory) {
  directory.createSync();
}

/// Equivalent of `mkdir -p directory`.
void mkdirs(Directory directory) {
  directory.createSync(recursive: true);
}

bool exists(FileSystemEntity entity) => entity.existsSync();

void section(String title) {
191 192 193 194 195 196 197 198 199 200 201 202
  String output;
  if (Platform.isWindows) {
    // Windows doesn't cope well with characters produced for *nix systems, so
    // just output the title with no decoration.
    output = title;
  } else {
    title = '╡ ••• $title ••• ╞';
    final String line = '═' * math.max((80 - title.length) ~/ 2, 2);
    output = '$line$title$line';
    if (output.length == 79)
      output += '═';
  }
203
  print('\n\n$output\n');
204 205 206 207
}

Future<String> getDartVersion() async {
  // The Dart VM returns the version text to stderr.
208
  final ProcessResult result = _processManager.runSync(<String>[dartBin, '--version']);
209
  String version = (result.stderr as String).trim();
210 211 212 213 214

  // Convert:
  //   Dart VM version: 1.17.0-dev.2.0 (Tue May  3 12:14:52 2016) on "macos_x64"
  // to:
  //   1.17.0-dev.2.0
215
  if (version.contains('('))
216
    version = version.substring(0, version.indexOf('(')).trim();
217
  if (version.contains(':'))
218 219 220 221 222 223 224
    version = version.substring(version.indexOf(':') + 1).trim();

  return version.replaceAll('"', "'");
}

Future<String> getCurrentFlutterRepoCommit() {
  if (!dir('${flutterDirectory.path}/.git').existsSync()) {
225
    return Future<String>.value(null);
226 227
  }

228
  return inDirectory<String>(flutterDirectory, () {
229 230 231 232 233 234
    return eval('git', <String>['rev-parse', 'HEAD']);
  });
}

Future<DateTime> getFlutterRepoCommitTimestamp(String commit) {
  // git show -s --format=%at 4b546df7f0b3858aaaa56c4079e5be1ba91fbb65
235
  return inDirectory<DateTime>(flutterDirectory, () async {
236
    final String unixTimestamp = await eval('git', <String>[
237 238 239 240 241
      'show',
      '-s',
      '--format=%at',
      commit,
    ]);
242
    final int secondsSinceEpoch = int.parse(unixTimestamp);
243
    return DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000);
244 245 246
  });
}

247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
/// Starts a subprocess.
///
/// The first argument is the full path to the executable to run.
///
/// The second argument is the list of arguments to provide on the command line.
/// This argument can be null, indicating no arguments (same as the empty list).
///
/// The `environment` argument can be provided to configure environment variables
/// that will be made available to the subprocess. The `BOT` environment variable
/// is always set and overrides any value provided in the `environment` argument.
/// The `isBot` argument controls the value of the `BOT` variable. It will either
/// be "true", if `isBot` is true (the default), or "false" if it is false.
///
/// The `BOT` variable is in particular used by the `flutter` tool to determine
/// how verbose to be and whether to enable analytics by default.
///
/// The working directory can be provided using the `workingDirectory` argument.
/// By default it will default to the current working directory (see [cwd]).
///
/// Information regarding the execution of the subprocess is printed to the
/// console.
///
/// The actual process executes asynchronously. A handle to the subprocess is
/// returned in the form of a [Future] that completes to a [Process] object.
271 272 273 274
Future<Process> startProcess(
  String executable,
  List<String> arguments, {
  Map<String, String> environment,
275
  bool isBot = true, // set to false to pretend not to be on a bot (e.g. to test user-facing outputs)
276 277
  String workingDirectory,
}) async {
278
  assert(isBot != null);
279
  final String command = '$executable ${arguments?.join(" ") ?? ""}';
280
  final String finalWorkingDirectory = workingDirectory ?? cwd;
281 282
  print('\nExecuting: $command in $finalWorkingDirectory'
      + (environment != null ? ' with environment $environment' : ''));
283
  environment ??= <String, String>{};
284
  environment['BOT'] = isBot ? 'true' : 'false';
285
  final Process process = await _processManager.start(
286
    <String>[executable, ...arguments],
287
    environment: environment,
288
    workingDirectory: finalWorkingDirectory,
289
  );
290
  final ProcessInfo processInfo = ProcessInfo(command, process);
291 292
  _runningProcesses.add(processInfo);

293
  process.exitCode.then<void>((int exitCode) {
294
    print('"$executable" exit code: $exitCode');
295
    _runningProcesses.remove(processInfo);
296 297
  });

298
  return process;
299 300
}

301
Future<void> forceQuitRunningProcesses() async {
302 303 304 305
  if (_runningProcesses.isEmpty)
    return;

  // Give normally quitting processes a chance to report their exit code.
306
  await Future<void>.delayed(const Duration(seconds: 1));
307 308

  // Whatever's left, kill it.
309
  for (final ProcessInfo p in _runningProcesses) {
310
    print('Force-quitting process:\n$p');
311 312 313 314 315 316 317 318
    if (!p.process.kill()) {
      print('Failed to force quit process');
    }
  }
  _runningProcesses.clear();
}

/// Executes a command and returns its exit code.
319 320 321 322
Future<int> exec(
  String executable,
  List<String> arguments, {
  Map<String, String> environment,
323
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
324
  String workingDirectory,
325
}) async {
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
  return _execute(
    executable,
    arguments,
    environment: environment,
    canFail : canFail,
    workingDirectory: workingDirectory,
  );
}

Future<int> _execute(
  String executable,
  List<String> arguments, {
  Map<String, String> environment,
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
  String workingDirectory,
  StringBuffer output, // if not null, the stdout will be written here
  StringBuffer stderr, // if not null, the stderr will be written here
  bool printStdout = true,
  bool printStderr = true,
}) async {
  final Process process = await startProcess(
    executable,
    arguments,
    environment: environment,
    workingDirectory: workingDirectory,
  );
  await forwardStandardStreams(
    process,
    output: output,
    stderr: stderr,
    printStdout: printStdout,
    printStderr: printStderr,
    );
359 360 361 362 363 364 365
  final int exitCode = await process.exitCode;

  if (exitCode != 0 && !canFail)
    fail('Executable "$executable" failed with exit code $exitCode.');

  return exitCode;
}
366

367
/// Forwards standard out and standard error from [process] to this process'
368 369
/// respective outputs. Also writes stdout to [output] and stderr to [stderr]
/// if they are not null.
370 371
///
/// Returns a future that completes when both out and error streams a closed.
372 373 374 375 376 377 378
Future<void> forwardStandardStreams(
  Process process, {
  StringBuffer output,
  StringBuffer stderr,
  bool printStdout = true,
  bool printStderr = true,
  }) {
379 380
  final Completer<void> stdoutDone = Completer<void>();
  final Completer<void> stderrDone = Completer<void>();
381
  process.stdout
382 383
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
384
      .listen((String line) {
385 386 387 388
        if (printStdout) {
          print('stdout: $line');
        }
        output?.writeln(line);
389
      }, onDone: () { stdoutDone.complete(); });
390
  process.stderr
391 392
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
393
      .listen((String line) {
394 395 396 397
        if (printStderr) {
          print('stderr: $line');
        }
        stderr?.writeln(line);
398
      }, onDone: () { stderrDone.complete(); });
399

400 401 402 403
  return Future.wait<void>(<Future<void>>[
    stdoutDone.future,
    stderrDone.future,
  ]);
404 405 406 407
}

/// Executes a command and returns its standard output as a String.
///
408
/// For logging purposes, the command's output is also printed out by default.
409 410 411 412
Future<String> eval(
  String executable,
  List<String> arguments, {
  Map<String, String> environment,
413
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
414
  String workingDirectory,
415
  StringBuffer stderr, // if not null, the stderr will be written here
416 417
  bool printStdout = true,
  bool printStderr = true,
418
}) async {
419
  final StringBuffer output = StringBuffer();
420 421 422 423 424 425 426 427 428 429 430
  await _execute(
    executable,
    arguments,
    environment: environment,
    canFail: canFail,
    workingDirectory: workingDirectory,
    output: output,
    stderr: stderr,
    printStdout: printStdout,
    printStderr: printStderr,
  );
431
  return output.toString().trimRight();
432 433
}

434 435
List<String> flutterCommandArgs(String command, List<String> options) {
  return <String>[
436 437 438 439 440
    command,
    if (localEngine != null) ...<String>['--local-engine', localEngine],
    if (localEngineSrcPath != null) ...<String>['--local-engine-src-path', localEngineSrcPath],
    ...options,
  ];
441 442 443 444 445 446 447 448
}

Future<int> flutter(String command, {
  List<String> options = const <String>[],
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
  Map<String, String> environment,
}) {
  final List<String> args = flutterCommandArgs(command, options);
449
  return exec(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
450
      canFail: canFail, environment: environment);
451 452
}

453
/// Runs a `flutter` command and returns the standard output as a string.
454
Future<String> evalFlutter(String command, {
455
  List<String> options = const <String>[],
456
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
457
  Map<String, String> environment,
458
  StringBuffer stderr, // if not null, the stderr will be written here.
459
}) {
460
  final List<String> args = flutterCommandArgs(command, options);
461
  return eval(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
462
      canFail: canFail, environment: environment, stderr: stderr);
463 464
}

465 466 467
String get dartBin =>
    path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'dart');

468 469 470
String get pubBin =>
    path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'pub');

471
Future<int> dart(List<String> args) => exec(dartBin, <String>['--disable-dart-dev', ...args]);
472

473 474 475 476 477 478 479 480 481 482 483 484 485 486
/// Returns a future that completes with a path suitable for JAVA_HOME
/// or with null, if Java cannot be found.
Future<String> findJavaHome() async {
  final Iterable<String> hits = grep(
    'Java binary at: ',
    from: await evalFlutter('doctor', options: <String>['-v']),
  );
  if (hits.isEmpty)
    return null;
  final String javaBinary = hits.first.split(': ').last;
  // javaBinary == /some/path/to/java/home/bin/java
  return path.dirname(path.dirname(javaBinary));
}

487
Future<T> inDirectory<T>(dynamic directory, Future<T> action()) async {
488
  final String previousCwd = cwd;
489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505
  try {
    cd(directory);
    return await action();
  } finally {
    cd(previousCwd);
  }
}

void cd(dynamic directory) {
  Directory d;
  if (directory is String) {
    cwd = directory;
    d = dir(directory);
  } else if (directory is Directory) {
    cwd = directory.path;
    d = directory;
  } else {
506
    throw FileSystemException('Unsupported directory type ${directory.runtimeType}', directory.toString());
507 508 509
  }

  if (!d.existsSync())
510
    throw FileSystemException('Cannot cd into directory that does not exist', d.toString());
511 512
}

513
Directory get flutterDirectory => Directory.current.parent.parent;
514 515

String requireEnvVar(String name) {
516
  final String value = Platform.environment[name];
517

518 519
  if (value == null)
    fail('$name environment variable is missing. Quitting.');
520 521 522 523

  return value;
}

524
T requireConfigProperty<T>(Map<String, dynamic> map, String propertyName) {
525 526
  if (!map.containsKey(propertyName))
    fail('Configuration property not found: $propertyName');
527
  final T result = map[propertyName] as T;
528
  return result;
529 530 531
}

String jsonEncode(dynamic data) {
532
  return const JsonEncoder.withIndent('  ').convert(data) + '\n';
533 534
}

535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550
Future<void> getNewGallery(String revision, Directory galleryDir) async {
  section('Get New Flutter Gallery!');

  if (exists(galleryDir)) {
    galleryDir.deleteSync(recursive: true);
  }

  await inDirectory<void>(galleryDir.parent, () async {
    await exec('git', <String>['clone', 'https://github.com/flutter/gallery.git']);
  });

  await inDirectory<void>(galleryDir, () async {
    await exec('git', <String>['checkout', revision]);
  });
}

551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
void checkNotNull(Object o1,
    [Object o2 = 1,
    Object o3 = 1,
    Object o4 = 1,
    Object o5 = 1,
    Object o6 = 1,
    Object o7 = 1,
    Object o8 = 1,
    Object o9 = 1,
    Object o10 = 1]) {
  if (o1 == null)
    throw 'o1 is null';
  if (o2 == null)
    throw 'o2 is null';
  if (o3 == null)
    throw 'o3 is null';
  if (o4 == null)
    throw 'o4 is null';
  if (o5 == null)
    throw 'o5 is null';
  if (o6 == null)
    throw 'o6 is null';
  if (o7 == null)
    throw 'o7 is null';
  if (o8 == null)
    throw 'o8 is null';
  if (o9 == null)
    throw 'o9 is null';
  if (o10 == null)
    throw 'o10 is null';
}

/// Splits [from] into lines and selects those that contain [pattern].
Iterable<String> grep(Pattern pattern, {@required String from}) {
  return from.split('\n').where((String line) {
    return line.contains(pattern);
  });
}

/// Captures asynchronous stack traces thrown by [callback].
///
/// This is a convenience wrapper around [Chain] optimized for use with
/// `async`/`await`.
///
/// Example:
///
///     try {
///       await captureAsyncStacks(() { /* async things */ });
///     } catch (error, chain) {
///
///     }
602 603
Future<void> runAndCaptureAsyncStacks(Future<void> callback()) {
  final Completer<void> completer = Completer<void>();
604 605 606
  Chain.capture(() async {
    await callback();
    completer.complete();
607
  }, onError: completer.completeError);
608 609
  return completer.future;
}
610

611
bool canRun(String path) => _processManager.canRun(path);
612

613 614
final RegExp _obsRegExp =
  RegExp('An Observatory debugger .* is available at: ');
615
final RegExp _obsPortRegExp = RegExp(r'(\S+:(\d+)/\S*)$');
616
final RegExp _obsUriRegExp = RegExp(r'((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)');
617

618 619 620
/// Tries to extract a port from the string.
///
/// The `prefix`, if specified, is a regular expression pattern and must not contain groups.
621
/// `prefix` defaults to the RegExp: `An Observatory debugger .* is available at: `.
622
int parseServicePort(String line, {
623
  Pattern prefix,
624
}) {
625
  prefix ??= _obsRegExp;
626 627
  final Iterable<Match> matchesIter = prefix.allMatches(line);
  if (matchesIter.isEmpty) {
628 629
    return null;
  }
630
  final Match prefixMatch = matchesIter.first;
631 632 633 634 635
  final List<Match> matches =
    _obsPortRegExp.allMatches(line, prefixMatch.end).toList();
  return matches.isEmpty ? null : int.parse(matches[0].group(2));
}

636
/// Tries to extract a URL from the string.
637 638 639 640 641 642 643
///
/// The `prefix`, if specified, is a regular expression pattern and must not contain groups.
/// `prefix` defaults to the RegExp: `An Observatory debugger .* is available at: `.
Uri parseServiceUri(String line, {
  Pattern prefix,
}) {
  prefix ??= _obsRegExp;
644 645
  final Iterable<Match> matchesIter = prefix.allMatches(line);
  if (matchesIter.isEmpty) {
646 647
    return null;
  }
648
  final Match prefixMatch = matchesIter.first;
649 650 651
  final List<Match> matches =
    _obsUriRegExp.allMatches(line, prefixMatch.end).toList();
  return matches.isEmpty ? null : Uri.parse(matches[0].group(0));
652
}
653

654 655 656
/// Checks that the file exists, otherwise throws a [FileSystemException].
void checkFileExists(String file) {
  if (!exists(File(file))) {
657
    throw FileSystemException('Expected file to exist.', file);
658 659
  }
}
660

xster's avatar
xster committed
661 662 663
/// Checks that the file does not exists, otherwise throws a [FileSystemException].
void checkFileNotExists(String file) {
  if (exists(File(file))) {
664
    throw FileSystemException('Expected file to not exist.', file);
xster's avatar
xster committed
665 666 667
  }
}

668 669 670 671 672 673 674
/// Checks that the directory exists, otherwise throws a [FileSystemException].
void checkDirectoryExists(String directory) {
  if (!exists(Directory(directory))) {
    throw FileSystemException('Expected directory to exist.', directory);
  }
}

675 676 677 678 679 680 681
/// Checks that the directory does not exist, otherwise throws a [FileSystemException].
void checkDirectoryNotExists(String directory) {
  if (exists(Directory(directory))) {
    throw FileSystemException('Expected directory to not exist.', directory);
  }
}

682 683
/// Check that `collection` contains all entries in `values`.
void checkCollectionContains<T>(Iterable<T> values, Iterable<T> collection) {
684
  for (final T value in values) {
685 686 687
    if (!collection.contains(value)) {
      throw TaskResult.failure('Expected to find `$value` in `${collection.toString()}`.');
    }
688 689 690
  }
}

691 692
/// Check that `collection` does not contain any entries in `values`
void checkCollectionDoesNotContain<T>(Iterable<T> values, Iterable<T> collection) {
693
  for (final T value in values) {
694 695 696 697
    if (collection.contains(value)) {
      throw TaskResult.failure('Did not expect to find `$value` in `$collection`.');
    }
  }
698 699
}

700 701 702 703
/// Checks that the contents of a [File] at `filePath` contains the specified
/// [Pattern]s, otherwise throws a [TaskResult].
void checkFileContains(List<Pattern> patterns, String filePath) {
  final String fileContent = File(filePath).readAsStringSync();
704
  for (final Pattern pattern in patterns) {
705 706 707 708 709 710 711
    if (!fileContent.contains(pattern)) {
      throw TaskResult.failure(
        'Expected to find `$pattern` in `$filePath` '
        'instead it found:\n$fileContent'
      );
    }
  }
712
}
713 714 715 716 717 718 719 720 721 722 723 724 725 726 727

/// Clones a git repository.
///
/// Removes the directory [path], then clones the git repository
/// specified by [repo] to the directory [path].
Future<int> gitClone({String path, String repo}) async {
  rmTree(Directory(path));

  await Directory(path).create(recursive: true);

  return await inDirectory<int>(
    path,
        () => exec('git', <String>['clone', repo]),
  );
}