]>
Commit | Line | Data |
---|---|---|
fc512014 XL |
1 | #!/usr/bin/env python |
2 | ||
3 | # This script can check that an expected json blob is a subset of what actually gets produced. | |
4 | # The comparison is independent of the value of IDs (which are unstable) and instead uses their | |
5 | # relative ordering to check them against eachother by looking them up in their respective blob's | |
6 | # `index` or `paths` mappings. To add a new test run `rustdoc --output-format json -o . yourtest.rs` | |
7 | # and then create `yourtest.expected` by stripping unnecessary details from `yourtest.json`. If | |
8 | # you're on windows, replace `\` with `/`. | |
9 | ||
10 | import copy | |
11 | import sys | |
12 | import json | |
13 | import types | |
14 | ||
15 | # Used instead of the string ids when used as references. | |
16 | # Not used as keys in `index` or `paths` | |
17 | class ID(str): | |
18 | pass | |
19 | ||
20 | ||
21 | class SubsetException(Exception): | |
22 | def __init__(self, msg, trace): | |
23 | self.msg = msg | |
24 | self.trace = msg | |
25 | super().__init__("{}: {}".format(trace, msg)) | |
26 | ||
27 | ||
28 | def check_subset(expected_main, actual_main, base_dir): | |
29 | expected_index = expected_main["index"] | |
30 | expected_paths = expected_main["paths"] | |
31 | actual_index = actual_main["index"] | |
32 | actual_paths = actual_main["paths"] | |
33 | already_checked = set() | |
34 | ||
35 | def _check_subset(expected, actual, trace): | |
36 | expected_type = type(expected) | |
37 | actual_type = type(actual) | |
38 | ||
39 | if actual_type is str: | |
40 | actual = normalize(actual).replace(base_dir, "$TEST_BASE_DIR") | |
41 | ||
42 | if expected_type is not actual_type: | |
43 | raise SubsetException( | |
44 | "expected type `{}`, got `{}`".format(expected_type, actual_type), trace | |
45 | ) | |
46 | ||
47 | ||
48 | if expected_type in (int, bool, str) and expected != actual: | |
49 | raise SubsetException("expected `{}`, got: `{}`".format(expected, actual), trace) | |
50 | if expected_type is dict: | |
51 | for key in expected: | |
52 | if key not in actual: | |
53 | raise SubsetException( | |
54 | "Key `{}` not found in output".format(key), trace | |
55 | ) | |
56 | new_trace = copy.deepcopy(trace) | |
57 | new_trace.append(key) | |
58 | _check_subset(expected[key], actual[key], new_trace) | |
59 | elif expected_type is list: | |
60 | expected_elements = len(expected) | |
61 | actual_elements = len(actual) | |
62 | if expected_elements != actual_elements: | |
63 | raise SubsetException( | |
64 | "Found {} items, expected {}".format( | |
65 | expected_elements, actual_elements | |
66 | ), | |
67 | trace, | |
68 | ) | |
69 | for expected, actual in zip(expected, actual): | |
70 | new_trace = copy.deepcopy(trace) | |
71 | new_trace.append(expected) | |
72 | _check_subset(expected, actual, new_trace) | |
73 | elif expected_type is ID and expected not in already_checked: | |
74 | already_checked.add(expected) | |
75 | _check_subset( | |
76 | expected_index.get(expected, {}), actual_index.get(actual, {}), trace | |
77 | ) | |
78 | _check_subset( | |
79 | expected_paths.get(expected, {}), actual_paths.get(actual, {}), trace | |
80 | ) | |
81 | ||
82 | _check_subset(expected_main["root"], actual_main["root"], []) | |
83 | ||
84 | ||
85 | def rustdoc_object_hook(obj): | |
86 | # No need to convert paths, index and external_crates keys to ids, since | |
87 | # they are the target of resolution, and never a source itself. | |
88 | if "id" in obj and obj["id"]: | |
89 | obj["id"] = ID(obj["id"]) | |
90 | if "root" in obj: | |
91 | obj["root"] = ID(obj["root"]) | |
92 | if "items" in obj: | |
93 | obj["items"] = [ID(id) for id in obj["items"]] | |
94 | if "variants" in obj: | |
95 | obj["variants"] = [ID(id) for id in obj["variants"]] | |
96 | if "fields" in obj: | |
97 | obj["fields"] = [ID(id) for id in obj["fields"]] | |
98 | if "impls" in obj: | |
99 | obj["impls"] = [ID(id) for id in obj["impls"]] | |
100 | if "implementors" in obj: | |
101 | obj["implementors"] = [ID(id) for id in obj["implementors"]] | |
102 | if "links" in obj: | |
103 | obj["links"] = {s: ID(id) for s, id in obj["links"]} | |
104 | if "variant_kind" in obj and obj["variant_kind"] == "struct": | |
105 | obj["variant_inner"] = [ID(id) for id in obj["variant_inner"]] | |
106 | return obj | |
107 | ||
108 | ||
109 | def main(expected_fpath, actual_fpath, base_dir): | |
110 | print( | |
111 | "checking that {} is a logical subset of {}".format( | |
112 | expected_fpath, actual_fpath | |
113 | ) | |
114 | ) | |
115 | with open(expected_fpath) as expected_file: | |
116 | expected_main = json.load(expected_file, object_hook=rustdoc_object_hook) | |
117 | with open(actual_fpath) as actual_file: | |
118 | actual_main = json.load(actual_file, object_hook=rustdoc_object_hook) | |
119 | check_subset(expected_main, actual_main, base_dir) | |
120 | print("all checks passed") | |
121 | ||
122 | def normalize(s): | |
123 | return s.replace('\\', '/') | |
124 | ||
125 | if __name__ == "__main__": | |
126 | if len(sys.argv) < 4: | |
127 | print("Usage: `compare.py expected.json actual.json test-dir`") | |
128 | else: | |
129 | main(sys.argv[1], sys.argv[2], normalize(sys.argv[3])) |