warn about outdated Flutter installations (#9163)

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 {}