# Part of the Carbon Language project, under the Apache License v2.0 with LLVM
# Exceptions. See /LICENSE for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

"""Rules for building fuzz tests."""

load("//bazel/cc_rules:defs.bzl", "cc_test")

def _cc_fuzz_test(corpus, args, data, **kwargs):
    """Generates a single test target.

    Append the corpus files to the test arguments. When run on a list of
    files rather than a directory, libFuzzer-based fuzzers will perform a
    regression test against the corpus.
    """
    cc_test(
        data = data + corpus,
        args = args + ["$(location %s)" % file for file in corpus],
        **kwargs
    )

def cc_fuzz_test(
        name,
        corpus,
        args = [],
        data = [],
        features = [],
        tags = [],
        deps = [],
        shard_count = 1,
        **kwargs):
    """Macro for C++ fuzzing test.

    In order to run tests on a single file, run the fuzzer binary under
    bazel-bin directly. That will avoid the args being passed by Bazel.

    Args:
        name: The main fuzz test rule name.
        corpus: List of files to use as a fuzzing corpus.
        args: Will have the locations of the corpus files added and passed down
            to the fuzz test.
        data: Will have the corpus added and passed down to the fuzz test.
        features: Will have the "fuzzer" feature added and passed down to the
            fuzz test.
        tags: Will have "fuzz_test" added and passed down to the fuzz test.
        deps: Will have "@llvm-project//compiler-rt:FuzzerMain" added and passed
            down to the fuzz test.
        shard_count: Provides sharding of the fuzz test.
        **kwargs: Remaining arguments passed down to the fuzz test.
    """

    # Add relevant tag and feature if necessary.
    if "fuzz_test" not in tags:
        tags = tags + ["fuzz_test"]
    if "fuzzer" not in features:
        features = features + ["fuzzer"]
    if "@llvm-project//compiler-rt:FuzzerMain" not in deps:
        deps = deps + ["@llvm-project//compiler-rt:FuzzerMain"]

    # The FuzzerMain library doesn't support sharding based on inputs, so we
    # general separate test targets in order to shard execution.
    if shard_count == 1:
        # When there's one shard, only one target is needed.
        _cc_fuzz_test(
            corpus,
            args,
            data,
            name = name,
            features = features,
            tags = tags,
            deps = deps,
            **kwargs
        )
    else:
        # Calculate the number of inputs per shard. This is equivalent to
        # ceiling division, so that the corpus subsetting doesn't miss odd
        # files.
        shard_size = len(corpus) // shard_count
        if shard_count * shard_size < len(corpus):
            shard_size += 1

        # Create separate targets for each shard.
        shards = []
        for shard in range(shard_count):
            shard_name = "{0}.shard{1}".format(name, shard)
            shards.append(shard_name)

            _cc_fuzz_test(
                corpus[shard * shard_size:(shard + 1) * shard_size],
                args,
                data,
                name = shard_name,
                features = features,
                tags = tags,
                deps = deps,
                **kwargs
            )

        # Create a suite containing all shards.
        native.test_suite(
            name = name,
            tests = shards,
        )

        # Create one target that includes the full corpus.
        _cc_fuzz_test(
            corpus,
            args,
            data,
            name = "{0}.full_corpus".format(name),
            features = features,
            tags = tags + ["manual"],
            deps = deps,
            **kwargs
        )
