Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dryrun response #283

Merged
merged 53 commits into from
Apr 14, 2022
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
7b62bab
Merge branch 'release/v1.8.0'
bricerisingalgorand Oct 4, 2021
6785baa
Merge branch 'release/v1.9.0b1'
onetechnical Nov 26, 2021
6bb2533
Merge branch 'release/v1.9.0b2'
egieseke Jan 3, 2022
1e4990f
Merge branch 'release/v1.9.0'
algobarb Jan 14, 2022
598a5b6
adding dryrun result object
barnjamin Jan 31, 2022
062ce94
fmt
barnjamin Jan 31, 2022
072e947
typo
barnjamin Jan 31, 2022
508025b
make space size configurable
barnjamin Jan 31, 2022
0d2f81c
zeph cr, thanks!
barnjamin Feb 4, 2022
4ce2a14
inline snake caser, fix str call
barnjamin Feb 4, 2022
8815d72
add check to make sure we have the attr we try to use
barnjamin Feb 4, 2022
0fa8d8e
first stab at test
barnjamin Feb 5, 2022
dd1b093
some changes to formatting to help with readability
barnjamin Feb 5, 2022
31d49f1
update
barnjamin Feb 5, 2022
d1083ce
use lsig disassembly
barnjamin Feb 5, 2022
c010e75
pad before line number, remove pipe
barnjamin Feb 5, 2022
efb281e
make the max line width work right
barnjamin Feb 5, 2022
33bb685
adding pc and headers
barnjamin Feb 6, 2022
5e2ef10
unpack into vars
barnjamin Feb 6, 2022
1e35187
take out empty string thing
barnjamin Feb 6, 2022
edd8328
fmt
barnjamin Feb 6, 2022
69a891a
adding methods to check if the DryrunTxnResult was successful
barnjamin Feb 8, 2022
357e1c6
take out case since its merged
barnjamin Feb 9, 2022
0e036ec
merge develop
barnjamin Feb 10, 2022
89182e8
changing padding math
barnjamin Feb 10, 2022
c01d415
adding dryrun trace for application test
barnjamin Feb 10, 2022
07eca10
fmt
barnjamin Feb 10, 2022
ede274a
added new given string, temporarily chekced out the branch with the t…
barnjamin Feb 11, 2022
5287739
fmt
barnjamin Feb 11, 2022
eaebf36
merge develop
barnjamin Feb 23, 2022
f661cca
revert sdk testing to master branch
barnjamin Feb 23, 2022
fa9211a
almost
barnjamin Mar 22, 2022
83dca7e
passing tests with new formatting
barnjamin Mar 22, 2022
2972067
dont use type, its incorrect in some cases
barnjamin Mar 22, 2022
3dfe4c4
trigger build
barnjamin Mar 22, 2022
48ffbda
alias package name
barnjamin Mar 22, 2022
a81d5b8
Update algosdk/dryrun_results.py
barnjamin Mar 23, 2022
8190e2b
Update algosdk/dryrun_results.py
barnjamin Mar 23, 2022
0d2620c
Update algosdk/dryrun_results.py
barnjamin Mar 23, 2022
e9022c1
Update algosdk/dryrun_results.py
barnjamin Mar 23, 2022
bd2a723
Update algosdk/dryrun_results.py
barnjamin Mar 23, 2022
ed3605a
Update test/steps/v2_steps.py
barnjamin Mar 23, 2022
0185f87
Update Makefile
barnjamin Mar 23, 2022
5181e2d
Update algosdk/dryrun_results.py
barnjamin Mar 23, 2022
8b54442
Update algosdk/dryrun_results.py
barnjamin Mar 23, 2022
78aa402
fix comma, fmt
barnjamin Mar 23, 2022
8ded7ec
adding tabulate as requirement
barnjamin Mar 23, 2022
371875f
remove tabulate dep
barnjamin Mar 23, 2022
846e7e9
better name of max width config param
barnjamin Mar 25, 2022
5efa115
Merge branch 'develop' into dryrun-response
barnjamin Mar 31, 2022
b7fb15c
merge fmt
barnjamin Mar 31, 2022
d336364
merge fmt
barnjamin Mar 31, 2022
8e562b9
Merge branch 'dryrun-response' of github.com:algorand/py-algorand-sdk…
barnjamin Mar 31, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
UNITS = "@unit.abijson or @unit.algod or @unit.applications or @unit.atomic_transaction_composer or @unit.dryrun or @unit.feetest or @unit.indexer or @unit.indexer.logs or @unit.offline or @unit.rekey or @unit.transactions.keyreg or @unit.responses or @unit.responses.231 or @unit.tealsign or @unit.transactions or @unit.transactions.payment or @unit.responses.unlimited_assets or @unit.indexer.ledger_refactoring or @unit.algod.ledger_refactoring"
UNITS = "@unit.abijson or @unit.algod or @unit.applications or @unit.atomic_transaction_composer or @unit.dryrun or @unit.feetest or @unit.indexer or @unit.indexer.logs or @unit.offline or @unit.rekey or @unit.transactions.keyreg or @unit.responses or @unit.responses.231 or @unit.tealsign or @unit.transactions or @unit.transactions.payment or @unit.responses.unlimited_assets or @unit.indexer.ledger_refactoring or @unit.algod.ledger_refactoring or @unit.dryrun.trace.application"
barnjamin marked this conversation as resolved.
Show resolved Hide resolved
unit:
behave --tags=$(UNITS) test -f progress2

Expand Down
1 change: 1 addition & 0 deletions algosdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
from . import v2client
from . import wallet
from . import wordlist
from . import dryrun_results
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we keep this in alphabetical order?


name = "algosdk"
227 changes: 227 additions & 0 deletions algosdk/dryrun_results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import base64
from typing import List
import tabulate as tlib
from tabulate import tabulate, TableFormat, DataRow


tlib.MIN_PADDING = 0


class StackPrinterConfig:
DEFAULT_MAX_WIDTH: int = 30

def __init__(self, max_width=DEFAULT_MAX_WIDTH, top_of_stack_first=True):
self.max_width = max_width
self.top_of_stack_first = top_of_stack_first


class DryrunResponse:
def __init__(self, drrjson: dict):
for param in ["error", "protocol-version", "txns"]:
assert (
param in drrjson
), f"expecting dryrun response object to have key '{param}' but it is missing"

# These are all required fields
barnjamin marked this conversation as resolved.
Show resolved Hide resolved
self.error = drrjson["error"]
self.protocol = drrjson["protocol-version"]
self.txns = [DryrunTransactionResult(txn) for txn in drrjson["txns"]]


class DryrunTransactionResult:
def __init__(self, dr):
assert (
"disassembly" in dr
), "expecting dryrun transaction result to have key 'disassembly' but its missing"

self.disassembly = dr["disassembly"]
barnjamin marked this conversation as resolved.
Show resolved Hide resolved

optionals = [
"app-call-messages",
"local-deltas",
"global-delta",
"cost",
"logic-sig-messages",
"logic-sig-disassembly",
"logs",
]
for field in optionals:
if field in dr:
setattr(self, field.replace("-", "_"), dr[field])
else:
setattr(self, field.replace("-", "_"), None)

barnjamin marked this conversation as resolved.
Show resolved Hide resolved
traces = ["app-call-trace", "logic-sig-trace"]
for trace_field in traces:
if trace_field in dr:
setattr(
self,
trace_field.replace("-", "_"),
barnjamin marked this conversation as resolved.
Show resolved Hide resolved
barnjamin marked this conversation as resolved.
Show resolved Hide resolved
DryrunTrace(dr[trace_field]),
)

def app_call_rejected(self) -> bool:
if self.app_call_messages is not None:
return "REJECT" in self.app_call_messages
return False
barnjamin marked this conversation as resolved.
Show resolved Hide resolved

def logic_sig_rejected(self) -> bool:
if self.logic_sig_messages is not None:
return "REJECT" in self.logic_sig_messages
return False

@classmethod
def trace(
cls,
dr_trace: "DryrunTrace",
disassembly: List[str],
spc: StackPrinterConfig,
) -> str:

# 16 for length of the header up to spaces
headers = ["pc#", "ln#", "source", "scratch", "stack"]
lines = []
for idx in range(len(dr_trace.trace)):

trace_line = dr_trace.trace[idx]

src = disassembly[trace_line.line]
if trace_line.error != "":
src = "!! {} !!".format(trace_line.error)

prev_scratch = []
if idx > 0:
prev_scratch = dr_trace.trace[idx - 1].scratch

scratch = scratch_to_string(prev_scratch, trace_line.scratch)
stack = stack_to_string(trace_line.stack, spc.top_of_stack_first)
lines.append(
[
"{}".format(trace_line.pc),
"{}".format(trace_line.line),
truncate(src, spc.max_width),
truncate(scratch, spc.max_width),
truncate(stack, spc.max_width),
]
)

return (
tabulate(
lines,
headers,
disable_numparse=True,
tablefmt=TableFormat(
barnjamin marked this conversation as resolved.
Show resolved Hide resolved
headerrow=DataRow("", " |", ""),
datarow=DataRow("", " |", ""),
padding=0,
lineabove=None,
linebelowheader=None,
linebetweenrows=None,
linebelow=None,
with_header_hide=None,
),
)
+ "\n"
)

def app_trace(self, spc: StackPrinterConfig = None) -> str:
if not hasattr(self, "app_call_trace"):
return ""

if spc == None:
spc = StackPrinterConfig(top_of_stack_first=False)

return self.trace(self.app_call_trace, self.disassembly, spc=spc)

def lsig_trace(self, spc: StackPrinterConfig = None) -> str:
if not hasattr(self, "logic_sig_trace"):
return ""

if (
not hasattr(self, "logic_sig_disassembly")
or self.logic_sig_disassembly is None
):
barnjamin marked this conversation as resolved.
Show resolved Hide resolved
return ""

if spc == None:
barnjamin marked this conversation as resolved.
Show resolved Hide resolved
spc = StackPrinterConfig(top_of_stack_first=False)

return self.trace(
self.logic_sig_trace, self.logic_sig_disassembly, spaces=spc
)


class DryrunTrace:
def __init__(self, trace: List[dict]):
self.trace = [DryrunTraceLine(line) for line in trace]

def get_trace(self) -> List[str]:
return [line.trace_line() for line in self.trace]


class DryrunTraceLine:
def __init__(self, tl):
self.line = tl["line"]
self.pc = tl["pc"]

self.error = ""
if "error" in tl:
self.error = tl["error"]

self.scratch = []
if "scratch" in tl:
self.scratch = [DryrunStackValue(sv) for sv in tl["scratch"]]

self.stack = [DryrunStackValue(sv) for sv in tl["stack"]]


class DryrunStackValue:
def __init__(self, v):
self.type = v["type"]
self.bytes = v["bytes"]
self.int = v["uint"]

def __str__(self) -> str:
if len(self.bytes) > 0:
return "0x" + base64.b64decode(self.bytes).hex()
return str(self.int)

def __eq__(self, other: "DryrunStackValue"):
return (
self.type == other.type
and self.bytes == other.bytes
and self.int == other.int
)


def truncate(s: str, max_width: int) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like the column width is really 3 + max_width. Is that the intention? Consistency across the API's trumps any other issues here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the SDKs have the same type of logic where we truncate at the max width, then add the 3 period ellipses. I went back and forth on this but I think having max width describe where we truncate the real value then adding ellipses to denote that the value is truncated is the right way to go. It is tough though so if you feel strongly I can change it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries. Since users have an option to specify no max_width at all, this really isn't a big enough deal to delay the major improvements that all this makes possible.

if len(s) > max_width and max_width > 0:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a weird edge case here when max_width < len(s) < max_with + 3. You get less data and more space usage.

So maybe it would be better to have the condition be:

if len(s) >= max_width + 3 and max_width > 0:

But that could break the test, unfortunately.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I should change the flag from max_width to max_value_width to prevent confusion on this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like a good compromise

return s[:max_width] + "..."
return s


def scratch_to_string(
prev_scratch: List[DryrunStackValue], curr_scratch: List[DryrunStackValue]
) -> str:
if len(curr_scratch) == 0:
return ""
barnjamin marked this conversation as resolved.
Show resolved Hide resolved

new_idx = None
for idx in range(len(curr_scratch)):
if idx >= len(prev_scratch):
new_idx = idx
continue

if curr_scratch[idx] != prev_scratch[idx]:
new_idx = idx

if new_idx == None:
return ""

return "{} = {}".format(new_idx, str(curr_scratch[new_idx]))
barnjamin marked this conversation as resolved.
Show resolved Hide resolved


def stack_to_string(stack: List[DryrunStackValue], reverse: bool) -> str:
if reverse:
stack.reverse()
return "[{}]".format(", ".join([str(sv) for sv in stack]))
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
black==21.9b0
glom==20.11.0
pytest==6.2.5
tabulate==0.8.9
git+https://github.com/behave/behave
46 changes: 46 additions & 0 deletions test/steps/v2_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
abi,
account,
atomic_transaction_composer,
dryrun_results,
encoding,
error,
logic,
Expand Down Expand Up @@ -3082,6 +3083,51 @@ def deserialize_json_to_contract(context):
assert actual == context.abi_contract


@given(
'a dryrun response file "{dryrun_response_file}" and a transaction at index "{txn_id}"'
)
def parse_dryrun_response_object(context, dryrun_response_file, txn_id):
dir_path = os.path.dirname(os.path.realpath(__file__))
dir_path = os.path.dirname(os.path.dirname(dir_path))
with open(
dir_path + "/test/features/resources/" + dryrun_response_file, "r"
) as f:
barnjamin marked this conversation as resolved.
Show resolved Hide resolved
drr_dict = json.loads(f.read())

context.dryrun_response_object = dryrun_results.DryrunResponse(drr_dict)
context.dryrun_txn_result = context.dryrun_response_object.txns[
int(txn_id)
]


@then('calling app trace produces "{app_trace_file}"')
def dryrun_compare_golden(context, app_trace_file):
dir_path = os.path.dirname(os.path.realpath(__file__))
dir_path = os.path.dirname(os.path.dirname(dir_path))
with open(
dir_path + "/test/features/resources/" + app_trace_file, "r"
) as f:
trace_expected = f.read()
barnjamin marked this conversation as resolved.
Show resolved Hide resolved

dryrun_trace = context.dryrun_txn_result.app_trace()

got_lines = dryrun_trace.split("\n")
expected_lines = trace_expected.split("\n")

print("{} {}".format(len(got_lines), len(expected_lines)))
for idx in range(len(got_lines)):
if got_lines[idx] != expected_lines[idx]:
print(
" {} \n{}\n{}\n".format(
idx, got_lines[idx], expected_lines[idx]
)
)

assert trace_expected == dryrun_trace, "Expected \n{}\ngot\n{}\n".format(
trace_expected, dryrun_trace
)


@then(
'I dig into the paths "{paths}" of the resulting atomic transaction tree I see group ids and they are all the same'
)
Expand Down