2020-07-09 14:44:00 +00:00
|
|
|
# click-test generator
|
|
|
|
#
|
|
|
|
# Beware: This tool is alpha-stage, both in terms of functionality and code quality.
|
|
|
|
# It is published here because it can be useful as-is. But it's full of bugs in any
|
|
|
|
# case.
|
|
|
|
#
|
|
|
|
# See docs/tests/click-tests.md for a brief instruction manual.
|
|
|
|
|
2020-07-08 14:14:43 +00:00
|
|
|
import inspect
|
|
|
|
import sys
|
|
|
|
import threading
|
|
|
|
import time
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
import click
|
|
|
|
|
|
|
|
from trezorlib import (
|
|
|
|
binance,
|
|
|
|
btc,
|
|
|
|
cardano,
|
|
|
|
cosi,
|
|
|
|
device,
|
|
|
|
eos,
|
|
|
|
ethereum,
|
|
|
|
fido,
|
|
|
|
firmware,
|
|
|
|
misc,
|
|
|
|
monero,
|
|
|
|
nem,
|
|
|
|
ripple,
|
|
|
|
stellar,
|
|
|
|
tezos,
|
|
|
|
)
|
|
|
|
from trezorlib.cli.trezorctl import cli as main
|
|
|
|
|
|
|
|
from trezorlib import cli, debuglink, protobuf # isort:skip
|
|
|
|
|
|
|
|
|
|
|
|
# make /tests part of sys.path so that we can import buttons.py as a module
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
sys.path.append(str(ROOT))
|
|
|
|
import buttons # isort:skip
|
|
|
|
|
|
|
|
MODULES = (
|
|
|
|
binance,
|
|
|
|
btc,
|
|
|
|
cardano,
|
|
|
|
cosi,
|
|
|
|
device,
|
|
|
|
eos,
|
|
|
|
ethereum,
|
|
|
|
fido,
|
|
|
|
firmware,
|
|
|
|
misc,
|
|
|
|
monero,
|
|
|
|
nem,
|
|
|
|
ripple,
|
|
|
|
stellar,
|
|
|
|
tezos,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
CALLS_DONE = []
|
|
|
|
DEBUGLINK = None
|
|
|
|
|
2024-07-22 06:40:27 +00:00
|
|
|
get_client_orig = cli.NewTrezorConnection.get_client
|
2020-07-08 14:14:43 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_client(conn):
|
|
|
|
global DEBUGLINK
|
|
|
|
client = get_client_orig(conn)
|
|
|
|
DEBUGLINK = debuglink.DebugLink(client.transport.find_debug())
|
|
|
|
DEBUGLINK.open()
|
|
|
|
DEBUGLINK.watch_layout(True)
|
|
|
|
return client
|
|
|
|
|
|
|
|
|
2024-07-22 06:40:27 +00:00
|
|
|
cli.NewTrezorConnection.get_client = get_client
|
2020-07-08 14:14:43 +00:00
|
|
|
|
|
|
|
|
|
|
|
def scan_layouts(dest):
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
layout = DEBUGLINK.wait_layout()
|
|
|
|
except Exception:
|
|
|
|
return
|
|
|
|
dest.append(layout)
|
|
|
|
|
|
|
|
|
|
|
|
CLICKS_HELP = """\
|
|
|
|
Type 'y' or just press Enter to click the right button (usually "OK")
|
|
|
|
Type 'n' to click the left button (usually "Cancel")
|
|
|
|
Type a digit (0-9) to click the appropriate button as if on a numpad.
|
|
|
|
Type 'g1,2' to click button in column 1 and row 2 of 3x5 grid (letter A on mnemonic keyboard).
|
|
|
|
Type 'i 1234' to send text "1234" without clicking (useful for PIN, passphrase, etc.)
|
|
|
|
Type 'u' or 'j' to swipe up, 'd' or 'k' to swipe down.
|
|
|
|
Type 'confirm' for hold-to-confirm (or a confirmation signal without clicking).
|
|
|
|
Type 'stop' to stop recording.
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def layout_to_output(layout):
|
|
|
|
if isinstance(layout, str):
|
|
|
|
return layout
|
|
|
|
|
|
|
|
if len(layout.lines) > 1:
|
|
|
|
text = " ".join(layout.lines[1:])
|
|
|
|
return f"assert {text!r} in layout.text"
|
|
|
|
else:
|
|
|
|
return f"assert layout.text == {layout.text!r}"
|
|
|
|
|
|
|
|
|
|
|
|
def echo(what):
|
|
|
|
click.secho(what, fg="blue", err=True)
|
|
|
|
sys.stderr.flush()
|
|
|
|
|
|
|
|
|
|
|
|
def send_clicks(dest):
|
|
|
|
echo(CLICKS_HELP)
|
|
|
|
while True:
|
|
|
|
key = click.prompt(click.style("Send click", fg="blue"), default="y")
|
|
|
|
sys.stderr.flush()
|
|
|
|
|
|
|
|
try:
|
|
|
|
layout = DEBUGLINK.read_layout()
|
|
|
|
echo("Please wait...")
|
|
|
|
|
|
|
|
if key == "confirm":
|
2021-12-13 11:42:53 +00:00
|
|
|
output = "debug.input(button=messages.DebugButton.YES)"
|
2020-07-08 14:14:43 +00:00
|
|
|
DEBUGLINK.press_yes()
|
|
|
|
elif key in "uj":
|
|
|
|
output = "debug.input(swipe=messages.DebugSwipeDirection.UP)"
|
|
|
|
DEBUGLINK.swipe_up()
|
|
|
|
elif key in "dk":
|
|
|
|
output = "debug.input(swipe=messages.DebugSwipeDirection.DOWN)"
|
|
|
|
DEBUGLINK.swipe_down()
|
|
|
|
elif key.startswith("i "):
|
|
|
|
input_str = key[2:]
|
|
|
|
output = f"debug.input({input_str!r})"
|
|
|
|
DEBUGLINK.input(input_str)
|
|
|
|
elif key.startswith("g"):
|
|
|
|
x, y = [int(s) - 1 for s in key[1:].split(",")]
|
|
|
|
output = f"debug.click(buttons.grid35({x}, {y}))"
|
|
|
|
DEBUGLINK.click(buttons.grid35(x, y))
|
|
|
|
elif key == "y":
|
|
|
|
output = "debug.click(buttons.OK)"
|
|
|
|
DEBUGLINK.click(buttons.OK)
|
|
|
|
elif key == "n":
|
|
|
|
output = "debug.click(buttons.CANCEL)"
|
|
|
|
DEBUGLINK.click(buttons.CANCEL)
|
|
|
|
elif key in "0123456789":
|
|
|
|
if key == "0":
|
|
|
|
x, y = 1, 4
|
|
|
|
else:
|
|
|
|
i = int(key) - 1
|
|
|
|
x = i % 3
|
|
|
|
y = 3 - (i // 3) # trust me
|
|
|
|
output = f"debug.click(buttons.grid35({x}, {y}))"
|
|
|
|
DEBUGLINK.click(buttons.grid35(x, y))
|
|
|
|
elif key == "stop":
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
raise Exception
|
|
|
|
|
|
|
|
# give emulator time to react
|
|
|
|
time.sleep(0.5)
|
|
|
|
new_layout = DEBUGLINK.read_layout()
|
|
|
|
if new_layout != layout:
|
|
|
|
# assume emulator reacted with a layout change
|
|
|
|
output = f"layout = {output[:-1]}, wait=True)"
|
|
|
|
|
|
|
|
dest.append(output)
|
|
|
|
except Exception:
|
|
|
|
echo("bad input")
|
|
|
|
|
|
|
|
|
|
|
|
def record_wrapper(name, func):
|
|
|
|
def wrapper(*args, **kwargs):
|
|
|
|
clicks = []
|
|
|
|
layouts = []
|
|
|
|
call_item = [name, args, kwargs, clicks, layouts]
|
|
|
|
|
|
|
|
clicks_thr = threading.Thread(target=send_clicks, args=(clicks,), daemon=True)
|
|
|
|
clicks_thr.start()
|
|
|
|
try:
|
|
|
|
result = func(*args, **kwargs)
|
|
|
|
call_item.extend([result, None])
|
|
|
|
return result
|
|
|
|
except BaseException as e:
|
|
|
|
call_item.extend([None, e])
|
|
|
|
raise
|
|
|
|
finally:
|
|
|
|
clicks_thr.join()
|
|
|
|
|
|
|
|
thr = threading.Thread(target=scan_layouts, args=(layouts,), daemon=True)
|
|
|
|
thr.start()
|
|
|
|
# 5 seconds to download all layout changes so far should be enough
|
|
|
|
thr.join(timeout=5)
|
|
|
|
|
|
|
|
DEBUGLINK.close() # this should shut down the layout scanning thread
|
|
|
|
thr.join()
|
|
|
|
DEBUGLINK.open()
|
|
|
|
|
|
|
|
CALLS_DONE.append(call_item)
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
|
|
|
for module in MODULES:
|
|
|
|
assert inspect.ismodule(module)
|
|
|
|
for name, member in inspect.getmembers(module):
|
|
|
|
if not inspect.isfunction(member):
|
|
|
|
continue
|
|
|
|
|
|
|
|
sig = inspect.signature(member)
|
|
|
|
params = list(sig.parameters)
|
|
|
|
if params[0] != "client":
|
|
|
|
continue
|
|
|
|
|
|
|
|
func_name = f"{module.__name__}.{name}"
|
|
|
|
setattr(module, name, record_wrapper(func_name, member))
|
|
|
|
|
|
|
|
|
|
|
|
def call_to_strs(call):
|
|
|
|
func_name, args, kwargs, clicks, layouts, result, exception = call
|
|
|
|
args = args[1:]
|
|
|
|
|
|
|
|
arg_text = [repr(arg) for arg in args]
|
|
|
|
arg_text.extend(f"{name}={value!r}" for name, value in kwargs.items())
|
|
|
|
|
|
|
|
call = f"device_handler.run({func_name}, {', '.join(arg_text)})"
|
|
|
|
|
|
|
|
result_list = []
|
|
|
|
if exception is None:
|
|
|
|
if isinstance(result, protobuf.MessageType):
|
|
|
|
result_list.append("result = device_handler.result()")
|
|
|
|
result_list.append(
|
|
|
|
f"assert isinstance(result, messages.{result.__class__.__name__}"
|
|
|
|
)
|
|
|
|
for k, v in result.__dict__.items():
|
|
|
|
if v is None or v == []:
|
|
|
|
continue
|
|
|
|
result_list.append(f"assert result.{k} == {v!r}")
|
|
|
|
else:
|
|
|
|
result_list.append(f"assert device_handler.result() == {result!r}")
|
|
|
|
else:
|
|
|
|
result_list = [
|
|
|
|
f"with pytest.raises({exception.__class__.__name__}):",
|
|
|
|
" device_handler.result()",
|
|
|
|
]
|
|
|
|
|
|
|
|
layouts_iter = iter(layouts)
|
|
|
|
layout_text = ["layout = debug.wait_layout()", next(layouts_iter)]
|
|
|
|
|
|
|
|
for instr in clicks:
|
|
|
|
layout_text.append(instr)
|
|
|
|
if "wait=True" in instr:
|
|
|
|
layout_text.append(next(layouts_iter))
|
|
|
|
|
|
|
|
# finish layouts iterator -- should not do anything ideally
|
|
|
|
layout_text.extend(layouts_iter)
|
|
|
|
|
|
|
|
layout_text = [layout_to_output(l) for l in layout_text]
|
|
|
|
|
|
|
|
all_lines = "\n".join([call] + layout_text + result_list).split("\n")
|
|
|
|
all_lines = [f" {l}" for l in all_lines]
|
|
|
|
func_name_under = func_name.replace(".", "_")
|
|
|
|
all_lines.insert(0, f"def test_{func_name_under}(device_handler):")
|
|
|
|
all_lines.insert(1, " debug = device_handler.debuglink()")
|
|
|
|
|
|
|
|
return "\n".join(all_lines)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
echo(
|
|
|
|
"""\
|
|
|
|
Quick&Dirty Test Case Recorder.
|
|
|
|
|
|
|
|
Use as you would use trezorctl, input clicking commands via host keyboard
|
|
|
|
for best results.
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
main()
|
|
|
|
finally:
|
|
|
|
if DEBUGLINK is not None:
|
|
|
|
DEBUGLINK.watch_layout(False)
|
|
|
|
DEBUGLINK.close()
|
|
|
|
echo("\n# ========== test cases ==========\n")
|
|
|
|
for call in CALLS_DONE:
|
|
|
|
testcase = call_to_strs(call)
|
|
|
|
echo(testcase)
|
|
|
|
echo("\n")
|