genlayer-test 0.4.1__py3-none-any.whl → 0.5.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.
Files changed (46) hide show
  1. {genlayer_test-0.4.1.dist-info → genlayer_test-0.5.0.dist-info}/METADATA +257 -24
  2. genlayer_test-0.5.0.dist-info/RECORD +76 -0
  3. gltest/__init__.py +7 -6
  4. gltest/{glchain/client.py → clients.py} +1 -1
  5. gltest/contracts/__init__.py +4 -0
  6. gltest/contracts/contract.py +205 -0
  7. gltest/{glchain/contract.py → contracts/contract_factory.py} +47 -144
  8. gltest/contracts/contract_functions.py +62 -0
  9. gltest/contracts/method_stats.py +163 -0
  10. gltest/contracts/stats_collector.py +259 -0
  11. gltest/contracts/utils.py +12 -0
  12. gltest/fixtures.py +2 -6
  13. gltest/helpers/take_snapshot.py +1 -1
  14. gltest/types.py +1 -0
  15. gltest_cli/config/constants.py +2 -0
  16. gltest_cli/config/plugin.py +121 -49
  17. gltest_cli/config/pytest_context.py +9 -0
  18. gltest_cli/config/types.py +73 -8
  19. gltest_cli/config/user.py +71 -28
  20. gltest_cli/logging.py +3 -2
  21. tests/examples/contracts/football_prediction_market.py +1 -1
  22. tests/examples/tests/test_football_prediction_market.py +2 -2
  23. tests/examples/tests/test_intelligent_oracle_factory.py +8 -24
  24. tests/examples/tests/test_llm_erc20.py +5 -5
  25. tests/examples/tests/test_llm_erc20_analyze.py +50 -0
  26. tests/examples/tests/test_log_indexer.py +23 -11
  27. tests/examples/tests/test_multi_file_contract.py +2 -2
  28. tests/examples/tests/test_multi_file_contract_legacy.py +2 -2
  29. tests/examples/tests/test_multi_read_erc20.py +14 -12
  30. tests/examples/tests/test_multi_tenant_storage.py +11 -7
  31. tests/examples/tests/test_read_erc20.py +1 -1
  32. tests/examples/tests/test_storage.py +4 -4
  33. tests/examples/tests/test_storage_legacy.py +5 -3
  34. tests/examples/tests/test_user_storage.py +20 -10
  35. tests/examples/tests/test_wizard_of_coin.py +1 -1
  36. tests/gltest_cli/config/test_config_integration.py +432 -0
  37. tests/gltest_cli/config/test_general_config.py +406 -0
  38. tests/gltest_cli/config/test_plugin.py +164 -1
  39. tests/gltest_cli/config/test_user.py +61 -1
  40. genlayer_test-0.4.1.dist-info/RECORD +0 -67
  41. gltest/glchain/__init__.py +0 -16
  42. {genlayer_test-0.4.1.dist-info → genlayer_test-0.5.0.dist-info}/WHEEL +0 -0
  43. {genlayer_test-0.4.1.dist-info → genlayer_test-0.5.0.dist-info}/entry_points.txt +0 -0
  44. {genlayer_test-0.4.1.dist-info → genlayer_test-0.5.0.dist-info}/licenses/LICENSE +0 -0
  45. {genlayer_test-0.4.1.dist-info → genlayer_test-0.5.0.dist-info}/top_level.txt +0 -0
  46. /gltest/{glchain/account.py → accounts.py} +0 -0
@@ -0,0 +1,205 @@
1
+ import types
2
+ from eth_account.signers.local import LocalAccount
3
+ from dataclasses import dataclass
4
+ from gltest.clients import get_gl_client
5
+ from gltest.types import (
6
+ CalldataEncodable,
7
+ GenLayerTransaction,
8
+ TransactionStatus,
9
+ TransactionHashVariant,
10
+ )
11
+ from typing import List, Any, Optional, Dict, Callable
12
+ from gltest_cli.config.general import get_general_config
13
+ from .contract_functions import ContractFunction
14
+ from .stats_collector import StatsCollector, SimulationConfig
15
+
16
+
17
+ def read_contract_wrapper(
18
+ self,
19
+ method_name: str,
20
+ args: Optional[List[CalldataEncodable]] = None,
21
+ ) -> Any:
22
+ """
23
+ Wrapper to the contract read method.
24
+ """
25
+
26
+ def call_method(
27
+ transaction_hash_variant: TransactionHashVariant = TransactionHashVariant.LATEST_NONFINAL,
28
+ ):
29
+ client = get_gl_client()
30
+ return client.read_contract(
31
+ address=self.address,
32
+ function_name=method_name,
33
+ account=self.account,
34
+ args=args,
35
+ transaction_hash_variant=transaction_hash_variant,
36
+ )
37
+
38
+ return ContractFunction(
39
+ method_name=method_name,
40
+ read_only=True,
41
+ call_method=call_method,
42
+ )
43
+
44
+
45
+ def write_contract_wrapper(
46
+ self,
47
+ method_name: str,
48
+ args: Optional[List[CalldataEncodable]] = None,
49
+ ) -> GenLayerTransaction:
50
+ """
51
+ Wrapper to the contract write method.
52
+ """
53
+
54
+ def transact_method(
55
+ value: int = 0,
56
+ consensus_max_rotations: Optional[int] = None,
57
+ wait_transaction_status: TransactionStatus = TransactionStatus.ACCEPTED,
58
+ wait_interval: Optional[int] = None,
59
+ wait_retries: Optional[int] = None,
60
+ wait_triggered_transactions: bool = False,
61
+ wait_triggered_transactions_status: TransactionStatus = TransactionStatus.ACCEPTED,
62
+ ):
63
+ """
64
+ Transact the contract method.
65
+ """
66
+ general_config = get_general_config()
67
+ actual_wait_interval = (
68
+ wait_interval
69
+ if wait_interval is not None
70
+ else general_config.get_default_wait_interval()
71
+ )
72
+ actual_wait_retries = (
73
+ wait_retries
74
+ if wait_retries is not None
75
+ else general_config.get_default_wait_retries()
76
+ )
77
+ leader_only = (
78
+ general_config.get_leader_only()
79
+ if general_config.check_studio_based_rpc()
80
+ else False
81
+ )
82
+ client = get_gl_client()
83
+ tx_hash = client.write_contract(
84
+ address=self.address,
85
+ function_name=method_name,
86
+ account=self.account,
87
+ value=value,
88
+ consensus_max_rotations=consensus_max_rotations,
89
+ leader_only=leader_only,
90
+ args=args,
91
+ )
92
+ receipt = client.wait_for_transaction_receipt(
93
+ transaction_hash=tx_hash,
94
+ status=wait_transaction_status,
95
+ interval=actual_wait_interval,
96
+ retries=actual_wait_retries,
97
+ )
98
+ if wait_triggered_transactions:
99
+ triggered_transactions = receipt["triggered_transactions"]
100
+ for triggered_transaction in triggered_transactions:
101
+ client.wait_for_transaction_receipt(
102
+ transaction_hash=triggered_transaction,
103
+ status=wait_triggered_transactions_status,
104
+ interval=actual_wait_interval,
105
+ retries=actual_wait_retries,
106
+ )
107
+ return receipt
108
+
109
+ def analyze_method(
110
+ provider: str,
111
+ model: str,
112
+ config: Optional[Dict[str, Any]] = None,
113
+ plugin: Optional[str] = None,
114
+ plugin_config: Optional[Dict[str, Any]] = None,
115
+ runs: int = 100,
116
+ ):
117
+ """
118
+ Analyze the contract method using StatsCollector.
119
+ """
120
+ collector = StatsCollector(
121
+ contract_address=self.address,
122
+ method_name=method_name,
123
+ account=self.account,
124
+ args=args,
125
+ )
126
+ sim_config = SimulationConfig(
127
+ provider=provider,
128
+ model=model,
129
+ config=config,
130
+ plugin=plugin,
131
+ plugin_config=plugin_config,
132
+ )
133
+ sim_results = collector.run_simulations(sim_config, runs)
134
+ return collector.analyze_results(sim_results, runs, sim_config)
135
+
136
+ return ContractFunction(
137
+ method_name=method_name,
138
+ read_only=False,
139
+ transact_method=transact_method,
140
+ analyze_method=analyze_method,
141
+ )
142
+
143
+
144
+ def contract_function_factory(method_name: str, read_only: bool) -> Callable:
145
+ """
146
+ Create a function that interacts with a specific contract method.
147
+ """
148
+ if read_only:
149
+ return lambda self, args=None: read_contract_wrapper(self, method_name, args)
150
+ return lambda self, args=None: write_contract_wrapper(self, method_name, args)
151
+
152
+
153
+ @dataclass
154
+ class Contract:
155
+ """
156
+ Class to interact with a contract, its methods
157
+ are implemented dynamically at build time.
158
+ """
159
+
160
+ address: str
161
+ account: Optional[LocalAccount] = None
162
+ _schema: Optional[Dict[str, Any]] = None
163
+
164
+ @classmethod
165
+ def new(
166
+ cls,
167
+ address: str,
168
+ schema: Dict[str, Any],
169
+ account: Optional[LocalAccount] = None,
170
+ ) -> "Contract":
171
+ """
172
+ Build the methods from the schema.
173
+ """
174
+ if not isinstance(schema, dict) or "methods" not in schema:
175
+ raise ValueError("Invalid schema: must contain 'methods' field")
176
+ instance = cls(address=address, _schema=schema, account=account)
177
+ instance._build_methods_from_schema()
178
+ return instance
179
+
180
+ def _build_methods_from_schema(self):
181
+ """
182
+ Build the methods from the schema.
183
+ """
184
+ if self._schema is None:
185
+ raise ValueError("No schema provided")
186
+ for method_name, method_info in self._schema["methods"].items():
187
+ if not isinstance(method_info, dict) or "readonly" not in method_info:
188
+ raise ValueError(
189
+ f"Invalid method info for '{method_name}': must contain 'readonly' field"
190
+ )
191
+ method_func = contract_function_factory(
192
+ method_name, method_info["readonly"]
193
+ )
194
+ bound_method = types.MethodType(method_func, self)
195
+ setattr(self, method_name, bound_method)
196
+
197
+ def connect(self, account: LocalAccount) -> "Contract":
198
+ """
199
+ Create a new instance of the contract with the same methods and a different account.
200
+ """
201
+ new_contract = self.__class__(
202
+ address=self.address, account=account, _schema=self._schema
203
+ )
204
+ new_contract._build_methods_from_schema()
205
+ return new_contract
@@ -1,145 +1,26 @@
1
+ from dataclasses import dataclass
2
+ from typing import Type, Union, Optional, List, Any
3
+ from pathlib import Path
1
4
  from eth_typing import (
2
5
  Address,
3
6
  ChecksumAddress,
4
7
  )
5
8
  from eth_account.signers.local import LocalAccount
6
- from typing import Union
7
- from pathlib import Path
8
- from dataclasses import dataclass
9
9
  from gltest.artifacts import (
10
10
  find_contract_definition_from_name,
11
11
  find_contract_definition_from_path,
12
12
  )
13
+ from gltest.clients import (
14
+ get_gl_client,
15
+ get_gl_hosted_studio_client,
16
+ get_local_client,
17
+ )
18
+ from .contract import Contract
19
+ from gltest.logging import logger
20
+ from gltest.types import TransactionStatus
13
21
  from gltest.assertions import tx_execution_failed
14
22
  from gltest.exceptions import DeploymentError
15
- from .client import get_gl_client, get_gl_hosted_studio_client, get_local_client
16
- from gltest.types import CalldataEncodable, GenLayerTransaction, TransactionStatus
17
- from typing import List, Any, Type, Optional, Dict, Callable
18
- import types
19
23
  from gltest_cli.config.general import get_general_config
20
- from gltest.logging import logger
21
-
22
-
23
- @dataclass
24
- class Contract:
25
- """
26
- Class to interact with a contract, its methods
27
- are implemented dynamically at build time.
28
- """
29
-
30
- address: str
31
- account: Optional[LocalAccount] = None
32
- _schema: Optional[Dict[str, Any]] = None
33
-
34
- @classmethod
35
- def new(
36
- cls,
37
- address: str,
38
- schema: Dict[str, Any],
39
- account: Optional[LocalAccount] = None,
40
- ) -> "Contract":
41
- """
42
- Build the methods from the schema.
43
- """
44
- if not isinstance(schema, dict) or "methods" not in schema:
45
- raise ValueError("Invalid schema: must contain 'methods' field")
46
- instance = cls(address=address, _schema=schema, account=account)
47
- instance._build_methods_from_schema()
48
- return instance
49
-
50
- def _build_methods_from_schema(self):
51
- if self._schema is None:
52
- raise ValueError("No schema provided")
53
- for method_name, method_info in self._schema["methods"].items():
54
- if not isinstance(method_info, dict) or "readonly" not in method_info:
55
- raise ValueError(
56
- f"Invalid method info for '{method_name}': must contain 'readonly' field"
57
- )
58
- method_func = self.contract_method_factory(
59
- method_name, method_info["readonly"]
60
- )
61
- bound_method = types.MethodType(method_func, self)
62
- setattr(self, method_name, bound_method)
63
-
64
- def connect(self, account: LocalAccount) -> "Contract":
65
- """
66
- Create a new instance of the contract with the same methods and a different account.
67
- """
68
- new_contract = self.__class__(
69
- address=self.address, account=account, _schema=self._schema
70
- )
71
- new_contract._build_methods_from_schema()
72
- return new_contract
73
-
74
- @staticmethod
75
- def contract_method_factory(method_name: str, read_only: bool) -> Callable:
76
- """
77
- Create a function that interacts with a specific contract method.
78
- """
79
-
80
- def read_contract_wrapper(
81
- self,
82
- args: Optional[List[CalldataEncodable]] = None,
83
- ) -> Any:
84
- """
85
- Wrapper to the contract read method.
86
- """
87
- client = get_gl_client()
88
- return client.read_contract(
89
- address=self.address,
90
- function_name=method_name,
91
- account=self.account,
92
- args=args,
93
- )
94
-
95
- def write_contract_wrapper(
96
- self,
97
- args: Optional[List[CalldataEncodable]] = None,
98
- value: int = 0,
99
- consensus_max_rotations: Optional[int] = None,
100
- leader_only: bool = False,
101
- wait_transaction_status: TransactionStatus = TransactionStatus.FINALIZED,
102
- wait_interval: Optional[int] = None,
103
- wait_retries: Optional[int] = None,
104
- wait_triggered_transactions: bool = True,
105
- wait_triggered_transactions_status: TransactionStatus = TransactionStatus.FINALIZED,
106
- ) -> GenLayerTransaction:
107
- """
108
- Wrapper to the contract write method.
109
- """
110
- general_config = get_general_config()
111
- if wait_interval is None:
112
- wait_interval = general_config.get_default_wait_interval()
113
- if wait_retries is None:
114
- wait_retries = general_config.get_default_wait_retries()
115
- client = get_gl_client()
116
- tx_hash = client.write_contract(
117
- address=self.address,
118
- function_name=method_name,
119
- account=self.account,
120
- value=value,
121
- consensus_max_rotations=consensus_max_rotations,
122
- leader_only=leader_only,
123
- args=args,
124
- )
125
- receipt = client.wait_for_transaction_receipt(
126
- transaction_hash=tx_hash,
127
- status=wait_transaction_status,
128
- interval=wait_interval,
129
- retries=wait_retries,
130
- )
131
- if wait_triggered_transactions:
132
- triggered_transactions = receipt["triggered_transactions"]
133
- for triggered_transaction in triggered_transactions:
134
- client.wait_for_transaction_receipt(
135
- transaction_hash=triggered_transaction,
136
- status=wait_triggered_transactions_status,
137
- interval=wait_interval,
138
- retries=wait_retries,
139
- )
140
- return receipt
141
-
142
- return read_contract_wrapper if read_only else write_contract_wrapper
143
24
 
144
25
 
145
26
  @dataclass
@@ -184,17 +65,17 @@ class ContractFactory:
184
65
  """Attempts to get the contract schema using multiple clients in a fallback pattern.
185
66
 
186
67
  This method tries to get the contract schema in the following order:
187
- 1. Hosted studio client
188
- 2. Local client
189
- 3. Regular client
68
+ 1. Default client
69
+ 2. Hosted studio client
70
+ 3. Local client
190
71
 
191
72
  Returns:
192
73
  Optional[Dict[str, Any]]: The contract schema if successful, None if all attempts fail.
193
74
  """
194
75
  clients = (
76
+ ("default", get_gl_client()),
195
77
  ("hosted studio", get_gl_hosted_studio_client()),
196
78
  ("local", get_local_client()),
197
- ("default", get_gl_client()),
198
79
  )
199
80
  for label, client in clients:
200
81
  try:
@@ -216,7 +97,7 @@ class ContractFactory:
216
97
  schema = self._get_schema_with_fallback()
217
98
  if schema is None:
218
99
  raise ValueError(
219
- "Failed to get schema from all clients (hosted studio, local, and regular)"
100
+ "Failed to get schema from all clients (default, hosted studio, and local)"
220
101
  )
221
102
 
222
103
  return Contract.new(address=contract_address, schema=schema, account=account)
@@ -226,19 +107,31 @@ class ContractFactory:
226
107
  args: List[Any] = [],
227
108
  account: Optional[LocalAccount] = None,
228
109
  consensus_max_rotations: Optional[int] = None,
229
- leader_only: bool = False,
230
110
  wait_interval: Optional[int] = None,
231
111
  wait_retries: Optional[int] = None,
232
- wait_transaction_status: TransactionStatus = TransactionStatus.FINALIZED,
112
+ wait_transaction_status: TransactionStatus = TransactionStatus.ACCEPTED,
113
+ wait_triggered_transactions: bool = False,
114
+ wait_triggered_transactions_status: TransactionStatus = TransactionStatus.ACCEPTED,
233
115
  ) -> Contract:
234
116
  """
235
117
  Deploy the contract
236
118
  """
237
119
  general_config = get_general_config()
238
- if wait_interval is None:
239
- wait_interval = general_config.get_default_wait_interval()
240
- if wait_retries is None:
241
- wait_retries = general_config.get_default_wait_retries()
120
+ actual_wait_interval = (
121
+ wait_interval
122
+ if wait_interval is not None
123
+ else general_config.get_default_wait_interval()
124
+ )
125
+ actual_wait_retries = (
126
+ wait_retries
127
+ if wait_retries is not None
128
+ else general_config.get_default_wait_retries()
129
+ )
130
+ leader_only = (
131
+ general_config.get_leader_only()
132
+ if general_config.check_studio_based_rpc()
133
+ else False
134
+ )
242
135
 
243
136
  client = get_gl_client()
244
137
  try:
@@ -252,14 +145,24 @@ class ContractFactory:
252
145
  tx_receipt = client.wait_for_transaction_receipt(
253
146
  transaction_hash=tx_hash,
254
147
  status=wait_transaction_status,
255
- interval=wait_interval,
256
- retries=wait_retries,
148
+ interval=actual_wait_interval,
149
+ retries=actual_wait_retries,
257
150
  )
258
151
  if tx_execution_failed(tx_receipt):
259
152
  raise ValueError(
260
153
  f"Deployment transaction finalized with error: {tx_receipt}"
261
154
  )
262
155
 
156
+ if wait_triggered_transactions:
157
+ triggered_transactions = tx_receipt["triggered_transactions"]
158
+ for triggered_transaction in triggered_transactions:
159
+ client.wait_for_transaction_receipt(
160
+ transaction_hash=triggered_transaction,
161
+ status=wait_triggered_transactions_status,
162
+ interval=actual_wait_interval,
163
+ retries=actual_wait_retries,
164
+ )
165
+
263
166
  if (
264
167
  "tx_data_decoded" in tx_receipt
265
168
  and "contract_address" in tx_receipt["tx_data_decoded"]
@@ -273,7 +176,7 @@ class ContractFactory:
273
176
  schema = self._get_schema_with_fallback()
274
177
  if schema is None:
275
178
  raise ValueError(
276
- "Failed to get schema from all clients (hosted studio, local, and regular)"
179
+ "Failed to get schema from all clients (default, hosted studio, and local)"
277
180
  )
278
181
 
279
182
  return Contract.new(
@@ -0,0 +1,62 @@
1
+ from dataclasses import dataclass
2
+ from typing import Callable, Optional, Dict, Any
3
+ from gltest.types import TransactionStatus, TransactionHashVariant
4
+
5
+
6
+ @dataclass
7
+ class ContractFunction:
8
+ method_name: str
9
+ read_only: bool
10
+ call_method: Optional[Callable] = None
11
+ analyze_method: Optional[Callable] = None
12
+ transact_method: Optional[Callable] = None
13
+
14
+ def call(
15
+ self,
16
+ transaction_hash_variant: TransactionHashVariant = TransactionHashVariant.LATEST_NONFINAL,
17
+ ):
18
+ if not self.read_only:
19
+ raise ValueError("call() not implemented for non-readonly method")
20
+ return self.call_method(transaction_hash_variant=transaction_hash_variant)
21
+
22
+ def transact(
23
+ self,
24
+ value: int = 0,
25
+ consensus_max_rotations: Optional[int] = None,
26
+ wait_transaction_status: TransactionStatus = TransactionStatus.ACCEPTED,
27
+ wait_interval: Optional[int] = None,
28
+ wait_retries: Optional[int] = None,
29
+ wait_triggered_transactions: bool = False,
30
+ wait_triggered_transactions_status: TransactionStatus = TransactionStatus.ACCEPTED,
31
+ ):
32
+ if self.read_only:
33
+ raise ValueError("Cannot transact read-only method")
34
+ return self.transact_method(
35
+ value=value,
36
+ consensus_max_rotations=consensus_max_rotations,
37
+ wait_transaction_status=wait_transaction_status,
38
+ wait_interval=wait_interval,
39
+ wait_retries=wait_retries,
40
+ wait_triggered_transactions=wait_triggered_transactions,
41
+ wait_triggered_transactions_status=wait_triggered_transactions_status,
42
+ )
43
+
44
+ def analyze(
45
+ self,
46
+ provider: str,
47
+ model: str,
48
+ config: Optional[Dict[str, Any]] = None,
49
+ plugin: Optional[str] = None,
50
+ plugin_config: Optional[Dict[str, Any]] = None,
51
+ runs: int = 100,
52
+ ):
53
+ if self.read_only:
54
+ raise ValueError("Cannot analyze read-only method")
55
+ return self.analyze_method(
56
+ provider=provider,
57
+ model=model,
58
+ config=config,
59
+ plugin=plugin,
60
+ plugin_config=plugin_config,
61
+ runs=runs,
62
+ )
@@ -0,0 +1,163 @@
1
+ from dataclasses import dataclass
2
+ from typing import List, Any, Optional, Dict
3
+ import json
4
+ from pathlib import Path
5
+ from .utils import safe_filename
6
+
7
+
8
+ @dataclass
9
+ class MethodStatsSummary:
10
+ """Statistical analysis results for a contract method execution."""
11
+
12
+ method: str
13
+ args: List[Any]
14
+ total_runs: int
15
+ executed_runs: int
16
+ server_error_runs: int
17
+ failed_runs: int
18
+ successful_runs: int
19
+ unique_states: int
20
+ most_common_state_count: int
21
+ reliability_score: float
22
+ execution_time: float
23
+ provider: str
24
+ model: str
25
+
26
+ @property
27
+ def success_rate(self) -> float:
28
+ """Calculate success rate as percentage."""
29
+ if self.total_runs == 0:
30
+ return 0.0
31
+ return (self.successful_runs / self.total_runs) * 100
32
+
33
+ def __str__(self) -> str:
34
+ """Format the statistical analysis results."""
35
+ return f"""Method analysis summary
36
+ ---------------------------
37
+ Method: {self.method}
38
+ Args: {self.args}
39
+ Provider: {self.provider}
40
+ Model: {self.model}
41
+ Total runs: {self.total_runs}
42
+ Server error runs: {self.server_error_runs}
43
+ Method executed runs: {self.executed_runs}
44
+ Method successful runs: {self.successful_runs}
45
+ Method failed runs: {self.failed_runs}
46
+ Unique states: {self.unique_states}
47
+ Reliability score: {self.reliability_score:.2f}% ({self.most_common_state_count}/{self.executed_runs} consistent)
48
+ Execution time: {self.execution_time:.1f}s"""
49
+
50
+
51
+ @dataclass
52
+ class StateGroup:
53
+ """Represents a group of runs with the same contract state."""
54
+
55
+ count: int
56
+ state_hash: str
57
+
58
+
59
+ @dataclass
60
+ class FailedRun:
61
+ """Represents a failed run with error details."""
62
+
63
+ run: int
64
+ error: str
65
+ error_type: str # "server" or "simulation"
66
+ genvm_result: Optional[Dict[str, str]]
67
+
68
+
69
+ @dataclass
70
+ class MethodStatsDetailed:
71
+ """Detailed statistical analysis results for a contract method execution."""
72
+
73
+ method: str
74
+ params: List[Any]
75
+ timestamp: str
76
+ configuration: Dict[str, Any]
77
+ execution_time: float
78
+ executed_runs: int
79
+ failed_runs: int
80
+ successful_runs: int
81
+ server_error_runs: int
82
+ most_common_state_count: int
83
+ reliability_score: float
84
+ sim_results: List[Any]
85
+ unique_states: int
86
+ state_groups: List[StateGroup]
87
+ failed_runs_results: List[FailedRun]
88
+
89
+ def to_dict(self) -> Dict[str, Any]:
90
+ """Convert to dictionary format."""
91
+ return {
92
+ "method": self.method,
93
+ "params": self.params,
94
+ "timestamp": self.timestamp,
95
+ "configuration": self.configuration,
96
+ "execution_time": self.execution_time,
97
+ "method_executed_runs": self.executed_runs,
98
+ "method_failed_runs": self.failed_runs,
99
+ "method_successful_runs": self.successful_runs,
100
+ "server_error_runs": self.server_error_runs,
101
+ "most_common_state_count": self.most_common_state_count,
102
+ "reliability_score": self.reliability_score,
103
+ "unique_states": self.unique_states,
104
+ "state_groups": [
105
+ {"count": sg.count, "state_hash": sg.state_hash}
106
+ for sg in self.state_groups
107
+ ],
108
+ "failed_runs": [
109
+ {
110
+ "run": fr.run,
111
+ "error": fr.error,
112
+ "error_type": fr.error_type,
113
+ "genvm_result": fr.genvm_result,
114
+ }
115
+ for fr in self.failed_runs_results
116
+ ],
117
+ "simulation_results": self.filter_sim_results(),
118
+ }
119
+
120
+ def filter_sim_results(self) -> List[Any]:
121
+ """Filter the simulation results to only include specific fields."""
122
+ filtered_results = []
123
+ allowed_fields = [
124
+ "calldata",
125
+ "contract_state",
126
+ "eq_outputs",
127
+ "execution_result",
128
+ "genvm_result",
129
+ "node_config",
130
+ "pending_transactions",
131
+ "result",
132
+ ]
133
+
134
+ for result in self.sim_results:
135
+ filtered_result = {
136
+ key: result.get(key) for key in allowed_fields if key in result
137
+ }
138
+ filtered_results.append(filtered_result)
139
+
140
+ return filtered_results
141
+
142
+ def save_to_directory(self, directory: str, filename: Optional[str] = None) -> str:
143
+ """
144
+ Save the detailed stats to a JSON file in the specified directory.
145
+
146
+ Raises:
147
+ OSError: If directory creation or file writing fails.
148
+ """
149
+ directory_path = Path(directory)
150
+ directory_path.mkdir(parents=True, exist_ok=True)
151
+ if filename is None:
152
+ safe_method = safe_filename(self.method)
153
+ safe_timestamp = safe_filename(self.timestamp)
154
+ filename = f"{safe_method}_{safe_timestamp}.json"
155
+ if not filename.endswith(".json"):
156
+ filename += ".json"
157
+ filepath = directory_path / filename
158
+ try:
159
+ with open(filepath, "w") as f:
160
+ json.dump(self.to_dict(), f, indent=2)
161
+ except (OSError, TypeError) as e:
162
+ raise OSError(f"Failed to save stats to {filepath}: {e}") from e
163
+ return str(filepath)