genlayer-test 0.4.0__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.
Files changed (44) hide show
  1. {genlayer_test-0.4.0.dist-info → genlayer_test-1.0.0.dist-info}/METADATA +54 -8
  2. genlayer_test-1.0.0.dist-info/RECORD +75 -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 +193 -0
  7. gltest/{glchain/contract.py → contracts/contract_factory.py} +17 -135
  8. gltest/contracts/contract_functions.py +61 -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/logging.py +17 -0
  15. gltest_cli/config/constants.py +1 -0
  16. gltest_cli/config/plugin.py +37 -0
  17. gltest_cli/config/pytest_context.py +9 -0
  18. gltest_cli/config/types.py +16 -0
  19. gltest_cli/config/user.py +9 -6
  20. gltest_cli/logging.py +1 -1
  21. tests/examples/tests/test_football_prediction_market.py +2 -2
  22. tests/examples/tests/test_intelligent_oracle_factory.py +6 -6
  23. tests/examples/tests/test_llm_erc20.py +5 -5
  24. tests/examples/tests/test_llm_erc20_analyze.py +50 -0
  25. tests/examples/tests/test_log_indexer.py +23 -11
  26. tests/examples/tests/test_multi_file_contract.py +2 -2
  27. tests/examples/tests/test_multi_file_contract_legacy.py +2 -2
  28. tests/examples/tests/test_multi_read_erc20.py +14 -12
  29. tests/examples/tests/test_multi_tenant_storage.py +11 -7
  30. tests/examples/tests/test_read_erc20.py +1 -1
  31. tests/examples/tests/test_storage.py +4 -4
  32. tests/examples/tests/test_storage_legacy.py +5 -3
  33. tests/examples/tests/test_user_storage.py +20 -10
  34. tests/examples/tests/test_wizard_of_coin.py +1 -1
  35. tests/gltest_cli/config/test_general_config.py +149 -0
  36. tests/gltest_cli/config/test_plugin.py +78 -0
  37. tests/gltest_cli/config/test_user.py +51 -1
  38. genlayer_test-0.4.0.dist-info/RECORD +0 -66
  39. gltest/glchain/__init__.py +0 -16
  40. {genlayer_test-0.4.0.dist-info → genlayer_test-1.0.0.dist-info}/WHEEL +0 -0
  41. {genlayer_test-0.4.0.dist-info → genlayer_test-1.0.0.dist-info}/entry_points.txt +0 -0
  42. {genlayer_test-0.4.0.dist-info → genlayer_test-1.0.0.dist-info}/licenses/LICENSE +0 -0
  43. {genlayer_test-0.4.0.dist-info → genlayer_test-1.0.0.dist-info}/top_level.txt +0 -0
  44. /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)
@@ -0,0 +1,12 @@
1
+ def safe_filename(filename: str) -> str:
2
+ """
3
+ Replace problematic characters in filename.
4
+ """
5
+ return (
6
+ filename.replace("/", "_")
7
+ .replace("\\", "_")
8
+ .replace(":", "_")
9
+ .replace("-", "_")
10
+ .replace(" ", "_")
11
+ .replace(".py", "")
12
+ )
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.glchain import (
8
- get_gl_client,
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
 
@@ -1,4 +1,4 @@
1
- from gltest.glchain import get_gl_provider
1
+ from gltest.clients import get_gl_provider
2
2
  from dataclasses import dataclass
3
3
  from typing import Callable
4
4
  from gltest.exceptions import HelperError, InvalidSnapshotError
gltest/logging.py ADDED
@@ -0,0 +1,17 @@
1
+ import logging
2
+
3
+
4
+ def setup_logger() -> logging.Logger:
5
+ """Setup a logger for the gltest package - disabled by default"""
6
+
7
+ logger = logging.getLogger("gltest")
8
+
9
+ logger.setLevel(logging.NOTSET)
10
+ logger.disabled = True
11
+ logger.addHandler(logging.NullHandler())
12
+ return logger
13
+
14
+
15
+ logger = setup_logger()
16
+
17
+ __all__ = ["logger"]
@@ -7,4 +7,5 @@ DEFAULT_NETWORK = "localnet"
7
7
  DEFAULT_RPC_URL = SIMULATOR_JSON_RPC_URL
8
8
  DEFAULT_ENVIRONMENT = ".env"
9
9
  DEFAULT_CONTRACTS_DIR = Path("contracts")
10
+ DEFAULT_ARTIFACTS_DIR = Path("artifacts")
10
11
  DEFAULT_NETWORK_ID = 61999
@@ -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"]
@@ -0,0 +1,9 @@
1
+ from typing import Optional
2
+ import threading
3
+
4
+ _pytest_context = threading.local()
5
+
6
+
7
+ def get_current_test_nodeid() -> Optional[str]:
8
+ item = getattr(_pytest_context, "current_item", None)
9
+ return item.nodeid if item is not None else None
@@ -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