// 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' show ListEquality; 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 'src/context.dart'; 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() { MockProcessManager mockProcessManager; MockCache mockCache; setUp(() { mockProcessManager = new MockProcessManager(); mockCache = new MockCache(); when(mockProcessManager.runSync( <String>['git', 'rev-parse', '--abbrev-ref', '--symbolic', '@{u}'], workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'), )).thenReturn(new ProcessResult(101, 0, 'channel', '')); when(mockProcessManager.runSync( <String>['git', 'rev-parse', '--abbrev-ref', 'HEAD'], workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'), )).thenReturn(new ProcessResult(102, 0, 'branch', '')); when(mockProcessManager.runSync( <String>['git', 'log', '-n', '1', '--pretty=format:%H'], workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'), )).thenReturn(new ProcessResult(103, 0, '1234abcd', '')); when(mockProcessManager.runSync( <String>['git', 'log', '-n', '1', '--pretty=format:%ar'], workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'), )).thenReturn(new ProcessResult(104, 0, '1 second ago', '')); when(mockProcessManager.runSync( <String>['git', 'describe', '--match', 'v*.*.*', '--first-parent', '--long', '--tags'], workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'), )).thenReturn(new ProcessResult(105, 0, 'v0.1.2-3-1234abcd', '')); }); group('$FlutterVersion', () { setUpAll(() { Cache.disableLocking(); FlutterVersion.kPauseToLetUserReadTheMessage = Duration.zero; }); testUsingContext('prints nothing when Flutter installation looks fresh', () async { fakeData(mockProcessManager, mockCache, localCommitDate: _upToDateVersion); await FlutterVersion.instance.checkFlutterVersionFreshness(); _expectVersionMessage(''); }, overrides: <Type, Generator>{ FlutterVersion: () => new FlutterVersion(_testClock), ProcessManager: () => mockProcessManager, Cache: () => mockCache, }); testUsingContext('prints nothing when Flutter installation looks out-of-date by is actually up-to-date', () async { final FlutterVersion version = FlutterVersion.instance; fakeData( mockProcessManager, mockCache, localCommitDate: _outOfDateVersion, stamp: new VersionCheckStamp( lastTimeVersionWasChecked: _stampOutOfDate, lastKnownRemoteVersion: _outOfDateVersion, ), remoteCommitDate: _outOfDateVersion, expectSetStamp: true, expectServerPing: true, ); await version.checkFlutterVersionFreshness(); _expectVersionMessage(''); }, overrides: <Type, Generator>{ FlutterVersion: () => new FlutterVersion(_testClock), ProcessManager: () => mockProcessManager, Cache: () => mockCache, }); testUsingContext('does not ping server when version stamp is up-to-date', () async { final FlutterVersion version = FlutterVersion.instance; fakeData( mockProcessManager, mockCache, localCommitDate: _outOfDateVersion, stamp: new VersionCheckStamp( lastTimeVersionWasChecked: _stampUpToDate, lastKnownRemoteVersion: _upToDateVersion, ), expectSetStamp: true, ); await version.checkFlutterVersionFreshness(); _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(_outOfDateVersion))); }, overrides: <Type, Generator>{ FlutterVersion: () => new FlutterVersion(_testClock), ProcessManager: () => mockProcessManager, Cache: () => mockCache, }); testUsingContext('does not print warning if printed recently', () async { final FlutterVersion version = FlutterVersion.instance; fakeData( mockProcessManager, mockCache, localCommitDate: _outOfDateVersion, 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(''); }, overrides: <Type, Generator>{ FlutterVersion: () => new FlutterVersion(_testClock), ProcessManager: () => mockProcessManager, Cache: () => mockCache, }); testUsingContext('pings server when version stamp is missing then does not', () async { final FlutterVersion version = FlutterVersion.instance; fakeData( mockProcessManager, mockCache, 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( mockProcessManager, mockCache, localCommitDate: _outOfDateVersion, stamp: await VersionCheckStamp.load(), ); await version.checkFlutterVersionFreshness(); _expectVersionMessage(''); }, overrides: <Type, Generator>{ FlutterVersion: () => new FlutterVersion(_testClock), ProcessManager: () => mockProcessManager, Cache: () => mockCache, }); testUsingContext('pings server when version stamp is out-of-date', () async { final FlutterVersion version = FlutterVersion.instance; fakeData( mockProcessManager, mockCache, localCommitDate: _outOfDateVersion, stamp: new VersionCheckStamp( lastTimeVersionWasChecked: _stampOutOfDate, lastKnownRemoteVersion: _testClock.ago(days: 2), ), remoteCommitDate: _upToDateVersion, expectSetStamp: true, expectServerPing: true, ); await version.checkFlutterVersionFreshness(); _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(_outOfDateVersion))); }, overrides: <Type, Generator>{ FlutterVersion: () => new FlutterVersion(_testClock), ProcessManager: () => mockProcessManager, Cache: () => mockCache, }); testUsingContext('ignores network issues', () async { final FlutterVersion version = FlutterVersion.instance; fakeData( mockProcessManager, mockCache, localCommitDate: _outOfDateVersion, errorOnFetch: true, expectServerPing: true, ); await version.checkFlutterVersionFreshness(); _expectVersionMessage(''); }, overrides: <Type, Generator>{ FlutterVersion: () => new FlutterVersion(_testClock), ProcessManager: () => mockProcessManager, Cache: () => mockCache, }); testUsingContext('versions comparison', () async { when(mockProcessManager.runSync( <String>['git', 'merge-base', '--is-ancestor', 'abcdef', '123456'], workingDirectory: anyNamed('workingDirectory'), )).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: anyNamed('workingDirectory'), )); }, overrides: <Type, Generator>{ FlutterVersion: () => new FlutterVersion(_testClock), ProcessManager: () => mockProcessManager, }); }); group('$VersionCheckStamp', () { void _expectDefault(VersionCheckStamp stamp) { expect(stamp.lastKnownRemoteVersion, isNull); expect(stamp.lastTimeVersionWasChecked, isNull); expect(stamp.lastTimeWarningWasPrinted, isNull); } testUsingContext('loads blank when stamp file missing', () async { fakeData(mockProcessManager, mockCache); _expectDefault(await VersionCheckStamp.load()); }, overrides: <Type, Generator>{ FlutterVersion: () => new FlutterVersion(_testClock), ProcessManager: () => mockProcessManager, Cache: () => mockCache, }); testUsingContext('loads blank when stamp file is malformed JSON', () async { fakeData(mockProcessManager, mockCache, stampJson: '<'); _expectDefault(await VersionCheckStamp.load()); }, overrides: <Type, Generator>{ FlutterVersion: () => new FlutterVersion(_testClock), ProcessManager: () => mockProcessManager, Cache: () => mockCache, }); testUsingContext('loads blank when stamp file is well-formed but invalid JSON', () async { fakeData(mockProcessManager, mockCache, stampJson: '[]'); _expectDefault(await VersionCheckStamp.load()); }, overrides: <Type, Generator>{ FlutterVersion: () => new FlutterVersion(_testClock), ProcessManager: () => mockProcessManager, Cache: () => mockCache, }); testUsingContext('loads valid JSON', () async { fakeData(mockProcessManager, mockCache, 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()); }, overrides: <Type, Generator>{ FlutterVersion: () => new FlutterVersion(_testClock), ProcessManager: () => mockProcessManager, Cache: () => mockCache, }); testUsingContext('stores version stamp', () async { fakeData(mockProcessManager, mockCache, 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()); }, overrides: <Type, Generator>{ FlutterVersion: () => new FlutterVersion(_testClock), ProcessManager: () => mockProcessManager, Cache: () => mockCache, }); testUsingContext('overwrites individual fields', () async { fakeData(mockProcessManager, mockCache, 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()); }, overrides: <Type, Generator>{ FlutterVersion: () => new FlutterVersion(_testClock), ProcessManager: () => mockProcessManager, Cache: () => mockCache, }); }); } void _expectVersionMessage(String message) { final BufferLogger logger = context[Logger]; expect(logger.statusText.trim(), message.trim()); logger.clear(); } void fakeData( ProcessManager pm, Cache cache, { DateTime localCommitDate, DateTime remoteCommitDate, VersionCheckStamp stamp, String stampJson, bool errorOnFetch = false, bool expectSetStamp = false, bool expectServerPing = false, }) { 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, VersionCheckStamp.kFlutterVersionCheckStampFile); if (stampJson != null) return stampJson; if (stamp != null) return json.encode(stamp.toJson()); return null; }); when(cache.setStampFor(any, any)).thenAnswer((Invocation invocation) { expect(invocation.positionalArguments.first, VersionCheckStamp.kFlutterVersionCheckStampFile); if (expectSetStamp) { stamp = VersionCheckStamp.fromJson(json.decode(invocation.positionalArguments[1])); return null; } throw new StateError('Unexpected call to Cache.setStampFor(${invocation.positionalArguments}, ${invocation.namedArguments})'); }); final Answering<ProcessResult> 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')) { 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()); } throw new StateError('Unexpected call to ProcessManager.run(${invocation.positionalArguments}, ${invocation.namedArguments})'); }; when(pm.runSync(any, workingDirectory: anyNamed('workingDirectory'))).thenAnswer(syncAnswer); when(pm.run(any, workingDirectory: anyNamed('workingDirectory'))).thenAnswer((Invocation invocation) async { return syncAnswer(invocation); }); } class MockProcessManager extends Mock implements ProcessManager {} class MockCache extends Mock implements Cache {}