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.
- {genlayer_test-0.4.1.dist-info → genlayer_test-2.0.0.dist-info}/METADATA +75 -14
- genlayer_test-2.0.0.dist-info/RECORD +76 -0
- gltest/__init__.py +7 -6
- gltest/{glchain/client.py → clients.py} +1 -1
- gltest/contracts/__init__.py +4 -0
- gltest/contracts/contract.py +197 -0
- gltest/{glchain/contract.py → contracts/contract_factory.py} +22 -137
- gltest/contracts/contract_functions.py +59 -0
- gltest/contracts/method_stats.py +163 -0
- gltest/contracts/stats_collector.py +259 -0
- gltest/contracts/utils.py +12 -0
- gltest/fixtures.py +2 -6
- gltest/helpers/take_snapshot.py +1 -1
- gltest_cli/config/constants.py +1 -0
- gltest_cli/config/plugin.py +53 -0
- gltest_cli/config/pytest_context.py +9 -0
- gltest_cli/config/types.py +41 -0
- gltest_cli/config/user.py +21 -8
- tests/examples/contracts/football_prediction_market.py +1 -1
- tests/examples/tests/test_football_prediction_market.py +2 -2
- tests/examples/tests/test_intelligent_oracle_factory.py +6 -6
- tests/examples/tests/test_llm_erc20.py +5 -5
- tests/examples/tests/test_llm_erc20_analyze.py +50 -0
- tests/examples/tests/test_log_indexer.py +23 -11
- tests/examples/tests/test_multi_file_contract.py +2 -2
- tests/examples/tests/test_multi_file_contract_legacy.py +2 -2
- tests/examples/tests/test_multi_read_erc20.py +14 -12
- tests/examples/tests/test_multi_tenant_storage.py +11 -7
- tests/examples/tests/test_read_erc20.py +1 -1
- tests/examples/tests/test_storage.py +4 -4
- tests/examples/tests/test_storage_legacy.py +5 -3
- tests/examples/tests/test_user_storage.py +20 -10
- tests/examples/tests/test_wizard_of_coin.py +1 -1
- tests/gltest_cli/config/test_config_integration.py +432 -0
- tests/gltest_cli/config/test_general_config.py +406 -0
- tests/gltest_cli/config/test_plugin.py +167 -0
- tests/gltest_cli/config/test_user.py +61 -1
- genlayer_test-0.4.1.dist-info/RECORD +0 -67
- gltest/glchain/__init__.py +0 -16
- {genlayer_test-0.4.1.dist-info → genlayer_test-2.0.0.dist-info}/WHEEL +0 -0
- {genlayer_test-0.4.1.dist-info → genlayer_test-2.0.0.dist-info}/entry_points.txt +0 -0
- {genlayer_test-0.4.1.dist-info → genlayer_test-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {genlayer_test-0.4.1.dist-info → genlayer_test-2.0.0.dist-info}/top_level.txt +0 -0
- /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.
|
188
|
-
2.
|
189
|
-
3.
|
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,
|
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,
|
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)
|