Skip to content

Commit d87450e

Browse files
authored
Implement throw_exception fixture, up readme and docs #4 (#5)
* Implement no throw_exception fixture, up readme and docs #4 * Fix windows compatibly tests
1 parent c6814c3 commit d87450e

File tree

14 files changed

+216
-40
lines changed

14 files changed

+216
-40
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,17 @@ name: ci
33
on:
44
push:
55
pull_request:
6-
schedule:
7-
# Weekly
8-
- cron: "0 0 * * 0"
96

107
jobs:
118
test:
129
strategy:
1310
fail-fast: false
1411
matrix:
15-
os: [ubuntu-20.04]
16-
python-version: [ 3.7, 3.8, 3.9, "3.10"]
17-
toxenv: [""]
12+
os: [ ubuntu-20.04 ]
13+
python-version: [ 3.7, 3.8, 3.9, "3.10" ]
14+
toxenv: [ "" ]
1815
experimental: [ false ]
19-
chrome: ["stable"]
16+
chrome: [ "stable" ]
2017
include:
2118
- toxenv: qa
2219
python-version: 3.7
@@ -36,7 +33,7 @@ jobs:
3633
experimental: true
3734
python-version: 3.7
3835
os: windows-latest
39-
chrome: "959905"
36+
# chrome: "959905"
4037

4138
- experimental: false
4239
python-version: "3.9"

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,45 @@ def test_regression(image_regression):
4040
image_regression(image, threshold=0.5)
4141
```
4242

43+
Also use with assert
44+
45+
```python
46+
import pytest
47+
48+
from typing import Union
49+
from PIL import Image
50+
51+
@pytest.fixture(scope="session")
52+
def image_diff_throw_exception() -> bool:
53+
"""
54+
Set default throw exception. By default - True
55+
"""
56+
return False
57+
58+
def test_compare(image_diff):
59+
image: Image or str or bytes = Image.new()
60+
image2: Image or str or bytes = '/path/to/image.jpeg'
61+
assert image_diff(image, image2)
62+
assert image_diff(image, image2, threshold=0.5)
63+
# Also can check threshold in compare, ie
64+
assert image_diff(image, image2) < 0.5
65+
# For different checks in one test
66+
assert image_diff(image, image2, threshold=0.5, suffix="one")
67+
# Or without fixture image_diff_throw_exception
68+
assert image_diff(image, image2, threshold=0.5, throw_exception=False)
69+
70+
71+
def test_regression(image_regression):
72+
image: Union[Image, str, bytes] = Image.new()
73+
assert image_regression(image, threshold=0.5)
74+
# Also can check threshold in compare, ie
75+
assert image_regression(image) < 0.5
76+
# For different checks in one test
77+
assert image_regression(image, threshold=0.5, suffix="foo")
78+
# Or without fixture image_diff_throw_exception
79+
assert image_regression(image, threshold=0.5, throw_exception=False)
80+
```
81+
4382
First run creates reference images
4483

4584
## pytest-splinter

docs/source/code.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Config fixtures
1010
.. autofunction:: pytest_image_diff.plugin.image_diff_dir
1111
.. autofunction:: pytest_image_diff.plugin.image_diff_reference_dir
1212

13+
.. autofunction:: pytest_image_diff.plugin.image_diff_throw_exception
14+
1315

1416
Fixtures
1517
--------

pytest_image_diff/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
# -*- coding: utf-8 -*-
2+
import pytest
3+
4+
pytest.register_assert_rewrite("pytest_image_diff.helpers")
25

36
__version__ = "0.0.8"

pytest_image_diff/_types.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from pathlib import Path
2-
from typing import BinaryIO, Union, Tuple, Optional
2+
from typing import BinaryIO, Union, Tuple, Optional, TYPE_CHECKING
3+
from typing_extensions import Literal, Protocol
34

45
from PIL.Image import Image
5-
from typing_extensions import Literal, Protocol
6+
7+
if TYPE_CHECKING:
8+
from .plugin import DiffCompareResult
69

710
PathOrFileType = Union[str, bytes, Path, BinaryIO]
811
ImageFileType = Union[Image, PathOrFileType]
@@ -15,7 +18,8 @@ def __call__(
1518
image: ImageFileType,
1619
threshold: Optional[float] = None,
1720
suffix: Optional[str] = None,
18-
) -> bool:
21+
throw_exception: Optional[bool] = None,
22+
) -> "DiffCompareResult":
1923
pass
2024

2125

@@ -26,7 +30,8 @@ def __call__(
2630
image_2: ImageFileType,
2731
threshold: Optional[float] = None,
2832
suffix: Optional[str] = None,
29-
) -> bool:
33+
throw_exception: Optional[bool] = None,
34+
) -> "DiffCompareResult":
3035
pass
3136

3237

pytest_image_diff/helpers.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,13 @@ def temp_file(suffix: typing.Optional[str] = None) -> typing.Iterator[pathlib.Pa
3838
yield temp_image_path
3939
finally:
4040
try:
41+
tf.close()
4142
temp_image_path.unlink()
4243
except FileNotFoundError:
4344
pass
45+
except PermissionError:
46+
# Opps, windows issue
47+
pass
4448

4549

4650
class TestInfo:
@@ -96,3 +100,8 @@ def ensure_dirs(filepath: str) -> None:
96100
file_dir = os.path.dirname(filepath)
97101
if not os.path.exists(file_dir):
98102
os.makedirs(file_dir)
103+
104+
105+
# def pytest_assertrepr_compare(config, op, left, right):
106+
# print(op, left, right)
107+
# pass

pytest_image_diff/plugin.py

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os
2-
from typing import Optional, NamedTuple, cast, Callable, Generator
2+
from typing import Optional, NamedTuple, cast, Callable, Generator, Union
33

44
import pytest
55
from _pytest.fixtures import FixtureRequest
@@ -36,12 +36,20 @@ def image_diff_threshold() -> float:
3636
return 0.001
3737

3838

39+
@pytest.fixture(scope="session")
40+
def image_diff_throw_exception() -> bool:
41+
"""
42+
Set default throw exception. By default - True
43+
"""
44+
return True
45+
46+
3947
@pytest.fixture(scope="session")
4048
def image_diff_root(request: FixtureRequest) -> str:
4149
"""
4250
Root path for storing diff images. By default - `request.config.rootdir`
4351
"""
44-
return str(request.config.rootdir)
52+
return str(request.config.rootdir) # type: ignore
4553

4654

4755
@pytest.fixture(scope="session") # pragma: no cover
@@ -69,6 +77,37 @@ class DiffInfo(NamedTuple):
6977
DiffInfoCallableType = Callable[[ImageFileType, Optional[str]], DiffInfo]
7078

7179

80+
class DiffCompareResult:
81+
value: float
82+
thresh: float
83+
diff_path: Optional[str]
84+
85+
CompareType = Union[int, float, "DiffCompareResult"]
86+
87+
def __init__(self, value: float, thresh: float, diff_path: Optional[str] = None):
88+
self.value = value
89+
self.thresh = thresh
90+
self.diff_path = diff_path
91+
92+
def __repr__(self) -> str:
93+
return f"diff: {round(self.value, 4)}; thresh: {self.thresh}; diff path: {self.diff_path}"
94+
95+
def __bool__(self) -> bool:
96+
return self.value <= self.thresh
97+
98+
def __gt__(self, other: CompareType) -> bool:
99+
return self.value > other
100+
101+
def __ge__(self, other: CompareType) -> bool:
102+
return self.value >= other
103+
104+
def __lt__(self, other: CompareType) -> bool:
105+
return self.value < other
106+
107+
def __le__(self, other: CompareType) -> bool:
108+
return self.value <= other
109+
110+
72111
@pytest.fixture(scope="function")
73112
def _image_diff_info(
74113
request: FixtureRequest, image_diff_reference_dir: str, image_diff_dir: str
@@ -108,6 +147,7 @@ def image_regression(
108147
request: FixtureRequest,
109148
_image_diff_info: DiffInfoCallableType,
110149
image_diff_threshold: float,
150+
image_diff_throw_exception: bool,
111151
) -> Generator[ImageRegressionCallableType, None, None]:
112152
"""
113153
Check regression image.
@@ -122,35 +162,43 @@ def _factory(
122162
image: ImageFileType,
123163
threshold: Optional[float] = None,
124164
suffix: Optional[str] = None,
125-
) -> bool:
165+
throw_exception: Optional[bool] = None,
166+
) -> "DiffCompareResult":
126167
if threshold is None:
127168
threshold = image_diff_threshold
169+
if throw_exception is None:
170+
throw_exception = image_diff_throw_exception
128171
diff_info = _image_diff_info(image, suffix)
129172
reference_name = diff_info.reference_name
130173
diff_name = diff_info.diff_name
131174
image_name = diff_info.image_name
132175
if not os.path.exists(reference_name):
133176
ensure_dirs(reference_name)
134177
image_save(image, reference_name)
135-
return True
178+
return DiffCompareResult(0, threshold)
136179

137180
def _cleanup() -> None:
138181
if request.node.rep_setup.passed and request.node.rep_call.failed:
139182
# Do not cleanup regression artifacts
140183
return
141184
for f in [image_name, diff_name]:
142185
if os.path.exists(f):
143-
os.unlink(f)
186+
try:
187+
os.unlink(f)
188+
except PermissionError:
189+
# Opps, have windows issue
190+
pass
144191

145192
request.addfinalizer(_cleanup)
146193

147194
ensure_dirs(diff_name)
148195
ensure_dirs(image_name)
149196
image_save(image, image_name)
150197
diff_ratio = _diff(reference_name, image_name, diff_name)
151-
assert diff_ratio <= threshold, "Image not equals!" # noqa
152-
153-
return True
198+
cond = DiffCompareResult(diff_ratio, threshold, diff_name)
199+
if throw_exception:
200+
assert cond, "Image not equals!" # noqa
201+
return cond
154202

155203
yield _factory
156204

@@ -160,6 +208,7 @@ def image_diff(
160208
request: FixtureRequest,
161209
_image_diff_info: DiffInfoCallableType,
162210
image_diff_threshold: float,
211+
image_diff_throw_exception: bool,
163212
) -> Generator[ImageDiffCallableType, None, None]:
164213
"""
165214
Compare two image
@@ -176,9 +225,12 @@ def _factory(
176225
image_2: ImageFileType,
177226
threshold: Optional[float] = None,
178227
suffix: Optional[str] = None,
179-
) -> bool:
228+
throw_exception: Optional[bool] = None,
229+
) -> DiffCompareResult:
180230
if threshold is None:
181231
threshold = image_diff_threshold
232+
if throw_exception is None:
233+
throw_exception = image_diff_throw_exception
182234

183235
_info = _image_diff_info(image, suffix)
184236
diff_path = cast(str, _info.diff_name)
@@ -201,7 +253,9 @@ def _cleanup() -> None:
201253
image_save(image, path=image_temp_file)
202254
image_save(image_2, path=image_2_temp_file)
203255
diff_ratio = _diff(image_temp_file, image_2_temp_file, diff_path)
204-
assert diff_ratio <= threshold, "Image not equals! See %s" % diff_path # noqa
205-
return True
256+
cond = DiffCompareResult(diff_ratio, threshold, diff_path)
257+
if throw_exception:
258+
assert cond, "Image not equals! See %s" % diff_path # noqa
259+
return cond
206260

207261
yield _factory

pytest_image_diff/splinter.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import os
2-
from typing import Optional, Generator
2+
from typing import Optional, Generator, TYPE_CHECKING
33

44
import pytest
55
from typing_extensions import Protocol
66

77
from .helpers import temp_file
88

9+
if TYPE_CHECKING:
10+
from .plugin import DiffCompareResult
11+
912
try:
1013
# Check pytest-splinter
1114
from pytest_splinter.plugin import Browser
@@ -15,23 +18,24 @@
1518
if Browser:
1619
from ._types import ImageRegressionCallableType
1720

21+
__all__ = ["screenshot_regression"]
22+
1823
class ScreenshotRegressionCallableType(Protocol):
1924
def __call__(
2025
self,
2126
browser: Optional[Browser] = None,
2227
threshold: Optional[float] = None,
2328
suffix: Optional[str] = None,
2429
xpath: Optional[str] = "",
25-
) -> bool:
30+
) -> "DiffCompareResult":
2631
pass
2732

28-
__all__ = ["screenshot_regression"]
29-
3033
@pytest.fixture(scope="function")
3134
def screenshot_regression(
3235
browser: Browser,
3336
image_regression: ImageRegressionCallableType,
3437
image_diff_threshold: float,
38+
image_diff_throw_exception: bool,
3539
) -> Generator[ScreenshotRegressionCallableType, None, None]:
3640
"""
3741
Check regression browser screenshot
@@ -49,13 +53,17 @@ def _factory(
4953
threshold: Optional[float] = None,
5054
suffix: Optional[str] = "",
5155
xpath: Optional[str] = "",
52-
) -> bool:
56+
throw_exception: Optional[bool] = None,
57+
) -> "DiffCompareResult":
5358
if browser is None:
5459
browser = default_browser
5560

5661
if threshold is None:
5762
threshold = image_diff_threshold
5863

64+
if throw_exception is None:
65+
throw_exception = image_diff_throw_exception
66+
5967
with temp_file(suffix=".png") as temp_image_path:
6068
screenshot_path = os.fspath(temp_image_path)
6169

@@ -75,7 +83,9 @@ def _factory(
7583
else:
7684
browser.driver.save_screenshot(screenshot_path)
7785

78-
result = image_regression(screenshot_path, threshold, suffix)
86+
result = image_regression(
87+
screenshot_path, threshold, suffix, throw_exception=throw_exception
88+
)
7989
return result
8090

8191
yield _factory

requirements-dev.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ tox
55
twine
66

77
# Tests
8-
pytest>=6,<7
9-
pytest-cov==2.12.1
8+
pytest
9+
pytest-cov
1010

1111
# Docs
1212

0 commit comments

Comments
 (0)