Unverified Commit 584e4f25 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] cleanup to devfs Operations (#58332)

removes globals from devFS operations and cleans up testing to be context free. Removes unused and unnecessarily temp directory in testing.
parent bb6c1e05
...@@ -12,13 +12,13 @@ import 'asset.dart'; ...@@ -12,13 +12,13 @@ import 'asset.dart';
import 'base/context.dart'; import 'base/context.dart';
import 'base/file_system.dart'; import 'base/file_system.dart';
import 'base/io.dart'; import 'base/io.dart';
import 'base/logger.dart';
import 'base/net.dart'; import 'base/net.dart';
import 'base/os.dart'; import 'base/os.dart';
import 'build_info.dart'; import 'build_info.dart';
import 'bundle.dart'; import 'bundle.dart';
import 'compile.dart'; import 'compile.dart';
import 'convert.dart' show base64, utf8; import 'convert.dart' show base64, utf8;
import 'globals.dart' as globals;
import 'vmservice.dart'; import 'vmservice.dart';
class DevFSConfig { class DevFSConfig {
...@@ -52,9 +52,6 @@ abstract class DevFSContent { ...@@ -52,9 +52,6 @@ abstract class DevFSContent {
) { ) {
return osUtils.gzipLevel1Stream(contentsAsStream()); return osUtils.gzipLevel1Stream(contentsAsStream());
} }
/// Return the list of files this content depends on.
List<String> get fileDependencies => <String>[];
} }
// File content to be copied to the device. // File content to be copied to the device.
...@@ -71,7 +68,7 @@ class DevFSFileContent extends DevFSContent { ...@@ -71,7 +68,7 @@ class DevFSFileContent extends DevFSContent {
} }
if (file is Link) { if (file is Link) {
// The link target. // The link target.
return globals.fs.file(file.resolveSymbolicLinksSync()); return file.fileSystem.file(file.resolveSymbolicLinksSync());
} }
return file as File; return file as File;
} }
...@@ -92,7 +89,7 @@ class DevFSFileContent extends DevFSContent { ...@@ -92,7 +89,7 @@ class DevFSFileContent extends DevFSContent {
if (_fileStat != null && _fileStat.type == FileSystemEntityType.link) { if (_fileStat != null && _fileStat.type == FileSystemEntityType.link) {
// Resolve, stat, and maybe cache the symlink target. // Resolve, stat, and maybe cache the symlink target.
final String resolved = file.resolveSymbolicLinksSync(); final String resolved = file.resolveSymbolicLinksSync();
final File linkTarget = globals.fs.file(resolved); final File linkTarget = file.fileSystem.file(resolved);
// Stat the link target. // Stat the link target.
final FileStat fileStat = linkTarget.statSync(); final FileStat fileStat = linkTarget.statSync();
if (fileStat.type == FileSystemEntityType.notFound) { if (fileStat.type == FileSystemEntityType.notFound) {
...@@ -102,14 +99,8 @@ class DevFSFileContent extends DevFSContent { ...@@ -102,14 +99,8 @@ class DevFSFileContent extends DevFSContent {
_linkTarget = linkTarget; _linkTarget = linkTarget;
} }
} }
if (_fileStat == null) {
globals.printError('Unable to get status of file "${file.path}": file not found.');
}
} }
@override
List<String> get fileDependencies => <String>[_getFile().path];
@override @override
bool get isModified { bool get isModified {
final FileStat _oldFileStat = _fileStat; final FileStat _oldFileStat = _fileStat;
...@@ -211,37 +202,6 @@ class DevFSStringContent extends DevFSByteContent { ...@@ -211,37 +202,6 @@ class DevFSStringContent extends DevFSByteContent {
} }
} }
/// Abstract DevFS operations interface.
abstract class DevFSOperations {
Future<Uri> create(String fsName);
Future<dynamic> destroy(String fsName);
Future<dynamic> writeFile(String fsName, Uri deviceUri, DevFSContent content);
}
/// An implementation of [DevFSOperations] that speaks to the
/// vm service.
class ServiceProtocolDevFSOperations implements DevFSOperations {
ServiceProtocolDevFSOperations(this.vmService);
final vm_service.VmService vmService;
@override
Future<Uri> create(String fsName) async {
final vm_service.Response response = await vmService.createDevFS(fsName);
return Uri.parse(response.json['uri'] as String);
}
@override
Future<dynamic> destroy(String fsName) async {
await vmService.deleteDevFS(fsName);
}
@override
Future<dynamic> writeFile(String fsName, Uri deviceUri, DevFSContent content) async {
throw UnsupportedError('Use the HTTP devFS api.');
}
}
class DevFSException implements Exception { class DevFSException implements Exception {
DevFSException(this.message, [this.error, this.stackTrace]); DevFSException(this.message, [this.error, this.stackTrace]);
final String message; final String message;
...@@ -254,17 +214,20 @@ class _DevFSHttpWriter { ...@@ -254,17 +214,20 @@ class _DevFSHttpWriter {
this.fsName, this.fsName,
vm_service.VmService serviceProtocol, { vm_service.VmService serviceProtocol, {
@required OperatingSystemUtils osUtils, @required OperatingSystemUtils osUtils,
@required HttpClient httpClient,
@required Logger logger,
}) })
: httpAddress = serviceProtocol.httpAddress, : httpAddress = serviceProtocol.httpAddress,
_client = (context.get<HttpClientFactory>() == null) _client = httpClient,
? HttpClient() _osUtils = osUtils,
: context.get<HttpClientFactory>()(), _logger = logger;
_osUtils = osUtils;
final String fsName;
final Uri httpAddress;
final HttpClient _client; final HttpClient _client;
final OperatingSystemUtils _osUtils; final OperatingSystemUtils _osUtils;
final Logger _logger;
final String fsName;
final Uri httpAddress;
static const int kMaxInFlight = 6; static const int kMaxInFlight = 6;
...@@ -309,20 +272,18 @@ class _DevFSHttpWriter { ...@@ -309,20 +272,18 @@ class _DevFSHttpWriter {
await request.addStream(contents); await request.addStream(contents);
final HttpClientResponse response = await request.close(); final HttpClientResponse response = await request.close();
response.listen((_) => null, response.listen((_) => null,
onError: (dynamic error) { globals.printTrace('error: $error'); }, onError: (dynamic error) {
cancelOnError: true); _logger.printTrace('error: $error');
},
cancelOnError: true,
);
break; break;
} catch (error, trace) { // ignore: avoid_catches_without_on_clauses } on Exception catch (error, trace) {
// We treat OSError as an Exception.
// See: https://github.com/dart-lang/sdk/issues/40934
if (error is! Exception && error is! OSError) {
rethrow;
}
if (!_completer.isCompleted) { if (!_completer.isCompleted) {
globals.printTrace('Error writing "$deviceUri" to DevFS: $error'); _logger.printTrace('Error writing "$deviceUri" to DevFS: $error');
if (retry > 0) { if (retry > 0) {
retry--; retry--;
globals.printTrace('trying again in a few - $retry more attempts left'); _logger.printTrace('trying again in a few - $retry more attempts left');
await Future<void>.delayed(const Duration(milliseconds: 500)); await Future<void>.delayed(const Duration(milliseconds: 500));
continue; continue;
} }
...@@ -378,31 +339,34 @@ class DevFS { ...@@ -378,31 +339,34 @@ class DevFS {
this.fsName, this.fsName,
this.rootDirectory, { this.rootDirectory, {
@required OperatingSystemUtils osUtils, @required OperatingSystemUtils osUtils,
@visibleForTesting bool disableUpload = false, @required Logger logger,
}) : _operations = ServiceProtocolDevFSOperations(serviceProtocol), @required FileSystem fileSystem,
HttpClient httpClient,
}) : _vmService = serviceProtocol,
_logger = logger,
_fileSystem = fileSystem,
_httpWriter = _DevFSHttpWriter( _httpWriter = _DevFSHttpWriter(
fsName, fsName,
serviceProtocol, serviceProtocol,
osUtils: osUtils, osUtils: osUtils,
), logger: logger,
_disableUpload = disableUpload; httpClient: httpClient ?? ((context.get<HttpClientFactory>() == null)
? HttpClient()
DevFS.operations( : context.get<HttpClientFactory>()())
this._operations, );
this.fsName,
this.rootDirectory,
) : _httpWriter = null,
_disableUpload = false;
final DevFSOperations _operations; final vm_service.VmService _vmService;
final _DevFSHttpWriter _httpWriter; final _DevFSHttpWriter _httpWriter;
final Logger _logger;
final FileSystem _fileSystem;
final String fsName; final String fsName;
final Directory rootDirectory; final Directory rootDirectory;
final Set<String> assetPathsToEvict = <String>{}; final Set<String> assetPathsToEvict = <String>{};
List<Uri> sources = <Uri>[]; List<Uri> sources = <Uri>[];
DateTime lastCompiled; DateTime lastCompiled;
PackageConfig lastPackageConfig; PackageConfig lastPackageConfig;
final bool _disableUpload;
Uri _baseUri; Uri _baseUri;
Uri get baseUri => _baseUri; Uri get baseUri => _baseUri;
...@@ -418,26 +382,28 @@ class DevFS { ...@@ -418,26 +382,28 @@ class DevFS {
} }
Future<Uri> create() async { Future<Uri> create() async {
globals.printTrace('DevFS: Creating new filesystem on the device ($_baseUri)'); _logger.printTrace('DevFS: Creating new filesystem on the device ($_baseUri)');
try { try {
_baseUri = await _operations.create(fsName); final vm_service.Response response = await _vmService.createDevFS(fsName);
_baseUri = Uri.parse(response.json['uri'] as String);
} on vm_service.RPCError catch (rpcException) { } on vm_service.RPCError catch (rpcException) {
// 1001 is kFileSystemAlreadyExists in //dart/runtime/vm/json_stream.h // 1001 is kFileSystemAlreadyExists in //dart/runtime/vm/json_stream.h
if (rpcException.code != 1001) { if (rpcException.code != 1001) {
rethrow; rethrow;
} }
globals.printTrace('DevFS: Creating failed. Destroying and trying again'); _logger.printTrace('DevFS: Creating failed. Destroying and trying again');
await destroy(); await destroy();
_baseUri = await _operations.create(fsName); final vm_service.Response response = await _vmService.createDevFS(fsName);
_baseUri = Uri.parse(response.json['uri'] as String);
} }
globals.printTrace('DevFS: Created new filesystem on the device ($_baseUri)'); _logger.printTrace('DevFS: Created new filesystem on the device ($_baseUri)');
return _baseUri; return _baseUri;
} }
Future<void> destroy() async { Future<void> destroy() async {
globals.printTrace('DevFS: Deleting filesystem on the device ($_baseUri)'); _logger.printTrace('DevFS: Deleting filesystem on the device ($_baseUri)');
await _operations.destroy(fsName); await _vmService.deleteDevFS(fsName);
globals.printTrace('DevFS: Deleted filesystem on the device ($_baseUri)'); _logger.printTrace('DevFS: Deleted filesystem on the device ($_baseUri)');
} }
/// Updates files on the device. /// Updates files on the device.
...@@ -465,17 +431,17 @@ class DevFS { ...@@ -465,17 +431,17 @@ class DevFS {
lastPackageConfig = packageConfig; lastPackageConfig = packageConfig;
// Update modified files // Update modified files
final String assetBuildDirPrefix = _asUriPath(getAssetBuildDirectory());
final Map<Uri, DevFSContent> dirtyEntries = <Uri, DevFSContent>{}; final Map<Uri, DevFSContent> dirtyEntries = <Uri, DevFSContent>{};
int syncedBytes = 0; int syncedBytes = 0;
if (bundle != null && !skipAssets) { if (bundle != null && !skipAssets) {
globals.printTrace('Scanning asset files'); _logger.printTrace('Scanning asset files');
final String assetBuildDirPrefix = _asUriPath(getAssetBuildDirectory());
// We write the assets into the AssetBundle working dir so that they // We write the assets into the AssetBundle working dir so that they
// are in the same location in DevFS and the iOS simulator. // are in the same location in DevFS and the iOS simulator.
final String assetDirectory = getAssetBuildDirectory(); final String assetDirectory = getAssetBuildDirectory();
bundle.entries.forEach((String archivePath, DevFSContent content) { bundle.entries.forEach((String archivePath, DevFSContent content) {
final Uri deviceUri = globals.fs.path.toUri(globals.fs.path.join(assetDirectory, archivePath)); final Uri deviceUri = _fileSystem.path.toUri(_fileSystem.path.join(assetDirectory, archivePath));
if (deviceUri.path.startsWith(assetBuildDirPrefix)) { if (deviceUri.path.startsWith(assetBuildDirPrefix)) {
archivePath = deviceUri.path.substring(assetBuildDirPrefix.length); archivePath = deviceUri.path.substring(assetBuildDirPrefix.length);
} }
...@@ -496,7 +462,7 @@ class DevFS { ...@@ -496,7 +462,7 @@ class DevFS {
// On a full restart, or on an initial compile for the attach based workflow, // On a full restart, or on an initial compile for the attach based workflow,
// this will produce a full dill. Subsequent invocations will produce incremental // this will produce a full dill. Subsequent invocations will produce incremental
// dill files that depend on the invalidated files. // dill files that depend on the invalidated files.
globals.printTrace('Compiling dart to kernel with ${invalidatedFiles.length} updated files'); _logger.printTrace('Compiling dart to kernel with ${invalidatedFiles.length} updated files');
final CompilerOutput compilerOutput = await generator.recompile( final CompilerOutput compilerOutput = await generator.recompile(
mainUri, mainUri,
invalidatedFiles, invalidatedFiles,
...@@ -516,34 +482,32 @@ class DevFS { ...@@ -516,34 +482,32 @@ class DevFS {
if (!bundleFirstUpload) { if (!bundleFirstUpload) {
final String compiledBinary = compilerOutput?.outputFilename; final String compiledBinary = compilerOutput?.outputFilename;
if (compiledBinary != null && compiledBinary.isNotEmpty) { if (compiledBinary != null && compiledBinary.isNotEmpty) {
final Uri entryUri = globals.fs.path.toUri(projectRootPath != null final Uri entryUri = _fileSystem.path.toUri(projectRootPath != null
? globals.fs.path.relative(pathToReload, from: projectRootPath) ? _fileSystem.path.relative(pathToReload, from: projectRootPath)
: pathToReload, : pathToReload,
); );
final DevFSFileContent content = DevFSFileContent(globals.fs.file(compiledBinary)); final DevFSFileContent content = DevFSFileContent(_fileSystem.file(compiledBinary));
syncedBytes += content.size; syncedBytes += content.size;
dirtyEntries[entryUri] = content; dirtyEntries[entryUri] = content;
} }
} }
globals.printTrace('Updating files'); _logger.printTrace('Updating files');
if (dirtyEntries.isNotEmpty) { if (dirtyEntries.isNotEmpty) {
try { try {
if (!_disableUpload) { await _httpWriter.write(dirtyEntries);
await _httpWriter.write(dirtyEntries);
}
} on SocketException catch (socketException, stackTrace) { } on SocketException catch (socketException, stackTrace) {
globals.printTrace('DevFS sync failed. Lost connection to device: $socketException'); _logger.printTrace('DevFS sync failed. Lost connection to device: $socketException');
throw DevFSException('Lost connection to device.', socketException, stackTrace); throw DevFSException('Lost connection to device.', socketException, stackTrace);
} on Exception catch (exception, stackTrace) { } on Exception catch (exception, stackTrace) {
globals.printError('Could not update files on device: $exception'); _logger.printError('Could not update files on device: $exception');
throw DevFSException('Sync failed', exception, stackTrace); throw DevFSException('Sync failed', exception, stackTrace);
} }
} }
globals.printTrace('DevFS: Sync finished'); _logger.printTrace('DevFS: Sync finished');
return UpdateFSReport(success: true, syncedBytes: syncedBytes, return UpdateFSReport(success: true, syncedBytes: syncedBytes,
invalidatedSourcesCount: invalidatedFiles.length); invalidatedSourcesCount: invalidatedFiles.length);
} }
}
/// Converts a platform-specific file path to a platform-independent URL path. /// Converts a platform-specific file path to a platform-independent URL path.
String _asUriPath(String filePath) => globals.fs.path.toUri(filePath).path + '/'; String _asUriPath(String filePath) => _fileSystem.path.toUri(filePath).path + '/';
}
...@@ -293,6 +293,8 @@ class FlutterDevice { ...@@ -293,6 +293,8 @@ class FlutterDevice {
fsName, fsName,
rootDirectory, rootDirectory,
osUtils: globals.os, osUtils: globals.os,
fileSystem: globals.fs,
logger: globals.logger,
); );
return devFS.create(); return devFS.create();
} }
......
...@@ -4,345 +4,259 @@ ...@@ -4,345 +4,259 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; // ignore: dart_io_import
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart'; 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/net.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/compile.dart';
import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/vmservice.dart'; import 'package:flutter_tools/src/vmservice.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:package_config/package_config.dart'; import 'package:package_config/package_config.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart'; import '../src/context.dart';
import '../src/mocks.dart';
final FakeVmServiceRequest createDevFSRequest = FakeVmServiceRequest(
method: '_createDevFS',
args: <String, Object>{
'fsName': 'test',
},
jsonResponse: <String, Object>{
'uri': Uri.parse('test').toString(),
}
);
void main() { void main() {
FileSystem fs; testWithoutContext('DevFSByteContent', () {
String filePath; final DevFSByteContent content = DevFSByteContent(<int>[4, 5, 6]);
Directory tempDir;
String basePath; expect(content.bytes, orderedEquals(<int>[4, 5, 6]));
expect(content.isModified, isTrue);
setUpAll(() { expect(content.isModified, isFalse);
fs = MemoryFileSystem.test(); content.bytes = <int>[7, 8, 9, 2];
filePath = fs.path.join('lib', 'foo.txt'); expect(content.bytes, orderedEquals(<int>[7, 8, 9, 2]));
expect(content.isModified, isTrue);
expect(content.isModified, isFalse);
}); });
group('DevFSContent', () { testWithoutContext('DevFSStringContent', () {
test('bytes', () { final DevFSStringContent content = DevFSStringContent('some string');
final DevFSByteContent content = DevFSByteContent(<int>[4, 5, 6]);
expect(content.bytes, orderedEquals(<int>[4, 5, 6])); expect(content.string, 'some string');
expect(content.isModified, isTrue); expect(content.bytes, orderedEquals(utf8.encode('some string')));
expect(content.isModified, isFalse); expect(content.isModified, isTrue);
content.bytes = <int>[7, 8, 9, 2]; expect(content.isModified, isFalse);
expect(content.bytes, orderedEquals(<int>[7, 8, 9, 2])); content.string = 'another string';
expect(content.isModified, isTrue); expect(content.string, 'another string');
expect(content.isModified, isFalse); expect(content.bytes, orderedEquals(utf8.encode('another string')));
}); expect(content.isModified, isTrue);
test('string', () { expect(content.isModified, isFalse);
final DevFSStringContent content = DevFSStringContent('some string'); content.bytes = utf8.encode('foo bar');
expect(content.string, 'some string'); expect(content.string, 'foo bar');
expect(content.bytes, orderedEquals(utf8.encode('some string'))); expect(content.bytes, orderedEquals(utf8.encode('foo bar')));
expect(content.isModified, isTrue); expect(content.isModified, isTrue);
expect(content.isModified, isFalse); expect(content.isModified, isFalse);
content.string = 'another string';
expect(content.string, 'another string');
expect(content.bytes, orderedEquals(utf8.encode('another string')));
expect(content.isModified, isTrue);
expect(content.isModified, isFalse);
content.bytes = utf8.encode('foo bar');
expect(content.string, 'foo bar');
expect(content.bytes, orderedEquals(utf8.encode('foo bar')));
expect(content.isModified, isTrue);
expect(content.isModified, isFalse);
});
testUsingContext('file', () async {
final File file = fs.file(filePath);
final DevFSFileContent content = DevFSFileContent(file);
expect(content.isModified, isFalse);
expect(content.isModified, isFalse);
file.parent.createSync(recursive: true);
file.writeAsBytesSync(<int>[1, 2, 3], flush: true);
final DateTime fiveSecondsAgo = file.statSync().modified.subtract(const Duration(seconds: 5));
expect(content.isModifiedAfter(fiveSecondsAgo), isTrue);
expect(content.isModifiedAfter(fiveSecondsAgo), isTrue);
expect(content.isModifiedAfter(null), isTrue);
file.writeAsBytesSync(<int>[2, 3, 4], flush: true);
expect(content.fileDependencies, <String>[filePath]);
expect(content.isModified, isTrue);
expect(content.isModified, isFalse);
expect(await content.contentsAsBytes(), <int>[2, 3, 4]);
updateFileModificationTime(file.path, fiveSecondsAgo, 0);
expect(content.isModified, isFalse);
expect(content.isModified, isFalse);
file.deleteSync();
expect(content.isModified, isTrue);
expect(content.isModified, isFalse);
expect(content.isModified, isFalse);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
}, skip: Platform.isWindows); // TODO(jonahwilliams): fix or disable this functionality.
}); });
group('mocked http client', () { testWithoutContext('DevFSFileContent', () async {
HttpOverrides savedHttpOverrides; final FileSystem fileSystem = MemoryFileSystem.test();
HttpClient httpClient; final File file = fileSystem.file('foo.txt');
OperatingSystemUtils osUtils; final DevFSFileContent content = DevFSFileContent(file);
expect(content.isModified, isFalse);
setUpAll(() { expect(content.isModified, isFalse);
tempDir = _newTempDir(fs);
basePath = tempDir.path;
savedHttpOverrides = HttpOverrides.current;
httpClient = MockOddlyFailingHttpClient();
HttpOverrides.global = MyHttpOverrides(httpClient);
osUtils = MockOperatingSystemUtils();
});
tearDownAll(() async { file.parent.createSync(recursive: true);
HttpOverrides.global = savedHttpOverrides; file.writeAsBytesSync(<int>[1, 2, 3], flush: true);
});
final List<dynamic> exceptions = <dynamic>[ final DateTime fiveSecondsAgo = file.statSync().modified.subtract(const Duration(seconds: 5));
Exception('Connection resert by peer'), expect(content.isModifiedAfter(fiveSecondsAgo), isTrue);
const OSError('Connection reset by peer'), expect(content.isModifiedAfter(fiveSecondsAgo), isTrue);
]; expect(content.isModifiedAfter(null), isTrue);
for (final dynamic exception in exceptions) {
testUsingContext('retry uploads when failure: $exception', () async {
final File file = fs.file(fs.path.join(basePath, filePath));
await file.parent.create(recursive: true);
file.writeAsBytesSync(<int>[1, 2, 3]);
// simulate package
await _createPackage(fs, 'somepkg', 'somefile.txt');
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[
FakeVmServiceRequest(
method: '_createDevFS',
args: <String, Object>{
'fsName': 'test',
},
jsonResponse: <String, Object>{
'uri': Uri.parse('test').toString(),
}
)
],
);
setHttpAddress(Uri.parse('http://localhost'), fakeVmServiceHost.vmService);
reset(httpClient);
final MockHttpClientRequest httpRequest = MockHttpClientRequest();
when(httpRequest.headers).thenReturn(MockHttpHeaders());
when(httpClient.putUrl(any)).thenAnswer((Invocation invocation) {
return Future<HttpClientRequest>.value(httpRequest);
});
final MockHttpClientResponse httpClientResponse = MockHttpClientResponse();
int nRequest = 0;
const int kFailedAttempts = 5;
when(httpRequest.close()).thenAnswer((Invocation invocation) {
if (nRequest++ < kFailedAttempts) {
throw exception;
}
return Future<HttpClientResponse>.value(httpClientResponse);
});
final DevFS devFS = DevFS(
fakeVmServiceHost.vmService,
'test',
tempDir,
osUtils: osUtils,
);
await devFS.create();
final MockResidentCompiler residentCompiler = MockResidentCompiler();
final UpdateFSReport report = await devFS.update(
mainUri: Uri.parse('lib/foo.txt'),
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
invalidatedFiles: <Uri>[],
packageConfig: PackageConfig.empty,
);
expect(report.syncedBytes, 22);
expect(report.success, isTrue);
verify(httpClient.putUrl(any)).called(kFailedAttempts + 1);
verify(httpRequest.close()).called(kFailedAttempts + 1);
verify(osUtils.gzipLevel1Stream(any)).called(kFailedAttempts + 1);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
HttpClientFactory: () => () => httpClient,
ProcessManager: () => FakeProcessManager.any(),
});
}
});
group('devfs remote', () { file.writeAsBytesSync(<int>[2, 3, 4], flush: true);
DevFS devFS;
setUpAll(() async { expect(content.isModified, isTrue);
tempDir = _newTempDir(fs); expect(content.isModified, isFalse);
basePath = tempDir.path; expect(await content.contentsAsBytes(), <int>[2, 3, 4]);
});
setUp(() { expect(content.isModified, isFalse);
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( expect(content.isModified, isFalse);
requests: <VmServiceExpectation>[
FakeVmServiceRequest(
method: '_createDevFS',
args: <String, Object>{
'fsName': 'test',
},
jsonResponse: <String, Object>{
'uri': Uri.parse('test').toString(),
}
)
],
);
setHttpAddress(Uri.parse('http://localhost'), fakeVmServiceHost.vmService);
devFS = DevFS(
fakeVmServiceHost.vmService,
'test',
tempDir,
osUtils: FakeOperatingSystemUtils(),
// TODO(jonahwilliams): remove and prevent usage of http writer.
disableUpload: true,
);
});
tearDownAll(() async { file.deleteSync();
_cleanupTempDirs(); expect(content.isModified, isTrue);
}); expect(content.isModified, isFalse);
expect(content.isModified, isFalse);
});
testUsingContext('reports unsuccessful compile when errors are returned', () async { testWithoutContext('DevFS retries uploads when connection resert by peer', () async {
await devFS.create(); final HttpClient httpClient = MockHttpClient();
final DateTime previousCompile = devFS.lastCompiled; final FileSystem fileSystem = MemoryFileSystem.test();
final OperatingSystemUtils osUtils = MockOperatingSystemUtils();
final RealMockResidentCompiler residentCompiler = RealMockResidentCompiler(); final MockResidentCompiler residentCompiler = MockResidentCompiler();
when(residentCompiler.recompile( final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
any, requests: <VmServiceExpectation>[createDevFSRequest],
any, );
outputPath: anyNamed('outputPath'), setHttpAddress(Uri.parse('http://localhost'), fakeVmServiceHost.vmService);
packageConfig: anyNamed('packageConfig'),
)).thenAnswer((Invocation invocation) { final MockHttpClientRequest httpRequest = MockHttpClientRequest();
return Future<CompilerOutput>.value(const CompilerOutput('example', 2, <Uri>[])); when(httpRequest.headers).thenReturn(MockHttpHeaders());
}); when(httpClient.putUrl(any)).thenAnswer((Invocation invocation) {
return Future<HttpClientRequest>.value(httpRequest);
final UpdateFSReport report = await devFS.update( });
mainUri: Uri.parse('lib/foo.txt'), final MockHttpClientResponse httpClientResponse = MockHttpClientResponse();
generator: residentCompiler, int nRequest = 0;
pathToReload: 'lib/foo.txt.dill', const int kFailedAttempts = 5;
trackWidgetCreation: false, when(httpRequest.close()).thenAnswer((Invocation invocation) {
invalidatedFiles: <Uri>[], if (nRequest++ < kFailedAttempts) {
packageConfig: PackageConfig.empty, throw const OSError('Connection Reset by peer');
); }
return Future<HttpClientResponse>.value(httpClientResponse);
expect(report.success, false);
expect(devFS.lastCompiled, previousCompile);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
}); });
testUsingContext('correctly updates last compiled time when compilation does not fail', () async { when(residentCompiler.recompile(
// simulate package any,
final File sourceFile = await _createPackage(fs, 'somepkg', 'main.dart'); any,
outputPath: anyNamed('outputPath'),
await devFS.create(); packageConfig: anyNamed('packageConfig'),
final DateTime previousCompile = devFS.lastCompiled; )).thenAnswer((Invocation invocation) async {
fileSystem.file('lib/foo.dill')
final RealMockResidentCompiler residentCompiler = RealMockResidentCompiler(); ..createSync(recursive: true)
when(residentCompiler.recompile( ..writeAsBytesSync(<int>[1, 2, 3, 4, 5]);
any, return const CompilerOutput('lib/foo.dill', 0, <Uri>[]);
any,
outputPath: anyNamed('outputPath'),
packageConfig: anyNamed('packageConfig'),
)).thenAnswer((Invocation invocation) {
fs.file('example').createSync();
return Future<CompilerOutput>.value(CompilerOutput('example', 0, <Uri>[sourceFile.uri]));
});
final UpdateFSReport report = await devFS.update(
mainUri: Uri.parse('lib/main.dart'),
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
invalidatedFiles: <Uri>[],
packageConfig: PackageConfig.empty,
);
expect(report.success, true);
expect(devFS.lastCompiled, isNot(previousCompile));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
HttpClient: () => () => HttpClient(),
ProcessManager: () => FakeProcessManager.any(),
}); });
final DevFS devFS = DevFS(
fakeVmServiceHost.vmService,
'test',
fileSystem.currentDirectory,
osUtils: osUtils,
fileSystem: fileSystem,
logger: BufferLogger.test(),
httpClient: httpClient,
);
await devFS.create();
final UpdateFSReport report = await devFS.update(
mainUri: Uri.parse('lib/foo.txt'),
dillOutputPath: 'lib/foo.dill',
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
invalidatedFiles: <Uri>[],
packageConfig: PackageConfig.empty,
);
expect(report.syncedBytes, 5);
expect(report.success, isTrue);
verify(httpClient.putUrl(any)).called(kFailedAttempts + 1);
verify(httpRequest.close()).called(kFailedAttempts + 1);
verify(osUtils.gzipLevel1Stream(any)).called(kFailedAttempts + 1);
}); });
}
class RealMockResidentCompiler extends Mock implements ResidentCompiler {} testWithoutContext('DevFS reports unsuccessful compile when errors are returned', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[createDevFSRequest],
);
setHttpAddress(Uri.parse('http://localhost'), fakeVmServiceHost.vmService);
final DevFS devFS = DevFS(
fakeVmServiceHost.vmService,
'test',
fileSystem.currentDirectory,
fileSystem: fileSystem,
logger: BufferLogger.test(),
osUtils: FakeOperatingSystemUtils(),
httpClient: MockHttpClient(),
);
await devFS.create();
final DateTime previousCompile = devFS.lastCompiled;
final MockResidentCompiler residentCompiler = MockResidentCompiler();
when(residentCompiler.recompile(
any,
any,
outputPath: anyNamed('outputPath'),
packageConfig: anyNamed('packageConfig'),
)).thenAnswer((Invocation invocation) async {
return const CompilerOutput('lib/foo.dill', 2, <Uri>[]);
});
final List<Directory> _tempDirs = <Directory>[]; final UpdateFSReport report = await devFS.update(
final Map <String, Uri> _packages = <String, Uri>{}; mainUri: Uri.parse('lib/foo.txt'),
generator: residentCompiler,
dillOutputPath: 'lib/foo.dill',
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
invalidatedFiles: <Uri>[],
packageConfig: PackageConfig.empty,
);
expect(report.success, false);
expect(devFS.lastCompiled, previousCompile);
});
Directory _newTempDir(FileSystem fs) { testWithoutContext('DevFS correctly updates last compiled time when compilation does not fail', () async {
final Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_devfs${_tempDirs.length}_test.'); final FileSystem fileSystem = MemoryFileSystem.test();
_tempDirs.add(tempDir); final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
return tempDir; requests: <VmServiceExpectation>[createDevFSRequest],
} );
final HttpClient httpClient = MockHttpClient();
final MockHttpClientRequest httpRequest = MockHttpClientRequest();
when(httpRequest.headers).thenReturn(MockHttpHeaders());
when(httpClient.putUrl(any)).thenAnswer((Invocation invocation) {
return Future<HttpClientRequest>.value(httpRequest);
});
final MockHttpClientResponse httpClientResponse = MockHttpClientResponse();
when(httpRequest.close()).thenAnswer((Invocation invocation) async {
return httpClientResponse;
});
void _cleanupTempDirs() { final DevFS devFS = DevFS(
while (_tempDirs.isNotEmpty) { fakeVmServiceHost.vmService,
tryToDelete(_tempDirs.removeLast()); 'test',
} fileSystem.currentDirectory,
} fileSystem: fileSystem,
logger: BufferLogger.test(),
osUtils: FakeOperatingSystemUtils(),
httpClient: httpClient,
);
await devFS.create();
final DateTime previousCompile = devFS.lastCompiled;
final MockResidentCompiler residentCompiler = MockResidentCompiler();
when(residentCompiler.recompile(
any,
any,
outputPath: anyNamed('outputPath'),
packageConfig: anyNamed('packageConfig'),
)).thenAnswer((Invocation invocation) async {
fileSystem.file('example').createSync();
return const CompilerOutput('lib/foo.txt.dill', 0, <Uri>[]);
});
Future<File> _createPackage(FileSystem fs, String pkgName, String pkgFileName, { bool doubleSlash = false }) async { final UpdateFSReport report = await devFS.update(
final Directory pkgTempDir = _newTempDir(fs); mainUri: Uri.parse('lib/main.dart'),
String pkgFilePath = fs.path.join(pkgTempDir.path, pkgName, 'lib', pkgFileName); generator: residentCompiler,
if (doubleSlash) { dillOutputPath: 'lib/foo.dill',
// Force two separators into the path. pathToReload: 'lib/foo.txt.dill',
final String doubleSlash = fs.path.separator + fs.path.separator; trackWidgetCreation: false,
pkgFilePath = pkgTempDir.path + doubleSlash + fs.path.join(pkgName, 'lib', pkgFileName); invalidatedFiles: <Uri>[],
} packageConfig: PackageConfig.empty,
final File pkgFile = fs.file(pkgFilePath); );
await pkgFile.parent.create(recursive: true);
pkgFile.writeAsBytesSync(<int>[11, 12, 13]); expect(report.success, true);
_packages[pkgName] = fs.path.toUri(pkgFile.parent.path); expect(devFS.lastCompiled, isNot(previousCompile));
final StringBuffer sb = StringBuffer();
_packages.forEach((String pkgName, Uri pkgUri) {
sb.writeln('$pkgName:$pkgUri');
}); });
return fs.file(fs.path.join(_tempDirs[0].path, '.packages'))
..writeAsStringSync(sb.toString());
}
class MyHttpOverrides extends HttpOverrides {
MyHttpOverrides(this._httpClient);
@override
HttpClient createHttpClient(SecurityContext context) {
return _httpClient;
}
final HttpClient _httpClient;
} }
class MockOddlyFailingHttpClient extends Mock implements HttpClient {}
class MockHttpClientRequest extends Mock implements HttpClientRequest {} class MockHttpClientRequest extends Mock implements HttpClientRequest {}
class MockHttpHeaders extends Mock implements HttpHeaders {} class MockHttpHeaders extends Mock implements HttpHeaders {}
class MockHttpClientResponse extends Mock implements HttpClientResponse {} class MockHttpClientResponse extends Mock implements HttpClientResponse {}
class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {} class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {}
class MockVMService extends Mock implements vm_service.VmService {} class MockResidentCompiler extends Mock implements ResidentCompiler {}
...@@ -85,16 +85,6 @@ CommandRunner<void> createTestCommandRunner([ FlutterCommand command ]) { ...@@ -85,16 +85,6 @@ CommandRunner<void> createTestCommandRunner([ FlutterCommand command ]) {
return runner; return runner;
} }
/// Updates [path] to have a modification time [seconds] from now.
void updateFileModificationTime(
String path,
DateTime baseTime,
int seconds,
) {
final DateTime modificationTime = baseTime.add(Duration(seconds: seconds));
globals.fs.file(path).setLastModifiedSync(modificationTime);
}
/// Matcher for functions that throw [AssertionError]. /// Matcher for functions that throw [AssertionError].
final Matcher throwsAssertionError = throwsA(isA<AssertionError>()); final Matcher throwsAssertionError = throwsA(isA<AssertionError>());
......
...@@ -16,7 +16,6 @@ import 'package:flutter_tools/src/base/io.dart'; ...@@ -16,7 +16,6 @@ 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:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/compile.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/ios/devices.dart'; import 'package:flutter_tools/src/ios/devices.dart';
...@@ -600,31 +599,6 @@ class BasicMock { ...@@ -600,31 +599,6 @@ class BasicMock {
} }
} }
class MockDevFSOperations extends BasicMock implements DevFSOperations {
Map<Uri, DevFSContent> devicePathToContent = <Uri, DevFSContent>{};
@override
Future<Uri> create(String fsName) async {
messages.add('create $fsName');
return Uri.parse('file:///$fsName');
}
@override
Future<dynamic> destroy(String fsName) async {
messages.add('destroy $fsName');
}
@override
Future<dynamic> writeFile(String fsName, Uri deviceUri, DevFSContent content) async {
String message = 'writeFile $fsName $deviceUri';
if (content is DevFSFileContent) {
message += ' ${content.file.path}';
}
messages.add(message);
devicePathToContent[deviceUri] = content;
}
}
class MockResidentCompiler extends BasicMock implements ResidentCompiler { class MockResidentCompiler extends BasicMock implements ResidentCompiler {
@override @override
void accept() { } void accept() { }
......
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