From 96cf6b61348ee98d2980bfafe48b3a3c0c34d9be Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 16 May 2023 20:31:19 +0100 Subject: [PATCH 01/11] feat: completion_configuration file --- .../installer/completion_configuration.dart | 214 ++++++++++++++++++ lib/src/installer/installer.dart | 1 + .../completion_configuration_test.dart | 162 +++++++++++++ 3 files changed, 377 insertions(+) create mode 100644 lib/src/installer/completion_configuration.dart create mode 100644 test/src/installer/completion_configuration_test.dart diff --git a/lib/src/installer/completion_configuration.dart b/lib/src/installer/completion_configuration.dart new file mode 100644 index 0000000..c2568ea --- /dev/null +++ b/lib/src/installer/completion_configuration.dart @@ -0,0 +1,214 @@ +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:cli_completion/installer.dart'; +import 'package:cli_completion/parser.dart'; +import 'package:meta/meta.dart'; + +/// A map of [SystemShell]s to a list of uninstalled commands. +/// +/// The map and its content are unmodifiable. This is to ensure that +/// [CompletionConfiguration]s is fully immutable. +typedef Uninstalls + = UnmodifiableMapView>; + +/// {@template completion_configuration} +/// A configuration that stores data on how to handle command completions. +/// {@endtemplate} +@immutable +class CompletionConfiguration { + /// {@macro completion_configuration} + const CompletionConfiguration._({ + required this.uninstalls, + }); + + /// Creates an empty [CompletionConfiguration]. + @visibleForTesting + CompletionConfiguration.empty() : uninstalls = UnmodifiableMapView({}); + + /// Creates a [CompletionConfiguration] from the given [file] content. + /// + /// If the file does not exist, an empty [CompletionConfiguration] is created + /// and stored in the file. + /// + /// If the file is empty, an empty [CompletionConfiguration] is created. + /// + /// If the file is not empty, a [CompletionConfiguration] is created from the + /// file's content. This content is assumed to be a JSON string. It doesn't + /// throw when the content is not a valid JSON string. Instead it gracefully + /// handles the missing or invalid values. + factory CompletionConfiguration.fromFile(File file) { + if (!file.existsSync()) { + return CompletionConfiguration.empty()..writeTo(file); + } + + final json = file.readAsStringSync(); + return CompletionConfiguration._fromJson(json); + } + + /// Creates a [CompletionConfiguration] from the given JSON string. + factory CompletionConfiguration._fromJson(String json) { + late final Map decodedJson; + try { + decodedJson = jsonDecode(json) as Map; + } on FormatException { + decodedJson = {}; + } + + return CompletionConfiguration._( + uninstalls: _jsonDecodeUninstalls(decodedJson), + ); + } + + /// The JSON key for the [uninstalls] field. + static const String _uninstallsJsonKey = 'uninstalls'; + + /// Stores those commands that have been manually uninstalled by the user. + /// + /// Uninstalls are specific to a given [SystemShell]. + final Uninstalls uninstalls; + + /// Stores the [CompletionConfiguration] in the given [file]. + void writeTo(File file) { + if (!file.existsSync()) { + file.createSync(recursive: true); + } + file.writeAsStringSync(_toJson()); + } + + /// Returns a copy of this [CompletionConfiguration] with the given fields + /// replaced. + CompletionConfiguration copyWith({ + Uninstalls? uninstalls, + }) { + return CompletionConfiguration._( + uninstalls: uninstalls ?? this.uninstalls, + ); + } + + /// Returns a JSON representation of this [CompletionConfiguration]. + String _toJson() { + return jsonEncode({ + _uninstallsJsonKey: _jsonEncodeUninstalls(uninstalls), + }); + } +} + +/// Decodes [Uninstalls] from the given [json]. +/// +/// If the [json] is not partially or fully valid, it handles issues gracefully +/// without throwing an [Exception]. +Uninstalls _jsonDecodeUninstalls(Map json) { + late final Uninstalls uninstalls; + if (json.containsKey(CompletionConfiguration._uninstallsJsonKey)) { + final rawUninstalls = json[CompletionConfiguration._uninstallsJsonKey]; + if (rawUninstalls is! String) { + uninstalls = UnmodifiableMapView({}); + } else { + final decodedUninstalls = jsonDecode(rawUninstalls); + if (decodedUninstalls is! Map) { + uninstalls = UnmodifiableMapView({}); + } else { + final newUninstalls = >{}; + + for (final entry in decodedUninstalls.entries) { + if (!entry.key.canParseSystemShell()) continue; + final systemShell = entry.key.toSystemShell(); + final uninstallSet = {}; + if (entry.value is List) { + for (final uninstall in entry.value as List) { + if (uninstall is String) { + uninstallSet.add(uninstall); + } + } + } + + newUninstalls[systemShell] = UnmodifiableSetView(uninstallSet); + } + uninstalls = UnmodifiableMapView(newUninstalls); + } + } + } else { + uninstalls = UnmodifiableMapView({}); + } + + return uninstalls; +} + +/// Returns a JSON representation of this [Uninstalls]. +String _jsonEncodeUninstalls(Uninstalls uninstalls) { + return jsonEncode({ + for (final entry in uninstalls.entries) + 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 add({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]. + void remove({required String command, required SystemShell systemShell}) { + final modifiable = _modifiable(); + + if (modifiable.containsKey(systemShell)) { + modifiable[systemShell]!.remove(command); + } + } + + Map> _modifiable() { + return map((key, value) => MapEntry(key, value.toSet())); + } +} + +extension on String { + /// Whether this [String] can be parsed into a [SystemShell]. + bool canParseSystemShell() { + try { + toSystemShell(); + return true; + } catch (_) { + return false; + } + } + + /// Parses a [SystemShell] from the [String]. + /// + /// The value is assumed to be a string representation of a [SystemShell] + /// derived from [SystemShell.toString]. + /// + /// Throws an [ArgumentError] if the string cannot be parsed into a + /// [SystemShell]. + SystemShell toSystemShell() { + if (_equals(SystemShell.bash.toString())) { + return SystemShell.bash; + } else if (_equals(SystemShell.zsh.toString())) { + return SystemShell.zsh; + } else { + throw ArgumentError.value( + this, + 'value', + '''Failed to parse $SystemShell from "$this"''', + ); + } + } + + bool _equals(String other) => + startsWith(other) && endsWith(other) && length == other.length; +} diff --git a/lib/src/installer/installer.dart b/lib/src/installer/installer.dart index 03776bd..f341398 100644 --- a/lib/src/installer/installer.dart +++ b/lib/src/installer/installer.dart @@ -1,3 +1,4 @@ +export 'completion_configuration.dart'; export 'completion_installation.dart'; export 'exceptions.dart'; export 'script_configuration_entry.dart'; diff --git a/test/src/installer/completion_configuration_test.dart b/test/src/installer/completion_configuration_test.dart new file mode 100644 index 0000000..34b6f8c --- /dev/null +++ b/test/src/installer/completion_configuration_test.dart @@ -0,0 +1,162 @@ +// TODO(alestiago): Use barrel file for imports. +// ignore_for_file: prefer_const_constructors + +import 'dart:collection'; +import 'dart:io'; + +import 'package:cli_completion/installer.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +void main() { + group('$CompletionConfiguration', () { + final testUninstalls = UnmodifiableMapView({ + SystemShell.bash: UnmodifiableSetView({'very_bad'}), + }); + + group('fromFile', () { + test( + 'creates file with empty cache when file does not exist', + () { + final tempDirectory = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDirectory.deleteSync(recursive: true)); + + final file = File(path.join(tempDirectory.path, 'config.json')); + expect( + file.existsSync(), + isFalse, + reason: 'File should not exist', + ); + + final cache = CompletionConfiguration.fromFile(file); + expect( + cache.uninstalls, + isEmpty, + reason: 'Uninstalls should be initially empty', + ); + expect( + file.existsSync(), + isTrue, + reason: 'File should exist after cache creation', + ); + }, + ); + + test('has empty members when file is empty', () { + final tempDirectory = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDirectory.deleteSync(recursive: true)); + + final file = File(path.join(tempDirectory.path, 'config.json')) + ..writeAsStringSync(''); + + final cache = CompletionConfiguration.fromFile(file); + expect( + cache.uninstalls, + isEmpty, + reason: 'Uninstalls should be initially empty', + ); + }); + + test("creates $CompletionConfiguration with the file's defined members", + () { + final tempDirectory = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDirectory.deleteSync(recursive: true)); + + final file = File(path.join(tempDirectory.path, 'config.json')); + final cache = CompletionConfiguration.empty().copyWith( + uninstalls: testUninstalls, + )..writeTo(file); + + final newCache = CompletionConfiguration.fromFile(file); + expect( + newCache.uninstalls, + cache.uninstalls, + reason: 'Uninstalls should match those defined in the file', + ); + }); + }); + + group('writeTo', () { + test('creates a file when it does not exist', () { + final tempDirectory = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDirectory.deleteSync(recursive: true)); + + final file = File(path.join(tempDirectory.path, 'config.json')); + expect( + file.existsSync(), + isFalse, + reason: 'File should not exist', + ); + + CompletionConfiguration.empty().writeTo(file); + + expect( + file.existsSync(), + isTrue, + reason: 'File should exist after cache creation', + ); + }); + + test('returns normally when file already exists', () { + final tempDirectory = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDirectory.deleteSync(recursive: true)); + + final file = File(path.join(tempDirectory.path, 'config.json')) + ..createSync(); + expect( + file.existsSync(), + isTrue, + reason: 'File should exist', + ); + + expect( + () => CompletionConfiguration.empty().writeTo(file), + returnsNormally, + reason: 'Should not throw when file exists', + ); + }); + + test('content can be read succesfully after written', () { + final tempDirectory = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDirectory.deleteSync(recursive: true)); + + final file = File(path.join(tempDirectory.path, 'config.json')); + final cache = CompletionConfiguration.empty().copyWith( + uninstalls: testUninstalls, + )..writeTo(file); + + final newCache = CompletionConfiguration.fromFile(file); + expect( + newCache.uninstalls, + cache.uninstalls, + reason: 'Uninstalls should match those defined in the file', + ); + }); + }); + + group('copyWith', () { + test('members remain unchanged when nothing is specified', () { + final cache = CompletionConfiguration.empty(); + final newCache = cache.copyWith(); + + expect( + newCache.uninstalls, + cache.uninstalls, + reason: 'Uninstalls should remain unchanged', + ); + }); + + test('modifies uninstalls when specified', () { + final cache = CompletionConfiguration.empty(); + final uninstalls = testUninstalls; + final newCache = cache.copyWith(uninstalls: uninstalls); + + expect( + newCache.uninstalls, + equals(uninstalls), + reason: 'Uninstalls should be modified', + ); + }); + }); + }); +} From e8690943cd76177f02ae0a93d5ae4364567d621c Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 16 May 2023 21:48:50 +0100 Subject: [PATCH 02/11] refactor: removed _equals --- lib/src/installer/completion_configuration.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/src/installer/completion_configuration.dart b/lib/src/installer/completion_configuration.dart index c2568ea..3ad5805 100644 --- a/lib/src/installer/completion_configuration.dart +++ b/lib/src/installer/completion_configuration.dart @@ -196,9 +196,9 @@ extension on String { /// Throws an [ArgumentError] if the string cannot be parsed into a /// [SystemShell]. SystemShell toSystemShell() { - if (_equals(SystemShell.bash.toString())) { + if (this == SystemShell.bash.toString()) { return SystemShell.bash; - } else if (_equals(SystemShell.zsh.toString())) { + } else if (this == SystemShell.zsh.toString()) { return SystemShell.zsh; } else { throw ArgumentError.value( @@ -208,7 +208,4 @@ extension on String { ); } } - - bool _equals(String other) => - startsWith(other) && endsWith(other) && length == other.length; } From 701733116565a2dd92ccb8f61dd417bbb6fe220d Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 16 May 2023 21:53:40 +0100 Subject: [PATCH 03/11] refactor: removed unused extension --- .../installer/completion_configuration.dart | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/lib/src/installer/completion_configuration.dart b/lib/src/installer/completion_configuration.dart index 3ad5805..bef07bf 100644 --- a/lib/src/installer/completion_configuration.dart +++ b/lib/src/installer/completion_configuration.dart @@ -144,39 +144,6 @@ String _jsonEncodeUninstalls(Uninstalls uninstalls) { }); } -/// Provides convinience methods for [Uninstalls]. -extension UninstallsExtension on Uninstalls { - /// Returns a new [Uninstalls] with the given [command] added to - /// [systemShell]. - Uninstalls add({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]. - void remove({required String command, required SystemShell systemShell}) { - final modifiable = _modifiable(); - - if (modifiable.containsKey(systemShell)) { - modifiable[systemShell]!.remove(command); - } - } - - Map> _modifiable() { - return map((key, value) => MapEntry(key, value.toSet())); - } -} - extension on String { /// Whether this [String] can be parsed into a [SystemShell]. bool canParseSystemShell() { From a446ee1c2ff57a9591e8f0e38240f378615d62b8 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 16 May 2023 23:17:46 +0100 Subject: [PATCH 04/11] refactor: used SystemShell.tryParse --- .../installer/completion_configuration.dart | 37 +----------------- lib/src/system_shell.dart | 9 +++++ test/src/system_shell_test.dart | 38 +++++++++++++++++++ 3 files changed, 49 insertions(+), 35 deletions(-) diff --git a/lib/src/installer/completion_configuration.dart b/lib/src/installer/completion_configuration.dart index bef07bf..f8d4c07 100644 --- a/lib/src/installer/completion_configuration.dart +++ b/lib/src/installer/completion_configuration.dart @@ -113,8 +113,8 @@ Uninstalls _jsonDecodeUninstalls(Map json) { final newUninstalls = >{}; for (final entry in decodedUninstalls.entries) { - if (!entry.key.canParseSystemShell()) continue; - final systemShell = entry.key.toSystemShell(); + final systemShell = SystemShell.tryParse(entry.key); + if (systemShell == null) continue; final uninstallSet = {}; if (entry.value is List) { for (final uninstall in entry.value as List) { @@ -143,36 +143,3 @@ String _jsonEncodeUninstalls(Uninstalls uninstalls) { entry.key.toString(): entry.value.toList(), }); } - -extension on String { - /// Whether this [String] can be parsed into a [SystemShell]. - bool canParseSystemShell() { - try { - toSystemShell(); - return true; - } catch (_) { - return false; - } - } - - /// Parses a [SystemShell] from the [String]. - /// - /// The value is assumed to be a string representation of a [SystemShell] - /// derived from [SystemShell.toString]. - /// - /// Throws an [ArgumentError] if the string cannot be parsed into a - /// [SystemShell]. - SystemShell toSystemShell() { - if (this == SystemShell.bash.toString()) { - return SystemShell.bash; - } else if (this == SystemShell.zsh.toString()) { - return SystemShell.zsh; - } else { - throw ArgumentError.value( - this, - 'value', - '''Failed to parse $SystemShell from "$this"''', - ); - } - } -} diff --git a/lib/src/system_shell.dart b/lib/src/system_shell.dart index 1ce1568..ec744c7 100644 --- a/lib/src/system_shell.dart +++ b/lib/src/system_shell.dart @@ -54,7 +54,16 @@ enum SystemShell { // On windows basename can be bash.exe return SystemShell.bash; } + return null; + } + /// Tries to parse a [SystemShell] from a [String]. + /// + /// Returns `null` if the [value] does not match any of the shells. + static SystemShell? tryParse(String value) { + for (final shell in SystemShell.values) { + if (value == shell.name || value == shell.toString()) return shell; + } return null; } } diff --git a/test/src/system_shell_test.dart b/test/src/system_shell_test.dart index fa819af..5c6a97f 100644 --- a/test/src/system_shell_test.dart +++ b/test/src/system_shell_test.dart @@ -103,5 +103,43 @@ void main() { }); }); }); + + group('tryParse', () { + group('returns bash', () { + test('when given "bash"', () { + expect( + SystemShell.tryParse('bash'), + equals(SystemShell.bash), + ); + }); + + test('when given "SystemShell.bash"', () { + expect( + SystemShell.tryParse('SystemShell.bash'), + equals(SystemShell.bash), + ); + }); + }); + + group('returns zsh', () { + test('when given "zsh"', () { + expect( + SystemShell.tryParse('zsh'), + equals(SystemShell.zsh), + ); + }); + + test('when given "SystemShell.zsh"', () { + expect( + SystemShell.tryParse('SystemShell.zsh'), + equals(SystemShell.zsh), + ); + }); + }); + + test('returns null for unknown shell', () { + expect(SystemShell.tryParse('unknown'), equals(null)); + }); + }); }); } From 623002530789398ab29e8146559e3098d0e5fec5 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 16 May 2023 23:18:49 +0100 Subject: [PATCH 05/11] docs: removed TODO --- test/src/installer/completion_configuration_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/src/installer/completion_configuration_test.dart b/test/src/installer/completion_configuration_test.dart index 34b6f8c..383f56c 100644 --- a/test/src/installer/completion_configuration_test.dart +++ b/test/src/installer/completion_configuration_test.dart @@ -1,4 +1,3 @@ -// TODO(alestiago): Use barrel file for imports. // ignore_for_file: prefer_const_constructors import 'dart:collection'; From 0a84f2f1e5c724b36ae3412caee9f71056c676f1 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 16 May 2023 23:24:22 +0100 Subject: [PATCH 06/11] refactor: simplified _jsonDecodeUninstalls --- .../installer/completion_configuration.dart | 53 ++++++++----------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/lib/src/installer/completion_configuration.dart b/lib/src/installer/completion_configuration.dart index f8d4c07..740ce81 100644 --- a/lib/src/installer/completion_configuration.dart +++ b/lib/src/installer/completion_configuration.dart @@ -100,40 +100,33 @@ class CompletionConfiguration { /// If the [json] is not partially or fully valid, it handles issues gracefully /// without throwing an [Exception]. Uninstalls _jsonDecodeUninstalls(Map json) { - late final Uninstalls uninstalls; - if (json.containsKey(CompletionConfiguration._uninstallsJsonKey)) { - final rawUninstalls = json[CompletionConfiguration._uninstallsJsonKey]; - if (rawUninstalls is! String) { - uninstalls = UnmodifiableMapView({}); - } else { - final decodedUninstalls = jsonDecode(rawUninstalls); - if (decodedUninstalls is! Map) { - uninstalls = UnmodifiableMapView({}); - } else { - final newUninstalls = >{}; - - for (final entry in decodedUninstalls.entries) { - final systemShell = SystemShell.tryParse(entry.key); - if (systemShell == null) continue; - final uninstallSet = {}; - if (entry.value is List) { - for (final uninstall in entry.value as List) { - if (uninstall is String) { - uninstallSet.add(uninstall); - } - } - } - - newUninstalls[systemShell] = UnmodifiableSetView(uninstallSet); + if (!json.containsKey(CompletionConfiguration._uninstallsJsonKey)) { + return UnmodifiableMapView({}); + } + final jsonUninstalls = json[CompletionConfiguration._uninstallsJsonKey]; + if (jsonUninstalls is! String) { + return UnmodifiableMapView({}); + } + final decodedUninstalls = jsonDecode(jsonUninstalls); + if (decodedUninstalls is! Map) { + return UnmodifiableMapView({}); + } + + final newUninstalls = >{}; + for (final entry in decodedUninstalls.entries) { + final systemShell = SystemShell.tryParse(entry.key); + if (systemShell == null) continue; + final uninstallSet = {}; + if (entry.value is List) { + for (final uninstall in entry.value as List) { + if (uninstall is String) { + uninstallSet.add(uninstall); } - uninstalls = UnmodifiableMapView(newUninstalls); } } - } else { - uninstalls = UnmodifiableMapView({}); + newUninstalls[systemShell] = UnmodifiableSetView(uninstallSet); } - - return uninstalls; + return UnmodifiableMapView(newUninstalls); } /// Returns a JSON representation of this [Uninstalls]. From ec8930de359d8971d8c04ad470f27afa6ac0550e Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 16 May 2023 23:24:46 +0100 Subject: [PATCH 07/11] docs: improved docs --- lib/src/installer/completion_configuration.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/installer/completion_configuration.dart b/lib/src/installer/completion_configuration.dart index 740ce81..483ce1d 100644 --- a/lib/src/installer/completion_configuration.dart +++ b/lib/src/installer/completion_configuration.dart @@ -129,7 +129,7 @@ Uninstalls _jsonDecodeUninstalls(Map json) { return UnmodifiableMapView(newUninstalls); } -/// Returns a JSON representation of this [Uninstalls]. +/// Returns a JSON representation of the given [Uninstalls]. String _jsonEncodeUninstalls(Uninstalls uninstalls) { return jsonEncode({ for (final entry in uninstalls.entries) From acd6f02be4dcf3e26411cc1691d7eaf70cda3489 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 16 May 2023 23:25:45 +0100 Subject: [PATCH 08/11] docs: improved docs --- lib/src/installer/completion_configuration.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/installer/completion_configuration.dart b/lib/src/installer/completion_configuration.dart index 483ce1d..468e38a 100644 --- a/lib/src/installer/completion_configuration.dart +++ b/lib/src/installer/completion_configuration.dart @@ -35,9 +35,9 @@ class CompletionConfiguration { /// If the file is empty, an empty [CompletionConfiguration] is created. /// /// If the file is not empty, a [CompletionConfiguration] is created from the - /// file's content. This content is assumed to be a JSON string. It doesn't - /// throw when the content is not a valid JSON string. Instead it gracefully - /// handles the missing or invalid values. + /// file's content. This content is assumed to be a JSON string. The parsing + /// is handled gracefully, so if the JSON is partially or fully invalid, it + /// handles issues without throwing an [Exception]. factory CompletionConfiguration.fromFile(File file) { if (!file.existsSync()) { return CompletionConfiguration.empty()..writeTo(file); From 5aadb843e00e8f779b4180c081b292f987e3be77 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 16 May 2023 23:27:38 +0100 Subject: [PATCH 09/11] docs: improved docs --- lib/src/installer/completion_configuration.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/installer/completion_configuration.dart b/lib/src/installer/completion_configuration.dart index 468e38a..f1f32c0 100644 --- a/lib/src/installer/completion_configuration.dart +++ b/lib/src/installer/completion_configuration.dart @@ -14,7 +14,8 @@ typedef Uninstalls = UnmodifiableMapView>; /// {@template completion_configuration} -/// A configuration that stores data on how to handle command completions. +/// A configuration that stores information on how to handle command +/// completions. /// {@endtemplate} @immutable class CompletionConfiguration { From 14409483d22b8165a9badd24cce91e62842f86ea Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 16 May 2023 23:39:01 +0100 Subject: [PATCH 10/11] test: added parsing tests --- .../installer/completion_configuration.dart | 6 ++- .../completion_configuration_test.dart | 40 +++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/lib/src/installer/completion_configuration.dart b/lib/src/installer/completion_configuration.dart index f1f32c0..71be246 100644 --- a/lib/src/installer/completion_configuration.dart +++ b/lib/src/installer/completion_configuration.dart @@ -108,8 +108,10 @@ Uninstalls _jsonDecodeUninstalls(Map json) { if (jsonUninstalls is! String) { return UnmodifiableMapView({}); } - final decodedUninstalls = jsonDecode(jsonUninstalls); - if (decodedUninstalls is! Map) { + late final Map decodedUninstalls; + try { + decodedUninstalls = jsonDecode(jsonUninstalls) as Map; + } on FormatException { return UnmodifiableMapView({}); } diff --git a/test/src/installer/completion_configuration_test.dart b/test/src/installer/completion_configuration_test.dart index 383f56c..ca013e1 100644 --- a/test/src/installer/completion_configuration_test.dart +++ b/test/src/installer/completion_configuration_test.dart @@ -73,6 +73,46 @@ void main() { reason: 'Uninstalls should match those defined in the file', ); }); + + test( + '''creates $CompletionConfiguration with empty uninstalls if the file has a string value''', + () { + final tempDirectory = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDirectory.deleteSync(recursive: true)); + + const json = '{"uninstalls": "very_bad"}'; + final file = File(path.join(tempDirectory.path, 'config.json')) + ..writeAsStringSync(json); + + final cache = CompletionConfiguration.fromFile(file); + expect( + cache.uninstalls, + isEmpty, + reason: + '''Uninstalls should be empty when the value is of an invalid type''', + ); + }, + ); + + test( + '''creates $CompletionConfiguration with empty uninstalls if the file has a numeric value''', + () { + final tempDirectory = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDirectory.deleteSync(recursive: true)); + + const json = '{"uninstalls": 1}'; + final file = File(path.join(tempDirectory.path, 'config.json')) + ..writeAsStringSync(json); + + final cache = CompletionConfiguration.fromFile(file); + expect( + cache.uninstalls, + isEmpty, + reason: + '''Uninstalls should be empty when the value is of an invalid type''', + ); + }, + ); }); group('writeTo', () { From b68c3779981551f4eb68349d1a4f46e788fb8033 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 16 May 2023 23:44:09 +0100 Subject: [PATCH 11/11] test: changed factory streategy --- lib/src/installer/completion_configuration.dart | 8 +++----- .../installer/completion_configuration_test.dart | 15 +++++---------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/lib/src/installer/completion_configuration.dart b/lib/src/installer/completion_configuration.dart index 71be246..7a10957 100644 --- a/lib/src/installer/completion_configuration.dart +++ b/lib/src/installer/completion_configuration.dart @@ -30,10 +30,8 @@ class CompletionConfiguration { /// Creates a [CompletionConfiguration] from the given [file] content. /// - /// If the file does not exist, an empty [CompletionConfiguration] is created - /// and stored in the file. - /// - /// If the file is empty, an empty [CompletionConfiguration] is created. + /// If the file does not exist or is empty, a [CompletionConfiguration.empty] + /// is created. /// /// If the file is not empty, a [CompletionConfiguration] is created from the /// file's content. This content is assumed to be a JSON string. The parsing @@ -41,7 +39,7 @@ class CompletionConfiguration { /// handles issues without throwing an [Exception]. factory CompletionConfiguration.fromFile(File file) { if (!file.existsSync()) { - return CompletionConfiguration.empty()..writeTo(file); + return CompletionConfiguration.empty(); } final json = file.readAsStringSync(); diff --git a/test/src/installer/completion_configuration_test.dart b/test/src/installer/completion_configuration_test.dart index ca013e1..ae158e8 100644 --- a/test/src/installer/completion_configuration_test.dart +++ b/test/src/installer/completion_configuration_test.dart @@ -15,7 +15,7 @@ void main() { group('fromFile', () { test( - 'creates file with empty cache when file does not exist', + 'returns empty cache when file does not exist', () { final tempDirectory = Directory.systemTemp.createTempSync(); addTearDown(() => tempDirectory.deleteSync(recursive: true)); @@ -33,15 +33,10 @@ void main() { isEmpty, reason: 'Uninstalls should be initially empty', ); - expect( - file.existsSync(), - isTrue, - reason: 'File should exist after cache creation', - ); }, ); - test('has empty members when file is empty', () { + test('returns empty cache when file is empty', () { final tempDirectory = Directory.systemTemp.createTempSync(); addTearDown(() => tempDirectory.deleteSync(recursive: true)); @@ -56,7 +51,7 @@ void main() { ); }); - test("creates $CompletionConfiguration with the file's defined members", + test("returns a $CompletionConfiguration with the file's defined members", () { final tempDirectory = Directory.systemTemp.createTempSync(); addTearDown(() => tempDirectory.deleteSync(recursive: true)); @@ -75,7 +70,7 @@ void main() { }); test( - '''creates $CompletionConfiguration with empty uninstalls if the file has a string value''', + '''returns a $CompletionConfiguration with empty uninstalls if the file's JSON "uninstalls" key has a string value''', () { final tempDirectory = Directory.systemTemp.createTempSync(); addTearDown(() => tempDirectory.deleteSync(recursive: true)); @@ -95,7 +90,7 @@ void main() { ); test( - '''creates $CompletionConfiguration with empty uninstalls if the file has a numeric value''', + '''returns a $CompletionConfiguration with empty uninstalls if file's JSON "uninstalls" key has a numeric value''', () { final tempDirectory = Directory.systemTemp.createTempSync(); addTearDown(() => tempDirectory.deleteSync(recursive: true));