genlayer-test 0.11.0__tar.gz → 0.13.0__tar.gz

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 (50) hide show
  1. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/PKG-INFO +67 -1
  2. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/README.md +66 -0
  3. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/genlayer_test.egg-info/PKG-INFO +67 -1
  4. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/genlayer_test.egg-info/SOURCES.txt +7 -0
  5. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/genlayer_test.egg-info/entry_points.txt +1 -0
  6. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/genlayer_test.egg-info/top_level.txt +1 -0
  7. genlayer_test-0.13.0/gltest/direct/__init__.py +31 -0
  8. genlayer_test-0.13.0/gltest/direct/loader.py +288 -0
  9. genlayer_test-0.13.0/gltest/direct/pytest_plugin.py +117 -0
  10. genlayer_test-0.13.0/gltest/direct/sdk_loader.py +260 -0
  11. genlayer_test-0.13.0/gltest/direct/types.py +18 -0
  12. genlayer_test-0.13.0/gltest/direct/vm.py +432 -0
  13. genlayer_test-0.13.0/gltest/direct/wasi_mock.py +258 -0
  14. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/pyproject.toml +2 -1
  15. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/LICENSE +0 -0
  16. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/genlayer_test.egg-info/dependency_links.txt +0 -0
  17. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/genlayer_test.egg-info/requires.txt +0 -0
  18. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/__init__.py +0 -0
  19. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/accounts.py +0 -0
  20. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/artifacts/__init__.py +0 -0
  21. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/artifacts/contract.py +0 -0
  22. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/assertions.py +0 -0
  23. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/clients.py +0 -0
  24. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/contracts/__init__.py +0 -0
  25. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/contracts/contract.py +0 -0
  26. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/contracts/contract_factory.py +0 -0
  27. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/contracts/contract_functions.py +0 -0
  28. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/contracts/method_stats.py +0 -0
  29. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/contracts/stats_collector.py +0 -0
  30. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/contracts/utils.py +0 -0
  31. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/exceptions.py +0 -0
  32. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/fixtures.py +0 -0
  33. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/helpers/__init__.py +0 -0
  34. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/helpers/fixture_snapshot.py +0 -0
  35. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/helpers/take_snapshot.py +0 -0
  36. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/logging.py +0 -0
  37. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/types.py +0 -0
  38. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/utils.py +0 -0
  39. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/validators/__init__.py +0 -0
  40. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest/validators/validator_factory.py +0 -0
  41. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest_cli/config/__init__.py +0 -0
  42. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest_cli/config/constants.py +0 -0
  43. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest_cli/config/general.py +0 -0
  44. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest_cli/config/plugin.py +0 -0
  45. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest_cli/config/pytest_context.py +0 -0
  46. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest_cli/config/types.py +0 -0
  47. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest_cli/config/user.py +0 -0
  48. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest_cli/logging.py +0 -0
  49. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/gltest_cli/main.py +0 -0
  50. {genlayer_test-0.11.0 → genlayer_test-0.13.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: genlayer-test
3
- Version: 0.11.0
3
+ Version: 0.13.0
4
4
  Summary: GenLayer Testing Suite
5
5
  Author: GenLayer
6
6
  License-Expression: MIT
@@ -312,6 +312,72 @@ The chain type determines various behaviors including RPC endpoints, consensus m
312
312
  - **Prompt Testing & Statistical Analysis** – Evaluate and statistically test prompts for AI-driven contract execution.
313
313
  - **Scalability to Security & Audit Tools** – Designed to extend into security testing and smart contract auditing.
314
314
  - **Custom Transaction Context** – Set custom validators with specific LLM providers and models, and configure GenVM datetime for deterministic testing scenarios.
315
+ - **Direct Execution Mode** – Run contracts directly in Python for ultra-fast unit testing (~ms vs minutes).
316
+
317
+ ## ⚡ Direct vs Simulator Mode
318
+
319
+ The testing suite provides two execution modes:
320
+
321
+ | Mode | How it works | Speed | Use case |
322
+ |------|--------------|-------|----------|
323
+ | **Simulator** | Deploy to GenLayer simulator, interact via RPC | ~minutes | Integration tests, consensus validation |
324
+ | **Direct** | Run Python code directly in-memory | ~milliseconds | Unit tests, rapid development |
325
+
326
+ ### Quick Start with Direct Mode
327
+
328
+ ```python
329
+ def test_token_transfer(direct_vm, direct_deploy):
330
+ # Deploy contract directly in Python (no simulator)
331
+ token = direct_deploy("contracts/Token.py", initial_supply=1000)
332
+
333
+ # Create test addresses
334
+ from gltest.direct import create_address
335
+ alice = create_address("alice")
336
+ bob = create_address("bob")
337
+
338
+ # Set sender and interact
339
+ direct_vm.sender = alice
340
+ token.mint(alice, 500)
341
+ token.transfer(bob, 100)
342
+
343
+ assert token.balances[bob] == 100
344
+ ```
345
+
346
+ ### Available Fixtures
347
+
348
+ | Fixture | Description |
349
+ |---------|-------------|
350
+ | `direct_vm` | VM context with cheatcodes |
351
+ | `direct_deploy` | Deploy contracts directly |
352
+ | `direct_alice`, `direct_bob`, `direct_charlie` | Test addresses |
353
+ | `direct_owner` | Default sender address |
354
+ | `direct_accounts` | List of 10 test addresses |
355
+
356
+ ### Cheatcodes
357
+
358
+ ```python
359
+ # Change sender
360
+ direct_vm.sender = alice
361
+
362
+ # Prank (temporary sender change)
363
+ with direct_vm.prank(bob):
364
+ contract.method() # Called as bob
365
+
366
+ # Snapshots
367
+ snap_id = direct_vm.snapshot()
368
+ contract.modify_state()
369
+ direct_vm.revert(snap_id) # State restored
370
+
371
+ # Expect revert
372
+ with direct_vm.expect_revert("Insufficient balance"):
373
+ contract.transfer(bob, 1000000)
374
+
375
+ # Mock web/LLM (for nondet operations)
376
+ direct_vm.mock_web(r"api\.example\.com", {"status": 200, "body": "{}"})
377
+ direct_vm.mock_llm(r"analyze.*", "positive sentiment")
378
+ ```
379
+
380
+ 📖 **[Full Direct Mode Documentation](docs/direct-runner.md)**
315
381
 
316
382
  ## 📚 Examples
317
383
 
@@ -289,6 +289,72 @@ The chain type determines various behaviors including RPC endpoints, consensus m
289
289
  - **Prompt Testing & Statistical Analysis** – Evaluate and statistically test prompts for AI-driven contract execution.
290
290
  - **Scalability to Security & Audit Tools** – Designed to extend into security testing and smart contract auditing.
291
291
  - **Custom Transaction Context** – Set custom validators with specific LLM providers and models, and configure GenVM datetime for deterministic testing scenarios.
292
+ - **Direct Execution Mode** – Run contracts directly in Python for ultra-fast unit testing (~ms vs minutes).
293
+
294
+ ## ⚡ Direct vs Simulator Mode
295
+
296
+ The testing suite provides two execution modes:
297
+
298
+ | Mode | How it works | Speed | Use case |
299
+ |------|--------------|-------|----------|
300
+ | **Simulator** | Deploy to GenLayer simulator, interact via RPC | ~minutes | Integration tests, consensus validation |
301
+ | **Direct** | Run Python code directly in-memory | ~milliseconds | Unit tests, rapid development |
302
+
303
+ ### Quick Start with Direct Mode
304
+
305
+ ```python
306
+ def test_token_transfer(direct_vm, direct_deploy):
307
+ # Deploy contract directly in Python (no simulator)
308
+ token = direct_deploy("contracts/Token.py", initial_supply=1000)
309
+
310
+ # Create test addresses
311
+ from gltest.direct import create_address
312
+ alice = create_address("alice")
313
+ bob = create_address("bob")
314
+
315
+ # Set sender and interact
316
+ direct_vm.sender = alice
317
+ token.mint(alice, 500)
318
+ token.transfer(bob, 100)
319
+
320
+ assert token.balances[bob] == 100
321
+ ```
322
+
323
+ ### Available Fixtures
324
+
325
+ | Fixture | Description |
326
+ |---------|-------------|
327
+ | `direct_vm` | VM context with cheatcodes |
328
+ | `direct_deploy` | Deploy contracts directly |
329
+ | `direct_alice`, `direct_bob`, `direct_charlie` | Test addresses |
330
+ | `direct_owner` | Default sender address |
331
+ | `direct_accounts` | List of 10 test addresses |
332
+
333
+ ### Cheatcodes
334
+
335
+ ```python
336
+ # Change sender
337
+ direct_vm.sender = alice
338
+
339
+ # Prank (temporary sender change)
340
+ with direct_vm.prank(bob):
341
+ contract.method() # Called as bob
342
+
343
+ # Snapshots
344
+ snap_id = direct_vm.snapshot()
345
+ contract.modify_state()
346
+ direct_vm.revert(snap_id) # State restored
347
+
348
+ # Expect revert
349
+ with direct_vm.expect_revert("Insufficient balance"):
350
+ contract.transfer(bob, 1000000)
351
+
352
+ # Mock web/LLM (for nondet operations)
353
+ direct_vm.mock_web(r"api\.example\.com", {"status": 200, "body": "{}"})
354
+ direct_vm.mock_llm(r"analyze.*", "positive sentiment")
355
+ ```
356
+
357
+ 📖 **[Full Direct Mode Documentation](docs/direct-runner.md)**
292
358
 
293
359
  ## 📚 Examples
294
360
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: genlayer-test
3
- Version: 0.11.0
3
+ Version: 0.13.0
4
4
  Summary: GenLayer Testing Suite
5
5
  Author: GenLayer
6
6
  License-Expression: MIT
@@ -312,6 +312,72 @@ The chain type determines various behaviors including RPC endpoints, consensus m
312
312
  - **Prompt Testing & Statistical Analysis** – Evaluate and statistically test prompts for AI-driven contract execution.
313
313
  - **Scalability to Security & Audit Tools** – Designed to extend into security testing and smart contract auditing.
314
314
  - **Custom Transaction Context** – Set custom validators with specific LLM providers and models, and configure GenVM datetime for deterministic testing scenarios.
315
+ - **Direct Execution Mode** – Run contracts directly in Python for ultra-fast unit testing (~ms vs minutes).
316
+
317
+ ## ⚡ Direct vs Simulator Mode
318
+
319
+ The testing suite provides two execution modes:
320
+
321
+ | Mode | How it works | Speed | Use case |
322
+ |------|--------------|-------|----------|
323
+ | **Simulator** | Deploy to GenLayer simulator, interact via RPC | ~minutes | Integration tests, consensus validation |
324
+ | **Direct** | Run Python code directly in-memory | ~milliseconds | Unit tests, rapid development |
325
+
326
+ ### Quick Start with Direct Mode
327
+
328
+ ```python
329
+ def test_token_transfer(direct_vm, direct_deploy):
330
+ # Deploy contract directly in Python (no simulator)
331
+ token = direct_deploy("contracts/Token.py", initial_supply=1000)
332
+
333
+ # Create test addresses
334
+ from gltest.direct import create_address
335
+ alice = create_address("alice")
336
+ bob = create_address("bob")
337
+
338
+ # Set sender and interact
339
+ direct_vm.sender = alice
340
+ token.mint(alice, 500)
341
+ token.transfer(bob, 100)
342
+
343
+ assert token.balances[bob] == 100
344
+ ```
345
+
346
+ ### Available Fixtures
347
+
348
+ | Fixture | Description |
349
+ |---------|-------------|
350
+ | `direct_vm` | VM context with cheatcodes |
351
+ | `direct_deploy` | Deploy contracts directly |
352
+ | `direct_alice`, `direct_bob`, `direct_charlie` | Test addresses |
353
+ | `direct_owner` | Default sender address |
354
+ | `direct_accounts` | List of 10 test addresses |
355
+
356
+ ### Cheatcodes
357
+
358
+ ```python
359
+ # Change sender
360
+ direct_vm.sender = alice
361
+
362
+ # Prank (temporary sender change)
363
+ with direct_vm.prank(bob):
364
+ contract.method() # Called as bob
365
+
366
+ # Snapshots
367
+ snap_id = direct_vm.snapshot()
368
+ contract.modify_state()
369
+ direct_vm.revert(snap_id) # State restored
370
+
371
+ # Expect revert
372
+ with direct_vm.expect_revert("Insufficient balance"):
373
+ contract.transfer(bob, 1000000)
374
+
375
+ # Mock web/LLM (for nondet operations)
376
+ direct_vm.mock_web(r"api\.example\.com", {"status": 200, "body": "{}"})
377
+ direct_vm.mock_llm(r"analyze.*", "positive sentiment")
378
+ ```
379
+
380
+ 📖 **[Full Direct Mode Documentation](docs/direct-runner.md)**
315
381
 
316
382
  ## 📚 Examples
317
383
 
@@ -25,6 +25,13 @@ gltest/contracts/contract_functions.py
25
25
  gltest/contracts/method_stats.py
26
26
  gltest/contracts/stats_collector.py
27
27
  gltest/contracts/utils.py
28
+ gltest/direct/__init__.py
29
+ gltest/direct/loader.py
30
+ gltest/direct/pytest_plugin.py
31
+ gltest/direct/sdk_loader.py
32
+ gltest/direct/types.py
33
+ gltest/direct/vm.py
34
+ gltest/direct/wasi_mock.py
28
35
  gltest/helpers/__init__.py
29
36
  gltest/helpers/fixture_snapshot.py
30
37
  gltest/helpers/take_snapshot.py
@@ -3,3 +3,4 @@ gltest = gltest_cli.main:main
3
3
 
4
4
  [pytest11]
5
5
  gltest = gltest_cli.config.plugin
6
+ gltest_direct = gltest.direct.pytest_plugin
@@ -1,3 +1,4 @@
1
1
  dist
2
+ docs
2
3
  gltest
3
4
  gltest_cli
@@ -0,0 +1,31 @@
1
+ """
2
+ Native Python test runner for GenLayer intelligent contracts.
3
+
4
+ Runs contracts directly in Python without WASM/simulator, providing
5
+ Foundry-style cheatcodes and fast test execution.
6
+
7
+ Usage:
8
+ from gltest.direct import VMContext, deploy_contract
9
+
10
+ def test_transfer():
11
+ vm = VMContext()
12
+ with vm.activate():
13
+ token = deploy_contract("path/to/Token.py", vm, owner)
14
+ vm.sender = alice
15
+ token.transfer(bob, 100)
16
+ assert token.balances[bob] == 100
17
+ """
18
+
19
+ from .vm import VMContext
20
+ from .loader import deploy_contract, load_contract_class, create_address, create_test_addresses
21
+ from .wasi_mock import ContractRollback, MockNotFoundError
22
+
23
+ __all__ = [
24
+ "VMContext",
25
+ "deploy_contract",
26
+ "load_contract_class",
27
+ "create_address",
28
+ "create_test_addresses",
29
+ "ContractRollback",
30
+ "MockNotFoundError",
31
+ ]
@@ -0,0 +1,288 @@
1
+ """
2
+ Contract loader for direct test runner.
3
+
4
+ Handles:
5
+ - SDK version setup based on contract headers
6
+ - Message context injection via stdin
7
+ - WASI mock installation
8
+ - Contract class discovery and instantiation
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import sys
14
+ import hashlib
15
+ import importlib.util
16
+ from pathlib import Path
17
+ from typing import Any, Optional, Type, TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING:
20
+ from .vm import VMContext
21
+
22
+
23
+ def load_contract_class(
24
+ contract_path: Path,
25
+ vm: "VMContext",
26
+ sdk_version: Optional[str] = None,
27
+ ) -> Type[Any]:
28
+ """
29
+ Load a contract class from file.
30
+
31
+ Sets up SDK paths, WASI mock, and message context.
32
+ """
33
+ contract_path = Path(contract_path).resolve()
34
+
35
+ if not contract_path.exists():
36
+ raise FileNotFoundError(f"Contract not found: {contract_path}")
37
+
38
+ # 1. Setup WASI mock FIRST (before genlayer is imported)
39
+ from . import wasi_mock
40
+ wasi_mock.set_vm(vm)
41
+ sys.modules['_genlayer_wasi'] = wasi_mock
42
+
43
+ # 2. Setup SDK paths (this adds genlayer to sys.path)
44
+ from .sdk_loader import setup_sdk_paths
45
+ setup_sdk_paths(contract_path, sdk_version)
46
+
47
+ # 3. Inject message context into fd 0 BEFORE importing contract
48
+ # (genlayer reads message from fd 0 at import time)
49
+ _inject_message_to_fd0(vm)
50
+
51
+ # 4. Load the contract module
52
+ module = _load_module(contract_path)
53
+
54
+ contract_cls = _find_contract_class(module)
55
+
56
+ if contract_cls is None:
57
+ raise ValueError(f"No contract class found in {contract_path}")
58
+
59
+ return contract_cls
60
+
61
+
62
+ def deploy_contract(
63
+ contract_path: Path,
64
+ vm: "VMContext",
65
+ *args: Any,
66
+ sdk_version: Optional[str] = None,
67
+ **kwargs: Any,
68
+ ) -> Any:
69
+ """Deploy a contract and return an instance."""
70
+ contract_path = Path(contract_path).resolve()
71
+
72
+ if vm._contract_address is None:
73
+ addr_hash = hashlib.sha256(str(contract_path).encode()).digest()[:20]
74
+ vm._contract_address = addr_hash
75
+
76
+ contract_cls = load_contract_class(contract_path, vm, sdk_version)
77
+
78
+ instance = _allocate_contract(contract_cls, vm, *args, **kwargs)
79
+
80
+ return instance
81
+
82
+
83
+ def _inject_message_to_fd0(vm: "VMContext") -> None:
84
+ """Inject message context by replacing stdin (fd 0) with encoded message."""
85
+ import os
86
+ import tempfile
87
+
88
+ try:
89
+ from genlayer.py import calldata
90
+ from genlayer.py.types import Address
91
+ except ImportError:
92
+ return
93
+
94
+ # Convert addresses to Address type
95
+ sender_addr = vm.sender
96
+ if isinstance(sender_addr, bytes):
97
+ sender_addr = Address(sender_addr)
98
+
99
+ contract_addr = vm._contract_address
100
+ if isinstance(contract_addr, bytes):
101
+ contract_addr = Address(contract_addr)
102
+
103
+ origin_addr = vm.origin
104
+ if isinstance(origin_addr, bytes):
105
+ origin_addr = Address(origin_addr)
106
+
107
+ # Build message dict
108
+ message_data = {
109
+ 'contract_address': contract_addr,
110
+ 'sender_address': sender_addr,
111
+ 'origin_address': origin_addr,
112
+ 'stack': [],
113
+ 'value': vm._value,
114
+ 'datetime': vm._datetime,
115
+ 'is_init': False,
116
+ 'chain_id': vm._chain_id,
117
+ 'entry_kind': 0,
118
+ 'entry_data': b'',
119
+ 'entry_stage_data': None,
120
+ }
121
+
122
+ # Encode the message
123
+ encoded = calldata.encode(message_data)
124
+
125
+ # Create a temp file with the encoded message
126
+ fd, path = tempfile.mkstemp()
127
+ try:
128
+ os.write(fd, encoded)
129
+ os.lseek(fd, 0, os.SEEK_SET) # Reset to beginning
130
+
131
+ # Save original stdin fd
132
+ original_stdin = os.dup(0)
133
+ vm._original_stdin_fd = original_stdin
134
+
135
+ # Replace stdin with our temp file
136
+ os.dup2(fd, 0)
137
+ finally:
138
+ os.close(fd)
139
+ os.unlink(path)
140
+
141
+
142
+ def _load_module(contract_path: Path) -> Any:
143
+ """Load a Python module from file path."""
144
+ module_name = f"_contract_{contract_path.stem}"
145
+
146
+ if module_name in sys.modules:
147
+ del sys.modules[module_name]
148
+
149
+ spec = importlib.util.spec_from_file_location(module_name, contract_path)
150
+ if spec is None or spec.loader is None:
151
+ raise ImportError(f"Cannot load module from {contract_path}")
152
+
153
+ module = importlib.util.module_from_spec(spec)
154
+ sys.modules[module_name] = module
155
+
156
+ try:
157
+ spec.loader.exec_module(module)
158
+ except Exception as e:
159
+ if module_name in sys.modules:
160
+ del sys.modules[module_name]
161
+ raise ImportError(f"Failed to load contract: {e}") from e
162
+
163
+ return module
164
+
165
+
166
+ def _find_contract_class(module: Any) -> Optional[Type[Any]]:
167
+ """Find the contract class in a module."""
168
+ import dataclasses
169
+
170
+ candidates = []
171
+
172
+ for name in dir(module):
173
+ if name.startswith('_'):
174
+ continue
175
+
176
+ obj = getattr(module, name)
177
+
178
+ if not isinstance(obj, type):
179
+ continue
180
+
181
+ # Skip dataclasses - they're storage types, not contracts
182
+ if dataclasses.is_dataclass(obj):
183
+ continue
184
+
185
+ # Highest priority: explicit __gl_contract__ marker
186
+ if getattr(obj, '__gl_contract__', False):
187
+ return obj
188
+
189
+ # Second priority: inherits from Contract
190
+ for base in obj.__mro__:
191
+ if base.__name__ in ('Contract', 'gl.Contract'):
192
+ return obj
193
+
194
+ # Third priority: has storage-like annotations
195
+ # Collect as candidates but don't return immediately
196
+ if hasattr(obj, '__annotations__'):
197
+ annotations = obj.__annotations__
198
+ storage_types = ('TreeMap', 'DynArray', 'Array', 'u256', 'Address')
199
+ for ann in annotations.values():
200
+ ann_str = str(ann)
201
+ if any(st in ann_str for st in storage_types):
202
+ candidates.append(obj)
203
+ break
204
+
205
+ # Return first candidate if no explicit contract found
206
+ if candidates:
207
+ return candidates[0]
208
+
209
+ return None
210
+
211
+
212
+ def _allocate_contract(
213
+ contract_cls: Type[Any],
214
+ vm: "VMContext",
215
+ *args: Any,
216
+ **kwargs: Any,
217
+ ) -> Any:
218
+ """Allocate and initialize a contract instance."""
219
+ try:
220
+ from genlayer.py.storage import Root, ROOT_SLOT_ID
221
+ from genlayer.py.storage._internal.generate import (
222
+ ORIGINAL_INIT_ATTR,
223
+ _storage_build,
224
+ Lit,
225
+ )
226
+
227
+ # Build the storage type descriptor
228
+ td = _storage_build(contract_cls, {})
229
+ assert not isinstance(td, Lit)
230
+
231
+ # Use the VM's storage manager
232
+ slot = vm._storage.get_store_slot(ROOT_SLOT_ID)
233
+ instance = td.get(slot, 0)
234
+
235
+ # Find and call the original __init__
236
+ init = getattr(td, 'cls', None)
237
+ if init is None:
238
+ init = getattr(contract_cls, '__init__', None)
239
+ else:
240
+ init = getattr(init, '__init__', None)
241
+ if init is not None:
242
+ if hasattr(init, ORIGINAL_INIT_ATTR):
243
+ init = getattr(init, ORIGINAL_INIT_ATTR)
244
+ init(instance, *args, **kwargs)
245
+
246
+ return instance
247
+
248
+ except ImportError:
249
+ pass
250
+
251
+ try:
252
+ from genlayer.py.storage import Root
253
+
254
+ Root.MANAGER = vm._storage
255
+
256
+ root_slot = vm._storage.get_store_slot(b'\x00' * 32)
257
+
258
+ instance = contract_cls.__new__(contract_cls)
259
+
260
+ if hasattr(instance, '_storage_slot'):
261
+ instance._storage_slot = root_slot.indirect(0)
262
+ instance._off = 0
263
+
264
+ if args or kwargs:
265
+ instance.__init__(*args, **kwargs)
266
+
267
+ return instance
268
+
269
+ except ImportError:
270
+ pass
271
+
272
+ return contract_cls(*args, **kwargs)
273
+
274
+
275
+ def create_address(seed: str) -> Any:
276
+ """Create a deterministic address from seed string."""
277
+ addr_bytes = hashlib.sha256(seed.encode()).digest()[:20]
278
+
279
+ try:
280
+ from genlayer.py.types import Address
281
+ return Address(addr_bytes)
282
+ except ImportError:
283
+ return addr_bytes
284
+
285
+
286
+ def create_test_addresses(count: int = 10) -> list:
287
+ """Create a list of test addresses."""
288
+ return [create_address(f"test_address_{i}") for i in range(count)]
@@ -0,0 +1,117 @@
1
+ """
2
+ Pytest plugin for direct GenLayer contract testing.
3
+
4
+ Provides fixtures:
5
+ - direct_vm: VMContext for Foundry-style cheatcodes
6
+ - direct_deploy: Factory for deploying contracts
7
+
8
+ Usage:
9
+ def test_transfer(direct_vm, direct_deploy):
10
+ token = direct_deploy("contracts/Token.py")
11
+ direct_vm.sender = alice
12
+ token.transfer(bob, 100)
13
+ assert token.balances[bob] == 100
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import pytest
19
+ from pathlib import Path
20
+ from typing import Any, Optional, Callable
21
+
22
+ from .vm import VMContext
23
+ from .loader import deploy_contract, create_address, create_test_addresses
24
+
25
+
26
+ @pytest.fixture
27
+ def direct_vm() -> VMContext:
28
+ """
29
+ Provides a fresh VMContext for each test.
30
+ The VM is automatically activated for the test scope.
31
+ """
32
+ ctx = VMContext()
33
+ ctx.sender = create_address("default_sender")
34
+
35
+ with ctx.activate():
36
+ yield ctx
37
+
38
+
39
+ @pytest.fixture
40
+ def direct_deploy(direct_vm: VMContext) -> Callable[..., Any]:
41
+ """
42
+ Factory fixture for deploying contracts directly.
43
+
44
+ Usage:
45
+ def test_example(direct_deploy):
46
+ token = direct_deploy("path/to/Token.py", initial_supply=1000)
47
+ """
48
+ def _deploy(
49
+ contract_path: str,
50
+ *args: Any,
51
+ sdk_version: Optional[str] = None,
52
+ **kwargs: Any,
53
+ ) -> Any:
54
+ path = Path(contract_path)
55
+
56
+ if not path.is_absolute():
57
+ if path.exists():
58
+ path = path.resolve()
59
+ else:
60
+ for base in [
61
+ Path.cwd(),
62
+ Path.cwd() / "contracts",
63
+ Path.cwd() / "intelligent-contracts",
64
+ ]:
65
+ candidate = base / contract_path
66
+ if candidate.exists():
67
+ path = candidate.resolve()
68
+ break
69
+
70
+ return deploy_contract(path, direct_vm, *args, sdk_version=sdk_version, **kwargs)
71
+
72
+ return _deploy
73
+
74
+
75
+ @pytest.fixture
76
+ def direct_alice() -> Any:
77
+ """Test address: Alice."""
78
+ return create_address("alice")
79
+
80
+
81
+ @pytest.fixture
82
+ def direct_bob() -> Any:
83
+ """Test address: Bob."""
84
+ return create_address("bob")
85
+
86
+
87
+ @pytest.fixture
88
+ def direct_charlie() -> Any:
89
+ """Test address: Charlie."""
90
+ return create_address("charlie")
91
+
92
+
93
+ @pytest.fixture
94
+ def direct_owner() -> Any:
95
+ """Test address: Owner (default sender)."""
96
+ return create_address("default_sender")
97
+
98
+
99
+ @pytest.fixture
100
+ def direct_accounts() -> list:
101
+ """List of 10 test addresses."""
102
+ return create_test_addresses(10)
103
+
104
+
105
+ def pytest_configure(config):
106
+ """Register markers for direct tests."""
107
+ config.addinivalue_line(
108
+ "markers",
109
+ "direct: mark test as using direct contract execution (no simulator)",
110
+ )
111
+
112
+
113
+ def pytest_collection_modifyitems(config, items):
114
+ """Auto-mark tests using direct fixtures."""
115
+ for item in items:
116
+ if 'direct_vm' in item.fixturenames or 'direct_deploy' in item.fixturenames:
117
+ item.add_marker(pytest.mark.direct)