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