// 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:test/fake.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/fakes.dart'; import '../../src/test_flutter_command_runner.dart'; void main() { late FileSystem fileSystem; late BufferLogger bufferLogger; late FakeTerminal terminal; late ProcessManager processManager; late FakeStdio stdio; setUpAll(() { Cache.disableLocking(); }); tearDownAll(() { Cache.enableLocking(); }); setUp(() { stdio = FakeStdio(); processManager = FakeProcessManager.any(); terminal = FakeTerminal(); fileSystem = MemoryFileSystem.test(); bufferLogger = BufferLogger.test(terminal: terminal); }); testUsingContext('Downgrade exits on unknown channel', () async { final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(); 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: stdio, flutterVersion: fakeFlutterVersion, logger: bufferLogger, ); 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 { final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(channel: 'beta'); 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', ], stdout: 'v1.2.3', ), ]), terminal: terminal, stdio: stdio, flutterVersion: fakeFlutterVersion, logger: bufferLogger, ); expect(createTestCommandRunner(command).run(const <String>['downgrade']), throwsToolExit(message: 'There is no previously recorded version for channel "beta".\n' 'Channel "master" was previously on: v1.2.3.' ), ); }); testUsingContext('Downgrade exits on unknown recorded version', () async { final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(channel: '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: stdio, flutterVersion: fakeFlutterVersion, logger: bufferLogger, ); 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 { final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(channel: 'master'); stdio.hasTerminal = 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: stdio, flutterVersion: fakeFlutterVersion, logger: bufferLogger, ); terminal.addPrompt(const <String>['y', 'n'], 'y'); await createTestCommandRunner(command).run(const <String>['downgrade']); expect(bufferLogger.statusText, contains('Success')); }); testUsingContext('Downgrade prompts for user input when terminal is attached - n', () async { final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(channel: 'master'); stdio.hasTerminal = 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: stdio, flutterVersion: fakeFlutterVersion, logger: bufferLogger, ); terminal.addPrompt(const <String>['y', 'n'], 'n'); await createTestCommandRunner(command).run(const <String>['downgrade']); expect(bufferLogger.statusText, isNot(contains('Success'))); }); testUsingContext('Downgrade does not prompt when there is no terminal', () async { final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(channel: 'master'); stdio.hasTerminal = 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: stdio, flutterVersion: fakeFlutterVersion, logger: bufferLogger, ); await createTestCommandRunner(command).run(const <String>['downgrade']); expect(bufferLogger.statusText, contains('Success')); }); testUsingContext('Downgrade performs correct git commands', () async { final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(channel: 'master'); stdio.hasTerminal = 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: stdio, flutterVersion: fakeFlutterVersion, logger: bufferLogger, ); await createTestCommandRunner(command).run(const <String>['downgrade']); expect(bufferLogger.statusText, contains('Success')); }); } class FakeTerminal extends Fake implements Terminal { @override bool usesTerminalUi = false; void addPrompt(List<String> characters, String selected) { _characters = characters; _selected = selected; } List<String>? _characters; late String _selected; @override Future<String> promptForCharInput(List<String> acceptedCharacters, {Logger? logger, String? prompt, int? defaultChoiceIndex, bool displayAcceptedCharacters = true}) async { expect(acceptedCharacters, _characters); return _selected; } } class FakeStdio extends Fake implements Stdio { @override bool hasTerminal = true; }