[flutter test] Make flutter_test portable

This change relativizes the paths used in the generated
invocation scripts - and adds 'test spec' support to the target (meaning
that the build will register these targets as tests to pass onto
the infrastructure, running these in a shard on separate bot). Moreover,
this change stops supplying the test source directory as this is not
portable.

Bug: IN-823
Change-Id: Ib1a30d1a407a72cbc94c2550f1109ece8fe91fea
diff --git a/runtime/dart/flutter_test.gni b/runtime/dart/flutter_test.gni
index 0a4e95a..280e7fb 100644
--- a/runtime/dart/flutter_test.gni
+++ b/runtime/dart/flutter_test.gni
@@ -4,6 +4,7 @@
 
 import("//build/dart/dart_library.gni")
 import("//build/dart/toolchain.gni")
+import("//build/testing/test_spec.gni")
 import("//topaz/runtime/dart/dart_kernel.gni")
 
 # Defines a flutter test suite
@@ -42,10 +43,12 @@
 
     _main_target_name = target_name
     _library_target_name = "${target_name}_library"
+    _copy_target_name = "${target_name}_copy"
+    _snapshot_target_name = "${target_name}_snapshot"
 
-    _sources_dir = "test"
+    _source_dir = "test"
     if (defined(invoker.source_dir)) {
-      _sources_dir = invoker.source_dir
+      _source_dir = invoker.source_dir
     }
 
     dart_library(_library_target_name) {
@@ -58,17 +61,12 @@
       infer_package_name = true
       sources_required = false
 
-      source_dir = _sources_dir
+      source_dir = _source_dir
       sources = invoker.sources
     }
 
     _dot_packages_file = "$target_gen_dir/$_library_target_name.packages"
 
-    _fuchsia_tester_label = "//third_party/dart-pkg/git/flutter/packages/flutter_tools:fuchsia_tester($host_toolchain)"
-    _fuchsia_tester_out_dir =
-        get_label_info(_fuchsia_tester_label, "root_out_dir")
-    _fuchsia_tester_bin = "$_fuchsia_tester_out_dir/dart-tools/fuchsia_tester"
-
     _flutter_shell_label =
         "//third_party/flutter/shell/testing/($host_toolchain)"
     _flutter_shell_out_dir =
@@ -79,8 +77,10 @@
     _tests_json = []
     _tests_filename = "$target_gen_dir/tests.json"
 
+    _test_runtime_deps = [ _tests_filename ]
+
     foreach(_source_file, invoker.sources) {
-      _source_path = "$_sources_dir/$_source_file"
+      _source_path = "$_source_dir/$_source_file"
 
       _test_target =
           exec_script("//topaz/runtime/dart/gen_flutter_test_target_name.py",
@@ -95,19 +95,19 @@
         _kernel_target_name = "${_test_target_name}_dill"
         _bootstrap_target_name = "${_test_target_name}_bootstrap"
 
-        _bootstrap_file_name =
-            "${target_gen_dir}/${_bootstrap_target_name}.dart"
+        _bootstrap_filename = "${target_gen_dir}/${_bootstrap_target_name}.dart"
+        _dill_filename = "$target_gen_dir/${_kernel_target_name}_kernel.dil"
 
         action(_bootstrap_target_name) {
           script = "$root_out_dir/dart-tools/build_test_bootstrap"
           outputs = [
-            _bootstrap_file_name,
+            _bootstrap_filename,
           ]
 
           rebased_source = rebase_path(_source_path, target_gen_dir)
           args = [
             "--output",
-            rebase_path(_bootstrap_file_name),
+            rebase_path(_bootstrap_filename),
             "--test-name",
             "$rebased_source",
           ]
@@ -120,7 +120,7 @@
         dart_kernel(_kernel_target_name) {
           platform_name = "flutter_runner"
           disable_analysis = true
-          main_dart = _bootstrap_file_name
+          main_dart = _bootstrap_filename
           args = []
 
           # TODO(tvolkert): Change to flutter platform once libraries.json works
@@ -134,7 +134,7 @@
           platform_path = "$root_out_dir/flutter_runner_patched_sdk"
 
           sources = [
-            _bootstrap_file_name,
+            _bootstrap_filename,
           ]
           sources_required = false
           non_dart_deps = [ ":$_bootstrap_target_name" ]
@@ -150,18 +150,131 @@
 
         _tests_json += [
           {
-            source = rebase_path(_bootstrap_file_name)
-            dill =
-                rebase_path("$target_gen_dir/${_kernel_target_name}_kernel.dil")
+            source = rebase_path(_bootstrap_filename, root_build_dir)
+            dill = rebase_path(_dill_filename, root_build_dir)
           },
         ]
+        _test_runtime_deps += [
+          _bootstrap_filename,
+          _dill_filename,
+        ]
       }
 
       write_file(_tests_filename, _tests_json, "json")
     }
 
+    # Copies resources to the build directory so that it may be archived
+    # with the test and the rest of the test's dependencies, so that the
+    # archiving happens with respect to the build directory.
+    _data_dir = "$target_gen_dir/${_main_target_name}_data"
+    _icudtl_file = "$_data_dir/icudtl.dat"
+    _dart_binary = "$_data_dir/dart"
+    copy(_copy_target_name) {
+      sources = [
+        "//third_party/icu/common/icudtl.dat",
+        "//topaz/tools/prebuilt-dart-sdk/$host_os-$host_cpu/bin/dart",
+      ]
+      outputs = [
+        "$_data_dir/{{source_file_part}}",
+      ]
+    }
+
+    # Creates a snapshot file of the fuchsia tester, which allows the test to
+    # be invoked hermetically.
+    _snapshot = "$target_gen_dir/${_main_target_name}.snapshot"
+    _flutter_tools_label = "//third_party/dart-pkg/git/flutter/packages/flutter_tools:flutter_tools"
+    _dart_flutter_tools_gen_dir =
+        get_label_info("$_flutter_tools_label($dart_toolchain)",
+                       "target_gen_dir")
+    _packages_path =
+        "$_dart_flutter_tools_gen_dir/flutter_tools.packages"
+    _main_file = "//third_party/dart-pkg/git/flutter/packages/flutter_tools/bin/fuchsia_tester.dart"
+
+    action(_snapshot_target_name) {
+      depfile = "${_snapshot}.d"
+
+      outputs = [
+        _snapshot,
+      ]
+
+      script = _dart_binary
+
+      # The snapshot path needs to be rebased on top of the root build dir so
+      # that the resulting depfile gets properly formatted.
+      _rebased_snapshot = rebase_path(_snapshot, root_build_dir)
+      _rebased_depfile = rebase_path(depfile)
+      _rebased_packages_path = rebase_path(_packages_path)
+
+      args = [
+        "--snapshot=$_rebased_snapshot",
+        "--snapshot-depfile=$_rebased_depfile",
+        "--packages=$_rebased_packages_path",
+        rebase_path(_main_file),
+      ]
+
+      deps = dart_sdk_deps + [
+               "$_flutter_tools_label",
+               ":$_copy_target_name",
+             ]
+    }
+
+    _invocation_file = "$target_gen_dir/$target_name"
+
+    # _invocation_params encapsulates the parameters to pass to the
+    # invocation-generating action below. The utility lies in being able to
+    # construct the actions args and metadata at the same time.
+    _invocation_params = [
+      {
+        flag = "--wd"
+
+        # TODO(crbug.com/gn/56): Rebasing root_build_dir alone yields a path
+        # component that leaves root_build_dir, preventing portability.
+        path = "$root_build_dir/dummy/.."
+        base = get_path_info(_invocation_file, "dir")
+      },
+      {
+        flag = "--out"
+        path = _invocation_file
+        base = ""  # Will result in an absolute path.
+      },
+      {
+        flag = "--dart"
+        path = _dart_binary
+        base = root_build_dir
+      },
+      {
+        flag = "--snapshot"
+        path = _snapshot
+        base = root_build_dir
+      },
+      {
+        flag = "--tests"
+        path = _tests_filename
+        base = root_build_dir
+      },
+      {
+        flag = "--dot-packages"
+        path = _dot_packages_file
+        base = root_build_dir
+      },
+      {
+        flag = "--flutter-shell"
+        path = _flutter_shell_bin
+        base = root_build_dir
+      },
+      {
+        flag = "--icudtl"
+        path = _icudtl_file
+        base = root_build_dir
+      },
+      {
+        flag = "--sdk-root"
+        path = "$root_out_dir/flutter_runner_patched_sdk"
+        base = root_build_dir
+      },
+    ]
+
     action(_main_target_name) {
-      _invocation_file = "$target_gen_dir/$target_name"
       script = "//topaz/runtime/dart/gen_flutter_test_invocation.py"
       testonly = true
       outputs = [
@@ -170,41 +283,33 @@
 
       inputs = [
         _dot_packages_file,
-        _bootstrap_file_name,
-        _fuchsia_tester_bin,
+        _bootstrap_filename,
         _flutter_shell_bin,
         _tests_filename,
       ]
 
-      args = [
-        "--out",
-        rebase_path(_invocation_file),
-        "--tests",
-        rebase_path(_tests_filename),
-        "--source-dir",
-        rebase_path(_sources_dir),
-        "--dot-packages",
-        rebase_path(_dot_packages_file),
-        "--test-runner",
-        rebase_path(_fuchsia_tester_bin),
-        "--flutter-shell",
-        rebase_path(_flutter_shell_bin),
-        "--icudtl",
-        rebase_path("//third_party/icu/common/icudtl.dat"),
-        "--sdk-root",
-        rebase_path("$root_out_dir/flutter_runner_patched_sdk"),
-      ]
+      args = []
+      foreach(param, _invocation_params) {
+        args += [
+          param.flag,
+          rebase_path(param.path, param.base),
+        ]
+        if (param.flag != "--wd") {
+          _test_runtime_deps += [ param.path ]
+        }
+      }
 
       deps = [
                ":$_library_target_name",
                ":$_bootstrap_target_name",
                ":${_kernel_target_name}_kernel",
+               ":$_snapshot_target_name",
+               ":$_copy_target_name",
                _flutter_shell_label,
-               _fuchsia_tester_label,
              ] + _precompiled_kernel_target_names
 
       metadata = {
-        test_runtime_deps = [ rebase_path(_invocation_file, root_build_dir) ]
+        test_runtime_deps = _test_runtime_deps
       }
     }
   }
@@ -212,12 +317,24 @@
   # Not the Dart toolchain.
   template("flutter_test") {
     _main_target_name = target_name
+    _spec_target_name = "${target_name}_spec"
     _invocation_file = "$target_gen_dir/$target_name"
 
     if (is_linux || is_mac) {
-      # TODO(IN-819): Delete renaming when dart tests are no longer run out of one
-      # directory
+      # TODO(IN-819): Delete renaming when dart tests are no longer run out of
+      # $root_build_dir/host_tests/.
       _invocation_file = "$root_out_dir/$target_name"
+
+      test_spec(_spec_target_name) {
+        testonly = true
+        name = _main_target_name
+        location = _invocation_file
+        deps = [
+          ":$_main_target_name($dart_toolchain)",
+        ]
+      }
+    } else {
+      not_needed([ "_spec_target_name" ])
     }
 
     action(_main_target_name) {
@@ -231,13 +348,19 @@
 
       _dart_target_gen_dir =
           get_label_info(":bogus($dart_toolchain)", "target_gen_dir")
-      _delegate_file = "$_dart_target_gen_dir/$target_name"
+      _delegate_file = "$_dart_target_gen_dir/$_main_target_name"
 
       args = [
+        "--wd",
+
+        # TODO(crbug.com/gn/56): Rebasing root_build_dir alone yields a path
+        # component that leaves root_build_dir, preventing portability.
+        rebase_path("$root_build_dir/dummy/..",
+                    get_path_info(_invocation_file, "dir")),
         "--out",
         rebase_path(_invocation_file),
         "--test",
-        rebase_path(_delegate_file),
+        rebase_path(_delegate_file, root_build_dir),
       ]
 
       deps = [
@@ -259,6 +382,7 @@
             },
           ]
         }
+        deps += [ ":$_spec_target_name" ]
       }
     }
   }
diff --git a/runtime/dart/gen_flutter_test_bundle_invocation.py b/runtime/dart/gen_flutter_test_bundle_invocation.py
index 2a3fd4e..a61a2d8 100755
--- a/runtime/dart/gen_flutter_test_bundle_invocation.py
+++ b/runtime/dart/gen_flutter_test_bundle_invocation.py
@@ -9,20 +9,19 @@
 import string
 import sys
 
-
 def main():
   parser = argparse.ArgumentParser(
-      description='Generate a script that invokes multiple dart_test targets')
+      description='Generate a script that invokes multiple flutter_test targets')
+  parser.add_argument('--wd',
+                      help='Path to the working directory, relative to that of the invocation file',
+                      required=True)
   parser.add_argument('--out',
                       help='Path to the invocation file to generate',
                       required=True)
   parser.add_argument('--test',
                       action='append',
-                      help='Adds a target to the list of test executables',
+                      help='Adds a target to the list of test executables, relative to the working directory',
                       required=True)
-  parser.add_argument('--test-name',
-                      action='append',
-                      help='Adds a readable test name to the list of tests')
   args = parser.parse_args()
 
   test_file = args.out
@@ -30,52 +29,17 @@
   if not os.path.exists(test_dir):
     os.makedirs(test_dir)
 
-  test_failed_definition_block = '''
-FAILED_TESTS=()
-PIDS_NAMES=()
+  script = '''#!/bin/bash
 
-function run_test () {
-  local test_exe="$1"
-  local test_name="$2"
-  echo "Running $test_name"
-  env $test_exe & PIDS_NAMES+=($!:$test_name)
-}
-
-function test_failed () {
-  FAILED_TESTS+=("$1")
-}
+# DO NOT EDIT
+# This script is generated by:
+#   //topaz/runtime/dart/gen_test_bundle_invocation.py
+# See: //topaz/runtime/dart/dart_test.gni
 
 '''
-
-  test_wait_to_complete_block = '''
-for pid_name in ${PIDS_NAMES[*]}; do
-  pid="${pid_name%%:*}"
-  name="${pid_name#*:}"
-  wait $pid || test_failed $name
-done
-'''
-
-  test_failed_check_block = '''
-if [ ${#FAILED_TESTS[@]} -gt 0 ]; then
-  >&2 echo -e "\\e[91mThe following tests failed:\\e[0m"
-  for test_name in "${FAILED_TESTS[@]}"; do
-    >&2 echo -e "\\n  - \\e[91m$test_name\\e[0m"
-  done
-  exit 1
-fi
-'''
-
-  script = '#!/bin/bash\n\n'
-  if args.test_name:
-    script += test_failed_definition_block
-    # TODO(FL-104): Limit concurrency to number of cores
-    for test_executable, test_name in zip(args.test, args.test_name):
-      script += "run_test %s '%s'\n" % (test_executable, test_name)
-    script += test_wait_to_complete_block
-    script += test_failed_check_block
-  else:
-    for test_executable in args.test:
-      script += '%s "$@"\n' % test_executable
+  script += 'cd "$(dirname $0)/%s"\n' % args.wd
+  for test_executable in args.test:
+    script += '%s "$@"\n' % test_executable
 
   with open(test_file, 'w') as file:
       file.write(script)
diff --git a/runtime/dart/gen_flutter_test_invocation.py b/runtime/dart/gen_flutter_test_invocation.py
index e7fbb85..a3b182f 100755
--- a/runtime/dart/gen_flutter_test_invocation.py
+++ b/runtime/dart/gen_flutter_test_invocation.py
@@ -13,29 +13,32 @@
 def main():
   parser = argparse.ArgumentParser(
       description='Generate a script that invokes the Dart tester')
+  parser.add_argument('--wd',
+                      help='Path to the working directory, relative to that of the invocation file',
+                      required=True)
   parser.add_argument('--out',
                       help='Path to the invocation file to generate',
                       required=True)
-  parser.add_argument('--source-dir',
-                      help='Path to test sources',
+  parser.add_argument('--dart',
+                      help='Path to the dart binary, relative to the working directory',
+                      required=True)
+  parser.add_argument('--snapshot',
+                      help='Path to snapshot of the the test runner, relative to the working directory',
                       required=True)
   parser.add_argument('--sdk-root',
-                      help='Path to the SDK platform files',
+                      help='Path to the SDK platform files, relative to the working directory',
                       required=True)
   parser.add_argument('--tests',
-                      help='Path to test-to-precompiled-kernel file list',
+                      help='Path to test-to-precompiled-kernel file list, relative to the working directory',
                       required=True)
   parser.add_argument('--dot-packages',
-                      help='Path to the .packages file',
-                      required=True)
-  parser.add_argument('--test-runner',
-                      help='Path to the test runner',
+                      help='Path to the .packages file, relative to the working directory',
                       required=True)
   parser.add_argument('--flutter-shell',
-                      help='Path to the Flutter shell',
+                      help='Path to the Flutter shell, relative to the working directory',
                       required=True)
   parser.add_argument('--icudtl',
-                      help='Path to the ICU data file',
+                      help='Path to the ICU data file, relative to the working directory',
                       required=True)
   args = parser.parse_args()
 
@@ -50,10 +53,11 @@
 #   //topaz/runtime/dart/gen_flutter_test_invocation.py
 # See: //topaz/runtime/dart/flutter_test.gni
 
-$test_runner \\
+cd "$$(dirname $$0)/$wd"
+
+$dart $snapshot \\
   --packages=$dot_packages \\
   --shell=$flutter_shell \\
-  --test-directory=$source_dir \\
   --tests=$tests \\
   --sdk-root=$sdk_root \\
   --icudtl=$icudtl \\