test_runner_lib/
lib.rs

1/// A library which exposes some minimal configurable targets for creating [nextest](https://github.com/nextest-rs/nextest) targets.
2use std::collections::BTreeSet;
3
4use camino::Utf8PathBuf;
5use nextest_filtering::{Filterset, FiltersetKind, ParseContext};
6use nextest_metadata::{BuildPlatform, RustBinaryId, RustTestBinaryKind};
7use nextest_runner::cargo_config::{CargoConfigs, EnvironmentMap};
8use nextest_runner::config::core::NextestConfig;
9use nextest_runner::double_spawn::DoubleSpawnInfo;
10use nextest_runner::input::InputHandlerKind;
11use nextest_runner::list::{RustBuildMeta, RustTestArtifact, TestExecuteContext};
12use nextest_runner::platform::BuildPlatforms;
13use nextest_runner::reporter::structured::StructuredReporter;
14use nextest_runner::reporter::{ReporterBuilder, ReporterStderr};
15use nextest_runner::reuse_build::PathMapper;
16use nextest_runner::signal::SignalHandlerKind;
17use nextest_runner::target_runner::TargetRunner;
18use nextest_runner::test_filter::{FilterBound, RunIgnored, TestFilterBuilder, TestFilterPatterns};
19
20pub struct Config {
21    pub package: String,
22    pub config_path: Utf8PathBuf,
23    pub tmp_dir: Utf8PathBuf,
24    pub profile: String,
25    pub xml_output_file: Option<Utf8PathBuf>,
26    pub test_output_dir: Option<Utf8PathBuf>,
27    pub binaries: Vec<Binary>,
28    pub args: Args,
29    pub insta: bool,
30    pub source_dir: Utf8PathBuf,
31}
32
33#[derive(clap::Args, Debug)]
34pub struct Args {
35    #[arg(long = "expr", short = 'E')]
36    pub expressions: Vec<String>,
37    #[arg(long = "skip")]
38    pub skipped: Vec<String>,
39    #[arg(long)]
40    pub exact: bool,
41    #[arg(long)]
42    pub ignored: bool,
43    #[arg(long)]
44    pub include_ignored: bool,
45    #[arg(name = "TEST")]
46    pub tests: Vec<String>,
47}
48
49pub struct Binary {
50    pub name: String,
51    pub path: Utf8PathBuf,
52}
53
54pub fn run_nextest(config: Config) {
55    let cwd = &Utf8PathBuf::from_path_buf(std::env::current_dir().unwrap()).unwrap();
56
57    let metadata = serde_json::json!({
58        "version": 1,
59        "workspace_members": [],
60        "workspace_default_members": [],
61        "packages": [
62            {
63                "name": config.package,
64                "version": "0.0.0",
65                "id": config.package,
66                "license": null,
67                "license_file": null,
68                "description": null,
69                "source": null,
70                "dependencies": [],
71                "targets": [],
72                "features": {},
73                "manifest_path": cwd.join("Cargo.toml"),
74                "metadata": null,
75                "publish": null,
76                "readme": null,
77                "repository": null,
78                "homepage": null,
79                "documentation": null,
80                "edition": "2024",
81                "links": null,
82                "default_run": null,
83                "rust_version": null
84            },
85        ],
86        "resolve": null,
87        "workspace_root": cwd,
88        "target_directory": cwd,
89    })
90    .to_string();
91
92    let metadata = guppy::CargoMetadata::parse_json(metadata).unwrap();
93    let graph = metadata.build_graph().unwrap();
94
95    let package = graph.packages().find(|p| p.name() == config.package).unwrap();
96
97    let double_spawn = DoubleSpawnInfo::disabled();
98    let build_platforms = BuildPlatforms::new_with_no_target().unwrap();
99    let configs = CargoConfigs::new([] as [&str; 0]).unwrap();
100    let target_runner = TargetRunner::new(&configs, &build_platforms).unwrap();
101
102    let ctx = TestExecuteContext {
103        double_spawn: &double_spawn,
104        profile_name: "test",
105        target_runner: &target_runner,
106    };
107
108    let artifacts = config.binaries.iter().map(|binary| RustTestArtifact {
109        binary_id: RustBinaryId::new(&binary.name),
110        binary_name: binary.name.clone(),
111        binary_path: binary.path.clone(),
112        cwd: cwd.clone(),
113        build_platform: BuildPlatform::Target,
114        kind: RustTestBinaryKind::LIB,
115        non_test_binaries: BTreeSet::new(),
116        package,
117    });
118
119    let mut nextest_config = std::fs::read_to_string(&config.config_path)
120        .unwrap()
121        .parse::<toml_edit::DocumentMut>()
122        .unwrap();
123
124    nextest_config["store"]["dir"] = "".to_string().into();
125
126    let nextest_config_path = config.tmp_dir.join("__nextest-config.toml");
127
128    std::fs::write(&nextest_config_path, nextest_config.to_string()).unwrap();
129
130    let build_platforms = BuildPlatforms::new_with_no_target().unwrap();
131    let nextest_config = NextestConfig::from_sources(
132        &config.tmp_dir,
133        &ParseContext::new(&graph),
134        Some(&nextest_config_path),
135        [],
136        &BTreeSet::new(),
137    )
138    .unwrap();
139
140    let run_ignored = match (config.args.ignored, config.args.include_ignored) {
141        (true, _) => RunIgnored::Only,
142        (false, true) => RunIgnored::All,
143        (false, false) => RunIgnored::Default,
144    };
145
146    let mut patterns = TestFilterPatterns::default();
147
148    for test in config.args.tests {
149        if config.args.exact {
150            patterns.add_exact_pattern(test);
151        } else {
152            patterns.add_substring_pattern(test);
153        }
154    }
155
156    for test in config.args.skipped {
157        if config.args.exact {
158            patterns.add_skip_exact_pattern(test);
159        } else {
160            patterns.add_skip_pattern(test);
161        }
162    }
163
164    let exprs = config
165        .args
166        .expressions
167        .into_iter()
168        .map(|expr| Filterset::parse(expr, &ParseContext::new(&graph), FiltersetKind::Test))
169        .collect::<Result<Vec<_>, _>>()
170        .expect("failed to parse exprs");
171
172    let profile = nextest_config
173        .profile(&config.profile)
174        .unwrap()
175        .apply_build_platforms(&build_platforms);
176    let meta = RustBuildMeta::new(cwd, build_platforms).map_paths(&PathMapper::noop());
177    let filter = TestFilterBuilder::new(run_ignored, None, patterns, exprs).unwrap();
178    let env = EnvironmentMap::new(&configs);
179
180    let list = match nextest_runner::list::TestList::new(
181        &ctx,
182        artifacts,
183        meta,
184        &filter,
185        Utf8PathBuf::new(),
186        env,
187        &profile,
188        FilterBound::DefaultSet,
189        1,
190    ) {
191        Ok(l) => l,
192        Err(err) => {
193            panic!("{err:#}");
194        }
195    };
196
197    let runner = nextest_runner::runner::TestRunnerBuilder::default()
198        .build(
199            &list,
200            &profile,
201            Vec::new(),
202            SignalHandlerKind::Standard,
203            InputHandlerKind::Standard,
204            double_spawn,
205            target_runner,
206        )
207        .unwrap();
208
209    let mut reporter = ReporterBuilder::default()
210        .set_colorize(true)
211        .set_hide_progress_bar(true)
212        .build(&list, &profile, &configs, ReporterStderr::Terminal, StructuredReporter::new());
213
214    let r = runner
215        .execute(|event| {
216            reporter.report_event(event).unwrap();
217        })
218        .unwrap();
219
220    reporter.finish();
221
222    if let (Some(junit), Some(output)) = (profile.junit(), config.xml_output_file.as_ref()) {
223        let junit = std::fs::read(junit.path()).unwrap();
224        std::fs::write(output, junit).unwrap();
225    }
226
227    if config.insta
228        && let Some(test_output_dir) = &config.test_output_dir
229    {
230        for entry in walkdir::WalkDir::new(&config.source_dir) {
231            let Ok(entry) = entry else {
232                continue;
233            };
234
235            if entry
236                .file_name()
237                .to_str()
238                .is_some_and(|s| s.ends_with(".snap.new") || s.ends_with(".pending-snap"))
239            {
240                if let Some(parent) = entry.path().parent() {
241                    std::fs::create_dir_all(test_output_dir.join_os(parent)).unwrap();
242                }
243                std::fs::copy(entry.path(), test_output_dir.join_os(entry.path())).unwrap();
244            }
245        }
246    }
247
248    if r.has_failures() {
249        std::process::exit(1)
250    }
251}