Unverified Commit cd803ac7 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Improve hot reload performance (#28152)

parent bfac6048
......@@ -70,4 +70,5 @@ dev_dependencies:
mockito: 4.0.0
test_api: 0.2.2
# PUBSPEC CHECKSUM: 422e
......@@ -15,7 +15,6 @@ import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/attach.dart';
import 'package:flutter_tools/src/commands/doctor.dart';
import 'package:flutter_tools/src/fuchsia/fuchsia_sdk.dart';
import 'package:flutter_tools/src/run_hot.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
final ArgParser parser = ArgParser()
......@@ -100,8 +99,7 @@ Future<void> main(List<String> args) async {
platformKernelDill: platformKernelDill,
flutterPatchedSdk: flutterPatchedSdk,
),
HotRunnerConfig: () => HotRunnerConfig()..computeDartDependencies = false,
},
}
);
}
......
......@@ -18,6 +18,7 @@ import '../fuchsia/fuchsia_device.dart';
import '../globals.dart';
import '../ios/devices.dart';
import '../ios/simulators.dart';
import '../project.dart';
import '../protocol_discovery.dart';
import '../resident_runner.dart';
import '../run_cold.dart';
......@@ -129,6 +130,7 @@ class AttachCommand extends FlutterCommand {
Future<FlutterCommandResult> runCommand() async {
final String ipv4Loopback = InternetAddress.loopbackIPv4.address;
final String ipv6Loopback = InternetAddress.loopbackIPv6.address;
final FlutterProject flutterProject = await FlutterProject.current();
Cache.releaseLockEarly();
......@@ -228,6 +230,7 @@ class AttachCommand extends FlutterCommand {
projectRootPath: argResults['project-root'],
dillOutputPath: argResults['output-dill'],
ipv6: usesIpv6,
flutterProject: flutterProject,
)
: ColdRunner(
flutterDevices,
......@@ -289,6 +292,7 @@ class HotRunnerFactory {
String dillOutputPath,
bool stayResident = true,
bool ipv6 = false,
FlutterProject flutterProject,
}) => HotRunner(
devices,
target: target,
......@@ -302,6 +306,7 @@ class HotRunnerFactory {
dillOutputPath: dillOutputPath,
stayResident: stayResident,
ipv6: ipv6,
flutterProject: flutterProject,
);
}
......
......@@ -19,6 +19,7 @@ import '../convert.dart';
import '../device.dart';
import '../emulator.dart';
import '../globals.dart';
import '../project.dart';
import '../resident_runner.dart';
import '../run_cold.dart';
import '../run_hot.dart';
......@@ -341,6 +342,7 @@ class AppDomain extends Domain {
if (await device.isLocalEmulator && !options.buildInfo.supportsEmulator) {
throw '${toTitleCase(options.buildInfo.friendlyModeName)} mode is not supported for emulators.';
}
final FlutterProject flutterProject = await FlutterProject.current();
// We change the current working directory for the duration of the `start` command.
final Directory cwd = fs.currentDirectory;
......@@ -368,6 +370,7 @@ class AppDomain extends Domain {
dillOutputPath: dillOutputPath,
ipv6: ipv6,
hostIsIde: true,
flutterProject: flutterProject,
);
} else {
runner = ColdRunner(
......
......@@ -13,6 +13,7 @@ import '../cache.dart';
import '../device.dart';
import '../globals.dart';
import '../ios/mac.dart';
import '../project.dart';
import '../resident_runner.dart';
import '../run_cold.dart';
import '../run_hot.dart';
......@@ -280,6 +281,7 @@ class RunCommand extends RunCommandBase {
// Enable hot mode by default if `--no-hot` was not passed and we are in
// debug mode.
final bool hotMode = shouldUseHotMode();
final FlutterProject flutterProject = await FlutterProject.current();
writePidFile(argResults['pid-file']);
......@@ -389,6 +391,7 @@ class RunCommand extends RunCommandBase {
saveCompilationTrace: argResults['train'],
stayResident: stayResident,
ipv6: ipv6,
flutterProject: flutterProject,
);
} else {
runner = ColdRunner(
......
......@@ -1093,7 +1093,7 @@ class PubspecDependency extends PubspecLine {
/// Generates the File object for the pubspec.yaml file of a given Directory.
File _pubspecFor(Directory directory) {
return fs.file('${directory.path}/pubspec.yaml');
return fs.file(fs.path.join(directory.path, 'pubspec.yaml'));
}
/// Generates the source of a fake pubspec.yaml file given a list of
......
// 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.
// TODO(dnfield): This will be removed when @jonahwilliams' work on build lands
// ignore: deprecated_member_use
import 'package:analyzer/analyzer.dart' as analyzer;
import '../base/file_system.dart';
import '../dart/package_map.dart';
// List of flutter specific environment configurations.
// See https://github.com/munificent/dep-interface-libraries/blob/master/Proposal.md
// We will populate this list as required. Potentially, all of dart:* libraries
// supported by flutter would end up here.
final List<String> _configurationConstants = <String>['dart.library.io'];
String _dottedNameToString(analyzer.DottedName dottedName) {
String result = '';
for (analyzer.SimpleIdentifier identifier in dottedName.components) {
if (result.isEmpty) {
result += identifier.token.lexeme;
} else {
result += '.' + identifier.token.lexeme;
}
}
return result;
}
class DartDependencySetBuilder {
DartDependencySetBuilder(String mainScriptPath, String packagesFilePath)
: _mainScriptPath = canonicalizePath(mainScriptPath),
_mainScriptUri = fs.path.toUri(mainScriptPath),
_packagesFilePath = canonicalizePath(packagesFilePath);
final String _mainScriptPath;
final String _packagesFilePath;
final Uri _mainScriptUri;
Set<String> build() {
final List<String> dependencies = <String>[_mainScriptPath, _packagesFilePath];
final List<Uri> toProcess = <Uri>[_mainScriptUri];
final PackageMap packageMap = PackageMap(_packagesFilePath);
while (toProcess.isNotEmpty) {
final Uri currentUri = toProcess.removeLast();
final analyzer.CompilationUnit unit = _parse(currentUri.toFilePath());
for (analyzer.Directive directive in unit.directives) {
if (!(directive is analyzer.UriBasedDirective))
continue;
String uriAsString;
if (directive is analyzer.NamespaceDirective) {
final analyzer.NamespaceDirective namespaceDirective = directive;
// If the directive is a conditional import directive, we should
// select the imported uri based on the condition.
for (analyzer.Configuration configuration in namespaceDirective.configurations) {
if (_configurationConstants.contains(_dottedNameToString(configuration.name))) {
uriAsString = configuration.uri.stringValue;
break;
}
}
}
if (uriAsString == null) {
final analyzer.UriBasedDirective uriBasedDirective = directive;
uriAsString = uriBasedDirective.uri.stringValue;
}
Uri uri;
try {
uri = Uri.parse(uriAsString);
} on FormatException {
throw DartDependencyException('Unable to parse URI: $uriAsString');
}
Uri resolvedUri = analyzer.resolveRelativeUri(currentUri, uri);
if (resolvedUri.scheme.startsWith('dart'))
continue;
if (resolvedUri.scheme == 'package') {
final Uri newResolvedUri = packageMap.uriForPackage(resolvedUri);
if (newResolvedUri == null) {
throw DartDependencyException(
'The following Dart file:\n'
' ${currentUri.toFilePath()}\n'
'...refers, in an import, to the following library:\n'
' $resolvedUri\n'
'That library is in a package that is not known. Maybe you forgot to '
'mention it in your pubspec.yaml file?'
);
}
resolvedUri = newResolvedUri;
}
final String path = canonicalizePath(resolvedUri.toFilePath());
if (!dependencies.contains(path)) {
if (!fs.isFileSync(path)) {
throw DartDependencyException(
'The following Dart file:\n'
' ${currentUri.toFilePath()}\n'
'...refers, in an import, to the following library:\n'
' $path\n'
'Unfortunately, that library does not appear to exist on your file system.'
);
}
dependencies.add(path);
toProcess.add(resolvedUri);
}
}
}
return dependencies.toSet();
}
analyzer.CompilationUnit _parse(String path) {
String body;
try {
body = fs.file(path).readAsStringSync();
} on FileSystemException catch (error) {
throw DartDependencyException(
'Could not read "$path" when determining Dart dependencies.',
error,
);
}
try {
return analyzer.parseDirectives(body, name: path);
} on analyzer.AnalyzerError catch (error) {
throw DartDependencyException(
'When trying to parse this Dart file to find its dependencies:\n'
' $path\n'
'...the analyzer failed with the following error:\n'
' ${error.toString().trimRight()}',
error,
);
} on analyzer.AnalyzerErrorGroup catch (error) {
throw DartDependencyException(
'When trying to parse this Dart file to find its dependencies:\n'
' $path\n'
'...the analyzer failed with the following error:\n'
' ${error.toString().trimRight()}',
error,
);
}
}
}
class DartDependencyException implements Exception {
DartDependencyException(this.message, [this.parent]);
final String message;
final Exception parent;
@override
String toString() => message;
}
// 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 'asset.dart';
import 'base/file_system.dart';
import 'dart/dependencies.dart';
import 'globals.dart';
class DependencyChecker {
DependencyChecker(this.builder, this.assets);
final DartDependencySetBuilder builder;
final Set<String> _dependencies = <String>{};
final AssetBundle assets;
/// Returns [true] if any components have been modified after [threshold] or
/// if it cannot be determined.
bool check(DateTime threshold) {
_dependencies.clear();
// Build the set of Dart dependencies.
try {
_dependencies.addAll(builder.build());
} catch (e, st) {
printTrace('DependencyChecker: error determining .dart dependencies:\n$e\n$st');
return true;
}
// TODO(johnmccutchan): Extract dependencies from the AssetBundle too.
// Check all dependency modification times.
for (String path in _dependencies) {
final File file = fs.file(path);
final FileStat stat = file.statSync();
if (stat.type == FileSystemEntityType.notFound) {
printTrace('DependencyChecker: Error stating $path.');
return true;
}
if (stat.modified.isAfter(threshold)) {
printTrace('DependencyChecker: $path is newer than $threshold');
return true;
}
}
printTrace('DependencyChecker: nothing is modified after $threshold.');
return false;
}
}
......@@ -30,8 +30,6 @@ DevFSConfig get devFSConfig => context[DevFSConfig];
/// Common superclass for content copied to the device.
abstract class DevFSContent {
bool _exists = true;
/// 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;
......@@ -59,13 +57,6 @@ abstract class DevFSContent {
class DevFSFileContent extends DevFSContent {
DevFSFileContent(this.file);
static DevFSFileContent clone(DevFSFileContent fsFileContent) {
final DevFSFileContent newFsFileContent = DevFSFileContent(fsFileContent.file);
newFsFileContent._linkTarget = fsFileContent._linkTarget;
newFsFileContent._fileStat = fsFileContent._fileStat;
return newFsFileContent;
}
final FileSystemEntity file;
FileSystemEntity _linkTarget;
FileStat _fileStat;
......@@ -218,7 +209,6 @@ abstract class DevFSOperations {
Future<Uri> create(String fsName);
Future<dynamic> destroy(String fsName);
Future<dynamic> writeFile(String fsName, Uri deviceUri, DevFSContent content);
Future<dynamic> deleteFile(String fsName, Uri deviceUri);
}
/// An implementation of [DevFSOperations] that speaks to the
......@@ -261,11 +251,6 @@ class ServiceProtocolDevFSOperations implements DevFSOperations {
printTrace('DevFS: Failed to write $deviceUri: $error');
}
}
@override
Future<dynamic> deleteFile(String fsName, Uri deviceUri) async {
// TODO(johnmccutchan): Add file deletion to the devFS protocol.
}
}
class DevFSException implements Exception {
......@@ -288,16 +273,14 @@ class _DevFSHttpWriter {
int _inFlight = 0;
Map<Uri, DevFSContent> _outstanding;
Completer<void> _completer;
HttpClient _client;
final HttpClient _client = HttpClient();
Future<void> write(Map<Uri, DevFSContent> entries) async {
_client = HttpClient();
_client.maxConnectionsPerHost = kMaxInFlight;
_completer = Completer<void>();
_outstanding = Map<Uri, DevFSContent>.from(entries);
_scheduleWrites();
await _completer.future;
_client.close();
}
void _scheduleWrites() {
......@@ -405,9 +388,6 @@ class DevFS {
final Map<Uri, DevFSContent> _entries = <Uri, DevFSContent>{};
final Set<String> assetPathsToEvict = <String>{};
final List<Future<Map<String, dynamic>>> _pendingOperations =
<Future<Map<String, dynamic>>>[];
Uri _baseUri;
Uri get baseUri => _baseUri;
......@@ -453,66 +433,33 @@ class DevFS {
DateTime firstBuildTime,
bool bundleFirstUpload = false,
bool bundleDirty = false,
Set<String> fileFilter,
@required ResidentCompiler generator,
String dillOutputPath,
@required bool trackWidgetCreation,
bool fullRestart = false,
String projectRootPath,
@required String pathToReload,
@required List<String> invalidatedFiles,
}) async {
assert(trackWidgetCreation != null);
assert(generator != null);
// 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');
await _scanDirectory(rootDirectory,
recursive: true,
fileFilter: fileFilter);
if (fs.isFileSync(_packagesFilePath)) {
printTrace('Scanning package files');
await _scanPackages(fileFilter);
}
if (bundle != null) {
printTrace('Scanning asset files');
// 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 assetDirectory = getAssetBuildDirectory();
bundle.entries.forEach((String archivePath, DevFSContent content) {
_scanBundleEntry(archivePath, content);
final Uri deviceUri = fs.path.toUri(fs.path.join(assetDirectory, archivePath));
_entries[deviceUri] = content;
});
}
// Handle deletions.
printTrace('Scanning for deleted files');
// Update modified files
final String assetBuildDirPrefix = _asUriPath(getAssetBuildDirectory());
final List<Uri> toRemove = <Uri>[];
_entries.forEach((Uri deviceUri, DevFSContent content) {
if (!content._exists) {
final Future<Map<String, dynamic>> operation =
_operations.deleteFile(fsName, deviceUri)
.then<Map<String, dynamic>>((dynamic v) => v?.cast<String,dynamic>());
if (operation != null)
_pendingOperations.add(operation);
toRemove.add(deviceUri);
if (deviceUri.path.startsWith(assetBuildDirPrefix)) {
final String archivePath = deviceUri.path.substring(assetBuildDirPrefix.length);
assetPathsToEvict.add(archivePath);
}
}
});
if (toRemove.isNotEmpty) {
printTrace('Removing deleted files');
toRemove.forEach(_entries.remove);
await Future.wait<Map<String, dynamic>>(_pendingOperations);
_pendingOperations.clear();
}
final Map<Uri, DevFSContent> dirtyEntries = <Uri, DevFSContent>{};
// Update modified files
int syncedBytes = 0;
final Map<Uri, DevFSContent> dirtyEntries = <Uri, DevFSContent>{};
_entries.forEach((Uri deviceUri, DevFSContent content) {
String archivePath;
if (deviceUri.path.startsWith(assetBuildDirPrefix))
......@@ -527,30 +474,10 @@ class DevFS {
assetPathsToEvict.add(archivePath);
}
});
// We run generator even if [dirtyEntries] was empty because we want to
// keep logic of accepting/rejecting generator's output simple: we must
// accept/reject generator's output after every [update] call. Incremental
// run with no changes is supposed to be fast (considering that it is
// initiated by user key press).
final List<String> invalidatedFiles = <String>[];
final Set<Uri> filesUris = <Uri>{};
for (Uri uri in dirtyEntries.keys.toList()) {
if (!uri.path.startsWith(assetBuildDirPrefix)) {
final DevFSContent content = dirtyEntries[uri];
if (content is DevFSFileContent) {
filesUris.add(uri);
invalidatedFiles.add(content.file.uri.toString());
syncedBytes -= content.size;
}
}
}
// No need to send source files because all compilation is done on the
// host and result of compilation is single kernel file.
filesUris.forEach(dirtyEntries.remove);
printTrace('Compiling dart to kernel with ${invalidatedFiles.length} updated files');
if (fullRestart) {
generator.reset();
}
printTrace('Compiling dart to kernel with ${invalidatedFiles.length} updated files');
final CompilerOutput compilerOutput = await generator.recompile(
mainPath,
invalidatedFiles,
......@@ -566,228 +493,28 @@ class DevFS {
? fs.path.relative(pathToReload, from: projectRootPath)
: pathToReload,
);
if (!dirtyEntries.containsKey(entryUri)) {
final DevFSFileContent content = DevFSFileContent(fs.file(compiledBinary));
dirtyEntries[entryUri] = content;
syncedBytes += content.size;
}
final DevFSFileContent content = DevFSFileContent(fs.file(compiledBinary));
syncedBytes += content.size;
dirtyEntries[entryUri] = content;
}
}
printTrace('Updating files');
if (dirtyEntries.isNotEmpty) {
printTrace('Updating files');
if (_httpWriter != null) {
try {
await _httpWriter.write(dirtyEntries);
} on SocketException catch (socketException, stackTrace) {
printTrace('DevFS sync failed. Lost connection to device: $socketException');
throw DevFSException('Lost connection to device.', socketException, stackTrace);
} catch (exception, stackTrace) {
printError('Could not update files on device: $exception');
throw DevFSException('Sync failed', exception, stackTrace);
}
} else {
// Make service protocol requests for each.
dirtyEntries.forEach((Uri deviceUri, DevFSContent content) {
final Future<Map<String, dynamic>> operation =
_operations.writeFile(fsName, deviceUri, content)
.then<Map<String, dynamic>>((dynamic v) => v?.cast<String, dynamic>());
if (operation != null)
_pendingOperations.add(operation);
});
await Future.wait<Map<String, dynamic>>(_pendingOperations, eagerError: true);
_pendingOperations.clear();
try {
await _httpWriter.write(dirtyEntries);
} on SocketException catch (socketException, stackTrace) {
printTrace('DevFS sync failed. Lost connection to device: $socketException');
throw DevFSException('Lost connection to device.', socketException, stackTrace);
} catch (exception, stackTrace) {
printError('Could not update files on device: $exception');
throw DevFSException('Sync failed', exception, stackTrace);
}
}
printTrace('DevFS: Sync finished');
return UpdateFSReport(success: true, syncedBytes: syncedBytes,
invalidatedSourcesCount: invalidatedFiles.length);
}
void _scanFile(Uri deviceUri, FileSystemEntity file) {
final DevFSContent content = _entries.putIfAbsent(deviceUri, () => DevFSFileContent(file));
content._exists = true;
}
void _scanBundleEntry(String archivePath, DevFSContent content) {
// We write the assets into the AssetBundle working dir so that they
// are in the same location in DevFS and the iOS simulator.
final Uri deviceUri = fs.path.toUri(fs.path.join(getAssetBuildDirectory(), archivePath));
_entries[deviceUri] = content;
content._exists = true;
}
bool _shouldIgnore(Uri deviceUri) {
final List<String> ignoredUriPrefixes = <String>['android/',
_asUriPath(getBuildDirectory()),
'ios/',
'.pub/'];
for (String ignoredUriPrefix in ignoredUriPrefixes) {
if (deviceUri.path.startsWith(ignoredUriPrefix))
return true;
}
return false;
}
bool _shouldSkip(
FileSystemEntity file,
String relativePath,
Uri directoryUriOnDevice, {
bool ignoreDotFiles = true,
}) {
if (file is Directory) {
// Skip non-files.
return true;
}
assert((file is Link) || (file is File));
final String basename = fs.path.basename(file.path);
if (ignoreDotFiles && basename.startsWith('.')) {
// Skip dot files, but not the '.packages' file (even though in dart1
// mode devfs['.packages'] will be overwritten with synthesized string content).
return basename != '.packages';
}
return false;
}
Uri _directoryUriOnDevice(Uri directoryUriOnDevice, Directory directory) {
if (directoryUriOnDevice == null) {
final String relativeRootPath = fs.path.relative(directory.path, from: rootDirectory.path);
if (relativeRootPath == '.') {
directoryUriOnDevice = Uri();
} else {
directoryUriOnDevice = fs.path.toUri(relativeRootPath);
}
}
return directoryUriOnDevice;
}
/// Scan all files from the [fileFilter] that are contained in [directory] and
/// pass various filters (e.g. ignoreDotFiles).
Future<bool> _scanFilteredDirectory(
Set<String> fileFilter,
Directory directory, {
Uri directoryUriOnDevice,
bool ignoreDotFiles = true,
}) async {
directoryUriOnDevice =
_directoryUriOnDevice(directoryUriOnDevice, directory);
try {
final String absoluteDirectoryPath = canonicalizePath(directory.path);
// For each file in the file filter.
for (String filePath in fileFilter) {
if (!filePath.startsWith(absoluteDirectoryPath)) {
// File is not in this directory. Skip.
continue;
}
final String relativePath =
fs.path.relative(filePath, from: directory.path);
final FileSystemEntity file = fs.file(filePath);
if (_shouldSkip(file, relativePath, directoryUriOnDevice, ignoreDotFiles: ignoreDotFiles)) {
continue;
}
final Uri deviceUri = directoryUriOnDevice.resolveUri(fs.path.toUri(relativePath));
if (!_shouldIgnore(deviceUri))
_scanFile(deviceUri, file);
}
} on FileSystemException catch (e) {
_printScanDirectoryError(directory.path, e);
return false;
}
return true;
}
/// Scan all files in [directory] that pass various filters (e.g. ignoreDotFiles).
Future<bool> _scanDirectory(
Directory directory, {
Uri directoryUriOnDevice,
bool recursive = false,
bool ignoreDotFiles = true,
Set<String> fileFilter,
}) async {
directoryUriOnDevice = _directoryUriOnDevice(directoryUriOnDevice, directory);
if ((fileFilter != null) && fileFilter.isNotEmpty) {
// When the fileFilter isn't empty, we can skip crawling the directory
// tree and instead use the fileFilter as the source of potential files.
return _scanFilteredDirectory(fileFilter,
directory,
directoryUriOnDevice: directoryUriOnDevice,
ignoreDotFiles: ignoreDotFiles);
}
try {
final Stream<FileSystemEntity> files =
directory.list(recursive: recursive, followLinks: false);
await for (FileSystemEntity file in files) {
if (!devFSConfig.noDirectorySymlinks && (file is Link)) {
// Check if this is a symlink to a directory and skip it.
try {
final FileSystemEntityType linkType =
fs.statSync(file.resolveSymbolicLinksSync()).type;
if (linkType == FileSystemEntityType.directory)
continue;
} on FileSystemException catch (e) {
_printScanDirectoryError(file.path, e);
continue;
}
}
final String relativePath =
fs.path.relative(file.path, from: directory.path);
if (_shouldSkip(file, relativePath, directoryUriOnDevice, ignoreDotFiles: ignoreDotFiles)) {
continue;
}
final Uri deviceUri = directoryUriOnDevice.resolveUri(fs.path.toUri(relativePath));
if (!_shouldIgnore(deviceUri))
_scanFile(deviceUri, file);
}
} on FileSystemException catch (e) {
_printScanDirectoryError(directory.path, e);
return false;
}
return true;
}
void _printScanDirectoryError(String path, Exception e) {
printError(
'Error while scanning $path.\n'
'Hot Reload might not work until the following error is resolved:\n'
'$e\n'
);
}
Future<void> _scanPackages(Set<String> fileFilter) async {
StringBuffer sb;
final PackageMap packageMap = PackageMap(_packagesFilePath);
for (String packageName in packageMap.map.keys) {
final Uri packageUri = packageMap.map[packageName];
final String packagePath = fs.path.fromUri(packageUri);
final Directory packageDirectory = fs.directory(packageUri);
Uri directoryUriOnDevice = fs.path.toUri(fs.path.join('packages', packageName) + fs.path.separator);
bool packageExists = packageDirectory.existsSync();
if (!packageExists) {
// If the package directory doesn't exist at all, we ignore it.
continue;
}
if (fs.path.isWithin(rootDirectory.path, packagePath)) {
// We already scanned everything under the root directory.
directoryUriOnDevice = fs.path.toUri(
fs.path.relative(packagePath, from: rootDirectory.path) + fs.path.separator
);
} else {
packageExists =
await _scanDirectory(packageDirectory,
directoryUriOnDevice: directoryUriOnDevice,
recursive: true,
fileFilter: fileFilter);
}
if (packageExists) {
sb ??= StringBuffer();
sb.writeln('$packageName:$directoryUriOnDevice');
}
}
invalidatedSourcesCount: invalidatedFiles.length);
}
}
/// Converts a platform-specific file path to a platform-independent Uri path.
String _asUriPath(String filePath) => fs.path.toUri(filePath).path + '/';
......@@ -18,9 +18,7 @@ import 'base/utils.dart';
import 'build_info.dart';
import 'codegen.dart';
import 'compile.dart';
import 'dart/dependencies.dart';
import 'dart/package_map.dart';
import 'dependency_checker.dart';
import 'devfs.dart';
import 'device.dart';
import 'globals.dart';
......@@ -444,10 +442,10 @@ class FlutterDevice {
DateTime firstBuildTime,
bool bundleFirstUpload = false,
bool bundleDirty = false,
Set<String> fileFilter,
bool fullRestart = false,
String projectRootPath,
String pathToReload,
@required List<String> invalidatedFiles,
}) async {
final Status devFSStatus = logger.startProgress(
'Syncing files to device ${device.name}...',
......@@ -462,13 +460,13 @@ class FlutterDevice {
firstBuildTime: firstBuildTime,
bundleFirstUpload: bundleFirstUpload,
bundleDirty: bundleDirty,
fileFilter: fileFilter,
generator: generator,
fullRestart: fullRestart,
dillOutputPath: dillOutputPath,
trackWidgetCreation: trackWidgetCreation,
projectRootPath: projectRootPath,
pathToReload: pathToReload,
invalidatedFiles: invalidatedFiles,
);
} on DevFSException {
devFSStatus.cancel();
......@@ -945,21 +943,19 @@ abstract class ResidentRunner {
}
bool hasDirtyDependencies(FlutterDevice device) {
/// When using the build system, dependency analysis is handled by build
/// runner instead.
if (experimentalBuildEnabled) {
return false;
}
final DartDependencySetBuilder dartDependencySetBuilder =
DartDependencySetBuilder(mainPath, packagesFilePath);
final DependencyChecker dependencyChecker =
DependencyChecker(dartDependencySetBuilder, assetBundle);
if (device.package.packagesFile == null || !device.package.packagesFile.existsSync()) {
return true;
}
final DateTime lastBuildTime = device.package.packagesFile.statSync().modified;
return dependencyChecker.check(lastBuildTime);
// Leave pubspec null to check all dependencies.
final ProjectFileInvalidator projectFileInvalidator = ProjectFileInvalidator(device.package.packagesFile.path, null);
projectFileInvalidator.findInvalidated();
final int lastBuildTime = device.package.packagesFile.statSync().modified.millisecondsSinceEpoch;
for (int updateTime in projectFileInvalidator.updateTime.values) {
if (updateTime > lastBuildTime) {
return true;
}
}
return false;
}
Future<void> preStop() async { }
......
......@@ -7,29 +7,28 @@ import 'dart:async';
import 'package:json_rpc_2/error_code.dart' as rpc_error_code;
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:meta/meta.dart';
import 'package:yaml/yaml.dart';
import 'base/common.dart';
import 'base/context.dart';
import 'base/file_system.dart';
import 'base/logger.dart';
import 'base/platform.dart';
import 'base/terminal.dart';
import 'base/utils.dart';
import 'build_info.dart';
import 'codegen.dart';
import 'compile.dart';
import 'convert.dart';
import 'dart/dependencies.dart';
import 'dart/pub.dart';
import 'dart/package_map.dart';
import 'devfs.dart';
import 'device.dart';
import 'globals.dart';
import 'project.dart';
import 'resident_runner.dart';
import 'usage.dart';
import 'vmservice.dart';
class HotRunnerConfig {
/// Should the hot runner compute the minimal Dart dependencies?
bool computeDartDependencies = true;
/// Should the hot runner assume that the minimal Dart dependencies do not change?
bool stableDartDependencies = false;
/// A hook for implementations to perform any necessary initialization prior
......@@ -71,6 +70,7 @@ class HotRunner extends ResidentRunner {
bool saveCompilationTrace = false,
bool stayResident = true,
bool ipv6 = false,
FlutterProject flutterProject,
}) : super(devices,
target: target,
debuggingOptions: debuggingOptions,
......@@ -79,14 +79,19 @@ class HotRunner extends ResidentRunner {
packagesFilePath: packagesFilePath,
saveCompilationTrace: saveCompilationTrace,
stayResident: stayResident,
ipv6: ipv6);
ipv6: ipv6) {
fileInvalidator = ProjectFileInvalidator(
packagesFilePath ?? fs.path.absolute(PackageMap.globalPackagesPath),
flutterProject,
);
}
final bool benchmarkMode;
final File applicationBinary;
final bool hostIsIde;
bool _didAttach = false;
Set<String> _dartDependencies;
final String dillOutputPath;
ProjectFileInvalidator fileInvalidator;
final Map<String, List<int>> benchmarkData = <String, List<int>>{};
// The initial launch is from a snapshot.
......@@ -98,54 +103,8 @@ class HotRunner extends ResidentRunner {
benchmarkData[name].add(value);
}
Future<bool> _refreshDartDependencies() async {
if (!hotRunnerConfig.computeDartDependencies) {
// Disabled.
return true;
}
if (_dartDependencies != null) {
// Already computed.
return true;
}
try {
// Will return immediately if pubspec.yaml is up-to-date.
await pubGet(
context: PubContext.pubGet,
directory: projectRootPath,
);
} on ToolExit catch (error) {
printError(
'Unable to reload your application because "flutter packages get" failed to update '
'package dependencies.\n'
'$error'
);
return false;
}
/// When using the build system, dependency analysis is handled by build
/// runner instead.
if (experimentalBuildEnabled) {
return true;
}
final DartDependencySetBuilder dartDependencySetBuilder = DartDependencySetBuilder(mainPath, packagesFilePath);
try {
_dartDependencies = Set<String>.from(dartDependencySetBuilder.build());
} on DartDependencyException catch (error) {
printError(
'Your application could not be compiled, because its dependencies could not be established.\n'
'$error'
);
return false;
}
return true;
}
Future<void> _reloadSourcesService(
String isolateId, {
bool force = false,
bool pause = false,
}) async {
Future<void> _reloadSourcesService(String isolateId,
{ bool force = false, bool pause = false }) async {
// TODO(cbernaschina): check that isolateId is the id of the UI isolate.
final OperationResult result = await restart(pauseAfterRestart: pause);
if (!result.isOk) {
......@@ -257,7 +216,6 @@ class HotRunner extends ResidentRunner {
// Measure time to perform a hot restart.
printStatus('Benchmarking hot restart');
await restart(fullRestart: true);
// TODO(johnmccutchan): Modify script entry point.
printStatus('Benchmarking hot reload');
// Measure time to perform a hot reload.
await restart(fullRestart: false);
......@@ -296,12 +254,6 @@ class HotRunner extends ResidentRunner {
return 1;
}
// Determine the Dart dependencies eagerly.
if (!await _refreshDartDependencies()) {
// Some kind of source level error or missing file in the Dart code.
return 1;
}
firstBuildTime = DateTime.now();
for (FlutterDevice device in flutterDevices) {
......@@ -336,8 +288,6 @@ class HotRunner extends ResidentRunner {
result = await restart(fullRestart: false);
}
if (!result.isOk) {
// TODO(johnmccutchan): Attempt to determine the number of errors that
// occurred and tighten this message.
printStatus('Try again after fixing the above error(s).', emphasis: true);
}
} else if (lower == 'l') {
......@@ -364,10 +314,6 @@ class HotRunner extends ResidentRunner {
}
Future<UpdateFSReport> _updateDevFS({ bool fullRestart = false }) async {
if (!await _refreshDartDependencies()) {
// Did not update DevFS because of a Dart source error.
return UpdateFSReport(success: false);
}
final bool isFirstUpload = assetBundle.wasBuiltOnce() == false;
final bool rebuildBundle = assetBundle.needsBuild();
if (rebuildBundle) {
......@@ -376,7 +322,7 @@ class HotRunner extends ResidentRunner {
if (result != 0)
return UpdateFSReport(success: false);
}
final List<String> invalidatedFiles = fileInvalidator.findInvalidated();
final UpdateFSReport results = UpdateFSReport(success: true);
for (FlutterDevice device in flutterDevices) {
results.incorporateResults(await device.updateDevFS(
......@@ -386,39 +332,15 @@ class HotRunner extends ResidentRunner {
firstBuildTime: firstBuildTime,
bundleFirstUpload: isFirstUpload,
bundleDirty: isFirstUpload == false && rebuildBundle,
fileFilter: _dartDependencies,
fullRestart: fullRestart,
projectRootPath: projectRootPath,
pathToReload: getReloadPath(fullRestart: fullRestart),
invalidatedFiles: invalidatedFiles,
));
}
if (!results.success) {
return results;
}
if (!hotRunnerConfig.stableDartDependencies) {
// Clear the set after the sync so they are recomputed next time.
_dartDependencies = null;
}
return results;
}
Future<void> _evictDirtyAssets() {
final List<Future<Map<String, dynamic>>> futures = <Future<Map<String, dynamic>>>[];
for (FlutterDevice device in flutterDevices) {
if (device.devFS.assetPathsToEvict.isEmpty)
continue;
if (device.views.first.uiIsolate == null) {
printError('Application isolate not found for $device');
continue;
}
for (String assetPath in device.devFS.assetPathsToEvict)
futures.add(device.views.first.uiIsolate.flutterEvictAsset(assetPath));
device.devFS.assetPathsToEvict.clear();
}
return Future.wait<Map<String, dynamic>>(futures);
}
void _resetDirtyAssets() {
for (FlutterDevice device in flutterDevices)
device.devFS.assetPathsToEvict.clear();
......@@ -811,9 +733,9 @@ class HotRunner extends ResidentRunner {
return OperationResult(OperationResult.ok.code, reloadMessage);
}
}
assert(reassembleViews.isNotEmpty);
printTrace('Evicting dirty assets');
await _evictDirtyAssets();
assert(reassembleViews.isNotEmpty);
printTrace('Reassembling application');
bool failedReassemble = false;
final List<Future<void>> futures = <Future<void>>[];
......@@ -886,7 +808,6 @@ class HotRunner extends ResidentRunner {
// Only report timings if we reloaded a single view without any errors.
if ((reassembleViews.length == 1) && !failedReassemble && shouldReportReloadTime)
flutterUsage.sendTiming('hot', 'reload', reloadDuration);
return OperationResult(
failedReassemble ? 1 : OperationResult.ok.code,
reloadMessage,
......@@ -965,6 +886,23 @@ class HotRunner extends ResidentRunner {
}
}
Future<void> _evictDirtyAssets() {
final List<Future<Map<String, dynamic>>> futures = <Future<Map<String, dynamic>>>[];
for (FlutterDevice device in flutterDevices) {
if (device.devFS.assetPathsToEvict.isEmpty)
continue;
if (device.views.first.uiIsolate == null) {
printError('Application isolate not found for $device');
continue;
}
for (String assetPath in device.devFS.assetPathsToEvict) {
futures.add(device.views.first.uiIsolate.flutterEvictAsset(assetPath));
}
device.devFS.assetPathsToEvict.clear();
}
return Future.wait<Map<String, dynamic>>(futures);
}
@override
Future<void> cleanupAfterSignal() async {
await stopEchoingDeviceLog();
......@@ -988,3 +926,106 @@ class HotRunner extends ResidentRunner {
await stopEchoingDeviceLog();
}
}
class ProjectFileInvalidator {
ProjectFileInvalidator(this._packagesPath, this._flutterProject) {
final File packagesFile = fs.file(_packagesPath);
if (packagesFile.existsSync()) {
_packagesUpdateTime = packagesFile.statSync().modified.millisecondsSinceEpoch;
_packageMap = PackageMap(_packagesPath).map;
} else {
_packagesUpdateTime = -1;
_packageMap = const <String, Uri>{};
}
_computePackageMap(_packageMap, _flutterProject);
}
// Used to avoid watching pubspec directories. This will not change even with pub upgrade,
// because that actually switches the directory and requires a corresponding
// update to .packages
static const String _pubCachePathLinuxAndWindows = '.pub-cache';
static const String _pubCachePathWindows = 'Pub/Cache';
Map<String, Uri> _packageMap;
final String _packagesPath;
final FlutterProject _flutterProject;
final Map<String, int> _updateTime = <String, int>{};
int _packagesUpdateTime;
Map<String, int> get updateTime => _updateTime;
@visibleForTesting
Map<String, Uri> get packageMap => _packageMap;
static void _computePackageMap(Map<String, Uri> packageMap, FlutterProject flutterProject) {
if (flutterProject != null && flutterProject.pubspecFile.existsSync()) {
try {
final YamlMap pubspec = loadYamlDocument(flutterProject.pubspecFile.readAsStringSync()).contents;
final YamlMap dependencies = pubspec['dependencies'];
final Set<String> relevantDependencies = Set<String>.from(dependencies.keys);
// Remove any packages which were tagged as dev dependenices,
// But don't remove the app itself!
for (String packageName in packageMap.keys.toList()) {
if (!relevantDependencies.contains(packageName) && packageName != flutterProject.manifest.appName) {
packageMap.remove(packageName);
continue;
}
}
} catch (err) {
// If we detect a pubspec formatting problem, fallback to the packages file.
}
}
// Remove any packages which are derived from the pub cache.
for (String packageName in packageMap.keys.toList()) {
final String path = packageMap[packageName].path;
if ((platform.isWindows && path.contains(_pubCachePathWindows))
|| path.contains(_pubCachePathLinuxAndWindows)) {
packageMap.remove(packageName);
}
}
}
List<String> findInvalidated() {
final File packagesFile = fs.file(_packagesPath);
if (packagesFile.existsSync()) {
final int newPackagesUpdateTime = packagesFile.statSync().modified.millisecondsSinceEpoch;
// Hot reloading with an updated package will often times kill a non-trivial
// appliction. This _might_ work, given certain application size and package
// constraints, so instead of exiting we print a warning so that the user has
// some hint on what went wrong.
if (newPackagesUpdateTime > _packagesUpdateTime) {
printError('Warning: updated dependencies detected. The Flutter application will require a restart to safely use new packages.');
}
_packagesUpdateTime = newPackagesUpdateTime;
}
final List<String> invalidatedFiles = <String>[];
for (String packageName in _packageMap.keys) {
final Uri packageUri =_packageMap[packageName];
_scanDirectory(packageUri, invalidatedFiles);
}
return invalidatedFiles;
}
void _scanDirectory(Uri path, List<String> invalidatedFiles) {
final Directory directory = fs.directory(path);
if (!directory.existsSync()) {
return;
}
for (FileSystemEntity entity in directory.listSync(recursive: true)) {
if (entity.path.endsWith('.dart')) {
final int oldUpdatedAt = _updateTime[entity.path];
final int updatedAt = fs.statSync(entity.path).modified.millisecondsSinceEpoch;
if (oldUpdatedAt == null || updatedAt > oldUpdatedAt) {
// On windows convert to file uri in expected format.
if (platform.isWindows) {
final Uri uri = Uri.file(entity.path, windows: platform.isWindows);
invalidatedFiles.add(uri.toString());
} else {
invalidatedFiles.add(entity.path);
}
}
_updateTime[entity.path] = updatedAt;
}
}
}
}
......@@ -9,7 +9,6 @@ environment:
dependencies:
# To update these, use "flutter update-packages --force-upgrade".
analyzer: 0.35.3
archive: 2.0.8
args: 1.5.1
bsdiff: 0.1.0
......@@ -53,6 +52,7 @@ dependencies:
build_modules: 1.0.9
build_daemon: 0.4.2
analyzer: 0.35.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
async: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
bazel_worker: 0.1.20 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
boolean_selector: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
......@@ -100,8 +100,8 @@ dev_dependencies:
collection: 1.14.11
mockito: 4.0.0
file_testing: 2.1.0
test: 1.5.3
vm_service_lib: 3.14.2
test: 1.5.3
build_runner: 1.2.8
build_vm_compilers: 0.1.1+5
build_test: 0.10.6
......
......@@ -120,6 +120,7 @@ void main() {
debuggingOptions: anyNamed('debuggingOptions'),
packagesFilePath: anyNamed('packagesFilePath'),
usesTerminalUI: anyNamed('usesTerminalUI'),
flutterProject: anyNamed('flutterProject'),
ipv6: false,
),
).thenReturn(mockHotRunner);
......@@ -151,6 +152,7 @@ void main() {
debuggingOptions: anyNamed('debuggingOptions'),
packagesFilePath: anyNamed('packagesFilePath'),
usesTerminalUI: anyNamed('usesTerminalUI'),
flutterProject: anyNamed('flutterProject'),
ipv6: false,
),
)..called(1);
......@@ -225,6 +227,7 @@ void main() {
debuggingOptions: anyNamed('debuggingOptions'),
packagesFilePath: anyNamed('packagesFilePath'),
usesTerminalUI: anyNamed('usesTerminalUI'),
flutterProject: anyNamed('flutterProject'),
ipv6: false,
)).thenReturn(mockHotRunner);
......@@ -254,6 +257,7 @@ void main() {
debuggingOptions: anyNamed('debuggingOptions'),
packagesFilePath: anyNamed('packagesFilePath'),
usesTerminalUI: anyNamed('usesTerminalUI'),
flutterProject: anyNamed('flutterProject'),
ipv6: false,
)).called(1);
}, overrides: <Type, Generator>{
......
// 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 'package:flutter_tools/src/dart/dependencies.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'src/common.dart';
import 'src/context.dart';
void main() {
group('DartDependencySetBuilder', () {
final String dataPath = fs.path.join(
getFlutterRoot(),
'packages',
'flutter_tools',
'test',
'data',
'dart_dependencies_test',
);
testUsingContext('good', () {
final String testPath = fs.path.join(dataPath, 'good');
final String mainPath = fs.path.join(testPath, 'main.dart');
final String packagesPath = fs.path.join(testPath, '.packages');
final DartDependencySetBuilder builder =
DartDependencySetBuilder(mainPath, packagesPath);
final Set<String> dependencies = builder.build();
expect(dependencies.contains(canonicalizePath(mainPath)), isTrue);
expect(dependencies.contains(canonicalizePath(fs.path.join(testPath, 'foo.dart'))), isTrue);
});
testUsingContext('syntax_error', () {
final String testPath = fs.path.join(dataPath, 'syntax_error');
final String mainPath = fs.path.join(testPath, 'main.dart');
final String packagesPath = fs.path.join(testPath, '.packages');
final DartDependencySetBuilder builder =
DartDependencySetBuilder(mainPath, packagesPath);
try {
builder.build();
fail('expect an exception to be thrown.');
} on DartDependencyException catch (error) {
expect(error.toString(), contains('foo.dart: Expected a string literal'));
}
});
testUsingContext('bad_path', () {
final String testPath = fs.path.join(dataPath, 'bad_path');
final String mainPath = fs.path.join(testPath, 'main.dart');
final String packagesPath = fs.path.join(testPath, '.packages');
final DartDependencySetBuilder builder =
DartDependencySetBuilder(mainPath, packagesPath);
try {
builder.build();
fail('expect an exception to be thrown.');
} on DartDependencyException catch (error) {
expect(error.toString(), contains('amaze${fs.path.separator}and${fs.path.separator}astonish.dart'));
}
});
testUsingContext('bad_package', () {
final String testPath = fs.path.join(dataPath, 'bad_package');
final String mainPath = fs.path.join(testPath, 'main.dart');
final String packagesPath = fs.path.join(testPath, '.packages');
final DartDependencySetBuilder builder =
DartDependencySetBuilder(mainPath, packagesPath);
try {
builder.build();
fail('expect an exception to be thrown.');
} on DartDependencyException catch (error) {
expect(error.toString(), contains('rochambeau'));
expect(error.toString(), contains('pubspec.yaml'));
}
});
testUsingContext('does not change ASCII casing of path', () {
final String testPath = fs.path.join(dataPath, 'asci_casing');
final String mainPath = fs.path.join(testPath, 'main.dart');
final String packagesPath = fs.path.join(testPath, '.packages');
final DartDependencySetBuilder builder = DartDependencySetBuilder(mainPath, packagesPath);
final Set<String> deps = builder.build();
expect(deps, contains(endsWith('This_Import_Has_fuNNy_casING.dart')));
});
testUsingContext('bad_import', () {
final String testPath = fs.path.join(dataPath, 'bad_import');
final String mainPath = fs.path.join(testPath, 'main.dart');
final String packagesPath = fs.path.join(testPath, '.packages');
final DartDependencySetBuilder builder =
DartDependencySetBuilder(mainPath, packagesPath);
try {
builder.build();
fail('expect an exception to be thrown.');
} on DartDependencyException catch (error) {
expect(error.toString(), contains('Unable to parse URI'));
}
});
});
}
// Copyright 2017 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.
String dummy = 'Hello';
// Copyright 2017 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 'This_Import_Has_fuNNy_casING.dart';
void main() {
print(dummy);
}
// Copyright 2017 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 'data://object.dart';
// 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 'package:rochambeau/you_have_your_orders_now_go_man_go.dart';
// 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 'amaze/and/astonish.dart';
flutter:file:///a/wild/non-existent/directory/has/appeared
sdk-move-test:lib/
// Copyright 2017 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.
// No content
name: sdk-move-test
environment:
# The pub client defaults to an <2.0.0 sdk constraint which we need to explicitly overwrite.
sdk: ">=2.0.0-dev.68.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
// 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.
// 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.
// 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 'foo.dart';
import 'package:self/bar.dart';
// 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 bad programmer!
// 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 'foo.dart';
// 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 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/devices.dart';
import 'package:flutter_tools/src/dart/dependencies.dart';
import 'package:flutter_tools/src/dependency_checker.dart';
import 'src/common.dart';
import 'src/context.dart';
void main() {
group('DependencyChecker', () {
final String dataPath = fs.path.join(
getFlutterRoot(),
'packages',
'flutter_tools',
'test',
'data',
'dart_dependencies_test',
);
FileSystem testFileSystem;
setUpAll(() {
Cache.disableLocking();
});
setUp(() {
testFileSystem = MemoryFileSystem();
});
testUsingContext('good', () {
final String testPath = fs.path.join(dataPath, 'good');
final String mainPath = fs.path.join(testPath, 'main.dart');
final String fooPath = fs.path.join(testPath, 'foo.dart');
final String barPath = fs.path.join(testPath, 'lib', 'bar.dart');
final String packagesPath = fs.path.join(testPath, '.packages');
final DartDependencySetBuilder builder =
DartDependencySetBuilder(mainPath, packagesPath);
final DependencyChecker dependencyChecker =
DependencyChecker(builder, null);
// Set file modification time on all dependencies to be in the past.
final DateTime baseTime = DateTime.now();
updateFileModificationTime(packagesPath, baseTime, -10);
updateFileModificationTime(mainPath, baseTime, -10);
updateFileModificationTime(fooPath, baseTime, -10);
updateFileModificationTime(barPath, baseTime, -10);
expect(dependencyChecker.check(baseTime), isFalse);
// Set .packages file modification time to be in the future.
updateFileModificationTime(packagesPath, baseTime, 20);
expect(dependencyChecker.check(baseTime), isTrue);
// Reset .packages file modification time.
updateFileModificationTime(packagesPath, baseTime, 0);
expect(dependencyChecker.check(baseTime), isFalse);
// Set 'package:self/bar.dart' file modification time to be in the future.
updateFileModificationTime(barPath, baseTime, 10);
expect(dependencyChecker.check(baseTime), isTrue);
});
testUsingContext('syntax error', () {
final String testPath = fs.path.join(dataPath, 'syntax_error');
final String mainPath = fs.path.join(testPath, 'main.dart');
final String fooPath = fs.path.join(testPath, 'foo.dart');
final String packagesPath = fs.path.join(testPath, '.packages');
final DartDependencySetBuilder builder =
DartDependencySetBuilder(mainPath, packagesPath);
final DependencyChecker dependencyChecker =
DependencyChecker(builder, null);
final DateTime baseTime = DateTime.now();
// Set file modification time on all dependencies to be in the past.
updateFileModificationTime(packagesPath, baseTime, -10);
updateFileModificationTime(mainPath, baseTime, -10);
updateFileModificationTime(fooPath, baseTime, -10);
// Dependencies are considered dirty because there is a syntax error in
// the .dart file.
expect(dependencyChecker.check(baseTime), isTrue);
});
/// Test a flutter tool move.
///
/// Tests that the flutter tool doesn't crash and displays a warning when its own location
/// changed since it was last referenced to in a package's .packages file.
testUsingContext('moved flutter sdk', () async {
final Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_dependency_checker_test.');
// Copy the golden input and let the test run in an isolated temporary in-memory file system.
const LocalFileSystem localFileSystem = LocalFileSystem();
final Directory sourcePath = localFileSystem.directory(localFileSystem.path.join(dataPath, 'changed_sdk_location'));
copyDirectorySync(sourcePath, tempDir);
fs.currentDirectory = tempDir;
// Doesn't matter what commands we run. Arbitrarily list devices here.
await createTestCommandRunner(DevicesCommand()).run(<String>['devices']);
expect(testLogger.errorText, contains('.packages'));
tryToDelete(tempDir);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
});
});
}
......@@ -7,10 +7,8 @@ import 'dart:convert';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/asset.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
......@@ -22,16 +20,13 @@ import 'src/mocks.dart';
void main() {
FileSystem fs;
String filePath;
String filePath2;
Directory tempDir;
String basePath;
DevFS devFS;
final AssetBundle assetBundle = AssetBundleFactory.defaultInstance.createBundle();
setUpAll(() {
fs = MemoryFileSystem();
filePath = fs.path.join('lib', 'foo.txt');
filePath2 = fs.path.join('foo', 'bar.txt');
});
group('DevFSContent', () {
......@@ -94,362 +89,6 @@ void main() {
});
});
group('devfs local', () {
final MockDevFSOperations devFSOperations = MockDevFSOperations();
final MockResidentCompiler residentCompiler = MockResidentCompiler();
setUpAll(() {
tempDir = _newTempDir(fs);
basePath = tempDir.path;
});
tearDownAll(_cleanupTempDirs);
testUsingContext('create dev file system', () async {
// simulate workspace
final File file = fs.file(fs.path.join(basePath, filePath));
await file.parent.create(recursive: true);
file.writeAsBytesSync(<int>[1, 2, 3]);
_packages['my_project'] = fs.path.toUri('lib');
// simulate package
await _createPackage(fs, 'somepkg', 'somefile.txt');
devFS = DevFS.operations(devFSOperations, 'test', tempDir);
await devFS.create();
devFSOperations.expectMessages(<String>['create test']);
expect(devFS.assetPathsToEvict, isEmpty);
UpdateFSReport report = await devFS.update(
mainPath: 'lib/foo.txt',
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
);
devFSOperations.expectMessages(<String>[
'writeFile test lib/foo.txt.dill build/app.dill',
]);
expect(devFS.assetPathsToEvict, isEmpty);
report = await devFS.update(
mainPath: 'lib/foo.txt',
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: true,
);
devFSOperations.expectMessages(<String>[
'writeFile test lib/foo.txt.dill build/app.dill.track.dill',
]);
expect(devFS.assetPathsToEvict, isEmpty);
expect(report.syncedBytes, 22);
expect(report.success, true);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
});
testUsingContext('add new file to local file system', () async {
final File file = fs.file(fs.path.join(basePath, filePath2));
await file.parent.create(recursive: true);
file.writeAsBytesSync(<int>[1, 2, 3, 4, 5, 6, 7]);
final UpdateFSReport report = await devFS.update(
mainPath: 'lib/foo.txt',
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
);
devFSOperations.expectMessages(<String>[
'writeFile test lib/foo.txt.dill build/app.dill',
]);
expect(devFS.assetPathsToEvict, isEmpty);
expect(report.syncedBytes, 22);
expect(report.success, true);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
});
testUsingContext('modify existing file on local file system', () async {
UpdateFSReport report = await devFS.update(
mainPath: 'lib/foo.txt',
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
);
devFSOperations.expectMessages(<String>[
'writeFile test lib/foo.txt.dill build/app.dill',
]);
expect(devFS.assetPathsToEvict, isEmpty);
expect(report.syncedBytes, 22);
expect(report.success, true);
final File file = fs.file(fs.path.join(basePath, filePath));
// Set the last modified time to 5 seconds in the past.
updateFileModificationTime(file.path, DateTime.now(), -5);
report = await devFS.update(
mainPath: 'lib/foo.txt',
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
);
devFSOperations.expectMessages(<String>[
'writeFile test lib/foo.txt.dill build/app.dill',
]);
expect(devFS.assetPathsToEvict, isEmpty);
expect(report.syncedBytes, 22);
expect(report.success, true);
await file.writeAsBytes(<int>[1, 2, 3, 4, 5, 6]);
report = await devFS.update(
mainPath: 'lib/foo.txt',
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
);
devFSOperations.expectMessages(<String>[
'writeFile test lib/foo.txt.dill build/app.dill',
]);
expect(devFS.assetPathsToEvict, isEmpty);
expect(report.syncedBytes, 22);
expect(report.success, true);
// Set the last modified time to 5 seconds in the past.
updateFileModificationTime(file.path, DateTime.now(), -5);
report = await devFS.update(
mainPath: 'lib/foo.txt',
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: true,
);
devFSOperations.expectMessages(<String>[
'writeFile test lib/foo.txt.dill build/app.dill.track.dill',
]);
expect(devFS.assetPathsToEvict, isEmpty);
expect(report.syncedBytes, 22);
expect(report.success, true);
await file.writeAsBytes(<int>[1, 2, 3, 4, 5, 6]);
report = await devFS.update(
mainPath: 'lib/foo.txt',
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: true,
);
devFSOperations.expectMessages(<String>[
'writeFile test lib/foo.txt.dill build/app.dill.track.dill',
]);
expect(devFS.assetPathsToEvict, isEmpty);
expect(report.syncedBytes, 22);
expect(report.success, true);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
});
testUsingContext('delete a file from the local file system', () async {
final File file = fs.file(fs.path.join(basePath, filePath));
await file.delete();
final UpdateFSReport report = await devFS.update(
mainPath: 'lib/foo.txt',
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
);
devFSOperations.expectMessages(<String>[
'deleteFile test lib/foo.txt',
'writeFile test lib/foo.txt.dill build/app.dill',
]);
expect(devFS.assetPathsToEvict, isEmpty);
expect(report.syncedBytes, 22);
expect(report.success, true);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
});
testUsingContext('add new package', () async {
await _createPackage(fs, 'newpkg', 'anotherfile.txt');
UpdateFSReport report = await devFS.update(
mainPath: 'lib/foo.txt',
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
);
devFSOperations.expectMessages(<String>[
'writeFile test lib/foo.txt.dill build/app.dill',
]);
expect(devFS.assetPathsToEvict, isEmpty);
expect(report.syncedBytes, 22);
expect(report.success, true);
report = await devFS.update(
mainPath: 'lib/foo.txt',
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: true,
);
devFSOperations.expectMessages(<String>[
'writeFile test lib/foo.txt.dill build/app.dill.track.dill',
]);
expect(devFS.assetPathsToEvict, isEmpty);
expect(report.syncedBytes, 22);
expect(report.success, true);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
});
testUsingContext('add new package with double slashes in URI', () async {
const String packageName = 'doubleslashpkg';
await _createPackage(fs, packageName, 'somefile.txt', doubleSlash: true);
final Set<String> fileFilter = <String>{};
final List<Uri> pkgUris = <Uri>[fs.path.toUri(basePath)]..addAll(_packages.values);
for (Uri pkgUri in pkgUris) {
if (!pkgUri.isAbsolute) {
pkgUri = fs.path.toUri(fs.path.join(basePath, pkgUri.path));
}
fileFilter.addAll(fs.directory(pkgUri)
.listSync(recursive: true)
.whereType<File>()
.map<String>((File file) => canonicalizePath(file.path))
.toList());
}
final UpdateFSReport report = await devFS.update(
mainPath: 'lib/foo.txt',
fileFilter: fileFilter,
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
);
devFSOperations.expectMessages(<String>[
'writeFile test lib/foo.txt.dill build/app.dill',
]);
expect(devFS.assetPathsToEvict, isEmpty);
expect(report.syncedBytes, 22);
expect(report.success, true);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
});
testUsingContext('add an asset bundle', () async {
assetBundle.entries['a.txt'] = DevFSStringContent('abc');
final UpdateFSReport report = await devFS.update(
mainPath: 'lib/foo.txt',
bundle: assetBundle,
bundleDirty: true,
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
);
devFSOperations.expectMessages(<String>[
'writeFile test ${_inAssetBuildDirectory(fs, 'a.txt')}',
'writeFile test lib/foo.txt.dill build/app.dill',
]);
expect(devFS.assetPathsToEvict, unorderedMatches(<String>['a.txt']));
devFS.assetPathsToEvict.clear();
expect(report.syncedBytes, 25);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
});
testUsingContext('add a file to the asset bundle - bundleDirty', () async {
assetBundle.entries['b.txt'] = DevFSStringContent('abcd');
final UpdateFSReport report = await devFS.update(
mainPath: 'lib/foo.txt',
bundle: assetBundle,
bundleDirty: true,
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
);
// Expect entire asset bundle written because bundleDirty is true
devFSOperations.expectMessages(<String>[
'writeFile test ${_inAssetBuildDirectory(fs, 'a.txt')}',
'writeFile test ${_inAssetBuildDirectory(fs, 'b.txt')}',
'writeFile test lib/foo.txt.dill build/app.dill',
]);
expect(devFS.assetPathsToEvict, unorderedMatches(<String>[
'a.txt', 'b.txt']));
devFS.assetPathsToEvict.clear();
expect(report.syncedBytes, 29);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
});
testUsingContext('add a file to the asset bundle', () async {
assetBundle.entries['c.txt'] = DevFSStringContent('12');
final UpdateFSReport report = await devFS.update(
mainPath: 'lib/foo.txt',
bundle: assetBundle,
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
);
devFSOperations.expectMessages(<String>[
'writeFile test ${_inAssetBuildDirectory(fs, 'c.txt')}',
'writeFile test lib/foo.txt.dill build/app.dill',
]);
expect(devFS.assetPathsToEvict, unorderedMatches(<String>[
'c.txt']));
devFS.assetPathsToEvict.clear();
expect(report.syncedBytes, 24);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
});
testUsingContext('delete a file from the asset bundle', () async {
assetBundle.entries.remove('c.txt');
final UpdateFSReport report = await devFS.update(
mainPath: 'lib/foo.txt',
bundle: assetBundle,
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
);
devFSOperations.expectMessages(<String>[
'deleteFile test ${_inAssetBuildDirectory(fs, 'c.txt')}',
'writeFile test lib/foo.txt.dill build/app.dill',
]);
expect(devFS.assetPathsToEvict, unorderedMatches(<String>['c.txt']));
devFS.assetPathsToEvict.clear();
expect(report.syncedBytes, 22);
expect(report.success, true);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
});
testUsingContext('delete all files from the asset bundle', () async {
assetBundle.entries.clear();
final UpdateFSReport report = await devFS.update(
mainPath: 'lib/foo.txt',
bundle: assetBundle,
bundleDirty: true,
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
);
devFSOperations.expectMessages(<String>[
'deleteFile test ${_inAssetBuildDirectory(fs, 'a.txt')}',
'deleteFile test ${_inAssetBuildDirectory(fs, 'b.txt')}',
'writeFile test lib/foo.txt.dill build/app.dill',
]);
expect(devFS.assetPathsToEvict, unorderedMatches(<String>[
'a.txt', 'b.txt',
]));
devFS.assetPathsToEvict.clear();
expect(report.syncedBytes, 22);
expect(report.success, true);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
});
testUsingContext('delete dev file system', () async {
await devFS.destroy();
devFSOperations.expectMessages(<String>['destroy test']);
expect(devFS.assetPathsToEvict, isEmpty);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
});
});
group('devfs remote', () {
MockVMService vmService;
final MockResidentCompiler residentCompiler = MockResidentCompiler();
......@@ -484,6 +123,7 @@ void main() {
generator: residentCompiler,
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
invalidatedFiles: <String>[],
);
vmService.expectMessages(<String>[
'writeFile test lib/foo.txt.dill',
......@@ -652,6 +292,3 @@ Future<void> _createPackage(FileSystem fs, String pkgName, String pkgFileName, {
fs.file(fs.path.join(_tempDirs[0].path, '.packages')).writeAsStringSync(sb.toString());
}
String _inAssetBuildDirectory(FileSystem fs, String filename) {
return '${fs.path.toUri(getAssetBuildDirectory()).path}/$filename';
}
......@@ -105,13 +105,13 @@ void main() {
firstBuildTime: anyNamed('firstBuildTime'),
bundleFirstUpload: anyNamed('bundleFirstUpload'),
bundleDirty: anyNamed('bundleDirty'),
fileFilter: anyNamed('fileFilter'),
generator: anyNamed('generator'),
fullRestart: anyNamed('fullRestart'),
dillOutputPath: anyNamed('dillOutputPath'),
trackWidgetCreation: anyNamed('trackWidgetCreation'),
projectRootPath: anyNamed('projectRootPath'),
pathToReload: anyNamed('pathToReload'),
invalidatedFiles: anyNamed('invalidatedFiles'),
)).thenAnswer((Invocation _) => Future<UpdateFSReport>.value(
UpdateFSReport(success: true, syncedBytes: 1000, invalidatedSourcesCount: 1)));
when(mockDevFs.assetPathsToEvict).thenReturn(<String>{});
......@@ -122,18 +122,6 @@ void main() {
when(mockArtifacts.getArtifactPath(Artifact.flutterPatchedSdkPath)).thenReturn('some/path');
});
testUsingContext('no setup', () async {
final MockDevice mockDevice = MockDevice();
when(mockDevice.supportsHotReload).thenReturn(true);
when(mockDevice.supportsHotRestart).thenReturn(true);
final List<FlutterDevice> devices = <FlutterDevice>[
FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false),
];
expect((await HotRunner(devices).restart(fullRestart: true)).isOk, false);
}, overrides: <Type, Generator>{
Artifacts: () => mockArtifacts,
});
testUsingContext('Does not hot restart when device does not support it', () async {
// Setup mocks
final MockDevice mockDevice = MockDevice();
......@@ -149,7 +137,7 @@ void main() {
expect(result.message, 'hotRestart not supported');
}, overrides: <Type, Generator>{
Artifacts: () => mockArtifacts,
HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true, computeDartDependencies: false),
HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
});
testUsingContext('Does not hot restart when one of many devices does not support it', () async {
......@@ -171,7 +159,7 @@ void main() {
expect(result.message, 'hotRestart not supported');
}, overrides: <Type, Generator>{
Artifacts: () => mockArtifacts,
HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true, computeDartDependencies: false),
HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
});
testUsingContext('Does hot restarts when all devices support it', () async {
......@@ -193,7 +181,7 @@ void main() {
expect(result.message, isNot('hotRestart not supported'));
}, overrides: <Type, Generator>{
Artifacts: () => mockArtifacts,
HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true, computeDartDependencies: false),
HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
});
testUsingContext('setup function fails', () async {
......@@ -226,7 +214,7 @@ void main() {
expect(result.message, isNot('setupHotRestart failed'));
}, overrides: <Type, Generator>{
Artifacts: () => mockArtifacts,
HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true, computeDartDependencies: false),
HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
});
group('shutdown hook tests', () {
......@@ -235,7 +223,6 @@ void main() {
setUp(() {
shutdownTestingConfig = TestHotRunnerConfig(
successfulSetup: true,
computeDartDependencies: false,
);
});
......@@ -283,10 +270,7 @@ class MockDevice extends Mock implements Device {
}
class TestHotRunnerConfig extends HotRunnerConfig {
TestHotRunnerConfig({@required this.successfulSetup, bool computeDartDependencies = true}) {
this.computeDartDependencies = computeDartDependencies;
}
TestHotRunnerConfig({@required this.successfulSetup});
bool successfulSetup;
bool shutdownHookCalled = false;
......
// Copyright 2019 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 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/run_hot.dart';
import 'package:mockito/mockito.dart';
import 'src/common.dart';
import 'src/context.dart';
void main() {
final Platform windowsPlatform = MockPlatform();
final Platform notWindowsPlatform = MockPlatform();
final BufferLogger bufferLogger = BufferLogger();
when(windowsPlatform.isWindows).thenReturn(true);
when(notWindowsPlatform.isWindows).thenReturn(false);
group('ProjectFileInvalidator linux/mac', () {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
final File packagesFile = memoryFileSystem.file('.packages')
..createSync()
..writeAsStringSync(r'''
foo:file:///foo/lib/
bar:file:///.pub-cache/bar/lib/
baz:file:///baz/lib/
test_package:file:///lib/
''');
final File pubspecFile = memoryFileSystem.file('pubspec.yaml')
..createSync()
..writeAsStringSync(r'''
name: test_package
dependencies:
foo: any
bar:
dev_dependencies:
baz: any
''');
final File mainFile = memoryFileSystem.file('lib/main.dart')
..createSync(recursive: true)
..writeAsStringSync(r'''
void main() {}
''');
final File fooFile = memoryFileSystem.file('foo/lib/foo.dart')
..createSync(recursive: true)
..writeAsStringSync('');
memoryFileSystem.file('bar/lib/bar.dart')
..createSync(recursive: true)
..writeAsStringSync('');
final File bazFile = memoryFileSystem.file('baz/lib/baz.dart')
..createSync(recursive: true)
..writeAsStringSync('');
testUsingContext('No .packages, no pubspec', () async {
// Instead of setting up multiple filesystems, passing a .packages file which does not exist.
final ProjectFileInvalidator invalidator = ProjectFileInvalidator('.packages-wrong', null);
invalidator.findInvalidated();
expect(invalidator.packageMap, isEmpty);
expect(invalidator.updateTime, isEmpty);
}, overrides: <Type, Generator>{
FileSystem: () => memoryFileSystem,
Platform: () => notWindowsPlatform,
});
testUsingContext('.packages only', () async {
final ProjectFileInvalidator invalidator = ProjectFileInvalidator(packagesFile.path, null);
invalidator.findInvalidated();
expect(invalidator.packageMap, <String, Uri>{
'foo': Uri.parse('file:///foo/lib/'),
// Excluded because it is in pub cache.
// 'bar': Uri.parse('file:///.pub-cache/bar/lib/'),
'baz': Uri.parse('file:///baz/lib/'),
'test_package': Uri.parse('file:///lib/'),
});
expect(invalidator.updateTime, <String, int>{
'/baz/lib/baz.dart': bazFile.statSync().modified.millisecondsSinceEpoch,
'/lib/main.dart': mainFile.statSync().modified.millisecondsSinceEpoch,
'/foo/lib/foo.dart': fooFile.statSync().modified.millisecondsSinceEpoch,
});
}, overrides: <Type, Generator>{
FileSystem: () => memoryFileSystem,
Platform: () => notWindowsPlatform,
});
testUsingContext('.packages and pubspec', () async {
final FlutterProject flutterProject = await FlutterProject.fromDirectory(pubspecFile.parent);
final ProjectFileInvalidator invalidator = ProjectFileInvalidator(packagesFile.path, flutterProject);
invalidator.findInvalidated();
expect(invalidator.packageMap, <String, Uri>{
'foo': Uri.parse('file:///foo/lib/'),
// Excluded because it is in pub cache.
// 'bar': Uri.parse('file:///.pub-cache/bar/lib/'),
// Excluded because it is a dev dependency/
// 'baz': Uri.parse('file:///baz/lib/'),
'test_package': Uri.parse('file:///lib/'),
});
expect(invalidator.updateTime, <String, int>{
'/foo/lib/foo.dart': fooFile.statSync().modified.millisecondsSinceEpoch,
'/lib/main.dart':mainFile.statSync().modified.millisecondsSinceEpoch,
});
expect(invalidator.findInvalidated(), isEmpty);
// Invalidate main.dart.
mainFile.writeAsStringSync('void main() { }');
expect(invalidator.findInvalidated(), <String>['/lib/main.dart']);
}, overrides: <Type, Generator>{
FileSystem: () => memoryFileSystem,
Platform: () => notWindowsPlatform,
});
testUsingContext('update to .packages triggers warning', () async {
final FlutterProject flutterProject = await FlutterProject.fromDirectory(pubspecFile.parent);
final ProjectFileInvalidator invalidator = ProjectFileInvalidator(packagesFile.path, flutterProject);
invalidator.findInvalidated();
packagesFile.writeAsStringSync(r'''
foo:file:///foo/lib/
bar:file:///.pub-cache/bar/lib/
baz:file:///baz/lib/
new_dep:file:///new_dep/lib/
test_package:file:///lib/
''');
invalidator.findInvalidated();
expect(bufferLogger.errorText, 'Warning: updated dependencies detected. The Flutter application will require a restart to safely use new packages.\n');
}, overrides: <Type, Generator>{
FileSystem: () => memoryFileSystem,
Platform: () => notWindowsPlatform,
Logger: () => bufferLogger,
});
});
group('ProjectFileInvalidator windows', () {
final MemoryFileSystem memoryFileSystem = MemoryFileSystem(style: FileSystemStyle.windows);
// On windows .packages still contains file Uris, albeit ones with the Drive prefix.
final File packagesFile = memoryFileSystem.file(r'C:\.packages')
..createSync()
..writeAsStringSync(r'''
foo:file:///C:/foo/lib/
bar:file:///C:/Pub/Cache/bar/lib/
baz:file:///C:/baz/lib/
test_package:file:///C:/lib/
''');
memoryFileSystem.file(r'C:\pubspec.yaml')
..createSync()
..writeAsStringSync(r'''
name: test_package
dependencies:
foo: any
bar:
dev_dependencies:
baz: any
''');
final File mainFile = memoryFileSystem.file(r'C:\lib\main.dart')
..createSync(recursive: true)
..writeAsStringSync(r'''
void main() {}
''');
final File fooFile = memoryFileSystem.file(r'C:\foo\lib\foo.dart')
..createSync(recursive: true)
..writeAsStringSync('');
memoryFileSystem.file(r'C:\bar\lib\bar.dart')
..createSync(recursive: true)
..writeAsStringSync('');
final File bazFile = memoryFileSystem.file(r'C:\baz\lib\baz.dart')
..createSync(recursive: true)
..writeAsStringSync('');
testUsingContext('No .packages, no pubspec', () async {
// Instead of setting up multiple filesystems, passing a .packages file which does not exist.
final ProjectFileInvalidator invalidator = ProjectFileInvalidator('.packages-wrong', null);
invalidator.findInvalidated();
expect(invalidator.packageMap, isEmpty);
expect(invalidator.updateTime, isEmpty);
}, overrides: <Type, Generator>{
FileSystem: () => memoryFileSystem,
Platform: () => windowsPlatform,
});
testUsingContext('.packages only', () async {
final ProjectFileInvalidator invalidator = ProjectFileInvalidator(packagesFile.path, null);
invalidator.findInvalidated();
expect(invalidator.packageMap, <String, Uri>{
'foo': Uri.file(r'C:\foo\lib\', windows: true),
// Excluded because it is in pub cache.
// 'bar': Uri.parse('file:///Pub/Cache/bar/lib/'),
'baz': Uri.file(r'C:\baz\lib\', windows: true),
'test_package': Uri.file(r'C:\lib\', windows: true),
});
expect(invalidator.updateTime, <String, int>{
r'C:\baz\lib\baz.dart': bazFile.statSync().modified.millisecondsSinceEpoch,
r'C:\lib\main.dart': mainFile.statSync().modified.millisecondsSinceEpoch,
r'C:\foo\lib\foo.dart': fooFile.statSync().modified.millisecondsSinceEpoch,
});
}, overrides: <Type, Generator>{
FileSystem: () => memoryFileSystem,
Platform: () => windowsPlatform,
});
testUsingContext('.packages and pubspec', () async {
final FlutterProject flutterProject = await FlutterProject.fromDirectory(fs.directory(r'C:\'));
final ProjectFileInvalidator invalidator = ProjectFileInvalidator(packagesFile.path, flutterProject);
invalidator.findInvalidated();
expect(invalidator.packageMap, <String, Uri>{
'foo': Uri.file(r'C:\foo\lib\', windows: true),
// Excluded because it is in pub cache.
// 'bar': Uri.parse('file:///C:/Pub/Cache/bar/lib/'),
// Excluded because it is a dev dependency/
// 'baz': Uri.parse('file:///baz/lib/'),
'test_package': Uri.file(r'C:\lib\', windows: true),
});
expect(invalidator.updateTime, <String, int>{
r'C:\lib\main.dart': mainFile.statSync().modified.millisecondsSinceEpoch,
r'C:\foo\lib\foo.dart': fooFile.statSync().modified.millisecondsSinceEpoch,
});
expect(invalidator.findInvalidated(), isEmpty);
// Invalidate main.dart.
mainFile.writeAsStringSync('void main() { }');
expect(invalidator.findInvalidated(), <String>['file:///C:/lib/main.dart']);
}, overrides: <Type, Generator>{
FileSystem: () => memoryFileSystem,
Platform: () => windowsPlatform,
});
});
}
class MockPlatform extends Mock implements Platform {}
......@@ -460,12 +460,6 @@ class MockDevFSOperations extends BasicMock implements DevFSOperations {
messages.add(message);
devicePathToContent[deviceUri] = content;
}
@override
Future<dynamic> deleteFile(String fsName, Uri deviceUri) async {
messages.add('deleteFile $fsName $deviceUri');
devicePathToContent.remove(deviceUri);
}
}
class MockResidentCompiler extends BasicMock implements ResidentCompiler {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment