diff options
| author | Rune Tynan <runetynan@gmail.com> | 2021-01-15 20:34:15 -0500 |
|---|---|---|
| committer | Rune Tynan <runetynan@gmail.com> | 2021-01-19 14:24:25 -0500 |
| commit | 7715656edd201b8c6bbddf0040f424c27e4db4df (patch) | |
| tree | a3c2f7b0835fd4f3728d85755df926566ee29f36 /src/tools | |
| parent | f09fb488f70c5965ec4f64453a6e681fbfcff56c (diff) | |
| download | rust-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.rs | 3 | ||||
| -rw-r--r-- | src/tools/compiletest/src/header/tests.rs | 1 | ||||
| -rw-r--r-- | src/tools/compiletest/src/main.rs | 3 | ||||
| -rw-r--r-- | src/tools/compiletest/src/runtest.rs | 22 | ||||
| -rw-r--r-- | src/tools/jsondocck/Cargo.toml | 14 | ||||
| -rw-r--r-- | src/tools/jsondocck/src/cache.rs | 70 | ||||
| -rw-r--r-- | src/tools/jsondocck/src/config.rs | 41 | ||||
| -rw-r--r-- | src/tools/jsondocck/src/error.rs | 28 | ||||
| -rw-r--r-- | src/tools/jsondocck/src/main.rs | 260 |
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(()) + } +} |
