!111: tests: aircox.management (#114)

!111

Co-authored-by: bkfox <thomas bkfox net>
Reviewed-on: rc/aircox#114
This commit is contained in:
Thomas Kairos
2023-06-30 16:39:55 +02:00
parent faecdf5495
commit f9ad81ddac
27 changed files with 1534 additions and 625 deletions

View File

@ -0,0 +1,3 @@
Artist 1;Title 1;1;0;tag1,tag12;info1
Artist 2;Title 2;2;1;tag2,tag12;info2
Artist 3;Title 3;3;2;;
1 Artist 1 Title 1 1 0 tag1,tag12 info1
2 Artist 2 Title 2 2 1 tag2,tag12 info2
3 Artist 3 Title 3 3 2

View File

@ -0,0 +1,110 @@
from django.utils import timezone as tz
import pytest
from model_bakery import baker
from aircox import models
from aircox.test import Interface, File
from aircox.controllers import log_archiver
@pytest.fixture
def diffusions(episodes):
items = [
baker.prepare(
models.Diffusion,
program=episode.program,
episode=episode,
type=models.Diffusion.TYPE_ON_AIR,
)
for episode in episodes
]
models.Diffusion.objects.bulk_create(items)
return items
@pytest.fixture
def logs(diffusions, sound, tracks):
now = tz.now()
station = diffusions[0].program.station
items = [
models.Log(
station=diffusion.program.station,
type=models.Log.TYPE_START,
date=now + tz.timedelta(hours=-10, minutes=i),
source="13",
diffusion=diffusion,
)
for i, diffusion in enumerate(diffusions)
]
items += [
models.Log(
station=station,
type=models.Log.TYPE_ON_AIR,
date=now + tz.timedelta(hours=-9, minutes=i),
source="14",
track=track,
sound=track.sound,
)
for i, track in enumerate(tracks)
]
models.Log.objects.bulk_create(items)
return items
@pytest.fixture
def logs_qs(logs):
return models.Log.objects.filter(pk__in=(r.pk for r in logs))
@pytest.fixture
def file():
return File(data=b"")
@pytest.fixture
def gzip(file):
gzip = Interface.inject(log_archiver, "gzip", {"open": file})
yield gzip
gzip._irelease()
@pytest.fixture
def archiver():
return log_archiver.LogArchiver()
class TestLogArchiver:
@pytest.mark.django_db
def test_archive_then_load_file(self, archiver, file, gzip, logs, logs_qs):
# before logs are deleted from db, get data
sorted = archiver.sort_logs(logs_qs)
paths = {
archiver.get_path(station, date) for station, date in sorted.keys()
}
count = archiver.archive(logs_qs, keep=False)
assert count == len(logs)
assert not logs_qs.count()
assert all(
path in paths for path, *_ in gzip._traces("open", args=True)
)
results = archiver.load_file("dummy path")
assert results
@pytest.mark.django_db
def test_archive_no_qs(self, archiver):
count = archiver.archive(models.Log.objects.none())
assert not count
@pytest.mark.django_db
def test_sort_log(self, archiver, logs_qs):
sorted = archiver.sort_logs(logs_qs)
assert sorted
for (station, date), logs in sorted.items():
assert all(
log.station == station and log.date.date() == date
for log in logs
)

View File

@ -0,0 +1,64 @@
import os
import pytest
from aircox.test import Interface
from aircox.controllers import playlist_import
csv_data = [
{
"artist": "Artist 1",
"title": "Title 1",
"minutes": "1",
"seconds": "0",
"tags": "tag1,tag12",
"info": "info1",
},
{
"artist": "Artist 2",
"title": "Title 2",
"minutes": "2",
"seconds": "1",
"tags": "tag2,tag12",
"info": "info2",
},
{
"artist": "Artist 3",
"title": "Title 3",
"minutes": "3",
"seconds": "2",
"tags": "",
"info": "",
},
]
@pytest.fixture
def importer(sound):
path = os.path.join(os.path.dirname(__file__), "playlist.csv")
return playlist_import.PlaylistImport(path, sound=sound)
class TestPlaylistImport:
@pytest.mark.django_db
def test_run(self, importer):
iface = Interface(None, {"read": None, "make_playlist": None})
importer.read = iface.read
importer.make_playlist = iface.make_playlist
importer.run()
assert iface._trace("read")
assert iface._trace("make_playlist")
@pytest.mark.django_db
def test_read(self, importer):
importer.read()
assert importer.data == csv_data
@pytest.mark.django_db
def test_make_playlist(self, importer, sound):
importer.data = csv_data
importer.make_playlist()
track_artists = sound.track_set.all().values_list("artist", flat=True)
csv_artists = {r["artist"] for r in csv_data}
assert set(track_artists) == csv_artists
# TODO: check other values

View File

@ -0,0 +1,111 @@
import pytest
from datetime import timedelta
from django.conf import settings as conf
from django.utils import timezone as tz
from aircox import models
from aircox.controllers.sound_file import SoundFile
@pytest.fixture
def path_infos():
return {
"test/20220101_10h13_1_sample_1.mp3": {
"year": 2022,
"month": 1,
"day": 1,
"hour": 10,
"minute": 13,
"n": 1,
"name": "Sample 1",
},
"test/20220102_10h13_sample_2.mp3": {
"year": 2022,
"month": 1,
"day": 2,
"hour": 10,
"minute": 13,
"name": "Sample 2",
},
"test/20220103_1_sample_3.mp3": {
"year": 2022,
"month": 1,
"day": 3,
"n": 1,
"name": "Sample 3",
},
"test/20220104_sample_4.mp3": {
"year": 2022,
"month": 1,
"day": 4,
"name": "Sample 4",
},
"test/20220105.mp3": {
"year": 2022,
"month": 1,
"day": 5,
"name": "20220105",
},
}
@pytest.fixture
def sound_files(path_infos):
return {
k: r
for k, r in (
(path, SoundFile(conf.MEDIA_ROOT + "/" + path))
for path in path_infos.keys()
)
}
def test_sound_path(sound_files):
for path, sound_file in sound_files.items():
assert path == sound_file.sound_path
def test_read_path(path_infos, sound_files):
for path, sound_file in sound_files.items():
expected = path_infos[path]
result = sound_file.read_path(path)
# remove None values
result = {k: v for k, v in result.items() if v is not None}
assert expected == result, "path: {}".format(path)
def _setup_diff(program, info):
episode = models.Episode(program=program, title="test-episode")
at = tz.datetime(
**{
k: info[k]
for k in ("year", "month", "day", "hour", "minute")
if info.get(k)
}
)
at = tz.make_aware(at)
diff = models.Diffusion(
episode=episode, start=at, end=at + timedelta(hours=1)
)
episode.save()
diff.save()
return diff
@pytest.mark.django_db(transaction=True)
def test_find_episode(sound_files):
station = models.Station(name="test-station")
program = models.Program(station=station, title="test")
station.save()
program.save()
for path, sound_file in sound_files.items():
infos = sound_file.read_path(path)
diff = _setup_diff(program, infos)
sound = models.Sound(program=diff.program, file=path)
result = sound_file.find_episode(sound, infos)
assert diff.episode == result
# TODO: find_playlist, sync

View File

@ -0,0 +1,265 @@
from concurrent import futures
import pytest
from django.utils import timezone as tz
from aircox.conf import settings
from aircox.models import Sound
from aircox.controllers import sound_monitor
from aircox.test import Interface, interface
now = tz.datetime.now()
@pytest.fixture
def event():
return Interface(src_path="/tmp/src_path", dest_path="/tmp/dest_path")
@pytest.fixture
def interfaces():
items = {
"SoundFile": Interface.inject(
sound_monitor,
"SoundFile",
{
"sync": None,
},
),
"time": Interface.inject(
sound_monitor,
"time",
{
"sleep": None,
},
),
"datetime": Interface.inject(sound_monitor, "datetime", {"now": now}),
}
yield items
for item in items.values():
item._irelease()
@pytest.fixture
def task(interfaces):
return sound_monitor.Task()
@pytest.fixture
def delete_task(interfaces):
return sound_monitor.DeleteTask()
@pytest.fixture
def move_task(interfaces):
return sound_monitor.MoveTask()
@pytest.fixture
def modified_task(interfaces):
return sound_monitor.ModifiedTask()
@pytest.fixture
def monitor_handler(interfaces):
pool = Interface(
None,
{
"submit": lambda imeta, *a, **kw: Interface(
None,
{
"add_done_callback": None,
"done": False,
},
)
},
)
return sound_monitor.MonitorHandler("/tmp", pool=pool, sync_kw=13)
class TestTask:
def test___init__(self, task):
assert task.timestamp is not None
def test_ping(self, task):
task.timestamp = None
task.ping()
assert task.timestamp >= now
@pytest.mark.django_db
def test___call__(self, logger, task, event):
task.log_msg = "--{event.src_path}--"
sound_file = task(event, logger=logger, kw=13)
assert sound_file._trace("sync", kw=True) == {"kw": 13}
assert logger._trace("info", args=True) == (
task.log_msg.format(event=event),
)
class TestDeleteTask:
@pytest.mark.django_db
def test___call__(self, delete_task, logger, task, event):
sound_file = delete_task(event, logger=logger)
assert sound_file._trace("sync", kw=True) == {"deleted": True}
class TestMoveTask:
@pytest.mark.django_db
def test__call___with_sound(self, move_task, sound, event, logger):
event.src_path = sound.file.name
sound_file = move_task(event, logger=logger)
assert isinstance(sound_file._trace("sync", kw="sound"), Sound)
assert sound_file.path == sound.file.name
@pytest.mark.django_db
def test__call___no_sound(self, move_task, event, logger):
sound_file = move_task(event, logger=logger)
assert sound_file._trace("sync", kw=True) == {}
assert sound_file.path == event.dest_path
class TestModifiedTask:
def test_wait(self, modified_task):
dt_now = now + modified_task.timeout_delta - tz.timedelta(hours=10)
datetime = Interface.inject(sound_monitor, "datetime", {"now": dt_now})
def sleep(imeta, n):
datetime._imeta.funcs[
"now"
] = modified_task.timestamp + tz.timedelta(hours=10)
time = Interface.inject(sound_monitor, "time", {"sleep": sleep})
modified_task.wait()
assert time._trace("sleep", args=True)
datetime._imeta.release()
def test__call__(self, modified_task, event):
interface(modified_task, {"wait": None})
modified_task(event)
assert modified_task.calls["wait"]
class TestMonitorHandler:
def test_on_created(self, monitor_handler, event):
monitor_handler._submit = monitor_handler.pool.submit
monitor_handler.on_created(event)
trace_args, trace_kwargs = monitor_handler.pool._trace("submit")
assert isinstance(trace_args[0], sound_monitor.CreateTask)
assert trace_args[1:] == (event, "new")
assert trace_kwargs == monitor_handler.sync_kw
def test_on_deleted(self, monitor_handler, event):
monitor_handler._submit = monitor_handler.pool.submit
monitor_handler.on_deleted(event)
trace_args, _ = monitor_handler.pool._trace("submit")
assert isinstance(trace_args[0], sound_monitor.DeleteTask)
assert trace_args[1:] == (event, "del")
def test_on_moved(self, monitor_handler, event):
monitor_handler._submit = monitor_handler.pool.submit
monitor_handler.on_moved(event)
trace_args, trace_kwargs = monitor_handler.pool._trace("submit")
assert isinstance(trace_args[0], sound_monitor.MoveTask)
assert trace_args[1:] == (event, "mv")
assert trace_kwargs == monitor_handler.sync_kw
def test_on_modified(self, monitor_handler, event):
monitor_handler._submit = monitor_handler.pool.submit
monitor_handler.on_modified(event)
trace_args, trace_kwargs = monitor_handler.pool._trace("submit")
assert isinstance(trace_args[0], sound_monitor.ModifiedTask)
assert trace_args[1:] == (event, "up")
assert trace_kwargs == monitor_handler.sync_kw
def test__submit(self, monitor_handler, event):
handler = Interface()
handler, created = monitor_handler._submit(
handler, event, "prefix", kw=13
)
assert created
assert handler.future._trace("add_done_callback")
assert monitor_handler.pool._trace("submit") == (
(handler, event),
{"kw": 13},
)
key = f"prefix:{event.src_path}"
assert monitor_handler.jobs.get(key) == handler
@pytest.fixture
def monitor_interfaces():
items = {
"atexit": Interface.inject(
sound_monitor, "atexit", {"register": None, "leave": None}
),
"observer": Interface.inject(
sound_monitor,
"Observer",
{
"schedule": None,
"start": None,
},
),
}
yield items
for item in items.values():
item.release()
@pytest.fixture
def monitor():
yield sound_monitor.SoundMonitor()
class SoundMonitor:
def test_report(self, monitor, program, logger):
monitor.report(program, "component", "content", logger=logger)
msg = f"{program}, component: content"
assert logger._trace("info", args=True) == (msg,)
def test_scan(self, monitor, program, logger):
interface = Interface(None, {"scan_for_program": None})
monitor.scan_for_program = interface.scan_for_program
dirs = monitor.scan(logger)
assert logger._traces("info") == (
"scan all programs...",
f"#{program.id} {program.title}",
)
assert dirs == [program.abspath]
assert interface._traces("scan_for_program") == (
((program, settings.SOUND_ARCHIVES_SUBDIR), {"logger": logger})(
(program, settings.SOUND_EXCERPTS_SUBDIR), {"logger": logger}
)
)
def test_monitor(self, monitor, monitor_interfaces, logger):
def sleep(*args, **kwargs):
monitor.stop()
time = Interface.inject(sound_monitor, "time", {"sleep": sleep})
monitor.monitor(logger=logger)
time._irelease()
observers = monitor_interfaces["observer"].instances
observer = observers and observers[0]
assert observer
schedules = observer._traces("schedule")
for (handler, *_), kwargs in schedules:
assert isinstance(handler, sound_monitor.MonitorHandler)
assert isinstance(handler.pool, futures.ThreadPoolExecutor)
assert (handler.subdir, handler.type) in (
(settings.SOUND_ARCHIVES_SUBDIR, Sound.TYPE_ARCHIVE),
(settings.SOUND_EXCERPTS_SUBDIR, Sound.TYPE_EXCERPT),
)
assert observer._trace("start")
atexit = monitor_interfaces["atexit"]
assert atexit._trace("register")
assert atexit._trace("unregister")
assert observers

View File

@ -0,0 +1,123 @@
import subprocess
import pytest
from aircox.test import Interface
from aircox.controllers import sound_stats
sox_output = """
DC offset 0.000000\n
Min level 0.000000\n
Max level 0.000000\n
Pk lev dB -inf\n
RMS lev dB -inf\n
RMS Pk dB -inf\n
RMS Tr dB -inf\n
Crest factor 1.00\n
Flat factor 179.37\n
Pk count 1.86G\n
Bit-depth 0/0\n
Num samples 930M\n
Length s 19383.312\n
Scale max 1.000000\n
Window s 0.050\n
"""
sox_values = {
"DC offset": 0.0,
"Min level": 0.0,
"Max level": 0.0,
"Pk lev dB": float("-inf"),
"RMS lev dB": float("-inf"),
"RMS Pk dB": float("-inf"),
"RMS Tr dB": float("-inf"),
"Flat factor": 179.37,
"length": 19383.312,
}
@pytest.fixture
def sox_interfaces():
process = Interface(
None, {"communicate": ("", sox_output.encode("utf-8"))}
)
subprocess = Interface.inject(
sound_stats, "subprocess", {"Popen": lambda *_, **__: process}
)
yield {"process": process, "subprocess": subprocess}
subprocess._irelease()
@pytest.fixture
def sox_stats(sox_interfaces):
return sound_stats.SoxStats()
@pytest.fixture
def stats():
return sound_stats.SoundStats("/tmp/audio.wav", sample_length=10)
@pytest.fixture
def stats_interfaces(stats):
def iw(path, **kw):
kw["path"] = path
kw.setdefault("length", stats.sample_length * 2)
return kw
SxS = sound_stats.SoxStats
sound_stats.SoxStats = iw
yield iw
sound_stats.SoxStats = SxS
class TestSoxStats:
def test_parse(self, sox_stats):
values = sox_stats.parse(sox_output)
assert values == sox_values
def test_analyse(self, sox_stats, sox_interfaces):
sox_stats.analyse("fake_path", 1, 2)
assert sox_interfaces["subprocess"]._trace("Popen") == (
(["sox", "fake_path", "-n", "trim", "1", "2", "stats"],),
{"stdout": subprocess.PIPE, "stderr": subprocess.PIPE},
)
assert sox_stats.values == sox_values
class TestSoundStats:
def test_get_file_stats(self, stats):
file_stats = {"a": 134}
stats.stats = [file_stats]
assert stats.get_file_stats() is file_stats
def test_get_file_stats_none(self, stats):
stats.stats = []
assert stats.get_file_stats() is None
def test_analyse(self, stats, stats_interfaces):
stats.analyse()
assert stats.stats == [
{"path": stats.path, "length": stats.sample_length * 2},
{"path": stats.path, "at": 0, "length": stats.sample_length},
{"path": stats.path, "at": 10, "length": stats.sample_length},
]
def test_analyse_no_sample_length(self, stats, stats_interfaces):
stats.sample_length = 0
stats.analyse()
assert stats.stats == [{"length": 0, "path": stats.path}]
def test_check(self, stats):
good = [{"val": i} for i in range(0, 11)]
bad = [{"val": i} for i in range(-10, 0)] + [
{"val": i} for i in range(11, 20)
]
stats.stats = good + bad
calls = {}
stats.resume = lambda *_: calls.setdefault("resume", True)
stats.check("val", 0, 10)
assert calls == {"resume": True}
assert all(i < len(good) for i in stats.good)
assert all(i >= len(good) for i in stats.bad)