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