import itertools import os from datetime import datetime, time import tzlocal import pytest from model_bakery import baker from aircox import models from aircox_streamer import controllers from aircox_streamer.connector import Connector local_tz = tzlocal.get_localzone() working_dir = os.path.join(os.path.dirname(__file__), "working_dir") class FakeSocket: FAILING_ADDRESS = -1 """Connect with this address fails.""" family, type, address = None, None, None sent_data = None """List of data that have been `send[all]`""" recv_data = None """Response data to return on recv.""" def __init__(self, family, type): self.family = family self.type = type self.sent_data = [] self.recv_data = "" def connect(self, address): if address == self.FAILING_ADDRESS: raise RuntimeError("invalid connection") self.address = address def close(self): pass def sendall(self, data): self.sent_data.append(data.decode()) def recv(self, count): if isinstance(self.recv_data, list): if len(self.recv_data): data, self.recv_data = self.recv_data[0], self.recv_data[1:] else: data = "" else: data = self.recv_data self.recv_data = self.recv_data[count:] data = data[:count] return (data.encode("utf-8") if isinstance(data, str) else data) or b"\nEND" def is_sent(self, data): """Return True if provided data have been sent.""" # use [:-1] because connector add "\n" at sent data return any(r for r in self.sent_data if r == data or r[:-1] == data) # -- models @pytest.fixture def station(): station = models.Station(name="test", path=working_dir, default=True, active=True) station.save() return station @pytest.fixture def stations(station): objs = [ models.Station( name=f"test-{i}", slug=f"test-{i}", path=working_dir, default=(i == 0), active=True, ) for i in range(0, 3) ] models.Station.objects.bulk_create(objs) return [station] + objs @pytest.fixture def station_ports(station): return _stations_ports(station) @pytest.fixture def stations_ports(stations): return _stations_ports(*stations) def _stations_ports(*stations): items = list( itertools.chain( *[ ( models.Port( station=station, direction=models.Port.DIRECTION_INPUT, type=models.Port.TYPE_HTTP, active=True, ), models.Port( station=station, direction=models.Port.DIRECTION_OUTPUT, type=models.Port.TYPE_FILE, active=True, ), ) for station in stations ] ) ) models.Port.objects.bulk_create(items) return items @pytest.fixture def program(station): program = models.Program(title="test", station=station) program.save() return program @pytest.fixture def stream(program): stream = models.Stream(program=program, begin=time(10, 12), end=time(12, 13)) stream.save() return stream @pytest.fixture def episode(program): return baker.make(models.Episode, title="test episode", program=program) @pytest.fixture def sound(program, episode): sound = models.Sound( program=program, episode=episode, name="sound", type=models.Sound.TYPE_ARCHIVE, position=0, file="sound.mp3", ) sound.save(check=False) return sound @pytest.fixture def sounds(program): items = [ models.Sound( name=f"sound {i}", program=program, type=models.Sound.TYPE_ARCHIVE, position=i, file=f"sound-{i}.mp3", ) for i in range(0, 3) ] models.Sound.objects.bulk_create(items) return items # -- connectors @pytest.fixture def connector(): obj = Connector(os.path.join(working_dir, "test.sock")) obj.socket_class = FakeSocket yield obj obj.close() @pytest.fixture def fail_connector(): obj = Connector(FakeSocket.FAILING_ADDRESS) obj.socket_class = FakeSocket yield obj obj.close() @pytest.fixture def controller(station, connector): connector.open() return controllers.Streamer(station, connector) @pytest.fixture def socket(controller): return controller.connector.socket # -- metadata @pytest.fixture def metadata(controller): return controllers.Metadata(controller, 1) @pytest.fixture def metadata_data_air_time(): return local_tz.localize(datetime(2023, 5, 1, 12, 10, 5)) @pytest.fixture def metadata_data(metadata_data_air_time): return { "rid": 1, "initial_uri": "request_uri", "on_air": metadata_data_air_time.strftime("%Y/%m/%d %H:%M:%S"), "status": "playing", } @pytest.fixture def metadata_string(metadata_data): return "\n".join(f"{key}={value}" for key, value in metadata_data.items()) + "\nEND" # -- streamers class FakeStreamer(controllers.Streamer): calls = {} is_ready = False def __init__(self, **kwargs): self.__dict__.update(**kwargs) def fetch(self): self.calls["fetch"] = True class FakeSource(controllers.Source): def __init__(self, id, *args, **kwargs): self.id = id self.args = args self.kwargs = kwargs self.calls = {} def fetch(self): self.calls["sync"] = True def sync(self): self.calls["sync"] = True def push(self, *path): self.calls["push"] = path return path def skip(self): self.calls["skip"] = True def restart(self): self.calls["restart"] = True def seek(self, 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=station) streamer.sources = [FakePlaylist(i, uri=f"source-{i}") for i in range(0, 3)] streamer.dealer = FakeQueueSource(len(streamer.sources)) streamer.sources.append(streamer.dealer) return streamer @pytest.fixture def streamers(stations, stations_ports): streamers = controllers.Streamers(streamer_class=FakeStreamer) # avoid unecessary db calls streamers.streamers = {station.pk: FakeStreamer(station=station) for station in stations} for j, streamer in enumerate(streamers.values()): streamer.sources = [FakePlaylist(i, uri=f"source-{j}-{i}") for i in range(0, 3)] streamer.sources.append(FakeQueueSource(len(streamer.sources))) return streamers