Refactor DevFS for kernel code (#7529) Refactor DevFS so that it's easier to add new types of content such as kernel code * add tests for DevFS package scanning * add tests for DevFS over VMService protocol * which covers _DevFSHttpWriter and ServiceProtocolDevFSOperations * replace AssetBundleEntry and DevFSEntry with DevFSContent * refactor to cleanup common code and replace some fields with locals * rework .package file generation refactor away DevFSOperations.writeSource * only update .package file if it has changed * only write/delete/evict assets that have been changed/removed
diff --git a/packages/flutter_tools/test/asset_bundle_test.dart b/packages/flutter_tools/test/asset_bundle_test.dart index be4d150..2e1f5f7 100644 --- a/packages/flutter_tools/test/asset_bundle_test.dart +++ b/packages/flutter_tools/test/asset_bundle_test.dart
@@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:flutter_tools/src/asset.dart'; import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/devfs.dart'; import 'package:path/path.dart' as path; import 'package:test/test.dart'; @@ -37,29 +38,26 @@ AssetBundle ab = new AssetBundle.fixed('', 'apple.txt'); expect(ab.entries, isNotEmpty); expect(ab.entries.length, 1); - AssetBundleEntry entry = ab.entries.first; - expect(entry, isNotNull); - expect(entry.archivePath, 'apple.txt'); + String archivePath = ab.entries.keys.first; + expect(archivePath, isNotNull); + expect(archivePath, 'apple.txt'); }); test('two entries', () async { AssetBundle ab = new AssetBundle.fixed('', 'apple.txt,packages/flutter_gallery_assets/shrine/products/heels.png'); expect(ab.entries, isNotEmpty); expect(ab.entries.length, 2); - AssetBundleEntry firstEntry = ab.entries.first; - expect(firstEntry, isNotNull); - expect(firstEntry.archivePath, 'apple.txt'); - AssetBundleEntry lastEntry = ab.entries.last; - expect(lastEntry, isNotNull); - expect(lastEntry.archivePath, 'packages/flutter_gallery_assets/shrine/products/heels.png'); + List<String> archivePaths = ab.entries.keys.toList()..sort(); + expect(archivePaths[0], 'apple.txt'); + expect(archivePaths[1], 'packages/flutter_gallery_assets/shrine/products/heels.png'); }); test('file contents', () async { AssetBundle ab = new AssetBundle.fixed(projectRoot, assetPath); expect(ab.entries, isNotEmpty); expect(ab.entries.length, 1); - AssetBundleEntry entry = ab.entries.first; - expect(entry, isNotNull); - expect(entry.archivePath, assetPath); - expect(assetContents, UTF8.decode(entry.contentsAsBytes())); + String archivePath = ab.entries.keys.first; + DevFSContent content = ab.entries[archivePath]; + expect(archivePath, assetPath); + expect(assetContents, UTF8.decode(await content.contentsAsBytes())); }); });
diff --git a/packages/flutter_tools/test/devfs_test.dart b/packages/flutter_tools/test/devfs_test.dart index 4211cc9..4a9b91f 100644 --- a/packages/flutter_tools/test/devfs_test.dart +++ b/packages/flutter_tools/test/devfs_test.dart
@@ -2,10 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; +import 'dart:convert'; + import 'package:flutter_tools/src/asset.dart'; +import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/vmservice.dart'; import 'package:path/path.dart' as path; import 'package:test/test.dart'; @@ -14,72 +19,319 @@ import 'src/mocks.dart'; void main() { - String filePath = 'bar/foo.txt'; - String filePath2 = 'foo/bar.txt'; + final String filePath = 'bar/foo.txt'; + final String filePath2 = 'foo/bar.txt'; Directory tempDir; String basePath; - MockDevFSOperations devFSOperations = new MockDevFSOperations(); DevFS devFS; - AssetBundle assetBundle = new AssetBundle(); - assetBundle.entries.add(new AssetBundleEntry.fromString('a.txt', '')); - group('devfs', () { - testUsingContext('create local file system', () async { - tempDir = fs.systemTempDirectory.createTempSync(); + final AssetBundle assetBundle = new AssetBundle(); + + group('DevFSContent', () { + test('bytes', () { + DevFSByteContent content = new DevFSByteContent(<int>[4, 5, 6]); + expect(content.bytes, orderedEquals(<int>[4, 5, 6])); + expect(content.isModified, isTrue); + expect(content.isModified, isFalse); + content.bytes = <int>[7, 8, 9, 2]; + expect(content.bytes, orderedEquals(<int>[7, 8, 9, 2])); + expect(content.isModified, isTrue); + expect(content.isModified, isFalse); + }); + test('string', () { + DevFSStringContent content = new DevFSStringContent('some string'); + expect(content.string, 'some string'); + expect(content.bytes, orderedEquals(UTF8.encode('some string'))); + expect(content.isModified, isTrue); + 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); + }); + }); + + group('devfs local', () { + MockDevFSOperations devFSOperations = new MockDevFSOperations(); + + setUpAll(() { + tempDir = _newTempDir(); basePath = tempDir.path; + }); + tearDownAll(_cleanupTempDirs); + + testUsingContext('create dev file system', () async { + // simulate workspace File file = fs.file(path.join(basePath, filePath)); await file.parent.create(recursive: true); file.writeAsBytesSync(<int>[1, 2, 3]); - }); - testUsingContext('create dev file system', () async { + + // simulate package + await _createPackage('somepkg', 'somefile.txt'); + devFS = new DevFS.operations(devFSOperations, 'test', tempDir); await devFS.create(); - expect(devFSOperations.contains('create test'), isTrue); - }); - testUsingContext('populate dev file system', () async { - await devFS.update(); - expect(devFSOperations.contains('writeFile test bar/foo.txt'), isTrue); + devFSOperations.expectMessages(<String>['create test']); + expect(devFS.assetPathsToEvict, isEmpty); + + int bytes = await devFS.update(); + devFSOperations.expectMessages(<String>[ + 'writeFile test .packages', + 'writeFile test bar/foo.txt', + 'writeFile test packages/somepkg/somefile.txt', + ]); + expect(devFS.assetPathsToEvict, isEmpty); + expect(bytes, 31); }); testUsingContext('add new file to local file system', () async { File file = fs.file(path.join(basePath, filePath2)); await file.parent.create(recursive: true); file.writeAsBytesSync(<int>[1, 2, 3, 4, 5, 6, 7]); - await devFS.update(); - expect(devFSOperations.contains('writeFile test foo/bar.txt'), isTrue); + int bytes = await devFS.update(); + devFSOperations.expectMessages(<String>[ + 'writeFile test foo/bar.txt', + ]); + expect(devFS.assetPathsToEvict, isEmpty); + expect(bytes, 7); }); testUsingContext('modify existing file on local file system', () async { File file = fs.file(path.join(basePath, filePath)); // Set the last modified time to 5 seconds in the past. updateFileModificationTime(file.path, new DateTime.now(), -5); - await devFS.update(); + int bytes = await devFS.update(); + devFSOperations.expectMessages(<String>[]); + expect(devFS.assetPathsToEvict, isEmpty); + expect(bytes, 0); + await file.writeAsBytes(<int>[1, 2, 3, 4, 5, 6]); - await devFS.update(); - expect(devFSOperations.contains('writeFile test bar/foo.txt'), isTrue); + bytes = await devFS.update(); + devFSOperations.expectMessages(<String>[ + 'writeFile test bar/foo.txt', + ]); + expect(devFS.assetPathsToEvict, isEmpty); + expect(bytes, 6); }); testUsingContext('delete a file from the local file system', () async { File file = fs.file(path.join(basePath, filePath)); await file.delete(); - await devFS.update(); - expect(devFSOperations.contains('deleteFile test bar/foo.txt'), isTrue); + int bytes = await devFS.update(); + devFSOperations.expectMessages(<String>[ + 'deleteFile test bar/foo.txt', + ]); + expect(devFS.assetPathsToEvict, isEmpty); + expect(bytes, 0); }); - testUsingContext('add file in an asset bundle', () async { - await devFS.update(bundle: assetBundle, bundleDirty: true); - expect(devFSOperations.contains( - 'writeFile test ${getAssetBuildDirectory()}/a.txt'), isTrue); + testUsingContext('add new package', () async { + await _createPackage('newpkg', 'anotherfile.txt'); + int bytes = await devFS.update(); + devFSOperations.expectMessages(<String>[ + 'writeFile test .packages', + 'writeFile test packages/newpkg/anotherfile.txt', + ]); + expect(devFS.assetPathsToEvict, isEmpty); + expect(bytes, 51); + }); + testUsingContext('add an asset bundle', () async { + assetBundle.entries['a.txt'] = new DevFSStringContent('abc'); + int bytes = await devFS.update(bundle: assetBundle, bundleDirty: true); + devFSOperations.expectMessages(<String>[ + 'writeFile test ${getAssetBuildDirectory()}/a.txt', + ]); + expect(devFS.assetPathsToEvict, unorderedMatches(<String>['a.txt'])); + devFS.assetPathsToEvict.clear(); + expect(bytes, 3); + }); + testUsingContext('add a file to the asset bundle - bundleDirty', () async { + assetBundle.entries['b.txt'] = new DevFSStringContent('abcd'); + int bytes = await devFS.update(bundle: assetBundle, bundleDirty: true); + // Expect entire asset bundle written because bundleDirty is true + devFSOperations.expectMessages(<String>[ + 'writeFile test ${getAssetBuildDirectory()}/a.txt', + 'writeFile test ${getAssetBuildDirectory()}/b.txt', + ]); + expect(devFS.assetPathsToEvict, unorderedMatches(<String>[ + 'a.txt', 'b.txt'])); + devFS.assetPathsToEvict.clear(); + expect(bytes, 7); }); testUsingContext('add a file to the asset bundle', () async { - assetBundle.entries.add(new AssetBundleEntry.fromString('b.txt', '')); - await devFS.update(bundle: assetBundle, bundleDirty: true); - expect(devFSOperations.contains( - 'writeFile test ${getAssetBuildDirectory()}/b.txt'), isTrue); + assetBundle.entries['c.txt'] = new DevFSStringContent('12'); + int bytes = await devFS.update(bundle: assetBundle); + devFSOperations.expectMessages(<String>[ + 'writeFile test ${getAssetBuildDirectory()}/c.txt', + ]); + expect(devFS.assetPathsToEvict, unorderedMatches(<String>[ + 'c.txt'])); + devFS.assetPathsToEvict.clear(); + expect(bytes, 2); }); testUsingContext('delete a file from the asset bundle', () async { + assetBundle.entries.remove('c.txt'); + int bytes = await devFS.update(bundle: assetBundle); + devFSOperations.expectMessages(<String>[ + 'deleteFile test ${getAssetBuildDirectory()}/c.txt', + ]); + expect(devFS.assetPathsToEvict, unorderedMatches(<String>['c.txt'])); + devFS.assetPathsToEvict.clear(); + expect(bytes, 0); + }); + testUsingContext('delete all files from the asset bundle', () async { assetBundle.entries.clear(); - await devFS.update(bundle: assetBundle, bundleDirty: true); - expect(devFSOperations.contains( - 'deleteFile test ${getAssetBuildDirectory()}/b.txt'), isTrue); + int bytes = await devFS.update(bundle: assetBundle, bundleDirty: true); + devFSOperations.expectMessages(<String>[ + 'deleteFile test ${getAssetBuildDirectory()}/a.txt', + 'deleteFile test ${getAssetBuildDirectory()}/b.txt', + ]); + expect(devFS.assetPathsToEvict, unorderedMatches(<String>[ + 'a.txt', 'b.txt' + ])); + devFS.assetPathsToEvict.clear(); + expect(bytes, 0); }); testUsingContext('delete dev file system', () async { await devFS.destroy(); + devFSOperations.expectMessages(<String>['destroy test']); + expect(devFS.assetPathsToEvict, isEmpty); }); }); + + group('devfs remote', () { + MockVMService vmService; + + setUpAll(() async { + tempDir = _newTempDir(); + basePath = tempDir.path; + vmService = new MockVMService(); + await vmService.setUp(); + }); + tearDownAll(() async { + await vmService.tearDown(); + _cleanupTempDirs(); + }); + + testUsingContext('create dev file system', () async { + // simulate workspace + File file = fs.file(path.join(basePath, filePath)); + await file.parent.create(recursive: true); + file.writeAsBytesSync(<int>[1, 2, 3]); + + // simulate package + await _createPackage('somepkg', 'somefile.txt'); + + devFS = new DevFS(vmService, 'test', tempDir); + await devFS.create(); + vmService.expectMessages(<String>['create test']); + expect(devFS.assetPathsToEvict, isEmpty); + + int bytes = await devFS.update(); + vmService.expectMessages(<String>[ + 'writeFile test .packages', + 'writeFile test bar/foo.txt', + 'writeFile test packages/somepkg/somefile.txt', + ]); + expect(devFS.assetPathsToEvict, isEmpty); + expect(bytes, 31); + }, timeout: new Timeout(new Duration(seconds: 5))); + + testUsingContext('delete dev file system', () async { + await devFS.destroy(); + vmService.expectMessages(<String>['_deleteDevFS {fsName: test}']); + expect(devFS.assetPathsToEvict, isEmpty); + }); + }); +} + +class MockVMService extends BasicMock implements VMService { + Uri _httpAddress; + HttpServer _server; + MockVM _vm; + + MockVMService() { + _vm = new MockVM(this); + } + + @override + Uri get httpAddress => _httpAddress; + + @override + VM get vm => _vm; + + Future<Null> setUp() async { + _server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 0); + _httpAddress = Uri.parse('http://127.0.0.1:${_server.port}'); + _server.listen((HttpRequest request) { + String fsName = request.headers.value('dev_fs_name'); + String devicePath = UTF8.decode(BASE64.decode(request.headers.value('dev_fs_path_b64'))); + messages.add('writeFile $fsName $devicePath'); + request.drain().then((_) { + request.response + ..write('Got it') + ..close(); + }); + }); + } + + Future<Null> tearDown() async { + await _server.close(); + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class MockVM implements VM { + final MockVMService _service; + final Uri _baseUri = Uri.parse('file:///tmp/devfs/test'); + + MockVM(this._service); + + @override + Future<Map<String, dynamic>> createDevFS(String fsName) async { + _service.messages.add('create $fsName'); + return <String, dynamic>{'uri': '$_baseUri'}; + } + + @override + Future<Map<String, dynamic>> invokeRpcRaw( + String method, [Map<String, dynamic> params, Duration timeout]) async { + _service.messages.add('$method $params'); + return <String, dynamic>{'success': true}; + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + + +final List<Directory> _tempDirs = <Directory>[]; +final Map <String, Directory> _packages = <String, Directory>{}; + +Directory _newTempDir() { + Directory tempDir = fs.systemTempDirectory.createTempSync('devfs${_tempDirs.length}'); + _tempDirs.add(tempDir); + return tempDir; +} + +void _cleanupTempDirs() { + while (_tempDirs.length > 0) { + _tempDirs.removeLast().deleteSync(recursive: true); + } +} + +Future<Null> _createPackage(String pkgName, String pkgFileName) async { + final Directory pkgTempDir = _newTempDir(); + File pkgFile = fs.file(path.join(pkgTempDir.path, pkgName, 'lib', pkgFileName)); + await pkgFile.parent.create(recursive: true); + pkgFile.writeAsBytesSync(<int>[11, 12, 13]); + _packages[pkgName] = pkgTempDir; + StringBuffer sb = new StringBuffer(); + _packages.forEach((String pkgName, Directory pkgTempDir) { + sb.writeln('$pkgName:${pkgTempDir.path}/$pkgName/lib'); + }); + fs.file(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 fb56b7c..706022b 100644 --- a/packages/flutter_tools/test/src/mocks.dart +++ b/packages/flutter_tools/test/src/mocks.dart
@@ -13,6 +13,7 @@ import 'package:flutter_tools/src/ios/simulators.dart'; import 'package:flutter_tools/src/runner/flutter_command.dart'; import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; class MockApplicationPackageStore extends ApplicationPackageStore { MockApplicationPackageStore() : super( @@ -74,9 +75,16 @@ ..commandValidator = () => true; } -class MockDevFSOperations implements DevFSOperations { +/// Common functionality for tracking mock interaction +class BasicMock { final List<String> messages = new List<String>(); + void expectMessages(List<String> expectedMessages) { + List<String> actualMessages = new List<String>.from(messages); + messages.clear(); + expect(actualMessages, unorderedEquals(expectedMessages)); + } + bool contains(String match) { print('Checking for `$match` in:'); print(messages); @@ -84,7 +92,9 @@ messages.clear(); return result; } +} +class MockDevFSOperations extends BasicMock implements DevFSOperations { @override Future<Uri> create(String fsName) async { messages.add('create $fsName'); @@ -97,19 +107,12 @@ } @override - Future<dynamic> writeFile(String fsName, DevFSEntry entry) async { - messages.add('writeFile $fsName ${entry.devicePath}'); + Future<dynamic> writeFile(String fsName, String devicePath, DevFSContent content) async { + messages.add('writeFile $fsName $devicePath'); } @override - Future<dynamic> deleteFile(String fsName, DevFSEntry entry) async { - messages.add('deleteFile $fsName ${entry.devicePath}'); - } - - @override - Future<dynamic> writeSource(String fsName, - String devicePath, - String contents) async { - messages.add('writeSource $fsName $devicePath'); + Future<dynamic> deleteFile(String fsName, String devicePath) async { + messages.add('deleteFile $fsName $devicePath'); } }