1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-21 23:18:13 +00:00

tests: introduce UI tests for core with diffs (#784)

tests: introduce UI tests for core with diffs
This commit is contained in:
Tomas Susanka 2020-01-10 20:39:31 +01:00 committed by GitHub
commit 7c41b40dff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
653 changed files with 1373 additions and 88 deletions

View File

@ -18,6 +18,7 @@ flaky = ">=3.6.1" # https://github.com/box/flaky/issues/156
pytest-ordering = "*"
pytest-random-order = "*"
tox = "*"
dominate = "*"
## test requirements
shamir-mnemonic = "*"

10
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "ef2e6e714004592166f0d16d22887ef55ddb3d61b49ac882e7ead764b638d7f9"
"sha256": "e8d9a82935300b8716e549422ded189b2ce4408bc31b8d5c91bbe9979bf15a0a"
},
"pipfile-spec": 6,
"requires": {},
@ -196,6 +196,14 @@
"index": "pypi",
"version": "==2.2.4"
},
"dominate": {
"hashes": [
"sha256:6e833aea505f0236a9fc692326bac575f8bd38ae0f3a1bdc73d20ca606ac75d5",
"sha256:a92474b4312bd8b4c1789792f3ec8c571cd8afa8e7502a2b1c64dd48cd67e59c"
],
"index": "pypi",
"version": "==2.4.0"
},
"ecdsa": {
"hashes": [
"sha256:867ec9cf6df0b03addc8ef66b56359643cb5d0c1dc329df76ba7ecfe256c8061",

View File

@ -194,3 +194,18 @@ upgrade tests legacy deploy:
- branches # run for tags only
tags:
- deploy
# UI tests
ui tests core fixtures deploy:
stage: deploy
variables:
DEPLOY_PATH: "${DEPLOY_BASE_DIR}/ui_tests/"
before_script: [] # no pipenv
dependencies:
- core unix device ui test
script:
- echo "Deploying to $DEPLOY_PATH"
- rsync --delete -va ci/ui_test_records/* "$DEPLOY_PATH"
tags:
- deploy

View File

@ -0,0 +1,21 @@
import hashlib
import shutil
from pathlib import Path
def _hash_files(path):
files = path.iterdir()
hasher = hashlib.sha256()
for file in sorted(files):
hasher.update(file.read_bytes())
return hasher.digest().hex()
fixture_root = Path().cwd() / "../tests/ui_tests/fixtures/"
for recorded_dir in fixture_root.glob("*/recorded"):
expected_hash = (recorded_dir.parent / "hash.txt").read_text()
actual_hash = _hash_files(recorded_dir)
assert expected_hash == actual_hash
shutil.make_archive("ui_test_records/" + actual_hash, "zip", recorded_dir)

View File

@ -38,6 +38,29 @@ 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}
- cd ../ci
- pipenv run python prepare_ui_artifacts.py
artifacts:
name: core-unix-device-ui-test
paths:
- trezor.log
- ci/ui_test_records/
- tests/ui_tests/reports/
- tests/junit.xml
when: always
expire_in: 1 week
reports:
junit: tests/junit.xml
core unix device test:
stage: test
<<: *only_changes_core

2
ci/ui_test_records/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

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,12 @@ 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 --ui=test -m "not skip_ui" $(TESTOPTS)
test_emu_ui_record: ## record and hash screens for ui integration tests
cd tests ; ./run_tests_device_emu.sh --ui=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,12 @@ if __debug__:
layout_change_chan = loop.chan()
current_content = None # type: Optional[List[str]]
def screenshot() -> bool:
if utils.SAVE_SCREEN or save_screen:
ui.display.save(save_screen_directory + "/refresh-")
return True
return False
def notify_layout_change(layout: ui.Layout) -> None:
global current_content
current_content = layout.read_content()
@ -104,12 +117,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,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

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:
display.bar(Display.WIDTH - 8, 0, 8, 8, 0xF800)
def refresh() -> None:
if not screenshot():
display.bar(Display.WIDTH - 8, 0, 8, 8, 0xF800)
display.refresh()
if utils.SAVE_SCREEN:
display.save("refresh")
loop.after_step_hook = debug_display_refresh
else:
refresh = 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,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):

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

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,11 +32,14 @@ def render_scrollbar(pages: int, page: int) -> None:
def render_swipe_icon() -> None:
PULSE_PERIOD = const(1200000)
if utils.DISABLE_ANIMATION:
c = ui.GREY
else:
PULSE_PERIOD = const(1200000)
t = ui.pulse(PULSE_PERIOD)
c = ui.blend(ui.GREY, ui.DARK_GREY, t)
icon = res.load(ui.ICON_SWIPE)
t = ui.pulse(PULSE_PERIOD)
c = ui.blend(ui.GREY, ui.DARK_GREY, t)
ui.display.icon(70, 205, icon, c, ui.BG)

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,6 @@ 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 max_size:30
DebugLinkLayout.lines max_count:10 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

@ -24,7 +24,9 @@ from trezorlib.device import apply_settings, wipe as wipe_device
from trezorlib.messages.PassphraseSourceType import HOST as PASSPHRASE_ON_HOST
from trezorlib.transport import enumerate_devices, get_transport
from . import ui_tests
from .device_handler import BackgroundDeviceHandler
from .ui_tests import report
def get_device():
@ -89,9 +91,18 @@ def client(request):
" pytest -m 'not sd_card' <test path>"
)
test_ui = request.config.getoption("ui")
if test_ui not in ("", "record", "test"):
raise ValueError("Invalid ui option.")
run_ui_tests = not request.node.get_closest_marker("skip_ui") and test_ui
client.open()
if run_ui_tests:
# we need to reseed before the wipe
client.debug.reseed(0)
wipe_device(client)
# fmt: off
setup_params = dict(
uninitialized=False,
mnemonic=" ".join(["all"] * 12),
@ -100,7 +111,6 @@ def client(request):
needs_backup=False,
no_backup=False,
)
# fmt: on
marker = request.node.get_closest_marker("setup_client")
if marker:
@ -127,11 +137,40 @@ def client(request):
# ClearSession locks the device. We only do that if the PIN is set.
client.clear_session()
client.open()
yield client
if run_ui_tests:
with ui_tests.screen_recording(client, request):
yield client
else:
yield client
client.close()
def pytest_sessionstart(session):
if session.config.getoption("ui") == "test":
report.clear_dir()
def pytest_sessionfinish(session, exitstatus):
if session.config.getoption("ui") == "test":
report.index()
def pytest_terminal_summary(terminalreporter, exitstatus, config):
terminalreporter.writer.line(
"\nUI tests summary: %s" % (report.REPORTS_PATH / "index.html")
)
def pytest_addoption(parser):
parser.addoption(
"--ui",
action="store",
default="",
help="Enable UI intergration tests: 'record' or 'test'",
)
def pytest_configure(config):
"""Called at testsuite setup time.
@ -144,6 +183,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

@ -25,9 +25,12 @@ 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

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):
@ -178,13 +180,16 @@ VECTORS = [
@pytest.mark.parametrize("backup_type, backup_flow", VECTORS)
@pytest.mark.setup_client(uninitialized=True)
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
@ -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,16 +15,19 @@
# 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.skip_ui
@pytest.mark.setup_client(uninitialized=True)
def test_reset_recovery(client):
mnemonic = reset(client)
@ -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

View File

@ -14,16 +14,24 @@
# 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.skip_ui
@pytest.mark.setup_client(uninitialized=True)
def test_reset_recovery(client):
mnemonics = reset(client)
@ -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)

4
tests/ui_tests/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.png
*.html
*.zip
reports/

112
tests/ui_tests/__init__.py Normal file
View File

@ -0,0 +1,112 @@
import hashlib
import re
import shutil
from contextlib import contextmanager
from pathlib import Path
import pytest
from . import report
UI_TESTS_DIR = Path(__file__).parent.resolve()
def get_test_name(node_id):
# 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.
new_name = node_id.replace("tests/device_tests/", "")
# remove ::TestClass:: if present because it is usually the same as the test file name
new_name = re.sub(r"::.*?::", "-", new_name)
new_name = new_name.replace("/", "-") # in case there is "/"
return new_name[:100]
def _check_fixture_directory(fixture_dir, screen_path):
# create the fixture dir if it does not exist
if not fixture_dir.exists():
fixture_dir.mkdir()
# delete old files
shutil.rmtree(screen_path, ignore_errors=True)
screen_path.mkdir()
def _process_recorded(screen_path):
# create hash
digest = _hash_files(screen_path)
(screen_path.parent / "hash.txt").write_text(digest)
_rename_records(screen_path)
def _rename_records(screen_path):
# rename screenshots
for index, record in enumerate(sorted(screen_path.iterdir())):
record.replace(screen_path / f"{index:08}.png")
def _hash_files(path):
files = path.iterdir()
hasher = hashlib.sha256()
for file in sorted(files):
hasher.update(file.read_bytes())
return hasher.digest().hex()
def _process_tested(fixture_test_path, test_name):
hash_file = fixture_test_path / "hash.txt"
if not hash_file.exists():
raise ValueError("File hash.txt not found.")
expected_hash = hash_file.read_text()
actual_path = fixture_test_path / "actual"
actual_hash = _hash_files(actual_path)
_rename_records(actual_path)
if actual_hash != expected_hash:
file_path = report.failed(
fixture_test_path, test_name, actual_hash, expected_hash
)
pytest.fail(
"Hash of {} differs.\nExpected: {}\nActual: {}\nDiff file: {}".format(
test_name, expected_hash, actual_hash, file_path
)
)
else:
report.passed(fixture_test_path, test_name, actual_hash)
@contextmanager
def screen_recording(client, request):
test_ui = request.config.getoption("ui")
test_name = get_test_name(request.node.nodeid)
fixture_test_path = UI_TESTS_DIR / "fixtures" / test_name
if test_ui == "record":
screen_path = fixture_test_path / "recorded"
elif test_ui == "test":
screen_path = fixture_test_path / "actual"
else:
raise ValueError("Invalid 'ui' option.")
# remove previous files
shutil.rmtree(screen_path, ignore_errors=True)
screen_path.mkdir()
try:
client.debug.start_recording(str(screen_path))
yield
finally:
client.debug.stop_recording()
if test_ui == "record":
_process_recorded(screen_path)
elif test_ui == "test":
_process_tested(fixture_test_path, test_name)
else:
raise ValueError("Invalid 'ui' option.")

View File

@ -0,0 +1,24 @@
import urllib.error
import urllib.request
import zipfile
RECORDS_WEBSITE = "https://firmware.corp.sldev.cz/ui_tests/"
def fetch_recorded(recorded_hash, recorded_path):
zip_src = RECORDS_WEBSITE + recorded_hash + ".zip"
zip_dest = recorded_path / "recorded.zip"
try:
urllib.request.urlretrieve(zip_src, zip_dest)
except urllib.error.HTTPError:
raise RuntimeError("No such recorded collection was found on '%s'." % zip_src)
except urllib.error.URLError:
raise RuntimeError(
"Server firmware.corp.sldev.cz could not be found. Are you on VPN?"
)
with zipfile.ZipFile(zip_dest, "r") as z:
z.extractall(recorded_path)
zip_dest.unlink()

View File

@ -0,0 +1 @@
634ddda671de872d438cce58246154704a579e71c1137e3be298d7a1bf19e4dd

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

View File

@ -0,0 +1 @@
fb38537b921f8064f7ea6e1a584e70a8be74968a3be6726b7d36cf57de0d7865

View File

@ -0,0 +1 @@
4373cf99062b8e39369e273009cdbfae715d73d241605752a10c1ab57f2c8e77

View File

@ -0,0 +1 @@
b75b3c0103916bf4a2ec1aedad05e7b75a2ff1961a4ee40a7773b7f7d4f463ed

View File

@ -0,0 +1 @@
b0cc08c03ba2089d538e1dca1d4f949031100195a2a8ef5eb8e84542da817f7a

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

View File

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

View File

@ -0,0 +1 @@
07e93c712f63190a9bdb01f30c10750afd264fd2f491d9f7b89c431b9550edc8

View File

@ -0,0 +1 @@
7b8bbe5ba7d7b07c95065608fb1cf9aeafcb3f9671835a6e5e5a6997ff4ff99b

View File

@ -0,0 +1 @@
813ad1b802dee1ace4dfa378edd840dbcea57c1a1b8eed67134def024c40a6e9

View File

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

View File

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

View File

@ -0,0 +1 @@
5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586

View File

@ -0,0 +1 @@
612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0

View File

@ -0,0 +1 @@
612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0

View File

@ -0,0 +1 @@
612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0

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

View File

@ -0,0 +1 @@
612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0

View File

@ -0,0 +1 @@
612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0

View File

@ -0,0 +1 @@
c8efc839222488aea6b0b1bc8cf595f348b1f9d77221b3017992b8c1733228cd

View File

@ -0,0 +1 @@
c8efc839222488aea6b0b1bc8cf595f348b1f9d77221b3017992b8c1733228cd

View File

@ -0,0 +1 @@
cd1a289b31604e366464951cda3b7ce90125ff3f23f98cd2f60caf96e03c37c2

View File

@ -0,0 +1 @@
f504163122424398b008ec86cbd219e543eea7889d52651e0e69f707b4a14649

View File

@ -0,0 +1 @@
f504163122424398b008ec86cbd219e543eea7889d52651e0e69f707b4a14649

View File

@ -0,0 +1 @@
f504163122424398b008ec86cbd219e543eea7889d52651e0e69f707b4a14649

View File

@ -0,0 +1 @@
0440233304d5589c5ef16a8d304297992220d6fb9413f1d2e3680b106db3fc0d

View File

@ -0,0 +1 @@
0440233304d5589c5ef16a8d304297992220d6fb9413f1d2e3680b106db3fc0d

View File

@ -0,0 +1 @@
b1aaeafb0dd82dea3c39cd4b27bc3ee70b1dff797460deb4294dd477a4b4aea2

View File

@ -0,0 +1 @@
625526b30bd45a9f05dd46ec459a908464649f808862445f4d845511bd90a944

View File

@ -0,0 +1 @@
68aa16f42a827b1e288ca109a59328440dc348298779c816efc293ed47753825

View File

@ -0,0 +1 @@
0a8089d97e7bb9e6292557ae803f1ab35f74d845653bb00389360dbbcdc1e74d

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