version_test.dart 16 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 18
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';

19
import 'src/context.dart';
20 21 22 23 24 25 26 27

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

void main() {
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
  MockProcessManager mockProcessManager;
  MockCache mockCache;

  setUp(() {
    mockProcessManager = new MockProcessManager();
    mockCache = new MockCache();

    when(mockProcessManager.runSync(
      <String>['git', 'rev-parse', '--abbrev-ref', '--symbolic', '@{u}'],
      workingDirectory: any,
      environment: any,
    )).thenReturn(new ProcessResult(101, 0, 'channel', ''));
    when(mockProcessManager.runSync(
      <String>['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
      workingDirectory: any,
      environment: any,
    )).thenReturn(new ProcessResult(102, 0, 'branch', ''));
    when(mockProcessManager.runSync(
      <String>['git', 'log', '-n', '1', '--pretty=format:%H'],
      workingDirectory: any,
      environment: any,
    )).thenReturn(new ProcessResult(103, 0, '1234abcd', ''));
    when(mockProcessManager.runSync(
      <String>['git', 'log', '-n', '1', '--pretty=format:%ar'],
      workingDirectory: any,
      environment: any,
    )).thenReturn(new ProcessResult(104, 0, '1 second ago', ''));
    when(mockProcessManager.runSync(
      <String>['git', 'describe', '--match', 'v*.*.*', '--first-parent', '--long', '--tags'],
      workingDirectory: any,
      environment: any,
    )).thenReturn(new ProcessResult(105, 0, 'v0.1.2-3-1234abcd', ''));
  });
61

62
  group('$FlutterVersion', () {
63 64
    setUpAll(() {
      Cache.disableLocking();
65
      FlutterVersion.kPauseToLetUserReadTheMessage = Duration.zero;
66 67
    });

68 69
    testUsingContext('prints nothing when Flutter installation looks fresh', () async {
      fakeData(mockProcessManager, mockCache, localCommitDate: _upToDateVersion);
70 71
      await FlutterVersion.instance.checkFlutterVersionFreshness();
      _expectVersionMessage('');
72 73 74 75
    }, overrides: <Type, Generator>{
      FlutterVersion: () => new FlutterVersion(_testClock),
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
76 77
    });

78
    testUsingContext('prints nothing when Flutter installation looks out-of-date by is actually up-to-date', () async {
79 80 81
      final FlutterVersion version = FlutterVersion.instance;

      fakeData(
82 83
        mockProcessManager,
        mockCache,
84
        localCommitDate: _outOfDateVersion,
85
        stamp: new VersionCheckStamp(
86 87 88 89 90
          lastTimeVersionWasChecked: _stampOutOfDate,
          lastKnownRemoteVersion: _outOfDateVersion,
        ),
        remoteCommitDate: _outOfDateVersion,
        expectSetStamp: true,
91
        expectServerPing: true,
92 93 94 95
      );

      await version.checkFlutterVersionFreshness();
      _expectVersionMessage('');
96 97 98 99
    }, overrides: <Type, Generator>{
      FlutterVersion: () => new FlutterVersion(_testClock),
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
100 101
    });

102
    testUsingContext('does not ping server when version stamp is up-to-date', () async {
103 104 105
      final FlutterVersion version = FlutterVersion.instance;

      fakeData(
106 107
        mockProcessManager,
        mockCache,
108
        localCommitDate: _outOfDateVersion,
109
        stamp: new VersionCheckStamp(
110 111 112
          lastTimeVersionWasChecked: _stampUpToDate,
          lastKnownRemoteVersion: _upToDateVersion,
        ),
113 114 115 116 117
        expectSetStamp: true,
      );

      await version.checkFlutterVersionFreshness();
      _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(_outOfDateVersion)));
118 119 120 121
    }, overrides: <Type, Generator>{
      FlutterVersion: () => new FlutterVersion(_testClock),
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
122 123
    });

124
    testUsingContext('does not print warning if printed recently', () async {
125 126 127
      final FlutterVersion version = FlutterVersion.instance;

      fakeData(
128 129 130 131 132 133 134 135
        mockProcessManager,
        mockCache,
        localCommitDate: _outOfDateVersion,
        stamp: new VersionCheckStamp(
            lastTimeVersionWasChecked: _stampUpToDate,
            lastKnownRemoteVersion: _upToDateVersion,
        ),
        expectSetStamp: true,
136 137 138 139
      );

      await version.checkFlutterVersionFreshness();
      _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(_outOfDateVersion)));
140 141 142 143
      expect((await VersionCheckStamp.load()).lastTimeWarningWasPrinted, _testClock.now());

      await version.checkFlutterVersionFreshness();
      _expectVersionMessage('');
144 145 146 147
    }, overrides: <Type, Generator>{
      FlutterVersion: () => new FlutterVersion(_testClock),
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
148 149
    });

150
    testUsingContext('pings server when version stamp is missing then does not', () async {
151 152 153
      final FlutterVersion version = FlutterVersion.instance;

      fakeData(
154 155 156 157 158 159
        mockProcessManager,
        mockCache,
        localCommitDate: _outOfDateVersion,
        remoteCommitDate: _upToDateVersion,
        expectSetStamp: true,
        expectServerPing: true,
160 161 162 163
      );

      await version.checkFlutterVersionFreshness();
      _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(_outOfDateVersion)));
164 165 166

      // Immediate subsequent check is not expected to ping the server.
      fakeData(
167 168
        mockProcessManager,
        mockCache,
169 170 171 172 173
        localCommitDate: _outOfDateVersion,
        stamp: await VersionCheckStamp.load(),
      );
      await version.checkFlutterVersionFreshness();
      _expectVersionMessage('');
174 175 176 177
    }, overrides: <Type, Generator>{
      FlutterVersion: () => new FlutterVersion(_testClock),
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
178 179
    });

180
    testUsingContext('pings server when version stamp is out-of-date', () async {
181 182 183
      final FlutterVersion version = FlutterVersion.instance;

      fakeData(
184 185 186 187 188 189 190 191 192 193
        mockProcessManager,
        mockCache,
        localCommitDate: _outOfDateVersion,
        stamp: new VersionCheckStamp(
            lastTimeVersionWasChecked: _stampOutOfDate,
            lastKnownRemoteVersion: _testClock.ago(days: 2),
        ),
        remoteCommitDate: _upToDateVersion,
        expectSetStamp: true,
        expectServerPing: true,
194 195 196 197
      );

      await version.checkFlutterVersionFreshness();
      _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(_outOfDateVersion)));
198 199 200 201
    }, overrides: <Type, Generator>{
      FlutterVersion: () => new FlutterVersion(_testClock),
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
202 203
    });

204
    testUsingContext('ignores network issues', () async {
205 206 207
      final FlutterVersion version = FlutterVersion.instance;

      fakeData(
208 209 210 211 212
        mockProcessManager,
        mockCache,
        localCommitDate: _outOfDateVersion,
        errorOnFetch: true,
        expectServerPing: true,
213 214 215 216
      );

      await version.checkFlutterVersionFreshness();
      _expectVersionMessage('');
217 218 219 220
    }, overrides: <Type, Generator>{
      FlutterVersion: () => new FlutterVersion(_testClock),
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
221
    });
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245

    testUsingContext('versions comparison', () async {
      when(mockProcessManager.runSync(
        <String>['git', 'merge-base', '--is-ancestor', 'abcdef', '123456'],
        workingDirectory: any,
      )).thenReturn(new ProcessResult(1, 0, '', ''));

      expect(
        FlutterVersion.instance.checkRevisionAncestry(
          tentativeDescendantRevision: '123456',
          tentativeAncestorRevision: 'abcdef',
        ),
        true
      );

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

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

255 256
    testUsingContext('loads blank when stamp file missing', () async {
      fakeData(mockProcessManager, mockCache);
257
      _expectDefault(await VersionCheckStamp.load());
258 259 260 261
    }, overrides: <Type, Generator>{
      FlutterVersion: () => new FlutterVersion(_testClock),
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
262 263
    });

264 265
    testUsingContext('loads blank when stamp file is malformed JSON', () async {
      fakeData(mockProcessManager, mockCache, stampJson: '<');
266
      _expectDefault(await VersionCheckStamp.load());
267 268 269 270
    }, overrides: <Type, Generator>{
      FlutterVersion: () => new FlutterVersion(_testClock),
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
271 272
    });

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

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

301 302
    testUsingContext('stores version stamp', () async {
      fakeData(mockProcessManager, mockCache, expectSetStamp: true);
303 304 305 306 307 308 309 310 311 312 313 314 315 316

      _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());
317 318 319 320
    }, overrides: <Type, Generator>{
      FlutterVersion: () => new FlutterVersion(_testClock),
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
321 322
    });

323 324
    testUsingContext('overwrites individual fields', () async {
      fakeData(mockProcessManager, mockCache, expectSetStamp: true);
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342

      _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());
343 344 345 346
    }, overrides: <Type, Generator>{
      FlutterVersion: () => new FlutterVersion(_testClock),
      ProcessManager: () => mockProcessManager,
      Cache: () => mockCache,
347 348
    });
  });
349 350 351 352 353
}

void _expectVersionMessage(String message) {
  final BufferLogger logger = context[Logger];
  expect(logger.statusText.trim(), message.trim());
354
  logger.clear();
355 356
}

357 358 359
void fakeData(
  ProcessManager pm,
  Cache cache, {
360
  DateTime localCommitDate,
361
  DateTime remoteCommitDate,
362 363
  VersionCheckStamp stamp,
  String stampJson,
364
  bool errorOnFetch: false,
365 366
  bool expectSetStamp: false,
  bool expectServerPing: false,
367 368 369 370 371 372 373 374 375 376
}) {
  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) {
377
    expect(invocation.positionalArguments.single, VersionCheckStamp.kFlutterVersionCheckStampFile);
378

379 380
    if (stampJson != null)
      return stampJson;
381

382
    if (stamp != null)
383
      return json.encode(stamp.toJson());
384

385
    return null;
386 387 388
  });

  when(cache.setStampFor(any, any)).thenAnswer((Invocation invocation) {
389
    expect(invocation.positionalArguments.first, VersionCheckStamp.kFlutterVersionCheckStampFile);
390 391

    if (expectSetStamp) {
392
      stamp = VersionCheckStamp.fromJson(json.decode(invocation.positionalArguments[1]));
393 394 395 396 397 398
      return null;
    }

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

399
  final Answering<ProcessResult> syncAnswer = (Invocation invocation) {
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
    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')) {
417 418
      if (!expectServerPing)
        fail('Did not expect server ping');
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434
      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 {}