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