chromium/mojo/public/tools/fuzzers/mojolpm.gni

# Copyright 2020 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import("//mojo/public/tools/bindings/mojom.gni")
import("//testing/libfuzzer/fuzzer_test.gni")
import("//third_party/jinja2/jinja2.gni")
import("//third_party/protobuf/proto_library.gni")

# Generate a MojoLPM-based fuzzer test.
#
# This rule will copy the proto file defining the fuzzer testcases into the
# output directory so that it can be compiled against the generated MojoLPM
# protos. It then adds a rule to compile that proto, and finally a fuzzer
# test target which uses the compiled proto.
#
# Optionally it can also handle converting a seed corpus of text protos into
# a binary corpus as part of the build.
#
# Parameters:
#   sources
#       List of source .cc files to compile.
#
#   deps
#       List of dependencies to compile this target.
#
#   proto_source
#       Single source .proto file defining the structure of a testcase.
#
#   proto_deps
#       List of additional dependencies for compiling proto_source.
#
#   proto_in_dir
#       See documentation at //third_party/protobuf/proto_library.gni.
#
#   exclude_main
#       See documentation at //testing/libfuzzer/fuzzer_test.gni.
#
#   proto_out_dir (optional)
#       See documentation at //third_party/protobuf/proto_library.gni.
#
#   testcase_proto_kind (optional, required if seed_corpus_sources provided)
#       Name of proto message type representing a testcase.
#
#   seed_corpus_sources (optional)
#       List of source .textproto files used to build a seed corpus.
#
# Example:
#  mojolpm_fuzzer_test("foo_mojolpm_fuzzer") {
#    sources = [ "foo_mojolpm_fuzzer.cc" ]
#
#    deps = [
#      "//content/browser/foo:foo_mojolpm_fuzzer_proto",
#      "//content/browser:for_content_tests",
#      "//content/public/browser:browser_sources",
#      "//content/test:test_support",
#      "//mojo/core/embedder",
#      "//mojo/public/tools/fuzzers:mojolpm",
#      "//third_party/libprotobuf-mutator",
#    ]
#
#    proto_deps = [
#      "//content/browser/bar/mojom:mojom_mojolpm","
#    ]
#
#    testcase_proto = "foo_mojolpm_fuzzer.proto"
#    testcase_proto_kind = "foo.mojolpm.proto.Testcase"
#
#    seed_corpus_sources = [
#      "foo_mojolpm_fuzzer_corpus/seed_one.textproto",
#      "foo_mojolpm_fuzzer_corpus/seed_two.textproto",
#    ]
#  }
template("mojolpm_fuzzer_test") {
  assert(defined(invoker.sources) && defined(invoker.proto_source),
         "\"sources\" and \"proto_source\" must be defined for $target_name")

  assert(
      !defined(invoker.seed_corpus_sources) ||
          defined(invoker.testcase_proto_kind),
      "\"testcase_proto_kind\" must be defined for $target_name since \"seed_corpus_sources\" is defined.")

  if (enable_mojom_fuzzer) {
    proto_target_name = "${target_name}_proto"

    proto_library(proto_target_name) {
      # Work relative to src (//) instead of (by default) the BUILD file.
      if (defined(invoker.proto_in_dir)) {
        proto_in_dir = invoker.proto_in_dir
      } else {
        proto_in_dir = "//"
      }
      if (defined(invoker.proto_out_dir)) {
        proto_out_dir = invoker.proto_out_dir
      }

      sources = [ invoker.proto_source ]
      generate_python = false

      proto_deps = []

      import_dirs = [
        root_gen_dir,
        "//",
      ]

      link_deps = []

      if (defined(invoker.proto_deps)) {
        proto_deps += invoker.proto_deps
        link_deps += invoker.proto_deps
      }

      testonly = true
    }

    if (defined(invoker.seed_corpus_sources)) {
      protoc_convert_target_name = "${target_name}_protoc_convert"
      seed_corpus_path = "${target_gen_dir}/${target_name}_seed_corpus"

      protoc_convert(protoc_convert_target_name) {
        sources = invoker.seed_corpus_sources

        inputs = [ invoker.proto_source ]

        output_pattern = "${seed_corpus_path}/{{source_name_part}}.binarypb"

        args = [
          "--encode=${invoker.testcase_proto_kind}",
          "-I",
          rebase_path(root_gen_dir),
          "-I",
          rebase_path("//"),
          rebase_path(inputs[0]),
        ]

        deps = []

        if (defined(invoker.proto_deps)) {
          deps += invoker.proto_deps
        }

        testonly = true
      }
    }

    fuzzer_test(target_name) {
      forward_variables_from(invoker,
                             "*",
                             [
                               "sources",
                               "deps",
                               "seed_corpus_sources",
                               "exclude_main",
                             ])
      sources = invoker.sources
      deps = [
        ":${proto_target_name}",
        "//mojo/core/embedder",
        "//mojo/public/tools/fuzzers:mojolpm",
        "//third_party/libprotobuf-mutator",
      ]
      if (defined(invoker.deps)) {
        deps += invoker.deps
      }

      if (defined(invoker.seed_corpus_sources)) {
        seed_corpus = seed_corpus_path
        seed_corpus_deps = [ ":${protoc_convert_target_name}" ]
      }
      if (defined(invoker.exclude_main)) {
        exclude_main = invoker.exclude_main
      }
    }
  } else {
    not_needed(invoker, "*")
  }
}

# Generate a MojoLPM-based fuzzer test.
#
# This rule will copy the proto file defining the fuzzer testcases into the
# output directory so that it can be compiled against the generated MojoLPM
# protos. It then adds a rule to compile that proto, and finally a fuzzer
# test target which uses the compiled proto.
#
# Optionally it can also handle converting a seed corpus of text protos into
# a binary corpus as part of the build.
#
# Parameters:
#   sources
#       List of source .cc files to compile.
#
#   interfaces
#       List of [interface name, mojom source file, remote type] 3-element
#       lists.
#       The remote type must be either "Remote" or "AssociatedRemote".
#
#   interface_file
#       JSON file containing the list of interfaces to generate.
#       Format:
#       {
#         "interfaces": {
#           ["InterfaceName", "//path/to/interface.mojom", "Remote"],
#           ["AnotherInterface", "//path/to/another.mojom", "AssociatedRemote" ],
#            ...
#         }
#       }
#
#   deps
#       List of dependencies to compile this target.
#
#   proto_deps
#       List of additional dependencies for compiling proto_source.
#
#   seed_corpus_sources (optional)
#       List of source .textproto files used to build a seed corpus.
#
#   ensure_remote (optional)
#       Ensures that for ever listed remotes, the "new" action is called before
#       any other actions related to this remote. This ensures the remote
#       always exists.
#
# Example:
#  mojolpm_generated_fuzzer("foo_mojolpm_fuzzer") {
#    sources = [ "foo_mojolpm_fuzzer.cc" ]
#
#    interfaces = [
#      ["InterfaceName", "//path/to/interface.mojom", "Remote"],
#      ["AnotherInterface", "//path/to/another.mojom", "AssociatedRemote" ],
#    ]
#
#    deps = [
#      "//content/browser/foo:foo_mojolpm_fuzzer_proto",
#      "//content/browser:for_content_tests",
#      "//content/public/browser:browser_sources",
#      "//content/test:test_support",
#      "//mojo/core/embedder",
#      "//mojo/public/tools/fuzzers:mojolpm",
#      "//third_party/libprotobuf-mutator",
#    ]
#
#    proto_deps = [
#      "//content/browser/bar/mojom:mojom_mojolpm","
#    ]
#
#    seed_corpus_sources = [
#      "foo_mojolpm_fuzzer_corpus/seed_one.textproto",
#      "foo_mojolpm_fuzzer_corpus/seed_two.textproto",
#    ]
#  }
template("mojolpm_generated_fuzzer") {
  assert(defined(invoker.sources),
         "\"sources\" must be defined for $target_name")
  assert(
      defined(invoker.interfaces) || defined(invoker.interface_file),
      "\"interfaces\" or \"interface_file\" must be defined for $target_name")

  if (enable_mojom_fuzzer) {
    _target_name = target_name
    _generate_target_name = _target_name + "_mojolpm_generator_generate"

    if (defined(invoker.interfaces)) {
      # Generates the correct argument format give invoker.interfaces.
      _script_inputs = []
      foreach(elt, invoker.interfaces) {
        _script_inputs +=
            [ rebase_path("$root_gen_dir/" + elt[0] + "-module",
                          root_build_dir) + ":" + elt[1] + ":" + elt[2] ]
      }
    }

    action(_generate_target_name) {
      testonly = true
      script = "//mojo/public/tools/fuzzers/mojolpm_generator.py"
      inputs = jinja2_sources

      deps = []
      if (defined(invoker.proto_deps)) {
        deps += invoker.proto_deps
      }

      args = []
      if (defined(invoker.interfaces)) {
        args += [ "--input" ]
        args += _script_inputs
      } else if (defined(invoker.interface_file)) {
        args += [
          "-f",
          rebase_path(invoker.interface_file, root_build_dir),
        ]
      }
      args += [
        "--output_file_format",
        rebase_path("${target_gen_dir}/${_target_name}", root_build_dir),
      ]

      if (defined(invoker.ensure_remote) && invoker.ensure_remote) {
        args += [ "--ensure-remote" ]
      }

      outputs = [
        "${target_gen_dir}/${_target_name}.h",
        "${target_gen_dir}/${_target_name}.proto",
      ]
    }

    mojolpm_fuzzer_test(target_name) {
      proto_deps = [ ":${_generate_target_name}" ]
      deps = [ ":${_generate_target_name}" ]
      proto_source = "${target_gen_dir}/${_target_name}.proto"
      proto_in_dir = "${root_gen_dir}"
      proto_out_dir = "."

      if (defined(invoker.proto_deps)) {
        proto_deps += invoker.proto_deps
      }

      if (defined(invoker.deps)) {
        deps += invoker.deps
      }

      if (defined(invoker.seed_corpus_sources)) {
        testcase_proto_kind = "mojolpmgenerator.${_target_name}.Testcase"
      }

      forward_variables_from(invoker,
                             "*",
                             [
                               "deps",
                               "proto_deps",
                               "proto_source",
                             ])
    }
  } else {
    not_needed("*")
    not_needed(invoker, "*")
  }
}