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(
diff --git a/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart b/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart index 59dead0..1f4bfcf 100644 --- a/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart +++ b/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart
@@ -6,6 +6,7 @@ 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 @@ 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 @@ ); 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'); + }); }
diff --git a/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart b/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart new file mode 100644 index 0000000..6c1bc2c --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart
@@ -0,0 +1,122 @@ +// 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)); + }); +}