mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-02-02 10:51:06 +00:00
chore(tests): tests recording also generates "results web page"
This commit is contained in:
parent
0388268b57
commit
a69e43d1a6
@ -165,7 +165,7 @@ def client(
|
||||
|
||||
def pytest_sessionstart(session: pytest.Session) -> None:
|
||||
ui_tests.read_fixtures()
|
||||
if session.config.getoption("ui") == "test":
|
||||
if session.config.getoption("ui"):
|
||||
testreport.clear_dir()
|
||||
|
||||
|
||||
@ -181,13 +181,19 @@ def pytest_sessionfinish(session: pytest.Session, exitstatus: pytest.ExitCode) -
|
||||
return
|
||||
|
||||
missing = session.config.getoption("ui_check_missing")
|
||||
if session.config.getoption("ui") == "test":
|
||||
test_ui = session.config.getoption("ui")
|
||||
|
||||
if test_ui == "test":
|
||||
if missing and ui_tests.list_missing():
|
||||
session.exitstatus = pytest.ExitCode.TESTS_FAILED
|
||||
ui_tests.write_fixtures_suggestion(missing)
|
||||
testreport.index()
|
||||
if session.config.getoption("ui") == "record":
|
||||
ui_tests.write_fixtures(missing)
|
||||
if test_ui == "record":
|
||||
if exitstatus == pytest.ExitCode.OK:
|
||||
ui_tests.write_fixtures(missing)
|
||||
else:
|
||||
ui_tests.write_fixtures_suggestion(missing, only_passed_tests=True)
|
||||
testreport.index()
|
||||
|
||||
|
||||
def pytest_terminal_summary(
|
||||
@ -216,6 +222,13 @@ def pytest_terminal_summary(
|
||||
print("See", ui_tests.SUGGESTION_FILE)
|
||||
println("")
|
||||
|
||||
if ui_option == "record" and exitstatus != pytest.ExitCode.OK:
|
||||
println(
|
||||
f"\n-------- WARNING! Recording to {ui_tests.HASH_FILE.name} was disabled due to failed tests. --------"
|
||||
)
|
||||
print("See", ui_tests.SUGGESTION_FILE, "for suggestions for ONLY PASSED tests.")
|
||||
println("")
|
||||
|
||||
if _should_write_ui_report(exitstatus):
|
||||
println("-------- UI tests summary: --------")
|
||||
println("Run ./tests/show_results.py to open test summary")
|
||||
@ -278,7 +291,7 @@ def pytest_runtest_teardown(item: pytest.Item) -> None:
|
||||
|
||||
Dumps the current UI test report HTML.
|
||||
"""
|
||||
if item.session.config.getoption("ui") == "test":
|
||||
if item.session.config.getoption("ui"):
|
||||
testreport.index()
|
||||
|
||||
|
||||
@ -298,12 +311,13 @@ def device_handler(client: Client, request: pytest.FixtureRequest) -> None:
|
||||
device_handler = BackgroundDeviceHandler(client)
|
||||
yield device_handler
|
||||
|
||||
# if test did not finish, e.g. interrupted by Ctrl+C, the pytest_runtest_makereport
|
||||
# did not create the attribute we need
|
||||
if not hasattr(request.node, "rep_call"):
|
||||
# get call test result
|
||||
test_res = ui_tests.get_last_call_test_result(request)
|
||||
|
||||
if test_res is None:
|
||||
return
|
||||
|
||||
# if test finished, make sure all background tasks are done
|
||||
finalized_ok = device_handler.check_finalize()
|
||||
if request.node.rep_call.passed and not finalized_ok: # type: ignore [rep_call must exist]
|
||||
if test_res and not finalized_ok: # type: ignore [rep_call must exist]
|
||||
raise RuntimeError("Test did not check result of background task")
|
||||
|
@ -5,7 +5,7 @@ import re
|
||||
import shutil
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Dict, Generator, Set
|
||||
from typing import Dict, Generator, Optional, Set
|
||||
|
||||
import pytest
|
||||
from _pytest.outcomes import Failed
|
||||
@ -22,6 +22,7 @@ SUGGESTION_FILE = UI_TESTS_DIR / "fixtures.suggestion.json"
|
||||
FILE_HASHES: Dict[str, str] = {}
|
||||
ACTUAL_HASHES: Dict[str, str] = {}
|
||||
PROCESSED: Set[str] = set()
|
||||
FAILED_TESTS: Set[str] = set()
|
||||
|
||||
# T1/TT, to be set in screen_recording(), as we do not know it beforehand
|
||||
# TODO: it is not the cleanest, we could create a class out of this file
|
||||
@ -44,9 +45,11 @@ def get_test_name(node_id: str) -> str:
|
||||
|
||||
def _process_recorded(screen_path: Path, test_name: str) -> None:
|
||||
# calculate hash
|
||||
FILE_HASHES[test_name] = _hash_files(screen_path)
|
||||
actual_hash = _hash_files(screen_path)
|
||||
FILE_HASHES[test_name] = actual_hash
|
||||
ACTUAL_HASHES[test_name] = actual_hash
|
||||
_rename_records(screen_path)
|
||||
PROCESSED.add(test_name)
|
||||
testreport.recorded(screen_path, test_name, actual_hash)
|
||||
|
||||
|
||||
def _rename_records(screen_path: Path) -> None:
|
||||
@ -74,8 +77,6 @@ def _get_bytes_from_png(png_file: str) -> bytes:
|
||||
|
||||
|
||||
def _process_tested(fixture_test_path: Path, test_name: str) -> None:
|
||||
PROCESSED.add(test_name)
|
||||
|
||||
actual_path = fixture_test_path / "actual"
|
||||
actual_hash = _hash_files(actual_path)
|
||||
ACTUAL_HASHES[test_name] = actual_hash
|
||||
@ -102,6 +103,15 @@ def _process_tested(fixture_test_path: Path, test_name: str) -> None:
|
||||
testreport.passed(fixture_test_path, test_name, actual_hash)
|
||||
|
||||
|
||||
def get_last_call_test_result(request: pytest.FixtureRequest) -> Optional[bool]:
|
||||
# if test did not finish, e.g. interrupted by Ctrl+C, the pytest_runtest_makereport
|
||||
# did not create the attribute we need
|
||||
if not hasattr(request.node, "rep_call"):
|
||||
return None
|
||||
|
||||
return request.node.rep_call.passed
|
||||
|
||||
|
||||
@contextmanager
|
||||
def screen_recording(
|
||||
client: Client, request: pytest.FixtureRequest
|
||||
@ -141,10 +151,15 @@ def screen_recording(
|
||||
client.init_device()
|
||||
client.debug.stop_recording()
|
||||
|
||||
if test_ui == "record":
|
||||
_process_recorded(screen_path, test_name)
|
||||
else:
|
||||
_process_tested(screens_test_path, test_name)
|
||||
if test_ui:
|
||||
PROCESSED.add(test_name)
|
||||
if get_last_call_test_result(request) is False:
|
||||
FAILED_TESTS.add(test_name)
|
||||
|
||||
if test_ui == "record":
|
||||
_process_recorded(screen_path, test_name)
|
||||
else:
|
||||
_process_tested(screens_test_path, test_name)
|
||||
|
||||
|
||||
def list_missing() -> Set[str]:
|
||||
@ -166,17 +181,28 @@ def write_fixtures(remove_missing: bool) -> None:
|
||||
HASH_FILE.write_text(_get_fixtures_content(FILE_HASHES, remove_missing))
|
||||
|
||||
|
||||
def write_fixtures_suggestion(remove_missing: bool) -> None:
|
||||
SUGGESTION_FILE.write_text(_get_fixtures_content(ACTUAL_HASHES, remove_missing))
|
||||
def write_fixtures_suggestion(
|
||||
remove_missing: bool, only_passed_tests: bool = False
|
||||
) -> None:
|
||||
SUGGESTION_FILE.write_text(
|
||||
_get_fixtures_content(ACTUAL_HASHES, remove_missing, only_passed_tests)
|
||||
)
|
||||
|
||||
|
||||
def _get_fixtures_content(fixtures: Dict[str, str], remove_missing: bool) -> str:
|
||||
def _get_fixtures_content(
|
||||
fixtures: Dict[str, str], remove_missing: bool, only_passed_tests: bool = False
|
||||
) -> str:
|
||||
if remove_missing:
|
||||
# Not removing the ones for different model
|
||||
nonrelevant_cases = {
|
||||
f: h for f, h in FILE_HASHES.items() if not f.startswith(f"{MODEL}_")
|
||||
}
|
||||
processed_fixtures = {i: fixtures[i] for i in PROCESSED}
|
||||
|
||||
filtered_processed_tests = PROCESSED
|
||||
if only_passed_tests:
|
||||
filtered_processed_tests = PROCESSED - FAILED_TESTS
|
||||
|
||||
processed_fixtures = {i: fixtures[i] for i in filtered_processed_tests}
|
||||
fixtures = {**nonrelevant_cases, **processed_fixtures}
|
||||
else:
|
||||
fixtures = fixtures
|
||||
|
@ -2,7 +2,7 @@ import base64
|
||||
import filecmp
|
||||
from itertools import zip_longest
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from dominate.tags import a, i, img, table, td, th, tr
|
||||
|
||||
@ -30,7 +30,7 @@ def write(fixture_test_path: Path, doc, filename: str) -> Path:
|
||||
return fixture_test_path / filename
|
||||
|
||||
|
||||
def image(src: Path) -> None:
|
||||
def image(src: Path, image_width: Optional[int] = None) -> None:
|
||||
with td():
|
||||
if src:
|
||||
# open image file
|
||||
@ -40,17 +40,26 @@ def image(src: Path) -> None:
|
||||
# convert output to str
|
||||
image = image.decode()
|
||||
# img(src=src.relative_to(fixture_test_path))
|
||||
img(src="data:image/png;base64, " + image)
|
||||
img(
|
||||
src="data:image/png;base64, " + image,
|
||||
style=f"width: {image_width}px; image-rendering: pixelated;"
|
||||
if image_width
|
||||
else "",
|
||||
)
|
||||
else:
|
||||
i("missing")
|
||||
|
||||
|
||||
def diff_table(left_screens: List[Path], right_screens: List[Path]) -> None:
|
||||
def diff_table(
|
||||
left_screens: List[Path],
|
||||
right_screens: List[Path],
|
||||
image_width: Optional[int] = None,
|
||||
) -> None:
|
||||
for left, right in zip_longest(left_screens, right_screens):
|
||||
if left and right and filecmp.cmp(right, left):
|
||||
background = "white"
|
||||
else:
|
||||
background = "red"
|
||||
with tr(bgcolor=background):
|
||||
image(left)
|
||||
image(right)
|
||||
image(left, image_width)
|
||||
image(right, image_width)
|
||||
|
@ -16,6 +16,11 @@ REPORTS_PATH = HERE / "reports" / "test"
|
||||
|
||||
STYLE = (HERE / "testreport.css").read_text()
|
||||
SCRIPT = (HERE / "testreport.js").read_text()
|
||||
SCREENSHOTS_WIDTH_PX_TO_DISPLAY = {
|
||||
"T1": 128 * 2, # original is 128px
|
||||
"TT": 240, # original is 240px
|
||||
"TR": 128 * 2, # original is 128px
|
||||
}
|
||||
|
||||
ACTUAL_HASHES: Dict[str, str] = {}
|
||||
|
||||
@ -147,7 +152,11 @@ def failed(
|
||||
th("Expected")
|
||||
th("Actual")
|
||||
|
||||
html.diff_table(recorded_screens, actual_screens)
|
||||
html.diff_table(
|
||||
recorded_screens,
|
||||
actual_screens,
|
||||
SCREENSHOTS_WIDTH_PX_TO_DISPLAY[test_name[:2]],
|
||||
)
|
||||
|
||||
return html.write(REPORTS_PATH / "failed", doc, test_name + ".html")
|
||||
|
||||
@ -155,9 +164,12 @@ def failed(
|
||||
def passed(fixture_test_path: Path, test_name: str, actual_hash: str) -> Path:
|
||||
copy_tree(str(fixture_test_path / "actual"), str(fixture_test_path / "recorded"))
|
||||
|
||||
return recorded(fixture_test_path / "actual", test_name, actual_hash)
|
||||
|
||||
|
||||
def recorded(fixture_test_path: Path, test_name: str, actual_hash: str) -> Path:
|
||||
doc = document(title=test_name)
|
||||
actual_path = fixture_test_path / "actual"
|
||||
actual_screens = sorted(actual_path.iterdir())
|
||||
actual_screens = sorted(fixture_test_path.iterdir())
|
||||
|
||||
with doc:
|
||||
_header(test_name, actual_hash, actual_hash)
|
||||
@ -168,6 +180,6 @@ def passed(fixture_test_path: Path, test_name: str, actual_hash: str) -> Path:
|
||||
|
||||
for screen in actual_screens:
|
||||
with tr():
|
||||
html.image(screen)
|
||||
html.image(screen, SCREENSHOTS_WIDTH_PX_TO_DISPLAY[test_name[:2]])
|
||||
|
||||
return html.write(REPORTS_PATH / "passed", doc, test_name + ".html")
|
||||
|
Loading…
Reference in New Issue
Block a user