[test spec] Add support for linux and mac tests

This change
(1) adds support to test_spec() target a VM for linux and mac tests - and adds
support test() to record test specs in this case.
(2) adds package processing logic to create host_tests.json, a manifest of host
tests included in a build, mirroring packages.json

Bug: IN-823
Test: verified locally that test() gives the desired metadata, and that
host_tests.json is produced.

Change-Id: I994b24402bb7ada8c4e68308930ef22723965a47
diff --git a/gn/BUILD.gn b/gn/BUILD.gn
index 0da99f4..c8d38c4 100644
--- a/gn/BUILD.gn
+++ b/gn/BUILD.gn
@@ -37,6 +37,8 @@
 }
 
 # Copy host test binaries to $root_build_dir/host_tests.
+# TODO(IN-819): Delete this copy target once host tests are no longer run out
+# of a single directory.
 if (package_host_tests != []) {
   copy("host_tests") {
     deps = []
@@ -61,6 +63,21 @@
   }
 }
 
+# Write a JSON metadata file about the host tests in the build.
+host_tests = []
+foreach(label, package_host_tests) {
+  host_label = "$label($host_toolchain)"
+  host_tests += [
+    {
+      dir = get_label_info(host_label, "dir")
+      name = get_label_info(host_label, "name")
+      build_dir = rebase_path(get_label_info(host_label, "target_out_dir"),
+                              root_build_dir)
+    },
+  ]
+}
+write_file("$root_build_dir/host_tests.json", host_tests, "json")
+
 # Collect the source files that are dependencies of the create_gn_rules.py
 # script, below.  Unfortunately, exec_script cannot use a depfile produced
 # by the script and only supports a separately computed list of dependencies.
@@ -170,7 +187,7 @@
 # their properties.
 target_platforms = []
 foreach(platform, test_platforms) {
-  if (platform.cpu == current_cpu) {
+  if (!defined(platform.cpu) || platform.cpu == current_cpu) {
     target_platforms += [ platform ]
   }
 }
diff --git a/package.gni b/package.gni
index 752131c..837b37e 100644
--- a/package.gni
+++ b/package.gni
@@ -494,7 +494,8 @@
           source = rebase_path(test.name, "", root_out_dir)
 
           if (!is_disabled) {
-            test_spec(test.name) {
+            test_spec("${test.name}.spec") {
+              name = test.name
               if (pkg.deprecated_system_image) {
                 location = "/system/$dest"
               } else {
@@ -507,41 +508,7 @@
               # included in the build.
               output_dir = "$target_out_dir/$pkg_target_name"
 
-              if (defined(test.environments)) {
-                envs_specified = true
-                environments = test.environments
-              } else {
-                envs_specified = false
-                environments = [
-                  {
-                    dimensions = {
-                      device_type = "QEMU"
-                    }
-                  },
-                ]
-              }
-
-              if (current_cpu == "x64" && !envs_specified) {
-                # TODO(joshuaseaton): This is temporary until we are confident that all
-                # tests that need to specify hardware are doing so.
-                environments += [
-                  {
-                    dimensions = {
-                      device_type = "Intel NUC Kit NUC7i5DNHE"
-                    }
-                  },
-                ]
-              } else if (current_cpu == "arm64" && !envs_specified) {
-                # TODO(IN-571): Remove after vim2s are ready to be targeted.
-                environments += [
-                  {
-                    dimensions = {
-                      device_type = "Khadas Vim2 Max"
-                    }
-                    label = "vim2"
-                  },
-                ]
-              }
+              forward_variables_from(test, [ "environments" ])
             }
           }
         },
diff --git a/test.gni b/test.gni
index 40c789f..08da507 100644
--- a/test.gni
+++ b/test.gni
@@ -2,10 +2,27 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import("//build/testing/test_spec.gni")
+
+# This declares a test executable.
+#
+# The parameters are precisely those of an `executable`.
+#
 template("test") {
+  if (is_linux || is_mac) {
+    test_spec("${target_name}_spec") {
+      name = invoker.target_name
+      location = "$root_out_dir/$target_name"
+    }
+  }
+
   executable(target_name) {
     forward_variables_from(invoker, "*")
     testonly = true
+    if (is_linux || is_mac) {
+      test_name = get_path_info(target_name, "name")
+      write_runtime_deps = "$target_out_dir/$test_name.$test_data_ext"
+    }
   }
 }
 
diff --git a/testing/platforms.gni b/testing/platforms.gni
index 5d62fe8..f6935d2 100644
--- a/testing/platforms.gni
+++ b/testing/platforms.gni
@@ -20,6 +20,7 @@
 all_dimension_keys = [
   "device_type",
   "cpu",
+  "os",
 ]
 
 # Scopes of dimensions for every available platform.
@@ -37,9 +38,17 @@
     cpu = "x64"
   },
 
-  # Experimental. May not yet be targeted. See IN-571.
+  # The platforms below are experimental and may not yet be targeted.
+  # See IN-571.
   {
     device_type = "Khadas Vim2 Max"
     cpu = "arm64"
   },
+  # See IN-819.
+  {
+    os = "Linux"
+  },
+  {
+    os = "Mac"
+  },
 ]
diff --git a/testing/test_spec.gni b/testing/test_spec.gni
index 1891032..f012adc 100644
--- a/testing/test_spec.gni
+++ b/testing/test_spec.gni
@@ -7,8 +7,8 @@
 # Extension identifying a test spec JSON file.
 _test_spec_ext = "spec.json"
 
-# TODO(joshuaseaton): Only used in package() today; consider a scheme in which
-#   test specs are re-used for host tests.
+# Extension identifying the runtime dependencies of a test.
+test_data_ext = "spec.data"
 
 # Describes the target device environment in which a test should run. This
 # specification is written in JSON to the build directory, to be aggregated
@@ -16,14 +16,17 @@
 #
 # Parameters
 #
+#   name (required)
+#     [string] The test's name.
+#
 #   location (required)
 #     [string]: Unique reference to a test, e.g., a filesystem path or a
 #     fuchsia URI.
 #
-#   output_dir (required)
+#   output_dir (required for fuchsia; else is optional)
 #     [string]: Directory where the test spec will be written.
 #
-#   environments (required)
+#   environments (optional, default: QEMU for fuchsia; else a VM)
 #     [list of scopes] Device environments in which the test should run.
 #
 #     Each scope in $environments contains:
@@ -40,12 +43,87 @@
 #         default testing pipeline for special tests or environments.
 #
 template("test_spec") {
-  assert(defined(invoker.location), "location must be defined.")
-  assert(defined(invoker.output_dir), "output_dir must be defined.")
-  assert(defined(invoker.environments) && invoker.environments != [],
-         "environments must be defined.")
+  # A canonical target name is the name of the test itself; however that name
+  # must be reserved for the actual test target in the expansion of other
+  # templates that give both a test and its spec. With that in mind,
+  # `test_name` is provided as a parameter, and the choice of an alternative
+  # target name does not ultimately matter.
+  not_needed([ "target_name" ])
 
-  foreach(env, invoker.environments) {
+  assert(defined(invoker.name), "name must be defined.")
+  assert(defined(invoker.location), "location must be defined.")
+  forward_variables_from(invoker,
+                         [
+                           "environments",
+                           "output_dir",
+                         ])
+
+  # Set default environments: QEMU for a fuchsia test, and VMs for linux and mac
+  # tests.
+  if (is_fuchsia) {
+    assert(defined(invoker.output_dir), "output_dir must be defined.")
+    envs_specified = defined(environments)
+
+    if (!envs_specified) {
+      environments = [
+        {
+          dimensions = {
+            device_type = "QEMU"
+          }
+        },
+      ]
+    }
+
+    if (current_cpu == "x64" && !envs_specified) {
+      # TODO(joshuaseaton): This is temporary until we are confident that all
+      # tests that need to specify hardware are doing so.
+      environments += [
+        {
+          dimensions = {
+            device_type = "Intel NUC Kit NUC7i5DNHE"
+          }
+        },
+      ]
+    } else if (current_cpu == "arm64" && !envs_specified) {
+      # TODO(IN-571): Remove after vim2s are ready to be targeted.
+      environments += [
+        {
+          dimensions = {
+            device_type = "Khadas Vim2 Max"
+          }
+          label = "vim2"
+        },
+      ]
+    }
+  } else if (is_linux || is_mac) {
+    if (defined(output_dir)) {
+      assert(output_dir == target_out_dir,
+             "tests specs must be written to target_out_dir")
+    } else {
+      output_dir = target_out_dir
+    }
+
+    if (!defined(environments)) {
+      environments = [
+        {
+          dimensions = {
+            if (is_linux) {
+              os = "Linux"  # Used to target a Linux VM.
+            } else if (is_mac) {
+              os = "Mac"  # Used to target a Mac VM.
+            }
+          }
+
+          # TODO(IN-819) Use this label only for bring-up; delete afterward.
+          label = "host"
+        },
+      ]
+    }
+  } else {
+    assert(false, "$current_os not supported")
+  }
+
+  foreach(env, environments) {
     empty_scope = {  # Clear from previous iteration
     }
     assert(defined(env.dimensions) && env.dimensions != empty_scope,
@@ -76,7 +154,15 @@
       ]
     }
 
-    if (platform.cpu == current_cpu) {
+    # Empty scopes may have been introduced to platform_dims, corresponding to
+    # non-existent keys;
+    # Add and then subtract an empty scope to remove them.
+    empty_dim = {  # Clear from previous iteration.
+    }
+    platform_dims += [ empty_dim ]
+    platform_dims -= [ empty_dim ]
+
+    if (!defined(platform.cpu) || platform.cpu == current_cpu) {
       target_platform_dims += [ platform_dims ]
     } else {
       other_platform_dims += [ platform_dims ]
@@ -84,7 +170,7 @@
   }
 
   target_envs = []
-  foreach(env, invoker.environments) {
+  foreach(env, environments) {
     dims = []  # Clear from previous iteration.
     if (defined(env.dimensions)) {
       foreach(key, all_dimension_keys) {
@@ -95,9 +181,6 @@
         ]
       }
     }
-
-    # Empty scopes may have been introduced to dims, corresponding to unset keys;
-    # Add and then subtract an empty scope to remove them.
     empty_dim = {  # Clear from previous iteration.
     }
     dims += [ empty_dim ]
@@ -133,28 +216,35 @@
 
   test_spec = {
     test = {
-      name = get_label_info(":$target_name", "label_no_toolchain")
+      name = get_label_info(":${invoker.name}", "label_no_toolchain")
       location = invoker.location
       os = current_os
       cpu = current_cpu
     }
+    environments = []  # Clear from above.
     environments = target_envs
   }
 
-  # TODO(IN-571): Delete this block once vim2s are ready to be targeted.
   foreach(env, target_envs) {
     dims = []  # Clear from previous iteration.
     dims = env.dimensions
+
+    # TODO(IN-571): Delete this block once vim2s are ready to be targeted.
     if (defined(dims.device_type) && dims.device_type == "Khadas Vim2 Max") {
       assert(defined(env.label) && env.label == "vim2",
              "vim2s may not yet be targeted.")
     }
+
+    # TODO(IN-819): Delete this block once Linux VMs and Macs are ready to be
+    # targeted.
+    if (defined(dims.os)) {
+      assert(defined(env.label) && env.label == "host",
+             "Linux VMs or Macs may not yet be targeted for tests.")
+    }
   }
 
   # We take the basename just to make sure no other path components are given
   # in the name, for which we have no guarantees.
-  target_base_name = get_path_info(target_name, "name")
-  write_file("${invoker.output_dir}/${target_base_name}.${_test_spec_ext}",
-             test_spec,
-             "json")
+  test_name = get_path_info(invoker.name, "name")
+  write_file("${output_dir}/${test_name}.${_test_spec_ext}", test_spec, "json")
 }