genlayer-test 0.4.1__py3-none-any.whl → 2.0.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 (44) hide show
  1. {genlayer_test-0.4.1.dist-info → genlayer_test-2.0.0.dist-info}/METADATA +75 -14
  2. genlayer_test-2.0.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 +197 -0
  7. gltest/{glchain/contract.py → contracts/contract_factory.py} +22 -137
  8. gltest/contracts/contract_functions.py +59 -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_cli/config/constants.py +1 -0
  15. gltest_cli/config/plugin.py +53 -0
  16. gltest_cli/config/pytest_context.py +9 -0
  17. gltest_cli/config/types.py +41 -0
  18. gltest_cli/config/user.py +21 -8
  19. tests/examples/contracts/football_prediction_market.py +1 -1
  20. tests/examples/tests/test_football_prediction_market.py +2 -2
  21. tests/examples/tests/test_intelligent_oracle_factory.py +6 -6
  22. tests/examples/tests/test_llm_erc20.py +5 -5
  23. tests/examples/tests/test_llm_erc20_analyze.py +50 -0
  24. tests/examples/tests/test_log_indexer.py +23 -11
  25. tests/examples/tests/test_multi_file_contract.py +2 -2
  26. tests/examples/tests/test_multi_file_contract_legacy.py +2 -2
  27. tests/examples/tests/test_multi_read_erc20.py +14 -12
  28. tests/examples/tests/test_multi_tenant_storage.py +11 -7
  29. tests/examples/tests/test_read_erc20.py +1 -1
  30. tests/examples/tests/test_storage.py +4 -4
  31. tests/examples/tests/test_storage_legacy.py +5 -3
  32. tests/examples/tests/test_user_storage.py +20 -10
  33. tests/examples/tests/test_wizard_of_coin.py +1 -1
  34. tests/gltest_cli/config/test_config_integration.py +432 -0
  35. tests/gltest_cli/config/test_general_config.py +406 -0
  36. tests/gltest_cli/config/test_plugin.py +167 -0
  37. tests/gltest_cli/config/test_user.py +61 -1
  38. genlayer_test-0.4.1.dist-info/RECORD +0 -67
  39. gltest/glchain/__init__.py +0 -16
  40. {genlayer_test-0.4.1.dist-info → genlayer_test-2.0.0.dist-info}/WHEEL +0 -0
  41. {genlayer_test-0.4.1.dist-info → genlayer_test-2.0.0.dist-info}/entry_points.txt +0 -0
  42. {genlayer_test-0.4.1.dist-info → genlayer_test-2.0.0.dist-info}/licenses/LICENSE +0 -0
  43. {genlayer_test-0.4.1.dist-info → genlayer_test-2.0.0.dist-info}/top_level.txt +0 -0
  44. /gltest/{glchain/account.py → accounts.py} +0 -0
@@ -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,7 +107,6 @@ 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
112
  wait_transaction_status: TransactionStatus = TransactionStatus.FINALIZED,
@@ -239,6 +119,11 @@ class ContractFactory:
239
119
  wait_interval = general_config.get_default_wait_interval()
240
120
  if wait_retries is None:
241
121
  wait_retries = general_config.get_default_wait_retries()
122
+ leader_only = (
123
+ general_config.get_leader_only()
124
+ if general_config.check_studio_based_rpc()
125
+ else False
126
+ )
242
127
 
243
128
  client = get_gl_client()
244
129
  try:
@@ -273,7 +158,7 @@ class ContractFactory:
273
158
  schema = self._get_schema_with_fallback()
274
159
  if schema is None:
275
160
  raise ValueError(
276
- "Failed to get schema from all clients (hosted studio, local, and regular)"
161
+ "Failed to get schema from all clients (default, hosted studio, and local)"
277
162
  )
278
163
 
279
164
  return Contract.new(
@@ -0,0 +1,59 @@
1
+ from dataclasses import dataclass
2
+ from typing import Callable, Optional, Dict, Any
3
+ from gltest.types import TransactionStatus
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(self):
15
+ if not self.read_only:
16
+ raise ValueError("call() not implemented for non-readonly method")
17
+ return self.call_method()
18
+
19
+ def transact(
20
+ self,
21
+ value: int = 0,
22
+ consensus_max_rotations: Optional[int] = None,
23
+ wait_transaction_status: TransactionStatus = TransactionStatus.FINALIZED,
24
+ wait_interval: Optional[int] = None,
25
+ wait_retries: Optional[int] = None,
26
+ wait_triggered_transactions: bool = True,
27
+ wait_triggered_transactions_status: TransactionStatus = TransactionStatus.FINALIZED,
28
+ ):
29
+ if self.read_only:
30
+ raise ValueError("Cannot transact read-only method")
31
+ return self.transact_method(
32
+ value=value,
33
+ consensus_max_rotations=consensus_max_rotations,
34
+ wait_transaction_status=wait_transaction_status,
35
+ wait_interval=wait_interval,
36
+ wait_retries=wait_retries,
37
+ wait_triggered_transactions=wait_triggered_transactions,
38
+ wait_triggered_transactions_status=wait_triggered_transactions_status,
39
+ )
40
+
41
+ def analyze(
42
+ self,
43
+ provider: str,
44
+ model: str,
45
+ config: Optional[Dict[str, Any]] = None,
46
+ plugin: Optional[str] = None,
47
+ plugin_config: Optional[Dict[str, Any]] = None,
48
+ runs: int = 100,
49
+ ):
50
+ if self.read_only:
51
+ raise ValueError("Cannot analyze read-only method")
52
+ return self.analyze_method(
53
+ provider=provider,
54
+ model=model,
55
+ config=config,
56
+ plugin=plugin,
57
+ plugin_config=plugin_config,
58
+ runs=runs,
59
+ )
@@ -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)
@@ -0,0 +1,259 @@
1
+ """
2
+ Stats collector module for contract method analysis.
3
+
4
+ This module contains classes and functions to collect and analyze statistics
5
+ from contract method executions, simplifying the analyze_method implementation.
6
+ """
7
+
8
+ import time
9
+ import json
10
+ import hashlib
11
+ from datetime import datetime, timezone
12
+ from typing import List, Dict, Any, Optional
13
+ from dataclasses import dataclass
14
+
15
+ from gltest.clients import get_gl_client
16
+ from gltest.types import CalldataEncodable
17
+ from .method_stats import StateGroup, FailedRun, MethodStatsDetailed, MethodStatsSummary
18
+ from gltest_cli.config.general import get_general_config
19
+ from gltest_cli.config.pytest_context import get_current_test_nodeid
20
+ from .utils import safe_filename
21
+
22
+
23
+ @dataclass
24
+ class SimulationConfig:
25
+ """Configuration for simulation runs."""
26
+
27
+ provider: str
28
+ model: str
29
+ config: Optional[Dict[str, Any]] = None
30
+ plugin: Optional[str] = None
31
+ plugin_config: Optional[Dict[str, Any]] = None
32
+
33
+
34
+ @dataclass
35
+ class SimulationResults:
36
+ """Results from simulation runs."""
37
+
38
+ sim_results: List[Dict[str, Any]]
39
+ failed_runs: List[FailedRun]
40
+ server_errors: int
41
+ execution_time: float
42
+ timestamp: str
43
+
44
+
45
+ class StatsCollector:
46
+ """Collects and analyzes statistics for contract method executions."""
47
+
48
+ def __init__(
49
+ self,
50
+ contract_address: str,
51
+ method_name: str,
52
+ account: Any,
53
+ args: Optional[List[CalldataEncodable]] = None,
54
+ ):
55
+ self.contract_address = contract_address
56
+ self.method_name = method_name
57
+ self.account = account
58
+ self.args = args or []
59
+ self.client = get_gl_client()
60
+
61
+ def run_simulations(
62
+ self, sim_config: SimulationConfig, runs: int
63
+ ) -> SimulationResults:
64
+ """Execute multiple simulation runs and collect results."""
65
+ start_time = time.time()
66
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
67
+
68
+ sim_results = []
69
+ failed_runs_list = []
70
+ server_errors = 0
71
+
72
+ for run_idx in range(runs):
73
+ try:
74
+ sim_result = self._execute_single_simulation(sim_config)
75
+ sim_results.append(sim_result)
76
+
77
+ if sim_result.get("execution_result") != "SUCCESS":
78
+ failed_runs_list.append(
79
+ self._create_failed_run(run_idx, sim_result)
80
+ )
81
+ except Exception as e:
82
+ server_errors += 1
83
+ failed_runs_list.append(
84
+ FailedRun(
85
+ run=run_idx,
86
+ error=str(e),
87
+ error_type="server",
88
+ genvm_result=None,
89
+ )
90
+ )
91
+
92
+ execution_time = time.time() - start_time
93
+
94
+ return SimulationResults(
95
+ sim_results=sim_results,
96
+ failed_runs=failed_runs_list,
97
+ server_errors=server_errors,
98
+ execution_time=execution_time,
99
+ timestamp=timestamp,
100
+ )
101
+
102
+ def _execute_single_simulation(
103
+ self, sim_config: SimulationConfig
104
+ ) -> Dict[str, Any]:
105
+ """Execute a single simulation."""
106
+ config_dict = {
107
+ "provider": sim_config.provider,
108
+ "model": sim_config.model,
109
+ }
110
+
111
+ if (
112
+ sim_config.config is not None
113
+ and sim_config.plugin is not None
114
+ and sim_config.plugin_config is not None
115
+ ):
116
+ config_dict["config"] = sim_config.config
117
+ config_dict["plugin"] = sim_config.plugin
118
+ config_dict["plugin_config"] = sim_config.plugin_config
119
+
120
+ return self.client.simulate_write_contract(
121
+ address=self.contract_address,
122
+ function_name=self.method_name,
123
+ account=self.account,
124
+ args=self.args,
125
+ sim_config=config_dict,
126
+ )
127
+
128
+ def _create_failed_run(self, run_idx: int, sim_result: Dict[str, Any]) -> FailedRun:
129
+ """Create a FailedRun object from a failed simulation result."""
130
+ return FailedRun(
131
+ run=run_idx,
132
+ error=sim_result.get("error", "unknown error"),
133
+ error_type="simulation",
134
+ genvm_result={
135
+ "stderr": sim_result.get("genvm_result", {}).get("stderr", ""),
136
+ "stdout": sim_result.get("genvm_result", {}).get("stdout", ""),
137
+ },
138
+ )
139
+
140
+ def analyze_results(
141
+ self,
142
+ sim_results: SimulationResults,
143
+ runs: int,
144
+ sim_config: SimulationConfig,
145
+ ) -> MethodStatsSummary:
146
+ """Analyze simulation results and generate statistics."""
147
+ state_groups = self._analyze_states(sim_results.sim_results)
148
+
149
+ executed_runs = runs - sim_results.server_errors
150
+ successful_runs = sum(
151
+ 1
152
+ for sim_receipt in sim_results.sim_results
153
+ if sim_receipt.get("execution_result") == "SUCCESS"
154
+ )
155
+
156
+ most_common_count = max((group.count for group in state_groups), default=0)
157
+ reliability_score = (
158
+ (most_common_count / executed_runs) if executed_runs > 0 else 0.0
159
+ )
160
+
161
+ # Save detailed stats
162
+ detailed_stats = self._create_detailed_stats(
163
+ sim_results=sim_results,
164
+ state_groups=state_groups,
165
+ runs=runs,
166
+ executed_runs=executed_runs,
167
+ successful_runs=successful_runs,
168
+ most_common_count=most_common_count,
169
+ reliability_score=reliability_score,
170
+ sim_config=sim_config,
171
+ )
172
+ self._save_detailed_stats(detailed_stats)
173
+
174
+ # Return summary
175
+ return MethodStatsSummary(
176
+ method=self.method_name,
177
+ args=self.args,
178
+ total_runs=runs,
179
+ server_error_runs=sim_results.server_errors,
180
+ executed_runs=executed_runs,
181
+ failed_runs=len(sim_results.failed_runs),
182
+ successful_runs=successful_runs,
183
+ unique_states=len(state_groups),
184
+ most_common_state_count=most_common_count,
185
+ reliability_score=reliability_score,
186
+ execution_time=sim_results.execution_time,
187
+ provider=sim_config.provider,
188
+ model=sim_config.model,
189
+ )
190
+
191
+ def _analyze_states(self, sim_results: List[Dict[str, Any]]) -> List[StateGroup]:
192
+ """Analyze contract states from simulation results."""
193
+ state_counts = {}
194
+ state_to_hash_str = {}
195
+
196
+ for sim_receipt in sim_results:
197
+ contract_state = sim_receipt.get("contract_state", {})
198
+ state_json = json.dumps(contract_state, sort_keys=True)
199
+ state_hash = hashlib.sha256(state_json.encode()).hexdigest()
200
+ state_hash_str = f"0x{state_hash}"
201
+ state_to_hash_str[state_hash] = state_hash_str
202
+ state_counts[state_hash] = state_counts.get(state_hash, 0) + 1
203
+
204
+ return [
205
+ StateGroup(count=count, state_hash=state_to_hash_str[state_hash])
206
+ for state_hash, count in sorted(
207
+ state_counts.items(), key=lambda x: x[1], reverse=True
208
+ )
209
+ ]
210
+
211
+ def _create_detailed_stats(
212
+ self,
213
+ sim_results: SimulationResults,
214
+ state_groups: List[StateGroup],
215
+ runs: int,
216
+ executed_runs: int,
217
+ successful_runs: int,
218
+ most_common_count: int,
219
+ reliability_score: float,
220
+ sim_config: SimulationConfig,
221
+ ) -> MethodStatsDetailed:
222
+ """Create detailed statistics object."""
223
+ configuration = {
224
+ "runs": runs,
225
+ "provider": sim_config.provider,
226
+ "model": sim_config.model,
227
+ "config": sim_config.config,
228
+ "plugin": sim_config.plugin,
229
+ "plugin_config": sim_config.plugin_config,
230
+ }
231
+
232
+ return MethodStatsDetailed(
233
+ method=self.method_name,
234
+ params=self.args,
235
+ timestamp=sim_results.timestamp,
236
+ configuration=configuration,
237
+ execution_time=sim_results.execution_time,
238
+ executed_runs=executed_runs,
239
+ failed_runs=len(sim_results.failed_runs),
240
+ successful_runs=successful_runs,
241
+ server_error_runs=sim_results.server_errors,
242
+ unique_states=len(state_groups),
243
+ most_common_state_count=most_common_count,
244
+ reliability_score=reliability_score,
245
+ state_groups=state_groups,
246
+ failed_runs_results=sim_results.failed_runs,
247
+ sim_results=sim_results.sim_results,
248
+ )
249
+
250
+ def _save_detailed_stats(self, detailed_stats: MethodStatsDetailed) -> None:
251
+ """Save detailed statistics to the configured directory."""
252
+ general_config = get_general_config()
253
+ current_nodeid = get_current_test_nodeid()
254
+ if current_nodeid is None:
255
+ safe_name = "no_test"
256
+ else:
257
+ safe_name = safe_filename(current_nodeid)
258
+ stats_dir = general_config.get_analysis_dir() / safe_name
259
+ detailed_stats.save_to_directory(stats_dir)