diff options
author | Nick Fitzgerald <fitzgen@gmail.com> | 2016-10-18 15:14:56 -0700 |
---|---|---|
committer | Nick Fitzgerald <fitzgen@gmail.com> | 2016-10-18 15:57:10 -0700 |
commit | ffd8f27613438477dcc1a46030ad3d32bf765dd0 (patch) | |
tree | 3e142c6b7359e31aeb1a58633a7c858429d35b13 | |
parent | 2d359fa2ac1a67f23bd20fdc856690d08862c4bc (diff) |
Rewrite tests/tools/run-bindgen.py
Changes include:
* Using argparse for argument parsing. This will help when extending the
script to skip tests that will only pass when --features llvm_stable
is used to build bindgen.
* Pulling out all the various steps of the script into helper functions
that are easier to digest at a glance, and have docstrings describing
their role in the script.
* Printing diffs between expected and actual generated bindings rather
than the full source text of each.
-rwxr-xr-x | tests/tools/run-bindgen.py | 187 |
1 files changed, 140 insertions, 47 deletions
diff --git a/tests/tools/run-bindgen.py b/tests/tools/run-bindgen.py index 3ccea5c2..c9ddc4f2 100755 --- a/tests/tools/run-bindgen.py +++ b/tests/tools/run-bindgen.py @@ -1,69 +1,162 @@ #!/usr/bin/env python +from __future__ import print_function + +import argparse +import difflib import os import sys import subprocess import tempfile BINDGEN_FLAGS_PREFIX = "// bindgen-flags: "; -CLANG_FLAGS_SEPARATOR = "-- " + COMMON_PRELUDE = """ #![allow(non_snake_case)] """ -if len(sys.argv) != 4: - print("Usage: {} [bindgen-path] [c-path] [rust-path]\n".format(sys.argv[0])) - sys.exit(1) +DESCRIPTION = """ +Run bindgen on a test header and check the generated bindings against expected +output. +""" -[_, bindgen_path, c_path, rust_path] = sys.argv +def make_parser(): + """Make the commandline parser""" + parser = argparse.ArgumentParser(description=DESCRIPTION) + parser.add_argument("bindgen", + metavar="BINDGEN", + help="The path to the bindgen executable") + parser.add_argument("header", + metavar="HEADER", + help="The path to the input header") + parser.add_argument("rust_bindings", + metavar="RUST_BINDINGS", + help="The path to the generated rust output. If a file \ + at this path already exists, the newly generated \ + bindings will be checked against those extant \ + expected bindings.") + return parser -flags = ["--no-unstable-rust"] +def usage_and_exit(*args): + """Print the program usage and exit. If args are given, print them first""" + if len(args) > 0: + print(*args) + make_parser().print_help() + sys.exit(1) -with open(c_path) as f: - for line in f: - if line.startswith(BINDGEN_FLAGS_PREFIX): - flags.extend(line.strip().split(BINDGEN_FLAGS_PREFIX)[1].split(" ")) - break +def parse_args(): + """Get, parse, and validate commandline arguments.""" + parser = make_parser() + args = parser.parse_args() -base_command = [bindgen_path, "-o", rust_path] + if not os.path.isfile(args.bindgen): + usage_and_exit("error: bindgen is not a file:", args.bindgen) -for line in COMMON_PRELUDE.split("\n"): - base_command.append("--raw-line") - base_command.append(line) + if not os.path.isfile(args.header): + usage_and_exit("error: header is not a file:", args.header) -base_command.extend(flags) -base_command.append(c_path) + return args -env = os.environ.copy() +def make_bindgen_env(): + """Build the environment to run bindgen in.""" + env = os.environ.copy() -# El Capitan likes to unset dyld variables -# https://forums.developer.apple.com/thread/9233 -if "DYLD_LIBRARY_PATH" not in env and "LIBCLANG_PATH" in env: + # El Capitan likes to unset dyld variables + # https://forums.developer.apple.com/thread/9233 + if "DYLD_LIBRARY_PATH" not in env and "LIBCLANG_PATH" in env: env["DYLD_LIBRARY_PATH"] = env["LIBCLANG_PATH"] -# If the rust file already exists, read it now so we can compare its contents -# before and after. -original_rust_contents = None -if os.path.isfile(rust_path): - with open(rust_path) as f: - original_rust_contents = f.read() - -subprocess.check_call(base_command, cwd=os.getcwd(), env=env) - -name = None -with tempfile.NamedTemporaryFile(delete=False) as tests: - name = tests.name - subprocess.check_call(["rustc", "--test", sys.argv[3], "-o", tests.name]) -subprocess.check_call([tests.name]) - -if original_rust_contents is not None: - new_rust_contents = None - with open(rust_path) as f: - new_rust_contents = f.read() - if new_rust_contents != original_rust_contents: - print("Generated rust bindings do not match expectation!") - print("Expected rust bindings:") - print(original_rust_contents) - print("Actual rust bindings:") - print(new_rust_contents) - sys.exit(1) + return env + +def get_bindgen_flags_for_header(header_path): + """Get the flags to pass to bindgen for this header.""" + flags = ["--no-unstable-rust"] + + for line in COMMON_PRELUDE.split("\n"): + flags.append("--raw-line") + flags.append(line) + + with open(header_path) as f: + for line in f: + if line.startswith(BINDGEN_FLAGS_PREFIX): + flags.extend(line.strip().split(BINDGEN_FLAGS_PREFIX)[1].split(" ")) + break + + return flags + +def get_expected_bindings(rust_bindings_path): + """ + Get the expected, generated rust bindings output, or None if there is no + expected output yet. + """ + expected_bindings = None + if os.path.isfile(rust_bindings_path): + with open(rust_bindings_path) as f: + expected_bindings = f.read() + return expected_bindings + +def get_actual_bindings(rust_bindings_path): + """Get the actual generated rust bindings output.""" + assert os.path.isfile(rust_bindings_path) + with open(rust_bindings_path) as f: + return f.read() + +def run_cmd(command, **kwargs): + """Run the given command, passing through **kwargs to subprocess.check_call""" + print("run-bindgen.py: running", command) + subprocess.check_call(command, **kwargs) + +def generate_bindings(bindgen, header, output): + """Generate the rust bindings.""" + command = [bindgen, "-o", output] + command.extend(get_bindgen_flags_for_header(header)) + command.append(header) + run_cmd(command, cwd=os.getcwd(), env=make_bindgen_env()) + +def test_generated_bindings(bindings): + """Run the generated bindings's #[test]s.""" + name = None + # Do not delete the temp file, because we need to end the with block before + # we can run the tests. + with tempfile.NamedTemporaryFile(delete=False) as tests: + name = tests.name + run_cmd(["rustc", "--test", bindings, "-o", name]) + run_cmd([name]) + +def check_actual_vs_expected(expected_bindings, rust_bindings_path): + """ + Check the actual generated rust bindings versus our expected generated rust + bindings. If they don't match up, print a diff between them and exit with a + failure. + """ + if expected_bindings is None: + return + + actual_bindings = get_actual_bindings(rust_bindings_path) + if actual_bindings == expected_bindings: + return + + print("error: actual generated bindings do not match expected generated bindings!") + + def to_diffable(s): + return map(lambda l: l + "\n", s.split("\n")) + + diff = difflib.unified_diff(to_diffable(expected_bindings), + to_diffable(actual_bindings), + fromfile="expected_bindings.rs", + tofile="actual_bindings.rs") + sys.stderr.writelines(diff) + sys.stderr.write("\n") + + sys.exit(1) + +def main(): + args = parse_args() + expected_bindings = get_expected_bindings(args.rust_bindings) + generate_bindings(args.bindgen, args.header, args.rust_bindings) + test_generated_bindings(args.rust_bindings) + check_actual_vs_expected(expected_bindings, args.rust_bindings) + sys.exit(0) + +if __name__ == "__main__": + main() |