add aircox.test utilities

This commit is contained in:
bkfox 2023-06-18 16:07:09 +02:00
parent 95f5dca683
commit a1ca0e70cf
4 changed files with 129 additions and 27 deletions

33
aircox/test.py Normal file
View File

@ -0,0 +1,33 @@
"""This module provide test utilities."""
__all__ = ("interface",)
def interface(obj, funcs):
"""Override provided object's functions using dict of funcs, as
``{func_name: return_value}``.
Attribute ``obj.calls`` is a dict with all call done using those
methods, as ``{func_name: (args, kwargs) | list[(args, kwargs]]}``.
"""
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):
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)

View File

@ -5,14 +5,10 @@
# x when liquidsoap fails to start/exists: exit # x when liquidsoap fails to start/exists: exit
# - handle restart after failure # - handle restart after failure
# - is stream restart after live ok? # - is stream restart after live ok?
import pytz
from django.utils import timezone as tz from django.utils import timezone as tz
from aircox.models import Diffusion, Log, Sound, Track from aircox.models import Diffusion, Log, Sound, Track
# force using UTC
tz.activate(pytz.UTC)
class Monitor: class Monitor:
"""Log and launch diffusions for the given station. """Log and launch diffusions for the given station.
@ -33,9 +29,9 @@ class Monitor:
""" Timedelta: minimal delay between two call of monitor. """ """ Timedelta: minimal delay between two call of monitor. """
logs = None logs = None
"""Queryset to station's logs (ordered by -pk)""" """Queryset to station's logs (ordered by -pk)"""
cancel_timeout = 20 cancel_timeout = tz.timedelta(minutes=20)
"""Timeout in minutes before cancelling a diffusion.""" """Timeout in minutes before cancelling a diffusion."""
sync_timeout = 5 sync_timeout = tz.timedelta(minutes=5)
"""Timeout in minutes between two streamer's sync.""" """Timeout in minutes between two streamer's sync."""
sync_next = None sync_next = None
"""Datetime of the next sync.""" """Datetime of the next sync."""
@ -56,11 +52,10 @@ class Monitor:
"""Log of last triggered item (sound or diffusion).""" """Log of last triggered item (sound or diffusion)."""
return self.logs.start().with_diff().first() return self.logs.start().with_diff().first()
def __init__(self, streamer, delay, cancel_timeout, **kwargs): def __init__(self, streamer, delay, **kwargs):
self.streamer = streamer self.streamer = streamer
# adding time ensure all calculation have a margin # adding time ensures all calculations have a margin
self.delay = delay + tz.timedelta(seconds=5) self.delay = delay + tz.timedelta(seconds=5)
self.cancel_timeout = cancel_timeout
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
self.logs = self.get_logs_queryset() self.logs = self.get_logs_queryset()
self.init_last_sound_logs() self.init_last_sound_logs()
@ -117,18 +112,6 @@ class Monitor:
self.handle_diffusions() self.handle_diffusions()
self.sync() self.sync()
def log(self, source, **kwargs):
"""Create a log using **kwargs, and print info."""
kwargs.setdefault("station", self.station)
kwargs.setdefault("date", tz.now())
log = Log(source=source, **kwargs)
log.save()
log.print()
if log.sound:
self.last_sound_logs[source] = log
return log
def trace_sound(self, source): def trace_sound(self, source):
"""Return on air sound log (create if not present).""" """Return on air sound log (create if not present)."""
air_uri, air_time = source.uri, source.air_time air_uri, air_time = source.uri, source.air_time
@ -246,6 +229,18 @@ class Monitor:
if diff.start < now - self.cancel_timeout: if diff.start < now - self.cancel_timeout:
self.cancel_diff(dealer, diff) self.cancel_diff(dealer, diff)
def log(self, source, **kwargs):
"""Create a log using **kwargs, and print info."""
kwargs.setdefault("station", self.station)
kwargs.setdefault("date", tz.now())
log = Log(source=source, **kwargs)
log.save()
log.print()
if log.sound:
self.last_sound_logs[source] = log
return log
def start_diff(self, source, diff): def start_diff(self, source, diff):
playlist = Sound.objects.episode(id=diff.episode_id).playlist() playlist = Sound.objects.episode(id=diff.episode_id).playlist()
source.push(*playlist) source.push(*playlist)
@ -272,7 +267,7 @@ class Monitor:
if self.sync_next is not None and now < self.sync_next: if self.sync_next is not None and now < self.sync_next:
return return
self.sync_next = now + tz.timedelta(minutes=self.sync_timeout) self.sync_next = now + self.sync_timeout
for source in self.streamer.playlists: for source in self.streamer.playlists:
source.sync() source.sync()

View File

@ -17,6 +17,7 @@ from django.utils import timezone as tz
from aircox.models import Station from aircox.models import Station
from aircox_streamer.controllers import Monitor, Streamer from aircox_streamer.controllers import Monitor, Streamer
# force using UTC # force using UTC
tz.activate(pytz.UTC) tz.activate(pytz.UTC)
@ -68,7 +69,7 @@ class Command(BaseCommand):
"-t", "-t",
"--timeout", "--timeout",
type=float, type=float,
default=Monitor.cancel_timeout, default=Monitor.cancel_timeout.total_seconds() / 60,
help="time to wait in MINUTES before canceling a diffusion that " help="time to wait in MINUTES before canceling a diffusion that "
"should have ran but did not. ", "should have ran but did not. ",
) )
@ -106,7 +107,8 @@ class Command(BaseCommand):
delay = tz.timedelta(milliseconds=delay) delay = tz.timedelta(milliseconds=delay)
timeout = tz.timedelta(minutes=timeout) timeout = tz.timedelta(minutes=timeout)
monitors = [ monitors = [
Monitor(streamer, delay, timeout) for streamer in streamers Monitor(streamer, delay, cancel_timeout=timeout)
for streamer in streamers
] ]
while not run or streamer.is_running: while not run or streamer.is_running:

View File

@ -5,6 +5,7 @@ from datetime import datetime, time
import tzlocal import tzlocal
import pytest import pytest
from model_bakery import baker
from aircox import models from aircox import models
from aircox_streamer import controllers from aircox_streamer import controllers
@ -17,6 +18,33 @@ local_tz = tzlocal.get_localzone()
working_dir = os.path.join(os.path.dirname(__file__), "working_dir") working_dir = os.path.join(os.path.dirname(__file__), "working_dir")
def interface(self, obj, funcs):
"""Override provided object's functions using dict of funcs, as ``{
func_name: return_value}``.
Attribute ``obj.calls`` is a dict
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)
class FakeSocket: class FakeSocket:
FAILING_ADDRESS = -1 FAILING_ADDRESS = -1
"""Connect with this address fails.""" """Connect with this address fails."""
@ -142,6 +170,26 @@ def stream(program):
return stream return stream
@pytest.fixture
def episode(program):
episode = baker.make(models.Episode, title="test episode", program=program)
episode.playlist = lambda: ["/tmp/a", "/tmp/b"]
return episode
@pytest.fixture
def sound(program, episode):
return baker.make(
models.Sound,
program=program,
episode=episode,
name="sound",
type=models.Sound.TYPE_ARCHIVE,
position=0,
file="sound.mp3",
)
@pytest.fixture @pytest.fixture
def sounds(program): def sounds(program):
items = [ items = [
@ -219,6 +267,9 @@ def metadata_string(metadata_data):
class FakeStreamer(controllers.Streamer): class FakeStreamer(controllers.Streamer):
calls = {} calls = {}
def __init__(self, **kwargs):
self.__dict__.update(**kwargs)
def fetch(self): def fetch(self):
self.calls["fetch"] = True self.calls["fetch"] = True
@ -236,7 +287,7 @@ class FakeSource(controllers.Source):
def sync(self): def sync(self):
self.calls["sync"] = True self.calls["sync"] = True
def push(self, path): def push(self, *path):
self.calls["push"] = path self.calls["push"] = path
return path return path
@ -250,6 +301,24 @@ class FakeSource(controllers.Source):
self.calls["seek"] = c self.calls["seek"] = c
class FakePlaylist(FakeSource, controllers.PlaylistSource):
pass
class FakeQueueSource(FakeSource, controllers.QueueSource):
pass
@pytest.fixture
def streamer(station, station_ports):
streamer = FakeStreamer(station)
streamer.sources = [
FakePlaylist(i, uri=f"source-{i}") for i in range(0, 3)
]
streamer.sources.append(FakeQueueSource(len(streamer.sources)))
return streamer
@pytest.fixture @pytest.fixture
def streamers(stations, stations_ports): def streamers(stations, stations_ports):
streamers = controllers.Streamers(streamer_class=FakeStreamer) streamers = controllers.Streamers(streamer_class=FakeStreamer)
@ -257,6 +326,9 @@ def streamers(stations, stations_ports):
streamers.streamers = { streamers.streamers = {
station.pk: FakeStreamer(station) for station in stations station.pk: FakeStreamer(station) for station in stations
} }
for streamer in streamers.values(): for j, streamer in enumerate(streamers.values()):
streamer.sources = [FakeSource(i) for i in range(0, 3)] streamer.sources = [
FakePlaylist(i, uri=f"source-{j}-{i}") for i in range(0, 3)
]
streamer.sources.append(FakeQueueSource(len(streamer.sources)))
return streamers return streamers