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

5 6
// @dart = 2.8

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

12
import 'package:flutter_devicelab/common.dart';
13
import 'package:flutter_devicelab/framework/devices.dart';
14 15
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
16
import 'package:process/process.dart';
17 18
import 'package:stack_trace/stack_trace.dart';

19
import 'host_agent.dart';
20
import 'task_result.dart';
21

22 23 24
/// Virtual current working directory, which affect functions, such as [exec].
String cwd = Directory.current.path;

25
/// The local engine to use for [flutter] and [evalFlutter], if any.
26 27 28 29 30 31 32 33
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;
}
34 35 36

/// The local engine source path to use if a local engine is used for [flutter]
/// and [evalFlutter].
37 38 39 40 41 42 43 44
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;
}
45

46
List<ProcessInfo> _runningProcesses = <ProcessInfo>[];
47
ProcessManager _processManager = const LocalProcessManager();
48 49 50 51

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

52
  final DateTime startTime = DateTime.now();
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
  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() {
80
    final StringBuffer buf = StringBuffer(succeeded ? 'succeeded' : 'failed');
81 82 83
    if (details != null && details.trim().isNotEmpty) {
      buf.writeln();
      // Indent details by 4 spaces
84
      for (final String line in details.trim().split('\n')) {
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
        buf.writeln('    $line');
      }
    }
    return '$buf';
  }
}

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

  final String message;

  @override
  String toString() => message;
}

void fail(String message) {
102
  throw BuildFailedError(message);
103 104
}

105 106 107 108 109 110 111 112 113 114 115 116
// 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');
    }
  }
117 118 119 120
}

/// Remove recursively.
void rmTree(FileSystemEntity entity) {
121
  rm(entity, recursive: true);
122 123 124 125
}

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

126
Directory dir(String path) => Directory(path);
127

128
File file(String path) => File(path);
129 130

void copy(File sourceFile, Directory targetDirectory, {String name}) {
131
  final File target = file(
132 133 134 135
      path.join(targetDirectory.path, name ?? path.basename(sourceFile.path)));
  target.writeAsBytesSync(sourceFile.readAsBytesSync());
}

136 137 138 139
void recursiveCopy(Directory source, Directory target) {
  if (!target.existsSync())
    target.createSync();

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

156 157 158 159 160 161
FileSystemEntity move(FileSystemEntity whatToMove,
    {Directory to, String name}) {
  return whatToMove
      .renameSync(path.join(to.path, name ?? path.basename(whatToMove.path)));
}

162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
/// 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,
    );
  }
}

183 184 185 186 187 188 189 190 191 192 193 194 195
/// 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) {
196 197 198 199 200 201 202 203 204 205 206 207
  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 += '═';
  }
208
  print('\n\n$output\n');
209 210 211 212
}

Future<String> getDartVersion() async {
  // The Dart VM returns the version text to stderr.
213
  final ProcessResult result = _processManager.runSync(<String>[dartBin, '--version']);
214
  String version = (result.stderr as String).trim();
215 216 217 218 219

  // 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
220
  if (version.contains('('))
221
    version = version.substring(0, version.indexOf('(')).trim();
222
  if (version.contains(':'))
223 224 225 226 227 228 229
    version = version.substring(version.indexOf(':') + 1).trim();

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

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

233
  return inDirectory<String>(flutterDirectory, () {
234 235 236 237 238 239
    return eval('git', <String>['rev-parse', 'HEAD']);
  });
}

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

252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
/// 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.
276 277 278 279
Future<Process> startProcess(
  String executable,
  List<String> arguments, {
  Map<String, String> environment,
280
  bool isBot = true, // set to false to pretend not to be on a bot (e.g. to test user-facing outputs)
281 282
  String workingDirectory,
}) async {
283
  assert(isBot != null);
284
  final String command = '$executable ${arguments?.join(" ") ?? ""}';
285
  final String finalWorkingDirectory = workingDirectory ?? cwd;
286 287
  final Map<String, String> newEnvironment = Map<String, String>.from(environment ?? <String, String>{});
  newEnvironment['BOT'] = isBot ? 'true' : 'false';
288 289
  newEnvironment['LANG'] = 'en_US.UTF-8';
  print('\nExecuting: $command in $finalWorkingDirectory with environment $newEnvironment');
290
  final Process process = await _processManager.start(
291
    <String>[executable, ...arguments],
292
    environment: newEnvironment,
293
    workingDirectory: finalWorkingDirectory,
294
  );
295
  final ProcessInfo processInfo = ProcessInfo(command, process);
296 297
  _runningProcesses.add(processInfo);

298
  unawaited(process.exitCode.then<void>((int exitCode) {
299
    print('"$executable" exit code: $exitCode');
300
    _runningProcesses.remove(processInfo);
301
  }));
302

303
  return process;
304 305
}

306
Future<void> forceQuitRunningProcesses() async {
307 308 309 310
  if (_runningProcesses.isEmpty)
    return;

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

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

/// Executes a command and returns its exit code.
324 325 326 327
Future<int> exec(
  String executable,
  List<String> arguments, {
  Map<String, String> environment,
328
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
329
  String workingDirectory,
330
}) async {
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 359 360 361 362 363
  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,
    );
364 365 366 367 368 369 370
  final int exitCode = await process.exitCode;

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

  return exitCode;
}
371

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

405 406 407 408
  return Future.wait<void>(<Future<void>>[
    stdoutDone.future,
    stderrDone.future,
  ]);
409 410 411 412
}

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

439
List<String> flutterCommandArgs(String command, List<String> options) {
440
  // Commands support the --device-timeout flag.
441
  final Set<String> supportedDeviceTimeoutCommands = <String>{
442 443 444 445 446 447 448
    'attach',
    'devices',
    'drive',
    'install',
    'logs',
    'run',
    'screenshot',
449
  };
450
  return <String>[
451
    command,
452 453 454
    if (deviceOperatingSystem == DeviceOperatingSystem.ios && supportedDeviceTimeoutCommands.contains(command))
      ...<String>[
        '--device-timeout',
455
        '5',
456
      ],
457 458 459 460 461

    if (command == 'drive' && hostAgent.dumpDirectory != null) ...<String>[
      '--screenshot',
      hostAgent.dumpDirectory.path,
    ],
462 463 464 465
    if (localEngine != null) ...<String>['--local-engine', localEngine],
    if (localEngineSrcPath != null) ...<String>['--local-engine-src-path', localEngineSrcPath],
    ...options,
  ];
466 467
}

468 469
/// Runs the flutter `command`, and returns the exit code.
/// If `canFail` is `false`, the future completes with an error.
470 471 472
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.
473
  Map<String, String> environment = const <String, String>{},
474 475
}) {
  final List<String> args = flutterCommandArgs(command, options);
476
  return exec(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
477
    canFail: canFail, environment: environment);
478 479
}

480 481 482 483 484 485 486 487 488 489 490 491
Future<Process> startFlutter(String command, {
  List<String> options = const <String>[],
  Map<String, String> environment = const <String, String>{},
}) {
  final List<String> args = flutterCommandArgs(command, options);
  return startProcess(
    path.join(flutterDirectory.path, 'bin', 'flutter'),
    args,
    environment: environment,
  );
}

492
/// Runs a `flutter` command and returns the standard output as a string.
493
Future<String> evalFlutter(String command, {
494
  List<String> options = const <String>[],
495
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
496
  Map<String, String> environment,
497
  StringBuffer stderr, // if not null, the stderr will be written here.
498
}) {
499
  final List<String> args = flutterCommandArgs(command, options);
500
  return eval(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
501
      canFail: canFail, environment: environment, stderr: stderr);
502 503
}

504 505 506 507 508 509 510 511 512 513
Future<ProcessResult> executeFlutter(String command, {
  List<String> options = const <String>[],
}) async {
  final List<String> args = flutterCommandArgs(command, options);
  return _processManager.run(
    <String>[path.join(flutterDirectory.path, 'bin', 'flutter'), ...args],
    workingDirectory: cwd,
  );
}

514 515 516
String get dartBin =>
    path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'dart');

517 518 519
String get pubBin =>
    path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'pub');

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

522 523 524
/// Returns a future that completes with a path suitable for JAVA_HOME
/// or with null, if Java cannot be found.
Future<String> findJavaHome() async {
525 526 527 528 529 530 531 532 533 534 535 536 537 538
  if (_javaHome == null) {
    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
    _javaHome = path.dirname(path.dirname(javaBinary));
  }
  return _javaHome;
539
}
540
String _javaHome;
541

542
Future<T> inDirectory<T>(dynamic directory, Future<T> Function() action) async {
543
  final String previousCwd = cwd;
544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560
  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 {
561
    throw FileSystemException('Unsupported directory type ${directory.runtimeType}', directory.toString());
562 563 564
  }

  if (!d.existsSync())
565
    throw FileSystemException('Cannot cd into directory that does not exist', d.toString());
566 567
}

568
Directory get flutterDirectory => Directory.current.parent.parent;
569 570

String requireEnvVar(String name) {
571
  final String value = Platform.environment[name];
572

573 574
  if (value == null)
    fail('$name environment variable is missing. Quitting.');
575 576 577 578

  return value;
}

579
T requireConfigProperty<T>(Map<String, dynamic> map, String propertyName) {
580 581
  if (!map.containsKey(propertyName))
    fail('Configuration property not found: $propertyName');
582
  final T result = map[propertyName] as T;
583
  return result;
584 585 586
}

String jsonEncode(dynamic data) {
587 588
  final String jsonValue = const JsonEncoder.withIndent('  ').convert(data);
  return '$jsonValue\n';
589 590
}

591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606
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]);
  });
}

607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657
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) {
///
///     }
658
Future<void> runAndCaptureAsyncStacks(Future<void> Function() callback) {
659
  final Completer<void> completer = Completer<void>();
660 661 662
  Chain.capture(() async {
    await callback();
    completer.complete();
663
  }, onError: completer.completeError);
664 665
  return completer.future;
}
666

667
bool canRun(String path) => _processManager.canRun(path);
668

669 670
final RegExp _obsRegExp =
  RegExp('An Observatory debugger .* is available at: ');
671
final RegExp _obsPortRegExp = RegExp(r'(\S+:(\d+)/\S*)$');
672
final RegExp _obsUriRegExp = RegExp(r'((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)');
673

674 675 676
/// Tries to extract a port from the string.
///
/// The `prefix`, if specified, is a regular expression pattern and must not contain groups.
677
/// `prefix` defaults to the RegExp: `An Observatory debugger .* is available at: `.
678
int parseServicePort(String line, {
679
  Pattern prefix,
680
}) {
681
  prefix ??= _obsRegExp;
682 683
  final Iterable<Match> matchesIter = prefix.allMatches(line);
  if (matchesIter.isEmpty) {
684 685
    return null;
  }
686
  final Match prefixMatch = matchesIter.first;
687 688 689 690 691
  final List<Match> matches =
    _obsPortRegExp.allMatches(line, prefixMatch.end).toList();
  return matches.isEmpty ? null : int.parse(matches[0].group(2));
}

692
/// Tries to extract a URL from the string.
693 694 695 696 697 698 699
///
/// 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;
700 701
  final Iterable<Match> matchesIter = prefix.allMatches(line);
  if (matchesIter.isEmpty) {
702 703
    return null;
  }
704
  final Match prefixMatch = matchesIter.first;
705 706 707
  final List<Match> matches =
    _obsUriRegExp.allMatches(line, prefixMatch.end).toList();
  return matches.isEmpty ? null : Uri.parse(matches[0].group(0));
708
}
709

710 711 712
/// Checks that the file exists, otherwise throws a [FileSystemException].
void checkFileExists(String file) {
  if (!exists(File(file))) {
713
    throw FileSystemException('Expected file to exist.', file);
714 715
  }
}
716

xster's avatar
xster committed
717 718 719
/// Checks that the file does not exists, otherwise throws a [FileSystemException].
void checkFileNotExists(String file) {
  if (exists(File(file))) {
720
    throw FileSystemException('Expected file to not exist.', file);
xster's avatar
xster committed
721 722 723
  }
}

724 725 726 727 728 729 730
/// Checks that the directory exists, otherwise throws a [FileSystemException].
void checkDirectoryExists(String directory) {
  if (!exists(Directory(directory))) {
    throw FileSystemException('Expected directory to exist.', directory);
  }
}

731 732 733 734 735 736 737
/// 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);
  }
}

738 739
/// Check that `collection` contains all entries in `values`.
void checkCollectionContains<T>(Iterable<T> values, Iterable<T> collection) {
740
  for (final T value in values) {
741 742 743
    if (!collection.contains(value)) {
      throw TaskResult.failure('Expected to find `$value` in `${collection.toString()}`.');
    }
744 745 746
  }
}

747 748
/// Check that `collection` does not contain any entries in `values`
void checkCollectionDoesNotContain<T>(Iterable<T> values, Iterable<T> collection) {
749
  for (final T value in values) {
750 751 752 753
    if (collection.contains(value)) {
      throw TaskResult.failure('Did not expect to find `$value` in `$collection`.');
    }
  }
754 755
}

756 757 758 759
/// 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();
760
  for (final Pattern pattern in patterns) {
761 762 763 764 765 766 767
    if (!fileContent.contains(pattern)) {
      throw TaskResult.failure(
        'Expected to find `$pattern` in `$filePath` '
        'instead it found:\n$fileContent'
      );
    }
  }
768
}
769 770 771 772 773 774 775 776 777 778

/// 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);

779
  return inDirectory<int>(
780 781 782 783
    path,
        () => exec('git', <String>['clone', repo]),
  );
}
784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814

/// Call [fn] retrying so long as [retryIf] return `true` for the exception
/// thrown and [maxAttempts] has not been reached.
///
/// If no [retryIf] function is given this will retry any for any [Exception]
/// thrown. To retry on an [Error], the error must be caught and _rethrown_
/// as an [Exception].
///
/// Waits a constant duration of [delayDuration] between every retry attempt.
Future<T> retry<T>(
  FutureOr<T> Function() fn, {
  FutureOr<bool> Function(Exception) retryIf,
  int maxAttempts = 5,
  Duration delayDuration = const Duration(seconds: 3),
}) async {
  int attempt = 0;
  while (true) {
    attempt++; // first invocation is the first attempt
    try {
      return await fn();
    } on Exception catch (e) {
      if (attempt >= maxAttempts ||
          (retryIf != null && !(await retryIf(e)))) {
        rethrow;
      }
    }

    // Sleep for a delay
    await Future<void>.delayed(delayDuration);
  }
}