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