]>
Commit | Line | Data |
---|---|---|
1a4d82fc JJ |
1 | // Copyright 2013-2014 The Rust Project Developers. See the COPYRIGHT |
2 | // file at the top-level directory of this distribution and at | |
3 | // http://rust-lang.org/COPYRIGHT. | |
4 | // | |
5 | // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or | |
6 | // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license | |
7 | // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your | |
8 | // option. This file may not be copied, modified, or distributed | |
9 | // except according to those terms. | |
10 | ||
85aaf69f | 11 | use std::env; |
c34b1796 AL |
12 | use std::ffi::OsString; |
13 | use std::io::prelude::*; | |
14 | use std::io; | |
8bb4bdeb | 15 | use std::path::{Path, PathBuf}; |
54a0048b | 16 | use std::panic::{self, AssertUnwindSafe}; |
c34b1796 | 17 | use std::process::Command; |
1a4d82fc | 18 | use std::str; |
0531ce1d | 19 | use rustc_data_structures::sync::Lrc; |
c34b1796 | 20 | use std::sync::{Arc, Mutex}; |
1a4d82fc | 21 | |
1a4d82fc | 22 | use testing; |
c34b1796 | 23 | use rustc_lint; |
476ff2be SL |
24 | use rustc::hir; |
25 | use rustc::hir::intravisit; | |
041b39d2 | 26 | use rustc::session::{self, CompileIncomplete, config}; |
83c7162d | 27 | use rustc::session::config::{OutputType, OutputTypes, Externs, CodegenOptions}; |
1a4d82fc | 28 | use rustc::session::search_paths::{SearchPaths, PathKind}; |
ff7c6d11 | 29 | use rustc_metadata::dynamic_lib::DynamicLibrary; |
8faf50e0 | 30 | use tempfile::Builder as TempFileBuilder; |
83c7162d | 31 | use rustc_driver::{self, driver, target_features, Compilation}; |
3157f602 | 32 | use rustc_driver::driver::phase_2_configure_and_expand; |
92a42be0 | 33 | use rustc_metadata::cstore::CStore; |
3157f602 | 34 | use rustc_resolve::MakeGlobMap; |
476ff2be | 35 | use syntax::ast; |
b7449926 | 36 | use syntax::source_map::SourceMap; |
0531ce1d | 37 | use syntax::edition::Edition; |
9e0c209e | 38 | use syntax::feature_gate::UnstableFeatures; |
0531ce1d | 39 | use syntax::with_globals; |
b7449926 | 40 | use syntax_pos::{BytePos, DUMMY_SP, Pos, Span, FileName}; |
3157f602 XL |
41 | use errors; |
42 | use errors::emitter::ColorConfig; | |
1a4d82fc | 43 | |
476ff2be | 44 | use clean::Attributes; |
b7449926 | 45 | use html::markdown::{self, ErrorCodes, LangString}; |
1a4d82fc | 46 | |
9346a6ac AL |
47 | #[derive(Clone, Default)] |
48 | pub struct TestOptions { | |
83c7162d | 49 | /// Whether to disable the default `extern crate my_crate;` when creating doctests. |
9346a6ac | 50 | pub no_crate_inject: bool, |
83c7162d XL |
51 | /// Whether to emit compilation warnings when compiling doctests. Setting this will suppress |
52 | /// the default `#![allow(unused)]`. | |
53 | pub display_warnings: bool, | |
54 | /// Additional crate-level attributes to add to doctests. | |
9346a6ac AL |
55 | pub attrs: Vec<String>, |
56 | } | |
57 | ||
ff7c6d11 | 58 | pub fn run(input_path: &Path, |
1a4d82fc JJ |
59 | cfgs: Vec<String>, |
60 | libs: SearchPaths, | |
5bcae85e | 61 | externs: Externs, |
1a4d82fc | 62 | mut test_args: Vec<String>, |
32a655c1 | 63 | crate_name: Option<String>, |
cc61c64b | 64 | maybe_sysroot: Option<PathBuf>, |
abe05a73 | 65 | display_warnings: bool, |
0531ce1d | 66 | linker: Option<PathBuf>, |
83c7162d XL |
67 | edition: Edition, |
68 | cg: CodegenOptions) | |
c34b1796 | 69 | -> isize { |
ff7c6d11 | 70 | let input = config::Input::File(input_path.to_owned()); |
1a4d82fc JJ |
71 | |
72 | let sessopts = config::Options { | |
32a655c1 SL |
73 | maybe_sysroot: maybe_sysroot.clone().or_else( |
74 | || Some(env::current_exe().unwrap().parent().unwrap().parent().unwrap().to_path_buf())), | |
1a4d82fc | 75 | search_paths: libs.clone(), |
b7449926 | 76 | crate_types: vec![config::CrateType::Dylib], |
83c7162d | 77 | cg: cg.clone(), |
1a4d82fc | 78 | externs: externs.clone(), |
9e0c209e | 79 | unstable_features: UnstableFeatures::from_environment(), |
3b2f2976 | 80 | lint_cap: Some(::rustc::lint::Level::Allow), |
32a655c1 | 81 | actually_rustdoc: true, |
0531ce1d | 82 | debugging_opts: config::DebuggingOptions { |
0531ce1d XL |
83 | ..config::basic_debugging_options() |
84 | }, | |
83c7162d | 85 | edition, |
b7449926 | 86 | ..config::Options::default() |
1a4d82fc | 87 | }; |
94b46f34 | 88 | driver::spawn_thread_pool(sessopts, |sessopts| { |
b7449926 | 89 | let source_map = Lrc::new(SourceMap::new(sessopts.file_path_mapping())); |
94b46f34 XL |
90 | let handler = |
91 | errors::Handler::with_tty_emitter(ColorConfig::Auto, | |
92 | true, false, | |
b7449926 | 93 | Some(source_map.clone())); |
94b46f34 XL |
94 | |
95 | let mut sess = session::build_session_( | |
b7449926 | 96 | sessopts, Some(input_path.to_owned()), handler, source_map.clone(), |
94b46f34 XL |
97 | ); |
98 | let codegen_backend = rustc_driver::get_codegen_backend(&sess); | |
99 | let cstore = CStore::new(codegen_backend.metadata_loader()); | |
100 | rustc_lint::register_builtins(&mut sess.lint_store.borrow_mut(), Some(&sess)); | |
101 | ||
102 | let mut cfg = config::build_configuration(&sess, config::parse_cfgspecs(cfgs.clone())); | |
103 | target_features::add_configuration(&mut cfg, &sess, &*codegen_backend); | |
104 | sess.parse_sess.config = cfg; | |
105 | ||
106 | let krate = panictry!(driver::phase_1_parse_input(&driver::CompileController::basic(), | |
107 | &sess, | |
108 | &input)); | |
109 | let driver::ExpansionResult { defs, mut hir_forest, .. } = { | |
110 | phase_2_configure_and_expand( | |
111 | &sess, | |
112 | &cstore, | |
113 | krate, | |
114 | None, | |
115 | "rustdoc-test", | |
116 | None, | |
117 | MakeGlobMap::No, | |
118 | |_| Ok(()), | |
119 | ).expect("phase_2_configure_and_expand aborted in rustdoc!") | |
476ff2be | 120 | }; |
94b46f34 XL |
121 | |
122 | let crate_name = crate_name.unwrap_or_else(|| { | |
123 | ::rustc_codegen_utils::link::find_crate_name(None, &hir_forest.krate().attrs, &input) | |
476ff2be | 124 | }); |
94b46f34 XL |
125 | let mut opts = scrape_test_config(hir_forest.krate()); |
126 | opts.display_warnings |= display_warnings; | |
127 | let mut collector = Collector::new( | |
128 | crate_name, | |
129 | cfgs, | |
130 | libs, | |
131 | cg, | |
132 | externs, | |
133 | false, | |
134 | opts, | |
135 | maybe_sysroot, | |
b7449926 | 136 | Some(source_map), |
94b46f34 XL |
137 | None, |
138 | linker, | |
139 | edition | |
140 | ); | |
141 | ||
142 | { | |
143 | let map = hir::map::map_crate(&sess, &cstore, &mut hir_forest, &defs); | |
144 | let krate = map.krate(); | |
145 | let mut hir_collector = HirCollector { | |
146 | sess: &sess, | |
147 | collector: &mut collector, | |
b7449926 XL |
148 | map: &map, |
149 | codes: ErrorCodes::from(sess.opts.unstable_features.is_nightly_build()), | |
94b46f34 XL |
150 | }; |
151 | hir_collector.visit_testable("".to_string(), &krate.attrs, |this| { | |
152 | intravisit::walk_crate(this, krate); | |
153 | }); | |
154 | } | |
1a4d82fc | 155 | |
94b46f34 | 156 | test_args.insert(0, "rustdoctest".to_string()); |
1a4d82fc | 157 | |
94b46f34 XL |
158 | testing::test_main(&test_args, |
159 | collector.tests.into_iter().collect(), | |
160 | testing::Options::new().display_output(display_warnings)); | |
161 | 0 | |
162 | }) | |
1a4d82fc JJ |
163 | } |
164 | ||
c34b1796 | 165 | // Look for #![doc(test(no_crate_inject))], used by crates in the std facade |
54a0048b | 166 | fn scrape_test_config(krate: &::rustc::hir::Crate) -> TestOptions { |
b039eaaf | 167 | use syntax::print::pprust; |
c34b1796 | 168 | |
9346a6ac AL |
169 | let mut opts = TestOptions { |
170 | no_crate_inject: false, | |
83c7162d | 171 | display_warnings: false, |
9346a6ac AL |
172 | attrs: Vec::new(), |
173 | }; | |
174 | ||
cc61c64b XL |
175 | let test_attrs: Vec<_> = krate.attrs.iter() |
176 | .filter(|a| a.check_name("doc")) | |
177 | .flat_map(|a| a.meta_item_list().unwrap_or_else(Vec::new)) | |
178 | .filter(|a| a.check_name("test")) | |
179 | .collect(); | |
180 | let attrs = test_attrs.iter().flat_map(|a| a.meta_item_list().unwrap_or(&[])); | |
181 | ||
9346a6ac AL |
182 | for attr in attrs { |
183 | if attr.check_name("no_crate_inject") { | |
184 | opts.no_crate_inject = true; | |
185 | } | |
186 | if attr.check_name("attr") { | |
187 | if let Some(l) = attr.meta_item_list() { | |
188 | for item in l { | |
9e0c209e | 189 | opts.attrs.push(pprust::meta_list_item_to_string(item)); |
c34b1796 AL |
190 | } |
191 | } | |
192 | } | |
193 | } | |
194 | ||
c30ab7b3 | 195 | opts |
c34b1796 AL |
196 | } |
197 | ||
2c00a5a8 XL |
198 | fn run_test(test: &str, cratename: &str, filename: &FileName, line: usize, |
199 | cfgs: Vec<String>, libs: SearchPaths, | |
83c7162d | 200 | cg: CodegenOptions, externs: Externs, |
041b39d2 XL |
201 | should_panic: bool, no_run: bool, as_test_harness: bool, |
202 | compile_fail: bool, mut error_codes: Vec<String>, opts: &TestOptions, | |
0531ce1d | 203 | maybe_sysroot: Option<PathBuf>, linker: Option<PathBuf>, edition: Edition) { |
1a4d82fc JJ |
204 | // the test harness wants its own `main` & top level functions, so |
205 | // never wrap the test in `fn main() { ... }` | |
2c00a5a8 | 206 | let (test, line_offset) = make_test(test, Some(cratename), as_test_harness, opts); |
ea8adc8c | 207 | // FIXME(#44940): if doctests ever support path remapping, then this filename |
b7449926 | 208 | // needs to be the result of SourceMap::span_to_unmapped_path |
54a0048b | 209 | let input = config::Input::Str { |
041b39d2 | 210 | name: filename.to_owned(), |
0bf4aa26 | 211 | input: test, |
54a0048b | 212 | }; |
5bcae85e | 213 | let outputs = OutputTypes::new(&[(OutputType::Exe, None)]); |
1a4d82fc JJ |
214 | |
215 | let sessopts = config::Options { | |
32a655c1 SL |
216 | maybe_sysroot: maybe_sysroot.or_else( |
217 | || Some(env::current_exe().unwrap().parent().unwrap().parent().unwrap().to_path_buf())), | |
1a4d82fc | 218 | search_paths: libs, |
b7449926 | 219 | crate_types: vec![config::CrateType::Executable], |
b039eaaf | 220 | output_types: outputs, |
3b2f2976 | 221 | externs, |
1a4d82fc | 222 | cg: config::CodegenOptions { |
abe05a73 | 223 | linker, |
83c7162d | 224 | ..cg |
1a4d82fc JJ |
225 | }, |
226 | test: as_test_harness, | |
9e0c209e | 227 | unstable_features: UnstableFeatures::from_environment(), |
0531ce1d | 228 | debugging_opts: config::DebuggingOptions { |
0531ce1d XL |
229 | ..config::basic_debugging_options() |
230 | }, | |
83c7162d | 231 | edition, |
b7449926 | 232 | ..config::Options::default() |
1a4d82fc JJ |
233 | }; |
234 | ||
74d20737 XL |
235 | // Shuffle around a few input and output handles here. We're going to pass |
236 | // an explicit handle into rustc to collect output messages, but we also | |
237 | // want to catch the error message that rustc prints when it fails. | |
238 | // | |
239 | // We take our thread-local stderr (likely set by the test runner) and replace | |
240 | // it with a sink that is also passed to rustc itself. When this function | |
241 | // returns the output of the sink is copied onto the output of our own thread. | |
242 | // | |
243 | // The basic idea is to not use a default Handler for rustc, and then also | |
244 | // not print things by default to the actual stderr. | |
245 | struct Sink(Arc<Mutex<Vec<u8>>>); | |
246 | impl Write for Sink { | |
247 | fn write(&mut self, data: &[u8]) -> io::Result<usize> { | |
248 | Write::write(&mut *self.0.lock().unwrap(), data) | |
c34b1796 | 249 | } |
74d20737 XL |
250 | fn flush(&mut self) -> io::Result<()> { Ok(()) } |
251 | } | |
8faf50e0 | 252 | struct Bomb(Arc<Mutex<Vec<u8>>>, Box<dyn Write+Send>); |
74d20737 XL |
253 | impl Drop for Bomb { |
254 | fn drop(&mut self) { | |
255 | let _ = self.1.write_all(&self.0.lock().unwrap()); | |
94b46f34 | 256 | } |
74d20737 XL |
257 | } |
258 | let data = Arc::new(Mutex::new(Vec::new())); | |
259 | ||
260 | let old = io::set_panic(Some(box Sink(data.clone()))); | |
261 | let _bomb = Bomb(data.clone(), old.unwrap_or(box io::stdout())); | |
262 | ||
263 | let (libdir, outdir, compile_result) = driver::spawn_thread_pool(sessopts, |sessopts| { | |
b7449926 | 264 | let source_map = Lrc::new(SourceMap::new_doctest( |
94b46f34 XL |
265 | sessopts.file_path_mapping(), filename.clone(), line as isize - line_offset as isize |
266 | )); | |
267 | let emitter = errors::emitter::EmitterWriter::new(box Sink(data.clone()), | |
b7449926 | 268 | Some(source_map.clone()), |
94b46f34 XL |
269 | false, |
270 | false); | |
94b46f34 XL |
271 | |
272 | // Compile the code | |
273 | let diagnostic_handler = errors::Handler::with_emitter(true, false, box emitter); | |
274 | ||
275 | let mut sess = session::build_session_( | |
b7449926 | 276 | sessopts, None, diagnostic_handler, source_map, |
94b46f34 XL |
277 | ); |
278 | let codegen_backend = rustc_driver::get_codegen_backend(&sess); | |
279 | let cstore = CStore::new(codegen_backend.metadata_loader()); | |
280 | rustc_lint::register_builtins(&mut sess.lint_store.borrow_mut(), Some(&sess)); | |
281 | ||
8faf50e0 XL |
282 | let outdir = Mutex::new( |
283 | TempFileBuilder::new().prefix("rustdoctest").tempdir().expect("rustdoc needs a tempdir") | |
284 | ); | |
94b46f34 XL |
285 | let libdir = sess.target_filesearch(PathKind::All).get_lib_path(); |
286 | let mut control = driver::CompileController::basic(); | |
287 | ||
288 | let mut cfg = config::build_configuration(&sess, config::parse_cfgspecs(cfgs.clone())); | |
289 | target_features::add_configuration(&mut cfg, &sess, &*codegen_backend); | |
290 | sess.parse_sess.config = cfg; | |
291 | ||
292 | let out = Some(outdir.lock().unwrap().path().to_path_buf()); | |
293 | ||
294 | if no_run { | |
295 | control.after_analysis.stop = Compilation::Stop; | |
c34b1796 | 296 | } |
7453a54e | 297 | |
94b46f34 XL |
298 | let res = panic::catch_unwind(AssertUnwindSafe(|| { |
299 | driver::compile_input( | |
300 | codegen_backend, | |
301 | &sess, | |
302 | &cstore, | |
303 | &None, | |
304 | &input, | |
305 | &out, | |
306 | &None, | |
307 | None, | |
308 | &control | |
309 | ) | |
310 | })); | |
311 | ||
312 | let compile_result = match res { | |
313 | Ok(Ok(())) | Ok(Err(CompileIncomplete::Stopped)) => Ok(()), | |
314 | Err(_) | Ok(Err(CompileIncomplete::Errored(_))) => Err(()) | |
315 | }; | |
041b39d2 | 316 | |
74d20737 XL |
317 | (libdir, outdir, compile_result) |
318 | }); | |
319 | ||
320 | match (compile_result, compile_fail) { | |
321 | (Ok(()), true) => { | |
322 | panic!("test compiled while it wasn't supposed to") | |
323 | } | |
324 | (Ok(()), false) => {} | |
325 | (Err(()), true) => { | |
326 | if error_codes.len() > 0 { | |
327 | let out = String::from_utf8(data.lock().unwrap().to_vec()).unwrap(); | |
328 | error_codes.retain(|err| !out.contains(err)); | |
3157f602 XL |
329 | } |
330 | } | |
74d20737 XL |
331 | (Err(()), false) => { |
332 | panic!("couldn't compile the test") | |
041b39d2 | 333 | } |
74d20737 | 334 | } |
3157f602 | 335 | |
74d20737 XL |
336 | if error_codes.len() > 0 { |
337 | panic!("Some expected error codes were not found: {:?}", error_codes); | |
338 | } | |
1a4d82fc JJ |
339 | |
340 | if no_run { return } | |
341 | ||
342 | // Run the code! | |
343 | // | |
344 | // We're careful to prepend the *target* dylib search path to the child's | |
345 | // environment to ensure that the target loads the right libraries at | |
346 | // runtime. It would be a sad day if the *host* libraries were loaded as a | |
347 | // mistake. | |
7453a54e | 348 | let mut cmd = Command::new(&outdir.lock().unwrap().path().join("rust_out")); |
c34b1796 | 349 | let var = DynamicLibrary::envvar(); |
1a4d82fc | 350 | let newpath = { |
c34b1796 AL |
351 | let path = env::var_os(var).unwrap_or(OsString::new()); |
352 | let mut path = env::split_paths(&path).collect::<Vec<_>>(); | |
0bf4aa26 | 353 | path.insert(0, libdir); |
62682a34 | 354 | env::join_paths(path).unwrap() |
1a4d82fc | 355 | }; |
c34b1796 | 356 | cmd.env(var, &newpath); |
1a4d82fc JJ |
357 | |
358 | match cmd.output() { | |
359 | Err(e) => panic!("couldn't run the test: {}{}", e, | |
c34b1796 | 360 | if e.kind() == io::ErrorKind::PermissionDenied { |
1a4d82fc JJ |
361 | " - maybe your tempdir is mounted with noexec?" |
362 | } else { "" }), | |
363 | Ok(out) => { | |
c34b1796 | 364 | if should_panic && out.status.success() { |
1a4d82fc | 365 | panic!("test executable succeeded when it should have failed"); |
c34b1796 | 366 | } else if !should_panic && !out.status.success() { |
8bb4bdeb | 367 | panic!("test executable failed:\n{}\n{}\n", |
c34b1796 AL |
368 | str::from_utf8(&out.stdout).unwrap_or(""), |
369 | str::from_utf8(&out.stderr).unwrap_or("")); | |
1a4d82fc JJ |
370 | } |
371 | } | |
372 | } | |
373 | } | |
374 | ||
2c00a5a8 | 375 | /// Makes the test file. Also returns the number of lines before the code begins |
041b39d2 XL |
376 | pub fn make_test(s: &str, |
377 | cratename: Option<&str>, | |
378 | dont_insert_main: bool, | |
379 | opts: &TestOptions) | |
2c00a5a8 | 380 | -> (String, usize) { |
c34b1796 | 381 | let (crate_attrs, everything_else) = partition_source(s); |
0531ce1d | 382 | let everything_else = everything_else.trim(); |
2c00a5a8 | 383 | let mut line_offset = 0; |
1a4d82fc | 384 | let mut prog = String::new(); |
c34b1796 | 385 | |
83c7162d | 386 | if opts.attrs.is_empty() && !opts.display_warnings { |
abe05a73 XL |
387 | // If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some |
388 | // lints that are commonly triggered in doctests. The crate-level test attributes are | |
389 | // commonly used to make tests fail in case they trigger warnings, so having this there in | |
390 | // that case may cause some tests to pass when they shouldn't have. | |
391 | prog.push_str("#![allow(unused)]\n"); | |
2c00a5a8 | 392 | line_offset += 1; |
abe05a73 | 393 | } |
c34b1796 | 394 | |
abe05a73 | 395 | // Next, any attributes that came from the crate root via #![doc(test(attr(...)))]. |
9346a6ac AL |
396 | for attr in &opts.attrs { |
397 | prog.push_str(&format!("#![{}]\n", attr)); | |
2c00a5a8 | 398 | line_offset += 1; |
1a4d82fc JJ |
399 | } |
400 | ||
abe05a73 XL |
401 | // Now push any outer attributes from the example, assuming they |
402 | // are intended to be crate attributes. | |
403 | prog.push_str(&crate_attrs); | |
404 | ||
1a4d82fc JJ |
405 | // Don't inject `extern crate std` because it's already injected by the |
406 | // compiler. | |
d9579d0f | 407 | if !s.contains("extern crate") && !opts.no_crate_inject && cratename != Some("std") { |
54a0048b SL |
408 | if let Some(cratename) = cratename { |
409 | if s.contains(cratename) { | |
410 | prog.push_str(&format!("extern crate {};\n", cratename)); | |
2c00a5a8 | 411 | line_offset += 1; |
1a4d82fc | 412 | } |
1a4d82fc JJ |
413 | } |
414 | } | |
ea8adc8c XL |
415 | |
416 | // FIXME (#21299): prefer libsyntax or some other actual parser over this | |
417 | // best-effort ad hoc approach | |
418 | let already_has_main = s.lines() | |
419 | .map(|line| { | |
420 | let comment = line.find("//"); | |
421 | if let Some(comment_begins) = comment { | |
422 | &line[0..comment_begins] | |
423 | } else { | |
424 | line | |
425 | } | |
426 | }) | |
427 | .any(|code| code.contains("fn main")); | |
428 | ||
429 | if dont_insert_main || already_has_main { | |
0531ce1d | 430 | prog.push_str(everything_else); |
1a4d82fc | 431 | } else { |
c30ab7b3 | 432 | prog.push_str("fn main() {\n"); |
2c00a5a8 | 433 | line_offset += 1; |
0531ce1d | 434 | prog.push_str(everything_else); |
1a4d82fc JJ |
435 | prog.push_str("\n}"); |
436 | } | |
437 | ||
c34b1796 AL |
438 | info!("final test program: {}", prog); |
439 | ||
2c00a5a8 | 440 | (prog, line_offset) |
1a4d82fc JJ |
441 | } |
442 | ||
8bb4bdeb | 443 | // FIXME(aburka): use a real parser to deal with multiline attributes |
c34b1796 | 444 | fn partition_source(s: &str) -> (String, String) { |
c34b1796 AL |
445 | let mut after_header = false; |
446 | let mut before = String::new(); | |
447 | let mut after = String::new(); | |
448 | ||
449 | for line in s.lines() { | |
450 | let trimline = line.trim(); | |
0531ce1d XL |
451 | let header = trimline.chars().all(|c| c.is_whitespace()) || |
452 | trimline.starts_with("#![") || | |
453 | trimline.starts_with("#[macro_use] extern crate") || | |
454 | trimline.starts_with("extern crate"); | |
c34b1796 AL |
455 | if !header || after_header { |
456 | after_header = true; | |
457 | after.push_str(line); | |
458 | after.push_str("\n"); | |
459 | } else { | |
460 | before.push_str(line); | |
461 | before.push_str("\n"); | |
462 | } | |
463 | } | |
464 | ||
c30ab7b3 | 465 | (before, after) |
c34b1796 AL |
466 | } |
467 | ||
0bf4aa26 XL |
468 | pub trait Tester { |
469 | fn add_test(&mut self, test: String, config: LangString, line: usize); | |
470 | fn get_line(&self) -> usize { | |
471 | 0 | |
472 | } | |
473 | fn register_header(&mut self, _name: &str, _level: u32) {} | |
474 | } | |
475 | ||
1a4d82fc JJ |
476 | pub struct Collector { |
477 | pub tests: Vec<testing::TestDescAndFn>, | |
abe05a73 XL |
478 | |
479 | // The name of the test displayed to the user, separated by `::`. | |
480 | // | |
481 | // In tests from Rust source, this is the path to the item | |
482 | // e.g. `["std", "vec", "Vec", "push"]`. | |
483 | // | |
484 | // In tests from a markdown file, this is the titles of all headers (h1~h6) | |
485 | // of the sections that contain the code block, e.g. if the markdown file is | |
486 | // written as: | |
487 | // | |
488 | // ``````markdown | |
489 | // # Title | |
490 | // | |
491 | // ## Subtitle | |
492 | // | |
493 | // ```rust | |
494 | // assert!(true); | |
495 | // ``` | |
496 | // `````` | |
497 | // | |
498 | // the `names` vector of that test will be `["Title", "Subtitle"]`. | |
1a4d82fc | 499 | names: Vec<String>, |
abe05a73 | 500 | |
92a42be0 | 501 | cfgs: Vec<String>, |
1a4d82fc | 502 | libs: SearchPaths, |
83c7162d | 503 | cg: CodegenOptions, |
5bcae85e | 504 | externs: Externs, |
1a4d82fc | 505 | use_headers: bool, |
1a4d82fc | 506 | cratename: String, |
9346a6ac | 507 | opts: TestOptions, |
32a655c1 | 508 | maybe_sysroot: Option<PathBuf>, |
8bb4bdeb | 509 | position: Span, |
b7449926 | 510 | source_map: Option<Lrc<SourceMap>>, |
ff7c6d11 | 511 | filename: Option<PathBuf>, |
ff7c6d11 | 512 | linker: Option<PathBuf>, |
0531ce1d | 513 | edition: Edition, |
1a4d82fc JJ |
514 | } |
515 | ||
516 | impl Collector { | |
83c7162d XL |
517 | pub fn new(cratename: String, cfgs: Vec<String>, libs: SearchPaths, cg: CodegenOptions, |
518 | externs: Externs, use_headers: bool, opts: TestOptions, | |
b7449926 | 519 | maybe_sysroot: Option<PathBuf>, source_map: Option<Lrc<SourceMap>>, |
83c7162d | 520 | filename: Option<PathBuf>, linker: Option<PathBuf>, edition: Edition) -> Collector { |
1a4d82fc JJ |
521 | Collector { |
522 | tests: Vec::new(), | |
523 | names: Vec::new(), | |
3b2f2976 XL |
524 | cfgs, |
525 | libs, | |
83c7162d | 526 | cg, |
3b2f2976 | 527 | externs, |
3b2f2976 | 528 | use_headers, |
3b2f2976 XL |
529 | cratename, |
530 | opts, | |
531 | maybe_sysroot, | |
8bb4bdeb | 532 | position: DUMMY_SP, |
b7449926 | 533 | source_map, |
3b2f2976 | 534 | filename, |
abe05a73 | 535 | linker, |
0531ce1d | 536 | edition, |
1a4d82fc JJ |
537 | } |
538 | } | |
539 | ||
ff7c6d11 | 540 | fn generate_name(&self, line: usize, filename: &FileName) -> String { |
abe05a73 | 541 | format!("{} - {} (line {})", filename, self.names.join("::"), line) |
cc61c64b XL |
542 | } |
543 | ||
0bf4aa26 XL |
544 | pub fn set_position(&mut self, position: Span) { |
545 | self.position = position; | |
546 | } | |
547 | ||
548 | fn get_filename(&self) -> FileName { | |
549 | if let Some(ref source_map) = self.source_map { | |
550 | let filename = source_map.span_to_filename(self.position); | |
551 | if let FileName::Real(ref filename) = filename { | |
552 | if let Ok(cur_dir) = env::current_dir() { | |
553 | if let Ok(path) = filename.strip_prefix(&cur_dir) { | |
554 | return path.to_owned().into(); | |
555 | } | |
556 | } | |
557 | } | |
558 | filename | |
559 | } else if let Some(ref filename) = self.filename { | |
560 | filename.clone().into() | |
561 | } else { | |
562 | FileName::Custom("input".to_owned()) | |
563 | } | |
564 | } | |
565 | } | |
566 | ||
567 | impl Tester for Collector { | |
568 | fn add_test(&mut self, test: String, config: LangString, line: usize) { | |
b7449926 | 569 | let filename = self.get_filename(); |
cc61c64b | 570 | let name = self.generate_name(line, &filename); |
92a42be0 | 571 | let cfgs = self.cfgs.clone(); |
1a4d82fc | 572 | let libs = self.libs.clone(); |
83c7162d | 573 | let cg = self.cg.clone(); |
1a4d82fc JJ |
574 | let externs = self.externs.clone(); |
575 | let cratename = self.cratename.to_string(); | |
9346a6ac | 576 | let opts = self.opts.clone(); |
32a655c1 | 577 | let maybe_sysroot = self.maybe_sysroot.clone(); |
abe05a73 | 578 | let linker = self.linker.clone(); |
0bf4aa26 | 579 | let edition = config.edition.unwrap_or(self.edition); |
1a4d82fc JJ |
580 | debug!("Creating test {}: {}", name, test); |
581 | self.tests.push(testing::TestDescAndFn { | |
582 | desc: testing::TestDesc { | |
74d20737 | 583 | name: testing::DynTestName(name.clone()), |
b7449926 | 584 | ignore: config.ignore, |
9346a6ac AL |
585 | // compiler failures are test failures |
586 | should_panic: testing::ShouldPanic::No, | |
b7449926 | 587 | allow_fail: config.allow_fail, |
1a4d82fc | 588 | }, |
ff7c6d11 | 589 | testfn: testing::DynTestFn(box move || { |
32a655c1 SL |
590 | let panic = io::set_panic(None); |
591 | let print = io::set_print(None); | |
592 | match { | |
74d20737 | 593 | rustc_driver::in_named_rustc_thread(name, move || with_globals(move || { |
32a655c1 SL |
594 | io::set_panic(panic); |
595 | io::set_print(print); | |
041b39d2 XL |
596 | run_test(&test, |
597 | &cratename, | |
598 | &filename, | |
2c00a5a8 | 599 | line, |
041b39d2 XL |
600 | cfgs, |
601 | libs, | |
83c7162d | 602 | cg, |
041b39d2 | 603 | externs, |
b7449926 XL |
604 | config.should_panic, |
605 | config.no_run, | |
606 | config.test_harness, | |
607 | config.compile_fail, | |
608 | config.error_codes, | |
041b39d2 | 609 | &opts, |
abe05a73 | 610 | maybe_sysroot, |
0531ce1d XL |
611 | linker, |
612 | edition) | |
613 | })) | |
32a655c1 SL |
614 | } { |
615 | Ok(()) => (), | |
616 | Err(err) => panic::resume_unwind(err), | |
617 | } | |
618 | }), | |
1a4d82fc JJ |
619 | }); |
620 | } | |
621 | ||
0bf4aa26 | 622 | fn get_line(&self) -> usize { |
b7449926 | 623 | if let Some(ref source_map) = self.source_map { |
ea8adc8c | 624 | let line = self.position.lo().to_usize(); |
b7449926 | 625 | let line = source_map.lookup_char_pos(BytePos(line as u32)).line; |
8bb4bdeb XL |
626 | if line > 0 { line - 1 } else { line } |
627 | } else { | |
628 | 0 | |
629 | } | |
630 | } | |
631 | ||
0bf4aa26 | 632 | fn register_header(&mut self, name: &str, level: u32) { |
abe05a73 | 633 | if self.use_headers { |
1a4d82fc JJ |
634 | // we use these headings as test names, so it's good if |
635 | // they're valid identifiers. | |
636 | let name = name.chars().enumerate().map(|(i, c)| { | |
637 | if (i == 0 && c.is_xid_start()) || | |
638 | (i != 0 && c.is_xid_continue()) { | |
639 | c | |
640 | } else { | |
641 | '_' | |
642 | } | |
643 | }).collect::<String>(); | |
644 | ||
abe05a73 XL |
645 | // Here we try to efficiently assemble the header titles into the |
646 | // test name in the form of `h1::h2::h3::h4::h5::h6`. | |
647 | // | |
648 | // Suppose originally `self.names` contains `[h1, h2, h3]`... | |
649 | let level = level as usize; | |
650 | if level <= self.names.len() { | |
651 | // ... Consider `level == 2`. All headers in the lower levels | |
652 | // are irrelevant in this new level. So we should reset | |
653 | // `self.names` to contain headers until <h2>, and replace that | |
654 | // slot with the new name: `[h1, name]`. | |
655 | self.names.truncate(level); | |
656 | self.names[level - 1] = name; | |
657 | } else { | |
658 | // ... On the other hand, consider `level == 5`. This means we | |
659 | // need to extend `self.names` to contain five headers. We fill | |
660 | // in the missing level (<h4>) with `_`. Thus `self.names` will | |
661 | // become `[h1, h2, h3, "_", name]`. | |
662 | if level - 1 > self.names.len() { | |
663 | self.names.resize(level - 1, "_".to_owned()); | |
664 | } | |
665 | self.names.push(name); | |
666 | } | |
1a4d82fc JJ |
667 | } |
668 | } | |
669 | } | |
670 | ||
476ff2be | 671 | struct HirCollector<'a, 'hir: 'a> { |
3b2f2976 | 672 | sess: &'a session::Session, |
476ff2be | 673 | collector: &'a mut Collector, |
b7449926 XL |
674 | map: &'a hir::map::Map<'hir>, |
675 | codes: ErrorCodes, | |
476ff2be | 676 | } |
92a42be0 | 677 | |
476ff2be SL |
678 | impl<'a, 'hir> HirCollector<'a, 'hir> { |
679 | fn visit_testable<F: FnOnce(&mut Self)>(&mut self, | |
680 | name: String, | |
681 | attrs: &[ast::Attribute], | |
682 | nested: F) { | |
3b2f2976 XL |
683 | let mut attrs = Attributes::from_ast(self.sess.diagnostic(), attrs); |
684 | if let Some(ref cfg) = attrs.cfg { | |
0531ce1d | 685 | if !cfg.matches(&self.sess.parse_sess, Some(&self.sess.features_untracked())) { |
3b2f2976 XL |
686 | return; |
687 | } | |
688 | } | |
689 | ||
476ff2be SL |
690 | let has_name = !name.is_empty(); |
691 | if has_name { | |
692 | self.collector.names.push(name); | |
693 | } | |
92a42be0 | 694 | |
476ff2be SL |
695 | attrs.collapse_doc_comments(); |
696 | attrs.unindent_doc_comments(); | |
ff7c6d11 XL |
697 | // the collapse-docs pass won't combine sugared/raw doc attributes, or included files with |
698 | // anything else, this will combine them for us | |
699 | if let Some(doc) = attrs.collapsed_doc_value() { | |
b7449926 XL |
700 | self.collector.set_position(attrs.span.unwrap_or(DUMMY_SP)); |
701 | let res = markdown::find_testable_code(&doc, self.collector, self.codes); | |
702 | if let Err(err) = res { | |
703 | self.sess.diagnostic().span_warn(attrs.span.unwrap_or(DUMMY_SP), | |
704 | &err.to_string()); | |
705 | } | |
1a4d82fc | 706 | } |
92a42be0 | 707 | |
476ff2be SL |
708 | nested(self); |
709 | ||
710 | if has_name { | |
711 | self.collector.names.pop(); | |
1a4d82fc | 712 | } |
476ff2be SL |
713 | } |
714 | } | |
92a42be0 | 715 | |
476ff2be SL |
716 | impl<'a, 'hir> intravisit::Visitor<'hir> for HirCollector<'a, 'hir> { |
717 | fn nested_visit_map<'this>(&'this mut self) -> intravisit::NestedVisitorMap<'this, 'hir> { | |
718 | intravisit::NestedVisitorMap::All(&self.map) | |
719 | } | |
92a42be0 | 720 | |
476ff2be | 721 | fn visit_item(&mut self, item: &'hir hir::Item) { |
8faf50e0 | 722 | let name = if let hir::ItemKind::Impl(.., ref ty, _) = item.node { |
32a655c1 | 723 | self.map.node_to_pretty_string(ty.id) |
476ff2be SL |
724 | } else { |
725 | item.name.to_string() | |
726 | }; | |
92a42be0 | 727 | |
476ff2be SL |
728 | self.visit_testable(name, &item.attrs, |this| { |
729 | intravisit::walk_item(this, item); | |
730 | }); | |
731 | } | |
92a42be0 | 732 | |
476ff2be | 733 | fn visit_trait_item(&mut self, item: &'hir hir::TraitItem) { |
8faf50e0 | 734 | self.visit_testable(item.ident.to_string(), &item.attrs, |this| { |
476ff2be SL |
735 | intravisit::walk_trait_item(this, item); |
736 | }); | |
737 | } | |
92a42be0 | 738 | |
476ff2be | 739 | fn visit_impl_item(&mut self, item: &'hir hir::ImplItem) { |
8faf50e0 | 740 | self.visit_testable(item.ident.to_string(), &item.attrs, |this| { |
476ff2be SL |
741 | intravisit::walk_impl_item(this, item); |
742 | }); | |
743 | } | |
744 | ||
745 | fn visit_foreign_item(&mut self, item: &'hir hir::ForeignItem) { | |
746 | self.visit_testable(item.name.to_string(), &item.attrs, |this| { | |
747 | intravisit::walk_foreign_item(this, item); | |
748 | }); | |
749 | } | |
750 | ||
751 | fn visit_variant(&mut self, | |
752 | v: &'hir hir::Variant, | |
753 | g: &'hir hir::Generics, | |
754 | item_id: ast::NodeId) { | |
755 | self.visit_testable(v.node.name.to_string(), &v.node.attrs, |this| { | |
756 | intravisit::walk_variant(this, v, g, item_id); | |
757 | }); | |
758 | } | |
759 | ||
760 | fn visit_struct_field(&mut self, f: &'hir hir::StructField) { | |
94b46f34 | 761 | self.visit_testable(f.ident.to_string(), &f.attrs, |this| { |
476ff2be SL |
762 | intravisit::walk_struct_field(this, f); |
763 | }); | |
764 | } | |
765 | ||
766 | fn visit_macro_def(&mut self, macro_def: &'hir hir::MacroDef) { | |
767 | self.visit_testable(macro_def.name.to_string(), ¯o_def.attrs, |_| ()); | |
1a4d82fc JJ |
768 | } |
769 | } | |
0531ce1d XL |
770 | |
771 | #[cfg(test)] | |
772 | mod tests { | |
773 | use super::{TestOptions, make_test}; | |
774 | ||
775 | #[test] | |
776 | fn make_test_basic() { | |
777 | //basic use: wraps with `fn main`, adds `#![allow(unused)]` | |
778 | let opts = TestOptions::default(); | |
779 | let input = | |
780 | "assert_eq!(2+2, 4);"; | |
781 | let expected = | |
782 | "#![allow(unused)] | |
783 | fn main() { | |
784 | assert_eq!(2+2, 4); | |
785 | }".to_string(); | |
786 | let output = make_test(input, None, false, &opts); | |
0bf4aa26 | 787 | assert_eq!(output, (expected, 2)); |
0531ce1d XL |
788 | } |
789 | ||
790 | #[test] | |
791 | fn make_test_crate_name_no_use() { | |
792 | //if you give a crate name but *don't* use it within the test, it won't bother inserting | |
793 | //the `extern crate` statement | |
794 | let opts = TestOptions::default(); | |
795 | let input = | |
796 | "assert_eq!(2+2, 4);"; | |
797 | let expected = | |
798 | "#![allow(unused)] | |
799 | fn main() { | |
800 | assert_eq!(2+2, 4); | |
801 | }".to_string(); | |
802 | let output = make_test(input, Some("asdf"), false, &opts); | |
803 | assert_eq!(output, (expected, 2)); | |
804 | } | |
805 | ||
806 | #[test] | |
807 | fn make_test_crate_name() { | |
808 | //if you give a crate name and use it within the test, it will insert an `extern crate` | |
809 | //statement before `fn main` | |
810 | let opts = TestOptions::default(); | |
811 | let input = | |
812 | "use asdf::qwop; | |
813 | assert_eq!(2+2, 4);"; | |
814 | let expected = | |
815 | "#![allow(unused)] | |
816 | extern crate asdf; | |
817 | fn main() { | |
818 | use asdf::qwop; | |
819 | assert_eq!(2+2, 4); | |
820 | }".to_string(); | |
821 | let output = make_test(input, Some("asdf"), false, &opts); | |
822 | assert_eq!(output, (expected, 3)); | |
823 | } | |
824 | ||
825 | #[test] | |
826 | fn make_test_no_crate_inject() { | |
827 | //even if you do use the crate within the test, setting `opts.no_crate_inject` will skip | |
828 | //adding it anyway | |
829 | let opts = TestOptions { | |
830 | no_crate_inject: true, | |
83c7162d | 831 | display_warnings: false, |
0531ce1d XL |
832 | attrs: vec![], |
833 | }; | |
834 | let input = | |
835 | "use asdf::qwop; | |
836 | assert_eq!(2+2, 4);"; | |
837 | let expected = | |
838 | "#![allow(unused)] | |
839 | fn main() { | |
840 | use asdf::qwop; | |
841 | assert_eq!(2+2, 4); | |
842 | }".to_string(); | |
843 | let output = make_test(input, Some("asdf"), false, &opts); | |
844 | assert_eq!(output, (expected, 2)); | |
845 | } | |
846 | ||
847 | #[test] | |
848 | fn make_test_ignore_std() { | |
849 | //even if you include a crate name, and use it in the doctest, we still won't include an | |
850 | //`extern crate` statement if the crate is "std" - that's included already by the compiler! | |
851 | let opts = TestOptions::default(); | |
852 | let input = | |
853 | "use std::*; | |
854 | assert_eq!(2+2, 4);"; | |
855 | let expected = | |
856 | "#![allow(unused)] | |
857 | fn main() { | |
858 | use std::*; | |
859 | assert_eq!(2+2, 4); | |
860 | }".to_string(); | |
861 | let output = make_test(input, Some("std"), false, &opts); | |
862 | assert_eq!(output, (expected, 2)); | |
863 | } | |
864 | ||
865 | #[test] | |
866 | fn make_test_manual_extern_crate() { | |
867 | //when you manually include an `extern crate` statement in your doctest, make_test assumes | |
868 | //you've included one for your own crate too | |
869 | let opts = TestOptions::default(); | |
870 | let input = | |
871 | "extern crate asdf; | |
872 | use asdf::qwop; | |
873 | assert_eq!(2+2, 4);"; | |
874 | let expected = | |
875 | "#![allow(unused)] | |
876 | extern crate asdf; | |
877 | fn main() { | |
878 | use asdf::qwop; | |
879 | assert_eq!(2+2, 4); | |
880 | }".to_string(); | |
881 | let output = make_test(input, Some("asdf"), false, &opts); | |
882 | assert_eq!(output, (expected, 2)); | |
883 | } | |
884 | ||
885 | #[test] | |
886 | fn make_test_manual_extern_crate_with_macro_use() { | |
887 | let opts = TestOptions::default(); | |
888 | let input = | |
889 | "#[macro_use] extern crate asdf; | |
890 | use asdf::qwop; | |
891 | assert_eq!(2+2, 4);"; | |
892 | let expected = | |
893 | "#![allow(unused)] | |
894 | #[macro_use] extern crate asdf; | |
895 | fn main() { | |
896 | use asdf::qwop; | |
897 | assert_eq!(2+2, 4); | |
898 | }".to_string(); | |
899 | let output = make_test(input, Some("asdf"), false, &opts); | |
900 | assert_eq!(output, (expected, 2)); | |
901 | } | |
902 | ||
903 | #[test] | |
904 | fn make_test_opts_attrs() { | |
905 | //if you supplied some doctest attributes with #![doc(test(attr(...)))], it will use those | |
906 | //instead of the stock #![allow(unused)] | |
907 | let mut opts = TestOptions::default(); | |
908 | opts.attrs.push("feature(sick_rad)".to_string()); | |
909 | let input = | |
910 | "use asdf::qwop; | |
911 | assert_eq!(2+2, 4);"; | |
912 | let expected = | |
913 | "#![feature(sick_rad)] | |
914 | extern crate asdf; | |
915 | fn main() { | |
916 | use asdf::qwop; | |
917 | assert_eq!(2+2, 4); | |
918 | }".to_string(); | |
919 | let output = make_test(input, Some("asdf"), false, &opts); | |
920 | assert_eq!(output, (expected, 3)); | |
921 | ||
922 | //adding more will also bump the returned line offset | |
923 | opts.attrs.push("feature(hella_dope)".to_string()); | |
924 | let expected = | |
925 | "#![feature(sick_rad)] | |
926 | #![feature(hella_dope)] | |
927 | extern crate asdf; | |
928 | fn main() { | |
929 | use asdf::qwop; | |
930 | assert_eq!(2+2, 4); | |
931 | }".to_string(); | |
932 | let output = make_test(input, Some("asdf"), false, &opts); | |
933 | assert_eq!(output, (expected, 4)); | |
934 | } | |
935 | ||
936 | #[test] | |
937 | fn make_test_crate_attrs() { | |
938 | //including inner attributes in your doctest will apply them to the whole "crate", pasting | |
939 | //them outside the generated main function | |
940 | let opts = TestOptions::default(); | |
941 | let input = | |
942 | "#![feature(sick_rad)] | |
943 | assert_eq!(2+2, 4);"; | |
944 | let expected = | |
945 | "#![allow(unused)] | |
946 | #![feature(sick_rad)] | |
947 | fn main() { | |
948 | assert_eq!(2+2, 4); | |
949 | }".to_string(); | |
950 | let output = make_test(input, None, false, &opts); | |
951 | assert_eq!(output, (expected, 2)); | |
952 | } | |
953 | ||
954 | #[test] | |
955 | fn make_test_with_main() { | |
956 | //including your own `fn main` wrapper lets the test use it verbatim | |
957 | let opts = TestOptions::default(); | |
958 | let input = | |
959 | "fn main() { | |
960 | assert_eq!(2+2, 4); | |
961 | }"; | |
962 | let expected = | |
963 | "#![allow(unused)] | |
964 | fn main() { | |
965 | assert_eq!(2+2, 4); | |
966 | }".to_string(); | |
967 | let output = make_test(input, None, false, &opts); | |
968 | assert_eq!(output, (expected, 1)); | |
969 | } | |
970 | ||
971 | #[test] | |
972 | fn make_test_fake_main() { | |
973 | //...but putting it in a comment will still provide a wrapper | |
974 | let opts = TestOptions::default(); | |
975 | let input = | |
976 | "//Ceci n'est pas une `fn main` | |
977 | assert_eq!(2+2, 4);"; | |
978 | let expected = | |
979 | "#![allow(unused)] | |
980 | fn main() { | |
981 | //Ceci n'est pas une `fn main` | |
982 | assert_eq!(2+2, 4); | |
983 | }".to_string(); | |
984 | let output = make_test(input, None, false, &opts); | |
0bf4aa26 | 985 | assert_eq!(output, (expected, 2)); |
0531ce1d XL |
986 | } |
987 | ||
988 | #[test] | |
989 | fn make_test_dont_insert_main() { | |
990 | //even with that, if you set `dont_insert_main`, it won't create the `fn main` wrapper | |
991 | let opts = TestOptions::default(); | |
992 | let input = | |
993 | "//Ceci n'est pas une `fn main` | |
994 | assert_eq!(2+2, 4);"; | |
995 | let expected = | |
996 | "#![allow(unused)] | |
997 | //Ceci n'est pas une `fn main` | |
998 | assert_eq!(2+2, 4);".to_string(); | |
999 | let output = make_test(input, None, true, &opts); | |
0bf4aa26 | 1000 | assert_eq!(output, (expected, 1)); |
0531ce1d | 1001 | } |
83c7162d XL |
1002 | |
1003 | #[test] | |
1004 | fn make_test_display_warnings() { | |
1005 | //if the user is asking to display doctest warnings, suppress the default allow(unused) | |
1006 | let mut opts = TestOptions::default(); | |
1007 | opts.display_warnings = true; | |
1008 | let input = | |
1009 | "assert_eq!(2+2, 4);"; | |
1010 | let expected = | |
1011 | "fn main() { | |
1012 | assert_eq!(2+2, 4); | |
1013 | }".to_string(); | |
1014 | let output = make_test(input, None, false, &opts); | |
0bf4aa26 | 1015 | assert_eq!(output, (expected, 1)); |
83c7162d | 1016 | } |
0531ce1d | 1017 | } |