Unverified Commit e2554a92 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Add "flutter downgrade" command (#50506)

parent 1f3d423f
...@@ -25,6 +25,7 @@ import 'src/commands/create.dart'; ...@@ -25,6 +25,7 @@ import 'src/commands/create.dart';
import 'src/commands/daemon.dart'; import 'src/commands/daemon.dart';
import 'src/commands/devices.dart'; import 'src/commands/devices.dart';
import 'src/commands/doctor.dart'; import 'src/commands/doctor.dart';
import 'src/commands/downgrade.dart';
import 'src/commands/drive.dart'; import 'src/commands/drive.dart';
import 'src/commands/emulators.dart'; import 'src/commands/emulators.dart';
import 'src/commands/format.dart'; import 'src/commands/format.dart';
...@@ -76,6 +77,7 @@ Future<void> main(List<String> args) async { ...@@ -76,6 +77,7 @@ Future<void> main(List<String> args) async {
DaemonCommand(hidden: !verboseHelp), DaemonCommand(hidden: !verboseHelp),
DevicesCommand(), DevicesCommand(),
DoctorCommand(verbose: verbose), DoctorCommand(verbose: verbose),
DowngradeCommand(),
DriveCommand(), DriveCommand(),
EmulatorsCommand(), EmulatorsCommand(),
FormatCommand(), FormatCommand(),
......
// Copyright 2014 The Flutter 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 'package:process/process.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../base/terminal.dart';
import '../base/time.dart';
import '../cache.dart';
import '../globals.dart' as globals;
import '../persistent_tool_state.dart';
import '../runner/flutter_command.dart';
import '../version.dart';
/// The flutter downgrade command returns the SDK to the last recorded version
/// for a particular branch.
///
/// For example, suppose a user on the beta channel upgrades from 1.2.3 to 1.4.6.
/// The tool will record that sha "abcdefg" was the last active beta channel in the
/// persistent tool state. If the user is still on the beta channel and runs
/// flutter downgrade, this will take the user back to "abcdefg". They will not be
/// able to downgrade again, since the tool only records one prior version.
/// Additionally, if they had switched channels to stable before trying to downgrade,
/// the command would fail since there was no previously recorded stable version.
class DowngradeCommand extends FlutterCommand {
DowngradeCommand({
PersistentToolState persistentToolState,
Logger logger,
ProcessManager processManager,
FlutterVersion flutterVersion,
AnsiTerminal terminal,
Stdio stdio,
FileSystem fileSystem,
}) : _terminal = terminal,
_flutterVersion = flutterVersion,
_persistentToolState = persistentToolState,
_processManager = processManager,
_stdio = stdio,
_logger = logger,
_fileSystem = fileSystem {
argParser.addOption(
'working-directory',
hide: true,
help: 'Override the downgrade working directory for integration testing.'
);
argParser.addFlag(
'prompt',
defaultsTo: true,
hide: true,
help: 'Disable the downgrade prompt for integration testing.'
);
}
AnsiTerminal _terminal;
FlutterVersion _flutterVersion;
PersistentToolState _persistentToolState;
ProcessUtils _processUtils;
ProcessManager _processManager;
Logger _logger;
Stdio _stdio;
FileSystem _fileSystem;
@override
String get description => 'Downgrade Flutter to the last active version for the current channel.';
@override
String get name => 'downgrade';
@override
Future<FlutterCommandResult> runCommand() async {
// Note: commands do not necessarily have access to the correct zone injected
// values when being created. Fields must be lazily instantiated in runCommand,
// at least until the zone injection is refactored.
_terminal ??= globals.terminal;
_logger ??= globals.logger;
_flutterVersion ??= globals.flutterVersion;
_persistentToolState ??= globals.persistentToolState;
_processManager ??= globals.processManager;
_processUtils ??= ProcessUtils(processManager: _processManager, logger: _logger);
_stdio ??= globals.stdio;
_fileSystem ??= globals.fs;
String workingDirectory = Cache.flutterRoot;
if (argResults.wasParsed('working-directory')) {
workingDirectory = stringArg('working-directory');
_flutterVersion = FlutterVersion(const SystemClock(), workingDirectory);
}
final String currentChannel = _flutterVersion.channel;
final Channel channel = getChannelForName(currentChannel);
if (channel == null) {
throwToolExit(
'Flutter is not currently on a known channel. Use "flutter channel <name>" '
'to switch to an official channel.',
);
}
final String lastFlutterVesion = _persistentToolState.lastActiveVersion(channel);
final String currentFlutterVersion = _flutterVersion.frameworkRevision;
if (lastFlutterVesion == null || currentFlutterVersion == lastFlutterVesion) {
final String trailing = await _createErrorMessage(workingDirectory, channel);
throwToolExit(
'There is no previously recorded version for channel "$currentChannel".\n'
'$trailing'
);
}
// Detect unkown versions.
final RunResult parseResult = await _processUtils.run(<String>[
'git', 'describe', '--tags', lastFlutterVesion,
], workingDirectory: workingDirectory);
if (parseResult.exitCode != 0) {
throwToolExit('Failed to parse version for downgrade:\n${parseResult.stderr}');
}
final String humanReadableVersion = parseResult.stdout;
// If there is a terminal attached, prompt the user to confirm the downgrade.
if (_stdio.hasTerminal && boolArg('prompt')) {
_terminal.usesTerminalUi = true;
final String result = await _terminal.promptForCharInput(
const <String>['y', 'n'],
prompt: 'Downgrade flutter to version $humanReadableVersion?',
logger: _logger,
);
if (result == 'n') {
return FlutterCommandResult.success();
}
} else {
_logger.printStatus('Downgrading Flutter to version $humanReadableVersion');
}
// To downgrade the tool, we perform a git checkout --hard, and then
// switch channels. The version recorded must have existed on that branch
// so this operation is safe.
try {
await _processUtils.run(
<String>['git', 'reset', '--hard', lastFlutterVesion],
throwOnError: true,
workingDirectory: workingDirectory,
);
} on ProcessException catch (error) {
throwToolExit(
'Unable to downgrade Flutter: The tool could not update to the version '
'$humanReadableVersion. This may be due to git not being installed or an '
'internal error. Please ensure that git is installed on your computer and '
'retry again.\nError: $error.'
);
}
try {
await _processUtils.run(
<String>['git', 'checkout', currentChannel, '--'],
throwOnError: true,
workingDirectory: workingDirectory,
);
} on ProcessException catch (error) {
throwToolExit(
'Unable to downgrade Flutter: The tool could not switch to the channel '
'$currentChannel. This may be due to git not being installed or an '
'internal error. Please ensure that git is installed on your computer '
'and retry again.\nError: $error.'
);
}
await FlutterVersion.resetFlutterVersionFreshnessCheck();
_logger.printStatus('Success');
return FlutterCommandResult.success();
}
// Formats an error message that lists the currently stored versions.
Future<String> _createErrorMessage(String workingDirectory, Channel currentChannel) async {
final StringBuffer buffer = StringBuffer();
for (final Channel channel in Channel.values) {
if (channel == currentChannel) {
continue;
}
final String sha = _persistentToolState.lastActiveVersion(channel);
if (sha == null) {
continue;
}
final RunResult parseResult = await _processUtils.run(<String>[
'git', 'describe', '--tags', sha,
], workingDirectory: workingDirectory);
if (parseResult.exitCode == 0) {
buffer.writeln('Channel "${getNameForChannel(channel)}" was previously on: ${parseResult.stdout}.');
}
}
return buffer.toString();
}
}
...@@ -10,6 +10,7 @@ import '../base/common.dart'; ...@@ -10,6 +10,7 @@ import '../base/common.dart';
import '../base/io.dart'; import '../base/io.dart';
import '../base/os.dart'; import '../base/os.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../base/time.dart';
import '../cache.dart'; import '../cache.dart';
import '../dart/pub.dart'; import '../dart/pub.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
...@@ -34,6 +35,11 @@ class UpgradeCommand extends FlutterCommand { ...@@ -34,6 +35,11 @@ class UpgradeCommand extends FlutterCommand {
help: 'For the second half of the upgrade flow requiring the new ' help: 'For the second half of the upgrade flow requiring the new '
'version of Flutter. Should not be invoked manually, but ' 'version of Flutter. Should not be invoked manually, but '
're-entrantly by the standard upgrade command.', 're-entrantly by the standard upgrade command.',
)
..addOption(
'working-directory',
hide: true,
help: 'Override the upgrade working directoy for integration testing.'
); );
} }
...@@ -50,36 +56,50 @@ class UpgradeCommand extends FlutterCommand { ...@@ -50,36 +56,50 @@ class UpgradeCommand extends FlutterCommand {
@override @override
Future<FlutterCommandResult> runCommand() { Future<FlutterCommandResult> runCommand() {
_commandRunner.workingDirectory = stringArg('working-directory') ?? Cache.flutterRoot;
return _commandRunner.runCommand( return _commandRunner.runCommand(
boolArg('force'), force: boolArg('force'),
boolArg('continue'), continueFlow: boolArg('continue'),
GitTagVersion.determine(), testFlow: stringArg('working-directory') != null,
globals.flutterVersion, gitTagVersion: GitTagVersion.determine(processUtils),
flutterVersion: stringArg('working-directory') == null
? globals.flutterVersion
: FlutterVersion(const SystemClock(), _commandRunner.workingDirectory),
); );
} }
} }
@visibleForTesting @visibleForTesting
class UpgradeCommandRunner { class UpgradeCommandRunner {
Future<FlutterCommandResult> runCommand(
bool force, String workingDirectory;
bool continueFlow,
GitTagVersion gitTagVersion, Future<FlutterCommandResult> runCommand({
FlutterVersion flutterVersion, @required bool force,
) async { @required bool continueFlow,
@required bool testFlow,
@required GitTagVersion gitTagVersion,
@required FlutterVersion flutterVersion,
}) async {
if (!continueFlow) { if (!continueFlow) {
await runCommandFirstHalf(force, gitTagVersion, flutterVersion); await runCommandFirstHalf(
force: force,
gitTagVersion: gitTagVersion,
flutterVersion: flutterVersion,
testFlow: testFlow,
);
} else { } else {
await runCommandSecondHalf(flutterVersion); await runCommandSecondHalf(flutterVersion);
} }
return FlutterCommandResult.success(); return FlutterCommandResult.success();
} }
Future<void> runCommandFirstHalf( Future<void> runCommandFirstHalf({
bool force, @required bool force,
GitTagVersion gitTagVersion, @required GitTagVersion gitTagVersion,
FlutterVersion flutterVersion, @required FlutterVersion flutterVersion,
) async { @required bool testFlow,
}) async {
await verifyUpstreamConfigured(); await verifyUpstreamConfigured();
if (!force && gitTagVersion == const GitTagVersion.unknown()) { if (!force && gitTagVersion == const GitTagVersion.unknown()) {
// If the commit is a recognized branch and not master, // If the commit is a recognized branch and not master,
...@@ -110,6 +130,7 @@ class UpgradeCommandRunner { ...@@ -110,6 +130,7 @@ class UpgradeCommandRunner {
'command with --force.' 'command with --force.'
); );
} }
recordState(flutterVersion);
await resetChanges(gitTagVersion); await resetChanges(gitTagVersion);
await upgradeChannel(flutterVersion); await upgradeChannel(flutterVersion);
final bool alreadyUpToDate = await attemptFastForward(flutterVersion); final bool alreadyUpToDate = await attemptFastForward(flutterVersion);
...@@ -117,11 +138,19 @@ class UpgradeCommandRunner { ...@@ -117,11 +138,19 @@ class UpgradeCommandRunner {
// If the upgrade was a no op, then do not continue with the second half. // If the upgrade was a no op, then do not continue with the second half.
globals.printStatus('Flutter is already up to date on channel ${flutterVersion.channel}'); globals.printStatus('Flutter is already up to date on channel ${flutterVersion.channel}');
globals.printStatus('$flutterVersion'); globals.printStatus('$flutterVersion');
} else { } else if (!testFlow) {
await flutterUpgradeContinue(); await flutterUpgradeContinue();
} }
} }
void recordState(FlutterVersion flutterVersion) {
final Channel channel = getChannelForName(flutterVersion.channel);
if (channel == null) {
return;
}
globals.persistentToolState.updateLastActiveVersion(flutterVersion.frameworkRevision, channel);
}
Future<void> flutterUpgradeContinue() async { Future<void> flutterUpgradeContinue() async {
final int code = await processUtils.stream( final int code = await processUtils.stream(
<String>[ <String>[
...@@ -130,7 +159,7 @@ class UpgradeCommandRunner { ...@@ -130,7 +159,7 @@ class UpgradeCommandRunner {
'--continue', '--continue',
'--no-version-check', '--no-version-check',
], ],
workingDirectory: Cache.flutterRoot, workingDirectory: workingDirectory,
allowReentrantFlutter: true, allowReentrantFlutter: true,
environment: Map<String, String>.of(globals.platform.environment), environment: Map<String, String>.of(globals.platform.environment),
); );
...@@ -156,7 +185,7 @@ class UpgradeCommandRunner { ...@@ -156,7 +185,7 @@ class UpgradeCommandRunner {
final RunResult result = await processUtils.run( final RunResult result = await processUtils.run(
<String>['git', 'status', '-s'], <String>['git', 'status', '-s'],
throwOnError: true, throwOnError: true,
workingDirectory: Cache.flutterRoot, workingDirectory: workingDirectory,
); );
return result.stdout.trim().isNotEmpty; return result.stdout.trim().isNotEmpty;
} on ProcessException catch (error) { } on ProcessException catch (error) {
...@@ -179,13 +208,13 @@ class UpgradeCommandRunner { ...@@ -179,13 +208,13 @@ class UpgradeCommandRunner {
await processUtils.run( await processUtils.run(
<String>[ 'git', 'rev-parse', '@{u}'], <String>[ 'git', 'rev-parse', '@{u}'],
throwOnError: true, throwOnError: true,
workingDirectory: Cache.flutterRoot, workingDirectory: workingDirectory,
); );
} catch (e) { } catch (e) {
throwToolExit( throwToolExit(
'Unable to upgrade Flutter: no origin repository configured. ' 'Unable to upgrade Flutter: no origin repository configured. '
"Run 'git remote add origin " "Run 'git remote add origin "
"https://github.com/flutter/flutter' in ${Cache.flutterRoot}", "https://github.com/flutter/flutter' in $workingDirectory",
); );
} }
} }
...@@ -206,7 +235,7 @@ class UpgradeCommandRunner { ...@@ -206,7 +235,7 @@ class UpgradeCommandRunner {
await processUtils.run( await processUtils.run(
<String>['git', 'reset', '--hard', tag], <String>['git', 'reset', '--hard', tag],
throwOnError: true, throwOnError: true,
workingDirectory: Cache.flutterRoot, workingDirectory: workingDirectory,
); );
} on ProcessException catch (error) { } on ProcessException catch (error) {
throwToolExit( throwToolExit(
...@@ -223,7 +252,7 @@ class UpgradeCommandRunner { ...@@ -223,7 +252,7 @@ class UpgradeCommandRunner {
/// If the user is on a deprecated channel, attempts to migrate them off of /// If the user is on a deprecated channel, attempts to migrate them off of
/// it. /// it.
Future<void> upgradeChannel(FlutterVersion flutterVersion) async { Future<void> upgradeChannel(FlutterVersion flutterVersion) async {
globals.printStatus('Upgrading Flutter from ${Cache.flutterRoot}...'); globals.printStatus('Upgrading Flutter from $workingDirectory...');
await ChannelCommand.upgradeChannel(); await ChannelCommand.upgradeChannel();
} }
...@@ -237,7 +266,7 @@ class UpgradeCommandRunner { ...@@ -237,7 +266,7 @@ class UpgradeCommandRunner {
Future<bool> attemptFastForward(FlutterVersion oldFlutterVersion) async { Future<bool> attemptFastForward(FlutterVersion oldFlutterVersion) async {
final int code = await processUtils.stream( final int code = await processUtils.stream(
<String>['git', 'pull', '--ff'], <String>['git', 'pull', '--ff'],
workingDirectory: Cache.flutterRoot, workingDirectory: workingDirectory,
mapFunction: (String line) => matchesGitLine(line) ? null : line, mapFunction: (String line) => matchesGitLine(line) ? null : line,
); );
if (code != 0) { if (code != 0) {
...@@ -247,7 +276,7 @@ class UpgradeCommandRunner { ...@@ -247,7 +276,7 @@ class UpgradeCommandRunner {
// Check if the upgrade did anything. // Check if the upgrade did anything.
bool alreadyUpToDate = false; bool alreadyUpToDate = false;
try { try {
final FlutterVersion newFlutterVersion = FlutterVersion(); final FlutterVersion newFlutterVersion = FlutterVersion(const SystemClock(), workingDirectory);
alreadyUpToDate = newFlutterVersion.channel == oldFlutterVersion.channel && alreadyUpToDate = newFlutterVersion.channel == oldFlutterVersion.channel &&
newFlutterVersion.frameworkRevision == oldFlutterVersion.frameworkRevision; newFlutterVersion.frameworkRevision == oldFlutterVersion.frameworkRevision;
} catch (e) { } catch (e) {
...@@ -268,7 +297,7 @@ class UpgradeCommandRunner { ...@@ -268,7 +297,7 @@ class UpgradeCommandRunner {
<String>[ <String>[
globals.fs.path.join('bin', 'flutter'), '--no-color', '--no-version-check', 'precache', globals.fs.path.join('bin', 'flutter'), '--no-color', '--no-version-check', 'precache',
], ],
workingDirectory: Cache.flutterRoot, workingDirectory: workingDirectory,
allowReentrantFlutter: true, allowReentrantFlutter: true,
environment: Map<String, String>.of(globals.platform.environment), environment: Map<String, String>.of(globals.platform.environment),
); );
...@@ -296,7 +325,7 @@ class UpgradeCommandRunner { ...@@ -296,7 +325,7 @@ class UpgradeCommandRunner {
<String>[ <String>[
globals.fs.path.join('bin', 'flutter'), '--no-version-check', 'doctor', globals.fs.path.join('bin', 'flutter'), '--no-version-check', 'doctor',
], ],
workingDirectory: Cache.flutterRoot, workingDirectory: workingDirectory,
allowReentrantFlutter: true, allowReentrantFlutter: true,
); );
} }
......
...@@ -57,8 +57,29 @@ class VersionCommand extends FlutterCommand { ...@@ -57,8 +57,29 @@ class VersionCommand extends FlutterCommand {
final List<String> tags = await getTags(); final List<String> tags = await getTags();
if (argResults.rest.isEmpty) { if (argResults.rest.isEmpty) {
tags.forEach(globals.printStatus); tags.forEach(globals.printStatus);
return const FlutterCommandResult(ExitStatus.success); return FlutterCommandResult.success();
} }
globals.printStatus(
'╔══════════════════════════════════════════════════════════════════════════════╗\n'
'║ Warning: "flutter version" will leave the SDK in a detached HEAD state. ║\n'
'║ If you are using the command to return to a previously installed SDK version ║\n'
'║ consider using the "flutter downgrade" command instead. ║\n'
'╚══════════════════════════════════════════════════════════════════════════════╝\n',
emphasis: true,
);
if (globals.stdio.stdinHasTerminal) {
globals.terminal.usesTerminalUi = true;
final String result = await globals.terminal.promptForCharInput(
<String>['y', 'n'],
logger: globals.logger,
prompt: 'Are you sure you want to proceed?'
);
if (result == 'n') {
return FlutterCommandResult.success();
}
}
final String version = argResults.rest[0].replaceFirst('v', ''); final String version = argResults.rest[0].replaceFirst('v', '');
if (!tags.contains('v$version')) { if (!tags.contains('v$version')) {
globals.printError('There is no version: $version'); globals.printError('There is no version: $version');
......
...@@ -9,6 +9,7 @@ import 'base/config.dart'; ...@@ -9,6 +9,7 @@ import 'base/config.dart';
import 'base/context.dart'; import 'base/context.dart';
import 'base/file_system.dart'; import 'base/file_system.dart';
import 'base/logger.dart'; import 'base/logger.dart';
import 'version.dart';
/// A class that represents global (non-project-specific) internal state that /// A class that represents global (non-project-specific) internal state that
/// must persist across tool invocations. /// must persist across tool invocations.
...@@ -37,6 +38,14 @@ abstract class PersistentToolState { ...@@ -37,6 +38,14 @@ abstract class PersistentToolState {
/// ///
/// May give null if the value has not been set. /// May give null if the value has not been set.
bool redisplayWelcomeMessage; bool redisplayWelcomeMessage;
/// Returns the last active version for a given [channel].
///
/// If there was no active prior version, returns `null` instead.
String lastActiveVersion(Channel channel);
/// Update the last active version for a given [channel].
void updateLastActiveVersion(String fullGitHash, Channel channel);
} }
class _DefaultPersistentToolState implements PersistentToolState { class _DefaultPersistentToolState implements PersistentToolState {
...@@ -63,6 +72,12 @@ class _DefaultPersistentToolState implements PersistentToolState { ...@@ -63,6 +72,12 @@ class _DefaultPersistentToolState implements PersistentToolState {
static const String _kFileName = '.flutter_tool_state'; static const String _kFileName = '.flutter_tool_state';
static const String _kRedisplayWelcomeMessage = 'redisplay-welcome-message'; static const String _kRedisplayWelcomeMessage = 'redisplay-welcome-message';
static const Map<Channel, String> _lastActiveVersionKeys = <Channel,String>{
Channel.master: 'last-active-master-version',
Channel.dev: 'last-active-dev-version',
Channel.beta: 'last-active-beta-version',
Channel.stable: 'last-active-stable-version'
};
final Config _config; final Config _config;
...@@ -71,8 +86,26 @@ class _DefaultPersistentToolState implements PersistentToolState { ...@@ -71,8 +86,26 @@ class _DefaultPersistentToolState implements PersistentToolState {
return _config.getValue(_kRedisplayWelcomeMessage) as bool; return _config.getValue(_kRedisplayWelcomeMessage) as bool;
} }
@override
String lastActiveVersion(Channel channel) {
final String versionKey = _versionKeyFor(channel);
assert(versionKey != null);
return _config.getValue(versionKey) as String;
}
@override @override
set redisplayWelcomeMessage(bool value) { set redisplayWelcomeMessage(bool value) {
_config.setValue(_kRedisplayWelcomeMessage, value); _config.setValue(_kRedisplayWelcomeMessage, value);
} }
@override
void updateLastActiveVersion(String fullGitHash, Channel channel) {
final String versionKey = _versionKeyFor(channel);
assert(versionKey != null);
_config.setValue(versionKey, fullGitHash);
}
String _versionKeyFor(Channel channel) {
return _lastActiveVersionKeys[channel];
}
} }
...@@ -15,14 +15,45 @@ import 'cache.dart'; ...@@ -15,14 +15,45 @@ import 'cache.dart';
import 'convert.dart'; import 'convert.dart';
import 'globals.dart' as globals; import 'globals.dart' as globals;
/// The names of each channel/branch in order of increasing stability.
enum Channel {
master,
dev,
beta,
stable,
}
/// Retrieve a human-readable name for a given [channel].
///
/// Requires [FlutterVersion.officialChannels] to be correctly ordered.
String getNameForChannel(Channel channel) {
return FlutterVersion.officialChannels.elementAt(channel.index);
}
/// Retrieve the [Channel] representation for a string [name].
///
/// Returns `null` if [name] is not in the list of official channels, according
/// to [FlutterVersion.officialChannels].
Channel getChannelForName(String name) {
if (FlutterVersion.officialChannels.contains(name)) {
return Channel.values[FlutterVersion.officialChannels.toList().indexOf(name)];
}
return null;
}
class FlutterVersion { class FlutterVersion {
FlutterVersion([this._clock = const SystemClock()]) { FlutterVersion([this._clock = const SystemClock(), this._workingDirectory]) {
_frameworkRevision = _runGit(gitLog(<String>['-n', '1', '--pretty=format:%H']).join(' ')); _frameworkRevision = _runGit(
_gitTagVersion = GitTagVersion.determine(); gitLog(<String>['-n', '1', '--pretty=format:%H']).join(' '),
processUtils,
_workingDirectory,
);
_gitTagVersion = GitTagVersion.determine(processUtils, _workingDirectory);
_frameworkVersion = gitTagVersion.frameworkVersionFor(_frameworkRevision); _frameworkVersion = gitTagVersion.frameworkVersionFor(_frameworkRevision);
} }
final SystemClock _clock; final SystemClock _clock;
final String _workingDirectory;
String _repositoryUrl; String _repositoryUrl;
String get repositoryUrl { String get repositoryUrl {
...@@ -60,11 +91,19 @@ class FlutterVersion { ...@@ -60,11 +91,19 @@ class FlutterVersion {
/// `master`, `dev`, `beta`, `stable`; or old ones, like `alpha`, `hackathon`, ... /// `master`, `dev`, `beta`, `stable`; or old ones, like `alpha`, `hackathon`, ...
String get channel { String get channel {
if (_channel == null) { if (_channel == null) {
final String channel = _runGit('git rev-parse --abbrev-ref --symbolic @{u}'); final String channel = _runGit(
'git rev-parse --abbrev-ref --symbolic @{u}',
processUtils,
_workingDirectory,
);
final int slash = channel.indexOf('/'); final int slash = channel.indexOf('/');
if (slash != -1) { if (slash != -1) {
final String remote = channel.substring(0, slash); final String remote = channel.substring(0, slash);
_repositoryUrl = _runGit('git ls-remote --get-url $remote'); _repositoryUrl = _runGit(
'git ls-remote --get-url $remote',
processUtils,
_workingDirectory,
);
_channel = channel.substring(slash + 1); _channel = channel.substring(slash + 1);
} else if (channel.isEmpty) { } else if (channel.isEmpty) {
_channel = 'unknown'; _channel = 'unknown';
...@@ -88,7 +127,11 @@ class FlutterVersion { ...@@ -88,7 +127,11 @@ class FlutterVersion {
String _frameworkAge; String _frameworkAge;
String get frameworkAge { String get frameworkAge {
return _frameworkAge ??= _runGit(gitLog(<String>['-n', '1', '--pretty=format:%ar']).join(' ')); return _frameworkAge ??= _runGit(
gitLog(<String>['-n', '1', '--pretty=format:%ar']).join(' '),
processUtils,
_workingDirectory,
);
} }
String _frameworkVersion; String _frameworkVersion;
...@@ -226,7 +269,7 @@ class FlutterVersion { ...@@ -226,7 +269,7 @@ class FlutterVersion {
/// the branch name will be returned as `'[user-branch]'`. /// the branch name will be returned as `'[user-branch]'`.
String getBranchName({ bool redactUnknownBranches = false }) { String getBranchName({ bool redactUnknownBranches = false }) {
_branch ??= () { _branch ??= () {
final String branch = _runGit('git rev-parse --abbrev-ref HEAD'); final String branch = _runGit('git rev-parse --abbrev-ref HEAD', processUtils);
return branch == 'HEAD' ? channel : branch; return branch == 'HEAD' ? channel : branch;
}(); }();
if (redactUnknownBranches || _branch.isEmpty) { if (redactUnknownBranches || _branch.isEmpty) {
...@@ -599,10 +642,10 @@ String _runSync(List<String> command, { bool lenient = true }) { ...@@ -599,10 +642,10 @@ String _runSync(List<String> command, { bool lenient = true }) {
return ''; return '';
} }
String _runGit(String command) { String _runGit(String command, ProcessUtils processUtils, [String workingDirectory]) {
return processUtils.runSync( return processUtils.runSync(
command.split(' '), command.split(' '),
workingDirectory: Cache.flutterRoot, workingDirectory: workingDirectory ?? Cache.flutterRoot,
).stdout.trim(); ).stdout.trim();
} }
...@@ -658,8 +701,8 @@ class GitTagVersion { ...@@ -658,8 +701,8 @@ class GitTagVersion {
/// The git hash (or an abbreviation thereof) for this commit. /// The git hash (or an abbreviation thereof) for this commit.
final String hash; final String hash;
static GitTagVersion determine() { static GitTagVersion determine(ProcessUtils processUtils, [String workingDirectory]) {
return parse(_runGit('git describe --match v*.*.* --first-parent --long --tags')); return parse(_runGit('git describe --match v*.*.* --first-parent --long --tags', processUtils, workingDirectory));
} }
static GitTagVersion parse(String version) { static GitTagVersion parse(String version) {
......
// Copyright 2014 The Flutter 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 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/downgrade.dart';
import 'package:flutter_tools/src/persistent_tool_state.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:mockito/mockito.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/mocks.dart';
void main() {
FileSystem fileSystem;
BufferLogger bufferLogger;
AnsiTerminal terminal;
ProcessManager processManager;
MockStdio mockStdio;
FlutterVersion flutterVersion;
setUpAll(() {
Cache.disableLocking();
});
tearDownAll(() {
Cache.enableLocking();
});
setUp(() {
flutterVersion = MockFlutterVersion();
mockStdio = MockStdio();
processManager = FakeProcessManager.any();
terminal = MockTerminal();
fileSystem = MemoryFileSystem.test();
bufferLogger = BufferLogger(terminal: terminal, outputPreferences: OutputPreferences.test());
});
testUsingContext('Downgrade exits on unknown channel', () async {
fileSystem.currentDirectory.childFile('.flutter_tool_state')
.writeAsStringSync('{"last-active-master-version":"invalid"}');
final DowngradeCommand command = DowngradeCommand(
persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
processManager: processManager,
terminal: terminal,
stdio: mockStdio,
flutterVersion: flutterVersion,
logger: bufferLogger,
);
applyMocksToCommand(command);
expect(createTestCommandRunner(command).run(const <String>['downgrade']),
throwsToolExit(message: 'Flutter is not currently on a known channel.'));
});
testUsingContext('Downgrade exits on no recorded version', () async {
when(flutterVersion.channel).thenReturn('dev');
fileSystem.currentDirectory.childFile('.flutter_tool_state')
.writeAsStringSync('{"last-active-master-version":"abcd"}');
final DowngradeCommand command = DowngradeCommand(
persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
processManager: FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>[
'git', 'describe', '--tags', 'abcd'
],
exitCode: 0,
stdout: 'v1.2.3'
)
]),
terminal: terminal,
stdio: mockStdio,
flutterVersion: flutterVersion,
logger: bufferLogger,
);
applyMocksToCommand(command);
expect(createTestCommandRunner(command).run(const <String>['downgrade']),
throwsToolExit(message:
'There is no previously recorded version for channel "dev".\n'
'Channel "master" was previously on: v1.2.3.'
),
);
});
testUsingContext('Downgrade exits on unknown recorded version', () async {
when(flutterVersion.channel).thenReturn('master');
fileSystem.currentDirectory.childFile('.flutter_tool_state')
.writeAsStringSync('{"last-active-master-version":"invalid"}');
final DowngradeCommand command = DowngradeCommand(
persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
processManager: FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>[
'git', 'describe', '--tags', 'invalid'
],
exitCode: 1,
)
]),
terminal: terminal,
stdio: mockStdio,
flutterVersion: flutterVersion,
logger: bufferLogger,
);
applyMocksToCommand(command);
expect(createTestCommandRunner(command).run(const <String>['downgrade']),
throwsToolExit(message: 'Failed to parse version for downgrade'));
});
testUsingContext('Downgrade prompts for user input when terminal is attached - y', () async {
when(flutterVersion.channel).thenReturn('master');
when(mockStdio.hasTerminal).thenReturn(true);
fileSystem.currentDirectory.childFile('.flutter_tool_state')
.writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}');
final DowngradeCommand command = DowngradeCommand(
persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
processManager: processManager,
terminal: terminal,
stdio: mockStdio,
flutterVersion: flutterVersion,
logger: bufferLogger,
);
applyMocksToCommand(command);
when(terminal.promptForCharInput(
const <String>['y', 'n'],
prompt: anyNamed('prompt'),
logger: anyNamed('logger'),
)).thenAnswer((Invocation invocation) async => 'y');
await createTestCommandRunner(command).run(const <String>['downgrade']);
verify(terminal.promptForCharInput(
const <String>['y', 'n'],
prompt: anyNamed('prompt'),
logger: anyNamed('logger'),
)).called(1);
expect(bufferLogger.statusText, contains('Success'));
});
testUsingContext('Downgrade prompts for user input when terminal is attached - n', () async {
when(flutterVersion.channel).thenReturn('master');
when(mockStdio.hasTerminal).thenReturn(true);
fileSystem.currentDirectory.childFile('.flutter_tool_state')
.writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}');
final DowngradeCommand command = DowngradeCommand(
persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
processManager: processManager,
terminal: terminal,
stdio: mockStdio,
flutterVersion: flutterVersion,
logger: bufferLogger,
);
applyMocksToCommand(command);
when(terminal.promptForCharInput(
const <String>['y', 'n'],
prompt: anyNamed('prompt'),
logger: anyNamed('logger'),
)).thenAnswer((Invocation invocation) async => 'n');
await createTestCommandRunner(command).run(const <String>['downgrade']);
verify(terminal.promptForCharInput(
const <String>['y', 'n'],
prompt: anyNamed('prompt'),
logger: anyNamed('logger'),
)).called(1);
expect(bufferLogger.statusText, isNot(contains('Success')));
});
testUsingContext('Downgrade does not prompt when there is no terminal', () async {
when(flutterVersion.channel).thenReturn('master');
when(mockStdio.hasTerminal).thenReturn(false);
fileSystem.currentDirectory.childFile('.flutter_tool_state')
.writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}');
final DowngradeCommand command = DowngradeCommand(
persistentToolState: PersistentToolState.test(
directory: fileSystem.currentDirectory,
logger: bufferLogger,
),
processManager: processManager,
terminal: terminal,
stdio: mockStdio,
flutterVersion: flutterVersion,
logger: bufferLogger,
);
applyMocksToCommand(command);
await createTestCommandRunner(command).run(const <String>['downgrade']);
verifyNever(terminal.promptForCharInput(
const <String>['y', 'n'],
prompt: anyNamed('prompt'),
logger: anyNamed('logger'),
));
expect(bufferLogger.statusText, contains('Success'));
});
testUsingContext('Downgrade performs correct git commands', () async {
when(flutterVersion.channel).thenReturn('master');
when(mockStdio.hasTerminal).thenReturn(false);
fileSystem.currentDirectory.childFile('.flutter_tool_state')
.writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}');
final DowngradeCommand command = DowngradeCommand(
persistentToolState: PersistentToolState.test(
directory: fileSystem.currentDirectory,
logger: bufferLogger,
),
processManager: FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>[
'git', 'describe', '--tags', 'g6b00b5e88'
],
stdout: 'v1.2.3',
),
const FakeCommand(
command: <String>[
'git', 'reset', '--hard', 'g6b00b5e88'
],
),
const FakeCommand(
command: <String>[
'git', 'checkout', 'master', '--'
]
),
]),
terminal: terminal,
stdio: mockStdio,
flutterVersion: flutterVersion,
logger: bufferLogger,
);
applyMocksToCommand(command);
await createTestCommandRunner(command).run(const <String>['downgrade']);
expect(bufferLogger.statusText, contains('Success'));
});
}
class MockTerminal extends Mock implements AnsiTerminal {}
class MockStdio extends Mock implements Stdio {}
...@@ -8,11 +8,13 @@ import 'dart:io'; ...@@ -8,11 +8,13 @@ import 'dart:io';
import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/version.dart'; import 'package:flutter_tools/src/commands/version.dart';
import 'package:flutter_tools/src/version.dart'; import 'package:flutter_tools/src/version.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/context.dart'; import '../../src/context.dart';
...@@ -20,10 +22,18 @@ import '../../src/mocks.dart' show MockProcess; ...@@ -20,10 +22,18 @@ import '../../src/mocks.dart' show MockProcess;
void main() { void main() {
group('version', () { group('version', () {
MockStdio mockStdio;
setUpAll(() { setUpAll(() {
Cache.disableLocking(); Cache.disableLocking();
}); });
setUp(() {
mockStdio = MockStdio();
when(mockStdio.stdinHasTerminal).thenReturn(false);
when(mockStdio.hasTerminal).thenReturn(false);
});
testUsingContext('version ls', () async { testUsingContext('version ls', () async {
final VersionCommand command = VersionCommand(); final VersionCommand command = VersionCommand();
await createTestCommandRunner(command).run(<String>[ await createTestCommandRunner(command).run(<String>[
...@@ -33,11 +43,18 @@ void main() { ...@@ -33,11 +43,18 @@ void main() {
expect(testLogger.statusText, equals('v10.0.0\r\nv20.0.0\n')); expect(testLogger.statusText, equals('v10.0.0\r\nv20.0.0\n'));
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(), ProcessManager: () => MockProcessManager(),
Stdio: () => mockStdio,
}); });
testUsingContext('version switch', () async { testUsingContext('version switch prompt is accepted', () async {
when(mockStdio.stdinHasTerminal).thenReturn(true);
const String version = '10.0.0'; const String version = '10.0.0';
final VersionCommand command = VersionCommand(); final VersionCommand command = VersionCommand();
when(globals.terminal.promptForCharInput(<String>['y', 'n'],
logger: anyNamed('logger'),
prompt: 'Are you sure you want to proceed?')
).thenAnswer((Invocation invocation) async => 'y');
await createTestCommandRunner(command).run(<String>[ await createTestCommandRunner(command).run(<String>[
'version', 'version',
'--no-pub', '--no-pub',
...@@ -46,6 +63,29 @@ void main() { ...@@ -46,6 +63,29 @@ void main() {
expect(testLogger.statusText, contains('Switching Flutter to version $version')); expect(testLogger.statusText, contains('Switching Flutter to version $version'));
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(), ProcessManager: () => MockProcessManager(),
Stdio: () => mockStdio,
AnsiTerminal: () => MockTerminal(),
});
testUsingContext('version switch prompt is declined', () async {
when(mockStdio.stdinHasTerminal).thenReturn(true);
const String version = '10.0.0';
final VersionCommand command = VersionCommand();
when(globals.terminal.promptForCharInput(<String>['y', 'n'],
logger: anyNamed('logger'),
prompt: 'Are you sure you want to proceed?')
).thenAnswer((Invocation invocation) async => 'n');
await createTestCommandRunner(command).run(<String>[
'version',
'--no-pub',
version,
]);
expect(testLogger.statusText, isNot(contains('Switching Flutter to version $version')));
}, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(),
Stdio: () => mockStdio,
AnsiTerminal: () => MockTerminal(),
}); });
testUsingContext('version switch, latest commit query fails', () async { testUsingContext('version switch, latest commit query fails', () async {
...@@ -59,6 +99,7 @@ void main() { ...@@ -59,6 +99,7 @@ void main() {
expect(testLogger.errorText, contains('git failed')); expect(testLogger.errorText, contains('git failed'));
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(latestCommitFails: true), ProcessManager: () => MockProcessManager(latestCommitFails: true),
Stdio: () => mockStdio,
}); });
testUsingContext('latest commit is parsable when query fails', () { testUsingContext('latest commit is parsable when query fails', () {
...@@ -69,6 +110,7 @@ void main() { ...@@ -69,6 +110,7 @@ void main() {
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(latestCommitFails: true), ProcessManager: () => MockProcessManager(latestCommitFails: true),
Stdio: () => mockStdio,
}); });
testUsingContext('switch to not supported version without force', () async { testUsingContext('switch to not supported version without force', () async {
...@@ -82,6 +124,7 @@ void main() { ...@@ -82,6 +124,7 @@ void main() {
expect(testLogger.errorText, contains('Version command is not supported in')); expect(testLogger.errorText, contains('Version command is not supported in'));
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(), ProcessManager: () => MockProcessManager(),
Stdio: () => mockStdio,
}); });
testUsingContext('switch to not supported version with force', () async { testUsingContext('switch to not supported version with force', () async {
...@@ -96,6 +139,7 @@ void main() { ...@@ -96,6 +139,7 @@ void main() {
expect(testLogger.statusText, contains('Switching Flutter to version $version with force')); expect(testLogger.statusText, contains('Switching Flutter to version $version with force'));
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(), ProcessManager: () => MockProcessManager(),
Stdio: () => mockStdio,
}); });
testUsingContext('tool exit on confusing version', () async { testUsingContext('tool exit on confusing version', () async {
...@@ -111,6 +155,7 @@ void main() { ...@@ -111,6 +155,7 @@ void main() {
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(), ProcessManager: () => MockProcessManager(),
Stdio: () => mockStdio,
}); });
testUsingContext("exit tool if can't get the tags", () async { testUsingContext("exit tool if can't get the tags", () async {
...@@ -123,10 +168,13 @@ void main() { ...@@ -123,10 +168,13 @@ void main() {
} }
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(failGitTag: true), ProcessManager: () => MockProcessManager(failGitTag: true),
Stdio: () => mockStdio,
}); });
}); });
} }
class MockTerminal extends Mock implements AnsiTerminal {}
class MockStdio extends Mock implements Stdio {}
class MockProcessManager extends Mock implements ProcessManager { class MockProcessManager extends Mock implements ProcessManager {
MockProcessManager({ MockProcessManager({
this.failGitTag = false, this.failGitTag = false,
......
...@@ -57,10 +57,11 @@ void main() { ...@@ -57,10 +57,11 @@ void main() {
testUsingContext('throws on unknown tag, official branch, noforce', () async { testUsingContext('throws on unknown tag, official branch, noforce', () async {
final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand( final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
false, force: false,
false, continueFlow: false,
const GitTagVersion.unknown(), testFlow: false,
flutterVersion, gitTagVersion: const GitTagVersion.unknown(),
flutterVersion: flutterVersion,
); );
expect(result, throwsToolExit()); expect(result, throwsToolExit());
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -69,10 +70,11 @@ void main() { ...@@ -69,10 +70,11 @@ void main() {
testUsingContext('does not throw on unknown tag, official branch, force', () async { testUsingContext('does not throw on unknown tag, official branch, force', () async {
final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand( final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
true, force: true,
false, continueFlow: false,
const GitTagVersion.unknown(), testFlow: false,
flutterVersion, gitTagVersion: const GitTagVersion.unknown(),
flutterVersion: flutterVersion,
); );
expect(await result, FlutterCommandResult.success()); expect(await result, FlutterCommandResult.success());
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -83,10 +85,11 @@ void main() { ...@@ -83,10 +85,11 @@ void main() {
testUsingContext('throws tool exit with uncommitted changes', () async { testUsingContext('throws tool exit with uncommitted changes', () async {
fakeCommandRunner.willHaveUncomittedChanges = true; fakeCommandRunner.willHaveUncomittedChanges = true;
final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand( final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
false, force: false,
false, continueFlow: false,
gitTagVersion, testFlow: false,
flutterVersion, gitTagVersion: gitTagVersion,
flutterVersion: flutterVersion,
); );
expect(result, throwsToolExit()); expect(result, throwsToolExit());
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -97,10 +100,11 @@ void main() { ...@@ -97,10 +100,11 @@ void main() {
fakeCommandRunner.willHaveUncomittedChanges = true; fakeCommandRunner.willHaveUncomittedChanges = true;
final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand( final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
true, force: true,
false, continueFlow: false,
gitTagVersion, testFlow: false,
flutterVersion, gitTagVersion: gitTagVersion,
flutterVersion: flutterVersion,
); );
expect(await result, FlutterCommandResult.success()); expect(await result, FlutterCommandResult.success());
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -110,10 +114,11 @@ void main() { ...@@ -110,10 +114,11 @@ void main() {
testUsingContext("Doesn't throw on known tag, dev branch, no force", () async { testUsingContext("Doesn't throw on known tag, dev branch, no force", () async {
final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand( final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
false, force: false,
false, continueFlow: false,
gitTagVersion, testFlow: false,
flutterVersion, gitTagVersion: gitTagVersion,
flutterVersion: flutterVersion,
); );
expect(await result, FlutterCommandResult.success()); expect(await result, FlutterCommandResult.success());
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -124,10 +129,11 @@ void main() { ...@@ -124,10 +129,11 @@ void main() {
testUsingContext("Doesn't continue on known tag, dev branch, no force, already up-to-date", () async { testUsingContext("Doesn't continue on known tag, dev branch, no force, already up-to-date", () async {
fakeCommandRunner.alreadyUpToDate = true; fakeCommandRunner.alreadyUpToDate = true;
final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand( final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
false, force: false,
false, continueFlow: false,
gitTagVersion, testFlow: false,
flutterVersion, gitTagVersion: gitTagVersion,
flutterVersion: flutterVersion,
); );
expect(await result, FlutterCommandResult.success()); expect(await result, FlutterCommandResult.success());
verifyNever(globals.processManager.start( verifyNever(globals.processManager.start(
......
...@@ -6,6 +6,7 @@ import 'package:file/memory.dart'; ...@@ -6,6 +6,7 @@ import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/persistent_tool_state.dart'; import 'package:flutter_tools/src/persistent_tool_state.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import '../src/common.dart'; import '../src/common.dart';
...@@ -14,8 +15,8 @@ class MockLogger extends Mock implements Logger {} ...@@ -14,8 +15,8 @@ class MockLogger extends Mock implements Logger {}
void main() { void main() {
testWithoutContext('state can be set and persists', () { testWithoutContext('state can be set and persists', () {
final MemoryFileSystem fs = MemoryFileSystem(); final MemoryFileSystem fileSystem = MemoryFileSystem();
final Directory directory = fs.directory('state_dir'); final Directory directory = fileSystem.directory('state_dir');
directory.createSync(); directory.createSync();
final File stateFile = directory.childFile('.flutter_tool_state'); final File stateFile = directory.childFile('.flutter_tool_state');
final PersistentToolState state1 = PersistentToolState.test( final PersistentToolState state1 = PersistentToolState.test(
...@@ -35,4 +36,28 @@ void main() { ...@@ -35,4 +36,28 @@ void main() {
); );
expect(state2.redisplayWelcomeMessage, false); expect(state2.redisplayWelcomeMessage, false);
}); });
testWithoutContext('channel versions can be cached and stored', () {
final MemoryFileSystem fileSystem = MemoryFileSystem();
final Directory directory = fileSystem.directory('state_dir')..createSync();
final PersistentToolState state1 = PersistentToolState.test(
directory: directory,
logger: MockLogger(),
);
state1.updateLastActiveVersion('abc', Channel.master);
state1.updateLastActiveVersion('def', Channel.dev);
state1.updateLastActiveVersion('ghi', Channel.beta);
state1.updateLastActiveVersion('jkl', Channel.stable);
final PersistentToolState state2 = PersistentToolState.test(
directory: directory,
logger: MockLogger(),
);
expect(state2.lastActiveVersion(Channel.master), 'abc');
expect(state2.lastActiveVersion(Channel.dev), 'def');
expect(state2.lastActiveVersion(Channel.beta), 'ghi');
expect(state2.lastActiveVersion(Channel.stable), 'jkl');
});
} }
// Copyright 2014 The Flutter 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 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import '../src/common.dart';
const String _kInitialVersion = 'v1.9.1+hotfix.6';
const String _kBranch = 'stable';
const FileSystem fileSystem = LocalFileSystem();
const ProcessManager processManager = LocalProcessManager();
final ProcessUtils processUtils = ProcessUtils(processManager: processManager, logger: StdoutLogger(
terminal: AnsiTerminal(
platform: const LocalPlatform(),
stdio: const Stdio(),
),
stdio: const Stdio(),
outputPreferences: OutputPreferences.test(wrapText: true),
timeoutConfiguration: const TimeoutConfiguration(),
));
final String flutterBin = fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter');
/// A test for flutter upgrade & downgrade that checks out a parallel flutter repo.
void main() {
Directory parentDirectory;
setUp(() {
parentDirectory = fileSystem.systemTempDirectory
.createTempSync('flutter_tools.');
parentDirectory.createSync(recursive: true);
});
tearDown(() {
try {
parentDirectory.deleteSync(recursive: true);
} on FileSystemException {
print('Failed to delete test directory');
}
});
test('Can upgrade and downgrade a Flutter checkout', () async {
final Directory testDirectory = parentDirectory.childDirectory('flutter');
testDirectory.createSync(recursive: true);
// Enable longpaths for windows integration test.
await processManager.run(<String>[
'git', 'config', '--system', 'core.longpaths', 'true',
]);
// Step 1. Clone the dev branch of flutter into the test directory.
await processUtils.stream(<String>[
'git',
'clone',
'https://github.com/flutter/flutter.git',
], workingDirectory: parentDirectory.path, trace: true);
// Step 2. Switch to the dev branch.
await processUtils.stream(<String>[
'git',
'checkout',
'--track',
'-b',
_kBranch,
'origin/$_kBranch',
], workingDirectory: testDirectory.path, trace: true);
// Step 3. Revert to a prior version.
await processUtils.stream(<String>[
'git',
'reset',
'--hard',
_kInitialVersion,
], workingDirectory: testDirectory.path, trace: true);
// Step 4. Upgrade to the newest dev. This should update the persistent
// tool state with the sha for v1.14.3
await processUtils.stream(<String>[
flutterBin,
'upgrade',
'--working-directory=${testDirectory.path}'
], workingDirectory: testDirectory.path, trace: true);
// Step 5. Verify that the version is different.
final RunResult versionResult = await processUtils.run(<String>[
'git',
'describe',
'--match',
'v*.*.*',
'--first-parent',
'--long',
'--tags',
], workingDirectory: testDirectory.path);
expect(versionResult.stdout, isNot(contains(_kInitialVersion)));
// Step 6. Downgrade back to initial version.
await processUtils.stream(<String>[
flutterBin,
'downgrade',
'--no-prompt',
'--working-directory=${testDirectory.path}'
], workingDirectory: testDirectory.path, trace: true);
// Step 7. Verify downgraded version matches original version.
final RunResult oldVersionResult = await processUtils.run(<String>[
'git',
'describe',
'--match',
'v*.*.*',
'--first-parent',
'--long',
'--tags',
], workingDirectory: testDirectory.path);
expect(oldVersionResult.stdout, contains(_kInitialVersion));
});
}
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