Add jsondocck tool, and use it for rustdoc JSON

This commit is contained in:
Rune Tynan 2021-01-15 20:34:15 -05:00
parent f09fb488f7
commit 7715656edd
No known key found for this signature in database
GPG key ID: 7ECC932F8B2C731E
25 changed files with 546 additions and 820 deletions

View file

@ -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>,

View file

@ -45,6 +45,7 @@ fn config() -> Config {
"--rustc-path=",
"--lldb-python=",
"--docck-python=",
"--jsondocck-path=",
"--src-base=",
"--build-base=",
"--stage-id=stage2",

View file

@ -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"),

View file

@ -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>>(

View 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"

View 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)))
}
}

View 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(),
}
}

View 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))
}
}

View 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(())
}
}