1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-02-06 04:40:56 +00:00

test(core): don't fetch DebugLinkState by default

In case the main workflow is restarting after a `DebugLinkDecision`,
the next `DebugLinkGetState` handling becomes inherently racy.

We are making the state fetching explicit, in order to avoid the
"restart" race condition (as described in #4401).

Following the above change, text-based layout recording is removed.

[no changelog]
This commit is contained in:
Roman Zeyde 2025-02-02 17:29:45 +02:00
parent 6865f053bf
commit 1c18a267af
19 changed files with 267 additions and 321 deletions

View File

@ -204,12 +204,12 @@ test_emu_persistence_ui: ## run persistence tests with UI testing
test_emu_ui: ## run ui integration tests
$(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests $(TESTOPTS) \
--ui=test --ui-check-missing --record-text-layout --do-master-diff \
--ui=test --ui-check-missing --do-master-diff \
--lang=$(TEST_LANG)
test_emu_ui_multicore: ## run ui integration tests using multiple cores
$(PYTEST) -n $(MULTICORE) $(TESTPATH)/device_tests $(TESTOPTS) --timeout $(PYTEST_TIMEOUT) \
--ui=test --ui-check-missing --record-text-layout --do-master-diff \
--ui=test --ui-check-missing --do-master-diff \
--control-emulators --model=core --random-order-seed=$(RANDOM) \
--lang=$(TEST_LANG)

View File

@ -64,8 +64,7 @@ if TYPE_CHECKING:
def __call__(
self,
hold_ms: int | None = None,
wait: bool | None = None,
) -> "LayoutContent": ...
) -> "None": ...
InputFlowType = Generator[None, messages.ButtonRequest, None]
@ -416,11 +415,10 @@ def _make_input_func(
def input_func(
self: "DebugLink",
hold_ms: int | None = None,
wait: bool | None = None,
) -> LayoutContent:
) -> None:
__tracebackhide__ = True # for pytest # pylint: disable=W0612
decision.hold_ms = hold_ms
return self._decision(decision, wait=wait)
self._decision(decision)
return input_func # type: ignore [Parameter name mismatch]
@ -442,12 +440,7 @@ class DebugLink:
self.t1_screenshot_directory: Path | None = None
self.t1_screenshot_counter = 0
# Optional file for saving text representation of the screen
self.screen_text_file: Path | None = None
self.last_screen_content = ""
self.waiting_for_layout_change = False
self.layout_dirty = True
self.input_wait_type = DebugWaitType.IMMEDIATE
@ -477,11 +470,6 @@ class DebugLink:
assert self.model is not None
return LayoutType.from_model(self.model)
def set_screen_text_file(self, file_path: Path | None) -> None:
if file_path is not None:
file_path.write_bytes(b"")
self.screen_text_file = file_path
def open(self) -> None:
self.transport.begin_session()
@ -543,8 +531,19 @@ class DebugLink:
raise TrezorFailure(result)
return result
def read_layout(self) -> LayoutContent:
return LayoutContent(self.state().tokens)
def read_layout(self, wait: bool | None = None) -> LayoutContent:
"""
Force waiting for the layout by setting `wait=True`. Force not waiting by
setting `wait=False` -- useful when, e.g., you are causing the next layout to be
deliberately delayed.
"""
if wait is True:
wait_type = DebugWaitType.CURRENT_LAYOUT
elif wait is False:
wait_type = DebugWaitType.IMMEDIATE
else:
wait_type = None
return LayoutContent(self.state(wait_type=wait_type).tokens)
def wait_layout(self, wait_for_external_change: bool = False) -> LayoutContent:
# Next layout change will be caused by external event
@ -558,18 +557,12 @@ class DebugLink:
obj = self._call(
messages.DebugLinkGetState(wait_layout=DebugWaitType.NEXT_LAYOUT)
)
self.layout_dirty = True
if isinstance(obj, messages.Failure):
raise TrezorFailure(obj)
return LayoutContent(obj.tokens)
@contextmanager
def wait_for_layout_change(self) -> Iterator[LayoutContent]:
# set up a dummy layout content object to be yielded
layout_content = LayoutContent(
["DUMMY CONTENT, WAIT UNTIL THE END OF THE BLOCK :("]
)
def wait_for_layout_change(self) -> Iterator[None]:
# make sure some current layout is up by issuing a dummy GetState
self.state()
@ -579,18 +572,14 @@ class DebugLink:
# allow the block to proceed
self.waiting_for_layout_change = True
try:
yield layout_content
yield
finally:
self.waiting_for_layout_change = False
self.layout_dirty = True
# wait for the reply
resp = self._read()
assert isinstance(resp, messages.DebugLinkState)
# replace contents of the yielded object with the new thing
layout_content.__init__(resp.tokens)
def reset_debug_events(self) -> None:
# Only supported on TT and above certain version
if (self.model is not models.T1B1) and not self.legacy_debug:
@ -634,44 +623,20 @@ class DebugLink:
state = self._call(messages.DebugLinkGetState(wait_word_list=True))
return state.reset_word
def _decision(
self, decision: messages.DebugLinkDecision, wait: bool | None = None
) -> LayoutContent:
"""Send a debuglink decision and returns the resulting layout.
def _decision(self, decision: messages.DebugLinkDecision) -> None:
"""Send a debuglink decision.
If hold_ms is set, an additional 200ms is added to account for processing
delays. (This is needed for hold-to-confirm to trigger reliably.)
If `wait` is unset, the following wait mode is used:
- `IMMEDIATE`, when in normal tests, which never deadlocks the device, but may
return an empty layout in case the next one didn't come up immediately. (E.g.,
in SignTx flow, the device is waiting for more TxRequest/TxAck exchanges
before showing the next UI layout.)
- `CURRENT_LAYOUT`, when in tests running through a `DeviceHandler`. This mode
returns the current layout or waits for some layout to come up if there is
none at the moment. The assumption is that wirelink is communicating on
another thread and won't be blocked by waiting on debuglink.
Force waiting for the layout by setting `wait=True`. Force not waiting by
setting `wait=False` -- useful when, e.g., you are causing the next layout to be
deliberately delayed.
"""
if not self.allow_interactions:
return self.wait_layout()
self.wait_layout()
return
if decision.hold_ms is not None:
decision.hold_ms += 200
self._write(decision)
self.layout_dirty = True
if wait is True:
wait_type = DebugWaitType.CURRENT_LAYOUT
elif wait is False:
wait_type = DebugWaitType.IMMEDIATE
else:
wait_type = self.input_wait_type
return self._snapshot_core(wait_type)
press_yes = _make_input_func(button=messages.DebugButton.YES)
"""Confirm current layout. See `_decision` for more details."""
@ -698,58 +663,14 @@ class DebugLink:
)
"""Press right button. See `_decision` for more details."""
def input(self, word: str, wait: bool | None = None) -> LayoutContent:
def input(self, word: str) -> None:
"""Send text input to the device. See `_decision` for more details."""
return self._decision(messages.DebugLinkDecision(input=word), wait)
self._decision(messages.DebugLinkDecision(input=word))
def click(
self,
click: Tuple[int, int],
hold_ms: int | None = None,
wait: bool | None = None,
) -> LayoutContent:
def click(self, click: Tuple[int, int], hold_ms: int | None = None) -> None:
"""Send a click to the device. See `_decision` for more details."""
x, y = click
return self._decision(
messages.DebugLinkDecision(x=x, y=y, hold_ms=hold_ms), wait
)
def _snapshot_core(
self, wait_type: DebugWaitType = DebugWaitType.IMMEDIATE
) -> LayoutContent:
"""Save text and image content of the screen to relevant directories."""
# skip the snapshot if we are on T1
if self.model is models.T1B1:
return LayoutContent([])
# take the snapshot
state = self.state(wait_type)
layout = LayoutContent(state.tokens)
if state.tokens and self.layout_dirty:
# save it, unless we already did or unless it's empty
self.save_debug_screen(layout.visible_screen())
self.layout_dirty = False
# return the layout
return layout
def save_debug_screen(self, screen_content: str) -> None:
if self.screen_text_file is None:
return
if not self.screen_text_file.exists():
self.screen_text_file.write_bytes(b"")
# Not writing the same screen twice
if screen_content == self.last_screen_content:
return
self.last_screen_content = screen_content
with open(self.screen_text_file, "a") as f:
f.write(screen_content)
f.write("\n" + 80 * "/" + "\n")
self._decision(messages.DebugLinkDecision(x=x, y=y, hold_ms=hold_ms))
def stop(self) -> None:
self._write(messages.DebugLinkStop())
@ -882,7 +803,9 @@ class DebugUI:
# Paginating (going as further as possible) and pressing Yes
if br.pages is not None:
for _ in range(br.pages - 1):
self.debuglink.swipe_up(wait=True)
self.debuglink.swipe_up()
self.debuglink.state(DebugWaitType.CURRENT_LAYOUT)
if self.debuglink.model is models.T3T1:
layout = self.debuglink.read_layout()
if "PromptScreen" in layout.all_components():

View File

@ -49,13 +49,14 @@ def get_char_category(char: str) -> PassphraseCategory:
def go_next(debug: "DebugLink") -> LayoutContent:
if debug.layout_type is LayoutType.Bolt:
return debug.click(buttons.OK)
debug.click(buttons.OK)
elif debug.layout_type is LayoutType.Caesar:
return debug.press_right()
debug.press_right()
elif debug.layout_type is LayoutType.Delizia:
return debug.swipe_up()
debug.swipe_up()
else:
raise RuntimeError("Unknown model")
return debug.read_layout()
def tap_to_confirm(debug: "DebugLink") -> LayoutContent:
@ -64,21 +65,23 @@ def tap_to_confirm(debug: "DebugLink") -> LayoutContent:
elif debug.layout_type is LayoutType.Caesar:
return debug.read_layout()
elif debug.layout_type is LayoutType.Delizia:
return debug.click(buttons.TAP_TO_CONFIRM)
debug.click(buttons.TAP_TO_CONFIRM)
return debug.read_layout()
else:
raise RuntimeError("Unknown model")
def go_back(debug: "DebugLink", r_middle: bool = False) -> LayoutContent:
if debug.layout_type in (LayoutType.Bolt, LayoutType.Delizia):
return debug.click(buttons.CANCEL)
debug.click(buttons.CANCEL)
elif debug.layout_type is LayoutType.Caesar:
if r_middle:
return debug.press_middle()
debug.press_middle()
else:
return debug.press_left()
debug.press_left()
else:
raise RuntimeError("Unknown model")
return debug.read_layout()
def navigate_to_action_and_press(
@ -108,13 +111,14 @@ def navigate_to_action_and_press(
if steps < 0:
for _ in range(-steps):
layout = debug.press_left()
debug.press_left()
else:
for _ in range(steps):
layout = debug.press_right()
debug.press_right()
# Press or hold
debug.press_middle(hold_ms=hold_ms)
debug.read_layout() # TODO: make sure the press above takes action
def _carousel_steps(current_index: int, wanted_index: int, length: int) -> int:
@ -125,13 +129,14 @@ def _carousel_steps(current_index: int, wanted_index: int, length: int) -> int:
def unlock_gesture(debug: "DebugLink") -> LayoutContent:
if debug.layout_type is LayoutType.Bolt:
return debug.click(buttons.OK)
debug.click(buttons.OK)
elif debug.layout_type is LayoutType.Caesar:
return debug.press_right()
debug.press_right()
elif debug.layout_type is LayoutType.Delizia:
return debug.click(buttons.TAP_TO_CONFIRM)
debug.click(buttons.TAP_TO_CONFIRM)
else:
raise RuntimeError("Unknown model")
return debug.read_layout()
def _get_action_index(wanted_action: str, all_actions: AllActionsType) -> int:

View File

@ -23,7 +23,8 @@ def enter_word(
if debug.layout_type is LayoutType.Delizia and not is_slip39 and len(word) > 4:
# T3T1 (delizia) BIP39 keyboard allows to "confirm" only if the word is fully written, you need to click the word to auto-complete
debug.click(buttons.CONFIRM_WORD)
return debug.click(buttons.CONFIRM_WORD)
debug.click(buttons.CONFIRM_WORD)
return debug.read_layout()
elif debug.layout_type is LayoutType.Caesar:
letter_index = 0
layout = debug.read_layout()
@ -32,16 +33,20 @@ def enter_word(
while layout.find_values_by_key("letter_choices"):
letter = word[letter_index]
while not layout.get_middle_choice() == letter:
layout = debug.press_right()
debug.press_right()
layout = debug.read_layout()
layout = debug.press_middle()
debug.press_middle()
layout = debug.read_layout()
letter_index += 1
# Word choices
while not layout.get_middle_choice() == word:
layout = debug.press_right()
debug.press_right()
layout = debug.read_layout()
return debug.press_middle()
debug.press_middle()
return debug.read_layout()
else:
raise ValueError("Unknown model")
@ -78,7 +83,8 @@ def select_number_of_words(
coords = coords_map.get(num_of_words)
if coords is None:
raise ValueError("Invalid num_of_words")
return debug.click(coords)
debug.click(coords)
return debug.read_layout()
def select_caesar() -> "LayoutContent":
# navigate to the number and confirm it
@ -86,7 +92,8 @@ def select_number_of_words(
index = word_options.index(num_of_words)
for _ in range(index):
debug.press_right()
return debug.press_middle()
debug.press_middle()
return debug.read_layout()
def select_delizia() -> "LayoutContent":
# click the button from ValuePad
@ -103,13 +110,15 @@ def select_number_of_words(
coords = coords_map.get(num_of_words)
if coords is None:
raise ValueError("Invalid num_of_words")
return debug.click(coords)
debug.click(coords)
return debug.read_layout()
if debug.layout_type is LayoutType.Bolt:
assert debug.read_layout().text_content() == TR.recovery__num_of_words
layout = select_bolt()
elif debug.layout_type is LayoutType.Caesar:
layout = debug.press_right()
debug.press_right()
layout = debug.read_layout()
assert layout.title() == TR.word_count__title
layout = select_caesar()
elif debug.layout_type is LayoutType.Delizia:
@ -150,12 +159,15 @@ def enter_share(
assert TR.translate(before_title) in debug.read_layout().title()
layout = debug.read_layout()
for _ in range(layout.page_count()):
layout = debug.press_right()
debug.press_right()
layout = debug.read_layout()
elif debug.layout_type is LayoutType.Delizia:
layout = debug.swipe_up()
debug.swipe_up()
layout = debug.read_layout()
else:
assert TR.translate(before_title) in debug.read_layout().title()
layout = debug.click(buttons.OK)
debug.click(buttons.OK)
layout = debug.read_layout()
assert "MnemonicKeyboard" in layout.all_components()
@ -236,13 +248,17 @@ def enter_seed_previous_correct(
layout = debug.read_layout()
while layout.get_middle_choice() not in DELETE_BTNS:
layout = debug.press_right()
layout = debug.press_middle()
debug.press_right()
layout = debug.read_layout()
debug.press_middle()
layout = debug.read_layout()
for _ in range(len(bad_word)):
while layout.get_middle_choice() not in DELETE_BTNS:
layout = debug.press_left()
layout = debug.press_middle()
debug.press_left()
layout = debug.read_layout()
debug.press_middle()
layout = debug.read_layout()
elif debug.layout_type is LayoutType.Delizia:
debug.click(buttons.RECOVERY_DELETE) # Top-left
for _ in range(len(bad_word)):
@ -278,7 +294,8 @@ def prepare_enter_seed(
elif debug.layout_type is LayoutType.Caesar:
debug.press_right()
debug.press_right()
layout = debug.press_right()
debug.press_right()
layout = debug.read_layout()
assert "MnemonicKeyboard" in layout.all_components()

View File

@ -44,6 +44,7 @@ def confirm_read(debug: "DebugLink", middle_r: bool = False) -> None:
debug.press_middle()
else:
debug.press_right()
debug.read_layout() # TODO: what is being confirmed here?
def cancel_backup(
@ -61,6 +62,7 @@ def cancel_backup(
elif debug.layout_type is LayoutType.Caesar:
debug.press_left()
debug.press_left()
debug.read_layout() # TODO: make sure cancellation took place
def set_selection(debug: "DebugLink", button: tuple[int, int], diff: int) -> None:
@ -79,7 +81,8 @@ def set_selection(debug: "DebugLink", button: tuple[int, int], diff: int) -> Non
in TR.reset__title_number_of_shares + TR.words__title_threshold
):
# Special info screens
layout = debug.press_right()
debug.press_right()
layout = debug.read_layout()
assert "NumberInput" in layout.all_components()
if button == buttons.reset_minus(debug.model.internal_name):
for _ in range(diff):
@ -102,7 +105,8 @@ def read_words(debug: "DebugLink", do_htc: bool = True) -> list[str]:
layout = debug.read_layout()
for _ in range(layout.page_count() - 1):
words.extend(layout.seed_words())
layout = debug.swipe_up()
debug.swipe_up()
layout = debug.read_layout()
assert layout is not None
if debug.layout_type in (LayoutType.Bolt, LayoutType.Delizia):
words.extend(layout.seed_words())
@ -147,7 +151,8 @@ def confirm_words(debug: "DebugLink", words: list[str]) -> None:
]
wanted_word = words[word_pos - 1].lower()
button_pos = btn_texts.index(wanted_word)
layout = debug.click(buttons.RESET_WORD_CHECK[button_pos])
debug.click(buttons.RESET_WORD_CHECK[button_pos])
layout = debug.read_layout()
elif debug.layout_type is LayoutType.Delizia:
assert TR.regexp("reset__select_word_x_of_y_template").match(layout.subtitle())
for _ in range(3):
@ -162,10 +167,12 @@ def confirm_words(debug: "DebugLink", words: list[str]) -> None:
]
wanted_word = words[word_pos - 1].lower()
button_pos = btn_texts.index(wanted_word)
layout = debug.click(buttons.VERTICAL_MENU[button_pos])
debug.click(buttons.VERTICAL_MENU[button_pos])
layout = debug.read_layout()
elif debug.layout_type is LayoutType.Caesar:
assert TR.reset__select_correct_word in layout.text_content()
layout = debug.press_right()
debug.press_right()
layout = debug.read_layout()
for _ in range(3):
# "SELECT 2ND WORD"
# ^
@ -176,9 +183,11 @@ def confirm_words(debug: "DebugLink", words: list[str]) -> None:
wanted_word = words[word_pos - 1].lower()
while not layout.get_middle_choice() == wanted_word:
layout = debug.press_right()
debug.press_right()
layout = debug.read_layout()
layout = debug.press_middle()
debug.press_middle()
layout = debug.read_layout()
def validate_mnemonics(mnemonics: list[str], expected_ems: bytes) -> None:

View File

@ -106,17 +106,20 @@ def test_autolock_interrupts_signing(device_handler: "BackgroundDeviceHandler"):
if debug.layout_type is LayoutType.Bolt:
debug.click(buttons.OK)
layout = debug.click(buttons.OK)
debug.click(buttons.OK)
layout = debug.read_layout()
assert TR.send__total_amount in layout.text_content()
assert "0.0039 BTC" in layout.text_content()
elif debug.layout_type is LayoutType.Delizia:
debug.swipe_up()
layout = debug.swipe_up()
debug.swipe_up()
layout = debug.read_layout()
assert TR.send__total_amount in layout.text_content()
assert "0.0039 BTC" in layout.text_content()
elif debug.layout_type is LayoutType.Caesar:
debug.press_right()
layout = debug.press_right()
debug.press_right()
layout = debug.read_layout()
assert TR.send__total_amount in layout.text_content()
assert "0.0039 BTC" in layout.text_content()
@ -158,18 +161,21 @@ def test_autolock_does_not_interrupt_signing(device_handler: "BackgroundDeviceHa
if debug.layout_type is LayoutType.Bolt:
debug.click(buttons.OK)
layout = debug.click(buttons.OK)
debug.click(buttons.OK)
layout = debug.read_layout()
assert TR.send__total_amount in layout.text_content()
assert "0.0039 BTC" in layout.text_content()
elif debug.layout_type is LayoutType.Delizia:
debug.swipe_up()
layout = debug.swipe_up()
debug.swipe_up()
layout = debug.read_layout()
assert TR.send__total_amount in layout.text_content()
assert "0.0039 BTC" in layout.text_content()
debug.swipe_up()
elif debug.layout_type is LayoutType.Caesar:
debug.press_right()
layout = debug.press_right()
debug.press_right()
layout = debug.read_layout()
assert TR.send__total_amount in layout.text_content()
assert "0.0039 BTC" in layout.text_content()
@ -181,17 +187,19 @@ def test_autolock_does_not_interrupt_signing(device_handler: "BackgroundDeviceHa
with device_handler.client:
device_handler.client.set_filter(messages.TxAck, sleepy_filter)
# confirm transaction
if debug.layout_type is LayoutType.Bolt:
debug.click(buttons.OK)
elif debug.layout_type is LayoutType.Delizia:
debug.click(buttons.TAP_TO_CONFIRM)
elif debug.layout_type is LayoutType.Caesar:
debug.press_middle()
# In all cases we set wait=False to avoid waiting for the screen and triggering
# the layout deadlock detection. In reality there is no deadlock but the
# `sleepy_filter` delays the response by 10 secs while the layout deadlock
# timeout is 3. In this test we don't need the result of the input event so
# waiting for it is not necessary.
if debug.layout_type is LayoutType.Bolt:
debug.click(buttons.OK, wait=False)
elif debug.layout_type is LayoutType.Delizia:
debug.click(buttons.TAP_TO_CONFIRM, wait=False)
elif debug.layout_type is LayoutType.Caesar:
debug.press_middle(wait=False)
debug.read_layout(wait=False)
signatures, tx = device_handler.result()
assert len(signatures) == 1
@ -277,7 +285,8 @@ def unlock_dry_run(debug: "DebugLink") -> "LayoutContent":
layout = go_next(debug)
assert "PinKeyboard" in layout.all_components()
layout = debug.input(PIN4)
debug.input(PIN4)
layout = debug.read_layout()
assert layout is not None
return layout
@ -307,7 +316,8 @@ def test_dryrun_locks_at_number_of_words(device_handler: "BackgroundDeviceHandle
layout = unlock_gesture(debug)
assert "PinKeyboard" in layout.all_components()
layout = debug.input(PIN4)
debug.input(PIN4)
layout = debug.read_layout()
assert layout is not None
# we are back at homescreen
@ -330,7 +340,8 @@ def test_dryrun_locks_at_word_entry(device_handler: "BackgroundDeviceHandler"):
layout = go_next(debug)
assert layout.main_component() == "MnemonicKeyboard"
elif debug.layout_type is LayoutType.Caesar:
layout = debug.press_right()
debug.press_right()
layout = debug.read_layout()
assert "MnemonicKeyboard" in layout.all_components()
# make sure keyboard locks
@ -353,31 +364,36 @@ def test_dryrun_enter_word_slowly(device_handler: "BackgroundDeviceHandler"):
recovery.select_number_of_words(debug, 20)
if debug.layout_type is LayoutType.Bolt:
layout = debug.click(buttons.OK)
debug.click(buttons.OK)
layout = debug.read_layout()
assert layout.main_component() == "MnemonicKeyboard"
# type the word OCEAN slowly
for coords in buttons.type_word("ocea", is_slip39=True):
time.sleep(9)
debug.click(coords)
layout = debug.click(buttons.CONFIRM_WORD)
debug.click(buttons.CONFIRM_WORD)
layout = debug.read_layout()
# should not have locked, even though we took 9 seconds to type each letter
assert layout.main_component() == "MnemonicKeyboard"
elif debug.layout_type is LayoutType.Delizia:
layout = debug.swipe_up()
debug.swipe_up()
layout = debug.read_layout()
assert layout.main_component() == "MnemonicKeyboard"
# type the word OCEAN slowly
for coords in buttons.type_word("ocea", is_slip39=True):
time.sleep(9)
debug.click(coords)
layout = debug.click(buttons.CONFIRM_WORD)
debug.click(buttons.CONFIRM_WORD)
layout = debug.read_layout()
# should not have locked, even though we took 9 seconds to type each letter
assert layout.main_component() == "MnemonicKeyboard"
elif debug.layout_type is LayoutType.Caesar:
layout = debug.press_right()
debug.press_right()
layout = debug.read_layout()
assert "MnemonicKeyboard" in layout.all_components()
# pressing middle button three times

View File

@ -55,6 +55,7 @@ def test_hold_to_lock(device_handler: "BackgroundDeviceHandler"):
debug.press_right(hold_ms=duration)
else:
debug.click((13, 37), hold_ms=duration)
debug.read_layout() # TODO: is it needed?
assert device_handler.features().unlocked is False
@ -79,11 +80,13 @@ def test_hold_to_lock(device_handler: "BackgroundDeviceHandler"):
# unlock by touching
if debug.layout_type is LayoutType.Caesar:
layout = debug.press_right()
debug.press_right()
else:
layout = debug.click(buttons.INFO)
debug.click(buttons.INFO)
layout = debug.read_layout()
assert "PinKeyboard" in layout.all_components()
debug.input("1234")
debug.read_layout() # TODO: is it needed?
assert device_handler.features().unlocked is True

View File

@ -126,6 +126,7 @@ def press_char(debug: "DebugLink", char: str) -> None:
TT_COORDS_PREV = coords # type: ignore
for _ in range(amount):
debug.click(coords)
debug.read_layout() # TODO: seems to be needed
def input_passphrase(debug: "DebugLink", passphrase: str, check: bool = True) -> None:

View File

@ -154,6 +154,7 @@ def press_char(debug: "DebugLink", char: str) -> None:
COORDS_PREV = coords # type: ignore
for _ in range(amount):
debug.click(coords)
debug.read_layout() # TODO: seems to be needed
def input_passphrase(debug: "DebugLink", passphrase: str, check: bool = True) -> None:

View File

@ -186,6 +186,7 @@ def _delete_pin(debug: "DebugLink", digits_to_delete: int, check: bool = True) -
for _ in range(digits_to_delete):
if debug.layout_type in (LayoutType.Bolt, LayoutType.Delizia):
debug.click(buttons.pin_passphrase_grid(9))
debug.read_layout() # TODO: make sure the press above takes action
elif debug.layout_type is LayoutType.Caesar:
navigate_to_action_and_press(debug, DELETE, TR_PIN_ACTIONS)

View File

@ -56,7 +56,8 @@ def go_through_tutorial_tr(debug: "DebugLink") -> None:
debug.press_right(hold_ms=1000)
debug.press_right()
debug.press_right()
layout = debug.press_middle()
debug.press_middle()
layout = debug.read_layout()
assert layout.title() == TR.tutorial__title_tutorial_complete

View File

@ -38,18 +38,17 @@ def test_tutorial_ignore_menu(device_handler: "BackgroundDeviceHandler"):
debug = device_handler.debuglink()
device_handler.run(device.show_device_tutorial)
layout = debug.read_layout()
assert layout.title() == TR.tutorial__welcome_safe5
layout = debug.click(buttons.TAP_TO_CONFIRM)
assert layout.title() == TR.tutorial__title_lets_begin
layout = debug.swipe_up()
assert layout.title() == TR.tutorial__title_easy_navigation
layout = debug.swipe_up()
assert layout.title() == TR.tutorial__title_handy_menu
layout = debug.swipe_up()
assert layout.title() == TR.tutorial__title_hold
layout = debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000)
assert layout.title() == TR.tutorial__title_well_done
assert debug.read_layout().title() == TR.tutorial__welcome_safe5
debug.click(buttons.TAP_TO_CONFIRM)
assert debug.read_layout().title() == TR.tutorial__title_lets_begin
debug.swipe_up()
assert debug.read_layout().title() == TR.tutorial__title_easy_navigation
debug.swipe_up()
assert debug.read_layout().title() == TR.tutorial__title_handy_menu
debug.swipe_up()
assert debug.read_layout().title() == TR.tutorial__title_hold
debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000)
assert debug.read_layout().title() == TR.tutorial__title_well_done
debug.swipe_up()
device_handler.result()
@ -59,24 +58,23 @@ def test_tutorial_menu_open_close(device_handler: "BackgroundDeviceHandler"):
debug = device_handler.debuglink()
device_handler.run(device.show_device_tutorial)
layout = debug.read_layout()
assert layout.title() == TR.tutorial__welcome_safe5
layout = debug.click(buttons.TAP_TO_CONFIRM)
assert layout.title() == TR.tutorial__title_lets_begin
layout = debug.swipe_up()
assert layout.title() == TR.tutorial__title_easy_navigation
layout = debug.swipe_up()
assert layout.title() == TR.tutorial__title_handy_menu
assert debug.read_layout().title() == TR.tutorial__welcome_safe5
debug.click(buttons.TAP_TO_CONFIRM)
assert debug.read_layout().title() == TR.tutorial__title_lets_begin
debug.swipe_up()
assert debug.read_layout().title() == TR.tutorial__title_easy_navigation
debug.swipe_up()
assert debug.read_layout().title() == TR.tutorial__title_handy_menu
layout = debug.click(buttons.CORNER_BUTTON)
assert TR.tutorial__did_you_know in layout.text_content()
layout = debug.click(buttons.CORNER_BUTTON)
assert layout.title() == TR.tutorial__title_handy_menu
debug.click(buttons.CORNER_BUTTON)
assert TR.tutorial__did_you_know in debug.read_layout().text_content()
debug.click(buttons.CORNER_BUTTON)
assert debug.read_layout().title() == TR.tutorial__title_handy_menu
layout = debug.swipe_up()
assert layout.title() == TR.tutorial__title_hold
layout = debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000)
assert layout.title() == TR.tutorial__title_well_done
debug.swipe_up()
assert debug.read_layout().title() == TR.tutorial__title_hold
debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000)
assert debug.read_layout().title() == TR.tutorial__title_well_done
debug.swipe_up()
device_handler.result()
@ -86,21 +84,20 @@ def test_tutorial_menu_exit(device_handler: "BackgroundDeviceHandler"):
debug = device_handler.debuglink()
device_handler.run(device.show_device_tutorial)
layout = debug.read_layout()
assert layout.title() == TR.tutorial__welcome_safe5
layout = debug.click(buttons.TAP_TO_CONFIRM)
assert layout.title() == TR.tutorial__title_lets_begin
layout = debug.swipe_up()
assert layout.title() == TR.tutorial__title_easy_navigation
layout = debug.swipe_up()
assert layout.title() == TR.tutorial__title_handy_menu
assert debug.read_layout().title() == TR.tutorial__welcome_safe5
debug.click(buttons.TAP_TO_CONFIRM)
assert debug.read_layout().title() == TR.tutorial__title_lets_begin
debug.swipe_up()
assert debug.read_layout().title() == TR.tutorial__title_easy_navigation
debug.swipe_up()
assert debug.read_layout().title() == TR.tutorial__title_handy_menu
layout = debug.click(buttons.CORNER_BUTTON)
assert TR.tutorial__did_you_know in layout.text_content()
layout = debug.click(buttons.VERTICAL_MENU[2])
assert TR.instructions__hold_to_exit_tutorial in layout.footer()
layout = debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000)
assert layout.title() == TR.tutorial__title_well_done
debug.click(buttons.CORNER_BUTTON)
assert TR.tutorial__did_you_know in debug.read_layout().text_content()
debug.click(buttons.VERTICAL_MENU[2])
assert TR.instructions__hold_to_exit_tutorial in debug.read_layout().footer()
debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000)
assert debug.read_layout().title() == TR.tutorial__title_well_done
debug.swipe_up()
device_handler.result()
@ -110,28 +107,27 @@ def test_tutorial_menu_repeat(device_handler: "BackgroundDeviceHandler"):
debug = device_handler.debuglink()
device_handler.run(device.show_device_tutorial)
layout = debug.read_layout()
assert layout.title() == TR.tutorial__welcome_safe5
layout = debug.click(buttons.TAP_TO_CONFIRM)
assert layout.title() == TR.tutorial__title_lets_begin
layout = debug.swipe_up()
assert layout.title() == TR.tutorial__title_easy_navigation
layout = debug.swipe_up()
assert layout.title() == TR.tutorial__title_handy_menu
assert debug.read_layout().title() == TR.tutorial__welcome_safe5
debug.click(buttons.TAP_TO_CONFIRM)
assert debug.read_layout().title() == TR.tutorial__title_lets_begin
debug.swipe_up()
assert debug.read_layout().title() == TR.tutorial__title_easy_navigation
debug.swipe_up()
assert debug.read_layout().title() == TR.tutorial__title_handy_menu
layout = debug.click(buttons.CORNER_BUTTON)
assert TR.tutorial__did_you_know in layout.text_content()
layout = debug.click(buttons.VERTICAL_MENU[1])
debug.click(buttons.CORNER_BUTTON)
assert TR.tutorial__did_you_know in debug.read_layout().text_content()
debug.click(buttons.VERTICAL_MENU[1])
assert layout.title() == TR.tutorial__title_lets_begin
layout = debug.swipe_up()
assert layout.title() == TR.tutorial__title_easy_navigation
layout = debug.swipe_up()
assert layout.title() == TR.tutorial__title_handy_menu
layout = debug.swipe_up()
assert layout.title() == TR.tutorial__title_hold
layout = debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000)
assert layout.title() == TR.tutorial__title_well_done
assert debug.read_layout().title() == TR.tutorial__title_lets_begin
debug.swipe_up()
assert debug.read_layout().title() == TR.tutorial__title_easy_navigation
debug.swipe_up()
assert debug.read_layout().title() == TR.tutorial__title_handy_menu
debug.swipe_up()
assert debug.read_layout().title() == TR.tutorial__title_hold
debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000)
assert debug.read_layout().title() == TR.tutorial__title_well_done
debug.swipe_up()
device_handler.result()
@ -141,29 +137,30 @@ def test_tutorial_menu_funfact(device_handler: "BackgroundDeviceHandler"):
debug = device_handler.debuglink()
device_handler.run(device.show_device_tutorial)
layout = debug.read_layout()
assert layout.title() == TR.tutorial__welcome_safe5
layout = debug.click(buttons.TAP_TO_CONFIRM)
assert layout.title() == TR.tutorial__title_lets_begin
layout = debug.swipe_up()
assert layout.title() == TR.tutorial__title_easy_navigation
layout = debug.swipe_up()
assert layout.title() == TR.tutorial__title_handy_menu
assert debug.read_layout().title() == TR.tutorial__welcome_safe5
debug.click(buttons.TAP_TO_CONFIRM)
assert debug.read_layout().title() == TR.tutorial__title_lets_begin
debug.swipe_up()
assert debug.read_layout().title() == TR.tutorial__title_easy_navigation
debug.swipe_up()
assert debug.read_layout().title() == TR.tutorial__title_handy_menu
layout = debug.click(buttons.CORNER_BUTTON)
assert TR.tutorial__did_you_know in layout.text_content()
layout = debug.click(buttons.VERTICAL_MENU[0])
assert layout.text_content() in TR.tutorial__first_wallet.replace("\n", " ")
debug.click(buttons.CORNER_BUTTON)
assert TR.tutorial__did_you_know in debug.read_layout().text_content()
debug.click(buttons.VERTICAL_MENU[0])
assert debug.read_layout().text_content() in TR.tutorial__first_wallet.replace(
"\n", " "
)
layout = debug.click(buttons.CORNER_BUTTON)
assert TR.tutorial__did_you_know in layout.text_content()
layout = debug.click(buttons.CORNER_BUTTON)
assert layout.title() == TR.tutorial__title_handy_menu
debug.click(buttons.CORNER_BUTTON)
assert TR.tutorial__did_you_know in debug.read_layout().text_content()
debug.click(buttons.CORNER_BUTTON)
assert debug.read_layout().title() == TR.tutorial__title_handy_menu
layout = debug.swipe_up()
assert layout.title() == TR.tutorial__title_hold
layout = debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000)
assert layout.title() == TR.tutorial__title_well_done
debug.swipe_up()
assert debug.read_layout().title() == TR.tutorial__title_hold
debug.click(buttons.TAP_TO_CONFIRM, hold_ms=1000)
assert debug.read_layout().title() == TR.tutorial__title_well_done
debug.swipe_up()
device_handler.result()

View File

@ -323,7 +323,8 @@ def click_info_button_bolt(debug: "DebugLink") -> Generator[Any, Any, ButtonRequ
def click_info_button_delizia(debug: "DebugLink"):
"""Click Shamir backup info button and return back."""
layout = debug.click(buttons.CORNER_BUTTON)
debug.click(buttons.CORNER_BUTTON)
layout = debug.read_layout()
assert "VerticalMenu" in layout.all_components()
debug.click(buttons.VERTICAL_MENU[0])
debug.click(buttons.CORNER_BUTTON)

View File

@ -375,7 +375,6 @@ def pytest_sessionfinish(session: pytest.Session, exitstatus: pytest.ExitCode) -
exitstatus,
test_ui, # type: ignore
bool(session.config.getoption("ui_check_missing")),
bool(session.config.getoption("record_text_layout")),
bool(session.config.getoption("do_master_diff")),
)

View File

@ -347,10 +347,12 @@ class InputFlowShowAddressQRCode(InputFlowBase):
# really cancel
self.debug.click(buttons.CORNER_BUTTON)
# menu
layout = self.debug.click(buttons.CORNER_BUTTON)
self.debug.click(buttons.CORNER_BUTTON)
layout = self.debug.read_layout()
while "PromptScreen" not in layout.all_components():
layout = self.debug.swipe_up()
self.debug.swipe_up()
layout = self.debug.read_layout()
self.debug.synchronize_at("PromptScreen")
# tap to confirm
self.debug.click(buttons.TAP_TO_CONFIRM)
@ -437,14 +439,16 @@ class InputFlowShowMultisigXPUBs(InputFlowBase):
self.debug.click(buttons.CORNER_BUTTON)
assert "Qr" in self.all_components()
layout = self.debug.swipe_left()
self.debug.swipe_left()
layout = self.debug.read_layout()
# address details
assert "Multisig 2 of 3" in layout.screen_content()
assert TR.address_details__derivation_path in layout.screen_content()
# Three xpub pages with the same testing logic
for xpub_num in range(3):
layout = self.debug.swipe_left()
self.debug.swipe_left()
layout = self.debug.read_layout()
self._assert_xpub_title(layout.title(), xpub_num)
content = layout.text_content().replace(" ", "")
assert self.xpubs[xpub_num] in content
@ -470,18 +474,21 @@ class InputFlowShowMultisigXPUBs(InputFlowBase):
self.debug.press_right()
assert "Qr" in self.all_components()
layout = self.debug.press_right()
self.debug.press_right()
layout = self.debug.read_layout()
# address details
# TODO: locate it more precisely
assert "Multisig 2 of 3" in layout.json_str
# Three xpub pages with the same testing logic
for xpub_num in range(3):
layout = self.debug.press_right()
self.debug.press_right()
layout = self.debug.read_layout()
self._assert_xpub_title(layout.title(), xpub_num)
xpub_part_1 = layout.text_content().replace(" ", "")
# Press "SHOW MORE"
layout = self.debug.press_middle()
self.debug.press_middle()
layout = self.debug.read_layout()
xpub_part_2 = layout.text_content().replace(" ", "")
# Go back
self.debug.press_left()
@ -525,24 +532,25 @@ class InputFlowShowMultisigXPUBs(InputFlowBase):
# three xpub pages with the same testing logic
for _xpub_num in range(3):
layout = self.debug.swipe_left()
layout = self.debug.swipe_left()
self.debug.swipe_left()
self.debug.swipe_left()
self.debug.click(buttons.CORNER_BUTTON)
layout = self.debug.synchronize_at("VerticalMenu")
self.debug.synchronize_at("VerticalMenu")
# menu
self.debug.click(buttons.VERTICAL_MENU[2])
# cancel
self.debug.swipe_up()
# really cancel
self.debug.click(buttons.CORNER_BUTTON)
layout = self.debug.synchronize_at("VerticalMenu")
self.debug.synchronize_at("VerticalMenu")
# menu
self.debug.click(buttons.CORNER_BUTTON)
layout = self.debug.synchronize_at("Paragraphs")
# address
while "PromptScreen" not in layout.all_components():
layout = self.debug.swipe_up()
self.debug.swipe_up()
layout = self.debug.read_layout()
self.debug.synchronize_at("PromptScreen")
# tap to confirm
self.debug.press_yes()
@ -652,7 +660,8 @@ class InputFlowShowXpubQRCode(InputFlowBase):
layout = self.debug.synchronize_at("Paragraphs")
# address
while "PromptScreen" not in layout.all_components():
layout = self.debug.swipe_up()
self.debug.swipe_up()
layout = self.debug.read_layout()
self.debug.synchronize_at("PromptScreen")
# tap to confirm
self.debug.press_yes()
@ -834,10 +843,12 @@ def sign_tx_go_to_info_tr(
client.debug.press_middle()
yield
layout = client.debug.press_right()
client.debug.press_right()
layout = client.debug.read_layout()
screen_texts.append(layout.visible_screen())
layout = client.debug.press_right()
client.debug.press_right()
layout = client.debug.read_layout()
screen_texts.append(layout.visible_screen())
client.debug.press_left()

View File

@ -179,7 +179,8 @@ class RecoveryFlow:
self.debug.synchronize_at("VerticalMenu")
self.debug.click(buttons.VERTICAL_MENU[0])
assert (yield).name == "abort_recovery"
layout = self.debug.swipe_up()
self.debug.swipe_up()
layout = self.debug.read_layout()
assert layout.title() == TR.recovery__title_cancel_recovery
self.debug.click(buttons.TAP_TO_CONFIRM)
else:

View File

@ -44,8 +44,6 @@ def screen_recording(
yield
return
record_text_layout = request.config.getoption("record_text_layout")
testcase = TestCase.build(client, request)
testcase.dir.mkdir(exist_ok=True, parents=True)
@ -55,9 +53,6 @@ def screen_recording(
try:
client.debug.start_recording(str(testcase.actual_dir))
if record_text_layout:
client.debug.set_screen_text_file(testcase.screen_text_file)
client.debug.watch_layout(True)
yield
finally:
client.ensure_open()
@ -65,9 +60,6 @@ def screen_recording(
# Wait for response to Initialize, which gives the emulator time to catch up
# and redraw the homescreen. Otherwise there's a race condition between that
# and stopping recording.
if record_text_layout:
client.debug.set_screen_text_file(None)
client.debug.watch_layout(False)
client.init_device()
client.debug.stop_recording()
@ -163,13 +155,12 @@ def sessionfinish(
exitstatus: pytest.ExitCode,
test_ui: str,
check_missing: bool,
record_text_layout: bool,
do_master_diff: bool,
) -> pytest.ExitCode:
if not _should_write_ui_report(exitstatus):
return exitstatus
testreport.generate_reports(record_text_layout, do_master_diff)
testreport.generate_reports(do_master_diff)
recents = list(TestResult.recent_results())

View File

@ -5,7 +5,6 @@ from collections import defaultdict
from datetime import datetime
from pathlib import Path
import dominate
import dominate.tags as t
from dominate.tags import (
a,
@ -31,7 +30,6 @@ from .common import REPORTS_PATH, document, generate_master_diff_report, get_dif
TESTREPORT_PATH = REPORTS_PATH / "test"
IMAGES_PATH = TESTREPORT_PATH / "images"
SCREEN_TEXT_FILE = TESTREPORT_PATH / "screen_text.txt"
# These two html files are referencing each other
ALL_SCREENS = "all_screens.html"
@ -201,35 +199,6 @@ def all_unique_screens() -> Path:
return html.write(TESTREPORT_PATH, doc, ALL_UNIQUE_SCREENS)
def screen_text_report() -> None:
"""Generate a report with text representation of all screens."""
recent_results = list(TestResult.recent_results())
# Creating both a text file (suitable for offline usage)
# and an HTML file (suitable for online usage).
with open(SCREEN_TEXT_FILE, "w") as f2:
for result in recent_results:
if not result.test.screen_text_file.exists():
continue
f2.write(f"\n{result.test.id}\n")
with open(result.test.screen_text_file, "r") as f:
for line in f.readlines():
f2.write(f"\t{line}")
doc = dominate.document(title="Screen text report")
with doc:
for result in recent_results:
if not result.test.screen_text_file.exists():
continue
with a(href=f"{ALL_SCREENS}#{result.test.id}"):
h2(result.test.id)
with open(result.test.screen_text_file, "r") as f:
for line in f.readlines():
p(line)
html.write(TESTREPORT_PATH, doc, "screen_text.html")
def differing_screens() -> None:
"""Creating an HTML page showing all the unique screens that got changed."""
unique_diffs: set[tuple[str | None, str | None]] = set()
@ -316,17 +285,13 @@ def master_index() -> Path:
return html.write(TESTREPORT_PATH, doc, "master_index.html")
def generate_reports(
do_screen_text: bool = False, do_master_diff: bool = False
) -> None:
def generate_reports(do_master_diff: bool = False) -> None:
"""Generate HTML reports for the test."""
html.set_image_dir(IMAGES_PATH)
index()
all_screens()
all_unique_screens()
differing_screens()
if do_screen_text:
screen_text_report()
if do_master_diff:
master_diff()
master_index()

View File

@ -11,17 +11,21 @@ def _enter_word(
) -> "LayoutContent":
typed_word = word[:4]
for coords in buttons.type_word(typed_word, is_slip39=is_slip39):
debug.click(coords, wait=False)
debug.click(coords)
debug.read_layout(wait=False)
return debug.click(buttons.CONFIRM_WORD, wait=True)
debug.click(buttons.CONFIRM_WORD)
return debug.read_layout(wait=True)
def confirm_recovery(debug: "DebugLink") -> None:
debug.click(buttons.OK)
debug.read_layout(wait=True)
def select_number_of_words(debug: "DebugLink", num_of_words: int = 20) -> None:
debug.click(buttons.OK)
debug.read_layout(wait=True)
# click the number
word_option_offset = 6
@ -31,12 +35,12 @@ def select_number_of_words(debug: "DebugLink", num_of_words: int = 20) -> None:
) # raises if num of words is invalid
coords = buttons.grid34(index % 3, index // 3)
debug.click(coords)
debug.read_layout(wait=True)
def enter_share(
debug: "DebugLink", share: str, is_first: bool = True
) -> "LayoutContent":
layout = debug.click(buttons.OK)
def enter_share(debug: "DebugLink", share: str) -> "LayoutContent":
debug.click(buttons.OK)
for word in share.split(" "):
layout = _enter_word(debug, word, is_slip39=True)
return layout
_enter_word(debug, word, is_slip39=True)
return debug.read_layout(wait=True)