blob: f3b512d8a69cc5ab546a798d20e38cabfcc9cfdd [file]
"""Regression tests for FFmpeg's component source generator."""
import ast
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
import generate_component_srcs as generator
def generated_assignments(lines):
return {
node.targets[0].id: ast.literal_eval(node.value)
for node in ast.parse("\n".join(lines)).body
if isinstance(node, ast.Assign)
}
class ComponentSourcesTest(unittest.TestCase):
def setUp(self):
self.tempdir = tempfile.TemporaryDirectory()
self.addCleanup(self.tempdir.cleanup)
self.root = Path(self.tempdir.name)
self.flags = patch.multiple(
generator,
PROFILE_EVERYTHING_SET={"hevc_decoder", "vvc_decoder", "matroska_muxer", "webm_muxer"},
CONFIG_EXTRA_SET={"dsp", "network"},
STATIC_CONFIG_SET=set(),
)
self.flags.start()
self.addCleanup(self.flags.stop)
def write(self, name, text=""):
path = self.root / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
return path
def process_codec(self, sub_makefiles=()):
return generator.process_library(
self.root,
generator.LibInfo("avcodec", "libavcodec", list(sub_makefiles)),
)
def test_registry_keys_are_read_without_evaluating_starlark(self):
path = self.write(
"component_defs.bzl",
'load(":unused.bzl", "unused")\n'
'PROFILE_EVERYTHING = ["hevc_decoder"] # "not_a_component"\n'
'CONFIG_EXTRA_REGISTRY = {"dsp": {"select": ["other"]}}\n',
)
self.assertEqual(generator.load_profile_everything(path), {"hevc_decoder"})
self.assertEqual(generator.load_registry_keys(path, "CONFIG_EXTRA_REGISTRY"), {"dsp"})
def test_makefile_static_architecture_and_feature_selection(self):
path = self.write(
"Makefile",
"OBJS = base.o \\\n"
" base2.o\n"
"OBJS-$(CONFIG_HEVC_DECODER) += decoder.o \\\n"
" common.o # ignored.o\n"
"STLIBOBJS-$(CONFIG_HEVC_DECODER) += static.o\n"
"SHLIBOBJS-$(CONFIG_HEVC_DECODER) += shared.o\n"
"ifdef ARCH_X86_64\n"
"X86ASM-OBJS-$(CONFIG_HEVC_DECODER) += x86/idct64.o\n"
"else\n"
"X86ASM-OBJS-$(CONFIG_HEVC_DECODER) += x86/idct32.o\n"
"endif\n"
"ifdef CONFIG_GPL\n"
"X86ASM-OBJS-$(CONFIG_HEVC_DECODER) += x86/gpl.o\n"
"endif\n"
"NEON-OBJS-$(CONFIG_HEVC_DECODER) += aarch64/neon.o\n"
"SVE-OBJS-$(CONFIG_HEVC_DECODER) += aarch64/sve.o\n"
"SME2-OBJS-$(CONFIG_HEVC_DECODER) += aarch64/sme2.o\n"
"OBJS-$(CONFIG_UNSTABLE) += unstable.o\n",
)
self.assertEqual(generator.parse_unconditional_objs(path), {"base.o", "base2.o"})
self.assertEqual(
generator.parse_makefile(path),
{"hevc_decoder": ["decoder.o", "common.o", "static.o", "shared.o", "x86/idct64.o", "aarch64/neon.o"]},
)
def test_unconditional_shared_objects_are_emitted_once_per_library(self):
for library in ("avcodec", "avformat"):
directory = "lib" + library
self.write(
directory + "/Makefile",
"OBJS = base.o\n"
"SHLIBOBJS = shared.o base.o\n"
"SHLIBOBJS += shared.o\n"
"OBJS-$(CONFIG_HEVC_DECODER) += shared.o decoder.o\n",
)
for name in ("base", "shared", "decoder"):
self.write(directory + "/" + name + ".c")
sources = generator.process_library(self.root, generator.LibInfo(library, directory, []))
with self.subTest(library=library):
self.assertEqual(sources.unconditional, [directory + "/shared.c"])
self.assertEqual(sources.components, ({"hevc_decoder": [directory + "/decoder.c"]}, []))
def test_conditional_shared_objects_keep_all_component_selections(self):
self.write(
"libavformat/Makefile",
"SHLIBOBJS-$(CONFIG_MATROSKA_MUXER) += opus_frame_duration_tab.o\n"
"SHLIBOBJS-$(CONFIG_WEBM_MUXER) += opus_frame_duration_tab.o\n"
"OBJS-$(CONFIG_MATROSKA_MUXER) += opus_frame_duration_tab.o\n",
)
self.write("libavformat/opus_frame_duration_tab.c")
sources = generator.process_library(self.root, generator.LibInfo("avformat", "libavformat", []))
self.assertEqual(sources.unconditional, [])
self.assertEqual(sources.components.exclusive, {})
self.assertEqual(
sources.components.shared,
[(["matroska_muxer", "webm_muxer"], ["libavformat/opus_frame_duration_tab.c"])],
)
def test_sources_shared_by_components_and_subsystems_appear_once(self):
self.write(
"libavcodec/Makefile",
"OBJS = base.o\n"
"OBJS-$(CONFIG_HEVC_DECODER) += base.o decoder.o shared.o missing.o\n"
"OBJS-$(CONFIG_VVC_DECODER) += shared.o\n"
"OBJS-$(CONFIG_DSP) += dsp.o shared.o\n",
)
for name in ["base", "decoder", "shared", "dsp"]:
self.write("libavcodec/" + name + ".c")
sources = self.process_codec()
self.assertEqual(sources.components.exclusive, {"hevc_decoder": ["libavcodec/decoder.c"]})
self.assertEqual(sources.components.shared, [])
self.assertEqual(sources.extra.exclusive, {"dsp": ["libavcodec/dsp.c"]})
self.assertEqual(
sources.extra.shared,
[(["dsp", "hevc_decoder", "vvc_decoder"], ["libavcodec/shared.c"])],
)
def test_static_sub_makefile_objects_keep_library_relative_names(self):
self.write("libavcodec/Makefile", "OBJS = base.o\n")
self.write(
"libavcodec/opus/Makefile",
"STLIBOBJS-$(CONFIG_MATROSKA_MUXER) += opus/frame_duration_tab.o\n"
"STLIBOBJS-$(CONFIG_WEBM_MUXER) += opus/frame_duration_tab.o\n",
)
self.write("libavcodec/opus/frame_duration_tab.c")
sources = self.process_codec(["opus"])
self.assertEqual(
sources.components.shared,
[(["matroska_muxer", "webm_muxer"], ["libavcodec/opus/frame_duration_tab.c"])],
)
def test_libjxl_animation_selects_sources_without_still_image_flags(self):
generator.PROFILE_EVERYTHING_SET = {
"libjxl_anim_decoder",
"libjxl_anim_encoder",
"libjxl_decoder",
"libjxl_encoder",
}
self.write(
"libavcodec/Makefile",
"OBJS-$(CONFIG_LIBJXL_DECODER) += libjxldec.o libjxl.o\n"
"OBJS-$(CONFIG_LIBJXL_ENCODER) += libjxlenc.o libjxl.o\n",
)
for name in ("libjxl", "libjxldec", "libjxlenc"):
self.write("libavcodec/" + name + ".c")
sources = self.process_codec()
for component_type, implementation in (("decoder", "libjxldec"), ("encoder", "libjxlenc")):
animation = f"libjxl_anim_{component_type}"
selected = list(sources.components.exclusive.get(animation, []))
for components, files in sources.components.shared:
if animation in components:
selected.extend(files)
with self.subTest(component=animation):
self.assertEqual(sorted(selected), ["libavcodec/libjxl.c", f"libavcodec/{implementation}.c"])
def test_libjxl_animation_does_not_change_older_profiles(self):
generator.PROFILE_EVERYTHING_SET = {"libjxl_decoder", "libjxl_encoder"}
self.write(
"libavcodec/Makefile",
"OBJS-$(CONFIG_LIBJXL_DECODER) += libjxldec.o libjxl.o\n"
"OBJS-$(CONFIG_LIBJXL_ENCODER) += libjxlenc.o libjxl.o\n",
)
self.assertEqual(
generator._collect_generic_mapping(self.root, generator.LibInfo("avcodec", "libavcodec", []), set()),
{
"libjxl_decoder": ["libjxldec.o", "libjxl.o"],
"libjxl_encoder": ["libjxlenc.o", "libjxl.o"],
},
)
def test_libjxl_animation_preserves_explicit_makefile_entries(self):
for component_type in ("decoder", "encoder"):
animation = f"libjxl_anim_{component_type}"
still_image = f"libjxl_{component_type}"
generator.PROFILE_EVERYTHING_SET = {animation, still_image}
for sub_makefiles in ([], ["jxl"]):
for objects in ("animation.o", ""):
with self.subTest(component=animation, sub_makefiles=sub_makefiles, objects=objects):
still_entry = f"OBJS-$(CONFIG_{still_image.upper()}) += still.o libjxl.o\n"
animation_entry = f"OBJS-$(CONFIG_{animation.upper()}) += {objects}\n"
self.write(
"libavcodec/Makefile",
still_entry if sub_makefiles else still_entry + animation_entry,
)
if sub_makefiles:
self.write("libavcodec/jxl/Makefile", animation_entry)
mapping = generator._collect_generic_mapping(
self.root,
generator.LibInfo("avcodec", "libavcodec", sub_makefiles),
set(),
)
self.assertEqual(mapping[still_image], ["still.o", "libjxl.o"])
self.assertEqual(mapping.get(animation, []), objects.split())
def test_fixed_configs_select_static_sources_without_conditional_duplicates(self):
template = self.write(
"config.h.in",
"#define CONFIG_AVFORMAT 1\n#define CONFIG_SMALL 0\n#define CONFIG_OTHER_VALUE 11\n#undef CONFIG_DYNAMIC\n",
)
generator.STATIC_CONFIG_SET = generator.load_static_configs(template)
self.assertEqual(generator.STATIC_CONFIG_SET, {"avformat"})
generator.PROFILE_EVERYTHING_SET = {"lcevc_parser"}
generator.CONFIG_EXTRA_SET = {"h264parse", "mpegvideo"}
self.write(
"libavcodec/Makefile",
"STLIBOBJS-$(CONFIG_AVFORMAT) += h2645_parse.o lcevctab.o to_upper4.o\n"
"OBJS-$(CONFIG_H264PARSE) += h2645_parse.o\n"
"OBJS-$(CONFIG_LCEVC_PARSER) += lcevc_parser.o lcevctab.o\n"
"OBJS-$(CONFIG_MPEGVIDEO) += mpegvideo.o to_upper4.o\n"
"OBJS-$(CONFIG_SMALL) += small.o\n",
)
for name in ["h2645_parse", "lcevctab", "to_upper4", "lcevc_parser", "mpegvideo", "small"]:
self.write("libavcodec/" + name + ".c")
sources = self.process_codec()
self.assertEqual(
sources.unconditional,
["libavcodec/h2645_parse.c", "libavcodec/lcevctab.c", "libavcodec/to_upper4.c"],
)
self.assertEqual(sources.components, ({"lcevc_parser": ["libavcodec/lcevc_parser.c"]}, []))
self.assertEqual(sources.extra, ({"mpegvideo": ["libavcodec/mpegvideo.c"]}, []))
output = []
generator._emit_library_to(
generator.LibInfo("avcodec", "libavcodec", []),
self.root,
set(),
lambda line="": output.append(line),
)
namespace = generated_assignments(output)
self.assertEqual(namespace["AVCODEC_EXTRA_SRCS"], sources.unconditional)
def test_neon_intrinsics_are_merged_into_aarch64_sources_only(self):
generator.CONFIG_EXTRA_SET = {"mpegvideo"}
self.write("libavcodec/Makefile", "")
for directory in ["aarch64", "neon"]:
self.write(
"libavcodec/" + directory + "/Makefile",
"OBJS-$(CONFIG_MPEGVIDEO) += " + directory + "/mpegvideo.o\n",
)
self.write("libavcodec/" + directory + "/mpegvideo.c")
sources = self.process_codec()
self.assertEqual(
sources.aarch64,
(
{"mpegvideo": ["libavcodec/aarch64/mpegvideo.c", "libavcodec/neon/mpegvideo.c"]},
[],
),
)
self.assertEqual(sources.unconditional, [])
self.assertEqual(sources.components, ({}, []))
self.assertEqual(sources.extra, ({}, []))
self.assertEqual(sources.x86, ({}, []))
self.assertEqual(sources.x86_asm, ({}, []))
def test_nested_architecture_makefiles_separate_nasm_and_c(self):
self.write("libavcodec/Makefile", "")
for codec in ["hevc", "vvc"]:
self.write(
"libavcodec/x86/" + codec + "/Makefile",
"X86ASM-OBJS-$(CONFIG_" + codec.upper() + "_DECODER) += "
"x86/" + codec + "/dsp_init.o x86/h26x/inter.o\n",
)
self.write("libavcodec/x86/" + codec + "/dsp_init.c")
self.write("libavcodec/x86/h26x/inter.asm")
self.write("libavcodec/x86/h26x/include_only.asm")
self.write(
"libavcodec/aarch64/vvc/Makefile",
"OBJS-$(CONFIG_VVC_DECODER) += aarch64/vvc/dsp_init.o\n"
"NEON-OBJS-$(CONFIG_VVC_DECODER) += aarch64/vvc/inter.o\n"
"SME2-OBJS-$(CONFIG_VVC_DECODER) += aarch64/vvc/inter_sme2.o\n",
)
self.write("libavcodec/aarch64/vvc/dsp_init.c")
self.write("libavcodec/aarch64/vvc/inter.S")
self.write("libavcodec/aarch64/vvc/inter_sme2.S")
sources = self.process_codec()
self.assertEqual(
sources.x86.exclusive,
{
"hevc_decoder": ["libavcodec/x86/hevc/dsp_init.c"],
"vvc_decoder": ["libavcodec/x86/vvc/dsp_init.c"],
},
)
self.assertEqual(
sources.x86_asm.shared,
[(["hevc_decoder", "vvc_decoder"], ["libavcodec/x86/h26x/inter.asm"])],
)
self.assertEqual(
sources.aarch64.exclusive,
{"vvc_decoder": ["libavcodec/aarch64/vvc/dsp_init.c", "libavcodec/aarch64/vvc/inter.S"]},
)
def test_tls_backend_and_network_sources_remain_selected_by_build(self):
generator.PROFILE_EVERYTHING_SET = {"dtls_protocol", "tls_protocol"}
self.write(
"libavformat/Makefile",
"OBJS-$(CONFIG_NETWORK) += network.o\n"
"OBJS-$(CONFIG_TLS_PROTOCOL) += tls.o tls_openssl.o\n"
"OBJS-$(CONFIG_DTLS_PROTOCOL) += tls.o tls_mbedtls.o\n",
)
for name in ["network", "tls", "tls_openssl", "tls_mbedtls"]:
self.write("libavformat/" + name + ".c")
sources = generator.process_library(self.root, generator.LibInfo("avformat", "libavformat", []))
self.assertEqual(sources.extra, ({}, []))
self.assertEqual(sources.components.exclusive, {})
self.assertEqual(
sources.components.shared,
[(["dtls_protocol", "tls_protocol"], ["libavformat/tls.c"])],
)
def test_emitted_architecture_and_extra_exports_register_shared_groups(self):
self.write(
"libavcodec/Makefile",
"OBJS-$(CONFIG_DSP) += shared.o\nOBJS-$(CONFIG_HEVC_DECODER) += shared.o\n",
)
self.write("libavcodec/shared.c")
groups = set()
output = []
generator._emit_library_to(
generator.LibInfo("avcodec", "libavcodec", []),
self.root,
groups,
lambda line="": output.append(line),
)
namespace = generated_assignments(output)
for prefix in ["AVCODEC", "AVCODEC_EXTRA", "AVCODEC_AARCH64", "AVCODEC_X86", "AVCODEC_X86_ASM"]:
self.assertIn(prefix + "_EXCLUSIVE_SRCS", namespace)
self.assertIn(prefix + "_SHARED_SRCS", namespace)
for name, value in namespace.items():
if name.endswith("_SHARED_SRCS"):
for components, _ in value:
self.assertIn(tuple(components), groups)
self.assertEqual(namespace["AVCODEC_EXTRA_SRCS"], [])
self.assertEqual(namespace["AVCODEC_AARCH64_COMPONENT_SRCS"], [])
self.assertEqual(namespace["AVCODEC_X86_COMPONENT_SRCS"], [])
def test_nasm_directory_partition_keeps_equal_basenames_separate(self):
groups = generator.SourceGroups(
{
"hevc_decoder": ["libavcodec/x86/hevc/mc.asm"],
"vvc_decoder": ["libavcodec/x86/vvc/mc.asm"],
},
[
(
["hevc_decoder", "vvc_decoder"],
["libavcodec/x86/h26x/common.asm", "libavcodec/x86/other.asm"],
),
],
)
output = []
generator._emit_asm_directories_to("AVCODEC_X86_ASM", groups, lambda line="": output.append(line))
namespace = generated_assignments(output)
exclusive = namespace["AVCODEC_X86_ASM_EXCLUSIVE_SRCS_BY_DIR"]
shared = namespace["AVCODEC_X86_ASM_SHARED_SRCS_BY_DIR"]
self.assertEqual(set(exclusive), set(shared))
self.assertEqual(exclusive["libavcodec/x86/hevc"], {"hevc_decoder": ["libavcodec/x86/hevc/mc.asm"]})
self.assertEqual(exclusive["libavcodec/x86/vvc"], {"vvc_decoder": ["libavcodec/x86/vvc/mc.asm"]})
self.assertEqual(
shared["libavcodec/x86/h26x"],
[(("hevc_decoder", "vvc_decoder"), ["libavcodec/x86/h26x/common.asm"])],
)
self.assertEqual(
shared["libavcodec/x86"],
[(("hevc_decoder", "vvc_decoder"), ["libavcodec/x86/other.asm"])],
)
class SharedGroupDefinitionsTest(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.reference = Path(self.directory.name) / "component_srcs.bzl"
self.reference.write_text(
"X86_SHARED_GROUP_DEFINITIONS = [\n"
' ("x86_shared_group_2", ["alpha_decoder", "beta_decoder"]),\n'
"]\n"
"SHARED_GROUP_DEFINITIONS = [\n"
' ("shared_group_9", ["alpha_decoder", "beta_decoder"]),\n'
' ("shared_group_3", ["delta_decoder", "gamma_decoder"]),\n'
' ("shared_group_99", ["removed_decoder", "removed_encoder"]),\n'
"]\n"
"EXTRA_SHARED_GROUP_DEFINITIONS = [\n"
' ("extra_shared_group_1", ["dsp", "hevc_decoder"]),\n'
"]\n"
"ALL_SHARED_GROUP_DEFINITIONS = SHARED_GROUP_DEFINITIONS + "
"EXTRA_SHARED_GROUP_DEFINITIONS + X86_SHARED_GROUP_DEFINITIONS\n",
encoding="utf-8",
)
def test_existing_ids_and_definition_order_survive(self):
sections, _ = generator.shared_group_definitions(
self.reference,
{("delta_decoder", "gamma_decoder"), ("alpha_decoder", "beta_decoder")},
)
self.assertEqual(
sections["SHARED_GROUP_DEFINITIONS"],
[
("shared_group_9", ["alpha_decoder", "beta_decoder"]),
("shared_group_3", ["delta_decoder", "gamma_decoder"]),
],
)
def test_new_groups_reserve_removed_ids_and_have_deterministic_order(self):
sections, _ = generator.shared_group_definitions(
self.reference,
{
("z_decoder", "z_encoder"),
("alpha_decoder", "beta_decoder"),
("a_decoder", "a_encoder"),
},
)
self.assertEqual(
sections["SHARED_GROUP_DEFINITIONS"],
[
("shared_group_9", ["alpha_decoder", "beta_decoder"]),
("shared_group_100", ["a_decoder", "a_encoder"]),
("shared_group_101", ["z_decoder", "z_encoder"]),
],
)
ids = [gid for entries in sections.values() for gid, _ in entries]
self.assertEqual(len(ids), len(set(ids)))
def test_removed_groups_are_not_emitted(self):
sections, _ = generator.shared_group_definitions(self.reference, set())
self.assertTrue(all(not entries for entries in sections.values()))
def test_old_sections_and_duplicate_membership_aliases_are_preserved(self):
sections, order = generator.shared_group_definitions(
self.reference,
{("alpha_decoder", "beta_decoder"), ("dsp", "hevc_decoder")},
)
self.assertEqual(
list(sections),
["X86_SHARED_GROUP_DEFINITIONS", "SHARED_GROUP_DEFINITIONS", "EXTRA_SHARED_GROUP_DEFINITIONS"],
)
self.assertEqual(
order,
["SHARED_GROUP_DEFINITIONS", "EXTRA_SHARED_GROUP_DEFINITIONS", "X86_SHARED_GROUP_DEFINITIONS"],
)
self.assertEqual(
sections["X86_SHARED_GROUP_DEFINITIONS"],
[("x86_shared_group_2", ["alpha_decoder", "beta_decoder"])],
)
self.assertEqual(
sections["EXTRA_SHARED_GROUP_DEFINITIONS"],
[("extra_shared_group_1", ["dsp", "hevc_decoder"])],
)
output = []
for name, entries in sections.items():
generator._emit_shared_group_definitions_to(name, entries, lambda line="": output.append(line))
self.assertEqual(generated_assignments(output), sections)
def test_duplicate_historical_ids_are_rejected(self):
self.reference.write_text(
self.reference.read_text(encoding="utf-8").replace('"x86_shared_group_2"', '"shared_group_9"'),
encoding="utf-8",
)
with self.assertRaisesRegex(ValueError, "Duplicate shared-group ID shared_group_9"):
generator.shared_group_definitions(self.reference, set())
if __name__ == "__main__":
unittest.main()