diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index 31974169a7..0b717559ab 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart @@ -388,6 +388,31 @@ class AndroidDevice extends Device { ); } + @override + bool get supportsHotMode => true; + + @override + Future runFromFile(ApplicationPackage package, + String scriptUri, + String packagesUri) async { + AndroidApk apk = package; + List cmd = adbCommandForDevice([ + 'shell', 'am', 'start', + '-a', 'android.intent.action.RUN', + '-d', _deviceBundlePath, + '-f', '0x20000000', // FLAG_ACTIVITY_SINGLE_TOP + ]); + cmd.addAll(['--es', 'file', scriptUri]); + cmd.addAll(['--es', 'packages', packagesUri]); + cmd.add(apk.launchActivity); + String result = runCheckedSync(cmd); + if (result.contains('Error: ')) { + printError(result.trim()); + return false; + } + return true; + } + @override bool get supportsRestart => true; diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index a328b5b5a2..0486ec3d7c 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -17,7 +17,6 @@ import '../ios/devices.dart'; import '../ios/simulators.dart'; import '../run.dart'; import '../runner/flutter_command.dart'; -import 'run.dart' as run; const String protocolVersion = '0.2.0'; @@ -292,7 +291,7 @@ class AppDomain extends Domain { String route = _getStringArg(args, 'route'); String mode = _getStringArg(args, 'mode'); String target = _getStringArg(args, 'target'); - bool reloadSources = _getBoolArg(args, 'reload-sources'); + bool hotMode = _getBoolArg(args, 'hot'); Device device = daemon.deviceDomain._getDevice(deviceId); if (device == null) @@ -301,9 +300,6 @@ class AppDomain extends Domain { if (!FileSystemEntity.isDirectorySync(projectDirectory)) throw "'$projectDirectory' does not exist"; - if (reloadSources != null) - run.useReloadSources = reloadSources; - BuildMode buildMode = getBuildModeForName(mode) ?? BuildMode.debug; DebuggingOptions options; @@ -327,7 +323,8 @@ class AppDomain extends Domain { device, target: target, debuggingOptions: options, - usesTerminalUI: false + usesTerminalUI: false, + hotMode: hotMode ); AppInstance app = new AppInstance(_getNextAppId(), runner); diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 87ad5699fc..7ff0d62d8c 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -19,9 +19,6 @@ import 'build_apk.dart'; import 'install.dart'; import 'trace.dart'; -/// Whether the user has passed the `--reload-sources` command-line option. -bool useReloadSources = false; - abstract class RunCommandBase extends FlutterCommand { RunCommandBase() { addBuildModeFlags(defaultToRelease: false); @@ -58,16 +55,16 @@ class RunCommand extends RunCommandBase { argParser.addOption('debug-port', help: 'Listen to the given port for a debug connection (defaults to $kDefaultObservatoryPort).'); usesPubOption(); + argParser.addFlag('resident', defaultsTo: true, help: 'Don\'t terminate the \'flutter run\' process after starting the application.'); - // Hidden option to ship all the sources of the current project over to the - // embedder via the DevFS observatory API. - argParser.addFlag('devfs', negatable: false, hide: true); - - // Send the _reloadSource command to the VM. - argParser.addFlag('reload-sources', negatable: true, defaultsTo: false, hide: true); + // Option to enable hot reloading. + argParser.addFlag('hot', + negatable: false, + defaultsTo: false, + help: 'Run with support for hot reloading.'); // Hidden option to enable a benchmarking mode. This will run the given // application, measure the startup time and the app restart time, write the @@ -122,14 +119,25 @@ class RunCommand extends RunCommandBase { Cache.releaseLockEarly(); - useReloadSources = argResults['reload-sources']; + // Do some early error checks for hot mode. + bool hotMode = argResults['hot']; + if (hotMode) { + if (getBuildMode() != BuildMode.debug) { + printError('Hot mode only works with debug builds.'); + return 1; + } + if (!deviceForCommand.supportsHotMode) { + printError('Hot mode is not supported by this device.'); + return 1; + } + } if (argResults['resident']) { RunAndStayResident runner = new RunAndStayResident( deviceForCommand, target: target, debuggingOptions: options, - useDevFS: argResults['devfs'] + hotMode: argResults['hot'] ); return runner.run( diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart new file mode 100644 index 0000000000..09b7f8eaa4 --- /dev/null +++ b/packages/flutter_tools/lib/src/devfs.dart @@ -0,0 +1,237 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// 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' show BASE64, UTF8; +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'dart/package_map.dart'; +import 'globals.dart'; +import 'observatory.dart'; + +// A file that has been added to a DevFS. +class DevFSEntry { + DevFSEntry(this.devicePath, this.file); + + final String devicePath; + final File file; + FileStat _fileStat; + + DateTime get lastModified => _fileStat?.modified; + bool get stillExists { + _stat(); + return _fileStat.type != FileSystemEntityType.NOT_FOUND; + } + bool get isModified { + if (_fileStat == null) { + _stat(); + return true; + } + FileStat _oldFileStat = _fileStat; + _stat(); + return _fileStat.modified.isAfter(_oldFileStat.modified); + } + + void _stat() { + _fileStat = file.statSync(); + } +} + + +/// Abstract DevFS operations interface. +abstract class DevFSOperations { + Future create(String fsName); + Future destroy(String fsName); + Future writeFile(String fsName, DevFSEntry entry); + Future writeSource(String fsName, + String devicePath, + String contents); +} + +/// An implementation of [DevFSOperations] that speaks to the +/// service protocol. +class ServiceProtocolDevFSOperations implements DevFSOperations { + final Observatory serviceProtocol; + + ServiceProtocolDevFSOperations(this.serviceProtocol); + + @override + Future create(String fsName) async { + Response response = await serviceProtocol.createDevFS(fsName); + return Uri.parse(response['uri']); + } + + @override + Future destroy(String fsName) async { + await serviceProtocol.sendRequest('_deleteDevFS', + { 'fsName': fsName }); + } + + @override + Future writeFile(String fsName, DevFSEntry entry) async { + List bytes; + try { + bytes = await entry.file.readAsBytes(); + } catch (e) { + return e; + } + String fileContents = BASE64.encode(bytes); + return await serviceProtocol.sendRequest('_writeDevFSFile', + { + 'fsName': fsName, + 'path': entry.devicePath, + 'fileContents': fileContents + }); + } + + @override + Future writeSource(String fsName, + String devicePath, + String contents) async { + String fileContents = BASE64.encode(UTF8.encode(contents)); + return await serviceProtocol.sendRequest('_writeDevFSFile', + { + 'fsName': fsName, + 'path': devicePath, + 'fileContents': fileContents + }); + } +} + +class DevFS { + /// Create a [DevFS] named [fsName] for the local files in [directory]. + DevFS(Observatory serviceProtocol, + this.fsName, + this.rootDirectory) + : _operations = new ServiceProtocolDevFSOperations(serviceProtocol); + + DevFS.operations(this._operations, + this.fsName, + this.rootDirectory); + + final DevFSOperations _operations; + final String fsName; + final Directory rootDirectory; + final Map _entries = {}; + final List> _pendingWrites = new List>(); + Uri _baseUri; + Uri get baseUri => _baseUri; + + Future create() async { + _baseUri = await _operations.create(fsName); + printTrace('DevFS: Created new filesystem on the device ($_baseUri)'); + return _baseUri; + } + + Future destroy() async { + printTrace('DevFS: Deleted filesystem on the device ($_baseUri)'); + return await _operations.destroy(fsName); + } + + Future update() async { + printTrace('DevFS: Starting sync from $rootDirectory'); + // Send the root and lib directories. + Directory directory = rootDirectory; + _syncDirectory(directory, recursive: true); + String packagesFilePath = path.join(rootDirectory.path, kPackagesFileName); + StringBuffer sb; + // Send the packages. + if (FileSystemEntity.isFileSync(packagesFilePath)) { + PackageMap packageMap = new PackageMap(kPackagesFileName); + + for (String packageName in packageMap.map.keys) { + Uri uri = packageMap.map[packageName]; + // Ignore self-references. + if (uri.toString() == 'lib/') + continue; + Directory directory = new Directory.fromUri(uri); + if (_syncDirectory(directory, + directoryName: 'packages/$packageName', + recursive: true)) { + if (sb == null) { + sb = new StringBuffer(); + } + sb.writeln('$packageName:packages/$packageName'); + } + } + } + printTrace('DevFS: Waiting for sync of ${_pendingWrites.length} files ' + 'to finish'); + await Future.wait(_pendingWrites); + _pendingWrites.clear(); + if (sb != null) { + await _operations.writeSource(fsName, '.packages', sb.toString()); + } + printTrace('DevFS: Sync finished'); + // NB: You must call flush after a printTrace if you want to be printed + // immediately. + logger.flush(); + } + + void _syncFile(String devicePath, File file) { + DevFSEntry entry = _entries[devicePath]; + if (entry == null) { + // New file. + entry = new DevFSEntry(devicePath, file); + _entries[devicePath] = entry; + } + bool needsWrite = entry.isModified; + if (needsWrite) { + Future pendingWrite = _operations.writeFile(fsName, entry); + if (pendingWrite != null) { + _pendingWrites.add(pendingWrite); + } else { + printTrace('DevFS: Failed to sync "$devicePath"'); + } + } + } + + bool _shouldIgnore(String path) { + List ignoredPrefixes = ['android/', + 'build/', + 'ios/', + 'packages/analyzer']; + for (String ignoredPrefix in ignoredPrefixes) { + if (path.startsWith(ignoredPrefix)) + return true; + } + return false; + } + + bool _syncDirectory(Directory directory, + {String directoryName, + bool recursive: false, + bool ignoreDotFiles: true}) { + String prefix = directoryName; + if (prefix == null) { + prefix = path.relative(directory.path, from: rootDirectory.path); + if (prefix == '.') + prefix = ''; + } + try { + List files = + directory.listSync(recursive: recursive, followLinks: false); + for (FileSystemEntity file in files) { + if (file is! File) { + // Skip non-files. + continue; + } + if (ignoreDotFiles && path.basename(file.path).startsWith('.')) { + // Skip dot files. + continue; + } + final String devicePath = + path.join(prefix, path.relative(file.path, from: directory.path)); + if (!_shouldIgnore(devicePath)) + _syncFile(devicePath, file); + } + } catch (e) { + // Ignore directory and error. + return false; + } + return true; + } +} diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 6786712b8c..d343e5f716 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -189,6 +189,19 @@ abstract class Device { Map platformArgs }); + /// Does this device implement support for hot reloading / restarting? + bool get supportsHotMode => false; + + /// Does this device need a DevFS to support hot mode? + bool get needsDevFS => true; + + /// Run from a file. Necessary for hot mode. + Future runFromFile(ApplicationPackage package, + String scriptUri, + String packagesUri) { + throw 'runFromFile unsupported'; + } + bool get supportsRestart => false; bool get restartSendsFrameworkInitEvent => true; diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index 4bc2eb9771..78cd7d9bb8 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart @@ -13,11 +13,9 @@ import '../application_package.dart'; import '../base/context.dart'; import '../base/process.dart'; import '../build_info.dart'; -import '../commands/run.dart' as run; import '../device.dart'; import '../flx.dart' as flx; import '../globals.dart'; -import '../observatory.dart'; import '../protocol_discovery.dart'; import 'mac.dart'; @@ -361,6 +359,12 @@ class IOSSimulator extends Device { @override bool get isLocalEmulator => true; + @override + bool get supportsHotMode => true; + + @override + bool get needsDevFS => false; + _IOSSimulatorLogReader _logReader; _IOSSimulatorDevicePortForwarder _portForwarder; @@ -575,32 +579,6 @@ class IOSSimulator extends Device { return (await flx.build(precompiledSnapshot: true)) == 0; } - @override - bool get supportsRestart => run.useReloadSources; - - @override - bool get restartSendsFrameworkInitEvent => false; - - @override - Future restartApp( - ApplicationPackage package, - LaunchResult result, { - String mainPath, - Observatory observatory - }) async { - if (observatory.firstIsolateId == null) - throw 'Application isolate not found'; - Event result = await observatory.reloadSources(observatory.firstIsolateId); - dynamic error = result.response['reloadError']; - if (error != null) { - printError('Error reloading application sources: $error'); - return false; - } else { - await observatory.flutterReassemble(observatory.firstIsolateId); - return true; - } - } - @override Future stopApp(ApplicationPackage app) async { // Currently we don't have a way to stop an app running on iOS. diff --git a/packages/flutter_tools/lib/src/observatory.dart b/packages/flutter_tools/lib/src/observatory.dart index b9d23be99c..bc6a31568e 100644 --- a/packages/flutter_tools/lib/src/observatory.dart +++ b/packages/flutter_tools/lib/src/observatory.dart @@ -9,6 +9,7 @@ import 'dart:io'; import 'package:json_rpc_2/json_rpc_2.dart' as rpc; import 'package:web_socket_channel/io.dart'; +// TODO(johnmccutchan): Rename this class to ServiceProtocol or VmService. class Observatory { Observatory._(this.peer, this.port) { peer.registerMethod('streamNotify', (rpc.Parameters event) { @@ -169,13 +170,13 @@ class Observatory { }); } - // Write multiple files into a file system. - Future writeDevFSFiles(String fsName, { List files }) { - assert(files != null); - - return sendRequest('_writeDevFSFiles', { + // Read one file from a file system. + Future> readDevFSFile(String fsName, String path) { + return sendRequest('_readDevFSFile', { 'fsName': fsName, - 'files': files.map((DevFSFile file) => file.toJson()).toList() + 'path': path + }).then((Response response) { + return BASE64.decode(response.response['fileContents']); }); } @@ -233,25 +234,6 @@ class Observatory { } } -abstract class DevFSFile { - DevFSFile(this.path); - - final String path; - - List getContents(); - - List toJson() => [path, BASE64.encode(getContents())]; -} - -class ByteDevFSFile extends DevFSFile { - ByteDevFSFile(String path, this.contents): super(path); - - final List contents; - - @override - List getContents() => contents; -} - class Response { Response(this.response); diff --git a/packages/flutter_tools/lib/src/run.dart b/packages/flutter_tools/lib/src/run.dart index c6837724b7..43a357f025 100644 --- a/packages/flutter_tools/lib/src/run.dart +++ b/packages/flutter_tools/lib/src/run.dart @@ -14,10 +14,10 @@ import 'build_info.dart'; import 'commands/build_apk.dart'; import 'commands/install.dart'; import 'commands/trace.dart'; -import 'dart/package_map.dart'; import 'device.dart'; import 'globals.dart'; import 'observatory.dart'; +import 'devfs.dart'; /// Given the value of the --target option, return the path of the Dart file /// where the app's main function should be. @@ -37,20 +37,20 @@ class RunAndStayResident { this.target, this.debuggingOptions, this.usesTerminalUI: true, - this.useDevFS: false + this.hotMode: false }); final Device device; final String target; final DebuggingOptions debuggingOptions; final bool usesTerminalUI; - final bool useDevFS; + final bool hotMode; ApplicationPackage _package; String _mainPath; LaunchResult _result; - Completer _exitCompleter = new Completer(); + final Completer _exitCompleter = new Completer(); StreamSubscription _loggingSubscription; Observatory observatory; @@ -207,7 +207,15 @@ class RunAndStayResident { if (debuggingOptions.debuggingEnabled) { observatory = await Observatory.connect(_result.observatoryPort); printTrace('Connected to observatory port: ${_result.observatoryPort}.'); - + if (hotMode && device.needsDevFS) { + bool result = await _updateDevFS(); + if (!result) { + printError('Could not perform initial file synchronization.'); + return 3; + } + printStatus('Launching from sources.'); + await _launchFromDevFS(_package, _mainPath); + } observatory.populateIsolateInfo(); observatory.onExtensionEvent.listen((Event event) { printTrace(event.toString()); @@ -250,15 +258,17 @@ class RunAndStayResident { // F1, help _printHelp(); } else if (lower == 'r' || code == AnsiTerminal.KEY_F5) { - if (device.supportsRestart) { - // F5, restart - restart(); + if (hotMode) { + _reloadSources(); + } else { + if (device.supportsRestart) { + // F5, restart + restart(); + } } } else if (lower == 'q' || code == AnsiTerminal.KEY_F10) { // F10, exit _stopApp(); - } else if (useDevFS && lower == 'd') { - _updateDevFS(); } else if (lower == 'w') { _debugDumpApp(); } else if (lower == 't') { @@ -269,12 +279,14 @@ class RunAndStayResident { ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) async { _resetTerminal(); + await _cleanupDevFS(); await _stopLogger(); await _stopApp(); exit(0); }); ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) async { _resetTerminal(); + await _cleanupDevFS(); await _stopLogger(); await _stopApp(); exit(0); @@ -311,78 +323,84 @@ class RunAndStayResident { observatory.flutterDebugDumpRenderTree(observatory.firstIsolateId); } - DevFS devFS; - - Future _updateDevFS() async { - if (devFS == null) { - devFS = new DevFS(Directory.current, observatory); + DevFS _devFS; + String _devFSProjectRootPath; + Future _updateDevFS() async { + if (_devFS == null) { + Directory directory = Directory.current; + _devFSProjectRootPath = directory.path; + String fsName = path.basename(directory.path); + _devFS = new DevFS(observatory, fsName, directory); try { - await devFS.init(); + await _devFS.create(); } catch (error) { - devFS = null; - printError('Error initializing development client: $error'); - return null; + _devFS = null; + printError('Error initializing DevFS: $error'); + return false; } + + _exitCompleter.future.then((_) async { + await _cleanupDevFS(); + }); } - // Send the root and lib directories. - Directory directory = Directory.current; - _sendFiles(directory, '', _dartFiles(directory.listSync())); - - directory = new Directory('lib'); - _sendFiles(directory, 'lib', _dartFiles(directory.listSync(recursive: true))); - - // Send the packages. - if (FileSystemEntity.isFileSync(kPackagesFileName)) { - PackageMap packageMap = new PackageMap(kPackagesFileName); - - for (String packageName in packageMap.map.keys) { - Uri uri = packageMap.map[packageName]; - // Ignore self-references. - if (uri.toString() == 'lib/') - continue; - Directory directory = new Directory.fromUri(uri); - if (directory.existsSync()) { - _sendFiles( - directory, - 'packages/$packageName', - _dartFiles(directory.listSync(recursive: true)) - ); - } - } - } - - try { - await devFS.flush(); - } catch (error) { - printError('Error sending sources to the client device: $error'); - } + printStatus('DevFS: Updating files on device...'); + await _devFS.update(); + printStatus('DevFS: Finished updating files on device...'); + return true; } - void _sendFiles(Directory base, String prefix, List files) { - String basePath = base.path; - - for (File file in files) { - String devPath = file.path.substring(basePath.length); - if (devPath.startsWith('/')) - devPath = devPath.substring(1); - devFS.stageFile(prefix.isEmpty ? devPath : '$prefix/$devPath', file); + Future _cleanupDevFS() async { + if (_devFS != null) { + // Cleanup the devFS. + await _devFS.destroy(); } + _devFS = null; } - List _dartFiles(List entities) { - return new List.from(entities - .where((FileSystemEntity entity) => entity is File && entity.path.endsWith('.dart'))); + Future _launchFromDevFS(ApplicationPackage package, + String mainScript) async { + String entryPath = path.relative(mainScript, from: _devFSProjectRootPath); + String deviceEntryPath = + _devFS.baseUri.resolve(entryPath).toFilePath(); + String devicePackagesPath = + _devFS.baseUri.resolve('.packages').toFilePath(); + await device.runFromFile(package, + deviceEntryPath, + devicePackagesPath); + } + + Future _reloadSources() async { + if (observatory.firstIsolateId == null) + throw 'Application isolate not found'; + if (_devFS != null) { + await _updateDevFS(); + } + Status reloadStatus = logger.startProgress('Performing hot reload'); + Event result = await observatory.reloadSources(observatory.firstIsolateId); + reloadStatus.stop(showElapsedTime: true); + dynamic error = result.response['reloadError']; + if (error != null) { + printError('Error reloading application sources: $error'); + return false; + } + Status reassembleStatus = + logger.startProgress('Reassembling application'); + await observatory.flutterReassemble(observatory.firstIsolateId); + reassembleStatus.stop(showElapsedTime: true); + return true; } void _printHelp() { - String restartText = device.supportsRestart ? ', "r" or F5 to restart the app,' : ''; + String restartText = ''; + if (hotMode) { + restartText = ', "r" or F5 to perform a hot reload of the app,'; + } else if (device.supportsRestart) { + restartText = ', "r" or F5 to restart the app,'; + } printStatus('Type "h" or F1 for help$restartText and "q", F10, or ctrl-c to quit.'); printStatus('Type "w" to print the widget hierarchy of the app, and "t" for the render tree.'); - - if (useDevFS) - printStatus('Type "d" to send modified project files to the the client\'s DevFS.'); } Future _stopLogger() { @@ -433,88 +451,3 @@ void writeRunBenchmarkFile(Stopwatch startTime, [Stopwatch restartTime]) { new File(benchmarkOut).writeAsStringSync(toPrettyJson(data)); printStatus('Run benchmark written to $benchmarkOut ($data).'); } - -class DevFS { - DevFS(this.directory, this.observatory) { - fsName = path.basename(directory.path); - } - - final Directory directory; - final Observatory observatory; - - String fsName; - String uri; - Map entries = {}; - - Future init() async { - CreateDevFSResponse response = await observatory.createDevFS(fsName); - uri = response.uri; - } - - void stageFile(String devPath, File file) { - entries.putIfAbsent(devPath, () => new _DevFSFileEntry(devPath, file)); - } - - /// Flush any modified files to the devfs. - Future flush() async { - List<_DevFSFileEntry> toSend = entries.values - .where((_DevFSFileEntry entry) => entry.isModified) - .toList(); - - for (_DevFSFileEntry entry in toSend) { - printTrace('sending to devfs: ${entry.devPath}'); - entry.updateLastModified(); - } - - Status status = logger.startProgress('Sending ${toSend.length} files...'); - - if (toSend.isEmpty) { - status.stop(showElapsedTime: true); - return; - } - - try { - List<_DevFSFile> files = toSend.map((_DevFSFileEntry entry) { - return new _DevFSFile('/${entry.devPath}', entry.file); - }).toList(); - - // TODO(devoncarew): Batch this up in larger groups using writeDevFSFiles(). - // The current implementation leaves dangling service protocol calls on a timeout. - await Future.wait(files.map((_DevFSFile file) { - return observatory.writeDevFSFile( - fsName, - path: file.path, - fileContents: file.getContents() - ); - })).timeout(new Duration(seconds: 10)); - } finally { - status.stop(showElapsedTime: true); - } - } - - Future> listDevFSFiles() => observatory.listDevFSFiles(fsName); -} - -class _DevFSFileEntry { - _DevFSFileEntry(this.devPath, this.file); - - final String devPath; - final File file; - - DateTime lastModified; - - bool get isModified => lastModified == null || file.lastModifiedSync().isAfter(lastModified); - - void updateLastModified() { - lastModified = file.lastModifiedSync(); - } -} - -class _DevFSFile extends DevFSFile { - _DevFSFile(String path, this.file) : super(path); - - final File file; - - @override - List getContents() => file.readAsBytesSync(); -} diff --git a/packages/flutter_tools/test/devfs_test.dart b/packages/flutter_tools/test/devfs_test.dart new file mode 100644 index 0000000000..3f8f18b970 --- /dev/null +++ b/packages/flutter_tools/test/devfs_test.dart @@ -0,0 +1,59 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:flutter_tools/src/devfs.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +import 'src/context.dart'; +import 'src/mocks.dart'; + +void main() { + String filePath = 'bar/foo.txt'; + String filePath2 = 'foo/bar.txt'; + Directory tempDir; + String basePath; + MockDevFSOperations devFSOperations = new MockDevFSOperations(); + DevFS devFS; + group('devfs', () { + testUsingContext('create local file system', () async { + tempDir = Directory.systemTemp.createTempSync(); + basePath = tempDir.path; + File file = new File(path.join(basePath, filePath)); + await file.parent.create(recursive: true); + file.writeAsBytesSync([1, 2, 3]); + }); + testUsingContext('create dev file system', () async { + 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); + }); + testUsingContext('modify existing file on local file system', () async { + File file = new File(path.join(basePath, filePath)); + file.writeAsBytesSync([1, 2, 3, 4, 5, 6]); + }); + testUsingContext('update dev file system', () async { + await devFS.update(); + expect(devFSOperations.contains('writeFile test bar/foo.txt'), isTrue); + }); + testUsingContext('add new file to local file system', () async { + File file = new File(path.join(basePath, filePath2)); + await file.parent.create(recursive: true); + file.writeAsBytesSync([1, 2, 3, 4, 5, 6, 7]); + }); + testUsingContext('update dev file system', () async { + await devFS.update(); + expect(devFSOperations.contains('writeFile test foo/bar.txt'), isTrue); + }); + testUsingContext('delete dev file system', () async { + await devFS.destroy(); + }); + }); +} diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart index aeccdb5cf6..49706e70a8 100644 --- a/packages/flutter_tools/test/src/mocks.dart +++ b/packages/flutter_tools/test/src/mocks.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:flutter_tools/src/android/android_device.dart'; import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/build_info.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'; @@ -69,3 +70,36 @@ void applyMocksToCommand(FlutterCommand command) { ..applicationPackages = new MockApplicationPackageStore() ..commandValidator = () => true; } + +class MockDevFSOperations implements DevFSOperations { + final List messages = new List(); + + bool contains(String match) { + bool result = messages.contains(match); + messages.clear(); + return result; + } + + @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, DevFSEntry entry) async { + messages.add('writeFile $fsName ${entry.devicePath}'); + } + + @override + Future writeSource(String fsName, + String devicePath, + String contents) async { + messages.add('writeSource $fsName $devicePath'); + } +}