write sound monitor tests
This commit is contained in:
parent
d15ca98447
commit
93e57d746c
|
@ -24,7 +24,7 @@ parameters given by the setting SOUND_QUALITY. This script requires
|
||||||
Sox (and soxi).
|
Sox (and soxi).
|
||||||
"""
|
"""
|
||||||
import atexit
|
import atexit
|
||||||
import concurrent.futures as futures
|
from concurrent import futures
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
|
@ -142,9 +142,9 @@ class MonitorHandler(PatternMatchingEventHandler):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pool = None
|
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 \
|
:param str subdir: sub-directory in program dirs to monitor \
|
||||||
(SOUND_ARCHIVES_SUBDIR or SOUND_EXCERPTS_SUBDIR);
|
(SOUND_ARCHIVES_SUBDIR or SOUND_EXCERPTS_SUBDIR);
|
||||||
|
@ -154,6 +154,7 @@ class MonitorHandler(PatternMatchingEventHandler):
|
||||||
"""
|
"""
|
||||||
self.subdir = subdir
|
self.subdir = subdir
|
||||||
self.pool = pool
|
self.pool = pool
|
||||||
|
self.jobs = jobs or {}
|
||||||
self.sync_kw = sync_kw
|
self.sync_kw = sync_kw
|
||||||
|
|
||||||
patterns = [
|
patterns = [
|
||||||
|
@ -199,29 +200,27 @@ class MonitorHandler(PatternMatchingEventHandler):
|
||||||
|
|
||||||
class SoundMonitor:
|
class SoundMonitor:
|
||||||
"""Monitor for filesystem changes in order to synchronise database and
|
"""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):
|
def report(self, program=None, component=None, *content, logger=logging):
|
||||||
if not component:
|
content = " ".join([str(c) for c in content])
|
||||||
logger.info(
|
logger.info(
|
||||||
"%s: %s", str(program), " ".join([str(c) for c in content])
|
f"{program}: {content}"
|
||||||
)
|
if not component
|
||||||
else:
|
else f"{program}, {component}: {content}"
|
||||||
logger.info(
|
)
|
||||||
"%s, %s: %s",
|
|
||||||
str(program),
|
|
||||||
str(component),
|
|
||||||
" ".join([str(c) for c in content]),
|
|
||||||
)
|
|
||||||
|
|
||||||
def scan(self, logger=logging):
|
def scan(self, logger=logging):
|
||||||
"""For all programs, scan dirs."""
|
"""For all programs, scan dirs.
|
||||||
|
|
||||||
|
Return scanned directories.
|
||||||
|
"""
|
||||||
logger.info("scan all programs...")
|
logger.info("scan all programs...")
|
||||||
programs = Program.objects.filter()
|
programs = Program.objects.filter()
|
||||||
|
|
||||||
dirs = []
|
dirs = []
|
||||||
for program in programs:
|
for program in programs:
|
||||||
logger.info("#%d %s", program.id, program.title)
|
logger.info(f"#{program.id} {program.title}")
|
||||||
self.scan_for_program(
|
self.scan_for_program(
|
||||||
program,
|
program,
|
||||||
settings.SOUND_ARCHIVES_SUBDIR,
|
settings.SOUND_ARCHIVES_SUBDIR,
|
||||||
|
@ -234,7 +233,7 @@ class SoundMonitor:
|
||||||
logger=logger,
|
logger=logger,
|
||||||
type=Sound.TYPE_EXCERPT,
|
type=Sound.TYPE_EXCERPT,
|
||||||
)
|
)
|
||||||
dirs.append(os.path.join(program.abspath))
|
dirs.append(program.abspath)
|
||||||
return dirs
|
return dirs
|
||||||
|
|
||||||
def scan_for_program(
|
def scan_for_program(
|
||||||
|
@ -272,7 +271,12 @@ class SoundMonitor:
|
||||||
if sound.check_on_file():
|
if sound.check_on_file():
|
||||||
SoundFile(sound.file.path).sync(sound=sound, **sync_kwargs)
|
SoundFile(sound.file.path).sync(sound=sound, **sync_kwargs)
|
||||||
|
|
||||||
|
_running = False
|
||||||
|
|
||||||
def monitor(self, logger=logging):
|
def monitor(self, logger=logging):
|
||||||
|
if self._running:
|
||||||
|
raise RuntimeError("already running")
|
||||||
|
|
||||||
"""Run in monitor mode."""
|
"""Run in monitor mode."""
|
||||||
with futures.ThreadPoolExecutor() as pool:
|
with futures.ThreadPoolExecutor() as pool:
|
||||||
archives_handler = MonitorHandler(
|
archives_handler = MonitorHandler(
|
||||||
|
@ -307,5 +311,13 @@ class SoundMonitor:
|
||||||
|
|
||||||
atexit.register(leave)
|
atexit.register(leave)
|
||||||
|
|
||||||
while True:
|
self._running = True
|
||||||
|
while self._running:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
leave()
|
||||||
|
atexit.unregister(leave)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop monitor() loop."""
|
||||||
|
self._running = False
|
||||||
|
|
100
aircox/test.py
100
aircox/test.py
|
@ -44,38 +44,64 @@ InterfaceTarget = namedtuple(
|
||||||
|
|
||||||
|
|
||||||
class WrapperMixin:
|
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
|
self.target = target
|
||||||
if ns:
|
if ns:
|
||||||
self.inject(ns, ns_attr)
|
self.inject(ns, ns_attr)
|
||||||
|
if self.type_interface:
|
||||||
|
self._set_type_interface(type_interface)
|
||||||
super().__init__(**kwargs)
|
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
|
@property
|
||||||
def ns_target(self):
|
def ns_target(self):
|
||||||
|
"""Actual namespace's target (using ns.ns_attr)"""
|
||||||
if self.ns and self.ns_attr:
|
if self.ns and self.ns_attr:
|
||||||
return getattr(self.ns, self.ns_attr, None)
|
return getattr(self.ns, self.ns_attr, None)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def inject(self, ns=None, ns_attr=None):
|
def inject(self, ns=None, ns_attr=None):
|
||||||
if ns and ns_attr:
|
"""Inject interface into namespace at given key."""
|
||||||
ns_target = getattr(ns, ns_attr, None)
|
if not (ns and ns_attr):
|
||||||
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:
|
|
||||||
raise ValueError("ns and ns_attr must be provided together")
|
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 = ns
|
||||||
self.ns_attr = ns_attr
|
self.ns_attr = ns_attr
|
||||||
|
|
||||||
def release(self):
|
def release(self):
|
||||||
if self.ns_target is self:
|
"""Remove injection from previously injected parent, reset target."""
|
||||||
setattr(self.target.namespace, self.target.name, self.target)
|
if self.ns_target is self.interface:
|
||||||
|
setattr(self.ns, self.ns_attr, self.target)
|
||||||
self.target = None
|
self.target = None
|
||||||
|
|
||||||
|
|
||||||
|
@ -83,7 +109,9 @@ class SpoofMixin:
|
||||||
traces = None
|
traces = None
|
||||||
|
|
||||||
def __init__(self, funcs=None, **kwargs):
|
def __init__(self, funcs=None, **kwargs):
|
||||||
self.reset(funcs or {})
|
self.reset(
|
||||||
|
funcs or {},
|
||||||
|
)
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def reset(self, funcs=None):
|
def reset(self, funcs=None):
|
||||||
|
@ -152,23 +180,19 @@ class SpoofMixin:
|
||||||
"""
|
"""
|
||||||
func = self.funcs[name]
|
func = self.funcs[name]
|
||||||
if callable(func):
|
if callable(func):
|
||||||
return func(*a, **kw)
|
return func(self, *a, **kw)
|
||||||
return func
|
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 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
|
_imeta = None
|
||||||
"""This contains a InterfaceMeta instance related to Interface one.
|
"""This contains a InterfaceMeta instance related to Interface one.
|
||||||
|
|
||||||
|
@ -182,7 +206,7 @@ class Interface:
|
||||||
_imeta_kw.setdefault("funcs", _funcs)
|
_imeta_kw.setdefault("funcs", _funcs)
|
||||||
if _target is not None:
|
if _target is not None:
|
||||||
_imeta_kw.setdefault("target", _target)
|
_imeta_kw.setdefault("target", _target)
|
||||||
self._imeta = InterfaceMeta(self, **_imeta_kw)
|
self._imeta = self.IMeta(self, **_imeta_kw)
|
||||||
self.__dict__.update(kwargs)
|
self.__dict__.update(kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -195,19 +219,29 @@ class Interface:
|
||||||
return cls(**kwargs)
|
return cls(**kwargs)
|
||||||
|
|
||||||
def _irelease(self):
|
def _irelease(self):
|
||||||
|
"""Shortcut to `self._imeta.release`."""
|
||||||
self._imeta.release()
|
self._imeta.release()
|
||||||
|
|
||||||
def _trace(self, *args, **kw):
|
def _trace(self, *args, **kw):
|
||||||
|
"""Shortcut to `self._imeta.get_trace`."""
|
||||||
return self._imeta.get_trace(*args, **kw)
|
return self._imeta.get_trace(*args, **kw)
|
||||||
|
|
||||||
def _traces(self, *args, **kw):
|
def _traces(self, *args, **kw):
|
||||||
|
"""Shortcut to `self._imeta.get_traces`."""
|
||||||
return self._imeta.get_traces(*args, **kw)
|
return self._imeta.get_traces(*args, **kw)
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
target = self._imeta.target
|
target = self._imeta.target
|
||||||
|
print("is it class?", self, target, inspect.isclass(target))
|
||||||
if inspect.isclass(target):
|
if inspect.isclass(target):
|
||||||
target = target(*args, **kwargs)
|
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)
|
self._imeta.add("__call__", args, kwargs)
|
||||||
return self._imeta.target(*args, **kwargs)
|
return self._imeta.target(*args, **kwargs)
|
||||||
|
@ -216,3 +250,7 @@ class Interface:
|
||||||
if attr in self._imeta.funcs:
|
if attr in self._imeta.funcs:
|
||||||
return lambda *args, **kwargs: self._imeta.call(attr, args, kwargs)
|
return lambda *args, **kwargs: self._imeta.call(attr, args, kwargs)
|
||||||
return getattr(self._imeta.target, attr)
|
return getattr(self._imeta.target, attr)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
iface = super().__str__()
|
||||||
|
return f"{iface}::{self._imeta.target}"
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
from concurrent import futures
|
||||||
import logging
|
import logging
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from django.utils import timezone as tz
|
from django.utils import timezone as tz
|
||||||
|
|
||||||
|
from aircox.conf import settings
|
||||||
from aircox.models import Sound
|
from aircox.models import Sound
|
||||||
from aircox.controllers import sound_monitor
|
from aircox.controllers import sound_monitor
|
||||||
from aircox.test import Interface, interface
|
from aircox.test import Interface, interface
|
||||||
|
@ -25,8 +27,8 @@ def logger():
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def interfaces(logger):
|
def interfaces():
|
||||||
return {
|
items = {
|
||||||
"SoundFile": Interface.inject(
|
"SoundFile": Interface.inject(
|
||||||
sound_monitor,
|
sound_monitor,
|
||||||
"SoundFile",
|
"SoundFile",
|
||||||
|
@ -41,8 +43,11 @@ def interfaces(logger):
|
||||||
"sleep": None,
|
"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
|
@pytest.fixture
|
||||||
|
@ -65,6 +70,23 @@ def modified_task(interfaces):
|
||||||
return sound_monitor.ModifiedTask()
|
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:
|
class TestTask:
|
||||||
def test___init__(self, task):
|
def test___init__(self, task):
|
||||||
assert task.timestamp is not None
|
assert task.timestamp is not None
|
||||||
|
@ -111,7 +133,7 @@ class TestModifiedTask:
|
||||||
dt_now = now + modified_task.timeout_delta - tz.timedelta(hours=10)
|
dt_now = now + modified_task.timeout_delta - tz.timedelta(hours=10)
|
||||||
datetime = Interface.inject(sound_monitor, "datetime", {"now": dt_now})
|
datetime = Interface.inject(sound_monitor, "datetime", {"now": dt_now})
|
||||||
|
|
||||||
def sleep(n):
|
def sleep(imeta, n):
|
||||||
datetime._imeta.funcs[
|
datetime._imeta.funcs[
|
||||||
"now"
|
"now"
|
||||||
] = modified_task.timestamp + tz.timedelta(hours=10)
|
] = modified_task.timestamp + tz.timedelta(hours=10)
|
||||||
|
@ -129,31 +151,124 @@ class TestModifiedTask:
|
||||||
|
|
||||||
|
|
||||||
class TestMonitorHandler:
|
class TestMonitorHandler:
|
||||||
def test_monitor___init__(self):
|
def test_on_created(self, monitor_handler, event):
|
||||||
pass
|
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):
|
def test_on_deleted(self, monitor_handler, event):
|
||||||
pass
|
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):
|
def test_on_moved(self, monitor_handler, event):
|
||||||
pass
|
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):
|
def test_on_modified(self, monitor_handler, event):
|
||||||
pass
|
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):
|
def test__submit(self, monitor_handler, event):
|
||||||
pass
|
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):
|
key = f"prefix:{event.src_path}"
|
||||||
pass
|
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:
|
class SoundMonitor:
|
||||||
def test_report(self):
|
def test_report(self, monitor, program, logger):
|
||||||
pass
|
monitor.report(program, "component", "content", logger=logger)
|
||||||
|
msg = f"{program}, component: content"
|
||||||
|
assert logger._trace("info", args=True) == (msg,)
|
||||||
|
|
||||||
def test_scan(self):
|
def test_scan(self, monitor, program, logger):
|
||||||
pass
|
interface = Interface(None, {"scan_for_program": None})
|
||||||
|
monitor.scan_for_program = interface.scan_for_program
|
||||||
|
dirs = monitor.scan(logger)
|
||||||
|
|
||||||
def test_monitor(self):
|
assert logger._traces("info") == (
|
||||||
pass
|
"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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user