genlayer-test 0.4.1__py3-none-any.whl → 1.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-1.0.0.dist-info}/METADATA +54 -8
- genlayer_test-1.0.0.dist-info/RECORD +75 -0
- gltest/__init__.py +7 -6
- gltest/{glchain/client.py → clients.py} +1 -1
- gltest/contracts/__init__.py +4 -0
- gltest/contracts/contract.py +193 -0
- gltest/{glchain/contract.py → contracts/contract_factory.py} +17 -135
- gltest/contracts/contract_functions.py +61 -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 +37 -0
- gltest_cli/config/pytest_context.py +9 -0
- gltest_cli/config/types.py +16 -0
- gltest_cli/config/user.py +9 -6
- 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_general_config.py +149 -0
- tests/gltest_cli/config/test_plugin.py +78 -0
- tests/gltest_cli/config/test_user.py +51 -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-1.0.0.dist-info}/WHEEL +0 -0
- {genlayer_test-0.4.1.dist-info → genlayer_test-1.0.0.dist-info}/entry_points.txt +0 -0
- {genlayer_test-0.4.1.dist-info → genlayer_test-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {genlayer_test-0.4.1.dist-info → genlayer_test-1.0.0.dist-info}/top_level.txt +0 -0
- /gltest/{glchain/account.py → accounts.py} +0 -0
@@ -0,0 +1,61 @@
|
|
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
|
+
leader_only: bool = False,
|
24
|
+
wait_transaction_status: TransactionStatus = TransactionStatus.FINALIZED,
|
25
|
+
wait_interval: Optional[int] = None,
|
26
|
+
wait_retries: Optional[int] = None,
|
27
|
+
wait_triggered_transactions: bool = True,
|
28
|
+
wait_triggered_transactions_status: TransactionStatus = TransactionStatus.FINALIZED,
|
29
|
+
):
|
30
|
+
if self.read_only:
|
31
|
+
raise ValueError("Cannot transact read-only method")
|
32
|
+
return self.transact_method(
|
33
|
+
value=value,
|
34
|
+
consensus_max_rotations=consensus_max_rotations,
|
35
|
+
leader_only=leader_only,
|
36
|
+
wait_transaction_status=wait_transaction_status,
|
37
|
+
wait_interval=wait_interval,
|
38
|
+
wait_retries=wait_retries,
|
39
|
+
wait_triggered_transactions=wait_triggered_transactions,
|
40
|
+
wait_triggered_transactions_status=wait_triggered_transactions_status,
|
41
|
+
)
|
42
|
+
|
43
|
+
def analyze(
|
44
|
+
self,
|
45
|
+
provider: str,
|
46
|
+
model: str,
|
47
|
+
config: Optional[Dict[str, Any]] = None,
|
48
|
+
plugin: Optional[str] = None,
|
49
|
+
plugin_config: Optional[Dict[str, Any]] = None,
|
50
|
+
runs: int = 100,
|
51
|
+
):
|
52
|
+
if self.read_only:
|
53
|
+
raise ValueError("Cannot analyze read-only method")
|
54
|
+
return self.analyze_method(
|
55
|
+
provider=provider,
|
56
|
+
model=model,
|
57
|
+
config=config,
|
58
|
+
plugin=plugin,
|
59
|
+
plugin_config=plugin_config,
|
60
|
+
runs=runs,
|
61
|
+
)
|
@@ -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)
|
gltest/fixtures.py
CHANGED
@@ -4,12 +4,8 @@ These fixtures can be imported and used in test files.
|
|
4
4
|
"""
|
5
5
|
|
6
6
|
import pytest
|
7
|
-
from gltest.
|
8
|
-
|
9
|
-
get_accounts,
|
10
|
-
get_default_account,
|
11
|
-
get_gl_provider,
|
12
|
-
)
|
7
|
+
from gltest.clients import get_gl_client, get_gl_provider
|
8
|
+
from gltest.accounts import get_accounts, get_default_account
|
13
9
|
from gltest_cli.config.general import get_general_config
|
14
10
|
|
15
11
|
|
gltest/helpers/take_snapshot.py
CHANGED
gltest_cli/config/constants.py
CHANGED
gltest_cli/config/plugin.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
from pathlib import Path
|
2
|
+
import shutil
|
2
3
|
from gltest_cli.logging import logger
|
3
4
|
from gltest_cli.config.user import (
|
4
5
|
user_config_exists,
|
@@ -9,6 +10,7 @@ from gltest_cli.config.general import (
|
|
9
10
|
get_general_config,
|
10
11
|
)
|
11
12
|
from gltest_cli.config.types import PluginConfig
|
13
|
+
from gltest_cli.config.pytest_context import _pytest_context
|
12
14
|
|
13
15
|
|
14
16
|
def pytest_addoption(parser):
|
@@ -20,6 +22,13 @@ def pytest_addoption(parser):
|
|
20
22
|
help="Path to directory containing contract files",
|
21
23
|
)
|
22
24
|
|
25
|
+
group.addoption(
|
26
|
+
"--artifacts-dir",
|
27
|
+
action="store",
|
28
|
+
default=None,
|
29
|
+
help="Path to directory for storing contract artifacts",
|
30
|
+
)
|
31
|
+
|
23
32
|
group.addoption(
|
24
33
|
"--default-wait-interval",
|
25
34
|
action="store",
|
@@ -76,6 +85,7 @@ def pytest_configure(config):
|
|
76
85
|
|
77
86
|
# Handle plugin config from command line
|
78
87
|
contracts_dir = config.getoption("--contracts-dir")
|
88
|
+
artifacts_dir = config.getoption("--artifacts-dir")
|
79
89
|
default_wait_interval = config.getoption("--default-wait-interval")
|
80
90
|
default_wait_retries = config.getoption("--default-wait-retries")
|
81
91
|
rpc_url = config.getoption("--rpc-url")
|
@@ -86,6 +96,9 @@ def pytest_configure(config):
|
|
86
96
|
plugin_config.contracts_dir = (
|
87
97
|
Path(contracts_dir) if contracts_dir is not None else None
|
88
98
|
)
|
99
|
+
plugin_config.artifacts_dir = (
|
100
|
+
Path(artifacts_dir) if artifacts_dir is not None else None
|
101
|
+
)
|
89
102
|
plugin_config.default_wait_interval = int(default_wait_interval)
|
90
103
|
plugin_config.default_wait_retries = int(default_wait_retries)
|
91
104
|
plugin_config.rpc_url = rpc_url
|
@@ -97,6 +110,18 @@ def pytest_configure(config):
|
|
97
110
|
|
98
111
|
def pytest_sessionstart(session):
|
99
112
|
general_config = get_general_config()
|
113
|
+
|
114
|
+
artifacts_dir = general_config.get_artifacts_dir()
|
115
|
+
if artifacts_dir and artifacts_dir.exists():
|
116
|
+
logger.info(f"Clearing artifacts directory: {artifacts_dir}")
|
117
|
+
try:
|
118
|
+
shutil.rmtree(artifacts_dir)
|
119
|
+
artifacts_dir.mkdir(parents=True, exist_ok=True)
|
120
|
+
except Exception as e:
|
121
|
+
logger.warning(f"Failed to clear artifacts directory: {e}")
|
122
|
+
elif artifacts_dir:
|
123
|
+
artifacts_dir.mkdir(parents=True, exist_ok=True)
|
124
|
+
|
100
125
|
logger.info("Using the following configuration:")
|
101
126
|
logger.info(f" RPC URL: {general_config.get_rpc_url()}")
|
102
127
|
logger.info(f" Selected Network: {general_config.get_network_name()}")
|
@@ -104,6 +129,7 @@ def pytest_sessionstart(session):
|
|
104
129
|
f" Available networks: {list(general_config.user_config.networks.keys())}"
|
105
130
|
)
|
106
131
|
logger.info(f" Contracts directory: {general_config.get_contracts_dir()}")
|
132
|
+
logger.info(f" Artifacts directory: {general_config.get_artifacts_dir()}")
|
107
133
|
logger.info(f" Environment: {general_config.user_config.environment}")
|
108
134
|
logger.info(
|
109
135
|
f" Default wait interval: {general_config.get_default_wait_interval()} ms"
|
@@ -112,4 +138,15 @@ def pytest_sessionstart(session):
|
|
112
138
|
logger.info(f" Test with mocks: {general_config.get_test_with_mocks()}")
|
113
139
|
|
114
140
|
|
141
|
+
def pytest_runtest_setup(item):
|
142
|
+
_pytest_context.current_item = item
|
143
|
+
|
144
|
+
|
145
|
+
def pytest_runtest_teardown(item):
|
146
|
+
try:
|
147
|
+
del _pytest_context.current_item
|
148
|
+
except AttributeError:
|
149
|
+
pass
|
150
|
+
|
151
|
+
|
115
152
|
pytest_plugins = ["gltest.fixtures"]
|
gltest_cli/config/types.py
CHANGED
@@ -15,6 +15,7 @@ class NetworkConfig(str, Enum):
|
|
15
15
|
@dataclass
|
16
16
|
class PluginConfig:
|
17
17
|
contracts_dir: Optional[Path] = None
|
18
|
+
artifacts_dir: Optional[Path] = None
|
18
19
|
rpc_url: Optional[str] = None
|
19
20
|
default_wait_interval: Optional[int] = None
|
20
21
|
default_wait_retries: Optional[int] = None
|
@@ -46,10 +47,13 @@ class NetworkConfigData:
|
|
46
47
|
@dataclass
|
47
48
|
class PathConfig:
|
48
49
|
contracts: Optional[Path] = None
|
50
|
+
artifacts: Optional[Path] = None
|
49
51
|
|
50
52
|
def __post_init__(self):
|
51
53
|
if self.contracts is not None and not isinstance(self.contracts, (str, Path)):
|
52
54
|
raise ValueError("contracts must be a string or Path")
|
55
|
+
if self.artifacts is not None and not isinstance(self.artifacts, (str, Path)):
|
56
|
+
raise ValueError("artifacts must be a string or Path")
|
53
57
|
|
54
58
|
|
55
59
|
@dataclass
|
@@ -93,6 +97,18 @@ class GeneralConfig:
|
|
93
97
|
def set_contracts_dir(self, contracts_dir: Path):
|
94
98
|
self.plugin_config.contracts_dir = contracts_dir
|
95
99
|
|
100
|
+
def get_artifacts_dir(self) -> Path:
|
101
|
+
if self.plugin_config.artifacts_dir is not None:
|
102
|
+
return self.plugin_config.artifacts_dir
|
103
|
+
return self.user_config.paths.artifacts
|
104
|
+
|
105
|
+
def set_artifacts_dir(self, artifacts_dir: Path):
|
106
|
+
self.plugin_config.artifacts_dir = artifacts_dir
|
107
|
+
|
108
|
+
def get_analysis_dir(self) -> Path:
|
109
|
+
artifacts_dir = self.get_artifacts_dir()
|
110
|
+
return artifacts_dir / "analysis"
|
111
|
+
|
96
112
|
def get_rpc_url(self) -> str:
|
97
113
|
if self.plugin_config.rpc_url is not None:
|
98
114
|
return self.plugin_config.rpc_url
|