Source code for tests.helpers.unit_testing_helpers

"""Module containing helper functions for testing PyTeal Utils."""
from typing import Any

import pyteal as pt
from algosdk.atomic_transaction_composer import AtomicTransactionComposer

from beaker import (
    Application,
    BuildOptions,
    client,
    sandbox,
    unconditional_create_approval,
    unconditional_opt_in_approval,
)

algod_client = None
sandbox_accounts = None


def returned_int_as_bytes(i: int, bits: int = 64) -> list[int]:
    return list(i.to_bytes(bits // 8, "big"))


def unit_test_app_blueprint(
    app: Application,
    /,
    expr_to_test: pt.Expr | None = None,
) -> Application:

    """Base unit testable application.

    There are 2 ways to use this class


    1) Initialize with a single Expr that returns bytes
        The bytes output from the Expr are returned from
        the abi method ``unit_test()[]byte``

    2) Subclass UnitTestingApp and override `unit_test`
        Any inputs or output may be specified but you're
        responsible for encoding the incoming arguments as a
        dict with keys matching the argument names of the custom `unit_test` method


    An instance of this class is passed to assert_output to check
    the return value against what you expect.
    """

    app = app.apply(unconditional_create_approval).apply(
        unconditional_opt_in_approval, initialize_local_state=True
    )

    @app.delete
    def delete() -> pt.Expr:
        return pt.Approve()

    @app.update
    def update() -> pt.Expr:
        return pt.Approve()

    @app.close_out
    def close_out() -> pt.Expr:
        return pt.Approve()

    @app.external
    def opup() -> pt.Expr:
        return pt.Approve()

    if expr_to_test is not None:
        test_expr = expr_to_test

        @app.external
        def unit_test(*, output: pt.abi.DynamicArray[pt.abi.Byte]) -> pt.Expr:

            return pt.Seq(
                (s := pt.abi.String()).set(test_expr), output.decode(s.encode())
            )

    return app


[docs]def UnitTestingApp( # noqa: N802 expr_to_test: pt.Expr | None = None, name: str = "UnitTestingApp", version: int = pt.MAX_PROGRAM_VERSION, state: Any | None = None, ) -> Application: return Application( name, build_options=BuildOptions(avm_version=version), state=state, ).apply(unit_test_app_blueprint, expr_to_test=expr_to_test)
[docs]def assert_output( app: Application, inputs: list[dict[str, Any]], outputs: list[Any], opups: int = 0, ) -> None: """ Creates and calls the UnitTestingApp passed and compares the return value with the expected output :param app: An instance of a UnitTestingApp to make call against its `unit_test` method :param inputs: A list of dicts where each entry contains keys matching the input args for the `unit_test` method and values corresponding to the type expected by the method :param outputs: A list of outputs to compare against the return value of the output of the `unit_test` method :param opups: A number of additional app call transactions to make to increase our budget """ # TODO: make these avail in a pytest session context? pass them in directly? global algod_client, sandbox_accounts if algod_client is None: algod_client = sandbox.get_algod_client() if sandbox_accounts is None: sandbox_accounts = sandbox.get_accounts() spec = app.build(algod_client) try: spec.contract.get_method_by_name("unit_test") except KeyError as err: raise Exception( "Expression undefined. Either pass the expr to test or implement unit_test method" ) from err app_client = client.ApplicationClient( algod_client, app=spec, signer=sandbox_accounts[0].signer ) app_client.create() has_state = ( spec.local_state_schema.num_byte_slices or spec.local_state_schema.num_uints ) if has_state: app_client.opt_in() try: for idx, output in enumerate(outputs): input = {} if len(inputs) == 0 else inputs[idx] if opups > 0: atc = AtomicTransactionComposer() app_client.add_method_call(atc, "unit_test", **input) for x in range(opups): app_client.add_method_call(atc, "opup", note=str(x).encode()) results = app_client._execute_atc(atc, wait_rounds=2) assert results.abi_results[0].return_value == output else: result = app_client.call("unit_test", **input) assert result.return_value == output except Exception as e: raise e finally: if has_state: app_client.close_out() app_client.delete()