diff --git a/packages/shorebird_cli/lib/src/commands/cache/clean_cache_command.dart b/packages/shorebird_cli/lib/src/commands/cache/clean_cache_command.dart index 8175f78a..09d5beae 100644 --- a/packages/shorebird_cli/lib/src/commands/cache/clean_cache_command.dart +++ b/packages/shorebird_cli/lib/src/commands/cache/clean_cache_command.dart @@ -1,9 +1,11 @@ import 'dart:async'; +import 'dart:io'; import 'package:mason_logger/mason_logger.dart'; import 'package:shorebird_cli/src/cache.dart'; import 'package:shorebird_cli/src/command.dart'; import 'package:shorebird_cli/src/logger.dart'; +import 'package:shorebird_cli/src/platform.dart'; import 'package:shorebird_cli/src/shorebird_config_mixin.dart'; /// {@template clean_cache_command} @@ -25,8 +27,34 @@ class CleanCacheCommand extends ShorebirdCommand with ShorebirdConfigMixin { @override Future run() async { - cache.clear(); - logger.success('✅ Cleared Cache!'); + final progress = logger.progress('Clearing Cache'); + try { + cache.clear(); + } on FileSystemException catch (error) { + final cachePath = Cache.shorebirdCacheDirectory.path; + progress.fail( + '''Failed to delete cache directory $cachePath: $error''', + ); + if (!platform.isWindows) { + return ExitCode.software.code; + } + + final superuserLink = link( + uri: Uri.parse( + 'https://superuser.com/questions/1333118/cant-delete-empty-folder-because-it-is-used', + ), + ); + + logger.info( + ''' +This could be because a program is using a file in the cache directory. To find and stop such a program, see: + ${lightCyan.wrap(superuserLink)} +''', + ); + return ExitCode.software.code; + } + + progress.complete('Cleared cache'); return ExitCode.success.code; } } diff --git a/packages/shorebird_cli/test/src/commands/cache/clean_cache_command_test.dart b/packages/shorebird_cli/test/src/commands/cache/clean_cache_command_test.dart index 1412e331..6a41328c 100644 --- a/packages/shorebird_cli/test/src/commands/cache/clean_cache_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/cache/clean_cache_command_test.dart @@ -1,19 +1,30 @@ +import 'dart:io'; + import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'package:scoped/scoped.dart'; import 'package:shorebird_cli/src/cache.dart'; import 'package:shorebird_cli/src/commands/commands.dart'; import 'package:shorebird_cli/src/logger.dart'; +import 'package:shorebird_cli/src/platform.dart'; import 'package:test/test.dart'; +class _MockCache extends Mock implements Cache {} + class _MockLogger extends Mock implements Logger {} -class _MockCache extends Mock implements Cache {} +class _MockPlatform extends Mock implements Platform {} + +class _MockProgress extends Mock implements Progress {} void main() { group('cache clean', () { late Cache cache; late Logger logger; + late Platform platform; + late Progress progress; late CleanCacheCommand command; R runWithOverrides(R Function() body) { @@ -22,6 +33,7 @@ void main() { values: { cacheRef.overrideWith(() => cache), loggerRef.overrideWith(() => logger), + platformRef.overrideWith(() => platform), }, ); } @@ -29,7 +41,21 @@ void main() { setUp(() { cache = _MockCache(); logger = _MockLogger(); + platform = _MockPlatform(); + progress = _MockProgress(); command = runWithOverrides(CleanCacheCommand.new); + + when(() => logger.progress(any())).thenReturn(progress); + when(() => platform.script).thenReturn( + Uri.file( + p.join( + Directory.systemTemp.createTempSync().path, + 'bin', + 'cache', + 'shorebird.snapshot', + ), + ), + ); }); test('has a description', () { @@ -39,8 +65,47 @@ void main() { test('clears the cache', () async { final result = await runWithOverrides(command.run); expect(result, equals(ExitCode.success.code)); - verify(() => logger.success('✅ Cleared Cache!')).called(1); + verify(() => progress.complete('Cleared cache')).called(1); verify(cache.clear).called(1); }); + + group('on failure', () { + group('on Windows', () { + setUp(() { + when(() => platform.isWindows).thenReturn(true); + }); + + test('tells the user how to find the issue and exits with code 70', + () async { + when(() => cache.clear()).thenThrow( + const FileSystemException('Failed to delete'), + ); + + final result = await runWithOverrides(command.run); + + expect(result, equals(ExitCode.software.code)); + verify(() => progress.fail(any())).called(1); + verify(() => logger.info(any())).called(1); + }); + }); + + group('on a non-Windows OS', () { + setUp(() { + when(() => platform.isWindows).thenReturn(false); + }); + + test('prints error message and exits with code 70', () async { + when(() => cache.clear()).thenThrow( + const FileSystemException('Failed to delete'), + ); + + final result = await runWithOverrides(command.run); + + expect(result, equals(ExitCode.software.code)); + verify(() => progress.fail(any())).called(1); + verifyNever(() => logger.info(any())); + }); + }); + }); }); }