genlayer-test 0.1.3__py3-none-any.whl → 0.3.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 (43) hide show
  1. {genlayer_test-0.1.3.dist-info → genlayer_test-0.3.0.dist-info}/METADATA +77 -10
  2. genlayer_test-0.3.0.dist-info/RECORD +65 -0
  3. {genlayer_test-0.1.3.dist-info → genlayer_test-0.3.0.dist-info}/entry_points.txt +1 -1
  4. gltest/__init__.py +4 -4
  5. gltest/artifacts/__init__.py +5 -2
  6. gltest/artifacts/contract.py +94 -14
  7. gltest/glchain/__init__.py +3 -3
  8. gltest/glchain/account.py +15 -11
  9. gltest/glchain/client.py +39 -3
  10. gltest/glchain/contract.py +98 -31
  11. gltest/helpers/fixture_snapshot.py +3 -2
  12. gltest_cli/config/__init__.py +0 -0
  13. gltest_cli/config/constants.py +10 -0
  14. gltest_cli/config/general.py +10 -0
  15. gltest_cli/config/plugin.py +102 -0
  16. gltest_cli/config/types.py +137 -0
  17. gltest_cli/config/user.py +222 -0
  18. gltest_cli/logging.py +51 -0
  19. tests/__init__.py +0 -0
  20. tests/examples/tests/test_llm_erc20.py +2 -2
  21. tests/examples/tests/test_multi_read_erc20.py +13 -3
  22. tests/examples/tests/test_multi_tenant_storage.py +12 -3
  23. tests/examples/tests/test_read_erc20.py +2 -2
  24. tests/examples/tests/test_storage.py +4 -2
  25. tests/examples/tests/test_user_storage.py +17 -3
  26. tests/gltest/__init__.py +0 -0
  27. tests/gltest/artifact/__init__.py +0 -0
  28. tests/gltest/artifact/contracts/duplicate_ic_contract_1.py +22 -0
  29. tests/gltest/artifact/contracts/duplicate_ic_contract_2.py +22 -0
  30. tests/{artifact → gltest/artifact}/test_contract_definition.py +29 -30
  31. tests/gltest_cli/__init__.py +0 -0
  32. tests/gltest_cli/config/test_plugin.py +127 -0
  33. tests/gltest_cli/config/test_user.py +351 -0
  34. genlayer_test-0.1.3.dist-info/RECORD +0 -53
  35. gltest/plugin_config.py +0 -42
  36. gltest/plugin_hooks.py +0 -51
  37. tests/plugin/test_plugin_hooks.py +0 -78
  38. {genlayer_test-0.1.3.dist-info → genlayer_test-0.3.0.dist-info}/WHEEL +0 -0
  39. {genlayer_test-0.1.3.dist-info → genlayer_test-0.3.0.dist-info}/licenses/LICENSE +0 -0
  40. {genlayer_test-0.1.3.dist-info → genlayer_test-0.3.0.dist-info}/top_level.txt +0 -0
  41. /tests/{plugin/conftest.py → conftest.py} +0 -0
  42. /tests/{artifact → gltest/artifact}/contracts/not_ic_contract.py +0 -0
  43. /tests/{assertions → gltest/assertions}/test_assertions.py +0 -0
@@ -4,15 +4,20 @@ from eth_typing import (
4
4
  )
5
5
  from eth_account.signers.local import LocalAccount
6
6
  from typing import Union
7
+ from pathlib import Path
7
8
  from dataclasses import dataclass
8
- from gltest.artifacts import find_contract_definition
9
+ from gltest.artifacts import (
10
+ find_contract_definition_from_name,
11
+ find_contract_definition_from_path,
12
+ )
9
13
  from gltest.assertions import tx_execution_failed
10
14
  from gltest.exceptions import DeploymentError
11
- from .client import get_gl_client
15
+ from .client import get_gl_client, get_gl_hosted_studio_client, get_local_client
12
16
  from gltest.types import CalldataEncodable, GenLayerTransaction, TransactionStatus
13
17
  from typing import List, Any, Type, Optional, Dict, Callable
14
18
  import types
15
- from gltest.plugin_config import get_default_wait_interval, get_default_wait_retries
19
+ from gltest_cli.config.general import get_general_config
20
+ from gltest_cli.logging import logger
16
21
 
17
22
 
18
23
  @dataclass
@@ -97,15 +102,16 @@ class Contract:
97
102
  wait_interval: Optional[int] = None,
98
103
  wait_retries: Optional[int] = None,
99
104
  wait_triggered_transactions: bool = True,
100
- wait_triggered_transactions_status: TransactionStatus = TransactionStatus.ACCEPTED,
105
+ wait_triggered_transactions_status: TransactionStatus = TransactionStatus.FINALIZED,
101
106
  ) -> GenLayerTransaction:
102
107
  """
103
108
  Wrapper to the contract write method.
104
109
  """
110
+ general_config = get_general_config()
105
111
  if wait_interval is None:
106
- wait_interval = get_default_wait_interval()
112
+ wait_interval = general_config.get_default_wait_interval()
107
113
  if wait_retries is None:
108
- wait_retries = get_default_wait_retries()
114
+ wait_retries = general_config.get_default_wait_retries()
109
115
  client = get_gl_client()
110
116
  tx_hash = client.write_contract(
111
117
  address=self.address,
@@ -146,13 +152,13 @@ class ContractFactory:
146
152
  contract_code: str
147
153
 
148
154
  @classmethod
149
- def from_artifact(
155
+ def from_name(
150
156
  cls: Type["ContractFactory"], contract_name: str
151
157
  ) -> "ContractFactory":
152
158
  """
153
159
  Create a ContractFactory instance given the contract name.
154
160
  """
155
- contract_info = find_contract_definition(contract_name)
161
+ contract_info = find_contract_definition_from_name(contract_name)
156
162
  if contract_info is None:
157
163
  raise ValueError(
158
164
  f"Contract {contract_name} not found in the contracts directory"
@@ -161,6 +167,44 @@ class ContractFactory:
161
167
  contract_name=contract_name, contract_code=contract_info.contract_code
162
168
  )
163
169
 
170
+ @classmethod
171
+ def from_file_path(
172
+ cls: Type["ContractFactory"], contract_file_path: Union[str, Path]
173
+ ) -> "ContractFactory":
174
+ """
175
+ Create a ContractFactory instance given the contract file path.
176
+ """
177
+ contract_info = find_contract_definition_from_path(contract_file_path)
178
+ return cls(
179
+ contract_name=contract_info.contract_name,
180
+ contract_code=contract_info.contract_code,
181
+ )
182
+
183
+ def _get_schema_with_fallback(self):
184
+ """Attempts to get the contract schema using multiple clients in a fallback pattern.
185
+
186
+ 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
190
+
191
+ Returns:
192
+ Optional[Dict[str, Any]]: The contract schema if successful, None if all attempts fail.
193
+ """
194
+ clients = (
195
+ ("hosted studio", get_gl_hosted_studio_client()),
196
+ ("local", get_local_client()),
197
+ ("default", get_gl_client()),
198
+ )
199
+ for label, client in clients:
200
+ try:
201
+ return client.get_contract_schema_for_code(
202
+ contract_code=self.contract_code
203
+ )
204
+ except Exception as e:
205
+ logger.warning("Schema fetch via %s client failed: %s", label, e)
206
+ return None
207
+
164
208
  def build_contract(
165
209
  self,
166
210
  contract_address: Union[Address, ChecksumAddress],
@@ -169,16 +213,13 @@ class ContractFactory:
169
213
  """
170
214
  Build contract from address
171
215
  """
172
- client = get_gl_client()
173
- try:
174
- schema = client.get_contract_schema(address=contract_address)
175
- return Contract.new(
176
- address=contract_address, schema=schema, account=account
177
- )
178
- except Exception as e:
216
+ schema = self._get_schema_with_fallback()
217
+ if schema is None:
179
218
  raise ValueError(
180
- f"Failed to build contract {self.contract_name}: {str(e)}"
181
- ) from e
219
+ "Failed to get schema from all clients (hosted studio, local, and regular)"
220
+ )
221
+
222
+ return Contract.new(address=contract_address, schema=schema, account=account)
182
223
 
183
224
  def deploy(
184
225
  self,
@@ -193,10 +234,12 @@ class ContractFactory:
193
234
  """
194
235
  Deploy the contract
195
236
  """
237
+ general_config = get_general_config()
196
238
  if wait_interval is None:
197
- wait_interval = get_default_wait_interval()
239
+ wait_interval = general_config.get_default_wait_interval()
198
240
  if wait_retries is None:
199
- wait_retries = get_default_wait_retries()
241
+ wait_retries = general_config.get_default_wait_retries()
242
+
200
243
  client = get_gl_client()
201
244
  try:
202
245
  tx_hash = client.deploy_contract(
@@ -212,22 +255,27 @@ class ContractFactory:
212
255
  interval=wait_interval,
213
256
  retries=wait_retries,
214
257
  )
215
- if (
216
- not tx_receipt
217
- or "data" not in tx_receipt
218
- or "contract_address" not in tx_receipt["data"]
219
- ):
258
+ if tx_execution_failed(tx_receipt):
220
259
  raise ValueError(
221
- "Invalid transaction receipt: missing contract address"
260
+ f"Deployment transaction finalized with error: {tx_receipt}"
222
261
  )
223
262
 
224
- if tx_execution_failed(tx_receipt):
263
+ if (
264
+ "tx_data_decoded" in tx_receipt
265
+ and "contract_address" in tx_receipt["tx_data_decoded"]
266
+ ):
267
+ contract_address = tx_receipt["tx_data_decoded"]["contract_address"]
268
+ elif "data" in tx_receipt and "contract_address" in tx_receipt["data"]:
269
+ contract_address = tx_receipt["data"]["contract_address"]
270
+ else:
271
+ raise ValueError("Transaction receipt missing contract address")
272
+
273
+ schema = self._get_schema_with_fallback()
274
+ if schema is None:
225
275
  raise ValueError(
226
- f"Deployment transaction finalized with error: {tx_receipt}"
276
+ "Failed to get schema from all clients (hosted studio, local, and regular)"
227
277
  )
228
278
 
229
- contract_address = tx_receipt["data"]["contract_address"]
230
- schema = client.get_contract_schema(address=contract_address)
231
279
  return Contract.new(
232
280
  address=contract_address, schema=schema, account=account
233
281
  )
@@ -237,8 +285,27 @@ class ContractFactory:
237
285
  ) from e
238
286
 
239
287
 
240
- def get_contract_factory(contract_name: str) -> ContractFactory:
288
+ def get_contract_factory(
289
+ contract_name: Optional[str] = None,
290
+ contract_file_path: Optional[Union[str, Path]] = None,
291
+ ) -> ContractFactory:
241
292
  """
242
293
  Get a ContractFactory instance for a contract.
294
+
295
+ Args:
296
+ contract_name: Name of the contract to load from artifacts
297
+ contract_file_path: Path to the contract file to load directly
298
+
299
+ Note: Exactly one of contract_name or contract_file_path must be provided.
243
300
  """
244
- return ContractFactory.from_artifact(contract_name)
301
+ if contract_name is not None and contract_file_path is not None:
302
+ raise ValueError(
303
+ "Only one of contract_name or contract_file_path should be provided"
304
+ )
305
+
306
+ if contract_name is None and contract_file_path is None:
307
+ raise ValueError("Either contract_name or contract_file_path must be provided")
308
+
309
+ if contract_name is not None:
310
+ return ContractFactory.from_name(contract_name)
311
+ return ContractFactory.from_file_path(contract_file_path)
@@ -7,7 +7,7 @@ from gltest.exceptions import (
7
7
  InvalidSnapshotError,
8
8
  FixtureAnonymousFunctionError,
9
9
  )
10
- from gltest.plugin_config import get_rpc_url
10
+ from gltest_cli.config.general import get_general_config
11
11
 
12
12
  SUPPORTED_RPC_DOMAINS = ["localhost", "127.0.0.1"]
13
13
 
@@ -34,7 +34,8 @@ def load_fixture(fixture: Callable[[], T]) -> T:
34
34
  if fixture.__name__ == "<lambda>":
35
35
  raise FixtureAnonymousFunctionError("Fixtures must be named functions")
36
36
 
37
- rpc_url = get_rpc_url()
37
+ general_config = get_general_config()
38
+ rpc_url = general_config.get_rpc_url()
38
39
  domain = urlparse(rpc_url).netloc.split(":")[0] # Extract domain without port
39
40
  if domain not in SUPPORTED_RPC_DOMAINS:
40
41
  return fixture()
File without changes
@@ -0,0 +1,10 @@
1
+ from genlayer_py.chains.localnet import SIMULATOR_JSON_RPC_URL
2
+ from pathlib import Path
3
+
4
+
5
+ GLTEST_CONFIG_FILE = "gltest.config.yaml"
6
+ DEFAULT_NETWORK = "localnet"
7
+ DEFAULT_RPC_URL = SIMULATOR_JSON_RPC_URL
8
+ DEFAULT_ENVIRONMENT = ".env"
9
+ DEFAULT_CONTRACTS_DIR = Path("contracts")
10
+ DEFAULT_NETWORK_ID = 61999
@@ -0,0 +1,10 @@
1
+ from gltest_cli.config.types import GeneralConfig
2
+
3
+
4
+ _general_config = GeneralConfig()
5
+
6
+
7
+ def get_general_config() -> GeneralConfig:
8
+ global _general_config
9
+
10
+ return _general_config
@@ -0,0 +1,102 @@
1
+ from pathlib import Path
2
+ from gltest_cli.logging import logger
3
+ from gltest_cli.config.user import (
4
+ user_config_exists,
5
+ load_user_config,
6
+ get_default_user_config,
7
+ )
8
+ from gltest_cli.config.general import (
9
+ get_general_config,
10
+ )
11
+ from gltest_cli.config.types import PluginConfig
12
+
13
+
14
+ def pytest_addoption(parser):
15
+ group = parser.getgroup("gltest")
16
+ group.addoption(
17
+ "--contracts-dir",
18
+ action="store",
19
+ default=None,
20
+ help="Path to directory containing contract files",
21
+ )
22
+
23
+ group.addoption(
24
+ "--default-wait-interval",
25
+ action="store",
26
+ default=10000,
27
+ help="Default interval (ms) between transaction receipt checks",
28
+ )
29
+
30
+ group.addoption(
31
+ "--default-wait-retries",
32
+ action="store",
33
+ default=15,
34
+ help="Default number of retries for transaction receipt checks",
35
+ )
36
+
37
+ group.addoption(
38
+ "--rpc-url",
39
+ action="store",
40
+ default=None,
41
+ help="RPC endpoint URL for the GenLayer network",
42
+ )
43
+
44
+ group.addoption(
45
+ "--network",
46
+ action="store",
47
+ default=None,
48
+ help="Target network (defaults to 'localnet' if no config file)",
49
+ )
50
+
51
+
52
+ def pytest_configure(config):
53
+ general_config = get_general_config()
54
+
55
+ # Handle user config from gltest.config.yaml
56
+ if not user_config_exists():
57
+ logger.warning(
58
+ "File `gltest.config.yaml` not found in the current directory, using default config"
59
+ )
60
+ logger.info("Create a `gltest.config.yaml` file to manage multiple networks")
61
+ user_config = get_default_user_config()
62
+ else:
63
+ logger.info(
64
+ "File `gltest.config.yaml` found in the current directory, using it"
65
+ )
66
+ user_config = load_user_config("gltest.config.yaml")
67
+
68
+ general_config.user_config = user_config
69
+
70
+ # Handle plugin config from command line
71
+ contracts_dir = config.getoption("--contracts-dir")
72
+ default_wait_interval = config.getoption("--default-wait-interval")
73
+ default_wait_retries = config.getoption("--default-wait-retries")
74
+ rpc_url = config.getoption("--rpc-url")
75
+ network = config.getoption("--network")
76
+
77
+ plugin_config = PluginConfig()
78
+ plugin_config.contracts_dir = (
79
+ Path(contracts_dir) if contracts_dir is not None else None
80
+ )
81
+ plugin_config.default_wait_interval = int(default_wait_interval)
82
+ plugin_config.default_wait_retries = int(default_wait_retries)
83
+ plugin_config.rpc_url = rpc_url
84
+ plugin_config.network_name = network
85
+
86
+ general_config.plugin_config = plugin_config
87
+
88
+
89
+ def pytest_sessionstart(session):
90
+ general_config = get_general_config()
91
+ logger.info("Using the following configuration:")
92
+ logger.info(f" RPC URL: {general_config.get_rpc_url()}")
93
+ logger.info(f" Selected Network: {general_config.get_network_name()}")
94
+ logger.info(
95
+ f" Available networks: {list(general_config.user_config.networks.keys())}"
96
+ )
97
+ logger.info(f" Contracts directory: {general_config.get_contracts_dir()}")
98
+ logger.info(f" Environment: {general_config.user_config.environment}")
99
+ logger.info(
100
+ f" Default wait interval: {general_config.get_default_wait_interval()} ms"
101
+ )
102
+ logger.info(f" Default wait retries: {general_config.get_default_wait_retries()}")
@@ -0,0 +1,137 @@
1
+ from enum import Enum
2
+ from dataclasses import dataclass, field
3
+ from pathlib import Path
4
+ from typing import Dict, List, Optional
5
+ from genlayer_py.chains import localnet, testnet_asimov
6
+ from genlayer_py.types import GenLayerChain
7
+
8
+
9
+ class NetworkConfig(str, Enum):
10
+ LOCALNET = "localnet"
11
+ TESTNET_ASIMOV = "testnet_asimov"
12
+
13
+
14
+ @dataclass
15
+ class PluginConfig:
16
+ contracts_dir: Optional[Path] = None
17
+ rpc_url: Optional[str] = None
18
+ default_wait_interval: Optional[int] = None
19
+ default_wait_retries: Optional[int] = None
20
+ network_name: Optional[str] = None
21
+
22
+
23
+ @dataclass
24
+ class NetworkConfigData:
25
+ id: Optional[int] = None
26
+ url: Optional[str] = None
27
+ accounts: Optional[List[str]] = None
28
+ from_account: Optional[str] = None
29
+
30
+ def __post_init__(self):
31
+ if self.id is not None and not isinstance(self.id, int):
32
+ raise ValueError("id must be an integer")
33
+ if self.url is not None and not isinstance(self.url, str):
34
+ raise ValueError("url must be a string")
35
+ if self.accounts is not None:
36
+ if not isinstance(self.accounts, list):
37
+ raise ValueError("accounts must be a list")
38
+ if not all(isinstance(acc, str) for acc in self.accounts):
39
+ raise ValueError("accounts must be strings")
40
+ if self.from_account is not None and not isinstance(self.from_account, str):
41
+ raise ValueError("from_account must be a string")
42
+
43
+
44
+ @dataclass
45
+ class PathConfig:
46
+ contracts: Optional[Path] = None
47
+
48
+ def __post_init__(self):
49
+ if self.contracts is not None and not isinstance(self.contracts, (str, Path)):
50
+ raise ValueError("contracts must be a string or Path")
51
+
52
+
53
+ @dataclass
54
+ class UserConfig:
55
+ networks: Dict[str, NetworkConfigData] = field(default_factory=dict)
56
+ paths: PathConfig = field(default_factory=PathConfig)
57
+ environment: Optional[str] = None
58
+ default_network: Optional[str] = None
59
+
60
+ def __post_init__(self):
61
+ if not isinstance(self.networks, dict):
62
+ raise ValueError("networks must be a dictionary")
63
+
64
+ if not isinstance(self.paths, PathConfig):
65
+ raise ValueError("paths must be a PathConfig instance")
66
+
67
+ if self.environment is not None and not isinstance(self.environment, str):
68
+ raise ValueError("environment must be a string")
69
+
70
+ if self.default_network is not None and not isinstance(
71
+ self.default_network, str
72
+ ):
73
+ raise ValueError("default_network must be a string")
74
+
75
+ # Validate network configurations
76
+ for name, network_config in self.networks.items():
77
+ if not isinstance(network_config, NetworkConfigData):
78
+ raise ValueError(f"network {name} must be a NetworkConfigData instance")
79
+
80
+
81
+ @dataclass
82
+ class GeneralConfig:
83
+ user_config: UserConfig = field(default_factory=UserConfig)
84
+ plugin_config: PluginConfig = field(default_factory=PluginConfig)
85
+
86
+ def get_contracts_dir(self) -> Path:
87
+ if self.plugin_config.contracts_dir is not None:
88
+ return self.plugin_config.contracts_dir
89
+ return self.user_config.paths.contracts
90
+
91
+ def set_contracts_dir(self, contracts_dir: Path):
92
+ self.plugin_config.contracts_dir = contracts_dir
93
+
94
+ def get_rpc_url(self) -> str:
95
+ if self.plugin_config.rpc_url is not None:
96
+ return self.plugin_config.rpc_url
97
+ network_name = self.get_network_name()
98
+ return self.user_config.networks[network_name].url
99
+
100
+ def get_default_account_key(self, network_name: Optional[str] = None) -> str:
101
+ if network_name is not None:
102
+ return self.user_config.networks[network_name].from_account
103
+ return self.user_config.networks[self.user_config.default_network].from_account
104
+
105
+ def get_accounts_keys(self, network_name: Optional[str] = None) -> List[str]:
106
+ if network_name is not None:
107
+ return self.user_config.networks[network_name].accounts
108
+ return self.user_config.networks[self.user_config.default_network].accounts
109
+
110
+ def get_chain(self) -> GenLayerChain:
111
+ chain_map_by_id = {
112
+ 61999: localnet,
113
+ 4221: testnet_asimov,
114
+ }
115
+ network_name = self.get_network_name()
116
+ network_id = self.user_config.networks[network_name].id
117
+ if network_id not in chain_map_by_id:
118
+ known = ", ".join(map(str, chain_map_by_id.keys()))
119
+ raise ValueError(
120
+ f"Unknown network: {network_name}, possible values: {known}"
121
+ )
122
+ return chain_map_by_id[network_id]
123
+
124
+ def get_default_wait_interval(self) -> int:
125
+ if self.plugin_config.default_wait_interval is not None:
126
+ return self.plugin_config.default_wait_interval
127
+ raise ValueError("default_wait_interval is not set")
128
+
129
+ def get_default_wait_retries(self) -> int:
130
+ if self.plugin_config.default_wait_retries is not None:
131
+ return self.plugin_config.default_wait_retries
132
+ raise ValueError("default_wait_retries is not set")
133
+
134
+ def get_network_name(self) -> str:
135
+ if self.plugin_config.network_name is not None:
136
+ return self.plugin_config.network_name
137
+ return self.user_config.default_network