diff --git a/tests/conftest.py b/tests/conftest.py index c9c85fae83..80bd2c3221 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,12 +14,7 @@ # You should have received a copy of the License along with this library. # If not, see . -import hashlib import os -import re -import shutil -from contextlib import contextmanager -from pathlib import Path import pytest @@ -29,8 +24,8 @@ 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.html import create_diff_doc def get_device(): @@ -54,118 +49,6 @@ 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_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): - records = sorted(screen_path.iterdir()) - - # create hash - digest = _hash_files(records) - with open(screen_path / "../hash.txt", "w") as f: - f.write(digest) - _rename_records(screen_path) - - -def _rename_records(screen_path): - # rename screenshots - for index, record in enumerate(sorted(screen_path.iterdir())): - filename = screen_path / "{:08}.png".format(index) - record.replace(filename) - - -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 _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.") - - with open(hash_file, "r") as f: - expected_hash = f.read() - - actual_path = fixture_test_path / "actual" - _rename_records(actual_path) - - records = sorted(actual_path.iterdir()) - actual_hash = _hash_files(records) - - if actual_hash != expected_hash: - create_diff_doc(fixture_test_path, test_name, actual_hash, expected_hash) - pytest.fail( - "Hash of {} differs.\nExpected: {}\nActual: {}".format( - test_name, expected_hash, actual_hash - ) - ) - else: - if (fixture_test_path / "diff.html").exists(): - (fixture_test_path / "diff.html").unlink() - - -@contextmanager -def _screen_recording(client, request): - if not request.node.get_closest_marker("skip_ui"): - test_screen = request.config.getoption("test_screen") - else: - test_screen = "" - - if not test_screen: - yield - return - - fixture_root = Path(__file__) / "../ui_tests/fixtures" - test_name = _get_test_dirname(request.node) - fixture_test_path = fixture_root.resolve() / test_name - - if test_screen == "record": - screen_path = fixture_test_path / "recorded" - elif test_screen == "test": - screen_path = fixture_test_path / "actual" - else: - raise ValueError("Invalid test_screen option.") - - _check_fixture_directory(fixture_test_path, screen_path) - - try: - client.debug.start_recording(str(screen_path)) - yield - finally: - client.debug.stop_recording() - if test_screen == "record": - _process_recorded(screen_path) - elif test_screen == "test": - _process_tested(fixture_test_path, test_name) - else: - raise ValueError("Invalid test_screen option.") - - @pytest.fixture(scope="function") def client(request): """Client fixture. @@ -209,7 +92,6 @@ def client(request): wipe_device(client) - # fmt: off setup_params = dict( uninitialized=False, mnemonic=" ".join(["all"] * 12), @@ -219,7 +101,6 @@ def client(request): no_backup=False, random_seed=None, ) - # fmt: on marker = request.node.get_closest_marker("setup_client") if marker: @@ -251,7 +132,7 @@ def client(request): if setup_params["random_seed"] is not None: client.debug.reseed(setup_params["random_seed"]) - with _screen_recording(client, request): + with ui_tests.screen_recording(client, request): yield client client.close() diff --git a/tests/ui_tests/__init__.py b/tests/ui_tests/__init__.py new file mode 100644 index 0000000000..28f428dc8b --- /dev/null +++ b/tests/ui_tests/__init__.py @@ -0,0 +1,121 @@ +import hashlib +import re +import shutil +from contextlib import contextmanager +from pathlib import Path + +import pytest + +from .html import create_diff_doc + + +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_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): + records = sorted(screen_path.iterdir()) + + # create hash + digest = _hash_files(records) + with open(screen_path / "../hash.txt", "w") as f: + f.write(digest) + _rename_records(screen_path) + + +def _rename_records(screen_path): + # rename screenshots + for index, record in enumerate(sorted(screen_path.iterdir())): + filename = screen_path / "{:08}.png".format(index) + record.replace(filename) + + +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 _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.") + + with open(hash_file, "r") as f: + expected_hash = f.read() + + actual_path = fixture_test_path / "actual" + _rename_records(actual_path) + + records = sorted(actual_path.iterdir()) + actual_hash = _hash_files(records) + + if actual_hash != expected_hash: + create_diff_doc(fixture_test_path, test_name, actual_hash, expected_hash) + pytest.fail( + "Hash of {} differs.\nExpected: {}\nActual: {}".format( + test_name, expected_hash, actual_hash + ) + ) + else: + if (fixture_test_path / "diff.html").exists(): + (fixture_test_path / "diff.html").unlink() + + +@contextmanager +def screen_recording(client, request): + if not request.node.get_closest_marker("skip_ui"): + test_screen = request.config.getoption("test_screen") + else: + test_screen = "" + + if not test_screen: + yield + return + + fixture_root = Path(__file__) / "../fixtures" + test_name = _get_test_dirname(request.node) + fixture_test_path = fixture_root.resolve() / test_name + + if test_screen == "record": + screen_path = fixture_test_path / "recorded" + elif test_screen == "test": + screen_path = fixture_test_path / "actual" + else: + raise ValueError("Invalid test_screen option.") + + _check_fixture_directory(fixture_test_path, screen_path) + + try: + client.debug.start_recording(str(screen_path)) + yield + finally: + client.debug.stop_recording() + if test_screen == "record": + _process_recorded(screen_path) + elif test_screen == "test": + _process_tested(fixture_test_path, test_name) + else: + raise ValueError("Invalid test_screen option.")