From fed076348ba436a55864718a8cd94b9fe7927740 Mon Sep 17 00:00:00 2001 From: bkfox Date: Sun, 18 Jun 2023 16:56:34 +0200 Subject: [PATCH] finish writing controllers.monitor tests --- aircox/test.py | 4 +- aircox_streamer/controllers/monitor.py | 21 +- aircox_streamer/tests/conftest.py | 51 ++-- .../tests/test_controllers_monitor.py | 251 ++++++++++++++++++ 4 files changed, 290 insertions(+), 37 deletions(-) create mode 100644 aircox_streamer/tests/test_controllers_monitor.py diff --git a/aircox/test.py b/aircox/test.py index 8400cab..0ce71ae 100644 --- a/aircox/test.py +++ b/aircox/test.py @@ -11,13 +11,13 @@ def interface(obj, funcs): Attribute ``obj.calls`` is a dict with all call done using those methods, as ``{func_name: (args, kwargs) | list[(args, kwargs]]}``. """ + if not isinstance(getattr(obj, "calls", None), dict): + obj.calls = {} for attr, value in funcs.items(): interface_wrap(obj, attr, value) def interface_wrap(obj, attr, value): - if not isinstance(getattr(obj, "calls", None), dict): - obj.calls = {} obj.calls[attr] = None def wrapper(*a, **kw): diff --git a/aircox_streamer/controllers/monitor.py b/aircox_streamer/controllers/monitor.py index fc9c685..0e8f46a 100644 --- a/aircox_streamer/controllers/monitor.py +++ b/aircox_streamer/controllers/monitor.py @@ -66,7 +66,7 @@ class Monitor: "diffusion", "sound", "track" ).order_by("-pk") - def init_last_sound_logs(self, key=None): + def init_last_sound_logs(self): """Retrieve last logs and initialize `last_sound_logs`""" logs = {} for source in self.streamer.sources: @@ -159,7 +159,7 @@ class Monitor: return tracks = Track.objects.filter( - sound__id=log.sound_id, timestamp__isnull=False + sound_id=log.sound_id, timestamp__isnull=False ).order_by("timestamp") if not tracks.exists(): return @@ -169,15 +169,14 @@ class Monitor: now = tz.now() for track in tracks: pos = log.date + tz.timedelta(seconds=track.timestamp) - if pos > now: - break - self.log( - type=Log.TYPE_ON_AIR, - date=pos, - source=log.source, - track=track, - comment=track, - ) + if pos <= now: + self.log( + type=Log.TYPE_ON_AIR, + date=pos, + source=log.source, + track=track, + comment=track, + ) def handle_diffusions(self): """Handle scheduled diffusion, trigger if needed, preload playlists and diff --git a/aircox_streamer/tests/conftest.py b/aircox_streamer/tests/conftest.py index 7098d89..b38e766 100644 --- a/aircox_streamer/tests/conftest.py +++ b/aircox_streamer/tests/conftest.py @@ -18,7 +18,25 @@ local_tz = tzlocal.get_localzone() working_dir = os.path.join(os.path.dirname(__file__), "working_dir") -def interface(self, obj, funcs): +def interface_wrap(obj, attr, value): + if not isinstance(getattr(obj, "calls", None), dict): + obj.calls = {} + obj.calls[attr] = None + + def wrapper(*a, **kw): + call = obj.calls.get(attr) + if call is None: + obj.calls[attr] = (a, kw) + elif isinstance(call, tuple): + obj.calls[attr] = [call, (a, kw)] + else: + call.append((a, kw)) + return value + + setattr(obj, attr, wrapper) + + +def interface(obj, funcs): """Override provided object's functions using dict of funcs, as ``{ func_name: return_value}``. @@ -26,23 +44,8 @@ def interface(self, obj, funcs): with all call done using those methods, as ``{func_name: (args, kwargs)}``. """ - if not isinstance(getattr(obj, "calls", None), dict): - obj.calls = {} - for attr, value in funcs.items(): - - def func(*a, **kw): - call = obj.calls.get(attr) - if call is None: - obj.calls[attr] = (a, kw) - elif isinstance(call, tuple): - obj.calls[attr] = [call, (a, kw)] - else: - call.append((a, kw)) - return value - - obj.calls[attr] = None - setattr(obj, attr, func) + interface_wrap(obj, attr, value) class FakeSocket: @@ -172,15 +175,12 @@ def stream(program): @pytest.fixture def episode(program): - episode = baker.make(models.Episode, title="test episode", program=program) - episode.playlist = lambda: ["/tmp/a", "/tmp/b"] - return episode + return baker.make(models.Episode, title="test episode", program=program) @pytest.fixture def sound(program, episode): - return baker.make( - models.Sound, + sound = models.Sound( program=program, episode=episode, name="sound", @@ -188,6 +188,8 @@ def sound(program, episode): position=0, file="sound.mp3", ) + sound.save(check=False) + return sound @pytest.fixture @@ -266,6 +268,7 @@ def metadata_string(metadata_data): # -- streamers class FakeStreamer(controllers.Streamer): calls = {} + is_ready = False def __init__(self, **kwargs): self.__dict__.update(**kwargs) @@ -311,7 +314,7 @@ class FakeQueueSource(FakeSource, controllers.QueueSource): @pytest.fixture def streamer(station, station_ports): - streamer = FakeStreamer(station) + streamer = FakeStreamer(station=station) streamer.sources = [ FakePlaylist(i, uri=f"source-{i}") for i in range(0, 3) ] @@ -324,7 +327,7 @@ def streamers(stations, stations_ports): streamers = controllers.Streamers(streamer_class=FakeStreamer) # avoid unecessary db calls streamers.streamers = { - station.pk: FakeStreamer(station) for station in stations + station.pk: FakeStreamer(station=station) for station in stations } for j, streamer in enumerate(streamers.values()): streamer.sources = [ diff --git a/aircox_streamer/tests/test_controllers_monitor.py b/aircox_streamer/tests/test_controllers_monitor.py new file mode 100644 index 0000000..6390265 --- /dev/null +++ b/aircox_streamer/tests/test_controllers_monitor.py @@ -0,0 +1,251 @@ +from django.utils import timezone as tz + +import pytest +from model_bakery import baker + +from aircox import models +from aircox.test import interface +from aircox_streamer import controllers + + +@pytest.fixture +def monitor(streamer): + streamer.calls = {} + return controllers.Monitor( + streamer, + tz.timedelta(seconds=10), + cancel_timeout=tz.timedelta(minutes=10), + sync_timeout=tz.timedelta(minutes=5), + ) + + +@pytest.fixture +def diffusion(program, episode): + return baker.make( + models.Diffusion, + program=program, + episode=episode, + start=tz.now() - tz.timedelta(minutes=10), + end=tz.now() + tz.timedelta(minutes=30), + schedule=None, + type=models.Diffusion.TYPE_ON_AIR, + ) + + +@pytest.fixture +def source(monitor, streamer, sound, diffusion): + source = next(monitor.streamer.playlists) + source.uri = sound.file.path + source.episode_id = sound.episode_id + source.air_time = diffusion.start + tz.timedelta(seconds=10) + return source + + +@pytest.fixture +def tracks(sound): + items = [ + baker.prepare(models.Track, sound=sound, position=i, timestamp=i * 60) + for i in range(0, 4) + ] + models.Track.objects.bulk_create(items) + return items + + +@pytest.fixture +def log(station, source, sound): + return baker.make( + models.Log, + station=station, + type=models.Log.TYPE_START, + sound=sound, + source=source.id, + ) + + +class TestMonitor: + @pytest.mark.django_db(transaction=True) + def test_last_diff_start(self, monitor): + pass + + @pytest.mark.django_db(transaction=True) + def test___init__(self, monitor): + assert isinstance(monitor.logs, models.LogQuerySet) + assert isinstance(monitor.last_sound_logs, dict) + + @pytest.mark.django_db(transaction=True) + def test_get_logs_queryset(self, monitor, station, sounds): + query = monitor.get_logs_queryset() + assert all(log.station_id == station.pk for log in query) + + @pytest.mark.django_db(transaction=True) + def test_init_last_sound_logs(self, monitor, source, log): + monitor.init_last_sound_logs() + assert monitor.last_sound_logs[source.id] == log + + @pytest.mark.django_db(transaction=True) + def test_monitor(self, monitor, source, log, sound): + monitor.streamer.is_ready = True + monitor.streamer.source = source + interface( + monitor, + { + "trace_sound": log, + "trace_tracks": None, + "handle_diffusions": None, + "sync": None, + }, + ) + + monitor.monitor() + assert monitor.streamer.calls.get("fetch") + assert monitor.calls["trace_sound"] == ((source,), {}) + assert monitor.calls["trace_tracks"] == ((log,), {}) + assert monitor.calls["handle_diffusions"] + assert monitor.calls["sync"] + + @pytest.mark.django_db(transaction=True) + def test_monitor_streamer_not_ready(self, monitor): + monitor.streamer.is_ready = False + interface( + monitor, + { + "trace_sound": log, + "trace_tracks": None, + "handle_diffusions": None, + "sync": None, + }, + ) + + monitor.monitor() + assert not monitor.streamer.calls.get("fetch") + assert monitor.calls["trace_sound"] is None + assert monitor.calls["trace_tracks"] is None + assert not monitor.calls["handle_diffusions"] + assert not monitor.calls["sync"] + + @pytest.mark.django_db(transaction=True) + def test_monitor_no_source_uri(self, monitor, log): + source.uri = None + monitor.streamer.is_ready = True + monitor.streamer.source = source + interface( + monitor, + { + "trace_sound": log, + "trace_tracks": None, + "handle_diffusions": None, + "sync": None, + }, + ) + + monitor.monitor() + assert monitor.streamer.calls.get("fetch") + assert monitor.calls["trace_sound"] is None + assert monitor.calls["trace_tracks"] is None + assert monitor.calls["handle_diffusions"] + assert monitor.calls["sync"] + + @pytest.mark.django_db(transaction=True) + def test_trace_sound(self, monitor, diffusion, source, sound): + monitor.last_sound_logs[source.id] = None + + result = monitor.trace_sound(source) + assert result.type == models.Log.TYPE_ON_AIR + assert result.source == source.id + assert result.sound == sound + assert result.diffusion == diffusion + + @pytest.mark.django_db(transaction=True) + def test_trace_sound_returns_last_log(self, monitor, source, sound, log): + log.sound = sound + monitor.last_sound_logs[source.id] = log + + result = monitor.trace_sound(source) + assert result == log + + @pytest.mark.django_db(transaction=True) + def test_trace_tracks(self, monitor, log, tracks): + interface(monitor, {"log": None}) + for track in tracks: + log.date = tz.now() - tz.timedelta(seconds=track.timestamp + 5) + monitor.trace_tracks(log) + + assert monitor.calls["log"] + log_by_track = [call[1].get("track") for call in monitor.calls["log"]] + # only one call of log + assert all(log_by_track.count(track) for track in tracks) + + @pytest.mark.django_db(transaction=True) + def test_trace_tracks_returns_on_log_diffusion( + self, monitor, log, diffusion, tracks + ): + log.diffusion = None + monitor.trace_tracks(log) + + @pytest.mark.django_db(transaction=True) + def test_trace_tracks_returns_on_no_tracks_exists(self, monitor, log): + log.diffusion = None + monitor.trace_tracks(log) + + @pytest.mark.django_db(transaction=True) + def test_handle_diffusions(self, monitor): + pass + + @pytest.mark.django_db(transaction=True) + def test_log(self, monitor, source): + log = monitor.log("source", type=models.Log.TYPE_START, comment="test") + assert log.source == "source" + assert log.type == models.Log.TYPE_START + assert log.comment == "test" + + @pytest.mark.django_db(transaction=True) + def test_start_diff( + self, monitor, diffusion, source, episode, sound, tracks + ): + result = {} + monitor.log = lambda **kw: result.update(kw) + + monitor.start_diff(source, diffusion) + assert source.calls["push"] == (sound.file.path,) + assert result == { + "type": models.Log.TYPE_START, + "source": source.id, + "diffusion": diffusion, + "comment": str(diffusion), + } + + @pytest.mark.django_db(transaction=True) + def test_cancel_diff(self, monitor, source, diffusion): + result = {} + monitor.log = lambda **kw: result.update(kw) + + monitor.cancel_diff(source, diffusion) + assert diffusion.type == models.Log.TYPE_CANCEL + assert result == { + "type": models.Log.TYPE_CANCEL, + "source": source.id, + "diffusion": diffusion, + "comment": str(diffusion), + } + + @pytest.mark.django_db(transaction=True) + def test_sync(self, monitor): + now = tz.now() + monitor.sync_next = now - tz.timedelta(minutes=1) + monitor.sync() + + assert monitor.sync_next >= now + monitor.sync_timeout + assert all( + source.calls.get("sync") for source in monitor.streamer.playlists + ) + + @pytest.mark.django_db(transaction=True) + def test_sync_timeout_not_reached_skip_sync(self, monitor): + monitor.sync_next = tz.now() + tz.timedelta( + seconds=monitor.sync_timeout.total_seconds() + 20 + ) + monitor.sync() + assert all( + not source.calls.get("sync") + for source in monitor.streamer.playlists + )