warn about outdated Flutter installations (#9163)

diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart
index de8f3e9..82846c6 100644
--- a/packages/flutter_tools/lib/src/cache.dart
+++ b/packages/flutter_tools/lib/src/cache.dart
@@ -74,6 +74,16 @@
     _lock = null;
   }
 
+  /// Checks if the current process owns the lock for the cache directory at
+  /// this very moment; throws a [StateError] if it doesn't.
+  static void checkLockAcquired() {
+    if (_lockEnabled && _lock == null) {
+      throw new StateError(
+        'The current process does not own the lock for the cache directory. This is a bug in Flutter CLI tools.',
+      );
+    }
+  }
+
   static String _dartSdkVersion;
 
   static String get dartSdkVersion {
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
index 9218b0b..d4d25f7 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
@@ -236,6 +236,7 @@
       flutterUsage.suppressAnalytics = true;
 
     _checkFlutterCopy();
+    await FlutterVersion.instance.checkFlutterVersionFreshness();
 
     if (globalResults.wasParsed('packages'))
       PackageMap.globalPackagesPath = fs.path.normalize(fs.path.absolute(globalResults['packages']));
diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart
index c15db62..f61327e 100644
--- a/packages/flutter_tools/lib/src/version.dart
+++ b/packages/flutter_tools/lib/src/version.dart
@@ -2,11 +2,19 @@
 // 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 'package:meta/meta.dart';
+import 'package:quiver/time.dart';
+
+import 'base/common.dart';
 import 'base/context.dart';
 import 'base/io.dart';
 import 'base/process.dart';
 import 'base/process_manager.dart';
 import 'cache.dart';
+import 'globals.dart';
 
 final Set<String> kKnownBranchNames = new Set<String>.from(<String>[
   'master',
@@ -17,7 +25,8 @@
 ]);
 
 class FlutterVersion {
-  FlutterVersion._() {
+  @visibleForTesting
+  FlutterVersion(this._clock) {
     _channel = _runGit('git rev-parse --abbrev-ref --symbolic @{u}');
 
     final int slash = _channel.indexOf('/');
@@ -33,6 +42,8 @@
     _frameworkAge = _runGit('git log -n 1 --pretty=format:%ar');
   }
 
+  final Clock _clock;
+
   String _repositoryUrl;
   String get repositoryUrl => _repositoryUrl;
 
@@ -72,20 +83,64 @@
   }
 
   /// A date String describing the last framework commit.
-  static String get frameworkCommitDate {
-    return _runSync(<String>['git', 'log', '-n', '1', '--pretty=format:%ad', '--date=format:%Y-%m-%d %H:%M:%S'], Cache.flutterRoot);
+  String get frameworkCommitDate => _latestGitCommitDate();
+
+  static String _latestGitCommitDate([String branch]) {
+    final List<String> args = <String>['git', 'log'];
+
+    if (branch != null)
+      args.add(branch);
+
+    args.addAll(<String>['-n', '1', '--pretty=format:%ad', '--date=iso']);
+    return _runSync(args, lenient: false);
   }
 
-  static FlutterVersion get instance => context.putIfAbsent(FlutterVersion, () => new FlutterVersion._());
+  /// The name of the temporary git remote used to check for the latest
+  /// available Flutter framework version.
+  ///
+  /// In the absence of bugs and crashes a Flutter developer should never see
+  /// this remote appear in their `git remote` list, but also if it happens to
+  /// persist we do the proper clean-up for extra robustness.
+  static const String _kVersionCheckRemote = '__flutter_version_check__';
+
+  /// The date of the latest framework commit in the remote repository.
+  ///
+  /// Throws [ToolExit] if a git command fails, for example, when the remote git
+  /// repository is not reachable due to a network issue.
+  static Future<String> fetchRemoteFrameworkCommitDate() async {
+    await _removeVersionCheckRemoteIfExists();
+    try {
+      await _run(<String>[
+        'git',
+        'remote',
+        'add',
+        _kVersionCheckRemote,
+        'https://github.com/flutter/flutter.git',
+      ]);
+      await _run(<String>['git', 'fetch', _kVersionCheckRemote, 'master']);
+      return _latestGitCommitDate('$_kVersionCheckRemote/master');
+    } finally {
+      await _removeVersionCheckRemoteIfExists();
+    }
+  }
+
+  static Future<Null> _removeVersionCheckRemoteIfExists() async {
+    final List<String> remotes = (await _run(<String>['git', 'remote']))
+        .split('\n')
+        .map((String name) => name.trim())  // to account for OS-specific line-breaks
+        .toList();
+    if (remotes.contains(_kVersionCheckRemote))
+      await _run(<String>['git', 'remote', 'remove', _kVersionCheckRemote]);
+  }
+
+  static FlutterVersion get instance => context.putIfAbsent(FlutterVersion, () => new FlutterVersion(const Clock()));
 
   /// Return a short string for the version (`alpha/a76bc8e22b`).
   static String getVersionString({ bool whitelistBranchName: false }) {
-    final String cwd = Cache.flutterRoot;
-
-    String commit = _shortGitRevision(_runSync(<String>['git', 'rev-parse', 'HEAD'], cwd));
+    String commit = _shortGitRevision(_runSync(<String>['git', 'rev-parse', 'HEAD']));
     commit = commit.isEmpty ? 'unknown' : commit;
 
-    String branch = _runSync(<String>['git', 'rev-parse', '--abbrev-ref', 'HEAD'], cwd);
+    String branch = _runSync(<String>['git', 'rev-parse', '--abbrev-ref', 'HEAD']);
     branch = branch == 'HEAD' ? 'master' : branch;
 
     if (whitelistBranchName || branch.isEmpty) {
@@ -96,11 +151,148 @@
 
     return '$branch/$commit';
   }
+
+  /// The amount of time we wait before pinging the server to check for the
+  /// availability of a newer version of Flutter.
+  @visibleForTesting
+  static const Duration kCheckAgeConsideredUpToDate = const Duration(days: 7);
+
+  /// We warn the user if the age of their Flutter installation is greater than
+  /// this duration.
+  @visibleForTesting
+  static final Duration kVersionAgeConsideredUpToDate = kCheckAgeConsideredUpToDate * 4;
+
+  /// The prefix of the stamp file where we cache Flutter version check data.
+  @visibleForTesting
+  static const String kFlutterVersionCheckStampFile = 'flutter_version_check';
+
+  /// Checks if the currently installed version of Flutter is up-to-date, and
+  /// warns the user if it isn't.
+  ///
+  /// This function must run while [Cache.lock] is acquired because it reads and
+  /// writes shared cache files.
+  Future<Null> checkFlutterVersionFreshness() async {
+    final DateTime localFrameworkCommitDate = DateTime.parse(frameworkCommitDate);
+    final Duration frameworkAge = _clock.now().difference(localFrameworkCommitDate);
+    final bool installationSeemsOutdated = frameworkAge > kVersionAgeConsideredUpToDate;
+
+    Future<bool> newerFrameworkVersionAvailable() async {
+      final DateTime latestFlutterCommitDate = await _getLatestAvailableFlutterVersion();
+
+      if (latestFlutterCommitDate == null)
+        return false;
+
+      return latestFlutterCommitDate.isAfter(localFrameworkCommitDate);
+    }
+
+    if (installationSeemsOutdated && await newerFrameworkVersionAvailable())
+      printStatus(versionOutOfDateMessage(frameworkAge), emphasis: true);
+  }
+
+  @visibleForTesting
+  static String versionOutOfDateMessage(Duration frameworkAge) {
+    String warning = 'WARNING: your installation of Flutter is ${frameworkAge.inDays} days old.';
+    // Append enough spaces to match the message box width.
+    warning += ' ' * (74 - warning.length);
+
+    return '''
+  ╔════════════════════════════════════════════════════════════════════════════╗
+  ║ $warning ║
+  ║                                                                            ║
+  ║ To update to the latest version, run "flutter upgrade".                    ║
+  ╚════════════════════════════════════════════════════════════════════════════╝
+''';
+  }
+
+  /// Gets the release date of the latest available Flutter version.
+  ///
+  /// This method sends a server request if it's been more than
+  /// [kCheckAgeConsideredUpToDate] since the last version check.
+  ///
+  /// Returns `null` if the cached version is out-of-date or missing, and we are
+  /// unable to reach the server to get the latest version.
+  Future<DateTime> _getLatestAvailableFlutterVersion() async {
+    Cache.checkLockAcquired();
+    const JsonEncoder kPrettyJsonEncoder = const JsonEncoder.withIndent('  ');
+    final String versionCheckStamp = Cache.instance.getStampFor(kFlutterVersionCheckStampFile);
+
+    if (versionCheckStamp != null) {
+      final Map<String, String> data = JSON.decode(versionCheckStamp);
+      final DateTime lastTimeVersionWasChecked = DateTime.parse(data['lastTimeVersionWasChecked']);
+      final Duration timeSinceLastCheck = _clock.now().difference(lastTimeVersionWasChecked);
+
+      // Don't ping the server too often. Return cached value if it's fresh.
+      if (timeSinceLastCheck < kCheckAgeConsideredUpToDate)
+        return DateTime.parse(data['lastKnownRemoteVersion']);
+    }
+
+    // Cache is empty or it's been a while since the last server ping. Ping the server.
+    try {
+      final DateTime remoteFrameworkCommitDate = DateTime.parse(await FlutterVersion.fetchRemoteFrameworkCommitDate());
+      Cache.instance.setStampFor(kFlutterVersionCheckStampFile, kPrettyJsonEncoder.convert(<String, String>{
+        'lastTimeVersionWasChecked': '${_clock.now()}',
+        'lastKnownRemoteVersion': '$remoteFrameworkCommitDate',
+      }));
+      return remoteFrameworkCommitDate;
+    } on VersionCheckError catch (error) {
+      // This happens when any of the git commands fails, which can happen when
+      // there's no Internet connectivity. Remote version check is best effort
+      // only. We do not prevent the command from running when it fails.
+      printTrace('Failed to check Flutter version in the remote repository: $error');
+      return null;
+    }
+  }
 }
 
-String _runSync(List<String> command, String cwd) {
-  final ProcessResult results = processManager.runSync(command, workingDirectory: cwd);
-  return results.exitCode == 0 ? results.stdout.trim() : '';
+/// Thrown when we fail to check Flutter version.
+///
+/// This can happen when we attempt to `git fetch` but there is no network, or
+/// when the installation is not git-based (e.g. a user clones the repo but
+/// then removes .git).
+class VersionCheckError implements Exception {
+
+  VersionCheckError(this.message);
+
+  final String message;
+
+  @override
+  String toString() => '$VersionCheckError: $message';
+}
+
+/// Runs [command] and returns the standard output as a string.
+///
+/// If [lenient] is `true` and the command fails, returns an empty string.
+/// Otherwise, throws a [ToolExit] exception.
+String _runSync(List<String> command, {bool lenient: true}) {
+  final ProcessResult results = processManager.runSync(command, workingDirectory: Cache.flutterRoot);
+
+  if (results.exitCode == 0)
+    return results.stdout.trim();
+
+  if (!lenient) {
+    throw new VersionCheckError(
+      'Command exited with code ${results.exitCode}: ${command.join(' ')}\n'
+      'Standard error: ${results.stderr}'
+    );
+  }
+
+  return '';
+}
+
+/// Runs [command] in the root of the Flutter installation and returns the
+/// standard output as a string.
+///
+/// If the command fails, throws a [ToolExit] exception.
+Future<String> _run(List<String> command) async {
+  final ProcessResult results = await processManager.run(command, workingDirectory: Cache.flutterRoot);
+
+  if (results.exitCode == 0)
+    return results.stdout.trim();
+
+  throw new VersionCheckError(
+    'Command exited with code ${results.exitCode}: ${command.join(' ')}\n'
+    'Standard error: ${results.stderr}'
+  );
 }
 
 String _shortGitRevision(String revision) {
diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml
index be09aee..e50e24c 100644
--- a/packages/flutter_tools/pubspec.yaml
+++ b/packages/flutter_tools/pubspec.yaml
@@ -10,6 +10,7 @@
 dependencies:
   archive: ^1.0.20
   args: ^0.13.4
+  collection: '>=1.9.1 <2.0.0'
   coverage: ^0.8.0
   crypto: '>=1.1.1 <3.0.0'
   file: 2.3.2
@@ -23,6 +24,7 @@
   package_config: '>=0.1.5 <2.0.0'
   platform: 1.1.1
   process: 2.0.1
+  quiver: ^0.24.0
   stack_trace: ^1.4.0
   usage: ^3.0.1
   vm_service_client: '0.2.2+4'
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index c679f94..8231365 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -20,6 +20,7 @@
 import 'package:flutter_tools/src/ios/simulators.dart';
 import 'package:flutter_tools/src/run_hot.dart';
 import 'package:flutter_tools/src/usage.dart';
+import 'package:flutter_tools/src/version.dart';
 import 'package:mockito/mockito.dart';
 import 'package:process/process.dart';
 import 'package:test/test.dart';
@@ -53,7 +54,8 @@
       return mock;
     })
     ..putIfAbsent(SimControl, () => new MockSimControl())
-    ..putIfAbsent(Usage, () => new MockUsage());
+    ..putIfAbsent(Usage, () => new MockUsage())
+    ..putIfAbsent(FlutterVersion, () => new MockFlutterVersion());
 }
 
 void testUsingContext(String description, dynamic testMethod(), {
@@ -230,3 +232,5 @@
   @override
   void finish() { }
 }
+
+class MockFlutterVersion extends Mock implements FlutterVersion {}
diff --git a/packages/flutter_tools/test/src/runner/flutter_command_runner_test.dart b/packages/flutter_tools/test/src/runner/flutter_command_runner_test.dart
new file mode 100644
index 0000000..b3d9932
--- /dev/null
+++ b/packages/flutter_tools/test/src/runner/flutter_command_runner_test.dart
@@ -0,0 +1,29 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:mockito/mockito.dart';
+import 'package:test/test.dart';
+
+import 'package:flutter_tools/src/version.dart';
+
+import '../context.dart';
+import '../common.dart';
+import 'flutter_command_test.dart';
+
+void main() {
+  group('FlutterCommandRunner', () {
+    testUsingContext('checks that Flutter installation is up-to-date', () async {
+      final MockFlutterVersion version = FlutterVersion.instance;
+      bool versionChecked = false;
+      when(version.checkFlutterVersionFreshness()).thenAnswer((_) async {
+        versionChecked = true;
+      });
+
+      await createTestCommandRunner(new DummyFlutterCommand(shouldUpdateCache: false))
+          .run(<String>['dummy']);
+
+      expect(versionChecked, isTrue);
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/src/version_test.dart b/packages/flutter_tools/test/src/version_test.dart
new file mode 100644
index 0000000..47da067
--- /dev/null
+++ b/packages/flutter_tools/test/src/version_test.dart
@@ -0,0 +1,223 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:collection/collection.dart';
+import 'package:meta/meta.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+import 'package:quiver/time.dart';
+import 'package:test/test.dart';
+
+import 'package:flutter_tools/src/base/context.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/version.dart';
+
+import 'context.dart';
+
+const JsonEncoder _kPrettyJsonEncoder = const JsonEncoder.withIndent('  ');
+final Clock _testClock = new Clock.fixed(new DateTime(2015, 1, 1));
+final DateTime _upToDateVersion = _testClock.agoBy(FlutterVersion.kVersionAgeConsideredUpToDate ~/ 2);
+final DateTime _outOfDateVersion = _testClock.agoBy(FlutterVersion.kVersionAgeConsideredUpToDate * 2);
+final DateTime _stampUpToDate = _testClock.agoBy(FlutterVersion.kCheckAgeConsideredUpToDate ~/ 2);
+final DateTime _stampOutOfDate = _testClock.agoBy(FlutterVersion.kCheckAgeConsideredUpToDate * 2);
+const String _stampMissing = '____stamp_missing____';
+
+void main() {
+  group('FlutterVersion', () {
+    setUpAll(() {
+      Cache.disableLocking();
+    });
+
+    testFlutterVersion('prints nothing when Flutter installation looks fresh', () async {
+      fakeData(localCommitDate: _upToDateVersion);
+      await FlutterVersion.instance.checkFlutterVersionFreshness();
+      _expectVersionMessage('');
+    });
+
+    testFlutterVersion('prints nothing when Flutter installation looks out-of-date by is actually up-to-date', () async {
+      final FlutterVersion version = FlutterVersion.instance;
+
+      fakeData(
+        localCommitDate: _outOfDateVersion,
+        versionCheckStamp: _testStamp(
+          lastTimeVersionWasChecked: _stampOutOfDate,
+          lastKnownRemoteVersion: _outOfDateVersion,
+        ),
+        remoteCommitDate: _outOfDateVersion,
+        expectSetStamp: true,
+      );
+
+      await version.checkFlutterVersionFreshness();
+      _expectVersionMessage('');
+    });
+
+    testFlutterVersion('does not ping server when version stamp is up-to-date', () async {
+      final FlutterVersion version = FlutterVersion.instance;
+
+      fakeData(
+        localCommitDate: _outOfDateVersion,
+        versionCheckStamp: _testStamp(
+          lastTimeVersionWasChecked: _stampUpToDate,
+          lastKnownRemoteVersion: _upToDateVersion,
+        ),
+      );
+
+      await version.checkFlutterVersionFreshness();
+      _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(_outOfDateVersion)));
+    });
+
+    testFlutterVersion('pings server when version stamp is missing', () async {
+      final FlutterVersion version = FlutterVersion.instance;
+
+      fakeData(
+          localCommitDate: _outOfDateVersion,
+          versionCheckStamp: _stampMissing,
+          remoteCommitDate: _upToDateVersion,
+          expectSetStamp: true,
+      );
+
+      await version.checkFlutterVersionFreshness();
+      _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(_outOfDateVersion)));
+    });
+
+    testFlutterVersion('pings server when version stamp is out-of-date', () async {
+      final FlutterVersion version = FlutterVersion.instance;
+
+      fakeData(
+          localCommitDate: _outOfDateVersion,
+          versionCheckStamp: _testStamp(
+              lastTimeVersionWasChecked: _stampOutOfDate,
+              lastKnownRemoteVersion: _testClock.ago(days: 2),
+          ),
+          remoteCommitDate: _upToDateVersion,
+          expectSetStamp: true,
+      );
+
+      await version.checkFlutterVersionFreshness();
+      _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(_outOfDateVersion)));
+    });
+
+    testFlutterVersion('ignores network issues', () async {
+      final FlutterVersion version = FlutterVersion.instance;
+
+      fakeData(
+          localCommitDate: _outOfDateVersion,
+          versionCheckStamp: _stampMissing,
+          errorOnFetch: true,
+      );
+
+      await version.checkFlutterVersionFreshness();
+      _expectVersionMessage('');
+    });
+  });
+}
+
+void _expectVersionMessage(String message) {
+  final BufferLogger logger = context[Logger];
+  expect(logger.statusText.trim(), message.trim());
+}
+
+String _testStamp({@required DateTime lastTimeVersionWasChecked, @required DateTime lastKnownRemoteVersion}) {
+  return _kPrettyJsonEncoder.convert(<String, String>{
+    'lastTimeVersionWasChecked': '$lastTimeVersionWasChecked',
+    'lastKnownRemoteVersion': '$lastKnownRemoteVersion',
+  });
+}
+
+void testFlutterVersion(String description, dynamic testMethod()) {
+  testUsingContext(
+    description,
+    testMethod,
+    overrides: <Type, Generator>{
+      FlutterVersion: () => new FlutterVersion(_testClock),
+      ProcessManager: () => new MockProcessManager(),
+      Cache: () => new MockCache(),
+    },
+  );
+}
+
+void fakeData({
+  @required DateTime localCommitDate,
+  DateTime remoteCommitDate,
+  String versionCheckStamp,
+  bool expectSetStamp: false,
+  bool errorOnFetch: false,
+}) {
+  final MockProcessManager pm = context[ProcessManager];
+  final MockCache cache = context[Cache];
+
+  ProcessResult success(String standardOutput) {
+    return new ProcessResult(1, 0, standardOutput, '');
+  }
+
+  ProcessResult failure(int exitCode) {
+    return new ProcessResult(1, exitCode, '', 'error');
+  }
+
+  when(cache.getStampFor(any)).thenAnswer((Invocation invocation) {
+    expect(invocation.positionalArguments.single, FlutterVersion.kFlutterVersionCheckStampFile);
+
+    if (versionCheckStamp == _stampMissing) {
+      return null;
+    }
+
+    if (versionCheckStamp != null) {
+      return versionCheckStamp;
+    }
+
+    throw new StateError('Unexpected call to Cache.getStampFor(${invocation.positionalArguments}, ${invocation.namedArguments})');
+  });
+
+  when(cache.setStampFor(any, any)).thenAnswer((Invocation invocation) {
+    expect(invocation.positionalArguments.first, FlutterVersion.kFlutterVersionCheckStampFile);
+
+    if (expectSetStamp) {
+      expect(invocation.positionalArguments[1], _testStamp(
+        lastKnownRemoteVersion: remoteCommitDate,
+        lastTimeVersionWasChecked: _testClock.now(),
+      ));
+      return null;
+    }
+
+    throw new StateError('Unexpected call to Cache.setStampFor(${invocation.positionalArguments}, ${invocation.namedArguments})');
+  });
+
+  final Answering syncAnswer = (Invocation invocation) {
+    bool argsAre(String a1, [String a2, String a3, String a4, String a5, String a6, String a7, String a8]) {
+      const ListEquality<String> equality = const ListEquality<String>();
+      final List<String> args = invocation.positionalArguments.single;
+      final List<String> expectedArgs =
+      <String>[a1, a2, a3, a4, a5, a6, a7, a8]
+          .where((String arg) => arg != null)
+          .toList();
+      return equality.equals(args, expectedArgs);
+    }
+
+    if (argsAre('git', 'log', '-n', '1', '--pretty=format:%ad', '--date=iso')) {
+      return success(localCommitDate.toString());
+    } else if (argsAre('git', 'remote')) {
+      return success('');
+    } else if (argsAre('git', 'remote', 'add', '__flutter_version_check__', 'https://github.com/flutter/flutter.git')) {
+      return success('');
+    } else if (argsAre('git', 'fetch', '__flutter_version_check__', 'master')) {
+      return errorOnFetch ? failure(128) : success('');
+    } else if (remoteCommitDate != null && argsAre('git', 'log', '__flutter_version_check__/master', '-n', '1', '--pretty=format:%ad', '--date=iso')) {
+      return success(remoteCommitDate.toString());
+    }
+
+    throw new StateError('Unexpected call to ProcessManager.run(${invocation.positionalArguments}, ${invocation.namedArguments})');
+  };
+
+  when(pm.runSync(any, workingDirectory: any)).thenAnswer(syncAnswer);
+  when(pm.run(any, workingDirectory: any)).thenAnswer((Invocation invocation) async {
+    return syncAnswer(invocation);
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockCache extends Mock implements Cache {}