Skip to content

Commit 6c9c045

Browse files
committed
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.
1 parent f71c0dc commit 6c9c045

File tree

6 files changed

+335
-40
lines changed

6 files changed

+335
-40
lines changed

src/librustdoc/clean.rs

+15-6
Original file line numberDiff line numberDiff line change
@@ -1147,13 +1147,17 @@ fn name_from_pat(p: &ast::Pat) -> ~str {
11471147
fn resolve_type(path: Path, tpbs: Option<~[TyParamBound]>,
11481148
id: ast::NodeId) -> Type {
11491149
let cx = local_data::get(super::ctxtkey, |x| *x.unwrap());
1150+
let tycx = match cx.tycx {
1151+
Some(tycx) => tycx,
1152+
// If we're extracting tests, this return value doesn't matter.
1153+
None => return Bool
1154+
};
11501155
debug!("searching for {:?} in defmap", id);
1151-
let d = match cx.tycx.def_map.find(&id) {
1156+
let d = match tycx.def_map.find(&id) {
11521157
Some(k) => k,
11531158
None => {
1154-
let ctxt = local_data::get(super::ctxtkey, |x| *x.unwrap());
11551159
debug!("could not find {:?} in defmap (`{}`)", id,
1156-
syntax::ast_map::node_id_to_str(ctxt.tycx.items, id, ctxt.sess.intr()));
1160+
syntax::ast_map::node_id_to_str(tycx.items, id, cx.sess.intr()));
11571161
fail!("Unexpected failure: unresolved id not in defmap (this is a bug!)")
11581162
}
11591163
};
@@ -1182,7 +1186,7 @@ fn resolve_type(path: Path, tpbs: Option<~[TyParamBound]>,
11821186
if ast_util::is_local(def_id) {
11831187
ResolvedPath{ path: path, typarams: tpbs, id: def_id.node }
11841188
} else {
1185-
let fqn = csearch::get_item_path(cx.tycx, def_id);
1189+
let fqn = csearch::get_item_path(tycx, def_id);
11861190
let fqn = fqn.move_iter().map(|i| {
11871191
match i {
11881192
ast_map::path_mod(id) |
@@ -1203,6 +1207,11 @@ fn resolve_use_source(path: Path, id: ast::NodeId) -> ImportSource {
12031207
}
12041208

12051209
fn resolve_def(id: ast::NodeId) -> Option<ast::DefId> {
1206-
let dm = local_data::get(super::ctxtkey, |x| *x.unwrap()).tycx.def_map;
1207-
dm.find(&id).map(|&d| ast_util::def_id_of_def(d))
1210+
let cx = local_data::get(super::ctxtkey, |x| *x.unwrap());
1211+
match cx.tycx {
1212+
Some(tcx) => {
1213+
tcx.def_map.find(&id).map(|&d| ast_util::def_id_of_def(d))
1214+
}
1215+
None => None
1216+
}
12081217
}

src/librustdoc/core.rs

+2-6
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use clean::Clean;
2727

2828
pub struct DocContext {
2929
crate: ast::Crate,
30-
tycx: middle::ty::ctxt,
30+
tycx: Option<middle::ty::ctxt>,
3131
sess: driver::session::Session
3232
}
3333

@@ -78,17 +78,13 @@ fn get_ast_and_resolve(cpath: &Path,
7878
} = phase_3_run_analysis_passes(sess, &crate);
7979

8080
debug!("crate: {:?}", crate);
81-
return (DocContext { crate: crate, tycx: ty_cx, sess: sess },
81+
return (DocContext { crate: crate, tycx: Some(ty_cx), sess: sess },
8282
CrateAnalysis { exported_items: exported_items });
8383
}
8484

8585
pub fn run_core (libs: HashSet<Path>, cfgs: ~[~str], path: &Path) -> (clean::Crate, CrateAnalysis) {
8686
let (ctxt, analysis) = get_ast_and_resolve(path, libs, cfgs);
8787
let ctxt = @ctxt;
88-
debug!("defmap:");
89-
for (k, v) in ctxt.tycx.def_map.iter() {
90-
debug!("{:?}: {:?}", k, v);
91-
}
9288
local_data::set(super::ctxtkey, ctxt);
9389

9490
let v = @mut RustdocVisitor::new();

src/librustdoc/html/markdown.rs

+84-6
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@
2222
//! // ... something using html
2323
//! ```
2424
25+
use std::cast;
2526
use std::fmt;
26-
use std::libc;
2727
use std::io;
28+
use std::libc;
29+
use std::str;
30+
use std::unstable::intrinsics;
2831
use std::vec;
2932

3033
/// A unit struct which has the `fmt::Default` trait implemented. When
@@ -41,8 +44,10 @@ static MKDEXT_STRIKETHROUGH: libc::c_uint = 1 << 4;
4144

4245
type sd_markdown = libc::c_void; // this is opaque to us
4346

44-
// this is a large struct of callbacks we don't use
45-
type sd_callbacks = [libc::size_t, ..26];
47+
struct sd_callbacks {
48+
blockcode: extern "C" fn(*buf, *buf, *buf, *libc::c_void),
49+
other: [libc::size_t, ..25],
50+
}
4651

4752
struct html_toc_data {
4853
header_count: libc::c_int,
@@ -56,6 +61,11 @@ struct html_renderopt {
5661
link_attributes: Option<extern "C" fn(*buf, *buf, *libc::c_void)>,
5762
}
5863

64+
struct my_opaque {
65+
opt: html_renderopt,
66+
dfltblk: extern "C" fn(*buf, *buf, *buf, *libc::c_void),
67+
}
68+
5969
struct buf {
6070
data: *u8,
6171
size: libc::size_t,
@@ -84,7 +94,28 @@ extern {
8494

8595
}
8696

87-
fn render(w: &mut io::Writer, s: &str) {
97+
pub fn render(w: &mut io::Writer, s: &str) {
98+
extern fn block(ob: *buf, text: *buf, lang: *buf, opaque: *libc::c_void) {
99+
unsafe {
100+
let my_opaque: &my_opaque = cast::transmute(opaque);
101+
vec::raw::buf_as_slice((*text).data, (*text).size as uint, |text| {
102+
let text = str::from_utf8(text);
103+
let mut lines = text.lines().filter(|l| {
104+
!l.trim().starts_with("#")
105+
});
106+
let text = lines.to_owned_vec().connect("\n");
107+
108+
let buf = buf {
109+
data: text.as_bytes().as_ptr(),
110+
size: text.len() as libc::size_t,
111+
asize: text.len() as libc::size_t,
112+
unit: 0,
113+
};
114+
(my_opaque.dfltblk)(ob, &buf, lang, opaque);
115+
})
116+
}
117+
}
118+
88119
// This code is all lifted from examples/sundown.c in the sundown repo
89120
unsafe {
90121
let ob = bufnew(OUTPUT_UNIT);
@@ -100,11 +131,16 @@ fn render(w: &mut io::Writer, s: &str) {
100131
flags: 0,
101132
link_attributes: None,
102133
};
103-
let callbacks: sd_callbacks = [0, ..26];
134+
let mut callbacks: sd_callbacks = intrinsics::init();
104135

105136
sdhtml_renderer(&callbacks, &options, 0);
137+
let opaque = my_opaque {
138+
opt: options,
139+
dfltblk: callbacks.blockcode,
140+
};
141+
callbacks.blockcode = block;
106142
let markdown = sd_markdown_new(extensions, 16, &callbacks,
107-
&options as *html_renderopt as *libc::c_void);
143+
&opaque as *my_opaque as *libc::c_void);
108144

109145

110146
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) {
118154
}
119155
}
120156

157+
pub fn find_testable_code(doc: &str, tests: &mut ::test::Collector) {
158+
extern fn block(_ob: *buf, text: *buf, lang: *buf, opaque: *libc::c_void) {
159+
unsafe {
160+
if text.is_null() || lang.is_null() { return }
161+
let (test, shouldfail, ignore) =
162+
vec::raw::buf_as_slice((*lang).data,
163+
(*lang).size as uint, |lang| {
164+
let s = str::from_utf8(lang);
165+
(s.contains("rust"), s.contains("should_fail"),
166+
s.contains("ignore"))
167+
});
168+
if !test { return }
169+
vec::raw::buf_as_slice((*text).data, (*text).size as uint, |text| {
170+
let tests: &mut ::test::Collector = intrinsics::transmute(opaque);
171+
let text = str::from_utf8(text);
172+
let mut lines = text.lines().map(|l| l.trim_chars(&'#'));
173+
let text = lines.to_owned_vec().connect("\n");
174+
tests.add_test(text, ignore, shouldfail);
175+
})
176+
}
177+
}
178+
179+
unsafe {
180+
let ob = bufnew(OUTPUT_UNIT);
181+
let extensions = MKDEXT_NO_INTRA_EMPHASIS | MKDEXT_TABLES |
182+
MKDEXT_FENCED_CODE | MKDEXT_AUTOLINK |
183+
MKDEXT_STRIKETHROUGH;
184+
let callbacks = sd_callbacks {
185+
blockcode: block,
186+
other: intrinsics::init()
187+
};
188+
189+
let tests = tests as *mut ::test::Collector as *libc::c_void;
190+
let markdown = sd_markdown_new(extensions, 16, &callbacks, tests);
191+
192+
sd_markdown_render(ob, doc.as_ptr(), doc.len() as libc::size_t,
193+
markdown);
194+
sd_markdown_free(markdown);
195+
bufrelease(ob);
196+
}
197+
}
198+
121199
impl<'a> fmt::Default for Markdown<'a> {
122200
fn fmt(md: &Markdown<'a>, fmt: &mut fmt::Formatter) {
123201
// This is actually common enough to special-case

src/librustdoc/lib.rs

+20-9
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub mod html {
4747
pub mod passes;
4848
pub mod plugins;
4949
pub mod visit_ast;
50+
pub mod test;
5051

5152
pub static SCHEMA_VERSION: &'static str = "0.8.1";
5253

@@ -100,6 +101,9 @@ pub fn opts() -> ~[groups::OptGroup] {
100101
optmulti("", "plugins", "space separated list of plugins to also load",
101102
"PLUGINS"),
102103
optflag("", "no-defaults", "don't run the default passes"),
104+
optflag("", "test", "run code examples as tests"),
105+
optmulti("", "test-args", "arguments to pass to the test runner",
106+
"ARGS"),
103107
]
104108
}
105109

@@ -114,6 +118,19 @@ pub fn main_args(args: &[~str]) -> int {
114118
return 0;
115119
}
116120

121+
if matches.free.len() == 0 {
122+
println("expected an input file to act on");
123+
return 1;
124+
} if matches.free.len() > 1 {
125+
println("only one input file may be specified");
126+
return 1;
127+
}
128+
let input = matches.free[0].as_slice();
129+
130+
if matches.opt_present("test") {
131+
return test::run(input, &matches);
132+
}
133+
117134
if matches.opt_strs("passes") == ~[~"list"] {
118135
println("Available passes for running rustdoc:");
119136
for &(name, _, description) in PASSES.iter() {
@@ -126,7 +143,7 @@ pub fn main_args(args: &[~str]) -> int {
126143
return 0;
127144
}
128145

129-
let (crate, res) = match acquire_input(&matches) {
146+
let (crate, res) = match acquire_input(input, &matches) {
130147
Ok(pair) => pair,
131148
Err(s) => {
132149
println!("input error: {}", s);
@@ -157,14 +174,8 @@ pub fn main_args(args: &[~str]) -> int {
157174

158175
/// Looks inside the command line arguments to extract the relevant input format
159176
/// and files and then generates the necessary rustdoc output for formatting.
160-
fn acquire_input(matches: &getopts::Matches) -> Result<Output, ~str> {
161-
if matches.free.len() == 0 {
162-
return Err(~"expected an input file to act on");
163-
} if matches.free.len() > 1 {
164-
return Err(~"only one input file may be specified");
165-
}
166-
167-
let input = matches.free[0].as_slice();
177+
fn acquire_input(input: &str,
178+
matches: &getopts::Matches) -> Result<Output, ~str> {
168179
match matches.opt_str("r") {
169180
Some(~"rust") => Ok(rust_input(input, matches)),
170181
Some(~"json") => json_input(input),

0 commit comments

Comments
 (0)