1use 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}