Add "flutter downgrade" command (#50506)
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/downgrade_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/downgrade_test.dart
new file mode 100644
index 0000000..db627c2
--- /dev/null
+++ b/packages/flutter_tools/test/commands.shard/hermetic/downgrade_test.dart
@@ -0,0 +1,249 @@
+// 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 {}
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/version_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/version_test.dart
index 98922e0..74596c3 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/version_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/version_test.dart
@@ -8,11 +8,13 @@
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 @@
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 @@
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 @@
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 @@
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 @@
);
}, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(latestCommitFails: true),
+ Stdio: () => mockStdio,
});
testUsingContext('switch to not supported version without force', () async {
@@ -82,6 +124,7 @@
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 @@
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 @@
);
}, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(),
+ Stdio: () => mockStdio,
});
testUsingContext("exit tool if can't get the tags", () async {
@@ -123,10 +168,13 @@
}
}, 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,
diff --git a/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart b/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart
index 1d71094..f5ac6fb 100644
--- a/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart
+++ b/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart
@@ -57,10 +57,11 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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(