[build][fuzzing] Generate unit tests for fuzzers

This CL adds automatically generated fuzzer tests that can be included
by adding a `fuzzer_tests_package` with the same fuzzers as the
`fuzzers_package`.

Bug: SEC-224
Test: fx run-test <fuzzer_tests_package>
Change-Id: I15729cec8e3320b5c94777befab234ff43861438
diff --git a/build/fuzzing/fuzzer.gni b/build/fuzzing/fuzzer.gni
index f00e497..51f37926 100644
--- a/build/fuzzing/fuzzer.gni
+++ b/build/fuzzing/fuzzer.gni
@@ -4,6 +4,7 @@
 
 import("//build/host.gni")
 import("//build/package.gni")
+import("//build/test.gni")
 
 # TODO(aarongreen): SEC-224.  Add tests to catch fuzzer building/packaging
 # regressions.
@@ -63,6 +64,41 @@
     }
   }
 
+  # Generate a unit test for the fuzzer.
+  test("${fuzzer_name}_test") {
+    deps = []
+    forward_variables_from(invoker,
+                           "*",
+                           [
+                             "dictionary",
+                             "options",
+                             "target_name",
+                             "visibility",
+                           ])
+
+    # Explicitly forward visibility for nested scopes.
+    forward_variables_from(invoker, [ "visibility" ])
+    deps += [
+      "//src/fuzzing/cpp:fuzzer_test",
+      "//third_party/googletest:gtest_main",
+    ]
+  }
+
+  # Generate the fuzzer test component manifest
+  action("${fuzzer_name}_test_cmx") {
+    script = "//build/fuzzing/gen_fuzzer_manifest.py"
+    outputs = [
+      "${target_gen_dir}/${fuzzer_name}_test.cmx",
+    ]
+    args = [
+      "--test",
+      "--out",
+      rebase_path(outputs[0]),
+      "--bin",
+      "${fuzzer_name}_test",
+    ]
+  }
+
   # Generate data files needed at runtime
   output_dictionary = "${target_gen_dir}/${fuzzer_name}/dictionary"
   if (defined(invoker.dictionary)) {
@@ -117,6 +153,9 @@
 #     [boolean] Indicates whether to also build fuzzer binaries on host.
 #     Defaults to false.
 #
+#   generated_test_package (optional)
+#     [string] The name of a package of fuzzer tests to create.
+#
 #   meta (optional)
 #   binaries (optional)
 #   components (optional)
@@ -156,9 +195,12 @@
     binaries = []
     resources = []
     deps = []
+    fuzzers = []
     host_deps = []
     host_outputs = []
-    fuzzers = []
+    tests = []
+    test_meta = []
+    test_deps = []
   }
 
   # Collect the selected fuzzers listed in this package based on the variants
@@ -166,6 +208,9 @@
   # sanitizers.
   foreach(fuzzer, invoker.fuzzers) {
     selected = false
+    fuzzer_name = get_label_info(fuzzer, "name")
+    fuzzer_path = get_label_info(fuzzer, "target_gen_dir")
+    fuzzer_label = get_label_info(fuzzer, "label_no_toolchain")
     foreach(sanitizer, sanitizers) {
       foreach(selector, select_variant_canonical) {
         if (!selected && selector.variant == "${sanitizer}-fuzzer" &&
@@ -175,9 +220,8 @@
              (defined(selector.output_name) &&
               selector.output_name == [ fuzzer ]))) {
           selected = true
-          fuzzer_name = get_label_info(fuzzer, "name")
-          fuzzer_path = get_label_info(fuzzer, "target_gen_dir")
-          fuzzer_label = get_label_info(fuzzer, "label_no_toolchain")
+
+          # Package details.
           pkg.meta += [
             {
               path = "${fuzzer_path}/${fuzzer_name}.cmx"
@@ -205,15 +249,34 @@
             "${fuzzer_label}_dictionary",
             "${fuzzer_label}_options",
           ]
+          pkg.fuzzers += [ fuzzer_name ]
+
+          # Host fuzzers
           if (fuzz_host) {
             pkg.deps += [ ":host_${target_name}" ]
             pkg.host_deps += [ fuzzer_label ]
             pkg.host_outputs += [ fuzzer_name ]
           }
-          pkg.fuzzers += [ fuzzer_name ]
         }
       }
     }
+
+    # Fuzzer tests
+    pkg.tests += [
+      {
+        name = "${fuzzer_name}_test"
+      },
+    ]
+    pkg.test_meta += [
+      {
+        path = "${fuzzer_path}/${fuzzer_name}_test.cmx"
+        dest = "${fuzzer_name}_test.cmx"
+      },
+    ]
+    pkg.test_deps += [
+      "${fuzzer_label}_test",
+      "${fuzzer_label}_test_cmx",
+    ]
     not_needed([ "selected" ])
   }
 
@@ -228,6 +291,7 @@
                              "*",
                              [
                                "fuzzers",
+                               "generated_test_package",
                                "metadata",
                                "sanitizers",
                                "visibility",
@@ -277,6 +341,19 @@
                  "fuzz_host",
                ])
   }
+
+  # Assemble the Fuchsia test package. This uses `package` instead of
+  # `test_package`, as the latter only provides an inconvenient constraint on
+  # where the cmx file must come from.
+  if (defined(invoker.generated_test_package)) {
+    package(invoker.generated_test_package) {
+      forward_variables_from(invoker, [ "visibility" ])
+      testonly = true
+      meta = pkg.test_meta
+      tests = pkg.tests
+      deps = pkg.test_deps
+    }
+  }
 }
 
 # TODO(aarongreen): Complete soft transition and remove.
diff --git a/build/fuzzing/gen_fuzzer_manifest.py b/build/fuzzing/gen_fuzzer_manifest.py
index b9d45bc..165afe4 100755
--- a/build/fuzzing/gen_fuzzer_manifest.py
+++ b/build/fuzzing/gen_fuzzer_manifest.py
@@ -17,6 +17,10 @@
       help="Path to the binary; absolute or relative to package's bin directory",
       required=True)
   parser.add_argument("--cmx", help="Optional starting manifest")
+  parser.add_argument(
+      "--test",
+      action="store_true",
+      help="Generate manifest for the fuzzer test package.")
   args = parser.parse_args()
 
   cmx = defaultdict(dict)
@@ -24,21 +28,18 @@
     with open(args.cmx, "r") as f:
       cmx = json.load(f)
 
-  if args.bin.startswith("/"):
-    # Zircon fuzz_targets are absolute paths in bootfs.
-    cmx["program"]["binary"] = args.bin
+  if args.test:
+    cmx["program"]["binary"] = "test/" + args.bin
   else:
-    # Fuchsia fuzz_targets are part of a package
     cmx["program"]["binary"] = "bin/" + args.bin
+    if "services" not in cmx["sandbox"]:
+      cmx["sandbox"]["services"] = []
+    cmx["sandbox"]["services"].append("fuchsia.process.Launcher")
 
-  if "services" not in cmx["sandbox"]:
-    cmx["sandbox"]["services"] = []
-  cmx["sandbox"]["services"].append("fuchsia.process.Launcher")
-
-  if "features" not in cmx["sandbox"]:
-    cmx["sandbox"]["features"] = []
-  if "isolated-persistent-storage" not in cmx["sandbox"]["features"]:
-    cmx["sandbox"]["features"].append("isolated-persistent-storage")
+    if "features" not in cmx["sandbox"]:
+      cmx["sandbox"]["features"] = []
+    if "isolated-persistent-storage" not in cmx["sandbox"]["features"]:
+      cmx["sandbox"]["features"].append("isolated-persistent-storage")
 
   with open(args.out, "w") as f:
     f.write(json.dumps(cmx, sort_keys=True, indent=4))
diff --git a/src/connectivity/BUILD.gn b/src/connectivity/BUILD.gn
index 3914575..9dcd505 100644
--- a/src/connectivity/BUILD.gn
+++ b/src/connectivity/BUILD.gn
@@ -18,6 +18,7 @@
 
   data_deps = [
     "//src/connectivity/bluetooth/core/bt-gap:tests",
+    "//src/connectivity/bluetooth/core/bt-host:bluetooth_fuzzers_test",
     "//src/connectivity/bluetooth/lib/bt-avdtp:tests",
     "//src/connectivity/bluetooth/lib/fuchsia-bluetooth:tests",
     "//src/connectivity/bluetooth/profiles/bt-a2dp-sink:tests",
@@ -27,6 +28,7 @@
     "//src/connectivity/network/testing/netemul:tests",
     "//src/connectivity/overnet/examples:tests",
     "//src/connectivity/overnet/lib:overnet_tests",
+    "//src/connectivity/overnet/lib:overnet_fuzzer_tests",
     "//src/connectivity/overnet/overnetstack:overnetstack_tests",
     "//src/connectivity/telephony/lib/qmi-protocol:tests",
     "//src/connectivity/wlan:tests",
diff --git a/src/connectivity/bluetooth/core/bt-host/BUILD.gn b/src/connectivity/bluetooth/core/bt-host/BUILD.gn
index fe75acb..703d419 100644
--- a/src/connectivity/bluetooth/core/bt-host/BUILD.gn
+++ b/src/connectivity/bluetooth/core/bt-host/BUILD.gn
@@ -124,4 +124,5 @@
     "l2cap:basic_mode_rx_engine_fuzzer",
     "l2cap:enhanced_retransmission_mode_rx_engine_fuzzer",
   ]
+  generated_test_package = "bluetooth_fuzzers_test"
 }
diff --git a/src/connectivity/overnet/lib/BUILD.gn b/src/connectivity/overnet/lib/BUILD.gn
index 8ce1c90..e645b12 100644
--- a/src/connectivity/overnet/lib/BUILD.gn
+++ b/src/connectivity/overnet/lib/BUILD.gn
@@ -71,4 +71,5 @@
   ]
   sanitizers = [ "asan" ]
   fuzz_host = true
+  generated_test_package = "overnet_fuzzer_tests"
 }
diff --git a/src/fuzzing/BUILD.gn b/src/fuzzing/BUILD.gn
index bdd572d..4207432 100644
--- a/src/fuzzing/BUILD.gn
+++ b/src/fuzzing/BUILD.gn
@@ -8,17 +8,6 @@
 group("tests") {
   testonly = true
   public_deps = [
-    ":fuzzing_host_test($host_toolchain)",
-  ]
-}
-
-go_library("fuzzing_host_test_lib") {
-  name = "fuzzing"
-}
-
-go_test("fuzzing_host_test") {
-  gopackage = "fuzzing"
-  deps = [
-    ":fuzzing_host_test_lib",
+    "host:tests",
   ]
 }
diff --git a/src/fuzzing/cpp/BUILD.gn b/src/fuzzing/cpp/BUILD.gn
new file mode 100644
index 0000000..b839040
--- /dev/null
+++ b/src/fuzzing/cpp/BUILD.gn
@@ -0,0 +1,14 @@
+# Copyright 2019 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+source_set("fuzzer_test") {
+  testonly = true
+  sources = [
+    "fuzzer_test.cc",
+  ]
+  deps = [
+    "//src/lib/files",
+    "//third_party/googletest:gtest",
+  ]
+}
diff --git a/src/fuzzing/cpp/fuzzer_test.cc b/src/fuzzing/cpp/fuzzer_test.cc
new file mode 100644
index 0000000..3b4b8c7
--- /dev/null
+++ b/src/fuzzing/cpp/fuzzer_test.cc
@@ -0,0 +1,35 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "src/lib/files/directory.h"
+#include "src/lib/files/file.h"
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
+
+TEST(FuzzerTest, EmptyInput) {
+  EXPECT_EQ(0, LLVMFuzzerTestOneInput(nullptr, 0));
+}
+
+// TODO(aarongreen): Placeholder for now, until we figure out how we want to
+// plumb the corpora from CIPD through to images built for test in CQ.
+const char *kCorpusDir = "data/corpus";
+TEST(FuzzerTest, WithCorpus) {
+  if (!files::IsDirectory(kCorpusDir)) {
+    return;
+  }
+  std::vector<std::string> inputs;
+  std::vector<uint8_t> data;
+  ASSERT_TRUE(files::ReadDirContents(kCorpusDir, &inputs));
+  for (auto input : inputs) {
+    ASSERT_TRUE(files::ReadFileToVector(input, &data));
+    EXPECT_EQ(0, LLVMFuzzerTestOneInput(&data[0], data.size()));
+  }
+}
diff --git a/src/fuzzing/host/BUILD.gn b/src/fuzzing/host/BUILD.gn
new file mode 100644
index 0000000..e2c7b80
--- /dev/null
+++ b/src/fuzzing/host/BUILD.gn
@@ -0,0 +1,24 @@
+# Copyright 2019 The Fuchsia 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("//build/go/go_library.gni")
+import("//build/go/go_test.gni")
+
+group("tests") {
+  testonly = true
+  public_deps = [
+    ":host_fuzzers_test($host_toolchain)",
+  ]
+}
+
+go_library("host_fuzzers_test_lib") {
+  name = "fuzzing"
+}
+
+go_test("host_fuzzers_test") {
+  gopackage = "fuzzing"
+  deps = [
+    ":host_fuzzers_test_lib",
+  ]
+}
diff --git a/src/fuzzing/host_test.go b/src/fuzzing/host/host_test.go
similarity index 100%
rename from src/fuzzing/host_test.go
rename to src/fuzzing/host/host_test.go
diff --git a/src/ledger/BUILD.gn b/src/ledger/BUILD.gn
index 0962b0e..eaf54bc 100644
--- a/src/ledger/BUILD.gn
+++ b/src/ledger/BUILD.gn
@@ -22,9 +22,9 @@
 
 group("tests") {
   testonly = true
-
-  data_deps = [
+  deps = [
     ":ledger_tests",
+    "bin:ledger_fuzzer_tests",
   ]
 }
 
diff --git a/src/ledger/bin/BUILD.gn b/src/ledger/bin/BUILD.gn
index 2f0a696..6361e1c 100644
--- a/src/ledger/bin/BUILD.gn
+++ b/src/ledger/bin/BUILD.gn
@@ -59,4 +59,5 @@
     "//src/ledger/bin/p2p_sync/impl:p2p_sync_fuzzer",
     "//src/ledger/bin/storage/impl/btree:encoding_fuzzer",
   ]
+  generated_test_package = "ledger_fuzzer_tests"
 }