Skip to content

Commit

Permalink
test(core): don't fetch DebugLinkState by default
Browse files Browse the repository at this point in the history
In case the main workflow is restarting after a `DebugLinkDecision`,
the next `DebugLinkGetState` handling becomes inherently racy.

I would suggest making the state fetching explicit,
in order to avoid the "restart" race condition.

[no changelog]
  • Loading branch information
romanz committed Feb 3, 2025
1 parent 9d1d062 commit 116377b
Show file tree
Hide file tree
Showing 13 changed files with 110 additions and 87 deletions.
45 changes: 33 additions & 12 deletions python/src/trezorlib/debuglink.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def __call__(
self,
hold_ms: int | None = None,
wait: bool | None = None,
) -> "LayoutContent": ...
) -> "LayoutContent | None": ...

InputFlowType = Generator[None, messages.ButtonRequest, None]

Expand Down Expand Up @@ -417,10 +417,11 @@ def input_func(
self: "DebugLink",
hold_ms: int | None = None,
wait: bool | None = None,
) -> LayoutContent:
return_layout: bool = False,
) -> LayoutContent | None:
__tracebackhide__ = True # for pytest # pylint: disable=W0612
decision.hold_ms = hold_ms
return self._decision(decision, wait=wait)
return self._decision(decision, wait=wait, return_layout=return_layout)

return input_func # type: ignore [Parameter name mismatch]

Expand Down Expand Up @@ -635,9 +636,13 @@ def read_reset_word(self) -> str:
return state.reset_word

def _decision(
self, decision: messages.DebugLinkDecision, wait: bool | None = None
) -> LayoutContent:
"""Send a debuglink decision and returns the resulting layout.
self,
decision: messages.DebugLinkDecision,
*,
wait: bool | None = None,
return_layout: bool = False,
) -> LayoutContent | None:
"""Send a debuglink decision and optionally returns the resulting layout.
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.)
Expand All @@ -657,8 +662,12 @@ def _decision(
setting `wait=False` -- useful when, e.g., you are causing the next layout to be
deliberately delayed.
"""
if wait is not None:
return_layout = True

if not self.allow_interactions:
return self.wait_layout()
layout = self.wait_layout()
return layout if return_layout else None

if decision.hold_ms is not None:
decision.hold_ms += 200
Expand All @@ -671,7 +680,9 @@ def _decision(
wait_type = DebugWaitType.IMMEDIATE
else:
wait_type = self.input_wait_type
return self._snapshot_core(wait_type)

if return_layout:
return self._snapshot_core(wait_type)

press_yes = _make_input_func(button=messages.DebugButton.YES)
"""Confirm current layout. See `_decision` for more details."""
Expand All @@ -698,20 +709,30 @@ def _decision(
)
"""Press right button. See `_decision` for more details."""

def input(self, word: str, wait: bool | None = None) -> LayoutContent:
def input(
self, word: str, *, wait: bool | None = None, return_layout: bool = False
) -> LayoutContent | None:
"""Send text input to the device. See `_decision` for more details."""
return self._decision(messages.DebugLinkDecision(input=word), wait)
return self._decision(
messages.DebugLinkDecision(input=word),
wait=wait,
return_layout=return_layout,
)

def click(
self,
click: Tuple[int, int],
*,
hold_ms: int | None = None,
wait: bool | None = None,
) -> LayoutContent:
return_layout: bool = False,
) -> LayoutContent | 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
messages.DebugLinkDecision(x=x, y=y, hold_ms=hold_ms),
wait=wait,
return_layout=return_layout,
)

def _snapshot_core(
Expand Down
24 changes: 12 additions & 12 deletions tests/click_tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ 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)
return debug.click(buttons.OK, return_layout=True)
elif debug.layout_type is LayoutType.Caesar:
return debug.press_right()
return debug.press_right(return_layout=True)
elif debug.layout_type is LayoutType.Delizia:
return debug.swipe_up()
return debug.swipe_up(return_layout=True)
else:
raise RuntimeError("Unknown model")

Expand All @@ -64,19 +64,19 @@ 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)
return debug.click(buttons.TAP_TO_CONFIRM, return_layout=True)
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)
return debug.click(buttons.CANCEL, return_layout=True)
elif debug.layout_type is LayoutType.Caesar:
if r_middle:
return debug.press_middle()
return debug.press_middle(return_layout=True)
else:
return debug.press_left()
return debug.press_left(return_layout=True)
else:
raise RuntimeError("Unknown model")

Expand Down Expand Up @@ -108,10 +108,10 @@ 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)
Expand All @@ -125,11 +125,11 @@ 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)
return debug.click(buttons.OK, return_layout=True)
elif debug.layout_type is LayoutType.Caesar:
return debug.press_right()
return debug.press_right(return_layout=True)
elif debug.layout_type is LayoutType.Delizia:
return debug.click(buttons.TAP_TO_CONFIRM)
return debug.click(buttons.TAP_TO_CONFIRM, return_layout=True)
else:
raise RuntimeError("Unknown model")

Expand Down
40 changes: 20 additions & 20 deletions tests/click_tests/recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ 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)
return debug.click(buttons.CONFIRM_WORD, return_layout=True)
elif debug.layout_type is LayoutType.Caesar:
letter_index = 0
layout = debug.read_layout()
Expand All @@ -32,16 +32,16 @@ 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()
layout = debug.press_right(return_layout=True)

layout = debug.press_middle()
layout = debug.press_middle(return_layout=True)
letter_index += 1

# Word choices
while not layout.get_middle_choice() == word:
layout = debug.press_right()
layout = debug.press_right(return_layout=True)

return debug.press_middle()
return debug.press_middle(return_layout=True)
else:
raise ValueError("Unknown model")

Expand Down Expand Up @@ -78,15 +78,15 @@ def select_bolt() -> "LayoutContent":
coords = coords_map.get(num_of_words)
if coords is None:
raise ValueError("Invalid num_of_words")
return debug.click(coords)
return debug.click(coords, return_layout=True)

def select_caesar() -> "LayoutContent":
# navigate to the number and confirm it
word_options = (20, 33) if unlock_repeated_backup else (12, 18, 20, 24, 33)
index = word_options.index(num_of_words)
for _ in range(index):
debug.press_right()
return debug.press_middle()
return debug.press_middle(return_layout=True)

def select_delizia() -> "LayoutContent":
# click the button from ValuePad
Expand All @@ -103,17 +103,17 @@ def select_delizia() -> "LayoutContent":
coords = coords_map.get(num_of_words)
if coords is None:
raise ValueError("Invalid num_of_words")
return debug.click(coords)
return debug.click(coords, return_layout=True)

if debug.layout_type is LayoutType.Bolt:
assert debug.read_layout().text_content() == TR.recovery__num_of_words
layout = select_bolt()
layout = select_bolt(return_layout=True)
elif debug.layout_type is LayoutType.Caesar:
layout = debug.press_right()
layout = debug.press_right(return_layout=True)
assert layout.title() == TR.word_count__title
layout = select_caesar()
layout = select_caesar(return_layout=True)
elif debug.layout_type is LayoutType.Delizia:
layout = select_delizia()
layout = select_delizia(return_layout=True)
else:
raise ValueError("Unknown model")

Expand Down Expand Up @@ -150,12 +150,12 @@ 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()
layout = debug.press_right(return_layout=True)
elif debug.layout_type is LayoutType.Delizia:
layout = debug.swipe_up()
layout = debug.swipe_up(return_layout=True)
else:
assert TR.translate(before_title) in debug.read_layout().title()
layout = debug.click(buttons.OK)
layout = debug.click(buttons.OK, return_layout=True)

assert "MnemonicKeyboard" in layout.all_components()

Expand Down Expand Up @@ -236,13 +236,13 @@ 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()
layout = debug.press_right(return_layout=True)
layout = debug.press_middle(return_layout=True)

for _ in range(len(bad_word)):
while layout.get_middle_choice() not in DELETE_BTNS:
layout = debug.press_left()
layout = debug.press_middle()
layout = debug.press_left(return_layout=True)
layout = debug.press_middle(return_layout=True)
elif debug.layout_type is LayoutType.Delizia:
debug.click(buttons.RECOVERY_DELETE) # Top-left
for _ in range(len(bad_word)):
Expand Down Expand Up @@ -278,7 +278,7 @@ def prepare_enter_seed(
elif debug.layout_type is LayoutType.Caesar:
debug.press_right()
debug.press_right()
layout = debug.press_right()
layout = debug.press_right(return_layout=True)
assert "MnemonicKeyboard" in layout.all_components()


Expand Down
18 changes: 10 additions & 8 deletions tests/click_tests/reset.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ 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()
layout = debug.press_right(return_layout=True)
assert "NumberInput" in layout.all_components()
if button == buttons.reset_minus(debug.model.internal_name):
for _ in range(diff):
Expand All @@ -102,7 +102,7 @@ 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()
layout = debug.swipe_up(return_layout=True)
assert layout is not None
if debug.layout_type in (LayoutType.Bolt, LayoutType.Delizia):
words.extend(layout.seed_words())
Expand Down Expand Up @@ -130,7 +130,7 @@ def confirm_words(debug: "DebugLink", words: list[str]) -> None:
if debug.layout_type is LayoutType.Delizia:
debug.swipe_up()

layout = debug.read_layout()
layout = debug.read_layout(return_layout=True)
if debug.layout_type is LayoutType.Bolt:
assert TR.regexp("reset__select_word_x_of_y_template").match(
layout.text_content()
Expand All @@ -147,7 +147,9 @@ 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])
layout = debug.click(
buttons.RESET_WORD_CHECK[button_pos], return_layout=True
)
elif debug.layout_type is LayoutType.Delizia:
assert TR.regexp("reset__select_word_x_of_y_template").match(layout.subtitle())
for _ in range(3):
Expand All @@ -162,10 +164,10 @@ 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])
layout = debug.click(buttons.VERTICAL_MENU[button_pos], return_layout=True)
elif debug.layout_type is LayoutType.Caesar:
assert TR.reset__select_correct_word in layout.text_content()
layout = debug.press_right()
layout = debug.press_right(return_layout=True)
for _ in range(3):
# "SELECT 2ND WORD"
# ^
Expand All @@ -176,9 +178,9 @@ 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()
layout = debug.press_right(return_layout=True)

layout = debug.press_middle()
layout = debug.press_middle(return_layout=True)


def validate_mnemonics(mnemonics: list[str], expected_ems: bytes) -> None:
Expand Down
Loading

0 comments on commit 116377b

Please sign in to comment.