DebugLink click tests (#632)
commit
09c3fd1981
@ -0,0 +1,26 @@
|
|||||||
|
# Automatically generated by pb2py
|
||||||
|
# fmt: off
|
||||||
|
import protobuf as p
|
||||||
|
|
||||||
|
if __debug__:
|
||||||
|
try:
|
||||||
|
from typing import Dict, List # noqa: F401
|
||||||
|
from typing_extensions import Literal # noqa: F401
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DebugLinkLayout(p.MessageType):
|
||||||
|
MESSAGE_WIRE_TYPE = 9001
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
lines: List[str] = None,
|
||||||
|
) -> None:
|
||||||
|
self.lines = lines if lines is not None else []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_fields(cls) -> Dict:
|
||||||
|
return {
|
||||||
|
1: ('lines', p.UnicodeType, p.FLAG_REPEATED),
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
# Automatically generated by pb2py
|
||||||
|
# fmt: off
|
||||||
|
from .. import protobuf as p
|
||||||
|
|
||||||
|
if __debug__:
|
||||||
|
try:
|
||||||
|
from typing import Dict, List # noqa: F401
|
||||||
|
from typing_extensions import Literal # noqa: F401
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DebugLinkLayout(p.MessageType):
|
||||||
|
MESSAGE_WIRE_TYPE = 9001
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
lines: List[str] = None,
|
||||||
|
) -> None:
|
||||||
|
self.lines = lines if lines is not None else []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_fields(cls) -> Dict:
|
||||||
|
return {
|
||||||
|
1: ('lines', p.UnicodeType, p.FLAG_REPEATED),
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
DISPLAY_WIDTH = 240
|
||||||
|
DISPLAY_HEIGHT = 240
|
||||||
|
|
||||||
|
|
||||||
|
def grid(dim, grid_cells, cell):
|
||||||
|
step = dim // grid_cells
|
||||||
|
ofs = step // 2
|
||||||
|
return cell * step + ofs
|
||||||
|
|
||||||
|
|
||||||
|
LEFT = grid(DISPLAY_WIDTH, 3, 0)
|
||||||
|
MID = grid(DISPLAY_WIDTH, 3, 1)
|
||||||
|
RIGHT = grid(DISPLAY_WIDTH, 3, 2)
|
||||||
|
|
||||||
|
TOP = grid(DISPLAY_HEIGHT, 4, 0)
|
||||||
|
BOTTOM = grid(DISPLAY_HEIGHT, 4, 3)
|
||||||
|
|
||||||
|
OK = (RIGHT, BOTTOM)
|
||||||
|
CANCEL = (LEFT, BOTTOM)
|
||||||
|
INFO = (MID, BOTTOM)
|
||||||
|
|
||||||
|
CONFIRM_WORD = (MID, TOP)
|
||||||
|
|
||||||
|
MINUS = (LEFT, grid(DISPLAY_HEIGHT, 5, 2))
|
||||||
|
PLUS = (RIGHT, grid(DISPLAY_HEIGHT, 5, 2))
|
||||||
|
|
||||||
|
|
||||||
|
BUTTON_LETTERS = ("ab", "cd", "ef", "ghij", "klm", "nopq", "rs", "tuv", "wxyz")
|
||||||
|
|
||||||
|
|
||||||
|
def grid35(x, y):
|
||||||
|
return grid(DISPLAY_WIDTH, 3, x), grid(DISPLAY_HEIGHT, 5, y)
|
||||||
|
|
||||||
|
|
||||||
|
def grid34(x, y):
|
||||||
|
return grid(DISPLAY_WIDTH, 3, x), grid(DISPLAY_HEIGHT, 4, y)
|
||||||
|
|
||||||
|
|
||||||
|
def type_word(word):
|
||||||
|
for l in word:
|
||||||
|
idx = next(i for i, letters in enumerate(BUTTON_LETTERS) if l in letters)
|
||||||
|
grid_x = idx % 3
|
||||||
|
grid_y = idx // 3 + 1 # first line is empty
|
||||||
|
yield grid34(grid_x, grid_y)
|
@ -0,0 +1,76 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2019 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import device, messages
|
||||||
|
|
||||||
|
from .. import buttons
|
||||||
|
from ..common import MNEMONIC_SLIP39_BASIC_20_3of6
|
||||||
|
|
||||||
|
|
||||||
|
def enter_word(debug, word):
|
||||||
|
word = word[:4]
|
||||||
|
for coords in buttons.type_word(word):
|
||||||
|
debug.click(coords)
|
||||||
|
return debug.click(buttons.CONFIRM_WORD, wait=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip_t1
|
||||||
|
@pytest.mark.setup_client(uninitialized=True)
|
||||||
|
def test_recovery(device_handler):
|
||||||
|
features = device_handler.features()
|
||||||
|
debug = device_handler.debuglink()
|
||||||
|
|
||||||
|
assert features.initialized is False
|
||||||
|
device_handler.run(device.recover, pin_protection=False)
|
||||||
|
|
||||||
|
# select number of words
|
||||||
|
layout = debug.wait_layout()
|
||||||
|
assert layout.text.startswith("Recovery mode")
|
||||||
|
layout = debug.click(buttons.OK, wait=True)
|
||||||
|
|
||||||
|
assert "Select number of words" in layout.text
|
||||||
|
layout = debug.click(buttons.OK, wait=True)
|
||||||
|
|
||||||
|
assert layout.text == "WordSelector"
|
||||||
|
# click "20" at 2, 2
|
||||||
|
coords = buttons.grid34(2, 2)
|
||||||
|
lines = debug.click(coords, wait=True)
|
||||||
|
layout = " ".join(lines)
|
||||||
|
|
||||||
|
expected_text = "Enter any share (20 words)"
|
||||||
|
remaining = len(MNEMONIC_SLIP39_BASIC_20_3of6)
|
||||||
|
for share in MNEMONIC_SLIP39_BASIC_20_3of6:
|
||||||
|
assert expected_text in layout.text
|
||||||
|
layout = debug.click(buttons.OK, wait=True)
|
||||||
|
|
||||||
|
assert layout.text == "Slip39Keyboard"
|
||||||
|
for word in share.split(" "):
|
||||||
|
layout = enter_word(debug, word)
|
||||||
|
|
||||||
|
remaining -= 1
|
||||||
|
expected_text = "RecoveryHomescreen {} more".format(remaining)
|
||||||
|
|
||||||
|
assert "You have successfully recovered your wallet" in layout.text
|
||||||
|
layout = debug.click(buttons.OK, wait=True)
|
||||||
|
|
||||||
|
assert layout.text == "Homescreen"
|
||||||
|
|
||||||
|
assert isinstance(device_handler.result(), messages.Success)
|
||||||
|
features = device_handler.features()
|
||||||
|
assert features.initialized is True
|
||||||
|
assert features.recovery_mode is False
|
@ -0,0 +1,84 @@
|
|||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
from trezorlib.transport import udp
|
||||||
|
|
||||||
|
udp.SOCKET_TIMEOUT = 0.1
|
||||||
|
|
||||||
|
|
||||||
|
class NullUI:
|
||||||
|
@staticmethod
|
||||||
|
def button_request(code):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_pin(code=None):
|
||||||
|
raise NotImplementedError("Should not be used with T1")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_passphrase():
|
||||||
|
raise NotImplementedError("Should not be used with T1")
|
||||||
|
|
||||||
|
|
||||||
|
class BackgroundDeviceHandler:
|
||||||
|
_pool = ThreadPoolExecutor()
|
||||||
|
|
||||||
|
def __init__(self, client):
|
||||||
|
self.client = client
|
||||||
|
self.client.ui = NullUI
|
||||||
|
self.task = None
|
||||||
|
|
||||||
|
def run(self, function, *args, **kwargs):
|
||||||
|
if self.task is not None:
|
||||||
|
raise RuntimeError("Wait for previous task first")
|
||||||
|
self.task = self._pool.submit(function, self.client, *args, **kwargs)
|
||||||
|
|
||||||
|
def kill_task(self):
|
||||||
|
if self.task is not None:
|
||||||
|
# Force close the client, which should raise an exception in a client
|
||||||
|
# waiting on IO. Does not work over Bridge, because bridge doesn't have
|
||||||
|
# a close() method.
|
||||||
|
while self.client.session_counter > 0:
|
||||||
|
self.client.close()
|
||||||
|
try:
|
||||||
|
self.task.result()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.task = None
|
||||||
|
|
||||||
|
def restart(self, emulator):
|
||||||
|
# TODO handle actual restart as well
|
||||||
|
self.kill_task()
|
||||||
|
emulator.restart()
|
||||||
|
self.client = emulator.client
|
||||||
|
self.client.ui = NullUI
|
||||||
|
|
||||||
|
def result(self):
|
||||||
|
if self.task is None:
|
||||||
|
raise RuntimeError("No task running")
|
||||||
|
try:
|
||||||
|
return self.task.result()
|
||||||
|
finally:
|
||||||
|
self.task = None
|
||||||
|
|
||||||
|
def features(self):
|
||||||
|
if self.task is not None:
|
||||||
|
raise RuntimeError("Cannot query features while task is running")
|
||||||
|
self.client.init_device()
|
||||||
|
return self.client.features
|
||||||
|
|
||||||
|
def debuglink(self):
|
||||||
|
return self.client.debug
|
||||||
|
|
||||||
|
def check_finalize(self):
|
||||||
|
if self.task is not None:
|
||||||
|
self.kill_task()
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
finalized_ok = self.check_finalize()
|
||||||
|
if exc_type is None and not finalized_ok:
|
||||||
|
raise RuntimeError("Exit while task is unfinished")
|
Binary file not shown.
@ -0,0 +1,37 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ..emulators import EmulatorWrapper
|
||||||
|
|
||||||
|
SELECTED_GENS = [
|
||||||
|
gen.strip() for gen in os.environ.get("TREZOR_UPGRADE_TEST", "").split(",") if gen
|
||||||
|
]
|
||||||
|
|
||||||
|
if SELECTED_GENS:
|
||||||
|
# if any gens were selected via the environment variable, force enable all selected
|
||||||
|
LEGACY_ENABLED = "legacy" in SELECTED_GENS
|
||||||
|
CORE_ENABLED = "core" in SELECTED_GENS
|
||||||
|
|
||||||
|
else:
|
||||||
|
# if no selection was provided, select those for which we have emulators
|
||||||
|
try:
|
||||||
|
EmulatorWrapper("legacy")
|
||||||
|
LEGACY_ENABLED = True
|
||||||
|
except Exception:
|
||||||
|
LEGACY_ENABLED = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
EmulatorWrapper("core")
|
||||||
|
CORE_ENABLED = True
|
||||||
|
except Exception:
|
||||||
|
CORE_ENABLED = False
|
||||||
|
|
||||||
|
|
||||||
|
legacy_only = pytest.mark.skipif(
|
||||||
|
not LEGACY_ENABLED, reason="This test requires legacy emulator"
|
||||||
|
)
|
||||||
|
|
||||||
|
core_only = pytest.mark.skipif(
|
||||||
|
not CORE_ENABLED, reason="This test requires core emulator"
|
||||||
|
)
|
@ -0,0 +1,72 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2019 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import device
|
||||||
|
|
||||||
|
from .. import buttons
|
||||||
|
from ..device_handler import BackgroundDeviceHandler
|
||||||
|
from ..emulators import EmulatorWrapper
|
||||||
|
from . import core_only
|
||||||
|
|
||||||
|
|
||||||
|
def enter_word(debug, word):
|
||||||
|
word = word[:4]
|
||||||
|
for coords in buttons.type_word(word):
|
||||||
|
debug.click(coords)
|
||||||
|
return debug.click(buttons.CONFIRM_WORD, wait=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def emulator():
|
||||||
|
emu = EmulatorWrapper("core")
|
||||||
|
with emu:
|
||||||
|
yield emu
|
||||||
|
|
||||||
|
|
||||||
|
@core_only
|
||||||
|
def test_persistence(emulator):
|
||||||
|
device_handler = BackgroundDeviceHandler(emulator.client)
|
||||||
|
debug = device_handler.debuglink()
|
||||||
|
features = device_handler.features()
|
||||||
|
|
||||||
|
assert features.recovery_mode is False
|
||||||
|
|
||||||
|
device_handler.run(device.recover, pin_protection=False)
|
||||||
|
layout = debug.wait_layout()
|
||||||
|
assert layout.text.startswith("Recovery mode")
|
||||||
|
|
||||||
|
layout = debug.click(buttons.OK, wait=True)
|
||||||
|
assert "Select number of words" in layout.text
|
||||||
|
|
||||||
|
device_handler.restart(emulator)
|
||||||
|
debug = device_handler.debuglink()
|
||||||
|
features = device_handler.features()
|
||||||
|
|
||||||
|
assert features.recovery_mode is True
|
||||||
|
|
||||||
|
# no waiting for layout because layout doesn't change
|
||||||
|
layout = debug.read_layout()
|
||||||
|
assert "Select number of words" in layout.text
|
||||||
|
layout = debug.click(buttons.CANCEL, wait=True)
|
||||||
|
|
||||||
|
assert layout.text.startswith("Abort recovery")
|
||||||
|
layout = debug.click(buttons.OK, wait=True)
|
||||||
|
|
||||||
|
assert layout.text == "Homescreen"
|
||||||
|
features = device_handler.features()
|
||||||
|
assert features.recovery_mode is False
|
Loading…
Reference in new issue