• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2
3import argparse
4import os
5import re
6import shlex
7import subprocess
8import sys
9from tempfile import NamedTemporaryFile
10
11DESC = """
12
13Determine whether `bindgen` can successfully process a C or C++ input header.
14
15First, `bindgen` is run on the input header. Then the emitted bindings are
16compiled with `rustc`. Finally, the compiled bindings' layout tests are run.
17
18By default, this script will exit zero if all of the above steps are successful,
19and non-zero if any of them fail. This is appropriate for determining if some
20test case (perhaps generated with `csmith` or another fuzzer) uncovers any bugs
21in `bindgen`.
22
23However, this script can also be used when reducing (perhaps with `creduce`) a
24known-bad test case into a new, smaller test case that exhibits the same bad
25behavior. In this mode, you might expect that the emitted bindings fail to
26compile with `rustc`, and want to exit non-zero early if that is not the
27case. See the "reducing arguments" for details and what knobs are available.
28
29"""
30
31parser = argparse.ArgumentParser(
32    formatter_class=argparse.RawDescriptionHelpFormatter,
33    description=DESC.strip())
34
35BINDGEN_ARGS = "--with-derive-partialeq \
36--with-derive-eq \
37--with-derive-partialord \
38--with-derive-ord \
39--with-derive-hash \
40--with-derive-default"
41
42parser.add_argument(
43    "--bindgen-args",
44    type=str,
45    default=BINDGEN_ARGS,
46    help="An argument string that `bindgen` should be invoked with. By default, all traits are derived. Note that the input header and output bindings file will automatically be provided by this script, and you should not manually specify them.")
47
48parser.add_argument(
49    "--save-temp-files",
50    action="store_true",
51    help="Do not delete temporary files.")
52
53parser.add_argument(
54    "input",
55    type=str,
56    default="input.h",
57    help="The input header file. Defaults to 'input.h'.")
58
59REDUCING_DESC = """
60
61Arguments that are useful when reducing known-bad test cases into
62equivalent-but-smaller test cases that exhibit the same bug with `creduce`.
63
64"""
65
66reducing = parser.add_argument_group("reducing arguments", REDUCING_DESC.strip())
67
68reducing.add_argument(
69    "--release",
70    action="store_true",
71    help="Use a release instead of a debug build.")
72reducing.add_argument(
73    "--expect-bindgen-fail",
74    action="store_true",
75    help="Exit non-zero if `bindgen` successfully emits bindings.")
76reducing.add_argument(
77    "--bindgen-grep",
78    type=str,
79    help="Exit non-zero if the given regexp pattern is not found in `bindgen`'s output.")
80reducing.add_argument(
81    "--bindings-grep",
82    type=str,
83    nargs='*',
84    help="Exit non-zero if the given regexp pattern is not found in the emitted bindings.")
85
86reducing.add_argument(
87    "--no-compile-bindings",
88    action="store_false",
89    dest="rustc",
90    help="Do not attempt to compile the emitted bindings with `rustc`.")
91reducing.add_argument(
92    "--extra-compile-file",
93    type=str,
94    help="Append the content of this extra file to the end of the emitted bindings just before compiling it.")
95reducing.add_argument(
96    "--expect-compile-fail",
97    action="store_true",
98    help="Exit non-zero if `rustc` successfully compiles the emitted bindings.")
99reducing.add_argument(
100    "--rustc-grep",
101    type=str,
102    help="Exit non-zero if the output from compiling the bindings with `rustc` does not contain the given regexp pattern")
103
104reducing.add_argument(
105    "--no-layout-tests",
106    action="store_false",
107    dest="layout_tests",
108    help="Do not run the compiled bindings' layout tests.")
109reducing.add_argument(
110    "--expect-layout-tests-fail",
111    action="store_true",
112    help="Exit non-zero if the compiled bindings' layout tests pass.")
113reducing.add_argument(
114    "--layout-tests-grep",
115    type=str,
116    help="Exit non-zero if the output of running the compiled bindings' layout tests does not contain the given regexp pattern.")
117
118################################################################################
119
120class ExitOne(Exception):
121    pass
122
123def exit_1(msg, child=None):
124    print(msg)
125
126    if child:
127        stdout = decode(child.stdout)
128        for line in stdout.splitlines():
129            sys.stdout.write("+")
130            sys.stdout.write(line)
131            sys.stdout.write("\n")
132        stderr = decode(child.stderr)
133        for line in stderr.splitlines():
134            sys.stderr.write("+")
135            sys.stderr.write(line)
136            sys.stderr.write("\n")
137
138    raise ExitOne()
139
140def main():
141    args = parser.parse_args()
142    os.environ["RUST_BACKTRACE"] = "full"
143
144    exit_code = 0
145    try:
146        bindings = new_temp_file(prefix="bindings-", suffix=".rs")
147        run_bindgen(args, bindings)
148
149        if args.rustc and not args.expect_bindgen_fail:
150            test_exe = new_temp_file(prefix="layout-tests-")
151            run_rustc(args, bindings, test_exe)
152
153            if args.layout_tests and not args.expect_compile_fail:
154                run_layout_tests(args, test_exe)
155    except ExitOne:
156        exit_code = 1
157    except Exception as e:
158        exit_code = 2
159        print("Unexpected exception:", e)
160
161    if not args.save_temp_files:
162        for path in TEMP_FILES:
163            try:
164                os.remove(path)
165            except Exception as e:
166                print("Unexpected exception:", e)
167
168    sys.exit(exit_code)
169
170def run(cmd, **kwargs):
171    print("Running:", cmd)
172    return subprocess.run(cmd, **kwargs)
173
174def decode(f):
175    return f.decode(encoding="utf-8", errors="ignore")
176
177TEMP_FILES = []
178
179def new_temp_file(prefix, suffix=None):
180    temp = NamedTemporaryFile(delete=False, prefix=prefix, suffix=suffix)
181    temp.close()
182    TEMP_FILES.append(temp.name)
183    return temp.name
184
185def contains(pattern, lines):
186    for line in lines:
187        if re.match(pattern, line):
188            return True
189    return False
190
191def regexp(pattern):
192    if not pattern.startswith("^"):
193        pattern = ".*" + pattern
194    if not pattern.endswith("$"):
195        pattern = pattern + ".*"
196    return re.compile(pattern)
197
198def run_bindgen(args, bindings):
199    manifest_path = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]),
200                                                 "..",
201                                                 "Cargo.toml"))
202    command = ["cargo", "run", "--manifest-path", manifest_path]
203    if args.release:
204        command += ["--release"]
205    command += ["--", args.input, "-o", bindings]
206    command += shlex.split(args.bindgen_args)
207
208    child = run(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
209
210    if args.bindgen_grep:
211        pattern = regexp(args.bindgen_grep)
212        if not (contains(pattern, decode(child.stdout).splitlines()) or
213                contains(pattern, decode(child.stderr).splitlines())):
214            exit_1("Error: did not find '{}' in `bindgen`'s output".format(args.bindgen_grep), child)
215
216    if args.expect_bindgen_fail and child.returncode == 0:
217        exit_1("Error: expected running `bindgen` to fail, but it didn't", child)
218
219    if not args.expect_bindgen_fail and child.returncode != 0:
220        exit_1("Error: running `bindgen` failed", child)
221
222    for arg in args.bindings_grep or []:
223        pattern = regexp(arg)
224        with open(bindings, mode="r") as f:
225            if not contains(pattern, f):
226                print("Error: expected the emitted bindings to contain '{}', but they didn't".format(arg))
227                print("---------- {} ----------------------------------------------".format(bindings))
228                f.seek(0)
229                print(f.read())
230                raise ExitOne()
231
232def run_rustc(args, bindings, test_exe):
233    if args.extra_compile_file:
234        with open(bindings, mode="a") as outfile:
235            with open(args.extra_compile_file, mode="r") as infile:
236                outfile.write(infile.read())
237    child = run(
238        ["rustc", "--crate-type", "lib", "--test", "-o", test_exe, bindings],
239        stdout=subprocess.PIPE,
240        stderr=subprocess.PIPE)
241
242    if args.rustc_grep:
243        pattern = regexp(args.rustc_grep)
244        if not (contains(pattern, decode(child.stdout).splitlines()) or
245                contains(pattern, decode(child.stderr).splitlines())):
246            exit_1("Error: did not find '{}' in `rustc`'s output".format(args.rustc_grep), child)
247
248    if args.expect_compile_fail and child.returncode == 0:
249        exit_1("Error: expected running `rustc` on the emitted bindings to fail, but it didn't", child)
250
251    if not args.expect_compile_fail and child.returncode != 0:
252        exit_1("Error: running `rustc` on the emitted bindings failed", child)
253
254def run_layout_tests(args, test_exe):
255    child = run(
256        [test_exe],
257        stdout=subprocess.PIPE,
258        stderr=subprocess.PIPE)
259
260    if args.layout_tests_grep:
261        pattern = regexp(args.layout_tests_grep)
262        if not (contains(pattern, decode(child.stdout).splitlines()) or
263                contains(pattern, decode(child.stderr).splitlines())):
264            exit_1("Error: did not find '{}' in the compiled bindings' layout tests' output".format(args.layout_tests_grep), child)
265
266    if args.expect_layout_tests_fail and child.returncode == 0:
267        exit_1("Error: expected running the compiled bindings' layout tests to fail, but it didn't", child)
268
269    if not args.expect_layout_tests_fail and child.returncode != 0:
270        exit_1("Error: running the compiled bindings' layout tests failed", child)
271
272if __name__ == "__main__":
273    main()
274