Add jsondocck tool, and use it for rustdoc JSON
This commit is contained in:
parent
f09fb488f7
commit
7715656edd
25 changed files with 546 additions and 820 deletions
|
|
@ -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>,
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ fn config() -> Config {
|
|||
"--rustc-path=",
|
||||
"--lldb-python=",
|
||||
"--docck-python=",
|
||||
"--jsondocck-path=",
|
||||
"--src-base=",
|
||||
"--build-base=",
|
||||
"--stage-id=stage2",
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -2487,31 +2487,31 @@ impl<'test> TestCx<'test> {
|
|||
}
|
||||
|
||||
let root = self.config.find_rust_src_root().unwrap();
|
||||
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.jsondocck_path)
|
||||
.arg("--doc-dir")
|
||||
.arg(root.join(&out_dir))
|
||||
.arg("--template")
|
||||
.arg(&self.testpaths.file),
|
||||
);
|
||||
|
||||
if !res.status.success() {
|
||||
self.fatal_proc_rec("jsondocck failed!", &res)
|
||||
}
|
||||
|
||||
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(root.join("src/etc/check_missing_items.py"))
|
||||
.arg(&json_out),
|
||||
);
|
||||
|
||||
if !res.status.success() {
|
||||
self.fatal_proc_rec("check_missing_items failed!", &res);
|
||||
}
|
||||
|
||||
let mut expected = self.testpaths.file.clone();
|
||||
expected.set_extension("expected");
|
||||
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()),
|
||||
);
|
||||
|
||||
if !res.status.success() {
|
||||
self.fatal_proc_rec("compare failed!", &res);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_lines<P: AsRef<Path>>(
|
||||
|
|
|
|||
14
src/tools/jsondocck/Cargo.toml
Normal file
14
src/tools/jsondocck/Cargo.toml
Normal file
|
|
@ -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"
|
||||
70
src/tools/jsondocck/src/cache.rs
Normal file
70
src/tools/jsondocck/src/cache.rs
Normal file
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
41
src/tools/jsondocck/src/config.rs
Normal file
41
src/tools/jsondocck/src/config.rs
Normal file
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
28
src/tools/jsondocck/src/error.rs
Normal file
28
src/tools/jsondocck/src/error.rs
Normal file
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
260
src/tools/jsondocck/src/main.rs
Normal file
260
src/tools/jsondocck/src/main.rs
Normal file
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue