ytdl-sub/tests/unit/config/test_preset.py
Jesse Bannon 1abe2a44f5
[BACKEND] Refactor validation, preparation for subscription dissect (#1398)
Completely rewrite how subscription validation is performed. Optimizes it quite a bit while also adding backend support for the upcoming `dissect` sub-command, where you can resolve any subscription into its 'raw' form for easier debugging when making scripting changes.
2025-12-30 17:03:59 -08:00

319 lines
11 KiB
Python

import os.path
import re
from pathlib import Path
import pytest
from ytdl_sub.config.preset import Preset
from ytdl_sub.plugins.nfo_tags import NfoTagsOptions
from ytdl_sub.utils.exceptions import ValidationException
class TestPreset:
@pytest.mark.parametrize(
"download_value",
[
{"url": "youtube.com/watch?v=123abc"}, # url download strategy
{"urls": [{"url": "youtube.com/watch?v=123abc"}]}, # multi-url download strategy
"youtube.com/watch?v=123abc", # single string
["youtube.com/watch?v=123abc", "youtube.com/watch?v=123xyz"], # list of strings
[{"url": "youtube.com/watch?v=123abc"}, "youtube.com/watch?v=123abc"], # dict and str
],
)
def test_bare_minimum_preset(self, config_file, output_options, download_value):
_ = Preset(
config=config_file,
name="test",
value={"download": download_value, "output_options": output_options},
)
def test_preset_with_override_variable(self, config_file, output_options, youtube_video):
_ = Preset(
config=config_file,
name="test",
value={
"download": youtube_video,
"output_options": {"output_directory": "dir", "file_name": "{dne_var}"},
"overrides": {"dne_var": "not dne"},
},
)
def test_preset_with_output_directory_tilda(self, config_file, output_options, youtube_video):
out = Preset(
config=config_file,
name="test",
value={
"download": youtube_video,
"output_options": {"output_directory": "~/output/dir", "file_name": "{dne_var}"},
"overrides": {"dne_var": "not dne"},
},
)
assert out.overrides.apply_formatter(out.output_options.output_directory) == str(
Path(os.path.expanduser("~")) / "output" / "dir"
)
def test_preset_parent(self, config_file, output_options, youtube_video):
preset = Preset(
config=config_file,
name="test",
value={
"preset": "parent_preset_1",
"download": youtube_video,
"output_options": output_options,
"nfo_tags": {"tags": {"key-2": "this-preset"}},
},
)
nfo_options: NfoTagsOptions = preset.plugins.get(NfoTagsOptions)
tags_string_dict = {
key: formatter[0].format_string
for key, formatter in nfo_options.tags.string_tags.items()
}
assert tags_string_dict == {"key-1": "preset_0", "key-2": "this-preset"}
@pytest.mark.parametrize(
"preset_value", [["parent_preset_1", "parent_preset_2"], "parent_preset_3"]
)
def test_preset_multiple_parents(
self, config_file, output_options, youtube_video, preset_value
):
preset = Preset(
config=config_file,
name="test",
value={
"preset": preset_value,
"download": youtube_video,
"output_options": output_options,
"nfo_tags": {"tags": {"key-3": "this-preset"}},
},
)
nfo_options: NfoTagsOptions = preset.plugins.get(NfoTagsOptions)
tags_string_dict = {
key: formatter[0].format_string
for key, formatter in nfo_options.tags.string_tags.items()
}
assert tags_string_dict == {
"key-1": "preset_0",
"key-2": "preset_2",
"key-3": "this-preset",
}
def test_preset_datetime_with_override(self, config_file, youtube_video, output_options):
preset = Preset(
config=config_file,
name="test",
value={
"download": youtube_video,
"output_options": dict(
output_options,
**{"maintain_download_archive": True, "keep_files_after": "today-{ttl}"},
),
"overrides": {"ttl": "2months"},
},
)
assert (
preset.overrides.apply_formatter(formatter=preset.output_options.keep_files_after)
== "today-2months"
)
def test_preset_preserve_mtime_option(self, config_file, youtube_video, output_options):
# Test preserve_mtime defaults to False
preset_default = Preset(
config=config_file,
name="test_default",
value={"download": youtube_video, "output_options": output_options},
)
assert preset_default.output_options.preserve_mtime is False
# Test preserve_mtime can be set to True
preset_enabled = Preset(
config=config_file,
name="test_enabled",
value={
"download": youtube_video,
"output_options": dict(output_options, **{"preserve_mtime": True}),
},
)
assert preset_enabled.output_options.preserve_mtime is True
@pytest.mark.parametrize(
"parent_preset", ["preset_self_loop", "preset_loop_0", "preset_loop_1"]
)
def test_preset_error__parent_loop(
self, config_file, output_options, youtube_video, parent_preset
):
with pytest.raises(ValidationException, match="preset loop detected"):
_ = Preset(
config=config_file,
name="test",
value={
"preset": parent_preset,
"download": youtube_video,
"output_options": output_options,
},
)
def test_preset_with_multi_url__contains_empty_url(self, config_file, output_options):
_ = Preset(
config=config_file,
name="test",
value={
"download": [{"url": "non-empty url"}, {"url": ""}], # empty url
"output_options": output_options,
},
)
@pytest.mark.parametrize(
"entry_variable_name",
[
"title",
"playlist_uid",
"source_title",
"playlist_max_upload_year",
],
)
def test_preset_error_override_variable_collides_with_entry_variable(
self, config_file, output_options, youtube_video, entry_variable_name: str
):
with pytest.raises(
ValidationException,
match=re.escape(
f"Override variable with name {entry_variable_name} cannot be used since"
" it is a built-in ytdl-sub entry variable name."
),
):
_ = Preset(
config=config_file,
name="test",
value={
"download": youtube_video,
"output_options": {"output_directory": "dir", "file_name": "{dne_var}"},
"overrides": {entry_variable_name: "fail"},
},
)
def test_preset_error_override_variable_collides_added_variable(
self, config_file, output_options, youtube_video
):
with pytest.raises(
ValidationException,
match=re.escape(
f"Override variable with name subtitles_ext cannot be used since"
" it is added by a plugin."
),
):
_ = Preset(
config=config_file,
name="test",
value={
"download": youtube_video,
"output_options": {"output_directory": "dir", "file_name": "ack"},
"subtitles": {
"embed_subtitles": True,
},
"overrides": {"subtitles_ext": "collide"},
},
)
@pytest.mark.parametrize(
"function_name",
[
"%extract_field_from_siblings",
"%sanitize",
"%array",
],
)
def test_preset_error_override_variable_collides_with_custom_function(
self, config_file, output_options, youtube_video, function_name: str
):
with pytest.raises(
ValidationException,
match=re.escape(
f"Override function definition with name {function_name} cannot be used since"
" it is a built-in ytdl-sub function name."
),
):
_ = Preset(
config=config_file,
name="test",
value={
"download": youtube_video,
"output_options": {"output_directory": "dir", "file_name": "{dne_var}"},
"overrides": {function_name: "fail"},
},
)
def test_preset_error_override_added_variable_collides_with_built_in(
self, config_file, output_options
):
with pytest.raises(
ValidationException,
match=re.escape(
"Cannot use the variable name title because it exists as a "
"built-in ytdl-sub variable name."
),
):
_ = Preset(
config=config_file,
name="test",
value={
"download": {
"url": "youtube.com/watch?v=123abc",
"variables": {"title": "nope"},
},
"output_options": {"output_directory": "dir", "file_name": "acjk"},
},
)
def test_preset_error_override_added_variable_collides_with_override(
self, config_file, output_options
):
with pytest.raises(
ValidationException,
match=re.escape(
"Override variable with name the_bad_one cannot be used since "
"it is added by a plugin."
),
):
_ = Preset(
config=config_file,
name="test",
value={
"download": {
"url": "youtube.com/watch?v=123abc",
"variables": {"the_bad_one": "should error"},
},
"output_options": {"output_directory": "dir", "file_name": "acjk"},
"overrides": {"the_bad_one": "ack"},
},
)
@pytest.mark.parametrize(
"name", ["!ack", "*asfsaf", "1234352", "--234asdf", "___asdf", "1asdfasdfasd"]
)
@pytest.mark.parametrize("is_function", [True, False])
def test_preset_error_overrides_invalid_variable_name(
self, config_file, youtube_video, output_options, name: str, is_function: bool
):
name_type = "function" if is_function else "variable"
name = f"%{name}" if is_function else name
with pytest.raises(
ValidationException,
match=re.escape(
f"Override {name_type} with name {name} is invalid."
" Names must be lower_snake_cased and begin with a letter."
),
):
_ = Preset(
config=config_file,
name="test",
value={
"download": youtube_video,
"output_options": output_options,
"overrides": {name: "ack"},
},
)