diff --git a/.gitignore b/.gitignore index 33a0b50..bc8e7e5 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,3 @@ examples/**/*.trace # coverage output .coverage coverage.xml - -docs/jupyter_execute/ diff --git a/docs/algopy.md b/docs/algopy.md new file mode 100644 index 0000000..426f3e8 --- /dev/null +++ b/docs/algopy.md @@ -0,0 +1,9 @@ +# Algorand Python + +Algorand Python is a partial implementation of the Python programming language that runs on the AVM. It includes a statically typed framework for development of Algorand smart contracts and logic signatures, with Pythonic interfaces to underlying AVM functionality that works with standard Python tooling. + +Algorand Python is compiled for execution on the AVM by PuyaPy, an optimising compiler that ensures the resulting AVM bytecode execution semantics that match the given Python code. PuyaPy produces output that is directly compatible with [AlgoKit typed clients](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/generate.md#1-typed-clients) to make deployment and calling easy. + +## Quick start + +To get started refer to the [official documentation](https://algorandfoundation.github.io/puya). diff --git a/docs/conf.py b/docs/conf.py index 8f4793e..669b62a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,8 +20,9 @@ "sphinx.ext.intersphinx", "sphinx_copybutton", "myst_parser", - "autodoc2", # Add this line + "autodoc2", "sphinx.ext.doctest", + "sphinxmermaid", ] templates_path = ["_templates"] @@ -45,24 +46,22 @@ # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output - html_theme = "furo" html_static_path = ["_static"] html_css_files = [ "custom.css", ] -html_js_files = ["https://unpkg.com/thebe@latest/lib/index.js"] python_maximum_signature_line_length = 80 # -- Options for myst --- myst_enable_extensions = ["colon_fence", "fieldlist"] -# Add autodoc2 configuration +# -- Options for autodoc2 autodoc2_packages = [ { "path": "../src/algopy_testing", - "auto_mode": True, + "auto_mode": False, }, ] autodoc2_render_plugin = "myst" @@ -74,6 +73,7 @@ add_module_names = False autodoc2_index_template = None +# -- Options for doctest -- doctest_global_setup = """ import algopy from algopy import arc4 @@ -81,3 +81,8 @@ from algopy_testing import AlgopyTestContext, algopy_testing_context """ doctest_test_doctest_blocks = "default" + +# -- Options for mermaid -- +sphinxmermaid_mermaid_init = { + "theme": "dark", +} diff --git a/docs/coverage.md b/docs/coverage.md index bbe7660..8fc6e91 100644 --- a/docs/coverage.md +++ b/docs/coverage.md @@ -6,157 +6,157 @@ Based on the definitions provided and the implementation details in the `src` di | Name | Implementation type | | ------------------------------------------- | ------------------- | -| algopy.UInt64 | Native | +| algopy.Account | Emulated | +| algopy.Application | Emulated | +| algopy.Asset | Emulated | | algopy.BigUInt | Native | +| algopy.Box | Emulated | +| algopy.BoxMap | Emulated | +| algopy.BoxRef | Emulated | | algopy.Bytes | Native | +| algopy.BytesBacked | Native | +| algopy.CompiledContract | Mockable | +| algopy.CompiledLogicSig | Mockable | +| algopy.Contract | Emulated | +| algopy.Global | Emulated | +| algopy.GlobalState | Emulated | +| algopy.LocalState | Emulated | +| algopy.LogicSig | Emulated | +| algopy.OnCompleteAction | Native | +| algopy.OpUpFeeSource | Native | +| algopy.StateTotals | Emulated | | algopy.String | Native | -| algopy.arc4.Bool | Native | -| algopy.Application | Emulated | -| algopy.Asset | Emulated | -| algopy.Account | Emulated | -| algopy.urange | Native | -| algopy.subroutine | Native | -| algopy.op.vrf_verify | Mockable | -| algopy.op.substring | Native | -| algopy.op.sqrt | Native | -| algopy.op.shr | Native | -| algopy.op.shl | Native | -| algopy.op.sha512_256 | Native | -| algopy.op.sha3_256 | Native | -| algopy.op.sha256 | Native | -| algopy.op.setbyte | Native | -| algopy.op.setbit_uint64 | Native | -| algopy.op.setbit_bytes | Native | -| algopy.op.select_uint64 | Native | -| algopy.op.select_bytes | Native | -| algopy.op.replace | Native | -| algopy.op.mulw | Native | -| algopy.op.min_balance | Emulated | -| algopy.op.keccak256 | Native | -| algopy.op.itob | Native | -| algopy.op.gload_uint64 | Emulated | -| algopy.op.gload_bytes | Emulated | -| algopy.op.getbyte | Native | -| algopy.op.getbit | Native | -| algopy.op.gaid | Emulated | -| algopy.op.extract_uint64 | Native | -| algopy.op.extract_uint32 | Native | -| algopy.op.extract_uint16 | Native | -| algopy.op.extract | Native | -| algopy.op.expw | Native | -| algopy.op.exp | Native | -| algopy.op.exit | Native | -| algopy.op.err | Native | -| algopy.op.ed25519verify_bare | Native | -| algopy.op.ed25519verify | Native | -| algopy.op.ecdsa_verify | Native | -| algopy.op.ecdsa_pk_recover | Native | -| algopy.op.ecdsa_pk_decompress | Native | -| algopy.op.divw | Native | -| algopy.op.divmodw | Native | -| algopy.op.concat | Native | -| algopy.op.bzero | Native | -| algopy.op.btoi | Native | -| algopy.op.bsqrt | Native | -| algopy.op.bitlen | Native | -| algopy.op.base64_decode | Native | -| algopy.op.balance | Emulated | -| algopy.op.arg | Emulated | -| algopy.op.app_opted_in | Emulated | -| algopy.op.addw | Native | -| algopy.op.VrfVerify | Mockable | -| algopy.op.Txn | Emulated | -| algopy.op.Scratch | Emulated | -| algopy.op.JsonRef | Native | -| algopy.op.ITxn | Emulated | -| algopy.op.Global | Emulated | -| algopy.op.GTxn | Emulated | -| algopy.op.GITxn | Emulated | -| algopy.op.ECDSA | Native | -| algopy.op.EC | Mockable | -| algopy.op.Box | Emulated | -| algopy.op.Block | Emulated | -| algopy.op.Base64 | Native | -| algopy.logicsig | Emulated | +| algopy.TemplateVar | Emulated | +| algopy.TransactionType | Native | +| algopy.Txn | Emulated | +| algopy.UInt64 | Native | +| algopy.compile_contract | Mockable | +| algopy.compile_logicsig | Mockable | +| algopy.ensure_budget | Emulated | | algopy.log | Emulated | -| algopy.itxn.submit_txns | Emulated | -| algopy.itxn.PaymentInnerTransaction | Emulated | -| algopy.itxn.Payment | Emulated | -| algopy.itxn.KeyRegistrationInnerTransaction | Emulated | -| algopy.itxn.KeyRegistration | Emulated | -| algopy.itxn.InnerTransactionResult | Emulated | -| algopy.itxn.InnerTransaction | Emulated | -| algopy.itxn.AssetTransferInnerTransaction | Emulated | -| algopy.itxn.AssetTransfer | Emulated | -| algopy.itxn.AssetFreezeInnerTransaction | Emulated | -| algopy.itxn.AssetFreeze | Emulated | -| algopy.itxn.AssetConfigInnerTransaction | Emulated | -| algopy.itxn.AssetConfig | Emulated | -| algopy.itxn.ApplicationCall | Emulated | -| algopy.gtxn.TransactionBase | Emulated | -| algopy.gtxn.Transaction | Emulated | -| algopy.gtxn.PaymentTransaction | Emulated | -| algopy.gtxn.KeyRegistrationTransaction | Emulated | -| algopy.gtxn.AssetTransferTransaction | Emulated | -| algopy.gtxn.AssetFreezeTransaction | Emulated | -| algopy.gtxn.AssetConfigTransaction | Emulated | -| algopy.gtxn.ApplicationCallTransaction | Emulated | -| algopy.ensure_budget | Mockable | -| algopy.arc4.emit | Emulated | -| algopy.arc4.baremethod | Emulated | -| algopy.arc4.arc4_signature | Native | +| algopy.logicsig | Emulated | +| algopy.subroutine | Native | +| algopy.uenumerate | Native | +| algopy.urange | Native | +| algopy.arc4.ARC4Client | Emulated | +| algopy.arc4.ARC4Contract | Emulated | +| algopy.arc4.Address | Native | +| algopy.arc4.BigUFixedNxM | Native | +| algopy.arc4.BigUIntN | Native | +| algopy.arc4.Bool | Native | +| algopy.arc4.Byte | Native | +| algopy.arc4.DynamicArray | Native | +| algopy.arc4.DynamicBytes | Native | +| algopy.arc4.StaticArray | Native | +| algopy.arc4.String | Native | +| algopy.arc4.Struct | Native | +| algopy.arc4.Tuple | Native | +| algopy.arc4.UFixedNxM | Native | +| algopy.arc4.UInt128 | Native | +| algopy.arc4.UInt16 | Native | +| algopy.arc4.UInt256 | Native | +| algopy.arc4.UInt32 | Native | +| algopy.arc4.UInt512 | Native | +| algopy.arc4.UInt64 | Native | +| algopy.arc4.UInt8 | Native | +| algopy.arc4.UIntN | Native | | algopy.arc4.abimethod | Emulated | +| algopy.arc4.arc4_signature | Native | +| algopy.arc4.baremethod | Emulated | +| algopy.arc4.emit | Emulated | | algopy.arc4.abi_call | Mockable | -| algopy.arc4.UIntN | Native | -| algopy.arc4.UInt8 | Native | -| algopy.arc4.UInt64 | Native | -| algopy.arc4.UInt512 | Native | -| algopy.arc4.UInt32 | Native | -| algopy.arc4.UInt256 | Native | -| algopy.arc4.UInt16 | Native | -| algopy.arc4.UInt128 | Native | -| algopy.arc4.Tuple | Native | -| algopy.arc4.Struct | Native | -| algopy.arc4.String | Native | -| algopy.arc4.StaticArray | Native | -| algopy.arc4.DynamicBytes | Native | -| algopy.arc4.DynamicArray | Native | -| algopy.arc4.Byte | Native | -| algopy.arc4.BigUIntN | Native | -| algopy.arc4.Address | Native | -| algopy.arc4.ARC4Client | Mockable | -| algopy.Txn | Emulated | -| algopy.TransactionType | Native | -| algopy.TemplateVar | Emulated | -| algopy.StateTotals | Emulated | -| algopy.OpUpFeeSource | Native | -| algopy.OnCompleteAction | Native | -| algopy.LogicSig | Emulated | -| algopy.LocalState | Emulated | -| algopy.GlobalState | Emulated | -| algopy.Global | Emulated | -| algopy.Contract | Emulated | -| algopy.BytesBacked | Native | -| algopy.BoxRef | Emulated | -| algopy.BoxMap | Emulated | -| algopy.Box | Emulated | -| algopy.ARC4Contract | Emulated | -| algopy.uenumerate | Native | -| algopy.op.ITxnCreate | Emulated | -| algopy.op.EllipticCurve | Mockable | -| algopy.op.AssetParamsGet | Emulated | -| algopy.op.AssetHoldingGet | Emulated | -| algopy.op.AppParamsGet | Emulated | -| algopy.op.AppLocal | Emulated | +| algopy.arc4.arc4_create | Mockable | +| algopy.arc4.arc4_update | Mockable | +| algopy.gtxn.ApplicationCallTransaction | Emulated | +| algopy.gtxn.AssetConfigTransaction | Emulated | +| algopy.gtxn.AssetFreezeTransaction | Emulated | +| algopy.gtxn.AssetTransferTransaction | Emulated | +| algopy.gtxn.KeyRegistrationTransaction | Emulated | +| algopy.gtxn.PaymentTransaction | Emulated | +| algopy.gtxn.Transaction | Emulated | +| algopy.gtxn.TransactionBase | Emulated | +| algopy.itxn.ApplicationCall | Emulated | +| algopy.itxn.ApplicationCallInnerTransaction | Emulated | +| algopy.itxn.AssetConfig | Emulated | +| algopy.itxn.AssetConfigInnerTransaction | Emulated | +| algopy.itxn.AssetFreeze | Emulated | +| algopy.itxn.AssetFreezeInnerTransaction | Emulated | +| algopy.itxn.AssetTransfer | Emulated | +| algopy.itxn.AssetTransferInnerTransaction | Emulated | +| algopy.itxn.InnerTransaction | Emulated | +| algopy.itxn.InnerTransactionResult | Emulated | +| algopy.itxn.KeyRegistration | Emulated | +| algopy.itxn.KeyRegistrationInnerTransaction | Emulated | +| algopy.itxn.Payment | Emulated | +| algopy.itxn.PaymentInnerTransaction | Emulated | +| algopy.itxn.submit_txns | Emulated | +| algopy.op.Base64 | Native | +| algopy.op.EC | Native | +| algopy.op.ECDSA | Native | +| algopy.op.JsonRef | Native | +| algopy.op.addw | Native | +| algopy.op.arg | Emulated | +| algopy.op.base64_decode | Native | +| algopy.op.bitlen | Native | +| algopy.op.bsqrt | Native | +| algopy.op.btoi | Native | +| algopy.op.bzero | Native | +| algopy.op.concat | Native | +| algopy.op.divmodw | Native | +| algopy.op.divw | Native | +| algopy.op.ecdsa_pk_decompress | Native | +| algopy.op.ecdsa_pk_recover | Native | +| algopy.op.ecdsa_verify | Native | +| algopy.op.ed25519verify | Native | +| algopy.op.ed25519verify_bare | Native | +| algopy.op.err | Native | +| algopy.op.exit | Native | +| algopy.op.exp | Native | +| algopy.op.expw | Native | +| algopy.op.extract | Native | +| algopy.op.extract_uint16 | Native | +| algopy.op.extract_uint32 | Native | +| algopy.op.extract_uint64 | Native | +| algopy.op.getbit | Native | +| algopy.op.getbyte | Native | +| algopy.op.itob | Native | +| algopy.op.keccak256 | Native | +| algopy.op.mulw | Native | +| algopy.op.replace | Native | +| algopy.op.select_bytes | Native | +| algopy.op.select_uint64 | Native | +| algopy.op.setbit_bytes | Native | +| algopy.op.setbit_uint64 | Native | +| algopy.op.setbyte | Native | +| algopy.op.sha256 | Native | +| algopy.op.sha3_256 | Native | +| algopy.op.sha512_256 | Native | +| algopy.op.shl | Native | +| algopy.op.shr | Native | +| algopy.op.sqrt | Native | +| algopy.op.substring | Native | | algopy.op.AppGlobal | Emulated | +| algopy.op.AppLocal | Emulated | +| algopy.op.AppParamsGet | Emulated | +| algopy.op.AssetHoldingGet | Emulated | +| algopy.op.AssetParamsGet | Emulated | +| algopy.op.Block | Emulated | +| algopy.op.Box | Emulated | +| algopy.op.GITxn | Emulated | +| algopy.op.GTxn | Emulated | +| algopy.op.Global | Emulated | +| algopy.op.ITxn | Emulated | +| algopy.op.ITxnCreate | Emulated | +| algopy.op.Scratch | Emulated | +| algopy.op.Txn | Emulated | +| algopy.op.app_opted_in | Emulated | +| algopy.op.balance | Emulated | +| algopy.op.gaid | Emulated | +| algopy.op.gload_bytes | Emulated | +| algopy.op.gload_uint64 | Emulated | +| algopy.op.min_balance | Emulated | | algopy.op.AcctParamsGet | Emulated | -| algopy.itxn.ApplicationCallInnerTransaction | Emulated | -| algopy.compile_logicsig | Mockable | -| algopy.compile_contract | Mockable | -| algopy.arc4.arc4_update | Mockable | -| algopy.arc4.arc4_create | Mockable | -| algopy.arc4.UFixedNxM | Native | -| algopy.arc4.BigUFixedNxM | Native | -| algopy.arc4.ARC4Contract | Emulated | -| algopy.CompiledLogicSig | Mockable | -| algopy.CompiledContract | Mockable | +| algopy.op.EllipticCurve | Mockable | +| algopy.op.VrfVerify | Mockable | +| algopy.op.vrf_verify | Mockable | + diff --git a/docs/examples.md b/docs/examples.md index bc87337..a240441 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -2,10 +2,13 @@ Below is a showcase of various examples of unit testing real and sample Algorand Python smart contracts using `algorand-python-testing`. -| Contract Name | Test File | Key Features Demonstrated | Test versions of Algopy Abstractions used | -| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -| Auction | [test_contract.py](https://github.com/algorandfoundation/algorand-python-testing/blob/main/examples/auction/test_contract.py) | - Use of algopy_testing_context
- Mocking of global state and transaction fields
- Testing of ARC4 contract methods
- Emulation of asset creation and transfers
- Verification of inner transactions | - **ARC4Contract**
- **Global**
- **Txn**
- **Asset**
- **Account** | -| Proof of Attendance | [test_contract.py](https://github.com/algorandfoundation/algorand-python-testing/blob/main/examples/proof_of_attendance/test_contract.py) | - Creation and management of dummy assets
- Testing of box storage operations
- Verification of inner transactions for asset transfers
- Use of any\_\* methods for generating test data | - **Contract**
- **Box**
- **Asset**
- **Account**
- **op** (for various operations) | -| Simple Voting | [test_contract.py](https://github.com/algorandfoundation/algorand-python-testing/blob/main/examples/simple_voting/test_contract.py) | - Testing of global and local state operations
- Verification of transaction group operations
- Mocking of payment transactions | - **Contract**
- **GlobalState**
- **LocalState**
- **Txn**
- **GTxn** (group transactions) | -| ZK Whitelist | [test_contract.py](https://github.com/algorandfoundation/algorand-python-testing/blob/main/examples/zk_whitelist/test_contract.py) | - Testing of zero-knowledge proof verification
- Mocking of external application calls
- Use of ARC4 types and methods | - **ARC4Contract**
- **arc4 types** (Address, DynamicArray, StaticArray, etc.)
- Application logs | -| HTLC LogicSig | [test_signature.py](https://github.com/algorandfoundation/algorand-python-testing/blob/main/examples/htlc_logicsig/test_signature.py) | - Testing of LogicSig contracts
- Verification of time-based conditions
- Mocking of transaction parameters | - **LogicSig**
- **Txn**
- **Global** | +| Contract Name | Test File | Key Features Demonstrated | Test versions of Algopy Abstractions used | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Auction | [test_contract.py](https://github.com/algorandfoundation/algorand-python-testing/blob/main/examples/auction/test_contract.py) | - Use of algopy_testing_context
- Mocking of global state and transaction fields
- Testing of ARC4 contract methods
- Emulation of asset creation and transfers
- Verification of inner transactions | - **ARC4Contract**
- **Global**
- **Txn**
- **Asset**
- **Account**
- **LocalState** | +| Proof of Attendance | [test_contract.py](https://github.com/algorandfoundation/algorand-python-testing/blob/main/examples/proof_of_attendance/test_contract.py) | - Creation and management of dummy assets
- Testing of box storage operations
- Verification of inner transactions for asset transfers
- Use of any\_\* methods for generating test data | - **ARC4Contract**
- **Box**
- **BoxMap**
- **Asset**
- **Account**
- **op** | +| Simple Voting | [test_contract.py](https://github.com/algorandfoundation/algorand-python-testing/blob/main/examples/simple_voting/test_contract.py) | - Testing of global and local state operations
- Verification of transaction group operations
- Mocking of payment transactions | - **Contract**
- **GlobalState**
- **LocalState**
- **Txn**
- **op.GTxn** | +| ZK Whitelist | [test_contract.py](https://github.com/algorandfoundation/algorand-python-testing/blob/main/examples/zk_whitelist/test_contract.py) | - Testing of zero-knowledge proof verification
- Mocking of external application calls
- Use of ARC4 types and methods | - **ARC4Contract**
- **arc4 types**
- **LocalState**
- **Global**
- **Txn** | +| HTLC LogicSig | [test_signature.py](https://github.com/algorandfoundation/algorand-python-testing/blob/main/examples/htlc_logicsig/test_signature.py) | - Testing of LogicSig contracts
- Verification of time-based conditions
- Mocking of transaction parameters | - **logicsig**
- **Account**
- **Txn**
- **Global**
- **op** | +| Box | [test_contract.py](https://github.com/algorandfoundation/algorand-python-testing/blob/main/examples/box/test_contract.py) | - Testing of box storage operations
- Use of enums in box storage | - **ARC4Contract**
- **Box**
- **op** | +| Marketplace | [test_contract.py](https://github.com/algorandfoundation/algorand-python-testing/blob/main/examples/marketplace/test_contract.py) | - Testing of complex marketplace operations
- Use of BoxMap for listings
- Testing of asset transfers and payments | - **ARC4Contract**
- **BoxMap**
- **Asset**
- **arc4 types**
- **Global**
- **Txn** | +| Scratch Storage | [test_contract.py](https://github.com/algorandfoundation/algorand-python-testing/blob/main/examples/scratch_storage/test_contract.py) | - Testing of scratch space usage
- Verification of scratch slot values | - **ARC4Contract**
- **Contract**
- **op** | diff --git a/docs/index.md b/docs/index.md index cb14956..edffaca 100644 --- a/docs/index.md +++ b/docs/index.md @@ -173,4 +173,5 @@ examples coverage glossary api +algopy ``` diff --git a/docs/testing-guide/arc4-types.md b/docs/testing-guide/arc4-types.md index 4d12e20..28e3c64 100644 --- a/docs/testing-guide/arc4-types.md +++ b/docs/testing-guide/arc4-types.md @@ -3,16 +3,27 @@ These types are available under the `algopy.arc4` namespace. Refer to the [ARC4 specification](https://arc.algorand.foundation/ARCs/arc-0004) for more details on the spec. ```{hint} -Test context manager provides _value generators_ for ARC4 types. To access those, use `{context_instance}.arc4` property. See more examples below. +Test context manager provides _value generators_ for ARC4 types. To access their _value generators_, use `{context_instance}.any.arc4` property. See more examples below. ``` ```{note} -For all `algopy.arc4` types with and without respective _value generator_, instantiation can be performed directly. If you have a suggestion for a new _value generator_ implementation, please open an issue in the [`algorand-python-testing`](https://github.com/algorand-sdk/algorand-python-testing) repository or contribute by following the [contribution guide](https://github.com/algorand-sdk/algorand-python-testing/blob/main/CONTRIBUTING.md). +For all `algopy.arc4` types with and without respective _value generator_, instantiation can be performed directly. If you have a suggestion for a new _value generator_ implementation, please open an issue in the [`algorand-python-testing`](https://github.com/algorandfoundation/algorand-python-testing) repository or contribute by following the [contribution guide](https://github.com/algorandfoundation/algorand-python-testing/blob/main/CONTRIBUTING.md). +``` + +```{testsetup} +import algopy +import algopy_testing + +# Create the context manager for snippets below +ctx_manager = algopy_testing_context() + +# Enter the context +ctx = ctx_manager.__enter__() ``` ## Unsigned Integers -```python +```{testcode} from algopy import arc4 # Integer types @@ -23,53 +34,59 @@ uint64_value = arc4.UInt64(18446744073709551615) ... # instantiate test context # Generate a random unsigned arc4 integer with default range -uint8 = ctx.arc4.any_uint8() -uint16 = ctx.arc4.any_uint16() -uint32 = ctx.arc4.any_uint32() -uint64 = ctx.arc4.any_uint64() -uint128 = ctx.arc4.any_biguint128() -uint256 = ctx.arc4.any_biguint256() -uint512 = ctx.arc4.any_biguint512() +uint8 = ctx.any.arc4.uint8() +uint16 = ctx.any.arc4.uint16() +uint32 = ctx.any.arc4.uint32() +uint64 = ctx.any.arc4.uint64() +biguint128 = ctx.any.arc4.biguint128() +biguint256 = ctx.any.arc4.biguint256() +biguint512 = ctx.any.arc4.biguint512() # Generate a random unsigned arc4 integer with specified range -uint8_custom = ctx.arc4.any_uint8(min_value=10, max_value=100) -uint16_custom = ctx.arc4.any_uint16(min_value=1000, max_value=5000) -uint32_custom = ctx.arc4.any_uint32(min_value=100000, max_value=1000000) -uint64_custom = ctx.arc4.any_uint64(min_value=1000000000, max_value=10000000000) -uint128_custom = ctx.arc4.any_biguint128(min_value=1000000000000000, max_value=10000000000000000) -uint256_custom = ctx.arc4.any_biguint256(min_value=1000000000000000000000000, max_value=10000000000000000000000000) -uint512_custom = ctx.arc4.any_biguint512(min_value=1000000000000000000000000000000000, max_value=10000000000000000000000000000000000) +uint8_custom = ctx.any.arc4.uint8(min_value=10, max_value=100) +uint16_custom = ctx.any.arc4.uint16(min_value=1000, max_value=5000) +uint32_custom = ctx.any.arc4.uint32(min_value=100000, max_value=1000000) +uint64_custom = ctx.any.arc4.uint64(min_value=1000000000, max_value=10000000000) +biguint128_custom = ctx.any.arc4.biguint128(min_value=1000000000000000, max_value=10000000000000000) +biguint256_custom = ctx.any.arc4.biguint256(min_value=1000000000000000000000000, max_value=10000000000000000000000000) +biguint512_custom = ctx.any.arc4.biguint512(min_value=10000000000000000000000000000000000, max_value=10000000000000000000000000000000000) ``` ## Address -```python +```{testcode} from algopy import arc4 # Address type address_value = arc4.Address("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ") # Generate a random address -random_address = ctx.arc4.any_address() +random_address = ctx.any.arc4.address() + +# Access native underlaying type +native = random_address.native ``` ## Dynamic Bytes -```python +```{testcode} from algopy import arc4 # Dynamic byte string bytes_value = arc4.DynamicBytes(b"Hello, Algorand!") # Generate random dynamic bytes -random_dynamic_bytes = ctx.arc4.any_dynamic_bytes() +random_dynamic_bytes = ctx.any.arc4.dynamic_bytes(n=123) # n is the number of bits in the arc4 dynamic bytes ``` ## String -```python +```{testcode} from algopy import arc4 # UTF-8 encoded string string_value = arc4.String("Hello, Algorand!") + +# Generate random string +random_string = ctx.any.arc4.string(n=12) # n is the number of bits in the arc4 string ``` diff --git a/docs/testing-guide/avm-types.md b/docs/testing-guide/avm-types.md index 1422b78..36a8e5c 100644 --- a/docs/testing-guide/avm-types.md +++ b/docs/testing-guide/avm-types.md @@ -2,11 +2,19 @@ These types are available directly under the `algopy` namespace. They represent the basic AVM primitive types and can be instantiated directly or via _value generators_: +```{note} +For 'primitive `algopy` types such as `Account`, `Application`, `Asset`, `UInt64`, `BigUint`, `Bytes`, `Sting` with and without respective _value generator_, instantiation can be performed directly. If you have a suggestion for a new _value generator_ implementation, please open an issue in the [`algorand-python-testing`](https://github.com/algorandfoundation/algorand-python-testing) repository or contribute by following the [contribution guide](https://github.com/algorandfoundation/algorand-python-testing/blob/main/CONTRIBUTING.md). +``` + ```{testsetup} import algopy import algopy_testing -ctx = algopy_testing.AlgopyTestContext() +# Create the context manager for snippets below +ctx_manager = algopy_testing_context() + +# Enter the context +ctx = ctx_manager.__enter__() ``` ## UInt64 @@ -23,76 +31,51 @@ random_uint64 = ctx.any.uint64() # Specify a range random_uint64 = ctx.any.uint64(min_value=1000, max_value=9999) -print(random_uint64) -``` - -```{testoutput} -... ``` ## Bytes -```python +```{testcode} # Direct instantiation bytes_value = algopy.Bytes(b"Hello, Algorand!") + # Instantiate test context ... # Generate random byte sequences -random_bytes = ctx.any_bytes() +random_bytes = ctx.any.bytes() # Specify the length -random_bytes = ctx.any_bytes(length=32) +random_bytes = ctx.any.bytes(length=32) ``` ## String -```python +```{testcode} # Direct instantiation string_value = algopy.String("Hello, Algorand!") -# Instantiate test context -... - # Generate random strings -random_string = ctx.any_string() +random_string = ctx.any.string() # Specify the length -random_string = ctx.any_string(length=16) +random_string = ctx.any.string(length=16) ``` ## BigUInt -```python +```{testcode} # Direct instantiation biguint_value = algopy.BigUInt(100) -# Instantiate test context -... - # Generate a random BigUInt value -random_biguint128 = ctx.any_biguint128() - -# Specify a range for UInt128 -random_biguint128 = ctx.any_biguint128(min_value=1000, max_value=999999999999) - -# Generate a random UInt256 value -random_biguint256 = ctx.any_biguint256() - -# Specify a range for UInt256 -random_biguint256 = ctx.any_biguint256(min_value=1000, max_value=999999999999) - -# Generate a random UInt512 value -random_biguint512 = ctx.any_biguint512() - -# Specify a range for UInt512 -random_biguint512 = ctx.any_biguint512(min_value=1000, max_value=999999999999) +random_biguint = ctx.any.biguint() ``` ## Asset -```python +```{testcode} # Direct instantiation asset = algopy.Asset(asset_id=1001) @@ -100,7 +83,7 @@ asset = algopy.Asset(asset_id=1001) ... # Generate a random asset -random_asset = ctx.any_asset( +random_asset = ctx.any.asset( id=..., # Optional: Uses next value under asset id counter in test context creator=..., # Optional: Creator account name=..., # Optional: Asset name @@ -117,11 +100,11 @@ random_asset = ctx.any_asset( ) # Get an asset by ID -asset = ctx.get_asset(asset_id={ID}) +asset = ctx.ledger.get_asset(asset_id=random_asset.id) # Update an asset -ctx.update_asset( - asset_id={ID}, +ctx.ledger.update_asset( + asset_id=random_asset.id, name=..., # Optional: New asset name total=..., # Optional: New total supply decimals=..., # Optional: Number of decimals @@ -133,52 +116,48 @@ ctx.update_asset( freeze=..., # Optional: New freeze address clawback=... # Optional: New clawback address ) - -# Clear an asset by ID -ctx.clear_asset(asset_id=1) ``` ## Account -```python +```{testcode} # Direct instantiation -account = algopy.Account({ADDRESS}) +raw_address = 'PUYAGEGVCOEBP57LUKPNOCSMRWHZJSU4S62RGC2AONDUEIHC6P7FOPJQ4I' +account = algopy.Account(raw_address) # zero address by default # Instantiate test context ... # Generate a random account -random_account = ctx.any_account( - address=..., # Optional: Specify a custom address, defaults to a random address +random_account = ctx.any.account( + address=str(raw_address), # Optional: Specify a custom address, defaults to a random address opted_asset_balances=..., # Optional: Specify opted asset balances as dict of algopy.UInt64 as key and algopy.UInt64 as value - opted_apps=..., # Optional: Specify opted apps as sequence of algopy.Application objects + opted_apps=[], # Optional: Specify opted apps as sequence of algopy.Application objects balance=..., # Optional: Specify an initial balance min_balance=..., # Optional: Specify a minimum balance - auth_addr=..., # Optional: Specify an auth address + auth_address=..., # Optional: Specify an auth address total_assets=..., # Optional: Specify the total number of assets - total_created_assets=..., # Optional: Specify the total number of created assets + total_assets_created=..., # Optional: Specify the total number of created assets total_apps_created=..., # Optional: Specify the total number of created applications total_apps_opted_in=..., # Optional: Specify the total number of applications opted into - total_extra_app_pages=..., # Optional: Specify the total number of extra application pages - rewards=..., # Optional: Specify the rewards - status=... # Optional: Specify the account status + total_extra_app_pages=..., # Optional: Specify the total number of extra ) # Generate a random account that is opted into a specific asset -mock_asset = ctx.any_asset() -mock_account = ctx.any_account( - opted_asset_balances={mock_asset.id: algopy.UInt64({TEST_BALANCE})} +mock_asset = ctx.any.asset() +mock_account = ctx.any.account( + opted_asset_balances={mock_asset.id: algopy.UInt64(123)} ) # Get an account by address -account = ctx.get_account(str(mock_account)) +account = ctx.ledger.get_account(str(mock_account)) # Update an account -ctx.update_account( - "{ADDRESS}", +ctx.ledger.update_account( + str(mock_account), balance=..., # Optional: New balance min_balance=..., # Optional: New minimum balance - auth_addr=..., # Optional: New auth address + auth_address=ctx.any.account(), # Optional: New auth address total_assets=..., # Optional: New total number of assets total_created_assets=..., # Optional: New total number of created assets total_apps_created=..., # Optional: New total number of created applications @@ -188,42 +167,38 @@ ctx.update_account( status=... # Optional: New account status ) -# Get opted asset balance for an account -opted_asset_balance = ctx.get_opted_asset_balance(account, asset_id) - -# Clear all accounts -ctx.clear_accounts() +# Check if an account is opted into a specific asset +opted_in = account.is_opted_in(mock_asset) ``` ## Application -```python +```{testcode} # Direct instantiation -application = algopy.Application(application_id=1001) +application = algopy.Application() # Instantiate test context ... # Generate a random application -random_app = ctx.any_application( - id=..., # Optional: Specify a custom ID, defaults to the next available ID in test context +random_app = ctx.any.application( address=..., # Optional: Specify a custom address for the application - approval_program=..., # Optional: Specify a custom approval program - clear_state_program=..., # Optional: Specify a custom clear state program - global_num_uint=..., # Optional: Number of global uint values - global_num_bytes=..., # Optional: Number of global byte values - local_num_uint=..., # Optional: Number of local uint values - local_num_bytes=..., # Optional: Number of local byte values - extra_program_pages=..., # Optional: Number of extra program pages - creator=... # Optional: Specify the creator account + approval_program=algopy.Bytes(b''), # Optional: Specify a custom approval program + clear_state_program=algopy.Bytes(b''), # Optional: Specify a custom clear state program + global_num_uint=algopy.UInt64(1), # Optional: Number of global uint values + global_num_bytes=algopy.UInt64(1), # Optional: Number of global byte values + local_num_uint=algopy.UInt64(1), # Optional: Number of local uint values + local_num_bytes=algopy.UInt64(1), # Optional: Number of local byte values + extra_program_pages=algopy.UInt64(1), # Optional: Number of extra program pages + creator=ctx.default_sender # Optional: Specify the creator account ) # Get an application by ID -app = ctx.get_application(app_id={ID}) +app = ctx.ledger.get_app(app_id=random_app.id) # Update an application -ctx.update_application( - app_id={ID}, +ctx.ledger.update_app( + app_id=random_app.id, approval_program=..., # Optional: New approval program clear_state_program=..., # Optional: New clear state program global_num_uint=..., # Optional: New number of global uint values @@ -234,25 +209,13 @@ ctx.update_application( creator=... # Optional: New creator account ) -# Add logs for an application -ctx.add_application_logs( - app_id={ID}, - logs=b"log entry" or [b"log entry 1", b"log entry 2"], - prepend_arc4_prefix=False # Optional: Prepend ARC4 prefix to logs -) - -# Get logs for an application -app_logs = ctx.get_application_logs(app_id={ID}) - -# Set the active application -ctx.set_active_contract(contract) - -# Get the active application -active_app = ctx.get_active_application() +# Patch logs for an application. When accessing via transactions or inner transaction related opcodes, will return the patched logs unless new logs where added into the transaction during execution. +test_app = ctx.any.application(logs=b"log entry" or [b"log entry 1", b"log entry 2"]) -# Clear all applications -ctx.clear_applications() +# Get app associated with the active contract +class MyContract(algopy.ARC4Contract): + ... -# Clear all application logs -ctx.clear_application_logs() +contract = MyContract() +active_app = ctx.get_app_for_contract(contract) ``` diff --git a/docs/testing-guide/concepts.md b/docs/testing-guide/concepts.md index 660293c..2f5c9b5 100644 --- a/docs/testing-guide/concepts.md +++ b/docs/testing-guide/concepts.md @@ -1,109 +1,58 @@ # Concepts -The following sections provide an overview of the key concepts and features of the Algorand Python Testing framework. +The following sections provide an overview of key concepts and features in the Algorand Python Testing framework. ## Test Context -The main abstraction to interact with the testing framework is the [`AlgopyTestContext`](../api-context.md#algopy_testing.AlgopyTestContext). It creates an emulated Algorand environment that closely mimics AVM behavior relevant to unit testing the contracts and provides a Pythonic interface for interacting with the emulated environment. +The main abstraction for interacting with the testing framework is the [`AlgopyTestContext`](../api-context.md#algopy_testing.AlgopyTestContext). It creates an emulated Algorand environment that closely mimics AVM behavior relevant to unit testing the contracts and provides a Pythonic interface for interacting with the emulated environment. ```python from algopy_testing import algopy_testing_context def test_my_contract(): - # 1. Instantiating the test context + # Recommended way to instantiate the test context with algopy_testing_context() as ctx: - ... # your test code here - - # 2. Alternatively, you can instantiate the test context manually - ctx = AlgopyTestContext() - ... # your test code here - ctx.reset() # Reset the emulated environment + # Your test code here + pass + # ctx is automatically reset after the test code is executed ``` -In short, context manager exposes three main properties: - -1. `.any` - A property returning an instance of AlgopyValueGenerator. This provides methods for generating randomized test data for various AVM types like accounts, assets, applications, transactions, ARC4 types, etc. It allows generating constrained random values when exact values are not needed. -2. `.ledger` - A property returning an instance of LedgerContext. This provides methods for interacting with and querying the emulated Algorand ledger state, including accounts, assets, applications, global state, etc. -3. `.txn` - A property returning an instance of TransactionContext. This provides methods for creating and managing transaction groups, submitting transactions, and accessing transaction results in the emulated environment. - -Certainly! I'll provide a concise overview of the user-facing usage for the `LedgerContext` and `TransactionContext` classes without being redundant. For detailed method signatures and docstrings, users should refer to the auto-generated API documentation in `api-context.md`. - -### Managing Test Context State - -1. **Automatic Reset with Context Manager**: - - ```python - with algopy_testing_context() as ctx: - ... # your test code here - # Context is reset after exiting the block - ``` - - Recommended for its automatic management and efficiency. - -2. **Manual Reset with `reset()`**: - ```python - ctx = AlgopyTestContext() - ... # your test code here - ctx.reset() - ``` - Thoroughly resets the context, reinitializing all data structures and settings. Ideal for unrelated test suites. - -## Context properties +The context manager interface exposes three main properties: -These classes are part of the Algorand Python Testing framework and provide methods for interacting with the emulated Algorand environment. +1. `ledger`: An instance of `LedgerContext` for interacting with and querying the emulated Algorand ledger state. +2. `txn`: An instance of `TransactionContext` for creating and managing transaction groups, submitting transactions, and accessing transaction results. +3. `any`: An instance of `AlgopyValueGenerator` for generating randomized test data. -### 1. Ledger +For detailed method signatures, parameters, and return types, refer to the following API sections: +- [`algopy_testing.LedgerContext`](../api.md) +- [`algopy_testing.TransactionContext`](../api.md) +- [`algopy_testing.AVMValueGenerator`, `algopy_testing.TxnValueGenerator`, `algopy_testing.ARC4ValueGenerator`](../api.md) -The `LedgerContext` allows you to interact with and query the emulated Algorand ledger state. Key operations include: +The `any` property provides access to different value generators: -1. Account management (get, check existence, update) -2. Asset operations (get, check existence, update) -3. Application interactions (get, check existence, update) -4. State management (global and local) -5. Box operations (get, set, delete, check existence) -6. Block operations (set, get content) -7. `algopy.Global` fields patching +- `AVMValueGenerator`: Base abstractions for AVM types. All methods are available directly on the instance returned from `any`. +- `TxnValueGenerator`: Accessible via `any.txn`, for transaction-related data. +- `ARC4ValueGenerator`: Accessible via `any.arc4`, for ARC4 type data. -> Refer to the [`algopy_testing.LedgerContext`](../api.md) in the API section for detailed method signatures, parameters and return types. +These generators allow creation of constrained random values for various AVM entities (accounts, assets, applications, etc.) when specific values are not required. -### 2. Transactions +```{hint} +Value generators are powerful tools for generating test data for specified AVM types. They allow further constraints on random value generation via arguments, making it easier to generate test data when exact values are not necessary. -The `TransactionContext` enables creation and management of transaction groups, submission of transactions, and access to transaction results. Key features include: +When used with the 'Arrange, Act, Assert' pattern, value generators can be especially useful in setting up clear and concise test data in arrange steps. -1. Transaction group management -2. Deferred application calls -3. Inner transaction handling -4. Access to individual transactions -5. Scratch space operations - -> Refer to the [`algopy_testing.TransactionContext`](../api.md) for detailed method signatures, parameters and return types. - -### 3. Value generators - -Testing context provides an range of helper methods called _value generators_ which allow quick generate and/or instantiation of randomized values for specified AVM types, which is also a common building block in _property-based_ testing methodologies. To access them, refer to methods prefixed with word `any_*` or `arc4.any_*` on the test context instance. - -For detailed breakdown of all available value generators and their arguments, refer to the [API docs](api.md). - -```{note} -Value generators are a powerful tool for generating test data for specified AVM types. Additionally, they allow further constraints on random value generation via arguments, making it easier to generate test data when the exact values are not necessary as long as the generated values meet the constraints. - -If used with the 'Arrange, Act, Assert' pattern, value generators can be especially useful in setting up clear and concise test data in arrange steps. +They can also serve as a base building block that can be integrated/reused with popular Python property-based testing frameworks like [`hypothesis`](https://hypothesis.readthedocs.io/en/latest/). ``` -> Refer to the [`algopy_testing.AVMValueGenerator`, `algopy_testing.TxnValueGenerator`, `algopy_testing.ARC4ValueGenerator`](../api.md) for detailed method signatures, parameters and return types. - -#### Property-based testing - -`algorand-python-testing` aims to be agnostic of the specific Python testing framework being used. The [`value generators`](#value-generators), serve as a base building block that can be integrated/reused with popular Python property-based testing frameworks like [`hypothesis`](https://hypothesis.readthedocs.io/en/latest/). - ## Types of `algopy` stub implementations -As explained in the [introduction](index.md), `algorand-python-testing` _injects_ test implementations for stubs available in `algorand-python` package. However, not all of the stubs implemented in the same manner: +As explained in the [introduction](index.md), `algorand-python-testing` _injects_ test implementations for stubs available in the `algorand-python` package. However, not all of the stubs are implemented in the same manner: -1. **Native**: Fully matches AVM computation in Python. For example, `algopy.op.sha256` and other cryptographic operations behave identically in AVM and unit tests. This implies the majority of opcodes that are 'pure' functions in AVM also have a native Python implementation provided by this package. These abstractions and opcodes can be used within and outside of the testing context. +1. **Native**: Fully matches AVM computation in Python. For example, `algopy.op.sha256` and other cryptographic operations behave identically in AVM and unit tests. This implies that the majority of opcodes that are 'pure' functions in AVM also have a native Python implementation provided by this package. These abstractions and opcodes can be used within and outside of the testing context. 2. **Emulated**: Uses `AlgopyTestContext` to mimic AVM behavior. For example, `Box.put` on an `algopy.Box` within a test context stores data in the test manager, not the real Algorand network, but provides the same interface. -3. **Mockable**: Not implemented but can be mocked or patched. For example, `algopy.abi_call` can be mocked to return specific values or behaviors; otherwise, it raises a `NotImplementedError`. In other words, this category covers the cases where native or emulated implementation in a unit test context is impractical or overly complex. +3. **Mockable**: Not implemented, but can be mocked or patched. For example, `algopy.abi_call` can be mocked to return specific values or behaviors; otherwise, it raises a `NotImplementedError`. This category covers cases where native or emulated implementation in a unit test context is impractical or overly complex. For a full list of all public `algopy` types and their corresponding implementation category, refer to the [Coverage](coverage.md) section. +``` diff --git a/docs/testing-guide/contract-testing.md b/docs/testing-guide/contract-testing.md index d6bba3c..11254f9 100644 --- a/docs/testing-guide/contract-testing.md +++ b/docs/testing-guide/contract-testing.md @@ -8,13 +8,13 @@ This guide provides an overview of how to test smart contracts using the Algoran The code snippets showcasing the contract testing capabilities are using [pytest](https://docs.pytest.org/en/latest/) as the test framework. However, note that the `algorand-python-testing` package can be used with any other test framework that supports Python. `pytest` is used for demonstration purposes in this documentation. ``` -## ARC4Contract +## `algopy.ARC4Contract` Classes prefixed with `algopy.ARC4Contract` are **required** to be instantiated withing test context. As part of instantiation, the test context will automatically create a matching `algopy.Application` object instance. -Within the class implementation, methods decorated with `algopy.arc4.abimethod` and `algopy.arc4.baremethod` will automatically assemble an `algopy.gtxn.ApplicationCallTransaction` transaction to emulate the AVM application call. This behavior can be overriden by setting the transaction group manually as part of test setup, this is done via implicit invocation of [algopy_testing.context.any_application()](#algopy_testing.context.AlgopyTestContext.any_application) _value generator_. +Within the class implementation, methods decorated with `algopy.arc4.abimethod` and `algopy.arc4.baremethod` will automatically assemble an `algopy.gtxn.ApplicationCallTransaction` transaction to emulate the AVM application call. This behavior can be overriden by setting the transaction group manually as part of test setup, this is done via implicit invocation of `algopy_testing.context.any_application()` _value generator_ (refer to [APIs](../apis.md) for more details). -```python +```{testcode} from algopy import ARC4Contract, GlobalState, LocalState, UInt64, Txn, arc4 from algopy_testing import algopy_testing_context import pytest @@ -102,13 +102,13 @@ def test_simple_voting_contract(context): For more examples of tests using `algopy.ARC4Contract`, see the [examples](../examples.md) section. -## Contract +## `algopy.Contract`` Classes prefixed with `algopy.Contract` (parent class of `algopy.ARC4Contract`) are **required** to be instantiated withing test context. As part of instantiation, the test context will automatically create a matching `algopy.Application` object instance. This behaviour is identical to `algopy.ARC4Contract` class instances. Unlike `algopy.ARC4Contract`, `algopy.Contract` requires manual setup of the transaction context and explicit method calls. Here's an example demonstrating how to test a `Contract` class: -```python +```{testcode} import algopy import pytest from algopy_testing import AlgopyTestContext, algopy_testing_context @@ -117,16 +117,16 @@ class CounterContract(algopy.Contract): def __init__(self): self.counter = algopy.UInt64(0) - @algopy.submethod + @algopy.subroutine def increment(self): self.counter.set(self.counter.value + algopy.UInt64(1)) return algopy.UInt64(1) - @algopy.baremethod + @algopy.arc4.baremethod def approval_program(self): return self.increment() - @algopy.baremethod + @algopy.arc4.baremethod def clear_state_program(self): return algopy.UInt64(1) @@ -167,3 +167,24 @@ def test_counter_contract(context: AlgopyTestContext): # Test clear state program assert contract.clear_state_program() == algopy.UInt64(1) ``` + +## Defer contract method invocation + +You can create deferred application calls for more complex testing scenarios where order of transactions needs to be controlled: + +```python +def test_deferred_call(context): + contract = MyARC4Contract() + + extra_payment = context.any.txn.payment() + extra_asset_transfer = context.any.txn.asset_transfer() + implicit_payment = context.any.txn.payment() + deferred_call = context.txn.defer_app_call(contract.some_method, implicit_payment) + + with context.txn.create_group([extra_payment, deferred_call, extra_asset_transfer]): + result = deferred_call.submit() + + print(context.txn.last_group) # [extra_payment, implicit_payment, app call, extra_asset_transfer] +``` + +A deferred application call, assembles the application call transaction and can be later executed by calling `.submit()` method on the deferred application call instance. As shown on the example, it can also be passed to the transaction group creation context manager to be executed as part of the transaction group. In such cases if there are more than an application call inside the deferred objects, they will be executed in the order they were added to the transaction group. diff --git a/docs/testing-guide/index.md b/docs/testing-guide/index.md index d509736..9ed6369 100644 --- a/docs/testing-guide/index.md +++ b/docs/testing-guide/index.md @@ -6,7 +6,22 @@ The Algorand Python Testing framework provides powerful tools for testing Algora For all code examples in the _Testing Guide_ section, assume `context` is an instance of `AlgopyTestContext` obtained using the `algopy_testing_context()` context manager. All subsequent code is executed within this context. ``` -![](https://mermaid.ink/img/pako:eNp1kk1OwzAQha8yMptWClKQEIsIVUob9kiwQTVCbjxJTBM7mtiFqOkRWHEArsgRcGjVJvx4YY3nfR7PG3nLUiORRSwnURdwn3ANfjVutU9w9mAcQYIbLE1dobZwozeKjO5jzvZ4v-IlZ6LMTd3CxLY1QmPdqply9nhi5kfmyWJjlc49ewgyEhW-GFpPr1c0m_hnQRBCgX77_Hh_G1da-Eq1awVMUlPVqkQ66agl13_7iP3jJLSEWzLPmI4MJMsfSGsLoyE12pJI7e_iCZyfz7pvr4XStgGlMyRC6b2YqoN4iB3alOAa77aDxaiGn4GXNkp0MD_2btsSIQZ6jS7CgFq_D4X5UAgyVZbRWRheXYYjavHf9WQosIBVSJVQ0v-EbY9xZguskLPIh1LQup_UznPCWXPX6pRFlhwGjIzLCxZlomz8ydVSWEyU8FOvDtndF7zxxLg?type=png) +```{mermaid} +graph TD + subgraph GA["Your Development Environment"] + A["algopy (type stubs)"] + B["algopy_testing (testing framework)
(You are here 📍)"] + C["puya (compiler)"] + end + + subgraph GB["Your Algorand Project"] + D[Your Algorand Python contract] + end + + D -->|type hints inferred from| A + D -->|compiled using| C + D -->|tested via| B +``` > _High-level overview of the relationship between your smart contracts project, Algorand Python Testing framework, Algorand Python type stubs, and the compiler_ diff --git a/docs/testing-guide/opcodes.md b/docs/testing-guide/opcodes.md index ee63fa9..9fb8905 100644 --- a/docs/testing-guide/opcodes.md +++ b/docs/testing-guide/opcodes.md @@ -1,7 +1,278 @@ # AVM Opcodes -The [coverage](coverage.md) file contains a complete list of all opcodes and respective types as well as whether they are _Mockable_, _Emulated_, or _Native_ within the `algorand-python-testing` package. The following section will highlight common opcodes and types that usually require interaction with test context manager. `Native` opcodes are assumed to function as they are in the Algorand Virtual Machine given that their functionality is stateless, if you experience any issues with any of the `Native` opcodes, please raise an issue in the [`algorand-python-testing` repo](https://github.com/algorandfoundation/algorand-python-testing/issues/new/choose) repository or contribute a PR by following [Contributing](https://github.com/algorandfoundation/algorand-python-testing/blob/main/CONTRIBUTING.md) guide. +The [coverage](coverage.md) file contains a complete list of all opcodes and respective types as well as whether they are _Mockable_, _Emulated_, or _Native_ within the `algorand-python-testing` package. The following section will highlight **only** common opcodes and types that usually require interaction with test context manager. `Native` opcodes are assumed to function as they are in the Algorand Virtual Machine given that their functionality is stateless, if you experience any issues with any of the `Native` opcodes, please raise an issue in the [`algorand-python-testing` repo](https://github.com/algorandfoundation/algorand-python-testing/issues/new/choose) repository or contribute a PR by following [Contributing](https://github.com/algorandfoundation/algorand-python-testing/blob/main/CONTRIBUTING.md) guide. -## Mockable types +## Implemented Types -Refer to [coverage](coverage.md) to see which opcodes are of type _Mockable_ - implying that they aren't either emulated or implemented by `algorand-python-testing` due to complexity or edge cases which require real AVM interaction. +These types are fully implemented in Python and behave identically to their AVM counterparts. Some examples include: + +### 1. Cryptographic Operations + +```{testcode} +import algopy.op as op + +# SHA256 hash +data = algopy.Bytes(b"Hello, World!") +hashed = op.sha256(data) + +# Keccak256 hash +keccak_hashed = op.keccak256(data) + +# ECDSA verification +message_hash = bytes.fromhex( + "f809fd0aa0bb0f20b354c6b2f86ea751957a4e262a546bd716f34f69b9516ae1" +) +sig_r = bytes.fromhex("18d96c7cda4bc14d06277534681ded8a94828eb731d8b842e0da8105408c83cf") +sig_s = bytes.fromhex("7d33c61acf39cbb7a1d51c7126f1718116179adebd31618c4604a1f03b5c274a") +pubkey_x = bytes.fromhex("f8140e3b2b92f7cbdc8196bc6baa9ce86cf15c18e8ad0145d50824e6fa890264") +pubkey_y = bytes.fromhex("bd437b75d6f1db67155a95a0da4b41f2b6b3dc5d42f7db56238449e404a6c0a3") + +result = op.ecdsa_verify(op.ECDSA.Secp256r1, message_hash, sig_r, sig_s, pubkey_x, pubkey_y) +assert result +``` + +### 2. Arithmetic and Bitwise Operations + +```{testcode} +import algopy.op as op + +# Addition with carry +result, carry = op.addw(algopy.UInt64(2**63), algopy.UInt64(2**63)) + +# Bitwise operations +value = algopy.UInt64(42) +bit_length = op.bitlen(value) +is_bit_set = op.getbit(value, 3) +new_value = op.setbit_uint64(value, 2, 1) +``` + +> Native types are implemented in Python and execute as per their stubs. For a **full** list of all opcodes and types, see the [coverage](../coverage.md) page. + +## Emulated Types Requiring Transaction Context + +These types require interaction with the transaction context to set or update them: + +### 1. Global Values + +```{testcode} +from algopy_testing import algopy_testing_context +import algopy.op as op + +with algopy_testing_context() as ctx: + # Patch global fields + ctx.ledger.patch_global_fields( + min_txn_fee=algopy.UInt64(1000), + min_balance=algopy.UInt64(100000) + ) + + # Access global values in your contract + class MyContract(algopy.ARC4Contract): + @algopy.arc4.abimethod + def check_globals(self) -> algopy.UInt64: + return op.Global.min_txn_fee + op.Global.min_balance + + contract = MyContract() + result = contract.check_globals() + assert result == algopy.UInt64(101000) +``` + +### 2. Transaction Fields + +```python +from algopy_testing import algopy_testing_context +import algopy.op as op + +with algopy_testing_context() as ctx: + class MyContract(algopy.ARC4Contract): + @algopy.arc4.abimethod + def check_txn_fields(self) -> algopy.Bytes: + return op.Txn.sender + + contract = MyContract() + + # Set custom transaction fields + custom_sender = ctx.any.account() + with ctx.txn.create_group(txn_op_fields={"sender": custom_sender}): + result = contract.check_txn_fields() + + assert result == custom_sender.bytes +``` + +### 3. Asset Holdings and Parameters + +```python +from algopy_testing import algopy_testing_context +import algopy.op as op + +with algopy_testing_context() as ctx: + class AssetContract(algopy.ARC4Contract): + @algopy.arc4.abimethod + def check_asset_holding(self, account: algopy.Account, asset: algopy.Asset) -> algopy.UInt64: + balance, _ = op.AssetHoldingGet.asset_balance(account, asset) + return balance + + # Create an asset and set up holdings + asset = ctx.any.asset(total=algopy.UInt64(1000000)) + account = ctx.any.account(opted_asset_balances={asset.id: algopy.UInt64(5000)}) + + contract = AssetContract() + result = contract.check_asset_holding(account, asset) + assert result == algopy.UInt64(5000) +``` + +### 4. Application Local and Global State + +```python +from algopy_testing import algopy_testing_context +import algopy.op as op + +with algopy_testing_context() as ctx: + class StateContract(algopy.ARC4Contract): + @algopy.arc4.abimethod + def set_and_get_state(self, key: algopy.Bytes, value: algopy.UInt64) -> algopy.UInt64: + op.AppGlobal.put(key, value) + return op.AppGlobal.get_uint64(key) + + contract = StateContract() + key = algopy.Bytes(b"test_key") + value = algopy.UInt64(42) + + result = contract.set_and_get_state(key, value) + assert result == value + + # Verify state outside the contract + stored_value = ctx.ledger.get_global_state(contract, key) + assert stored_value == 42 +``` + +# Mockable Opcodes + +This section covers the mockable opcodes in `algorand-python-testing` and demonstrates how to mock them in unit tests. This category covers the cases where native or emulated implementation in a unit test context is impractical or overly complex. + +## algopy.compile_contract + +Used to compile a contract. In tests, you can mock its behavior to return predefined compilation results. + +```{testcode} +import unittest +from unittest.mock import patch, MagicMock +import algopy +from algopy_testing.primitives import Bytes + +mocked_response = MagicMock() +mocked_response.approval_program = (Bytes(b'mock_approval'), Bytes(b'mock_approval')) +mocked_response.clear_state_program = (Bytes(b'mock_clear'), Bytes(b'mock_clear')) + +class MockContract(algopy.Contract): + pass + +with patch('algopy.compile_contract', return_value=mocked_response) as mock_compile_contract: + compiled = algopy.compile_contract(MockContract) + +compiled.approval_program == (Bytes(b'mock_approval'), Bytes(b'mock_approval')) +compiled.clear_state_program == (Bytes(b'mock_clear'), Bytes(b'mock_clear')) +mock_compile_contract.assert_called_once_with(MockContract) +``` + +## algopy.arc4.abi_call + +Used for ABI method calls. Mocking allows testing contract interactions without actual execution. + +```{testcode} +import unittest +from unittest.mock import patch +import algopy +from algopy_testing.primitives import UInt64 + +class MyContract(algopy.ARC4Contract): + @algopy.arc4.abimethod + def my_method(self, arg1: algopy.UInt64, arg2: algopy.UInt64) -> algopy.UInt64: + return algopy.arc4.abi_call("my_other_method", arg1, arg2) + +class MyOtherContract(algopy.ARC4Contract): + @algopy.arc4.abimethod + def my_other_method(self, arg1: algopy.UInt64, arg2: algopy.UInt64) -> algopy.UInt64: + return arg1 + arg2 + +def test_mock_abi_call(): + with algopy_testing_context() as ctx: + mock_abi_call = MagicMock() + mock_abi_call.return_value = UInt64(11) + contract = MyContract() + + with patch('algopy.arc4.abi_call', mock_abi_call): + result = contract.my_method(UInt64(10), UInt64(1)) + + assert result == UInt64(11) + mock_abi_call.assert_called_once_with("my_other_method", UInt64(10), UInt64(1)) + +test_mock_abi_call() +``` + +## algopy.op.vrf_verify + +Verifiable Random Function (VRF) verification. Mocking is useful for testing without actual cryptographic computations. + +```{testcode} +import unittest +from unittest.mock import patch +import algopy +from algopy_testing.primitives import Bytes + +def test_mock_vrf_verify(): + mock_result = (Bytes(b'mock_output'), True) + + with patch('algopy.op.vrf_verify', MagicMock(return_value=mock_result)) as mock_vrf_verify: + result = algopy.op.vrf_verify( + algopy.op.VrfVerify.VrfAlgorand, + Bytes(b'proof'), + Bytes(b'message'), + Bytes(b'public_key') + ) + + assert result == mock_result + mock_vrf_verify.assert_called_once_with( + algopy.op.VrfVerify.VrfAlgorand, + Bytes(b'proof'), + Bytes(b'message'), + Bytes(b'public_key') + ) + +test_mock_vrf_verify() +``` + +## algopy.op.EllipticCurve + +Elliptic curve operations. Mocking allows testing without actual cryptographic computations. + +```{testcode} +import unittest +from unittest.mock import patch +import algopy +from algopy_testing.primitives import Bytes + +def test_mock_elliptic_curve_decompress(): + mock_result = (Bytes(b'x_coord'), Bytes(b'y_coord')) + + with patch('algopy.op.EllipticCurve.decompress', MagicMock(return_value=mock_result)) as mock_decompress: + result = algopy.op.EllipticCurve.decompress( + algopy.op.EC.BN254g1, + Bytes(b'compressed_point') + ) + + assert result == mock_result + mock_decompress.assert_called_once_with( + algopy.op.EC.BN254g1, + Bytes(b'compressed_point') + ) + +test_mock_elliptic_curve_decompress() +``` + +These examples demonstrate how to mock key mockable opcodes in `algorand-python-testing`. Use similar techniques (in your preferred testing framework) for other mockable opcodes like `algopy.compile_logicsig`, `algopy.arc4.arc4_create`, and `algopy.arc4.arc4_update`. + +Mocking these opcodes allows you to: + +1. Control complex operations' behavior not covered by _implemented_ and _emulated_ types. +2. Test edge cases and error conditions. +3. Isolate contract logic from external dependencies. diff --git a/docs/testing-guide/signature-testing.md b/docs/testing-guide/signature-testing.md index 9681591..199e288 100644 --- a/docs/testing-guide/signature-testing.md +++ b/docs/testing-guide/signature-testing.md @@ -1,76 +1,69 @@ -# Smart Signature testing +# Smart Signature Testing -Smart signatures, also known as logic signatures or LogicSigs, are programs that can be used to sign transactions. The Algorand Python Testing framework provides support for testing these programs. +Test Algorand smart signatures (LogicSigs) with ease using the Algorand Python Testing framework. -## Defining a Logic Signature +```{testsetup} +import algopy +import algopy_testing -To define a logic signature, you can use the `@logicsig` decorator: +# Create the context manager for snippets below +ctx_manager = algopy_testing_context() -```python +# Enter the context +context = ctx_manager.__enter__() +``` + +## Define a LogicSig + +Use the `@logicsig` decorator to create a LogicSig: + +```{testcode} from algopy import logicsig, Account, Txn, Global, UInt64, Bytes @logicsig def hashed_time_locked_lsig() -> bool: - # Your logic signature code here - return True # Approve the transaction + # LogicSig code here + return True # Approve transaction ``` -## Executing a Logic Signature - -To test a logic signature, use the `execute_logicsig` method of the `AlgopyTestContext`. You can provide arguments to the logic signature using the `scoped_lsig_args` context manager: - -```python -from algopy_testing import AlgopyTestContext +## Execute and Test -def test_logic_signature(context: AlgopyTestContext) -> None: - # Set up the transaction group - context.set_transaction_group( - [ - context.any_payment_transaction( - # Transaction fields... - ), - ], - active_transaction_index=0, - ) +Use `AlgopyTestContext.execute_logicsig()` to run and verify LogicSigs: - # Execute the logic signature with arguments - with context.scoped_lsig_args([algopy.Bytes(b"secret")]): - result = context.execute_logicsig(hashed_time_locked_lsig) +```{testcode} +with context.txn.create_group([ + context.any.txn.payment(), +]): + result = context.execute_logicsig(hashed_time_locked_lsig, algopy.Bytes(b"secret")) - assert result is True +assert result is True ``` -The `execute_logicsig` method takes one parameter which is an `lsig` itself, an instance of the logic signature function decorated with `@logicsig`. +`execute_logicsig()` returns a boolean: -The method returns the result of executing the logic signature, which is a `bool`: +- `True`: Transaction approved +- `False`: Transaction rejected -- If the logic signature returns `True`, it emulates approval of the transaction. -- If it returns `False`, it emulates rejection of the transaction. +## Pass Arguments -## Using `scoped_lsig_args` +Provide arguments to LogicSigs using `execute_logicsig()`: -The [`scoped_lsig_args`](#algopy_testing.context.AlgopyTestContext.scoped_lsig_args) context manager allows you to provide arguments to the logic signature for the duration of its execution. This is particularly useful when your logic signature expects input parameters accessed via `algopy.op.arg(n)`. - -```python -with context.scoped_lsig_args([algopy.Bytes(b"secret")]): - result = context.execute_logicsig(hashed_time_locked_lsig) +```{testcode} +result = context.execute_logicsig(hashed_time_locked_lsig, algopy.Bytes(b"secret")) ``` -In this example, `b"secret"` is passed as an argument to the logic signature. Inside the logic signature, you can access this argument using `algopy.op.arg(0)`. - -## Accessing Arguments in the Logic Signature - -Within your logic signature, you can access the provided arguments using the `algopy.op.arg()` function: +Access arguments in the LogicSig with `algopy.op.arg()` opcode: -```python +```{testcode} @logicsig def hashed_time_locked_lsig() -> bool: secret = algopy.op.arg(0) - # Use the secret in your logic - is_secret_correct = algopy.op.sha256(secret) == expected_hash - # ... rest of the logic -``` + expected_hash = algopy.op.sha256(algopy.Bytes(b"secret")) + return algopy.op.sha256(secret) == expected_hash -```{hint} -For coverage details on `algopy.op.arg` and other available operations, see the [Algorand Python Testing Framework Reference](../coverage.md). +# Example usage +secret = algopy.Bytes(b"secret") +assert context.execute_logicsig(hashed_time_locked_lsig, secret) ``` + +For more details on available operations, see the [coverage](../coverage.md). diff --git a/docs/testing-guide/state-management.md b/docs/testing-guide/state-management.md index 5e1c5b2..73cd709 100644 --- a/docs/testing-guide/state-management.md +++ b/docs/testing-guide/state-management.md @@ -1,150 +1,100 @@ # State Management -`algorand-python-testing` provides functionality to unit test all state related abstractions available in AVM and represented by `algorand-python` stubs: +`algorand-python-testing` provides tools to test state-related abstractions in Algorand smart contracts. This guide covers global state, local state, boxes, and scratch space management. -1. `algopy.Global` -2. `algopy.LocalState` -3. `algopy.GlobalState` -4. `algopy.Box`, `algopy.BoxRef`, `algopy.BoxMap` -5. `algopy.op.Box` ops -6. `algopy.op.Scratch` ops +```{testsetup} +import algopy +import algopy_testing -## Patching State +# Create the context manager for snippets below +ctx_manager = algopy_testing_context() -### Distinction between Global and GlobalState - -In the context of Algorand smart contracts, it is important to understand the distinction between `Global` and `GlobalState` as represented in the `algorand-python` stubs. - -#### Global - -`Global` is a class that provides access to global properties and values within the Algorand Virtual Machine (AVM). These properties are not specific to any particular application but are instead global to the entire blockchain. Examples of such properties include the current round number, the minimum transaction fee, and the latest confirmed block timestamp. The `Global` class allows smart contracts to query these values using native TEAL opcodes. - -```python -... # instantiate test context -ctx.patch_global_fields( - min_txn_fee=..., # Optional: Minimum transaction fee - min_balance=..., # Optional: Minimum balance - max_txn_life=..., # Optional: Maximum transaction lifetime - zero_address=..., # Optional: Zero address - creator_address=..., # Optional: Creator address - asset_create_min_balance=..., # Optional: Minimum balance for asset creation - asset_opt_in_min_balance=..., # Optional: Minimum balance for asset opt-in - genesis_hash=..., # Optional: Genesis hash - latest_timestamp=... # Optional: Latest timestamp -) +# Enter the context +context = ctx_manager.__enter__() ``` -## GlobalState - -Global state within the test context is represented as instance attributes on instances of `algopy.Contract` and `algopy.ARC4Contract` classes. +## Global State -Therefore, to modify values of global state of particular contract instance, you would do so as if you were modifying a regular Python instance attribute. +Global state is represented as instance attributes on `algopy.Contract` and `algopy.ARC4Contract` classes. -```python -# Assume some contract instance -class SomeContract(algopy.ARC4Contract): - def __init__(self) -> None: - # Notice that `state_a` defines global state via `GlobalState` proxy class - self.state_a = algopy.GlobalState(algopy.UInt64, key="global_uint64") - # `state_b` showcases global state declaration as a regular Python instance attribute +```{testcode} +class MyContract(algopy.ARC4Contract): + def __init__(self): + self.state_a = algopy.GlobalState(algopy.UInt64, key="global_uint64") self.state_b = algopy.UInt64(1) -... # instantiate test context -contract = SomeContract() - -# Modify global state +# In your test +contract = MyContract() contract.state_a = algopy.UInt64(10) contract.state_b = algopy.UInt64(20) ``` ## Local State -Similar to global state, local state is defined as a regular Python instance attribute on contract instances. +Local state is defined similarly to global state, but accessed using account addresses as keys. ```python -# Assume some contract instance -class SomeContract(algopy.ARC4Contract): - def __init__(self) -> None: - # Notice that `state_a` defines global state via `GlobalState` proxy class - self.local_state_a = algopy.LocalState(algopy.UInt64, key="state_a") +class MyContract(algopy.ARC4Contract): + def __init__(self): + self.local_state_a = algopy.LocalState(algopy.UInt64, key="state_a") -... # instantiate test context -contract = SomeContract() - -# Modify local state -account = ctx.any_account() - -## Access is done via account address as key. Existence of specific key represents whether account is opted-in to the contract to store local state. +# In your test +contract = MyContract() +account = context.any.account() contract.local_state_a[account] = algopy.UInt64(10) ``` ## Boxes -The testing framework covers whole set of Box abstractions available in `algorand-python`. Below demonstrates usage of test context to access Boxes used during contract 'execution' in an emulated AVM environment. +The framework supports various Box abstractions available in `algorand-python`. -```python -class SomeContract(algopy.ARC4Contract): - def __init(self) -> None: +```{testcode} +class MyContract(algopy.ARC4Contract): + def __init__(self): self.box_map = algopy.BoxMap(algopy.Bytes, algopy.UInt64) + @algopy.arc4.abimethod() def some_method(self, key_a: algopy.Bytes, key_b: algopy.Bytes, key_c: algopy.Bytes) -> None: - # Notice that `state_a` defines global state via `GlobalState` proxy class self.box = algopy.Box(algopy.UInt64, key=key_a) self.box.value = algopy.UInt64(1) self.box_map[key_b] = algopy.UInt64(1) self.box_map[key_c] = algopy.UInt64(2) - # or use low level ops - algopy.op.Box.put(key_a, algopy.op.itobytes(algopy.UInt64(1))) - algopy.op.Box.put(key_b, algopy.op.itobytes(algopy.UInt64(1))) - algopy.op.Box.put(key_c, algopy.op.itobytes(algopy.UInt64(2))) - -... # instantiate test context -contract = SomeContract() - -key_a = ctx.any_uint64() -key_b = ctx.any_uint64() -key_c = ctx.any_uint64() - -contract.some_method(key_a, key_b, key_c) +# In your test +contract = MyContract() +key_a = b"key_a" +key_b = b"key_b" +key_c = b"key_c" -# access boxes -box_from_context = context.get_box({BOX_KEY}) +contract.some_method(algopy.Bytes(key_a), algopy.Bytes(key_b), algopy.Bytes(key_c)) -# Check if box exists -context.does_box_exist({BOX_KEY}) +# Access boxes +box_content = context.ledger.get_box(contract, key_a) +assert context.ledger.box_exists(contract, key_a) -# Set box content on test context manually -# Can be useful if a certain value has to be preset prior to test execution -context.set_box_value({BOX_KEY}, algopy.op.itob(algopy.UInt64(1))) +# Set box content manually +context.ledger.set_box(contract, key_a, algopy.op.itob(algopy.UInt64(1))) ``` -## Scratch space - -The test context represents scratch slots as a dictionary mapping transactions to lists of 256 bytes. When executing a method, scratch slots are allocated for the transaction in the test context. To use scratch storage: +## Scratch Space -```python -class MyContract(Contract, scratch_slots=(1, TWO, urange(3, TWENTY))): - def approval_program(self) -> bool: - op.Scratch.store(1, UInt64(5)) - - assert op.Scratch.load_uint64(1) == UInt64(5) +Scratch space is represented as a list of 256 slots for each transaction. +```{testcode} +class MyContract(algopy.Contract, scratch_slots=(1, 2, algopy.urange(3, 20))): + def approval_program(self): + algopy.op.Scratch.store(1, algopy.UInt64(5)) + assert algopy.op.Scratch.load_uint64(1) == algopy.UInt64(5) return True -... # instantiate test context +# In your test contract = MyContract() -contract.some_method() - -# access scratch space -context.get_scratch_value(1) +with context.txn.create_group([context.any.txn.application_call()]): + result = contract.approval_program() -# Set scratch space for a transaction -context.set_scratch_space(txn, {1: algopy.UInt64(5), 2: algopy.Bytes(b"hello")}) - -# Set a specific scratch slot for a transaction -context.set_scratch_slot(txn, 3, algopy.UInt64(10)) - -# Get the whole scratch space list for a specific transaction -scratch_space = context.get_scratch_space(txn) +assert result +scratch_space = context.txn.last_group.get_scratch_space() +assert scratch_space[1] == algopy.UInt64(5) ``` + +For more detailed information, explore the example contracts in the `examples/` directory, the [coverage](../coverage.md) page, and the [API documentation](../api.md). diff --git a/docs/testing-guide/subroutines.md b/docs/testing-guide/subroutines.md index ea22e14..cd8a42c 100644 --- a/docs/testing-guide/subroutines.md +++ b/docs/testing-guide/subroutines.md @@ -1,9 +1,46 @@ # Subroutines -Any python function decorated with `@algopy.subroutine` is accessible as regular python function when accessed within the testing context. Which implies no additional setup or teardown is required, simply instantiate the class holding the function and access the function as a regular python instance attribute. +Subroutines allow direct testing of internal contract logic without full application calls. -See `simple_voting` examples under [examples](../examples.md) for a showcase of testing subroutines. +## Overview -```{hint} -Testing `subroutines` is a unique feature of `algorand-python-testing`, in contrast with integration tests against real AVM network, this approach allows validating critical logic of narrowly scoped business logic of the contract class without the need to access it via public method that relies on it. In a real AVM network, a user would have to deploy a contract, assemble and submit application call/group to the network, and await for the right results to be implcitly hit by the `subroutine`. +The `@algopy.subroutine` decorator exposes contract methods for isolated testing within the Algorand Python Testing framework. This enables focused validation of core business logic without the overhead of full application deployment and execution. + +## Usage + +1. Decorate internal methods with `@algopy.subroutine`: + +```{testcode} +from algopy import subroutine, UInt64 + +class MyContract: + @subroutine + def calculate_value(self, input: UInt64) -> UInt64: + return input * UInt64(2) +``` + +2. Test the subroutine directly: + +```{testcode} +def test_calculate_value(context: AlgopyTestContext): + contract = MyContract() + result = contract.calculate_value(UInt64(5)) + assert result == UInt64(10) ``` + +## Benefits + +- Faster test execution +- Simplified debugging +- Focused unit testing of core logic + +## Best Practices + +- Use subroutines for complex internal calculations +- Prefer writing `pure` subroutines in ARC4Contract classes +- Combine with full application tests for comprehensive coverage +- Maintain realistic input and output types (e.g., `UInt64`, `Bytes`) + +## Example + +For a complete example, see the `simple_voting` contract in the [examples](../examples.md) section. diff --git a/docs/testing-guide/transactions.md b/docs/testing-guide/transactions.md index abf1d8c..c000575 100644 --- a/docs/testing-guide/transactions.md +++ b/docs/testing-guide/transactions.md @@ -1,184 +1,203 @@ # Transactions -The testing framework follows the Transaction definitions described in [`algorand-python` docs](https://algorand-python.readthedocs.io/en/latest/algorand_sdk/transactions.html). Which implies that transaction related abstractions fall under the following categories: +The testing framework follows the Transaction definitions described in [`algorand-python` docs](https://algorand-python.readthedocs.io/en/latest/algorand_sdk/transactions.html). This section focuses on _value generators_ and interactions with inner transactions, it also explains how the framework identifies _active_ transaction group during contract method/subroutine/logicsig invocation. -## `algopy.Txn` opcode - -In contrast with [`algopy.Global`](state-management#Global), `algopy.Txn` opcode provides access to transactions submitted by the current executing transaction. - -### `scoped_txn_fields`: - -`scoped_txn_fields` is a context manager property available on test context instance that allows setting temporary transaction fields within a specific scope. It's defined in the AlgopyTestContext class: - -```python +```{testsetup} import algopy -from algopy_testing import AlgopyTestContext, algopy_testing_context +import algopy_testing -class SimpleContract(algopy.ARC4Contract): - @algopy.arc4.abimethod - def check_sender(self) -> algopy.Bytes: - return algopy.Txn.sender - -# Create a test context -with algopy_testing_context() as ctx: - # Create a contract instance - contract = SimpleContract() - # Use scoped_txn_fields to temporarily change the sender - patched_sender = ctx.any_account() - with ctx.scoped_txn_fields(sender=patched_sender): - # Call the contract method - result = contract.check_sender() +# Create the context manager for snippets below +ctx_manager = algopy_testing_context() - # Assert that the sender is the default creator - assert result == patched_sender +# Enter the context +ctx = ctx_manager.__enter__() ``` -The `scoped_txn_fields` context manager temporarily sets transaction fields and restores them to their previous values when exiting the context. This is useful for applying specific transaction fields for a limited scope without affecting the global state. - ## Group Transactions -Refers to transaction abstractions available under `algopy.gtxn.*` namespace. +Refers to test implementation of transaction stubs available under `algopy.gtxn.*` namespace. Available under [`algopy.TxnValueGenerator`](../api.md) instance accessible via `ctx.any.txn` property: + +```{mermaid} +graph TD + A[TxnValueGenerator] --> B[payment] + A --> C[asset_transfer] + A --> D[application_call] + A --> E[asset_config] + A --> F[key_registration] + A --> G[asset_freeze] + A --> H[transaction] +``` -```python +```{testcode} ... # instantiate test context # Generate a random payment transaction -pay_txn = ctx.any_payment_transaction( - sender=ctx.any_account(), # Defaults to a random account generated by ctx.any_account() - receiver=ctx.any_account(), # Defaults to a random account generated by ctx.any_account() - amount=algopy.UInt64(1000000) # Specified amount +pay_txn = ctx.any.txn.payment( + sender=ctx.any.account(), # Optional: Defaults to context's default sender if not provided + receiver=ctx.any.account(), # Required + amount=algopy.UInt64(1000000) # Required ) # Generate a random asset transfer transaction -asset_transfer_txn = ctx.any_asset_transfer_transaction( - sender=ctx.any_account(), # Defaults to a random account generated by ctx.any_account() - receiver=ctx.any_account(), # Defaults to a random account generated by ctx.any_account() - asset_id=algopy.UInt64(1), # Specified asset ID - amount=algopy.UInt64(1000) # Specified amount +asset_transfer_txn = ctx.any.txn.asset_transfer( + sender=ctx.any.account(), # Optional: Defaults to context's default sender if not provided + receiver=ctx.any.account(), # Required + asset_id=algopy.UInt64(1), # Required + amount=algopy.UInt64(1000) # Required ) # Generate a random application call transaction -app_call_txn = ctx.any_application_call_transaction( - app_id=ctx.any_application(), # Defaults to a random application generated by ctx.any_application() - app_args=[algopy.Bytes(b"arg1"), algopy.Bytes(b"arg2")], # Specified application arguments - accounts=[ctx.any_account()], # Defaults to a list with a single random account generated by ctx.any_account() - assets=[ctx.any_asset()], # Defaults to a list with a single random asset generated by ctx.any_asset() - apps=[ctx.any_application()], # Defaults to a list with a single random application generated by ctx.any_application() - approval_program_pages=[algopy.Bytes(b"approval_code")], # Specified approval program pages - clear_state_program_pages=[algopy.Bytes(b"clear_code")], # Specified clear state program pages - scratch_space={0: algopy.Bytes(b"scratch")} # Specified scratch space +app_call_txn = ctx.any.txn.application_call( + app_id=ctx.any.application(), # Required + app_args=[algopy.Bytes(b"arg1"), algopy.Bytes(b"arg2")], # Optional: Defaults to empty list if not provided + accounts=[ctx.any.account()], # Optional: Defaults to empty list if not provided + assets=[ctx.any.asset()], # Optional: Defaults to empty list if not provided + apps=[ctx.any.application()], # Optional: Defaults to empty list if not provided + approval_program_pages=[algopy.Bytes(b"approval_code")], # Optional: Defaults to empty list if not provided + clear_state_program_pages=[algopy.Bytes(b"clear_code")], # Optional: Defaults to empty list if not provided + scratch_space={0: algopy.Bytes(b"scratch")} # Optional: Defaults to empty dict if not provided ) # Generate a random asset config transaction -asset_config_txn = ctx.any_asset_config_transaction( - sender=ctx.any_account(), # Defaults to a random account generated by ctx.any_account() - asset_id=algopy.UInt64(1), # Specified asset ID - params=algopy.AssetParams( - total=1000000, # Specified total - decimals=0, # Specified decimals - default_frozen=False, # Specified default frozen state - unit_name="UNIT", # Specified unit name - asset_name="Asset", # Specified asset name - url="http://asset-url", # Specified URL - metadata_hash=b"metadata_hash", # Specified metadata hash - manager=ctx.any_account(), # Defaults to a random account generated by ctx.any_account() - reserve=ctx.any_account(), # Defaults to a random account generated by ctx.any_account() - freeze=ctx.any_account(), # Defaults to a random account generated by ctx.any_account() - clawback=ctx.any_account() # Defaults to a random account generated by ctx.any_account() - ) +asset_config_txn = ctx.any.txn.asset_config( + sender=ctx.any.account(), # Optional: Defaults to context's default sender if not provided + asset_id=algopy.UInt64(1), # Optional: If not provided, creates a new asset + total=1000000, # Required for new assets + decimals=0, # Required for new assets + default_frozen=False, # Optional: Defaults to False if not provided + unit_name="UNIT", # Optional: Defaults to empty string if not provided + asset_name="Asset", # Optional: Defaults to empty string if not provided + url="http://asset-url", # Optional: Defaults to empty string if not provided + metadata_hash=b"metadata_hash", # Optional: Defaults to empty bytes if not provided + manager=ctx.any.account(), # Optional: Defaults to sender if not provided + reserve=ctx.any.account(), # Optional: Defaults to zero address if not provided + freeze=ctx.any.account(), # Optional: Defaults to zero address if not provided + clawback=ctx.any.account() # Optional: Defaults to zero address if not provided ) # Generate a random key registration transaction -key_reg_txn = ctx.any_key_registration_transaction( - sender=ctx.any_account(), # Defaults to a random account generated by ctx.any_account() - vote_pk=algopy.Bytes(b"vote_pk"), # Specified vote public key - selection_pk=algopy.Bytes(b"selection_pk"), # Specified selection public key - vote_first=algopy.UInt64(1), # Specified vote first round - vote_last=algopy.UInt64(1000), # Specified vote last round - vote_key_dilution=algopy.UInt64(10000) # Specified vote key dilution +key_reg_txn = ctx.any.txn.key_registration( + sender=ctx.any.account(), # Optional: Defaults to context's default sender if not provided + vote_pk=algopy.Bytes(b"vote_pk"), # Optional: Defaults to empty bytes if not provided + selection_pk=algopy.Bytes(b"selection_pk"), # Optional: Defaults to empty bytes if not provided + vote_first=algopy.UInt64(1), # Optional: Defaults to 0 if not provided + vote_last=algopy.UInt64(1000), # Optional: Defaults to 0 if not provided + vote_key_dilution=algopy.UInt64(10000) # Optional: Defaults to 0 if not provided ) # Generate a random asset freeze transaction -asset_freeze_txn = ctx.any_asset_freeze_transaction( - sender=ctx.any_account(), # Defaults to a random account generated by ctx.any_account() - asset_id=algopy.UInt64(1), # Specified asset ID - freeze_target=ctx.any_account(), # Defaults to a random account generated by ctx.any_account() - freeze_state=True # Specified freeze state +asset_freeze_txn = ctx.any.txn.asset_freeze( + sender=ctx.any.account(), # Optional: Defaults to context's default sender if not provided + asset_id=algopy.UInt64(1), # Required + freeze_target=ctx.any.account(), # Required + freeze_state=True # Required ) # Generate a random transaction of a specified type -generic_txn = ctx.any_transaction( - type=algopy.TransactionType.Payment, # Specified transaction type - sender=ctx.any_account(), # Defaults to a random account generated by ctx.any_account() - receiver=ctx.any_account(), # Defaults to a random account generated by ctx.any_account() - amount=algopy.UInt64(1000000) # Specified amount +generic_txn = ctx.any.txn.transaction( + type=algopy.TransactionType.Payment, # Required + sender=ctx.any.account(), # Optional: Defaults to context's default sender if not provided + receiver=ctx.any.account(), # Required for Payment + amount=algopy.UInt64(1000000) # Required for Payment ) +``` -class MyContract(algopy.ARC4Contract): - ... - @algopy.arc4.abimethod - def my_method(self): - ... - return Txn.amount +## Preparing for execution -# Setting transaction group with index of current executing transaction -ctx.set_transaction_group(gtxn=[pay_txn, asset_transfer_txn, app_call_txn], active_transaction_index=3) -# Now when you access abstractions like `algopy.Txn` to access 'current' transaction it will point to 'app_call_txn' at index 3. -contract = MyContract() -# Given that we set active transaction index to 3, the `Txn` reference will correctly refer to 'app_call_txn' instance. -assert contract.my_method() == algopy.UInt64(1000000) +When a smart contract instance (application) is interacted with on the Algorand network, it must be performed in relation to a specific transaction or transaction group where one or many transactions are application calls to target smart contract instances. -## Note: active_transaction_index is optional, defaults to 0 and controls which transaction to consider when -## interacting with `algopy.Txn` and affects behaviour when invoking contract methods using `abimethod` or `baremethod` decorators. -## Refer to `ARC4Contract` and `Contract` sections for additional usage examples. +To emulate this behaviour, the `create_group` context manager is available on [`algopy.TransactionContext`](../api.md) instance that allows setting temporary transaction fields within a specific scope, passing in emulated transaction objects and identifying the active transaction index within the transaction group -# Get the transaction group. -txn_group = ctx.get_transaction_group() +```{testcode} +import algopy +from algopy_testing import AlgopyTestContext, algopy_testing_context -# Set the active transaction index -ctx.set_active_transaction_index(0) +class SimpleContract(algopy.ARC4Contract): + @algopy.arc4.abimethod + def check_sender(self) -> algopy.Bytes: + return algopy.Txn.sender -# Get the active transaction -active_txn = ctx.get_active_transaction() +# Create a test context +with algopy_testing_context() as ctx: + # Create a contract instance + contract = SimpleContract() + # Use scoped_txn_fields to temporarily change the sender + patched_sender = ctx.any.account() + with ctx.txn.create_group(txn_op_fields={"sender": patched_sender}): + # Call the contract method + result = contract.check_sender() + + # Assert that the sender is the default creator + # NOTE: by default, 'default_sender' property of the test context + # is set to the creator of the contract + assert result == patched_sender -# Clear the transaction group -ctx.clear_transaction_group() + # Assert that the sender is the default creator after exiting the + # transaction group context + assert ctx.txn.last_active.sender == patched_sender + # Assert the size of last transaction group + assert len(ctx.txn.last_group.txns) == 1 ``` ## Inner Transaction -Inner transactions are AVM transactions that are signed and executed by an AVM applications (instances of deployed smart contract/signatures). When testing smart contracts, you may interact and manage inner transactions via the test context manager as such: +Inner transactions are AVM transactions that are signed and executed by AVM applications (instances of deployed smart contracts or signatures). -```python -# Lets use a method borrowed from our auction contract from examples. The method performs an inner asset transfer. -# Hence after execution we expect the test context to automatically capture and store itxn.AssetTransfer transaction submitted by the contract method to be available for further unit testing assertions. -class AuctionContract(algopy.ARC4Contract): - ... - @arc4.abimethod - def claim_asset(self, asset: Asset) -> None: - ... - itxn.AssetTransfer( - xfer_asset=asset, - asset_close_to=self.previous_bidder, - asset_receiver=self.previous_bidder, - asset_amount=self.asa_amount, +When testing smart contracts, to stay consistent with AVM, the framework _does not allow you to submit inner transactions outside of contract/subroutine/logicsig invocation_, but you can interact with and manage inner transactions using the test context manager as follows: + +```{testcode} +import algopy +from algopy_testing import AlgopyTestContext, algopy_testing_context + +class MyContract(algopy.ARC4Contract): + @algopy.arc4.abimethod + def pay_via_itxn(self, asset: algopy.Asset) -> None: + algopy.itxn.Payment( + receiver=algopy.Txn.sender, + amount=algopy.UInt64(1) ).submit() - ... - -... # instantiate test context and invoke contract method -contract = AuctionContract() -contract.claim_asset(asset=ctx.any_asset()) - -# Test context will automatically capture and store itxn.AssetTransfer transaction submitted by the contract method. -# To access the 'submitted' inner transaction with implicit assertions of whether any inner transaction has been submitted, and type of last submitted inner transaction is of specified type: -asset_transfer_txn = ctx.last_submitted_inner_txn.asset_transfer - -# Alternatively you can access the entire group to retrieve a specific transaction or iterate over groups of transactions: -for txn in ctx.last_submitted_inner_txns: - ... -## or -asset_transfer_txn_2 = ctx.get_submitted_itxn_group(0).asset_transfer(0) # since we expect only one asset transfer inner transaction to be submitted -# Note that above also performs type validation and will throw an error if the type of inner transaction at index is not of specified type. + +# Create a test context +with algopy_testing_context() as ctx: + # Create a contract instance + contract = MyContract() + + # Generate a random asset + asset = ctx.any.asset() + + # Execute the contract method + contract.pay_via_itxn(asset=asset) + + # Access the last submitted inner transaction + payment_txn = ctx.txn.last_group.last_itxn.payment + + # Assert properties of the inner transaction + assert payment_txn.receiver == ctx.txn.last_active.sender + assert payment_txn.amount == algopy.UInt64(1) + + # Access all inner transactions in the last group + for itxn in ctx.txn.last_group.itxn_groups[-1]: + # Perform assertions on each inner transaction + ... + + # Access a specific inner transaction group + first_itxn_group = ctx.txn.last_group.get_itxn_group(0) + first_payment_txn = first_itxn_group.payment(0) ``` -See [Examples](../examples.md) for more examples of accessing inner transactions. +In this example, we define a contract method `pay_via_itxn` that creates and submits an inner payment transaction. The test context automatically captures and stores the inner transactions submitted by the contract method. + +Note that we don't need to wrap the execution in a `create_group` context manager because the method is decorated with `@algopy.arc4.abimethod`, which automatically creates a transaction group for the method. The `create_group` context manager is only needed when you want to create more complex transaction groups or patch transaction fields for various transaction-related opcodes in AVM. + +To access the submitted inner transactions: + +1. Use `ctx.txn.last_group.last_itxn` to access the last submitted inner transaction of a specific type. +2. Iterate over all inner transactions in the last group using `ctx.txn.last_group.itxn_groups[-1]`. +3. Access a specific inner transaction group using `ctx.txn.last_group.get_itxn_group(index)`. + +These methods provide type validation and will raise an error if the requested transaction type doesn't match the actual type of the inner transaction. + +## References + +- [API](../api.md) for more details on the test context manager and inner transactions related methods that perform implicit inner transaction type validation. +- [Examples](../examples.md) for more examples of smart contracts and associated tests that interact with inner transactions. diff --git a/pyproject.toml b/pyproject.toml index 65ee350..6982162 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,6 +134,7 @@ dependencies = [ "sphinx-autodoc2>=0.5.0", "sphinx-copybutton>=0.5.2", "sphinx-autobuild>=2024.4.16", + "sphinx-mermaid", "ipykernel", "pytest", "py-algorand-sdk", @@ -148,7 +149,7 @@ dependencies = [ [tool.hatch.envs.docs.scripts] build = "sphinx-build -b doctest docs docs/_build -W --keep-going -n -E" -dev = "sphinx-autobuild docs docs/_build" +dev = "sphinx-build -b doctest docs docs/_build && sphinx-autobuild docs docs/_build" # Examples environment [tool.hatch.envs.examples] diff --git a/src/algopy_testing/_context_helpers/ledger_context.py b/src/algopy_testing/_context_helpers/ledger_context.py index 17c534e..3419460 100644 --- a/src/algopy_testing/_context_helpers/ledger_context.py +++ b/src/algopy_testing/_context_helpers/ledger_context.py @@ -114,21 +114,21 @@ def update_asset(self, asset_id: int, **asset_fields: typing.Unpack[AssetFields] raise ValueError("Asset not found in testing context!") self.asset_data[asset_id].update(asset_fields) - def get_app(self, app: algopy.UInt64 | int) -> algopy.Application: + def get_app(self, app_id: algopy.UInt64 | int) -> algopy.Application: """Get an application by ID. Args: - app (algopy.UInt64 | int): The application ID. + app_id (algopy.UInt64 | int): The application ID. Returns: algopy.Application: The application object. """ import algopy - app_data = self._get_app_data(app) + app_data = self._get_app_data(app_id) return algopy.Application(app_data.app_id) - def app_exists(self, app: algopy.UInt64 | int) -> bool: + def app_exists(self, app_id: algopy.UInt64 | int) -> bool: """Check if an application exists. Args: @@ -137,7 +137,7 @@ def app_exists(self, app: algopy.UInt64 | int) -> bool: Returns: bool: True if the application exists, False otherwise. """ - app_id = _get_app_id(app) + app_id = _get_app_id(app_id) return app_id in self.app_data def update_app( diff --git a/src/algopy_testing/_value_generators/avm.py b/src/algopy_testing/_value_generators/avm.py index b38a567..2ab5260 100644 --- a/src/algopy_testing/_value_generators/avm.py +++ b/src/algopy_testing/_value_generators/avm.py @@ -9,7 +9,12 @@ import algopy_testing from algopy_testing._context_helpers import lazy_context -from algopy_testing.constants import ALWAYS_APPROVE_TEAL_PROGRAM, MAX_BYTES_SIZE, MAX_UINT64 +from algopy_testing.constants import ( + ALWAYS_APPROVE_TEAL_PROGRAM, + MAX_BYTES_SIZE, + MAX_UINT64, + MAX_UINT512, +) from algopy_testing.models.account import AccountContextData, AccountFields from algopy_testing.models.application import ApplicationContextData, ApplicationFields from algopy_testing.models.asset import AssetFields @@ -25,16 +30,17 @@ class AVMValueGenerator: def uint64(self, min_value: int = 0, max_value: int = MAX_UINT64) -> algopy.UInt64: """Generate a random UInt64 value within a specified range. - - :param min_value: Minimum value. Defaults to 0. - :type min_value: int - :param max_value: Maximum value. Defaults to MAX_UINT64. - :type max_value: int - :param min_value: int: (Default value = 0) - :param max_value: int: (Default value = MAX_UINT64) - :returns: The randomly generated UInt64 value. - :rtype: algopy.UInt64 - :raises ValueError: If `max_value` exceeds MAX_UINT64 or `min_value` exceeds `max_value`. + — + :param min_value: Minimum value. Defaults to 0. + :type min_value: int + :param max_value: Maximum value. Defaults to MAX_UINT64. + :type max_value: int + :param min_value: int: (Default value = 0) + :param max_value: int: (Default value = MAX_UINT64) + :returns: The randomly generated UInt64 value. + :rtype: algopy.UInt64 + :raises ValueError: If `max_value` exceeds MAX_UINT64 or `min_value` + exceeds `max_value`. """ if max_value > MAX_UINT64: raise ValueError("max_value must be less than or equal to MAX_UINT64") @@ -45,6 +51,20 @@ def uint64(self, min_value: int = 0, max_value: int = MAX_UINT64) -> algopy.UInt return algopy_testing.UInt64(generate_random_int(min_value, max_value)) + def biguint(self, min_value: int = 0) -> algopy.BigUInt: + """Generate a random BigUInt value within a specified range. + + :param min_value: Minimum value. Defaults to 0. + :type min_value: int + :returns: The randomly generated BigUInt value. + :rtype: algopy.BigUInt + :raises ValueError: If `min_value` is negative. + """ + if min_value < 0: + raise ValueError("min_value must be greater than or equal to 0") + + return algopy_testing.BigUInt(generate_random_int(min_value, MAX_UINT512)) + def bytes(self, length: int | None = None) -> algopy.Bytes: """Generate a random byte sequence of a specified length.