1use crate::result::{
2 SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA, SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA_VERSION,
3 SuiteResult, SymbolicArtifactRef, SymbolicCounterexampleArtifact,
4 SymbolicCounterexampleArtifactKind, SymbolicRegressionRef, SymbolicReplayStatus,
5};
6use alloy_primitives::{U256, hex};
7use eyre::{Result, bail};
8use foundry_common::{TestFunctionExt, contracts::ContractsByArtifact, fs, sh_warn};
9use foundry_config::Config;
10use std::{
11 collections::{HashMap, HashSet},
12 fmt::Write,
13 path::{Component, Path, PathBuf},
14};
15
16pub(crate) const SYMBOLIC_REGRESSION_MARKER: &str = "__foundry_symbolic_regression_marker";
17
18#[derive(Clone, Debug)]
20pub(crate) struct SymbolicRegressionConfig {
21 pub out: Option<PathBuf>,
22 pub overwrite: bool,
23}
24
25#[derive(Clone, Debug)]
27pub(crate) struct SymbolicRegression {
28 pub artifact: PathBuf,
29 pub path: PathBuf,
30}
31
32struct PlannedSymbolicRegression {
33 regression: SymbolicRegression,
34 contents: String,
35 test: String,
36}
37
38pub(crate) fn emit_symbolic_regressions(
39 config: &Config,
40 regression: &SymbolicRegressionConfig,
41 known_contracts: &ContractsByArtifact,
42 results: &[SymbolicArtifactRef],
43) -> Result<Vec<SymbolicRegression>> {
44 let mut planned = Vec::new();
45 let mut seen_tests = HashSet::new();
46 for artifact_ref in results {
47 let artifact = load_artifact(&artifact_ref.path)?;
48 let (_, contract) = artifact_source_and_contract(&artifact_ref.path, &artifact)?;
49 if contract.ends_with("_SymbolicRegression") {
50 continue;
51 }
52 let key = (
53 artifact.test.contract.clone(),
54 artifact.test.test.clone(),
55 match artifact.kind {
56 SymbolicCounterexampleArtifactKind::SingleCall => "single_call",
57 SymbolicCounterexampleArtifactKind::Sequence => "sequence",
58 },
59 );
60 let suffix = if seen_tests.insert(key) {
61 None
62 } else if let Some(suffix) = handler_regression_suffix(&artifact_ref.path) {
63 Some(suffix)
64 } else {
65 continue;
66 };
67 planned.push(plan_symbolic_regression(
68 config,
69 regression,
70 &artifact_ref.path,
71 &artifact,
72 known_contracts,
73 suffix.as_deref(),
74 )?);
75 }
76
77 ensure_unique_regression_paths(&planned)?;
78
79 let mut emitted = Vec::new();
80 for plan in planned {
81 if let Some(regression) = write_symbolic_regression(regression, plan)? {
82 emitted.push(regression);
83 }
84 }
85 Ok(emitted)
86}
87
88fn artifact_source_and_contract<'a>(
89 artifact_path: &Path,
90 artifact: &'a SymbolicCounterexampleArtifact,
91) -> Result<(&'a str, &'a str)> {
92 let Some((source, contract)) = artifact.test.contract.rsplit_once(':') else {
93 bail!(
94 "symbolic counterexample artifact {} test.contract must be `path:Contract`, got `{}`",
95 artifact_path.display(),
96 artifact.test.contract
97 );
98 };
99 if source.is_empty() || contract.is_empty() {
100 bail!(
101 "symbolic counterexample artifact {} test.contract must be `path:Contract`, got `{}`",
102 artifact_path.display(),
103 artifact.test.contract
104 );
105 }
106 Ok((source, contract))
107}
108
109pub(crate) fn collect_symbolic_artifacts_from_suites<'a>(
110 suites: impl IntoIterator<Item = &'a SuiteResult>,
111) -> Vec<SymbolicArtifactRef> {
112 let mut artifacts = Vec::new();
113 for suite in suites {
114 for result in suite.test_results.values() {
115 for artifact in &result.counterexample_artifacts {
116 if !artifacts.contains(artifact) {
117 artifacts.push(artifact.clone());
118 }
119 }
120 }
121 }
122 artifacts
123}
124
125pub(crate) fn attach_symbolic_regressions_to_suites<'a>(
126 suites: impl IntoIterator<Item = &'a mut SuiteResult>,
127 regressions: &[SymbolicRegression],
128) {
129 for suite in suites {
130 for result in suite.test_results.values_mut() {
131 for artifact in &result.counterexample_artifacts {
132 for regression in regressions {
133 if artifact.path == regression.artifact
134 && !result
135 .symbolic_regressions
136 .iter()
137 .any(|existing| existing.path == regression.path)
138 {
139 result.symbolic_regressions.push(SymbolicRegressionRef {
140 artifact: regression.artifact.clone(),
141 path: regression.path.clone(),
142 });
143 }
144 }
145 }
146 }
147 }
148}
149
150fn load_artifact(path: &Path) -> Result<SymbolicCounterexampleArtifact> {
151 let value = fs::read_json_file::<serde_json::Value>(path)?;
152 let schema_version =
153 value.get("schema_version").and_then(serde_json::Value::as_u64).ok_or_else(|| {
154 eyre::eyre!(
155 "symbolic counterexample artifact {} is missing numeric schema_version",
156 path.display()
157 )
158 })?;
159 if schema_version != u64::from(SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA_VERSION) {
160 bail!(
161 "unsupported symbolic counterexample artifact schema version {} in {}",
162 schema_version,
163 path.display()
164 );
165 }
166 let schema = value.get("schema").and_then(serde_json::Value::as_str).ok_or_else(|| {
167 eyre::eyre!("symbolic counterexample artifact {} is missing string schema", path.display())
168 })?;
169 if schema != SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA {
170 bail!(
171 "unsupported symbolic counterexample artifact schema `{}` in {}",
172 schema,
173 path.display()
174 );
175 }
176 let artifact = serde_json::from_value::<SymbolicCounterexampleArtifact>(value)?;
177 if artifact.replay.status != SymbolicReplayStatus::Confirmed {
178 bail!(
179 "symbolic counterexample artifact {} replay status must be confirmed, got {:?}",
180 path.display(),
181 artifact.replay.status
182 );
183 }
184 if artifact.calls.is_empty() {
185 bail!("symbolic counterexample artifact {} has no calls", path.display());
186 }
187 Ok(artifact)
188}
189
190fn plan_symbolic_regression(
191 config: &Config,
192 regression: &SymbolicRegressionConfig,
193 artifact_path: &Path,
194 artifact: &SymbolicCounterexampleArtifact,
195 known_contracts: &ContractsByArtifact,
196 suffix: Option<&str>,
197) -> Result<PlannedSymbolicRegression> {
198 let (source, contract) = artifact_source_and_contract(artifact_path, artifact)?;
199
200 let test_name =
201 artifact.test.test.split_once('(').map_or(artifact.test.test.as_str(), |(name, _)| name);
202 let contract_ident = sanitize_identifier(contract);
203 let test_ident = sanitize_identifier(test_name);
204 let generated_contract = suffix.map_or_else(
205 || format!("{contract_ident}_{test_ident}_SymbolicRegression"),
206 |suffix| format!("{contract_ident}_{test_ident}_{suffix}_SymbolicRegression"),
207 );
208 let vm_interface = format!("{generated_contract}_Vm");
209 let generated_test = format!("test_regression_{test_ident}_symbolic");
210 let default_path = config.test.join("regressions").join(format!("{generated_contract}.t.sol"));
211 let path = regression.out.as_ref().map_or(default_path, |out| {
212 let out_is_file = if out.exists() {
213 !out.is_dir()
214 } else {
215 out.extension().is_some_and(|ext| ext == std::ffi::OsStr::new("sol"))
216 };
217 if out_is_file { out.clone() } else { out.join(format!("{generated_contract}.t.sol")) }
218 });
219
220 if let Some(parent) = path.parent() {
221 fs::create_dir_all(parent)?;
222 }
223
224 let import_path =
225 relative_solidity_import(path.parent().unwrap_or(&config.root), &config.root.join(source));
226 let mut contents = String::new();
227 writeln!(contents, "// SPDX-License-Identifier: UNLICENSED")?;
228 writeln!(contents, "pragma solidity >=0.8.0;")?;
229 writeln!(contents)?;
230 writeln!(contents, "import \"{import_path}\";")?;
231 writeln!(contents)?;
232 writeln!(contents, "interface {vm_interface} {{")?;
233 writeln!(contents, " function deal(address who, uint256 newBalance) external;")?;
234 writeln!(contents, " function prank(address msgSender) external;")?;
235 writeln!(contents, " function roll(uint256 newHeight) external;")?;
236 writeln!(contents, " function warp(uint256 newTimestamp) external;")?;
237 writeln!(contents, "}}")?;
238 writeln!(contents)?;
239 writeln!(contents, "contract {generated_contract} is {contract} {{")?;
240 writeln!(
241 contents,
242 " {vm_interface} private constant __foundrySymbolicVm = {vm_interface}(address(uint160(uint256(keccak256(\"hevm cheat code\")))));"
243 )?;
244 writeln!(contents)?;
245 writeln!(
246 contents,
247 " function {SYMBOLIC_REGRESSION_MARKER}() external pure returns (bytes32) {{"
248 )?;
249 writeln!(contents, " return keccak256(\"foundry.symbolic.regression\");")?;
250 writeln!(contents, " }}")?;
251 writeln!(contents)?;
252 writeln!(contents, " function {generated_test}() public payable {{")?;
253 match artifact.kind {
254 SymbolicCounterexampleArtifactKind::SingleCall => {
255 let call = artifact.calls.first().expect("single-call artifact has at least one call");
256 write_call(&mut contents, call, "address(this)", true, None)?;
257 }
258 SymbolicCounterexampleArtifactKind::Sequence => {
259 let call_after_invariant = call_after_invariant(known_contracts, artifact)?;
260 for (index, call) in artifact.calls.iter().enumerate() {
261 write_call(
262 &mut contents,
263 call,
264 &format!("address({})", call.target),
265 artifact.replay_semantics.fail_on_revert,
266 Some(index),
267 )?;
268 }
269 writeln!(
270 contents,
271 " __foundrySymbolicRegressionCall(address(this), hex\"{}\", 0, true);",
272 hex::encode(selector_from_signature(&artifact.test.test))
273 )?;
274 if call_after_invariant {
275 writeln!(
276 contents,
277 " __foundrySymbolicRegressionCall(address(this), hex\"{}\", 0, true);",
278 hex::encode(selector_from_signature("afterInvariant()"))
279 )?;
280 }
281 }
282 }
283 writeln!(contents, " }}")?;
284 writeln!(contents)?;
285 writeln!(
286 contents,
287 " function __foundrySymbolicRegressionCall(address target, bytes memory data, uint256 value, bool bubbleFailure) internal returns (bool) {{"
288 )?;
289 writeln!(contents, " if (target != address(this)) {{")?;
290 writeln!(contents, " uint256 codeSize;")?;
291 writeln!(contents, " assembly {{ codeSize := extcodesize(target) }}")?;
292 writeln!(
293 contents,
294 " require(codeSize != 0, \"symbolic regression target has no code\");"
295 )?;
296 writeln!(contents, " }}")?;
297 writeln!(contents, " (bool ok, bytes memory ret) = target.call{{value: value}}(data);")?;
298 writeln!(contents, " if (!ok && bubbleFailure) {{")?;
299 writeln!(contents, " assembly {{")?;
300 writeln!(contents, " revert(add(ret, 0x20), mload(ret))")?;
301 writeln!(contents, " }}")?;
302 writeln!(contents, " }}")?;
303 writeln!(contents, " return ok;")?;
304 writeln!(contents, " }}")?;
305 writeln!(contents, "}}")?;
306
307 Ok(PlannedSymbolicRegression {
308 regression: SymbolicRegression { artifact: artifact_path.to_path_buf(), path },
309 contents,
310 test: format!("{}::{}", artifact.test.contract, artifact.test.test),
311 })
312}
313
314fn ensure_unique_regression_paths(planned: &[PlannedSymbolicRegression]) -> Result<()> {
315 let mut seen = HashMap::<&Path, &PlannedSymbolicRegression>::new();
316 for plan in planned {
317 if let Some(previous) = seen.insert(&plan.regression.path, plan) {
318 bail!(
319 "multiple symbolic regressions resolve to {}; {} and {} cannot share one output file",
320 plan.regression.path.display(),
321 previous.test,
322 plan.test
323 );
324 }
325 }
326 Ok(())
327}
328
329fn write_symbolic_regression(
330 regression: &SymbolicRegressionConfig,
331 plan: PlannedSymbolicRegression,
332) -> Result<Option<SymbolicRegression>> {
333 let SymbolicRegression { artifact, path } = &plan.regression;
334 if path.exists() && !regression.overwrite {
335 if std::fs::read_to_string(path).is_ok_and(|existing| existing == plan.contents) {
336 return Ok(Some(plan.regression));
337 }
338 sh_warn!(
339 "Regression test {} already exists; skipping {} (pass --regression-overwrite to replace it)",
340 path.display(),
341 artifact.display()
342 )?;
343 return Ok(None);
344 }
345
346 std::fs::write(path, plan.contents)?;
347 Ok(Some(plan.regression))
348}
349
350fn write_call(
351 out: &mut String,
352 call: &crate::result::SymbolicCounterexampleCall,
353 target: &str,
354 bubble_failure: bool,
355 sequence_call_index: Option<usize>,
356) -> Result<()> {
357 if let Some(warp) = call.warp {
358 writeln!(
359 out,
360 " __foundrySymbolicVm.warp(block.timestamp + {});",
361 u256_literal(warp)
362 )?;
363 }
364 if let Some(roll) = call.roll {
365 writeln!(out, " __foundrySymbolicVm.roll(block.number + {});", u256_literal(roll))?;
366 }
367 if let Some(value) = call.value
368 && !value.is_zero()
369 {
370 if let Some(index) = sequence_call_index {
371 writeln!(
372 out,
373 " uint256 __foundrySymbolicValue{index} = {} <= address({}).balance ? {} : address({}).balance;",
374 u256_literal(value),
375 call.sender,
376 u256_literal(value),
377 call.sender
378 )?;
379 writeln!(
380 out,
381 " if (__foundrySymbolicValue{index} != 0 && {} != address(this)) {{",
382 call.sender
383 )?;
384 writeln!(
385 out,
386 " __foundrySymbolicVm.deal(address(this), address(this).balance + __foundrySymbolicValue{index});"
387 )?;
388 writeln!(
389 out,
390 " __foundrySymbolicVm.deal({}, address({}).balance - __foundrySymbolicValue{index});",
391 call.sender, call.sender
392 )?;
393 writeln!(out, " }}")?;
394 } else {
395 writeln!(
396 out,
397 " __foundrySymbolicVm.deal(address(this), {});",
398 u256_literal(value)
399 )?;
400 writeln!(
401 out,
402 " __foundrySymbolicVm.deal({}, {});",
403 call.sender,
404 u256_literal(value)
405 )?;
406 }
407 }
408 writeln!(out, " __foundrySymbolicVm.prank({});", call.sender)?;
409 let value = sequence_call_index
410 .filter(|_| call.value.is_some_and(|value| !value.is_zero()))
411 .map_or_else(
412 || call.value.map_or_else(|| "0".to_string(), u256_literal),
413 |index| format!("__foundrySymbolicValue{index}"),
414 );
415 if let Some(index) = sequence_call_index {
416 writeln!(
417 out,
418 " bool __foundrySymbolicOk{index} = __foundrySymbolicRegressionCall({target}, hex\"{}\", {}, {});",
419 hex::encode(call.calldata.as_ref()),
420 value,
421 if bubble_failure { "true" } else { "false" }
422 )?;
423 if call.value.is_some_and(|value| !value.is_zero()) {
424 writeln!(
425 out,
426 " if (__foundrySymbolicValue{index} != 0 && {} != address(this)) {{",
427 call.sender
428 )?;
429 writeln!(out, " if (!__foundrySymbolicOk{index}) {{")?;
430 writeln!(
431 out,
432 " __foundrySymbolicVm.deal({}, address({}).balance + __foundrySymbolicValue{index});",
433 call.sender, call.sender
434 )?;
435 writeln!(
436 out,
437 " __foundrySymbolicVm.deal(address(this), address(this).balance - __foundrySymbolicValue{index});"
438 )?;
439 writeln!(out, " }}")?;
440 writeln!(out, " }}")?;
441 }
442 } else {
443 writeln!(
444 out,
445 " __foundrySymbolicRegressionCall({target}, hex\"{}\", {}, {});",
446 hex::encode(call.calldata.as_ref()),
447 value,
448 if bubble_failure { "true" } else { "false" }
449 )?;
450 }
451 Ok(())
452}
453
454fn selector_from_signature(signature: &str) -> [u8; 4] {
455 let hash = alloy_primitives::keccak256(signature.as_bytes());
456 [hash[0], hash[1], hash[2], hash[3]]
457}
458
459fn u256_literal(value: U256) -> String {
460 if value <= U256::from(u128::MAX) { value.to_string() } else { format!("0x{value:x}") }
461}
462
463fn sanitize_identifier(value: &str) -> String {
464 let mut ident = String::with_capacity(value.len());
465 for ch in value.chars() {
466 if ch.is_ascii_alphanumeric() || ch == '_' {
467 ident.push(ch);
468 } else {
469 ident.push('_');
470 }
471 }
472 if ident.is_empty() || ident.as_bytes()[0].is_ascii_digit() {
473 ident.insert(0, '_');
474 }
475 ident
476}
477
478fn handler_regression_suffix(path: &Path) -> Option<String> {
479 let stem = path.file_stem()?.to_string_lossy();
480 stem.starts_with("handler-").then(|| sanitize_identifier(&stem))
481}
482
483fn call_after_invariant(
484 known_contracts: &ContractsByArtifact,
485 artifact: &SymbolicCounterexampleArtifact,
486) -> Result<bool> {
487 let Some((_, contract)) =
488 known_contracts.find_by_name_or_identifier(&artifact.test.contract)?
489 else {
490 return Ok(false);
491 };
492 let mut after_invariant_fns =
493 contract.abi.functions().filter(|func| func.name.is_after_invariant());
494 let Some(after_invariant) = after_invariant_fns.next() else {
495 return Ok(false);
496 };
497 Ok(after_invariant.name == "afterInvariant" && after_invariant_fns.next().is_none())
498}
499
500fn relative_solidity_import(from_dir: &Path, to_file: &Path) -> String {
501 let from = std::fs::canonicalize(from_dir).unwrap_or_else(|_| from_dir.to_path_buf());
502 let to = std::fs::canonicalize(to_file).unwrap_or_else(|_| to_file.to_path_buf());
503 let from = normalized_components(&from);
504 let to = normalized_components(&to);
505 let shared = from.iter().zip(&to).take_while(|(left, right)| left == right).count();
506 let mut parts = Vec::new();
507 parts.extend(std::iter::repeat_n("..".to_string(), from.len().saturating_sub(shared)));
508 parts.extend(to[shared..].iter().cloned());
509 if parts.is_empty() {
510 ".".to_string()
511 } else {
512 let path = parts.join("/");
513 if path.starts_with('.') { path } else { format!("./{path}") }
514 }
515}
516
517fn normalized_components(path: &Path) -> Vec<String> {
518 path.components()
519 .filter_map(|component| match component {
520 Component::Normal(value) => Some(value.to_string_lossy().to_string()),
521 Component::ParentDir => Some("..".to_string()),
522 Component::CurDir => None,
523 Component::RootDir | Component::Prefix(_) => {
524 Some(component.as_os_str().to_string_lossy().to_string())
525 }
526 })
527 .collect()
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533 use crate::result::{
534 SymbolicCallTrace, SymbolicCounterexample, SymbolicCounterexampleReplaySemantics,
535 SymbolicCounterexampleTestIdentity, SymbolicReplayMetadata, SymbolicResult,
536 };
537 use alloy_primitives::{Address, Bytes};
538 use foundry_config::SymbolicConfig;
539 use foundry_evm_symbolic::SymbolicStats;
540
541 fn config(root: &Path) -> Config {
542 Config { root: root.to_path_buf(), test: root.join("test"), ..Default::default() }
543 }
544
545 fn artifact(
546 kind: SymbolicCounterexampleArtifactKind,
547 calls: Vec<crate::result::SymbolicCounterexampleCall>,
548 ) -> SymbolicCounterexampleArtifact {
549 let call = calls.first().expect("artifact has at least one call");
550 let symbolic = SymbolicResult::fail_counterexample(
551 &SymbolicConfig::default(),
552 SymbolicStats::default(),
553 SymbolicCallTrace::none(),
554 SymbolicCounterexample {
555 calldata: call.calldata.clone(),
556 args: None,
557 raw_args: None,
558 value: call.value,
559 },
560 );
561 let mut artifact = SymbolicCounterexampleArtifact::new(
562 kind,
563 SymbolicCounterexampleTestIdentity {
564 contract: "test/Regression.t.sol:Regression".to_string(),
565 test: "invariant_value()".to_string(),
566 },
567 &symbolic,
568 SymbolicCounterexampleReplaySemantics { fail_on_revert: false },
569 calls,
570 );
571 artifact.replay = SymbolicReplayMetadata::confirmed();
572 artifact
573 }
574
575 fn value_call(target: Address) -> crate::result::SymbolicCounterexampleCall {
576 crate::result::SymbolicCounterexampleCall {
577 warp: None,
578 roll: None,
579 sender: Address::repeat_byte(0xb0),
580 target,
581 calldata: Bytes::from_static(&[0x12, 0x34, 0x56, 0x78]),
582 value: Some(U256::from(10)),
583 contract_name: None,
584 function_name: None,
585 signature: None,
586 args: None,
587 raw_args: None,
588 }
589 }
590
591 fn plan(
592 kind: SymbolicCounterexampleArtifactKind,
593 calls: Vec<crate::result::SymbolicCounterexampleCall>,
594 ) -> String {
595 let temp = tempfile::tempdir().unwrap();
596 let source = temp.path().join("test/Regression.t.sol");
597 fs::create_dir_all(source.parent().unwrap()).unwrap();
598 std::fs::write(&source, "contract Regression {}").unwrap();
599
600 let artifact_path = temp.path().join("cache/artifact.json");
601 let artifact = artifact(kind, calls);
602 plan_symbolic_regression(
603 &config(temp.path()),
604 &SymbolicRegressionConfig { out: None, overwrite: false },
605 &artifact_path,
606 &artifact,
607 &ContractsByArtifact::default(),
608 None,
609 )
610 .unwrap()
611 .contents
612 }
613
614 #[test]
615 fn sequence_regression_clamps_and_debits_sender_balance() {
616 let contents = plan(
617 SymbolicCounterexampleArtifactKind::Sequence,
618 vec![value_call(Address::repeat_byte(0x7f)), value_call(Address::repeat_byte(0x8f))],
619 );
620 let lowercase = contents.to_lowercase();
621
622 assert!(lowercase.contains(
623 "uint256 __foundrysymbolicvalue0 = 10 <= address(0xb0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0).balance ? 10 : address(0xb0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0).balance;"
624 ));
625 assert!(lowercase.contains(
626 "uint256 __foundrysymbolicvalue1 = 10 <= address(0xb0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0).balance ? 10 : address(0xb0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0).balance;"
627 ));
628 assert!(contents.contains("if (__foundrySymbolicValue0 != 0"));
629 assert!(contents.contains("if (__foundrySymbolicValue1 != 0"));
630 assert!(contents.contains(
631 "__foundrySymbolicVm.deal(address(this), address(this).balance + __foundrySymbolicValue0);"
632 ));
633 assert!(contents.contains("bool __foundrySymbolicOk0 = __foundrySymbolicRegressionCall"));
634 assert!(contents.contains("if (!__foundrySymbolicOk0) {"));
635 assert!(lowercase.contains(
636 "__foundrysymbolicvm.deal(0xb0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0, address(0xb0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0).balance - __foundrysymbolicvalue0);"
637 ));
638 assert!(lowercase.contains(
639 "__foundrysymbolicvm.deal(0xb0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0, address(0xb0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0).balance - __foundrysymbolicvalue1);"
640 ));
641 assert!(lowercase.contains(
642 "__foundrysymbolicvm.deal(0xb0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0, address(0xb0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0).balance + __foundrysymbolicvalue0);"
643 ));
644 assert!(contents.contains(
645 "__foundrySymbolicVm.deal(address(this), address(this).balance - __foundrySymbolicValue0);"
646 ));
647 assert!(lowercase.contains(
648 "__foundrysymbolicregressioncall(address(0x7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f), hex\"12345678\", __foundrysymbolicvalue0, false);"
649 ));
650 assert!(lowercase.contains(
651 "__foundrysymbolicregressioncall(address(0x8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f), hex\"12345678\", __foundrysymbolicvalue1, false);"
652 ));
653 assert!(
654 !lowercase.contains(
655 "__foundrysymbolicvm.deal(0xb0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0, 10);"
656 )
657 );
658 }
659
660 #[test]
661 fn single_call_regression_preserves_exact_value_funding() {
662 let contents = plan(
663 SymbolicCounterexampleArtifactKind::SingleCall,
664 vec![value_call(Address::repeat_byte(0x7f))],
665 );
666 let lowercase = contents.to_lowercase();
667
668 assert!(contents.contains("__foundrySymbolicVm.deal(address(this), 10);"));
669 assert!(
670 lowercase.contains(
671 "__foundrysymbolicvm.deal(0xb0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0, 10);"
672 )
673 );
674 assert!(contents.contains(
675 "__foundrySymbolicRegressionCall(address(this), hex\"12345678\", 10, true);"
676 ));
677 assert!(!contents.contains("bool __foundrySymbolicOk0 ="));
678 }
679
680 #[test]
681 fn relative_solidity_import_canonicalizes_symlinked_absolute_paths() {
682 let tmp = Path::new("/tmp");
683 let Ok(canonical_tmp) = std::fs::canonicalize(tmp) else { return };
684 if canonical_tmp == tmp {
685 return;
686 }
687
688 let root =
689 tmp.join(format!("foundry-symbolic-regression-import-test-{}", std::process::id()));
690 let _ = std::fs::remove_dir_all(&root);
691 let from_dir = root.join("test");
692 std::fs::create_dir_all(&from_dir).unwrap();
693 let to_file = from_dir.join("Original.t.sol");
694 std::fs::write(&to_file, "contract Original {}").unwrap();
695 let canonical_to_file = std::fs::canonicalize(&to_file).unwrap();
696
697 let import = relative_solidity_import(&from_dir, &canonical_to_file);
698
699 let _ = std::fs::remove_dir_all(&root);
700 assert_eq!(import, "./Original.t.sol");
701 }
702}