Unverified Commit 2d2cd1f5 authored by Christopher Fujino's avatar Christopher Fujino Committed by GitHub

[flutter_tools] Refactor checkVersionFreshness (#95056)

parent 7addb913
...@@ -278,3 +278,6 @@ FlutterProjectFactory get projectFactory { ...@@ -278,3 +278,6 @@ FlutterProjectFactory get projectFactory {
CustomDevicesConfig get customDevicesConfig => context.get<CustomDevicesConfig>()!; CustomDevicesConfig get customDevicesConfig => context.get<CustomDevicesConfig>()!;
PreRunValidator get preRunValidator => context.get<PreRunValidator>() ?? const NoOpPreRunValidator(); PreRunValidator get preRunValidator => context.get<PreRunValidator>() ?? const NoOpPreRunValidator();
// TODO(fujino): Migrate to 'main' https://github.com/flutter/flutter/issues/95041
const String kDefaultFrameworkChannel = 'master';
...@@ -18,6 +18,7 @@ const String _unknownFrameworkVersion = '0.0.0-unknown'; ...@@ -18,6 +18,7 @@ const String _unknownFrameworkVersion = '0.0.0-unknown';
/// The names of each channel/branch in order of increasing stability. /// The names of each channel/branch in order of increasing stability.
enum Channel { enum Channel {
// TODO(fujino): update to main https://github.com/flutter/flutter/issues/95041
master, master,
dev, dev,
beta, beta,
...@@ -26,7 +27,7 @@ enum Channel { ...@@ -26,7 +27,7 @@ enum Channel {
// Beware: Keep order in accordance with stability // Beware: Keep order in accordance with stability
const Set<String> kOfficialChannels = <String>{ const Set<String> kOfficialChannels = <String>{
'master', globals.kDefaultFrameworkChannel,
'dev', 'dev',
'beta', 'beta',
'stable', 'stable',
...@@ -241,15 +242,15 @@ class FlutterVersion { ...@@ -241,15 +242,15 @@ class FlutterVersion {
} }
final DateTime? latestFlutterCommitDate = await _getLatestAvailableFlutterDate(); final DateTime? latestFlutterCommitDate = await _getLatestAvailableFlutterDate();
await checkVersionFreshness( return VersionFreshnessValidator(
this, version: this,
clock: _clock, clock: _clock,
localFrameworkCommitDate: localFrameworkCommitDate, localFrameworkCommitDate: localFrameworkCommitDate,
latestFlutterCommitDate: latestFlutterCommitDate, latestFlutterCommitDate: latestFlutterCommitDate,
logger: globals.logger, logger: globals.logger,
cache: globals.cache, cache: globals.cache,
pauseTime: timeToPauseToLetUserReadTheMessage, pauseTime: VersionFreshnessValidator.timeToPauseToLetUserReadTheMessage,
); ).run();
} }
/// The name of the temporary git remote used to check for the latest /// The name of the temporary git remote used to check for the latest
...@@ -361,13 +362,14 @@ class FlutterVersion { ...@@ -361,13 +362,14 @@ class FlutterVersion {
globals.cache.checkLockAcquired(); globals.cache.checkLockAcquired();
final VersionCheckStamp versionCheckStamp = await VersionCheckStamp.load(globals.cache, globals.logger); final VersionCheckStamp versionCheckStamp = await VersionCheckStamp.load(globals.cache, globals.logger);
final DateTime now = _clock.now();
if (versionCheckStamp.lastTimeVersionWasChecked != null) { if (versionCheckStamp.lastTimeVersionWasChecked != null) {
final Duration timeSinceLastCheck = _clock.now().difference( final Duration timeSinceLastCheck = now.difference(
versionCheckStamp.lastTimeVersionWasChecked!, versionCheckStamp.lastTimeVersionWasChecked!,
); );
// Don't ping the server too often. Return cached value if it's fresh. // Don't ping the server too often. Return cached value if it's fresh.
if (timeSinceLastCheck < checkAgeConsideredUpToDate) { if (timeSinceLastCheck < VersionFreshnessValidator.checkAgeConsideredUpToDate) {
return versionCheckStamp.lastKnownRemoteVersion; return versionCheckStamp.lastKnownRemoteVersion;
} }
} }
...@@ -378,7 +380,7 @@ class FlutterVersion { ...@@ -378,7 +380,7 @@ class FlutterVersion {
await FlutterVersion.fetchRemoteFrameworkCommitDate(channel), await FlutterVersion.fetchRemoteFrameworkCommitDate(channel),
); );
await versionCheckStamp.store( await versionCheckStamp.store(
newTimeVersionWasChecked: _clock.now(), newTimeVersionWasChecked: now,
newKnownRemoteVersion: remoteFrameworkCommitDate, newKnownRemoteVersion: remoteFrameworkCommitDate,
); );
return remoteFrameworkCommitDate; return remoteFrameworkCommitDate;
...@@ -390,7 +392,7 @@ class FlutterVersion { ...@@ -390,7 +392,7 @@ class FlutterVersion {
// Still update the timestamp to avoid us hitting the server on every single // Still update the timestamp to avoid us hitting the server on every single
// command if for some reason we cannot connect (eg. we may be offline). // command if for some reason we cannot connect (eg. we may be offline).
await versionCheckStamp.store( await versionCheckStamp.store(
newTimeVersionWasChecked: _clock.now(), newTimeVersionWasChecked: now,
); );
return null; return null;
} }
...@@ -743,53 +745,152 @@ enum VersionCheckResult { ...@@ -743,53 +745,152 @@ enum VersionCheckResult {
newVersionAvailable, newVersionAvailable,
} }
@visibleForTesting /// Determine whether or not the provided [version] is "fresh" and notify the user if appropriate.
Future<void> checkVersionFreshness(FlutterVersion version, { ///
required DateTime localFrameworkCommitDate, /// To initiate the validation check, call [run].
required DateTime? latestFlutterCommitDate, ///
required SystemClock clock, /// We do not want to check with the upstream git remote for newer commits on
required Cache cache, /// every tool invocation, as this would significantly slow down running tool
required Logger logger, /// commands. Thus, the tool writes to the [VersionCheckStamp] every time that
Duration pauseTime = Duration.zero, /// it actually has fetched commits from upstream, and this validator only
}) async { /// checks again if it has been more than [checkAgeConsideredUpToDate] since the
/// last fetch.
///
/// We do not want to notify users with "reasonably" fresh versions about new
/// releases. The method [versionAgeConsideredUpToDate] defines a different
/// duration of freshness for each channel. If [localFrameworkCommitDate] is
/// newer than this duration, then we do not show the warning.
///
/// We do not want to annoy users who intentionally disregard the warning and
/// choose not to upgrade. Thus, we only show the message if it has been more
/// than [maxTimeSinceLastWarning] since the last time the user saw the warning.
class VersionFreshnessValidator {
VersionFreshnessValidator({
required this.version,
required this.localFrameworkCommitDate,
required this.clock,
required this.cache,
required this.logger,
this.latestFlutterCommitDate,
this.pauseTime = Duration.zero,
});
final FlutterVersion version;
final DateTime localFrameworkCommitDate;
final SystemClock clock;
final Cache cache;
final Logger logger;
final Duration pauseTime;
final DateTime? latestFlutterCommitDate;
late final DateTime now = clock.now();
late final Duration frameworkAge = now.difference(localFrameworkCommitDate);
/// The amount of time we wait before pinging the server to check for the
/// availability of a newer version of Flutter.
@visibleForTesting
static const Duration checkAgeConsideredUpToDate = Duration(days: 3);
/// The amount of time we wait between issuing a warning.
///
/// This is to avoid annoying users who are unable to upgrade right away.
@visibleForTesting
static const Duration maxTimeSinceLastWarning = Duration(days: 1);
/// The amount of time we pause for to let the user read the message about
/// outdated Flutter installation.
///
/// This can be customized in tests to speed them up.
@visibleForTesting
static Duration timeToPauseToLetUserReadTheMessage = const Duration(seconds: 2);
// We show a warning if either we know there is a new remote version, or we
// couldn't tell but the local version is outdated.
@visibleForTesting
bool canShowWarning(VersionCheckResult remoteVersionStatus) {
final bool installationSeemsOutdated = frameworkAge > versionAgeConsideredUpToDate(version.channel);
if (remoteVersionStatus == VersionCheckResult.newVersionAvailable) {
return true;
}
if (!installationSeemsOutdated) {
return false;
}
return remoteVersionStatus == VersionCheckResult.unknown;
}
/// We warn the user if the age of their Flutter installation is greater than
/// this duration. The durations are slightly longer than the expected release
/// cadence for each channel, to give the user a grace period before they get
/// notified.
///
/// For example, for the beta channel, this is set to eight weeks because
/// beta releases happen approximately every month.
@visibleForTesting
static Duration versionAgeConsideredUpToDate(String channel) {
switch (channel) {
case 'stable':
return const Duration(days: 365 ~/ 2); // Six months
case 'beta':
return const Duration(days: 7 * 8); // Eight weeks
case 'dev':
return const Duration(days: 7 * 4); // Four weeks
default:
return const Duration(days: 7 * 3); // Three weeks
}
}
/// Execute validations and print warning to [logger] if necessary.
Future<void> run() async {
// Don't perform update checks if we're not on an official channel. // Don't perform update checks if we're not on an official channel.
if (!kOfficialChannels.contains(version.channel)) { if (!kOfficialChannels.contains(version.channel)) {
return; return;
} }
final Duration frameworkAge = clock.now().difference(localFrameworkCommitDate);
final bool installationSeemsOutdated = frameworkAge > versionAgeConsideredUpToDate(version.channel);
// Get whether there's a newer version on the remote. This only goes // Get whether there's a newer version on the remote. This only goes
// to the server if we haven't checked recently so won't happen on every // to the server if we haven't checked recently so won't happen on every
// command. // command.
final VersionCheckResult remoteVersionStatus = latestFlutterCommitDate == null final VersionCheckResult remoteVersionStatus;
? VersionCheckResult.unknown
: latestFlutterCommitDate.isAfter(localFrameworkCommitDate) if (latestFlutterCommitDate == null) {
? VersionCheckResult.newVersionAvailable remoteVersionStatus = VersionCheckResult.unknown;
: VersionCheckResult.versionIsCurrent; } else {
if (latestFlutterCommitDate!.isAfter(localFrameworkCommitDate)) {
remoteVersionStatus = VersionCheckResult.newVersionAvailable;
} else {
remoteVersionStatus = VersionCheckResult.versionIsCurrent;
}
}
// Do not load the stamp before the above server check as it may modify the stamp file. // Do not load the stamp before the above server check as it may modify the stamp file.
final VersionCheckStamp stamp = await VersionCheckStamp.load(cache, logger); final VersionCheckStamp stamp = await VersionCheckStamp.load(cache, logger);
final DateTime lastTimeWarningWasPrinted = stamp.lastTimeWarningWasPrinted ?? clock.ago(maxTimeSinceLastWarning * 2); final DateTime lastTimeWarningWasPrinted = stamp.lastTimeWarningWasPrinted ?? clock.ago(maxTimeSinceLastWarning * 2);
final bool beenAWhileSinceWarningWasPrinted = clock.now().difference(lastTimeWarningWasPrinted) > maxTimeSinceLastWarning; final bool beenAWhileSinceWarningWasPrinted = now.difference(lastTimeWarningWasPrinted) > maxTimeSinceLastWarning;
if (!beenAWhileSinceWarningWasPrinted) {
// We show a warning if either we know there is a new remote version, or we couldn't tell but the local return;
// version is outdated. }
final bool canShowWarning =
remoteVersionStatus == VersionCheckResult.newVersionAvailable || final bool canShowWarningResult = canShowWarning(remoteVersionStatus);
(remoteVersionStatus == VersionCheckResult.unknown &&
installationSeemsOutdated); if (!canShowWarningResult) {
return;
if (beenAWhileSinceWarningWasPrinted && canShowWarning) { }
final String updateMessage =
remoteVersionStatus == VersionCheckResult.newVersionAvailable // By this point, we should show the update message
? newVersionAvailableMessage() final String updateMessage;
: versionOutOfDateMessage(frameworkAge); switch (remoteVersionStatus) {
case VersionCheckResult.newVersionAvailable:
updateMessage = newVersionAvailableMessage();
break;
case VersionCheckResult.versionIsCurrent:
case VersionCheckResult.unknown:
updateMessage = versionOutOfDateMessage(frameworkAge);
break;
}
logger.printStatus(updateMessage, emphasis: true); logger.printStatus(updateMessage, emphasis: true);
await Future.wait<void>(<Future<void>>[ await Future.wait<void>(<Future<void>>[
stamp.store( stamp.store(
newTimeWarningWasPrinted: clock.now(), newTimeWarningWasPrinted: now,
cache: cache, cache: cache,
), ),
Future<void>.delayed(pauseTime), Future<void>.delayed(pauseTime),
...@@ -797,45 +898,6 @@ Future<void> checkVersionFreshness(FlutterVersion version, { ...@@ -797,45 +898,6 @@ Future<void> checkVersionFreshness(FlutterVersion version, {
} }
} }
/// The amount of time we wait before pinging the server to check for the
/// availability of a newer version of Flutter.
@visibleForTesting
const Duration checkAgeConsideredUpToDate = Duration(days: 3);
/// We warn the user if the age of their Flutter installation is greater than
/// this duration. The durations are slightly longer than the expected release
/// cadence for each channel, to give the user a grace period before they get
/// notified.
///
/// For example, for the beta channel, this is set to five weeks because
/// beta releases happen approximately every month.
@visibleForTesting
Duration versionAgeConsideredUpToDate(String channel) {
switch (channel) {
case 'stable':
return const Duration(days: 365 ~/ 2); // Six months
case 'beta':
return const Duration(days: 7 * 8); // Eight weeks
case 'dev':
return const Duration(days: 7 * 4); // Four weeks
default:
return const Duration(days: 7 * 3); // Three weeks
}
}
/// The amount of time we wait between issuing a warning.
///
/// This is to avoid annoying users who are unable to upgrade right away.
@visibleForTesting
const Duration maxTimeSinceLastWarning = Duration(days: 1);
/// The amount of time we pause for to let the user read the message about
/// outdated Flutter installation.
///
/// This can be customized in tests to speed them up.
@visibleForTesting
Duration timeToPauseToLetUserReadTheMessage = const Duration(seconds: 2);
@visibleForTesting @visibleForTesting
String versionOutOfDateMessage(Duration frameworkAge) { String versionOutOfDateMessage(Duration frameworkAge) {
String warning = 'WARNING: your installation of Flutter is ${frameworkAge.inDays} days old.'; String warning = 'WARNING: your installation of Flutter is ${frameworkAge.inDays} days old.';
......
...@@ -20,8 +20,8 @@ import '../src/context.dart'; ...@@ -20,8 +20,8 @@ import '../src/context.dart';
import '../src/fake_process_manager.dart'; import '../src/fake_process_manager.dart';
final SystemClock _testClock = SystemClock.fixed(DateTime(2015)); final SystemClock _testClock = SystemClock.fixed(DateTime(2015));
final DateTime _stampUpToDate = _testClock.ago(checkAgeConsideredUpToDate ~/ 2); final DateTime _stampUpToDate = _testClock.ago(VersionFreshnessValidator.checkAgeConsideredUpToDate ~/ 2);
final DateTime _stampOutOfDate = _testClock.ago(checkAgeConsideredUpToDate * 2); final DateTime _stampOutOfDate = _testClock.ago(VersionFreshnessValidator.checkAgeConsideredUpToDate * 2);
void main() { void main() {
FakeCache cache; FakeCache cache;
...@@ -42,17 +42,17 @@ void main() { ...@@ -42,17 +42,17 @@ void main() {
for (final String channel in kOfficialChannels) { for (final String channel in kOfficialChannels) {
DateTime getChannelUpToDateVersion() { DateTime getChannelUpToDateVersion() {
return _testClock.ago(versionAgeConsideredUpToDate(channel) ~/ 2); return _testClock.ago(VersionFreshnessValidator.versionAgeConsideredUpToDate(channel) ~/ 2);
} }
DateTime getChannelOutOfDateVersion() { DateTime getChannelOutOfDateVersion() {
return _testClock.ago(versionAgeConsideredUpToDate(channel) * 2); return _testClock.ago(VersionFreshnessValidator.versionAgeConsideredUpToDate(channel) * 2);
} }
group('$FlutterVersion for $channel', () { group('$FlutterVersion for $channel', () {
setUpAll(() { setUpAll(() {
Cache.disableLocking(); Cache.disableLocking();
timeToPauseToLetUserReadTheMessage = Duration.zero; VersionFreshnessValidator.timeToPauseToLetUserReadTheMessage = Duration.zero;
}); });
testUsingContext('prints nothing when Flutter installation looks fresh', () async { testUsingContext('prints nothing when Flutter installation looks fresh', () async {
...@@ -151,14 +151,14 @@ void main() { ...@@ -151,14 +151,14 @@ void main() {
); );
cache.versionStamp = json.encode(stamp); cache.versionStamp = json.encode(stamp);
await checkVersionFreshness( await VersionFreshnessValidator(
flutterVersion, version: flutterVersion,
cache: cache, cache: cache,
clock: _testClock, clock: _testClock,
logger: logger, logger: logger,
localFrameworkCommitDate: getChannelOutOfDateVersion(), localFrameworkCommitDate: getChannelOutOfDateVersion(),
latestFlutterCommitDate: getChannelOutOfDateVersion(), latestFlutterCommitDate: getChannelOutOfDateVersion(),
); ).run();
_expectVersionMessage('', logger); _expectVersionMessage('', logger);
}); });
...@@ -172,14 +172,14 @@ void main() { ...@@ -172,14 +172,14 @@ void main() {
); );
cache.versionStamp = json.encode(stamp); cache.versionStamp = json.encode(stamp);
await checkVersionFreshness( await VersionFreshnessValidator(
flutterVersion, version: flutterVersion,
cache: cache, cache: cache,
clock: _testClock, clock: _testClock,
logger: logger, logger: logger,
localFrameworkCommitDate: getChannelOutOfDateVersion(), localFrameworkCommitDate: getChannelOutOfDateVersion(),
latestFlutterCommitDate: getChannelUpToDateVersion(), latestFlutterCommitDate: getChannelUpToDateVersion(),
); ).run();
_expectVersionMessage(newVersionAvailableMessage(), logger); _expectVersionMessage(newVersionAvailableMessage(), logger);
expect(cache.setVersionStamp, true); expect(cache.setVersionStamp, true);
...@@ -195,14 +195,14 @@ void main() { ...@@ -195,14 +195,14 @@ void main() {
); );
cache.versionStamp = json.encode(stamp); cache.versionStamp = json.encode(stamp);
await checkVersionFreshness( await VersionFreshnessValidator(
flutterVersion, version: flutterVersion,
cache: cache, cache: cache,
clock: _testClock, clock: _testClock,
logger: logger, logger: logger,
localFrameworkCommitDate: getChannelOutOfDateVersion(), localFrameworkCommitDate: getChannelOutOfDateVersion(),
latestFlutterCommitDate: getChannelUpToDateVersion(), latestFlutterCommitDate: getChannelUpToDateVersion(),
); ).run();
_expectVersionMessage('', logger); _expectVersionMessage('', logger);
}); });
...@@ -212,14 +212,14 @@ void main() { ...@@ -212,14 +212,14 @@ void main() {
final BufferLogger logger = BufferLogger.test(); final BufferLogger logger = BufferLogger.test();
cache.versionStamp = '{}'; cache.versionStamp = '{}';
await checkVersionFreshness( await VersionFreshnessValidator(
flutterVersion, version: flutterVersion,
cache: cache, cache: cache,
clock: _testClock, clock: _testClock,
logger: logger, logger: logger,
localFrameworkCommitDate: getChannelOutOfDateVersion(), localFrameworkCommitDate: getChannelOutOfDateVersion(),
latestFlutterCommitDate: getChannelUpToDateVersion(), latestFlutterCommitDate: getChannelUpToDateVersion(),
); ).run();
_expectVersionMessage(newVersionAvailableMessage(), logger); _expectVersionMessage(newVersionAvailableMessage(), logger);
expect(cache.setVersionStamp, true); expect(cache.setVersionStamp, true);
...@@ -234,14 +234,14 @@ void main() { ...@@ -234,14 +234,14 @@ void main() {
); );
cache.versionStamp = json.encode(stamp); cache.versionStamp = json.encode(stamp);
await checkVersionFreshness( await VersionFreshnessValidator(
flutterVersion, version: flutterVersion,
cache: cache, cache: cache,
clock: _testClock, clock: _testClock,
logger: logger, logger: logger,
localFrameworkCommitDate: getChannelOutOfDateVersion(), localFrameworkCommitDate: getChannelOutOfDateVersion(),
latestFlutterCommitDate: getChannelUpToDateVersion(), latestFlutterCommitDate: getChannelUpToDateVersion(),
); ).run();
_expectVersionMessage(newVersionAvailableMessage(), logger); _expectVersionMessage(newVersionAvailableMessage(), logger);
}); });
...@@ -251,14 +251,14 @@ void main() { ...@@ -251,14 +251,14 @@ void main() {
final BufferLogger logger = BufferLogger.test(); final BufferLogger logger = BufferLogger.test();
cache.versionStamp = '{}'; cache.versionStamp = '{}';
await checkVersionFreshness( await VersionFreshnessValidator(
flutterVersion, version: flutterVersion,
cache: cache, cache: cache,
clock: _testClock, clock: _testClock,
logger: logger, logger: logger,
localFrameworkCommitDate: getChannelUpToDateVersion(), localFrameworkCommitDate: getChannelUpToDateVersion(),
latestFlutterCommitDate: null, // Failed to get remote version // latestFlutterCommitDate defaults to null because we failed to get remote version
); ).run();
_expectVersionMessage('', logger); _expectVersionMessage('', logger);
}); });
...@@ -272,14 +272,14 @@ void main() { ...@@ -272,14 +272,14 @@ void main() {
); );
cache.versionStamp = json.encode(stamp); cache.versionStamp = json.encode(stamp);
await checkVersionFreshness( await VersionFreshnessValidator(
flutterVersion, version: flutterVersion,
cache: cache, cache: cache,
clock: _testClock, clock: _testClock,
logger: logger, logger: logger,
localFrameworkCommitDate: getChannelOutOfDateVersion(), localFrameworkCommitDate: getChannelOutOfDateVersion(),
latestFlutterCommitDate: null, // Failed to get remote version // latestFlutterCommitDate defaults to null because we failed to get remote version
); ).run();
_expectVersionMessage(versionOutOfDateMessage(_testClock.now().difference(getChannelOutOfDateVersion())), logger); _expectVersionMessage(versionOutOfDateMessage(_testClock.now().difference(getChannelOutOfDateVersion())), logger);
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment