Compile flutter tests via the sdk

This CL includes:
* A dart script that - given a flutter checkout - finds and compiles
  tests to ensure we actually can.
* A shell script that checks out flutter, compiles the platform
  (to cope with any changes to the kernel format) and runs the above
  script.

Change-Id: I02806a375943361c3c111d9aa12a76d745391ca3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/98140
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Alexander Thomas <athom@google.com>
Reviewed-by: Dmitry Stefantsov <dmitryas@google.com>
diff --git a/pkg/vm/test/frontend_server_flutter.dart b/pkg/vm/test/frontend_server_flutter.dart
new file mode 100644
index 0000000..be1ee67
--- /dev/null
+++ b/pkg/vm/test/frontend_server_flutter.dart
@@ -0,0 +1,251 @@
+import 'dart:async' show StreamController;
+import 'dart:convert' show utf8, LineSplitter;
+import 'dart:io' show Directory, File, FileSystemEntity, IOSink, stdout;
+
+import 'package:kernel/ast.dart' show Component;
+import 'package:kernel/kernel.dart' show loadComponentFromBytes;
+import 'package:kernel/verifier.dart' show verifyComponent;
+
+import '../lib/frontend_server.dart';
+
+main(List<String> args) async {
+  String flutterDir;
+  String flutterPlatformDir;
+  for (String arg in args) {
+    if (arg.startsWith("--flutterDir=")) {
+      flutterDir = arg.substring(13);
+    } else if (arg.startsWith("--flutterPlatformDir=")) {
+      flutterPlatformDir = arg.substring(21);
+    }
+  }
+  if (flutterDir == null || !(new Directory(flutterDir).existsSync())) {
+    throw "Didn't get a valid flutter directory to work with.";
+  }
+  Directory flutterDirectory = new Directory(flutterDir);
+  List<FileSystemEntity> allFlutterFiles =
+      flutterDirectory.listSync(recursive: true, followLinks: false);
+  Directory flutterPlatformDirectory;
+  if (flutterPlatformDir == null) {
+    List<File> platformFiles = new List<File>.from(allFlutterFiles.where((f) =>
+        f.uri
+            .toString()
+            .endsWith("/flutter_patched_sdk/platform_strong.dill")));
+    if (platformFiles.length < 1) {
+      throw "Expected to find a flutter platform file but didn't.";
+    }
+    flutterPlatformDirectory = platformFiles.first.parent;
+  } else {
+    flutterPlatformDirectory = Directory(flutterPlatformDir);
+  }
+  if (!flutterPlatformDirectory.existsSync()) {
+    throw "$flutterPlatformDirectory doesn't exist.";
+  }
+  if (!new File.fromUri(
+          flutterPlatformDirectory.uri.resolve("platform_strong.dill"))
+      .existsSync()) {
+    throw "$flutterPlatformDirectory doesn't contain a "
+        "platform_strong.dill file.";
+  }
+  print("Using $flutterPlatformDirectory as platform directory.");
+  List<File> dotPackagesFiles = new List<File>.from(allFlutterFiles.where((f) =>
+      (f.uri.toString().contains("/examples/") ||
+          f.uri.toString().contains("/packages/")) &&
+      f.uri.toString().endsWith("/.packages")));
+
+  for (File dotPackage in dotPackagesFiles) {
+    Directory tempDir;
+    Directory systemTempDir = Directory.systemTemp;
+    tempDir = systemTempDir.createTempSync('foo bar');
+    try {
+      Directory testDir =
+          new Directory.fromUri(dotPackage.parent.uri.resolve("test/"));
+      if (!testDir.existsSync()) continue;
+      print("Go for $testDir");
+      await attemptStuff(
+          tempDir, flutterPlatformDirectory, dotPackage, testDir);
+    } finally {
+      tempDir.delete(recursive: true);
+    }
+  }
+}
+
+void attemptStuff(Directory tempDir, Directory flutterPlatformDirectory,
+    File dotPackage, Directory testDir) async {
+  File dillFile = new File('${tempDir.path}/dill.dill');
+  if (dillFile.existsSync()) {
+    throw "$dillFile already exists.";
+  }
+  List<int> platformData = new File.fromUri(
+          flutterPlatformDirectory.uri.resolve("platform_strong.dill"))
+      .readAsBytesSync();
+  final List<String> args = <String>[
+    '--sdk-root',
+    flutterPlatformDirectory.path,
+    '--incremental',
+    '--strong',
+    '--target=flutter',
+    '--packages',
+    dotPackage.path,
+    '--output-dill=${dillFile.path}',
+    // '--unsafe-package-serialization',
+  ];
+
+  List<File> testFiles = new List<File>.from(testDir
+      .listSync(recursive: true)
+      .where((f) => f.path.endsWith("_test.dart")));
+  if (testFiles.isEmpty) return;
+
+  Stopwatch stopwatch = new Stopwatch()..start();
+
+  final StreamController<List<int>> inputStreamController =
+      new StreamController<List<int>>();
+  final StreamController<List<int>> stdoutStreamController =
+      new StreamController<List<int>>();
+  final IOSink ioSink = new IOSink(stdoutStreamController.sink);
+  StreamController<Result> receivedResults = new StreamController<Result>();
+
+  final outputParser = new OutputParser(receivedResults);
+  stdoutStreamController.stream
+      .transform(utf8.decoder)
+      .transform(const LineSplitter())
+      .listen(outputParser.listener);
+
+  Iterator<File> testFileIterator = testFiles.iterator;
+  testFileIterator.moveNext();
+
+  final Future<int> result =
+      starter(args, input: inputStreamController.stream, output: ioSink);
+  String shortTestPath =
+      testFileIterator.current.path.substring(testDir.path.length);
+  stdout.write("    => $shortTestPath");
+  Stopwatch stopwatch2 = new Stopwatch()..start();
+  inputStreamController
+      .add('compile ${testFileIterator.current.path}\n'.codeUnits);
+  int compilations = 0;
+  receivedResults.stream.listen((Result compiledResult) {
+    stdout.write(" --- done in ${stopwatch2.elapsedMilliseconds} ms\n");
+    stopwatch2.reset();
+    compiledResult.expectNoErrors();
+
+    List<int> resultBytes = dillFile.readAsBytesSync();
+    Component component = loadComponentFromBytes(platformData);
+    component = loadComponentFromBytes(resultBytes, component);
+    verifyComponent(component);
+    print("        => verified in ${stopwatch2.elapsedMilliseconds} ms.");
+    stopwatch2.reset();
+
+    inputStreamController.add('accept\n'.codeUnits);
+    inputStreamController.add('reset\n'.codeUnits);
+    compilations++;
+
+    if (!testFileIterator.moveNext()) {
+      inputStreamController.add('quit\n'.codeUnits);
+      return;
+    }
+    String shortTestPath =
+        testFileIterator.current.path.substring(testDir.path.length);
+    stdout.write("    => $shortTestPath");
+    inputStreamController.add('recompile ${testFileIterator.current.path} abc\n'
+            '${testFileIterator.current.uri}\n'
+            'abc\n'
+        .codeUnits);
+  });
+
+  int resultDone = await result;
+  if (resultDone != 0) {
+    throw "Got $resultDone, expected 0";
+  }
+
+  inputStreamController.close();
+
+  print("Did $compilations compilations and verifications in "
+      "${stopwatch.elapsedMilliseconds} ms.");
+}
+
+// The below is copied from incremental_compiler_test.dart,
+// but with expect stuff replaced with ifs and throws
+// (expect can only be used in tests via the test framework).
+
+class OutputParser {
+  OutputParser(this._receivedResults);
+  bool expectSources = true;
+
+  StreamController<Result> _receivedResults;
+  List<String> _receivedSources;
+
+  String _boundaryKey;
+  bool _readingSources;
+
+  void listener(String s) {
+    if (_boundaryKey == null) {
+      const String RESULT_OUTPUT_SPACE = 'result ';
+      if (s.startsWith(RESULT_OUTPUT_SPACE)) {
+        _boundaryKey = s.substring(RESULT_OUTPUT_SPACE.length);
+      }
+      _readingSources = false;
+      _receivedSources?.clear();
+      return;
+    }
+
+    if (s.startsWith(_boundaryKey)) {
+      // First boundaryKey separates compiler output from list of sources
+      // (if we expect list of sources, which is indicated by receivedSources
+      // being not null)
+      if (expectSources && !_readingSources) {
+        _readingSources = true;
+        return;
+      }
+      // Second boundaryKey indicates end of frontend server response
+      expectSources = true;
+      _receivedResults.add(new Result(
+          s.length > _boundaryKey.length
+              ? s.substring(_boundaryKey.length + 1)
+              : null,
+          _receivedSources));
+      _boundaryKey = null;
+    } else {
+      if (_readingSources) {
+        if (_receivedSources == null) {
+          _receivedSources = <String>[];
+        }
+        _receivedSources.add(s);
+      }
+    }
+  }
+}
+
+class Result {
+  String status;
+  List<String> sources;
+
+  Result(this.status, this.sources);
+
+  void expectNoErrors({String filename}) {
+    CompilationResult result = new CompilationResult.parse(status);
+    if (result.errorsCount != 0) {
+      throw "Got ${result.errorsCount} errors. Expected 0.";
+    }
+    if (filename != null) {
+      if (result.filename != filename) {
+        throw "Got ${result.filename} errors. Expected $filename.";
+      }
+    }
+  }
+}
+
+class CompilationResult {
+  String filename;
+  int errorsCount;
+
+  CompilationResult.parse(String filenameAndErrorCount) {
+    if (filenameAndErrorCount == null) {
+      return;
+    }
+    int delim = filenameAndErrorCount.lastIndexOf(' ');
+    if (delim <= 0) {
+      throw "Expected $delim > 0...";
+    }
+    filename = filenameAndErrorCount.substring(0, delim);
+    errorsCount = int.parse(filenameAndErrorCount.substring(delim + 1).trim());
+  }
+}
diff --git a/tools/bots/flutter/compile_flutter.sh b/tools/bots/flutter/compile_flutter.sh
new file mode 100755
index 0000000..1c81fba
--- /dev/null
+++ b/tools/bots/flutter/compile_flutter.sh
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+# Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+# Compile flutter tests with a locally built SDK.
+
+set -e
+
+checkout=$(pwd)
+dart=$checkout/out/ReleaseX64/dart-sdk/bin/dart
+sdk=$checkout/out/ReleaseX64/dart-sdk
+tmpdir=$(mktemp -d)
+cleanup() {
+  rm -rf "$tmpdir"
+}
+trap cleanup EXIT HUP INT QUIT TERM PIPE
+pushd "$tmpdir"
+
+git clone -vv https://chromium.googlesource.com/external/github.com/flutter/flutter
+
+pushd flutter
+bin/flutter config --no-analytics
+pinned_dart_sdk=$(cat bin/cache/dart-sdk/revision)
+patch=$checkout/tools/patches/flutter-engine/${pinned_dart_sdk}.flutter.patch
+if [ -e "$patch" ]; then
+  git apply $patch
+fi
+bin/flutter update-packages
+popd
+
+# Directly in temp directory again.
+mkdir engine
+pushd engine
+git clone -vv --depth 1 https://chromium.googlesource.com/external/github.com/flutter/engine flutter
+mkdir third_party
+pushd third_party
+ln -s $checkout dart
+popd
+popd
+
+mkdir flutter_patched_sdk
+
+$checkout/tools/sdks/dart-sdk/bin/dart --packages=$checkout/.packages $checkout/pkg/front_end/tool/_fasta/compile_platform.dart dart:core --single-root-scheme=org-dartlang-sdk --single-root-base=$checkout/ org-dartlang-sdk:///sdk/lib/libraries.json vm_outline_strong.dill vm_platform_strong.dill vm_outline_strong.dill
+$checkout/tools/sdks/dart-sdk/bin/dart --packages=$checkout/.packages $checkout/pkg/front_end/tool/_fasta/compile_platform.dart --target=flutter dart:core --single-root-scheme=org-dartlang-sdk --single-root-base=engine org-dartlang-sdk:///flutter/lib/snapshot/libraries.json vm_outline_strong.dill flutter_patched_sdk/platform_strong.dill flutter_patched_sdk/outline_strong.dill
+
+popd
+
+$dart --enable-asserts pkg/vm/test/frontend_server_flutter.dart --flutterDir=$tmpdir/flutter --flutterPlatformDir=$tmpdir/flutter_patched_sdk
diff --git a/tools/bots/test_matrix.json b/tools/bots/test_matrix.json
index 78e10bc..c50e05b 100644
--- a/tools/bots/test_matrix.json
+++ b/tools/bots/test_matrix.json
@@ -1839,6 +1839,30 @@
       ]
     },
     {
+      "builders": [
+        "flutter-frontend"
+      ],
+      "meta": {
+        "description": "This configuration is used for running frontend tests on flutter code."
+      },
+      "steps": [
+        {
+          "name": "build dart",
+          "script": "tools/build.py",
+          "arguments": [
+            "--mode=release",
+            "--arch=x64",
+            "create_sdk"
+          ]
+        },
+        {
+
+          "name": "compile flutter tests",
+          "script": "tools/bots/flutter/compile_flutter.sh"
+        }
+      ]
+    },
+    {
       "builders": ["fuzz-linux"],
       "meta": {
         "description": "This configuration is used for fuzz testing."