This release is a pre-release and may not be stable for production use.
GenLayer Testing Suite
A pytest-based testing framework for GenLayer intelligent contracts. Built on top of genlayer-py.
pip install genlayer-test
Two Ways to Test
The testing suite provides two execution modes. Pick the one that fits your workflow:
| Direct Mode | Studio Mode | |
|---|---|---|
| How it works | Runs contract Python code directly in-memory | Deploys to GenLayer Studio, interacts via RPC |
| Speed | ~milliseconds per test | ~minutes per test |
| Prerequisites | Python >= 3.12 | Python >= 3.12 + GenLayer Studio (Docker) |
| Best for | Unit tests, rapid development, CI/CD | Integration tests, consensus validation, testnet |
| Mocking | Foundry-style cheatcodes (mock_web, mock_llm) |
Mock validators with transaction context |
Start with Direct Mode. It's faster, simpler, and doesn't require Docker. Use Studio Mode when you need full network behavior, multi-validator consensus, or testnet deployment.
Direct Mode
Run contracts directly in Python — no simulator, no Docker, no network. Tests execute in milliseconds.
Quick Start
def test_storage(direct_vm, direct_deploy):
# Deploy contract in-memory
storage = direct_deploy("contracts/Storage.py", "initial")
# Read state directly
assert storage.get_storage() == "initial"
# Write state directly
storage.update_storage("updated")
assert storage.get_storage() == "updated"
Run with pytest:
pytest tests/ -v
Fixtures
| Fixture | Description |
|---|---|
direct_vm |
VM context with cheatcodes |
direct_deploy |
Deploy contracts directly |
direct_alice, direct_bob, direct_charlie |
Test addresses |
direct_owner |
Default sender address |
direct_accounts |
List of 10 test addresses |
Cheatcodes
# Change sender
direct_vm.sender = alice
# Prank (temporary sender change)
with direct_vm.prank(bob):
contract.method() # Called as bob
# Snapshots (captures full state: storage, mocks, sender, validators)
snap_id = direct_vm.snapshot()
contract.modify_state()
direct_vm.revert(snap_id) # Full state restored
# Expect revert
with direct_vm.expect_revert("Insufficient balance"):
contract.transfer(bob, 1000000)
# Mock web/LLM (regex pattern matching)
direct_vm.mock_web(r"api\.example\.com", {"status": 200, "body": "{}"})
direct_vm.mock_llm(r"analyze.*", "positive sentiment")
# Test validator consensus logic
contract.update_price() # Runs leader_fn, captures validator
direct_vm.clear_mocks() # Swap mocks for validator
direct_vm.mock_llm(r".*", "different result")
assert direct_vm.run_validator() is False # Validator disagrees
# Strict mocks (detect unused mocks)
direct_vm.strict_mocks = True
# Pickling validation (catch production serialization issues)
direct_vm.check_pickling = True
Full Direct Mode Documentation — fixtures, cheatcodes, validator testing, limitations, and complete examples.
Studio Mode
Deploy contracts to a running GenLayer Studio instance and interact via RPC. This gives you full network behavior including multi-validator consensus.
Prerequisites
- Python >= 3.12
- GenLayer Studio running (Docker)
Quick Start
from gltest import get_contract_factory, get_default_account
from gltest.assertions import tx_execution_succeeded
factory = get_contract_factory("MyContract")
contract = factory.deploy()
# Read method — returns value directly
result = contract.get_value().call()
# Write method — returns transaction receipt
tx_receipt = contract.set_value(args=["new_value"]).transact()
assert tx_execution_succeeded(tx_receipt)
Run with the gltest CLI:
gltest # Run all tests
gltest tests/test_mycontract.py # Specific file
gltest --network studio_devnet # Hosted release preview
gltest --network studionet # Specific network
gltest --leader-only # Skip consensus (faster)
gltest -v # Verbose output
Configuration
Create a gltest.config.yaml in your project root:
networks:
default: localnet
localnet:
url: "http://127.0.0.1:4000/api"
leader_only: false
studio_devnet:
# Pre-configured release preview — accounts auto-generated
studionet:
# Pre-configured — accounts auto-generated
testnet_asimov:
accounts:
- "${ACCOUNT_PRIVATE_KEY_1}"
- "${ACCOUNT_PRIVATE_KEY_2}"
from: "${ACCOUNT_PRIVATE_KEY_1}"
paths:
contracts: "contracts"
artifacts: "artifacts"
environment: .env
Key options:
- Networks:
localnet,studio_devnet, andstudionetwork out of the box.testnet_asimovrequires account keys. - Paths: Where your contracts and artifacts live.
- Environment:
.envfile for private keys.
Override via CLI:
gltest --network testnet_asimov
gltest --contracts-dir custom/contracts/path
gltest --rpc-url http://custom:4000/api
gltest --chain-type localnet
Contract Deployment
from gltest import get_contract_factory, get_default_account
from gltest.assertions import tx_execution_succeeded
factory = get_contract_factory("Storage")
# deploy() returns the contract instance (recommended)
contract = factory.deploy(
args=["initial_value"],
account=get_default_account(),
consensus_max_rotations=3,
)
# deploy_contract_tx() returns only the receipt
receipt = factory.deploy_contract_tx(args=["initial_value"])
assert tx_execution_succeeded(receipt)
Read and Write Methods
# Read — call() returns the value
result = contract.get_storage().call()
# Write — transact() returns a receipt
tx_receipt = contract.update_storage(args=["new_value"]).transact(
value=0,
consensus_max_rotations=3,
wait_interval=1000,
wait_retries=10,
)
assert tx_execution_succeeded(tx_receipt)
Fee Profiling
Generate a frontend-ready fee profile from the deploys and write transactions executed during a gltest session:
gltest --fee-profile artifacts/fee-profile.json
gltest --fee-profile artifacts/fee-profile.json --fee-profile-headroom 1.5
--fee-profile writes JSON that can be used as developer fee suggestions by
transaction-kit, genlayer-js, genlayer-py, or CLI-based submission flows. The optional
--fee-profile-headroom multiplier defaults to 1.25. Fee and time-unit
values are multiplied by headroom, rounded up, and emitted as decimal strings.
When the same method is observed in multiple tests, the profile records the
maximum observed value for each field across all of those branches.
rotationsPerRound is recorded exactly because it is a posture choice rather
than a consumed fee amount.
{
"version": 1,
"network": "localnet",
"chainId": 61127,
"measuredAt": "2026-06-10T12:00:00Z",
"deploy": {
"leaderTimeunitsAllocation": "125",
"validatorTimeunitsAllocation": "250",
"executionBudgetPerRound": "625000",
"totalMessageFees": "0",
"rotationsPerRound": "0"
},
"methods": {
"create_bet": {
"leaderTimeunitsAllocation": "125",
"validatorTimeunitsAllocation": "250",
"executionBudgetPerRound": "312500",
"totalMessageFees": "12500",
"rotationsPerRound": "0"
}
}
}
Fee profiling is currently measurable on Studio-based networks whose receipts
include consumed fee data. Testnet receipts do not expose consumed fees yet, and
direct/sim mode does not go through these receipt paths. Time-unit allocations
are recorded from the submitted fee distribution when the backend receipt
includes it. Live price caps and feeValue are intentionally omitted so the SDK
can quote them from the current network policy at transaction time. The numeric
chainId scopes the measurements to the exact selected runtime chain; consumers
must fall back to network defaults when it is absent or does not match.
Assertions
from gltest.assertions import tx_execution_succeeded, tx_execution_failed
assert tx_execution_succeeded(tx_receipt)
assert tx_execution_failed(tx_receipt)
# Regex matching on stdout/stderr (localnet/studionet only)
assert tx_execution_succeeded(tx_receipt, match_std_out=r".*code \d+")
assert tx_execution_failed(tx_receipt, match_std_err=r"Method.*failed")
Fixtures
| Fixture | Scope | Description |
|---|---|---|
gl_client |
session | GenLayer client for network operations |
default_account |
session | Default account for transactions |
accounts |
session | List of test accounts |
def test_workflow(gl_client, default_account, accounts):
factory = get_contract_factory("MyContract")
contract = factory.deploy(account=default_account)
tx_receipt = contract.some_method(args=["value"], account=accounts[1])
assert tx_execution_succeeded(tx_receipt)
Mock LLM Responses
Simulate LLM responses for deterministic tests:
from gltest import get_contract_factory, get_validator_factory
from gltest.types import MockedLLMResponse
mock_response: MockedLLMResponse = {
"nondet_exec_prompt": {
"analyze this": "positive sentiment"
},
"eq_principle_prompt_comparative": {
"values match": True
}
}
validator_factory = get_validator_factory()
validators = validator_factory.batch_create_mock_validators(
count=5,
mock_llm_response=mock_response
)
transaction_context = {
"validators": [v.to_dict() for v in validators],
"genvm_datetime": "2024-01-01T00:00:00Z"
}
factory = get_contract_factory("LLMContract")
contract = factory.deploy(transaction_context=transaction_context)
result = contract.analyze_text(args=["analyze this"]).transact(
transaction_context=transaction_context
)
Mock keys map to GenLayer methods:
| Mock Key | GenLayer Method |
|---|---|
"nondet_exec_prompt" |
gl.nondet.exec_prompt |
"eq_principle_prompt_comparative" |
gl.eq_principle.prompt_comparative |
"eq_principle_prompt_non_comparative" |
gl.eq_principle.prompt_non_comparative |
The system performs substring matching on the internal user message — your mock key must appear within the message.
Mock Web Responses
Simulate HTTP responses for contracts that call gl.nondet.web.get(), etc.:
from gltest.types import MockedWebResponse
import json
mock_web_response: MockedWebResponse = {
"nondet_web_request": {
"https://api.example.com/price": {
"method": "GET",
"status": 200,
"body": json.dumps({"price": 100.50})
}
}
}
validators = validator_factory.batch_create_mock_validators(
count=5,
mock_web_response=mock_web_response
)
You can combine both mock_llm_response and mock_web_response in a single batch_create_mock_validators call. URL matching is exact (including query parameters).
Custom Validators
from gltest import get_validator_factory
factory = get_validator_factory()
# Real validators with specific LLM providers
validators = factory.batch_create_validators(
count=5,
stake=10,
provider="openai",
model="gpt-4o",
config={"temperature": 0.7},
plugin="openai-compatible",
plugin_config={"api_key_env_var": "OPENAI_API_KEY"}
)
# Use in transaction context
transaction_context = {
"validators": [v.to_dict() for v in validators],
"genvm_datetime": "2024-03-15T14:30:00Z"
}
Statistical Analysis
For LLM-based contracts, .analyze() runs multiple simulations to measure consistency:
analysis = contract.process_with_llm(args=["input"]).analyze(
provider="openai",
model="gpt-4o",
runs=100,
)
print(f"Success rate: {analysis.success_rate:.2f}%")
print(f"Reliability: {analysis.reliability_score:.2f}%")
print(f"Unique states: {analysis.unique_states}")
Full Studio Mode Documentation — configuration reference, all CLI flags, mock LLM/web details, custom validators, statistical analysis, and complete examples.
Example Contract
import genlayer as gl
class Storage(gl.contract.Contract):
storage: str
def __init__(self, initial_storage: str):
self.storage = initial_storage
@gl.public.view
def get_storage(self) -> str:
return self.storage
@gl.public.write
def update_storage(self, new_storage: str) -> None:
self.storage = new_storage
Project Structure
my-project/
├── contracts/
│ └── Storage.py
├── tests/
│ ├── test_direct.py # Direct mode tests (fast)
│ └── test_integration.py # Studio mode tests
└── gltest.config.yaml # Studio mode config
For more examples, see the contracts directory.
Troubleshooting
Contract not found: Ensure contracts are in contracts/ or specify --contracts-dir. Contracts must inherit from gl.contract.Contract.
Transaction timeouts (Studio mode): Increase wait_interval and wait_retries in .transact().
Consensus failures (Studio mode): Increase consensus_max_rotations or use --leader-only for faster iteration.
Environment issues: Verify Python >= 3.12. For Studio mode, check Docker is running (docker ps).
Contributing
See our Contributing Guide.
License
MIT — see LICENSE.
Support
Metadata
Release files for genlayer-test 0.30.0rc2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| genlayer_test-0.30.0rc2.tar.gz | 83.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| genlayer_test-0.30.0rc2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 179.4 kB
Release files / genlayer_test-0.30.0rc2.tar.gz
| Download URL | genlayer_test-0.30.0rc2.tar.gz |
|---|---|
| Size | 83.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9a30bf80601aaf6b8d1c669f96aecaa322b1d07d0fbd9d542b379e0fc77d5c4c
|
|
BLAKE2b-256 checksum How to use checksums |
126efb1bb8049e9de136add26b178594b31d407f9829b23f5267c01a70289845
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / genlayer_test-0.30.0rc2-py3-none-any.whl
| Download URL | genlayer_test-0.30.0rc2-py3-none-any.whl |
|---|---|
| Size | 96.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
895ec37a584ee8566893a0dbd44b138ed2de58572ede96630464a6d2edaaa53f
|
|
BLAKE2b-256 checksum How to use checksums |
f3f1f507f3b3fb95ac883c34cb59b48cd03103f8804d088861ec762d15a0fa34
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|