diff --git a/packages/flutter_tools/lib/src/build_runner/devfs_web.dart b/packages/flutter_tools/lib/src/build_runner/devfs_web.dart index 90e982631d..2bb3f7ebb0 100644 --- a/packages/flutter_tools/lib/src/build_runner/devfs_web.dart +++ b/packages/flutter_tools/lib/src/build_runner/devfs_web.dart @@ -7,7 +7,7 @@ import 'dart:typed_data'; import 'package:dwds/data/build_result.dart'; import 'package:dwds/dwds.dart'; -import 'package:logging/logging.dart'; +import 'package:logging/logging.dart' as logging; import 'package:meta/meta.dart'; import 'package:mime/mime.dart' as mime; import 'package:package_config/package_config.dart'; @@ -19,6 +19,7 @@ import '../asset.dart'; import '../base/common.dart'; import '../base/file_system.dart'; import '../base/io.dart'; +import '../base/logger.dart'; import '../base/net.dart'; import '../base/platform.dart'; import '../base/utils.dart'; @@ -45,7 +46,7 @@ typedef DwdsLauncher = Future Function({ bool useSseForDebugProxy, bool useSseForDebugBackend, bool serveDevTools, - void Function(Level, String) logWriter, + void Function(logging.Level, String) logWriter, bool verbose, UrlEncoder urlEncoder, bool useFileProvider, @@ -227,6 +228,21 @@ class WebAssetServer implements AssetReader { } return null; } + // Ensure dwds is present and provide middleware to avoid trying to + // load the through the isolate APIs. + final Directory directory = await _loadDwdsDirectory(globals.fs, globals.logger); + final shelf.Middleware middleware = (FutureOr Function(shelf.Request) innerHandler) { + return (shelf.Request request) async { + if (request.url.path.endsWith('dwds/src/injected/client.js')) { + final Uri uri = directory.uri.resolve('src/injected/client.js'); + final String result = await globals.fs.file(uri.toFilePath()).readAsString(); + return shelf.Response.ok(result, headers: { + HttpHeaders.contentTypeHeader: 'application/javascript' + }); + } + return innerHandler(request); + }; + }; // In debug builds, spin up DWDS and the full asset server. final Dwds dwds = await dwdsLauncher( @@ -243,7 +259,7 @@ class WebAssetServer implements AssetReader { useSseForDebugProxy: useSseForDebugProxy, useSseForDebugBackend: useSseForDebugBackend, serveDevTools: false, - logWriter: (Level logLevel, String message) => globals.printTrace(message), + logWriter: (logging.Level logLevel, String message) => globals.printTrace(message), loadStrategy: RequireStrategy( ReloadConfiguration.none, '.lib.js', @@ -258,6 +274,7 @@ class WebAssetServer implements AssetReader { ); shelf.Pipeline pipeline = const shelf.Pipeline(); if (enableDwds) { + pipeline = pipeline.addMiddleware(middleware); pipeline = pipeline.addMiddleware(dwds.middleware); } final shelf.Handler dwdsHandler = pipeline.addHandler(server.handleRequest); @@ -946,3 +963,14 @@ class ReleaseAssetServer { return shelf.Response.notFound(''); } } + +Future _loadDwdsDirectory(FileSystem fileSystem, Logger logger) async { + final String toolPackagePath = fileSystem.path.join( + Cache.flutterRoot, 'packages', 'flutter_tools'); + final String packageFilePath = fileSystem.path.join(toolPackagePath, kPackagesFileName); + final PackageConfig packageConfig = await loadPackageConfigWithLogging( + fileSystem.file(packageFilePath), + logger: logger, + ); + return fileSystem.directory(packageConfig['dwds'].packageUriRoot); +} diff --git a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart index 88f3690bf7..89a8b57d37 100644 --- a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart @@ -22,9 +22,7 @@ import '../base/terminal.dart'; import '../base/utils.dart'; import '../build_info.dart'; import '../build_system/targets/web.dart'; -import '../cache.dart'; import '../dart/language_version.dart'; -import '../dart/pub.dart'; import '../devfs.dart'; import '../device.dart'; import '../features.dart'; @@ -444,15 +442,6 @@ class _ResidentWebRunner extends ResidentWebRunner { try { return await asyncGuard(() async { - // Ensure dwds resources are cached. If the .packages file is missing then - // the client.js script cannot be located by the injected handler in dwds. - // This will result in a NoSuchMethodError thrown by injected_handler.darts - await pub.get( - context: PubContext.pubGet, - directory: globals.fs.path.join(Cache.flutterRoot, 'packages', 'flutter_tools'), - generateSyntheticPackage: false, - ); - final ExpressionCompiler expressionCompiler = debuggingOptions.webEnableExpressionEvaluation ? WebExpressionCompiler(device.generator) diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart index 898c601fe2..b752f047e4 100644 --- a/packages/flutter_tools/lib/src/cache.dart +++ b/packages/flutter_tools/lib/src/cache.dart @@ -4,6 +4,7 @@ import 'package:archive/archive.dart'; import 'package:meta/meta.dart'; +import 'package:package_config/package_config.dart'; import 'android/gradle_utils.dart'; import 'base/common.dart'; @@ -14,8 +15,11 @@ import 'base/net.dart'; import 'base/os.dart' show OperatingSystemUtils; import 'base/platform.dart'; import 'base/process.dart'; +import 'dart/package_map.dart'; +import 'dart/pub.dart'; import 'features.dart'; import 'globals.dart' as globals; +import 'runner/flutter_command.dart'; /// A tag for a set of development artifacts that need to be cached. class DevelopmentArtifact { @@ -126,6 +130,14 @@ class Cache { _artifacts.add(IosUsbArtifacts(artifactName, this)); } _artifacts.add(FontSubsetArtifacts(this)); + _artifacts.add(PubDependencies( + fileSystem: _fileSystem, + logger: _logger, + // flutter root and pub must be lazily initialized to avoid accessing + // before the version is determined. + flutterRoot: () => flutterRoot, + pub: () => pub, + )); } else { _artifacts.addAll(artifacts); } @@ -432,7 +444,14 @@ class Cache { ); } - bool isUpToDate() => _artifacts.every((ArtifactSet artifact) => artifact.isUpToDate()); + Future isUpToDate() async { + for (final ArtifactSet artifact in _artifacts) { + if (!await artifact.isUpToDate()) { + return false; + } + } + return true; + } /// Update the cache to contain all `requiredArtifacts`. Future updateAll(Set requiredArtifacts) async { @@ -444,7 +463,7 @@ class Cache { _logger.printTrace('Artifact $artifact is not required, skipping update.'); continue; } - if (artifact.isUpToDate()) { + if (await artifact.isUpToDate()) { continue; } try { @@ -502,7 +521,7 @@ abstract class ArtifactSet { final DevelopmentArtifact developmentArtifact; /// [true] if the artifact is up to date. - bool isUpToDate(); + Future isUpToDate(); /// The environment variables (if any) required to consume the artifacts. Map get environment { @@ -546,7 +565,7 @@ abstract class CachedArtifact extends ArtifactSet { } @override - bool isUpToDate() { + Future isUpToDate() async { if (!location.existsSync()) { return false; } @@ -592,6 +611,66 @@ abstract class CachedArtifact extends ArtifactSet { Uri _toStorageUri(String path) => Uri.parse('${cache.storageBaseUrl}/$path'); } +/// Ensures that the source files for all of the dependencies for the +/// flutter_tool are present. +/// +/// This does not handle cases wheere the source files are modified or the +/// directory contents are incomplete. +class PubDependencies extends ArtifactSet { + PubDependencies({ + // Needs to be lazy to avoid reading from the cache before the root is initialized. + @required String Function() flutterRoot, + @required FileSystem fileSystem, + @required Logger logger, + @required Pub Function() pub, + }) : _logger = logger, + _fileSystem = fileSystem, + _flutterRoot = flutterRoot, + _pub = pub, + super(DevelopmentArtifact.universal); + + final String Function() _flutterRoot; + final FileSystem _fileSystem; + final Logger _logger; + final Pub Function() _pub; + + @override + Future isUpToDate() async { + final File toolPackageConfig = _fileSystem.file( + _fileSystem.path.join(_flutterRoot(), 'packages', 'flutter_tools', kPackagesFileName), + ); + if (!toolPackageConfig.existsSync()) { + return false; + } + final PackageConfig packageConfig = await loadPackageConfigWithLogging( + toolPackageConfig, + logger: _logger, + throwOnError: false, + ); + if (packageConfig == null || packageConfig == PackageConfig.empty ) { + return false; + } + for (final Package package in packageConfig.packages) { + if (!_fileSystem.directory(package.packageUriRoot).existsSync()) { + return false; + } + } + return true; + } + + @override + String get name => 'pub_dependencies'; + + @override + Future update(ArtifactUpdater artifactUpdater) async { + await _pub().get( + context: PubContext.pubGet, + directory: _fileSystem.path.join(_flutterRoot(), 'packages', 'flutter_tools'), + generateSyntheticPackage: false, + ); + } +} + /// A cached artifact containing fonts used for Material Design. class MaterialFonts extends CachedArtifact { MaterialFonts(Cache cache) : super( @@ -979,7 +1058,7 @@ class AndroidMavenArtifacts extends ArtifactSet { } @override - bool isUpToDate() { + Future isUpToDate() async { // The dependencies are downloaded and cached by Gradle. // The tool doesn't know if the dependencies are already cached at this point. // Therefore, call Gradle to figure this out. diff --git a/packages/flutter_tools/lib/src/commands/precache.dart b/packages/flutter_tools/lib/src/commands/precache.dart index d21c814939..78a5c911e4 100644 --- a/packages/flutter_tools/lib/src/commands/precache.dart +++ b/packages/flutter_tools/lib/src/commands/precache.dart @@ -156,7 +156,7 @@ class PrecacheCommand extends FlutterCommand { requiredArtifacts.add(artifact); } } - if (!_cache.isUpToDate()) { + if (!await _cache.isUpToDate()) { await _cache.updateAll(requiredArtifacts); } else { _logger.printStatus('Already up-to-date.'); diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index a8eb4ff903..4bd5da0d64 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -232,7 +232,8 @@ Future runInContext( botDetector: globals.botDetector, platform: globals.platform, usage: globals.flutterUsage, - toolStampFile: globals.cache.getStampFileFor('flutter_tools'), + // Avoid a circular dependency by making this access lazy. + toolStampFile: () => globals.cache.getStampFileFor('flutter_tools'), ), ShutdownHooks: () => ShutdownHooks(logger: globals.logger), Stdio: () => Stdio(), diff --git a/packages/flutter_tools/lib/src/dart/pub.dart b/packages/flutter_tools/lib/src/dart/pub.dart index ce7d328b80..9bff641361 100644 --- a/packages/flutter_tools/lib/src/dart/pub.dart +++ b/packages/flutter_tools/lib/src/dart/pub.dart @@ -80,7 +80,7 @@ abstract class Pub { @required Platform platform, @required BotDetector botDetector, @required Usage usage, - File toolStampFile, + File Function() toolStampFile, }) = _DefaultPub; /// Runs `pub get`. @@ -141,7 +141,7 @@ class _DefaultPub implements Pub { @required Platform platform, @required BotDetector botDetector, @required Usage usage, - File toolStampFile, + File Function() toolStampFile, }) : _toolStampFile = toolStampFile, _fileSystem = fileSystem, _logger = logger, @@ -159,7 +159,7 @@ class _DefaultPub implements Pub { final Platform _platform; final BotDetector _botDetector; final Usage _usage; - final File _toolStampFile; + final File Function() _toolStampFile; @override Future get({ @@ -399,10 +399,10 @@ class _DefaultPub implements Pub { if (pubSpecYaml.lastModifiedSync().isAfter(dotPackagesLastModified)) { return true; } - - if (_toolStampFile != null && - _toolStampFile.existsSync() && - _toolStampFile.lastModifiedSync().isAfter(dotPackagesLastModified)) { + final File toolStampFile = _toolStampFile != null ? _toolStampFile() : null; + if (toolStampFile != null && + toolStampFile.existsSync() && + toolStampFile.lastModifiedSync().isAfter(dotPackagesLastModified)) { return true; } return false; diff --git a/packages/flutter_tools/lib/src/template.dart b/packages/flutter_tools/lib/src/template.dart index 094321b417..dcbc6ff728 100644 --- a/packages/flutter_tools/lib/src/template.dart +++ b/packages/flutter_tools/lib/src/template.dart @@ -291,36 +291,13 @@ Future _templateImageDirectory(String name, FileSystem fileSystem, Lo final String toolPackagePath = fileSystem.path.join( Cache.flutterRoot, 'packages', 'flutter_tools'); final String packageFilePath = fileSystem.path.join(toolPackagePath, kPackagesFileName); - // Ensure that .packgaes is present. - if (!fileSystem.file(packageFilePath).existsSync()) { - await _ensurePackageDependencies(toolPackagePath, pub); - } - PackageConfig packageConfig = await loadPackageConfigWithLogging( + final PackageConfig packageConfig = await loadPackageConfigWithLogging( fileSystem.file(packageFilePath), logger: logger, ); - Uri imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot; - // Ensure that the template image package is present. - if (imagePackageLibDir == null || !fileSystem.directory(imagePackageLibDir).existsSync()) { - await _ensurePackageDependencies(toolPackagePath, pub); - packageConfig = await loadPackageConfigWithLogging( - fileSystem.file(packageFilePath), - logger: logger, - ); - imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot; - } + final Uri imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot; return fileSystem.directory(imagePackageLibDir) .parent .childDirectory('templates') .childDirectory(name); } - -// Runs 'pub get' for the given path to ensure that .packages is created and -// all dependencies are present. -Future _ensurePackageDependencies(String packagePath, Pub pub) async { - await pub.get( - context: PubContext.pubGet, - directory: packagePath, - generateSyntheticPackage: false, - ); -} diff --git a/packages/flutter_tools/test/commands.shard/hermetic/analyze_continuously_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/analyze_continuously_test.dart index 06dd6aeb71..badc6932cb 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/analyze_continuously_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/analyze_continuously_test.dart @@ -117,7 +117,7 @@ void main() { platform: const LocalPlatform(), usage: globals.flutterUsage, botDetector: globals.botDetector, - toolStampFile: globals.fs.file('test'), + toolStampFile: () => globals.fs.file('test'), ); await pub.get( context: PubContext.flutterTests, diff --git a/packages/flutter_tools/test/commands.shard/hermetic/precache_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/precache_test.dart index b4dcd818fe..9b79ff0724 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/precache_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/precache_test.dart @@ -23,7 +23,7 @@ void main() { // Release lock between test cases. Cache.releaseLock(); - when(cache.isUpToDate()).thenReturn(false); + when(cache.isUpToDate()).thenAnswer((Invocation _) => Future.value(false)); when(cache.updateAll(any)).thenAnswer((Invocation invocation) { artifacts = invocation.positionalArguments.first as Set; return Future.value(null); @@ -410,7 +410,7 @@ void main() { }); testUsingContext('precache deletes artifact stampfiles when --force is provided', () async { - when(cache.isUpToDate()).thenReturn(true); + when(cache.isUpToDate()).thenAnswer((Invocation _) => Future.value(true)); final PrecacheCommand command = PrecacheCommand( cache: cache, logger: BufferLogger.test(), diff --git a/packages/flutter_tools/test/general.shard/cache_test.dart b/packages/flutter_tools/test/general.shard/cache_test.dart index 111bdc4cb2..427f14c16e 100644 --- a/packages/flutter_tools/test/general.shard/cache_test.dart +++ b/packages/flutter_tools/test/general.shard/cache_test.dart @@ -13,6 +13,7 @@ import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/dart/pub.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:meta/meta.dart'; import 'package:mockito/mockito.dart'; @@ -137,25 +138,25 @@ void main() { ProcessManager: () => FakeProcessManager.any(), }); - testUsingContext('should not be up to date, if some cached artifact is not', () { + testUsingContext('should not be up to date, if some cached artifact is not', () async { final CachedArtifact artifact1 = MockCachedArtifact(); final CachedArtifact artifact2 = MockCachedArtifact(); - when(artifact1.isUpToDate()).thenReturn(true); - when(artifact2.isUpToDate()).thenReturn(false); + when(artifact1.isUpToDate()).thenAnswer((Invocation _) => Future.value(true)); + when(artifact2.isUpToDate()).thenAnswer((Invocation _) => Future.value(false)); final Cache cache = Cache(artifacts: [artifact1, artifact2]); - expect(cache.isUpToDate(), isFalse); + expect(await cache.isUpToDate(), isFalse); }, overrides: { ProcessManager: () => FakeProcessManager.any(), FileSystem: () => MemoryFileSystem.test(), }); - testUsingContext('should be up to date, if all cached artifacts are', () { + testUsingContext('should be up to date, if all cached artifacts are', () async { final CachedArtifact artifact1 = MockCachedArtifact(); final CachedArtifact artifact2 = MockCachedArtifact(); - when(artifact1.isUpToDate()).thenReturn(true); - when(artifact2.isUpToDate()).thenReturn(true); + when(artifact1.isUpToDate()).thenAnswer((Invocation _) => Future.value(true)); + when(artifact2.isUpToDate()).thenAnswer((Invocation _) => Future.value(true)); final Cache cache = Cache(artifacts: [artifact1, artifact2]); - expect(cache.isUpToDate(), isTrue); + expect(await cache.isUpToDate(), isTrue); }, overrides: { ProcessManager: () => FakeProcessManager.any(), FileSystem: () => MemoryFileSystem.test(), @@ -164,8 +165,8 @@ void main() { testUsingContext('should update cached artifacts which are not up to date', () async { final CachedArtifact artifact1 = MockCachedArtifact(); final CachedArtifact artifact2 = MockCachedArtifact(); - when(artifact1.isUpToDate()).thenReturn(true); - when(artifact2.isUpToDate()).thenReturn(false); + when(artifact1.isUpToDate()).thenAnswer((Invocation _) => Future.value(true)); + when(artifact2.isUpToDate()).thenAnswer((Invocation _) => Future.value(false)); final Cache cache = Cache(artifacts: [artifact1, artifact2]); await cache.updateAll({ null, @@ -206,8 +207,8 @@ void main() { testUsingContext('failed storage.googleapis.com download shows China warning', () async { final CachedArtifact artifact1 = MockCachedArtifact(); final CachedArtifact artifact2 = MockCachedArtifact(); - when(artifact1.isUpToDate()).thenReturn(false); - when(artifact2.isUpToDate()).thenReturn(false); + when(artifact1.isUpToDate()).thenAnswer((Invocation _) => Future.value(false)); + when(artifact2.isUpToDate()).thenAnswer((Invocation _) => Future.value(false)); final MockInternetAddress address = MockInternetAddress(); when(address.host).thenReturn('storage.googleapis.com'); when(artifact1.update(any)).thenThrow(SocketException( @@ -317,7 +318,7 @@ void main() { ..createSync(recursive: true); when(mockCache.getRoot()).thenReturn(cacheRoot); final AndroidMavenArtifacts mavenArtifacts = AndroidMavenArtifacts(mockCache); - expect(mavenArtifacts.isUpToDate(), isFalse); + expect(await mavenArtifacts.isUpToDate(), isFalse); final Directory gradleWrapperDir = globals.fs.systemTempDirectory.createTempSync('flutter_cache_test_gradle_wrapper.'); when(mockCache.getArtifactDirectory('gradle_wrapper')).thenReturn(gradleWrapperDir); @@ -340,7 +341,7 @@ void main() { await mavenArtifacts.update(MockArtifactUpdater()); - expect(mavenArtifacts.isUpToDate(), isFalse); + expect(await mavenArtifacts.isUpToDate(), isFalse); }, overrides: { Cache: () => mockCache, FileSystem: () => memoryFileSystem, @@ -660,6 +661,69 @@ void main() { expect(cache.getStampFor('foo'), 'ABC'); }); + + testWithoutContext('PubDependencies needs to be updated if the package config' + ' file or the source directories are missing', () async { + final BufferLogger logger = BufferLogger.test(); + final MemoryFileSystem fileSystem = MemoryFileSystem.test(); + final PubDependencies pubDependencies = PubDependencies( + flutterRoot: () => '', + fileSystem: fileSystem, + logger: logger, + pub: () => MockPub(), + ); + + expect(await pubDependencies.isUpToDate(), false); // no package config + + fileSystem.file('packages/flutter_tools/.packages') + ..createSync(recursive: true) + ..writeAsStringSync('\n'); + fileSystem.file('packages/flutter_tools/.dart_tool/package_config.json') + ..createSync(recursive: true) + ..writeAsStringSync(''' +{ + "configVersion": 2, + "packages": [ + { + "name": "example", + "rootUri": "file:///.pub-cache/hosted/pub.dartlang.org/example-7.0.0", + "packageUri": "lib/", + "languageVersion": "2.7" + } + ], + "generated": "2020-09-15T20:29:20.691147Z", + "generator": "pub", + "generatorVersion": "2.10.0-121.0.dev" +} +'''); + + expect(await pubDependencies.isUpToDate(), false); // dependencies are missing. + + fileSystem.file('.pub-cache/hosted/pub.dartlang.org/example-7.0.0/lib/foo.dart') + .createSync(recursive: true); + + expect(await pubDependencies.isUpToDate(), true); + }); + + testWithoutContext('PubDependencies updates via pub get', () async { + final BufferLogger logger = BufferLogger.test(); + final MemoryFileSystem fileSystem = MemoryFileSystem.test(); + final MockPub pub = MockPub(); + final PubDependencies pubDependencies = PubDependencies( + flutterRoot: () => '', + fileSystem: fileSystem, + logger: logger, + pub: () => pub, + ); + + await pubDependencies.update(MockArtifactUpdater()); + + verify(pub.get( + context: PubContext.pubGet, + directory: 'packages/flutter_tools', + generateSyntheticPackage: false, + )).called(1); + }); } class FakeCachedArtifact extends EngineCachedArtifact { @@ -724,6 +788,7 @@ class MockInternetAddress extends Mock implements InternetAddress {} class MockCache extends Mock implements Cache {} class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {} class MockVersionedPackageResolver extends Mock implements VersionedPackageResolver {} +class MockPub extends Mock implements Pub {} class FakeCache extends Cache { FakeCache({ @required Logger logger, diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index 78494a4183..b902c7c205 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -294,12 +294,6 @@ void main() { verify(mockAppConnection.runMain()).called(1); verify(status.stop()).called(1); - verify(pub.get( - context: PubContext.pubGet, - directory: anyNamed('directory'), - generateSyntheticPackage: false, - )).called(1); - expect(bufferLogger.statusText, contains('Debug service listening on ws://127.0.0.1/abcd/')); expect(debugConnectionInfo.wsUri.toString(), 'ws://127.0.0.1/abcd/'); }, overrides: { diff --git a/packages/flutter_tools/test/src/testbed.dart b/packages/flutter_tools/test/src/testbed.dart index 0914900a23..4a4e2a9bfe 100644 --- a/packages/flutter_tools/test/src/testbed.dart +++ b/packages/flutter_tools/test/src/testbed.dart @@ -931,7 +931,7 @@ class FakeCache implements Cache { } @override - bool isUpToDate() { + Future isUpToDate() async { return true; }