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/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart index 70ca377..1ccb438 100644 --- a/packages/flutter_tools/lib/src/asset.dart +++ b/packages/flutter_tools/lib/src/asset.dart
@@ -12,45 +12,20 @@ import 'base/file_system.dart'; import 'build_info.dart'; import 'cache.dart'; +import 'devfs.dart'; import 'dart/package_map.dart'; import 'globals.dart'; -/// An entry in an asset bundle. -class AssetBundleEntry { - /// An entry backed by a File. - AssetBundleEntry.fromFile(this.archivePath, this.file) - : _contents = null; - - /// An entry backed by a String. - AssetBundleEntry.fromString(this.archivePath, this._contents) - : file = null; - - /// The path within the bundle. - final String archivePath; - - /// The payload. - List<int> contentsAsBytes() { - if (_contents != null) { - return UTF8.encode(_contents); - } else { - return file.readAsBytesSync(); - } - } - - bool get isStringEntry => _contents != null; - int get contentsLength => _contents.length; - - final File file; - final String _contents; -} - /// A bundle of assets. class AssetBundle { - final Set<AssetBundleEntry> entries = new Set<AssetBundleEntry>(); + final Map<String, DevFSContent> entries = <String, DevFSContent>{}; static const String defaultManifestPath = 'flutter.yaml'; + static const String _kAssetManifestJson = 'AssetManifest.json'; + static const String _kFontManifestJson = 'FontManifest.json'; static const String _kFontSetMaterial = 'material'; static const String _kFontSetRoboto = 'roboto'; + static const String _kLICENSE = 'LICENSE'; bool _fixed = false; DateTime _lastBuildTimestamp; @@ -73,8 +48,7 @@ continue; final String assetPath = path.join(projectRoot, asset); final String archivePath = asset; - entries.add( - new AssetBundleEntry.fromFile(archivePath, fs.file(assetPath))); + entries[archivePath] = new DevFSFileContent(fs.file(assetPath)); } } @@ -111,7 +85,7 @@ } if (manifest == null) { // No manifest file found for this application. - entries.add(new AssetBundleEntry.fromString('AssetManifest.json', '{}')); + entries[_kAssetManifestJson] = new DevFSStringContent('{}'); return 0; } if (manifest != null) { @@ -141,16 +115,11 @@ manifestDescriptor['uses-material-design']; for (_Asset asset in assetVariants.keys) { - AssetBundleEntry assetEntry = _createAssetEntry(asset); - if (assetEntry == null) - return 1; - entries.add(assetEntry); - + assert(asset.assetFileExists); + entries[asset.assetEntry] = new DevFSFileContent(asset.assetFile); for (_Asset variant in assetVariants[asset]) { - AssetBundleEntry variantEntry = _createAssetEntry(variant); - if (variantEntry == null) - return 1; - entries.add(variantEntry); + assert(variant.assetFileExists); + entries[variant.assetEntry] = new DevFSFileContent(variant.assetFile); } } @@ -161,29 +130,27 @@ materialAssets.addAll(_getMaterialAssets(_kFontSetRoboto)); } for (_Asset asset in materialAssets) { - AssetBundleEntry assetEntry = _createAssetEntry(asset); - if (assetEntry == null) - return 1; - entries.add(assetEntry); + assert(asset.assetFileExists); + entries[asset.assetEntry] = new DevFSFileContent(asset.assetFile); } - entries.add(_createAssetManifest(assetVariants)); + entries[_kAssetManifestJson] = _createAssetManifest(assetVariants); - AssetBundleEntry fontManifest = + DevFSContent fontManifest = _createFontManifest(manifestDescriptor, usesMaterialDesign, includeDefaultFonts, includeRobotoFonts); if (fontManifest != null) - entries.add(fontManifest); + entries[_kFontManifestJson] = fontManifest; // TODO(ianh): Only do the following line if we've changed packages or if our LICENSE file changed - entries.add(await _obtainLicenses(packageMap, assetBasePath, reportPackages: reportLicensedPackages)); + entries[_kLICENSE] = await _obtainLicenses(packageMap, assetBasePath, reportPackages: reportLicensedPackages); return 0; } void dump() { printTrace('Dumping AssetBundle:'); - for (AssetBundleEntry entry in entries) { - printTrace(entry.archivePath); + for (String archivePath in entries.keys.toList()..sort()) { + printTrace(archivePath); } } } @@ -256,8 +223,8 @@ final String _licenseSeparator = '\n' + ('-' * 80) + '\n'; -/// Returns a AssetBundleEntry representing the license file. -Future<AssetBundleEntry> _obtainLicenses( +/// Returns a DevFSContent representing the license file. +Future<DevFSContent> _obtainLicenses( PackageMap packageMap, String assetBase, { bool reportPackages } @@ -323,17 +290,10 @@ final String combinedLicenses = combinedLicensesList.join(_licenseSeparator); - return new AssetBundleEntry.fromString('LICENSE', combinedLicenses); + return new DevFSStringContent(combinedLicenses); } - -/// Create a [AssetBundleEntry] from the given [_Asset]; the asset must exist. -AssetBundleEntry _createAssetEntry(_Asset asset) { - assert(asset.assetFileExists); - return new AssetBundleEntry.fromFile(asset.assetEntry, asset.assetFile); -} - -AssetBundleEntry _createAssetManifest(Map<_Asset, List<_Asset>> assetVariants) { +DevFSContent _createAssetManifest(Map<_Asset, List<_Asset>> assetVariants) { Map<String, List<String>> json = <String, List<String>>{}; for (_Asset main in assetVariants.keys) { List<String> variants = <String>[]; @@ -341,10 +301,10 @@ variants.add(variant.relativePath); json[main.relativePath] = variants; } - return new AssetBundleEntry.fromString('AssetManifest.json', JSON.encode(json)); + return new DevFSStringContent(JSON.encode(json)); } -AssetBundleEntry _createFontManifest(Map<String, dynamic> manifestDescriptor, +DevFSContent _createFontManifest(Map<String, dynamic> manifestDescriptor, bool usesMaterialDesign, bool includeDefaultFonts, bool includeRobotoFonts) { @@ -358,7 +318,7 @@ fonts.addAll(manifestDescriptor['fonts']); if (fonts.isEmpty) return null; - return new AssetBundleEntry.fromString('FontManifest.json', JSON.encode(fonts)); + return new DevFSStringContent(JSON.encode(fonts)); } /// Given an assetBase location and a flutter.yaml manifest, return a map of
diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart index 3a4d4dc..e741fef 100644 --- a/packages/flutter_tools/lib/src/devfs.dart +++ b/packages/flutter_tools/lib/src/devfs.dart
@@ -27,60 +27,45 @@ DevFSConfig get devFSConfig => context[DevFSConfig]; -// A file that has been added to a DevFS. -class DevFSEntry { - DevFSEntry(this.devicePath, this.file) - : bundleEntry = null; +/// Common superclass for content copied to the device. +abstract class DevFSContent { + bool _exists = true; - DevFSEntry.bundle(this.devicePath, AssetBundleEntry bundleEntry) - : bundleEntry = bundleEntry, - file = bundleEntry.file; + /// Return `true` if this is the first time this method is called + /// or if the entry has been modified since this method was last called. + bool get isModified; - final String devicePath; - final AssetBundleEntry bundleEntry; - String get assetPath => bundleEntry.archivePath; + int get size; + + Future<List<int>> contentsAsBytes(); + + Stream<List<int>> contentsAsStream(); + + Stream<List<int>> contentsAsCompressedStream() { + return contentsAsStream().transform(GZIP.encoder); + } +} + +// File content to be copied to the device. +class DevFSFileContent extends DevFSContent { + DevFSFileContent(this.file); final FileSystemEntity file; FileSystemEntity _linkTarget; FileStat _fileStat; - // When we scanned for files, did this still exist? - bool _exists = false; - DateTime get lastModified => _fileStat?.modified; - bool get _isSourceEntry => file == null; - bool get _isAssetEntry => bundleEntry != null; - bool get stillExists { - if (_isSourceEntry) - return true; - _stat(); - return _fileStat.type != FileSystemEntityType.NOT_FOUND; - } - bool get isModified { - if (_isSourceEntry) - return true; - if (_fileStat == null) { - _stat(); - return true; + File _getFile() { + if (_linkTarget != null) { + return _linkTarget; } - FileStat _oldFileStat = _fileStat; - _stat(); - return _fileStat.modified.isAfter(_oldFileStat.modified); - } - - int get size { - if (_isSourceEntry) { - return bundleEntry.contentsLength; - } else { - if (_fileStat == null) { - _stat(); - } - return _fileStat.size; + if (file is Link) { + // The link target. + return fs.file(file.resolveSymbolicLinksSync()); } + return file; } void _stat() { - if (_isSourceEntry) - return; if (_linkTarget != null) { // Stat the cached symlink target. _fileStat = _linkTarget.statSync(); @@ -99,48 +84,86 @@ } } - File _getFile() { - if (_linkTarget != null) { - return _linkTarget; - } - if (file is Link) { - // The link target. - return fs.file(file.resolveSymbolicLinksSync()); - } - return file; + @override + bool get isModified { + FileStat _oldFileStat = _fileStat; + _stat(); + return _oldFileStat == null || _fileStat.modified.isAfter(_oldFileStat.modified); } - Future<List<int>> contentsAsBytes() async { - if (_isSourceEntry) - return bundleEntry.contentsAsBytes(); - final File file = _getFile(); - return file.readAsBytes(); + @override + int get size { + if (_fileStat == null) + _stat(); + return _fileStat.size; } - Stream<List<int>> contentsAsStream() { - if (_isSourceEntry) { - return new Stream<List<int>>.fromIterable( - <List<int>>[bundleEntry.contentsAsBytes()]); - } - final File file = _getFile(); - return file.openRead(); - } + @override + Future<List<int>> contentsAsBytes() => _getFile().readAsBytes(); - Stream<List<int>> contentsAsCompressedStream() { - return contentsAsStream().transform(GZIP.encoder); - } + @override + Stream<List<int>> contentsAsStream() => _getFile().openRead(); } +/// Byte content to be copied to the device. +class DevFSByteContent extends DevFSContent { + DevFSByteContent(this._bytes); + + List<int> _bytes; + + bool _isModified = true; + + List<int> get bytes => _bytes; + + set bytes(List<int> newBytes) { + _bytes = newBytes; + _isModified = true; + } + + /// Return `true` only once so that the content is written to the device only once. + @override + bool get isModified { + bool modified = _isModified; + _isModified = false; + return modified; + } + + @override + int get size => _bytes.length; + + @override + Future<List<int>> contentsAsBytes() async => _bytes; + + @override + Stream<List<int>> contentsAsStream() => + new Stream<List<int>>.fromIterable(<List<int>>[_bytes]); +} + +/// String content to be copied to the device. +class DevFSStringContent extends DevFSByteContent { + DevFSStringContent(String string) : _string = string, super(UTF8.encode(string)); + + String _string; + + String get string => _string; + + set string(String newString) { + _string = newString; + super.bytes = UTF8.encode(_string); + } + + @override + set bytes(List<int> newBytes) { + string = UTF8.decode(newBytes); + } +} /// Abstract DevFS operations interface. abstract class DevFSOperations { Future<Uri> create(String fsName); Future<dynamic> destroy(String fsName); - Future<dynamic> writeFile(String fsName, DevFSEntry entry); - Future<dynamic> deleteFile(String fsName, DevFSEntry entry); - Future<dynamic> writeSource(String fsName, - String devicePath, - String contents); + Future<dynamic> writeFile(String fsName, String devicePath, DevFSContent content); + Future<dynamic> deleteFile(String fsName, String devicePath); } /// An implementation of [DevFSOperations] that speaks to the @@ -163,10 +186,10 @@ } @override - Future<dynamic> writeFile(String fsName, DevFSEntry entry) async { + Future<dynamic> writeFile(String fsName, String devicePath, DevFSContent content) async { List<int> bytes; try { - bytes = await entry.contentsAsBytes(); + bytes = await content.contentsAsBytes(); } catch (e) { return e; } @@ -175,31 +198,18 @@ return await vmService.vm.invokeRpcRaw('_writeDevFSFile', <String, dynamic> { 'fsName': fsName, - 'path': entry.devicePath, + 'path': devicePath, 'fileContents': fileContents }); } catch (e) { - printTrace('DevFS: Failed to write ${entry.devicePath}: $e'); + printTrace('DevFS: Failed to write $devicePath: $e'); } } @override - Future<dynamic> deleteFile(String fsName, DevFSEntry entry) async { + Future<dynamic> deleteFile(String fsName, String devicePath) async { // TODO(johnmccutchan): Add file deletion to the devFS protocol. } - - @override - Future<dynamic> writeSource(String fsName, - String devicePath, - String contents) async { - String fileContents = BASE64.encode(UTF8.encode(contents)); - return await vmService.vm.invokeRpcRaw('_writeDevFSFile', - <String, dynamic> { - 'fsName': fsName, - 'path': devicePath, - 'fileContents': fileContents - }); - } } class _DevFSHttpWriter { @@ -212,18 +222,18 @@ static const int kMaxInFlight = 6; int _inFlight = 0; - List<DevFSEntry> _outstanding; + Map<String, DevFSContent> _outstanding; Completer<Null> _completer; HttpClient _client; int _done; int _max; - Future<Null> write(Set<DevFSEntry> entries, + Future<Null> write(Map<String, DevFSContent> entries, {DevFSProgressReporter progressReporter}) async { _client = new HttpClient(); _client.maxConnectionsPerHost = kMaxInFlight; _completer = new Completer<Null>(); - _outstanding = entries.toList(); + _outstanding = new Map<String, DevFSContent>.from(entries); _done = 0; _max = _outstanding.length; _scheduleWrites(progressReporter); @@ -233,30 +243,32 @@ void _scheduleWrites(DevFSProgressReporter progressReporter) { while (_inFlight < kMaxInFlight) { - if (_outstanding.length == 0) { + if (_outstanding.length == 0) { // Finished. break; } - DevFSEntry entry = _outstanding.removeLast(); - _scheduleWrite(entry, progressReporter); + String devicePath = _outstanding.keys.first; + DevFSContent content = _outstanding.remove(devicePath); + _scheduleWrite(devicePath, content, progressReporter); _inFlight++; } } - Future<Null> _scheduleWrite(DevFSEntry entry, + Future<Null> _scheduleWrite(String devicePath, + DevFSContent content, DevFSProgressReporter progressReporter) async { try { HttpClientRequest request = await _client.putUrl(httpAddress); request.headers.removeAll(HttpHeaders.ACCEPT_ENCODING); request.headers.add('dev_fs_name', fsName); request.headers.add('dev_fs_path_b64', - BASE64.encode(UTF8.encode(entry.devicePath))); - Stream<List<int>> contents = entry.contentsAsCompressedStream(); + BASE64.encode(UTF8.encode(devicePath))); + Stream<List<int>> contents = content.contentsAsCompressedStream(); await request.addStream(contents); HttpClientResponse response = await request.close(); await response.drain(); } catch (e) { - printError('Error writing "${entry.devicePath}" to DevFS: $e'); + printError('Error writing "$devicePath" to DevFS: $e'); } if (progressReporter != null) { _done++; @@ -300,16 +312,12 @@ final String fsName; final Directory rootDirectory; String _packagesFilePath; - final Map<String, DevFSEntry> _entries = <String, DevFSEntry>{}; - final Set<DevFSEntry> _dirtyEntries = new Set<DevFSEntry>(); - final Set<DevFSEntry> _deletedEntries = new Set<DevFSEntry>(); - final Set<DevFSEntry> dirtyAssetEntries = new Set<DevFSEntry>(); + final Map<String, DevFSContent> _entries = <String, DevFSContent>{}; + final Set<String> assetPathsToEvict = new Set<String>(); final List<Future<Map<String, dynamic>>> _pendingOperations = new List<Future<Map<String, dynamic>>>(); - int _bytes = 0; - int get bytes => _bytes; Uri _baseUri; Uri get baseUri => _baseUri; @@ -324,118 +332,88 @@ return _operations.destroy(fsName); } - void _reset() { - // Reset the dirty byte count. - _bytes = 0; - // Mark all entries as possibly deleted. - _entries.forEach((String path, DevFSEntry entry) { - entry._exists = false; - }); - // Clear the dirt entries list. - _dirtyEntries.clear(); - // Clear the deleted entries list. - _deletedEntries.clear(); - // Clear the dirty asset entries. - dirtyAssetEntries.clear(); - } - - Future<dynamic> update({ DevFSProgressReporter progressReporter, + /// Update files on the device and return the number of bytes sync'd + Future<int> update({ DevFSProgressReporter progressReporter, AssetBundle bundle, bool bundleDirty: false, Set<String> fileFilter}) async { - _reset(); + // Mark all entries as possibly deleted. + for (DevFSContent content in _entries.values) { + content._exists = false; + } + + // Scan workspace, packages, and assets printTrace('DevFS: Starting sync from $rootDirectory'); logger.printTrace('Scanning project files'); - Directory directory = rootDirectory; - await _scanDirectory(directory, + await _scanDirectory(rootDirectory, recursive: true, fileFilter: fileFilter); - - printTrace('Scanning package files'); - - StringBuffer sb; if (fs.isFileSync(_packagesFilePath)) { - PackageMap packageMap = new PackageMap(_packagesFilePath); - - for (String packageName in packageMap.map.keys) { - Uri uri = packageMap.map[packageName]; - // This project's own package. - final bool isProjectPackage = uri.toString() == 'lib/'; - final String directoryName = - isProjectPackage ? 'lib' : 'packages/$packageName'; - // If this is the project's package, we need to pass both - // package:<package_name> and lib/ as paths to be checked against - // the filter because we must support both package: imports and relative - // path imports within the project's own code. - final String packagesDirectoryName = - isProjectPackage ? 'packages/$packageName' : null; - Directory directory = fs.directory(uri); - bool packageExists = - await _scanDirectory(directory, - directoryName: directoryName, - recursive: true, - packagesDirectoryName: packagesDirectoryName, - fileFilter: fileFilter); - if (packageExists) { - sb ??= new StringBuffer(); - sb.writeln('$packageName:$directoryName'); - } - } + printTrace('Scanning package files'); + await _scanPackages(fileFilter); } if (bundle != null) { printTrace('Scanning asset files'); - // Synchronize asset bundle. - for (AssetBundleEntry entry in bundle.entries) { - // We write the assets into the AssetBundle working dir so that they - // are in the same location in DevFS and the iOS simulator. - final String devicePath = - path.join(getAssetBuildDirectory(), entry.archivePath); - _scanBundleEntry(devicePath, entry, bundleDirty); - } + bundle.entries.forEach((String archivePath, DevFSContent content) { + _scanBundleEntry(archivePath, content, bundleDirty); + }); } + // Handle deletions. printTrace('Scanning for deleted files'); + String assetBuildDirPrefix = getAssetBuildDirectory() + path.separator; final List<String> toRemove = new List<String>(); - _entries.forEach((String path, DevFSEntry entry) { - if (!entry._exists) { - _deletedEntries.add(entry); - toRemove.add(path); - } - }); - for (int i = 0; i < toRemove.length; i++) { - _entries.remove(toRemove[i]); - } - - if (_deletedEntries.length > 0) { - printTrace('Removing deleted files'); - for (DevFSEntry entry in _deletedEntries) { + _entries.forEach((String devicePath, DevFSContent content) { + if (!content._exists) { Future<Map<String, dynamic>> operation = - _operations.deleteFile(fsName, entry); + _operations.deleteFile(fsName, devicePath); if (operation != null) _pendingOperations.add(operation); + toRemove.add(devicePath); + if (devicePath.startsWith(assetBuildDirPrefix)) { + String archivePath = devicePath.substring(assetBuildDirPrefix.length); + assetPathsToEvict.add(archivePath); + } } + }); + if (toRemove.isNotEmpty) { + printTrace('Removing deleted files'); + toRemove.forEach(_entries.remove); await Future.wait(_pendingOperations); _pendingOperations.clear(); - _deletedEntries.clear(); } - if (_dirtyEntries.length > 0) { + // Update modified files + int numBytes = 0; + Map<String, DevFSContent> dirtyEntries = <String, DevFSContent>{}; + _entries.forEach((String devicePath, DevFSContent content) { + String archivePath; + if (devicePath.startsWith(assetBuildDirPrefix)) + archivePath = devicePath.substring(assetBuildDirPrefix.length); + if (content.isModified || (bundleDirty && archivePath != null)) { + dirtyEntries[devicePath] = content; + numBytes += content.size; + if (archivePath != null) + assetPathsToEvict.add(archivePath); + } + }); + if (dirtyEntries.length > 0) { printTrace('Updating files'); if (_httpWriter != null) { try { - await _httpWriter.write(_dirtyEntries, + await _httpWriter.write(dirtyEntries, progressReporter: progressReporter); } catch (e) { printError("Could not update files on device: $e"); } } else { // Make service protocol requests for each. - for (DevFSEntry entry in _dirtyEntries) { + dirtyEntries.forEach((String devicePath, DevFSContent content) { Future<Map<String, dynamic>> operation = - _operations.writeFile(fsName, entry); + _operations.writeFile(fsName, devicePath, content); if (operation != null) _pendingOperations.add(operation); - } + }); if (progressReporter != null) { final int max = _pendingOperations.length; int complete = 0; @@ -447,53 +425,24 @@ await Future.wait(_pendingOperations, eagerError: true); _pendingOperations.clear(); } - _dirtyEntries.clear(); } - if (sb != null) - await _operations.writeSource(fsName, '.packages', sb.toString()); - printTrace('DevFS: Sync finished'); + return numBytes; } void _scanFile(String devicePath, FileSystemEntity file) { - DevFSEntry entry = _entries[devicePath]; - if (entry == null) { - // New file. - entry = new DevFSEntry(devicePath, file); - _entries[devicePath] = entry; - } - entry._exists = true; - bool needsWrite = entry.isModified; - if (needsWrite) { - if (_dirtyEntries.add(entry)) - _bytes += entry.size; - } + DevFSContent content = _entries.putIfAbsent(devicePath, () => new DevFSFileContent(file)); + content._exists = true; } - void _scanBundleEntry(String devicePath, - AssetBundleEntry assetBundleEntry, - bool bundleDirty) { - DevFSEntry entry = _entries[devicePath]; - if (entry == null) { - // New file. - entry = new DevFSEntry.bundle(devicePath, assetBundleEntry); - _entries[devicePath] = entry; - } - entry._exists = true; - if (!bundleDirty && assetBundleEntry.isStringEntry) { - // String bundle entries are synthetic files that only change if the - // bundle itself changes. Skip them if the bundle is not dirty. - return; - } - bool needsWrite = entry.isModified; - if (needsWrite) { - if (_dirtyEntries.add(entry)) { - _bytes += entry.size; - if (entry._isAssetEntry) - dirtyAssetEntries.add(entry); - } - } + void _scanBundleEntry(String archivePath, DevFSContent content, bool bundleDirty) { + // We write the assets into the AssetBundle working dir so that they + // are in the same location in DevFS and the iOS simulator. + final String devicePath = path.join(getAssetBuildDirectory(), archivePath); + + _entries[devicePath] = content; + content._exists = true; } bool _shouldIgnore(String devicePath) { @@ -578,4 +527,42 @@ } return true; } + + Future<Null> _scanPackages(Set<String> fileFilter) async { + StringBuffer sb; + PackageMap packageMap = new PackageMap(_packagesFilePath); + + for (String packageName in packageMap.map.keys) { + Uri uri = packageMap.map[packageName]; + // This project's own package. + final bool isProjectPackage = uri.toString() == 'lib/'; + final String directoryName = + isProjectPackage ? 'lib' : 'packages/$packageName'; + // If this is the project's package, we need to pass both + // package:<package_name> and lib/ as paths to be checked against + // the filter because we must support both package: imports and relative + // path imports within the project's own code. + final String packagesDirectoryName = + isProjectPackage ? 'packages/$packageName' : null; + Directory directory = fs.directory(uri); + bool packageExists = + await _scanDirectory(directory, + directoryName: directoryName, + recursive: true, + packagesDirectoryName: packagesDirectoryName, + fileFilter: fileFilter); + if (packageExists) { + sb ??= new StringBuffer(); + sb.writeln('$packageName:$directoryName'); + } + } + if (sb != null) { + DevFSContent content = _entries['.packages']; + if (content is DevFSStringContent && content.string == sb.toString()) { + content._exists = true; + return; + } + _entries['.packages'] = new DevFSStringContent(sb.toString()); + } + } }
diff --git a/packages/flutter_tools/lib/src/flx.dart b/packages/flutter_tools/lib/src/flx.dart index d0567dc..7cd8ff6 100644 --- a/packages/flutter_tools/lib/src/flx.dart +++ b/packages/flutter_tools/lib/src/flx.dart
@@ -11,6 +11,7 @@ import 'base/file_system.dart'; import 'base/process.dart'; import 'dart/package_map.dart'; +import 'devfs.dart'; import 'build_info.dart'; import 'globals.dart'; import 'toolchain.dart'; @@ -156,12 +157,12 @@ zipBuilder.entries.addAll(assetBundle.entries); if (snapshotFile != null) - zipBuilder.addEntry(new AssetBundleEntry.fromFile(_kSnapshotKey, snapshotFile)); + zipBuilder.entries[_kSnapshotKey] = new DevFSFileContent(snapshotFile); ensureDirectoryExists(outputPath); printTrace('Encoding zip file to $outputPath'); - zipBuilder.createZip(fs.file(outputPath), fs.directory(workingDirPath)); + await zipBuilder.createZip(fs.file(outputPath), fs.directory(workingDirPath)); printTrace('Built $outputPath.'); }
diff --git a/packages/flutter_tools/lib/src/hot.dart b/packages/flutter_tools/lib/src/hot.dart index 2287575..59a5252 100644 --- a/packages/flutter_tools/lib/src/hot.dart +++ b/packages/flutter_tools/lib/src/hot.dart
@@ -273,7 +273,7 @@ return false; } Status devFSStatus = logger.startProgress('Syncing files to device...'); - await _devFS.update(progressReporter: progressReporter, + int bytes = await _devFS.update(progressReporter: progressReporter, bundle: assetBundle, bundleDirty: rebuildBundle, fileFilter: _dartDependencies); @@ -282,18 +282,19 @@ // Clear the set after the sync so they are recomputed next time. _dartDependencies = null; } - printTrace('Synced ${getSizeAsMB(_devFS.bytes)}.'); + printTrace('Synced ${getSizeAsMB(bytes)}.'); return true; } Future<Null> _evictDirtyAssets() async { - if (_devFS.dirtyAssetEntries.length == 0) + if (_devFS.assetPathsToEvict.isEmpty) return; if (currentView.uiIsolate == null) throw 'Application isolate not found'; - for (DevFSEntry entry in _devFS.dirtyAssetEntries) { - await currentView.uiIsolate.flutterEvictAsset(entry.assetPath); + for (String assetPath in _devFS.assetPathsToEvict) { + await currentView.uiIsolate.flutterEvictAsset(assetPath); } + _devFS.assetPathsToEvict.clear(); } Future<Null> _cleanupDevFS() async {
diff --git a/packages/flutter_tools/lib/src/zip.dart b/packages/flutter_tools/lib/src/zip.dart index 332b913..0aae818 100644 --- a/packages/flutter_tools/lib/src/zip.dart +++ b/packages/flutter_tools/lib/src/zip.dart
@@ -2,10 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:archive/archive.dart'; import 'package:path/path.dart' as path; -import 'asset.dart'; +import 'devfs.dart'; import 'base/file_system.dart'; import 'base/process.dart'; @@ -20,27 +22,32 @@ ZipBuilder._(); - List<AssetBundleEntry> entries = <AssetBundleEntry>[]; + Map<String, DevFSContent> entries = <String, DevFSContent>{}; - void addEntry(AssetBundleEntry entry) => entries.add(entry); - - void createZip(File outFile, Directory zipBuildDir); + Future<Null> createZip(File outFile, Directory zipBuildDir); } class _ArchiveZipBuilder extends ZipBuilder { _ArchiveZipBuilder() : super._(); @override - void createZip(File outFile, Directory zipBuildDir) { + Future<Null> createZip(File outFile, Directory zipBuildDir) async { Archive archive = new Archive(); - for (AssetBundleEntry entry in entries) { - List<int> data = entry.contentsAsBytes(); - archive.addFile(new ArchiveFile.noCompress(entry.archivePath, data.length, data)); - } + final Completer<Null> finished = new Completer<Null>(); + int count = entries.length; + entries.forEach((String archivePath, DevFSContent content) { + content.contentsAsBytes().then((List<int> data) { + archive.addFile(new ArchiveFile.noCompress(archivePath, data.length, data)); + --count; + if (count == 0) + finished.complete(); + }); + }); + await finished.future; List<int> zipData = new ZipEncoder().encode(archive); - outFile.writeAsBytesSync(zipData); + await outFile.writeAsBytes(zipData); } } @@ -48,11 +55,11 @@ _ZipToolBuilder() : super._(); @override - void createZip(File outFile, Directory zipBuildDir) { + Future<Null> createZip(File outFile, Directory zipBuildDir) async { // If there are no assets, then create an empty zip file. if (entries.isEmpty) { List<int> zipData = new ZipEncoder().encode(new Archive()); - outFile.writeAsBytesSync(zipData); + await outFile.writeAsBytes(zipData); return; } @@ -63,23 +70,33 @@ zipBuildDir.deleteSync(recursive: true); zipBuildDir.createSync(recursive: true); - for (AssetBundleEntry entry in entries) { - List<int> data = entry.contentsAsBytes(); - File file = fs.file(path.join(zipBuildDir.path, entry.archivePath)); - file.parent.createSync(recursive: true); - file.writeAsBytesSync(data); - } + final Completer<Null> finished = new Completer<Null>(); + int count = entries.length; + entries.forEach((String archivePath, DevFSContent content) { + content.contentsAsBytes().then((List<int> data) { + File file = fs.file(path.join(zipBuildDir.path, archivePath)); + file.parent.createSync(recursive: true); + file.writeAsBytes(data).then((_) { + --count; + if (count == 0) + finished.complete(); + }); + }); + }); + await finished.future; - if (_getCompressedNames().isNotEmpty) { + final Iterable<String> compressedNames = _getCompressedNames(); + if (compressedNames.isNotEmpty) { runCheckedSync( - <String>['zip', '-q', outFile.absolute.path]..addAll(_getCompressedNames()), + <String>['zip', '-q', outFile.absolute.path]..addAll(compressedNames), workingDirectory: zipBuildDir.path ); } - if (_getStoredNames().isNotEmpty) { + final Iterable<String> storedNames = _getStoredNames(); + if (storedNames.isNotEmpty) { runCheckedSync( - <String>['zip', '-q', '-0', outFile.absolute.path]..addAll(_getStoredNames()), + <String>['zip', '-q', '-0', outFile.absolute.path]..addAll(storedNames), workingDirectory: zipBuildDir.path ); } @@ -87,21 +104,14 @@ static const List<String> _kNoCompressFileExtensions = const <String>['.png', '.jpg']; - bool isAssetCompressed(AssetBundleEntry entry) { + bool isAssetCompressed(String archivePath) { return !_kNoCompressFileExtensions.any( - (String extension) => entry.archivePath.endsWith(extension) + (String extension) => archivePath.endsWith(extension) ); } - Iterable<String> _getCompressedNames() { - return entries - .where(isAssetCompressed) - .map((AssetBundleEntry entry) => entry.archivePath); - } + Iterable<String> _getCompressedNames() => entries.keys.where(isAssetCompressed); - Iterable<String> _getStoredNames() { - return entries - .where((AssetBundleEntry entry) => !isAssetCompressed(entry)) - .map((AssetBundleEntry entry) => entry.archivePath); - } + Iterable<String> _getStoredNames() => entries.keys + .where((String archivePath) => !isAssetCompressed(archivePath)); }
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'); } }