Skip to main content

foundry_common/
compile.rs

1//! Support for compiling [foundry_compilers::Project]
2
3use crate::{
4    TestFunctionExt, preprocessor::DynamicTestLinkingPreprocessor, shell, term::SpinnerReporter,
5};
6use comfy_table::{Cell, Color, Table, modifiers::UTF8_ROUND_CORNERS, presets::ASCII_MARKDOWN};
7use eyre::Result;
8use foundry_block_explorers::contract::Metadata;
9use foundry_compilers::{
10    Artifact, Project, ProjectBuilder, ProjectCompileOutput, ProjectPathsConfig, SolcConfig,
11    artifacts::{BytecodeObject, Contract, Source, remappings::Remapping},
12    compilers::{
13        Compiler,
14        solc::{Solc, SolcCompiler},
15    },
16    info::ContractInfo as CompilerContractInfo,
17    multi::{MultiCompiler, MultiCompilerSettings},
18    project::Preprocessor,
19    report::{BasicStdoutReporter, NoReporter, Report},
20    solc::SolcSettings,
21};
22use num_format::{Locale, ToFormattedString};
23use std::{
24    collections::BTreeMap,
25    fmt::Display,
26    io::IsTerminal,
27    path::{Path, PathBuf},
28    str::FromStr,
29    sync::Arc,
30    time::Instant,
31};
32
33/// A Solar compiler instance, to grant syntactic and semantic analysis capabilities.
34pub type Analysis = Arc<solar::sema::Compiler>;
35
36/// Builder type to configure how to compile a project.
37///
38/// This is merely a wrapper for [`Project::compile()`] which also prints to stdout depending on its
39/// settings.
40#[must_use = "ProjectCompiler does nothing unless you call a `compile*` method"]
41pub struct ProjectCompiler {
42    /// The root of the project.
43    project_root: PathBuf,
44
45    /// Whether to also print contract names.
46    print_names: Option<bool>,
47
48    /// Whether to also print contract sizes.
49    print_sizes: Option<bool>,
50
51    /// Whether to print anything at all. Overrides other `print` options.
52    quiet: Option<bool>,
53
54    /// Whether to bail on compiler errors.
55    bail: Option<bool>,
56
57    /// Whether to ignore the contract initcode size limit introduced by EIP-3860.
58    ignore_eip_3860: bool,
59
60    /// Contract size limits used when reporting compiled contract sizes.
61    size_limits: ContractSizeLimits,
62
63    /// Extra files to include, that are not necessarily in the project's source directory.
64    files: Vec<PathBuf>,
65
66    /// Whether to compile with dynamic linking tests and scripts.
67    dynamic_test_linking: bool,
68}
69
70impl Default for ProjectCompiler {
71    #[inline]
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77impl ProjectCompiler {
78    /// Create a new builder with the default settings.
79    #[inline]
80    pub fn new() -> Self {
81        Self {
82            project_root: PathBuf::new(),
83            print_names: None,
84            print_sizes: None,
85            quiet: Some(crate::shell::is_quiet()),
86            bail: None,
87            ignore_eip_3860: false,
88            size_limits: ContractSizeLimits::default(),
89            files: Vec::new(),
90            dynamic_test_linking: false,
91        }
92    }
93
94    /// Sets whether to print contract names.
95    #[inline]
96    pub const fn print_names(mut self, yes: bool) -> Self {
97        self.print_names = Some(yes);
98        self
99    }
100
101    /// Sets whether to print contract sizes.
102    #[inline]
103    pub const fn print_sizes(mut self, yes: bool) -> Self {
104        self.print_sizes = Some(yes);
105        self
106    }
107
108    /// Sets whether to print anything at all. Overrides other `print` options.
109    #[inline]
110    #[doc(alias = "silent")]
111    pub const fn quiet(mut self, yes: bool) -> Self {
112        self.quiet = Some(yes);
113        self
114    }
115
116    /// Sets whether to bail on compiler errors.
117    #[inline]
118    pub const fn bail(mut self, yes: bool) -> Self {
119        self.bail = Some(yes);
120        self
121    }
122
123    /// Sets whether to ignore EIP-3860 initcode size limits.
124    #[inline]
125    pub const fn ignore_eip_3860(mut self, yes: bool) -> Self {
126        self.ignore_eip_3860 = yes;
127        self
128    }
129
130    /// Sets the contract size limits for size reports.
131    #[inline]
132    pub const fn size_limits(mut self, limits: ContractSizeLimits) -> Self {
133        self.size_limits = limits;
134        self
135    }
136
137    /// Sets extra files to include, that are not necessarily in the project's source dir.
138    #[inline]
139    pub fn files(mut self, files: impl IntoIterator<Item = PathBuf>) -> Self {
140        self.files.extend(files);
141        self
142    }
143
144    /// Sets if tests should be dynamically linked.
145    #[inline]
146    pub const fn dynamic_test_linking(mut self, preprocess: bool) -> Self {
147        self.dynamic_test_linking = preprocess;
148        self
149    }
150
151    /// Compiles the project.
152    #[instrument(target = "forge::compile", skip_all)]
153    pub fn compile<C: Compiler<CompilerContract = Contract>>(
154        mut self,
155        project: &Project<C>,
156    ) -> Result<ProjectCompileOutput<C>>
157    where
158        DynamicTestLinkingPreprocessor: Preprocessor<C>,
159    {
160        self.project_root = project.root().to_path_buf();
161
162        // TODO: Avoid using std::process::exit(0).
163        // Replacing this with a return (e.g., Ok(ProjectCompileOutput::default())) would be more
164        // idiomatic, but it currently requires a `Default` bound on `C::Language`, which
165        // breaks compatibility with downstream crates like `foundry-cli`. This would need a
166        // broader refactor across the call chain. Leaving it as-is for now until a larger
167        // refactor is feasible.
168        if !project.paths.has_input_files() && self.files.is_empty() {
169            sh_println!("Nothing to compile")?;
170            std::process::exit(0);
171        }
172
173        // Taking is fine since we don't need these in `compile_with`.
174        let files = std::mem::take(&mut self.files);
175        let preprocess = self.dynamic_test_linking;
176        self.compile_with(|| {
177            let sources = if files.is_empty() {
178                project.paths.read_input_files()?
179            } else {
180                Source::read_all(files)?
181            };
182
183            let mut compiler =
184                foundry_compilers::project::ProjectCompiler::with_sources(project, sources)?;
185            if preprocess {
186                compiler = compiler.with_preprocessor(DynamicTestLinkingPreprocessor);
187            }
188            compiler.compile().map_err(Into::into)
189        })
190    }
191
192    /// Compiles the project with the given closure
193    fn compile_with<C: Compiler<CompilerContract = Contract>, F>(
194        self,
195        f: F,
196    ) -> Result<ProjectCompileOutput<C>>
197    where
198        F: FnOnce() -> Result<ProjectCompileOutput<C>>,
199    {
200        let quiet = self.quiet.unwrap_or(false);
201        let bail = self.bail.unwrap_or(true);
202
203        let output = with_compilation_reporter(quiet, Some(self.project_root.clone()), || {
204            tracing::debug!("compiling project");
205
206            let timer = Instant::now();
207            let r = f();
208            let elapsed = timer.elapsed();
209
210            tracing::debug!("finished compiling in {:.3}s", elapsed.as_secs_f64());
211            r
212        })?;
213
214        if bail && output.has_compiler_errors() {
215            eyre::bail!("{output}")
216        }
217
218        if !quiet {
219            if !shell::is_json() {
220                if output.is_unchanged() {
221                    sh_println!("No files changed, compilation skipped")?;
222                } else {
223                    // print the compiler output / warnings
224                    sh_println!("{output}")?;
225                }
226            }
227
228            self.handle_output(&output)?;
229        }
230
231        Ok(output)
232    }
233
234    /// If configured, this will print sizes or names
235    fn handle_output<C: Compiler<CompilerContract = Contract>>(
236        &self,
237        output: &ProjectCompileOutput<C>,
238    ) -> Result<()> {
239        let print_names = self.print_names.unwrap_or(false);
240        let print_sizes = self.print_sizes.unwrap_or(false);
241
242        // print any sizes or names
243        if print_names {
244            let mut artifacts: BTreeMap<_, Vec<_>> = BTreeMap::new();
245            for (name, (_, version)) in output.versioned_artifacts() {
246                artifacts.entry(version).or_default().push(name);
247            }
248
249            if shell::is_json() {
250                sh_println!("{}", serde_json::to_string(&artifacts).unwrap())?;
251            } else {
252                for (version, names) in artifacts {
253                    sh_println!(
254                        "  compiler version: {}.{}.{}",
255                        version.major,
256                        version.minor,
257                        version.patch
258                    )?;
259                    for name in names {
260                        sh_println!("    - {name}")?;
261                    }
262                }
263            }
264        }
265
266        if print_sizes {
267            // add extra newline if names were already printed
268            if print_names && !shell::is_json() {
269                sh_println!()?;
270            }
271
272            let mut size_report =
273                SizeReport { contracts: BTreeMap::new(), limits: self.size_limits };
274
275            let mut artifacts: BTreeMap<String, Vec<_>> = BTreeMap::new();
276            for (id, artifact) in output.artifact_ids().filter(|(id, _)| {
277                // filter out forge-std specific contracts
278                !id.source.to_string_lossy().contains("/forge-std/src/")
279            }) {
280                artifacts.entry(id.name.clone()).or_default().push((id.source.clone(), artifact));
281            }
282
283            for (name, artifact_list) in artifacts {
284                for (path, artifact) in &artifact_list {
285                    let runtime_size = contract_size(*artifact, false).unwrap_or_default();
286                    let init_size = contract_size(*artifact, true).unwrap_or_default();
287
288                    let is_dev_contract = artifact
289                        .abi
290                        .as_ref()
291                        .map(|abi| {
292                            abi.functions().any(|f| {
293                                f.test_function_kind().is_known()
294                                    || matches!(f.name.as_str(), "IS_TEST" | "IS_SCRIPT")
295                            })
296                        })
297                        .unwrap_or(false);
298
299                    let unique_name = if artifact_list.len() > 1 {
300                        format!(
301                            "{} ({})",
302                            name,
303                            path.strip_prefix(&self.project_root).unwrap_or(path).display()
304                        )
305                    } else {
306                        name.clone()
307                    };
308
309                    size_report.contracts.insert(
310                        unique_name,
311                        ContractInfo { runtime_size, init_size, is_dev_contract },
312                    );
313                }
314            }
315
316            sh_println!("{size_report}")?;
317
318            let runtime_eip = if size_report.limits.runtime == CONTRACT_RUNTIME_SIZE_LIMIT {
319                "EIP-170: "
320            } else {
321                ""
322            };
323            eyre::ensure!(
324                !size_report.exceeds_runtime_size_limit(),
325                "some contracts exceed the runtime size limit ({runtime_eip}{} bytes)",
326                size_report.limits.runtime
327            );
328            // Check size limits only if not ignoring EIP-3860
329            let initcode_eip = if size_report.limits.initcode == CONTRACT_INITCODE_SIZE_LIMIT {
330                "EIP-3860: "
331            } else {
332                ""
333            };
334            eyre::ensure!(
335                self.ignore_eip_3860 || !size_report.exceeds_initcode_size_limit(),
336                "some contracts exceed the initcode size limit ({initcode_eip}{} bytes)",
337                size_report.limits.initcode
338            );
339        }
340
341        Ok(())
342    }
343}
344
345// https://eips.ethereum.org/EIPS/eip-170
346const CONTRACT_RUNTIME_SIZE_LIMIT: usize = 24576;
347
348// https://eips.ethereum.org/EIPS/eip-3860
349const CONTRACT_INITCODE_SIZE_LIMIT: usize = 49152;
350
351const CONTRACT_RUNTIME_SIZE_WARN_THRESHOLD: usize = 18_000;
352const CONTRACT_INITCODE_SIZE_WARN_THRESHOLD: usize = 36_000;
353
354/// Runtime and initcode byte-size limits for compiled contract size reports.
355#[derive(Clone, Copy, Debug, PartialEq, Eq)]
356pub struct ContractSizeLimits {
357    /// Maximum deployed runtime bytecode size.
358    pub runtime: usize,
359    /// Maximum initcode bytecode size.
360    pub initcode: usize,
361}
362
363impl ContractSizeLimits {
364    /// Creates a new set of contract size limits.
365    pub const fn new(runtime: usize, initcode: usize) -> Self {
366        Self { runtime, initcode }
367    }
368
369    /// Creates limits from a runtime code-size limit, using the EIP-3860 2x initcode ratio.
370    pub const fn with_runtime_limit(runtime: usize) -> Self {
371        Self { runtime, initcode: runtime.saturating_mul(2) }
372    }
373
374    const fn runtime_warning_threshold(self) -> usize {
375        scaled_threshold(
376            self.runtime,
377            CONTRACT_RUNTIME_SIZE_WARN_THRESHOLD,
378            CONTRACT_RUNTIME_SIZE_LIMIT,
379        )
380    }
381
382    const fn initcode_warning_threshold(self) -> usize {
383        scaled_threshold(
384            self.initcode,
385            CONTRACT_INITCODE_SIZE_WARN_THRESHOLD,
386            CONTRACT_INITCODE_SIZE_LIMIT,
387        )
388    }
389}
390
391impl Default for ContractSizeLimits {
392    fn default() -> Self {
393        Self::new(CONTRACT_RUNTIME_SIZE_LIMIT, CONTRACT_INITCODE_SIZE_LIMIT)
394    }
395}
396
397const fn scaled_threshold(limit: usize, threshold: usize, default_limit: usize) -> usize {
398    limit.saturating_mul(threshold) / default_limit
399}
400
401/// Contracts with info about their size
402pub struct SizeReport {
403    /// `contract name -> info`
404    pub contracts: BTreeMap<String, ContractInfo>,
405    /// Size limits used to calculate margins and failures.
406    pub limits: ContractSizeLimits,
407}
408
409impl SizeReport {
410    /// Returns the maximum runtime code size, excluding dev contracts.
411    pub fn max_runtime_size(&self) -> usize {
412        self.contracts
413            .values()
414            .filter(|c| !c.is_dev_contract)
415            .map(|c| c.runtime_size)
416            .max()
417            .unwrap_or(0)
418    }
419
420    /// Returns the maximum initcode size, excluding dev contracts.
421    pub fn max_init_size(&self) -> usize {
422        self.contracts
423            .values()
424            .filter(|c| !c.is_dev_contract)
425            .map(|c| c.init_size)
426            .max()
427            .unwrap_or(0)
428    }
429
430    /// Returns true if any contract exceeds the runtime size limit, excluding dev contracts.
431    pub fn exceeds_runtime_size_limit(&self) -> bool {
432        self.max_runtime_size() > self.limits.runtime
433    }
434
435    /// Returns true if any contract exceeds the initcode size limit, excluding dev contracts.
436    pub fn exceeds_initcode_size_limit(&self) -> bool {
437        self.max_init_size() > self.limits.initcode
438    }
439}
440
441impl Display for SizeReport {
442    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
443        if shell::is_json() {
444            writeln!(f, "{}", self.format_json_output())?;
445        } else {
446            writeln!(f, "\n{}", self.format_table_output())?;
447        }
448        Ok(())
449    }
450}
451
452impl SizeReport {
453    fn format_json_output(&self) -> String {
454        let contracts = self
455            .contracts
456            .iter()
457            .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0))
458            .map(|(name, contract)| {
459                (
460                    name.clone(),
461                    serde_json::json!({
462                        "runtime_size": contract.runtime_size,
463                        "init_size": contract.init_size,
464                        "runtime_margin": self.limits.runtime as isize - contract.runtime_size as isize,
465                        "init_margin": self.limits.initcode as isize - contract.init_size as isize,
466                    }),
467                )
468            })
469            .collect::<serde_json::Map<_, _>>();
470
471        serde_json::to_string(&contracts).unwrap()
472    }
473
474    fn format_table_output(&self) -> Table {
475        let mut table = Table::new();
476        if shell::is_markdown() {
477            table.load_preset(ASCII_MARKDOWN);
478        } else {
479            table.apply_modifier(UTF8_ROUND_CORNERS);
480        }
481
482        table.set_header(vec![
483            Cell::new("Contract"),
484            Cell::new("Runtime Size (B)"),
485            Cell::new("Initcode Size (B)"),
486            Cell::new("Runtime Margin (B)"),
487            Cell::new("Initcode Margin (B)"),
488        ]);
489
490        // Filters out dev contracts (Test or Script)
491        let contracts = self
492            .contracts
493            .iter()
494            .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0));
495        let runtime_warning_threshold = self.limits.runtime_warning_threshold();
496        let initcode_warning_threshold = self.limits.initcode_warning_threshold();
497        for (name, contract) in contracts {
498            let runtime_margin = self.limits.runtime as isize - contract.runtime_size as isize;
499            let init_margin = self.limits.initcode as isize - contract.init_size as isize;
500
501            let runtime_color = if contract.runtime_size < runtime_warning_threshold {
502                Color::Reset
503            } else if contract.runtime_size <= self.limits.runtime {
504                Color::Yellow
505            } else {
506                Color::Red
507            };
508
509            let init_color = if contract.init_size < initcode_warning_threshold {
510                Color::Reset
511            } else if contract.init_size <= self.limits.initcode {
512                Color::Yellow
513            } else {
514                Color::Red
515            };
516
517            let locale = &Locale::en;
518            table.add_row([
519                Cell::new(name),
520                Cell::new(contract.runtime_size.to_formatted_string(locale)).fg(runtime_color),
521                Cell::new(contract.init_size.to_formatted_string(locale)).fg(init_color),
522                Cell::new(runtime_margin.to_formatted_string(locale)).fg(runtime_color),
523                Cell::new(init_margin.to_formatted_string(locale)).fg(init_color),
524            ]);
525        }
526
527        table
528    }
529}
530
531/// Returns the deployed or init size of the contract.
532fn contract_size<T: Artifact>(artifact: &T, initcode: bool) -> Option<usize> {
533    let bytecode = if initcode {
534        artifact.get_bytecode_object()?
535    } else {
536        artifact.get_deployed_bytecode_object()?
537    };
538
539    let size = match bytecode.as_ref() {
540        BytecodeObject::Bytecode(bytes) => bytes.len(),
541        BytecodeObject::Unlinked(unlinked) => {
542            // we don't need to account for placeholders here, because library placeholders take up
543            // 40 characters: `__$<library hash>$__` which is the same as a 20byte address in hex.
544            let mut size = unlinked.len();
545            if unlinked.starts_with("0x") {
546                size -= 2;
547            }
548            // hex -> bytes
549            size / 2
550        }
551    };
552
553    Some(size)
554}
555
556/// How big the contract is and whether it is a dev contract where size limits can be neglected
557#[derive(Clone, Copy, Debug)]
558pub struct ContractInfo {
559    /// Size of the runtime code in bytes
560    pub runtime_size: usize,
561    /// Size of the initcode in bytes
562    pub init_size: usize,
563    /// A development contract is either a Script or a Test contract.
564    pub is_dev_contract: bool,
565}
566
567/// Compiles target file path.
568///
569/// If `quiet` is set, the compilation reporter's progress/status output is suppressed.
570/// (When not suppressed, that output is emitted to stderr; see `with_compilation_reporter`.)
571///
572/// **Note:** this expects the `target_path` to be absolute
573pub fn compile_target<C: Compiler<CompilerContract = Contract>>(
574    target_path: &Path,
575    project: &Project<C>,
576    quiet: bool,
577) -> Result<ProjectCompileOutput<C>>
578where
579    DynamicTestLinkingPreprocessor: Preprocessor<C>,
580{
581    ProjectCompiler::new().quiet(quiet).files([target_path.into()]).compile(project)
582}
583
584/// Creates a [Project] from an Etherscan source.
585pub fn etherscan_project(metadata: &Metadata, target_path: &Path) -> Result<Project> {
586    let target_path = dunce::canonicalize(target_path)?;
587    let sources_path = target_path.join(&metadata.contract_name);
588    metadata.source_tree().write_to(&target_path)?;
589
590    let mut settings = metadata.settings()?;
591
592    // make remappings absolute with our root
593    for remapping in &mut settings.remappings {
594        let new_path = sources_path.join(remapping.path.trim_start_matches('/'));
595        remapping.path = new_path.display().to_string();
596    }
597
598    // add missing remappings
599    if !settings.remappings.iter().any(|remapping| remapping.name.starts_with("@openzeppelin/")) {
600        let oz = Remapping {
601            context: None,
602            name: "@openzeppelin/".into(),
603            path: sources_path.join("@openzeppelin").display().to_string(),
604        };
605        settings.remappings.push(oz);
606    }
607
608    // root/
609    //   ContractName/
610    //     [source code]
611    let paths = ProjectPathsConfig::builder()
612        .sources(sources_path.clone())
613        .remappings(settings.remappings.clone())
614        .build_with_root(sources_path);
615
616    // TODO: detect vyper
617    let v = metadata.compiler_version()?;
618    let solc = Solc::find_or_install(&v)?;
619
620    let compiler = MultiCompiler { solc: Some(SolcCompiler::Specific(solc)), vyper: None };
621
622    Ok(ProjectBuilder::<MultiCompiler>::default()
623        .settings(MultiCompilerSettings {
624            solc: SolcSettings {
625                settings: SolcConfig::builder().settings(settings).build(),
626                ..Default::default()
627            },
628            ..Default::default()
629        })
630        .paths(paths)
631        .ephemeral()
632        .no_artifacts()
633        .build(compiler)?)
634}
635
636/// Configures the reporter and runs the given closure.
637///
638/// In TTY mode, [`SpinnerReporter`] paints the progress to stderr. The non-TTY fallback
639/// still writes to stdout via `BasicStdoutReporter`; migrating that path to stderr is
640/// part of the per-command stdout migration tracked in `docs/dev/output-channels.md`
641/// (it would shift many existing snapshot tests at once).
642pub fn with_compilation_reporter<O>(
643    quiet: bool,
644    project_root: Option<PathBuf>,
645    f: impl FnOnce() -> O,
646) -> O {
647    #[expect(clippy::collapsible_else_if)]
648    let reporter = if quiet || shell::is_json() {
649        Report::new(NoReporter::default())
650    } else {
651        if std::io::stderr().is_terminal() {
652            Report::new(SpinnerReporter::spawn(project_root))
653        } else {
654            Report::new(BasicStdoutReporter::default())
655        }
656    };
657
658    foundry_compilers::report::with_scoped(&reporter, f)
659}
660
661/// Container type for parsing contract identifiers from CLI.
662///
663/// Passed string can be of the following forms:
664/// - `src/Counter.sol` - path to the contract file, in the case where it only contains one contract
665/// - `src/Counter.sol:Counter` - path to the contract file and the contract name
666/// - `Counter` - contract name only
667#[derive(Clone, PartialEq, Eq)]
668pub enum PathOrContractInfo {
669    /// Non-canonicalized path provided via CLI.
670    Path(PathBuf),
671    /// Contract info provided via CLI.
672    ContractInfo(CompilerContractInfo),
673}
674
675impl PathOrContractInfo {
676    /// Returns the path to the contract file if provided.
677    pub fn path(&self) -> Option<PathBuf> {
678        match self {
679            Self::Path(path) => Some(path.clone()),
680            Self::ContractInfo(info) => info.path.as_ref().map(PathBuf::from),
681        }
682    }
683
684    /// Returns the contract name if provided.
685    pub fn name(&self) -> Option<&str> {
686        match self {
687            Self::Path(_) => None,
688            Self::ContractInfo(info) => Some(&info.name),
689        }
690    }
691}
692
693impl FromStr for PathOrContractInfo {
694    type Err = eyre::Error;
695
696    fn from_str(s: &str) -> Result<Self> {
697        if let Ok(contract) = CompilerContractInfo::from_str(s) {
698            return Ok(Self::ContractInfo(contract));
699        }
700        let path = PathBuf::from(s);
701        if path.extension().is_some_and(|ext| ext == "sol" || ext == "vy") {
702            return Ok(Self::Path(path));
703        }
704        Err(eyre::eyre!("Invalid contract identifier, file is not *.sol or *.vy: {}", s))
705    }
706}
707
708impl std::fmt::Debug for PathOrContractInfo {
709    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
710        match self {
711            Self::Path(path) => write!(f, "Path({})", path.display()),
712            Self::ContractInfo(info) => {
713                write!(f, "ContractInfo({info})")
714            }
715        }
716    }
717}
718
719#[cfg(test)]
720mod tests {
721    use super::*;
722
723    #[test]
724    fn parse_contract_identifiers() {
725        let t = ["src/Counter.sol", "src/Counter.sol:Counter", "Counter"];
726
727        let i1 = PathOrContractInfo::from_str(t[0]).unwrap();
728        assert_eq!(i1, PathOrContractInfo::Path(PathBuf::from(t[0])));
729
730        let i2 = PathOrContractInfo::from_str(t[1]).unwrap();
731        assert_eq!(
732            i2,
733            PathOrContractInfo::ContractInfo(CompilerContractInfo {
734                path: Some("src/Counter.sol".to_string()),
735                name: "Counter".to_string()
736            })
737        );
738
739        let i3 = PathOrContractInfo::from_str(t[2]).unwrap();
740        assert_eq!(
741            i3,
742            PathOrContractInfo::ContractInfo(CompilerContractInfo {
743                path: None,
744                name: "Counter".to_string()
745            })
746        );
747    }
748
749    #[test]
750    fn size_report_uses_configured_limits() {
751        let mut contracts = BTreeMap::new();
752        contracts.insert(
753            "LargeContract".to_string(),
754            ContractInfo { runtime_size: 30_000, init_size: 60_000, is_dev_contract: false },
755        );
756
757        let default_report =
758            SizeReport { contracts: contracts.clone(), limits: ContractSizeLimits::default() };
759        assert!(default_report.exceeds_runtime_size_limit());
760        assert!(default_report.exceeds_initcode_size_limit());
761
762        let custom_report =
763            SizeReport { contracts, limits: ContractSizeLimits::new(131_072, 262_144) };
764        assert!(!custom_report.exceeds_runtime_size_limit());
765        assert!(!custom_report.exceeds_initcode_size_limit());
766        let output: serde_json::Value =
767            serde_json::from_str(&custom_report.format_json_output()).unwrap();
768        assert_eq!(
769            output,
770            serde_json::json!({
771                "LargeContract": {
772                    "runtime_size": 30000,
773                    "init_size": 60000,
774                    "runtime_margin": 101072,
775                    "init_margin": 202144,
776                }
777            })
778        );
779    }
780
781    #[test]
782    fn contract_size_limits_derive_initcode_limit_from_runtime_limit() {
783        assert_eq!(
784            ContractSizeLimits::with_runtime_limit(50_000),
785            ContractSizeLimits::new(50_000, 100_000)
786        );
787    }
788}