Skip to content

Commit

Permalink
Testing ABI Functionality via Graviton / Dry-runs + Txn Friendly Grav…
Browse files Browse the repository at this point in the history
…iton Logic Sig Example (#322)

# Blackbox Testing ABI Functionality
This PR brings in dry-run testing of ABI types and subroutines. The space of ABI-Functionality that needs testing is large, and this PR doesn't purport to provide full coverage. However, the basic tools are provided to obtain greater coverage in the future. The following specific ABI use cases are tested:

* abi_sum() which computes the sume of a DynamicArray
* integers and complex integer number (Gassian integers) are built up from basic types and basic operations are validated
* round-trip encoding and decoding of a number of ABI types

# Testing Logic Sigs which interact with their Payment Transaction
#249 introduced examples/signature/factorizer_game.py which includes a family of logic sigs that provide a payout that depends on the solution to an algebra puzzle. With a recent improvement to graviton we can now run blackbox tests on this logic sig family, and gain confidence in its correctness. See tests/integration/pure_logicsig_test.py for how this is done.
  • Loading branch information
tzaffi authored May 19, 2022
1 parent 8558f07 commit ef7fe0e
Show file tree
Hide file tree
Showing 82 changed files with 11,679 additions and 339 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Install python dependencies
run: make setup-development
- name: Build and Test
run: make build-and-test
run: make lint-and-test

run-integration-tests:
runs-on: ubuntu-20.04
Expand Down Expand Up @@ -63,8 +63,8 @@ jobs:
run: make sandbox-dev-up
- name: Install python dependencies
run: make setup-development
- name: Build, Unit Tests and Integration Tests
run: make all-tests
- name: Integration Tests Only
run: make test-integration
- name: Stop running images
run: make sandbox-dev-stop

Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ celerybeat-schedule
*.sage.py

# Environments
_env
.env
.venv
env/
Expand Down Expand Up @@ -138,3 +139,6 @@ dmypy.json

# mac OS
.DS_Store

# asdf
.tool-versions
9 changes: 4 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ black:
flake8:
flake8 $(ALLPY)

# TODO: add `tests` to $MYPY when graviton respects mypy (version 🐗)
MYPY = pyteal scripts
MYPY = pyteal scripts tests
mypy:
mypy $(MYPY)

Expand All @@ -55,9 +54,9 @@ test-unit:
pytest -n $(NUM_PROCS) --durations=10 -sv pyteal tests/unit --ignore tests/unit/blackbox_test.py --ignore tests/unit/user_guide_test.py
pytest -n 1 -sv tests/unit/blackbox_test.py tests/unit/user_guide_test.py

build-and-test: check-generate-init lint test-unit
lint-and-test: check-generate-init lint test-unit

# ---- Integration Test (algod required) ---- #
# ---- Integration Tests (algod required) ---- #

sandbox-dev-up:
docker-compose up -d algod
Expand All @@ -70,7 +69,7 @@ integration-run:

test-integration: integration-run

all-tests: build-and-test test-integration
all-tests: lint-and-test test-integration

# ---- Local Github Actions Simulation via `act` ---- #
# assumes act is installed, e.g. via `brew install act`
Expand Down
9 changes: 8 additions & 1 deletion pyteal/ast/abi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@
from pyteal.ast.abi.array_dynamic import DynamicArrayTypeSpec, DynamicArray

from pyteal.ast.abi.method_return import MethodReturn
from pyteal.ast.abi.util import type_spec_from_annotation, make
from pyteal.ast.abi.util import (
algosdk_from_annotation,
algosdk_from_type_spec,
make,
type_spec_from_annotation,
)

__all__ = [
"String",
Expand Down Expand Up @@ -81,4 +86,6 @@
"MethodReturn",
"type_spec_from_annotation",
"make",
"algosdk_from_annotation",
"algosdk_from_type_spec",
]
3 changes: 3 additions & 0 deletions pyteal/ast/abi/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ def __init__(self) -> None:
def new_instance(self) -> "Address":
return Address()

def annotation_type(self) -> "type[Address]":
return Address

def __str__(self) -> str:
return "address"

Expand Down
4 changes: 4 additions & 0 deletions pyteal/ast/abi/address_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ def test_AddressTypeSpec_byte_length_static():
assert (abi.AddressTypeSpec()).byte_length_static() == abi.AddressLength.Bytes


def test_AddressTypeSpec_length_static():
assert (abi.AddressTypeSpec()).length_static() == abi.AddressLength.Bytes


def test_AddressTypeSpec_new_instance():
assert isinstance(abi.AddressTypeSpec().new_instance(), abi.Address)

Expand Down
3 changes: 3 additions & 0 deletions pyteal/ast/abi/array_dynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ class DynamicArrayTypeSpec(ArrayTypeSpec[T]):
def new_instance(self) -> "DynamicArray[T]":
return DynamicArray(self)

def annotation_type(self) -> "type[DynamicArray[T]]":
return DynamicArray[self.value_type_spec().annotation_type()] # type: ignore[misc]

def is_length_dynamic(self) -> bool:
return True

Expand Down
18 changes: 9 additions & 9 deletions pyteal/ast/abi/array_static.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
from typing import (
Union,
Sequence,
TypeVar,
Generic,
Final,
cast,
)
from typing import Final, Generic, Literal, Sequence, TypeVar, Union, cast

from pyteal.errors import TealInputError
from pyteal.ast.expr import Expr
Expand All @@ -25,11 +18,18 @@ def __init__(self, value_type_spec: TypeSpec, array_length: int) -> None:
super().__init__(value_type_spec)
if not isinstance(array_length, int) or array_length < 0:
raise TypeError(f"Unsupported StaticArray length: {array_length}")
self.array_length: Final = array_length

# Casts to `int` to handle downstream usage where value is a subclass of int like `IntEnum`.
self.array_length: Final = int(array_length)

def new_instance(self) -> "StaticArray[T, N]":
return StaticArray(self)

def annotation_type(self) -> "type[StaticArray[T, N]]":
return StaticArray[ # type: ignore[misc]
self.value_spec.annotation_type(), Literal[self.array_length] # type: ignore
]

def length_static(self) -> int:
"""Get the size of this static array type.
Expand Down
3 changes: 3 additions & 0 deletions pyteal/ast/abi/bool.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ class BoolTypeSpec(TypeSpec):
def new_instance(self) -> "Bool":
return Bool()

def annotation_type(self) -> "type[Bool]":
return Bool

def is_dynamic(self) -> bool:
# Only accurate if this value is alone, since up to 8 consecutive bools will fit into a single byte
return False
Expand Down
3 changes: 3 additions & 0 deletions pyteal/ast/abi/string.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ def __init__(self) -> None:
def new_instance(self) -> "String":
return String()

def annotation_type(self) -> "type[String]":
return String

def __str__(self) -> str:
return "string"

Expand Down
38 changes: 33 additions & 5 deletions pyteal/ast/abi/tuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,33 @@ def length_static(self) -> int:
def new_instance(self) -> "Tuple":
return Tuple(self)

def annotation_type(self) -> "type[Tuple]":
vtses = self.value_type_specs()

def annotater():
return [x.annotation_type() for x in vtses]

match len(vtses):
case 0:
return Tuple0
case 1:
v0 = annotater()[0]
return Tuple1[v0] # type: ignore[valid-type]
case 2:
v0, v1 = annotater()
return Tuple2[v0, v1] # type: ignore[valid-type]
case 3:
v0, v1, v2 = annotater()
return Tuple3[v0, v1, v2] # type: ignore[valid-type]
case 4:
v0, v1, v2, v3 = annotater()
return Tuple4[v0, v1, v2, v3] # type: ignore[valid-type]
case 5:
v0, v1, v2, v3, v4 = annotater()
return Tuple5[v0, v1, v2, v3, v4] # type: ignore[valid-type]

raise TypeError(f"Cannot annotate tuple of length {len(vtses)}")

def is_dynamic(self) -> bool:
return any(type_spec.is_dynamic() for type_spec in self.value_type_specs())

Expand All @@ -243,8 +270,7 @@ def __str__(self) -> str:

TupleTypeSpec.__module__ = "pyteal"


T = TypeVar("T", bound="Tuple")
T_tuple = TypeVar("T_tuple", bound="Tuple")


class Tuple(BaseType):
Expand Down Expand Up @@ -272,7 +298,7 @@ def set(self, *values: BaseType) -> Expr:
...

@overload
def set(self: T, value: ComputedValue[T]) -> Expr:
def set(self: T_tuple, value: ComputedValue[T_tuple]) -> Expr:
...

def set(self, *values):
Expand Down Expand Up @@ -307,8 +333,10 @@ def __getitem__(self, index: int) -> "TupleElement":

Tuple.__module__ = "pyteal"

T = TypeVar("T", bound=BaseType)


class TupleElement(ComputedValue[BaseType]):
class TupleElement(ComputedValue[T]):
"""Represents the extraction of a specific element from a Tuple."""

def __init__(self, tuple: Tuple, index: int) -> None:
Expand All @@ -319,7 +347,7 @@ def __init__(self, tuple: Tuple, index: int) -> None:
def produced_type_spec(self) -> TypeSpec:
return self.tuple.type_spec().value_type_specs()[self.index]

def store_into(self, output: BaseType) -> Expr:
def store_into(self, output: T) -> Expr:
return indexTuple(
self.tuple.type_spec().value_type_specs(),
self.tuple.encode(),
Expand Down
5 changes: 5 additions & 0 deletions pyteal/ast/abi/type.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ def new_instance(self) -> "BaseType":
"""Create a new instance of the specified type."""
pass

@abstractmethod
def annotation_type(self) -> "type[BaseType]":
"""Get the annotation type associated with this spec"""
pass

@abstractmethod
def is_dynamic(self) -> bool:
"""Check if this ABI type is dynamic.
Expand Down
19 changes: 19 additions & 0 deletions pyteal/ast/abi/uint.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ def __init__(self, bit_size: int) -> None:
def new_instance(self) -> "Uint":
pass

@abstractmethod
def annotation_type(self) -> "type[Uint]":
pass

def bit_size(self) -> int:
"""Get the bit size of this uint type"""
return self.size
Expand Down Expand Up @@ -157,6 +161,9 @@ def __init__(self) -> None:
def new_instance(self) -> "Byte":
return Byte()

def annotation_type(self) -> "type[Byte]":
return Byte

def __str__(self) -> str:
return "byte"

Expand All @@ -171,6 +178,9 @@ def __init__(self) -> None:
def new_instance(self) -> "Uint8":
return Uint8()

def annotation_type(self) -> "type[Uint8]":
return Uint8


Uint8TypeSpec.__module__ = "pyteal"

Expand All @@ -182,6 +192,9 @@ def __init__(self) -> None:
def new_instance(self) -> "Uint16":
return Uint16()

def annotation_type(self) -> "type[Uint16]":
return Uint16


Uint16TypeSpec.__module__ = "pyteal"

Expand All @@ -193,6 +206,9 @@ def __init__(self) -> None:
def new_instance(self) -> "Uint32":
return Uint32()

def annotation_type(self) -> "type[Uint32]":
return Uint32


Uint32TypeSpec.__module__ = "pyteal"

Expand All @@ -204,6 +220,9 @@ def __init__(self) -> None:
def new_instance(self) -> "Uint64":
return Uint64()

def annotation_type(self) -> "type[Uint64]":
return Uint64


Uint32TypeSpec.__module__ = "pyteal"

Expand Down
10 changes: 10 additions & 0 deletions pyteal/ast/abi/util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from typing import TypeVar, Any, Literal, get_origin, get_args, cast

import algosdk.abi

from pyteal.errors import TealInputError
from pyteal.ast.expr import Expr
from pyteal.ast.int import Int
Expand Down Expand Up @@ -232,3 +234,11 @@ def make(t: type[T]) -> T:
A new instance of the given type class.
"""
return cast(T, type_spec_from_annotation(t).new_instance())


def algosdk_from_type_spec(t: TypeSpec) -> algosdk.abi.ABIType:
return algosdk.abi.ABIType.from_string(str(t))


def algosdk_from_annotation(t: type[T]) -> algosdk.abi.ABIType:
return algosdk_from_type_spec(type_spec_from_annotation(t))
Loading

0 comments on commit ef7fe0e

Please sign in to comment.