write sound monitor tests

This commit is contained in:
bkfox 2023-06-21 22:33:37 +02:00
parent d15ca98447
commit 93e57d746c
3 changed files with 238 additions and 73 deletions

View File

@ -24,7 +24,7 @@ parameters given by the setting SOUND_QUALITY. This script requires
Sox (and soxi).
"""
import atexit
import concurrent.futures as futures
from concurrent import futures
import logging
import time
import os
@ -142,9 +142,9 @@ class MonitorHandler(PatternMatchingEventHandler):
"""
pool = None
jobs = {}
jobs = None
def __init__(self, subdir, pool, **sync_kw):
def __init__(self, subdir, pool, jobs=None, **sync_kw):
"""
:param str subdir: sub-directory in program dirs to monitor \
(SOUND_ARCHIVES_SUBDIR or SOUND_EXCERPTS_SUBDIR);
@ -154,6 +154,7 @@ class MonitorHandler(PatternMatchingEventHandler):
"""
self.subdir = subdir
self.pool = pool
self.jobs = jobs or {}
self.sync_kw = sync_kw
patterns = [
@ -199,29 +200,27 @@ class MonitorHandler(PatternMatchingEventHandler):
class SoundMonitor:
"""Monitor for filesystem changes in order to synchronise database and
analyse files."""
analyse files of a provided program."""
def report(self, program=None, component=None, logger=logging, *content):
if not component:
logger.info(
"%s: %s", str(program), " ".join([str(c) for c in content])
)
else:
logger.info(
"%s, %s: %s",
str(program),
str(component),
" ".join([str(c) for c in content]),
)
def report(self, program=None, component=None, *content, logger=logging):
content = " ".join([str(c) for c in content])
logger.info(
f"{program}: {content}"
if not component
else f"{program}, {component}: {content}"
)
def scan(self, logger=logging):
"""For all programs, scan dirs."""
"""For all programs, scan dirs.
Return scanned directories.
"""
logger.info("scan all programs...")
programs = Program.objects.filter()
dirs = []
for program in programs:
logger.info("#%d %s", program.id, program.title)
logger.info(f"#{program.id} {program.title}")
self.scan_for_program(
program,
settings.SOUND_ARCHIVES_SUBDIR,
@ -234,7 +233,7 @@ class SoundMonitor:
logger=logger,
type=Sound.TYPE_EXCERPT,
)
dirs.append(os.path.join(program.abspath))
dirs.append(program.abspath)
return dirs
def scan_for_program(
@ -272,7 +271,12 @@ class SoundMonitor:
if sound.check_on_file():
SoundFile(sound.file.path).sync(sound=sound, **sync_kwargs)
_running = False
def monitor(self, logger=logging):
if self._running:
raise RuntimeError("already running")
"""Run in monitor mode."""
with futures.ThreadPoolExecutor() as pool:
archives_handler = MonitorHandler(
@ -307,5 +311,13 @@ class SoundMonitor:
atexit.register(leave)
while True:
self._running = True
while self._running:
time.sleep(1)
leave()
atexit.unregister(leave)
def stop(self):
"""Stop monitor() loop."""
self._running = False

View File

@ -44,38 +44,64 @@ InterfaceTarget = namedtuple(
class WrapperMixin:
def __init__(self, target=None, ns=None, ns_attr=None, **kwargs):
type_interface = None
"""For instance of class wrapped by an Interface, this is the wrapping
interface of the class."""
instances = None
ns = None
ns_attr = None
def __init__(
self, target=None, ns=None, ns_attr=None, type_interface=None, **kwargs
):
self.target = target
if ns:
self.inject(ns, ns_attr)
if self.type_interface:
self._set_type_interface(type_interface)
super().__init__(**kwargs)
def _set_type_interface(self, type_interface):
if self.type_interface:
raise RuntimeError("a type interface is already assigned")
self.type_interface = type_interface
if not type_interface.instances:
type_interface.instances = [self]
else:
type_interface.instances.append(self)
@property
def ns_target(self):
"""Actual namespace's target (using ns.ns_attr)"""
if self.ns and self.ns_attr:
return getattr(self.ns, self.ns_attr, None)
return None
def inject(self, ns=None, ns_attr=None):
if ns and ns_attr:
ns_target = getattr(ns, ns_attr, None)
if self.target is ns_target:
return
elif self.target is not None:
raise RuntimeError(
"self target already injected. It must be "
"`release` before `inject`."
)
self.target = ns_target
setattr(ns, ns_attr, self.parent)
elif not ns or not ns_attr:
"""Inject interface into namespace at given key."""
if not (ns and ns_attr):
raise ValueError("ns and ns_attr must be provided together")
ns_target = getattr(ns, ns_attr, None)
if self.target is ns_target:
return
elif self.target is not None:
raise RuntimeError(
"self target already injected. It must be "
"`release` before `inject`."
)
self.target = ns_target
setattr(ns, ns_attr, self.interface)
self.ns = ns
self.ns_attr = ns_attr
def release(self):
if self.ns_target is self:
setattr(self.target.namespace, self.target.name, self.target)
"""Remove injection from previously injected parent, reset target."""
if self.ns_target is self.interface:
setattr(self.ns, self.ns_attr, self.target)
self.target = None
@ -83,7 +109,9 @@ class SpoofMixin:
traces = None
def __init__(self, funcs=None, **kwargs):
self.reset(funcs or {})
self.reset(
funcs or {},
)
super().__init__(**kwargs)
def reset(self, funcs=None):
@ -152,23 +180,19 @@ class SpoofMixin:
"""
func = self.funcs[name]
if callable(func):
return func(*a, **kw)
return func(self, *a, **kw)
return func
class InterfaceMeta(SpoofMixin, WrapperMixin):
calls = None
"""Calls done."""
def __init__(self, parent, **kwargs):
self.parent = parent
super().__init__(**kwargs)
def __getitem__(self, name):
return self.traces[name]
class Interface:
class IMeta(SpoofMixin, WrapperMixin):
def __init__(self, interface, **kwargs):
self.interface = interface
super().__init__(**kwargs)
def __getitem__(self, name):
return self.traces[name]
_imeta = None
"""This contains a InterfaceMeta instance related to Interface one.
@ -182,7 +206,7 @@ class Interface:
_imeta_kw.setdefault("funcs", _funcs)
if _target is not None:
_imeta_kw.setdefault("target", _target)
self._imeta = InterfaceMeta(self, **_imeta_kw)
self._imeta = self.IMeta(self, **_imeta_kw)
self.__dict__.update(kwargs)
@property
@ -195,19 +219,29 @@ class Interface:
return cls(**kwargs)
def _irelease(self):
"""Shortcut to `self._imeta.release`."""
self._imeta.release()
def _trace(self, *args, **kw):
"""Shortcut to `self._imeta.get_trace`."""
return self._imeta.get_trace(*args, **kw)
def _traces(self, *args, **kw):
"""Shortcut to `self._imeta.get_traces`."""
return self._imeta.get_traces(*args, **kw)
def __call__(self, *args, **kwargs):
target = self._imeta.target
print("is it class?", self, target, inspect.isclass(target))
if inspect.isclass(target):
target = target(*args, **kwargs)
return type(self)(target, _imeta_kw={"funcs": self._imeta.funcs})
return type(self)(
target,
_imeta_kw={"type_interface": self, "funcs": self._imeta.funcs},
)
if "__call__" in self._imeta.funcs:
return self._imeta.call("__call__", args, kwargs)
self._imeta.add("__call__", args, kwargs)
return self._imeta.target(*args, **kwargs)
@ -216,3 +250,7 @@ class Interface:
if attr in self._imeta.funcs:
return lambda *args, **kwargs: self._imeta.call(attr, args, kwargs)
return getattr(self._imeta.target, attr)
def __str__(self):
iface = super().__str__()
return f"{iface}::{self._imeta.target}"

View File

@ -1,8 +1,10 @@
from concurrent import futures
import logging
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
@ -25,8 +27,8 @@ def logger():
@pytest.fixture
def interfaces(logger):
return {
def interfaces():
items = {
"SoundFile": Interface.inject(
sound_monitor,
"SoundFile",
@ -41,8 +43,11 @@ def interfaces(logger):
"sleep": None,
},
),
"datetime": Interface.inject(sound_monitor, "datetime", {now: now}),
"datetime": Interface.inject(sound_monitor, "datetime", {"now": now}),
}
yield items
for item in items.values():
item._irelease()
@pytest.fixture
@ -65,6 +70,23 @@ 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
@ -111,7 +133,7 @@ class TestModifiedTask:
dt_now = now + modified_task.timeout_delta - tz.timedelta(hours=10)
datetime = Interface.inject(sound_monitor, "datetime", {"now": dt_now})
def sleep(n):
def sleep(imeta, n):
datetime._imeta.funcs[
"now"
] = modified_task.timestamp + tz.timedelta(hours=10)
@ -129,31 +151,124 @@ class TestModifiedTask:
class TestMonitorHandler:
def test_monitor___init__(self):
pass
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_created(self):
pass
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_deleted(self):
pass
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_moved(self):
pass
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_on_modified(self):
pass
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},
)
def test__submit(self):
pass
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):
pass
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):
pass
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)
def test_monitor(self):
pass
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