blob: 25769af3ffb6e85d1181dfb632549fb6d810508f [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
import 'dart:io' as io; // flutter_ignore: dart_io_import;
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/error_handling_io.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import '../../src/common.dart';
import '../../src/fake_process_manager.dart';
class MockFile extends Mock implements File {}
class MockFileSystem extends Mock implements FileSystem {}
class MockDirectory extends Mock implements Directory {}
class MockRandomAccessFile extends Mock implements RandomAccessFile {}
final Platform windowsPlatform = FakePlatform(
operatingSystem: 'windows',
environment: <String, String>{}
);
final Platform linuxPlatform = FakePlatform(
operatingSystem: 'linux',
environment: <String, String>{}
);
final Platform macOSPlatform = FakePlatform(
operatingSystem: 'macos',
environment: <String, String>{}
);
void setupWriteMocks({
FileSystem mockFileSystem,
ErrorHandlingFileSystem fs,
int errorCode,
}) {
final MockFile mockFile = MockFile();
final MockDirectory mockParentDirectory = MockDirectory();
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(
any,
mode: anyNamed('mode'),
flush: anyNamed('flush'),
)).thenAnswer((_) async {
throw FileSystemException('', '', OSError('', errorCode));
});
when(mockFile.writeAsString(
any,
mode: anyNamed('mode'),
encoding: anyNamed('encoding'),
flush: anyNamed('flush'),
)).thenAnswer((_) async {
throw FileSystemException('', '', OSError('', errorCode));
});
when(mockFile.writeAsBytesSync(
any,
mode: anyNamed('mode'),
flush: anyNamed('flush'),
)).thenThrow(FileSystemException('', '', OSError('', errorCode)));
when(mockFile.writeAsStringSync(
any,
mode: anyNamed('mode'),
encoding: anyNamed('encoding'),
flush: anyNamed('flush'),
)).thenThrow(FileSystemException('', '', OSError('', errorCode)));
when(mockFile.openSync(
mode: anyNamed('mode'),
)).thenThrow(FileSystemException('', '', OSError('', errorCode)));
when(mockFile.createSync(recursive: anyNamed('recursive')))
.thenThrow(FileSystemException('', '', OSError('', errorCode)));
}
void setupReadMocks({
FileSystem mockFileSystem,
ErrorHandlingFileSystem fs,
int errorCode,
}) {
final MockFile mockFile = MockFile();
final MockDirectory mockParentDirectory = MockDirectory();
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(mockFile.readAsStringSync(
encoding: anyNamed('encoding'),
)).thenThrow(FileSystemException('', '', OSError('', errorCode)));
}
void setupDirectoryMocks({
FileSystem mockFileSystem,
ErrorHandlingFileSystem fs,
int errorCode,
}) {
final MockDirectory mockDirectory = MockDirectory();
final MockDirectory mockParentDirectory = MockDirectory();
when(mockDirectory.parent).thenReturn(mockParentDirectory);
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 {
throw FileSystemException('', '', OSError('', errorCode));
});
when(mockDirectory.createTempSync(any))
.thenThrow(FileSystemException('', '', OSError('', errorCode)));
when(mockDirectory.createSync(recursive: anyNamed('recursive')))
.thenThrow(FileSystemException('', '', OSError('', errorCode)));
when(mockDirectory.create())
.thenThrow(FileSystemException('', '', OSError('', errorCode)));
when(mockDirectory.createSync())
.thenThrow(FileSystemException('', '', OSError('', errorCode)));
when(mockDirectory.delete())
.thenThrow(FileSystemException('', '', OSError('', errorCode)));
when(mockDirectory.deleteSync())
.thenThrow(FileSystemException('', '', OSError('', errorCode)));
when(mockDirectory.existsSync())
.thenThrow(FileSystemException('', '', OSError('', errorCode)));
}
void main() {
testWithoutContext('deleteIfExists does not delete if file does not exist', () {
final File file = MockFile();
when(file.existsSync()).thenReturn(false);
expect(ErrorHandlingFileSystem.deleteIfExists(file), false);
});
testWithoutContext('deleteIfExists deletes if file exists', () {
final File file = MockFile();
when(file.existsSync()).thenReturn(true);
expect(ErrorHandlingFileSystem.deleteIfExists(file), true);
});
testWithoutContext('deleteIfExists handles separate program deleting file', () {
final File file = MockFile();
bool exists = true;
// Return true for the first call, false for any subsequent calls.
when(file.existsSync()).thenAnswer((Invocation _) {
final bool result = exists;
exists = false;
return result;
});
when(file.deleteSync(recursive: false))
.thenThrow(const FileSystemException('', '', OSError('', 2)));
expect(ErrorHandlingFileSystem.deleteIfExists(file), true);
});
testWithoutContext('deleteIfExists throws tool exit if file exists on read-only volume', () {
final File file = MockFile();
when(file.existsSync()).thenReturn(true);
when(file.deleteSync(recursive: false))
.thenThrow(const FileSystemException('', '', OSError('', 2)));
expect(() => ErrorHandlingFileSystem.deleteIfExists(file), throwsToolExit());
});
testWithoutContext('deleteIfExists does not tool exit if file exists on read-only '
'volume and it is run under noExitOnFailure', () {
final File file = MockFile();
when(file.existsSync()).thenReturn(true);
when(file.deleteSync(recursive: false))
.thenThrow(const FileSystemException('', '', OSError('', 2)));
expect(() {
ErrorHandlingFileSystem.noExitOnFailure(() {
ErrorHandlingFileSystem.deleteIfExists(file);
});
}, throwsFileSystemException());
});
group('throws ToolExit on Windows', () {
const int kDeviceFull = 112;
const int kUserMappedSectionOpened = 1224;
const int kUserPermissionDenied = 5;
const int kFatalDeviceHardwareError = 483;
MockFileSystem mockFileSystem;
ErrorHandlingFileSystem fs;
setUp(() {
mockFileSystem = MockFileSystem();
fs = ErrorHandlingFileSystem(
delegate: mockFileSystem,
platform: windowsPlatform,
);
// For fs.path.absolute usage.
when(mockFileSystem.path).thenReturn(MemoryFileSystem.test().path);
});
testWithoutContext('bypasses error handling when withAllowedFailure is used', () {
setupWriteMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: kUserPermissionDenied,
);
final File file = fs.file('file');
expect(() => ErrorHandlingFileSystem.noExitOnFailure(
() => file.writeAsStringSync('')), throwsException);
// nesting does not unconditionally re-enable errors.
expect(() {
ErrorHandlingFileSystem.noExitOnFailure(() {
ErrorHandlingFileSystem.noExitOnFailure(() { });
file.writeAsStringSync('');
});
}, throwsException);
// Check that state does not leak.
expect(() => file.writeAsStringSync(''), throwsToolExit());
});
testWithoutContext('when access is denied', () async {
setupWriteMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: kUserPermissionDenied,
);
final File file = fs.file('file');
const String expectedMessage = 'The flutter tool cannot access the file';
expect(() async => file.writeAsBytes(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() async => file.writeAsString(''),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsBytesSync(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsStringSync(''),
throwsToolExit(message: expectedMessage));
expect(() => file.openSync(),
throwsToolExit(message: expectedMessage));
expect(() => file.createSync(),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when writing to a full device', () async {
setupWriteMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: kDeviceFull,
);
final File file = fs.file('file');
const String expectedMessage = 'The target device is full';
expect(() async => file.writeAsBytes(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() async => file.writeAsString(''),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsBytesSync(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsStringSync(''),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when the file is being used by another program', () async {
setupWriteMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: kUserMappedSectionOpened,
);
final File file = fs.file('file');
const String expectedMessage = 'The file is being used by another program';
expect(() async => file.writeAsBytes(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() async => file.writeAsString(''),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsBytesSync(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsStringSync(''),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when the device driver has a fatal error', () async {
setupWriteMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: kFatalDeviceHardwareError,
);
final File file = fs.file('file');
const String expectedMessage = 'There is a problem with the device driver '
'that this file or directory is stored on';
expect(() async => file.writeAsBytes(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() async => file.writeAsString(''),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsBytesSync(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsStringSync(''),
throwsToolExit(message: expectedMessage));
expect(() => file.openSync(),
throwsToolExit(message: expectedMessage));
expect(() => file.createSync(),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when creating a temporary dir on a full device', () async {
setupDirectoryMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: kDeviceFull,
);
final Directory directory = fs.directory('directory');
const String expectedMessage = 'The target device is full';
expect(() async => directory.createTemp('prefix'),
throwsToolExit(message: expectedMessage));
expect(() => directory.createTempSync('prefix'),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when creating a directory with permission issues', () async {
setupDirectoryMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: kUserPermissionDenied,
);
final Directory directory = fs.directory('directory');
const String expectedMessage = 'Flutter failed to create a directory at';
expect(() => directory.createSync(recursive: true),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when checking for directory existence with permission issues', () async {
setupDirectoryMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: kUserPermissionDenied,
);
final Directory directory = fs.directory('directory');
const String expectedMessage = 'Flutter failed to check for directory existence at';
expect(() => directory.existsSync(),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('When reading from a file or directory without permission', () {
setupReadMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: kUserPermissionDenied,
);
final File file = fs.file('file');
const String expectedMessage = 'Flutter failed to read a file at';
expect(() => file.readAsStringSync(),
throwsToolExit(message: expectedMessage));
expect(() => fs.currentDirectory,
throwsToolExit(message: 'The flutter tool cannot access the file or directory'));
});
});
group('throws ToolExit on Linux', () {
const int eperm = 1;
const int enospc = 28;
const int eacces = 13;
MockFileSystem mockFileSystem;
ErrorHandlingFileSystem fs;
FileExceptionHandler exceptionHandler;
setUp(() {
mockFileSystem = MockFileSystem();
fs = ErrorHandlingFileSystem(
delegate: mockFileSystem,
platform: linuxPlatform,
);
// For fs.path.absolute usage.
when(mockFileSystem.path).thenReturn(MemoryFileSystem.test().path);
exceptionHandler = FileExceptionHandler();
});
testWithoutContext('when access is denied', () async {
final ErrorHandlingFileSystem fileSystem = ErrorHandlingFileSystem(
delegate: MemoryFileSystem.test(opHandle: exceptionHandler.opHandle),
platform: linuxPlatform,
);
final Directory directory = fileSystem.directory('dir')..createSync();
final File file = directory.childFile('file');
exceptionHandler.addError(
file,
FileSystemOp.create,
FileSystemException('', file.path, const OSError('', eacces)),
);
exceptionHandler.addError(
file,
FileSystemOp.write,
FileSystemException('', file.path, const OSError('', eacces)),
);
exceptionHandler.addError(
file,
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 {
final ErrorHandlingFileSystem fileSystem = ErrorHandlingFileSystem(
delegate: MemoryFileSystem.test(opHandle: exceptionHandler.opHandle),
platform: linuxPlatform,
);
final Directory parent = fileSystem.directory('parent')..createSync();
final Directory directory = parent.childDirectory('childDir');
exceptionHandler.addError(
directory,
FileSystemOp.create,
FileSystemException('', directory.path, const OSError('', eperm)),
);
exceptionHandler.addError(
directory,
FileSystemOp.delete,
FileSystemException('', directory.path, const OSError('', eperm)),
);
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(),
throwsToolExit(message: createMessage));
expect(() => directory.createSync(),
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(),
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 {
setupWriteMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: enospc,
);
final File file = fs.file('file');
const String expectedMessage = 'The target device is full';
expect(() async => file.writeAsBytes(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() async => file.writeAsString(''),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsBytesSync(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsStringSync(''),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when creating a temporary dir on a full device', () async {
setupDirectoryMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: enospc,
);
final Directory directory = fs.directory('directory');
const String expectedMessage = 'The target device is full';
expect(() async => directory.createTemp('prefix'),
throwsToolExit(message: expectedMessage));
expect(() => directory.createTempSync('prefix'),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when checking for directory existence with permission issues', () async {
setupDirectoryMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: eacces,
);
final Directory directory = fs.directory('directory');
const String expectedMessage = 'Flutter failed to check for directory existence at';
expect(() => directory.existsSync(),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('When the current working directory disappears', () async {
setupReadMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: kSystemCannotFindFile,
);
expect(() => fs.currentDirectory, throwsToolExit(message: 'Unable to read current working directory'));
// Error is not caught by other operations.
expect(() => fs.file('foo').readAsStringSync(), throwsFileSystemException(kSystemCannotFindFile));
});
});
group('throws ToolExit on macOS', () {
const int eperm = 1;
const int enospc = 28;
const int eacces = 13;
MockFileSystem mockFileSystem;
ErrorHandlingFileSystem fs;
FileExceptionHandler exceptionHandler;
setUp(() {
mockFileSystem = MockFileSystem();
fs = ErrorHandlingFileSystem(
delegate: mockFileSystem,
platform: macOSPlatform,
);
// For fs.path.absolute usage.
when(mockFileSystem.path).thenReturn(MemoryFileSystem.test().path);
exceptionHandler = FileExceptionHandler();
});
testWithoutContext('when access is denied', () async {
final ErrorHandlingFileSystem fileSystem = ErrorHandlingFileSystem(
delegate: MemoryFileSystem.test(opHandle: exceptionHandler.opHandle),
platform: macOSPlatform,
);
final Directory directory = fileSystem.directory('dir')..createSync();
final File file = directory.childFile('file');
exceptionHandler.addError(
file,
FileSystemOp.create,
FileSystemException('', file.path, const OSError('', eacces)),
);
exceptionHandler.addError(
file,
FileSystemOp.write,
FileSystemException('', file.path, const OSError('', eacces)),
);
exceptionHandler.addError(
file,
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 {
final ErrorHandlingFileSystem fileSystem = ErrorHandlingFileSystem(
delegate: MemoryFileSystem.test(opHandle: exceptionHandler.opHandle),
platform: macOSPlatform,
);
final Directory parent = fileSystem.directory('parent')..createSync();
final Directory directory = parent.childDirectory('childDir');
exceptionHandler.addError(
directory,
FileSystemOp.create,
FileSystemException('', directory.path, const OSError('', eperm)),
);
exceptionHandler.addError(
directory,
FileSystemOp.delete,
FileSystemException('', directory.path, const OSError('', eperm)),
);
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(),
throwsToolExit(message: createMessage));
expect(() => directory.createSync(), 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(),
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 {
setupWriteMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: enospc,
);
final File file = fs.file('file');
const String expectedMessage = 'The target device is full';
expect(() async => file.writeAsBytes(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() async => file.writeAsString(''),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsBytesSync(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsStringSync(''),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when creating a temporary dir on a full device', () async {
setupDirectoryMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: enospc,
);
final Directory directory = fs.directory('directory');
const String expectedMessage = 'The target device is full';
expect(() async => directory.createTemp('prefix'),
throwsToolExit(message: expectedMessage));
expect(() => directory.createTempSync('prefix'),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when checking for directory existence with permission issues', () async {
setupDirectoryMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: eacces,
);
final Directory directory = fs.directory('directory');
const String expectedMessage = 'Flutter failed to check for directory existence at';
expect(() => directory.existsSync(),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('When reading from a file or directory without permission', () {
setupReadMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: eacces,
);
final File file = fs.file('file');
const String expectedMessage = 'Flutter failed to read a file at';
expect(() => file.readAsStringSync(),
throwsToolExit(message: expectedMessage));
expect(() => fs.currentDirectory,
throwsToolExit(message: 'The flutter tool cannot access the file or directory'));
});
});
testWithoutContext('Caches path context correctly', () {
final MockFileSystem mockFileSystem = MockFileSystem();
final FileSystem fs = ErrorHandlingFileSystem(
delegate: mockFileSystem,
platform: const LocalPlatform(),
);
expect(identical(fs.path, fs.path), true);
});
testWithoutContext('Clears cache when CWD changes', () {
final MockFileSystem mockFileSystem = MockFileSystem();
final FileSystem fs = ErrorHandlingFileSystem(
delegate: mockFileSystem,
platform: const LocalPlatform(),
);
final Object firstPath = fs.path;
fs.currentDirectory = null;
// For fs.path.absolute usage.
when(mockFileSystem.path).thenReturn(MemoryFileSystem.test().path);
expect(identical(firstPath, fs.path), false);
});
group('toString() gives toString() of delegate', () {
testWithoutContext('ErrorHandlingFileSystem', () {
final MockFileSystem mockFileSystem = MockFileSystem();
final FileSystem fs = ErrorHandlingFileSystem(
delegate: mockFileSystem,
platform: const LocalPlatform(),
);
expect(mockFileSystem.toString(), isNotNull);
expect(fs.toString(), equals(mockFileSystem.toString()));
});
testWithoutContext('ErrorHandlingFile', () {
final MockFileSystem mockFileSystem = MockFileSystem();
final FileSystem fs = ErrorHandlingFileSystem(
delegate: mockFileSystem,
platform: const LocalPlatform(),
);
final MockFile mockFile = MockFile();
when(mockFileSystem.file(any)).thenReturn(mockFile);
expect(mockFile.toString(), isNotNull);
expect(fs.file('file').toString(), equals(mockFile.toString()));
});
testWithoutContext('ErrorHandlingDirectory', () {
final MockFileSystem mockFileSystem = MockFileSystem();
final FileSystem fs = ErrorHandlingFileSystem(
delegate: mockFileSystem,
platform: const LocalPlatform(),
);
final MockDirectory mockDirectory = MockDirectory();
when(mockFileSystem.directory(any)).thenReturn(mockDirectory);
expect(mockDirectory.toString(), isNotNull);
expect(fs.directory('directory').toString(), equals(mockDirectory.toString()));
when(mockFileSystem.currentDirectory).thenReturn(mockDirectory);
expect(fs.currentDirectory.toString(), equals(mockDirectory.toString()));
expect(fs.currentDirectory, isA<ErrorHandlingDirectory>());
});
});
group('ProcessManager on windows throws tool exit', () {
const int kDeviceFull = 112;
const int kUserMappedSectionOpened = 1224;
const int kUserPermissionDenied = 5;
testWithoutContext('when PackageProcess throws an exception containg non-executable bits', () {
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'])),
]);
final ProcessManager processManager = ErrorHandlingProcessManager(
delegate: fakeProcessManager,
platform: windowsPlatform,
);
const String expectedMessage = 'The Flutter tool could not locate an executable with suitable permissions';
expect(() async => processManager.start(<String>['foo']),
throwsToolExit(message: expectedMessage));
expect(() async => processManager.runSync(<String>['foo']),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when PackageProcess throws an exception without containing non-executable bits', () {
final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(command: <String>['foo'], exception: ProcessPackageExecutableNotFoundException('', candidates: <String>[])),
const FakeCommand(command: <String>['foo'], exception: ProcessPackageExecutableNotFoundException('', candidates: <String>[])),
]);
final ProcessManager processManager = ErrorHandlingProcessManager(
delegate: fakeProcessManager,
platform: windowsPlatform,
);
// If there were no located executables treat this as a programming error and rethrow the original
// exception.
expect(() async => processManager.start(<String>['foo']), throwsProcessException());
expect(() async => processManager.runSync(<String>['foo']), throwsProcessException());
});
testWithoutContext('when the device is full', () {
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)),
]);
final ProcessManager processManager = ErrorHandlingProcessManager(
delegate: fakeProcessManager,
platform: windowsPlatform,
);
const String expectedMessage = 'The target device is full';
expect(() async => processManager.start(<String>['foo']),
throwsToolExit(message: expectedMessage));
expect(() async => processManager.run(<String>['foo']),
throwsToolExit(message: expectedMessage));
expect(() => processManager.runSync(<String>['foo']),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when the file is being used by another program', () {
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)),
]);
final ProcessManager processManager = ErrorHandlingProcessManager(
delegate: fakeProcessManager,
platform: windowsPlatform,
);
const String expectedMessage = 'The file is being used by another program';
expect(() async => processManager.start(<String>['foo']),
throwsToolExit(message: expectedMessage));
expect(() async => processManager.run(<String>['foo']),
throwsToolExit(message: expectedMessage));
expect(() => processManager.runSync(<String>['foo']),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when permissions are denied', () {
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)),
]);
final ProcessManager processManager = ErrorHandlingProcessManager(
delegate: fakeProcessManager,
platform: windowsPlatform,
);
const String expectedMessage = 'The flutter tool cannot access the file';
expect(() async => processManager.start(<String>['foo']),
throwsToolExit(message: expectedMessage));
expect(() async => processManager.run(<String>['foo']),
throwsToolExit(message: expectedMessage));
expect(() => processManager.runSync(<String>['foo']),
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', () {
const int enospc = 28;
const int eacces = 13;
testWithoutContext('when writing to a full device', () {
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)),
]);
final ProcessManager processManager = ErrorHandlingProcessManager(
delegate: fakeProcessManager,
platform: linuxPlatform,
);
const String expectedMessage = 'The target device is full';
expect(() async => processManager.start(<String>['foo']),
throwsToolExit(message: expectedMessage));
expect(() async => processManager.run(<String>['foo']),
throwsToolExit(message: expectedMessage));
expect(() => processManager.runSync(<String>['foo']),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when permissions are denied', () {
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)),
]);
final ProcessManager processManager = ErrorHandlingProcessManager(
delegate: fakeProcessManager,
platform: linuxPlatform,
);
const String expectedMessage = 'The flutter tool cannot access the file';
expect(() async => processManager.start(<String>['foo']),
throwsToolExit(message: expectedMessage));
expect(() async => processManager.run(<String>['foo']),
throwsToolExit(message: expectedMessage));
expect(() => processManager.runSync(<String>['foo']),
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', () {
const int enospc = 28;
const int eacces = 13;
testWithoutContext('when writing to a full device', () {
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)),
]);
final ProcessManager processManager = ErrorHandlingProcessManager(
delegate: fakeProcessManager,
platform: macOSPlatform,
);
const String expectedMessage = 'The target device is full';
expect(() async => processManager.start(<String>['foo']),
throwsToolExit(message: expectedMessage));
expect(() async => processManager.run(<String>['foo']),
throwsToolExit(message: expectedMessage));
expect(() => processManager.runSync(<String>['foo']),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when permissions are denied', () {
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)),
]);
final ProcessManager processManager = ErrorHandlingProcessManager(
delegate: fakeProcessManager,
platform: macOSPlatform,
);
const String expectedMessage = 'The flutter tool cannot access the file';
expect(() async => processManager.start(<String>['foo']),
throwsToolExit(message: expectedMessage));
expect(() async => processManager.run(<String>['foo']),
throwsToolExit(message: expectedMessage));
expect(() => processManager.runSync(<String>['foo']),
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 {
final FakeSignalProcessManager fakeProcessManager = FakeSignalProcessManager();
final ProcessManager processManager = ErrorHandlingProcessManager(
delegate: fakeProcessManager,
platform: linuxPlatform,
);
expect(processManager.killPid(1, io.ProcessSignal.sigterm), true);
expect(processManager.killPid(3, io.ProcessSignal.sigkill), true);
expect(fakeProcessManager.killedProcesses, <int, io.ProcessSignal>{
1: io.ProcessSignal.sigterm,
3: io.ProcessSignal.sigkill,
});
});
group('CopySync' , () {
const int eaccess = 13;
MockFileSystem mockFileSystem;
ErrorHandlingFileSystem fileSystem;
setUp(() {
mockFileSystem = MockFileSystem();
fileSystem = ErrorHandlingFileSystem(
delegate: mockFileSystem,
platform: linuxPlatform,
);
// For fs.path.absolute usage.
when(mockFileSystem.path).thenReturn(MemoryFileSystem.test().path);
});
testWithoutContext('copySync handles error if openSync on source file fails', () {
final MockFile source = MockFile();
when(source.path).thenReturn('source');
when(source.openSync(mode: anyNamed('mode')))
.thenThrow(const FileSystemException('', '', OSError('', eaccess)));
when(mockFileSystem.file('source')).thenReturn(source);
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', () {
final MockFile source = MockFile();
when(source.path).thenReturn('source');
final MockDirectory parent = MockDirectory();
when(parent.path).thenReturn('destParent');
final MockFile dest = MockFile();
when(dest.parent).thenReturn(parent);
when(source.openSync(mode: anyNamed('mode')))
.thenReturn(MockRandomAccessFile());
when(dest.createSync(recursive: anyNamed('recursive')))
.thenThrow(const FileSystemException('', '', OSError('', eaccess)));
when(mockFileSystem.file('source')).thenReturn(source);
when(mockFileSystem.file('dest')).thenReturn(dest);
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.
testWithoutContext('copySync will copySync even if the destination is not writable', () {
final MockFile source = MockFile();
when(source.path).thenReturn('source');
final MockDirectory parent = MockDirectory();
when(parent.path).thenReturn('destParent');
final MockFile dest = MockFile();
when(dest.parent).thenReturn(parent);
when(source.copySync(any)).thenReturn(dest);
when(mockFileSystem.file('source')).thenReturn(source);
when(source.openSync(mode: anyNamed('mode')))
.thenReturn(MockRandomAccessFile());
when(mockFileSystem.file('dest')).thenReturn(dest);
when(dest.openSync(mode: FileMode.writeOnly))
.thenThrow(const FileSystemException('', '', OSError('', eaccess)));
fileSystem.file('source').copySync('dest');
verify(source.copySync('dest')).called(1);
});
testWithoutContext('copySync will copySync if there are no exceptions', () {
final MockFile source = MockFile();
when(source.path).thenReturn('source');
final MockDirectory parent = MockDirectory();
when(parent.path).thenReturn('destParent');
final MockFile dest = MockFile();
when(dest.parent).thenReturn(parent);
when(source.copySync(any)).thenReturn(dest);
when(mockFileSystem.file('source')).thenReturn(source);
when(source.openSync(mode: anyNamed('mode')))
.thenReturn(MockRandomAccessFile());
when(mockFileSystem.file('dest')).thenReturn(dest);
when(dest.openSync(mode: anyNamed('mode')))
.thenReturn(MockRandomAccessFile());
fileSystem.file('source').copySync('dest');
verify(source.copySync('dest')).called(1);
});
testWithoutContext('copySync can directly copy bytes if both files can be opened but copySync fails', () {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem.test();
final MockFile source = MockFile();
when(source.path).thenReturn('source');
final MockDirectory parent = MockDirectory();
when(parent.path).thenReturn('destParent');
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 File memorySource = memoryFileSystem.file('source')
..writeAsBytesSync(expectedBytes);
final File memoryDest = memoryFileSystem.file('dest')
..createSync();
when(source.copySync(any))
.thenThrow(const FileSystemException('', '', OSError('', eaccess)));
when(source.openSync(mode: anyNamed('mode')))
.thenAnswer((Invocation invocation) => memorySource.openSync(mode: invocation.namedArguments[#mode] as FileMode));
when(dest.openSync(mode: anyNamed('mode')))
.thenAnswer((Invocation invocation) => memoryDest.openSync(mode: invocation.namedArguments[#mode] as FileMode));
when(mockFileSystem.file('source')).thenReturn(source);
when(mockFileSystem.file('dest')).thenReturn(dest);
fileSystem.file('source').copySync('dest');
expect(memoryDest.readAsBytesSync(), expectedBytes);
});
testWithoutContext('copySync deletes the result file if the fallback fails', () {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem.test();
final MockFile source = MockFile();
when(source.path).thenReturn('source');
final MockDirectory parent = MockDirectory();
when(parent.path).thenReturn('destParent');
final MockFile dest = MockFile();
when(dest.parent).thenReturn(parent);
final File memorySource = memoryFileSystem.file('source')
..createSync();
final File memoryDest = memoryFileSystem.file('dest')
..createSync();
int calledCount = 0;
when(dest.existsSync()).thenReturn(true);
when(source.copySync(any))
.thenThrow(const FileSystemException('', '', OSError('', eaccess)));
when(source.openSync(mode: anyNamed('mode')))
.thenAnswer((Invocation invocation) {
if (calledCount == 1) {
throw const FileSystemException('', '', OSError('', eaccess));
}
calledCount += 1;
return memorySource.openSync(mode: invocation.namedArguments[#mode] as FileMode);
});
when(dest.openSync(mode: anyNamed('mode')))
.thenAnswer((Invocation invocation) => memoryDest.openSync(mode: invocation.namedArguments[#mode] as FileMode));
when(mockFileSystem.file('source')).thenReturn(source);
when(mockFileSystem.file('dest')).thenReturn(dest);
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);
});
});
}
class FakeSignalProcessManager extends Fake implements ProcessManager {
final Map<int, io.ProcessSignal> killedProcesses = <int, io.ProcessSignal>{};
@override
bool killPid(int pid, [io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
killedProcesses[pid] = signal;
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;
}
}