Add timer checking and Fake http client to testbed (#35392)
diff --git a/packages/flutter_tools/lib/src/base/context.dart b/packages/flutter_tools/lib/src/base/context.dart index 97ea7c9..a21bd45 100644 --- a/packages/flutter_tools/lib/src/base/context.dart +++ b/packages/flutter_tools/lib/src/base/context.dart
@@ -142,6 +142,7 @@ String name, Map<Type, Generator> overrides, Map<Type, Generator> fallbacks, + ZoneSpecification zoneSpecification, }) async { final AppContext child = AppContext._( this, @@ -152,6 +153,7 @@ return await runZoned<Future<V>>( () async => await body(), zoneValues: <_Key, AppContext>{_Key.key: child}, + zoneSpecification: zoneSpecification, ); }
diff --git a/packages/flutter_tools/test/src/testbed.dart b/packages/flutter_tools/test/src/testbed.dart index acd1526..ee01dbf 100644 --- a/packages/flutter_tools/test/src/testbed.dart +++ b/packages/flutter_tools/test/src/testbed.dart
@@ -3,8 +3,12 @@ // 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/io.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/context.dart'; import 'package:flutter_tools/src/base/file_system.dart'; @@ -25,7 +29,9 @@ // - More TBD. final Map<Type, Generator> _testbedDefaults = <Type, Generator>{ // Keeps tests fast by avoid actual file system. - FileSystem: () => MemoryFileSystem(style: platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix), + FileSystem: () => MemoryFileSystem(style: platform.isWindows + ? FileSystemStyle.windows + : FileSystemStyle.posix), Logger: () => BufferLogger(), // Allows reading logs and prevents stdout. OutputPreferences: () => OutputPreferences(showColor: false), // configures BufferLogger to avoid color codes. Usage: () => NoOpUsage(), // prevent addition of analytics from burdening test mocks @@ -67,9 +73,8 @@ /// `setup` may be provided to apply mocks within the tool managed zone, /// including any specified overrides. Testbed({FutureOr<void> Function() setup, Map<Type, Generator> overrides}) - : _setup = setup, - _overrides = overrides; - + : _setup = setup, + _overrides = overrides; final Future<void> Function() _setup; final Map<Type, Generator> _overrides; @@ -88,21 +93,42 @@ }; // Cache the original flutter root to restore after the test case. final String originalFlutterRoot = Cache.flutterRoot; - return runInContext<T>(() { - return context.run<T>( - name: 'testbed', - overrides: testOverrides, - body: () async { - Cache.flutterRoot = ''; - if (_setup != null) { - await _setup(); - } - await test(); - Cache.flutterRoot = originalFlutterRoot; - return null; - } - ); - }); + // Track pending timers to verify that they were correctly cleaned up. + final Map<Timer, StackTrace> timers = <Timer, StackTrace>{}; + + return HttpOverrides.runZoned(() { + return runInContext<T>(() { + return context.run<T>( + 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<Timer, StackTrace> 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()); } } @@ -135,12 +161,259 @@ void sendCommand(String command, {Map<String, String> parameters}) {} @override - void sendEvent(String category, String parameter, {Map<String, String> parameters}) {} + void sendEvent(String category, String parameter,{ Map<String, String> parameters }) {} @override void sendException(dynamic exception, StackTrace trace) {} @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<bool> Function(Uri url, String scheme, String realm) f) {} + + @override + set authenticateProxy( + Future<bool> 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<HttpClientRequest> delete(String host, int port, String path) async { + return FakeHttpClientRequest(); + } + + @override + Future<HttpClientRequest> deleteUrl(Uri url) async { + return FakeHttpClientRequest(); + } + + @override + set findProxy(String Function(Uri url) f) {} + + @override + Future<HttpClientRequest> get(String host, int port, String path) async { + return FakeHttpClientRequest(); + } + + @override + Future<HttpClientRequest> getUrl(Uri url) async { + return FakeHttpClientRequest(); + } + + @override + Future<HttpClientRequest> head(String host, int port, String path) async { + return FakeHttpClientRequest(); + } + + @override + Future<HttpClientRequest> headUrl(Uri url) async { + return FakeHttpClientRequest(); + } + + @override + Future<HttpClientRequest> open(String method, String host, int port, String path) async { + return FakeHttpClientRequest(); + } + + @override + Future<HttpClientRequest> openUrl(String method, Uri url) async { + return FakeHttpClientRequest(); + } + + @override + Future<HttpClientRequest> patch(String host, int port, String path) async { + return FakeHttpClientRequest(); + } + + @override + Future<HttpClientRequest> patchUrl(Uri url) async { + return FakeHttpClientRequest(); + } + + @override + Future<HttpClientRequest> post(String host, int port, String path) async { + return FakeHttpClientRequest(); + } + + @override + Future<HttpClientRequest> postUrl(Uri url) async { + return FakeHttpClientRequest(); + } + + @override + Future<HttpClientRequest> put(String host, int port, String path) async { + return FakeHttpClientRequest(); + } + + @override + Future<HttpClientRequest> 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<int> data) {} + + @override + void addError(Object error, [StackTrace stackTrace]) {} + + @override + Future<void> addStream(Stream<List<int>> stream) async {} + + @override + Future<HttpClientResponse> close() async { + return FakeHttpClientResponse(); + } + + @override + HttpConnectionInfo get connectionInfo => null; + + @override + List<Cookie> get cookies => <Cookie>[]; + + @override + Future<HttpClientResponse> get done => null; + + @override + Future<void> flush() { + return Future<void>.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<Object> objects, [String separator = '']) {} + + @override + void writeCharCode(int charCode) {} + + @override + void writeln([Object obj = '']) {} +} + +class FakeHttpClientResponse extends Stream<Uint8List> + implements HttpClientResponse { + + final Stream<List<int>> _content = const Stream<List<int>>.empty(); + + @override + X509Certificate get certificate => null; + + @override + HttpClientResponseCompressionState get compressionState => null; + + @override + HttpConnectionInfo get connectionInfo => null; + + @override + int get contentLength => 0; + + @override + List<Cookie> get cookies => <Cookie>[]; + + @override + Future<Socket> detachSocket() async { + return null; + } + + @override + HttpHeaders get headers => null; + + @override + bool get isRedirect => null; + + @override + StreamSubscription<Uint8List> listen(void Function(Uint8List event) onData, + {Function onError, void Function() onDone, bool cancelOnError}) { + return _content.listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError + ); + } + + @override + bool get persistentConnection => false; + + @override + String get reasonPhrase => null; + + @override + Future<HttpClientResponse> redirect( + [String method, Uri url, bool followLoops]) { + return null; + } + + @override + List<RedirectInfo> get redirects => const <RedirectInfo>[]; + + @override + int get statusCode => HttpStatus.badRequest; void sendTiming(String category, String variableName, Duration duration, {String label}) {} }
diff --git a/packages/flutter_tools/test/testbed_test.dart b/packages/flutter_tools/test/testbed_test.dart index e75e4cc..33c3bc8 100644 --- a/packages/flutter_tools/test/testbed_test.dart +++ b/packages/flutter_tools/test/testbed_test.dart
@@ -2,6 +2,9 @@ // 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:io'; + import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/context.dart'; import 'package:flutter_tools/src/base/file_system.dart'; @@ -50,6 +53,35 @@ expect(instance, isA<B>()); }); + + test('provides a mocked http client', () async { + final Testbed testbed = Testbed(); + await testbed.run(() async { + final HttpClient client = HttpClient(); + final HttpClientRequest request = await client.getUrl(null); + final HttpClientResponse response = await request.close(); + + expect(response.statusCode, HttpStatus.badRequest); + expect(response.contentLength, 0); + }); + }); + + test('Throws StateError if Timer is left pending', () async { + final Testbed testbed = Testbed(); + + expect(testbed.run(() async { + Timer.periodic(const Duration(seconds: 1), (Timer timer) { }); + }), throwsA(isInstanceOf<StateError>())); + }); + + test('Doesnt throw a StateError if Timer is left cleaned up', () async { + final Testbed testbed = Testbed(); + + testbed.run(() async { + final Timer timer = Timer.periodic(const Duration(seconds: 1), (Timer timer) { }); + timer.cancel(); + }); + }); }); }