about summary refs log tree commit diff
path: root/src/tools
diff options
context:
space:
mode:
authorRune Tynan <runetynan@gmail.com>2021-01-15 20:34:15 -0500
committerRune Tynan <runetynan@gmail.com>2021-01-19 14:24:25 -0500
commit7715656edd201b8c6bbddf0040f424c27e4db4df (patch)
treea3c2f7b0835fd4f3728d85755df926566ee29f36 /src/tools
parentf09fb488f70c5965ec4f64453a6e681fbfcff56c (diff)
downloadrust-7715656edd201b8c6bbddf0040f424c27e4db4df.tar.gz
rust-7715656edd201b8c6bbddf0040f424c27e4db4df.zip
Add jsondocck tool, and use it for rustdoc JSON
Diffstat (limited to 'src/tools')
-rw-r--r--src/tools/compiletest/src/common.rs3
-rw-r--r--src/tools/compiletest/src/header/tests.rs1
-rw-r--r--src/tools/compiletest/src/main.rs3
-rw-r--r--src/tools/compiletest/src/runtest.rs22
-rw-r--r--src/tools/jsondocck/Cargo.toml14
-rw-r--r--src/tools/jsondocck/src/cache.rs70
-rw-r--r--src/tools/jsondocck/src/config.rs41
-rw-r--r--src/tools/jsondocck/src/error.rs28
-rw-r--r--src/tools/jsondocck/src/main.rs260
9 files changed, 431 insertions, 11 deletions
diff --git a/src/tools/compiletest/src/common.rs b/src/tools/compiletest/src/common.rs
index 43dbaeb4655..4cefb562b13 100644
--- a/src/tools/compiletest/src/common.rs
+++ b/src/tools/compiletest/src/common.rs
@@ -198,6 +198,9 @@ pub struct Config {
     /// The Python executable to use for htmldocck.
     pub docck_python: String,
 
+    /// The jsondocck executable.
+    pub jsondocck_path: String,
+
     /// The LLVM `FileCheck` binary path.
     pub llvm_filecheck: Option<PathBuf>,
 
diff --git a/src/tools/compiletest/src/header/tests.rs b/src/tools/compiletest/src/header/tests.rs
index 4bcbd89f095..ec99fde0df9 100644
--- a/src/tools/compiletest/src/header/tests.rs
+++ b/src/tools/compiletest/src/header/tests.rs
@@ -45,6 +45,7 @@ fn config() -> Config {
         "--rustc-path=",
         "--lldb-python=",
         "--docck-python=",
+        "--jsondocck-path=",
         "--src-base=",
         "--build-base=",
         "--stage-id=stage2",
diff --git a/src/tools/compiletest/src/main.rs b/src/tools/compiletest/src/main.rs
index aefcfe222e5..03a443c8bff 100644
--- a/src/tools/compiletest/src/main.rs
+++ b/src/tools/compiletest/src/main.rs
@@ -60,6 +60,7 @@ pub fn parse_config(args: Vec<String>) -> Config {
         .optopt("", "rust-demangler-path", "path to rust-demangler to use in tests", "PATH")
         .reqopt("", "lldb-python", "path to python to use for doc tests", "PATH")
         .reqopt("", "docck-python", "path to python to use for doc tests", "PATH")
+        .reqopt("", "jsondocck-path", "path to jsondocck to use for doc tests", "PATH")
         .optopt("", "valgrind-path", "path to Valgrind executable for Valgrind tests", "PROGRAM")
         .optflag("", "force-valgrind", "fail if Valgrind tests cannot be run under Valgrind")
         .optopt("", "run-clang-based-tests-with", "path to Clang executable", "PATH")
@@ -196,6 +197,7 @@ pub fn parse_config(args: Vec<String>) -> Config {
     let has_tidy = Command::new("tidy")
         .arg("--version")
         .stdout(Stdio::null())
+        .stderr(Stdio::null())
         .status()
         .map_or(false, |status| status.success());
     Config {
@@ -207,6 +209,7 @@ pub fn parse_config(args: Vec<String>) -> Config {
         rust_demangler_path: matches.opt_str("rust-demangler-path").map(PathBuf::from),
         lldb_python: matches.opt_str("lldb-python").unwrap(),
         docck_python: matches.opt_str("docck-python").unwrap(),
+        jsondocck_path: matches.opt_str("jsondocck-path").unwrap(),
         valgrind_path: matches.opt_str("valgrind-path"),
         force_valgrind: matches.opt_present("force-valgrind"),
         run_clang_based_tests_with: matches.opt_str("run-clang-based-tests-with"),
diff --git a/src/tools/compiletest/src/runtest.rs b/src/tools/compiletest/src/runtest.rs
index 9f31b3ae1b1..b15a563a930 100644
--- a/src/tools/compiletest/src/runtest.rs
+++ b/src/tools/compiletest/src/runtest.rs
@@ -2490,27 +2490,27 @@ impl<'test> TestCx<'test> {
         let mut json_out = out_dir.join(self.testpaths.file.file_stem().unwrap());
         json_out.set_extension("json");
         let res = self.cmd2procres(
-            Command::new(&self.config.docck_python)
-                .arg(root.join("src/test/rustdoc-json/check_missing_items.py"))
-                .arg(&json_out),
+            Command::new(&self.config.jsondocck_path)
+                .arg("--doc-dir")
+                .arg(root.join(&out_dir))
+                .arg("--template")
+                .arg(&self.testpaths.file),
         );
 
         if !res.status.success() {
-            self.fatal_proc_rec("check_missing_items failed!", &res);
+            self.fatal_proc_rec("jsondocck failed!", &res)
         }
 
-        let mut expected = self.testpaths.file.clone();
-        expected.set_extension("expected");
+        let mut json_out = out_dir.join(self.testpaths.file.file_stem().unwrap());
+        json_out.set_extension("json");
         let res = self.cmd2procres(
             Command::new(&self.config.docck_python)
-                .arg(root.join("src/test/rustdoc-json/compare.py"))
-                .arg(&expected)
-                .arg(&json_out)
-                .arg(&expected.parent().unwrap()),
+                .arg(root.join("src/etc/check_missing_items.py"))
+                .arg(&json_out),
         );
 
         if !res.status.success() {
-            self.fatal_proc_rec("compare failed!", &res);
+            self.fatal_proc_rec("check_missing_items failed!", &res);
         }
     }
 
diff --git a/src/tools/jsondocck/Cargo.toml b/src/tools/jsondocck/Cargo.toml
new file mode 100644
index 00000000000..97052ef58d6
--- /dev/null
+++ b/src/tools/jsondocck/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "jsondocck"
+version = "0.1.0"
+authors = ["Rune Tynan <runetynan@gmail.com>"]
+edition = "2018"
+
+[dependencies]
+jsonpath_lib = "0.2"
+getopts = "0.2"
+regex = "1.4"
+lazy_static = "1.4"
+shlex = "0.1"
+serde = "1.0"
+serde_json = "1.0"
diff --git a/src/tools/jsondocck/src/cache.rs b/src/tools/jsondocck/src/cache.rs
new file mode 100644
index 00000000000..32c32bc0890
--- /dev/null
+++ b/src/tools/jsondocck/src/cache.rs
@@ -0,0 +1,70 @@
+use crate::error::CkError;
+use serde_json::Value;
+use std::collections::HashMap;
+use std::fs;
+use std::path::{Path, PathBuf};
+
+#[derive(Debug)]
+pub struct Cache {
+    root: PathBuf,
+    files: HashMap<PathBuf, String>,
+    values: HashMap<PathBuf, Value>,
+    last_path: Option<PathBuf>,
+}
+
+impl Cache {
+    pub fn new(doc_dir: &str) -> Cache {
+        Cache {
+            root: <str as AsRef<Path>>::as_ref(doc_dir).to_owned(),
+            files: HashMap::new(),
+            values: HashMap::new(),
+            last_path: None,
+        }
+    }
+
+    fn resolve_path(&mut self, path: &String) -> Result<PathBuf, CkError> {
+        if path != "-" {
+            let resolve = self.root.join(path);
+            self.last_path = Some(resolve.clone());
+            Ok(resolve)
+        } else {
+            match &self.last_path {
+                Some(p) => Ok(p.clone()),
+                None => unreachable!(),
+            }
+        }
+    }
+
+    pub fn get_file(&mut self, path: &String) -> Result<String, CkError> {
+        let path = self.resolve_path(path)?;
+
+        if let Some(f) = self.files.get(&path) {
+            return Ok(f.clone());
+        }
+
+        let file = fs::read_to_string(&path)?;
+
+        self.files.insert(path, file.clone());
+
+        Ok(file)
+        // Err(_) => Err(CkError::FailedCheck(format!("File {:?} does not exist / could not be opened", path)))
+    }
+
+    pub fn get_value(&mut self, path: &String) -> Result<Value, CkError> {
+        let path = self.resolve_path(path)?;
+
+        if let Some(v) = self.values.get(&path) {
+            return Ok(v.clone());
+        }
+
+        let file = fs::File::open(&path)?;
+        // Err(_) => return Err(CkError::FailedCheck(format!("File {:?} does not exist / could not be opened", path)))
+
+        let val = serde_json::from_reader::<_, Value>(file)?;
+
+        self.values.insert(path, val.clone());
+
+        Ok(val)
+        // Err(_) => Err(CkError::FailedCheck(format!("File {:?} did not contain valid JSON", path)))
+    }
+}
diff --git a/src/tools/jsondocck/src/config.rs b/src/tools/jsondocck/src/config.rs
new file mode 100644
index 00000000000..e76ebe1fa80
--- /dev/null
+++ b/src/tools/jsondocck/src/config.rs
@@ -0,0 +1,41 @@
+use getopts::Options;
+
+#[derive(Debug)]
+pub struct Config {
+    /// The directory documentation output was generated in
+    pub doc_dir: String,
+    /// The file documentation was generated for, with docck commands to check
+    pub template: String,
+}
+
+pub fn parse_config(args: Vec<String>) -> Config {
+    let mut opts = Options::new();
+    opts.reqopt("", "doc-dir", "Path to the documentation directory", "PATH")
+        .reqopt("", "template", "Path to the template file", "PATH")
+        .optflag("h", "help", "show this message");
+
+    let (argv0, args_) = args.split_first().unwrap();
+    if args.len() == 1 || args[1] == "-h" || args[1] == "--help" {
+        let message = format!("Usage: {} <doc-dir> <template>", argv0);
+        println!("{}", opts.usage(&message));
+        println!();
+        panic!()
+    }
+
+    let matches = &match opts.parse(args_) {
+        Ok(m) => m,
+        Err(f) => panic!("{:?}", f),
+    };
+
+    if matches.opt_present("h") || matches.opt_present("help") {
+        let message = format!("Usage: {} <doc-dir> <template>", argv0);
+        println!("{}", opts.usage(&message));
+        println!();
+        panic!()
+    }
+
+    Config {
+        doc_dir: matches.opt_str("doc-dir").unwrap(),
+        template: matches.opt_str("template").unwrap(),
+    }
+}
diff --git a/src/tools/jsondocck/src/error.rs b/src/tools/jsondocck/src/error.rs
new file mode 100644
index 00000000000..53b9af2874b
--- /dev/null
+++ b/src/tools/jsondocck/src/error.rs
@@ -0,0 +1,28 @@
+use crate::Command;
+use std::error::Error;
+use std::fmt;
+
+#[derive(Debug)]
+pub enum CkError {
+    /// A check failed. File didn't exist or failed to match the command
+    FailedCheck(String, Command),
+    /// An error triggered by some other error
+    Induced(Box<dyn Error>),
+}
+
+impl fmt::Display for CkError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            CkError::FailedCheck(msg, cmd) => {
+                write!(f, "Failed check: {} on line {}", msg, cmd.lineno)
+            }
+            CkError::Induced(err) => write!(f, "Check failed: {}", err),
+        }
+    }
+}
+
+impl<T: Error + 'static> From<T> for CkError {
+    fn from(err: T) -> CkError {
+        CkError::Induced(Box::new(err))
+    }
+}
diff --git a/src/tools/jsondocck/src/main.rs b/src/tools/jsondocck/src/main.rs
new file mode 100644
index 00000000000..6155128f1a7
--- /dev/null
+++ b/src/tools/jsondocck/src/main.rs
@@ -0,0 +1,260 @@
+use jsonpath_lib::select;
+use lazy_static::lazy_static;
+use regex::{Regex, RegexBuilder};
+use serde_json::Value;
+use std::{env, fmt, fs};
+
+mod cache;
+mod config;
+mod error;
+
+use cache::Cache;
+use config::parse_config;
+use error::CkError;
+
+fn main() -> Result<(), String> {
+    let config = parse_config(env::args().collect());
+
+    let mut failed = Vec::new();
+    let mut cache = Cache::new(&config.doc_dir);
+    let commands = get_commands(&config.template)
+        .map_err(|_| format!("Jsondocck failed for {}", &config.template))?;
+
+    for command in commands {
+        if let Err(e) = check_command(command, &mut cache) {
+            failed.push(e);
+        }
+    }
+
+    if failed.is_empty() {
+        Ok(())
+    } else {
+        for i in failed {
+            eprintln!("{}", i);
+        }
+        Err(format!("Jsondocck failed for {}", &config.template))
+    }
+}
+
+#[derive(Debug)]
+pub struct Command {
+    negated: bool,
+    kind: CommandKind,
+    args: Vec<String>,
+    lineno: usize,
+}
+
+#[derive(Debug)]
+pub enum CommandKind {
+    Has,
+    Count,
+}
+
+impl CommandKind {
+    fn validate(&self, args: &[String], command_num: usize, lineno: usize) -> bool {
+        let count = match self {
+            CommandKind::Has => (1..=3).contains(&args.len()),
+            CommandKind::Count => 3 == args.len(),
+        };
+
+        if !count {
+            print_err(&format!("Incorrect number of arguments to `@{}`", self), lineno);
+            return false;
+        }
+
+        match self {
+            CommandKind::Has => {
+                if args[0] == "-" && command_num == 0 {
+                    print_err(
+                        &format!("Tried to use the previous path in the first command"),
+                        lineno,
+                    );
+                    return false;
+                }
+            }
+            CommandKind::Count => {
+                if args[0] == "-" && command_num == 0 {
+                    print_err(
+                        &format!("Tried to use the previous path in the first command"),
+                        lineno,
+                    );
+                    return false;
+                }
+                if args[2].parse::<usize>().is_err() {
+                    print_err(&format!("Third argument to @count must be a valid usize"), lineno);
+                    return false;
+                }
+            }
+        }
+
+        true
+    }
+}
+
+impl fmt::Display for CommandKind {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let text = match self {
+            CommandKind::Has => "has",
+            CommandKind::Count => "count",
+        };
+        write!(f, "{}", text)
+    }
+}
+
+lazy_static! {
+    static ref LINE_PATTERN: Regex = RegexBuilder::new(
+        r#"
+        \s(?P<invalid>!?)@(?P<negated>!?)
+        (?P<cmd>[A-Za-z]+(?:-[A-Za-z]+)*)
+        (?P<args>.*)$
+    "#
+    )
+    .ignore_whitespace(true)
+    .unicode(true)
+    .build()
+    .unwrap();
+}
+
+fn print_err(msg: &str, lineno: usize) {
+    eprintln!("Invalid command: {} on line {}", msg, lineno)
+}
+
+fn get_commands(template: &str) -> Result<Vec<Command>, ()> {
+    let mut commands = Vec::new();
+    let mut errors = false;
+    let file = fs::read_to_string(template).unwrap();
+
+    for (lineno, line) in file.split('\n').enumerate() {
+        let lineno = lineno + 1;
+
+        let cap = match LINE_PATTERN.captures(line) {
+            Some(c) => c,
+            None => continue,
+        };
+
+        let negated = match cap.name("negated") {
+            Some(m) => m.as_str() == "!",
+            None => false,
+        };
+        let cmd = cap.name("cmd").unwrap().as_str();
+
+        let cmd = match cmd {
+            "has" => CommandKind::Has,
+            "count" => CommandKind::Count,
+            _ => {
+                print_err(&format!("Unrecognized command name `@{}`", cmd), lineno);
+                errors = true;
+                continue;
+            }
+        };
+
+        if let Some(m) = cap.name("invalid") {
+            if m.as_str() == "!" {
+                print_err(
+                    &format!(
+                        "`!@{0}{1}`, (help: try with `@!{1}`)",
+                        if negated { "!" } else { "" },
+                        cmd,
+                    ),
+                    lineno,
+                );
+                errors = true;
+                continue;
+            }
+        }
+
+        let args = match cap.name("args") {
+            Some(m) => shlex::split(m.as_str()).unwrap(),
+            None => vec![],
+        };
+
+        if !cmd.validate(&args, commands.len(), lineno) {
+            errors = true;
+            continue;
+        }
+
+        commands.push(Command { negated, kind: cmd, args, lineno })
+    }
+
+    if !errors { Ok(commands) } else { Err(()) }
+}
+
+fn check_command(command: Command, cache: &mut Cache) -> Result<(), CkError> {
+    let result = match command.kind {
+        CommandKind::Has => {
+            match command.args.len() {
+                // @has <path> = file existence
+                1 => match cache.get_file(&command.args[0]) {
+                    Ok(_) => true,
+                    Err(_) => false,
+                },
+                // @has <path> <jsonpath> = check path exists
+                2 => {
+                    let val = cache.get_value(&command.args[0])?;
+
+                    match select(&val, &command.args[1]) {
+                        Ok(results) => !results.is_empty(),
+                        Err(_) => false,
+                    }
+                }
+                // @has <path> <jsonpath> <value> = check *any* item matched by path equals value
+                3 => {
+                    let val = cache.get_value(&command.args[0])?;
+                    match select(&val, &command.args[1]) {
+                        Ok(results) => {
+                            let pat: Value = serde_json::from_str(&command.args[2]).unwrap();
+
+                            !results.is_empty() && results.into_iter().any(|val| *val == pat)
+                        }
+                        Err(_) => false,
+                    }
+                }
+                _ => {
+                    unreachable!()
+                }
+            }
+        }
+        CommandKind::Count => {
+            match command.args.len() {
+                // @count <path> <jsonpath> <count> = Check that the jsonpath matches exactly [count] times
+                3 => {
+                    let expected: usize = command.args[2].parse().unwrap();
+
+                    let val = cache.get_value(&command.args[0])?;
+                    match select(&val, &command.args[1]) {
+                        Ok(results) => results.len() == expected,
+                        Err(_) => false,
+                    }
+                }
+                _ => {
+                    unreachable!()
+                }
+            }
+        }
+    };
+
+    if result == command.negated {
+        if command.negated {
+            Err(CkError::FailedCheck(
+                format!(
+                    "`@!{} {}` matched when it shouldn't",
+                    command.kind,
+                    command.args.join(" ")
+                ),
+                command,
+            ))
+        } else {
+            // FIXME: In the future, try 'peeling back' each step, and see at what level the match failed
+            Err(CkError::FailedCheck(
+                format!(
+                    "`@{} {}` didn't match when it should",
+                    command.kind,
+                    command.args.join(" ")
+                ),
+                command,
+            ))
+        }
+    } else {
+        Ok(())
+    }
+}