diff --git a/packages/flutter_tools/bin/fuchsia_asset_builder.dart b/packages/flutter_tools/bin/fuchsia_asset_builder.dart index 0163a1bdb6a..2de562425f4 100644 --- a/packages/flutter_tools/bin/fuchsia_asset_builder.dart +++ b/packages/flutter_tools/bin/fuchsia_asset_builder.dart @@ -36,9 +36,10 @@ Future main(List args) { }); } -void writeFile(libfs.File outputFile, DevFSContent content) { +Future writeFile(libfs.File outputFile, DevFSContent content) async { outputFile.createSync(recursive: true); - content.copyToFile(outputFile); + final List data = await content.contentsAsBytes(); + outputFile.writeAsBytesSync(data); } Future run(List args) async { @@ -70,10 +71,12 @@ Future run(List args) async { exit(1); } + final List> calls = >[]; assets.entries.forEach((String fileName, DevFSContent content) { final libfs.File outputFile = libfs.fs.file(libfs.fs.path.join(assetDir, fileName)); - writeFile(outputFile, content); + calls.add(writeFile(outputFile, content)); }); + await Future.wait(calls); final String outputMan = argResults[_kOptionAssetManifestOut]; await writeFuchsiaManifest(assets, argResults[_kOptionAsset], outputMan, argResults[_kOptionComponentName]); diff --git a/packages/flutter_tools/lib/src/bundle.dart b/packages/flutter_tools/lib/src/bundle.dart index 3355f720191..70a42653ece 100644 --- a/packages/flutter_tools/lib/src/bundle.dart +++ b/packages/flutter_tools/lib/src/bundle.dart @@ -147,7 +147,7 @@ Future build({ if (assets == null) throwToolExit('Error building assets', exitCode: 1); - assemble( + await assemble( buildMode: buildMode, assetBundle: assets, kernelContent: kernelContent, @@ -182,14 +182,14 @@ Future buildAssets({ return assetBundle; } -void assemble({ +Future assemble({ BuildMode buildMode, AssetBundle assetBundle, DevFSContent kernelContent, String privateKeyPath = defaultPrivateKeyPath, String assetDirPath, String compilationTraceFilePath, -}) { +}) async { assetDirPath ??= getAssetBuildDirectory(); printTrace('Building bundle'); @@ -214,21 +214,22 @@ void assemble({ printTrace('Writing asset files to $assetDirPath'); ensureDirectoryExists(assetDirPath); - writeBundle(fs.directory(assetDirPath), assetEntries); + await writeBundle(fs.directory(assetDirPath), assetEntries); printTrace('Wrote $assetDirPath'); } -void writeBundle( +Future writeBundle( Directory bundleDir, Map assetEntries, -) { +) async { if (bundleDir.existsSync()) bundleDir.deleteSync(recursive: true); bundleDir.createSync(recursive: true); - for (MapEntry entry in assetEntries.entries) { - final File file = fs.file(fs.path.join(bundleDir.path, entry.key)); - file.parent.createSync(recursive: true); - entry.value.copyToFile(file); - } + await Future.wait( + assetEntries.entries.map>((MapEntry entry) async { + final File file = fs.file(fs.path.join(bundleDir.path, entry.key)); + file.parent.createSync(recursive: true); + await file.writeAsBytes(await entry.value.contentsAsBytes()); + })); } diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index 6a2307fb2ac..6f1caa480f4 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -242,7 +242,7 @@ class TestCommand extends FastFlutterCommand { throwToolExit('Error: Failed to build asset bundle'); } if (_needRebuild(assetBundle.entries)) { - writeBundle(fs.directory(fs.path.join('build', 'unit_test_assets')), + await writeBundle(fs.directory(fs.path.join('build', 'unit_test_assets')), assetBundle.entries); } } diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart index ec97f957dbb..e15fb5c7dd1 100644 --- a/packages/flutter_tools/lib/src/devfs.dart +++ b/packages/flutter_tools/lib/src/devfs.dart @@ -38,25 +38,21 @@ abstract class DevFSContent { /// or if the given time is null. bool isModifiedAfter(DateTime time); - /// The number of bytes in this file. int get size; - /// Returns the raw bytes of this file. - List contentsAsBytes(); + Future> contentsAsBytes(); - /// Returns a gzipped representation of the contents of this file. - List contentsAsCompressedBytes() { - return gzip.encode(contentsAsBytes()); + Stream> contentsAsStream(); + + Stream> contentsAsCompressedStream() { + return contentsAsStream().cast>().transform>(gzip.encoder); } - /// Copies the content into the provided file. - /// - /// Requires that the `destination` directory already exists, but the target - /// file need not. - void copyToFile(File destination); + /// Return the list of files this content depends on. + List get fileDependencies => []; } -/// File content to be copied to the device. +// File content to be copied to the device. class DevFSFileContent extends DevFSContent { DevFSFileContent(this.file); @@ -106,6 +102,9 @@ class DevFSFileContent extends DevFSContent { } } + @override + List get fileDependencies => [_getFile().path]; + @override bool get isModified { final FileStat _oldFileStat = _fileStat; @@ -136,12 +135,10 @@ class DevFSFileContent extends DevFSContent { } @override - List contentsAsBytes() => _getFile().readAsBytesSync().cast(); + Future> contentsAsBytes() => _getFile().readAsBytes(); @override - void copyToFile(File destination) { - _getFile().copySync(destination.path); - } + Stream> contentsAsStream() => _getFile().openRead(); } /// Byte content to be copied to the device. @@ -178,15 +175,14 @@ class DevFSByteContent extends DevFSContent { int get size => _bytes.length; @override - List contentsAsBytes() => _bytes; + Future> contentsAsBytes() async => _bytes; @override - void copyToFile(File destination) { - destination.writeAsBytesSync(contentsAsBytes()); - } + Stream> contentsAsStream() => + Stream>.fromIterable(>[_bytes]); } -/// String content to be copied to the device encoded as utf8. +/// String content to be copied to the device. class DevFSStringContent extends DevFSByteContent { DevFSStringContent(String string) : _string = string, @@ -207,29 +203,75 @@ class DevFSStringContent extends DevFSByteContent { } } -class DevFSOperations { - DevFSOperations(this.vmService, this.fsName) - : httpAddress = vmService.httpAddress; +/// Abstract DevFS operations interface. +abstract class DevFSOperations { + Future create(String fsName); + Future destroy(String fsName); + Future 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 VMService vmService; + + @override + Future create(String fsName) async { + final Map response = await vmService.vm.createDevFS(fsName); + return Uri.parse(response['uri']); + } + + @override + Future destroy(String fsName) async { + await vmService.vm.deleteDevFS(fsName); + } + + @override + Future writeFile(String fsName, Uri deviceUri, DevFSContent content) async { + List bytes; + try { + bytes = await content.contentsAsBytes(); + } catch (e) { + return e; + } + final String fileContents = base64.encode(bytes); + try { + return await vmService.vm.invokeRpcRaw( + '_writeDevFSFile', + params: { + 'fsName': fsName, + 'uri': deviceUri.toString(), + 'fileContents': fileContents, + }, + ); + } catch (error) { + printTrace('DevFS: Failed to write $deviceUri: $error'); + } + } +} + +class DevFSException implements Exception { + DevFSException(this.message, [this.error, this.stackTrace]); + final String message; + final dynamic error; + final StackTrace stackTrace; +} + +class _DevFSHttpWriter { + _DevFSHttpWriter(this.fsName, VMService serviceProtocol) + : httpAddress = serviceProtocol.httpAddress; + final String fsName; final Uri httpAddress; - final HttpClient _client = HttpClient(); static const int kMaxInFlight = 6; int _inFlight = 0; Map _outstanding; Completer _completer; - - Future create(String fsName) async { - final Map response = await vmService.vm.createDevFS(fsName); - return Uri.parse(response['uri']); - } - - Future destroy(String fsName) async { - await vmService.vm.deleteDevFS(fsName); - } + final HttpClient _client = HttpClient(); Future write(Map entries) async { _client.maxConnectionsPerHost = kMaxInFlight; @@ -259,9 +301,9 @@ class DevFSOperations { final HttpClientRequest request = await _client.putUrl(httpAddress); request.headers.removeAll(HttpHeaders.acceptEncodingHeader); request.headers.add('dev_fs_name', fsName); - request.headers.add('dev_fs_uri_b64', - base64.encode(utf8.encode(deviceUri.toString()))); - request.add(content.contentsAsCompressedBytes()); + request.headers.add('dev_fs_uri_b64', base64.encode(utf8.encode('$deviceUri'))); + final Stream> contents = content.contentsAsCompressedStream(); + await request.addStream(contents); final HttpClientResponse response = await request.close(); await response.drain(); } catch (error, trace) { @@ -275,14 +317,6 @@ class DevFSOperations { } } -class DevFSException implements Exception { - DevFSException(this.message, [this.error, this.stackTrace]); - - final String message; - final dynamic error; - final StackTrace stackTrace; -} - // Basic statistics for DevFS update operation. class UpdateFSReport { UpdateFSReport({ @@ -319,7 +353,8 @@ class DevFS { this.fsName, this.rootDirectory, { String packagesFilePath, - }) : _operations = DevFSOperations(serviceProtocol, fsName), + }) : _operations = ServiceProtocolDevFSOperations(serviceProtocol), + _httpWriter = _DevFSHttpWriter(fsName, serviceProtocol), _packagesFilePath = packagesFilePath ?? fs.path.join(rootDirectory.path, kPackagesFileName); DevFS.operations( @@ -327,9 +362,11 @@ class DevFS { this.fsName, this.rootDirectory, { String packagesFilePath, - }) : _packagesFilePath = packagesFilePath ?? fs.path.join(rootDirectory.path, kPackagesFileName); + }) : _httpWriter = null, + _packagesFilePath = packagesFilePath ?? fs.path.join(rootDirectory.path, kPackagesFileName); final DevFSOperations _operations; + final _DevFSHttpWriter _httpWriter; final String fsName; final Directory rootDirectory; String _packagesFilePath; @@ -452,7 +489,7 @@ class DevFS { printTrace('Updating files'); if (dirtyEntries.isNotEmpty) { try { - await _operations.write(dirtyEntries); + await _httpWriter.write(dirtyEntries); } on SocketException catch (socketException, stackTrace) { printTrace('DevFS sync failed. Lost connection to device: $socketException'); throw DevFSException('Lost connection to device.', socketException, stackTrace); diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart index 3dac651cbe5..00d95fa7840 100644 --- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart +++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_build.dart @@ -52,7 +52,7 @@ Future _buildAssets( final Map assetEntries = Map.from(assets.entries); - writeBundle(fs.directory(assetDir), assetEntries); + await writeBundle(fs.directory(assetDir), assetEntries); final String appName = fuchsiaProject.project.manifest.appName; final String outDir = getFuchsiaBuildDirectory(); diff --git a/packages/flutter_tools/lib/src/resident_web_runner.dart b/packages/flutter_tools/lib/src/resident_web_runner.dart index aa1b61da715..1652628c72c 100644 --- a/packages/flutter_tools/lib/src/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/resident_web_runner.dart @@ -120,7 +120,8 @@ class ResidentWebRunner extends ResidentRunner { if (build != 0) { throwToolExit('Error: Failed to build asset bundle'); } - writeBundle(fs.directory(getAssetBuildDirectory()), assetBundle.entries); + await writeBundle( + fs.directory(getAssetBuildDirectory()), assetBundle.entries); // Step 2: Start an HTTP server _server = WebAssetServer(flutterProject, target, ipv6); diff --git a/packages/flutter_tools/lib/src/web/web_device.dart b/packages/flutter_tools/lib/src/web/web_device.dart index 3288e41fa78..d8ce9ce3e5a 100644 --- a/packages/flutter_tools/lib/src/web/web_device.dart +++ b/packages/flutter_tools/lib/src/web/web_device.dart @@ -119,7 +119,7 @@ class WebDevice extends Device { if (build != 0) { throwToolExit('Error: Failed to build asset bundle'); } - writeBundle(fs.directory(getAssetBuildDirectory()), assetBundle.entries); + await writeBundle(fs.directory(getAssetBuildDirectory()), assetBundle.entries); _package = package; _server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); diff --git a/packages/flutter_tools/test/asset_bundle_package_fonts_test.dart b/packages/flutter_tools/test/asset_bundle_package_fonts_test.dart index 0d2a3a2d1a6..648a092549a 100644 --- a/packages/flutter_tools/test/asset_bundle_package_fonts_test.dart +++ b/packages/flutter_tools/test/asset_bundle_package_fonts_test.dart @@ -74,7 +74,7 @@ $fontsSection final String entryKey = 'packages/$packageName/$packageFont'; expect(bundle.entries.containsKey(entryKey), true); expect( - utf8.decode(bundle.entries[entryKey].contentsAsBytes()), + utf8.decode(await bundle.entries[entryKey].contentsAsBytes()), packageFont, ); } @@ -82,14 +82,14 @@ $fontsSection for (String localFont in localFonts) { expect(bundle.entries.containsKey(localFont), true); expect( - utf8.decode(bundle.entries[localFont].contentsAsBytes()), + utf8.decode(await bundle.entries[localFont].contentsAsBytes()), localFont, ); } } expect( - json.decode(utf8.decode(bundle.entries['FontManifest.json'].contentsAsBytes())), + json.decode(utf8.decode(await bundle.entries['FontManifest.json'].contentsAsBytes())), json.decode(expectedAssetManifest), ); } diff --git a/packages/flutter_tools/test/asset_bundle_package_test.dart b/packages/flutter_tools/test/asset_bundle_package_test.dart index 1e67c1107ef..2ea79460ece 100644 --- a/packages/flutter_tools/test/asset_bundle_package_test.dart +++ b/packages/flutter_tools/test/asset_bundle_package_test.dart @@ -79,14 +79,14 @@ $assetsSection final String entryKey = Uri.encodeFull('packages/$packageName/$asset'); expect(bundle.entries.containsKey(entryKey), true, reason: 'Cannot find key on bundle: $entryKey'); expect( - utf8.decode(bundle.entries[entryKey].contentsAsBytes()), + utf8.decode(await bundle.entries[entryKey].contentsAsBytes()), asset, ); } } expect( - utf8.decode(bundle.entries['AssetManifest.json'].contentsAsBytes()), + utf8.decode(await bundle.entries['AssetManifest.json'].contentsAsBytes()), expectedAssetManifest, ); } @@ -126,11 +126,11 @@ $assetsSection expect(bundle.entries.length, 3); // LICENSE, AssetManifest, FontManifest const String expectedAssetManifest = '{}'; expect( - utf8.decode(bundle.entries['AssetManifest.json'].contentsAsBytes()), + utf8.decode(await bundle.entries['AssetManifest.json'].contentsAsBytes()), expectedAssetManifest, ); expect( - utf8.decode(bundle.entries['FontManifest.json'].contentsAsBytes()), + utf8.decode(await bundle.entries['FontManifest.json'].contentsAsBytes()), '[]', ); }, overrides: { @@ -153,11 +153,11 @@ $assetsSection expect(bundle.entries.length, 3); // LICENSE, AssetManifest, FontManifest const String expectedAssetManifest = '{}'; expect( - utf8.decode(bundle.entries['AssetManifest.json'].contentsAsBytes()), + utf8.decode(await bundle.entries['AssetManifest.json'].contentsAsBytes()), expectedAssetManifest, ); expect( - utf8.decode(bundle.entries['FontManifest.json'].contentsAsBytes()), + utf8.decode(await bundle.entries['FontManifest.json'].contentsAsBytes()), '[]', ); }, overrides: { diff --git a/packages/flutter_tools/test/asset_bundle_test.dart b/packages/flutter_tools/test/asset_bundle_test.dart index 6bd084e18b5..6d99cde99ac 100644 --- a/packages/flutter_tools/test/asset_bundle_test.dart +++ b/packages/flutter_tools/test/asset_bundle_test.dart @@ -50,7 +50,7 @@ void main() { expect(bundle.entries.length, 1); const String expectedAssetManifest = '{}'; expect( - utf8.decode(bundle.entries['AssetManifest.json'].contentsAsBytes()), + utf8.decode(await bundle.entries['AssetManifest.json'].contentsAsBytes()), expectedAssetManifest, ); }, overrides: { diff --git a/packages/flutter_tools/test/asset_bundle_variant_test.dart b/packages/flutter_tools/test/asset_bundle_variant_test.dart index 77bdbfdc87d..9a7b9f53355 100644 --- a/packages/flutter_tools/test/asset_bundle_variant_test.dart +++ b/packages/flutter_tools/test/asset_bundle_variant_test.dart @@ -76,7 +76,7 @@ flutter: // The main asset file, /a/b/c/foo, and its variants exist. for (String asset in assets) { expect(bundle.entries.containsKey(asset), true); - expect(utf8.decode(bundle.entries[asset].contentsAsBytes()), asset); + expect(utf8.decode(await bundle.entries[asset].contentsAsBytes()), asset); } fs.file(fixPath('a/b/c/foo')).deleteSync(); @@ -88,7 +88,7 @@ flutter: expect(bundle.entries.containsKey('a/b/c/foo'), false); for (String asset in assets.skip(1)) { expect(bundle.entries.containsKey(asset), true); - expect(utf8.decode(bundle.entries[asset].contentsAsBytes()), asset); + expect(utf8.decode(await bundle.entries[asset].contentsAsBytes()), asset); } }, overrides: { FileSystem: () => testFileSystem, diff --git a/packages/flutter_tools/test/asset_test.dart b/packages/flutter_tools/test/asset_test.dart index af961eb73ce..bb8f2598e35 100644 --- a/packages/flutter_tools/test/asset_test.dart +++ b/packages/flutter_tools/test/asset_test.dart @@ -7,7 +7,6 @@ import 'dart:async'; import 'package:flutter_tools/src/asset.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/cache.dart'; -import 'package:flutter_tools/src/convert.dart'; import 'src/common.dart'; import 'src/context.dart'; @@ -66,5 +65,5 @@ void main() { } Future getValueAsString(String key, AssetBundle asset) async { - return utf8.decode(asset.entries[key].contentsAsBytes()); + return String.fromCharCodes(await asset.entries[key].contentsAsBytes()); } diff --git a/packages/flutter_tools/test/devfs_test.dart b/packages/flutter_tools/test/devfs_test.dart index 1887e749136..b8d93d4e10a 100644 --- a/packages/flutter_tools/test/devfs_test.dart +++ b/packages/flutter_tools/test/devfs_test.dart @@ -4,17 +4,15 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; // ignore: dart_io_import import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_tools/src/base/context.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:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/vmservice.dart'; import 'package:json_rpc_2/json_rpc_2.dart' as rpc; -import 'package:mockito/mockito.dart'; import 'src/common.dart'; import 'src/context.dart'; @@ -22,39 +20,17 @@ import 'src/mocks.dart'; void main() { FileSystem fs; - MockPlatform mockPlatform; + String filePath; + Directory tempDir; + String basePath; + DevFS devFS; - setUp(() { - fs = MemoryFileSystem(style: FileSystemStyle.posix); - mockPlatform = MockPlatform(); - when(mockPlatform.pathSeparator).thenReturn('/'); - when(mockPlatform.isWindows).thenReturn(false); + setUpAll(() { + fs = MemoryFileSystem(); + filePath = fs.path.join('lib', 'foo.txt'); }); group('DevFSContent', () { - test('copyToFile', () { - final String filePath = fs.path.join('lib', 'foo.txt'); - final File file = fs.file(filePath) - ..createSync(recursive: true) - ..writeAsStringSync('hello, world'); - final DevFSByteContent byteContent = DevFSByteContent([4, 5, 6]); - final DevFSStringContent stringContent = DevFSStringContent('some string'); - final DevFSFileContent fileContent = DevFSFileContent(file); - - final File byteDestination = fs.file('byte_dest'); - final File stringDestination = fs.file('string_dest'); - final File fileDestination = fs.file('file_dest'); - - byteContent.copyToFile(byteDestination); - expect(byteDestination.readAsBytesSync(), [4, 5, 6]); - - stringContent.copyToFile(stringDestination); - expect(stringDestination.readAsStringSync(), 'some string'); - - fileContent.copyToFile(fileDestination); - expect(fileDestination.readAsStringSync(), 'hello, world'); - }); - test('bytes', () { final DevFSByteContent content = DevFSByteContent([4, 5, 6]); expect(content.bytes, orderedEquals([4, 5, 6])); @@ -65,7 +41,6 @@ void main() { expect(content.isModified, isTrue); expect(content.isModified, isFalse); }); - test('string', () { final DevFSStringContent content = DevFSStringContent('some string'); expect(content.string, 'some string'); @@ -83,9 +58,7 @@ void main() { expect(content.isModified, isTrue); expect(content.isModified, isFalse); }); - testUsingContext('file', () async { - final String filePath = fs.path.join('lib', 'foo.txt'); final File file = fs.file(filePath); final DevFSFileContent content = DevFSFileContent(file); expect(content.isModified, isFalse); @@ -100,9 +73,10 @@ void main() { expect(content.isModifiedAfter(null), isTrue); file.writeAsBytesSync([2, 3, 4], flush: true); + expect(content.fileDependencies, [filePath]); expect(content.isModified, isTrue); expect(content.isModified, isFalse); - expect(content.contentsAsBytes(), [2, 3, 4]); + expect(await content.contentsAsBytes(), [2, 3, 4]); updateFileModificationTime(file.path, fiveSecondsAgo, 0); expect(content.isModified, isFalse); expect(content.isModified, isFalse); @@ -113,56 +87,36 @@ void main() { expect(content.isModified, isFalse); }, overrides: { FileSystem: () => fs, - }, skip: platform.isWindows); // Still flaky, but only on CI :( + }, skip: Platform.isWindows); // TODO(jonahwilliams): fix or disable this functionality. }); group('devfs remote', () { - DevFS devFS; - MockResidentCompiler residentCompiler; - MockDevFSOperations mockDevFSOperations; - int created; - int destroyed; - List writtenFiles; - bool exists; + MockVMService vmService; + final MockResidentCompiler residentCompiler = MockResidentCompiler(); - setUp(() async { - mockDevFSOperations = MockDevFSOperations(); - devFS = DevFS.operations(mockDevFSOperations, 'test', fs.currentDirectory); - residentCompiler = MockResidentCompiler(); - created = 0; - destroyed = 0; - exists = false; - writtenFiles = []; - when(mockDevFSOperations.create('test')).thenAnswer((Invocation invocation) async { - if (exists) { - throw rpc.RpcException(1001, 'already exists'); - } - exists = true; - created += 1; - return Uri.parse(InternetAddress.loopbackIPv4.toString()); - }); - when(mockDevFSOperations.destroy('test')).thenAnswer((Invocation invocation) async { - exists = false; - destroyed += 1; - }); - when(mockDevFSOperations.write(any)).thenAnswer((Invocation invocation) async { - final Map entries = invocation.positionalArguments.first; - writtenFiles.addAll(entries.keys.map((Uri uri) => uri.toFilePath())); - }); + setUpAll(() async { + tempDir = _newTempDir(fs); + basePath = tempDir.path; + vmService = MockVMService(); + await vmService.setUp(); + }); + tearDownAll(() async { + await vmService.tearDown(); + _cleanupTempDirs(); }); testUsingContext('create dev file system', () async { // simulate workspace - final String filePath = fs.path.join('lib', 'foo.txt'); - final File file = fs.file(filePath); + final File file = fs.file(fs.path.join(basePath, filePath)); await file.parent.create(recursive: true); file.writeAsBytesSync([1, 2, 3]); // simulate package await _createPackage(fs, 'somepkg', 'somefile.txt'); - await devFS.create(); - expect(created, 1); + devFS = DevFS(vmService, 'test', tempDir); + await devFS.create(); + vmService.expectMessages(['create test']); expect(devFS.assetPathsToEvict, isEmpty); final UpdateFSReport report = await devFS.update( @@ -172,8 +126,9 @@ void main() { trackWidgetCreation: false, invalidatedFiles: [], ); - - expect(writtenFiles.single, contains('foo.txt.dill')); + vmService.expectMessages([ + 'writeFile test lib/foo.txt.dill', + ]); expect(devFS.assetPathsToEvict, isEmpty); expect(report.syncedBytes, 22); expect(report.success, true); @@ -182,8 +137,9 @@ void main() { }); testUsingContext('delete dev file system', () async { + expect(vmService.messages, isEmpty, reason: 'prior test timeout'); await devFS.destroy(); - expect(destroyed, 1); + vmService.expectMessages(['destroy test']); expect(devFS.assetPathsToEvict, isEmpty); }, overrides: { FileSystem: () => fs, @@ -191,27 +147,26 @@ void main() { testUsingContext('cleanup preexisting file system', () async { // simulate workspace - final String filePath = fs.path.join('lib', 'foo.txt'); - final File file = fs.file(filePath); + final File file = fs.file(fs.path.join(basePath, filePath)); await file.parent.create(recursive: true); file.writeAsBytesSync([1, 2, 3]); // simulate package await _createPackage(fs, 'somepkg', 'somefile.txt'); + devFS = DevFS(vmService, 'test', tempDir); await devFS.create(); - expect(created, 1); + vmService.expectMessages(['create test']); expect(devFS.assetPathsToEvict, isEmpty); // Try to create again. await devFS.create(); - expect(created, 2); - expect(destroyed, 1); + vmService.expectMessages(['create test', 'destroy test', 'create test']); expect(devFS.assetPathsToEvict, isEmpty); // Really destroy. await devFS.destroy(); - expect(destroyed, 2); + vmService.expectMessages(['destroy test']); expect(devFS.assetPathsToEvict, isEmpty); }, overrides: { FileSystem: () => fs, @@ -219,20 +174,113 @@ void main() { }); } -class MockVMService extends Mock implements VMService {} +class MockVMService extends BasicMock implements VMService { + MockVMService() { + _vm = MockVM(this); + } -class MockDevFSOperations extends Mock implements DevFSOperations {} + Uri _httpAddress; + HttpServer _server; + MockVM _vm; -class MockPlatform extends Mock implements Platform {} + @override + Uri get httpAddress => _httpAddress; + @override + VM get vm => _vm; + + Future setUp() async { + try { + _server = await HttpServer.bind(InternetAddress.loopbackIPv6, 0); + _httpAddress = Uri.parse('http://[::1]:${_server.port}'); + } on SocketException { + // Fall back to IPv4 if the host doesn't support binding to IPv6 localhost + _server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); + _httpAddress = Uri.parse('http://127.0.0.1:${_server.port}'); + } + _server.listen((HttpRequest request) { + final String fsName = request.headers.value('dev_fs_name'); + final String devicePath = utf8.decode(base64.decode(request.headers.value('dev_fs_uri_b64'))); + messages.add('writeFile $fsName $devicePath'); + request.drain>().then((List value) { + request.response + ..write('Got it') + ..close(); + }); + }); + } + + Future tearDown() async { + await _server?.close(); + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class MockVM implements VM { + MockVM(this._service); + + final MockVMService _service; + final Uri _baseUri = Uri.parse('file:///tmp/devfs/test'); + bool _devFSExists = false; + + static const int kFileSystemAlreadyExists = 1001; + + @override + Future> createDevFS(String fsName) async { + _service.messages.add('create $fsName'); + if (_devFSExists) { + throw rpc.RpcException(kFileSystemAlreadyExists, 'File system already exists'); + } + _devFSExists = true; + return {'uri': '$_baseUri'}; + } + + @override + Future> deleteDevFS(String fsName) async { + _service.messages.add('destroy $fsName'); + _devFSExists = false; + return {'type': 'Success'}; + } + + @override + Future> invokeRpcRaw( + String method, { + Map params = const {}, + Duration timeout, + bool timeoutFatal = true, + }) async { + _service.messages.add('$method $params'); + return {'success': true}; + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + + +final List _tempDirs = []; final Map _packages = {}; +Directory _newTempDir(FileSystem fs) { + final Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_devfs${_tempDirs.length}_test.'); + _tempDirs.add(tempDir); + return tempDir; +} + +void _cleanupTempDirs() { + while (_tempDirs.isNotEmpty) + tryToDelete(_tempDirs.removeLast()); +} Future _createPackage(FileSystem fs, String pkgName, String pkgFileName, { bool doubleSlash = false }) async { - String pkgFilePath = fs.path.join(pkgName, 'lib', pkgFileName); + final Directory pkgTempDir = _newTempDir(fs); + String pkgFilePath = fs.path.join(pkgTempDir.path, pkgName, 'lib', pkgFileName); if (doubleSlash) { // Force two separators into the path. - pkgFilePath = fs.path.join(pkgName, 'lib', pkgFileName); + final String doubleSlash = fs.path.separator + fs.path.separator; + pkgFilePath = pkgTempDir.path + doubleSlash + fs.path.join(pkgName, 'lib', pkgFileName); } final File pkgFile = fs.file(pkgFilePath); await pkgFile.parent.create(recursive: true); @@ -242,5 +290,6 @@ Future _createPackage(FileSystem fs, String pkgName, String pkgFileName, { _packages.forEach((String pkgName, Uri pkgUri) { sb.writeln('$pkgName:$pkgUri'); }); - fs.file('.packages').writeAsStringSync(sb.toString()); + fs.file(fs.path.join(_tempDirs[0].path, '.packages')).writeAsStringSync(sb.toString()); } + diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart index 1d1da520612..8c318bc8c6d 100644 --- a/packages/flutter_tools/test/src/mocks.dart +++ b/packages/flutter_tools/test/src/mocks.dart @@ -14,6 +14,7 @@ import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/build_info.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/ios/devices.dart'; import 'package:flutter_tools/src/ios/simulators.dart'; @@ -477,6 +478,30 @@ class BasicMock { } } +class MockDevFSOperations extends BasicMock implements DevFSOperations { + Map devicePathToContent = {}; + + @override + Future create(String fsName) async { + messages.add('create $fsName'); + return Uri.parse('file:///$fsName'); + } + + @override + Future destroy(String fsName) async { + messages.add('destroy $fsName'); + } + + @override + Future 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 { @override