]> git.lizzy.rs Git - rust.git/commitdiff
rustdoc: Add the ability to test code in comments
authorAlex Crichton <alex@alexcrichton.com>
Sun, 22 Dec 2013 19:23:04 +0000 (11:23 -0800)
committerAlex Crichton <alex@alexcrichton.com>
Mon, 23 Dec 2013 17:10:36 +0000 (09:10 -0800)
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.

src/librustdoc/clean.rs
src/librustdoc/core.rs
src/librustdoc/html/markdown.rs
src/librustdoc/lib.rs
src/librustdoc/test.rs [new file with mode: 0644]
src/librustdoc/visit_ast.rs

index dd921fb9e93b5d66ef66218aa8a81b86ab89eeb0..1111be5f417ec8fec7a635afb223c7d7afcdf961 100644 (file)
@@ -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
+    }
 }
index b0ff224badbfe17d3018c80d3bbcb2238873cea8..5c3d3484657349da3e03e2363f3e5e848a099596 100644 (file)
@@ -27,7 +27,7 @@
 
 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();
index 513144ad73132f5f1ded439e94ed698a355e1aeb..6fd83af3b2e760c7bac51f97b571be70a38a84aa 100644 (file)
 //! // ... 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
 
 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 @@ fn sd_markdown_render(ob: *buf,
 
 }
 
-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
index 3645161ee1cea871b0abd6595ebe586fb89802e5..3d9a0a73e57545e37e0590f2152dcf6cc0fe6667 100644 (file)
@@ -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),
diff --git a/src/librustdoc/test.rs b/src/librustdoc/test.rs
new file mode 100644 (file)
index 0000000..9462f81
--- /dev/null
@@ -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;
+    }
+}
index 15dbc68b2ea1158d5dde025a1153d75c551f19ad..03ab85918c759fe3a5df2fc45cc90d2793be2e53 100644 (file)
 //! 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 @@ fn visit_fn(item: &ast::item, fd: &ast::fn_decl, purity: &ast::purity,
         }
 
         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 @@ fn visit_item(item: &ast::item, om: &mut Module) {
             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 @@ fn visit_item(item: &ast::item, om: &mut Module) {
         }
 
         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);
     }
 }