#88 #89 : use pytest + reorganise settings (#92)

- !88 pytest on existing tests
- !89 reorganise settings (! see notes for deployment)

Co-authored-by: bkfox <thomas bkfox net>
Reviewed-on: rc/aircox#92
This commit is contained in:
Thomas Kairos
2023-03-28 14:40:49 +02:00
parent 4bebc56a28
commit 0e183099ed
31 changed files with 511 additions and 368 deletions

180
aircox/conf.py Executable file
View File

@ -0,0 +1,180 @@
import os
import inspect
from django.conf import settings as d_settings
__all__ = ("Settings", "settings")
# code from django-fox
class BaseSettings:
"""Utility class used to load and save settings, can be used as model.
Some members are excluded from being configuration:
- Protected/private members;
- On django model, "objects" and "Meta";
- Class declaration and callables
Example:
```
class MySettings(Settings):
a = 13
b = 12
my_settings = MySettings().load('MY_SETTINGS_KEY')
print(my_settings.a, my_settings.get('b'))
```
This will load values from django project settings.
"""
def __init__(self, key, module=None):
self.load(key, module)
def load(self, key, module=None):
"""Load settings from module's item specified by its member name. When
no module is provided, uses ``django.conf.settings``.
:param str key: module member name.
:param module: configuration object.
:returns self
"""
if module is None:
module = d_settings
settings = getattr(module, key, None)
if settings:
self.update(settings)
return self
def update(self, settings):
"""Update self's values from provided settings. ``settings`` can be an
iterable of ``(key, value)``.
:param dict|Settings|iterable settings: value to update from.
"""
if isinstance(settings, (dict, Settings)):
settings = settings.items()
for key, value in settings:
if self.is_config_item(key, value):
setattr(self, key, value)
def get(self, key, default=None):
"""Return settings' value for provided key."""
return getattr(self, key, default)
def items(self):
"""Iterate over items members, as tupple of ``key, value``."""
for key in dir(self):
value = getattr(self, key)
if self.is_config_item(key, value):
yield key, value
def is_config_item(self, key, value):
"""Return True if key/value item is a configuration setting."""
if key.startswith("_") or callable(value) or inspect.isclass(value):
return False
return True
class Settings(BaseSettings):
# --- Global & misc
DEFAULT_USER_GROUPS = {
"radio hosts": (
# TODO include content_type in order to avoid clash with potential
# extra applications
# aircox
"change_program",
"change_episode",
"change_diffusion",
"add_comment",
"change_comment",
"delete_comment",
"add_article",
"change_article",
"delete_article",
"change_sound",
"add_track",
"change_track",
"delete_track",
# taggit
"add_tag",
"change_tag",
"delete_tag",
# filer
"add_folder",
"change_folder",
"delete_folder",
"can_use_directory_listing",
"add_image",
"change_image",
"delete_image",
),
}
"""Groups to assign to users at their creation, along with the permissions
to add to each group."""
PROGRAMS_DIR = "programs"
"""Directory for the programs data."""
@property
def PROGRAMS_DIR_ABS(self):
return os.path.join(d_settings.MEDIA_ROOT, self.PROGRAMS_DIR)
# --- Programs & episodes
EPISODE_TITLE = "{program.title} - {date}"
"""Default title for episodes."""
EPISODE_TITLE_DATE_FORMAT = "%-d %B %Y"
"""Date format in episode title (python's strftime)"""
# --- Logs & archives
LOGS_ARCHIVES_DIR = "logs/archives"
"""Directory where to save logs' archives."""
@property
def LOGS_ARCHIVES_DIR_ABS(self):
return os.path.join(d_settings.PROJECT_ROOT, self.LOGS_ARCHIVES_DIR)
LOGS_ARCHIVES_AGE = 60
"""In days, minimal age of a log before it is archived."""
# --- Sounds
SOUND_ARCHIVES_SUBDIR = "archives"
"""Sub directory used for the complete episode sounds."""
SOUND_EXCERPTS_SUBDIR = "excerpts"
"""Sub directory used for the excerpts of the episode."""
SOUND_QUALITY = {
"attribute": "RMS lev dB",
"range": (-18.0, -8.0),
"sample_length": 120,
}
"""Quality attributes passed to sound_quality_check from sounds_monitor
(Soxi parameters)."""
SOUND_FILE_EXT = (".ogg", ".flac", ".wav", ".mp3", ".opus")
"""Extension of sound files."""
SOUND_KEEP_DELETED = False
"""Tag sounds as deleted instead of deleting them when file has been
removed from filesystem (sound monitoring)."""
# --- Streamer & Controllers
CONTROLLERS_WORKING_DIR = "/tmp/aircox"
"""Controllers working directory."""
# --- Playlist import from CSV
IMPORT_PLAYLIST_CSV_COLS = (
"artist",
"title",
"minutes",
"seconds",
"tags",
"info",
)
"""Columns for CSV file."""
IMPORT_PLAYLIST_CSV_DELIMITER = ";"
"""Column delimiter of csv text files."""
IMPORT_PLAYLIST_CSV_TEXT_QUOTE = '"'
"""Text delimiter of csv text files."""
settings = Settings("AIRCOX")

View File

@ -9,7 +9,7 @@ from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand
from django.utils import timezone as tz
import aircox.settings as settings
from aircox.conf import settings
from aircox.models import Log
from aircox.models.log import LogArchiver
@ -29,9 +29,9 @@ class Command(BaseCommand):
"-a",
"--age",
type=int,
default=settings.AIRCOX_LOGS_ARCHIVES_AGE,
default=settings.LOGS_ARCHIVES_AGE,
help="minimal age in days of logs to archive. Default is "
"settings.AIRCOX_LOGS_ARCHIVES_AGE",
"settings.LOGS_ARCHIVES_AGE",
)
group.add_argument(
"-k",

View File

@ -2,9 +2,9 @@
sound.
Playlists are in CSV format, where columns are separated with a
'{settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER}'. Text quote is
{settings.AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE}.
The order of the elements is: {settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS}
'{settings.IMPORT_PLAYLIST_CSV_DELIMITER}'. Text quote is
{settings.IMPORT_PLAYLIST_CSV_TEXT_QUOTE}.
The order of the elements is: {settings.IMPORT_PLAYLIST_CSV_COLS}
If 'minutes' or 'seconds' are given, position will be expressed as timed
position, instead of position in playlist.
@ -16,7 +16,7 @@ from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand
from aircox import settings
from aircox.conf import settings
from aircox.models import Sound, Track
__doc__ = __doc__.format(settings=settings)
@ -61,9 +61,9 @@ class PlaylistImport:
)
and row.strip()
),
fieldnames=settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS,
delimiter=settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER,
quotechar=settings.AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE,
fieldnames=settings.IMPORT_PLAYLIST_CSV_COLS,
delimiter=settings.IMPORT_PLAYLIST_CSV_DELIMITER,
quotechar=settings.IMPORT_PLAYLIST_CSV_TEXT_QUOTE,
)
)
@ -80,7 +80,7 @@ class PlaylistImport:
)
return
maps = settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS
maps = settings.IMPORT_PLAYLIST_CSV_COLS
tracks = []
logger.info("parse csv file " + self.path)

View File

@ -20,7 +20,7 @@ Where:
To check quality of files, call the command sound_quality_check using the
parameters given by the setting AIRCOX_SOUND_QUALITY. This script requires
parameters given by the setting SOUND_QUALITY. This script requires
Sox (and soxi).
"""
import atexit
@ -33,7 +33,7 @@ from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand
from watchdog.observers import Observer
from aircox import settings
from aircox.conf import settings
from aircox.management.sound_file import SoundFile
from aircox.management.sound_monitor import MonitorHandler
from aircox.models import Program, Sound
@ -67,12 +67,12 @@ class Command(BaseCommand):
logger.info("#%d %s", program.id, program.title)
self.scan_for_program(
program,
settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
settings.SOUND_ARCHIVES_SUBDIR,
type=Sound.TYPE_ARCHIVE,
)
self.scan_for_program(
program,
settings.AIRCOX_SOUND_EXCERPTS_SUBDIR,
settings.SOUND_EXCERPTS_SUBDIR,
type=Sound.TYPE_EXCERPT,
)
dirs.append(os.path.join(program.abspath))
@ -91,7 +91,7 @@ class Command(BaseCommand):
# sounds in directory
for path in os.listdir(subdir):
path = os.path.join(subdir, path)
if not path.endswith(settings.AIRCOX_SOUND_FILE_EXT):
if not path.endswith(settings.SOUND_FILE_EXT):
continue
sound_file = SoundFile(path)
@ -115,12 +115,12 @@ class Command(BaseCommand):
"""Run in monitor mode."""
with futures.ThreadPoolExecutor() as pool:
archives_handler = MonitorHandler(
settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
settings.SOUND_ARCHIVES_SUBDIR,
pool,
type=Sound.TYPE_ARCHIVE,
)
excerpts_handler = MonitorHandler(
settings.AIRCOX_SOUND_EXCERPTS_SUBDIR,
settings.SOUND_EXCERPTS_SUBDIR,
pool,
type=Sound.TYPE_EXCERPT,
)
@ -128,12 +128,12 @@ class Command(BaseCommand):
observer = Observer()
observer.schedule(
archives_handler,
settings.AIRCOX_PROGRAMS_DIR_ABS,
settings.PROGRAMS_DIR_ABS,
recursive=True,
)
observer.schedule(
excerpts_handler,
settings.AIRCOX_PROGRAMS_DIR_ABS,
settings.PROGRAMS_DIR_ABS,
recursive=True,
)
observer.start()

View File

@ -17,7 +17,7 @@ Where:
Sound Quality
=============
To check quality of files, call the command sound_quality_check using the
parameters given by the setting AIRCOX_SOUND_QUALITY. This script requires
parameters given by the setting SOUND_QUALITY. This script requires
Sox (and soxi).
"""
import logging
@ -175,7 +175,7 @@ class SoundFile:
at = tz.datetime(
year, month, day, pi.get("hour", 0), pi.get("minute", 0)
)
at = tz.get_current_timezone().localize(at)
at = tz.make_aware(at)
else:
at = date(year, month, day)

View File

@ -20,7 +20,7 @@ Where:
To check quality of files, call the command sound_quality_check using the
parameters given by the setting AIRCOX_SOUND_QUALITY. This script requires
parameters given by the setting SOUND_QUALITY. This script requires
Sox (and soxi).
"""
import logging
@ -29,7 +29,7 @@ from datetime import datetime, timedelta
from watchdog.events import PatternMatchingEventHandler
from aircox import settings
from aircox.conf import settings
from aircox.models import Sound
from .sound_file import SoundFile
@ -121,7 +121,7 @@ class MonitorHandler(PatternMatchingEventHandler):
def __init__(self, subdir, pool, **sync_kw):
"""
:param str subdir: sub-directory in program dirs to monitor \
(AIRCOX_SOUND_ARCHIVES_SUBDIR or AIRCOX_SOUND_EXCERPTS_SUBDIR);
(SOUND_ARCHIVES_SUBDIR or SOUND_EXCERPTS_SUBDIR);
:param concurrent.futures.Executor pool: pool executing jobs on file
change;
:param **sync_kw: kwargs passed to `SoundFile.sync`;
@ -132,7 +132,7 @@ class MonitorHandler(PatternMatchingEventHandler):
patterns = [
"*/{}/*{}".format(self.subdir, ext)
for ext in settings.AIRCOX_SOUND_FILE_EXT
for ext in settings.SOUND_FILE_EXT
]
super().__init__(patterns=patterns, ignore_directories=True)

View File

@ -1,4 +1,3 @@
from . import signals
from .article import Article
from .episode import Diffusion, DiffusionQuerySet, Episode
from .log import Log, LogArchiver, LogQuerySet

View File

@ -7,7 +7,8 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from easy_thumbnails.files import get_thumbnailer
from aircox import settings, utils
from aircox.conf import settings
from aircox import utils
from .page import Page
from .program import (
@ -70,18 +71,18 @@ class Episode(Page):
@classmethod
def get_default_title(cls, page, date):
return settings.AIRCOX_EPISODE_TITLE.format(
return settings.EPISODE_TITLE.format(
program=page,
date=date.strftime(settings.AIRCOX_EPISODE_TITLE_DATE_FORMAT),
date=date.strftime(settings.EPISODE_TITLE_DATE_FORMAT),
)
@classmethod
def get_init_kwargs_from(cls, page, date, title=None, **kwargs):
"""Get default Episode's title."""
title = (
settings.AIRCOX_EPISODE_TITLE.format(
settings.EPISODE_TITLE.format(
program=page,
date=date.strftime(settings.AIRCOX_EPISODE_TITLE_DATE_FORMAT),
date=date.strftime(settings.EPISODE_TITLE_DATE_FORMAT),
)
if title is None
else title

View File

@ -10,8 +10,9 @@ from django.utils import timezone as tz
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from aircox import settings
from aircox.conf import settings
__all__ = ("Settings", "settings")
from .episode import Diffusion
from .sound import Sound, Track
from .station import Station
@ -251,7 +252,7 @@ class LogArchiver:
@staticmethod
def get_path(station, date):
return os.path.join(
settings.AIRCOX_LOGS_ARCHIVES_DIR,
settings.LOGS_ARCHIVES_DIR_ABS,
"{}_{}.log.gz".format(date.strftime("%Y%m%d"), station.pk),
)
@ -264,7 +265,7 @@ class LogArchiver:
if not qs.exists():
return 0
os.makedirs(settings.AIRCOX_LOGS_ARCHIVES_DIR, exist_ok=True)
os.makedirs(settings.LOGS_ARCHIVES_DIR_ABS, exist_ok=True)
count = qs.count()
logs = self.sort_logs(qs)

View File

@ -15,7 +15,8 @@ from django.utils import timezone as tz
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from aircox import settings, utils
from aircox import utils
from aircox.conf import settings
from .page import Page, PageQuerySet
from .station import Station
@ -77,9 +78,7 @@ class Program(Page):
@property
def path(self):
"""Return program's directory path."""
return os.path.join(
settings.AIRCOX_PROGRAMS_DIR, self.slug.replace("-", "_")
)
return os.path.join(settings.PROGRAMS_DIR, self.slug.replace("-", "_"))
@property
def abspath(self):
@ -88,11 +87,11 @@ class Program(Page):
@property
def archives_path(self):
return os.path.join(self.path, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR)
return os.path.join(self.path, settings.SOUND_ARCHIVES_SUBDIR)
@property
def excerpts_path(self):
return os.path.join(self.path, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR)
return os.path.join(self.path, settings.SOUND_ARCHIVES_SUBDIR)
def __init__(self, *kargs, **kwargs):
super().__init__(*kargs, **kwargs)
@ -107,8 +106,8 @@ class Program(Page):
We assume the path has been given in a previous time by this
model (Program.path getter).
"""
if path.startswith(settings.AIRCOX_PROGRAMS_DIR_ABS):
path = path.replace(settings.AIRCOX_PROGRAMS_DIR_ABS, "")
if path.startswith(settings.PROGRAMS_DIR_ABS):
path = path.replace(settings.PROGRAMS_DIR_ABS, "")
while path[0] == "/":
path = path[1:]
path = path[: path.index("/")]

View File

@ -4,7 +4,8 @@ from django.db.models import signals
from django.dispatch import receiver
from django.utils import timezone as tz
from .. import settings, utils
from aircox import utils
from aircox.conf import settings
from . import Diffusion, Episode, Page, Program, Schedule
@ -20,7 +21,7 @@ def user_default_groups(sender, instance, created, *args, **kwargs):
if not created or instance.is_superuser:
return
for group_name, permissions in settings.AIRCOX_DEFAULT_USER_GROUPS.items():
for group_name, permissions in settings.DEFAULT_USER_GROUPS.items():
if instance.groups.filter(name=group_name).count():
continue

View File

@ -8,7 +8,7 @@ from django.utils import timezone as tz
from django.utils.translation import gettext_lazy as _
from taggit.managers import TaggableManager
from aircox import settings
from aircox.conf import settings
from .episode import Episode
from .program import Program
@ -123,9 +123,9 @@ class Sound(models.Model):
def _upload_to(self, filename):
subdir = (
settings.AIRCOX_SOUND_ARCHIVES_SUBDIR
settings.SOUND_ARCHIVES_SUBDIR
if self.type == self.TYPE_ARCHIVE
else settings.AIRCOX_SOUND_EXCERPTS_SUBDIR
else settings.SOUND_EXCERPTS_SUBDIR
)
return os.path.join(self.program.path, subdir, filename)

View File

@ -5,7 +5,7 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from filer.fields.image import FilerImageField
from .. import settings
from aircox.conf import settings
__all__ = ("Station", "StationQuerySet", "Port")
@ -92,7 +92,7 @@ class Station(models.Model):
def save(self, make_sources=True, *args, **kwargs):
if not self.path:
self.path = os.path.join(
settings.AIRCOX_CONTROLLERS_WORKING_DIR,
settings.CONTROLLERS_WORKING_DIR,
self.slug.replace("-", "_"),
)

168
aircox/settings.py Executable file → Normal file
View File

@ -1,125 +1,73 @@
import os
import inspect
from django.conf import settings
from django.conf import settings as django_settings
from django.db import models
def ensure(key, default):
value = getattr(settings, key, default)
globals()[key] = value
return value
class Settings:
"""Utility class used to load and save settings, can be used as model.
Some members are excluded from being configuration:
- Protected/private members;
- On django model, "objects" and "Meta";
- Class declaration and callables
########################################################################
# Global & misc
########################################################################
# group to assign to users at their creation, along with the permissions
# to add to each group.
ensure(
"AIRCOX_DEFAULT_USER_GROUPS",
{
"radio hosts": (
# TODO include content_type in order to avoid clash with potential
# extra applications
# aircox
"change_program",
"change_episode",
"change_diffusion",
"add_comment",
"change_comment",
"delete_comment",
"add_article",
"change_article",
"delete_article",
"change_sound",
"add_track",
"change_track",
"delete_track",
# taggit
"add_tag",
"change_tag",
"delete_tag",
# filer
"add_folder",
"change_folder",
"delete_folder",
"can_use_directory_listing",
"add_image",
"change_image",
"delete_image",
),
},
)
Example:
# Directory for the programs data
AIRCOX_PROGRAMS_DIR = ensure("AIRCOX_PROGRAMS_DIR", "programs")
ensure(
"AIRCOX_PROGRAMS_DIR_ABS",
os.path.join(settings.MEDIA_ROOT, AIRCOX_PROGRAMS_DIR),
)
```
class MySettings(Settings):
a = 13
b = 12
my_settings = MySettings().load('MY_SETTINGS_KEY')
print(my_settings.a, my_settings.get('b'))
```
########################################################################
# Programs & Episodes
########################################################################
# default title for episodes
ensure("AIRCOX_EPISODE_TITLE", "{program.title} - {date}")
# date format in episode title (python's strftime)
ensure("AIRCOX_EPISODE_TITLE_DATE_FORMAT", "%-d %B %Y")
This will load values from django project settings.
"""
########################################################################
# Logs & Archives
########################################################################
# Directory where to save logs' archives
ensure(
"AIRCOX_LOGS_ARCHIVES_DIR",
os.path.join(settings.PROJECT_ROOT, "logs/archives"),
)
# In days, minimal age of a log before it is archived
ensure("AIRCOX_LOGS_ARCHIVES_AGE", 60)
def load(self, key, module=None):
"""Load settings from module's item specified by its member name. When
no module is provided, uses ``django.conf.settings``.
:param str key: module member name.
:param module: configuration object.
:returns self
"""
if module is None:
module = django_settings
settings = getattr(module, key, None)
if settings:
self.update(settings)
return self
########################################################################
# Sounds
########################################################################
# Sub directory used for the complete episode sounds
ensure("AIRCOX_SOUND_ARCHIVES_SUBDIR", "archives")
# Sub directory used for the excerpts of the episode
ensure("AIRCOX_SOUND_EXCERPTS_SUBDIR", "excerpts")
def update(self, settings):
"""Update self's values from provided settings. ``settings`` can be an
iterable of ``(key, value)``.
# Quality attributes passed to sound_quality_check from sounds_monitor
ensure(
"AIRCOX_SOUND_QUALITY",
{
"attribute": "RMS lev dB",
"range": (-18.0, -8.0),
"sample_length": 120,
},
)
:param dict|Settings|iterable settings: value to update from.
"""
if isinstance(settings, (dict, Settings)):
settings = settings.items()
for key, value in settings:
if hasattr(self, key) and self.is_config_item(key, value):
setattr(self, key, value)
# Extension of sound files
ensure("AIRCOX_SOUND_FILE_EXT", (".ogg", ".flac", ".wav", ".mp3", ".opus"))
def get(self, key, default=None):
"""Return settings' value for provided key."""
return getattr(self, key, default)
# Tag sounds as deleted instead of deleting them when file has been removed
# from filesystem (sound monitoring)
ensure("AIRCOX_SOUND_KEEP_DELETED", False)
def items(self):
"""Iterate over items members, as tupple of ``key, value``."""
for key in dir(self):
value = getattr(self, key)
if self.is_config_item(key, value):
yield key, value
########################################################################
# Streamer & Controllers
########################################################################
# Controllers working directory
ensure("AIRCOX_CONTROLLERS_WORKING_DIR", "/tmp/aircox")
########################################################################
# Playlist import from CSV
########################################################################
# Columns for CSV file
ensure(
"AIRCOX_IMPORT_PLAYLIST_CSV_COLS",
("artist", "title", "minutes", "seconds", "tags", "info"),
)
# Column delimiter of csv text files
ensure("AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER", ";")
# Text delimiter of csv text files
ensure("AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE", '"')
def is_config_item(self, key, value):
"""Return True if key/value item is a configuration setting."""
if key.startswith("_") or callable(value) or inspect.isclass(value):
return False
if isinstance(self, models.Model) and key == "object":
return False
return True

View File

@ -1,15 +0,0 @@
from .sound_file import SoundFileTestCase
from .sound_monitor import (
ModifiedHandlerTestCase,
MonitorHandlerTestCase,
MoveHandlerTestCase,
NotifyHandlerTestCase,
)
__all__ = (
"SoundFileTestCase",
"NotifyHandlerTestCase",
"MoveHandlerTestCase",
"ModifiedHandlerTestCase",
"MonitorHandlerTestCase",
)

View File

@ -1,103 +0,0 @@
from datetime import timedelta
from django.conf import settings as conf
from django.test import TestCase
from django.utils import timezone as tz
from aircox import models
from aircox.management.sound_file import SoundFile
__all__ = ("SoundFileTestCase",)
class SoundFileTestCase(TestCase):
path_infos = {
"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",
},
}
subdir_prefix = "test"
sound_files = {
k: r
for k, r in (
(path, SoundFile(conf.MEDIA_ROOT + "/" + path))
for path in path_infos.keys()
)
}
def test_sound_path(self):
for path, sound_file in self.sound_files.items():
self.assertEqual(path, sound_file.sound_path)
def test_read_path(self):
for path, sound_file in self.sound_files.items():
expected = self.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}
self.assertEqual(expected, result, "path: {}".format(path))
def _setup_diff(self, 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
def test_find_episode(self):
station = models.Station(name="test-station")
program = models.Program(station=station, title="test")
station.save()
program.save()
for path, sound_file in self.sound_files.items():
infos = sound_file.read_path(path)
diff = self._setup_diff(program, infos)
sound = models.Sound(program=diff.program, file=path)
result = sound_file.find_episode(sound, infos)
self.assertEquals(diff.episode, result)
# TODO: find_playlist, sync

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.management.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

@ -1,22 +1,15 @@
import pytest
import concurrent.futures as futures
import time
from datetime import datetime, timedelta
from django.test import TestCase
from aircox.management.sound_monitor import (
ModifiedHandler,
MonitorHandler,
NotifyHandler,
)
__all__ = (
"NotifyHandlerTestCase",
"MoveHandlerTestCase",
"ModifiedHandlerTestCase",
"MonitorHandlerTestCase",
)
class FakeEvent:
def __init__(self, **kwargs):
@ -31,22 +24,28 @@ class WaitHandler(NotifyHandler):
pass
class NotifyHandlerTestCase(TestCase):
@pytest.fixture
def monitor():
pool = futures.ThreadPoolExecutor(2)
return MonitorHandler("archives", pool)
class TestNotifyHandler:
pass
class MoveHandlerTestCase(TestCase):
class TestMoveHandler:
pass
class ModifiedHandlerTestCase(TestCase):
class TestModifiedHandler:
def test_wait(self):
handler = ModifiedHandler()
handler.timeout_delta = timedelta(seconds=0.1)
start = datetime.now()
handler.wait()
delta = datetime.now() - start
self.assertTrue(delta < handler.timeout_delta + timedelta(seconds=0.1))
assert delta < handler.timeout_delta + timedelta(seconds=0.1)
def test_wait_ping(self):
pool = futures.ThreadPoolExecutor(1)
@ -57,28 +56,24 @@ class ModifiedHandlerTestCase(TestCase):
time.sleep(0.3)
handler.ping()
time.sleep(0.3)
self.assertTrue(future.running())
assert future.running()
class MonitorHandlerTestCase(TestCase):
def setUp(self):
pool = futures.ThreadPoolExecutor(2)
self.monitor = MonitorHandler("archives", pool)
def test_submit_new_job(self):
class TestMonitorHandler:
def test_submit_new_job(self, monitor):
event = FakeEvent(src_path="dummy_src")
handler = NotifyHandler()
result, _ = self.monitor._submit(handler, event, "up")
self.assertIs(handler, result)
self.assertIsInstance(handler.future, futures.Future)
self.monitor.pool.shutdown()
result, _ = monitor._submit(handler, event, "up")
assert handler == result
assert isinstance(handler.future, futures.Future)
monitor.pool.shutdown()
def test_submit_job_exists(self):
def test_submit_job_exists(self, monitor):
event = FakeEvent(src_path="dummy_src")
job_1, new_1 = self.monitor._submit(WaitHandler(), event, "up")
job_2, new_2 = self.monitor._submit(NotifyHandler(), event, "up")
self.assertIs(job_1, job_2)
self.assertTrue(new_1)
self.assertFalse(new_2)
self.monitor.pool.shutdown()
job_1, new_1 = monitor._submit(WaitHandler(), event, "up")
job_2, new_2 = monitor._submit(NotifyHandler(), event, "up")
assert job_1 == job_2
assert new_1
assert not new_2
monitor.pool.shutdown()

View File

@ -1,8 +1,7 @@
import datetime
import django.utils.timezone as tz
__all__ = [
__all__ = (
"Redirect",
"redirect",
"date_range",
@ -10,9 +9,10 @@ __all__ = [
"date_or_default",
"to_timedelta",
"seconds_to_time",
]
)
# FIXME: usage & why we don't use Django's
class Redirect(Exception):
"""Redirect exception -- see `redirect()`."""