// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/context.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/base/signals.dart'; import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/context_runner.dart'; import 'package:flutter_tools/src/dart/pub.dart'; import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/version.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:meta/meta.dart'; import 'package:process/process.dart'; import 'common.dart' as tester; import 'context.dart'; import 'fake_process_manager.dart'; import 'throwing_pub.dart'; export 'package:flutter_tools/src/base/context.dart' show Generator; // A default value should be provided if the vast majority of tests should use // this provider. For example, [BufferLogger], [MemoryFileSystem]. final Map _testbedDefaults = { // Keeps tests fast by avoiding the actual file system. FileSystem: () => MemoryFileSystem(style: globals.platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix), ProcessManager: () => FakeProcessManager.any(), Logger: () => BufferLogger( terminal: AnsiTerminal(stdio: globals.stdio, platform: globals.platform), // Danger, using real stdio. outputPreferences: OutputPreferences.test(), ), // Allows reading logs and prevents stdout. OperatingSystemUtils: () => FakeOperatingSystemUtils(), OutputPreferences: () => OutputPreferences.test(), // configures BufferLogger to avoid color codes. Usage: () => NoOpUsage(), // prevent addition of analytics from burdening test mocks FlutterVersion: () => FakeFlutterVersion(), // prevent requirement to mock git for test runner. Signals: () => FakeSignals(), // prevent registering actual signal handlers. Pub: () => ThrowingPub(), // prevent accidental invocations of pub. }; /// Manages interaction with the tool injection and runner system. /// /// The Testbed automatically injects reasonable defaults through the context /// DI system such as a [BufferLogger] and a [MemoryFileSytem]. /// /// Example: /// /// Testing that a filesystem operation works as expected /// /// void main() { /// group('Example', () { /// Testbed testbed; /// /// setUp(() { /// testbed = Testbed(setUp: () { /// globals.fs.file('foo').createSync() /// }); /// }) /// /// test('Can delete a file', () => testbed.run(() { /// expect(globals.fs.file('foo').existsSync(), true); /// globals.fs.file('foo').deleteSync(); /// expect(globals.fs.file('foo').existsSync(), false); /// })); /// }); /// } /// /// For a more detailed example, see the code in test_compiler_test.dart. class Testbed { /// Creates a new [TestBed] /// /// `overrides` provides more overrides in addition to the test defaults. /// `setup` may be provided to apply mocks within the tool managed zone, /// including any specified overrides. Testbed({FutureOr Function() setup, Map overrides}) : _setup = setup, _overrides = overrides; final FutureOr Function() _setup; final Map _overrides; /// Runs the `test` within a tool zone. /// /// Unlike [run], this sets up a test group on its own. @isTest void test(String name, FutureOr Function() test, {Map overrides}) { tester.test(name, () { return run(test, overrides: overrides); }); } /// Runs `test` within a tool zone. /// /// `overrides` may be used to provide new context values for the single test /// case or override any context values from the setup. Future run(FutureOr Function() test, {Map overrides}) { final Map testOverrides = { ..._testbedDefaults, // Add the initial setUp overrides ...?_overrides, // Add the test-specific overrides ...?overrides, }; if (testOverrides.containsKey(ProcessUtils)) { throw StateError('Do not inject ProcessUtils for testing, use ProcessManager instead.'); } // Cache the original flutter root to restore after the test case. final String originalFlutterRoot = Cache.flutterRoot; // Track pending timers to verify that they were correctly cleaned up. final Map timers = {}; return HttpOverrides.runZoned(() { return runInContext(() { return context.run( name: 'testbed', overrides: testOverrides, zoneSpecification: ZoneSpecification( createTimer: (Zone self, ZoneDelegate parent, Zone zone, Duration duration, void Function() timer) { final Timer result = parent.createTimer(zone, duration, timer); timers[result] = StackTrace.current; return result; }, createPeriodicTimer: (Zone self, ZoneDelegate parent, Zone zone, Duration period, void Function(Timer) timer) { final Timer result = parent.createPeriodicTimer(zone, period, timer); timers[result] = StackTrace.current; return result; }, ), body: () async { Cache.flutterRoot = ''; if (_setup != null) { await _setup(); } await test(); Cache.flutterRoot = originalFlutterRoot; for (final MapEntry entry in timers.entries) { if (entry.key.isActive) { throw StateError('A Timer was active at the end of a test: ${entry.value}'); } } return null; }); }); }, createHttpClient: (SecurityContext c) => FakeHttpClient()); } } /// A no-op implementation of [Usage] for testing. class NoOpUsage implements Usage { @override bool enabled = false; @override bool suppressAnalytics = true; @override String get clientId => 'test'; @override Future ensureAnalyticsSent() { return null; } @override bool get isFirstRun => false; @override Stream> get onSend => const Stream>.empty(); @override void printWelcome() {} @override void sendCommand(String command, {Map parameters}) {} @override void sendEvent(String category, String parameter, { String label, int value, Map parameters, }) {} @override void sendException(dynamic exception) {} @override void sendTiming(String category, String variableName, Duration duration, { String label }) {} } class FakeHttpClient implements HttpClient { @override bool autoUncompress; @override Duration connectionTimeout; @override Duration idleTimeout; @override int maxConnectionsPerHost; @override String userAgent; @override void addCredentials(Uri url, String realm, HttpClientCredentials credentials) {} @override void addProxyCredentials(String host, int port, String realm, HttpClientCredentials credentials) {} @override set authenticate(Future Function(Uri url, String scheme, String realm) f) {} @override set authenticateProxy(Future Function(String host, int port, String scheme, String realm) f) {} @override set badCertificateCallback(bool Function(X509Certificate cert, String host, int port) callback) {} @override void close({bool force = false}) {} @override Future delete(String host, int port, String path) async { return FakeHttpClientRequest(); } @override Future deleteUrl(Uri url) async { return FakeHttpClientRequest(); } @override set findProxy(String Function(Uri url) f) {} @override Future get(String host, int port, String path) async { return FakeHttpClientRequest(); } @override Future getUrl(Uri url) async { return FakeHttpClientRequest(); } @override Future head(String host, int port, String path) async { return FakeHttpClientRequest(); } @override Future headUrl(Uri url) async { return FakeHttpClientRequest(); } @override Future open(String method, String host, int port, String path) async { return FakeHttpClientRequest(); } @override Future openUrl(String method, Uri url) async { return FakeHttpClientRequest(); } @override Future patch(String host, int port, String path) async { return FakeHttpClientRequest(); } @override Future patchUrl(Uri url) async { return FakeHttpClientRequest(); } @override Future post(String host, int port, String path) async { return FakeHttpClientRequest(); } @override Future postUrl(Uri url) async { return FakeHttpClientRequest(); } @override Future put(String host, int port, String path) async { return FakeHttpClientRequest(); } @override Future putUrl(Uri url) async { return FakeHttpClientRequest(); } } class FakeHttpClientRequest implements HttpClientRequest { FakeHttpClientRequest(); @override bool bufferOutput; @override int contentLength; @override Encoding encoding; @override bool followRedirects; @override int maxRedirects; @override bool persistentConnection; @override void add(List data) {} @override void addError(Object error, [StackTrace stackTrace]) {} @override Future addStream(Stream> stream) async {} @override Future close() async { return FakeHttpClientResponse(); } @override HttpConnectionInfo get connectionInfo => null; @override List get cookies => []; @override Future get done => null; @override Future flush() { return Future.value(); } @override HttpHeaders get headers => FakeHttpHeaders(); @override String get method => null; @override Uri get uri => null; @override void write(Object obj) {} @override void writeAll(Iterable objects, [String separator = '']) {} @override void writeCharCode(int charCode) {} @override void writeln([Object obj = '']) {} } class FakeHttpClientResponse implements HttpClientResponse { final Stream> _delegate = Stream>.fromIterable(const Iterable>.empty()); @override final HttpHeaders headers = FakeHttpHeaders(); @override X509Certificate get certificate => null; @override HttpConnectionInfo get connectionInfo => null; @override int get contentLength => 0; @override HttpClientResponseCompressionState get compressionState { return HttpClientResponseCompressionState.decompressed; } @override List get cookies => null; @override Future detachSocket() { return Future.error(UnsupportedError('Mocked response')); } @override bool get isRedirect => false; @override StreamSubscription> listen(void Function(List event) onData, { Function onError, void Function() onDone, bool cancelOnError }) { return const Stream>.empty().listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); } @override bool get persistentConnection => null; @override String get reasonPhrase => null; @override Future redirect([ String method, Uri url, bool followLoops ]) { return Future.error(UnsupportedError('Mocked response')); } @override List get redirects => []; @override int get statusCode => 400; @override Future any(bool Function(List element) test) { return _delegate.any(test); } @override Stream> asBroadcastStream({ void Function(StreamSubscription> subscription) onListen, void Function(StreamSubscription> subscription) onCancel, }) { return _delegate.asBroadcastStream(onListen: onListen, onCancel: onCancel); } @override Stream asyncExpand(Stream Function(List event) convert) { return _delegate.asyncExpand(convert); } @override Stream asyncMap(FutureOr Function(List event) convert) { return _delegate.asyncMap(convert); } @override Stream cast() { return _delegate.cast(); } @override Future contains(Object needle) { return _delegate.contains(needle); } @override Stream> distinct([bool Function(List previous, List next) equals]) { return _delegate.distinct(equals); } @override Future drain([E futureValue]) { return _delegate.drain(futureValue); } @override Future> elementAt(int index) { return _delegate.elementAt(index); } @override Future every(bool Function(List element) test) { return _delegate.every(test); } @override Stream expand(Iterable Function(List element) convert) { return _delegate.expand(convert); } @override Future> get first => _delegate.first; @override Future> firstWhere( bool Function(List element) test, { List Function() orElse, }) { return _delegate.firstWhere(test, orElse: orElse); } @override Future fold(S initialValue, S Function(S previous, List element) combine) { return _delegate.fold(initialValue, combine); } @override Future forEach(void Function(List element) action) { return _delegate.forEach(action); } @override Stream> handleError( Function onError, { bool Function(dynamic error) test, }) { return _delegate.handleError(onError, test: test); } @override bool get isBroadcast => _delegate.isBroadcast; @override Future get isEmpty => _delegate.isEmpty; @override Future join([String separator = '']) { return _delegate.join(separator); } @override Future> get last => _delegate.last; @override Future> lastWhere( bool Function(List element) test, { List Function() orElse, }) { return _delegate.lastWhere(test, orElse: orElse); } @override Future get length => _delegate.length; @override Stream map(S Function(List event) convert) { return _delegate.map(convert); } @override Future pipe(StreamConsumer> streamConsumer) { return _delegate.pipe(streamConsumer); } @override Future> reduce(List Function(List previous, List element) combine) { return _delegate.reduce(combine); } @override Future> get single => _delegate.single; @override Future> singleWhere(bool Function(List element) test, {List Function() orElse}) { return _delegate.singleWhere(test, orElse: orElse); } @override Stream> skip(int count) { return _delegate.skip(count); } @override Stream> skipWhile(bool Function(List element) test) { return _delegate.skipWhile(test); } @override Stream> take(int count) { return _delegate.take(count); } @override Stream> takeWhile(bool Function(List element) test) { return _delegate.takeWhile(test); } @override Stream> timeout( Duration timeLimit, { void Function(EventSink> sink) onTimeout, }) { return _delegate.timeout(timeLimit, onTimeout: onTimeout); } @override Future>> toList() { return _delegate.toList(); } @override Future>> toSet() { return _delegate.toSet(); } @override Stream transform(StreamTransformer, S> streamTransformer) { return _delegate.transform(streamTransformer); } @override Stream> where(bool Function(List event) test) { return _delegate.where(test); } } /// A fake [HttpHeaders] that ignores all writes. class FakeHttpHeaders extends HttpHeaders { @override List operator [](String name) => []; @override void add(String name, Object value) { } @override void clear() { } @override void forEach(void Function(String name, List values) f) { } @override void noFolding(String name) { } @override void remove(String name, Object value) { } @override void removeAll(String name) { } @override void set(String name, Object value) { } @override String value(String name) => null; } class FakeFlutterVersion implements FlutterVersion { @override String get channel => 'master'; @override Future checkFlutterVersionFreshness() async { } @override bool checkRevisionAncestry({String tentativeDescendantRevision, String tentativeAncestorRevision}) { throw UnimplementedError(); } @override String get dartSdkVersion => '12'; @override String get engineRevision => '42.2'; @override String get engineRevisionShort => '42'; @override Future ensureVersionFile() async { } @override String get frameworkAge => null; @override String get frameworkCommitDate => null; @override String get frameworkDate => null; @override String get frameworkRevision => null; @override String get frameworkRevisionShort => null; @override String get frameworkVersion => null; @override GitTagVersion get gitTagVersion => null; @override String getBranchName({bool redactUnknownBranches = false}) { return 'master'; } @override String getVersionString({bool redactUnknownBranches = false}) { return 'v0.0.0'; } @override bool get isMaster => true; @override String get repositoryUrl => null; @override Map toJson() { return null; } } // A test implementation of [FeatureFlags] that allows enabling without reading // config. If not otherwise specified, all values default to false. class TestFeatureFlags implements FeatureFlags { TestFeatureFlags({ this.isLinuxEnabled = false, this.isMacOSEnabled = false, this.isWebEnabled = false, this.isWindowsEnabled = false, this.isAndroidEmbeddingV2Enabled = false, this.isWebIncrementalCompilerEnabled = false, }); @override final bool isLinuxEnabled; @override final bool isMacOSEnabled; @override final bool isWebEnabled; @override final bool isWindowsEnabled; @override final bool isAndroidEmbeddingV2Enabled; @override final bool isWebIncrementalCompilerEnabled; @override bool isEnabled(Feature feature) { switch (feature) { case flutterWebFeature: return isWebEnabled; case flutterLinuxDesktopFeature: return isLinuxEnabled; case flutterMacOSDesktopFeature: return isMacOSEnabled; case flutterWindowsDesktopFeature: return isWindowsEnabled; case flutterAndroidEmbeddingV2Feature: return isAndroidEmbeddingV2Enabled; case flutterWebIncrementalCompiler: return isWebIncrementalCompilerEnabled; } return false; } } class DelegateLogger implements Logger { DelegateLogger(this.delegate); final Logger delegate; Status status; @override bool get quiet => delegate.quiet; @override set quiet(bool value) => delegate.quiet; @override bool get hasTerminal => delegate.hasTerminal; @override bool get isVerbose => delegate.isVerbose; @override void printError(String message, {StackTrace stackTrace, bool emphasis, TerminalColor color, int indent, int hangingIndent, bool wrap}) { delegate.printError( message, stackTrace: stackTrace, emphasis: emphasis, color: color, indent: indent, hangingIndent: hangingIndent, wrap: wrap, ); } @override void printStatus(String message, {bool emphasis, TerminalColor color, bool newline, int indent, int hangingIndent, bool wrap}) { delegate.printStatus(message, emphasis: emphasis, color: color, indent: indent, hangingIndent: hangingIndent, wrap: wrap, ); } @override void printTrace(String message) { delegate.printTrace(message); } @override void sendEvent(String name, [Map args]) { delegate.sendEvent(name, args); } @override Status startProgress(String message, {Duration timeout, String progressId, bool multilineOutput = false, int progressIndicatorPadding = kDefaultStatusPadding}) { return status; } @override bool get supportsColor => delegate.supportsColor; } /// An implementation of the Cache which does not download or require locking. class FakeCache implements Cache { @override bool includeAllPlatforms; @override bool useUnsignedMacBinaries; @override Future areRemoteArtifactsAvailable({String engineVersion, bool includeAllPlatforms = true}) async { return true; } @override String get dartSdkVersion => null; @override String get storageBaseUrl => null; @override MapEntry get dyLdLibEntry => null; @override String get engineRevision => null; @override Directory getArtifactDirectory(String name) { return globals.fs.currentDirectory; } @override Directory getCacheArtifacts() { return globals.fs.currentDirectory; } @override Directory getCacheDir(String name) { return globals.fs.currentDirectory; } @override Directory getDownloadDir() { return globals.fs.currentDirectory; } @override Directory getRoot() { return globals.fs.currentDirectory; } @override File getLicenseFile() { return globals.fs.currentDirectory.childFile('LICENSE'); } @override File getStampFileFor(String artifactName) { throw UnsupportedError('Not supported in the fake Cache'); } @override String getStampFor(String artifactName) { throw UnsupportedError('Not supported in the fake Cache'); } @override Future getThirdPartyFile(String urlStr, String serviceName) { throw UnsupportedError('Not supported in the fake Cache'); } @override String getVersionFor(String artifactName) { throw UnsupportedError('Not supported in the fake Cache'); } @override Directory getWebSdkDirectory() { return globals.fs.currentDirectory; } @override bool isOlderThanToolsStamp(FileSystemEntity entity) { return false; } @override bool isUpToDate() { return true; } @override void setStampFor(String artifactName, String version) { throw UnsupportedError('Not supported in the fake Cache'); } @override Future updateAll(Set requiredArtifacts) async { } @override Future downloadFile(Uri url, File location) async { } @override Future doesRemoteExist(String message, Uri url) async { return true; } }