1use std::fs::{self, File};
2use std::io::{Read, Seek, SeekFrom, Write};
3use std::path::{Path, PathBuf};
4
5use dfir_lang::graph::DfirGraph;
6use sha2::{Digest, Sha256};
7use stageleft::internal::quote;
8use syn::visit_mut::VisitMut;
9use trybuild_internals_api::cargo::{self, Metadata};
10use trybuild_internals_api::env::Update;
11use trybuild_internals_api::run::{PathDependency, Project};
12use trybuild_internals_api::{Runner, dependencies, features, path};
13
14use super::trybuild_rewriters::UseTestModeStaged;
15
16pub const HYDRO_RUNTIME_FEATURES: &[&str] = &["deploy_integration", "runtime_measure"];
17
18static IS_TEST: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
19
20static CONCURRENT_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
21
22pub fn init_test() {
36 IS_TEST.store(true, std::sync::atomic::Ordering::Relaxed);
37}
38
39fn clean_name_hint(name_hint: &str) -> String {
40 name_hint
41 .replace("::", "__")
42 .replace(" ", "_")
43 .replace(",", "_")
44 .replace("<", "_")
45 .replace(">", "")
46 .replace("(", "")
47 .replace(")", "")
48}
49
50pub struct TrybuildConfig {
51 pub project_dir: PathBuf,
52 pub target_dir: PathBuf,
53 pub features: Option<Vec<String>>,
54}
55
56pub fn create_graph_trybuild(
57 graph: DfirGraph,
58 extra_stmts: Vec<syn::Stmt>,
59 name_hint: &Option<String>,
60) -> (String, TrybuildConfig) {
61 let source_dir = cargo::manifest_dir().unwrap();
62 let source_manifest = dependencies::get_manifest(&source_dir).unwrap();
63 let crate_name = &source_manifest.package.name.to_string().replace("-", "_");
64
65 let is_test = IS_TEST.load(std::sync::atomic::Ordering::Relaxed);
66
67 let generated_code = compile_graph_trybuild(graph, extra_stmts, crate_name.clone(), is_test);
68
69 let inlined_staged: syn::File = if is_test {
70 let gen_staged = stageleft_tool::gen_staged_trybuild(
71 &path!(source_dir / "src" / "lib.rs"),
72 &path!(source_dir / "Cargo.toml"),
73 crate_name.clone(),
74 is_test,
75 );
76
77 syn::parse_quote! {
78 #[allow(
79 unused,
80 ambiguous_glob_reexports,
81 clippy::suspicious_else_formatting,
82 unexpected_cfgs,
83 reason = "generated code"
84 )]
85 pub mod __staged {
86 #gen_staged
87 }
88 }
89 } else {
90 let crate_name_ident = syn::Ident::new(crate_name, proc_macro2::Span::call_site());
91 syn::parse_quote!(
92 pub use #crate_name_ident::__staged;
93 )
94 };
95
96 let source = prettyplease::unparse(&syn::parse_quote! {
97 #generated_code
98
99 #inlined_staged
100 });
101
102 let hash = format!("{:X}", Sha256::digest(&source))
103 .chars()
104 .take(8)
105 .collect::<String>();
106
107 let bin_name = if let Some(name_hint) = &name_hint {
108 format!("{}_{}", clean_name_hint(name_hint), &hash)
109 } else {
110 hash
111 };
112
113 let (project_dir, target_dir, mut cur_bin_enabled_features) = create_trybuild().unwrap();
114
115 fs::create_dir_all(path!(project_dir / "src" / "bin")).unwrap();
117
118 let out_path = path!(project_dir / "src" / "bin" / format!("{bin_name}.rs"));
119 {
120 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
121 write_atomic(source.as_ref(), &out_path).unwrap();
122 }
123
124 if is_test {
125 if cur_bin_enabled_features.is_none() {
126 cur_bin_enabled_features = Some(vec![]);
127 }
128
129 cur_bin_enabled_features
130 .as_mut()
131 .unwrap()
132 .push("hydro___test".to_string());
133 }
134
135 (
136 bin_name,
137 TrybuildConfig {
138 project_dir,
139 target_dir,
140 features: cur_bin_enabled_features,
141 },
142 )
143}
144
145pub fn compile_graph_trybuild(
146 partitioned_graph: DfirGraph,
147 extra_stmts: Vec<syn::Stmt>,
148 crate_name: String,
149 is_test: bool,
150) -> syn::File {
151 let mut diagnostics = Vec::new();
152 let mut dfir_expr: syn::Expr = syn::parse2(partitioned_graph.as_code(
153 "e! { __root_dfir_rs },
154 true,
155 quote!(),
156 &mut diagnostics,
157 ))
158 .unwrap();
159
160 if is_test {
161 UseTestModeStaged {
162 crate_name: crate_name.clone(),
163 }
164 .visit_expr_mut(&mut dfir_expr);
165 }
166
167 let source_ast: syn::File = syn::parse_quote! {
168 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
169 use hydro_lang::prelude::*;
170 use hydro_lang::runtime_support::dfir_rs as __root_dfir_rs;
171
172 #[allow(unused)]
173 fn __hydro_runtime<'a>(__hydro_lang_trybuild_cli: &'a hydro_lang::runtime_support::dfir_rs::util::deploy::DeployPorts<hydro_lang::__staged::deploy::deploy_runtime::HydroMeta>) -> hydro_lang::runtime_support::dfir_rs::scheduled::graph::Dfir<'a> {
174 #(#extra_stmts)*
175 #dfir_expr
176 }
177
178 #[hydro_lang::runtime_support::tokio::main(crate = "hydro_lang::runtime_support::tokio", flavor = "current_thread")]
179 async fn main() {
180 let ports = hydro_lang::runtime_support::dfir_rs::util::deploy::init_no_ack_start().await;
181 let flow = __hydro_runtime(&ports);
182 println!("ack start");
183
184 hydro_lang::runtime_support::resource_measurement::run(flow).await;
185 }
186 };
187 source_ast
188}
189
190pub fn create_trybuild()
191-> Result<(PathBuf, PathBuf, Option<Vec<String>>), trybuild_internals_api::error::Error> {
192 let Metadata {
193 target_directory: target_dir,
194 workspace_root: workspace,
195 packages,
196 } = cargo::metadata()?;
197
198 let source_dir = cargo::manifest_dir()?;
199 let mut source_manifest = dependencies::get_manifest(&source_dir)?;
200
201 let mut dev_dependency_features = vec![];
202 source_manifest.dev_dependencies.retain(|k, v| {
203 if source_manifest.dependencies.contains_key(k) {
204 for feat in &v.features {
206 dev_dependency_features.push(format!("{}/{}", k, feat));
207 }
208
209 false
210 } else {
211 dev_dependency_features.push(format!("dep:{k}"));
213
214 v.optional = true;
215 true
216 }
217 });
218
219 let mut features = features::find();
220
221 let path_dependencies = source_manifest
222 .dependencies
223 .iter()
224 .filter_map(|(name, dep)| {
225 let path = dep.path.as_ref()?;
226 if packages.iter().any(|p| &p.name == name) {
227 None
229 } else {
230 Some(PathDependency {
231 name: name.clone(),
232 normalized_path: path.canonicalize().ok()?,
233 })
234 }
235 })
236 .collect();
237
238 let crate_name = source_manifest.package.name.clone();
239 let project_dir = path!(target_dir / "hydro_trybuild" / crate_name /);
240 fs::create_dir_all(&project_dir)?;
241
242 let project_name = format!("{}-hydro-trybuild", crate_name);
243 let mut manifest = Runner::make_manifest(
244 &workspace,
245 &project_name,
246 &source_dir,
247 &packages,
248 &[],
249 source_manifest,
250 )?;
251
252 if let Some(enabled_features) = &mut features {
253 enabled_features
254 .retain(|feature| manifest.features.contains_key(feature) || feature == "default");
255 }
256
257 for runtime_feature in HYDRO_RUNTIME_FEATURES {
258 manifest.features.insert(
259 format!("hydro___feature_{runtime_feature}"),
260 vec![format!("hydro_lang/{runtime_feature}")],
261 );
262 }
263
264 manifest
265 .dependencies
266 .get_mut("hydro_lang")
267 .unwrap()
268 .features
269 .push("runtime_support".to_string());
270
271 manifest
272 .features
273 .insert("hydro___test".to_string(), dev_dependency_features);
274
275 let project = Project {
276 dir: project_dir,
277 source_dir,
278 target_dir,
279 name: project_name,
280 update: Update::env()?,
281 has_pass: false,
282 has_compile_fail: false,
283 features,
284 workspace,
285 path_dependencies,
286 manifest,
287 keep_going: false,
288 };
289
290 {
291 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
292
293 let project_lock = File::create(path!(project.dir / ".hydro-trybuild-lock"))?;
294 project_lock.lock()?;
295
296 let manifest_toml = toml::to_string(&project.manifest)?;
297 write_atomic(manifest_toml.as_ref(), &path!(project.dir / "Cargo.toml"))?;
298
299 let workspace_cargo_lock = path!(project.workspace / "Cargo.lock");
300 if workspace_cargo_lock.exists() {
301 write_atomic(
302 fs::read_to_string(&workspace_cargo_lock)?.as_ref(),
303 &path!(project.dir / "Cargo.lock"),
304 )?;
305 } else {
306 let _ = cargo::cargo(&project).arg("generate-lockfile").status();
307 }
308
309 let workspace_dot_cargo_config_toml = path!(project.workspace / ".cargo" / "config.toml");
310 if workspace_dot_cargo_config_toml.exists() {
311 let dot_cargo_folder = path!(project.dir / ".cargo");
312 fs::create_dir_all(&dot_cargo_folder)?;
313
314 write_atomic(
315 fs::read_to_string(&workspace_dot_cargo_config_toml)?.as_ref(),
316 &path!(dot_cargo_folder / "config.toml"),
317 )?;
318 }
319
320 let vscode_folder = path!(project.dir / ".vscode");
321 fs::create_dir_all(&vscode_folder)?;
322 write_atomic(
323 include_bytes!("./vscode-trybuild.json"),
324 &path!(vscode_folder / "settings.json"),
325 )?;
326 }
327
328 Ok((
329 project.dir.as_ref().into(),
330 path!(project.target_dir / "hydro_trybuild"),
331 project.features,
332 ))
333}
334
335fn write_atomic(contents: &[u8], path: &Path) -> Result<(), std::io::Error> {
336 let mut file = File::options()
337 .read(true)
338 .write(true)
339 .create(true)
340 .truncate(false)
341 .open(path)?;
342 file.lock()?;
343
344 let mut existing_contents = Vec::new();
345 file.read_to_end(&mut existing_contents)?;
346 if existing_contents != contents {
347 file.seek(SeekFrom::Start(0))?;
348 file.set_len(0)?;
349 file.write_all(contents)?;
350 }
351
352 Ok(())
353}