mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 09:06:06 +00:00
- Improved logging in the Stream model for better debugging of profile evaluations. - Introduced a new method `_stream_assignment_is_reusable` in the Channel model to determine if existing stream assignments can be reused, enhancing efficiency. - Updated the release logic in `release_profile_slot` to utilize stored credential keys, reducing unnecessary database lookups. - Simplified error handling in the `get_stream_info_for_switch` function to ensure proper stream release on exceptions. - Enhanced tests for connection pool management and error handling in the ServerGroupsTable component to improve reliability and user feedback.
173 lines
5.8 KiB
Python
173 lines
5.8 KiB
Python
"""Tests for Channel.get_stream() assignment reuse and stale cleanup."""
|
|
|
|
from unittest.mock import patch
|
|
|
|
from django.test import TestCase
|
|
|
|
from apps.channels.models import Channel, ChannelStream, Stream
|
|
from apps.m3u.models import M3UAccount, M3UAccountProfile
|
|
from apps.proxy.live_proxy.constants import ChannelMetadataField, ChannelState
|
|
from apps.proxy.live_proxy.redis_keys import RedisKeys
|
|
|
|
|
|
class FakeAssignmentRedis:
|
|
"""In-memory Redis for channel_stream assignment tests."""
|
|
|
|
def __init__(self):
|
|
self._strings = {}
|
|
self._hashes = {}
|
|
|
|
def _decode(self, value):
|
|
if isinstance(value, bytes):
|
|
return value.decode()
|
|
return value
|
|
|
|
def get(self, key):
|
|
value = self._strings.get(key)
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, int):
|
|
return str(value).encode()
|
|
return str(value).encode()
|
|
|
|
def set(self, key, value):
|
|
self._strings[key] = value
|
|
|
|
def delete(self, key):
|
|
self._strings.pop(key, None)
|
|
self._hashes.pop(key, None)
|
|
|
|
def exists(self, key):
|
|
return key in self._strings or key in self._hashes
|
|
|
|
def hget(self, key, field):
|
|
return self._hashes.get(key, {}).get(field)
|
|
|
|
def hset(self, key, mapping=None, **kwargs):
|
|
bucket = self._hashes.setdefault(key, {})
|
|
if mapping:
|
|
bucket.update(mapping)
|
|
bucket.update(kwargs)
|
|
|
|
def incr(self, key):
|
|
current = int(self._decode(self.get(key)) or 0)
|
|
current += 1
|
|
self._strings[key] = current
|
|
return current
|
|
|
|
def decr(self, key):
|
|
current = int(self._decode(self.get(key)) or 0)
|
|
current -= 1
|
|
self._strings[key] = current
|
|
return current
|
|
|
|
|
|
class ChannelGetStreamAssignmentTests(TestCase):
|
|
def setUp(self):
|
|
self.redis = FakeAssignmentRedis()
|
|
self.account = M3UAccount.objects.create(
|
|
name="assignment-test",
|
|
account_type="XC",
|
|
username="user",
|
|
password="pass",
|
|
max_streams=5,
|
|
)
|
|
self.profile = M3UAccountProfile.objects.get(
|
|
m3u_account=self.account, is_default=True
|
|
)
|
|
self.profile.max_streams = 2
|
|
self.profile.save()
|
|
|
|
self.stream = Stream.objects.create(
|
|
name="Test Stream",
|
|
url="http://example.com/live/user/pass/1.ts",
|
|
m3u_account=self.account,
|
|
)
|
|
self.channel = Channel.objects.create(channel_number=501, name="Assignment Ch")
|
|
ChannelStream.objects.create(channel=self.channel, stream=self.stream, order=0)
|
|
|
|
self.metadata_key = RedisKeys.channel_metadata(str(self.channel.uuid))
|
|
|
|
def _seed_assignment(self):
|
|
self.redis.set(f"channel_stream:{self.channel.id}", self.stream.id)
|
|
self.redis.set(f"stream_profile:{self.stream.id}", self.profile.id)
|
|
|
|
@patch("apps.channels.models.RedisClient.get_client")
|
|
@patch("apps.channels.models.reserve_profile_slot")
|
|
def test_reuses_assignment_when_proxy_active(
|
|
self, mock_reserve, mock_get_client
|
|
):
|
|
mock_get_client.return_value = self.redis
|
|
self._seed_assignment()
|
|
self.redis.hset(
|
|
self.metadata_key,
|
|
{ChannelMetadataField.STATE: ChannelState.ACTIVE},
|
|
)
|
|
|
|
stream_id, profile_id, error, slot_reserved = self.channel.get_stream()
|
|
|
|
self.assertEqual(stream_id, self.stream.id)
|
|
self.assertEqual(profile_id, self.profile.id)
|
|
self.assertIsNone(error)
|
|
self.assertFalse(slot_reserved)
|
|
mock_reserve.assert_not_called()
|
|
|
|
@patch("apps.channels.models.RedisClient.get_client")
|
|
@patch("apps.channels.models.reserve_profile_slot")
|
|
def test_reuses_assignment_during_init_before_metadata(
|
|
self, mock_reserve, mock_get_client
|
|
):
|
|
mock_get_client.return_value = self.redis
|
|
self._seed_assignment()
|
|
|
|
stream_id, profile_id, error, slot_reserved = self.channel.get_stream()
|
|
|
|
self.assertEqual(stream_id, self.stream.id)
|
|
self.assertEqual(profile_id, self.profile.id)
|
|
self.assertIsNone(error)
|
|
self.assertFalse(slot_reserved)
|
|
mock_reserve.assert_not_called()
|
|
|
|
@patch("apps.channels.models.RedisClient.get_client")
|
|
@patch("apps.channels.models.release_profile_slot")
|
|
@patch("apps.channels.models.reserve_profile_slot")
|
|
def test_releases_stale_assignment_when_proxy_stopped(
|
|
self, mock_reserve, mock_release, mock_get_client
|
|
):
|
|
mock_get_client.return_value = self.redis
|
|
mock_reserve.return_value = (True, 1, None)
|
|
self._seed_assignment()
|
|
self.redis.hset(
|
|
self.metadata_key,
|
|
{ChannelMetadataField.STATE: ChannelState.STOPPED},
|
|
)
|
|
|
|
stream_id, profile_id, error, slot_reserved = self.channel.get_stream()
|
|
|
|
mock_release.assert_called_once_with(self.profile.id, self.redis)
|
|
mock_reserve.assert_called_once()
|
|
self.assertEqual(stream_id, self.stream.id)
|
|
self.assertEqual(profile_id, self.profile.id)
|
|
self.assertTrue(slot_reserved)
|
|
|
|
@patch("apps.channels.models.RedisClient.get_client")
|
|
def test_stream_assignment_is_reusable_during_init_pending(self, mock_get_client):
|
|
mock_get_client.return_value = self.redis
|
|
self._seed_assignment()
|
|
|
|
self.assertTrue(
|
|
self.channel._stream_assignment_is_reusable(self.redis, self.stream.id)
|
|
)
|
|
|
|
@patch("apps.channels.models.RedisClient.get_client")
|
|
def test_stream_assignment_not_reusable_when_stopped(self, mock_get_client):
|
|
mock_get_client.return_value = self.redis
|
|
self._seed_assignment()
|
|
self.redis.hset(
|
|
self.metadata_key,
|
|
{ChannelMetadataField.STATE: ChannelState.STOPPED},
|
|
)
|
|
|
|
self.assertFalse(
|
|
self.channel._stream_assignment_is_reusable(self.redis, self.stream.id)
|
|
)
|