certora-cli-beta-mirror 7.28.0__py3-none-any.whl → 7.29.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- certora_cli/CertoraProver/Compiler/CompilerCollectorVy.py +48 -13
- certora_cli/CertoraProver/certoraBuild.py +61 -30
- certora_cli/CertoraProver/certoraBuildDataClasses.py +5 -2
- certora_cli/CertoraProver/certoraBuildRust.py +77 -55
- certora_cli/CertoraProver/certoraCloudIO.py +52 -63
- certora_cli/CertoraProver/certoraCollectConfigurationLayout.py +205 -70
- certora_cli/CertoraProver/certoraCollectRunMetadata.py +3 -1
- certora_cli/CertoraProver/certoraConfigIO.py +14 -15
- certora_cli/CertoraProver/certoraContext.py +17 -5
- certora_cli/CertoraProver/certoraContextAttributes.py +98 -26
- certora_cli/CertoraProver/certoraContextValidator.py +47 -5
- certora_cli/CertoraProver/certoraParseBuildScript.py +7 -10
- certora_cli/CertoraProver/certoraVerifyGenerator.py +12 -0
- certora_cli/CertoraProver/splitRules.py +3 -1
- certora_cli/Mutate/mutateApp.py +3 -3
- certora_cli/Shared/certoraAttrUtil.py +10 -0
- certora_cli/Shared/certoraUtils.py +9 -1
- certora_cli/Shared/certoraValidateFuncs.py +7 -0
- certora_cli/certoraRanger.py +71 -0
- certora_cli/certoraRun.py +13 -15
- certora_cli/certoraSolanaProver.py +9 -2
- certora_cli/certoraSorobanProver.py +253 -4
- certora_cli_beta_mirror-7.29.1.dist-info/LICENSE +15 -0
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-7.29.1.dist-info}/METADATA +18 -4
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-7.29.1.dist-info}/RECORD +30 -29
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-7.29.1.dist-info}/WHEEL +1 -1
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-7.29.1.dist-info}/entry_points.txt +1 -0
- certora_jars/CERTORA-CLI-VERSION-METADATA.json +1 -1
- certora_jars/Typechecker.jar +0 -0
- certora_cli_beta_mirror-7.28.0.dist-info/LICENSE +0 -22
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-7.29.1.dist-info}/top_level.txt +0 -0
|
@@ -47,6 +47,7 @@ import logging
|
|
|
47
47
|
cloud_logger = logging.getLogger("cloud")
|
|
48
48
|
|
|
49
49
|
MAX_FILE_SIZE = 25 * 1024 * 1024
|
|
50
|
+
SOLANA_MAX_FILE_SIZE = 2 * MAX_FILE_SIZE
|
|
50
51
|
NO_OUTPUT_LIMIT_MINUTES = 15
|
|
51
52
|
MAX_POLLING_TIME_MINUTES = 150
|
|
52
53
|
LOG_READ_FREQUENCY = 10
|
|
@@ -69,6 +70,16 @@ Response = requests.models.Response
|
|
|
69
70
|
|
|
70
71
|
FEATURES_REPORT_FILE = Path("featuresReport.json")
|
|
71
72
|
|
|
73
|
+
class EcoEnum(Util.NoValEnum):
|
|
74
|
+
EVM = Util.auto()
|
|
75
|
+
SOROBAN = Util.auto()
|
|
76
|
+
SOLANA = Util.auto()
|
|
77
|
+
|
|
78
|
+
class ProductEnum(Util.NoValEnum):
|
|
79
|
+
PROVER = Util.auto()
|
|
80
|
+
RANGER = Util.auto()
|
|
81
|
+
SOPHY = Util.auto()
|
|
82
|
+
|
|
72
83
|
|
|
73
84
|
class TimeError(Exception):
|
|
74
85
|
"""A custom exception used to report on time elapsed errors"""
|
|
@@ -264,7 +275,9 @@ def compress_files(zip_file_path: Path, *resource_paths: Path, short_output: boo
|
|
|
264
275
|
cloud_logger.error(f"{GENERAL_ERR_PREFIX}" f"Could not compress {path}")
|
|
265
276
|
return False
|
|
266
277
|
|
|
267
|
-
|
|
278
|
+
# Solana binary files can become heavy, thus we need to increase size limit.
|
|
279
|
+
max_file_size = MAX_FILE_SIZE if not Attrs.is_rust_app() else SOLANA_MAX_FILE_SIZE
|
|
280
|
+
if zip_file_path.stat().st_size > max_file_size:
|
|
268
281
|
cloud_logger.error(f"{GENERAL_ERR_PREFIX} Max 25MB file size exceeded.")
|
|
269
282
|
return False
|
|
270
283
|
|
|
@@ -592,43 +605,25 @@ class CloudVerification:
|
|
|
592
605
|
|
|
593
606
|
jar_settings = Ctx.collect_jar_args(self.context)
|
|
594
607
|
|
|
595
|
-
if Attrs.
|
|
596
|
-
|
|
597
|
-
# unzip will also do that.
|
|
598
|
-
solana_jar_settings = []
|
|
599
|
-
if hasattr(self.context, 'build_script') and self.context.build_script:
|
|
600
|
-
solana_jar_settings.append(Path(self.context.rust_executables).name)
|
|
608
|
+
if Attrs.is_rust_app():
|
|
609
|
+
rust_jar_settings = [Path(self.context.files[0]).name]
|
|
601
610
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
611
|
+
if Attrs.is_solana_app():
|
|
612
|
+
def paths_in_source_dir(attr_values: List[str]) -> str:
|
|
613
|
+
cwd_rel_in_sources = Util.get_certora_sources_dir() / self.context.cwd_rel_in_sources
|
|
614
|
+
values: list[str] = \
|
|
615
|
+
[os.path.relpath(cwd_rel_in_sources / value, Util.get_build_dir()) for value in attr_values]
|
|
616
|
+
return ','.join(values)
|
|
605
617
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
solana_jar_settings.append(arg.split('../')[-1])
|
|
610
|
-
is_file = False
|
|
611
|
-
else:
|
|
612
|
-
solana_jar_settings.append(arg)
|
|
618
|
+
if self.context.solana_summaries:
|
|
619
|
+
rust_jar_settings.append('-solanaSummaries')
|
|
620
|
+
rust_jar_settings.append(paths_in_source_dir(self.context.solana_summaries))
|
|
613
621
|
|
|
614
|
-
if
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
auth_data["jarSettings"] =
|
|
619
|
-
elif Attrs.is_soroban_app():
|
|
620
|
-
# We need to strip "../" path component from all file paths because
|
|
621
|
-
# unzip will also do that.
|
|
622
|
-
soroban_jar_settings = []
|
|
623
|
-
# not needed - should be in files
|
|
624
|
-
if hasattr(self.context, 'build_script') and self.context.build_script:
|
|
625
|
-
soroban_jar_settings.append(Path(self.context.rust_executables).name)
|
|
626
|
-
else:
|
|
627
|
-
for file in self.context.files:
|
|
628
|
-
soroban_jar_settings.append(file.split('../')[-1])
|
|
629
|
-
for arg in jar_settings:
|
|
630
|
-
soroban_jar_settings.append(arg)
|
|
631
|
-
auth_data["jarSettings"] = soroban_jar_settings
|
|
622
|
+
if self.context.solana_inlining:
|
|
623
|
+
rust_jar_settings.append('-solanaInlining')
|
|
624
|
+
rust_jar_settings.append(paths_in_source_dir(self.context.solana_inlining))
|
|
625
|
+
|
|
626
|
+
auth_data["jarSettings"] = rust_jar_settings + jar_settings
|
|
632
627
|
else:
|
|
633
628
|
auth_data["jarSettings"] = jar_settings
|
|
634
629
|
|
|
@@ -646,6 +641,20 @@ class CloudVerification:
|
|
|
646
641
|
|
|
647
642
|
auth_data["useLatestFe"] = self.context.fe_version == str(Util.FeValue.LATEST)
|
|
648
643
|
|
|
644
|
+
if Attrs.is_solana_app():
|
|
645
|
+
auth_data["ecosystem"] = EcoEnum.SOLANA.name
|
|
646
|
+
elif Attrs.is_soroban_app():
|
|
647
|
+
auth_data["ecosystem"] = EcoEnum.SOROBAN.name
|
|
648
|
+
else:
|
|
649
|
+
auth_data["ecosystem"] = EcoEnum.EVM.name
|
|
650
|
+
|
|
651
|
+
if Attrs.is_ranger_app():
|
|
652
|
+
auth_data["product"] = ProductEnum.RANGER.name
|
|
653
|
+
elif Attrs.is_sophy_app():
|
|
654
|
+
auth_data["product"] = ProductEnum.SOPHY.name
|
|
655
|
+
else:
|
|
656
|
+
auth_data["product"] = ProductEnum.PROVER.name
|
|
657
|
+
|
|
649
658
|
if Attrs.is_evm_app() and self.context.cache is not None:
|
|
650
659
|
auth_data["toolSceneCacheKey"] = self.context.cache
|
|
651
660
|
|
|
@@ -747,7 +756,10 @@ class CloudVerification:
|
|
|
747
756
|
result = compress_files(self.ZipFilePath, *paths,
|
|
748
757
|
short_output=Ctx.is_minimal_cli_output(self.context))
|
|
749
758
|
elif Attrs.is_rust_app():
|
|
750
|
-
files_list = [Util.get_certora_metadata_file(),
|
|
759
|
+
files_list = [Util.get_certora_metadata_file(),
|
|
760
|
+
Util.get_configuration_layout_data_file(),
|
|
761
|
+
Util.get_build_dir() / Path(self.context.files[0]).name]
|
|
762
|
+
|
|
751
763
|
if Util.get_certora_sources_dir().exists():
|
|
752
764
|
files_list.append(Util.get_certora_sources_dir())
|
|
753
765
|
|
|
@@ -759,18 +771,11 @@ class CloudVerification:
|
|
|
759
771
|
return False
|
|
760
772
|
files_list.append(self.logZipFilePath)
|
|
761
773
|
|
|
762
|
-
files_list.append(Util.get_build_dir() / Path(self.context.rust_executables).name)
|
|
763
|
-
|
|
764
774
|
# Create a .RustExecution file to classify zipInput as a rust source code
|
|
765
775
|
rust_execution_file = Util.get_build_dir() / ".RustExecution"
|
|
766
776
|
rust_execution_file.touch(exist_ok=True)
|
|
767
777
|
files_list.append(rust_execution_file)
|
|
768
778
|
|
|
769
|
-
additional_files = (getattr(self.context, 'solana_inlining', None) or []) + \
|
|
770
|
-
(getattr(self.context, 'solana_summaries', None) or [])
|
|
771
|
-
for file in additional_files:
|
|
772
|
-
files_list.append(Util.get_build_dir() / Path(file).name)
|
|
773
|
-
|
|
774
779
|
if attr_file := getattr(self.context, 'rust_logs_stdout', None):
|
|
775
780
|
files_list.append(Util.get_build_dir() / Path(attr_file).name)
|
|
776
781
|
if attr_file := getattr(self.context, 'rust_logs_stderr', None):
|
|
@@ -779,29 +784,10 @@ class CloudVerification:
|
|
|
779
784
|
result = compress_files(self.ZipFilePath, *files_list,
|
|
780
785
|
short_output=Ctx.is_minimal_cli_output(self.context))
|
|
781
786
|
|
|
782
|
-
elif Attrs.is_solana_app():
|
|
783
|
-
# We zip the ELF files and the two configuration files
|
|
784
|
-
jar_args = Ctx.collect_jar_args(self.context)
|
|
785
|
-
|
|
787
|
+
elif Attrs.is_solana_app() or Attrs.is_soroban_app():
|
|
786
788
|
for file in self.context.files:
|
|
787
789
|
files_list.append(Path(file))
|
|
788
|
-
is_file = False
|
|
789
|
-
for arg in jar_args:
|
|
790
|
-
if is_file:
|
|
791
|
-
files_list.append(Path(arg))
|
|
792
|
-
is_file = False
|
|
793
|
-
|
|
794
|
-
if arg == '-solanaInlining':
|
|
795
|
-
is_file = True
|
|
796
|
-
elif arg == '-solanaSummaries':
|
|
797
|
-
is_file = True
|
|
798
|
-
result = compress_files(self.ZipFilePath, *files_list,
|
|
799
|
-
short_output=Ctx.is_minimal_cli_output(self.context))
|
|
800
790
|
|
|
801
|
-
elif Attrs.is_soroban_app():
|
|
802
|
-
# We zip the wat file
|
|
803
|
-
for file in self.context.files:
|
|
804
|
-
files_list.append(Path(file))
|
|
805
791
|
result = compress_files(self.ZipFilePath, *files_list,
|
|
806
792
|
short_output=Ctx.is_minimal_cli_output(self.context))
|
|
807
793
|
else:
|
|
@@ -829,6 +815,8 @@ class CloudVerification:
|
|
|
829
815
|
Util.flush_stdout()
|
|
830
816
|
if not result:
|
|
831
817
|
return False
|
|
818
|
+
if self.context.test == str(Util.TestValue.CHECK_ZIP):
|
|
819
|
+
raise Util.TestResultsReady(self.ZipFilePath)
|
|
832
820
|
|
|
833
821
|
cloud_logger.debug("Uploading files...")
|
|
834
822
|
if self.upload(self.presigned_url, self.ZipFilePath):
|
|
@@ -866,6 +854,7 @@ class CloudVerification:
|
|
|
866
854
|
return False
|
|
867
855
|
|
|
868
856
|
file_upload_success = self.__compress_and_upload_zip_files()
|
|
857
|
+
|
|
869
858
|
if not file_upload_success:
|
|
870
859
|
return False
|
|
871
860
|
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
#
|
|
13
13
|
# You should have received a copy of the GNU General Public License
|
|
14
14
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
|
-
|
|
15
|
+
import dataclasses
|
|
16
16
|
import json
|
|
17
17
|
from enum import Enum
|
|
18
|
-
from typing import Optional,
|
|
18
|
+
from typing import Optional, Any
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
import sys
|
|
21
21
|
|
|
@@ -31,21 +31,40 @@ class MainSection(Enum):
|
|
|
31
31
|
GENERAL = "GENERAL"
|
|
32
32
|
SOLIDITY_COMPILER = "SOLIDITY_COMPILER"
|
|
33
33
|
GIT = "GIT"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
NEW_SECTION = "NEW_SECTION"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ContentType(Enum):
|
|
38
|
+
SIMPLE = "SIMPLE"
|
|
39
|
+
COMPLEX = "COMPLEX"
|
|
40
|
+
FLAG = "FLAG"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclasses.dataclass
|
|
44
|
+
class InnerContent:
|
|
45
|
+
inner_title: str
|
|
46
|
+
content_type: str
|
|
47
|
+
content: Any
|
|
48
|
+
doc_link: str = ''
|
|
49
|
+
tooltip: str = ''
|
|
50
|
+
unsound: str = 'false'
|
|
38
51
|
|
|
52
|
+
def __post_init__(self) -> None:
|
|
53
|
+
if isinstance(self.content, bool):
|
|
54
|
+
self.content = 'true' if self.content else 'false'
|
|
55
|
+
if isinstance(self.unsound, bool):
|
|
56
|
+
self.unsound = 'true' if self.unsound else 'false'
|
|
39
57
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
58
|
+
|
|
59
|
+
@dataclasses.dataclass
|
|
60
|
+
class CardContent:
|
|
61
|
+
card_title: str
|
|
62
|
+
content_type: str
|
|
63
|
+
content: Any
|
|
44
64
|
|
|
45
65
|
|
|
46
66
|
DOC_LINK_PREFIX = 'https://docs.certora.com/en/latest/docs/'
|
|
47
67
|
GIT_ATTRIBUTES = ['origin', 'revision', 'branch', 'dirty']
|
|
48
|
-
SPECIAL_MAIN_SECTIONS = ['files', 'links', 'packages']
|
|
49
68
|
|
|
50
69
|
|
|
51
70
|
class AttributeJobConfigData:
|
|
@@ -87,18 +106,25 @@ class RunConfigurationLayout:
|
|
|
87
106
|
configuration_layout : Dict -- An aggregated configuration for a specific run, nested by main section, subsection.
|
|
88
107
|
Each leaf contains data about attribute value, type, documentation link and UI data.
|
|
89
108
|
"""
|
|
90
|
-
|
|
109
|
+
|
|
110
|
+
configuration_layout: list[Any]
|
|
111
|
+
|
|
112
|
+
def __init__(self, configuration_layout: list[Any]):
|
|
91
113
|
# Dynamically allocate class attributes from dict
|
|
92
|
-
|
|
93
|
-
setattr(self, key, value)
|
|
114
|
+
self.configuration_layout = configuration_layout
|
|
94
115
|
|
|
95
116
|
def __repr__(self) -> str:
|
|
96
|
-
|
|
117
|
+
try:
|
|
118
|
+
return json.dumps(self.configuration_layout, indent=2, sort_keys=True)
|
|
119
|
+
except TypeError:
|
|
120
|
+
# Fallback if something isn't serializable
|
|
121
|
+
return str(self.configuration_layout)
|
|
97
122
|
|
|
98
123
|
@classmethod
|
|
99
|
-
def dump_file(cls, data:
|
|
124
|
+
def dump_file(cls, data: list) -> None:
|
|
125
|
+
sorted_data = sort_configuration_layout(data)
|
|
100
126
|
with Utils.get_configuration_layout_data_file().open("w+") as f:
|
|
101
|
-
json.dump(
|
|
127
|
+
json.dump(sorted_data, f, indent=4, cls=MetadataEncoder)
|
|
102
128
|
|
|
103
129
|
@classmethod
|
|
104
130
|
def load_file(cls) -> dict:
|
|
@@ -110,12 +136,11 @@ class RunConfigurationLayout:
|
|
|
110
136
|
raise
|
|
111
137
|
|
|
112
138
|
def dump(self) -> None:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
raise
|
|
139
|
+
try:
|
|
140
|
+
self.dump_file(self.configuration_layout)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
print(f"Failed to write configuration layout file: {Utils.get_configuration_layout_data_file()}\n{e}")
|
|
143
|
+
raise
|
|
119
144
|
|
|
120
145
|
|
|
121
146
|
def collect_configuration_layout() -> RunConfigurationLayout:
|
|
@@ -127,7 +152,7 @@ def collect_configuration_layout() -> RunConfigurationLayout:
|
|
|
127
152
|
metadata = RunMetaData.load_file()
|
|
128
153
|
except Exception as e:
|
|
129
154
|
print(f"failed to load job metadata! cannot create a configuration layout file without metadata!\n{e}")
|
|
130
|
-
return RunConfigurationLayout(configuration_layout=
|
|
155
|
+
return RunConfigurationLayout(configuration_layout=[])
|
|
131
156
|
|
|
132
157
|
attributes_configs = collect_attribute_configs(metadata)
|
|
133
158
|
configuration_layout = collect_run_config_from_metadata(attributes_configs, metadata)
|
|
@@ -154,89 +179,199 @@ def get_doc_link(attr) -> str: # type: ignore
|
|
|
154
179
|
return doc_link
|
|
155
180
|
|
|
156
181
|
|
|
157
|
-
def
|
|
182
|
+
def create_or_get_card_content(output: list[CardContent], name: str) -> CardContent:
|
|
183
|
+
"""
|
|
184
|
+
Returns an existing CardContent by name or creates and appends a new one if it doesn't exist.
|
|
185
|
+
Card content type will always be complex in this case.
|
|
186
|
+
Args:
|
|
187
|
+
output (list[CardContent]): List of CardContent objects.
|
|
188
|
+
name (str): Title of the card to find or create.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
CardContent: The found or newly created CardContent.
|
|
192
|
+
"""
|
|
193
|
+
main_section = next((section for section in output if section.card_title == name), None)
|
|
194
|
+
if main_section is None:
|
|
195
|
+
main_section = CardContent(
|
|
196
|
+
card_title=name,
|
|
197
|
+
content_type=ContentType.COMPLEX.value,
|
|
198
|
+
content=[]
|
|
199
|
+
)
|
|
200
|
+
output.append(main_section)
|
|
201
|
+
return main_section
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def create_inner_content(name: str, content_type: ContentType, value: Any, doc_link: str,
|
|
205
|
+
config_data: AttributeJobConfigData) -> InnerContent:
|
|
206
|
+
return InnerContent(
|
|
207
|
+
inner_title=name,
|
|
208
|
+
content_type=content_type.value,
|
|
209
|
+
content=value,
|
|
210
|
+
doc_link=doc_link,
|
|
211
|
+
tooltip=config_data.tooltip or '',
|
|
212
|
+
unsound='true' if config_data.unsound else 'false'
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def collect_attribute_configs(metadata: dict) -> list[CardContent]:
|
|
217
|
+
"""
|
|
218
|
+
Collects and organizes attribute configurations into a structured list of CardContent objects.
|
|
219
|
+
|
|
220
|
+
This function iterates through all available attributes defined, checks if relevant metadata is provided
|
|
221
|
+
for each attribute, and organizes the data into sections and subsections based on configuration rules.
|
|
222
|
+
|
|
223
|
+
Attributes are grouped under their respective main sections, with special handling for:
|
|
224
|
+
- Simple value attributes
|
|
225
|
+
- List and dictionary attributes
|
|
226
|
+
- Attributes requiring new sections (e.g., Files, Links, Packages)
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
metadata (dict): Metadata dictionary containing attribute values.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
list: A list of CardContent objects representing the structured configuration view,
|
|
233
|
+
ready for rendering or further processing.
|
|
234
|
+
"""
|
|
158
235
|
attr_list = Attrs.get_attribute_class().attribute_list()
|
|
159
|
-
output:
|
|
236
|
+
output: list[CardContent] = []
|
|
160
237
|
|
|
161
238
|
for attr in attr_list:
|
|
162
239
|
attr_name = attr.name.lower()
|
|
163
240
|
if attr.config_data is None:
|
|
164
241
|
continue
|
|
165
242
|
|
|
166
|
-
|
|
243
|
+
attr_value = metadata.get(attr_name) or metadata.get('conf', {}).get(attr_name)
|
|
244
|
+
if attr_value is None:
|
|
167
245
|
continue
|
|
168
246
|
|
|
169
|
-
attr_value = metadata.get(attr_name) or metadata.get('conf', {}).get(attr_name)
|
|
170
247
|
config_data: AttributeJobConfigData = attr.config_data
|
|
171
248
|
doc_link = config_data.doc_link or get_doc_link(attr)
|
|
172
249
|
|
|
173
|
-
#
|
|
250
|
+
# Find or create the main section
|
|
174
251
|
main_section_key = config_data.main_section.value.lower()
|
|
175
|
-
main_section = output
|
|
252
|
+
main_section = create_or_get_card_content(output, main_section_key)
|
|
253
|
+
|
|
254
|
+
# Files, Links and Packages are special cases where the main section is the attribute itself
|
|
255
|
+
if main_section_key == MainSection.NEW_SECTION.value.lower():
|
|
256
|
+
main_section.card_title = attr_name
|
|
257
|
+
main_section.content_type = ContentType.SIMPLE.value
|
|
258
|
+
main_section.content.append(
|
|
259
|
+
create_inner_content(attr_name, ContentType.SIMPLE, attr_value, doc_link, config_data)
|
|
260
|
+
)
|
|
261
|
+
continue
|
|
176
262
|
|
|
177
|
-
#
|
|
263
|
+
# Find or create the subsection (if it exists)
|
|
178
264
|
if isinstance(attr_value, list):
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
attr_name = main_section_key
|
|
183
|
-
else:
|
|
184
|
-
current_section = main_section
|
|
185
|
-
|
|
186
|
-
flag_type = FlagType.LIST_FLAG
|
|
265
|
+
current_section: Any = main_section
|
|
266
|
+
content_type = ContentType.SIMPLE
|
|
267
|
+
|
|
187
268
|
elif isinstance(attr_value, dict):
|
|
188
269
|
current_section = main_section
|
|
189
|
-
|
|
270
|
+
content_type = ContentType.COMPLEX
|
|
271
|
+
attr_value = [
|
|
272
|
+
create_inner_content(key, ContentType.FLAG, value, doc_link, config_data)
|
|
273
|
+
for key, value in attr_value.items()
|
|
274
|
+
]
|
|
190
275
|
else:
|
|
276
|
+
# this attribute is a value attribute without a subsection, it will be placed in flags.
|
|
191
277
|
subsection_key = config_data.subsection.lower() if config_data.subsection else 'flags'
|
|
192
|
-
current_section = main_section.
|
|
193
|
-
|
|
278
|
+
current_section = next((section for section in main_section.content
|
|
279
|
+
if section.inner_title == subsection_key), None)
|
|
280
|
+
if current_section is None:
|
|
281
|
+
current_section = InnerContent(
|
|
282
|
+
inner_title=subsection_key,
|
|
283
|
+
content_type=ContentType.COMPLEX.value,
|
|
284
|
+
content=[]
|
|
285
|
+
)
|
|
286
|
+
main_section.content.append(current_section)
|
|
287
|
+
|
|
288
|
+
content_type = ContentType.FLAG
|
|
194
289
|
|
|
195
290
|
# Update the current section with attribute details
|
|
196
|
-
current_section
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
'doc_link': doc_link,
|
|
200
|
-
'tooltip': config_data.tooltip,
|
|
201
|
-
'unsound': config_data.unsound
|
|
202
|
-
}
|
|
291
|
+
current_section.content.append(
|
|
292
|
+
create_inner_content(attr_name, content_type, attr_value, doc_link, config_data)
|
|
293
|
+
)
|
|
203
294
|
|
|
204
295
|
return output
|
|
205
296
|
|
|
206
297
|
|
|
207
|
-
def collect_run_config_from_metadata(attributes_configs:
|
|
298
|
+
def collect_run_config_from_metadata(attributes_configs: list[CardContent], metadata: dict) -> list[CardContent]:
|
|
208
299
|
"""
|
|
209
300
|
Adding CLI and Git configuration from metadata
|
|
210
301
|
"""
|
|
211
|
-
metadata_section = attributes_configs.setdefault(MainSection.METADATA.value.lower(), {})
|
|
212
302
|
|
|
213
303
|
# Define a mapping of metadata attributes to their keys in general_section
|
|
214
304
|
metadata_mappings = {
|
|
215
|
-
'
|
|
216
|
-
'
|
|
217
|
-
'solc_version': metadata.get('conf', {}).get('solc'),
|
|
218
|
-
'verify': metadata.get('conf', {}).get('verify'),
|
|
305
|
+
'CLI Version': metadata.get('CLI_version'),
|
|
306
|
+
'Verify': metadata.get('conf', {}).get('verify'),
|
|
219
307
|
}
|
|
220
308
|
|
|
309
|
+
general_section = create_or_get_card_content(attributes_configs, MainSection.GENERAL.value.lower())
|
|
310
|
+
|
|
221
311
|
# Add metadata attributes dynamically if they exist
|
|
222
312
|
for key, value in metadata_mappings.items():
|
|
223
313
|
if value:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
git_section = attributes_configs.setdefault(MainSection.GIT.value.lower(), {})
|
|
314
|
+
general_section.content.append(InnerContent(
|
|
315
|
+
inner_title=key,
|
|
316
|
+
content_type=ContentType.FLAG.value,
|
|
317
|
+
content=value,
|
|
318
|
+
))
|
|
319
|
+
|
|
320
|
+
# Adding GIT configuration from metadata only if attributes are found
|
|
321
|
+
git_content = []
|
|
233
322
|
for attr in GIT_ATTRIBUTES:
|
|
234
323
|
if attr_value := metadata.get(attr):
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
324
|
+
git_content.append(InnerContent(
|
|
325
|
+
inner_title=attr,
|
|
326
|
+
content_type=ContentType.FLAG.value,
|
|
327
|
+
content=attr_value,
|
|
328
|
+
))
|
|
329
|
+
|
|
330
|
+
if git_content:
|
|
331
|
+
git_section = create_or_get_card_content(attributes_configs, MainSection.GIT.value.lower())
|
|
332
|
+
git_section.content = git_content
|
|
241
333
|
|
|
242
334
|
return attributes_configs
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def sort_configuration_layout(data: list[CardContent]) -> list[CardContent]:
|
|
338
|
+
"""
|
|
339
|
+
Sorts a configuration layout:
|
|
340
|
+
- Top-level sorted by 'card_title'
|
|
341
|
+
- Nested content sorted by 'inner_title', with 'Verify' first
|
|
342
|
+
"""
|
|
343
|
+
priority = {
|
|
344
|
+
"Verify": 0,
|
|
345
|
+
"general": 0,
|
|
346
|
+
"solc": 0,
|
|
347
|
+
"CLI Version": 1,
|
|
348
|
+
"flags": 2
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
def inner_sort_key(item: Any) -> Any:
|
|
352
|
+
if isinstance(item, CardContent):
|
|
353
|
+
title = item.card_title
|
|
354
|
+
return priority.get(title, 3), title.lower()
|
|
355
|
+
elif isinstance(item, InnerContent):
|
|
356
|
+
title = item.inner_title
|
|
357
|
+
return priority.get(title, 3), title.lower()
|
|
358
|
+
else:
|
|
359
|
+
return item
|
|
360
|
+
|
|
361
|
+
def sort_content(content: list[InnerContent]) -> list[InnerContent]:
|
|
362
|
+
sorted_content = []
|
|
363
|
+
for item in content:
|
|
364
|
+
if isinstance(item.content, list):
|
|
365
|
+
# Recurse into nested 'content'
|
|
366
|
+
item.content = sorted(item.content, key=inner_sort_key)
|
|
367
|
+
sorted_content.append(item)
|
|
368
|
+
return sorted(sorted_content, key=inner_sort_key)
|
|
369
|
+
|
|
370
|
+
# Sort top-level entries by 'card_title'
|
|
371
|
+
sorted_data = sorted(data, key=inner_sort_key)
|
|
372
|
+
|
|
373
|
+
# Sort nested 'content'
|
|
374
|
+
for section in sorted_data:
|
|
375
|
+
section.content = sort_content(section.content)
|
|
376
|
+
|
|
377
|
+
return sorted_data
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
#
|
|
13
13
|
# You should have received a copy of the GNU General Public License
|
|
14
14
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
|
-
|
|
15
|
+
import dataclasses
|
|
16
16
|
import json
|
|
17
17
|
from typing import Any, Dict, List, Optional
|
|
18
18
|
import subprocess
|
|
@@ -47,6 +47,8 @@ class MetadataEncoder(json.JSONEncoder):
|
|
|
47
47
|
def default(self, obj: Any) -> Any:
|
|
48
48
|
if isinstance(obj, set):
|
|
49
49
|
return list(obj)
|
|
50
|
+
if dataclasses.is_dataclass(obj):
|
|
51
|
+
return dataclasses.asdict(obj)
|
|
50
52
|
return json.JSONEncoder.default(self, obj)
|
|
51
53
|
|
|
52
54
|
|
|
@@ -75,9 +75,9 @@ def read_from_conf_file(context: CertoraContext) -> None:
|
|
|
75
75
|
|
|
76
76
|
try:
|
|
77
77
|
with conf_file_path.open() as conf_file:
|
|
78
|
-
conf_file_attr = json5.load(conf_file, allow_duplicate_keys=False)
|
|
78
|
+
context.conf_file_attr = json5.load(conf_file, allow_duplicate_keys=False)
|
|
79
79
|
try:
|
|
80
|
-
check_conf_content(
|
|
80
|
+
check_conf_content(context)
|
|
81
81
|
except Util.CertoraUserInputError as e:
|
|
82
82
|
raise Util.CertoraUserInputError(f"Error when reading {conf_file_path}: {str(e)}", e) from None
|
|
83
83
|
context.conf_file = str(conf_file_path)
|
|
@@ -99,6 +99,7 @@ def handle_override_base_config(context: CertoraContext) -> None:
|
|
|
99
99
|
with Path(context.override_base_config).open() as conf_file:
|
|
100
100
|
try:
|
|
101
101
|
override_base_config_attrs = json5.load(conf_file, allow_duplicate_keys=False)
|
|
102
|
+
context.conf_file_attr = {**override_base_config_attrs, **context.conf_file_attr}
|
|
102
103
|
|
|
103
104
|
if 'override_base_config' in override_base_config_attrs:
|
|
104
105
|
raise Util.CertoraUserInputError("base config cannot include 'override_base_config'")
|
|
@@ -113,7 +114,7 @@ def handle_override_base_config(context: CertoraContext) -> None:
|
|
|
113
114
|
raise Util.CertoraUserInputError(f"{attr} appears in the base conf file {context.override_base_config} but is not a known attribute.")
|
|
114
115
|
|
|
115
116
|
|
|
116
|
-
def check_conf_content(
|
|
117
|
+
def check_conf_content(context: CertoraContext) -> None:
|
|
117
118
|
"""
|
|
118
119
|
validating content read from the conf file
|
|
119
120
|
Note: a command line definition trumps the definition in the file.
|
|
@@ -122,15 +123,15 @@ def check_conf_content(conf_file_attr: Dict[str, Any], context: CertoraContext)
|
|
|
122
123
|
@param context: A namespace containing options from the command line, if any
|
|
123
124
|
"""
|
|
124
125
|
|
|
125
|
-
for option in conf_file_attr:
|
|
126
|
+
for option in context.conf_file_attr:
|
|
126
127
|
if hasattr(context, option):
|
|
127
128
|
val = getattr(context, option)
|
|
128
129
|
if val is None or val is False:
|
|
129
|
-
setattr(context, option, conf_file_attr[option])
|
|
130
|
-
elif option !=
|
|
130
|
+
setattr(context, option, context.conf_file_attr[option])
|
|
131
|
+
elif option != 'files' and val != context.conf_file_attr[option]:
|
|
131
132
|
cli_val = ' '.join(val) if isinstance(val, list) else str(val)
|
|
132
|
-
conf_val = ' '.join(conf_file_attr[option]) \
|
|
133
|
-
if isinstance(conf_file_attr[option], list) else str(conf_file_attr[option])
|
|
133
|
+
conf_val = ' '.join(context.conf_file_attr[option]) \
|
|
134
|
+
if isinstance(context.conf_file_attr[option], list) else str(context.conf_file_attr[option])
|
|
134
135
|
run_logger.warning(f"Note: attribute {option} value in CLI ({cli_val}) overrides value stored in conf"
|
|
135
136
|
f" file ({conf_val})")
|
|
136
137
|
else:
|
|
@@ -138,12 +139,10 @@ def check_conf_content(conf_file_attr: Dict[str, Any], context: CertoraContext)
|
|
|
138
139
|
|
|
139
140
|
handle_override_base_config(context)
|
|
140
141
|
|
|
141
|
-
if Attrs.is_evm_app() and
|
|
142
|
+
if Attrs.is_evm_app() and not context.files and not context.project_sanity and not context.foundry:
|
|
142
143
|
raise Util.CertoraUserInputError("Mandatory 'files' attribute is missing from the configuration")
|
|
144
|
+
context.files = context.conf_file_attr.get('files')
|
|
145
|
+
if Attrs.is_soroban_app() and not context.files and not context.build_script:
|
|
146
|
+
raise Util.CertoraUserInputError("'files' or 'build script' must be set for Soroban runs")
|
|
143
147
|
|
|
144
|
-
|
|
145
|
-
has_build_script = getattr(context, 'build_script', False)
|
|
146
|
-
if not has_build_script and 'files' not in conf_file_attr:
|
|
147
|
-
raise Util.CertoraUserInputError("Mandatory 'build_script' or 'files' attribute is missing from the configuration")
|
|
148
|
-
|
|
149
|
-
context.files = conf_file_attr.get('files')
|
|
148
|
+
context.files = context.conf_file_attr.get('files')
|