Skip to main content

forge/
symbolic_regression.rs

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/// Configuration for emitting Solidity regression tests from symbolic counterexamples.
20#[derive(Clone, Debug)]
21pub(crate) struct SymbolicRegressionConfig {
22    pub out: Option<PathBuf>,
23    pub overwrite: bool,
24}
25
26/// Solidity regression file emitted from a symbolic counterexample artifact.
27#[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}