1use crate::{
4 TestFunctionExt, preprocessor::DynamicTestLinkingPreprocessor, shell, term::SpinnerReporter,
5};
6use alloy_json_abi::JsonAbi;
7use comfy_table::{Cell, Color, Table, modifiers::UTF8_ROUND_CORNERS, presets::ASCII_MARKDOWN};
8use eyre::{OptionExt, Result};
9use foundry_block_explorers::contract::Metadata;
10use foundry_compilers::{
11 Artifact, Project, ProjectBuilder, ProjectCompileOutput, ProjectPathsConfig, SolcConfig,
12 artifacts::{
13 BytecodeObject, Contract, Source, output_selection::OutputSelection, remappings::Remapping,
14 },
15 compilers::{
16 Compiler,
17 solc::{Solc, SolcCompiler},
18 },
19 info::ContractInfo as CompilerContractInfo,
20 multi::{MultiCompiler, MultiCompilerSettings},
21 project::Preprocessor,
22 report::{BasicStdoutReporter, NoReporter, Report},
23 solc::SolcSettings,
24};
25use num_format::{Locale, ToFormattedString};
26use solar::{
27 ast::{Arena, ContractKind, ItemKind},
28 interface::{Session, source_map::FileName},
29 parse::Parser,
30};
31use std::{
32 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
33 fmt::Display,
34 io::IsTerminal,
35 path::{Path, PathBuf},
36 str::FromStr,
37 sync::Arc,
38 time::Instant,
39};
40
41pub type Analysis = Arc<solar::sema::Compiler>;
43
44#[must_use = "ProjectCompiler does nothing unless you call a `compile*` method"]
49pub struct ProjectCompiler {
50 project_root: PathBuf,
52
53 print_names: Option<bool>,
55
56 print_sizes: Option<bool>,
58
59 quiet: Option<bool>,
61
62 bail: Option<bool>,
64
65 ignore_eip_3860: bool,
67
68 size_limits: ContractSizeLimits,
70
71 files: Vec<PathBuf>,
73
74 dynamic_test_linking: bool,
76}
77
78impl Default for ProjectCompiler {
79 #[inline]
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85impl ProjectCompiler {
86 #[inline]
88 pub fn new() -> Self {
89 Self {
90 project_root: PathBuf::new(),
91 print_names: None,
92 print_sizes: None,
93 quiet: Some(crate::shell::is_quiet()),
94 bail: None,
95 ignore_eip_3860: false,
96 size_limits: ContractSizeLimits::default(),
97 files: Vec::new(),
98 dynamic_test_linking: false,
99 }
100 }
101
102 #[inline]
104 pub const fn print_names(mut self, yes: bool) -> Self {
105 self.print_names = Some(yes);
106 self
107 }
108
109 #[inline]
111 pub const fn print_sizes(mut self, yes: bool) -> Self {
112 self.print_sizes = Some(yes);
113 self
114 }
115
116 #[inline]
118 #[doc(alias = "silent")]
119 pub const fn quiet(mut self, yes: bool) -> Self {
120 self.quiet = Some(yes);
121 self
122 }
123
124 #[inline]
126 pub const fn bail(mut self, yes: bool) -> Self {
127 self.bail = Some(yes);
128 self
129 }
130
131 #[inline]
133 pub const fn ignore_eip_3860(mut self, yes: bool) -> Self {
134 self.ignore_eip_3860 = yes;
135 self
136 }
137
138 #[inline]
140 pub const fn size_limits(mut self, limits: ContractSizeLimits) -> Self {
141 self.size_limits = limits;
142 self
143 }
144
145 #[inline]
147 pub fn files(mut self, files: impl IntoIterator<Item = PathBuf>) -> Self {
148 self.files.extend(files);
149 self
150 }
151
152 #[inline]
154 pub const fn dynamic_test_linking(mut self, preprocess: bool) -> Self {
155 self.dynamic_test_linking = preprocess;
156 self
157 }
158
159 #[instrument(target = "forge::compile", skip_all)]
161 pub fn compile<C: Compiler<CompilerContract = Contract>>(
162 mut self,
163 project: &Project<C>,
164 ) -> Result<ProjectCompileOutput<C>>
165 where
166 DynamicTestLinkingPreprocessor: Preprocessor<C>,
167 {
168 self.project_root = project.root().to_path_buf();
169
170 if !project.paths.has_input_files() && self.files.is_empty() {
177 sh_println!("Nothing to compile")?;
178 std::process::exit(0);
179 }
180
181 let files = std::mem::take(&mut self.files);
183 let preprocess = self.dynamic_test_linking;
184 self.compile_with(|| {
185 let sources = if files.is_empty() {
186 project.paths.read_input_files()?
187 } else {
188 Source::read_all(files)?
189 };
190
191 let mut compiler =
192 foundry_compilers::project::ProjectCompiler::with_sources(project, sources)?;
193 if preprocess {
194 compiler = compiler.with_preprocessor(DynamicTestLinkingPreprocessor);
195 }
196 compiler.compile().map_err(Into::into)
197 })
198 }
199
200 fn compile_with<C: Compiler<CompilerContract = Contract>, F>(
202 self,
203 f: F,
204 ) -> Result<ProjectCompileOutput<C>>
205 where
206 F: FnOnce() -> Result<ProjectCompileOutput<C>>,
207 {
208 let quiet = self.quiet.unwrap_or(false);
209 let bail = self.bail.unwrap_or(true);
210
211 let output = with_compilation_reporter(quiet, Some(self.project_root.clone()), || {
212 tracing::debug!("compiling project");
213
214 let timer = Instant::now();
215 let r = f();
216 let elapsed = timer.elapsed();
217
218 tracing::debug!("finished compiling in {:.3}s", elapsed.as_secs_f64());
219 r
220 })?;
221
222 if bail && output.has_compiler_errors() {
223 eyre::bail!("{output}");
224 }
225
226 if !quiet {
227 if !shell::is_json() {
228 if output.is_unchanged() {
229 sh_println!("No files changed, compilation skipped")?;
230 } else {
231 sh_println!("{output}")?;
233 }
234 }
235
236 if !(shell::is_json() && output.has_compiler_errors()) {
237 self.handle_output(&output)?;
238 }
239 }
240
241 Ok(output)
242 }
243
244 fn handle_output<C: Compiler<CompilerContract = Contract>>(
246 &self,
247 output: &ProjectCompileOutput<C>,
248 ) -> Result<()> {
249 let print_names = self.print_names.unwrap_or(false);
250 let print_sizes = self.print_sizes.unwrap_or(false);
251
252 if print_names {
254 let mut artifacts: BTreeMap<_, Vec<_>> = BTreeMap::new();
255 for (name, (_, version)) in output.versioned_artifacts() {
256 artifacts.entry(version).or_default().push(name);
257 }
258
259 if shell::is_json() {
260 sh_println!("{}", serde_json::to_string(&artifacts).unwrap())?;
261 } else {
262 for (version, names) in artifacts {
263 sh_println!(
264 " compiler version: {}.{}.{}",
265 version.major,
266 version.minor,
267 version.patch
268 )?;
269 for name in names {
270 sh_println!(" - {name}")?;
271 }
272 }
273 }
274 }
275
276 if print_sizes {
277 if print_names && !shell::is_json() {
279 sh_println!()?;
280 }
281
282 let mut size_report =
283 SizeReport { contracts: BTreeMap::new(), limits: self.size_limits };
284
285 let mut artifacts: BTreeMap<String, Vec<_>> = BTreeMap::new();
286 for (id, artifact) in output.artifact_ids().filter(|(id, _)| {
287 !id.source.to_string_lossy().contains("/forge-std/src/")
289 }) {
290 artifacts.entry(id.name.clone()).or_default().push((id.source.clone(), artifact));
291 }
292
293 let abs_source = |path: &Path| -> PathBuf {
297 if path.is_absolute() { path.to_path_buf() } else { self.project_root.join(path) }
298 };
299 let source_paths = artifacts
300 .values()
301 .flatten()
302 .filter(|(_, artifact)| {
303 artifact.abi.as_ref().is_some_and(|abi| abi.functions().next().is_none())
304 })
305 .map(|(path, _)| abs_source(path))
306 .collect::<BTreeSet<_>>();
307 let libraries = collect_libraries(&source_paths);
308
309 for (name, artifact_list) in artifacts {
310 let kept = artifact_list
313 .iter()
314 .filter(|(path, artifact)| {
315 let is_library = libraries
316 .get(&abs_source(path))
317 .is_some_and(|libs| libs.contains(&name));
318 let has_no_abi_functions = artifact
319 .abi
320 .as_ref()
321 .is_some_and(|abi| abi.functions().next().is_none());
322 !(is_library && has_no_abi_functions)
323 })
324 .collect::<Vec<_>>();
325
326 for (path, artifact) in &kept {
327 let runtime_size = contract_size(*artifact, false).unwrap_or_default();
328 let init_size = contract_size(*artifact, true).unwrap_or_default();
329
330 let is_dev_contract = artifact
331 .abi
332 .as_ref()
333 .map(|abi| {
334 abi.functions().any(|f| {
335 f.test_function_kind().is_known()
336 || matches!(f.name.as_str(), "IS_TEST" | "IS_SCRIPT")
337 })
338 })
339 .unwrap_or(false);
340
341 let unique_name = if kept.len() > 1 {
342 format!(
343 "{} ({})",
344 name,
345 path.strip_prefix(&self.project_root).unwrap_or(path).display()
346 )
347 } else {
348 name.clone()
349 };
350
351 size_report.contracts.insert(
352 unique_name,
353 ContractInfo { runtime_size, init_size, is_dev_contract },
354 );
355 }
356 }
357
358 sh_println!("{size_report}")?;
359
360 let runtime_eip = if size_report.limits.runtime == CONTRACT_RUNTIME_SIZE_LIMIT {
361 "EIP-170: "
362 } else {
363 ""
364 };
365 eyre::ensure!(
366 !size_report.exceeds_runtime_size_limit(),
367 "some contracts exceed the runtime size limit ({runtime_eip}{} bytes)",
368 size_report.limits.runtime
369 );
370 let initcode_eip = if size_report.limits.initcode == CONTRACT_INITCODE_SIZE_LIMIT {
372 "EIP-3860: "
373 } else {
374 ""
375 };
376 eyre::ensure!(
377 self.ignore_eip_3860 || !size_report.exceeds_initcode_size_limit(),
378 "some contracts exceed the initcode size limit ({initcode_eip}{} bytes)",
379 size_report.limits.initcode
380 );
381 }
382
383 Ok(())
384 }
385}
386
387const CONTRACT_RUNTIME_SIZE_LIMIT: usize = 24576;
389
390const CONTRACT_INITCODE_SIZE_LIMIT: usize = 49152;
392
393const CONTRACT_RUNTIME_SIZE_WARN_THRESHOLD: usize = 18_000;
394const CONTRACT_INITCODE_SIZE_WARN_THRESHOLD: usize = 36_000;
395
396#[derive(Clone, Copy, Debug, PartialEq, Eq)]
398pub struct ContractSizeLimits {
399 pub runtime: usize,
401 pub initcode: usize,
403}
404
405impl ContractSizeLimits {
406 pub const fn new(runtime: usize, initcode: usize) -> Self {
408 Self { runtime, initcode }
409 }
410
411 pub const fn with_runtime_limit(runtime: usize) -> Self {
413 Self { runtime, initcode: runtime.saturating_mul(2) }
414 }
415
416 const fn runtime_warning_threshold(self) -> usize {
417 scaled_threshold(
418 self.runtime,
419 CONTRACT_RUNTIME_SIZE_WARN_THRESHOLD,
420 CONTRACT_RUNTIME_SIZE_LIMIT,
421 )
422 }
423
424 const fn initcode_warning_threshold(self) -> usize {
425 scaled_threshold(
426 self.initcode,
427 CONTRACT_INITCODE_SIZE_WARN_THRESHOLD,
428 CONTRACT_INITCODE_SIZE_LIMIT,
429 )
430 }
431}
432
433impl Default for ContractSizeLimits {
434 fn default() -> Self {
435 Self::new(CONTRACT_RUNTIME_SIZE_LIMIT, CONTRACT_INITCODE_SIZE_LIMIT)
436 }
437}
438
439const fn scaled_threshold(limit: usize, threshold: usize, default_limit: usize) -> usize {
440 limit.saturating_mul(threshold) / default_limit
441}
442
443pub struct SizeReport {
445 pub contracts: BTreeMap<String, ContractInfo>,
447 pub limits: ContractSizeLimits,
449}
450
451impl SizeReport {
452 pub fn max_runtime_size(&self) -> usize {
454 self.contracts
455 .values()
456 .filter(|c| !c.is_dev_contract)
457 .map(|c| c.runtime_size)
458 .max()
459 .unwrap_or(0)
460 }
461
462 pub fn max_init_size(&self) -> usize {
464 self.contracts
465 .values()
466 .filter(|c| !c.is_dev_contract)
467 .map(|c| c.init_size)
468 .max()
469 .unwrap_or(0)
470 }
471
472 pub fn exceeds_runtime_size_limit(&self) -> bool {
474 self.max_runtime_size() > self.limits.runtime
475 }
476
477 pub fn exceeds_initcode_size_limit(&self) -> bool {
479 self.max_init_size() > self.limits.initcode
480 }
481}
482
483impl Display for SizeReport {
484 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
485 if shell::is_json() {
486 writeln!(f, "{}", self.format_json_output())?;
487 } else {
488 writeln!(f, "\n{}", self.format_table_output())?;
489 }
490 Ok(())
491 }
492}
493
494impl SizeReport {
495 fn format_json_output(&self) -> String {
496 let contracts = self
497 .contracts
498 .iter()
499 .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0))
500 .map(|(name, contract)| {
501 (
502 name.clone(),
503 serde_json::json!({
504 "runtime_size": contract.runtime_size,
505 "init_size": contract.init_size,
506 "runtime_margin": self.limits.runtime as isize - contract.runtime_size as isize,
507 "init_margin": self.limits.initcode as isize - contract.init_size as isize,
508 }),
509 )
510 })
511 .collect::<serde_json::Map<_, _>>();
512
513 serde_json::to_string(&contracts).unwrap()
514 }
515
516 fn format_table_output(&self) -> Table {
517 let mut table = Table::new();
518 if shell::is_markdown() {
519 table.load_preset(ASCII_MARKDOWN);
520 } else {
521 table.apply_modifier(UTF8_ROUND_CORNERS);
522 }
523
524 table.set_header(vec![
525 Cell::new("Contract"),
526 Cell::new("Runtime Size (B)"),
527 Cell::new("Initcode Size (B)"),
528 Cell::new("Runtime Margin (B)"),
529 Cell::new("Initcode Margin (B)"),
530 ]);
531
532 let contracts = self
534 .contracts
535 .iter()
536 .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0));
537 let runtime_warning_threshold = self.limits.runtime_warning_threshold();
538 let initcode_warning_threshold = self.limits.initcode_warning_threshold();
539 for (name, contract) in contracts {
540 let runtime_margin = self.limits.runtime as isize - contract.runtime_size as isize;
541 let init_margin = self.limits.initcode as isize - contract.init_size as isize;
542
543 let runtime_color = if contract.runtime_size < runtime_warning_threshold {
544 Color::Reset
545 } else if contract.runtime_size <= self.limits.runtime {
546 Color::Yellow
547 } else {
548 Color::Red
549 };
550
551 let init_color = if contract.init_size < initcode_warning_threshold {
552 Color::Reset
553 } else if contract.init_size <= self.limits.initcode {
554 Color::Yellow
555 } else {
556 Color::Red
557 };
558
559 let locale = &Locale::en;
560 table.add_row([
561 Cell::new(name),
562 Cell::new(contract.runtime_size.to_formatted_string(locale)).fg(runtime_color),
563 Cell::new(contract.init_size.to_formatted_string(locale)).fg(init_color),
564 Cell::new(runtime_margin.to_formatted_string(locale)).fg(runtime_color),
565 Cell::new(init_margin.to_formatted_string(locale)).fg(init_color),
566 ]);
567 }
568
569 table
570 }
571}
572
573fn collect_libraries(sources: &BTreeSet<PathBuf>) -> HashMap<PathBuf, HashSet<String>> {
577 let mut result: HashMap<PathBuf, HashSet<String>> = HashMap::new();
578 let sess = Session::builder().with_silent_emitter(None).build();
579 let _ = sess.enter(|| -> solar::interface::Result<()> {
580 for path in sources {
581 let arena = Arena::new();
582 let mut parser = match Parser::from_lazy_source_code(
583 &sess,
584 &arena,
585 FileName::from(path.clone()),
586 || std::fs::read_to_string(path),
587 ) {
588 Ok(parser) => parser,
589 Err(_) => continue,
590 };
591 let Ok(ast) = parser.parse_file() else { continue };
592 let libs = ast
593 .items
594 .iter()
595 .filter_map(|item| match &item.kind {
596 ItemKind::Contract(c) if c.kind == ContractKind::Library => {
597 Some(c.name.as_str().to_string())
598 }
599 _ => None,
600 })
601 .collect::<HashSet<_>>();
602 if !libs.is_empty() {
603 result.insert(path.clone(), libs);
604 }
605 }
606 Ok(())
607 });
608 result
609}
610
611fn contract_size<T: Artifact>(artifact: &T, initcode: bool) -> Option<usize> {
613 let bytecode = if initcode {
614 artifact.get_bytecode_object()?
615 } else {
616 artifact.get_deployed_bytecode_object()?
617 };
618
619 let size = match bytecode.as_ref() {
620 BytecodeObject::Bytecode(bytes) => bytes.len(),
621 BytecodeObject::Unlinked(unlinked) => {
622 let mut size = unlinked.len();
625 if unlinked.starts_with("0x") {
626 size -= 2;
627 }
628 size / 2
630 }
631 };
632
633 Some(size)
634}
635
636#[derive(Clone, Copy, Debug)]
638pub struct ContractInfo {
639 pub runtime_size: usize,
641 pub init_size: usize,
643 pub is_dev_contract: bool,
645}
646
647pub fn compile_target<C: Compiler<CompilerContract = Contract>>(
654 target_path: &Path,
655 project: &Project<C>,
656 quiet: bool,
657) -> Result<ProjectCompileOutput<C>>
658where
659 DynamicTestLinkingPreprocessor: Preprocessor<C>,
660{
661 ProjectCompiler::new().quiet(quiet).files([target_path.into()]).compile(project)
662}
663
664pub fn compile_abi_project<C: Compiler<CompilerContract = Contract>>(
666 project: &mut Project<C>,
667 compiler: ProjectCompiler,
668) -> Result<ProjectCompileOutput<C>>
669where
670 DynamicTestLinkingPreprocessor: Preprocessor<C>,
671{
672 project.update_output_selection(|selection| {
673 *selection = OutputSelection::common_output_selection(["abi".to_string()]);
675 });
676 compiler.compile(project)
677}
678
679pub fn compile_target_abi(
681 project: &mut Project<MultiCompiler>,
682 target_path: &Path,
683 target_name: &str,
684) -> Result<JsonAbi> {
685 let target_path = dunce::canonicalize(target_path)?;
686 let output = compile_abi_project(
687 project,
688 ProjectCompiler::new().quiet(true).files([target_path.clone()]),
689 )?;
690
691 let artifact = output
692 .find(&target_path, target_name)
693 .ok_or_eyre("failed to find target artifact when compiling for abi")?;
694 artifact.abi.clone().ok_or_eyre("target artifact does not have an ABI")
695}
696
697pub fn etherscan_project(metadata: &Metadata, target_path: &Path) -> Result<Project> {
699 let target_path = dunce::canonicalize(target_path)?;
700 let sources_path = target_path.join(&metadata.contract_name);
701 metadata.source_tree().write_to(&target_path)?;
702
703 let mut settings = metadata.settings()?;
704
705 for remapping in &mut settings.remappings {
707 let new_path = sources_path.join(remapping.path.trim_start_matches('/'));
708 remapping.path = new_path.display().to_string();
709 }
710
711 if !settings.remappings.iter().any(|remapping| remapping.name.starts_with("@openzeppelin/")) {
713 let oz = Remapping {
714 context: None,
715 name: "@openzeppelin/".into(),
716 path: sources_path.join("@openzeppelin").display().to_string(),
717 };
718 settings.remappings.push(oz);
719 }
720
721 let paths = ProjectPathsConfig::builder()
725 .sources(sources_path.clone())
726 .remappings(settings.remappings.clone())
727 .build_with_root(sources_path);
728
729 let v = metadata.compiler_version()?;
731 let solc = Solc::find_or_install(&v)?;
732
733 let compiler = MultiCompiler { solc: Some(SolcCompiler::Specific(solc)), vyper: None };
734
735 Ok(ProjectBuilder::<MultiCompiler>::default()
736 .settings(MultiCompilerSettings {
737 solc: SolcSettings {
738 settings: SolcConfig::builder().settings(settings).build(),
739 ..Default::default()
740 },
741 ..Default::default()
742 })
743 .paths(paths)
744 .ephemeral()
745 .no_artifacts()
746 .build(compiler)?)
747}
748
749pub fn with_compilation_reporter<O>(
756 quiet: bool,
757 project_root: Option<PathBuf>,
758 f: impl FnOnce() -> O,
759) -> O {
760 #[expect(clippy::collapsible_else_if)]
761 let reporter = if quiet || shell::is_json() {
762 Report::new(NoReporter::default())
763 } else {
764 if std::io::stderr().is_terminal() {
765 Report::new(SpinnerReporter::spawn(project_root))
766 } else {
767 Report::new(BasicStdoutReporter::default())
768 }
769 };
770
771 foundry_compilers::report::with_scoped(&reporter, f)
772}
773
774#[derive(Clone, PartialEq, Eq)]
781pub enum PathOrContractInfo {
782 Path(PathBuf),
784 ContractInfo(CompilerContractInfo),
786}
787
788impl PathOrContractInfo {
789 pub fn path(&self) -> Option<PathBuf> {
791 match self {
792 Self::Path(path) => Some(path.clone()),
793 Self::ContractInfo(info) => info.path.as_ref().map(PathBuf::from),
794 }
795 }
796
797 pub fn name(&self) -> Option<&str> {
799 match self {
800 Self::Path(_) => None,
801 Self::ContractInfo(info) => Some(&info.name),
802 }
803 }
804}
805
806impl FromStr for PathOrContractInfo {
807 type Err = eyre::Error;
808
809 fn from_str(s: &str) -> Result<Self> {
810 if let Ok(contract) = CompilerContractInfo::from_str(s) {
811 return Ok(Self::ContractInfo(contract));
812 }
813 let path = PathBuf::from(s);
814 if path.extension().is_some_and(|ext| ext == "sol" || ext == "vy") {
815 return Ok(Self::Path(path));
816 }
817 Err(eyre::eyre!("Invalid contract identifier, file is not *.sol or *.vy: {}", s))
818 }
819}
820
821impl std::fmt::Debug for PathOrContractInfo {
822 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
823 match self {
824 Self::Path(path) => write!(f, "Path({})", path.display()),
825 Self::ContractInfo(info) => {
826 write!(f, "ContractInfo({info})")
827 }
828 }
829 }
830}
831
832#[cfg(test)]
833mod tests {
834 use super::*;
835
836 #[test]
837 fn parse_contract_identifiers() {
838 let t = ["src/Counter.sol", "src/Counter.sol:Counter", "Counter"];
839
840 let i1 = PathOrContractInfo::from_str(t[0]).unwrap();
841 assert_eq!(i1, PathOrContractInfo::Path(PathBuf::from(t[0])));
842
843 let i2 = PathOrContractInfo::from_str(t[1]).unwrap();
844 assert_eq!(
845 i2,
846 PathOrContractInfo::ContractInfo(CompilerContractInfo {
847 path: Some("src/Counter.sol".to_string()),
848 name: "Counter".to_string()
849 })
850 );
851
852 let i3 = PathOrContractInfo::from_str(t[2]).unwrap();
853 assert_eq!(
854 i3,
855 PathOrContractInfo::ContractInfo(CompilerContractInfo {
856 path: None,
857 name: "Counter".to_string()
858 })
859 );
860 }
861
862 #[test]
863 fn size_report_uses_configured_limits() {
864 let mut contracts = BTreeMap::new();
865 contracts.insert(
866 "LargeContract".to_string(),
867 ContractInfo { runtime_size: 30_000, init_size: 60_000, is_dev_contract: false },
868 );
869
870 let default_report =
871 SizeReport { contracts: contracts.clone(), limits: ContractSizeLimits::default() };
872 assert!(default_report.exceeds_runtime_size_limit());
873 assert!(default_report.exceeds_initcode_size_limit());
874
875 let custom_report =
876 SizeReport { contracts, limits: ContractSizeLimits::new(131_072, 262_144) };
877 assert!(!custom_report.exceeds_runtime_size_limit());
878 assert!(!custom_report.exceeds_initcode_size_limit());
879 let output: serde_json::Value =
880 serde_json::from_str(&custom_report.format_json_output()).unwrap();
881 assert_eq!(
882 output,
883 serde_json::json!({
884 "LargeContract": {
885 "runtime_size": 30000,
886 "init_size": 60000,
887 "runtime_margin": 101072,
888 "init_margin": 202144,
889 }
890 })
891 );
892 }
893
894 #[test]
895 fn contract_size_limits_derive_initcode_limit_from_runtime_limit() {
896 assert_eq!(
897 ContractSizeLimits::with_runtime_limit(50_000),
898 ContractSizeLimits::new(50_000, 100_000)
899 );
900 }
901}