Expose generateTestBootstrap() as public API in test harness (#17290)

This will allow external tools that wrap our test harness to share the
code that generates the test bootstrap.

This change exposed an issue whereby the LocalGoldenFileComparator
was being too strict in its URI handling, so this changes relaxes
that constraint as well (and adds associated tests).
diff --git a/packages/flutter_test/lib/src/goldens.dart b/packages/flutter_test/lib/src/goldens.dart
index 537cca0..8a141a9 100644
--- a/packages/flutter_test/lib/src/goldens.dart
+++ b/packages/flutter_test/lib/src/goldens.dart
@@ -125,8 +125,7 @@
   ///
   /// The [testFile] URI must represent a file.
   LocalFileComparator(Uri testFile, {path.Style pathStyle})
-      : assert(testFile.scheme == 'file'),
-        basedir = _getBasedir(testFile, pathStyle),
+      : basedir = _getBasedir(testFile, pathStyle),
         _path = _getPath(pathStyle);
 
   static path.Context _getPath(path.Style style) {
@@ -135,7 +134,9 @@
 
   static Uri _getBasedir(Uri testFile, path.Style pathStyle) {
     final path.Context context = _getPath(pathStyle);
-    return context.toUri(context.dirname(context.fromUri(testFile)) + context.separator);
+    final String testFilePath = context.fromUri(testFile);
+    final String testDirectoryPath = context.dirname(testFilePath);
+    return context.toUri(testDirectoryPath + context.separator);
   }
 
   /// The directory in which the test was loaded.
diff --git a/packages/flutter_test/test/goldens_test.dart b/packages/flutter_test/test/goldens_test.dart
index b84ed09..e93945b 100644
--- a/packages/flutter_test/test/goldens_test.dart
+++ b/packages/flutter_test/test/goldens_test.dart
@@ -78,6 +78,11 @@
       expect(comparator.basedir, fs.directory(fix('/foo/bar/')).uri);
     });
 
+    test('can be instantiated with uri that represents file in same folder', () {
+      comparator = new LocalFileComparator(Uri.parse('foo_test.dart'), pathStyle: fs.path.style);
+      expect(comparator.basedir, Uri.parse('./'));
+    });
+
     group('compare', () {
       Future<bool> doComparison([String golden = 'golden.png']) {
         final Uri uri = fs.file(fix(golden)).uri;
@@ -101,6 +106,28 @@
           final bool success = await doComparison('sub/foo.png');
           expect(success, isTrue);
         });
+
+        group('when comparator instantiated with uri that represents file in same folder', () {
+          test('and golden file is in same folder as test', () async {
+            fs.file(fix('/foo/bar/golden.png'))
+              ..createSync(recursive: true)
+              ..writeAsBytesSync(_kExpectedBytes);
+            fs.currentDirectory = fix('/foo/bar');
+            comparator = new LocalFileComparator(Uri.parse('local_test.dart'), pathStyle: fs.path.style);
+            final bool success = await doComparison('golden.png');
+            expect(success, isTrue);
+          });
+
+          test('and golden file is in subfolder of test', () async {
+            fs.file(fix('/foo/bar/baz/golden.png'))
+              ..createSync(recursive: true)
+              ..writeAsBytesSync(_kExpectedBytes);
+            fs.currentDirectory = fix('/foo/bar');
+            comparator = new LocalFileComparator(Uri.parse('local_test.dart'), pathStyle: fs.path.style);
+            final bool success = await doComparison('baz/golden.png');
+            expect(success, isTrue);
+          });
+        });
       });
 
       group('fails', () {
diff --git a/packages/flutter_tools/lib/src/test/flutter_platform.dart b/packages/flutter_tools/lib/src/test/flutter_platform.dart
index b2e68a0..bc2c8c1 100644
--- a/packages/flutter_tools/lib/src/test/flutter_platform.dart
+++ b/packages/flutter_tools/lib/src/test/flutter_platform.dart
@@ -95,6 +95,95 @@
   );
 }
 
+/// Generates the bootstrap entry point script that will be used to launch an
+/// individual test file.
+///
+/// The [testUrl] argument specifies the path to the test file that is being
+/// launched.
+///
+/// The [host] argument specifies the address at which the test harness is
+/// running.
+///
+/// If [testConfigFile] is specified, it must follow the conventions of test
+/// configuration files as outlined in the [flutter_test] library. By default,
+/// the test file will be launched directly.
+///
+/// The [updateGoldens] argument will set the [autoUpdateGoldens] global
+/// variable in the [flutter_test] package before invoking the test.
+String generateTestBootstrap({
+  @required Uri testUrl,
+  @required InternetAddress host,
+  File testConfigFile,
+  bool updateGoldens: false,
+}) {
+  assert(testUrl != null);
+  assert(host != null);
+  assert(updateGoldens != null);
+
+  final String websocketUrl = host.type == InternetAddressType.IP_V4 // ignore: deprecated_member_use
+      ? 'ws://${host.address}'
+      : 'ws://[${host.address}]';
+  final String encodedWebsocketUrl = Uri.encodeComponent(websocketUrl);
+
+  final StringBuffer buffer = new StringBuffer();
+  buffer.write('''
+import 'dart:convert';
+import 'dart:io';  // ignore: dart_io_import
+
+// We import this library first in order to trigger an import error for
+// package:test (rather than package:stream_channel) when the developer forgets
+// to add a dependency on package:test.
+import 'package:test/src/runner/plugin/remote_platform_helpers.dart';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/src/runner/vm/catch_isolate_errors.dart';
+
+import '$testUrl' as test;
+'''
+  );
+  if (testConfigFile != null) {
+    buffer.write('''
+import '${new Uri.file(testConfigFile.path)}' as test_config;
+'''
+    );
+  }
+  buffer.write('''
+
+void main() {
+  print('$_kStartTimeoutTimerMessage');
+  String serverPort = Platform.environment['SERVER_PORT'];
+  String server = Uri.decodeComponent('$encodedWebsocketUrl:\$serverPort');
+  StreamChannel channel = serializeSuite(() {
+    catchIsolateErrors();
+    goldenFileComparator = new LocalFileComparator(Uri.parse('$testUrl'));
+    autoUpdateGoldenFiles = $updateGoldens;
+'''
+  );
+  if (testConfigFile != null) {
+    buffer.write('''
+    return () => test_config.main(test.main);
+''');
+  } else {
+    buffer.write('''
+    return test.main;
+''');
+  }
+  buffer.write('''
+  });
+  WebSocket.connect(server).then((WebSocket socket) {
+    socket.map((dynamic x) {
+      assert(x is String);
+      return json.decode(x);
+    }).pipe(channel.sink);
+    socket.addStream(channel.stream.map(json.encode));
+  });
+}
+'''
+  );
+  return buffer.toString();
+}
+
 enum _InitialResult { crashed, timedOut, connected }
 enum _TestResult { crashed, harnessBailed, testBailed }
 typedef Future<Null> _Finalizer();
@@ -581,7 +670,6 @@
     listenerFile.createSync();
     listenerFile.writeAsStringSync(_generateTestMain(
       testUrl: fs.path.toUri(fs.path.absolute(testPath)),
-      encodedWebsocketUrl: Uri.encodeComponent(_getWebSocketUrl()),
     ));
     return listenerFile.path;
   }
@@ -621,15 +709,8 @@
     return tempBundleDirectory.path;
   }
 
-  String _getWebSocketUrl() {
-    return host.type == InternetAddressType.IP_V4 // ignore: deprecated_member_use
-        ? 'ws://${host.address}'
-        : 'ws://[${host.address}]';
-  }
-
   String _generateTestMain({
     Uri testUrl,
-    String encodedWebsocketUrl,
   }) {
     assert(testUrl.scheme == 'file');
     File testConfigFile;
@@ -648,63 +729,12 @@
       }
       directory = directory.parent;
     }
-    final StringBuffer buffer = new StringBuffer();
-    buffer.write('''
-import 'dart:convert';
-import 'dart:io';  // ignore: dart_io_import
-
-// We import this library first in order to trigger an import error for
-// package:test (rather than package:stream_channel) when the developer forgets
-// to add a dependency on package:test.
-import 'package:test/src/runner/plugin/remote_platform_helpers.dart';
-
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stream_channel/stream_channel.dart';
-import 'package:test/src/runner/vm/catch_isolate_errors.dart';
-
-import '$testUrl' as test;
-'''
+    return generateTestBootstrap(
+      testUrl: testUrl,
+      testConfigFile: testConfigFile,
+      host: host,
+      updateGoldens: updateGoldens,
     );
-    if (testConfigFile != null) {
-      buffer.write('''
-import '${new Uri.file(testConfigFile.path)}' as test_config;
-'''
-      );
-    }
-    buffer.write('''
-
-void main() {
-  print('$_kStartTimeoutTimerMessage');
-  String serverPort = Platform.environment['SERVER_PORT'];
-  String server = Uri.decodeComponent('$encodedWebsocketUrl:\$serverPort');
-  StreamChannel channel = serializeSuite(() {
-    catchIsolateErrors();
-    goldenFileComparator = new LocalFileComparator(Uri.parse('$testUrl'));
-    autoUpdateGoldenFiles = $updateGoldens;
-'''
-    );
-    if (testConfigFile != null) {
-      buffer.write('''
-    return () => test_config.main(test.main);
-''');
-    } else {
-      buffer.write('''
-    return test.main;
-''');
-    }
-    buffer.write('''
-  });
-  WebSocket.connect(server).then((WebSocket socket) {
-    socket.map((dynamic x) {
-      assert(x is String);
-      return json.decode(x);
-    }).pipe(channel.sink);
-    socket.addStream(channel.stream.map(json.encode));
-  });
-}
-'''
-    );
-    return buffer.toString();
   }
 
   File _cachedFontConfig;