Skip to content

Commit

Permalink
rustdoc: Add the ability to test code in comments
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
alexcrichton committed Dec 23, 2013
1 parent f71c0dc commit 6c9c045
Show file tree
Hide file tree
Showing 6 changed files with 335 additions and 40 deletions.
21 changes: 15 additions & 6 deletions src/librustdoc/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!)")
}
};
Expand Down Expand Up @@ -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) |
Expand All @@ -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
}
}
8 changes: 2 additions & 6 deletions src/librustdoc/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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();
Expand Down
90 changes: 84 additions & 6 deletions src/librustdoc/html/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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
Expand Down
29 changes: 20 additions & 9 deletions src/librustdoc/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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"),
]
}

Expand All @@ -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() {
Expand All @@ -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);
Expand Down Expand Up @@ -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),
Expand Down
Loading

0 comments on commit 6c9c045

Please sign in to comment.