1use crate::cargo::{self, Metadata, PackageMetadata};
2use crate::dependencies::{self, Dependency, EditionOrInherit};
3use crate::directory::Directory;
4use crate::env::Update;
5use crate::error::{Error, Result};
6use crate::expand::{expand_globs, ExpandedTest};
7use crate::flock::Lock;
8use crate::manifest::{Bin, Manifest, Name, Package, Workspace};
9use crate::message::{self, Fail, Warn};
10use crate::normalize::{self, Context, Variations};
11use crate::{features, Expected, Runner, Test};
12use serde_derive::Deserialize;
13use std::collections::{BTreeMap as Map, BTreeSet as Set};
14use std::env;
15use std::ffi::{OsStr, OsString};
16use std::fs::{self, File};
17use std::mem;
18use std::path::{Path, PathBuf};
19use std::str;
20
21#[derive(Debug)]
22pub struct Project {
23 pub dir: Directory,
24 pub source_dir: Directory,
25 pub target_dir: Directory,
26 pub name: String,
27 pub update: Update,
28 pub has_pass: bool,
29 pub has_compile_fail: bool,
30 pub features: Option<Vec<String>>,
31 pub workspace: Directory,
32 pub path_dependencies: Vec<PathDependency>,
33 pub manifest: Manifest,
34 pub keep_going: bool,
35}
36
37#[derive(Debug)]
38pub struct PathDependency {
39 pub name: String,
40 pub normalized_path: Directory,
41}
42
43struct Report {
44 failures: usize,
45 created_wip: usize,
46}
47
48impl Runner {
49 pub fn run(&mut self) {
50 let mut tests = expand_globs(&self.tests);
51 filter(&mut tests);
52
53 let (project, _lock) = (|| {
54 let mut project = self.prepare(&tests)?;
55 let lock = Lock::acquire(path!(project.dir / ".lock"))?;
56 self.write(&mut project)?;
57 Ok((project, lock))
58 })()
59 .unwrap_or_else(|err| {
60 message::prepare_fail(err);
61 panic!("tests failed");
62 });
63
64 print!("\n\n");
65
66 let len = tests.len();
67 let mut report = Report {
68 failures: 0,
69 created_wip: 0,
70 };
71
72 if tests.is_empty() {
73 message::no_tests_enabled();
74 } else if project.keep_going && !project.has_pass {
75 report = match self.run_all(&project, tests) {
76 Ok(failures) => failures,
77 Err(err) => {
78 message::test_fail(err);
79 Report {
80 failures: len,
81 created_wip: 0,
82 }
83 }
84 }
85 } else {
86 for test in tests {
87 match test.run(&project) {
88 Ok(Outcome::Passed) => {}
89 Ok(Outcome::CreatedWip) => report.created_wip += 1,
90 Err(err) => {
91 report.failures += 1;
92 message::test_fail(err);
93 }
94 }
95 }
96 }
97
98 print!("\n\n");
99
100 if report.failures > 0 && project.name != "trybuild-tests" {
101 panic!("{} of {} tests failed", report.failures, len);
102 }
103 if report.created_wip > 0 && project.name != "trybuild-tests" {
104 panic!(
105 "successfully created new stderr files for {} test cases",
106 report.created_wip,
107 );
108 }
109 }
110
111 fn prepare(&self, tests: &[ExpandedTest]) -> Result<Project> {
112 let Metadata {
113 target_directory: target_dir,
114 workspace_root: workspace,
115 packages,
116 } = cargo::metadata()?;
117
118 let mut has_pass = false;
119 let mut has_compile_fail = false;
120 for e in tests {
121 match e.test.expected {
122 Expected::Pass => has_pass = true,
123 Expected::CompileFail => has_compile_fail = true,
124 }
125 }
126
127 let source_dir = cargo::manifest_dir()?;
128 let source_manifest = dependencies::get_manifest(&source_dir)?;
129
130 let mut features = features::find();
131
132 let path_dependencies = source_manifest
133 .dependencies
134 .iter()
135 .filter_map(|(name, dep)| {
136 let path = dep.path.as_ref()?;
137 if packages.iter().any(|p| &p.name == name) {
138 None
140 } else {
141 Some(PathDependency {
142 name: name.clone(),
143 normalized_path: path.canonicalize().ok()?,
144 })
145 }
146 })
147 .collect();
148
149 let crate_name = &source_manifest.package.name;
150 let project_dir = path!(target_dir / "tests" / "trybuild" / crate_name /);
151 fs::create_dir_all(&project_dir)?;
152
153 let project_name = format!("{}-tests", crate_name);
154 let manifest = Self::make_manifest(
155 &workspace,
156 &project_name,
157 &source_dir,
158 &packages,
159 tests,
160 source_manifest,
161 )?;
162
163 if let Some(enabled_features) = &mut features {
164 enabled_features.retain(|feature| manifest.features.contains_key(feature));
165 }
166
167 Ok(Project {
168 dir: project_dir,
169 source_dir,
170 target_dir,
171 name: project_name,
172 update: Update::env()?,
173 has_pass,
174 has_compile_fail,
175 features,
176 workspace,
177 path_dependencies,
178 manifest,
179 keep_going: false,
180 })
181 }
182
183 fn write(&self, project: &mut Project) -> Result<()> {
184 let manifest_toml = toml::to_string(&project.manifest)?;
185 fs::write(path!(project.dir / "Cargo.toml"), manifest_toml)?;
186
187 let main_rs = b"\
188 #![allow(unused_crate_dependencies, missing_docs)]\n\
189 fn main() {}\n\
190 ";
191 fs::write(path!(project.dir / "main.rs"), &main_rs[..])?;
192
193 cargo::build_dependencies(project)?;
194
195 Ok(())
196 }
197
198 pub fn make_manifest(
199 workspace: &Directory,
200 project_name: &str,
201 source_dir: &Directory,
202 packages: &[PackageMetadata],
203 tests: &[ExpandedTest],
204 source_manifest: dependencies::Manifest,
205 ) -> Result<Manifest> {
206 let crate_name = source_manifest.package.name;
207 let workspace_manifest = dependencies::get_workspace_manifest(workspace);
208
209 let edition = match source_manifest.package.edition {
210 EditionOrInherit::Edition(edition) => edition,
211 EditionOrInherit::Inherit => workspace_manifest
212 .workspace
213 .package
214 .edition
215 .ok_or(Error::NoWorkspaceManifest)?,
216 };
217
218 let mut dependencies = Map::new();
219 dependencies.extend(source_manifest.dependencies);
220 dependencies.extend(source_manifest.dev_dependencies);
221
222 let cargo_toml_path = source_dir.join("Cargo.toml");
223 let mut has_lib_target = true;
224 for package_metadata in packages {
225 if package_metadata.manifest_path == cargo_toml_path {
226 has_lib_target = package_metadata
227 .targets
228 .iter()
229 .any(|target| target.crate_types != ["bin"]);
230 }
231 }
232 if has_lib_target {
233 dependencies.insert(
234 crate_name.clone(),
235 Dependency {
236 version: None,
237 path: Some(source_dir.clone()),
238 optional: false,
239 default_features: Some(false),
240 features: Vec::new(),
241 git: None,
242 branch: None,
243 tag: None,
244 rev: None,
245 workspace: false,
246 rest: Map::new(),
247 },
248 );
249 }
250
251 let mut targets = source_manifest.target;
252 for target in targets.values_mut() {
253 let dev_dependencies = mem::take(&mut target.dev_dependencies);
254 target.dependencies.extend(dev_dependencies);
255 }
256
257 let mut features = source_manifest.features;
258 for (feature, enables) in &mut features {
259 enables.retain(|en| {
260 let Some(dep_name) = en.strip_prefix("dep:") else {
261 return false;
262 };
263 if let Some(Dependency { optional: true, .. }) = dependencies.get(dep_name) {
264 return true;
265 }
266 for target in targets.values() {
267 if let Some(Dependency { optional: true, .. }) =
268 target.dependencies.get(dep_name)
269 {
270 return true;
271 }
272 }
273 false
274 });
275 if has_lib_target {
276 enables.insert(0, format!("{}/{}", crate_name, feature));
277 }
278 }
279
280 let mut manifest = Manifest {
281 cargo_features: source_manifest.cargo_features,
282 package: Package {
283 name: project_name.to_owned(),
284 version: "0.0.0".to_owned(),
285 edition,
286 resolver: source_manifest.package.resolver,
287 publish: false,
288 },
289 features,
290 dependencies,
291 target: targets,
292 bins: Vec::new(),
293 workspace: Some(Workspace {
294 dependencies: workspace_manifest.workspace.dependencies,
295 }),
296 patch: workspace_manifest.patch,
299 replace: workspace_manifest.replace,
300 };
301
302 manifest.bins.push(Bin {
303 name: Name(project_name.to_owned()),
304 path: Path::new("main.rs").to_owned(),
305 });
306
307 for expanded in tests {
308 if expanded.error.is_none() {
309 manifest.bins.push(Bin {
310 name: expanded.name.clone(),
311 path: source_dir.join(&expanded.test.path),
312 });
313 }
314 }
315
316 Ok(manifest)
317 }
318
319 fn run_all(&self, project: &Project, tests: Vec<ExpandedTest>) -> Result<Report> {
320 let mut report = Report {
321 failures: 0,
322 created_wip: 0,
323 };
324
325 let mut path_map = Map::new();
326 for t in &tests {
327 let src_path = project.source_dir.join(&t.test.path);
328 path_map.insert(src_path, (&t.name, &t.test));
329 }
330
331 let output = cargo::build_all_tests(project)?;
332 let parsed = parse_cargo_json(project, &output.stdout, &path_map);
333 let fallback = Stderr::default();
334
335 for mut t in tests {
336 let show_expected = false;
337 message::begin_test(&t.test, show_expected);
338
339 if t.error.is_none() {
340 t.error = check_exists(&t.test.path).err();
341 }
342
343 if t.error.is_none() {
344 let src_path = project.source_dir.join(&t.test.path);
345 let this_test = parsed.stderrs.get(&src_path).unwrap_or(&fallback);
346 match t.test.check(project, &t.name, this_test, "") {
347 Ok(Outcome::Passed) => {}
348 Ok(Outcome::CreatedWip) => report.created_wip += 1,
349 Err(error) => t.error = Some(error),
350 }
351 }
352
353 if let Some(err) = t.error {
354 report.failures += 1;
355 message::test_fail(err);
356 }
357 }
358
359 Ok(report)
360 }
361}
362
363enum Outcome {
364 Passed,
365 CreatedWip,
366}
367
368impl Test {
369 fn run(&self, project: &Project, name: &Name) -> Result<Outcome> {
370 let show_expected = project.has_pass && project.has_compile_fail;
371 message::begin_test(self, show_expected);
372 check_exists(&self.path)?;
373
374 let mut path_map = Map::new();
375 let src_path = project.source_dir.join(&self.path);
376 path_map.insert(src_path.clone(), (name, self));
377
378 let output = cargo::build_test(project, name)?;
379 let parsed = parse_cargo_json(project, &output.stdout, &path_map);
380 let fallback = Stderr::default();
381 let this_test = parsed.stderrs.get(&src_path).unwrap_or(&fallback);
382 self.check(project, name, this_test, &parsed.stdout)
383 }
384
385 fn check(
386 &self,
387 project: &Project,
388 name: &Name,
389 result: &Stderr,
390 build_stdout: &str,
391 ) -> Result<Outcome> {
392 let check = match self.expected {
393 Expected::Pass => Test::check_pass,
394 Expected::CompileFail => Test::check_compile_fail,
395 };
396
397 check(
398 self,
399 project,
400 name,
401 result.success,
402 build_stdout,
403 &result.stderr,
404 )
405 }
406
407 fn check_pass(
408 &self,
409 project: &Project,
410 name: &Name,
411 success: bool,
412 build_stdout: &str,
413 variations: &Variations,
414 ) -> Result<Outcome> {
415 let preferred = variations.preferred();
416 if !success {
417 message::failed_to_build(preferred);
418 return Err(Error::CargoFail);
419 }
420
421 let mut output = cargo::run_test(project, name)?;
422 output.stdout.splice(..0, build_stdout.bytes());
423 message::output(preferred, &output);
424 if output.status.success() {
425 Ok(Outcome::Passed)
426 } else {
427 Err(Error::RunFailed)
428 }
429 }
430
431 fn check_compile_fail(
432 &self,
433 project: &Project,
434 _name: &Name,
435 success: bool,
436 build_stdout: &str,
437 variations: &Variations,
438 ) -> Result<Outcome> {
439 let preferred = variations.preferred();
440
441 if success {
442 message::should_not_have_compiled();
443 message::fail_output(Fail, build_stdout);
444 message::warnings(preferred);
445 return Err(Error::ShouldNotHaveCompiled);
446 }
447
448 let stderr_path = self.path.with_extension("stderr");
449
450 if !stderr_path.exists() {
451 let outcome = match project.update {
452 Update::Wip => {
453 let wip_dir = Path::new("wip");
454 fs::create_dir_all(wip_dir)?;
455 let gitignore_path = wip_dir.join(".gitignore");
456 fs::write(gitignore_path, "*\n")?;
457 let stderr_name = stderr_path
458 .file_name()
459 .unwrap_or_else(|| OsStr::new("test.stderr"));
460 let wip_path = wip_dir.join(stderr_name);
461 message::write_stderr_wip(&wip_path, &stderr_path, preferred);
462 fs::write(wip_path, preferred).map_err(Error::WriteStderr)?;
463 Outcome::CreatedWip
464 }
465 Update::Overwrite => {
466 message::overwrite_stderr(&stderr_path, preferred);
467 fs::write(stderr_path, preferred).map_err(Error::WriteStderr)?;
468 Outcome::Passed
469 }
470 };
471 message::fail_output(Warn, build_stdout);
472 return Ok(outcome);
473 }
474
475 let expected = fs::read_to_string(&stderr_path)
476 .map_err(Error::ReadStderr)?
477 .replace("\r\n", "\n");
478
479 if variations.any(|stderr| expected == stderr) {
480 message::ok();
481 return Ok(Outcome::Passed);
482 }
483
484 match project.update {
485 Update::Wip => {
486 message::mismatch(&expected, preferred);
487 Err(Error::Mismatch)
488 }
489 Update::Overwrite => {
490 message::overwrite_stderr(&stderr_path, preferred);
491 fs::write(stderr_path, preferred).map_err(Error::WriteStderr)?;
492 Ok(Outcome::Passed)
493 }
494 }
495 }
496}
497
498fn check_exists(path: &Path) -> Result<()> {
499 if path.exists() {
500 return Ok(());
501 }
502 match File::open(path) {
503 Ok(_) => Ok(()),
504 Err(err) => Err(Error::Open(path.to_owned(), err)),
505 }
506}
507
508impl ExpandedTest {
509 fn run(self, project: &Project) -> Result<Outcome> {
510 match self.error {
511 None => self.test.run(project, &self.name),
512 Some(error) => {
513 let show_expected = false;
514 message::begin_test(&self.test, show_expected);
515 Err(error)
516 }
517 }
518 }
519}
520
521#[allow(clippy::needless_collect)] fn filter(tests: &mut Vec<ExpandedTest>) {
532 let filters = env::args_os()
533 .flat_map(OsString::into_string)
534 .filter_map(|mut arg| {
535 const PREFIX: &str = "trybuild=";
536 if arg.starts_with(PREFIX) && arg != PREFIX {
537 Some(arg.split_off(PREFIX.len()))
538 } else {
539 None
540 }
541 })
542 .collect::<Vec<String>>();
543
544 if filters.is_empty() {
545 return;
546 }
547
548 tests.retain(|t| {
549 filters
550 .iter()
551 .any(|f| t.test.path.to_string_lossy().contains(f))
552 });
553}
554
555#[derive(Deserialize)]
556struct CargoMessage {
557 #[allow(dead_code)]
558 reason: Reason,
559 target: RustcTarget,
560 message: RustcMessage,
561}
562
563#[derive(Deserialize)]
564enum Reason {
565 #[serde(rename = "compiler-message")]
566 CompilerMessage,
567}
568
569#[derive(Deserialize)]
570struct RustcTarget {
571 src_path: PathBuf,
572}
573
574#[derive(Deserialize)]
575struct RustcMessage {
576 rendered: String,
577 level: String,
578}
579
580struct ParsedOutputs {
581 stdout: String,
582 stderrs: Map<PathBuf, Stderr>,
583}
584
585struct Stderr {
586 success: bool,
587 stderr: Variations,
588}
589
590impl Default for Stderr {
591 fn default() -> Self {
592 Stderr {
593 success: true,
594 stderr: Variations::default(),
595 }
596 }
597}
598
599fn parse_cargo_json(
600 project: &Project,
601 stdout: &[u8],
602 path_map: &Map<PathBuf, (&Name, &Test)>,
603) -> ParsedOutputs {
604 let mut map = Map::new();
605 let mut nonmessage_stdout = String::new();
606 let mut remaining = &*String::from_utf8_lossy(stdout);
607 let mut seen = Set::new();
608 while !remaining.is_empty() {
609 let Some(begin) = remaining.find("{\"reason\":") else {
610 break;
611 };
612 let (nonmessage, rest) = remaining.split_at(begin);
613 nonmessage_stdout.push_str(nonmessage);
614 let len = match rest.find('\n') {
615 Some(end) => end + 1,
616 None => rest.len(),
617 };
618 let (message, rest) = rest.split_at(len);
619 remaining = rest;
620 if !seen.insert(message) {
621 continue;
626 }
627 if let Ok(de) = serde_json::from_str::<CargoMessage>(message) {
628 if de.message.level != "failure-note" {
629 let Some((name, test)) = path_map.get(&de.target.src_path) else {
630 continue;
631 };
632 let entry = map
633 .entry(de.target.src_path)
634 .or_insert_with(Stderr::default);
635 if de.message.level == "error" {
636 entry.success = false;
637 }
638 let normalized = normalize::diagnostics(
639 &de.message.rendered,
640 Context {
641 krate: &name.0,
642 source_dir: &project.source_dir,
643 workspace: &project.workspace,
644 input_file: &test.path,
645 target_dir: &project.target_dir,
646 path_dependencies: &project.path_dependencies,
647 },
648 );
649 entry.stderr.concat(&normalized);
650 }
651 }
652 }
653 nonmessage_stdout.push_str(remaining);
654 ParsedOutputs {
655 stdout: nonmessage_stdout,
656 stderrs: map,
657 }
658}