1
Fork 0

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:
Alex Crichton 2013-12-22 11:23:04 -08:00
parent f71c0dc2cd
commit 6c9c045064
6 changed files with 335 additions and 40 deletions

View file

@ -1147,13 +1147,17 @@ fn name_from_pat(p: &ast::Pat) -> ~str {
fn resolve_type(path: Path, tpbs: Option<~[TyParamBound]>, fn resolve_type(path: Path, tpbs: Option<~[TyParamBound]>,
id: ast::NodeId) -> Type { id: ast::NodeId) -> Type {
let cx = local_data::get(super::ctxtkey, |x| *x.unwrap()); 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); 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, Some(k) => k,
None => { None => {
let ctxt = local_data::get(super::ctxtkey, |x| *x.unwrap());
debug!("could not find {:?} in defmap (`{}`)", id, 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!)") 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) { if ast_util::is_local(def_id) {
ResolvedPath{ path: path, typarams: tpbs, id: def_id.node } ResolvedPath{ path: path, typarams: tpbs, id: def_id.node }
} else { } 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| { let fqn = fqn.move_iter().map(|i| {
match i { match i {
ast_map::path_mod(id) | 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> { fn resolve_def(id: ast::NodeId) -> Option<ast::DefId> {
let dm = local_data::get(super::ctxtkey, |x| *x.unwrap()).tycx.def_map; let cx = local_data::get(super::ctxtkey, |x| *x.unwrap());
dm.find(&id).map(|&d| ast_util::def_id_of_def(d)) match cx.tycx {
Some(tcx) => {
tcx.def_map.find(&id).map(|&d| ast_util::def_id_of_def(d))
}
None => None
}
} }

View file

@ -27,7 +27,7 @@ use clean::Clean;
pub struct DocContext { pub struct DocContext {
crate: ast::Crate, crate: ast::Crate,
tycx: middle::ty::ctxt, tycx: Option<middle::ty::ctxt>,
sess: driver::session::Session sess: driver::session::Session
} }
@ -78,17 +78,13 @@ fn get_ast_and_resolve(cpath: &Path,
} = phase_3_run_analysis_passes(sess, &crate); } = phase_3_run_analysis_passes(sess, &crate);
debug!("crate: {:?}", 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 }); CrateAnalysis { exported_items: exported_items });
} }
pub fn run_core (libs: HashSet<Path>, cfgs: ~[~str], path: &Path) -> (clean::Crate, CrateAnalysis) { 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, analysis) = get_ast_and_resolve(path, libs, cfgs);
let ctxt = @ctxt; let ctxt = @ctxt;
debug!("defmap:");
for (k, v) in ctxt.tycx.def_map.iter() {
debug!("{:?}: {:?}", k, v);
}
local_data::set(super::ctxtkey, ctxt); local_data::set(super::ctxtkey, ctxt);
let v = @mut RustdocVisitor::new(); let v = @mut RustdocVisitor::new();

View file

@ -22,9 +22,12 @@
//! // ... something using html //! // ... something using html
//! ``` //! ```
use std::cast;
use std::fmt; use std::fmt;
use std::libc;
use std::io; use std::io;
use std::libc;
use std::str;
use std::unstable::intrinsics;
use std::vec; use std::vec;
/// A unit struct which has the `fmt::Default` trait implemented. When /// 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 type sd_markdown = libc::c_void; // this is opaque to us
// this is a large struct of callbacks we don't use struct sd_callbacks {
type sd_callbacks = [libc::size_t, ..26]; blockcode: extern "C" fn(*buf, *buf, *buf, *libc::c_void),
other: [libc::size_t, ..25],
}
struct html_toc_data { struct html_toc_data {
header_count: libc::c_int, header_count: libc::c_int,
@ -56,6 +61,11 @@ struct html_renderopt {
link_attributes: Option<extern "C" fn(*buf, *buf, *libc::c_void)>, 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 { struct buf {
data: *u8, data: *u8,
size: libc::size_t, 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 // This code is all lifted from examples/sundown.c in the sundown repo
unsafe { unsafe {
let ob = bufnew(OUTPUT_UNIT); let ob = bufnew(OUTPUT_UNIT);
@ -100,11 +131,16 @@ fn render(w: &mut io::Writer, s: &str) {
flags: 0, flags: 0,
link_attributes: None, link_attributes: None,
}; };
let callbacks: sd_callbacks = [0, ..26]; let mut callbacks: sd_callbacks = intrinsics::init();
sdhtml_renderer(&callbacks, &options, 0); 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, 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); 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> { impl<'a> fmt::Default for Markdown<'a> {
fn fmt(md: &Markdown<'a>, fmt: &mut fmt::Formatter) { fn fmt(md: &Markdown<'a>, fmt: &mut fmt::Formatter) {
// This is actually common enough to special-case // This is actually common enough to special-case

View file

@ -47,6 +47,7 @@ pub mod html {
pub mod passes; pub mod passes;
pub mod plugins; pub mod plugins;
pub mod visit_ast; pub mod visit_ast;
pub mod test;
pub static SCHEMA_VERSION: &'static str = "0.8.1"; 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", optmulti("", "plugins", "space separated list of plugins to also load",
"PLUGINS"), "PLUGINS"),
optflag("", "no-defaults", "don't run the default passes"), 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; 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"] { if matches.opt_strs("passes") == ~[~"list"] {
println("Available passes for running rustdoc:"); println("Available passes for running rustdoc:");
for &(name, _, description) in PASSES.iter() { for &(name, _, description) in PASSES.iter() {
@ -126,7 +143,7 @@ pub fn main_args(args: &[~str]) -> int {
return 0; return 0;
} }
let (crate, res) = match acquire_input(&matches) { let (crate, res) = match acquire_input(input, &matches) {
Ok(pair) => pair, Ok(pair) => pair,
Err(s) => { Err(s) => {
println!("input error: {}", 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 /// Looks inside the command line arguments to extract the relevant input format
/// and files and then generates the necessary rustdoc output for formatting. /// and files and then generates the necessary rustdoc output for formatting.
fn acquire_input(matches: &getopts::Matches) -> Result<Output, ~str> { fn acquire_input(input: &str,
if matches.free.len() == 0 { matches: &getopts::Matches) -> Result<Output, ~str> {
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();
match matches.opt_str("r") { match matches.opt_str("r") {
Some(~"rust") => Ok(rust_input(input, matches)), Some(~"rust") => Ok(rust_input(input, matches)),
Some(~"json") => json_input(input), Some(~"json") => json_input(input),

207
src/librustdoc/test.rs Normal file
View 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;
}
}

View file

@ -12,11 +12,10 @@
//! usable for clean //! usable for clean
use syntax::abi::AbiSet; use syntax::abi::AbiSet;
use syntax::{ast, ast_map}; use syntax::ast;
use syntax::codemap::Span; use syntax::codemap::Span;
use doctree::*; use doctree::*;
use std::local_data;
pub struct RustdocVisitor { pub struct RustdocVisitor {
module: Module, module: Module,
@ -91,15 +90,8 @@ impl RustdocVisitor {
} }
fn visit_mod_contents(span: Span, attrs: ~[ast::Attribute], vis: fn visit_mod_contents(span: Span, attrs: ~[ast::Attribute], vis:
ast::visibility, id: ast::NodeId, m: &ast::_mod) -> Module { ast::visibility, id: ast::NodeId, m: &ast::_mod,
let am = local_data::get(super::ctxtkey, |x| *x.unwrap()).tycx.items; name: Option<ast::Ident>) -> Module {
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
};
let mut om = Module::new(name); let mut om = Module::new(name);
om.view_items = m.view_items.clone(); om.view_items = m.view_items.clone();
om.where = span; om.where = span;
@ -117,7 +109,8 @@ impl RustdocVisitor {
match item.node { match item.node {
ast::item_mod(ref m) => { ast::item_mod(ref m) => {
om.mods.push(visit_mod_contents(item.span, item.attrs.clone(), 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_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)), 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(), 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);
} }
} }