certora-cli-beta-mirror 7.28.0__py3-none-any.whl → 7.29.0__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 -41
- certora_cli/CertoraProver/certoraCloudIO.py +29 -64
- 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 +13 -5
- certora_cli/CertoraProver/certoraContextAttributes.py +95 -26
- certora_cli/CertoraProver/certoraContextValidator.py +39 -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 +11 -13
- certora_cli/certoraSolanaProver.py +7 -0
- certora_cli/certoraSorobanProver.py +253 -4
- certora_cli_beta_mirror-7.29.0.dist-info/LICENSE +15 -0
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-7.29.0.dist-info}/METADATA +18 -4
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-7.29.0.dist-info}/RECORD +30 -29
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-7.29.0.dist-info}/WHEEL +1 -1
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-7.29.0.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.0.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
|
|
@@ -264,7 +265,9 @@ def compress_files(zip_file_path: Path, *resource_paths: Path, short_output: boo
|
|
|
264
265
|
cloud_logger.error(f"{GENERAL_ERR_PREFIX}" f"Could not compress {path}")
|
|
265
266
|
return False
|
|
266
267
|
|
|
267
|
-
|
|
268
|
+
# Solana binary files can become heavy, thus we need to increase size limit.
|
|
269
|
+
max_file_size = MAX_FILE_SIZE if not Attrs.is_rust_app() else SOLANA_MAX_FILE_SIZE
|
|
270
|
+
if zip_file_path.stat().st_size > max_file_size:
|
|
268
271
|
cloud_logger.error(f"{GENERAL_ERR_PREFIX} Max 25MB file size exceeded.")
|
|
269
272
|
return False
|
|
270
273
|
|
|
@@ -592,43 +595,25 @@ class CloudVerification:
|
|
|
592
595
|
|
|
593
596
|
jar_settings = Ctx.collect_jar_args(self.context)
|
|
594
597
|
|
|
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)
|
|
598
|
+
if Attrs.is_rust_app():
|
|
599
|
+
rust_jar_settings = [Path(self.context.files[0]).name]
|
|
601
600
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
601
|
+
if Attrs.is_solana_app():
|
|
602
|
+
def paths_in_source_dir(attr_values: List[str]) -> str:
|
|
603
|
+
cwd_rel_in_sources = Util.get_certora_sources_dir() / self.context.cwd_rel_in_sources
|
|
604
|
+
values: list[str] = \
|
|
605
|
+
[os.path.relpath(cwd_rel_in_sources / value, Util.get_build_dir()) for value in attr_values]
|
|
606
|
+
return ','.join(values)
|
|
605
607
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
is_file = True
|
|
616
|
-
elif arg == '-solanaSummaries':
|
|
617
|
-
is_file = True
|
|
618
|
-
auth_data["jarSettings"] = solana_jar_settings
|
|
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
|
|
608
|
+
if self.context.solana_summaries:
|
|
609
|
+
rust_jar_settings.append('-solanaSummaries')
|
|
610
|
+
rust_jar_settings.append(paths_in_source_dir(self.context.solana_summaries))
|
|
611
|
+
|
|
612
|
+
if self.context.solana_inlining:
|
|
613
|
+
rust_jar_settings.append('-solanaInlining')
|
|
614
|
+
rust_jar_settings.append(paths_in_source_dir(self.context.solana_inlining))
|
|
615
|
+
|
|
616
|
+
auth_data["jarSettings"] = rust_jar_settings + jar_settings
|
|
632
617
|
else:
|
|
633
618
|
auth_data["jarSettings"] = jar_settings
|
|
634
619
|
|
|
@@ -747,7 +732,10 @@ class CloudVerification:
|
|
|
747
732
|
result = compress_files(self.ZipFilePath, *paths,
|
|
748
733
|
short_output=Ctx.is_minimal_cli_output(self.context))
|
|
749
734
|
elif Attrs.is_rust_app():
|
|
750
|
-
files_list = [Util.get_certora_metadata_file(),
|
|
735
|
+
files_list = [Util.get_certora_metadata_file(),
|
|
736
|
+
Util.get_configuration_layout_data_file(),
|
|
737
|
+
Util.get_build_dir() / Path(self.context.files[0]).name]
|
|
738
|
+
|
|
751
739
|
if Util.get_certora_sources_dir().exists():
|
|
752
740
|
files_list.append(Util.get_certora_sources_dir())
|
|
753
741
|
|
|
@@ -759,18 +747,11 @@ class CloudVerification:
|
|
|
759
747
|
return False
|
|
760
748
|
files_list.append(self.logZipFilePath)
|
|
761
749
|
|
|
762
|
-
files_list.append(Util.get_build_dir() / Path(self.context.rust_executables).name)
|
|
763
|
-
|
|
764
750
|
# Create a .RustExecution file to classify zipInput as a rust source code
|
|
765
751
|
rust_execution_file = Util.get_build_dir() / ".RustExecution"
|
|
766
752
|
rust_execution_file.touch(exist_ok=True)
|
|
767
753
|
files_list.append(rust_execution_file)
|
|
768
754
|
|
|
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
755
|
if attr_file := getattr(self.context, 'rust_logs_stdout', None):
|
|
775
756
|
files_list.append(Util.get_build_dir() / Path(attr_file).name)
|
|
776
757
|
if attr_file := getattr(self.context, 'rust_logs_stderr', None):
|
|
@@ -779,29 +760,10 @@ class CloudVerification:
|
|
|
779
760
|
result = compress_files(self.ZipFilePath, *files_list,
|
|
780
761
|
short_output=Ctx.is_minimal_cli_output(self.context))
|
|
781
762
|
|
|
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
|
-
|
|
763
|
+
elif Attrs.is_solana_app() or Attrs.is_soroban_app():
|
|
786
764
|
for file in self.context.files:
|
|
787
765
|
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
766
|
|
|
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
767
|
result = compress_files(self.ZipFilePath, *files_list,
|
|
806
768
|
short_output=Ctx.is_minimal_cli_output(self.context))
|
|
807
769
|
else:
|
|
@@ -829,6 +791,8 @@ class CloudVerification:
|
|
|
829
791
|
Util.flush_stdout()
|
|
830
792
|
if not result:
|
|
831
793
|
return False
|
|
794
|
+
if self.context.test == str(Util.TestValue.CHECK_ZIP):
|
|
795
|
+
raise Util.TestResultsReady(self.ZipFilePath)
|
|
832
796
|
|
|
833
797
|
cloud_logger.debug("Uploading files...")
|
|
834
798
|
if self.upload(self.presigned_url, self.ZipFilePath):
|
|
@@ -866,6 +830,7 @@ class CloudVerification:
|
|
|
866
830
|
return False
|
|
867
831
|
|
|
868
832
|
file_upload_success = self.__compress_and_upload_zip_files()
|
|
833
|
+
|
|
869
834
|
if not file_upload_success:
|
|
870
835
|
return False
|
|
871
836
|
|
|
@@ -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')
|
|
@@ -120,9 +120,9 @@ def get_local_run_cmd(context: CertoraContext) -> List[str]:
|
|
|
120
120
|
"""
|
|
121
121
|
run_args = []
|
|
122
122
|
|
|
123
|
-
if
|
|
124
|
-
run_args.append(
|
|
125
|
-
elif context.is_tac
|
|
123
|
+
if Attrs.is_rust_app():
|
|
124
|
+
run_args.append(Path(context.files[0]).name)
|
|
125
|
+
elif context.is_tac:
|
|
126
126
|
# For Rust app we assume the files holds the executable for the prover, currently we support a single file
|
|
127
127
|
try:
|
|
128
128
|
run_args.append(context.files[0])
|
|
@@ -159,10 +159,16 @@ class ProverParser(AttrUtil.ContextAttributeParser):
|
|
|
159
159
|
|
|
160
160
|
def format_help(self) -> str:
|
|
161
161
|
console = Console()
|
|
162
|
-
|
|
162
|
+
if Attrs.is_ranger_app():
|
|
163
|
+
console.print("\n\nRanger - Certora’s bounded model checker for smart contracts")
|
|
164
|
+
else:
|
|
165
|
+
console.print("\n\nThe Certora Prover - A formal verification tool for smart contracts")
|
|
163
166
|
# Using sys.stdout.write() as print() would color some of the strings here
|
|
164
167
|
sys.stdout.write(f"\n\nUsage: {sys.argv[0]} <Files> <Flags>\n\n")
|
|
165
|
-
if Attrs.
|
|
168
|
+
if Attrs.is_ranger_app():
|
|
169
|
+
sys.stdout.write("Ranger supports only Solidity (.sol) and configuration (.conf) files.\n"
|
|
170
|
+
"Rust and Vyper contracts are not currently supported.\n\n")
|
|
171
|
+
elif Attrs.is_evm_app():
|
|
166
172
|
sys.stdout.write("Acceptable files for EVM projects are Solidity files (.sol suffix), "
|
|
167
173
|
"Vyper files (.vy suffix), or conf files (.conf suffix)\n\n")
|
|
168
174
|
elif Attrs.is_solana_app():
|
|
@@ -252,6 +258,8 @@ def get_args(args_list: Optional[List[str]] = None) -> CertoraContext:
|
|
|
252
258
|
Cv.check_mode_of_operation(context) # Here boolean run characteristics are set
|
|
253
259
|
|
|
254
260
|
validator = Cv.CertoraContextValidator(context)
|
|
261
|
+
if Attrs.is_evm_app():
|
|
262
|
+
validator.handle_ranger_attrs()
|
|
255
263
|
validator.validate()
|
|
256
264
|
if Attrs.is_evm_app() or Attrs.is_rust_app():
|
|
257
265
|
current_build_directory = Util.get_build_dir()
|