do not warn about out-of-date Flutter installation too often (#9271)

* do not warn about out-of-date Flutter installation too often

* style fix
diff --git a/packages/flutter_tools/test/src/version_test.dart b/packages/flutter_tools/test/src/version_test.dart
index 47da067..fe8e351 100644
--- a/packages/flutter_tools/test/src/version_test.dart
+++ b/packages/flutter_tools/test/src/version_test.dart
@@ -5,7 +5,6 @@
 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';
@@ -25,12 +24,12 @@
 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', () {
+  group('$FlutterVersion', () {
     setUpAll(() {
       Cache.disableLocking();
+      FlutterVersion.kPauseToLetUserReadTheMessage = Duration.ZERO;
     });
 
     testFlutterVersion('prints nothing when Flutter installation looks fresh', () async {
@@ -44,12 +43,13 @@
 
       fakeData(
         localCommitDate: _outOfDateVersion,
-        versionCheckStamp: _testStamp(
+        stamp: new VersionCheckStamp(
           lastTimeVersionWasChecked: _stampOutOfDate,
           lastKnownRemoteVersion: _outOfDateVersion,
         ),
         remoteCommitDate: _outOfDateVersion,
         expectSetStamp: true,
+        expectServerPing: true,
       );
 
       await version.checkFlutterVersionFreshness();
@@ -61,28 +61,57 @@
 
       fakeData(
         localCommitDate: _outOfDateVersion,
-        versionCheckStamp: _testStamp(
+        stamp: new VersionCheckStamp(
           lastTimeVersionWasChecked: _stampUpToDate,
           lastKnownRemoteVersion: _upToDateVersion,
         ),
+        expectSetStamp: true,
       );
 
       await version.checkFlutterVersionFreshness();
       _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(_outOfDateVersion)));
     });
 
-    testFlutterVersion('pings server when version stamp is missing', () async {
+    testFlutterVersion('does not print warning if printed recently', () async {
       final FlutterVersion version = FlutterVersion.instance;
 
       fakeData(
           localCommitDate: _outOfDateVersion,
-          versionCheckStamp: _stampMissing,
-          remoteCommitDate: _upToDateVersion,
+          stamp: new VersionCheckStamp(
+              lastTimeVersionWasChecked: _stampUpToDate,
+              lastKnownRemoteVersion: _upToDateVersion,
+          ),
           expectSetStamp: true,
       );
 
       await version.checkFlutterVersionFreshness();
       _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(_outOfDateVersion)));
+      expect((await VersionCheckStamp.load()).lastTimeWarningWasPrinted, _testClock.now());
+
+      await version.checkFlutterVersionFreshness();
+      _expectVersionMessage('');
+    });
+
+    testFlutterVersion('pings server when version stamp is missing then does not', () async {
+      final FlutterVersion version = FlutterVersion.instance;
+
+      fakeData(
+          localCommitDate: _outOfDateVersion,
+          remoteCommitDate: _upToDateVersion,
+          expectSetStamp: true,
+          expectServerPing: true,
+      );
+
+      await version.checkFlutterVersionFreshness();
+      _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(_outOfDateVersion)));
+
+      // Immediate subsequent check is not expected to ping the server.
+      fakeData(
+        localCommitDate: _outOfDateVersion,
+        stamp: await VersionCheckStamp.load(),
+      );
+      await version.checkFlutterVersionFreshness();
+      _expectVersionMessage('');
     });
 
     testFlutterVersion('pings server when version stamp is out-of-date', () async {
@@ -90,12 +119,13 @@
 
       fakeData(
           localCommitDate: _outOfDateVersion,
-          versionCheckStamp: _testStamp(
+          stamp: new VersionCheckStamp(
               lastTimeVersionWasChecked: _stampOutOfDate,
               lastKnownRemoteVersion: _testClock.ago(days: 2),
           ),
           remoteCommitDate: _upToDateVersion,
           expectSetStamp: true,
+          expectServerPing: true,
       );
 
       await version.checkFlutterVersionFreshness();
@@ -107,26 +137,98 @@
 
       fakeData(
           localCommitDate: _outOfDateVersion,
-          versionCheckStamp: _stampMissing,
           errorOnFetch: true,
+          expectServerPing: true,
       );
 
       await version.checkFlutterVersionFreshness();
       _expectVersionMessage('');
     });
   });
+
+  group('$VersionCheckStamp', () {
+    void _expectDefault(VersionCheckStamp stamp) {
+      expect(stamp.lastKnownRemoteVersion, isNull);
+      expect(stamp.lastTimeVersionWasChecked, isNull);
+      expect(stamp.lastTimeWarningWasPrinted, isNull);
+    }
+
+    testFlutterVersion('loads blank when stamp file missing', () async {
+      fakeData();
+      _expectDefault(await VersionCheckStamp.load());
+    });
+
+    testFlutterVersion('loads blank when stamp file is malformed JSON', () async {
+      fakeData(stampJson: '<');
+      _expectDefault(await VersionCheckStamp.load());
+    });
+
+    testFlutterVersion('loads blank when stamp file is well-formed but invalid JSON', () async {
+      fakeData(stampJson: '[]');
+      _expectDefault(await VersionCheckStamp.load());
+    });
+
+    testFlutterVersion('loads valid JSON', () async {
+      fakeData(stampJson: '''
+      {
+        "lastKnownRemoteVersion": "${_testClock.ago(days: 1)}",
+        "lastTimeVersionWasChecked": "${_testClock.ago(days: 2)}",
+        "lastTimeWarningWasPrinted": "${_testClock.now()}"
+      }
+      ''');
+
+      final VersionCheckStamp stamp = await VersionCheckStamp.load();
+      expect(stamp.lastKnownRemoteVersion, _testClock.ago(days: 1));
+      expect(stamp.lastTimeVersionWasChecked, _testClock.ago(days: 2));
+      expect(stamp.lastTimeWarningWasPrinted, _testClock.now());
+    });
+
+    testFlutterVersion('stores version stamp', () async {
+      fakeData(expectSetStamp: true);
+
+      _expectDefault(await VersionCheckStamp.load());
+
+      final VersionCheckStamp stamp = new VersionCheckStamp(
+        lastKnownRemoteVersion: _testClock.ago(days: 1),
+        lastTimeVersionWasChecked: _testClock.ago(days: 2),
+        lastTimeWarningWasPrinted: _testClock.now(),
+      );
+      await stamp.store();
+
+      final VersionCheckStamp storedStamp = await VersionCheckStamp.load();
+      expect(storedStamp.lastKnownRemoteVersion, _testClock.ago(days: 1));
+      expect(storedStamp.lastTimeVersionWasChecked, _testClock.ago(days: 2));
+      expect(storedStamp.lastTimeWarningWasPrinted, _testClock.now());
+    });
+
+    testFlutterVersion('overwrites individual fields', () async {
+      fakeData(expectSetStamp: true);
+
+      _expectDefault(await VersionCheckStamp.load());
+
+      final VersionCheckStamp stamp = new VersionCheckStamp(
+        lastKnownRemoteVersion: _testClock.ago(days: 10),
+        lastTimeVersionWasChecked: _testClock.ago(days: 9),
+        lastTimeWarningWasPrinted: _testClock.ago(days: 8),
+      );
+      await stamp.store(
+        newKnownRemoteVersion: _testClock.ago(days: 1),
+        newTimeVersionWasChecked: _testClock.ago(days: 2),
+        newTimeWarningWasPrinted: _testClock.now(),
+      );
+
+      final VersionCheckStamp storedStamp = await VersionCheckStamp.load();
+      expect(storedStamp.lastKnownRemoteVersion, _testClock.ago(days: 1));
+      expect(storedStamp.lastTimeVersionWasChecked, _testClock.ago(days: 2));
+      expect(storedStamp.lastTimeWarningWasPrinted, _testClock.now());
+    });
+  });
 }
 
 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',
-  });
+  logger.clear();
 }
 
 void testFlutterVersion(String description, dynamic testMethod()) {
@@ -135,21 +237,24 @@
     testMethod,
     overrides: <Type, Generator>{
       FlutterVersion: () => new FlutterVersion(_testClock),
-      ProcessManager: () => new MockProcessManager(),
-      Cache: () => new MockCache(),
     },
   );
 }
 
 void fakeData({
-  @required DateTime localCommitDate,
+  DateTime localCommitDate,
   DateTime remoteCommitDate,
-  String versionCheckStamp,
-  bool expectSetStamp: false,
+  VersionCheckStamp stamp,
+  String stampJson,
   bool errorOnFetch: false,
+  bool expectSetStamp: false,
+  bool expectServerPing: false,
 }) {
-  final MockProcessManager pm = context[ProcessManager];
-  final MockCache cache = context[Cache];
+  final MockProcessManager pm = new MockProcessManager();
+  context.setVariable(ProcessManager, pm);
+
+  final MockCache cache = new MockCache();
+  context.setVariable(Cache, cache);
 
   ProcessResult success(String standardOutput) {
     return new ProcessResult(1, 0, standardOutput, '');
@@ -160,27 +265,22 @@
   }
 
   when(cache.getStampFor(any)).thenAnswer((Invocation invocation) {
-    expect(invocation.positionalArguments.single, FlutterVersion.kFlutterVersionCheckStampFile);
+    expect(invocation.positionalArguments.single, VersionCheckStamp.kFlutterVersionCheckStampFile);
 
-    if (versionCheckStamp == _stampMissing) {
-      return null;
-    }
+    if (stampJson != null)
+      return stampJson;
 
-    if (versionCheckStamp != null) {
-      return versionCheckStamp;
-    }
+    if (stamp != null)
+      return JSON.encode(stamp.toJson());
 
-    throw new StateError('Unexpected call to Cache.getStampFor(${invocation.positionalArguments}, ${invocation.namedArguments})');
+    return null;
   });
 
   when(cache.setStampFor(any, any)).thenAnswer((Invocation invocation) {
-    expect(invocation.positionalArguments.first, FlutterVersion.kFlutterVersionCheckStampFile);
+    expect(invocation.positionalArguments.first, VersionCheckStamp.kFlutterVersionCheckStampFile);
 
     if (expectSetStamp) {
-      expect(invocation.positionalArguments[1], _testStamp(
-        lastKnownRemoteVersion: remoteCommitDate,
-        lastTimeVersionWasChecked: _testClock.now(),
-      ));
+      stamp = VersionCheckStamp.fromJson(JSON.decode(invocation.positionalArguments[1]));
       return null;
     }
 
@@ -205,6 +305,8 @@
     } 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')) {
+      if (!expectServerPing)
+        fail('Did not expect server ping');
       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());