Initial sketch of tools testbed (#31765)


diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index 70049bc..8c318bc 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -4,7 +4,7 @@
 
 import 'dart:async';
 import 'dart:convert';
-import 'dart:io' as io show IOSink;
+import 'dart:io' as io show IOSink, ProcessSignal;
 
 import 'package:flutter_tools/src/android/android_device.dart';
 import 'package:flutter_tools/src/android/android_sdk.dart' show AndroidSdk;
@@ -196,6 +196,38 @@
   final Stream<List<int>> stderr;
 }
 
+/// A fake process implemenation which can be provided all necessary values.
+class FakeProcess implements Process {
+  FakeProcess({
+    this.pid = 1,
+    Future<int> exitCode,
+    Stream<List<int>> stdin,
+    this.stdout = const Stream<List<int>>.empty(),
+    this.stderr = const Stream<List<int>>.empty(),
+  }) : exitCode = exitCode ?? Future<int>.value(0),
+       stdin = stdin ?? MemoryIOSink();
+
+  @override
+  final int pid;
+
+  @override
+  final Future<int> exitCode;
+
+  @override
+  final io.IOSink stdin;
+
+  @override
+  final Stream<List<int>> stdout;
+
+  @override
+  final Stream<List<int>> stderr;
+
+  @override
+  bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
+    return true;
+  }
+}
+
 /// A process that prompts the user to proceed, then asynchronously writes
 /// some lines to stdout before it exits.
 class PromptingProcess implements Process {
diff --git a/packages/flutter_tools/test/src/testbed.dart b/packages/flutter_tools/test/src/testbed.dart
new file mode 100644
index 0000000..9582152
--- /dev/null
+++ b/packages/flutter_tools/test/src/testbed.dart
@@ -0,0 +1,86 @@
+// 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 '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/logger.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/context_runner.dart';
+
+export 'package:flutter_tools/src/base/context.dart' show Generator;
+
+final Map<Type, Generator> _testbedDefaults = <Type, Generator>{
+  FileSystem: () => MemoryFileSystem(),
+  Logger: () => BufferLogger(),
+};
+
+/// 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({Future<void> Function() setup, Map<Type, Generator> overrides})
+    : _setup = setup,
+      _overrides = overrides;
+
+
+  final Future<void> Function() _setup;
+  final Map<Type, Generator> _overrides;
+
+  /// Runs `test` within a tool zone.
+  FutureOr<T> run<T>(FutureOr<T> Function() test) {
+    final Map<Type, Generator> testOverrides = Map<Type, Generator>.from(_testbedDefaults);
+    if (_overrides != null) {
+      testOverrides.addAll(_overrides);
+    }
+    // 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;
+        }
+      );
+    });
+  }
+}
\ No newline at end of file
diff --git a/packages/flutter_tools/test/test_compiler_test.dart b/packages/flutter_tools/test/test_compiler_test.dart
index 3a3550d..f2fb4b1 100644
--- a/packages/flutter_tools/test/test_compiler_test.dart
+++ b/packages/flutter_tools/test/test_compiler_test.dart
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:file/memory.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/compile.dart';
 import 'package:flutter_tools/src/project.dart';
@@ -10,20 +9,31 @@
 import 'package:mockito/mockito.dart';
 
 import 'src/common.dart';
-import 'src/context.dart';
+import 'src/testbed.dart';
 
 void main() {
-  group('TestCompiler', () {
-    testUsingContext('compiles test file with no errors', () async {
-      fs.file('pubspec.yaml').createSync();
-      fs.file('.packages').createSync();
-      fs.file('test/foo.dart').createSync(recursive: true);
-      final MockResidentCompiler residentCompiler = MockResidentCompiler();
-      final TestCompiler testCompiler = FakeTestCompiler(
-        false,
-        FlutterProject.current(),
-        residentCompiler,
+  group(TestCompiler, () {
+    Testbed testbed;
+    FakeTestCompiler testCompiler;
+    MockResidentCompiler residentCompiler;
+
+    setUp(() {
+      testbed = Testbed(
+        setup: () {
+          fs.file('pubspec.yaml').createSync();
+          fs.file('.packages').createSync();
+          fs.file('test/foo.dart').createSync(recursive: true);
+          residentCompiler = MockResidentCompiler();
+          testCompiler = FakeTestCompiler(
+            false,
+            FlutterProject.current(),
+            residentCompiler,
+          );
+        },
       );
+    });
+
+    test('Reports a dill file when compile is successful', () => testbed.run(() async {
       when(residentCompiler.recompile(
         'test/foo.dart',
         <Uri>[Uri.parse('test/foo.dart')],
@@ -35,20 +45,9 @@
 
       expect(await testCompiler.compile('test/foo.dart'), 'test/foo.dart.dill');
       expect(fs.file('test/foo.dart.dill').existsSync(), true);
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-    });
+    }));
 
-    testUsingContext('does not compile test file with errors', () async {
-      fs.file('pubspec.yaml').createSync();
-      fs.file('.packages').createSync();
-      fs.file('test/foo.dart').createSync(recursive: true);
-      final MockResidentCompiler residentCompiler = MockResidentCompiler();
-      final TestCompiler testCompiler = FakeTestCompiler(
-        false,
-        FlutterProject.current(),
-        residentCompiler,
-      );
+    test('Reports null when a compile fails', () => testbed.run(() async {
       when(residentCompiler.recompile(
         'test/foo.dart',
         <Uri>[Uri.parse('test/foo.dart')],
@@ -60,9 +59,7 @@
 
       expect(await testCompiler.compile('test/foo.dart'), null);
       expect(fs.file('test/foo.dart.dill').existsSync(), false);
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-    });
+    }));
   });
 }
 
diff --git a/packages/flutter_tools/test/web/compile_test.dart b/packages/flutter_tools/test/web/compile_test.dart
index b7a33c5..905dd83 100644
--- a/packages/flutter_tools/test/web/compile_test.dart
+++ b/packages/flutter_tools/test/web/compile_test.dart
@@ -3,51 +3,47 @@
 // found in the LICENSE file.
 
 import 'package:flutter_tools/src/artifacts.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/globals.dart';
 import 'package:flutter_tools/src/web/compile.dart';
 import 'package:mockito/mockito.dart';
 import 'package:process/process.dart';
 
-import '../src/context.dart';
+import '../src/common.dart';
+import '../src/mocks.dart';
+import '../src/testbed.dart';
 
 void main() {
-  final MockProcessManager mockProcessManager = MockProcessManager();
-  final MockProcess mockProcess = MockProcess();
-  final BufferLogger mockLogger = BufferLogger();
+  group(WebCompiler, () {
+    MockProcessManager mockProcessManager;
+    Testbed testBed;
 
-  testUsingContext('invokes dart2js with correct arguments', () async {
-    const WebCompiler webCompiler = WebCompiler();
-    final String engineDartPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
-    final String dart2jsPath = artifacts.getArtifactPath(Artifact.dart2jsSnapshot);
-    final String flutterWebSdkPath = artifacts.getArtifactPath(Artifact.flutterWebSdk);
-    final String librariesPath = fs.path.join(flutterWebSdkPath, 'libraries.json');
+    setUp(() {
+      mockProcessManager = MockProcessManager();
+      testBed = Testbed(setup: () async {
+        final String engineDartPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
+        when(mockProcessManager.start(any)).thenAnswer((Invocation invocation) async => FakeProcess());
+        when(mockProcessManager.canRun(engineDartPath)).thenReturn(true);
 
-    when(mockProcess.stdout).thenAnswer((Invocation invocation) => const Stream<List<int>>.empty());
-    when(mockProcess.stderr).thenAnswer((Invocation invocation) => const Stream<List<int>>.empty());
-    when(mockProcess.exitCode).thenAnswer((Invocation invocation) async => 0);
-    when(mockProcessManager.start(any)).thenAnswer((Invocation invocation) async => mockProcess);
-    when(mockProcessManager.canRun(engineDartPath)).thenReturn(true);
+      }, overrides: <Type, Generator>{
+        ProcessManager: () => mockProcessManager,
+      });
+    });
 
-    await webCompiler.compile(target: 'lib/main.dart');
+    test('invokes dart2js with correct arguments', () => testBed.run(() async {
+      await webCompiler.compile(target: 'lib/main.dart');
 
-    final String outputPath = fs.path.join('build', 'web', 'main.dart.js');
-    verify(mockProcessManager.start(<String>[
-      engineDartPath,
-      dart2jsPath,
-      'lib/main.dart',
-      '-o',
-      outputPath,
-      '--libraries-spec=$librariesPath',
-      '-m',
-    ])).called(1);
-  }, overrides: <Type, Generator>{
-    ProcessManager: () => mockProcessManager,
-    Logger: () => mockLogger,
+      verify(mockProcessManager.start(<String>[
+        'bin/cache/dart-sdk/bin/dart',
+        'bin/cache/dart-sdk/bin/snapshots/dart2js.dart.snapshot',
+        'lib/main.dart',
+        '-o',
+        'build/web/main.dart.js',
+        '--libraries-spec=bin/cache/flutter_web_sdk/libraries.json',
+        '-m',
+      ])).called(1);
+
+    }));
   });
 }
 
 class MockProcessManager extends Mock implements ProcessManager {}
-class MockProcess extends Mock implements Process {}