backend/tests/test_ass_drawing_semantics.py
Rama master · réplica de lectura del repositorio original.
"""One reading of ``\\p`` tags, and it has to be the renderer's.
Sandbox VM, 1.14.4-rc.10: a differential harness put 47 override forms through
the ffmpeg/libass shipped in our own release image and through the product
helper, in both initial drawing states, and found 16 disagreements. Separately
it showed the security sanitizer using a *different*, older reading — and that
one deletes visible dialogue outright.
"""
from __future__ import annotations
import pytest
#: Spelled with chr() rather than written into the string literals: as literal
#: characters these are invisible, and two of the cases below would look like
#: exact duplicates of each other in any diff.
NBSP = chr(0x00A0)
EM_SPACE = chr(0x2003)
ARABIC_INDIC_ONE = chr(0x0661)
FULLWIDTH_ONE = chr(0xFF11)
@pytest.mark.parametrize(
"tag_block, before, expected",
[
# libass accepts a leading plus; rc.10 allowed only a minus, so the
# coordinates of a {\p+1} drawing were handed to the model.
(r"{\p+1}", False, True),
(r"{\p +1}", False, True),
# The last \p in one block wins, plus sign included.
(r"{\p1\p0\p+1}", False, True),
(r"{\p1\p0}", False, False),
# A no-break space is not ASCII whitespace, so the argument is not
# read as a number at all and drawing ends up off — rc.10 accepted it
# via Python's \s and skipped the visible dialogue that followed.
(r"{\p" + NBSP + "1}", False, False),
(r"{\p" + EM_SPACE + "1}", False, False),
# Python's \d and int() take these for ones; libass does not.
(r"{\p" + ARABIC_INDIC_ONE + "}", False, False),
(r"{\p" + FULLWIDTH_ONE + "}", False, False),
# An unparseable rest reads as 0, which turns drawing off rather than
# leaving the tag unread.
(r"{\pfoo}", True, False),
(r"{\pBO3}", True, False),
# ... but the real lowercase \pbo and \pos tags are left alone.
(r"{\pbo3}", True, True),
(r"{\pbo3}", False, False),
(r"{\pos(10,20)}", True, True),
(r"{\pos(10,20)}", False, False),
# The canonical forms keep working.
(r"{\p1}", False, True),
(r"{\p0}", True, False),
(r"{\p01}", False, True),
(r"{\p12}", False, True),
(r"{\p-1}", True, False),
(r"{\p}", True, False),
(r"{\an8\i1}", True, True),
(r"{\an8\i1}", False, False),
# Whitespace may sit between the backslash and the tag name.
(r"{\ p1}", False, True),
(r"{\ p1}", False, True),
("{" + chr(92) + chr(9) + "p1}", False, True),
(r"{\ p0}", True, False),
(r"{\ P1}", False, False),
(r"{\ pbo3}", True, True),
# One opening parenthesis is skipped, a second one is not.
(r"{\p(1)}", False, True),
(r"{\p(0)}", True, False),
(r"{\p(-1)}", True, False),
(r"{\p(+1)}", False, True),
(r"{\p( 1 )}", False, True),
(r"{\p((1))}", False, False),
(r"{\p(1}", False, True),
(r"{\p(foo)}", True, False),
# Digits are read greedily and whatever follows them is ignored.
(r"{\p1)}", False, True),
(r"{\p1x}", False, True),
(r"{\p0x}", True, False),
(r"{\p1 2}", False, True),
(r"{\ p(1)}", False, True),
],
)
def test_drawing_state_after(tag_block, before, expected):
from ass_drawing import drawing_state_after
assert drawing_state_after(tag_block, before) is expected
def test_a_huge_argument_does_not_raise():
"""A 5000-digit argument failed the whole file, ordinary lines included.
``int()`` refuses more than 4300 digits in Python 3.12, and the helper let
that ValueError escape, so one malformed event cost every dialogue in the
file its translation. Whether a number is positive needs no conversion.
"""
from ass_drawing import drawing_state_after
assert drawing_state_after("{\\p" + "9" * 5000 + "}", False) is True
assert drawing_state_after("{\\p" + "0" * 5000 + "}", True) is False
@pytest.mark.parametrize(
"text, expected",
[
("Good morning.", True),
("", False),
(" ", False),
# Layout escapes are not sentences. A lone \N counted as a stretch of
# dialogue, so a caption with a leading line break looked like two
# sentences with a drawing between them and was skipped entirely.
(r"\N", False),
(r"\h", False),
(r"\n", False),
(r"\N\h", False),
(r"Good\Nmorning.", True),
],
)
def test_holds_language(text, expected):
from ass_drawing import holds_language
assert holds_language(text) is expected
class TestSanitizerUsesTheSameReading:
"""The sanitizer deleted visible dialogue because it read \\p differently.
Both cases below report ``success=true`` from the flow while the saved
event is empty — confirmed by the VM against the renderer: the expected
frame shows "Guten Morgen.", the actual frame is black.
"""
def test_uppercase_p_is_not_an_opener(self):
from subtitle_sanitizer import strip_drawing_blocks
assert strip_drawing_blocks(r"{\P1}Guten Morgen.{\p0}") == r"{\P1}Guten Morgen.{\p0}"
def test_a_block_that_turns_drawing_off_again_is_not_an_opener(self):
from subtitle_sanitizer import strip_drawing_blocks
assert strip_drawing_blocks(r"{\p1\p0}Guten Morgen.{\p0}") == r"{\p1\p0}Guten Morgen.{\p0}"
def test_a_real_drawing_span_is_still_removed(self):
"""The security control itself must not weaken."""
from subtitle_sanitizer import strip_drawing_blocks
assert (
strip_drawing_blocks(r"{\p1}m 0 0 l 10 10{\p0}Guten Morgen.")
== r"{\p1}{\p0}Guten Morgen."
)
def test_an_unclosed_opener_keeps_its_geometry(self):
"""Unclosed is how an ASS drawing is normally written.
Removing it looked right — one such span lights 224 000 of 230 400
pixels, which is what this filter calls an overlay — until the library
was measured: in one typesetting-heavy episode 1 045 of 2 752 dialogue
lines carry a ``\\p`` opener and not one of them is ever closed; across
4 000 sampled files, 932 (23 %) and 2.7 million events. The geometry is
the whole event, so a closer would be pointless, and stripping it would
delete the signs and masks of a quarter of the library from every file
Sublarr writes.
"""
from subtitle_sanitizer import strip_drawing_blocks
assert strip_drawing_blocks(r"{\p1}m 0 0 l 10 10") == r"{\p1}m 0 0 l 10 10"
assert strip_drawing_blocks(r"{\an8\p1}m 0 0 l 10 10") == r"{\an8\p1}m 0 0 l 10 10"
def test_text_before_an_unclosed_opener_is_untouched(self):
from subtitle_sanitizer import strip_drawing_blocks
assert strip_drawing_blocks(r"Guten Morgen.{\p1}m 0 0") == r"Guten Morgen.{\p1}m 0 0"
def test_a_closed_span_is_still_removed_on_the_same_line(self):
"""The rule that did not change: a complete span still goes."""
from subtitle_sanitizer import strip_drawing_blocks
assert strip_drawing_blocks(r"{\p1}m 0 0{\p0}Guten Morgen.") == r"{\p1}{\p0}Guten Morgen."
def test_the_fast_path_uses_the_same_reading_as_the_walk(self):
"""A shortcut that asks a different question is a hole in the filter.
``strip_drawing_blocks`` returned early unless the literal two
characters ``\\p`` appeared. Once whitespace after the backslash became
an opener, ``{\\ p1}`` no longer contained that substring — so the file
was handed back untouched and a full-screen overlay went to disk. Found
by the acceptance probe against the rc.14 image, not by this suite.
"""
from subtitle_sanitizer import strip_drawing_blocks
# A closed span, because an unclosed one is deliberately kept — what is
# under test here is that the shortcut sees the opener at all.
assert strip_drawing_blocks(r"{\ p1}m 0 0{\p0}Guten Morgen.") == r"{\ p1}{\p0}Guten Morgen."
assert strip_drawing_blocks(r"{\ p1}m 0 0 l 640 0{\p0}") == r"{\ p1}{\p0}"
def test_text_without_any_drawing_tag_is_returned_unchanged(self):
from subtitle_sanitizer import strip_drawing_blocks
assert strip_drawing_blocks("Guten Morgen.") == "Guten Morgen."
assert strip_drawing_blocks(r"{\an8\i1}Guten Morgen.") == r"{\an8\i1}Guten Morgen."
def test_a_plus_signed_opener_is_removed_too(self):
from subtitle_sanitizer import strip_drawing_blocks
assert strip_drawing_blocks(r"{\p+1}m 0 0{\p0}Guten Morgen.") == r"{\p+1}{\p0}Guten Morgen."
class TestRemovingADrawingKeepsTheRestOfItsBlock:
"""Only the drawing goes; the overrides sharing its block have to stay.
A sign is usually authored as one event — ``{\\an8\\pos(...)\\p1}`` geometry
``{\\p0}`` caption — so deleting the opening block whole took the caption's
position with the drawing and dropped it to the default bottom-centre.
Sandbox VM, 1.14.4-rc.10. Older than this release series.
"""
def test_positioning_in_the_opening_block_survives(self):
from subtitle_sanitizer import strip_drawing_blocks
assert (
strip_drawing_blocks(r"{\an8\p1}m 0 0 l 10 10{\p0}Guten Morgen.")
== r"{\an8\p1}{\p0}Guten Morgen."
)
def test_overrides_in_the_closing_block_survive(self):
from subtitle_sanitizer import strip_drawing_blocks
assert (
strip_drawing_blocks(r"{\p1}m 0 0{\p0\i1}Guten Morgen.")
== r"{\p1}{\p0\i1}Guten Morgen."
)
def test_drawing_switches_survive_without_geometry(self):
from subtitle_sanitizer import strip_drawing_blocks
assert (
strip_drawing_blocks(r"{\p1}m 0 0 l 10 10{\p0}Guten Morgen.")
== r"{\p1}{\p0}Guten Morgen."
)
def test_several_overrides_keep_their_order(self):
from subtitle_sanitizer import strip_drawing_blocks
assert (
strip_drawing_blocks(r"{\an8\p1\pos(10,20)}m 0 0{\p0}Guten Morgen.")
== r"{\an8\p1\pos(10,20)}{\p0}Guten Morgen."
)