trybuild/
run.rs

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