rustdoc: Add the ability to test code in comments
This adds support for the `--test` flag to rustdoc which will parse a crate, extract all code examples in doc comments, and then run each test in the extra::test driver.
This commit is contained in:
parent
f71c0dc2cd
commit
6c9c045064
6 changed files with 335 additions and 40 deletions
|
@ -1147,13 +1147,17 @@ fn name_from_pat(p: &ast::Pat) -> ~str {
|
|||
fn resolve_type(path: Path, tpbs: Option<~[TyParamBound]>,
|
||||
id: ast::NodeId) -> Type {
|
||||
let cx = local_data::get(super::ctxtkey, |x| *x.unwrap());
|
||||
let tycx = match cx.tycx {
|
||||
Some(tycx) => tycx,
|
||||
// If we're extracting tests, this return value doesn't matter.
|
||||
None => return Bool
|
||||
};
|
||||
debug!("searching for {:?} in defmap", id);
|
||||
let d = match cx.tycx.def_map.find(&id) {
|
||||
let d = match tycx.def_map.find(&id) {
|
||||
Some(k) => k,
|
||||
None => {
|
||||
let ctxt = local_data::get(super::ctxtkey, |x| *x.unwrap());
|
||||
debug!("could not find {:?} in defmap (`{}`)", id,
|
||||
syntax::ast_map::node_id_to_str(ctxt.tycx.items, id, ctxt.sess.intr()));
|
||||
syntax::ast_map::node_id_to_str(tycx.items, id, cx.sess.intr()));
|
||||
fail!("Unexpected failure: unresolved id not in defmap (this is a bug!)")
|
||||
}
|
||||
};
|
||||
|
@ -1182,7 +1186,7 @@ fn resolve_type(path: Path, tpbs: Option<~[TyParamBound]>,
|
|||
if ast_util::is_local(def_id) {
|
||||
ResolvedPath{ path: path, typarams: tpbs, id: def_id.node }
|
||||
} else {
|
||||
let fqn = csearch::get_item_path(cx.tycx, def_id);
|
||||
let fqn = csearch::get_item_path(tycx, def_id);
|
||||
let fqn = fqn.move_iter().map(|i| {
|
||||
match i {
|
||||
ast_map::path_mod(id) |
|
||||
|
@ -1203,6 +1207,11 @@ fn resolve_use_source(path: Path, id: ast::NodeId) -> ImportSource {
|
|||
}
|
||||
|
||||
fn resolve_def(id: ast::NodeId) -> Option<ast::DefId> {
|
||||
let dm = local_data::get(super::ctxtkey, |x| *x.unwrap()).tycx.def_map;
|
||||
dm.find(&id).map(|&d| ast_util::def_id_of_def(d))
|
||||
let cx = local_data::get(super::ctxtkey, |x| *x.unwrap());
|
||||
match cx.tycx {
|
||||
Some(tcx) => {
|
||||
tcx.def_map.find(&id).map(|&d| ast_util::def_id_of_def(d))
|
||||
}
|
||||
None => None
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ use clean::Clean;
|
|||
|
||||
pub struct DocContext {
|
||||
crate: ast::Crate,
|
||||
tycx: middle::ty::ctxt,
|
||||
tycx: Option<middle::ty::ctxt>,
|
||||
sess: driver::session::Session
|
||||
}
|
||||
|
||||
|
@ -78,17 +78,13 @@ fn get_ast_and_resolve(cpath: &Path,
|
|||
} = phase_3_run_analysis_passes(sess, &crate);
|
||||
|
||||
debug!("crate: {:?}", crate);
|
||||
return (DocContext { crate: crate, tycx: ty_cx, sess: sess },
|
||||
return (DocContext { crate: crate, tycx: Some(ty_cx), sess: sess },
|
||||
CrateAnalysis { exported_items: exported_items });
|
||||
}
|
||||
|
||||
pub fn run_core (libs: HashSet<Path>, cfgs: ~[~str], path: &Path) -> (clean::Crate, CrateAnalysis) {
|
||||
let (ctxt, analysis) = get_ast_and_resolve(path, libs, cfgs);
|
||||
let ctxt = @ctxt;
|
||||
debug!("defmap:");
|
||||
for (k, v) in ctxt.tycx.def_map.iter() {
|
||||
debug!("{:?}: {:?}", k, v);
|
||||
}
|
||||
local_data::set(super::ctxtkey, ctxt);
|
||||
|
||||
let v = @mut RustdocVisitor::new();
|
||||
|
|
|
@ -22,9 +22,12 @@
|
|||
//! // ... something using html
|
||||
//! ```
|
||||
|
||||
use std::cast;
|
||||
use std::fmt;
|
||||
use std::libc;
|
||||
use std::io;
|
||||
use std::libc;
|
||||
use std::str;
|
||||
use std::unstable::intrinsics;
|
||||
use std::vec;
|
||||
|
||||
/// A unit struct which has the `fmt::Default` trait implemented. When
|
||||
|
@ -41,8 +44,10 @@ static MKDEXT_STRIKETHROUGH: libc::c_uint = 1 << 4;
|
|||
|
||||
type sd_markdown = libc::c_void; // this is opaque to us
|
||||
|
||||
// this is a large struct of callbacks we don't use
|
||||
type sd_callbacks = [libc::size_t, ..26];
|
||||
struct sd_callbacks {
|
||||
blockcode: extern "C" fn(*buf, *buf, *buf, *libc::c_void),
|
||||
other: [libc::size_t, ..25],
|
||||
}
|
||||
|
||||
struct html_toc_data {
|
||||
header_count: libc::c_int,
|
||||
|
@ -56,6 +61,11 @@ struct html_renderopt {
|
|||
link_attributes: Option<extern "C" fn(*buf, *buf, *libc::c_void)>,
|
||||
}
|
||||
|
||||
struct my_opaque {
|
||||
opt: html_renderopt,
|
||||
dfltblk: extern "C" fn(*buf, *buf, *buf, *libc::c_void),
|
||||
}
|
||||
|
||||
struct buf {
|
||||
data: *u8,
|
||||
size: libc::size_t,
|
||||
|
@ -84,7 +94,28 @@ extern {
|
|||
|
||||
}
|
||||
|
||||
fn render(w: &mut io::Writer, s: &str) {
|
||||
pub fn render(w: &mut io::Writer, s: &str) {
|
||||
extern fn block(ob: *buf, text: *buf, lang: *buf, opaque: *libc::c_void) {
|
||||
unsafe {
|
||||
let my_opaque: &my_opaque = cast::transmute(opaque);
|
||||
vec::raw::buf_as_slice((*text).data, (*text).size as uint, |text| {
|
||||
let text = str::from_utf8(text);
|
||||
let mut lines = text.lines().filter(|l| {
|
||||
!l.trim().starts_with("#")
|
||||
});
|
||||
let text = lines.to_owned_vec().connect("\n");
|
||||
|
||||
let buf = buf {
|
||||
data: text.as_bytes().as_ptr(),
|
||||
size: text.len() as libc::size_t,
|
||||
asize: text.len() as libc::size_t,
|
||||
unit: 0,
|
||||
};
|
||||
(my_opaque.dfltblk)(ob, &buf, lang, opaque);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// This code is all lifted from examples/sundown.c in the sundown repo
|
||||
unsafe {
|
||||
let ob = bufnew(OUTPUT_UNIT);
|
||||
|
@ -100,11 +131,16 @@ fn render(w: &mut io::Writer, s: &str) {
|
|||
flags: 0,
|
||||
link_attributes: None,
|
||||
};
|
||||
let callbacks: sd_callbacks = [0, ..26];
|
||||
let mut callbacks: sd_callbacks = intrinsics::init();
|
||||
|
||||
sdhtml_renderer(&callbacks, &options, 0);
|
||||
let opaque = my_opaque {
|
||||
opt: options,
|
||||
dfltblk: callbacks.blockcode,
|
||||
};
|
||||
callbacks.blockcode = block;
|
||||
let markdown = sd_markdown_new(extensions, 16, &callbacks,
|
||||
&options as *html_renderopt as *libc::c_void);
|
||||
&opaque as *my_opaque as *libc::c_void);
|
||||
|
||||
|
||||
sd_markdown_render(ob, s.as_ptr(), s.len() as libc::size_t, markdown);
|
||||
|
@ -118,6 +154,48 @@ fn render(w: &mut io::Writer, s: &str) {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn find_testable_code(doc: &str, tests: &mut ::test::Collector) {
|
||||
extern fn block(_ob: *buf, text: *buf, lang: *buf, opaque: *libc::c_void) {
|
||||
unsafe {
|
||||
if text.is_null() || lang.is_null() { return }
|
||||
let (test, shouldfail, ignore) =
|
||||
vec::raw::buf_as_slice((*lang).data,
|
||||
(*lang).size as uint, |lang| {
|
||||
let s = str::from_utf8(lang);
|
||||
(s.contains("rust"), s.contains("should_fail"),
|
||||
s.contains("ignore"))
|
||||
});
|
||||
if !test { return }
|
||||
vec::raw::buf_as_slice((*text).data, (*text).size as uint, |text| {
|
||||
let tests: &mut ::test::Collector = intrinsics::transmute(opaque);
|
||||
let text = str::from_utf8(text);
|
||||
let mut lines = text.lines().map(|l| l.trim_chars(&'#'));
|
||||
let text = lines.to_owned_vec().connect("\n");
|
||||
tests.add_test(text, ignore, shouldfail);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let ob = bufnew(OUTPUT_UNIT);
|
||||
let extensions = MKDEXT_NO_INTRA_EMPHASIS | MKDEXT_TABLES |
|
||||
MKDEXT_FENCED_CODE | MKDEXT_AUTOLINK |
|
||||
MKDEXT_STRIKETHROUGH;
|
||||
let callbacks = sd_callbacks {
|
||||
blockcode: block,
|
||||
other: intrinsics::init()
|
||||
};
|
||||
|
||||
let tests = tests as *mut ::test::Collector as *libc::c_void;
|
||||
let markdown = sd_markdown_new(extensions, 16, &callbacks, tests);
|
||||
|
||||
sd_markdown_render(ob, doc.as_ptr(), doc.len() as libc::size_t,
|
||||
markdown);
|
||||
sd_markdown_free(markdown);
|
||||
bufrelease(ob);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Default for Markdown<'a> {
|
||||
fn fmt(md: &Markdown<'a>, fmt: &mut fmt::Formatter) {
|
||||
// This is actually common enough to special-case
|
||||
|
|
|
@ -47,6 +47,7 @@ pub mod html {
|
|||
pub mod passes;
|
||||
pub mod plugins;
|
||||
pub mod visit_ast;
|
||||
pub mod test;
|
||||
|
||||
pub static SCHEMA_VERSION: &'static str = "0.8.1";
|
||||
|
||||
|
@ -100,6 +101,9 @@ pub fn opts() -> ~[groups::OptGroup] {
|
|||
optmulti("", "plugins", "space separated list of plugins to also load",
|
||||
"PLUGINS"),
|
||||
optflag("", "no-defaults", "don't run the default passes"),
|
||||
optflag("", "test", "run code examples as tests"),
|
||||
optmulti("", "test-args", "arguments to pass to the test runner",
|
||||
"ARGS"),
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -114,6 +118,19 @@ pub fn main_args(args: &[~str]) -> int {
|
|||
return 0;
|
||||
}
|
||||
|
||||
if matches.free.len() == 0 {
|
||||
println("expected an input file to act on");
|
||||
return 1;
|
||||
} if matches.free.len() > 1 {
|
||||
println("only one input file may be specified");
|
||||
return 1;
|
||||
}
|
||||
let input = matches.free[0].as_slice();
|
||||
|
||||
if matches.opt_present("test") {
|
||||
return test::run(input, &matches);
|
||||
}
|
||||
|
||||
if matches.opt_strs("passes") == ~[~"list"] {
|
||||
println("Available passes for running rustdoc:");
|
||||
for &(name, _, description) in PASSES.iter() {
|
||||
|
@ -126,7 +143,7 @@ pub fn main_args(args: &[~str]) -> int {
|
|||
return 0;
|
||||
}
|
||||
|
||||
let (crate, res) = match acquire_input(&matches) {
|
||||
let (crate, res) = match acquire_input(input, &matches) {
|
||||
Ok(pair) => pair,
|
||||
Err(s) => {
|
||||
println!("input error: {}", s);
|
||||
|
@ -157,14 +174,8 @@ pub fn main_args(args: &[~str]) -> int {
|
|||
|
||||
/// Looks inside the command line arguments to extract the relevant input format
|
||||
/// and files and then generates the necessary rustdoc output for formatting.
|
||||
fn acquire_input(matches: &getopts::Matches) -> Result<Output, ~str> {
|
||||
if matches.free.len() == 0 {
|
||||
return Err(~"expected an input file to act on");
|
||||
} if matches.free.len() > 1 {
|
||||
return Err(~"only one input file may be specified");
|
||||
}
|
||||
|
||||
let input = matches.free[0].as_slice();
|
||||
fn acquire_input(input: &str,
|
||||
matches: &getopts::Matches) -> Result<Output, ~str> {
|
||||
match matches.opt_str("r") {
|
||||
Some(~"rust") => Ok(rust_input(input, matches)),
|
||||
Some(~"json") => json_input(input),
|
||||
|
|
207
src/librustdoc/test.rs
Normal file
207
src/librustdoc/test.rs
Normal file
|
@ -0,0 +1,207 @@
|
|||
// Copyright 2013 The Rust Project Developers. See the COPYRIGHT
|
||||
// file at the top-level directory of this distribution and at
|
||||
// http://rust-lang.org/COPYRIGHT.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
use std::hashmap::HashSet;
|
||||
use std::local_data;
|
||||
use std::os;
|
||||
use std::run;
|
||||
use std::str;
|
||||
|
||||
use extra::tempfile::TempDir;
|
||||
use extra::getopts;
|
||||
use extra::test;
|
||||
use rustc::driver::driver;
|
||||
use rustc::driver::session;
|
||||
use syntax::diagnostic;
|
||||
use syntax::parse;
|
||||
|
||||
use core;
|
||||
use clean;
|
||||
use clean::Clean;
|
||||
use fold::DocFolder;
|
||||
use html::markdown;
|
||||
use passes;
|
||||
use visit_ast::RustdocVisitor;
|
||||
|
||||
pub fn run(input: &str, matches: &getopts::Matches) -> int {
|
||||
let parsesess = parse::new_parse_sess(None);
|
||||
let input = driver::file_input(Path::new(input));
|
||||
let libs = matches.opt_strs("L").map(|s| Path::new(s.as_slice()));
|
||||
let libs = @mut libs.move_iter().collect();
|
||||
|
||||
let sessopts = @session::options {
|
||||
binary: @"rustdoc",
|
||||
maybe_sysroot: Some(@os::self_exe_path().unwrap().dir_path()),
|
||||
addl_lib_search_paths: libs,
|
||||
outputs: ~[session::OutputDylib],
|
||||
.. (*session::basic_options()).clone()
|
||||
};
|
||||
|
||||
|
||||
let diagnostic_handler = diagnostic::mk_handler(None);
|
||||
let span_diagnostic_handler =
|
||||
diagnostic::mk_span_handler(diagnostic_handler, parsesess.cm);
|
||||
|
||||
let sess = driver::build_session_(sessopts,
|
||||
parsesess.cm,
|
||||
@diagnostic::DefaultEmitter as
|
||||
@diagnostic::Emitter,
|
||||
span_diagnostic_handler);
|
||||
|
||||
let cfg = driver::build_configuration(sess);
|
||||
let mut crate = driver::phase_1_parse_input(sess, cfg.clone(), &input);
|
||||
crate = driver::phase_2_configure_and_expand(sess, cfg, crate);
|
||||
|
||||
let ctx = @core::DocContext {
|
||||
crate: crate,
|
||||
tycx: None,
|
||||
sess: sess,
|
||||
};
|
||||
local_data::set(super::ctxtkey, ctx);
|
||||
|
||||
let v = @mut RustdocVisitor::new();
|
||||
v.visit(&ctx.crate);
|
||||
let crate = v.clean();
|
||||
let (crate, _) = passes::unindent_comments(crate);
|
||||
let (crate, _) = passes::collapse_docs(crate);
|
||||
|
||||
let mut collector = Collector {
|
||||
tests: ~[],
|
||||
names: ~[],
|
||||
cnt: 0,
|
||||
libs: libs,
|
||||
cratename: crate.name.to_owned(),
|
||||
};
|
||||
collector.fold_crate(crate);
|
||||
|
||||
let args = matches.opt_strs("test-args");
|
||||
let mut args = args.iter().flat_map(|s| s.words()).map(|s| s.to_owned());
|
||||
let mut args = args.to_owned_vec();
|
||||
args.unshift(~"rustdoctest");
|
||||
|
||||
test::test_main(args, collector.tests);
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
fn runtest(test: &str, cratename: &str, libs: HashSet<Path>) {
|
||||
let test = maketest(test, cratename);
|
||||
let parsesess = parse::new_parse_sess(None);
|
||||
let input = driver::str_input(test);
|
||||
|
||||
let sessopts = @session::options {
|
||||
binary: @"rustdoctest",
|
||||
maybe_sysroot: Some(@os::self_exe_path().unwrap().dir_path()),
|
||||
addl_lib_search_paths: @mut libs,
|
||||
outputs: ~[session::OutputExecutable],
|
||||
debugging_opts: session::prefer_dynamic,
|
||||
.. (*session::basic_options()).clone()
|
||||
};
|
||||
|
||||
let diagnostic_handler = diagnostic::mk_handler(None);
|
||||
let span_diagnostic_handler =
|
||||
diagnostic::mk_span_handler(diagnostic_handler, parsesess.cm);
|
||||
|
||||
let sess = driver::build_session_(sessopts,
|
||||
parsesess.cm,
|
||||
@diagnostic::DefaultEmitter as
|
||||
@diagnostic::Emitter,
|
||||
span_diagnostic_handler);
|
||||
|
||||
let outdir = TempDir::new("rustdoctest").expect("rustdoc needs a tempdir");
|
||||
let out = Some(outdir.path().clone());
|
||||
let cfg = driver::build_configuration(sess);
|
||||
driver::compile_input(sess, cfg, &input, &out, &None);
|
||||
|
||||
let exe = outdir.path().join("rust_out");
|
||||
let out = run::process_output(exe.as_str().unwrap(), []);
|
||||
match out {
|
||||
None => fail!("couldn't run the test"),
|
||||
Some(out) => {
|
||||
if !out.status.success() {
|
||||
fail!("test executable failed:\n{}",
|
||||
str::from_utf8(out.error));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn maketest(s: &str, cratename: &str) -> @str {
|
||||
let mut prog = ~r"
|
||||
#[deny(warnings)];
|
||||
#[allow(unused_variable, dead_assignment, unused_mut, attribute_usage, dead_code)];
|
||||
#[feature(macro_rules, globs, struct_variant, managed_boxes)];
|
||||
";
|
||||
if s.contains("extra") {
|
||||
prog.push_str("extern mod extra;\n");
|
||||
}
|
||||
if s.contains(cratename) {
|
||||
prog.push_str(format!("extern mod {};\n", cratename));
|
||||
}
|
||||
if s.contains("fn main") {
|
||||
prog.push_str(s);
|
||||
} else {
|
||||
prog.push_str("fn main() {\n");
|
||||
prog.push_str(s);
|
||||
prog.push_str("\n}");
|
||||
}
|
||||
|
||||
return prog.to_managed();
|
||||
}
|
||||
|
||||
pub struct Collector {
|
||||
priv tests: ~[test::TestDescAndFn],
|
||||
priv names: ~[~str],
|
||||
priv libs: @mut HashSet<Path>,
|
||||
priv cnt: uint,
|
||||
priv cratename: ~str,
|
||||
}
|
||||
|
||||
impl Collector {
|
||||
pub fn add_test(&mut self, test: &str, ignore: bool, should_fail: bool) {
|
||||
let test = test.to_owned();
|
||||
let name = format!("{}_{}", self.names.connect("::"), self.cnt);
|
||||
self.cnt += 1;
|
||||
let libs = (*self.libs).clone();
|
||||
let cratename = self.cratename.to_owned();
|
||||
self.tests.push(test::TestDescAndFn {
|
||||
desc: test::TestDesc {
|
||||
name: test::DynTestName(name),
|
||||
ignore: ignore,
|
||||
should_fail: should_fail,
|
||||
},
|
||||
testfn: test::DynTestFn(proc() {
|
||||
runtest(test, cratename, libs);
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl DocFolder for Collector {
|
||||
fn fold_item(&mut self, item: clean::Item) -> Option<clean::Item> {
|
||||
let pushed = match item.name {
|
||||
Some(ref name) if name.len() == 0 => false,
|
||||
Some(ref name) => { self.names.push(name.to_owned()); true }
|
||||
None => false
|
||||
};
|
||||
match item.doc_value() {
|
||||
Some(doc) => {
|
||||
self.cnt = 0;
|
||||
markdown::find_testable_code(doc, self);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
let ret = self.fold_item_recur(item);
|
||||
if pushed {
|
||||
self.names.pop();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
|
@ -12,11 +12,10 @@
|
|||
//! usable for clean
|
||||
|
||||
use syntax::abi::AbiSet;
|
||||
use syntax::{ast, ast_map};
|
||||
use syntax::ast;
|
||||
use syntax::codemap::Span;
|
||||
|
||||
use doctree::*;
|
||||
use std::local_data;
|
||||
|
||||
pub struct RustdocVisitor {
|
||||
module: Module,
|
||||
|
@ -91,15 +90,8 @@ impl RustdocVisitor {
|
|||
}
|
||||
|
||||
fn visit_mod_contents(span: Span, attrs: ~[ast::Attribute], vis:
|
||||
ast::visibility, id: ast::NodeId, m: &ast::_mod) -> Module {
|
||||
let am = local_data::get(super::ctxtkey, |x| *x.unwrap()).tycx.items;
|
||||
let name = match am.find(&id) {
|
||||
Some(m) => match m {
|
||||
&ast_map::node_item(ref it, _) => Some(it.ident),
|
||||
_ => fail!("mod id mapped to non-item in the ast map")
|
||||
},
|
||||
None => None
|
||||
};
|
||||
ast::visibility, id: ast::NodeId, m: &ast::_mod,
|
||||
name: Option<ast::Ident>) -> Module {
|
||||
let mut om = Module::new(name);
|
||||
om.view_items = m.view_items.clone();
|
||||
om.where = span;
|
||||
|
@ -117,7 +109,8 @@ impl RustdocVisitor {
|
|||
match item.node {
|
||||
ast::item_mod(ref m) => {
|
||||
om.mods.push(visit_mod_contents(item.span, item.attrs.clone(),
|
||||
item.vis, item.id, m));
|
||||
item.vis, item.id, m,
|
||||
Some(item.ident)));
|
||||
},
|
||||
ast::item_enum(ref ed, ref gen) => om.enums.push(visit_enum_def(item, ed, gen)),
|
||||
ast::item_struct(sd, ref gen) => om.structs.push(visit_struct_def(item, sd, gen)),
|
||||
|
@ -182,6 +175,7 @@ impl RustdocVisitor {
|
|||
}
|
||||
|
||||
self.module = visit_mod_contents(crate.span, crate.attrs.clone(),
|
||||
ast::public, ast::CRATE_NODE_ID, &crate.module);
|
||||
ast::public, ast::CRATE_NODE_ID,
|
||||
&crate.module, None);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue