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';
import 'src/commands/daemon.dart';
import 'src/commands/devices.dart';
import 'src/commands/doctor.dart';
import 'src/commands/downgrade.dart';
import 'src/commands/drive.dart';
import 'src/commands/emulators.dart';
import 'src/commands/format.dart';
......@@ -76,6 +77,7 @@ Future<void> main(List<String> args) async {
DaemonCommand(hidden: !verboseHelp),
DevicesCommand(),
DoctorCommand(verbose: verbose),
DowngradeCommand(),
DriveCommand(),
EmulatorsCommand(),
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';
import '../base/io.dart';
import '../base/os.dart';
import '../base/process.dart';
import '../base/time.dart';
import '../cache.dart';
import '../dart/pub.dart';
import '../globals.dart' as globals;
......@@ -34,6 +35,11 @@ class UpgradeCommand extends FlutterCommand {
help: 'For the second half of the upgrade flow requiring the new '
'version of Flutter. Should not be invoked manually, but '
'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 {
@override
Future<FlutterCommandResult> runCommand() {
_commandRunner.workingDirectory = stringArg('working-directory') ?? Cache.flutterRoot;
return _commandRunner.runCommand(
boolArg('force'),
boolArg('continue'),
GitTagVersion.determine(),
globals.flutterVersion,
force: boolArg('force'),
continueFlow: boolArg('continue'),
testFlow: stringArg('working-directory') != null,
gitTagVersion: GitTagVersion.determine(processUtils),
flutterVersion: stringArg('working-directory') == null
? globals.flutterVersion
: FlutterVersion(const SystemClock(), _commandRunner.workingDirectory),
);
}
}
@visibleForTesting
class UpgradeCommandRunner {
Future<FlutterCommandResult> runCommand(
bool force,
bool continueFlow,
GitTagVersion gitTagVersion,
FlutterVersion flutterVersion,
) async {
String workingDirectory;
Future<FlutterCommandResult> runCommand({
@required bool force,
@required bool continueFlow,
@required bool testFlow,
@required GitTagVersion gitTagVersion,
@required FlutterVersion flutterVersion,
}) async {
if (!continueFlow) {
await runCommandFirstHalf(force, gitTagVersion, flutterVersion);
await runCommandFirstHalf(
force: force,
gitTagVersion: gitTagVersion,
flutterVersion: flutterVersion,
testFlow: testFlow,
);
} else {
await runCommandSecondHalf(flutterVersion);
}
return FlutterCommandResult.success();
}
Future<void> runCommandFirstHalf(
bool force,
GitTagVersion gitTagVersion,
FlutterVersion flutterVersion,
) async {
Future<void> runCommandFirstHalf({
@required bool force,
@required GitTagVersion gitTagVersion,
@required FlutterVersion flutterVersion,
@required bool testFlow,
}) async {
await verifyUpstreamConfigured();
if (!force && gitTagVersion == const GitTagVersion.unknown()) {
// If the commit is a recognized branch and not master,
......@@ -110,6 +130,7 @@ class UpgradeCommandRunner {
'command with --force.'
);
}
recordState(flutterVersion);
await resetChanges(gitTagVersion);
await upgradeChannel(flutterVersion);
final bool alreadyUpToDate = await attemptFastForward(flutterVersion);
......@@ -117,11 +138,19 @@ class UpgradeCommandRunner {
// 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('$flutterVersion');
} else {
} else if (!testFlow) {
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 {
final int code = await processUtils.stream(
<String>[
......@@ -130,7 +159,7 @@ class UpgradeCommandRunner {
'--continue',
'--no-version-check',
],
workingDirectory: Cache.flutterRoot,
workingDirectory: workingDirectory,
allowReentrantFlutter: true,
environment: Map<String, String>.of(globals.platform.environment),
);
......@@ -156,7 +185,7 @@ class UpgradeCommandRunner {
final RunResult result = await processUtils.run(
<String>['git', 'status', '-s'],
throwOnError: true,
workingDirectory: Cache.flutterRoot,
workingDirectory: workingDirectory,
);
return result.stdout.trim().isNotEmpty;
} on ProcessException catch (error) {
......@@ -179,13 +208,13 @@ class UpgradeCommandRunner {
await processUtils.run(
<String>[ 'git', 'rev-parse', '@{u}'],
throwOnError: true,
workingDirectory: Cache.flutterRoot,
workingDirectory: workingDirectory,
);
} catch (e) {
throwToolExit(
'Unable to upgrade Flutter: no origin repository configured. '
"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 {
await processUtils.run(
<String>['git', 'reset', '--hard', tag],
throwOnError: true,
workingDirectory: Cache.flutterRoot,
workingDirectory: workingDirectory,
);
} on ProcessException catch (error) {
throwToolExit(
......@@ -223,7 +252,7 @@ class UpgradeCommandRunner {
/// If the user is on a deprecated channel, attempts to migrate them off of
/// it.
Future<void> upgradeChannel(FlutterVersion flutterVersion) async {
globals.printStatus('Upgrading Flutter from ${Cache.flutterRoot}...');
globals.printStatus('Upgrading Flutter from $workingDirectory...');
await ChannelCommand.upgradeChannel();
}
......@@ -237,7 +266,7 @@ class UpgradeCommandRunner {
Future<bool> attemptFastForward(FlutterVersion oldFlutterVersion) async {
final int code = await processUtils.stream(
<String>['git', 'pull', '--ff'],
workingDirectory: Cache.flutterRoot,
workingDirectory: workingDirectory,
mapFunction: (String line) => matchesGitLine(line) ? null : line,
);
if (code != 0) {
......@@ -247,7 +276,7 @@ class UpgradeCommandRunner {
// Check if the upgrade did anything.
bool alreadyUpToDate = false;
try {
final FlutterVersion newFlutterVersion = FlutterVersion();
final FlutterVersion newFlutterVersion = FlutterVersion(const SystemClock(), workingDirectory);
alreadyUpToDate = newFlutterVersion.channel == oldFlutterVersion.channel &&
newFlutterVersion.frameworkRevision == oldFlutterVersion.frameworkRevision;
} catch (e) {
......@@ -268,7 +297,7 @@ class UpgradeCommandRunner {
<String>[
globals.fs.path.join('bin', 'flutter'), '--no-color', '--no-version-check', 'precache',
],
workingDirectory: Cache.flutterRoot,
workingDirectory: workingDirectory,
allowReentrantFlutter: true,
environment: Map<String, String>.of(globals.platform.environment),
);
......@@ -296,7 +325,7 @@ class UpgradeCommandRunner {
<String>[
globals.fs.path.join('bin', 'flutter'), '--no-version-check', 'doctor',
],
workingDirectory: Cache.flutterRoot,
workingDirectory: workingDirectory,
allowReentrantFlutter: true,
);
}
......
......@@ -57,8 +57,29 @@ class VersionCommand extends FlutterCommand {
final List<String> tags = await getTags();
if (argResults.rest.isEmpty) {
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', '');
if (!tags.contains('v$version')) {
globals.printError('There is no version: $version');
......
......@@ -9,6 +9,7 @@ import 'base/config.dart';
import 'base/context.dart';
import 'base/file_system.dart';
import 'base/logger.dart';
import 'version.dart';
/// A class that represents global (non-project-specific) internal state that
/// must persist across tool invocations.
......@@ -37,6 +38,14 @@ abstract class PersistentToolState {
///
/// May give null if the value has not been set.
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 {
......@@ -63,6 +72,12 @@ class _DefaultPersistentToolState implements PersistentToolState {
static const String _kFileName = '.flutter_tool_state';
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;
......@@ -71,8 +86,26 @@ class _DefaultPersistentToolState implements PersistentToolState {
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
set redisplayWelcomeMessage(bool 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';
import 'convert.dart';
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 {
FlutterVersion([this._clock = const SystemClock()]) {
_frameworkRevision = _runGit(gitLog(<String>['-n', '1', '--pretty=format:%H']).join(' '));
_gitTagVersion = GitTagVersion.determine();
FlutterVersion([this._clock = const SystemClock(), this._workingDirectory]) {
_frameworkRevision = _runGit(
gitLog(<String>['-n', '1', '--pretty=format:%H']).join(' '),
processUtils,
_workingDirectory,
);
_gitTagVersion = GitTagVersion.determine(processUtils, _workingDirectory);
_frameworkVersion = gitTagVersion.frameworkVersionFor(_frameworkRevision);
}
final SystemClock _clock;
final String _workingDirectory;
String _repositoryUrl;
String get repositoryUrl {
......@@ -60,11 +91,19 @@ class FlutterVersion {
/// `master`, `dev`, `beta`, `stable`; or old ones, like `alpha`, `hackathon`, ...
String get channel {
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('/');
if (slash != -1) {
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);
} else if (channel.isEmpty) {
_channel = 'unknown';
......@@ -88,7 +127,11 @@ class FlutterVersion {
String _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;
......@@ -226,7 +269,7 @@ class FlutterVersion {
/// the branch name will be returned as `'[user-branch]'`.
String getBranchName({ bool redactUnknownBranches = false }) {
_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;
}();
if (redactUnknownBranches || _branch.isEmpty) {
......@@ -599,10 +642,10 @@ String _runSync(List<String> command, { bool lenient = true }) {
return '';
}
String _runGit(String command) {
String _runGit(String command, ProcessUtils processUtils, [String workingDirectory]) {
return processUtils.runSync(
command.split(' '),
workingDirectory: Cache.flutterRoot,
workingDirectory: workingDirectory ?? Cache.flutterRoot,
).stdout.trim();
}
......@@ -658,8 +701,8 @@ class GitTagVersion {
/// The git hash (or an abbreviation thereof) for this commit.
final String hash;
static GitTagVersion determine() {
return parse(_runGit('git describe --match v*.*.* --first-parent --long --tags'));
static GitTagVersion determine(ProcessUtils processUtils, [String workingDirectory]) {
return parse(_runGit('git describe --match v*.*.* --first-parent --long --tags', processUtils, workingDirectory));
}
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';
import 'package:flutter_tools/src/base/common.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/commands/version.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import '../../src/common.dart';
import '../../src/context.dart';
......@@ -20,10 +22,18 @@ import '../../src/mocks.dart' show MockProcess;
void main() {
group('version', () {
MockStdio mockStdio;
setUpAll(() {
Cache.disableLocking();
});
setUp(() {
mockStdio = MockStdio();
when(mockStdio.stdinHasTerminal).thenReturn(false);
when(mockStdio.hasTerminal).thenReturn(false);
});
testUsingContext('version ls', () async {
final VersionCommand command = VersionCommand();
await createTestCommandRunner(command).run(<String>[
......@@ -33,11 +43,18 @@ void main() {
expect(testLogger.statusText, equals('v10.0.0\r\nv20.0.0\n'));
}, overrides: <Type, Generator>{
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';
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>[
'version',
'--no-pub',
......@@ -46,6 +63,29 @@ void main() {
expect(testLogger.statusText, contains('Switching Flutter to version $version'));
}, overrides: <Type, Generator>{
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 {
......@@ -59,6 +99,7 @@ void main() {
expect(testLogger.errorText, contains('git failed'));
}, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(latestCommitFails: true),
Stdio: () => mockStdio,
});
testUsingContext('latest commit is parsable when query fails', () {
......@@ -69,6 +110,7 @@ void main() {
);
}, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(latestCommitFails: true),
Stdio: () => mockStdio,
});
testUsingContext('switch to not supported version without force', () async {
......@@ -82,6 +124,7 @@ void main() {
expect(testLogger.errorText, contains('Version command is not supported in'));
}, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(),
Stdio: () => mockStdio,
});
testUsingContext('switch to not supported version with force', () async {
......@@ -96,6 +139,7 @@ void main() {
expect(testLogger.statusText, contains('Switching Flutter to version $version with force'));
}, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(),
Stdio: () => mockStdio,
});
testUsingContext('tool exit on confusing version', () async {
......@@ -111,6 +155,7 @@ void main() {
);
}, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(),
Stdio: () => mockStdio,
});
testUsingContext("exit tool if can't get the tags", () async {
......@@ -123,10 +168,13 @@ void main() {
}
}, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(failGitTag: true),
Stdio: () => mockStdio,
});
});
}
class MockTerminal extends Mock implements AnsiTerminal {}
class MockStdio extends Mock implements Stdio {}
class MockProcessManager extends Mock implements ProcessManager {
MockProcessManager({
this.failGitTag = false,
......
......@@ -57,10 +57,11 @@ void main() {
testUsingContext('throws on unknown tag, official branch, noforce', () async {
final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
false,
false,
const GitTagVersion.unknown(),
flutterVersion,
force: false,
continueFlow: false,
testFlow: false,
gitTagVersion: const GitTagVersion.unknown(),
flutterVersion: flutterVersion,
);
expect(result, throwsToolExit());
}, overrides: <Type, Generator>{
......@@ -69,10 +70,11 @@ void main() {
testUsingContext('does not throw on unknown tag, official branch, force', () async {
final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
true,
false,
const GitTagVersion.unknown(),
flutterVersion,
force: true,
continueFlow: false,
testFlow: false,
gitTagVersion: const GitTagVersion.unknown(),
flutterVersion: flutterVersion,
);
expect(await result, FlutterCommandResult.success());
}, overrides: <Type, Generator>{
......@@ -83,10 +85,11 @@ void main() {
testUsingContext('throws tool exit with uncommitted changes', () async {
fakeCommandRunner.willHaveUncomittedChanges = true;
final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
false,
false,
gitTagVersion,
flutterVersion,
force: false,
continueFlow: false,
testFlow: false,
gitTagVersion: gitTagVersion,
flutterVersion: flutterVersion,
);
expect(result, throwsToolExit());
}, overrides: <Type, Generator>{
......@@ -97,10 +100,11 @@ void main() {
fakeCommandRunner.willHaveUncomittedChanges = true;
final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
true,
false,
gitTagVersion,
flutterVersion,
force: true,
continueFlow: false,
testFlow: false,
gitTagVersion: gitTagVersion,
flutterVersion: flutterVersion,
);
expect(await result, FlutterCommandResult.success());
}, overrides: <Type, Generator>{
......@@ -110,10 +114,11 @@ void main() {
testUsingContext("Doesn't throw on known tag, dev branch, no force", () async {
final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
false,
false,
gitTagVersion,
flutterVersion,
force: false,
continueFlow: false,
testFlow: false,
gitTagVersion: gitTagVersion,
flutterVersion: flutterVersion,
);
expect(await result, FlutterCommandResult.success());
}, overrides: <Type, Generator>{
......@@ -124,10 +129,11 @@ void main() {
testUsingContext("Doesn't continue on known tag, dev branch, no force, already up-to-date", () async {
fakeCommandRunner.alreadyUpToDate = true;
final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
false,
false,
gitTagVersion,
flutterVersion,
force: false,
continueFlow: false,
testFlow: false,
gitTagVersion: gitTagVersion,
flutterVersion: flutterVersion,
);
expect(await result, FlutterCommandResult.success());
verifyNever(globals.processManager.start(
......
......@@ -6,6 +6,7 @@ import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.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';
......@@ -14,8 +15,8 @@ class MockLogger extends Mock implements Logger {}
void main() {
testWithoutContext('state can be set and persists', () {
final MemoryFileSystem fs = MemoryFileSystem();
final Directory directory = fs.directory('state_dir');
final MemoryFileSystem fileSystem = MemoryFileSystem();
final Directory directory = fileSystem.directory('state_dir');
directory.createSync();
final File stateFile = directory.childFile('.flutter_tool_state');
final PersistentToolState state1 = PersistentToolState.test(
......@@ -35,4 +36,28 @@ void main() {
);
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