trybuild_internals_api/
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::{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                    // Skip path dependencies coming from the workspace itself
139                    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            // Within a workspace, only the [patch] and [replace] sections in
297            // the workspace root's Cargo.toml are applied by Cargo.
298            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// Filter which test cases are run by trybuild.
522//
523//     $ cargo test -- ui trybuild=tuple_structs.rs
524//
525// The first argument after `--` must be the trybuild test name i.e. the name of
526// the function that has the #[test] attribute and calls trybuild. That's to get
527// Cargo to run the test at all. The next argument starting with `trybuild=`
528// provides a filename filter. Only test cases whose filename contains the
529// filter string will be run.
530#[allow(clippy::needless_collect)] // false positive https://github.com/rust-lang/rust-clippy/issues/5991
531fn 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            // Discard duplicate messages. This might no longer be necessary
622            // after https://github.com/rust-lang/rust/issues/106571 is fixed.
623            // Normally rustc would filter duplicates itself and I think this is
624            // a short-lived bug.
625            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}