1use std::{
2 collections::{hash_map::Entry, HashMap},
3 ffi::OsString,
4 io::{BufReader, ErrorKind},
5 path::{Path, PathBuf},
6 process::Stdio,
7};
8
9use cargo_metadata::{
10 diagnostic::{Diagnostic, DiagnosticLevel},
11 BuildFinished, Edition, Message, MetadataCommand,
12};
13
14#[derive(thiserror::Error, Debug)]
15pub enum Error {
16 #[error("Missing environment variable: {0}. Expected to be run by cargo.")]
17 MissingEnvVar(&'static str),
18
19 #[error("Failed to get cargo metadata: {0}")]
20 CargoMetadata(#[from] cargo_metadata::Error),
21
22 #[error("Failed to find package: {0}")]
23 NoPackage(std::io::Error),
24
25 #[error("Failed to get path to test's executable: {0}")]
26 NoCurrentExe(std::io::Error),
27
28 #[error("Failed create cargo package for test binary: {0}")]
29 CreatingCargoPackage(std::io::Error),
30
31 #[error("Failed to run cargo build for test binary: {0}")]
32 CargoBuildCommand(std::io::Error),
33
34 #[error("Failed to build test binary.")]
35 BuildError(HashMap<DiagnosticLevel, Vec<Diagnostic>>),
36}
37
38type Result<T, E = Error> = std::result::Result<T, E>;
39
40#[derive(Debug)]
41pub struct DetectedEnvironment {
42 target: String,
43 package_name: String,
44 package_manifest: PathBuf,
45 test_name: String,
46 edition: Edition,
47 target_directory: PathBuf,
48}
49
50#[derive(Debug)]
51pub struct Program {
52 path: PathBuf,
54
55 name: String,
56
57 source: String,
58
59 dependencies: Vec<String>,
60
61 detected: DetectedEnvironment,
62}
63
64impl Program {
65 pub fn new(name: String, source: String) -> Result<Self> {
66 let package_name = package_name()?;
67
68 let (base_dir, edition) = base_dir_and_edition(&package_name)?;
69
70 let target = target();
71
72 let package_manifest = package_manifest()?;
73
74 let test_name = test_name()?;
75
76 let path = base_dir.join(&package_name).join(&test_name).join(&name);
77
78 let path_dependency = format!(
79 "{} = {{ path = \"{}\"}}",
80 package_name,
81 package_manifest.to_string_lossy()
82 );
83
84 Ok(Program {
85 path,
86 name,
87 source,
88 dependencies: vec![path_dependency],
89 detected: DetectedEnvironment {
90 target: target.to_owned(),
91 package_name,
92 package_manifest,
93 test_name,
94 edition,
95 target_directory: base_dir
96 .parent()
97 .expect("base dir should have parent")
98 .to_path_buf(),
99 },
100 })
101 }
102
103 pub fn add_dependency(&mut self, value: String) {
104 self.dependencies.push(value);
105 }
106
107 pub fn compile(self) -> Result<Binary> {
108 let cargo_path = cargo_path()?;
109
110 std::fs::create_dir_all(&self.path)
112 .map_err(Error::CreatingCargoPackage)?;
113
114 std::fs::write(self.path.join("Cargo.toml"), format!(r#"
117[package]
118name = "{name}"
119version = "0.1.0"
120edition = "{edition}"
121publish = false
122
123[[bin]]
124name = "{name}"
125path = "main.rs"
126
127[dependencies]
128{dependencies}
129
130[package.metadata.treadmill]
131notice = "This package was automatically generated by the `treadmill` crate for use with tests."
132"#,
133 name = &self.name,
134 dependencies = self.dependencies.join("\n"),
135 edition = self.detected.edition.as_str(),
136 )).map_err(Error::CreatingCargoPackage)?;
137
138 std::fs::write(self.path.join("main.rs"), self.source)
140 .map_err(Error::CreatingCargoPackage)?;
141
142 let mut command = std::process::Command::new(cargo_path);
144 command
145 .args(["build", "--message-format=json", "-q", "--manifest-path"])
146 .arg(self.path.join("Cargo.toml"))
147 .arg("--bin")
148 .arg(&self.name)
149 .arg("--target")
150 .arg(&self.detected.target)
151 .arg("--target-dir")
152 .arg(&self.detected.target_directory)
153 .stdout(Stdio::piped());
154
155 let mut spawned = command.spawn().map_err(Error::CargoBuildCommand)?;
156
157 let reader = BufReader::new(
158 spawned
159 .stdout
160 .take()
161 .expect("spawned should have stdout buffer"),
162 );
163
164 let messages = Message::parse_stream(reader);
165
166 let mut executable_path = None;
167 let mut diagnostics: HashMap<DiagnosticLevel, Vec<Diagnostic>> =
168 HashMap::new();
169 let mut build_finished = false;
170
171 for message in messages {
173 match message {
174 Ok(Message::CompilerArtifact(artifact))
175 if artifact.target.name == self.name =>
176 {
177 if let Some(path) = artifact.executable {
178 executable_path = Some(path);
179 }
180 }
181 Ok(Message::CompilerMessage(msg))
182 if msg.target.name == self.name =>
183 {
184 match diagnostics.entry(msg.message.level) {
185 Entry::Occupied(occupied) => {
186 occupied.into_mut().push(msg.message)
187 }
188 Entry::Vacant(vacant) => {
189 vacant.insert(vec![msg.message]);
190 }
191 }
192 }
193 Ok(Message::BuildFinished(BuildFinished {
194 success: true,
195 ..
196 })) => build_finished = true,
197 _ => {}
198 }
199 }
200
201 if build_finished {
202 if let Some(path) = executable_path {
203 Ok(Binary {
204 path: path.into_std_path_buf(),
205 })
206 } else {
207 Err(Error::CargoBuildCommand(std::io::Error::new(
208 ErrorKind::Other,
209 "No executable was generated.",
210 )))
211 }
212 } else {
213 Err(Error::BuildError(diagnostics))
214 }
215 }
216}
217
218fn environment_var(name: &'static str) -> Result<OsString> {
219 std::env::var_os(name).ok_or(Error::MissingEnvVar(name))
220}
221
222fn base_dir_and_edition(package_name: &str) -> Result<(PathBuf, Edition)> {
223 let manifest_path = package_manifest()?.join("Cargo.toml");
224
225 let metadata =
227 MetadataCommand::new().manifest_path(manifest_path).exec()?;
228
229 let package = metadata
230 .packages
231 .iter()
232 .find(|package| package.name == package_name)
233 .ok_or_else(|| {
234 Error::NoPackage(std::io::Error::new(ErrorKind::Other, ""))
235 })?;
236
237 let edition = package.edition.clone();
238
239 Ok((
242 metadata.target_directory.join("ttp").into_std_path_buf(),
243 edition,
244 ))
245}
246
247fn cargo_path() -> Result<PathBuf> {
248 environment_var("CARGO").map(PathBuf::from)
251}
252
253fn target() -> &'static str {
254 env!("TREADMILL_TARGET")
256}
257
258fn package_manifest() -> Result<PathBuf> {
259 environment_var("CARGO_MANIFEST_DIR").map(PathBuf::from)
262}
263
264fn package_name() -> Result<String> {
265 environment_var("CARGO_PKG_NAME")
268 .map(|name| name.to_string_lossy().into_owned())
269}
270
271fn test_name() -> Result<String> {
272 let current_exe = std::env::current_exe().map_err(Error::NoCurrentExe)?;
275
276 let name = current_exe
277 .file_name()
278 .ok_or_else(|| {
279 Error::NoCurrentExe(std::io::Error::new(
280 ErrorKind::Other,
281 "File doesn't have a file name.",
282 ))
283 })?
284 .to_string_lossy()
285 .into_owned();
286
287 if let Some((test_name, _)) = name.rsplit_once('-') {
289 Ok(test_name.to_owned())
290 } else {
291 Ok(name)
292 }
293}
294
295pub struct Binary {
296 path: PathBuf,
297}
298
299impl Binary {
300 pub fn command(&self) -> std::process::Command {
301 std::process::Command::new(&self.path)
302 }
303}