version_test.dart 17.1 KB
Newer Older
1 2 3 4 5 6
// 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';

7
import 'package:collection/collection.dart' show ListEquality;
8 9 10 11 12 13 14 15 16 17
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import 'package:quiver/time.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';

18
import 'src/common.dart';
19
import 'src/context.dart';
20

21
final Clock _testClock = Clock.fixed(DateTime(2015, 1, 1));
22 23 24 25 26 27
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);

void main() {
28 29 30 31
  MockProcessManager mockProcessManager;
  MockCache mockCache;

  setUp(() {
32 33
    mockProcessManager = MockProcessManager();
    mockCache = MockCache();
34
  });
35

36
  group('$FlutterVersion', () {
37 38
    setUpAll(() {
      Cache.disableLocking();
39
      FlutterVersion.timeToPauseToLetUserReadTheMessage = Duration.zero;
40 41
    });

42
    testUsingContext('prints nothing when Flutter installation looks fresh', () async {
43 44 45 46 47 48 49 50
      fakeData(
        mockProcessManager,
        mockCache,
        localCommitDate: _upToDateVersion,
        // Server will be pinged because we haven't pinged within last x days
        expectServerPing: true,
        remoteCommitDate: _outOfDateVersion,
        expectSetStamp: true);
51 52
      await FlutterVersion.instance.checkFlutterVersionFreshness();
      _expectVersionMessage('');
53
    }, overrides: <Type, Generator>{
54
      FlutterVersion: () => FlutterVersion(_testClock),
55 56
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
57 58
    });

59
    testUsingContext('prints nothing when Flutter installation looks out-of-date by is actually up-to-date', () async {
60
      fakeData(
61 62
        mockProcessManager,
        mockCache,
63
        localCommitDate: _outOfDateVersion,
64
        stamp: VersionCheckStamp(
65 66 67 68 69
          lastTimeVersionWasChecked: _stampOutOfDate,
          lastKnownRemoteVersion: _outOfDateVersion,
        ),
        remoteCommitDate: _outOfDateVersion,
        expectSetStamp: true,
70
        expectServerPing: true,
71
      );
72
      final FlutterVersion version = FlutterVersion.instance;
73 74 75

      await version.checkFlutterVersionFreshness();
      _expectVersionMessage('');
76
    }, overrides: <Type, Generator>{
77
      FlutterVersion: () => FlutterVersion(_testClock),
78 79
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
80 81
    });

82
    testUsingContext('does not ping server when version stamp is up-to-date', () async {
83
      fakeData(
84 85
        mockProcessManager,
        mockCache,
86
        localCommitDate: _outOfDateVersion,
87
        stamp: VersionCheckStamp(
88 89 90
          lastTimeVersionWasChecked: _stampUpToDate,
          lastKnownRemoteVersion: _upToDateVersion,
        ),
91 92 93
        expectSetStamp: true,
      );

94
      final FlutterVersion version = FlutterVersion.instance;
95
      await version.checkFlutterVersionFreshness();
96
      _expectVersionMessage(FlutterVersion.newVersionAvailableMessage());
97
    }, overrides: <Type, Generator>{
98
      FlutterVersion: () => FlutterVersion(_testClock),
99 100
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
101 102
    });

103
    testUsingContext('does not print warning if printed recently', () async {
104
      fakeData(
105 106 107
        mockProcessManager,
        mockCache,
        localCommitDate: _outOfDateVersion,
108
        stamp: VersionCheckStamp(
109 110 111 112
            lastTimeVersionWasChecked: _stampUpToDate,
            lastKnownRemoteVersion: _upToDateVersion,
        ),
        expectSetStamp: true,
113 114
      );

115
      final FlutterVersion version = FlutterVersion.instance;
116
      await version.checkFlutterVersionFreshness();
117
      _expectVersionMessage(FlutterVersion.newVersionAvailableMessage());
118 119 120 121
      expect((await VersionCheckStamp.load()).lastTimeWarningWasPrinted, _testClock.now());

      await version.checkFlutterVersionFreshness();
      _expectVersionMessage('');
122
    }, overrides: <Type, Generator>{
123
      FlutterVersion: () => FlutterVersion(_testClock),
124 125
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
126 127
    });

128
    testUsingContext('pings server when version stamp is missing then does not', () async {
129
      fakeData(
130 131 132 133 134 135
        mockProcessManager,
        mockCache,
        localCommitDate: _outOfDateVersion,
        remoteCommitDate: _upToDateVersion,
        expectSetStamp: true,
        expectServerPing: true,
136
      );
137
      final FlutterVersion version = FlutterVersion.instance;
138 139

      await version.checkFlutterVersionFreshness();
140
      _expectVersionMessage(FlutterVersion.newVersionAvailableMessage());
141 142 143

      // Immediate subsequent check is not expected to ping the server.
      fakeData(
144 145
        mockProcessManager,
        mockCache,
146 147 148 149 150
        localCommitDate: _outOfDateVersion,
        stamp: await VersionCheckStamp.load(),
      );
      await version.checkFlutterVersionFreshness();
      _expectVersionMessage('');
151
    }, overrides: <Type, Generator>{
152
      FlutterVersion: () => FlutterVersion(_testClock),
153 154
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
155 156
    });

157
    testUsingContext('pings server when version stamp is out-of-date', () async {
158
      fakeData(
159 160 161
        mockProcessManager,
        mockCache,
        localCommitDate: _outOfDateVersion,
162
        stamp: VersionCheckStamp(
163 164 165 166 167 168
            lastTimeVersionWasChecked: _stampOutOfDate,
            lastKnownRemoteVersion: _testClock.ago(days: 2),
        ),
        remoteCommitDate: _upToDateVersion,
        expectSetStamp: true,
        expectServerPing: true,
169
      );
170
      final FlutterVersion version = FlutterVersion.instance;
171 172

      await version.checkFlutterVersionFreshness();
173
      _expectVersionMessage(FlutterVersion.newVersionAvailableMessage());
174
    }, overrides: <Type, Generator>{
175
      FlutterVersion: () => FlutterVersion(_testClock),
176 177
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
178 179
    });

180
    testUsingContext('does not print warning when unable to connect to server if not out of date', () async {
181
      fakeData(
182 183
        mockProcessManager,
        mockCache,
184
        localCommitDate: _upToDateVersion,
185 186
        errorOnFetch: true,
        expectServerPing: true,
187
        expectSetStamp: true,
188
      );
189
      final FlutterVersion version = FlutterVersion.instance;
190 191 192

      await version.checkFlutterVersionFreshness();
      _expectVersionMessage('');
193
    }, overrides: <Type, Generator>{
194
      FlutterVersion: () => FlutterVersion(_testClock),
195 196
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
197
    });
198

199 200 201 202 203 204 205 206 207
    testUsingContext('prints warning when unable to connect to server if really out of date', () async {
      fakeData(
        mockProcessManager,
        mockCache,
        localCommitDate: _outOfDateVersion,
        errorOnFetch: true,
        expectServerPing: true,
        expectSetStamp: true
      );
208
      final FlutterVersion version = FlutterVersion.instance;
209 210 211 212

      await version.checkFlutterVersionFreshness();
      _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(_outOfDateVersion)));
    }, overrides: <Type, Generator>{
213
      FlutterVersion: () => FlutterVersion(_testClock),
214 215 216 217
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
    });

218
    testUsingContext('versions comparison', () async {
219 220 221 222 223 224 225 226 227 228
      fakeData(
          mockProcessManager,
          mockCache,
          localCommitDate: _outOfDateVersion,
          errorOnFetch: true,
          expectServerPing: true,
          expectSetStamp: true
      );
      final FlutterVersion version = FlutterVersion.instance;

229 230
      when(mockProcessManager.runSync(
        <String>['git', 'merge-base', '--is-ancestor', 'abcdef', '123456'],
231
        workingDirectory: anyNamed('workingDirectory'),
232
      )).thenReturn(ProcessResult(1, 0, '', ''));
233 234

      expect(
235
        version.checkRevisionAncestry(
236 237 238 239 240 241 242 243
          tentativeDescendantRevision: '123456',
          tentativeAncestorRevision: 'abcdef',
        ),
        true
      );

      verify(mockProcessManager.runSync(
        <String>['git', 'merge-base', '--is-ancestor', 'abcdef', '123456'],
244
        workingDirectory: anyNamed('workingDirectory'),
245 246 247
      ));
    },
    overrides: <Type, Generator>{
248
      FlutterVersion: () => FlutterVersion(_testClock),
249 250
      ProcessManager: () => mockProcessManager,
    });
251
  });
252 253 254 255 256 257 258 259

  group('$VersionCheckStamp', () {
    void _expectDefault(VersionCheckStamp stamp) {
      expect(stamp.lastKnownRemoteVersion, isNull);
      expect(stamp.lastTimeVersionWasChecked, isNull);
      expect(stamp.lastTimeWarningWasPrinted, isNull);
    }

260 261
    testUsingContext('loads blank when stamp file missing', () async {
      fakeData(mockProcessManager, mockCache);
262
      _expectDefault(await VersionCheckStamp.load());
263
    }, overrides: <Type, Generator>{
264
      FlutterVersion: () => FlutterVersion(_testClock),
265 266
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
267 268
    });

269 270
    testUsingContext('loads blank when stamp file is malformed JSON', () async {
      fakeData(mockProcessManager, mockCache, stampJson: '<');
271
      _expectDefault(await VersionCheckStamp.load());
272
    }, overrides: <Type, Generator>{
273
      FlutterVersion: () => FlutterVersion(_testClock),
274 275
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
276 277
    });

278 279
    testUsingContext('loads blank when stamp file is well-formed but invalid JSON', () async {
      fakeData(mockProcessManager, mockCache, stampJson: '[]');
280
      _expectDefault(await VersionCheckStamp.load());
281
    }, overrides: <Type, Generator>{
282
      FlutterVersion: () => FlutterVersion(_testClock),
283 284
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
285 286
    });

287 288
    testUsingContext('loads valid JSON', () async {
      fakeData(mockProcessManager, mockCache, stampJson: '''
289 290 291 292 293 294 295 296 297 298 299
      {
        "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());
300
    }, overrides: <Type, Generator>{
301
      FlutterVersion: () => FlutterVersion(_testClock),
302 303
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
304 305
    });

306 307
    testUsingContext('stores version stamp', () async {
      fakeData(mockProcessManager, mockCache, expectSetStamp: true);
308 309 310

      _expectDefault(await VersionCheckStamp.load());

311
      final VersionCheckStamp stamp = VersionCheckStamp(
312 313 314 315 316 317 318 319 320 321
        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());
322
    }, overrides: <Type, Generator>{
323
      FlutterVersion: () => FlutterVersion(_testClock),
324 325
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
326 327
    });

328 329
    testUsingContext('overwrites individual fields', () async {
      fakeData(mockProcessManager, mockCache, expectSetStamp: true);
330 331 332

      _expectDefault(await VersionCheckStamp.load());

333
      final VersionCheckStamp stamp = VersionCheckStamp(
334 335 336 337 338 339 340 341 342 343 344 345 346 347
        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());
348
    }, overrides: <Type, Generator>{
349
      FlutterVersion: () => FlutterVersion(_testClock),
350 351
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
352 353
    });
  });
354 355 356 357 358
}

void _expectVersionMessage(String message) {
  final BufferLogger logger = context[Logger];
  expect(logger.statusText.trim(), message.trim());
359
  logger.clear();
360 361
}

362 363 364
void fakeData(
  ProcessManager pm,
  Cache cache, {
365
  DateTime localCommitDate,
366
  DateTime remoteCommitDate,
367 368
  VersionCheckStamp stamp,
  String stampJson,
369 370 371
  bool errorOnFetch = false,
  bool expectSetStamp = false,
  bool expectServerPing = false,
372 373
}) {
  ProcessResult success(String standardOutput) {
374
    return ProcessResult(1, 0, standardOutput, '');
375 376 377
  }

  ProcessResult failure(int exitCode) {
378
    return ProcessResult(1, exitCode, '', 'error');
379 380 381
  }

  when(cache.getStampFor(any)).thenAnswer((Invocation invocation) {
382
    expect(invocation.positionalArguments.single, VersionCheckStamp.kFlutterVersionCheckStampFile);
383

384 385
    if (stampJson != null)
      return stampJson;
386

387
    if (stamp != null)
388
      return json.encode(stamp.toJson());
389

390
    return null;
391 392 393
  });

  when(cache.setStampFor(any, any)).thenAnswer((Invocation invocation) {
394
    expect(invocation.positionalArguments.first, VersionCheckStamp.kFlutterVersionCheckStampFile);
395 396

    if (expectSetStamp) {
397
      stamp = VersionCheckStamp.fromJson(json.decode(invocation.positionalArguments[1]));
398 399 400
      return null;
    }

401
    throw StateError('Unexpected call to Cache.setStampFor(${invocation.positionalArguments}, ${invocation.namedArguments})');
402 403
  });

404
  final Answering<ProcessResult> syncAnswer = (Invocation invocation) {
405
    bool argsAre(String a1, [String a2, String a3, String a4, String a5, String a6, String a7, String a8]) {
406
      const ListEquality<String> equality = ListEquality<String>();
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
      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')) {
422 423
      if (!expectServerPing)
        fail('Did not expect server ping');
424 425 426 427 428
      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());
    }

429
    throw StateError('Unexpected call to ProcessManager.run(${invocation.positionalArguments}, ${invocation.namedArguments})');
430 431
  };

432 433
  when(pm.runSync(any, workingDirectory: anyNamed('workingDirectory'))).thenAnswer(syncAnswer);
  when(pm.run(any, workingDirectory: anyNamed('workingDirectory'))).thenAnswer((Invocation invocation) async {
434 435
    return syncAnswer(invocation);
  });
436 437 438 439 440

  when(pm.runSync(
    <String>['git', 'rev-parse', '--abbrev-ref', '--symbolic', '@{u}'],
    workingDirectory: anyNamed('workingDirectory'),
    environment: anyNamed('environment'),
441
  )).thenReturn(ProcessResult(101, 0, 'master', ''));
442 443 444 445
  when(pm.runSync(
    <String>['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
    workingDirectory: anyNamed('workingDirectory'),
    environment: anyNamed('environment'),
446
  )).thenReturn(ProcessResult(102, 0, 'branch', ''));
447 448 449 450
  when(pm.runSync(
    <String>['git', 'log', '-n', '1', '--pretty=format:%H'],
    workingDirectory: anyNamed('workingDirectory'),
    environment: anyNamed('environment'),
451
  )).thenReturn(ProcessResult(103, 0, '1234abcd', ''));
452 453 454 455
  when(pm.runSync(
    <String>['git', 'log', '-n', '1', '--pretty=format:%ar'],
    workingDirectory: anyNamed('workingDirectory'),
    environment: anyNamed('environment'),
456
  )).thenReturn(ProcessResult(104, 0, '1 second ago', ''));
457 458 459 460
  when(pm.runSync(
    <String>['git', 'describe', '--match', 'v*.*.*', '--first-parent', '--long', '--tags'],
    workingDirectory: anyNamed('workingDirectory'),
    environment: anyNamed('environment'),
461
  )).thenReturn(ProcessResult(105, 0, 'v0.1.2-3-1234abcd', ''));
462 463 464 465
}

class MockProcessManager extends Mock implements ProcessManager {}
class MockCache extends Mock implements Cache {}