diff --git a/lib/src/command_runner/commands/commands.dart b/lib/src/command_runner/commands/commands.dart index cc3c9eb..69ceb3f 100644 --- a/lib/src/command_runner/commands/commands.dart +++ b/lib/src/command_runner/commands/commands.dart @@ -1,2 +1,3 @@ export 'handle_completion_command.dart'; export 'install_completion_files_command.dart'; +export 'uninstall_completion_files_command.dart'; diff --git a/lib/src/command_runner/commands/uninstall_completion_files_command.dart b/lib/src/command_runner/commands/uninstall_completion_files_command.dart new file mode 100644 index 0000000..0ecc312 --- /dev/null +++ b/lib/src/command_runner/commands/uninstall_completion_files_command.dart @@ -0,0 +1,52 @@ +import 'dart:async'; + +import 'package:args/command_runner.dart'; +import 'package:cli_completion/cli_completion.dart'; +import 'package:mason_logger/mason_logger.dart'; + +/// {@template ninstall_completion_command} +/// A hidden [Command] added by [CompletionCommandRunner] that handles the +/// "uninstall-completion-files" sub command. +/// +/// It can be used to manually uninstall the completion files +/// (those installed by [CompletionCommandRunner] or +/// [InstallCompletionFilesCommand]). +/// {@endtemplate} +class UnistallCompletionFilesCommand extends Command { + /// {@macro uninstall_completion_command} + UnistallCompletionFilesCommand() { + argParser.addFlag( + 'verbose', + abbr: 'v', + help: 'Verbose output', + negatable: false, + ); + } + + @override + String get description { + return 'Manually uninstalls completion files for the current shell.'; + } + + /// The string that the user can call to manually uninstall completion files. + static const commandName = 'uninstall-completion-files'; + + @override + String get name => commandName; + + @override + bool get hidden => true; + + @override + CompletionCommandRunner get runner { + return super.runner! as CompletionCommandRunner; + } + + @override + FutureOr? run() { + final verbose = argResults!['verbose'] as bool; + final level = verbose ? Level.verbose : Level.info; + runner.tryUninstallCompletionFiles(level); + return null; + } +} diff --git a/lib/src/command_runner/completion_command_runner.dart b/lib/src/command_runner/completion_command_runner.dart index cba7970..cd7a860 100644 --- a/lib/src/command_runner/completion_command_runner.dart +++ b/lib/src/command_runner/completion_command_runner.dart @@ -25,6 +25,7 @@ abstract class CompletionCommandRunner extends CommandRunner { CompletionCommandRunner(super.executableName, super.description) { addCommand(HandleCompletionRequestCommand()); addCommand(InstallCompletionFilesCommand()); + addCommand(UnistallCompletionFilesCommand()); } /// The [Logger] used to prompt the completion suggestions. @@ -94,6 +95,19 @@ abstract class CompletionCommandRunner extends CommandRunner { } } + /// Tries to uninstall completion files for the current shell. + @internal + void tryUninstallCompletionFiles(Level level) { + try { + completionInstallationLogger.level = level; + completionInstallation.uninstall(executableName); + } on CompletionUninstallationException catch (e) { + completionInstallationLogger.warn(e.toString()); + } on Exception catch (e) { + completionInstallationLogger.err(e.toString()); + } + } + /// Renders a [CompletionResult] into the current system shell. /// /// This is called after a completion request (sent by a shell function) is diff --git a/lib/src/installer/completion_installation.dart b/lib/src/installer/completion_installation.dart index 7552ddc..9904280 100644 --- a/lib/src/installer/completion_installation.dart +++ b/lib/src/installer/completion_installation.dart @@ -315,7 +315,7 @@ ${configuration!.sourceLineTemplate(scriptPath)}'''; /// [ScriptConfigurationEntry] for the [rootCommand]. /// /// If any of the above is not true, it throws a - /// [CompletionUnistallationException]. + /// [CompletionUninstallationException]. /// /// Upon a successful uninstallation the executable [ScriptConfigurationEntry] /// is removed from the shell configuration file. If after this removal the @@ -332,16 +332,16 @@ ${configuration!.sourceLineTemplate(scriptPath)}'''; final shellRCFile = File(_shellRCFilePath); if (!shellRCFile.existsSync()) { - throw CompletionUnistallationException( - executableName: rootCommand, + throw CompletionUninstallationException( + rootCommand: rootCommand, message: 'No shell RC file found at ${shellRCFile.path}', ); } const completionEntry = ScriptConfigurationEntry('Completion'); if (!completionEntry.existsIn(shellRCFile)) { - throw CompletionUnistallationException( - executableName: rootCommand, + throw CompletionUninstallationException( + rootCommand: rootCommand, message: 'Completion is not installed at ${shellRCFile.path}', ); } @@ -354,8 +354,8 @@ ${configuration!.sourceLineTemplate(scriptPath)}'''; ); final executableEntry = ScriptConfigurationEntry(rootCommand); if (!executableEntry.existsIn(shellCompletionConfigurationFile)) { - throw CompletionUnistallationException( - executableName: rootCommand, + throw CompletionUninstallationException( + rootCommand: rootCommand, message: '''No shell script file found at ${shellCompletionConfigurationFile.path}''', ); diff --git a/lib/src/installer/exceptions.dart b/lib/src/installer/exceptions.dart index 6c481b6..c29b82e 100644 --- a/lib/src/installer/exceptions.dart +++ b/lib/src/installer/exceptions.dart @@ -22,20 +22,20 @@ class CompletionInstallationException implements Exception { /// {@template completion_unistallation_exception} /// Describes an exception during the uninstallation of completion scripts. /// {@endtemplate} -class CompletionUnistallationException implements Exception { +class CompletionUninstallationException implements Exception { /// {@macro completion_unistallation_exception} - CompletionUnistallationException({ + CompletionUninstallationException({ required this.message, - required this.executableName, + required this.rootCommand, }); /// The error message for this exception final String message; /// The command for which the installation failed. - final String executableName; + final String rootCommand; @override String toString() => - '''Could not uninstall completion scripts for $executableName: $message'''; + '''Could not uninstall completion scripts for $rootCommand: $message'''; } diff --git a/test/src/command_runner/commands/uninstall_completion_files_command_test.dart b/test/src/command_runner/commands/uninstall_completion_files_command_test.dart new file mode 100644 index 0000000..f1309f9 --- /dev/null +++ b/test/src/command_runner/commands/uninstall_completion_files_command_test.dart @@ -0,0 +1,76 @@ +import 'package:cli_completion/cli_completion.dart'; +import 'package:cli_completion/installer.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class _MockLogger extends Mock implements Logger {} + +class _MockCompletionInstallation extends Mock + implements CompletionInstallation {} + +class _TestCompletionCommandRunner extends CompletionCommandRunner { + _TestCompletionCommandRunner() : super('test', 'Test command runner'); + + @override + // ignore: overridden_fields + final Logger completionInstallationLogger = _MockLogger(); + + @override + final CompletionInstallation completionInstallation = + _MockCompletionInstallation(); +} + +void main() { + group('$UnistallCompletionFilesCommand', () { + late _TestCompletionCommandRunner commandRunner; + + setUp(() { + commandRunner = _TestCompletionCommandRunner(); + }); + + test('can be instantiated', () { + expect(UnistallCompletionFilesCommand(), isNotNull); + }); + + test('is hidden', () { + expect(UnistallCompletionFilesCommand().hidden, isTrue); + }); + + test('description', () { + expect( + UnistallCompletionFilesCommand().description, + 'Manually uninstalls completion files for the current shell.', + ); + }); + + group('uninstalls completion files', () { + test('when normal', () async { + await commandRunner.run(['uninstall-completion-files']); + + verify( + () => commandRunner.completionInstallationLogger.level = Level.info, + ).called(1); + verify( + () => commandRunner.completionInstallation + .uninstall(commandRunner.executableName), + ).called(1); + }); + + test('when verbose', () async { + await commandRunner.run(['uninstall-completion-files', '--verbose']); + + verify( + () { + return commandRunner.completionInstallationLogger.level = + Level.verbose; + }, + ).called(1); + verify( + () => commandRunner.completionInstallation + .uninstall(commandRunner.executableName), + ).called(1); + }); + }); + }); +} diff --git a/test/src/command_runner/completion_command_runner_test.dart b/test/src/command_runner/completion_command_runner_test.dart index 1d2fc31..eb65636 100644 --- a/test/src/command_runner/completion_command_runner_test.dart +++ b/test/src/command_runner/completion_command_runner_test.dart @@ -180,6 +180,47 @@ void main() { .called(1); }); + group('tryUninstallCompletionFiles', () { + test( + 'logs a warning wen it throws $CompletionUninstallationException', + () async { + final commandRunner = _TestCompletionCommandRunner() + ..mockCompletionInstallation = MockCompletionInstallation(); + + when( + () => commandRunner.completionInstallation.uninstall('test'), + ).thenThrow( + CompletionUninstallationException( + message: 'oops', + rootCommand: 'test', + ), + ); + + commandRunner.tryUninstallCompletionFiles(Level.verbose); + + verify(() => commandRunner.completionInstallationLogger.warn(any())) + .called(1); + }, + ); + + test( + 'logs an error when an unknown exception happens during a install', + () async { + final commandRunner = _TestCompletionCommandRunner() + ..mockCompletionInstallation = MockCompletionInstallation(); + + when( + () => commandRunner.completionInstallation.uninstall('test'), + ).thenThrow(Exception('oops')); + + commandRunner.tryUninstallCompletionFiles(Level.verbose); + + verify(() => commandRunner.completionInstallationLogger.err(any())) + .called(1); + }, + ); + }); + group('renderCompletionResult', () { test('renders predefined suggestions on zsh', () { const completionResult = _TestCompletionResult({ diff --git a/test/src/installer/completion_installation_test.dart b/test/src/installer/completion_installation_test.dart index a7ead70..60a3def 100644 --- a/test/src/installer/completion_installation_test.dart +++ b/test/src/installer/completion_installation_test.dart @@ -750,7 +750,7 @@ void main() { expect( () => installation.uninstall('very_good'), throwsA( - isA().having( + isA().having( (e) => e.message, 'message', equals('No shell RC file found at ${rcFile.path}'), @@ -778,7 +778,7 @@ void main() { expect( () => installation.uninstall('very_good'), throwsA( - isA().having( + isA().having( (e) => e.message, 'message', equals('Completion is not installed at ${rcFile.path}'), @@ -815,7 +815,7 @@ void main() { expect( () => installation.uninstall('very_good'), throwsA( - isA().having( + isA().having( (e) => e.message, 'message', equals( diff --git a/test/src/installer/exceptions_test.dart b/test/src/installer/exceptions_test.dart index 221364e..dadb28d 100644 --- a/test/src/installer/exceptions_test.dart +++ b/test/src/installer/exceptions_test.dart @@ -2,12 +2,12 @@ import 'package:cli_completion/installer.dart'; import 'package:test/test.dart'; void main() { - group('$CompletionUnistallationException', () { + group('$CompletionUninstallationException', () { test('can be instantiated', () { expect( - () => CompletionUnistallationException( + () => CompletionUninstallationException( message: 'message', - executableName: 'executableName', + rootCommand: 'executableName', ), returnsNormally, ); @@ -15,9 +15,9 @@ void main() { test('has a message', () { expect( - CompletionUnistallationException( + CompletionUninstallationException( message: 'message', - executableName: 'executableName', + rootCommand: 'executableName', ).message, equals('message'), ); @@ -25,10 +25,10 @@ void main() { test('has an executableName', () { expect( - CompletionUnistallationException( + CompletionUninstallationException( message: 'message', - executableName: 'executableName', - ).executableName, + rootCommand: 'executableName', + ).rootCommand, equals('executableName'), ); }); @@ -36,9 +36,9 @@ void main() { group('toString', () { test('returns a string', () { expect( - CompletionUnistallationException( + CompletionUninstallationException( message: 'message', - executableName: 'executableName', + rootCommand: 'executableName', ).toString(), isA(), ); @@ -46,9 +46,9 @@ void main() { test('returns a correctly formatted string', () { expect( - CompletionUnistallationException( + CompletionUninstallationException( message: 'message', - executableName: 'executableName', + rootCommand: 'executableName', ).toString(), equals( '''Could not uninstall completion scripts for executableName: message''',