tests: introduce UI tests for core

pull/784/head
Tomas Susanka 4 years ago
parent bc0c10bf3e
commit 51ef963738

@ -38,6 +38,21 @@ core unix unit test:
- cd core
- pipenv run make test
core unix device ui test:
stage: test
<<: *only_changes_core
dependencies:
- core unix frozen regular build
script:
- cd core
- pipenv run make test_emu_ui
- cp /var/tmp/trezor.log ${CI_PROJECT_DIR}
artifacts:
name: core-unix-device-test.log
paths:
- trezor.log
expire_in: 1 week
core unix device test:
stage: test
<<: *only_changes_core

@ -39,6 +39,24 @@ message DebugLinkLayout {
repeated string lines = 1;
}
/**
* Request: Re-seed RNG with given value
* @start
* @next Success
*/
message DebugLinkReseedRandom {
optional uint32 value = 1;
}
/**
* Request: Start or stop recording screen changes into given target directory
* @start
* @next Success
*/
message DebugLinkRecordScreen {
optional string target_directory = 1; // empty or missing to stop recording
}
/**
* Request: Computer asks for device state
* @start

@ -107,6 +107,8 @@ enum MessageType {
MessageType_DebugLinkMemoryWrite = 112 [(wire_debug_in) = true];
MessageType_DebugLinkFlashErase = 113 [(wire_debug_in) = true];
MessageType_DebugLinkLayout = 9001 [(wire_debug_out) = true];
MessageType_DebugLinkReseedRandom = 9002 [(wire_debug_in) = true];
MessageType_DebugLinkRecordScreen = 9003 [(wire_debug_in) = true];
// Ethereum
MessageType_EthereumGetPublicKey = 450 [(wire_in) = true];

@ -79,6 +79,15 @@ test_emu_fido2: ## run fido2 device tests
test_emu_click: ## run click tests
cd tests ; ./run_tests_click_emu.sh $(TESTOPTS)
test_emu_ui: # run ui integration tests
cd tests ; ./run_tests_device_emu.sh --test_screen=test-hash -m "not skip_ui" $(TESTOPTS)
test_emu_ui_hash: # create hashes of ui integration test fixtures
cd tests ; ./run_tests_device_emu.sh --test_screen=hash -m "not skip_ui" $(TESTOPTS)
test_emu_ui_record: # record a full set of new ui fixtures
cd tests ; ./run_tests_device_emu.sh --test_screen=record -m "not skip_ui" $(TESTOPTS)
pylint: ## run pylint on application sources and tests
pylint -E $(shell find src tests -name *.py)

@ -84,6 +84,19 @@ STATIC mp_obj_t mod_trezorcrypto_random_shuffle(mp_obj_t data) {
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_random_shuffle_obj,
mod_trezorcrypto_random_shuffle);
#ifdef TREZOR_EMULATOR
/// def reseed(value: int) -> None:
/// """
/// Re-seed the RNG with given value.
/// """
STATIC mp_obj_t mod_trezorcrypto_random_reseed(mp_obj_t data) {
random_reseed(trezor_obj_get_uint(data));
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_random_reseed_obj,
mod_trezorcrypto_random_reseed);
#endif
STATIC const mp_rom_map_elem_t mod_trezorcrypto_random_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_random)},
{MP_ROM_QSTR(MP_QSTR_uniform),
@ -92,6 +105,10 @@ STATIC const mp_rom_map_elem_t mod_trezorcrypto_random_globals_table[] = {
MP_ROM_PTR(&mod_trezorcrypto_random_bytes_obj)},
{MP_ROM_QSTR(MP_QSTR_shuffle),
MP_ROM_PTR(&mod_trezorcrypto_random_shuffle_obj)},
#ifdef TREZOR_EMULATOR
{MP_ROM_QSTR(MP_QSTR_reseed),
MP_ROM_PTR(&mod_trezorcrypto_random_reseed_obj)},
#endif
};
STATIC MP_DEFINE_CONST_DICT(mod_trezorcrypto_random_globals,
mod_trezorcrypto_random_globals_table);

@ -499,3 +499,5 @@ void display_refresh(void) {
}
const char *display_save(const char *prefix) { return NULL; }
void display_clear_save(void) {}

@ -54,6 +54,8 @@ static SDL_Renderer *RENDERER;
static SDL_Surface *BUFFER;
static SDL_Texture *TEXTURE, *BACKGROUND;
static SDL_Surface *PREV_SAVED;
int sdl_display_res_x = DISPLAY_RESX, sdl_display_res_y = DISPLAY_RESY;
int sdl_touch_offset_x, sdl_touch_offset_y;
@ -219,7 +221,6 @@ const char *display_save(const char *prefix) {
}
static int count;
static char filename[256];
static SDL_Surface *prev;
// take a cropped view of the screen contents
const SDL_Rect rect = {0, 0, DISPLAY_RESX, DISPLAY_RESY};
SDL_Surface *crop = SDL_CreateRGBSurface(
@ -228,16 +229,21 @@ const char *display_save(const char *prefix) {
BUFFER->format->Amask);
SDL_BlitSurface(BUFFER, &rect, crop, NULL);
// compare with previous screen, skip if equal
if (prev != NULL) {
if (memcmp(prev->pixels, crop->pixels, crop->pitch * crop->h) == 0) {
if (PREV_SAVED != NULL) {
if (memcmp(PREV_SAVED->pixels, crop->pixels, crop->pitch * crop->h) == 0) {
SDL_FreeSurface(crop);
return filename;
}
SDL_FreeSurface(prev);
SDL_FreeSurface(PREV_SAVED);
}
// save to png
snprintf(filename, sizeof(filename), "%s%08d.png", prefix, count++);
IMG_SavePNG(crop, filename);
prev = crop;
PREV_SAVED = crop;
return filename;
}
void display_clear_save(void) {
SDL_FreeSurface(PREV_SAVED);
PREV_SAVED = NULL;
}

@ -69,6 +69,7 @@
void display_init(void);
void display_refresh(void);
const char *display_save(const char *prefix);
void display_clear_save(void);
// provided by common

@ -535,6 +535,17 @@ STATIC mp_obj_t mod_trezorui_Display_save(mp_obj_t self, mp_obj_t prefix) {
STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorui_Display_save_obj,
mod_trezorui_Display_save);
/// def clear_save(self) -> None:
/// """
/// Clears buffers in display saving.
/// """
STATIC mp_obj_t mod_trezorui_Display_clear_save(mp_obj_t self) {
display_clear_save();
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorui_Display_clear_save_obj,
mod_trezorui_Display_clear_save);
STATIC const mp_rom_map_elem_t mod_trezorui_Display_locals_dict_table[] = {
{MP_ROM_QSTR(MP_QSTR_clear), MP_ROM_PTR(&mod_trezorui_Display_clear_obj)},
{MP_ROM_QSTR(MP_QSTR_refresh),
@ -561,6 +572,8 @@ STATIC const mp_rom_map_elem_t mod_trezorui_Display_locals_dict_table[] = {
MP_ROM_PTR(&mod_trezorui_Display_backlight_obj)},
{MP_ROM_QSTR(MP_QSTR_offset), MP_ROM_PTR(&mod_trezorui_Display_offset_obj)},
{MP_ROM_QSTR(MP_QSTR_save), MP_ROM_PTR(&mod_trezorui_Display_save_obj)},
{MP_ROM_QSTR(MP_QSTR_clear_save),
MP_ROM_PTR(&mod_trezorui_Display_clear_save_obj)},
{MP_ROM_QSTR(MP_QSTR_WIDTH), MP_ROM_INT(DISPLAY_RESX)},
{MP_ROM_QSTR(MP_QSTR_HEIGHT), MP_ROM_INT(DISPLAY_RESY)},
{MP_ROM_QSTR(MP_QSTR_FONT_SIZE), MP_ROM_INT(FONT_SIZE)},

@ -21,3 +21,10 @@ def shuffle(data: list) -> None:
"""
Shuffles items of given list (in-place).
"""
# extmod/modtrezorcrypto/modtrezorcrypto-random.h
def reseed(value: int) -> None:
"""
Re-seed the RNG with given value.
"""

@ -187,3 +187,8 @@ class Display:
"""
Saves current display contents to PNG file with given prefix.
"""
def clear_save(self) -> None:
"""
Clears buffers in display saving.
"""

@ -1,5 +1,5 @@
import storage.device
from trezor import ui, workflow
from trezor import ui, utils, workflow
from trezor.crypto import bip39, slip39
from trezor.messages import BackupType
@ -34,7 +34,7 @@ def get_seed(passphrase: str = "", progress_bar: bool = True) -> bytes:
raise ValueError("Mnemonic not set")
render_func = None
if progress_bar:
if progress_bar and not utils.DISABLE_ANIMATION:
_start_progress()
render_func = _render_progress
@ -62,11 +62,11 @@ def _start_progress() -> None:
ui.backlight_fade(ui.BACKLIGHT_DIM)
ui.display.clear()
ui.header("Please wait")
ui.display.refresh()
ui.refresh()
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
def _render_progress(progress: int, total: int) -> None:
p = 1000 * progress // total
ui.display.loader(p, False, 18, ui.WHITE, ui.BG)
ui.display.refresh()
ui.refresh()

@ -4,16 +4,23 @@ if not __debug__:
halt("debug mode inactive")
if __debug__:
from trezor import config, io, log, loop, ui, utils, wire
from trezor import io, ui, wire
from trezor.messages import MessageType, DebugSwipeDirection
from trezor.messages.DebugLinkLayout import DebugLinkLayout
from trezor import config, crypto, log, loop, utils
from trezor.messages.Success import Success
if False:
from typing import List, Optional
from trezor.messages.DebugLinkDecision import DebugLinkDecision
from trezor.messages.DebugLinkGetState import DebugLinkGetState
from trezor.messages.DebugLinkRecordScreen import DebugLinkRecordScreen
from trezor.messages.DebugLinkReseedRandom import DebugLinkReseedRandom
from trezor.messages.DebugLinkState import DebugLinkState
save_screen = False
save_screen_directory = "."
reset_internal_entropy = None # type: Optional[bytes]
reset_current_words = loop.chan()
reset_word_index = loop.chan()
@ -30,6 +37,10 @@ if __debug__:
layout_change_chan = loop.chan()
current_content = None # type: Optional[List[str]]
def screenshot() -> None:
if utils.SAVE_SCREEN or save_screen:
ui.display.save(save_screen_directory + "/refresh-")
def notify_layout_change(layout: ui.Layout) -> None:
global current_content
current_content = layout.read_content()
@ -104,12 +115,35 @@ if __debug__:
m.reset_word = " ".join(await reset_current_words.take())
return m
async def dispatch_DebugLinkRecordScreen(
ctx: wire.Context, msg: DebugLinkRecordScreen
) -> Success:
global save_screen_directory
global save_screen
if msg.target_directory:
save_screen_directory = msg.target_directory
save_screen = True
else:
save_screen = False
ui.display.clear_save() # clear C buffers
return Success()
async def dispatch_DebugLinkReseedRandom(
ctx: wire.Context, msg: DebugLinkReseedRandom
) -> Success:
if msg.value is not None:
crypto.random.reseed(msg.value)
return Success()
def boot() -> None:
# wipe storage when debug build is used on real hardware
if not utils.EMULATOR:
config.wipe()
wire.add(MessageType.LoadDevice, __name__, "load_device")
wire.register(MessageType.DebugLinkDecision, dispatch_DebugLinkDecision)
wire.register(MessageType.DebugLinkGetState, dispatch_DebugLinkGetState)
wire.add(MessageType.LoadDevice, __name__, "load_device")
wire.register(MessageType.DebugLinkReseedRandom, dispatch_DebugLinkReseedRandom)
wire.register(MessageType.DebugLinkRecordScreen, dispatch_DebugLinkRecordScreen)

@ -1,4 +1,4 @@
from trezor import ui
from trezor import ui, utils
_progress = 0
_steps = 0
@ -24,5 +24,7 @@ def report_init():
def report():
if utils.DISABLE_ANIMATION:
return
p = 1000 * _progress // _steps
ui.display.loader(p, False, 18, ui.WHITE, ui.BG)

@ -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 DebugLinkRecordScreen(p.MessageType):
MESSAGE_WIRE_TYPE = 9003
def __init__(
self,
target_directory: str = None,
) -> None:
self.target_directory = target_directory
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('target_directory', p.UnicodeType, 0),
}

@ -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 DebugLinkReseedRandom(p.MessageType):
MESSAGE_WIRE_TYPE = 9002
def __init__(
self,
value: int = None,
) -> None:
self.value = value
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('value', p.UVarintType, 0),
}

@ -74,6 +74,8 @@ DebugLinkMemory = 111 # type: Literal[111]
DebugLinkMemoryWrite = 112 # type: Literal[112]
DebugLinkFlashErase = 113 # type: Literal[113]
DebugLinkLayout = 9001 # type: Literal[9001]
DebugLinkReseedRandom = 9002 # type: Literal[9002]
DebugLinkRecordScreen = 9003 # type: Literal[9003]
if not utils.BITCOIN_ONLY:
EthereumGetPublicKey = 450 # type: Literal[450]
EthereumPublicKey = 451 # type: Literal[451]

@ -1,4 +1,4 @@
from trezor import ui
from trezor import ui, utils
if False:
from typing import Any, Optional
@ -28,7 +28,9 @@ def show_pin_timeout(seconds: int, progress: int, message: str) -> bool:
ui.display.text_center(
ui.WIDTH // 2, 37, message, ui.BOLD, ui.FG, ui.BG, ui.WIDTH
)
ui.display.loader(progress, False, 0, ui.FG, ui.BG)
if not utils.DISABLE_ANIMATION:
ui.display.loader(progress, False, 0, ui.FG, ui.BG)
if seconds != _previous_seconds:
if seconds == 0:
@ -42,6 +44,6 @@ def show_pin_timeout(seconds: int, progress: int, message: str) -> bool:
)
_previous_seconds = seconds
ui.display.refresh()
ui.refresh()
_previous_progress = progress
return False

@ -39,18 +39,21 @@ _alert_in_progress = False
# in debug mode, display an indicator in top right corner
if __debug__:
from apps.debug import screenshot
def debug_display_refresh() -> None:
def refresh() -> None:
display.bar(Display.WIDTH - 8, 0, 8, 8, 0xF800)
display.refresh()
if utils.SAVE_SCREEN:
display.save("refresh")
screenshot()
else:
refresh = display.refresh
loop.after_step_hook = debug_display_refresh
# in both debug and production, emulator needs to draw the screen explicitly
elif utils.EMULATOR:
loop.after_step_hook = display.refresh
if utils.EMULATOR:
loop.after_step_hook = refresh
def lerpi(a: int, b: int, t: float) -> int:
@ -120,7 +123,7 @@ async def click() -> Pos:
def backlight_fade(val: int, delay: int = 14000, step: int = 15) -> None:
if __debug__:
if utils.DISABLE_FADE:
if utils.DISABLE_ANIMATION:
display.backlight(val)
return
current = display.backlight()
@ -346,7 +349,7 @@ class Layout(Component):
# Display is usually refreshed after every loop step, but here we are
# rendering everything synchronously, so refresh it manually and turn
# the brightness on again.
display.refresh()
refresh()
backlight_fade(style.BACKLIGHT_NORMAL)
sleep = loop.sleep(_RENDER_DELAY_US)
while True:

@ -1,6 +1,6 @@
from micropython import const
from trezor import loop, res, ui
from trezor import loop, res, ui, utils
from trezor.ui.button import Button, ButtonCancel, ButtonConfirm, ButtonDefault
from trezor.ui.loader import Loader, LoaderDefault
@ -150,13 +150,19 @@ class ConfirmPageable(Confirm):
t = ui.pulse(PULSE_PERIOD)
c = ui.blend(ui.GREY, ui.DARK_GREY, t)
icon = res.load(ui.ICON_SWIPE_RIGHT)
ui.display.icon(18, 68, icon, c, ui.BG)
if utils.DISABLE_ANIMATION:
ui.display.icon(18, 68, icon, ui.GREY, ui.BG)
else:
ui.display.icon(18, 68, icon, c, ui.BG)
if not self.pageable.is_last():
t = ui.pulse(PULSE_PERIOD, PULSE_PERIOD // 2)
c = ui.blend(ui.GREY, ui.DARK_GREY, t)
icon = res.load(ui.ICON_SWIPE_LEFT)
ui.display.icon(205, 68, icon, c, ui.BG)
if utils.DISABLE_ANIMATION:
ui.display.icon(205, 68, icon, ui.GREY, ui.BG)
else:
ui.display.icon(205, 68, icon, c, ui.BG)
class InfoConfirm(ui.Layout):

@ -1,7 +1,7 @@
import utime
from micropython import const
from trezor import res, ui
from trezor import res, ui, utils
from trezor.ui import display
if False:
@ -74,13 +74,13 @@ class Loader(ui.Component):
else:
s = self.active_style
Y = const(-24)
_Y = const(-24)
if s.icon is None:
display.loader(r, False, Y, s.fg_color, s.bg_color)
display.loader(r, False, _Y, s.fg_color, s.bg_color)
else:
display.loader(
r, False, Y, s.fg_color, s.bg_color, res.load(s.icon), s.icon_fg_color
r, False, _Y, s.fg_color, s.bg_color, res.load(s.icon), s.icon_fg_color
)
if (r == 0) and (self.stop_ms is not None):
self.start_ms = None
@ -107,5 +107,8 @@ class LoadingAnimation(ui.Layout):
self.loader.start()
self.loader.dispatch(event, x, y)
if utils.DISABLE_ANIMATION:
self.on_finish()
def on_finish(self) -> None:
raise ui.Result(None)

@ -1,4 +1,4 @@
from trezor import loop, ui
from trezor import loop, ui, utils
if False:
from typing import Tuple
@ -7,7 +7,10 @@ if False:
class Popup(ui.Layout):
def __init__(self, content: ui.Component, time_ms: int = 0) -> None:
self.content = content
self.time_ms = time_ms
if utils.DISABLE_ANIMATION:
self.time_ms = 0
else:
self.time_ms = time_ms
def dispatch(self, event: int, x: int, y: int) -> None:
self.content.dispatch(event, x, y)

@ -1,6 +1,6 @@
from micropython import const
from trezor import loop, res, ui
from trezor import loop, res, ui, utils
from trezor.ui.button import Button, ButtonCancel, ButtonConfirm, ButtonDefault
from trezor.ui.confirm import CANCELLED, CONFIRMED
from trezor.ui.swipe import SWIPE_DOWN, SWIPE_UP, SWIPE_VERTICAL, Swipe
@ -32,6 +32,11 @@ def render_scrollbar(pages: int, page: int) -> None:
def render_swipe_icon() -> None:
if utils.DISABLE_ANIMATION:
icon = res.load(ui.ICON_SWIPE)
ui.display.icon(70, 205, icon, ui.GREY, ui.BG)
return
PULSE_PERIOD = const(1200000)
icon = res.load(ui.ICON_SWIPE)

@ -14,17 +14,18 @@ from trezorutils import ( # type: ignore[attr-defined] # noqa: F401
set_mode_unprivileged,
)
DISABLE_ANIMATION = 0
if __debug__:
if EMULATOR:
import uos
TEST = int(uos.getenv("TREZOR_TEST") or "0")
DISABLE_FADE = int(uos.getenv("TREZOR_DISABLE_FADE") or "0")
DISABLE_ANIMATION = int(uos.getenv("TREZOR_DISABLE_ANIMATION") or "0")
SAVE_SCREEN = int(uos.getenv("TREZOR_SAVE_SCREEN") or "0")
LOG_MEMORY = int(uos.getenv("TREZOR_LOG_MEMORY") or "0")
else:
TEST = 0
DISABLE_FADE = 0
SAVE_SCREEN = 0
LOG_MEMORY = 0

@ -6,7 +6,7 @@ CORE_DIR="$(SHELL_SESSION_FILE='' && cd "$( dirname "${BASH_SOURCE[0]}" )/.." >/
MICROPYTHON="${MICROPYTHON:-$CORE_DIR/build/unix/micropython}"
TREZOR_SRC="${CORE_DIR}/src"
DISABLE_FADE=1
DISABLE_ANIMATION=1
PYOPT="${PYOPT:-0}"
upy_pid=""
@ -22,7 +22,7 @@ if [[ $RUN_TEST_EMU > 0 ]]; then
echo "Starting emulator: $MICROPYTHON $ARGS ${MAIN}"
TREZOR_TEST=1 \
TREZOR_DISABLE_FADE=$DISABLE_FADE \
TREZOR_DISABLE_ANIMATION=$DISABLE_ANIMATION \
$MICROPYTHON $ARGS "${MAIN}" &> "${TREZOR_LOGFILE}" &
upy_pid=$!
cd -

@ -6,7 +6,6 @@ CORE_DIR="$(SHELL_SESSION_FILE='' && cd "$( dirname "${BASH_SOURCE[0]}" )/.." >/
MICROPYTHON="${MICROPYTHON:-$CORE_DIR/build/unix/micropython}"
TREZOR_SRC="${CORE_DIR}/src"
DISABLE_FADE=1
PYOPT="${PYOPT:-0}"
upy_pid=""
@ -22,7 +21,7 @@ if [[ $RUN_TEST_EMU > 0 ]]; then
echo "Starting emulator: $MICROPYTHON $ARGS ${MAIN}"
TREZOR_TEST=1 \
TREZOR_DISABLE_FADE=$DISABLE_FADE \
TREZOR_DISABLE_ANIMATION=1 \
$MICROPYTHON $ARGS "${MAIN}" &> "${TREZOR_LOGFILE}" &
upy_pid=$!
cd -

@ -6,7 +6,7 @@ CORE_DIR="$(SHELL_SESSION_FILE='' && cd "$( dirname "${BASH_SOURCE[0]}" )/.." >/
MICROPYTHON="${MICROPYTHON:-$CORE_DIR/build/unix/micropython}"
TREZOR_SRC="${CORE_DIR}/src"
DISABLE_FADE=1
DISABLE_ANIMATION=1
PYOPT="${PYOPT:-0}"
upy_pid=""
@ -22,7 +22,7 @@ if [[ $RUN_TEST_EMU > 0 ]]; then
echo "Starting emulator: $MICROPYTHON $ARGS ${MAIN}"
TREZOR_TEST=1 \
TREZOR_DISABLE_FADE=$DISABLE_FADE \
TREZOR_DISABLE_ANIMATION=$DISABLE_ANIMATION \
$MICROPYTHON $ARGS "${MAIN}" &> "${TREZOR_LOGFILE}" &
upy_pid=$!
cd -

@ -8,7 +8,7 @@ CORE_DIR="$(SHELL_SESSION_FILE='' && cd "$( dirname "${BASH_SOURCE[0]}" )/.." >/
MICROPYTHON="${MICROPYTHON:-$CORE_DIR/build/unix/micropython}"
TREZOR_SRC="${CORE_DIR}/src"
DISABLE_FADE=1
DISABLE_ANIMATION=1
PYOPT="${PYOPT:-0}"
upy_pid=""
@ -24,7 +24,7 @@ if [[ $RUN_TEST_EMU > 0 ]]; then
echo "Starting emulator: $MICROPYTHON $ARGS ${MAIN}"
TREZOR_TEST=1 \
TREZOR_DISABLE_FADE=$DISABLE_FADE \
TREZOR_DISABLE_ANIMATION=$DISABLE_ANIMATION \
$MICROPYTHON $ARGS "${MAIN}" &> "${TREZOR_LOGFILE}" &
upy_pid=$!
cd -

@ -6,7 +6,7 @@ CORE_DIR="$(SHELL_SESSION_FILE='' && cd "$( dirname "${BASH_SOURCE[0]}" )/.." >/
MICROPYTHON="${MICROPYTHON:-$CORE_DIR/build/unix/micropython}"
TREZOR_SRC="${CORE_DIR}/src"
DISABLE_FADE=1
DISABLE_ANIMATION=1
PYOPT="${PYOPT:-0}"
upy_pid=""
@ -22,7 +22,7 @@ if [[ $RUN_TEST_EMU > 0 ]]; then
echo "Starting emulator: $MICROPYTHON $ARGS ${MAIN}"
TREZOR_TEST=1 \
TREZOR_DISABLE_FADE=$DISABLE_FADE \
TREZOR_DISABLE_ANIMATION=$DISABLE_ANIMATION \
$MICROPYTHON $ARGS "${MAIN}" &> "${TREZOR_LOGFILE}" &
upy_pid=$!
cd -

@ -82,9 +82,9 @@ If ``` TREZOR_SAVE_SCREEN=1 ``` is set, the emulator makes print screen on every
If ```TREZOR_LOG_MEMORY=1``` is set, the emulator prints memory usage information after each workflow task is finished.
#### Disable fade
#### Disable animations
```TREZOR_DISABLE_FADE=1``` disables fading, which speeds up the UI workflows (useful for tests).
```TREZOR_DISABLE_ANIMATION=1``` disables fading and other animations, which speeds up the UI workflows significantly (useful for tests). This is also requirement for UI integration tests.
#### Tests

@ -2,7 +2,7 @@ ifneq ($(V),1)
Q := @
endif
SKIPPED_MESSAGES := Binance Cardano DebugMonero Eos Monero Ontology Ripple SdProtect Tezos WebAuthn
SKIPPED_MESSAGES := Binance Cardano DebugMonero Eos Monero Ontology Ripple SdProtect Tezos WebAuthn DebugLinkRecordScreen DebugLinkReseedRandom
ifeq ($(BITCOIN_ONLY), 1)
SKIPPED_MESSAGES += Ethereum Lisk NEM Stellar

@ -15,7 +15,8 @@ DebugLinkMemory.memory max_size:1024
DebugLinkMemoryWrite.memory max_size:1024
# unused fields
DebugLinkState.layout_lines max_count:0
DebugLinkState.layout_lines max_size:1
DebugLinkLayout.lines max_size:1
DebugLinkLayout.lines max_count:0
DebugLinkState.layout_lines max_count:10
DebugLinkState.layout_lines max_size:30
DebugLinkLayout.lines max_count:10
DebugLinkLayout.lines max_size:30
DebugLinkRecordScreen.target_directory max_size:16

@ -138,6 +138,15 @@ class DebugLink:
def stop(self):
self._call(proto.DebugLinkStop(), nowait=True)
def reseed(self, value):
self._call(proto.DebugLinkReseedRandom(value=value))
def start_recording(self, directory):
self._call(proto.DebugLinkRecordScreen(target_directory=directory))
def stop_recording(self):
self._call(proto.DebugLinkRecordScreen(target_directory=None))
@expect(proto.DebugLinkMemory, field="memory")
def memory_read(self, address, length):
return self._call(proto.DebugLinkMemoryRead(address=address, length=length))

@ -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 DebugLinkRecordScreen(p.MessageType):
MESSAGE_WIRE_TYPE = 9003
def __init__(
self,
target_directory: str = None,
) -> None:
self.target_directory = target_directory
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('target_directory', p.UnicodeType, 0),
}

@ -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 DebugLinkReseedRandom(p.MessageType):
MESSAGE_WIRE_TYPE = 9002
def __init__(
self,
value: int = None,
) -> None:
self.value = value
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('value', p.UVarintType, 0),
}

@ -72,6 +72,8 @@ DebugLinkMemory = 111 # type: Literal[111]
DebugLinkMemoryWrite = 112 # type: Literal[112]
DebugLinkFlashErase = 113 # type: Literal[113]
DebugLinkLayout = 9001 # type: Literal[9001]
DebugLinkReseedRandom = 9002 # type: Literal[9002]
DebugLinkRecordScreen = 9003 # type: Literal[9003]
EthereumGetPublicKey = 450 # type: Literal[450]
EthereumPublicKey = 451 # type: Literal[451]
EthereumGetAddress = 56 # type: Literal[56]

@ -47,6 +47,8 @@ from .DebugLinkLog import DebugLinkLog
from .DebugLinkMemory import DebugLinkMemory
from .DebugLinkMemoryRead import DebugLinkMemoryRead
from .DebugLinkMemoryWrite import DebugLinkMemoryWrite
from .DebugLinkRecordScreen import DebugLinkRecordScreen
from .DebugLinkReseedRandom import DebugLinkReseedRandom
from .DebugLinkState import DebugLinkState
from .DebugLinkStop import DebugLinkStop
from .DebugMoneroDiagAck import DebugMoneroDiagAck

@ -37,6 +37,8 @@ MNEMONIC_SLIP39_ADVANCED_33 = [
"wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium",
"wildlife deal acrobat romp anxiety axis starting require metric flexible geology game drove editor edge screw helpful have huge holy making pitch unknown carve holiday numb glasses survive already tenant adapt goat fangs",
]
# External entropy mocked as received from trezorlib.
EXTERNAL_ENTROPY = b"zlutoucky kun upel divoke ody" * 2
# fmt: on

@ -14,7 +14,13 @@
# 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 filecmp
import hashlib
import itertools
import os
import re
from contextlib import contextmanager
from pathlib import Path
import pytest
@ -48,8 +54,124 @@ def get_device():
raise RuntimeError("No debuggable device found")
def _get_test_dirname(node):
# This composes the dirname from the test module name and test item name.
# Test item name is usually function name, but when parametrization is used,
# parameters are also part of the name. Some functions have very long parameter
# names (tx hashes etc) that run out of maximum allowable filename length, so
# we limit the name to first 100 chars. This is not a problem with txhashes.
node_name = re.sub(r"\W+", "_", node.name)[:100]
node_module_name = node.getparent(pytest.Module).name
return "{}_{}".format(node_module_name, node_name)
def _check_screen_fixtures_dir(fixture_dir):
if fixture_dir.exists():
# remove old fixtures
for fixture in fixture_dir.iterdir():
fixture.unlink()
else:
# create the fixture dir, if not present
fixture_dir.mkdir()
def _record_screen_fixtures(fixture_dir, test_dir):
_check_screen_fixtures_dir(fixture_dir)
# move recorded screenshots into fixture directory
records = sorted(test_dir.iterdir())
for index, record in enumerate(sorted(records)):
fixture = fixture_dir / "{:08}.png".format(index)
record.replace(fixture)
def _hash_screen_fixtures(fixture_dir, test_dir):
_check_screen_fixtures_dir(fixture_dir)
# hash recorded screenshots
records = sorted(test_dir.iterdir())
digest = _hash_files(records)
with open(fixture_dir / "hash.txt", "w") as f:
f.write(digest)
def _hash_files(files):
hasher = hashlib.sha256()
for file in sorted(files):
with open(file, "rb") as f:
content = f.read()
hasher.update(content)
return hasher.digest().hex()
def _assert_screen_recording(fixture_dir, test_dir):
fixtures = sorted(fixture_dir.iterdir())
records = sorted(test_dir.iterdir())
if not fixtures:
return
for fixture, image in itertools.zip_longest(fixtures, records):
if fixture is None:
pytest.fail("Missing fixture for image {}".format(image))
if image is None:
pytest.fail("Missing image for fixture {}".format(fixture))
if not filecmp.cmp(fixture, image):
pytest.fail("Image {} and fixture {} differ".format(image, fixture))
def _assert_screen_hashes(fixture_dir, test_dir):
records = sorted(test_dir.iterdir())
hash_file = fixture_dir / "hash.txt"
if not hash_file.exists():
raise ValueError("File hash.txt not found.")
with open(hash_file, "r") as f:
expected_hash = f.read()
actual_hash = _hash_files(records)
if actual_hash != expected_hash:
pytest.fail(
"Hash of {} differs.\nExpected: {}\nActual: {}".format(
fixture_dir.name, expected_hash, actual_hash
)
)
@contextmanager
def _screen_recording(client, request, tmp_path):
if not request.node.get_closest_marker("skip_ui"):
test_screen = request.config.getoption("test_screen")
else:
test_screen = ""
fixture_root = Path(__file__) / "../ui_tests"
try:
if test_screen:
client.debug.start_recording(str(tmp_path))
yield
finally:
if test_screen:
client.debug.stop_recording()
fixture_path = fixture_root.resolve() / _get_test_dirname(request.node)
if test_screen == "record":
_record_screen_fixtures(fixture_path, tmp_path)
elif test_screen == "hash":
_hash_screen_fixtures(fixture_path, tmp_path)
elif test_screen == "test-hash":
_assert_screen_hashes(fixture_path, tmp_path)
elif test_screen == "test-record":
_assert_screen_recording(fixture_path, tmp_path)
else:
raise ValueError("Invalid test_screen option.")
@pytest.fixture(scope="function")
def client(request):
def client(request, tmp_path):
"""Client fixture.
Every test function that requires a client instance will get it from here.
@ -99,6 +221,7 @@ def client(request):
passphrase=False,
needs_backup=False,
no_backup=False,
random_seed=None,
)
# fmt: on
@ -128,10 +251,25 @@ def client(request):
client.clear_session()
client.open()
yield client
if setup_params["random_seed"] is not None:
client.debug.reseed(setup_params["random_seed"])
with _screen_recording(client, request, tmp_path):
yield client
client.close()
def pytest_addoption(parser):
parser.addoption(
"--test_screen",
action="store",
default="",
help="Enable UI intergration tests: 'record', 'hash' or 'test-hash' and 'test-record'",
)
def pytest_configure(config):
"""Called at testsuite setup time.
@ -144,6 +282,9 @@ def pytest_configure(config):
"markers",
'setup_client(mnemonic="all all all...", pin=None, passphrase=False, uninitialized=False): configure the client instance',
)
config.addinivalue_line(
"markers", "skip_ui: skip UI integration checks for this test"
)
with open(os.path.join(os.path.dirname(__file__), "REGISTERED_MARKERS")) as f:
for line in f:
config.addinivalue_line("markers", line.strip())

@ -32,7 +32,7 @@ from ..common import (
@pytest.mark.skip_t1 # TODO we want this for t1 too
@pytest.mark.setup_client(needs_backup=True, mnemonic=MNEMONIC12)
@pytest.mark.setup_client(needs_backup=True, mnemonic=MNEMONIC12, random_seed=0)
def test_backup_bip39(client):
assert client.features.needs_backup is True
mnemonic = None
@ -71,7 +71,9 @@ def test_backup_bip39(client):
@pytest.mark.skip_t1
@pytest.mark.setup_client(needs_backup=True, mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6)
@pytest.mark.setup_client(
needs_backup=True, mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6, random_seed=0
)
def test_backup_slip39_basic(client):
assert client.features.needs_backup is True
mnemonics = []
@ -136,7 +138,9 @@ def test_backup_slip39_basic(client):
@pytest.mark.skip_t1
@pytest.mark.setup_client(needs_backup=True, mnemonic=MNEMONIC_SLIP39_ADVANCED_20)
@pytest.mark.setup_client(
needs_backup=True, mnemonic=MNEMONIC_SLIP39_ADVANCED_20, random_seed=0
)
def test_backup_slip39_advanced(client):
assert client.features.needs_backup is True
mnemonics = []

@ -100,7 +100,7 @@ def _check_wipe_code(client, pin, wipe_code):
device.change_pin(client)
@pytest.mark.setup_client(pin=PIN4)
@pytest.mark.setup_client(pin=PIN4, random_seed=0)
def test_set_remove_wipe_code(client):
# Test set wipe code.
assert client.features.wipe_code_protection is False
@ -143,6 +143,7 @@ def test_set_remove_wipe_code(client):
assert client.features.wipe_code_protection is False
@pytest.mark.setup_client(random_seed=0)
def test_set_wipe_code_mismatch(client):
# Let's set a wipe code.
def input_flow():
@ -170,7 +171,7 @@ def test_set_wipe_code_mismatch(client):
assert client.features.wipe_code_protection is False
@pytest.mark.setup_client(pin=PIN4)
@pytest.mark.setup_client(pin=PIN4, random_seed=0)
def test_set_wipe_code_to_pin(client):
def input_flow():
yield # do you want to set the wipe code?
@ -201,6 +202,7 @@ def test_set_wipe_code_to_pin(client):
_check_wipe_code(client, PIN4, WIPE_CODE4)
@pytest.mark.setup_client(random_seed=0)
def test_set_pin_to_wipe_code(client):
# Set wipe code.
with client:
@ -221,7 +223,10 @@ def test_set_pin_to_wipe_code(client):
device.change_pin(client)
# TODO: this UI test should not be skipped, but when setting random_seed=0 it fails
# on device id match and I am not sure why
@pytest.mark.setup_client(pin=PIN4)
@pytest.mark.skip_ui
def test_wipe_code_activate(client):
import time

@ -96,6 +96,7 @@ def _check_no_pin(client):
assert client.features.pin_protection is False
@pytest.mark.setup_client(random_seed=0)
def test_set_pin(client):
assert client.features.pin_protection is False
@ -116,7 +117,7 @@ def test_set_pin(client):
_check_pin(client, PIN6)
@pytest.mark.setup_client(pin=PIN4)
@pytest.mark.setup_client(pin=PIN4, random_seed=0)
def test_change_pin(client):
assert client.features.pin_protection is True
@ -139,7 +140,7 @@ def test_change_pin(client):
_check_pin(client, PIN6)
@pytest.mark.setup_client(pin=PIN4)
@pytest.mark.setup_client(pin=PIN4, random_seed=0)
def test_remove_pin(client):
assert client.features.pin_protection is True
@ -161,6 +162,7 @@ def test_remove_pin(client):
_check_no_pin(client)
@pytest.mark.setup_client(random_seed=0)
def test_set_failed(client):
assert client.features.pin_protection is False
@ -194,7 +196,7 @@ def test_set_failed(client):
_check_no_pin(client)
@pytest.mark.setup_client(pin=PIN4)
@pytest.mark.setup_client(pin=PIN4, random_seed=0)
def test_change_failed(client):
assert client.features.pin_protection is True

@ -23,7 +23,7 @@ from ..common import MNEMONIC12
@pytest.mark.skip_t1
class TestMsgRecoverydeviceT2:
@pytest.mark.setup_client(uninitialized=True)
@pytest.mark.setup_client(uninitialized=True, random_seed=0)
def test_pin_passphrase(self, client):
mnemonic = MNEMONIC12.split(" ")
ret = client.call_raw(

@ -66,7 +66,7 @@ def test_secret(client, shares, secret):
assert debug.read_mnemonic_secret().hex() == secret
@pytest.mark.setup_client(uninitialized=True)
@pytest.mark.setup_client(uninitialized=True, random_seed=0)
def test_extra_share_entered(client):
debug = client.debug

@ -70,7 +70,7 @@ def test_secret(client, shares, secret):
assert debug.read_mnemonic_secret().hex() == secret
@pytest.mark.setup_client(uninitialized=True)
@pytest.mark.setup_client(uninitialized=True, random_seed=0)
def test_recover_with_pin_passphrase(client):
debug = client.debug

@ -35,7 +35,7 @@ EXTERNAL_ENTROPY = b"zlutoucky kun upel divoke ody" * 2
@pytest.mark.skip_t1
class TestMsgResetDeviceT2:
@pytest.mark.setup_client(uninitialized=True)
@pytest.mark.setup_client(uninitialized=True, random_seed=0)
def test_reset_device(self, client):
mnemonic = None
strength = 128
@ -110,7 +110,7 @@ class TestMsgResetDeviceT2:
with pytest.raises(TrezorFailure, match="ProcessError: Seed already backed up"):
device.backup(client)
@pytest.mark.setup_client(uninitialized=True)
@pytest.mark.setup_client(uninitialized=True, random_seed=0)
def test_reset_device_pin(self, client):
mnemonic = None
strength = 128
@ -207,7 +207,7 @@ class TestMsgResetDeviceT2:
assert resp.pin_protection is True
assert resp.passphrase_protection is True
@pytest.mark.setup_client(uninitialized=True)
@pytest.mark.setup_client(uninitialized=True, random_seed=0)
def test_failed_pin(self, client):
# external_entropy = b'zlutoucky kun upel divoke ody' * 2
strength = 128

@ -31,7 +31,7 @@ EXTERNAL_ENTROPY = b"zlutoucky kun upel divoke ody" * 2
@pytest.mark.skip_t1
class TestMsgResetDeviceT2:
# TODO: test with different options
@pytest.mark.setup_client(uninitialized=True)
@pytest.mark.setup_client(uninitialized=True, random_seed=0)
def test_reset_device_slip39_advanced(self, client):
strength = 128
word_count = 20

@ -25,15 +25,18 @@ from trezorlib import device, messages as proto
from trezorlib.exceptions import TrezorFailure
from trezorlib.messages import BackupType, ButtonRequestType as B
from ..common import click_through, generate_entropy, read_and_confirm_mnemonic
EXTERNAL_ENTROPY = b"zlutoucky kun upel divoke ody" * 2
from ..common import (
EXTERNAL_ENTROPY,
click_through,
generate_entropy,
read_and_confirm_mnemonic,
)
@pytest.mark.skip_t1
class TestMsgResetDeviceT2:
# TODO: test with different options
@pytest.mark.setup_client(uninitialized=True)
@pytest.mark.setup_client(uninitialized=True, random_seed=0)
def test_reset_device_slip39_basic(self, client):
strength = 128
member_threshold = 3

@ -369,6 +369,7 @@ class TestMsgSigntx:
)
@pytest.mark.setup_client(mnemonic=MNEMONIC12)
@pytest.mark.skip_ui
def test_lots_of_inputs(self, client):
# Tests if device implements serialization of len(inputs) correctly
# tx 4a7b7e0403ae5607e473949cfa03f09f2cd8b0f404bf99ce10b7303d86280bf7 : 100 UTXO for spending for unit tests
@ -397,6 +398,7 @@ class TestMsgSigntx:
)
@pytest.mark.setup_client(mnemonic=MNEMONIC12)
@pytest.mark.skip_ui
def test_lots_of_outputs(self, client):
# Tests if device implements serialization of len(outputs) correctly

@ -15,13 +15,15 @@
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
from unittest import mock
import pytest
import shamir_mnemonic as shamir
from trezorlib import device, messages
from trezorlib.messages import BackupType, ButtonRequestType as B
from ..common import click_through, read_and_confirm_mnemonic
from ..common import EXTERNAL_ENTROPY, click_through, read_and_confirm_mnemonic
def backup_flow_bip39(client):
@ -176,15 +178,18 @@ VECTORS = [
@pytest.mark.skip_t1
@pytest.mark.parametrize("backup_type, backup_flow", VECTORS)
@pytest.mark.setup_client(uninitialized=True)
@pytest.mark.setup_client(uninitialized=True, random_seed=0)
def test_skip_backup_msg(client, backup_type, backup_flow):
device.reset(
client,
skip_backup=True,
passphrase_protection=False,
pin_protection=False,
backup_type=backup_type,
)
os_urandom = mock.Mock(return_value=EXTERNAL_ENTROPY)
with mock.patch("os.urandom", os_urandom), client:
device.reset(
client,
skip_backup=True,
passphrase_protection=False,
pin_protection=False,
backup_type=backup_type,
)
assert client.features.initialized is True
assert client.features.needs_backup is True
@ -208,7 +213,7 @@ def test_skip_backup_msg(client, backup_type, backup_flow):
@pytest.mark.skip_t1
@pytest.mark.parametrize("backup_type, backup_flow", VECTORS)
@pytest.mark.setup_client(uninitialized=True)
@pytest.mark.setup_client(uninitialized=True, random_seed=0)
def test_skip_backup_manual(client, backup_type, backup_flow):
def reset_skip_input_flow():
yield # Confirm Recovery
@ -220,7 +225,8 @@ def test_skip_backup_manual(client, backup_type, backup_flow):
yield # Confirm skip backup
client.debug.press_no()
with client:
os_urandom = mock.Mock(return_value=EXTERNAL_ENTROPY)
with mock.patch("os.urandom", os_urandom), client:
client.set_input_flow(reset_skip_input_flow)
client.set_expected_responses(
[

@ -15,17 +15,20 @@
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
from unittest import mock
import pytest
from trezorlib import btc, device, messages
from trezorlib.messages import BackupType, ButtonRequestType as B
from trezorlib.tools import parse_path
from ..common import click_through, read_and_confirm_mnemonic
from ..common import EXTERNAL_ENTROPY, click_through, read_and_confirm_mnemonic
@pytest.mark.skip_t1
@pytest.mark.setup_client(uninitialized=True)
@pytest.mark.skip_ui
@pytest.mark.setup_client(uninitialized=True, random_seed=0)
def test_reset_recovery(client):
mnemonic = reset(client)
address_before = btc.get_address(client, "Bitcoin", parse_path("44'/0'/0'/0/0"))
@ -79,17 +82,19 @@ def reset(client, strength=128, skip_backup=False):
)
client.set_input_flow(input_flow)
# No PIN, no passphrase, don't display random
device.reset(
client,
display_random=False,
strength=strength,
passphrase_protection=False,
pin_protection=False,
label="test",
language="en-US",
backup_type=BackupType.Bip39,
)
os_urandom = mock.Mock(return_value=EXTERNAL_ENTROPY)
with mock.patch("os.urandom", os_urandom), client:
# No PIN, no passphrase, don't display random
device.reset(
client,
display_random=False,
strength=strength,
passphrase_protection=False,
pin_protection=False,
label="test",
language="en-US",
backup_type=BackupType.Bip39,
)
# Check if device is properly initialized
assert client.features.initialized is True

@ -14,17 +14,25 @@
# 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>.
from unittest import mock
import pytest
from trezorlib import btc, device, messages
from trezorlib.messages import BackupType, ButtonRequestType as B
from trezorlib.tools import parse_path
from ..common import click_through, read_and_confirm_mnemonic, recovery_enter_shares
from ..common import (
EXTERNAL_ENTROPY,
click_through,
read_and_confirm_mnemonic,
recovery_enter_shares,
)
@pytest.mark.skip_t1
@pytest.mark.setup_client(uninitialized=True)
@pytest.mark.skip_ui
@pytest.mark.setup_client(uninitialized=True, random_seed=0)
def test_reset_recovery(client):
mnemonics = reset(client)
address_before = btc.get_address(client, "Bitcoin", parse_path("44'/0'/0'/0/0"))
@ -89,7 +97,8 @@ def reset(client, strength=128):
assert btn_code == B.Success
client.debug.press_yes()
with client:
os_urandom = mock.Mock(return_value=EXTERNAL_ENTROPY)
with mock.patch("os.urandom", os_urandom), client:
client.set_expected_responses(
[
messages.ButtonRequest(code=B.ResetDevice),

@ -26,6 +26,7 @@ from ..common import click_through, read_and_confirm_mnemonic, recovery_enter_sh
@pytest.mark.skip_t1
@pytest.mark.skip_ui
@pytest.mark.setup_client(uninitialized=True)
def test_reset_recovery(client):
mnemonics = reset(client)

@ -0,0 +1 @@
b696f69c57970f113b4a5f26473493da99d11b672741efc41b213c8844b3c3c0

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

@ -0,0 +1 @@
23205f8049143e3b8426c2c641ae06d0d1abb74bb957068f17ad4dfb5172c8e0

@ -0,0 +1 @@
42ef69a79450eeec396e36f7fd13f89163c0a1cda167fe27811a613ea98a1b3a

@ -0,0 +1 @@
b63863667bfbd65effdab66e47fd007c3bf0f5b183966c00e05527dfc4f5a2bf

@ -0,0 +1 @@
444af44427fd2e4de1069643b8a1d73d49de9f685d884101b0b654851b9e7c84

@ -0,0 +1 @@
86a586907d8879e641661709e38ad9208e3e9feb40ef0024f0922fd33a5ee826

@ -0,0 +1 @@
225b3da1acac6e9a65106fcc4a01de8a44de035aedb4dcc21c09f439199fdf40

@ -0,0 +1 @@
93039a9472cfc9058563bd56e4a3dbe2e41af64744a61f6ee3255a04bd3a9366

@ -0,0 +1 @@
14fcdd2ded299ca099a35966cc9f21204b31de8d6bab9ec91cb64537bd70440c

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

@ -0,0 +1 @@
dfa63984406f8f8ab0fbe9986564f82c7d960b87fa991818501d166989c2dca7

@ -0,0 +1 @@
1d548189e9801c7c4421a52c36805c9f34751c126aa21ac87d6b62679c9f4ba4

@ -0,0 +1 @@
e69158befea51d888aabe1681edfcdaacc1c7edbb2d90bb265600ffda20ad30d

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save