#!/usr/bin/env python3

import argparse
import os
import re
import shlex
import sys
from subprocess import run, SubprocessError, DEVNULL, PIPE
from tempfile import NamedTemporaryFile

DESC = """

A `csmith` fuzzing driver for `bindgen`.

Generates random C source files with `csmith` and then passes them to `bindgen`
(via `predicate.py`). If `bindgen` can't emit bindings, `rustc` can't compile
those bindings, or the compiled bindings' layout tests fail, then the driver has
found a bug, and will report the problematic test case to you.

"""

parser = argparse.ArgumentParser(
    formatter_class=argparse.RawDescriptionHelpFormatter,
    description=DESC.strip())

parser.add_argument(
    "--keep-going",
    action="store_true",
    help="Do not stop after finding a test case that exhibits a bug in `bindgen`. Instead, keep going.")

CSMITH_ARGS="\
--no-checksum \
--nomain \
--max-block-size 1 \
--max-block-depth 1"

parser.add_argument(
    "--csmith-args",
    type=str,
    default=CSMITH_ARGS,
    help="Pass this argument string to `csmith`. By default, very small functions are generated.")

BINDGEN_ARGS = "--with-derive-partialeq \
--with-derive-eq \
--with-derive-partialord \
--with-derive-ord \
--with-derive-hash \
--with-derive-default"

parser.add_argument(
    "--bindgen-args",
    type=str,
    default=BINDGEN_ARGS,
    help="Pass this argument string to `bindgen`. By default, all traits are derived.")

parser.add_argument(
    "--no-creduce",
    action="store_false",
    dest="creduce",
    help="Do not run `creduce` on any buggy test case(s) discovered.")

################################################################################

def cat(path, title=None):
    if not title:
        title = path
    print("-------------------- {} --------------------".format(title))
    print()
    print()
    run(["cat", path])

def decode(f):
    return f.decode(encoding="utf-8", errors="ignore")

def run_logged(cmd):
    result = run(cmd, stdin=DEVNULL, stdout=PIPE, stderr=PIPE)
    result.stdout = decode(result.stdout)
    result.stderr = decode(result.stderr)
    if result.returncode != 0:
        print()
        print()
        print("Error: {} exited with code {}".format(cmd, result.returncode))
        print()
        print()
        for line in result.stdout.splitlines():
            sys.stdout.write("+")
            sys.stdout.write(line)
            sys.stdout.write("\n")
        for line in result.stderr.splitlines():
            sys.stderr.write("+")
            sys.stderr.write(line)
            sys.stderr.write("\n")
    return result

def main():
    os.environ["RUST_BACKTRACE"] = "full"
    args = parser.parse_args()

    bindgen_args = args.bindgen_args
    if bindgen_args.find(" -- ") == -1:
        bindgen_args = bindgen_args + " -- "
    bindgen_args = bindgen_args + " -I{}".format(os.path.abspath(os.path.dirname(sys.argv[0])))
    args.bindgen_args = bindgen_args

    print()
    print()
    print("Fuzzing `bindgen` with C-Smith...")
    print()
    print()

    iterations = 0
    while True:
        print("\rIteration: {}".format(iterations), end="", flush=True)
        iterations += 1

        input = NamedTemporaryFile(delete=False, prefix="input-", suffix=".h")
        input.close()
        result = run_logged(["csmith", "-o", input.name] + shlex.split(args.csmith_args))
        if result.returncode != 0:
            exit(1)

        predicate_command = [
            "./predicate.py",
            "--bindgen-args",
            args.bindgen_args,
            input.name
        ]
        result = run_logged(predicate_command)

        if result.returncode != 0:
            print()
            print()
            cat(input.name, title="Failing test case: {}".format(input.name))
            print()
            print()

            if args.creduce:
                creduce(args, input.name, result)

            print_issue_template(args, input.name, predicate_command, result)

            if args.keep_going:
                continue
            exit(1)

        os.remove(input.name)

RUSTC_ERROR_REGEX = re.compile(r".*(error\[.*].*)")
LAYOUT_TEST_FAILURE = re.compile(r".*(test bindgen_test_layout_.* \.\.\. FAILED)")

def creduce(args, failing_test_case, result):
    print()
    print()
    print("Reducing failing test case with `creduce`...")

    match = re.search(RUSTC_ERROR_REGEX, result.stderr)
    if match:
        error_msg = match.group(1)
        print("...searching for \"{}\".".format(error_msg))
        return creduce_with_predicate_flags(
            args,
            failing_test_case,
            "--bindgen-args '{}' --expect-compile-fail --rustc-grep '{}'".format(
                args.bindgen_args,
                re.escape(error_msg)
            )
        )

    match = re.search(LAYOUT_TEST_FAILURE, result.stdout)
    if match:
        layout_failure = match.group(1)
        struct_name = layout_failure[len("test bindgen_test_layout_"):layout_failure.rindex(" ... FAILED")]
        print("...searching for \"{}\".".format(layout_failure))
        return creduce_with_predicate_flags(
            args,
            failing_test_case,
            "--bindgen-args '{}' --expect-layout-tests-fail --bindings-grep '{}' --layout-tests-grep '{}'".format(
                args.bindgen_args,
                re.escape(struct_name),
                re.escape(layout_failure)
            )
        )

    print("...nevermind, don't know how to `creduce` this bug. Skipping.")

def creduce_with_predicate_flags(args, failing_test_case, predicate_flags):
    predicate = """
#!/usr/bin/env bash
set -eu
{} {} {}
    """.format(
        os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "predicate.py")),
        predicate_flags,
        os.path.basename(failing_test_case)
    )

    print("...and reducing with this script:")
    print()
    print()
    print(predicate)
    print()
    print()

    predicate_path = failing_test_case + ".predicate.sh"
    with open(predicate_path, "w") as p:
        p.write(predicate)
    os.chmod(predicate_path, 0o755)

    creduce_command = ["creduce", "--n", str(os.cpu_count()), predicate_path, failing_test_case]
    print("Running:", creduce_command)
    result = run(creduce_command)
    if result.returncode == 0:
        print()
        print()
        print("`creduce` reduced the failing test case to:")
        print()
        print()
        cat(failing_test_case)
        print()
        print()
    else:
        print()
        print()
        print("`creduce` failed!")
        if not args.keep_going:
            sys.exit(1)

def print_issue_template(args, failing_test_case, predicate_command, result):
    test_case_contents = None
    with open(failing_test_case, "r") as f:
        test_case_contents = f.read()

    print("""

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !
! File this issue at https://github.com/rust-lang/rust-bindgen/issues/new !
! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

   --------------- 8< --------------- 8< --------------- 8< ---------------

This bug was found with `csmith` and `driver.py`.

### Input Header

```c
{}
```

### `bindgen` Invocation

```
$ {}
```

### Actual Results

<details>

```
{}
```

</details>

### Expected Results

`bindgen` emits bindings OK, then `rustc` compiles those bindings OK, then the
compiled bindings' layout tests pass OK.

   --------------- 8< --------------- 8< --------------- 8< ---------------

                         <3 <3 <3 Thank you! <3 <3 <3
    """.format(
        test_case_contents,
        " ".join(map(lambda s: "'{}'".format(s), predicate_command)),
        result.stdout + result.stderr
    ))

if __name__ == "__main__":
    try:
        os.chdir(os.path.abspath(os.path.dirname(sys.argv[0])))
        main()
    except KeyboardInterrupt:
        exit()
