diff --git a/src/librustdoc/clean.rs b/src/librustdoc/clean.rs index dd921fb9e93..1111be5f417 100644 --- a/src/librustdoc/clean.rs +++ b/src/librustdoc/clean.rs @@ -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 { - 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 + } } diff --git a/src/librustdoc/core.rs b/src/librustdoc/core.rs index b0ff224badb..5c3d3484657 100644 --- a/src/librustdoc/core.rs +++ b/src/librustdoc/core.rs @@ -27,7 +27,7 @@ use clean::Clean; pub struct DocContext { crate: ast::Crate, - tycx: middle::ty::ctxt, + tycx: Option, 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, 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(); diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 513144ad731..6fd83af3b2e 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -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, } +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 diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index 3645161ee1c..3d9a0a73e57 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -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 { - 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 { match matches.opt_str("r") { Some(~"rust") => Ok(rust_input(input, matches)), Some(~"json") => json_input(input), diff --git a/src/librustdoc/test.rs b/src/librustdoc/test.rs new file mode 100644 index 00000000000..9462f8118ba --- /dev/null +++ b/src/librustdoc/test.rs @@ -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 or the MIT license +// , 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) { + 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, + 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 { + 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; + } +} diff --git a/src/librustdoc/visit_ast.rs b/src/librustdoc/visit_ast.rs index 15dbc68b2ea..03ab85918c7 100644 --- a/src/librustdoc/visit_ast.rs +++ b/src/librustdoc/visit_ast.rs @@ -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) -> 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); } }