Unverified Commit c1c12aa3 authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Add posix permission chown suggestion to io error handling (#81942)

parent 0581c05c
...@@ -212,6 +212,7 @@ class ErrorHandlingFile ...@@ -212,6 +212,7 @@ class ErrorHandlingFile
)), )),
platform: _platform, platform: _platform,
failureMessage: 'Flutter failed to write to a file at "${delegate.path}"', failureMessage: 'Flutter failed to write to a file at "${delegate.path}"',
posixPermissionSuggestion: _posixPermissionSuggestion(<String>[delegate.path]),
); );
} }
...@@ -221,6 +222,7 @@ class ErrorHandlingFile ...@@ -221,6 +222,7 @@ class ErrorHandlingFile
() => delegate.readAsStringSync(), () => delegate.readAsStringSync(),
platform: _platform, platform: _platform,
failureMessage: 'Flutter failed to read a file at "${delegate.path}"', failureMessage: 'Flutter failed to read a file at "${delegate.path}"',
posixPermissionSuggestion: _posixPermissionSuggestion(<String>[delegate.path]),
); );
} }
...@@ -234,6 +236,7 @@ class ErrorHandlingFile ...@@ -234,6 +236,7 @@ class ErrorHandlingFile
() => delegate.writeAsBytesSync(bytes, mode: mode, flush: flush), () => delegate.writeAsBytesSync(bytes, mode: mode, flush: flush),
platform: _platform, platform: _platform,
failureMessage: 'Flutter failed to write to a file at "${delegate.path}"', failureMessage: 'Flutter failed to write to a file at "${delegate.path}"',
posixPermissionSuggestion: _posixPermissionSuggestion(<String>[delegate.path]),
); );
} }
...@@ -253,6 +256,7 @@ class ErrorHandlingFile ...@@ -253,6 +256,7 @@ class ErrorHandlingFile
)), )),
platform: _platform, platform: _platform,
failureMessage: 'Flutter failed to write to a file at "${delegate.path}"', failureMessage: 'Flutter failed to write to a file at "${delegate.path}"',
posixPermissionSuggestion: _posixPermissionSuggestion(<String>[delegate.path]),
); );
} }
...@@ -272,6 +276,7 @@ class ErrorHandlingFile ...@@ -272,6 +276,7 @@ class ErrorHandlingFile
), ),
platform: _platform, platform: _platform,
failureMessage: 'Flutter failed to write to a file at "${delegate.path}"', failureMessage: 'Flutter failed to write to a file at "${delegate.path}"',
posixPermissionSuggestion: _posixPermissionSuggestion(<String>[delegate.path]),
); );
} }
...@@ -283,6 +288,7 @@ class ErrorHandlingFile ...@@ -283,6 +288,7 @@ class ErrorHandlingFile
), ),
platform: _platform, platform: _platform,
failureMessage: 'Flutter failed to create file at "${delegate.path}"', failureMessage: 'Flutter failed to create file at "${delegate.path}"',
posixPermissionSuggestion: recursive ? null : _posixPermissionSuggestion(<String>[delegate.parent.path]),
); );
} }
...@@ -294,6 +300,7 @@ class ErrorHandlingFile ...@@ -294,6 +300,7 @@ class ErrorHandlingFile
), ),
platform: _platform, platform: _platform,
failureMessage: 'Flutter failed to open a file at "${delegate.path}"', failureMessage: 'Flutter failed to open a file at "${delegate.path}"',
posixPermissionSuggestion: _posixPermissionSuggestion(<String>[delegate.path]),
); );
} }
...@@ -307,7 +314,8 @@ class ErrorHandlingFile ...@@ -307,7 +314,8 @@ class ErrorHandlingFile
_runSync<void>( _runSync<void>(
() => delegate.openSync(mode: FileMode.read).closeSync(), () => delegate.openSync(mode: FileMode.read).closeSync(),
platform: _platform, platform: _platform,
failureMessage: 'Flutter failed to copy $path to $newPath due to source location error' failureMessage: 'Flutter failed to copy $path to $newPath due to source location error',
posixPermissionSuggestion: _posixPermissionSuggestion(<String>[path]),
); );
// Next check if the destination file can be written. If not, bail through // Next check if the destination file can be written. If not, bail through
// error handling. // error handling.
...@@ -347,11 +355,17 @@ class ErrorHandlingFile ...@@ -347,11 +355,17 @@ class ErrorHandlingFile
source?.closeSync(); source?.closeSync();
sink?.closeSync(); sink?.closeSync();
} }
}, platform: _platform, failureMessage: 'Flutter failed to copy $path to $newPath due to unknown error'); }, platform: _platform,
failureMessage: 'Flutter failed to copy $path to $newPath due to unknown error',
posixPermissionSuggestion: _posixPermissionSuggestion(<String>[path, resultFile.parent.path]),
);
// The original copy failed, but the manual copy worked. // The original copy failed, but the manual copy worked.
return wrapFile(resultFile); return wrapFile(resultFile);
} }
String _posixPermissionSuggestion(List<String> paths) => 'Try running:\n'
' sudo chown -R \$(whoami) ${paths.map(fileSystem.path.absolute).join(' ')}';
@override @override
String toString() => delegate.toString(); String toString() => delegate.toString();
} }
...@@ -420,6 +434,7 @@ class ErrorHandlingDirectory ...@@ -420,6 +434,7 @@ class ErrorHandlingDirectory
platform: _platform, platform: _platform,
failureMessage: failureMessage:
'Flutter failed to create a directory at "${delegate.path}"', 'Flutter failed to create a directory at "${delegate.path}"',
posixPermissionSuggestion: recursive ? null : _posixPermissionSuggestion(delegate.parent.path),
); );
} }
...@@ -450,6 +465,7 @@ class ErrorHandlingDirectory ...@@ -450,6 +465,7 @@ class ErrorHandlingDirectory
platform: _platform, platform: _platform,
failureMessage: failureMessage:
'Flutter failed to create a directory at "${delegate.path}"', 'Flutter failed to create a directory at "${delegate.path}"',
posixPermissionSuggestion: recursive ? null : _posixPermissionSuggestion(delegate.parent.path),
); );
} }
...@@ -460,6 +476,7 @@ class ErrorHandlingDirectory ...@@ -460,6 +476,7 @@ class ErrorHandlingDirectory
platform: _platform, platform: _platform,
failureMessage: failureMessage:
'Flutter failed to delete a directory at "${delegate.path}"', 'Flutter failed to delete a directory at "${delegate.path}"',
posixPermissionSuggestion: recursive ? null : _posixPermissionSuggestion(delegate.path),
); );
} }
...@@ -470,6 +487,7 @@ class ErrorHandlingDirectory ...@@ -470,6 +487,7 @@ class ErrorHandlingDirectory
platform: _platform, platform: _platform,
failureMessage: failureMessage:
'Flutter failed to delete a directory at "${delegate.path}"', 'Flutter failed to delete a directory at "${delegate.path}"',
posixPermissionSuggestion: recursive ? null : _posixPermissionSuggestion(delegate.path),
); );
} }
...@@ -480,9 +498,13 @@ class ErrorHandlingDirectory ...@@ -480,9 +498,13 @@ class ErrorHandlingDirectory
platform: _platform, platform: _platform,
failureMessage: failureMessage:
'Flutter failed to check for directory existence at "${delegate.path}"', 'Flutter failed to check for directory existence at "${delegate.path}"',
posixPermissionSuggestion: _posixPermissionSuggestion(delegate.parent.path),
); );
} }
String _posixPermissionSuggestion(String path) => 'Try running:\n'
' sudo chown -R \$(whoami) ${fileSystem.path.absolute(path)}';
@override @override
String toString() => delegate.toString(); String toString() => delegate.toString();
} }
...@@ -538,6 +560,7 @@ const String _kNoExecutableFound = 'The Flutter tool could not locate an executa ...@@ -538,6 +560,7 @@ const String _kNoExecutableFound = 'The Flutter tool could not locate an executa
Future<T> _run<T>(Future<T> Function() op, { Future<T> _run<T>(Future<T> Function() op, {
required Platform platform, required Platform platform,
String? failureMessage, String? failureMessage,
String? posixPermissionSuggestion,
}) async { }) async {
assert(platform != null); assert(platform != null);
try { try {
...@@ -551,14 +574,14 @@ Future<T> _run<T>(Future<T> Function() op, { ...@@ -551,14 +574,14 @@ Future<T> _run<T>(Future<T> Function() op, {
if (platform.isWindows) { if (platform.isWindows) {
_handleWindowsException(e, failureMessage, e.osError?.errorCode ?? 0); _handleWindowsException(e, failureMessage, e.osError?.errorCode ?? 0);
} else if (platform.isLinux || platform.isMacOS) { } else if (platform.isLinux || platform.isMacOS) {
_handlePosixException(e, failureMessage, e.osError?.errorCode ?? 0); _handlePosixException(e, failureMessage, e.osError?.errorCode ?? 0, posixPermissionSuggestion);
} }
rethrow; rethrow;
} on io.ProcessException catch (e) { } on io.ProcessException catch (e) {
if (platform.isWindows) { if (platform.isWindows) {
_handleWindowsException(e, failureMessage, e.errorCode); _handleWindowsException(e, failureMessage, e.errorCode);
} else if (platform.isLinux || platform.isMacOS) { } else if (platform.isLinux || platform.isMacOS) {
_handlePosixException(e, failureMessage, e.errorCode); _handlePosixException(e, failureMessage, e.errorCode, posixPermissionSuggestion);
} }
rethrow; rethrow;
} }
...@@ -567,6 +590,7 @@ Future<T> _run<T>(Future<T> Function() op, { ...@@ -567,6 +590,7 @@ Future<T> _run<T>(Future<T> Function() op, {
T _runSync<T>(T Function() op, { T _runSync<T>(T Function() op, {
required Platform platform, required Platform platform,
String? failureMessage, String? failureMessage,
String? posixPermissionSuggestion,
}) { }) {
assert(platform != null); assert(platform != null);
try { try {
...@@ -580,14 +604,14 @@ T _runSync<T>(T Function() op, { ...@@ -580,14 +604,14 @@ T _runSync<T>(T Function() op, {
if (platform.isWindows) { if (platform.isWindows) {
_handleWindowsException(e, failureMessage, e.osError?.errorCode ?? 0); _handleWindowsException(e, failureMessage, e.osError?.errorCode ?? 0);
} else if (platform.isLinux || platform.isMacOS) { } else if (platform.isLinux || platform.isMacOS) {
_handlePosixException(e, failureMessage, e.osError?.errorCode ?? 0); _handlePosixException(e, failureMessage, e.osError?.errorCode ?? 0, posixPermissionSuggestion);
} }
rethrow; rethrow;
} on io.ProcessException catch (e) { } on io.ProcessException catch (e) {
if (platform.isWindows) { if (platform.isWindows) {
_handleWindowsException(e, failureMessage, e.errorCode); _handleWindowsException(e, failureMessage, e.errorCode);
} else if (platform.isLinux || platform.isMacOS) { } else if (platform.isLinux || platform.isMacOS) {
_handlePosixException(e, failureMessage, e.errorCode); _handlePosixException(e, failureMessage, e.errorCode, posixPermissionSuggestion);
} }
rethrow; rethrow;
} }
...@@ -617,6 +641,9 @@ class ErrorHandlingProcessManager extends ProcessManager { ...@@ -617,6 +641,9 @@ class ErrorHandlingProcessManager extends ProcessManager {
return _runSync( return _runSync(
() => _delegate.canRun(executable, workingDirectory: workingDirectory), () => _delegate.canRun(executable, workingDirectory: workingDirectory),
platform: _platform, platform: _platform,
failureMessage: 'Flutter failed to run "$executable"',
posixPermissionSuggestion: 'Try running:\n'
' sudo chown -R \$(whoami) $executable && chmod u+rx $executable',
); );
} }
...@@ -695,7 +722,7 @@ class ErrorHandlingProcessManager extends ProcessManager { ...@@ -695,7 +722,7 @@ class ErrorHandlingProcessManager extends ProcessManager {
} }
} }
void _handlePosixException(Exception e, String? message, int errorCode) { void _handlePosixException(Exception e, String? message, int errorCode, String? posixPermissionSuggestion) {
// From: // From:
// https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno.h // https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno.h
// https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno-base.h // https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno-base.h
...@@ -714,10 +741,18 @@ void _handlePosixException(Exception e, String? message, int errorCode) { ...@@ -714,10 +741,18 @@ void _handlePosixException(Exception e, String? message, int errorCode) {
break; break;
case eperm: case eperm:
case eacces: case eacces:
errorMessage = final StringBuffer errorBuffer = StringBuffer();
'$message. The flutter tool cannot access the file or directory.\n' if (message != null && message.isNotEmpty) {
'Please ensure that the SDK and/or project is installed in a location ' errorBuffer.writeln('$message.');
'that has read/write permissions for the current user.'; } else {
errorBuffer.writeln('The flutter tool cannot access the file or directory.');
}
errorBuffer.writeln('Please ensure that the SDK and/or project is installed in a location '
'that has read/write permissions for the current user.');
if (posixPermissionSuggestion != null && posixPermissionSuggestion.isNotEmpty) {
errorBuffer.writeln(posixPermissionSuggestion);
}
errorMessage = errorBuffer.toString();
break; break;
default: default:
// Caller must rethrow the exception. // Caller must rethrow the exception.
......
...@@ -15,7 +15,6 @@ import 'package:flutter_tools/src/base/file_system.dart'; ...@@ -15,7 +15,6 @@ import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/platform.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:path/path.dart' as path; // flutter_ignore: package_path_import
import 'package:process/process.dart'; import 'package:process/process.dart';
import '../../src/common.dart'; import '../../src/common.dart';
...@@ -23,7 +22,6 @@ import '../../src/fake_process_manager.dart'; ...@@ -23,7 +22,6 @@ import '../../src/fake_process_manager.dart';
class MockFile extends Mock implements File {} class MockFile extends Mock implements File {}
class MockFileSystem extends Mock implements FileSystem {} class MockFileSystem extends Mock implements FileSystem {}
class MockPathContext extends Mock implements path.Context {}
class MockDirectory extends Mock implements Directory {} class MockDirectory extends Mock implements Directory {}
class MockRandomAccessFile extends Mock implements RandomAccessFile {} class MockRandomAccessFile extends Mock implements RandomAccessFile {}
...@@ -48,7 +46,11 @@ void setupWriteMocks({ ...@@ -48,7 +46,11 @@ void setupWriteMocks({
int errorCode, int errorCode,
}) { }) {
final MockFile mockFile = MockFile(); final MockFile mockFile = MockFile();
final MockDirectory mockParentDirectory = MockDirectory();
when(mockFileSystem.file(any)).thenReturn(mockFile); when(mockFileSystem.file(any)).thenReturn(mockFile);
when(mockFile.path).thenReturn('parent/file');
when(mockFile.parent).thenReturn(mockParentDirectory);
when(mockParentDirectory.path).thenReturn('parent');
when(mockFile.writeAsBytes( when(mockFile.writeAsBytes(
any, any,
mode: anyNamed('mode'), mode: anyNamed('mode'),
...@@ -88,7 +90,11 @@ void setupReadMocks({ ...@@ -88,7 +90,11 @@ void setupReadMocks({
int errorCode, int errorCode,
}) { }) {
final MockFile mockFile = MockFile(); final MockFile mockFile = MockFile();
final MockDirectory mockParentDirectory = MockDirectory();
when(mockFileSystem.file(any)).thenReturn(mockFile); when(mockFileSystem.file(any)).thenReturn(mockFile);
when(mockFile.path).thenReturn('parent/file');
when(mockFile.parent).thenReturn(mockParentDirectory);
when(mockParentDirectory.path).thenReturn('parent');
when(mockFileSystem.currentDirectory).thenThrow(FileSystemException('', '', OSError('', errorCode))); when(mockFileSystem.currentDirectory).thenThrow(FileSystemException('', '', OSError('', errorCode)));
when(mockFile.readAsStringSync( when(mockFile.readAsStringSync(
encoding: anyNamed('encoding'), encoding: anyNamed('encoding'),
...@@ -101,7 +107,12 @@ void setupDirectoryMocks({ ...@@ -101,7 +107,12 @@ void setupDirectoryMocks({
int errorCode, int errorCode,
}) { }) {
final MockDirectory mockDirectory = MockDirectory(); final MockDirectory mockDirectory = MockDirectory();
final MockDirectory mockParentDirectory = MockDirectory();
when(mockDirectory.parent).thenReturn(mockParentDirectory);
when(mockFileSystem.directory(any)).thenReturn(mockDirectory); when(mockFileSystem.directory(any)).thenReturn(mockDirectory);
when(mockDirectory.path).thenReturn('parent/directory');
when(mockDirectory.parent).thenReturn(mockParentDirectory);
when(mockParentDirectory.path).thenReturn('parent');
when(mockDirectory.createTemp(any)).thenAnswer((_) async { when(mockDirectory.createTemp(any)).thenAnswer((_) async {
throw FileSystemException('', '', OSError('', errorCode)); throw FileSystemException('', '', OSError('', errorCode));
}); });
...@@ -188,7 +199,8 @@ void main() { ...@@ -188,7 +199,8 @@ void main() {
delegate: mockFileSystem, delegate: mockFileSystem,
platform: windowsPlatform, platform: windowsPlatform,
); );
when(mockFileSystem.path).thenReturn(MockPathContext()); // For fs.path.absolute usage.
when(mockFileSystem.path).thenReturn(MemoryFileSystem.test().path);
}); });
testWithoutContext('bypasses error handling when withAllowedFailure is used', () { testWithoutContext('bypasses error handling when withAllowedFailure is used', () {
...@@ -372,6 +384,7 @@ void main() { ...@@ -372,6 +384,7 @@ void main() {
MockFileSystem mockFileSystem; MockFileSystem mockFileSystem;
ErrorHandlingFileSystem fs; ErrorHandlingFileSystem fs;
FileExceptionHandler exceptionHandler;
setUp(() { setUp(() {
mockFileSystem = MockFileSystem(); mockFileSystem = MockFileSystem();
...@@ -379,51 +392,113 @@ void main() { ...@@ -379,51 +392,113 @@ void main() {
delegate: mockFileSystem, delegate: mockFileSystem,
platform: linuxPlatform, platform: linuxPlatform,
); );
when(mockFileSystem.path).thenReturn(MockPathContext()); // For fs.path.absolute usage.
when(mockFileSystem.path).thenReturn(MemoryFileSystem.test().path);
exceptionHandler = FileExceptionHandler();
}); });
testWithoutContext('when access is denied', () async { testWithoutContext('when access is denied', () async {
setupWriteMocks( final ErrorHandlingFileSystem fileSystem = ErrorHandlingFileSystem(
mockFileSystem: mockFileSystem, delegate: MemoryFileSystem.test(opHandle: exceptionHandler.opHandle),
fs: fs, platform: linuxPlatform,
errorCode: eacces,
); );
final Directory directory = fileSystem.directory('dir')..createSync();
final File file = directory.childFile('file');
final File file = fs.file('file'); exceptionHandler.addError(
file,
const String expectedMessage = 'The flutter tool cannot access the file or directory'; FileSystemOp.create,
expect(() async => file.writeAsBytes(<int>[0]), FileSystemException('', file.path, const OSError('', eacces)),
throwsToolExit(message: expectedMessage)); );
expect(() async => file.writeAsString(''), exceptionHandler.addError(
throwsToolExit(message: expectedMessage)); file,
expect(() => file.writeAsBytesSync(<int>[0]), FileSystemOp.write,
throwsToolExit(message: expectedMessage)); FileSystemException('', file.path, const OSError('', eacces)),
expect(() => file.writeAsStringSync(''), );
throwsToolExit(message: expectedMessage)); exceptionHandler.addError(
expect(() => file.openSync(), file,
throwsToolExit(message: expectedMessage)); FileSystemOp.read,
expect(() => file.createSync(), FileSystemException('', file.path, const OSError('', eacces)),
throwsToolExit(message: expectedMessage)); );
exceptionHandler.addError(
file,
FileSystemOp.delete,
FileSystemException('', file.path, const OSError('', eacces)),
);
const String writeMessage =
'Flutter failed to write to a file at "dir/file".\n'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.\n'
'Try running:\n'
r' sudo chown -R $(whoami) /dir/file';
expect(() async => file.writeAsBytes(<int>[0]), throwsToolExit(message: writeMessage));
expect(() async => file.writeAsString(''), throwsToolExit(message: writeMessage));
expect(() => file.writeAsBytesSync(<int>[0]), throwsToolExit(message: writeMessage));
expect(() => file.writeAsStringSync(''), throwsToolExit(message: writeMessage));
const String createMessage =
'Flutter failed to create file at "dir/file".\n'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.\n'
'Try running:\n'
r' sudo chown -R $(whoami) /dir';
expect(() => file.createSync(), throwsToolExit(message: createMessage));
// Recursive does not contain the "sudo chown" suggestion.
expect(() async => file.createSync(recursive: true),
throwsA(isA<ToolExit>().having((ToolExit e) => e.message, 'message', isNot(contains('sudo chown')))));
const String readMessage =
'Flutter failed to read a file at "dir/file".\n'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.\n'
'Try running:\n'
r' sudo chown -R $(whoami) /dir/file';
expect(() => file.readAsStringSync(), throwsToolExit(message: readMessage));
}); });
testWithoutContext('when access is denied for directories', () async { testWithoutContext('when access is denied for directories', () async {
setupDirectoryMocks( final ErrorHandlingFileSystem fileSystem = ErrorHandlingFileSystem(
mockFileSystem: mockFileSystem, delegate: MemoryFileSystem.test(opHandle: exceptionHandler.opHandle),
fs: fs, platform: linuxPlatform,
errorCode: eperm,
); );
final Directory parent = fileSystem.directory('parent')..createSync();
final Directory directory = parent.childDirectory('childDir');
final Directory directory = fs.directory('file'); exceptionHandler.addError(
directory,
FileSystemOp.create,
FileSystemException('', directory.path, const OSError('', eperm)),
);
exceptionHandler.addError(
directory,
FileSystemOp.delete,
FileSystemException('', directory.path, const OSError('', eperm)),
);
const String expectedMessage = 'The flutter tool cannot access the file or directory'; const String createMessage =
'Flutter failed to create a directory at "parent/childDir".\n'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.\n'
'Try running:\n'
r' sudo chown -R $(whoami) /parent';
expect(() async => directory.create(), expect(() async => directory.create(),
throwsToolExit(message: expectedMessage)); throwsToolExit(message: createMessage));
expect(() async => directory.delete(),
throwsToolExit(message: expectedMessage));
expect(() => directory.createSync(), expect(() => directory.createSync(),
throwsToolExit(message: expectedMessage)); throwsToolExit(message: createMessage));
// Recursive does not contain the "sudo chown" suggestion.
expect(() async => directory.createSync(recursive: true),
throwsA(isA<ToolExit>().having((ToolExit e) => e.message, 'message', isNot(contains('sudo chown')))));
const String deleteMessage =
'Flutter failed to delete a directory at "parent/childDir".\n'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.\n'
'Try running:\n'
r' sudo chown -R $(whoami) /parent';
expect(() => directory.deleteSync(), expect(() => directory.deleteSync(),
throwsToolExit(message: expectedMessage)); throwsToolExit(message: deleteMessage));
expect(() async => directory.delete(),
throwsToolExit(message: deleteMessage));
// Recursive does not contain the "sudo chown" suggestion.
expect(() async => directory.deleteSync(recursive: true),
throwsA(isA<ToolExit>().having((ToolExit e) => e.message, 'message', isNot(contains('sudo chown')))));
}); });
testWithoutContext('when writing to a full device', () async { testWithoutContext('when writing to a full device', () async {
...@@ -496,6 +571,7 @@ void main() { ...@@ -496,6 +571,7 @@ void main() {
const int eacces = 13; const int eacces = 13;
MockFileSystem mockFileSystem; MockFileSystem mockFileSystem;
ErrorHandlingFileSystem fs; ErrorHandlingFileSystem fs;
FileExceptionHandler exceptionHandler;
setUp(() { setUp(() {
mockFileSystem = MockFileSystem(); mockFileSystem = MockFileSystem();
...@@ -503,49 +579,113 @@ void main() { ...@@ -503,49 +579,113 @@ void main() {
delegate: mockFileSystem, delegate: mockFileSystem,
platform: macOSPlatform, platform: macOSPlatform,
); );
when(mockFileSystem.path).thenReturn(MockPathContext()); // For fs.path.absolute usage.
when(mockFileSystem.path).thenReturn(MemoryFileSystem.test().path);
exceptionHandler = FileExceptionHandler();
}); });
testWithoutContext('when access is denied', () async { testWithoutContext('when access is denied', () async {
setupWriteMocks( final ErrorHandlingFileSystem fileSystem = ErrorHandlingFileSystem(
mockFileSystem: mockFileSystem, delegate: MemoryFileSystem.test(opHandle: exceptionHandler.opHandle),
fs: fs, platform: macOSPlatform,
errorCode: eacces,
); );
final Directory directory = fileSystem.directory('dir')..createSync();
final File file = directory.childFile('file');
final File file = fs.file('file'); exceptionHandler.addError(
file,
const String expectedMessage = 'The flutter tool cannot access the file'; FileSystemOp.create,
expect(() async => file.writeAsBytes(<int>[0]), FileSystemException('', file.path, const OSError('', eacces)),
throwsToolExit(message: expectedMessage)); );
expect(() async => file.writeAsString(''), exceptionHandler.addError(
throwsToolExit(message: expectedMessage)); file,
expect(() => file.writeAsBytesSync(<int>[0]), FileSystemOp.write,
throwsToolExit(message: expectedMessage)); FileSystemException('', file.path, const OSError('', eacces)),
expect(() => file.writeAsStringSync(''), );
throwsToolExit(message: expectedMessage)); exceptionHandler.addError(
expect(() => file.openSync(), file,
throwsToolExit(message: expectedMessage)); FileSystemOp.read,
FileSystemException('', file.path, const OSError('', eacces)),
);
exceptionHandler.addError(
file,
FileSystemOp.delete,
FileSystemException('', file.path, const OSError('', eacces)),
);
const String writeMessage =
'Flutter failed to write to a file at "dir/file".\n'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.\n'
'Try running:\n'
r' sudo chown -R $(whoami) /dir/file';
expect(() async => file.writeAsBytes(<int>[0]), throwsToolExit(message: writeMessage));
expect(() async => file.writeAsString(''), throwsToolExit(message: writeMessage));
expect(() => file.writeAsBytesSync(<int>[0]), throwsToolExit(message: writeMessage));
expect(() => file.writeAsStringSync(''), throwsToolExit(message: writeMessage));
const String createMessage =
'Flutter failed to create file at "dir/file".\n'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.\n'
'Try running:\n'
r' sudo chown -R $(whoami) /dir';
expect(() => file.createSync(), throwsToolExit(message: createMessage));
// Recursive does not contain the "sudo chown" suggestion.
expect(() async => file.createSync(recursive: true),
throwsA(isA<ToolExit>().having((ToolExit e) => e.message, 'message', isNot(contains('sudo chown')))));
const String readMessage =
'Flutter failed to read a file at "dir/file".\n'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.\n'
'Try running:\n'
r' sudo chown -R $(whoami) /dir/file';
expect(() => file.readAsStringSync(), throwsToolExit(message: readMessage));
}); });
testWithoutContext('when access is denied for directories', () async { testWithoutContext('when access is denied for directories', () async {
setupDirectoryMocks( final ErrorHandlingFileSystem fileSystem = ErrorHandlingFileSystem(
mockFileSystem: mockFileSystem, delegate: MemoryFileSystem.test(opHandle: exceptionHandler.opHandle),
fs: fs, platform: macOSPlatform,
errorCode: eperm,
); );
final Directory parent = fileSystem.directory('parent')..createSync();
final Directory directory = parent.childDirectory('childDir');
final Directory directory = fs.directory('file'); exceptionHandler.addError(
directory,
FileSystemOp.create,
FileSystemException('', directory.path, const OSError('', eperm)),
);
exceptionHandler.addError(
directory,
FileSystemOp.delete,
FileSystemException('', directory.path, const OSError('', eperm)),
);
const String expectedMessage = 'The flutter tool cannot access the file or directory'; const String createMessage =
'Flutter failed to create a directory at "parent/childDir".\n'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.\n'
'Try running:\n'
r' sudo chown -R $(whoami) /parent';
expect(() async => directory.create(), expect(() async => directory.create(),
throwsToolExit(message: expectedMessage)); throwsToolExit(message: createMessage));
expect(() async => directory.delete(), expect(() => directory.createSync(), throwsToolExit(message: createMessage));
throwsToolExit(message: expectedMessage));
expect(() => directory.createSync(), // Recursive does not contain the "sudo chown" suggestion.
throwsToolExit(message: expectedMessage)); expect(() async => directory.createSync(recursive: true),
throwsA(isA<ToolExit>().having((ToolExit e) => e.message, 'message', isNot(contains('sudo chown')))));
const String deleteMessage =
'Flutter failed to delete a directory at "parent/childDir".\n'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.\n'
'Try running:\n'
r' sudo chown -R $(whoami) /parent';
expect(() => directory.deleteSync(), expect(() => directory.deleteSync(),
throwsToolExit(message: expectedMessage)); throwsToolExit(message: deleteMessage));
expect(() async => directory.delete(),
throwsToolExit(message: deleteMessage));
// Recursive does not contain the "sudo chown" suggestion.
expect(() async => directory.deleteSync(recursive: true),
throwsA(isA<ToolExit>().having((ToolExit e) => e.message, 'message', isNot(contains('sudo chown')))));
}); });
testWithoutContext('when writing to a full device', () async { testWithoutContext('when writing to a full device', () async {
...@@ -635,7 +775,8 @@ void main() { ...@@ -635,7 +775,8 @@ void main() {
final Object firstPath = fs.path; final Object firstPath = fs.path;
fs.currentDirectory = null; fs.currentDirectory = null;
when(mockFileSystem.path).thenReturn(MockPathContext()); // For fs.path.absolute usage.
when(mockFileSystem.path).thenReturn(MemoryFileSystem.test().path);
expect(identical(firstPath, fs.path), false); expect(identical(firstPath, fs.path), false);
}); });
...@@ -689,7 +830,7 @@ void main() { ...@@ -689,7 +830,7 @@ void main() {
const int kUserMappedSectionOpened = 1224; const int kUserMappedSectionOpened = 1224;
const int kUserPermissionDenied = 5; const int kUserPermissionDenied = 5;
test('when PackageProcess throws an exception containg non-executable bits', () { testWithoutContext('when PackageProcess throws an exception containg non-executable bits', () {
final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[ final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(command: <String>['foo'], exception: ProcessPackageExecutableNotFoundException('', candidates: <String>['not-empty'])), const FakeCommand(command: <String>['foo'], exception: ProcessPackageExecutableNotFoundException('', candidates: <String>['not-empty'])),
const FakeCommand(command: <String>['foo'], exception: ProcessPackageExecutableNotFoundException('', candidates: <String>['not-empty'])), const FakeCommand(command: <String>['foo'], exception: ProcessPackageExecutableNotFoundException('', candidates: <String>['not-empty'])),
...@@ -708,7 +849,7 @@ void main() { ...@@ -708,7 +849,7 @@ void main() {
throwsToolExit(message: expectedMessage)); throwsToolExit(message: expectedMessage));
}); });
test('when PackageProcess throws an exception without containing non-executable bits', () { testWithoutContext('when PackageProcess throws an exception without containing non-executable bits', () {
final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[ final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(command: <String>['foo'], exception: ProcessPackageExecutableNotFoundException('', candidates: <String>[])), const FakeCommand(command: <String>['foo'], exception: ProcessPackageExecutableNotFoundException('', candidates: <String>[])),
const FakeCommand(command: <String>['foo'], exception: ProcessPackageExecutableNotFoundException('', candidates: <String>[])), const FakeCommand(command: <String>['foo'], exception: ProcessPackageExecutableNotFoundException('', candidates: <String>[])),
...@@ -725,7 +866,7 @@ void main() { ...@@ -725,7 +866,7 @@ void main() {
expect(() async => processManager.runSync(<String>['foo']), throwsProcessException()); expect(() async => processManager.runSync(<String>['foo']), throwsProcessException());
}); });
test('when the device is full', () { testWithoutContext('when the device is full', () {
final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[ final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', kDeviceFull)), const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', kDeviceFull)),
const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', kDeviceFull)), const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', kDeviceFull)),
...@@ -747,7 +888,7 @@ void main() { ...@@ -747,7 +888,7 @@ void main() {
throwsToolExit(message: expectedMessage)); throwsToolExit(message: expectedMessage));
}); });
test('when the file is being used by another program', () { testWithoutContext('when the file is being used by another program', () {
final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[ final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', kUserMappedSectionOpened)), const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', kUserMappedSectionOpened)),
const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', kUserMappedSectionOpened)), const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', kUserMappedSectionOpened)),
...@@ -768,7 +909,7 @@ void main() { ...@@ -768,7 +909,7 @@ void main() {
throwsToolExit(message: expectedMessage)); throwsToolExit(message: expectedMessage));
}); });
test('when permissions are denied', () { testWithoutContext('when permissions are denied', () {
final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[ final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', kUserPermissionDenied)), const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', kUserPermissionDenied)),
const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', kUserPermissionDenied)), const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', kUserPermissionDenied)),
...@@ -788,13 +929,25 @@ void main() { ...@@ -788,13 +929,25 @@ void main() {
expect(() => processManager.runSync(<String>['foo']), expect(() => processManager.runSync(<String>['foo']),
throwsToolExit(message: expectedMessage)); throwsToolExit(message: expectedMessage));
}); });
testWithoutContext('when cannot run executable', () {
final ThrowingFakeProcessManager throwingFakeProcessManager = ThrowingFakeProcessManager(const ProcessException('', <String>[], '', kUserPermissionDenied));
final ProcessManager processManager = ErrorHandlingProcessManager(
delegate: throwingFakeProcessManager,
platform: windowsPlatform,
);
const String expectedMessage = r'Flutter failed to run "C:\path\to\dart". The flutter tool cannot access the file or directory.';
expect(() async => processManager.canRun(r'C:\path\to\dart'), throwsToolExit(message: expectedMessage));
});
}); });
group('ProcessManager on linux throws tool exit', () { group('ProcessManager on linux throws tool exit', () {
const int enospc = 28; const int enospc = 28;
const int eacces = 13; const int eacces = 13;
test('when writing to a full device', () { testWithoutContext('when writing to a full device', () {
final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[ final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', enospc)), const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', enospc)),
const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', enospc)), const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', enospc)),
...@@ -815,7 +968,7 @@ void main() { ...@@ -815,7 +968,7 @@ void main() {
throwsToolExit(message: expectedMessage)); throwsToolExit(message: expectedMessage));
}); });
test('when permissions are denied', () { testWithoutContext('when permissions are denied', () {
final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[ final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', eacces)), const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', eacces)),
const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', eacces)), const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', eacces)),
...@@ -835,13 +988,29 @@ void main() { ...@@ -835,13 +988,29 @@ void main() {
expect(() => processManager.runSync(<String>['foo']), expect(() => processManager.runSync(<String>['foo']),
throwsToolExit(message: expectedMessage)); throwsToolExit(message: expectedMessage));
}); });
testWithoutContext('when cannot run executable', () {
final ThrowingFakeProcessManager throwingFakeProcessManager = ThrowingFakeProcessManager(const ProcessException('', <String>[], '', eacces));
final ProcessManager processManager = ErrorHandlingProcessManager(
delegate: throwingFakeProcessManager,
platform: linuxPlatform,
);
const String expectedMessage = 'Flutter failed to run "/path/to/dart".\n'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.\n'
'Try running:\n'
r' sudo chown -R $(whoami) /path/to/dart && chmod u+rx /path/to/dart';
expect(() async => processManager.canRun('/path/to/dart'), throwsToolExit(message: expectedMessage));
});
}); });
group('ProcessManager on macOS throws tool exit', () { group('ProcessManager on macOS throws tool exit', () {
const int enospc = 28; const int enospc = 28;
const int eacces = 13; const int eacces = 13;
test('when writing to a full device', () { testWithoutContext('when writing to a full device', () {
final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[ final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', enospc)), const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', enospc)),
const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', enospc)), const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', enospc)),
...@@ -862,7 +1031,7 @@ void main() { ...@@ -862,7 +1031,7 @@ void main() {
throwsToolExit(message: expectedMessage)); throwsToolExit(message: expectedMessage));
}); });
test('when permissions are denied', () { testWithoutContext('when permissions are denied', () {
final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[ final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', eacces)), const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', eacces)),
const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', eacces)), const FakeCommand(command: <String>['foo'], exception: ProcessException('', <String>[], '', eacces)),
...@@ -870,7 +1039,7 @@ void main() { ...@@ -870,7 +1039,7 @@ void main() {
]); ]);
final ProcessManager processManager = ErrorHandlingProcessManager( final ProcessManager processManager = ErrorHandlingProcessManager(
delegate: fakeProcessManager, delegate: fakeProcessManager,
platform: linuxPlatform, platform: macOSPlatform,
); );
const String expectedMessage = 'The flutter tool cannot access the file'; const String expectedMessage = 'The flutter tool cannot access the file';
...@@ -882,6 +1051,22 @@ void main() { ...@@ -882,6 +1051,22 @@ void main() {
expect(() => processManager.runSync(<String>['foo']), expect(() => processManager.runSync(<String>['foo']),
throwsToolExit(message: expectedMessage)); throwsToolExit(message: expectedMessage));
}); });
testWithoutContext('when cannot run executable', () {
final ThrowingFakeProcessManager throwingFakeProcessManager = ThrowingFakeProcessManager(const ProcessException('', <String>[], '', eacces));
final ProcessManager processManager = ErrorHandlingProcessManager(
delegate: throwingFakeProcessManager,
platform: macOSPlatform,
);
const String expectedMessage = 'Flutter failed to run "/path/to/dart".\n'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.\n'
'Try running:\n'
r' sudo chown -R $(whoami) /path/to/dart && chmod u+rx /path/to/dart';
expect(() async => processManager.canRun('/path/to/dart'), throwsToolExit(message: expectedMessage));
});
}); });
testWithoutContext('ErrorHandlingProcessManager delegates killPid correctly', () async { testWithoutContext('ErrorHandlingProcessManager delegates killPid correctly', () async {
...@@ -910,21 +1095,32 @@ void main() { ...@@ -910,21 +1095,32 @@ void main() {
delegate: mockFileSystem, delegate: mockFileSystem,
platform: linuxPlatform, platform: linuxPlatform,
); );
when(mockFileSystem.path).thenReturn(MockPathContext()); // For fs.path.absolute usage.
when(mockFileSystem.path).thenReturn(MemoryFileSystem.test().path);
}); });
testWithoutContext('copySync handles error if openSync on source file fails', () { testWithoutContext('copySync handles error if openSync on source file fails', () {
final MockFile source = MockFile(); final MockFile source = MockFile();
when(source.path).thenReturn('source');
when(source.openSync(mode: anyNamed('mode'))) when(source.openSync(mode: anyNamed('mode')))
.thenThrow(const FileSystemException('', '', OSError('', eaccess))); .thenThrow(const FileSystemException('', '', OSError('', eaccess)));
when(mockFileSystem.file('source')).thenReturn(source); when(mockFileSystem.file('source')).thenReturn(source);
expect(() => fileSystem.file('source').copySync('dest'), throwsToolExit()); const String expectedMessage =
'Flutter failed to copy source to dest due to source location error.\n'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.\n'
'Try running:\n'
r' sudo chown -R $(whoami) /source';
expect(() => fileSystem.file('source').copySync('dest'), throwsToolExit(message: expectedMessage));
}); });
testWithoutContext('copySync handles error if createSync on destination file fails', () { testWithoutContext('copySync handles error if createSync on destination file fails', () {
final MockFile source = MockFile(); final MockFile source = MockFile();
when(source.path).thenReturn('source');
final MockDirectory parent = MockDirectory();
when(parent.path).thenReturn('destParent');
final MockFile dest = MockFile(); final MockFile dest = MockFile();
when(dest.parent).thenReturn(parent);
when(source.openSync(mode: anyNamed('mode'))) when(source.openSync(mode: anyNamed('mode')))
.thenReturn(MockRandomAccessFile()); .thenReturn(MockRandomAccessFile());
when(dest.createSync(recursive: anyNamed('recursive'))) when(dest.createSync(recursive: anyNamed('recursive')))
...@@ -932,13 +1128,20 @@ void main() { ...@@ -932,13 +1128,20 @@ void main() {
when(mockFileSystem.file('source')).thenReturn(source); when(mockFileSystem.file('source')).thenReturn(source);
when(mockFileSystem.file('dest')).thenReturn(dest); when(mockFileSystem.file('dest')).thenReturn(dest);
expect(() => fileSystem.file('source').copySync('dest'), throwsToolExit()); const String expectedMessage =
'Flutter failed to copy source to dest due to destination location error.\n'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.';
expect(() => fileSystem.file('source').copySync('dest'), throwsToolExit(message: expectedMessage));
}); });
// dart:io is able to clobber read-only files. // dart:io is able to clobber read-only files.
testWithoutContext('copySync will copySync even if the destination is not writable', () { testWithoutContext('copySync will copySync even if the destination is not writable', () {
final MockFile source = MockFile(); final MockFile source = MockFile();
when(source.path).thenReturn('source');
final MockDirectory parent = MockDirectory();
when(parent.path).thenReturn('destParent');
final MockFile dest = MockFile(); final MockFile dest = MockFile();
when(dest.parent).thenReturn(parent);
when(source.copySync(any)).thenReturn(dest); when(source.copySync(any)).thenReturn(dest);
when(mockFileSystem.file('source')).thenReturn(source); when(mockFileSystem.file('source')).thenReturn(source);
...@@ -955,7 +1158,11 @@ void main() { ...@@ -955,7 +1158,11 @@ void main() {
testWithoutContext('copySync will copySync if there are no exceptions', () { testWithoutContext('copySync will copySync if there are no exceptions', () {
final MockFile source = MockFile(); final MockFile source = MockFile();
when(source.path).thenReturn('source');
final MockDirectory parent = MockDirectory();
when(parent.path).thenReturn('destParent');
final MockFile dest = MockFile(); final MockFile dest = MockFile();
when(dest.parent).thenReturn(parent);
when(source.copySync(any)).thenReturn(dest); when(source.copySync(any)).thenReturn(dest);
when(mockFileSystem.file('source')).thenReturn(source); when(mockFileSystem.file('source')).thenReturn(source);
...@@ -973,7 +1180,11 @@ void main() { ...@@ -973,7 +1180,11 @@ void main() {
testWithoutContext('copySync can directly copy bytes if both files can be opened but copySync fails', () { testWithoutContext('copySync can directly copy bytes if both files can be opened but copySync fails', () {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem.test(); final MemoryFileSystem memoryFileSystem = MemoryFileSystem.test();
final MockFile source = MockFile(); final MockFile source = MockFile();
when(source.path).thenReturn('source');
final MockDirectory parent = MockDirectory();
when(parent.path).thenReturn('destParent');
final MockFile dest = MockFile(); final MockFile dest = MockFile();
when(dest.parent).thenReturn(parent);
final List<int> expectedBytes = List<int>.generate(64 * 1024 + 3, (int i) => i.isEven ? 0 : 1); final List<int> expectedBytes = List<int>.generate(64 * 1024 + 3, (int i) => i.isEven ? 0 : 1);
final File memorySource = memoryFileSystem.file('source') final File memorySource = memoryFileSystem.file('source')
..writeAsBytesSync(expectedBytes); ..writeAsBytesSync(expectedBytes);
...@@ -997,7 +1208,11 @@ void main() { ...@@ -997,7 +1208,11 @@ void main() {
testWithoutContext('copySync deletes the result file if the fallback fails', () { testWithoutContext('copySync deletes the result file if the fallback fails', () {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem.test(); final MemoryFileSystem memoryFileSystem = MemoryFileSystem.test();
final MockFile source = MockFile(); final MockFile source = MockFile();
when(source.path).thenReturn('source');
final MockDirectory parent = MockDirectory();
when(parent.path).thenReturn('destParent');
final MockFile dest = MockFile(); final MockFile dest = MockFile();
when(dest.parent).thenReturn(parent);
final File memorySource = memoryFileSystem.file('source') final File memorySource = memoryFileSystem.file('source')
..createSync(); ..createSync();
final File memoryDest = memoryFileSystem.file('dest') final File memoryDest = memoryFileSystem.file('dest')
...@@ -1020,7 +1235,12 @@ void main() { ...@@ -1020,7 +1235,12 @@ void main() {
when(mockFileSystem.file('source')).thenReturn(source); when(mockFileSystem.file('source')).thenReturn(source);
when(mockFileSystem.file('dest')).thenReturn(dest); when(mockFileSystem.file('dest')).thenReturn(dest);
expect(() => fileSystem.file('source').copySync('dest'), throwsToolExit()); const String expectedMessage =
'Flutter failed to copy source to dest due to unknown error.\n'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.\n'
'Try running:\n'
r' sudo chown -R $(whoami) /source /destParent';
expect(() => fileSystem.file('source').copySync('dest'), throwsToolExit(message: expectedMessage));
verify(dest.deleteSync(recursive: true)).called(1); verify(dest.deleteSync(recursive: true)).called(1);
}); });
...@@ -1036,3 +1256,14 @@ class FakeSignalProcessManager extends Fake implements ProcessManager { ...@@ -1036,3 +1256,14 @@ class FakeSignalProcessManager extends Fake implements ProcessManager {
return true; return true;
} }
} }
class ThrowingFakeProcessManager extends Fake implements ProcessManager {
ThrowingFakeProcessManager(Exception exception) : _exception = exception;
final Exception _exception;
@override
bool canRun(dynamic executable, {String workingDirectory}) {
throw _exception;
}
}
...@@ -104,8 +104,7 @@ void main() { ...@@ -104,8 +104,7 @@ void main() {
config = Config.createForTesting(file, bufferLogger); config = Config.createForTesting(file, bufferLogger);
expect(bufferLogger.errorText, contains('Could not read preferences in testfile')); expect(bufferLogger.errorText, contains('Could not read preferences in testfile'));
// Also contains original error message: expect(bufferLogger.errorText, contains(r'sudo chown -R $(whoami) /testfile'));
expect(bufferLogger.errorText, contains('The flutter tool cannot access the file or directory'));
}); });
testWithoutContext('Config in home dir is used if it exists', () { testWithoutContext('Config in home dir is used if it exists', () {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment