Snapshot testing progress

This commit is contained in:
Darren Burns
2022-09-16 16:28:48 +01:00
parent e8acba5238
commit d619dae510
10 changed files with 1294 additions and 56 deletions

View File

@@ -4,7 +4,7 @@ from textual.widgets import Static
class GridLayoutExample(App):
def compose(self) -> ComposeResult:
yield Static("One", classes="box")
yield Static("Oneeee", classes="box")
yield Static("Two", classes="box")
yield Static("Three", classes="box")
yield Static("Four", classes="box")

104
poetry.lock generated
View File

@@ -50,14 +50,6 @@ category = "main"
optional = false
python-versions = ">=3.5"
[[package]]
name = "atomicwrites"
version = "1.4.1"
description = "Atomic file writes."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "attrs"
version = "22.1.0"
@@ -67,10 +59,10 @@ optional = false
python-versions = ">=3.5"
[package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"]
dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
[[package]]
name = "black"
@@ -142,6 +134,14 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "colored"
version = "1.4.3"
description = "Simple library for color and formatting to terminal"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "commonmark"
version = "0.9.1"
@@ -204,7 +204,7 @@ python-versions = "*"
python-dateutil = ">=2.8.1"
[package.extras]
dev = ["twine", "markdown", "flake8", "wheel"]
dev = ["flake8", "markdown", "twine", "wheel"]
[[package]]
name = "griffe"
@@ -252,9 +252,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
zipp = ">=0.5"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"]
perf = ["ipython"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
[[package]]
name = "iniconfig"
@@ -488,8 +488,8 @@ optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
[[package]]
name = "pluggy"
@@ -562,18 +562,17 @@ optional = false
python-versions = ">=3.6.8"
[package.extras]
diagrams = ["railroad-diagrams", "jinja2"]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "pytest"
version = "6.2.5"
version = "7.1.3"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
[package.dependencies]
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
@@ -581,10 +580,10 @@ iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
toml = "*"
tomli = ">=1.0.0"
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
[[package]]
name = "pytest-aiohttp"
@@ -615,7 +614,7 @@ pytest = ">=6.1.0"
typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""}
[package.extras]
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
[[package]]
name = "pytest-cov"
@@ -631,7 +630,7 @@ pytest = ">=4.6"
toml = "*"
[package.extras]
testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
[[package]]
name = "python-dateutil"
@@ -687,6 +686,18 @@ category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "syrupy"
version = "3.0.0"
description = "PyTest Snapshot Test Utility"
category = "dev"
optional = false
python-versions = ">=3.7,<4"
[package.dependencies]
colored = ">=1.3.92,<2.0.0"
pytest = ">=5.1.0,<8.0.0"
[[package]]
name = "time-machine"
version = "2.8.1"
@@ -781,8 +792,8 @@ optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"]
testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
[extras]
dev = ["aiohttp", "click", "msgpack"]
@@ -790,7 +801,7 @@ dev = ["aiohttp", "click", "msgpack"]
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "61db56567f708cd9ca1c27f0e4a4b4aa3dd808fc8411f80967a90995d7fdd8c8"
content-hash = "b28791133eec3bbb2debd610593c608c3ec62524be47e803fcbd31e55ddbffee"
[metadata.files]
aiohttp = [
@@ -879,7 +890,6 @@ asynctest = [
{file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"},
{file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"},
]
atomicwrites = []
attrs = []
black = [
{file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"},
@@ -914,7 +924,10 @@ cfgv = [
{file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
]
charset-normalizer = []
charset-normalizer = [
{file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
{file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
]
click = [
{file = "click-8.1.2-py3-none-any.whl", hash = "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e"},
{file = "click-8.1.2.tar.gz", hash = "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"},
@@ -923,19 +936,28 @@ colorama = [
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
colored = [
{file = "colored-1.4.3.tar.gz", hash = "sha256:b7b48b9f40e8a65bbb54813d5d79dd008dc8b8c5638d5bbfd30fc5a82e6def7a"},
]
commonmark = [
{file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
{file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
]
coverage = []
distlib = []
filelock = []
filelock = [
{file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"},
{file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"},
]
frozenlist = []
ghp-import = [
{file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
{file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
]
griffe = []
griffe = [
{file = "griffe-0.22.0-py3-none-any.whl", hash = "sha256:65c94cba634d6ad397c495b04ed5fd3f06d9b16c4f9f78bd63be9ea34d6b7113"},
{file = "griffe-0.22.0.tar.gz", hash = "sha256:a3c25a2b7bf729ecee7cd455b4eff548f01c620b8f58a8097a800caad221f12e"},
]
identify = []
idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
@@ -1008,7 +1030,10 @@ mkdocs-autorefs = [
{file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"},
{file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"},
]
mkdocs-material = []
mkdocs-material = [
{file = "mkdocs-material-8.4.2.tar.gz", hash = "sha256:704c64c3fff126a3923c2961d95f26b19be621342a6a4e49ed039f0bb7a5c540"},
{file = "mkdocs_material-8.4.2-py2.py3-none-any.whl", hash = "sha256:166287bb0e4197804906bf0389a852d5ced43182c30127ac8b48a4e497ecd7e5"},
]
mkdocs-material-extensions = [
{file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"},
{file = "mkdocs_material_extensions-1.0.3-py3-none-any.whl", hash = "sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44"},
@@ -1201,8 +1226,8 @@ pyparsing = [
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
pytest = [
{file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
{file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
{file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"},
{file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"},
]
pytest-aiohttp = [
{file = "pytest-aiohttp-1.0.4.tar.gz", hash = "sha256:39ff3a0d15484c01d1436cbedad575c6eafbf0f57cdf76fb94994c97b5b8c5a4"},
@@ -1261,6 +1286,10 @@ six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
syrupy = [
{file = "syrupy-3.0.0-py3-none-any.whl", hash = "sha256:e5e4d376766f46a52a3585365fb90aba0295611043a41a760302d2cd7aa74488"},
{file = "syrupy-3.0.0.tar.gz", hash = "sha256:e1e2b6504c85c871fed89b82ce1d02442cf7b622b4b88f95c1332e71e012cd86"},
]
time-machine = []
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
@@ -1296,7 +1325,10 @@ typed-ast = [
{file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
{file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
]
typing-extensions = []
typing-extensions = [
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
]
virtualenv = []
watchdog = [
{file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"},

View File

@@ -37,7 +37,7 @@ nanoid = "^2.0.0"
dev = ["aiohttp", "click", "msgpack"]
[tool.poetry.dev-dependencies]
pytest = "^6.2.3"
pytest = "^7.1.3"
black = "^22.3.0"
mypy = "^0.950"
pytest-cov = "^2.12.1"
@@ -48,6 +48,7 @@ pre-commit = "^2.13.0"
pytest-aiohttp = "^1.0.4"
time-machine = "^2.6.0"
Jinja2 = "<3.1.0"
syrupy = "^3.0.0"
[tool.black]
includes = "src"
@@ -55,8 +56,10 @@ includes = "src"
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
addopts = "--strict-markers"
markers = [
"integration_test: marks tests as slow integration tests (deselect with '-m \"not integration_test\"')",
"snapshot: marks test as a snapshot test",
]
[build-system]

View File

@@ -3,15 +3,16 @@ from __future__ import annotations
import runpy
import os
import shlex
from typing import cast, TYPE_CHECKING
from typing import cast, TYPE_CHECKING, Iterable
if TYPE_CHECKING:
from textual.app import App
# This module defines our "Custom Fences", powered by SuperFences
# @link https://facelessuser.github.io/pymdown-extensions/extensions/superfences/#custom-fences
def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str:
"""A superfences formatter to insert a SVG screenshot."""
"""A superfences formatter to insert an SVG screenshot."""
try:
cmd: list[str] = shlex.split(attrs["path"])
@@ -21,25 +22,13 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str
press = [*_press.split(",")] if _press else ["_"]
title = attrs.get("title")
os.environ["COLUMNS"] = attrs.get("columns", "80")
os.environ["LINES"] = attrs.get("lines", "24")
print(f"screenshotting {path!r}")
cwd = os.getcwd()
try:
app_vars = runpy.run_path(path)
if "sys" in app_vars:
app_vars["sys"].argv = cmd
app: App = cast("App", app_vars["app"])
app.run(
quit_after=5,
press=press or ["ctrl+c"],
headless=True,
screenshot=True,
screenshot_title=title,
)
svg = app._screenshot
rows = int(attrs.get("lines", 24))
columns = int(attrs.get("columns", 80))
svg = take_svg_screenshot(path, press, title, terminal_size=(rows, columns))
finally:
os.chdir(cwd)
@@ -52,8 +41,40 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str
traceback.print_exception(error)
def take_svg_screenshot(
app_path: str,
press: Iterable[str] = ("_",),
title: str | None = None,
terminal_size: tuple[int, int] = (24, 80),
) -> str:
rows, columns = terminal_size
os.environ["COLUMNS"] = str(columns)
os.environ["LINES"] = str(rows)
app_vars = runpy.run_path(app_path)
if "sys" in app_vars:
cmd: list[str] = shlex.split(app_path)
app_vars["sys"].argv = cmd
app: App = cast("App", app_vars["app"])
if title is None:
title = app.title
app.run(
quit_after=5,
press=press or ["ctrl+c"],
headless=True,
screenshot=True,
screenshot_title=title,
)
svg = app._screenshot
return svg
def rich(source, language, css_class, options, md, attrs, **kwargs) -> str:
"""A superfences formatter to insert a SVG screenshot."""
"""A superfences formatter to insert an SVG screenshot."""
from rich.console import Console
import io

View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,178 @@
import difflib
from dataclasses import dataclass
from datetime import datetime
from functools import partial
from operator import attrgetter
from os import PathLike
from pathlib import Path
from typing import Union, List, Optional, Any, Callable
import pytest
from _pytest.fixtures import FixtureDef, SubRequest, FixtureRequest
from jinja2 import Template
from rich.console import Console
from rich.panel import Panel
from textual._doc import take_svg_screenshot
snapshot_svg_key = pytest.StashKey[str]()
actual_svg_key = pytest.StashKey[str]()
snapshot_pass = pytest.StashKey[bool]()
@pytest.fixture
def snap_compare(snapshot, request: FixtureRequest) -> Callable[[str], bool]:
def compare(app_path: str, snapshot) -> bool:
node = request.node
actual_screenshot = take_svg_screenshot(app_path)
result = snapshot == actual_screenshot
if result is False:
# The split and join below is a mad hack, sorry...
node.stash[snapshot_svg_key] = "\n".join(str(snapshot).splitlines()[1:-1])
node.stash[actual_svg_key] = actual_screenshot
else:
node.stash[snapshot_pass] = True
return result
return partial(compare, snapshot=snapshot)
@dataclass
class SvgSnapshotDiff:
snapshot: Optional[str]
actual: Optional[str]
test_name: str
file_similarity: float
path: PathLike
line_number: int
#
# def pytest_runtestloop(session: "Session") -> Optional[object]:
@pytest.hookimpl(hookwrapper=True)
def pytest_pyfunc_call(pyfuncitem: "pytest.Function") -> Optional[object]:
"""Call underlying test function.
Stops at first non-None result, see :ref:`firstresult`.
"""
# Before
yield
# After
def pytest_runtest_teardown(item: pytest.Item, nextitem: Optional[pytest.Item]) -> None:
"""Called to perform the teardown phase for a test item.
The default implementation runs the finalizers and calls ``teardown()``
on ``item`` and all of its parents (which need to be torn down). This
includes running the teardown phase of fixtures required by the item (if
they go out of scope).
:param nextitem:
The scheduled-to-be-next test item (None if no further test item is
scheduled). This argument is used to perform exact teardowns, i.e.
calling just enough finalizers so that nextitem only needs to call
setup functions.
"""
@pytest.hookimpl(hookwrapper=True)
def pytest_fixture_setup(
fixturedef: FixtureDef[Any], request: SubRequest
) -> Optional[object]:
"""Perform fixture setup execution.
:returns: The return value of the call to the fixture function.
Stops at first non-None result, see :ref:`firstresult`.
.. note::
If the fixture function returns None, other implementations of
this hook function will continue to be called, according to the
behavior of the :ref:`firstresult` option.
"""
value = yield
value = value.get_result()
value = repr(value)
def pytest_sessionfinish(
session: "pytest.Session",
exitstatus: Union[int, "pytest.ExitCode"],
) -> None:
"""Called after whole test run finished, right before returning the exit status to the system.
:param pytest.Session session: The pytest session object.
:param int exitstatus: The status which pytest will return to the system.
"""
diffs: List[SvgSnapshotDiff] = []
num_snapshots_passing = 0
for item in session.items:
num_snapshots_passing += int(item.stash.get(snapshot_pass, False))
snapshot_svg = item.stash.get(snapshot_svg_key, None)
actual_svg = item.stash.get(actual_svg_key, None)
if snapshot_svg and actual_svg:
path, line_index, name = item.reportinfo()
diffs.append(
SvgSnapshotDiff(
snapshot=str(snapshot_svg),
actual=str(actual_svg),
file_similarity=100 * difflib.SequenceMatcher(a=str(snapshot_svg), b=str(actual_svg)).ratio(),
test_name=name,
path=path,
line_number=line_index + 1,
)
)
diff_sort_key = attrgetter("file_similarity")
diffs = sorted(diffs, key=diff_sort_key)
conftest_path = Path(__file__)
snapshot_template_path = conftest_path.parent / "snapshot_report_template.jinja2"
snapshot_report_path = conftest_path.parent / "snapshot_report.html"
template = Template(snapshot_template_path.read_text())
num_fails = len(diffs)
num_snapshot_tests = len(diffs) + num_snapshots_passing
rendered_report = template.render(
diffs=diffs,
passes=num_snapshots_passing,
fails=num_fails,
pass_percentage=100*(num_snapshots_passing/num_snapshot_tests),
fail_percentage=100*(num_fails/num_snapshot_tests),
num_snapshot_tests=num_snapshot_tests,
now=datetime.utcnow()
)
with open(snapshot_report_path, "wt") as snapshot_file:
snapshot_file.write(rendered_report)
session.config._textual_snapshots = diffs
session.config._textual_snapshot_html_report = snapshot_report_path
def pytest_terminal_summary(
terminalreporter: "pytest.TerminalReporter",
exitstatus: pytest.ExitCode,
config: pytest.Config,
) -> None:
"""Add a section to terminal summary reporting.
:param _pytest.terminal.TerminalReporter terminalreporter: The internal terminal reporter object.
:param int exitstatus: The exit status that will be reported back to the OS.
:param pytest.Config config: The pytest config object.
.. versionadded:: 4.2
The ``config`` parameter.
"""
diffs = config._textual_snapshots
snapshot_report_location = config._textual_snapshot_html_report
console = Console()
summary_panel = Panel(
f"[b]Report available for {len(diffs)} snapshot test failures.[/]\n\nView the report at:\n\n[blue]{snapshot_report_location}[/]",
title="[b red]Textual Snapshot Test Summary", padding=1)
console.print(summary_panel)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,98 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Textual Snapshot Test Report</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous">
</head>
<body>
<div class="container-fluid">
<div class="row mb-4" style="background-color:#F4F8F7;">
<div class="col-8 p-4">
<h4>
<strong>Textual</strong> Snapshot Tests
</h4>
<span class="text-muted">Showing diffs for {{ fails }} mismatched snapshot(s)</span>
</div>
<div class="col p-4">
<div class="w-100 d-flex justify-content-end mb-1 mt-2">
<span class="text-danger">
<strong>{{ diffs | length }}</strong> snapshots changed
</span>
<span class="text-muted mx-2">
·
</span>
<span class="text-success">
<strong>{{ passes }}</strong> snapshots matched
</span>
</div>
<div class="progress">
<div class="progress-bar bg-danger" role="progressbar" aria-label="Segment one"
style="width: {{ fail_percentage }}%"
aria-valuenow="{{ fails }}" aria-valuemin="0" aria-valuemax="{{ num_snapshot_tests }}"></div>
<div class="progress-bar bg-success" role="progressbar" aria-label="Segment two"
style="width: {{ pass_percentage }}%"
aria-valuenow="{{ num_snapshot_tests }}" aria-valuemin="0"
aria-valuemax="{{ num_snapshot_tests }}"></div>
</div>
</div>
</div>
{% for diff in diffs %}
<div class="row mb-4">
<div class="col">
<div class="card">
<div class="card-header d-flex justify-content-between">
<div>
<strong class="font-monospace">
{{ diff.test_name }}
</strong>
<span class="text-muted">({{ "%.2f"|format(diff.file_similarity) }}% similar)</span>
</div>
<span class="text-muted">{{ diff.path }}:{{ diff.line_number }}</span>
</div>
<div class="card-body">
<div class="row">
<div class="col">
{{ diff.actual }}
<div class="w-100 d-flex justify-content-center mt-1">
<span class="small">Output from test</span>
</div>
</div>
<div class="col">
{{ diff.snapshot }}
<div class="w-100 d-flex justify-content-center mt-1">
<span class="small">Historical snapshot</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
<div class="row" style="background-color:#F4F8F7;">
<div class="col">
<div class="card bg-light">
<div class="card-body">
<p class="card-text">If you're happy with the change, run pytest with the <span class="font-monospace text-primary">--snapshot-update</span> flag to update the snapshot.</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="w-100 d-flex p-4 justify-content-center">
<p class="text-muted">Report generated at UTC {{ now }}.</p>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,14 @@
def test_grid_layout_basic(snap_compare):
assert snap_compare("docs/examples/guide/layout/grid_layout1.py")
def test_grid_layout_basic_overflow(snap_compare):
assert snap_compare("docs/examples/guide/layout/grid_layout2.py")
def test_combining_layouts(snap_compare):
assert snap_compare("docs/examples/guide/layout/combining_layouts.py")
def test_layers(snap_compare):
assert snap_compare("docs/examples/guide/layout/layers.py")