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();
+      });
+    });
   });
 }