123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291 |
- import os
- from pathlib import Path
- import subprocess
- import sys
- import weakref
- import numpy as np
- import pytest
- import matplotlib as mpl
- from matplotlib import pyplot as plt
- from matplotlib import animation
- class NullMovieWriter(animation.AbstractMovieWriter):
- """
- A minimal MovieWriter. It doesn't actually write anything.
- It just saves the arguments that were given to the setup() and
- grab_frame() methods as attributes, and counts how many times
- grab_frame() is called.
- This class doesn't have an __init__ method with the appropriate
- signature, and it doesn't define an isAvailable() method, so
- it cannot be added to the 'writers' registry.
- """
- def setup(self, fig, outfile, dpi, *args):
- self.fig = fig
- self.outfile = outfile
- self.dpi = dpi
- self.args = args
- self._count = 0
- def grab_frame(self, **savefig_kwargs):
- self.savefig_kwargs = savefig_kwargs
- self._count += 1
- def finish(self):
- pass
- def make_animation(**kwargs):
- fig, ax = plt.subplots()
- line, = ax.plot([])
- def init():
- pass
- def animate(i):
- line.set_data([0, 1], [0, i])
- return line,
- return animation.FuncAnimation(fig, animate, **kwargs)
- def test_null_movie_writer():
- # Test running an animation with NullMovieWriter.
- num_frames = 5
- anim = make_animation(frames=num_frames)
- filename = "unused.null"
- dpi = 50
- savefig_kwargs = dict(foo=0)
- writer = NullMovieWriter()
- anim.save(filename, dpi=dpi, writer=writer,
- savefig_kwargs=savefig_kwargs)
- assert writer.fig == plt.figure(1) # The figure used by make_animation.
- assert writer.outfile == filename
- assert writer.dpi == dpi
- assert writer.args == ()
- assert writer.savefig_kwargs == savefig_kwargs
- assert writer._count == num_frames
- def test_movie_writer_dpi_default():
- class DummyMovieWriter(animation.MovieWriter):
- def _run(self):
- pass
- # Test setting up movie writer with figure.dpi default.
- fig = plt.figure()
- filename = "unused.null"
- fps = 5
- codec = "unused"
- bitrate = 1
- extra_args = ["unused"]
- writer = DummyMovieWriter(fps, codec, bitrate, extra_args)
- writer.setup(fig, filename)
- assert writer.dpi == fig.dpi
- @animation.writers.register('null')
- class RegisteredNullMovieWriter(NullMovieWriter):
- # To be able to add NullMovieWriter to the 'writers' registry,
- # we must define an __init__ method with a specific signature,
- # and we must define the class method isAvailable().
- # (These methods are not actually required to use an instance
- # of this class as the 'writer' argument of Animation.save().)
- def __init__(self, fps=None, codec=None, bitrate=None,
- extra_args=None, metadata=None):
- pass
- @classmethod
- def isAvailable(cls):
- return True
- WRITER_OUTPUT = [
- ('ffmpeg', 'movie.mp4'),
- ('ffmpeg_file', 'movie.mp4'),
- ('avconv', 'movie.mp4'),
- ('avconv_file', 'movie.mp4'),
- ('imagemagick', 'movie.gif'),
- ('imagemagick_file', 'movie.gif'),
- ('pillow', 'movie.gif'),
- ('html', 'movie.html'),
- ('null', 'movie.null')
- ]
- WRITER_OUTPUT += [
- (writer, Path(output)) for writer, output in WRITER_OUTPUT]
- # Smoke test for saving animations. In the future, we should probably
- # design more sophisticated tests which compare resulting frames a-la
- # matplotlib.testing.image_comparison
- @pytest.mark.parametrize('writer, output', WRITER_OUTPUT)
- def test_save_animation_smoketest(tmpdir, writer, output):
- if writer == 'pillow':
- pytest.importorskip("PIL")
- try:
- # for ImageMagick the rcparams must be patched to account for
- # 'convert' being a built in MS tool, not the imagemagick
- # tool.
- writer._init_from_registry()
- except AttributeError:
- pass
- if not animation.writers.is_available(writer):
- pytest.skip("writer '%s' not available on this system" % writer)
- fig, ax = plt.subplots()
- line, = ax.plot([], [])
- ax.set_xlim(0, 10)
- ax.set_ylim(-1, 1)
- dpi = None
- codec = None
- if writer == 'ffmpeg':
- # Issue #8253
- fig.set_size_inches((10.85, 9.21))
- dpi = 100.
- codec = 'h264'
- def init():
- line.set_data([], [])
- return line,
- def animate(i):
- x = np.linspace(0, 10, 100)
- y = np.sin(x + i)
- line.set_data(x, y)
- return line,
- # Use temporary directory for the file-based writers, which produce a file
- # per frame with known names.
- with tmpdir.as_cwd():
- anim = animation.FuncAnimation(fig, animate, init_func=init, frames=5)
- try:
- anim.save(output, fps=30, writer=writer, bitrate=500, dpi=dpi,
- codec=codec)
- except UnicodeDecodeError:
- pytest.xfail("There can be errors in the numpy import stack, "
- "see issues #1891 and #2679")
- def test_no_length_frames():
- (make_animation(frames=iter(range(5)))
- .save('unused.null', writer=NullMovieWriter()))
- def test_movie_writer_registry():
- assert len(animation.writers._registered) > 0
- mpl.rcParams['animation.ffmpeg_path'] = "not_available_ever_xxxx"
- assert not animation.writers.is_available("ffmpeg")
- # something which is guaranteed to be available in path
- # and exits immediately
- bin = "true" if sys.platform != 'win32' else "where"
- mpl.rcParams['animation.ffmpeg_path'] = bin
- assert animation.writers.is_available("ffmpeg")
- @pytest.mark.parametrize(
- "method_name",
- [pytest.param("to_html5_video", marks=pytest.mark.skipif(
- not animation.writers.is_available(mpl.rcParams["animation.writer"]),
- reason="animation writer not installed")),
- "to_jshtml"])
- def test_embed_limit(method_name, caplog, tmpdir):
- caplog.set_level("WARNING")
- with tmpdir.as_cwd():
- with mpl.rc_context({"animation.embed_limit": 1e-6}): # ~1 byte.
- getattr(make_animation(frames=1), method_name)()
- assert len(caplog.records) == 1
- record, = caplog.records
- assert (record.name == "matplotlib.animation"
- and record.levelname == "WARNING")
- @pytest.mark.parametrize(
- "method_name",
- [pytest.param("to_html5_video", marks=pytest.mark.skipif(
- not animation.writers.is_available(mpl.rcParams["animation.writer"]),
- reason="animation writer not installed")),
- "to_jshtml"])
- def test_cleanup_temporaries(method_name, tmpdir):
- with tmpdir.as_cwd():
- getattr(make_animation(frames=1), method_name)()
- assert list(Path(str(tmpdir)).iterdir()) == []
- @pytest.mark.skipif(os.name != "posix", reason="requires a POSIX OS")
- def test_failing_ffmpeg(tmpdir, monkeypatch):
- """
- Test that we correctly raise a CalledProcessError when ffmpeg fails.
- To do so, mock ffmpeg using a simple executable shell script that
- succeeds when called with no arguments (so that it gets registered by
- `isAvailable`), but fails otherwise, and add it to the $PATH.
- """
- with tmpdir.as_cwd():
- monkeypatch.setenv("PATH", ".:" + os.environ["PATH"])
- exe_path = Path(str(tmpdir), "ffmpeg")
- exe_path.write_text("#!/bin/sh\n"
- "[[ $@ -eq 0 ]]\n")
- os.chmod(str(exe_path), 0o755)
- with pytest.raises(subprocess.CalledProcessError):
- make_animation().save("test.mpeg")
- @pytest.mark.parametrize("cache_frame_data, weakref_assertion_fn", [
- pytest.param(
- False, lambda ref: ref is None, id='cache_frame_data_is_disabled'),
- pytest.param(
- True, lambda ref: ref is not None, id='cache_frame_data_is_enabled'),
- ])
- def test_funcanimation_holding_frames(cache_frame_data, weakref_assertion_fn):
- fig, ax = plt.subplots()
- line, = ax.plot([], [])
- class Frame(dict):
- # this subclassing enables to use weakref.ref()
- pass
- def init():
- line.set_data([], [])
- return line,
- def animate(frame):
- line.set_data(frame['x'], frame['y'])
- return line,
- frames_generated = []
- def frames_generator():
- for _ in range(5):
- x = np.linspace(0, 10, 100)
- y = np.random.rand(100)
- frame = Frame(x=x, y=y)
- # collect weak references to frames
- # to validate their references later
- frames_generated.append(weakref.ref(frame))
- yield frame
- anim = animation.FuncAnimation(fig, animate, init_func=init,
- frames=frames_generator,
- cache_frame_data=cache_frame_data)
- writer = NullMovieWriter()
- anim.save('unused.null', writer=writer)
- assert len(frames_generated) == 5
- for f in frames_generated:
- assert weakref_assertion_fn(f())
|