about summary refs log tree commit diff
path: root/src/test/rustdoc-json
diff options
context:
space:
mode:
authorNixon Enraght-Moony <nixon.emoony@gmail.com>2020-11-29 16:16:25 +0000
committerNixon Enraght-Moony <nixon.emoony@gmail.com>2020-12-01 18:34:39 +0000
commit1098cce27acb2d52cb3b5ddbcf28c3a06e38dc7c (patch)
tree672e12f1004ced7aa2858d6f829c48d06e78cb84 /src/test/rustdoc-json
parent66884e318f2edfb0dd66d76a7d3b080d0dd9e4c5 (diff)
downloadrust-1098cce27acb2d52cb3b5ddbcf28c3a06e38dc7c.tar.gz
rust-1098cce27acb2d52cb3b5ddbcf28c3a06e38dc7c.zip
Add tests for rustdoc json
Move rustdoc/rustdoc-json to rustdoc-json

Scaffold rustdoc-json test mode

Implement run_rustdoc_json_test

Fix up python

Make tidy happy
Diffstat (limited to 'src/test/rustdoc-json')
-rw-r--r--src/test/rustdoc-json/Makefile6
-rw-r--r--src/test/rustdoc-json/check_missing_items.py187
-rw-r--r--src/test/rustdoc-json/compare.py131
-rw-r--r--src/test/rustdoc-json/structs.expected468
-rw-r--r--src/test/rustdoc-json/structs.rs17
5 files changed, 809 insertions, 0 deletions
diff --git a/src/test/rustdoc-json/Makefile b/src/test/rustdoc-json/Makefile
new file mode 100644
index 00000000000..ad517ae95eb
--- /dev/null
+++ b/src/test/rustdoc-json/Makefile
@@ -0,0 +1,6 @@
+-include ../tools.mk
+
+tests: *.rs
+	$(RUSTDOC) $< -o $(TMPDIR) --output-format json
+	$(PYTHON) check_missing_items.py $(TMPDIR)/$(basename $<).json
+	$(PYTHON) compare.py $(basename $<).expected $(TMPDIR)/$(basename $<).json
diff --git a/src/test/rustdoc-json/check_missing_items.py b/src/test/rustdoc-json/check_missing_items.py
new file mode 100644
index 00000000000..3a3bf7fa3ed
--- /dev/null
+++ b/src/test/rustdoc-json/check_missing_items.py
@@ -0,0 +1,187 @@
+#!/usr/bin/env python
+
+# This test ensures that every ID in the produced json actually resolves to an item either in
+# `index` or `paths`. It DOES NOT check that the structure of the produced json is actually in
+# any way correct, for example an empty map would pass.
+
+import sys
+import json
+
+crate = json.load(open(sys.argv[1]))
+
+
+def get_local_item(item_id):
+    if item_id in crate["index"]:
+        return crate["index"][item_id]
+    print("Missing local ID:", item_id)
+    sys.exit(1)
+
+
+# local IDs have to be in `index`, external ones can sometimes be in `index` but otherwise have
+# to be in `paths`
+def valid_id(item_id):
+    return item_id in crate["index"] or item_id[0] != "0" and item_id in crate["paths"]
+
+
+def check_generics(generics):
+    for param in generics["params"]:
+        check_generic_param(param)
+    for where_predicate in generics["where_predicates"]:
+        if "bound_predicate" in where_predicate:
+            pred = where_predicate["bound_predicate"]
+            check_type(pred["ty"])
+            for bound in pred["bounds"]:
+                check_generic_bound(bound)
+        elif "region_predicate" in where_predicate:
+            pred = where_predicate["region_predicate"]
+            for bound in pred["bounds"]:
+                check_generic_bound(bound)
+        elif "eq_predicate" in where_predicate:
+            pred = where_predicate["eq_predicate"]
+            check_type(pred["rhs"])
+            check_type(pred["lhs"])
+
+
+def check_generic_param(param):
+    if "type" in param["kind"]:
+        ty = param["kind"]["type"]
+        if ty["default"]:
+            check_type(ty["default"])
+        for bound in ty["bounds"]:
+            check_generic_bound(bound)
+    elif "const" in param["kind"]:
+        check_type(param["kind"]["const"])
+
+
+def check_generic_bound(bound):
+    if "trait_bound" in bound:
+        for param in bound["trait_bound"]["generic_params"]:
+            check_generic_param(param)
+        check_type(bound["trait_bound"]["trait"])
+
+
+def check_decl(decl):
+    for (_name, ty) in decl["inputs"]:
+        check_type(ty)
+    if decl["output"]:
+        check_type(decl["output"])
+
+
+def check_type(ty):
+    if ty["kind"] == "resolved_path":
+        for bound in ty["inner"]["param_names"]:
+            check_generic_bound(bound)
+        args = ty["inner"]["args"]
+        if args:
+            if "angle_bracketed" in args:
+                for arg in args["angle_bracketed"]["args"]:
+                    if "type" in arg:
+                        check_type(arg["type"])
+                    elif "const" in arg:
+                        check_type(arg["const"]["type"])
+                for binding in args["angle_bracketed"]["bindings"]:
+                    if "equality" in binding["binding"]:
+                        check_type(binding["binding"]["equality"])
+                    elif "constraint" in binding["binding"]:
+                        for bound in binding["binding"]["constraint"]:
+                            check_generic_bound(bound)
+            elif "parenthesized" in args:
+                for ty in args["parenthesized"]["inputs"]:
+                    check_type(ty)
+                if args["parenthesized"]["output"]:
+                    check_type(args["parenthesized"]["output"])
+        if not valid_id(ty["inner"]["id"]):
+            print("Type contained an invalid ID:", ty["inner"]["id"])
+            sys.exit(1)
+    elif ty["kind"] == "tuple":
+        for ty in ty["inner"]:
+            check_type(ty)
+    elif ty["kind"] == "slice":
+        check_type(ty["inner"])
+    elif ty["kind"] == "impl_trait":
+        for bound in ty["inner"]:
+            check_generic_bound(bound)
+    elif ty["kind"] in ("raw_pointer", "borrowed_ref", "array"):
+        check_type(ty["inner"]["type"])
+    elif ty["kind"] == "function_pointer":
+        for param in ty["inner"]["generic_params"]:
+            check_generic_param(param)
+        check_decl(ty["inner"]["inner"])
+    elif ty["kind"] == "qualified_path":
+        check_type(ty["inner"]["self_type"])
+        check_type(ty["inner"]["trait"])
+
+
+work_list = set([crate["root"]])
+visited = work_list.copy()
+
+while work_list:
+    current = work_list.pop()
+    visited.add(current)
+    item = get_local_item(current)
+    # check intradoc links
+    for (_name, link) in item["links"].items():
+        if not valid_id(link):
+            print("Intra-doc link contains invalid ID:", link)
+
+    # check all fields that reference types such as generics as well as nested items
+    # (modules, structs, traits, and enums)
+    if item["kind"] == "module":
+        work_list |= set(item["inner"]["items"]) - visited
+    elif item["kind"] == "struct":
+        check_generics(item["inner"]["generics"])
+        work_list |= (
+            set(item["inner"]["fields"]) | set(item["inner"]["impls"])
+        ) - visited
+    elif item["kind"] == "struct_field":
+        check_type(item["inner"])
+    elif item["kind"] == "enum":
+        check_generics(item["inner"]["generics"])
+        work_list |= (
+            set(item["inner"]["variants"]) | set(item["inner"]["impls"])
+        ) - visited
+    elif item["kind"] == "variant":
+        if item["inner"]["variant_kind"] == "tuple":
+            for ty in item["inner"]["variant_inner"]:
+                check_type(ty)
+        elif item["inner"]["variant_kind"] == "struct":
+            work_list |= set(item["inner"]["variant_inner"]) - visited
+    elif item["kind"] in ("function", "method"):
+        check_generics(item["inner"]["generics"])
+        check_decl(item["inner"]["decl"])
+    elif item["kind"] in ("static", "constant", "assoc_const"):
+        check_type(item["inner"]["type"])
+    elif item["kind"] == "typedef":
+        check_type(item["inner"]["type"])
+        check_generics(item["inner"]["generics"])
+    elif item["kind"] == "opaque_ty":
+        check_generics(item["inner"]["generics"])
+        for bound in item["inner"]["bounds"]:
+            check_generic_bound(bound)
+    elif item["kind"] == "trait_alias":
+        check_generics(item["inner"]["params"])
+        for bound in item["inner"]["bounds"]:
+            check_generic_bound(bound)
+    elif item["kind"] == "trait":
+        check_generics(item["inner"]["generics"])
+        for bound in item["inner"]["bounds"]:
+            check_generic_bound(bound)
+        work_list |= (
+            set(item["inner"]["items"]) | set(item["inner"]["implementors"])
+        ) - visited
+    elif item["kind"] == "impl":
+        check_generics(item["inner"]["generics"])
+        if item["inner"]["trait"]:
+            check_type(item["inner"]["trait"])
+        if item["inner"]["blanket_impl"]:
+            check_type(item["inner"]["blanket_impl"])
+        check_type(item["inner"]["for"])
+        for assoc_item in item["inner"]["items"]:
+            if not valid_id(assoc_item):
+                print("Impl block referenced a missing ID:", assoc_item)
+                sys.exit(1)
+    elif item["kind"] == "assoc_type":
+        for bound in item["inner"]["bounds"]:
+            check_generic_bound(bound)
+        if item["inner"]["default"]:
+            check_type(item["inner"]["default"])
diff --git a/src/test/rustdoc-json/compare.py b/src/test/rustdoc-json/compare.py
new file mode 100644
index 00000000000..afc8066685c
--- /dev/null
+++ b/src/test/rustdoc-json/compare.py
@@ -0,0 +1,131 @@
+#!/usr/bin/env python
+
+# This script can check that an expected json blob is a subset of what actually gets produced.
+# The comparison is independent of the value of IDs (which are unstable) and instead uses their
+# relative ordering to check them against eachother by looking them up in their respective blob's
+# `index` or `paths` mappings. To add a new test run `rustdoc --output-format json -o . yourtest.rs`
+# and then create `yourtest.expected` by stripping unnecessary details from `yourtest.json`.
+
+import copy
+import sys
+import json
+import types
+
+# Used instead of the string ids when used as references.
+# Not used as keys in `index` or `paths`
+class ID(str):
+    pass
+
+
+class SubsetException(Exception):
+    def __init__(self, msg, trace):
+        self.msg = msg
+        self.trace = msg
+        super().__init__("{}: {}".format(trace, msg))
+
+
+def check_subset(expected_main, actual_main, base_dir):
+    expected_index = expected_main["index"]
+    expected_paths = expected_main["paths"]
+    actual_index = actual_main["index"]
+    actual_paths = actual_main["paths"]
+    already_checked = set()
+
+    def _check_subset(expected, actual, trace):
+        expected_type = type(expected)
+        actual_type = type(actual)
+        if expected_type is not actual_type:
+            raise SubsetException(
+                "expected type `{}`, got `{}`".format(expected_type, actual_type), trace
+            )
+        if expected_type in (str, int, bool) and expected != actual:
+            if expected_type == str and actual.startswith(base_dir):
+                if actual.replace(base_dir + "/", "") != expected:
+                    raise SubsetException(
+                        "expected `{}`, got: `{}`".format(
+                            expected, actual.replace(base_dir + "/", "")
+                        ),
+                        trace,
+                    )
+            else:
+                raise SubsetException(
+                    "expected `{}`, got: `{}`".format(expected, actual), trace
+                )
+        if expected_type is dict:
+            for key in expected:
+                if key not in actual:
+                    raise SubsetException(
+                        "Key `{}` not found in output".format(key), trace
+                    )
+                new_trace = copy.deepcopy(trace)
+                new_trace.append(key)
+                _check_subset(expected[key], actual[key], new_trace)
+        elif expected_type is list:
+            expected_elements = len(expected)
+            actual_elements = len(actual)
+            if expected_elements != actual_elements:
+                raise SubsetException(
+                    "Found {} items, expected {}".format(
+                        expected_elements, actual_elements
+                    ),
+                    trace,
+                )
+            for expected, actual in zip(expected, actual):
+                new_trace = copy.deepcopy(trace)
+                new_trace.append(expected)
+                _check_subset(expected, actual, new_trace)
+        elif expected_type is ID and expected not in already_checked:
+            already_checked.add(expected)
+            _check_subset(
+                expected_index.get(expected, {}), actual_index.get(actual, {}), trace
+            )
+            _check_subset(
+                expected_paths.get(expected, {}), actual_paths.get(actual, {}), trace
+            )
+
+    _check_subset(expected_main["root"], actual_main["root"], [])
+
+
+def rustdoc_object_hook(obj):
+    # No need to convert paths, index and external_crates keys to ids, since
+    # they are the target of resolution, and never a source itself.
+    if "id" in obj and obj["id"]:
+        obj["id"] = ID(obj["id"])
+    if "root" in obj:
+        obj["root"] = ID(obj["root"])
+    if "items" in obj:
+        obj["items"] = [ID(id) for id in obj["items"]]
+    if "variants" in obj:
+        obj["variants"] = [ID(id) for id in obj["variants"]]
+    if "fields" in obj:
+        obj["fields"] = [ID(id) for id in obj["fields"]]
+    if "impls" in obj:
+        obj["impls"] = [ID(id) for id in obj["impls"]]
+    if "implementors" in obj:
+        obj["implementors"] = [ID(id) for id in obj["implementors"]]
+    if "links" in obj:
+        obj["links"] = {s: ID(id) for s, id in obj["links"]}
+    if "variant_kind" in obj and obj["variant_kind"] == "struct":
+        obj["variant_inner"] = [ID(id) for id in obj["variant_inner"]]
+    return obj
+
+
+def main(expected_fpath, actual_fpath, base_dir):
+    print(
+        "checking that {} is a logical subset of {}".format(
+            expected_fpath, actual_fpath
+        )
+    )
+    with open(expected_fpath) as expected_file:
+        expected_main = json.load(expected_file, object_hook=rustdoc_object_hook)
+    with open(actual_fpath) as actual_file:
+        actual_main = json.load(actual_file, object_hook=rustdoc_object_hook)
+    check_subset(expected_main, actual_main, base_dir)
+    print("all checks passed")
+
+
+if __name__ == "__main__":
+    if len(sys.argv) < 4:
+        print("Usage: `compare.py expected.json actual.json test-dir`")
+    else:
+        main(sys.argv[1], sys.argv[2], sys.argv[3])
diff --git a/src/test/rustdoc-json/structs.expected b/src/test/rustdoc-json/structs.expected
new file mode 100644
index 00000000000..45b23534bc7
--- /dev/null
+++ b/src/test/rustdoc-json/structs.expected
@@ -0,0 +1,468 @@
+{
+  "root": "0:0",
+  "version": null,
+  "includes_private": false,
+  "index": {
+    "0:9": {
+      "crate_id": 0,
+      "name": "Unit",
+      "source": {
+        "filename": "structs.rs",
+        "begin": [
+          7,
+          0
+        ],
+        "end": [
+          7,
+          16
+        ]
+      },
+      "visibility": "public",
+      "docs": "",
+      "links": {},
+      "attrs": [],
+      "deprecation": null,
+      "kind": "struct",
+      "inner": {
+        "struct_type": "unit",
+        "generics": {
+          "params": [],
+          "where_predicates": []
+        },
+        "fields_stripped": false,
+        "fields": []
+      }
+    },
+    "0:8": {
+      "crate_id": 0,
+      "name": "1",
+      "source": {
+        "filename": "structs.rs",
+        "begin": [
+          5,
+          22
+        ],
+        "end": [
+          5,
+          28
+        ]
+      },
+      "visibility": "default",
+      "docs": "",
+      "links": {},
+      "attrs": [],
+      "deprecation": null,
+      "kind": "struct_field",
+      "inner": {
+        "kind": "resolved_path",
+        "inner": {
+          "name": "String",
+          "id": "5:5035",
+          "args": {
+            "angle_bracketed": {
+              "args": [],
+              "bindings": []
+            }
+          },
+          "param_names": []
+        }
+      }
+    },
+    "0:18": {
+      "crate_id": 0,
+      "name": "stuff",
+      "source": {
+        "filename": "structs.rs",
+        "begin": [
+          15,
+          4
+        ],
+        "end": [
+          15,
+          17
+        ]
+      },
+      "visibility": "default",
+      "docs": "",
+      "links": {},
+      "attrs": [],
+      "deprecation": null,
+      "kind": "struct_field",
+      "inner": {
+        "kind": "resolved_path",
+        "inner": {
+          "name": "Vec",
+          "id": "5:4322",
+          "args": {
+            "angle_bracketed": {
+              "args": [
+                {
+                  "type": {
+                    "kind": "generic",
+                    "inner": "T"
+                  }
+                }
+              ],
+              "bindings": []
+            }
+          },
+          "param_names": []
+        }
+      }
+    },
+    "0:11": {
+      "crate_id": 0,
+      "name": "WithPrimitives",
+      "source": {
+        "filename": "structs.rs",
+        "begin": [
+          9,
+          0
+        ],
+        "end": [
+          12,
+          1
+        ]
+      },
+      "visibility": "public",
+      "docs": "",
+      "links": {},
+      "attrs": [],
+      "deprecation": null,
+      "kind": "struct",
+      "inner": {
+        "struct_type": "plain",
+        "generics": {
+          "params": [
+            {
+              "name": "'a",
+              "kind": "lifetime"
+            }
+          ],
+          "where_predicates": []
+        },
+        "fields_stripped": true,
+        "fields": [
+          "0:13",
+          "0:14"
+        ]
+      }
+    },
+    "0:14": {
+      "crate_id": 0,
+      "name": "s",
+      "source": {
+        "filename": "structs.rs",
+        "begin": [
+          11,
+          4
+        ],
+        "end": [
+          11,
+          14
+        ]
+      },
+      "visibility": "default",
+      "docs": "",
+      "links": {},
+      "attrs": [],
+      "deprecation": null,
+      "kind": "struct_field",
+      "inner": {
+        "kind": "borrowed_ref",
+        "inner": {
+          "lifetime": "'a",
+          "mutable": false,
+          "type": {
+            "kind": "primitive",
+            "inner": "str"
+          }
+        }
+      }
+    },
+    "0:19": {
+      "crate_id": 0,
+      "name": "things",
+      "source": {
+        "filename": "structs.rs",
+        "begin": [
+          16,
+          4
+        ],
+        "end": [
+          16,
+          25
+        ]
+      },
+      "visibility": "default",
+      "docs": "",
+      "links": {},
+      "attrs": [],
+      "deprecation": null,
+      "kind": "struct_field",
+      "inner": {
+        "kind": "resolved_path",
+        "inner": {
+          "name": "HashMap",
+          "id": "1:6600",
+          "args": {
+            "angle_bracketed": {
+              "args": [
+                {
+                  "type": {
+                    "kind": "generic",
+                    "inner": "U"
+                  }
+                },
+                {
+                  "type": {
+                    "kind": "generic",
+                    "inner": "U"
+                  }
+                }
+              ],
+              "bindings": []
+            }
+          },
+          "param_names": []
+        }
+      }
+    },
+    "0:15": {
+      "crate_id": 0,
+      "name": "WithGenerics",
+      "source": {
+        "filename": "structs.rs",
+        "begin": [
+          14,
+          0
+        ],
+        "end": [
+          17,
+          1
+        ]
+      },
+      "visibility": "public",
+      "docs": "",
+      "links": {},
+      "attrs": [],
+      "deprecation": null,
+      "kind": "struct",
+      "inner": {
+        "struct_type": "plain",
+        "generics": {
+          "params": [
+            {
+              "name": "T",
+              "kind": {
+                "type": {
+                  "bounds": [],
+                  "default": null
+                }
+              }
+            },
+            {
+              "name": "U",
+              "kind": {
+                "type": {
+                  "bounds": [],
+                  "default": null
+                }
+              }
+            }
+          ],
+          "where_predicates": []
+        },
+        "fields_stripped": true,
+        "fields": [
+          "0:18",
+          "0:19"
+        ]
+      }
+    },
+    "0:0": {
+      "crate_id": 0,
+      "name": "structs",
+      "source": {
+        "filename": "structs.rs",
+        "begin": [
+          1,
+          0
+        ],
+        "end": [
+          17,
+          1
+        ]
+      },
+      "visibility": "public",
+      "docs": "",
+      "links": {},
+      "attrs": [],
+      "deprecation": null,
+      "kind": "module",
+      "inner": {
+        "is_crate": true,
+        "items": [
+          "0:4",
+          "0:5",
+          "0:9",
+          "0:11",
+          "0:15"
+        ]
+      }
+    },
+    "0:13": {
+      "crate_id": 0,
+      "name": "num",
+      "source": {
+        "filename": "structs.rs",
+        "begin": [
+          10,
+          4
+        ],
+        "end": [
+          10,
+          12
+        ]
+      },
+      "visibility": "default",
+      "docs": "",
+      "links": {},
+      "attrs": [],
+      "deprecation": null,
+      "kind": "struct_field",
+      "inner": {
+        "kind": "primitive",
+        "inner": "u32"
+      }
+    },
+    "0:5": {
+      "crate_id": 0,
+      "name": "Tuple",
+      "source": {
+        "filename": "structs.rs",
+        "begin": [
+          5,
+          0
+        ],
+        "end": [
+          5,
+          30
+        ]
+      },
+      "visibility": "public",
+      "docs": "",
+      "links": {},
+      "attrs": [],
+      "deprecation": null,
+      "kind": "struct",
+      "inner": {
+        "struct_type": "tuple",
+        "generics": {
+          "params": [],
+          "where_predicates": []
+        },
+        "fields_stripped": true,
+        "fields": [
+          "0:7",
+          "0:8"
+        ]
+      }
+    },
+    "0:4": {
+      "crate_id": 0,
+      "name": "PlainEmpty",
+      "source": {
+        "filename": "structs.rs",
+        "begin": [
+          3,
+          0
+        ],
+        "end": [
+          3,
+          24
+        ]
+      },
+      "visibility": "public",
+      "docs": "",
+      "links": {},
+      "attrs": [],
+      "deprecation": null,
+      "kind": "struct",
+      "inner": {
+        "struct_type": "plain",
+        "generics": {
+          "params": [],
+          "where_predicates": []
+        },
+        "fields_stripped": false,
+        "fields": []
+      }
+    },
+    "0:7": {
+      "crate_id": 0,
+      "name": "0",
+      "source": {
+        "filename": "structs.rs",
+        "begin": [
+          5,
+          17
+        ],
+        "end": [
+          5,
+          20
+        ]
+      },
+      "visibility": "default",
+      "docs": "",
+      "links": {},
+      "attrs": [],
+      "deprecation": null,
+      "kind": "struct_field",
+      "inner": {
+        "kind": "primitive",
+        "inner": "u32"
+      }
+    }
+  },
+  "paths": {
+    "5:4322": {
+      "crate_id": 5,
+      "path": [
+        "alloc",
+        "vec",
+        "Vec"
+      ],
+      "kind": "struct"
+    },
+    "5:5035": {
+      "crate_id": 5,
+      "path": [
+        "alloc",
+        "string",
+        "String"
+      ],
+      "kind": "struct"
+    },
+    "1:6600": {
+      "crate_id": 1,
+      "path": [
+        "std",
+        "collections",
+        "hash",
+        "map",
+        "HashMap"
+      ],
+      "kind": "struct"
+    }
+  },
+  "external_crates": {
+    "1": {
+      "name": "std"
+    },
+    "5": {
+      "name": "alloc"
+    }
+  },
+  "format_version": 1
+}
diff --git a/src/test/rustdoc-json/structs.rs b/src/test/rustdoc-json/structs.rs
new file mode 100644
index 00000000000..43fc4743503
--- /dev/null
+++ b/src/test/rustdoc-json/structs.rs
@@ -0,0 +1,17 @@
+use std::collections::HashMap;
+
+pub struct PlainEmpty {}
+
+pub struct Tuple(u32, String);
+
+pub struct Unit;
+
+pub struct WithPrimitives<'a> {
+    num: u32,
+    s: &'a str,
+}
+
+pub struct WithGenerics<T, U> {
+    stuff: Vec<T>,
+    things: HashMap<U, U>,
+}