#106: tests: aircox_streamer #110
							
								
								
									
										33
									
								
								aircox/test.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								aircox/test.py
									
									
									
									
									
										Normal 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)
 | 
				
			||||||
@ -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()
 | 
				
			||||||
 | 
				
			|||||||
@ -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:
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user