TwitchDownloader/test_twitch_archive.py

592 lines
22 KiB
Python
Raw Permalink Normal View History

"""
Unit tests for Twitch Archive command-line options and configuration.
Tests focus on:
- Command-line argument parsing (via getopt simulation)
- Options and option combinations
- Configuration loading and merging
- Mode selection logic
Excludes actual download/processing functionality.
To run these tests:
python test_twitch_archive.py
or
python -m pytest test_twitch_archive.py -v
"""
import unittest
import sys
import os
import json
import tempfile
import shutil
import getopt
from unittest.mock import patch, MagicMock, Mock
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from modules.config import ConfigManager
from modules.constants import DEFAULT_CONFIG
class TestCommandLineArgumentParsing(unittest.TestCase):
"""Test command-line argument parsing logic using getopt directly."""
def setUp(self):
"""Set up test fixtures."""
self.test_dir = tempfile.mkdtemp()
self.original_cwd = os.getcwd()
os.chdir(self.test_dir)
def tearDown(self):
"""Clean up test fixtures."""
os.chdir(self.original_cwd)
shutil.rmtree(self.test_dir, ignore_errors=True)
def test_help_short_option(self):
"""Test -h option parsing."""
argv = ['-h']
opts, args = getopt.getopt(
argv,
"hu:q:a:v:c:m:r:d:n:",
["help", "username=", "quality=", "ttv-lol=", "vod=", "chat=",
"metadata=", "upload=", "delete=", "notifications=", "legacy", "verbose",
"chat-only", "use-chat-downloader-primary", "no-chat-downloader-fallback"]
)
# Should parse successfully
self.assertEqual(len(opts), 1)
self.assertEqual(opts[0][0], '-h')
def test_help_long_option(self):
"""Test --help option parsing."""
argv = ['--help']
opts, args = getopt.getopt(
argv,
"hu:q:a:v:c:m:r:d:n:",
["help", "username=", "quality=", "ttv-lol=", "vod=", "chat=",
"metadata=", "upload=", "delete=", "notifications=", "legacy", "verbose",
"chat-only", "use-chat-downloader-primary", "no-chat-downloader-fallback"]
)
self.assertEqual(len(opts), 1)
self.assertEqual(opts[0][0], '--help')
def test_username_short_option(self):
"""Test -u username option parsing."""
argv = ['-u', 'teststreamer']
opts, args = getopt.getopt(
argv,
"h:u:q:a:v:c:m:r:d:n:",
["help", "username=", "quality=", "ttv-lol=", "vod=", "chat=",
"metadata=", "upload=", "delete=", "notifications=", "legacy", "verbose",
"chat-only", "use-chat-downloader-primary", "no-chat-downloader-fallback"]
)
self.assertEqual(len(opts), 1)
self.assertEqual(opts[0], ('-u', 'teststreamer'))
def test_username_long_option(self):
"""Test --username option parsing."""
argv = ['--username', 'teststreamer']
opts, args = getopt.getopt(
argv,
"h:u:q:a:v:c:m:r:d:n:",
["help", "username=", "quality=", "ttv-lol=", "vod=", "chat=",
"metadata=", "upload=", "delete=", "notifications=", "legacy", "verbose",
"chat-only", "use-chat-downloader-primary", "no-chat-downloader-fallback"]
)
self.assertEqual(len(opts), 1)
self.assertEqual(opts[0], ('--username', 'teststreamer'))
def test_verbose_option(self):
"""Test --verbose option parsing."""
argv = ['--verbose']
opts, args = getopt.getopt(
argv,
"h:u:q:a:v:c:m:r:d:n:",
["help", "username=", "quality=", "ttv-lol=", "vod=", "chat=",
"metadata=", "upload=", "delete=", "notifications=", "legacy", "verbose",
"chat-only", "use-chat-downloader-primary", "no-chat-downloader-fallback"]
)
self.assertEqual(len(opts), 1)
self.assertEqual(opts[0], ('--verbose', ''))
def test_chat_only_option(self):
"""Test --chat-only option parsing."""
argv = ['--chat-only']
opts, args = getopt.getopt(
argv,
"h:u:q:a:v:c:m:r:d:n:",
["help", "username=", "quality=", "ttv-lol=", "vod=", "chat=",
"metadata=", "upload=", "delete=", "notifications=", "legacy", "verbose",
"chat-only", "use-chat-downloader-primary", "no-chat-downloader-fallback"]
)
self.assertEqual(len(opts), 1)
self.assertEqual(opts[0], ('--chat-only', ''))
def test_legacy_option(self):
"""Test --legacy option parsing."""
argv = ['--legacy']
opts, args = getopt.getopt(
argv,
"h:u:q:a:v:c:m:r:d:n:",
["help", "username=", "quality=", "ttv-lol=", "vod=", "chat=",
"metadata=", "upload=", "delete=", "notifications=", "legacy", "verbose",
"chat-only", "use-chat-downloader-primary", "no-chat-downloader-fallback"]
)
self.assertEqual(len(opts), 1)
self.assertEqual(opts[0], ('--legacy', ''))
def test_chat_downloader_options(self):
"""Test chat downloader option parsing."""
argv = ['--use-chat-downloader-primary', '--no-chat-downloader-fallback']
opts, args = getopt.getopt(
argv,
"h:u:q:a:v:c:m:r:d:n:",
["help", "username=", "quality=", "ttv-lol=", "vod=", "chat=",
"metadata=", "upload=", "delete=", "notifications=", "legacy", "verbose",
"chat-only", "use-chat-downloader-primary", "no-chat-downloader-fallback"]
)
self.assertEqual(len(opts), 2)
self.assertEqual(opts[0], ('--use-chat-downloader-primary', ''))
self.assertEqual(opts[1], ('--no-chat-downloader-fallback', ''))
def test_legacy_quality_option(self):
"""Test -q quality option parsing."""
argv = ['-q', '720p']
opts, args = getopt.getopt(
argv,
"h:u:q:a:v:c:m:r:d:n:",
["help", "username=", "quality=", "ttv-lol=", "vod=", "chat=",
"metadata=", "upload=", "delete=", "notifications=", "legacy", "verbose",
"chat-only", "use-chat-downloader-primary", "no-chat-downloader-fallback"]
)
self.assertEqual(len(opts), 1)
self.assertEqual(opts[0], ('-q', '720p'))
def test_legacy_boolean_options(self):
"""Test legacy boolean option parsing."""
argv = ['-v', '1', '-c', '0', '-m', '1']
opts, args = getopt.getopt(
argv,
"h:u:q:a:v:c:m:r:d:n:",
["help", "username=", "quality=", "ttv-lol=", "vod=", "chat=",
"metadata=", "upload=", "delete=", "notifications=", "legacy", "verbose",
"chat-only", "use-chat-downloader-primary", "no-chat-downloader-fallback"]
)
self.assertEqual(len(opts), 3)
self.assertEqual(opts[0], ('-v', '1'))
self.assertEqual(opts[1], ('-c', '0'))
self.assertEqual(opts[2], ('-m', '1'))
def test_invalid_option(self):
"""Test that invalid option raises error."""
argv = ['--invalid-option']
with self.assertRaises(getopt.GetoptError):
getopt.getopt(
argv,
"h:u:q:a:v:c:m:r:d:n:",
["help", "username=", "quality=", "ttv-lol=", "vod=", "chat=",
"metadata=", "upload=", "delete=", "notifications=", "legacy", "verbose",
"chat-only", "use-chat-downloader-primary", "no-chat-downloader-fallback"]
)
def test_option_combination_username_verbose(self):
"""Test combining -u and --verbose options."""
argv = ['-u', 'testuser', '--verbose']
opts, args = getopt.getopt(
argv,
"h:u:q:a:v:c:m:r:d:n:",
["help", "username=", "quality=", "ttv-lol=", "vod=", "chat=",
"metadata=", "upload=", "delete=", "notifications=", "legacy", "verbose",
"chat-only", "use-chat-downloader-primary", "no-chat-downloader-fallback"]
)
self.assertEqual(len(opts), 2)
self.assertEqual(opts[0], ('-u', 'testuser'))
self.assertEqual(opts[1], ('--verbose', ''))
def test_option_combination_all_test_flags(self):
"""Test combining all test-related flags."""
argv = ['-u', 'testuser', '--verbose', '--chat-only', '--use-chat-downloader-primary']
opts, args = getopt.getopt(
argv,
"h:u:q:a:v:c:m:r:d:n:",
["help", "username=", "quality=", "ttv-lol=", "vod=", "chat=",
"metadata=", "upload=", "delete=", "notifications=", "legacy", "verbose",
"chat-only", "use-chat-downloader-primary", "no-chat-downloader-fallback"]
)
self.assertEqual(len(opts), 4)
opt_dict = dict(opts)
self.assertEqual(opt_dict['-u'], 'testuser')
self.assertIn('--verbose', opt_dict)
self.assertIn('--chat-only', opt_dict)
self.assertIn('--use-chat-downloader-primary', opt_dict)
def test_option_combination_legacy_mode_with_overrides(self):
"""Test legacy mode with multiple overrides."""
argv = ['--legacy', '-q', '720p', '-v', '1', '-c', '1', '-m', '0']
opts, args = getopt.getopt(
argv,
"h:u:q:a:v:c:m:r:d:n:",
["help", "username=", "quality=", "ttv-lol=", "vod=", "chat=",
"metadata=", "upload=", "delete=", "notifications=", "legacy", "verbose",
"chat-only", "use-chat-downloader-primary", "no-chat-downloader-fallback"]
)
self.assertEqual(len(opts), 5)
opt_dict = dict(opts)
self.assertIn('--legacy', opt_dict)
self.assertEqual(opt_dict['-q'], '720p')
self.assertEqual(opt_dict['-v'], '1')
self.assertEqual(opt_dict['-c'], '1')
self.assertEqual(opt_dict['-m'], '0')
class TestOptionLogicProcessing(unittest.TestCase):
"""Test the logic that processes parsed options."""
def test_boolean_conversion_true(self):
"""Test converting '1' to boolean True."""
value = '1'
result = bool(int(value))
self.assertTrue(result)
def test_boolean_conversion_false(self):
"""Test converting '0' to boolean False."""
value = '0'
result = bool(int(value))
self.assertFalse(result)
def test_chat_only_auto_enables_verbose(self):
"""Test that chat-only mode should auto-enable verbose."""
# Simulate the logic from main()
chat_only_mode = True
verbose_mode = False
if chat_only_mode:
verbose_mode = True
self.assertTrue(verbose_mode)
def test_default_chat_downloader_fallback(self):
"""Test that chat downloader fallback defaults to enabled."""
use_chat_downloader_fallback = True # Default
# Unless explicitly disabled
self.assertTrue(use_chat_downloader_fallback)
def test_mode_selection_legacy_with_config_json(self):
"""Test mode selection logic when config.json exists."""
# Simulate conditions
use_legacy_mode = False
legacy_config_exists = True
specific_streamer = None
global_config_exists = False
# Logic from main()
should_use_legacy = use_legacy_mode or (
legacy_config_exists and not specific_streamer and not global_config_exists
)
self.assertTrue(should_use_legacy)
def test_mode_selection_multi_streamer_with_global_json(self):
"""Test mode selection logic when global.json exists."""
use_legacy_mode = False
legacy_config_exists = True
specific_streamer = None
global_config_exists = True
should_use_legacy = use_legacy_mode or (
legacy_config_exists and not specific_streamer and not global_config_exists
)
self.assertFalse(should_use_legacy)
def test_mode_selection_multi_streamer_with_username_flag(self):
"""Test mode selection when -u flag is used."""
use_legacy_mode = False
legacy_config_exists = True
specific_streamer = 'testuser'
global_config_exists = False
should_use_legacy = use_legacy_mode or (
legacy_config_exists and not specific_streamer and not global_config_exists
)
self.assertFalse(should_use_legacy)
def test_mode_selection_explicit_legacy_flag(self):
"""Test mode selection with explicit --legacy flag."""
use_legacy_mode = True
legacy_config_exists = False
specific_streamer = None
global_config_exists = True
should_use_legacy = use_legacy_mode or (
legacy_config_exists and not specific_streamer and not global_config_exists
)
self.assertTrue(should_use_legacy)
class TestConfigManager(unittest.TestCase):
"""Test ConfigManager functionality."""
def setUp(self):
"""Set up test fixtures."""
self.test_dir = tempfile.mkdtemp()
self.original_cwd = os.getcwd()
os.chdir(self.test_dir)
# Create config directory structure
os.makedirs('config/streamers', exist_ok=True)
# Patch ConfigManager to use test directory
self.config_dir_patch = patch.object(
ConfigManager,
'__init__',
lambda self: self._init_with_test_dir(self.test_dir)
)
def _init_with_test_dir(self, test_dir):
"""Initialize ConfigManager with test directory."""
self.config_dir = Path(test_dir) / "config"
self.streamers_dir = self.config_dir / "streamers"
self.global_config = self._load_global_config()
def tearDown(self):
"""Clean up test fixtures."""
os.chdir(self.original_cwd)
shutil.rmtree(self.test_dir, ignore_errors=True)
def test_load_global_config_default(self):
"""Test loading default configuration when global.json doesn't exist."""
# Patch the config_dir to use temp directory
with patch.object(ConfigManager, '_ConfigManager__init__') as mock_init:
manager = ConfigManager.__new__(ConfigManager)
manager.config_dir = Path(self.test_dir) / "config"
manager.streamers_dir = manager.config_dir / "streamers"
manager.global_config = manager._load_global_config()
# Should have default config values
self.assertIsNotNone(manager.global_config)
self.assertIn('username', manager.global_config)
self.assertIn('quality', manager.global_config)
def test_load_global_config_from_file(self):
"""Test loading global configuration from file."""
# Create global config
global_config = {
'quality': '720p',
'downloadVOD': False,
'downloadCHAT': True
}
with open('config/global.json', 'w') as f:
json.dump(global_config, f)
manager = ConfigManager()
# Should merge with defaults
self.assertEqual(manager.global_config['quality'], '720p')
self.assertFalse(manager.global_config['downloadVOD'])
self.assertTrue(manager.global_config['downloadCHAT'])
def test_load_global_config_filters_comments(self):
"""Test that global config filters out comment fields."""
global_config = {
'_comment': 'This is a comment',
'quality': '720p',
'_note': 'Another comment'
}
with open('config/global.json', 'w') as f:
json.dump(global_config, f)
manager = ConfigManager()
# Comments should be filtered out
self.assertNotIn('_comment', manager.global_config)
self.assertNotIn('_note', manager.global_config)
self.assertEqual(manager.global_config['quality'], '720p')
def test_load_global_config_filters_schema(self):
"""Test that global config filters out $schema field."""
global_config = {
'$schema': './global.schema.json',
'quality': '720p'
}
with open('config/global.json', 'w') as f:
json.dump(global_config, f)
manager = ConfigManager()
# $schema should be filtered out
self.assertNotIn('$schema', manager.global_config)
self.assertEqual(manager.global_config['quality'], '720p')
def test_load_streamer_config_new_streamer(self):
"""Test loading config for a new streamer (doesn't exist yet)."""
manager = ConfigManager()
config = manager.load_streamer_config('newstreamer')
# Should create default config
self.assertEqual(config['username'], 'newstreamer')
self.assertTrue(config['enabled'])
# Config file should be created
self.assertTrue(os.path.exists('config/streamers/newstreamer.json'))
def test_load_streamer_config_existing_streamer(self):
"""Test loading config for existing streamer."""
# Create streamer config
streamer_config = {
'username': 'existingstreamer',
'enabled': True,
'quality': 'source',
'downloadVOD': True
}
with open('config/streamers/existingstreamer.json', 'w') as f:
json.dump(streamer_config, f)
manager = ConfigManager()
config = manager.load_streamer_config('existingstreamer')
# Should load streamer config
self.assertEqual(config['username'], 'existingstreamer')
self.assertEqual(config['quality'], 'source')
self.assertTrue(config['downloadVOD'])
def test_load_streamer_config_merges_with_global(self):
"""Test that streamer config merges with global config."""
# Create global config
with open('config/global.json', 'w') as f:
json.dump({
'quality': '720p',
'downloadVOD': True,
'downloadCHAT': False
}, f)
# Create streamer config with override
with open('config/streamers/teststreamer.json', 'w') as f:
json.dump({
'username': 'teststreamer',
'enabled': True,
'quality': 'source' # Override global
}, f)
manager = ConfigManager()
config = manager.load_streamer_config('teststreamer')
# Should have streamer's quality (override)
self.assertEqual(config['quality'], 'source')
# Should have global's downloadVOD (inherited)
self.assertTrue(config['downloadVOD'])
# Should have global's downloadCHAT (inherited)
self.assertFalse(config['downloadCHAT'])
def test_load_streamer_config_filters_comments(self):
"""Test that streamer config filters out comments."""
with open('config/streamers/teststreamer.json', 'w') as f:
json.dump({
'_comment': 'Test comment',
'username': 'teststreamer',
'enabled': True,
'_note': 'Another note'
}, f)
manager = ConfigManager()
config = manager.load_streamer_config('teststreamer')
# Comments should be filtered
self.assertNotIn('_comment', config)
self.assertNotIn('_note', config)
self.assertEqual(config['username'], 'teststreamer')
def test_get_all_enabled_streamers_empty(self):
"""Test getting enabled streamers when none exist."""
manager = ConfigManager()
streamers = manager.get_all_enabled_streamers()
self.assertEqual(len(streamers), 0)
def test_get_all_enabled_streamers_with_enabled(self):
"""Test getting enabled streamers."""
# Create multiple streamer configs
with open('config/streamers/streamer1.json', 'w') as f:
json.dump({'username': 'streamer1', 'enabled': True}, f)
with open('config/streamers/streamer2.json', 'w') as f:
json.dump({'username': 'streamer2', 'enabled': True}, f)
with open('config/streamers/streamer3.json', 'w') as f:
json.dump({'username': 'streamer3', 'enabled': False}, f)
manager = ConfigManager()
streamers = manager.get_all_enabled_streamers()
# Should return only enabled streamers
self.assertEqual(len(streamers), 2)
self.assertIn('streamer1', streamers)
self.assertIn('streamer2', streamers)
self.assertNotIn('streamer3', streamers)
def test_get_all_enabled_streamers_default_enabled(self):
"""Test that streamers are enabled by default if not specified."""
# Create config without explicit enabled field
with open('config/streamers/streamer1.json', 'w') as f:
json.dump({'username': 'streamer1'}, f)
manager = ConfigManager()
streamers = manager.get_all_enabled_streamers()
# Should be included (defaults to enabled)
self.assertIn('streamer1', streamers)
def test_create_default_streamer_config(self):
"""Test creating default streamer config."""
manager = ConfigManager()
manager.create_default_streamer_config('newstreamer')
# File should exist
config_file = Path('config/streamers/newstreamer.json')
self.assertTrue(config_file.exists())
# Should have correct structure
with open(config_file) as f:
config = json.load(f)
self.assertEqual(config['username'], 'newstreamer')
self.assertTrue(config['enabled'])
self.assertIn('$schema', config)
if __name__ == '__main__':
# Run tests with verbose output
print("="*70)
print("TWITCH ARCHIVE - Unit Tests for Options and Configuration")
print("="*70)
print()
unittest.main(verbosity=2)