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 {
CustomDevicesConfig get customDevicesConfig => context.get<CustomDevicesConfig>()!;
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';
/// The names of each channel/branch in order of increasing stability.
enum Channel {
// TODO(fujino): update to main https://github.com/flutter/flutter/issues/95041
master,
dev,
beta,
......@@ -26,7 +27,7 @@ enum Channel {
// Beware: Keep order in accordance with stability
const Set<String> kOfficialChannels = <String>{
'master',
globals.kDefaultFrameworkChannel,
'dev',
'beta',
'stable',
......@@ -241,15 +242,15 @@ class FlutterVersion {
}
final DateTime? latestFlutterCommitDate = await _getLatestAvailableFlutterDate();
await checkVersionFreshness(
this,
return VersionFreshnessValidator(
version: this,
clock: _clock,
localFrameworkCommitDate: localFrameworkCommitDate,
latestFlutterCommitDate: latestFlutterCommitDate,
logger: globals.logger,
cache: globals.cache,
pauseTime: timeToPauseToLetUserReadTheMessage,
);
pauseTime: VersionFreshnessValidator.timeToPauseToLetUserReadTheMessage,
).run();
}
/// The name of the temporary git remote used to check for the latest
......@@ -361,13 +362,14 @@ class FlutterVersion {
globals.cache.checkLockAcquired();
final VersionCheckStamp versionCheckStamp = await VersionCheckStamp.load(globals.cache, globals.logger);
final DateTime now = _clock.now();
if (versionCheckStamp.lastTimeVersionWasChecked != null) {
final Duration timeSinceLastCheck = _clock.now().difference(
final Duration timeSinceLastCheck = now.difference(
versionCheckStamp.lastTimeVersionWasChecked!,
);
// Don't ping the server too often. Return cached value if it's fresh.
if (timeSinceLastCheck < checkAgeConsideredUpToDate) {
if (timeSinceLastCheck < VersionFreshnessValidator.checkAgeConsideredUpToDate) {
return versionCheckStamp.lastKnownRemoteVersion;
}
}
......@@ -378,7 +380,7 @@ class FlutterVersion {
await FlutterVersion.fetchRemoteFrameworkCommitDate(channel),
);
await versionCheckStamp.store(
newTimeVersionWasChecked: _clock.now(),
newTimeVersionWasChecked: now,
newKnownRemoteVersion: remoteFrameworkCommitDate,
);
return remoteFrameworkCommitDate;
......@@ -390,7 +392,7 @@ class FlutterVersion {
// 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).
await versionCheckStamp.store(
newTimeVersionWasChecked: _clock.now(),
newTimeVersionWasChecked: now,
);
return null;
}
......@@ -743,53 +745,152 @@ enum VersionCheckResult {
newVersionAvailable,
}
@visibleForTesting
Future<void> checkVersionFreshness(FlutterVersion version, {
required DateTime localFrameworkCommitDate,
required DateTime? latestFlutterCommitDate,
required SystemClock clock,
required Cache cache,
required Logger logger,
Duration pauseTime = Duration.zero,
}) async {
// Don't perform update checks if we're not on an official channel.
if (!kOfficialChannels.contains(version.channel)) {
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
// to the server if we haven't checked recently so won't happen on every
// command.
final VersionCheckResult remoteVersionStatus = latestFlutterCommitDate == null
? VersionCheckResult.unknown
: latestFlutterCommitDate.isAfter(localFrameworkCommitDate)
? VersionCheckResult.newVersionAvailable
: VersionCheckResult.versionIsCurrent;
// 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 DateTime lastTimeWarningWasPrinted = stamp.lastTimeWarningWasPrinted ?? clock.ago(maxTimeSinceLastWarning * 2);
final bool beenAWhileSinceWarningWasPrinted = clock.now().difference(lastTimeWarningWasPrinted) > maxTimeSinceLastWarning;
// 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.
final bool canShowWarning =
remoteVersionStatus == VersionCheckResult.newVersionAvailable ||
(remoteVersionStatus == VersionCheckResult.unknown &&
installationSeemsOutdated);
if (beenAWhileSinceWarningWasPrinted && canShowWarning) {
final String updateMessage =
remoteVersionStatus == VersionCheckResult.newVersionAvailable
? newVersionAvailableMessage()
: versionOutOfDateMessage(frameworkAge);
/// Determine whether or not the provided [version] is "fresh" and notify the user if appropriate.
///
/// To initiate the validation check, call [run].
///
/// We do not want to check with the upstream git remote for newer commits on
/// every tool invocation, as this would significantly slow down running tool
/// commands. Thus, the tool writes to the [VersionCheckStamp] every time that
/// it actually has fetched commits from upstream, and this validator only
/// 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.
if (!kOfficialChannels.contains(version.channel)) {
return;
}
// 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
// command.
final VersionCheckResult remoteVersionStatus;
if (latestFlutterCommitDate == null) {
remoteVersionStatus = VersionCheckResult.unknown;
} 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.
final VersionCheckStamp stamp = await VersionCheckStamp.load(cache, logger);
final DateTime lastTimeWarningWasPrinted = stamp.lastTimeWarningWasPrinted ?? clock.ago(maxTimeSinceLastWarning * 2);
final bool beenAWhileSinceWarningWasPrinted = now.difference(lastTimeWarningWasPrinted) > maxTimeSinceLastWarning;
if (!beenAWhileSinceWarningWasPrinted) {
return;
}
final bool canShowWarningResult = canShowWarning(remoteVersionStatus);
if (!canShowWarningResult) {
return;
}
// By this point, we should show the update message
final String updateMessage;
switch (remoteVersionStatus) {
case VersionCheckResult.newVersionAvailable:
updateMessage = newVersionAvailableMessage();
break;
case VersionCheckResult.versionIsCurrent:
case VersionCheckResult.unknown:
updateMessage = versionOutOfDateMessage(frameworkAge);
break;
}
logger.printStatus(updateMessage, emphasis: true);
await Future.wait<void>(<Future<void>>[
stamp.store(
newTimeWarningWasPrinted: clock.now(),
newTimeWarningWasPrinted: now,
cache: cache,
),
Future<void>.delayed(pauseTime),
......@@ -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
String versionOutOfDateMessage(Duration frameworkAge) {
String warning = 'WARNING: your installation of Flutter is ${frameworkAge.inDays} days old.';
......
......@@ -20,8 +20,8 @@ import '../src/context.dart';
import '../src/fake_process_manager.dart';
final SystemClock _testClock = SystemClock.fixed(DateTime(2015));
final DateTime _stampUpToDate = _testClock.ago(checkAgeConsideredUpToDate ~/ 2);
final DateTime _stampOutOfDate = _testClock.ago(checkAgeConsideredUpToDate * 2);
final DateTime _stampUpToDate = _testClock.ago(VersionFreshnessValidator.checkAgeConsideredUpToDate ~/ 2);
final DateTime _stampOutOfDate = _testClock.ago(VersionFreshnessValidator.checkAgeConsideredUpToDate * 2);
void main() {
FakeCache cache;
......@@ -42,17 +42,17 @@ void main() {
for (final String channel in kOfficialChannels) {
DateTime getChannelUpToDateVersion() {
return _testClock.ago(versionAgeConsideredUpToDate(channel) ~/ 2);
return _testClock.ago(VersionFreshnessValidator.versionAgeConsideredUpToDate(channel) ~/ 2);
}
DateTime getChannelOutOfDateVersion() {
return _testClock.ago(versionAgeConsideredUpToDate(channel) * 2);
return _testClock.ago(VersionFreshnessValidator.versionAgeConsideredUpToDate(channel) * 2);
}
group('$FlutterVersion for $channel', () {
setUpAll(() {
Cache.disableLocking();
timeToPauseToLetUserReadTheMessage = Duration.zero;
VersionFreshnessValidator.timeToPauseToLetUserReadTheMessage = Duration.zero;
});
testUsingContext('prints nothing when Flutter installation looks fresh', () async {
......@@ -151,14 +151,14 @@ void main() {
);
cache.versionStamp = json.encode(stamp);
await checkVersionFreshness(
flutterVersion,
await VersionFreshnessValidator(
version: flutterVersion,
cache: cache,
clock: _testClock,
logger: logger,
localFrameworkCommitDate: getChannelOutOfDateVersion(),
latestFlutterCommitDate: getChannelOutOfDateVersion(),
);
).run();
_expectVersionMessage('', logger);
});
......@@ -172,14 +172,14 @@ void main() {
);
cache.versionStamp = json.encode(stamp);
await checkVersionFreshness(
flutterVersion,
await VersionFreshnessValidator(
version: flutterVersion,
cache: cache,
clock: _testClock,
logger: logger,
localFrameworkCommitDate: getChannelOutOfDateVersion(),
latestFlutterCommitDate: getChannelUpToDateVersion(),
);
).run();
_expectVersionMessage(newVersionAvailableMessage(), logger);
expect(cache.setVersionStamp, true);
......@@ -195,14 +195,14 @@ void main() {
);
cache.versionStamp = json.encode(stamp);
await checkVersionFreshness(
flutterVersion,
await VersionFreshnessValidator(
version: flutterVersion,
cache: cache,
clock: _testClock,
logger: logger,
localFrameworkCommitDate: getChannelOutOfDateVersion(),
latestFlutterCommitDate: getChannelUpToDateVersion(),
);
).run();
_expectVersionMessage('', logger);
});
......@@ -212,14 +212,14 @@ void main() {
final BufferLogger logger = BufferLogger.test();
cache.versionStamp = '{}';
await checkVersionFreshness(
flutterVersion,
await VersionFreshnessValidator(
version: flutterVersion,
cache: cache,
clock: _testClock,
logger: logger,
localFrameworkCommitDate: getChannelOutOfDateVersion(),
latestFlutterCommitDate: getChannelUpToDateVersion(),
);
).run();
_expectVersionMessage(newVersionAvailableMessage(), logger);
expect(cache.setVersionStamp, true);
......@@ -234,14 +234,14 @@ void main() {
);
cache.versionStamp = json.encode(stamp);
await checkVersionFreshness(
flutterVersion,
await VersionFreshnessValidator(
version: flutterVersion,
cache: cache,
clock: _testClock,
logger: logger,
localFrameworkCommitDate: getChannelOutOfDateVersion(),
latestFlutterCommitDate: getChannelUpToDateVersion(),
);
).run();
_expectVersionMessage(newVersionAvailableMessage(), logger);
});
......@@ -251,14 +251,14 @@ void main() {
final BufferLogger logger = BufferLogger.test();
cache.versionStamp = '{}';
await checkVersionFreshness(
flutterVersion,
await VersionFreshnessValidator(
version: flutterVersion,
cache: cache,
clock: _testClock,
logger: logger,
localFrameworkCommitDate: getChannelUpToDateVersion(),
latestFlutterCommitDate: null, // Failed to get remote version
);
// latestFlutterCommitDate defaults to null because we failed to get remote version
).run();
_expectVersionMessage('', logger);
});
......@@ -272,14 +272,14 @@ void main() {
);
cache.versionStamp = json.encode(stamp);
await checkVersionFreshness(
flutterVersion,
await VersionFreshnessValidator(
version: flutterVersion,
cache: cache,
clock: _testClock,
logger: logger,
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);
});
......
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