// 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 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; 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/platform.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 'context.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: platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix), Logger: () => BufferLogger(), // 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: () { /// fs.file('foo').createSync() /// }); /// }) /// /// test('Can delete a file', () => testBed.run(() { /// expect(fs.file('foo').existsSync(), true); /// fs.file('foo').deleteSync(); /// expect(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 `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. FutureOr run(FutureOr Function() test, {Map overrides}) { final Map testOverrides = { ..._testbedDefaults, // Add the initial setUp overrides ...?_overrides, // Add the test-specific overrides ...?overrides, }; // 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 (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 => null; @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(Uint8List 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(Uint8List 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(Uint8List event) convert) { return _delegate.asyncExpand(convert); } @override Stream asyncMap(FutureOr Function(Uint8List 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(Uint8List previous, Uint8List 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(Uint8List element) test) { return _delegate.every(test); } @override Stream expand(Iterable Function(Uint8List element) convert) { return _delegate.expand(convert); } @override Future get first => _delegate.first; @override Future firstWhere( bool Function(Uint8List element) test, { List Function() orElse, }) { return _delegate.firstWhere(test, orElse: orElse); } @override Future fold(S initialValue, S Function(S previous, Uint8List element) combine) { return _delegate.fold(initialValue, combine); } @override Future forEach(void Function(Uint8List 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(Uint8List element) test, { List Function() orElse, }) { return _delegate.lastWhere(test, orElse: orElse); } @override Future get length => _delegate.length; @override Stream map(S Function(Uint8List event) convert) { return _delegate.map(convert); } @override Future pipe(StreamConsumer> streamConsumer) { return _delegate.pipe(streamConsumer); } @override Future reduce(List Function(Uint8List previous, Uint8List element) combine) { return _delegate.reduce(combine); } @override Future get single => _delegate.single; @override Future singleWhere(bool Function(Uint8List 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(Uint8List element) test) { return _delegate.skipWhile(test); } @override Stream take(int count) { return _delegate.take(count); } @override Stream takeWhile(bool Function(Uint8List 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(Uint8List 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 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.isNewAndroidEmbeddingEnabled = false, }); @override final bool isLinuxEnabled; @override final bool isMacOSEnabled; @override final bool isWebEnabled; @override final bool isWindowsEnabled; @override final bool isNewAndroidEmbeddingEnabled; @override bool isEnabled(Feature feature) { switch (feature) { case flutterWebFeature: return isWebEnabled; case flutterLinuxDesktopFeature: return isLinuxEnabled; case flutterMacOSDesktopFeature: return isMacOSEnabled; case flutterWindowsDesktopFeature: return isWindowsEnabled; case flutterNewAndroidEmbeddingFeature: return isNewAndroidEmbeddingEnabled; } return false; } } class ThrowingPub implements Pub { @override Future batch(List arguments, { PubContext context, String directory, MessageFilter filter, String failureMessage = 'pub failed', bool retry, bool showTraceForErrors, }) { throw UnsupportedError('Attempted to inovke pub during test.'); } @override Future get({ PubContext context, String directory, bool skipIfAbsent = false, bool upgrade = false, bool offline = false, bool checkLastModified = true, bool skipPubspecYamlCheck = false, }) { throw UnsupportedError('Attempted to inovke pub during test.'); } @override Future interactively(List arguments, {String directory}) { throw UnsupportedError('Attempted to inovke pub during test.'); } }