Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6ce5d9a
feat: added logic
alestiago May 18, 2023
e4ae10c
refactor: removed empty line
alestiago May 18, 2023
d9e1100
refactor: formatting
alestiago May 18, 2023
677f897
refactor: refined autoinstallation logic
alestiago May 18, 2023
0d41fda
fix: resolved uninstallation directory removal logic
alestiago May 18, 2023
1f3cd35
test: added 'config.json' to files
alestiago May 18, 2023
08e2b72
test: fixed unmocked tests
alestiago May 18, 2023
6348750
test: added unmocked instances
alestiago May 18, 2023
43180f8
test: added TODOs
alestiago May 18, 2023
30e7cc0
refactor: formatter
alestiago May 18, 2023
adc1684
test: added UninstallsExtension tests
alestiago May 22, 2023
b99b5bd
refactor: fixed formatter
alestiago May 22, 2023
5525151
feat: added new command as reserved
alestiago May 22, 2023
86aee3b
test: don't auto install when uninstalled
alestiago May 22, 2023
82a6cc3
test: added installation tests
alestiago May 22, 2023
434fe91
test: does not auto install when it is unistalled
alestiago May 22, 2023
e657265
test: used executableName
alestiago May 22, 2023
b2422ec
test: used command runner shell
alestiago May 22, 2023
261aa3f
test: defined environment override for shell
alestiago May 22, 2023
a375d99
refactor: moved logic to install
alestiago May 23, 2023
b2dfa4b
refactor: removed errors
alestiago May 23, 2023
366a480
docs: fixed docs
alestiago May 23, 2023
2253015
refactor: modify logic
alestiago May 23, 2023
0a6b21f
test: soft install
alestiago May 23, 2023
ab652bf
docs: fixed docs
alestiago May 23, 2023
c741dd3
refactor: removed mocks
alestiago May 23, 2023
135e0d1
refactor: test name
alestiago May 23, 2023
39128a1
test: removed redundant variable
alestiago May 23, 2023
15f305f
test: simplified
alestiago May 23, 2023
190338f
test: small typo
alestiago May 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class InstallCompletionFilesCommand<T> extends Command<T> {
FutureOr<T>? run() {
final verbose = argResults!['verbose'] as bool;
final level = verbose ? Level.verbose : Level.info;
runner.tryInstallCompletionFiles(level);
runner.tryInstallCompletionFiles(level, force: true);
return null;
}
}
18 changes: 10 additions & 8 deletions lib/src/command_runner/completion_command_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,18 @@ abstract class CompletionCommandRunner<T> extends CommandRunner<T> {
return _completionInstallation = completionInstallation;
}

/// The list of commands that should not trigger the auto installation.
static const _reservedCommands = {
HandleCompletionRequestCommand.commandName,
InstallCompletionFilesCommand.commandName,
UnistallCompletionFilesCommand.commandName,
};

@override
@mustCallSuper
Future<T?> runCommand(ArgResults topLevelResults) async {
final reservedCommands = [
HandleCompletionRequestCommand.commandName,
InstallCompletionFilesCommand.commandName,
];

if (enableAutoInstall &&
!reservedCommands.contains(topLevelResults.command?.name)) {
!_reservedCommands.contains(topLevelResults.command?.name)) {
// When auto installing, use error level to display messages.
tryInstallCompletionFiles(Level.error);
}
Expand All @@ -84,10 +86,10 @@ abstract class CompletionCommandRunner<T> extends CommandRunner<T> {

/// Tries to install completion files for the current shell.
@internal
void tryInstallCompletionFiles(Level level) {
void tryInstallCompletionFiles(Level level, {bool force = false}) {
try {
completionInstallationLogger.level = level;
completionInstallation.install(executableName);
completionInstallation.install(executableName, force: force);
} on CompletionInstallationException catch (e) {
completionInstallationLogger.warn(e.toString());
} on Exception catch (e) {
Expand Down
51 changes: 51 additions & 0 deletions lib/src/installer/completion_configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,54 @@ String _jsonEncodeUninstalls(Uninstalls uninstalls) {
entry.key.toString(): entry.value.toList(),
});
}

/// Provides convinience methods for [Uninstalls].
extension UninstallsExtension on Uninstalls {
/// Returns a new [Uninstalls] with the given [command] added to
/// [systemShell].
Uninstalls include({
required String command,
required SystemShell systemShell,
}) {
final modifiable = _modifiable();

if (modifiable.containsKey(systemShell)) {
modifiable[systemShell]!.add(command);
} else {
modifiable[systemShell] = {command};
}

return UnmodifiableMapView(
modifiable.map((key, value) => MapEntry(key, UnmodifiableSetView(value))),
);
}

/// Returns a new [Uninstalls] with the given [command] removed from
/// [systemShell].
Uninstalls exclude({
required String command,
required SystemShell systemShell,
}) {
final modifiable = _modifiable();

if (modifiable.containsKey(systemShell)) {
modifiable[systemShell]!.remove(command);
}

return UnmodifiableMapView(
modifiable.map((key, value) => MapEntry(key, UnmodifiableSetView(value))),
);
}

/// Whether the [command] is contained in [systemShell].
bool contains({required String command, required SystemShell systemShell}) {
if (containsKey(systemShell)) {
return this[systemShell]!.contains(command);
}
return false;
}

Map<SystemShell, Set<String>> _modifiable() {
return map((key, value) => MapEntry(key, value.toSet()));
}
}
63 changes: 59 additions & 4 deletions lib/src/installer/completion_installation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ class CompletionInstallation {
}
}

/// Define the [File] in which the completion configuration is stored.
@visibleForTesting
File get completionConfigurationFile {
return File(path.join(completionConfigDir.path, 'config.json'));
}

/// Install completion configuration files for a [rootCommand] in the
/// current shell.
///
Expand All @@ -101,7 +107,11 @@ class CompletionInstallation {
/// completion script file.
/// - A line in the shell config file (e.g. `.bash_profile`) that sources
/// the aforementioned config file.
void install(String rootCommand) {
///
/// If [force] is true, it will overwrite the command's completion files even
/// if they already exist. If false, it will check if it has been explicitly
/// uninstalled before installing it.
void install(String rootCommand, {bool force = false}) {
final configuration = this.configuration;

if (configuration == null) {
Expand All @@ -111,6 +121,10 @@ class CompletionInstallation {
);
}

if (!force && !_shouldInstall(rootCommand)) {
return;
}

logger.detail(
'Installing completion for the command $rootCommand '
'on ${configuration.shell.name}',
Expand All @@ -124,6 +138,33 @@ class CompletionInstallation {
if (completionFileCreated) {
_logSourceInstructions(rootCommand);
}

final completionConfiguration =
CompletionConfiguration.fromFile(completionConfigurationFile);
completionConfiguration
.copyWith(
uninstalls: completionConfiguration.uninstalls.exclude(
command: rootCommand,
systemShell: configuration.shell,
),
)
.writeTo(completionConfigurationFile);
}

/// Wether the completion configuration files for a [rootCommand] should be
/// installed or not.
///
/// It will return false if the root command has been explicitly uninstalled.
bool _shouldInstall(String rootCommand) {
final completionConfiguration = CompletionConfiguration.fromFile(
completionConfigurationFile,
);
final systemShell = configuration!.shell;
final isUninstalled = completionConfiguration.uninstalls.contains(
command: rootCommand,
systemShell: systemShell,
);
return !isUninstalled;
}

/// Create a directory in which the completion config files shall be saved.
Expand Down Expand Up @@ -378,9 +419,23 @@ ${configuration!.sourceLineTemplate(scriptPath)}''';
if (!shellCompletionConfigurationFile.existsSync()) {
completionEntry.removeFrom(shellRCFile);
}

if (completionConfigDir.listSync().isEmpty) {
completionConfigDir.deleteSync();
final completionConfigDirContent = completionConfigDir.listSync();
final onlyHasConfigurationFile = completionConfigDirContent.length == 1 &&
path.absolute(completionConfigDirContent.first.path) ==
path.absolute(completionConfigurationFile.path);
if (completionConfigDirContent.isEmpty || onlyHasConfigurationFile) {
completionConfigDir.deleteSync(recursive: true);
} else {
final completionConfiguration =
CompletionConfiguration.fromFile(completionConfigurationFile);
completionConfiguration
.copyWith(
uninstalls: completionConfiguration.uninstalls.include(
command: rootCommand,
systemShell: configuration.shell,
),
)
.writeTo(completionConfigurationFile);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';

class MockLogger extends Mock implements Logger {}
class _MockLogger extends Mock implements Logger {}

class MockCompletionInstallation extends Mock
class _MockCompletionInstallation extends Mock
implements CompletionInstallation {}

class _TestCompletionCommandRunner extends CompletionCommandRunner<int> {
_TestCompletionCommandRunner() : super('test', 'Test command runner');

@override
// ignore: overridden_fields
final Logger completionInstallationLogger = MockLogger();
final Logger completionInstallationLogger = _MockLogger();

@override
final CompletionInstallation completionInstallation =
MockCompletionInstallation();
_MockCompletionInstallation();
}

void main() {
Expand All @@ -45,6 +45,15 @@ void main() {
});

group('install completion files', () {
test('forces install', () async {
await commandRunner.run(['install-completion-files']);

verify(
() => commandRunner.completionInstallation
.install(commandRunner.executableName, force: true),
).called(1);
});

test('when normal', () async {
await commandRunner.run(['install-completion-files']);

Expand Down
38 changes: 28 additions & 10 deletions test/src/command_runner/completion_command_runner_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import 'package:test/test.dart';

class MockLogger extends Mock implements Logger {}

class MockCompletionInstallation extends Mock
class _MockCompletionInstallation extends Mock
implements CompletionInstallation {}

class _TestCompletionCommandRunner extends CompletionCommandRunner<int> {
Expand Down Expand Up @@ -116,13 +116,13 @@ void main() {
test('Tries to install completion files on test subcommand', () async {
final commandRunner = _TestCompletionCommandRunner()
..addCommand(_TestUserCommand())
..mockCompletionInstallation = MockCompletionInstallation();
..mockCompletionInstallation = _MockCompletionInstallation();

await commandRunner.run(['ahoy']);

verify(() => commandRunner.completionInstallation.install('test'))
.called(1);

verify(
() => commandRunner.completionInstallation.install('test'),
).called(1);
verify(
() => commandRunner.completionInstallationLogger.level = Level.error,
).called(1);
Expand All @@ -132,7 +132,7 @@ void main() {
final commandRunner = _TestCompletionCommandRunner()
..enableAutoInstall = false
..addCommand(_TestUserCommand())
..mockCompletionInstallation = MockCompletionInstallation();
..mockCompletionInstallation = _MockCompletionInstallation();

await commandRunner.run(['ahoy']);

Expand All @@ -142,14 +142,32 @@ void main() {
() => commandRunner.completionInstallationLogger.level = any(),
);
});

test('softly tries to install when enabled', () async {
final commandRunner = _TestCompletionCommandRunner()
..enableAutoInstall = true
..addCommand(_TestUserCommand())
..mockCompletionInstallation = _MockCompletionInstallation()
..environmentOverride = {
'SHELL': '/foo/bar/zsh',
};

await commandRunner.run(['ahoy']);

verify(
() => commandRunner.completionInstallation.install(
commandRunner.executableName,
),
).called(1);
});
});

test(
'When it throws CompletionInstallationException, it logs as a warning',
() async {
final commandRunner = _TestCompletionCommandRunner()
..addCommand(_TestUserCommand())
..mockCompletionInstallation = MockCompletionInstallation();
..mockCompletionInstallation = _MockCompletionInstallation();

when(
() => commandRunner.completionInstallation.install('test'),
Expand All @@ -168,7 +186,7 @@ void main() {
() async {
final commandRunner = _TestCompletionCommandRunner()
..addCommand(_TestUserCommand())
..mockCompletionInstallation = MockCompletionInstallation();
..mockCompletionInstallation = _MockCompletionInstallation();

when(
() => commandRunner.completionInstallation.install('test'),
Expand All @@ -185,7 +203,7 @@ void main() {
'logs a warning wen it throws $CompletionUninstallationException',
() async {
final commandRunner = _TestCompletionCommandRunner()
..mockCompletionInstallation = MockCompletionInstallation();
..mockCompletionInstallation = _MockCompletionInstallation();

when(
() => commandRunner.completionInstallation.uninstall('test'),
Expand All @@ -207,7 +225,7 @@ void main() {
'logs an error when an unknown exception happens during a install',
() async {
final commandRunner = _TestCompletionCommandRunner()
..mockCompletionInstallation = MockCompletionInstallation();
..mockCompletionInstallation = _MockCompletionInstallation();

when(
() => commandRunner.completionInstallation.uninstall('test'),
Expand Down
Loading