1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701
|
import os
import sys
import json
import subprocess
from pathlib import Path
from unittest import TestCase
import matplotlib
import matplotlib.ft2font
import matplotlib.pyplot as plt
import pytest
from helpers import skip_if_format_unsupported
from packaging.version import Version
MPL_VERSION = Version(matplotlib.__version__)
baseline_dir = 'baseline'
if MPL_VERSION >= Version('2'):
baseline_subdir = '2.0.x'
baseline_dir_local = os.path.join(baseline_dir, baseline_subdir)
baseline_dir_remote = 'http://matplotlib.github.io/pytest-mpl/' + baseline_subdir + '/'
ftv = matplotlib.ft2font.__freetype_version__.replace('.', '')
hash_filename = f"mpl{MPL_VERSION.major}{MPL_VERSION.minor}_ft{ftv}.json"
if "+" in matplotlib.__version__:
hash_filename = "mpldev.json"
hash_library = (Path(__file__).parent / "baseline" / # noqa
"hashes" / hash_filename)
fail_hash_library = Path(__file__).parent / "baseline" / "test_hash_lib.json"
baseline_dir_abs = Path(__file__).parent / "baseline" / baseline_subdir
hash_baseline_dir_abs = Path(__file__).parent / "baseline" / "hybrid"
WIN = sys.platform.startswith('win')
# In some cases, the fonts on Windows can be quite different
DEFAULT_TOLERANCE = 10 if WIN else 2
def call_pytest(args):
return subprocess.call([sys.executable, '-m', 'pytest', '-s'] + args)
def assert_pytest_fails_with(args, output_substring):
try:
subprocess.check_output([sys.executable, '-m', 'pytest', '-s'] + args)
except subprocess.CalledProcessError as exc:
output = exc.output.decode()
assert output_substring in output, output
return output
else:
raise RuntimeError(f'pytest did not fail with args {args}')
@pytest.mark.mpl_image_compare(baseline_dir=baseline_dir_local,
tolerance=DEFAULT_TOLERANCE)
def test_succeeds():
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot([1, 2, 3])
assert fig
@pytest.mark.mpl_image_compare(baseline_dir=baseline_dir_remote,
tolerance=DEFAULT_TOLERANCE)
def test_succeeds_remote():
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot([1, 2, 3])
assert fig
# The following tries an invalid URL first (or at least a URL where the baseline
# image won't exist), but should succeed with the second mirror.
@pytest.mark.mpl_image_compare(baseline_dir='http://www.python.org,' + baseline_dir_remote,
filename='test_succeeds_remote.png',
tolerance=DEFAULT_TOLERANCE)
def test_succeeds_faulty_mirror():
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot([1, 2, 3])
assert fig
class TestClass(object):
@pytest.mark.mpl_image_compare(baseline_dir=baseline_dir_local,
tolerance=DEFAULT_TOLERANCE)
def test_succeeds(self):
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot([1, 2, 3])
assert fig
@pytest.mark.mpl_image_compare(baseline_dir=baseline_dir_local,
savefig_kwargs={'dpi': 30},
tolerance=DEFAULT_TOLERANCE)
def test_dpi():
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot([1, 2, 3])
assert fig
TEST_FAILING = """
import pytest
import matplotlib.pyplot as plt
@pytest.mark.mpl_image_compare
def test_fail():
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot([1,2,2])
assert fig
"""
def test_fails(tmp_path):
test_file = tmp_path / "test.py"
test_file.write_text(TEST_FAILING)
test_file = str(test_file)
# If we use --mpl, it should detect that the figure is wrong
code = call_pytest(['--mpl', test_file])
assert code != 0
# If we don't use --mpl option, the test should succeed
code = call_pytest([test_file])
assert code == 0
TEST_OUTPUT_DIR = """
import pytest
import matplotlib.pyplot as plt
@pytest.mark.mpl_image_compare
def test_output_dir():
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot([1, 2, 3])
assert fig
"""
@pytest.mark.skip(reason='works only under tox')
def test_output_dir(tmp_path):
test_file = tmp_path / "test.py"
test_file.write_text(TEST_OUTPUT_DIR)
output_dir = tmp_path / "test_output_dir"
# When we run the test, we should get output images where we specify
code = call_pytest([f'--mpl-results-path={output_dir}',
'--mpl', str(test_file)])
assert code != 0
assert output_dir.exists()
assert (output_dir / "test.test_output_dir" / "result.png").exists()
TEST_GENERATE = """
import pytest
import matplotlib.pyplot as plt
@pytest.mark.mpl_image_compare
def test_gen():
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot([1,2,3])
assert fig
"""
@pytest.mark.skip
def test_generate(tmp_path):
test_file = tmp_path / "test.py"
test_file.write_text(TEST_GENERATE)
test_file = str(test_file)
gen_dir = tmp_path / "spam" / "egg"
gen_dir.mkdir(parents=True)
# If we don't generate, the test will fail
assert_pytest_fails_with(['--mpl', test_file], 'Image file not found for comparison test')
# If we do generate, the test should succeed and a new file will appear
code = call_pytest([f'--mpl-generate-path={gen_dir}', test_file])
assert code == 0
assert os.path.exists(os.path.join(gen_dir, 'test_gen.png'))
# If we do generate hash, the test will fail as no image is present
hash_file = os.path.join(gen_dir, 'test_hashes.json')
code = call_pytest([f'--mpl-generate-hash-library={hash_file}', test_file])
assert code == 1
assert os.path.exists(hash_file)
with open(hash_file) as fp:
hash_lib = json.load(fp)
assert "test.test_gen" in hash_lib
@pytest.mark.mpl_image_compare(baseline_dir=baseline_dir_local, tolerance=20)
def test_tolerance():
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot([1, 2, 2])
assert fig
def test_nofigure():
pass
@pytest.mark.mpl_image_compare(baseline_dir=baseline_dir_local,
style='fivethirtyeight',
tolerance=DEFAULT_TOLERANCE)
def test_base_style():
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot([1, 2, 3])
assert fig
@pytest.mark.mpl_image_compare(baseline_dir=baseline_dir_local,
remove_text=True)
def test_remove_text():
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot([1, 2, 3])
assert fig
@pytest.mark.parametrize('s', [5, 50, 500])
@pytest.mark.mpl_image_compare(baseline_dir=baseline_dir_local,
remove_text=True)
def test_parametrized(s):
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.scatter([1, 3, 4, 3, 2], [1, 4, 3, 3, 1], s=s)
assert fig
class TestClassWithSetup:
# Regression test for a bug that occurred when using setup_method
def setup_method(self, method):
self.x = [1, 2, 3]
@pytest.mark.mpl_image_compare(baseline_dir=baseline_dir_local,
filename='test_succeeds.png',
tolerance=DEFAULT_TOLERANCE)
def test_succeeds(self):
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(self.x)
assert fig
class TestClassWithTestCase(TestCase):
# Regression test for a bug that occurred when using unittest.TestCase
def setUp(self):
self.x = [1, 2, 3]
@pytest.mark.mpl_image_compare(baseline_dir=baseline_dir_local,
filename='test_succeeds.png',
tolerance=DEFAULT_TOLERANCE)
def test_succeeds(self):
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(self.x)
assert fig
# hashlib
@pytest.mark.skipif(not hash_library.exists(), reason="No hash library for this mpl version")
@pytest.mark.mpl_image_compare(hash_library=hash_library, deterministic=True)
def test_hash_succeeds():
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot([1, 2, 3])
assert fig
TEST_FAILING_HASH = rf"""
import pytest
import matplotlib.pyplot as plt
@pytest.mark.mpl_image_compare(hash_library=r"{fail_hash_library}", deterministic=True)
def test_hash_fails():
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot([1,2,2])
assert fig
"""
@pytest.mark.skip(reason='works only under tox')
def test_hash_fails(tmp_path):
test_file = tmp_path / "test.py"
test_file.write_text(TEST_FAILING_HASH, encoding="ascii")
test_file = str(test_file)
# If we use --mpl, it should detect that the figure is wrong
output = assert_pytest_fails_with(['--mpl', test_file], "doesn't match hash FAIL in library")
# We didn't specify a baseline dir so we shouldn't attempt to find one
assert "Image file not found for comparison test" not in output, output
# Check that the summary path is printed and that it exists.
output = assert_pytest_fails_with(['--mpl', test_file, '--mpl-generate-summary=html'],
"doesn't match hash FAIL in library")
# We didn't specify a baseline dir so we shouldn't attempt to find one
print_message = "A summary of test results can be found at:"
assert print_message in output, output
printed_path = Path(output.split(print_message)[1].strip())
assert printed_path.exists()
# If we don't use --mpl option, the test should succeed
code = call_pytest([test_file])
assert code == 0
TEST_FAILING_HYBRID = rf"""
import pytest
import matplotlib.pyplot as plt
@pytest.mark.mpl_image_compare(hash_library=r"{fail_hash_library}",
tolerance=2, deterministic=True)
def test_hash_fail_hybrid():
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot([1,2,3])
assert fig
"""
@pytest.mark.skipif(ftv != '261', reason="Incorrect freetype version for hash check")
def test_hash_fail_hybrid(tmp_path):
test_file = tmp_path / "test.py"
test_file.write_text(TEST_FAILING_HYBRID, encoding="ascii")
test_file = str(test_file)
# Assert that image comparison runs and fails
output = assert_pytest_fails_with(['--mpl', test_file,
rf'--mpl-baseline-path={hash_baseline_dir_abs / "fail"}'],
"doesn't match hash FAIL in library")
assert "Error: Image files did not match." in output, output
# Assert reports missing baseline image
output = assert_pytest_fails_with(['--mpl', test_file,
'--mpl-baseline-path=/not/a/path'],
"doesn't match hash FAIL in library")
assert "Image file not found for comparison test" in output, output
# Assert reports image comparison succeeds
output = assert_pytest_fails_with(['--mpl', test_file,
rf'--mpl-baseline-path={hash_baseline_dir_abs / "succeed"}'],
"doesn't match hash FAIL in library")
assert "The comparison to the baseline image succeeded." in output, output
# If we don't use --mpl option, the test should succeed
code = call_pytest([test_file])
assert code == 0
TEST_FAILING_NEW_HASH = r"""
import pytest
import matplotlib.pyplot as plt
@pytest.mark.mpl_image_compare
def test_hash_fails():
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot([1,2,2])
assert fig
"""
@pytest.mark.skipif(ftv != '261', reason="Incorrect freetype version for hash check")
def test_hash_fail_new_hashes(tmp_path):
# Check that the hash comparison fails even if a new hash file is requested
test_file = tmp_path / "test.py"
test_file.write_text(TEST_FAILING_NEW_HASH, encoding="ascii")
test_file = str(test_file)
# Assert that image comparison runs and fails
assert_pytest_fails_with(['--mpl', test_file,
f'--mpl-hash-library={fail_hash_library}'],
"doesn't match hash FAIL in library")
hash_file = tmp_path / "new_hashes.json"
# Assert that image comparison runs and fails
assert_pytest_fails_with(['--mpl', test_file,
f'--mpl-hash-library={fail_hash_library}',
f'--mpl-generate-hash-library={hash_file}'],
"doesn't match hash FAIL")
TEST_MISSING_HASH = """
import pytest
import matplotlib.pyplot as plt
@pytest.mark.mpl_image_compare
def test_hash_missing():
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot([1,2,2])
assert fig
"""
@pytest.mark.skip(reason='works only under tox')
def test_hash_missing(tmp_path):
test_file = tmp_path / "test.py"
test_file.write_text(TEST_MISSING_HASH)
test_file = str(test_file)
# Assert fails if hash library missing
assert_pytest_fails_with(['--mpl', test_file, '--mpl-hash-library=/not/a/path'],
"Can't find hash library at path")
# Assert fails if hash not in library
assert_pytest_fails_with(['--mpl', test_file, f'--mpl-hash-library={fail_hash_library}'],
"Hash for test 'test.test_hash_missing' not found in")
# If we don't use --mpl option, the test should succeed
code = call_pytest([test_file])
assert code == 0
TEST_RESULTS_ALWAYS = """
import pytest
import matplotlib.pyplot as plt
def plot():
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot([1,2,2])
assert fig
@pytest.mark.mpl_image_compare(deterministic=True)
def test_modified(): assert plot()
@pytest.mark.mpl_image_compare(deterministic=True)
def test_new(): assert plot()
@pytest.mark.mpl_image_compare(deterministic=True)
def test_unmodified(): assert plot()
"""
@pytest.mark.skipif(not hash_library.exists(), reason="No hash library for this mpl version")
def test_results_always(tmp_path):
test_file = tmp_path / "test.py"
test_file.write_text(TEST_RESULTS_ALWAYS)
results_path = tmp_path / "results"
results_path.mkdir()
code = call_pytest(['--mpl', str(test_file), '--mpl-results-always',
rf'--mpl-hash-library={hash_library}',
rf'--mpl-baseline-path={baseline_dir_abs}',
'--mpl-generate-summary=html,json,basic-html',
rf'--mpl-results-path={results_path}'])
assert code == 0 # hashes correct, so all should pass
# assert files for interactive HTML exist
assert (results_path / "fig_comparison.html").exists()
assert (results_path / "styles.css").exists()
assert (results_path / "extra.js").exists()
html = (results_path / "fig_comparison_basic.html").read_text()
with (results_path / "results.json").open("r") as f:
json_results = json.load(f)
# each test, and which images should exist
for test, exists in [
('test_modified', ['baseline', 'result-failed-diff', 'result']),
('test_new', ['result']),
('test_unmodified', ['baseline', 'result']),
]:
test_name = f'test.{test}'
summary = f'<div class="test-name">{test_name.split(".")[-1]}</div>'
assert summary in html
assert test_name in json_results.keys()
json_res = json_results[test_name]
assert json_res['status'] == 'passed'
for image_type in ['baseline', 'result-failed-diff', 'result']:
image = f'{test_name}/{image_type}.png'
image_exists = (results_path / image).exists()
json_image_key = f"{image_type.split('-')[-1]}_image"
if image_type in exists: # assert image so pytest prints it on error
assert image and image_exists
assert image in html
assert json_res[json_image_key] == image
else:
assert image and not image_exists
assert image not in html
assert json_res[json_image_key] is None
TEST_FAILING_CLASS = """
import pytest
import matplotlib.pyplot as plt
class TestClass(object):
@pytest.mark.mpl_image_compare
def test_fails(self):
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot([1, 2, 3])
assert fig
"""
TEST_FAILING_CLASS_SETUP_METHOD = """
import pytest
import matplotlib.pyplot as plt
class TestClassWithSetup:
def setup_method(self, method):
self.x = [1, 2, 3]
@pytest.mark.mpl_image_compare
def test_fails(self):
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(self.x)
assert fig
"""
TEST_FAILING_UNITTEST_TESTCASE = """
from unittest import TestCase
import pytest
import matplotlib.pyplot as plt
class TestClassWithTestCase(TestCase):
def setUp(self):
self.x = [1, 2, 3]
@pytest.mark.mpl_image_compare
def test_fails(self):
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(self.x)
assert fig
"""
@pytest.mark.skip(reason='works only under tox')
def test_class_fail(code, tmp_path):
test_file = tmp_path / "test.py"
test_file.write_text(code)
test_file = str(test_file)
# Assert fails if hash library missing
assert_pytest_fails_with(['--mpl', test_file, '--mpl-hash-library=/not/a/path'],
"Can't find hash library at path")
# If we don't use --mpl option, the test should succeed
code = call_pytest([test_file])
assert code == 0
@pytest.mark.skip(reason='works only under tox')
def test_user_fail(pytester, runpytest_args):
pytester.makepyfile(
"""
import pytest
@pytest.mark.mpl_image_compare
def test_fail():
pytest.fail("Manually failed by user.")
"""
)
result = pytester.runpytest(*runpytest_args)
result.assert_outcomes(failed=1)
result.stdout.fnmatch_lines("FAILED*Manually failed by user.*")
@pytest.mark.skip(reason='works only under tox')
def test_user_skip(pytester, runpytest_args):
pytester.makepyfile(
"""
import pytest
@pytest.mark.mpl_image_compare
def test_skip():
pytest.skip("Manually skipped by user.")
"""
)
result = pytester.runpytest(*runpytest_args)
result.assert_outcomes(skipped=1)
@pytest.mark.skip(reason='works only under tox')
def test_user_importorskip(pytester, runpytest_args):
pytester.makepyfile(
"""
import pytest
@pytest.mark.mpl_image_compare
def test_importorskip():
pytest.importorskip("nonexistantmodule")
"""
)
result = pytester.runpytest(*runpytest_args)
result.assert_outcomes(skipped=1)
@pytest.mark.skip(reason='works only under tox')
def test_user_xfail(pytester, runpytest_args):
pytester.makepyfile(
"""
import pytest
@pytest.mark.mpl_image_compare
def test_xfail():
pytest.xfail()
"""
)
result = pytester.runpytest(*runpytest_args)
result.assert_outcomes(xfailed=1)
@pytest.mark.skip(reason='works only under tox')
def test_user_exit_success(pytester, runpytest_args):
pytester.makepyfile(
"""
import pytest
@pytest.mark.mpl_image_compare
def test_exit_success():
pytest.exit("Manually exited by user.", returncode=0)
"""
)
result = pytester.runpytest(*runpytest_args)
result.assert_outcomes()
assert result.ret == 0
result.stdout.fnmatch_lines("*Exit*Manually exited by user.*")
@pytest.mark.skip(reason='works only under tox')
def test_user_exit_failure(pytester, runpytest_args):
pytester.makepyfile(
"""
import pytest
@pytest.mark.mpl_image_compare
def test_exit_fail():
pytest.exit("Manually exited by user.", returncode=1)
"""
)
result = pytester.runpytest(*runpytest_args)
result.assert_outcomes()
assert result.ret == 1
result.stdout.fnmatch_lines("*Exit*Manually exited by user.*")
@pytest.mark.skip(reason='works only under tox')
def test_user_function_raises(pytester, runpytest_args):
pytester.makepyfile(
"""
import pytest
@pytest.mark.mpl_image_compare
def test_raises():
raise ValueError("User code raised an exception.")
"""
)
result = pytester.runpytest(*runpytest_args)
result.assert_outcomes(failed=1)
result.stdout.fnmatch_lines("FAILED*ValueError*User code*")
@pytest.mark.parametrize('use_hash_library', (False, True))
@pytest.mark.parametrize('passes', (False, True))
@pytest.mark.parametrize("file_format", ['eps', 'pdf', 'png', 'svg'])
def test_formats(pytester, use_hash_library, passes, file_format):
"""
Note that we don't test all possible formats as some do not compress well
and would bloat the baseline directory.
"""
skip_if_format_unsupported(file_format, using_hashes=use_hash_library)
if use_hash_library and not hash_library.exists():
pytest.skip("No hash library for this mpl version")
pytester.makepyfile(
f"""
import os
import pytest
import matplotlib.pyplot as plt
@pytest.mark.mpl_image_compare(baseline_dir=r"{baseline_dir_abs}",
{f'hash_library=r"{hash_library}",' if use_hash_library else ''}
tolerance={DEFAULT_TOLERANCE},
deterministic=True,
savefig_kwargs={{'format': '{file_format}'}})
def test_format_{file_format}():
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot([{1 if passes else 3}, 2, 3])
assert fig
"""
)
result = pytester.runpytest('--mpl', '-rs')
if passes:
result.assert_outcomes(passed=1)
else:
result.assert_outcomes(failed=1)
|