1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-25 00:48:19 +00:00

tests: introduce UI tests for core

This commit is contained in:
Tomas Susanka 2019-12-09 16:01:04 +00:00
parent bc0c10bf3e
commit 51ef963738
442 changed files with 943 additions and 101 deletions

View File

@ -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

View File

@ -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

View File

@ -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];

View File

@ -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)

View File

@ -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);

View File

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

View File

@ -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;
}

View File

@ -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

View File

@ -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)},

View File

@ -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.
"""

View File

@ -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.
"""

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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),
}

View File

@ -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),
}

View File

@ -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]

View File

@ -1,4 +1,4 @@
from trezor import ui
from trezor import ui, utils
if False:
from typing import Any, Optional
@ -28,6 +28,8 @@ 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
)
if not utils.DISABLE_ANIMATION:
ui.display.loader(progress, False, 0, ui.FG, ui.BG)
if seconds != _previous_seconds:
@ -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

View File

@ -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:

View File

@ -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,12 +150,18 @@ class ConfirmPageable(Confirm):
t = ui.pulse(PULSE_PERIOD)
c = ui.blend(ui.GREY, ui.DARK_GREY, t)
icon = res.load(ui.ICON_SWIPE_RIGHT)
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)
if utils.DISABLE_ANIMATION:
ui.display.icon(205, 68, icon, ui.GREY, ui.BG)
else:
ui.display.icon(205, 68, icon, c, ui.BG)

View File

@ -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)

View File

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

View File

@ -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)

View File

@ -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

View File

@ -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 -

View File

@ -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 -

View File

@ -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 -

View File

@ -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 -

View File

@ -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 -

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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),
}

View File

@ -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),
}

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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()
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())

View File

@ -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 = []

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,8 +178,11 @@ 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):
os_urandom = mock.Mock(return_value=EXTERNAL_ENTROPY)
with mock.patch("os.urandom", os_urandom), client:
device.reset(
client,
skip_backup=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(
[

View File

@ -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,6 +82,8 @@ def reset(client, strength=128, skip_backup=False):
)
client.set_input_flow(input_flow)
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,

View File

@ -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),

View File

@ -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)

View File

@ -0,0 +1 @@
b696f69c57970f113b4a5f26473493da99d11b672741efc41b213c8844b3c3c0

View File

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

View File

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

View File

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

View File

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

View File

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

View File

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

View File

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

View File

@ -0,0 +1 @@
23205f8049143e3b8426c2c641ae06d0d1abb74bb957068f17ad4dfb5172c8e0

View File

@ -0,0 +1 @@
42ef69a79450eeec396e36f7fd13f89163c0a1cda167fe27811a613ea98a1b3a

View File

@ -0,0 +1 @@
b63863667bfbd65effdab66e47fd007c3bf0f5b183966c00e05527dfc4f5a2bf

View File

@ -0,0 +1 @@
444af44427fd2e4de1069643b8a1d73d49de9f685d884101b0b654851b9e7c84

View File

@ -0,0 +1 @@
86a586907d8879e641661709e38ad9208e3e9feb40ef0024f0922fd33a5ee826

View File

@ -0,0 +1 @@
225b3da1acac6e9a65106fcc4a01de8a44de035aedb4dcc21c09f439199fdf40

View File

@ -0,0 +1 @@
93039a9472cfc9058563bd56e4a3dbe2e41af64744a61f6ee3255a04bd3a9366

View File

@ -0,0 +1 @@
14fcdd2ded299ca099a35966cc9f21204b31de8d6bab9ec91cb64537bd70440c

View File

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

View File

@ -0,0 +1 @@
43b1c496210d785bb032107ed5f647f5bd4471ca6b8bdd905afd8d34560bc03a

View File

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

View File

@ -0,0 +1 @@
a683bcaaa1469625167d0c9e0848e3785b0b0e82b4c904eb3c6bfcbb1d7bd262

View File

@ -0,0 +1 @@
737ac35c04567c6342ab3d34aac7ca1f99d4bcb15574a1d60b35215390e86857

View File

@ -0,0 +1 @@
d6d6bddda46fe2b43da4e11ca7cee24fb1f77f267f672b82bfb9951d749d5a26

View File

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

View File

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

View File

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

View File

@ -0,0 +1 @@
fe7055240ecba7d47b81acc4b896bc1376ef40bfbe17153b5ab496ffa7cc4acf

View File

@ -0,0 +1 @@
fe7055240ecba7d47b81acc4b896bc1376ef40bfbe17153b5ab496ffa7cc4acf

View File

@ -0,0 +1 @@
fe7055240ecba7d47b81acc4b896bc1376ef40bfbe17153b5ab496ffa7cc4acf

View File

@ -0,0 +1 @@
dfa63984406f8f8ab0fbe9986564f82c7d960b87fa991818501d166989c2dca7

View File

@ -0,0 +1 @@
1d548189e9801c7c4421a52c36805c9f34751c126aa21ac87d6b62679c9f4ba4

View File

@ -0,0 +1 @@
e69158befea51d888aabe1681edfcdaacc1c7edbb2d90bb265600ffda20ad30d

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