[DEV] Unit test filter plugin (#1066)

This commit is contained in:
Jesse Bannon 2024-09-26 11:54:15 -07:00 committed by GitHub
parent 513d1f5a97
commit fd5882051a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 315 additions and 438 deletions

View file

@ -10,7 +10,7 @@ from ytdl_sub.script.types.resolvable import Boolean
from ytdl_sub.script.types.resolvable import Float
from ytdl_sub.script.types.resolvable import Integer
from ytdl_sub.script.types.resolvable import String
from ytdl_sub.script.utils.exceptions import RuntimeException
from ytdl_sub.script.utils.exceptions import FunctionRuntimeException
def _re_output_to_array(re_out: Match[AnyStr] | None) -> Array:
@ -115,12 +115,12 @@ class RegexFunctions:
# ["2020-02-27", "2020", "02"]
"""
if len(regex_array) == 0:
raise RuntimeException("regex_array must contain at least a single element")
raise FunctionRuntimeException("regex_array must contain at least a single element")
regex_list: List[String] = []
for regex in regex_array.value:
if not isinstance(regex, String):
raise RuntimeException("All regex_array elements must be strings")
raise FunctionRuntimeException("All regex_array elements must be strings")
regex_list.append(regex)
if default is None:
@ -129,13 +129,13 @@ class RegexFunctions:
RegexFunctions.regex_capture_groups(regex).value != num_capture_groups
for regex in regex_list[1:]
):
raise RuntimeException(
raise FunctionRuntimeException(
"regex_array elements must contain the same number of capture groups"
)
elif any(
RegexFunctions.regex_capture_groups(regex).value > len(default) for regex in regex_list
):
raise RuntimeException(
raise FunctionRuntimeException(
"When using %regex_capture_array, number of regex capture groups must be less than "
"or equal to the number of defaults"
)
@ -147,7 +147,7 @@ class RegexFunctions:
break
if len(output) == 0 and not default:
raise RuntimeException(
raise FunctionRuntimeException(
f"no regex strings were captured for input string {string.value}"
)

View file

@ -1,299 +0,0 @@
import re
from typing import Any
from typing import Dict
import mergedeep
import pytest
from expected_transaction_log import assert_transaction_log_matches
from ytdl_sub.config.config_file import ConfigFile
from ytdl_sub.script.utils.exceptions import RuntimeException
from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError
from ytdl_sub.subscriptions.subscription import Subscription
@pytest.fixture
def regex_subscription_dict_base(output_directory):
return {
"preset": "Jellyfin Music Videos",
# override the output directory with our fixture-generated dir
"output_options": {"output_directory": output_directory},
"format": "best[height<=480]", # download the worst format so it is fast
"filter_include": ["{%not(%is_null(description_website))}"],
"overrides": {
"in_regex_default": "in regex default",
"url": "https://youtube.com/playlist?list=PL5BC0FC26BECA5A35",
"upload_capture": """{
%regex_capture_many_with_defaults(
upload_date_standardized,
["([0-9]+)-([0-9]+)-27"],
["First", %concat("Second containing ", in_regex_default)]
)
}""",
"upload_captured_year": "{%array_at(upload_capture, 1)}",
"upload_captured_month": "{%array_at(upload_capture, 2)}",
"description_capture": """{
%regex_capture_many_with_defaults(
description,
[".*http:\\/\\/(.+).com.*"],
[null]
)
}""",
"description_website": "{%array_at(description_capture, 1)}",
},
}
@pytest.fixture
def regex_subscription_dict(regex_subscription_dict_base, output_directory):
return mergedeep.merge(
regex_subscription_dict_base,
{
"nfo_tags": {
"tags": {
"title_cap_1": "{title_type}",
"title_cap_1_sanitized": "{title_type_sanitized}",
"title_cap_2": "{title_date}",
"desc_cap": "{description_website}",
"upload_date_both_caps": "{upload_captured_year} and {upload_captured_month}",
"override_with_capture_variable": "{contains_regex_default}",
"override_with_capture_variable_sanitized": "{contains_regex_sanitized_default}",
}
},
"filter_exclude": ["{%is_null(title_type)}"],
"overrides": {
"title_capture_list": """{
%regex_capture_many_with_defaults(
title,
[ "should not cap (.+) - (.+)", ".*\\[(.+) - (Feb.+)]" ],
[ null, null ]
)
}""",
"title_type": "{%array_at(title_capture_list, 1)}",
"title_date": "{%array_at(title_capture_list, 2)}",
"contains_regex_default": "contains {title_type}",
"contains_regex_sanitized_default": "contains {title_type_sanitized}",
},
},
)
@pytest.fixture
def regex_subscription_dict_exclude(regex_subscription_dict_base, output_directory):
return mergedeep.merge(
regex_subscription_dict_base,
{
"filter_exclude": [
"""{
%regex_search_any(
title,
[ "should not cap", ".*Feb.*" ]
)
}"""
]
},
)
@pytest.fixture
def regex_subscription_dict_match_and_exclude(regex_subscription_dict_base, output_directory):
return mergedeep.merge(
regex_subscription_dict_base,
{
"regex": {
# tests that skip_if_match_fails defaults to True
"from": {
"title": {
"match": [
"should not cap (.+) - (.+)",
".*\\[(.+) - (Feb.+)]", # should filter out march video
],
"capture_group_names": ["title_type", "title_date"],
"exclude": [
"should not cap",
".*27.*", # should filter out Feb 27th video
],
},
},
},
"nfo_tags": {
"tags": {
"title_cap_1": "{title_type}",
"title_cap_1_sanitized": "{title_type_sanitized}",
"title_cap_2": "{title_date}",
"desc_cap": "{description_website}",
"upload_date_both_caps": "{upload_captured_year} and {upload_captured_month}",
"override_with_capture_variable": "{contains_regex_default}",
"override_with_capture_variable_sanitized": "{contains_regex_sanitized_default}",
}
},
"overrides": {
"contains_regex_default": "contains {title_type}",
"contains_regex_sanitized_default": "contains {title_type_sanitized}",
},
},
)
@pytest.fixture
def regex_subscription_dict_match_and_exclude_override_variable(
regex_subscription_dict_base, output_directory
):
return mergedeep.merge(
regex_subscription_dict_base,
{
"regex": {
# tests that skip_if_match_fails defaults to True
"from": {
"override_title": {
"exclude": [
"should not cap",
".*Feb.*", # should filter out march video
],
},
"override_description": {
"match": [".*http:\\/\\/(.+).com.*"],
"capture_group_names": ["override_description_website"],
},
},
},
"overrides": {"override_title": "{title}", "override_description": "{description}"},
},
)
@pytest.fixture
def playlist_subscription(default_config, regex_subscription_dict):
return Subscription.from_dict(
config=default_config,
preset_name="regex_capture_playlist_test",
preset_dict=regex_subscription_dict,
)
@pytest.fixture
def playlist_subscription_no_match_fails(
default_config: ConfigFile, regex_subscription_dict: Dict[str, Any]
):
regex_subscription_dict["overrides"][
"title_capture_list"
] = """{
%regex_capture_many_required(
title,
[ "should not cap (.+) - (.+)", ".*\\[(.+) - (Feb.+)]" ]
)
}"""
return Subscription.from_dict(
config=default_config,
preset_name="regex_capture_playlist_test",
preset_dict=regex_subscription_dict,
)
@pytest.fixture
def playlist_subscription_exclude(
default_config: ConfigFile, regex_subscription_dict_exclude: Dict[str, Any]
) -> Subscription:
return Subscription.from_dict(
config=default_config,
preset_name="regex_exclude_playlist_test",
preset_dict=regex_subscription_dict_exclude,
)
@pytest.fixture
def playlist_subscription_overrides(
default_config: ConfigFile,
regex_subscription_dict_match_and_exclude_override_variable: Dict[str, Any],
) -> Subscription:
return Subscription.from_dict(
config=default_config,
preset_name="regex_using_overrides_test",
preset_dict=regex_subscription_dict_match_and_exclude_override_variable,
)
@pytest.fixture
def playlist_subscription_match_and_exclude(
default_config: ConfigFile, regex_subscription_dict_match_and_exclude: Dict[str, Any]
) -> Subscription:
return Subscription.from_dict(
config=default_config,
preset_name="regex_match_and_exclude_playlist_test",
preset_dict=regex_subscription_dict_match_and_exclude,
)
class TestFilter:
def test_regex_success(self, playlist_subscription, output_directory):
# Only dry run is needed to see if capture variables are created
transaction_log = playlist_subscription.download(dry_run=True)
assert_transaction_log_matches(
output_directory=output_directory,
transaction_log=transaction_log,
transaction_log_summary_file_name="plugins/test_regex.txt",
)
def test_regex_excludes_success(self, playlist_subscription_exclude, output_directory):
# Should only contain the march video
transaction_log = playlist_subscription_exclude.download(dry_run=True)
assert_transaction_log_matches(
output_directory=output_directory,
transaction_log=transaction_log,
transaction_log_summary_file_name="plugins/test_regex_exclude.txt",
)
def test_regex_match_and_excludes_success(
self, playlist_subscription_match_and_exclude, output_directory
):
# Should only contain the Feb 1st video
transaction_log = playlist_subscription_match_and_exclude.download(dry_run=True)
assert_transaction_log_matches(
output_directory=output_directory,
transaction_log=transaction_log,
transaction_log_summary_file_name="plugins/test_regex_match_and_exclude.txt",
)
def test_regex_using_overrides_success(self, playlist_subscription_overrides, output_directory):
# Should only contain the march video
transaction_log = playlist_subscription_overrides.download(dry_run=True)
assert_transaction_log_matches(
output_directory=output_directory,
transaction_log=transaction_log,
transaction_log_summary_file_name="plugins/test_regex_overrides.txt",
)
def test_regex_fails_no_match(self, playlist_subscription_no_match_fails, output_directory):
with pytest.raises(
RuntimeException,
match=re.escape(
"no regex strings were captured for input string Jesse's Minecraft Server [Trailer - Mar.21]"
),
):
_ = playlist_subscription_no_match_fails.download(dry_run=True)
def test_regex_fails_unequal_defaults(self, regex_subscription_dict, default_config):
regex_subscription_dict["overrides"][
"title_capture_list"
] = """{
%regex_capture_many_with_defaults(
title,
[ "should not cap (.+) - (.+)", ".*\\[(.+) - (Feb.+)]" ],
[ "one default, expects >= 2" ]
)
}"""
subscription = Subscription.from_dict(
config=default_config,
preset_name="regex_capture_playlist_test",
preset_dict=regex_subscription_dict,
)
with pytest.raises(
RuntimeException,
match=re.escape(
"When using %regex_capture_array, number of regex capture groups must be less than or equal to the number of defaults"
),
):
_ = subscription.download(dry_run=True)

View file

@ -0,0 +1,105 @@
Files created:
----------------------------------------
{output_directory}
.ytdl-sub-subscription_test-download-archive.json
{output_directory}/subscription_test
Mock Entry 20-1.info.json
Mock Entry 20-1.jpg
Mock Entry 20-1.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-08
title: Mock Entry 20-1
year: 2020
Mock Entry 20-1.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
desc_cap: Description
genre: ytdl-sub
override_with_capture_variable: contains 20
override_with_capture_variable_sanitized: contains 20
premiered: 2020-08-08
title: Mock Entry 20-1
title_cap_1: 20
title_cap_1_sanitized: 20
title_cap_2: 1
upload_date_both_caps: First and Second containing in regex default
Mock Entry 20-2.info.json
Mock Entry 20-2.jpg
Mock Entry 20-2.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-08
title: Mock Entry 20-2
year: 2020
Mock Entry 20-2.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
desc_cap: Description
genre: ytdl-sub
override_with_capture_variable: contains 20
override_with_capture_variable_sanitized: contains 20
premiered: 2020-08-08
title: Mock Entry 20-2
title_cap_1: 20
title_cap_1_sanitized: 20
title_cap_2: 2
upload_date_both_caps: First and Second containing in regex default
Mock Entry 20-3.info.json
Mock Entry 20-3.jpg
Mock Entry 20-3.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-07
title: Mock Entry 20-3
year: 2020
Mock Entry 20-3.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
desc_cap: Description
genre: ytdl-sub
override_with_capture_variable: contains 20
override_with_capture_variable_sanitized: contains 20
premiered: 2020-08-07
title: Mock Entry 20-3
title_cap_1: 20
title_cap_1_sanitized: 20
title_cap_2: 3
upload_date_both_caps: First and Second containing in regex default
Mock Entry 21-1.info.json
Mock Entry 21-1.jpg
Mock Entry 21-1.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2021-08-08
title: Mock Entry 21-1
year: 2021
Mock Entry 21-1.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
desc_cap: Description
genre: ytdl-sub
override_with_capture_variable: contains 21
override_with_capture_variable_sanitized: contains 21
premiered: 2021-08-08
title: Mock Entry 21-1
title_cap_1: 21
title_cap_1_sanitized: 21
title_cap_2: 1
upload_date_both_caps: First and Second containing in regex default

View file

@ -0,0 +1,23 @@
Files created:
----------------------------------------
{output_directory}
.ytdl-sub-subscription_test-download-archive.json
{output_directory}/subscription_test
Mock Entry 21-1.info.json
Mock Entry 21-1.jpg
Mock Entry 21-1.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2021-08-08
title: Mock Entry 21-1
year: 2021
Mock Entry 21-1.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2021-08-08
title: Mock Entry 21-1

View file

@ -1,55 +0,0 @@
Files created:
----------------------------------------
{output_directory}
.ytdl-sub-regex_capture_playlist_test-download-archive.json
{output_directory}/regex_capture_playlist_test
Jesse's Minecraft Server [Trailer - Feb.1].info.json
Jesse's Minecraft Server [Trailer - Feb.1].jpg
Jesse's Minecraft Server [Trailer - Feb.1].mp4
Video Tags:
album: Music Videos
artist: regex_capture_playlist_test
genre: ytdl-sub
premiered: 2011-02-01
title: Jesse's Minecraft Server [Trailer - Feb.1]
year: 2011
Jesse's Minecraft Server [Trailer - Feb.1].nfo
NFO tags:
musicvideo:
album: Music Videos
artist: regex_capture_playlist_test
desc_cap: www.jesseminecraft.webs
genre: ytdl-sub
override_with_capture_variable: contains Trailer
override_with_capture_variable_sanitized: contains Trailer
premiered: 2011-02-01
title: Jesse's Minecraft Server [Trailer - Feb.1]
title_cap_1: Trailer
title_cap_1_sanitized: Trailer
title_cap_2: Feb.1
upload_date_both_caps: First and Second containing in regex default
Jesse's Minecraft Server [Trailer - Feb.27].info.json
Jesse's Minecraft Server [Trailer - Feb.27].jpg
Jesse's Minecraft Server [Trailer - Feb.27].mp4
Video Tags:
album: Music Videos
artist: regex_capture_playlist_test
genre: ytdl-sub
premiered: 2011-02-27
title: Jesse's Minecraft Server [Trailer - Feb.27]
year: 2011
Jesse's Minecraft Server [Trailer - Feb.27].nfo
NFO tags:
musicvideo:
album: Music Videos
artist: regex_capture_playlist_test
desc_cap: jesseminecraft.webs
genre: ytdl-sub
override_with_capture_variable: contains Trailer
override_with_capture_variable_sanitized: contains Trailer
premiered: 2011-02-27
title: Jesse's Minecraft Server [Trailer - Feb.27]
title_cap_1: Trailer
title_cap_1_sanitized: Trailer
title_cap_2: Feb.27
upload_date_both_caps: 2011 and 02

View file

@ -1,23 +0,0 @@
Files created:
----------------------------------------
{output_directory}
.ytdl-sub-regex_exclude_playlist_test-download-archive.json
{output_directory}/regex_exclude_playlist_test
Jesse's Minecraft Server [Trailer - Mar.21].info.json
Jesse's Minecraft Server [Trailer - Mar.21].jpg
Jesse's Minecraft Server [Trailer - Mar.21].mp4
Video Tags:
album: Music Videos
artist: regex_exclude_playlist_test
genre: ytdl-sub
premiered: 2011-03-21
title: Jesse's Minecraft Server [Trailer - Mar.21]
year: 2011
Jesse's Minecraft Server [Trailer - Mar.21].nfo
NFO tags:
musicvideo:
album: Music Videos
artist: regex_exclude_playlist_test
genre: ytdl-sub
premiered: 2011-03-21
title: Jesse's Minecraft Server [Trailer - Mar.21]

View file

@ -1,30 +0,0 @@
Files created:
----------------------------------------
{output_directory}
.ytdl-sub-regex_match_and_exclude_playlist_test-download-archive.json
{output_directory}/regex_match_and_exclude_playlist_test
Jesse's Minecraft Server [Trailer - Feb.1].info.json
Jesse's Minecraft Server [Trailer - Feb.1].jpg
Jesse's Minecraft Server [Trailer - Feb.1].mp4
Video Tags:
album: Music Videos
artist: regex_match_and_exclude_playlist_test
genre: ytdl-sub
premiered: 2011-02-01
title: Jesse's Minecraft Server [Trailer - Feb.1]
year: 2011
Jesse's Minecraft Server [Trailer - Feb.1].nfo
NFO tags:
musicvideo:
album: Music Videos
artist: regex_match_and_exclude_playlist_test
desc_cap: www.jesseminecraft.webs
genre: ytdl-sub
override_with_capture_variable: contains Trailer
override_with_capture_variable_sanitized: contains Trailer
premiered: 2011-02-01
title: Jesse's Minecraft Server [Trailer - Feb.1]
title_cap_1: Trailer
title_cap_1_sanitized: Trailer
title_cap_2: Feb.1
upload_date_both_caps: First and Second containing in regex default

View file

@ -1,23 +0,0 @@
Files created:
----------------------------------------
{output_directory}
.ytdl-sub-regex_using_overrides_test-download-archive.json
{output_directory}/regex_using_overrides_test
Jesse's Minecraft Server [Trailer - Mar.21].info.json
Jesse's Minecraft Server [Trailer - Mar.21].jpg
Jesse's Minecraft Server [Trailer - Mar.21].mp4
Video Tags:
album: Music Videos
artist: regex_using_overrides_test
genre: ytdl-sub
premiered: 2011-03-21
title: Jesse's Minecraft Server [Trailer - Mar.21]
year: 2011
Jesse's Minecraft Server [Trailer - Mar.21].nfo
NFO tags:
musicvideo:
album: Music Videos
artist: regex_using_overrides_test
genre: ytdl-sub
premiered: 2011-03-21
title: Jesse's Minecraft Server [Trailer - Mar.21]

View file

@ -0,0 +1,145 @@
from typing import Any
from typing import Dict
import mergedeep
import pytest
from expected_transaction_log import assert_transaction_log_matches
from ytdl_sub.config.config_file import ConfigFile
from ytdl_sub.subscriptions.subscription import Subscription
@pytest.fixture
def regex_subscription_dict_base(output_directory):
return {
"preset": "Jellyfin Music Videos",
# override the output directory with our fixture-generated dir
"output_options": {"output_directory": output_directory},
"format": "best[height<=480]", # download the worst format so it is fast
"filter_include": ["{%not(%is_null(description_website))}"],
"overrides": {
"in_regex_default": "in regex default",
"url": "https://youtube.com/playlist?list=PL5BC0FC26BECA5A35",
"upload_capture": """{
%regex_capture_many_with_defaults(
upload_date_standardized,
["([0-9]+)-([0-9]+)-27"],
["First", %concat("Second containing ", in_regex_default)]
)
}""",
"upload_captured_year": "{%array_at(upload_capture, 1)}",
"upload_captured_month": "{%array_at(upload_capture, 2)}",
"description_capture": """{
%regex_capture_many_with_defaults(
description,
["The (.*)"],
[null]
)
}""",
"description_website": "{%array_at(description_capture, 1)}",
},
}
@pytest.fixture
def regex_subscription_dict(regex_subscription_dict_base, output_directory):
return mergedeep.merge(
regex_subscription_dict_base,
{
"nfo_tags": {
"tags": {
"title_cap_1": "{title_type}",
"title_cap_1_sanitized": "{title_type_sanitized}",
"title_cap_2": "{title_date}",
"desc_cap": "{description_website}",
"upload_date_both_caps": "{upload_captured_year} and {upload_captured_month}",
"override_with_capture_variable": "{contains_regex_default}",
"override_with_capture_variable_sanitized": "{contains_regex_sanitized_default}",
}
},
"filter_exclude": ["{%is_null(title_type)}"],
"overrides": {
"title_capture_list": """{
%regex_capture_many_with_defaults(
title,
[ "should not cap (.+) - (.+)", "Mock Entry (.*)-(.*)" ],
[ null, null ]
)
}""",
"title_type": "{%array_at(title_capture_list, 1)}",
"title_date": "{%array_at(title_capture_list, 2)}",
"contains_regex_default": "contains {title_type}",
"contains_regex_sanitized_default": "contains {title_type_sanitized}",
},
},
)
@pytest.fixture
def regex_subscription_dict_exclude(regex_subscription_dict_base, output_directory):
return mergedeep.merge(
regex_subscription_dict_base,
{
"filter_exclude": [
"""{
%regex_search_any(
title,
[ "should not cap", "Mock Entry 20-.*" ]
)
}"""
]
},
)
@pytest.fixture
def playlist_subscription(
config: ConfigFile, subscription_name: str, regex_subscription_dict: Dict[str, Any]
):
return Subscription.from_dict(
config=config,
preset_name=subscription_name,
preset_dict=regex_subscription_dict,
)
@pytest.fixture
def playlist_subscription_exclude(
config: ConfigFile, subscription_name: str, regex_subscription_dict_exclude: Dict[str, Any]
) -> Subscription:
return Subscription.from_dict(
config=config,
preset_name=subscription_name,
preset_dict=regex_subscription_dict_exclude,
)
class TestFilter:
def test_filter_regex_success(
self, mock_download_collection_entries, playlist_subscription, output_directory
):
with mock_download_collection_entries(
is_youtube_channel=False, num_urls=1, is_extracted_audio=False, is_dry_run=True
):
transaction_log = playlist_subscription.download(dry_run=True)
assert_transaction_log_matches(
output_directory=output_directory,
transaction_log=transaction_log,
transaction_log_summary_file_name="plugins/filter/test_regex.txt",
)
def test_filter_regex_excludes_success(
self, mock_download_collection_entries, playlist_subscription_exclude, output_directory
):
# Should only contain the march video
with mock_download_collection_entries(
is_youtube_channel=False, num_urls=1, is_extracted_audio=False, is_dry_run=True
):
transaction_log = playlist_subscription_exclude.download(dry_run=True)
assert_transaction_log_matches(
output_directory=output_directory,
transaction_log=transaction_log,
transaction_log_summary_file_name="plugins/filter/test_regex_exclude.txt",
)

View file

@ -1,10 +1,10 @@
import pytest
from unit.script.conftest import single_variable_output
from ytdl_sub.script.script import Script
from ytdl_sub.script.utils.exceptions import FunctionRuntimeException
class TestNumericFunctions:
class TestRegexFunctions:
@pytest.mark.parametrize(
"values, expected_output",
[
@ -59,3 +59,37 @@ class TestNumericFunctions:
def test_regex_sub(self, values: str, expected_output: str):
output = single_variable_output(f"{{%regex_sub({values})}}")
assert output == expected_output
def test_regex_capture_many_fails_no_match(self):
with pytest.raises(
FunctionRuntimeException,
match="no regex strings were captured for input string the string",
):
single_variable_output(
f"""{{
%regex_capture_many(
"the string",
[
"no (.*) match",
"not here either (.*)"
]
)
}}"""
)
def test_regex_capture_many_fails_unequal_capture_groups(self):
with pytest.raises(
FunctionRuntimeException,
match="regex_array elements must contain the same number of capture groups",
):
single_variable_output(
f"""{{
%regex_capture_many(
"the string",
[
"no (.*) match",
"(.*) not equal to 1 (.*)"
]
)
}}"""
)