Add MarkdownTextFilter tests
This commit is contained in:
@@ -26,7 +26,8 @@ from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
|||||||
class SleepFrame(SystemFrame):
|
class SleepFrame(SystemFrame):
|
||||||
"""This frame is used by test framework to introduce some sleep time before
|
"""This frame is used by test framework to introduce some sleep time before
|
||||||
the next frame is pushed. This is useful to control system frames vs data or
|
the next frame is pushed. This is useful to control system frames vs data or
|
||||||
control frames."""
|
control frames.
|
||||||
|
"""
|
||||||
|
|
||||||
sleep: float = 0.1
|
sleep: float = 0.1
|
||||||
|
|
||||||
|
|||||||
@@ -29,3 +29,4 @@ soxr~=0.5.0
|
|||||||
together~=1.2.7
|
together~=1.2.7
|
||||||
transformers~=4.48.0
|
transformers~=4.48.0
|
||||||
websockets~=13.1
|
websockets~=13.1
|
||||||
|
Markdown~=3.7
|
||||||
230
tests/test_markdown_text_filter.py
Normal file
230
tests/test_markdown_text_filter.py
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024-2025 Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from pipecat.utils.text.markdown_text_filter import MarkdownTextFilter
|
||||||
|
|
||||||
|
|
||||||
|
class TestMarkdownTextFilter(unittest.IsolatedAsyncioTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.filter = MarkdownTextFilter()
|
||||||
|
|
||||||
|
async def test_basic_markdown_removal(self):
|
||||||
|
"""Test removal of basic Markdown formatting while preserving content."""
|
||||||
|
input_text = """
|
||||||
|
**Bold text** and *italic text*
|
||||||
|
1. Numbered list item
|
||||||
|
- Bullet point
|
||||||
|
Some `inline code` here
|
||||||
|
"""
|
||||||
|
|
||||||
|
expected_text = """
|
||||||
|
Bold text and italic text
|
||||||
|
1. Numbered list item
|
||||||
|
- Bullet point
|
||||||
|
Some inline code here
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = self.filter.filter(input_text)
|
||||||
|
self.assertEqual(result.strip(), expected_text.strip())
|
||||||
|
|
||||||
|
async def test_space_preservation(self):
|
||||||
|
"""Test preservation of leading and trailing spaces (for
|
||||||
|
word-by-word streaming in bot-tts-text).
|
||||||
|
"""
|
||||||
|
input_text = [
|
||||||
|
" Leading spaces",
|
||||||
|
"Trailing spaces ",
|
||||||
|
" Both ends ",
|
||||||
|
" Multiple spaces between words ",
|
||||||
|
]
|
||||||
|
|
||||||
|
for text in input_text:
|
||||||
|
result = self.filter.filter(text)
|
||||||
|
self.assertEqual(
|
||||||
|
len(result), len(text), f"Space preservation failed for: '{text}'\nGot: '{result}'"
|
||||||
|
)
|
||||||
|
# Check if spaces are in the same positions
|
||||||
|
for i, char in enumerate(text):
|
||||||
|
if char == " ":
|
||||||
|
self.assertEqual(
|
||||||
|
result[i], " ", f"Space at position {i} was not preserved in: '{text}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_repeated_character_removal(self):
|
||||||
|
"""Test removal of repeated character sequences (5 or more)."""
|
||||||
|
test_cases = {
|
||||||
|
"Hello!!!!!World": "HelloWorld", # 5 exclamations removed
|
||||||
|
"Test####ing": "Test####ing", # 4 hashes preserved
|
||||||
|
"Normal text": "Normal text", # No repeated chars
|
||||||
|
"!!!!!": "", # All repeated chars removed
|
||||||
|
"Mixed!!!!!...../////": "Mixed", # Multiple repeated sequences
|
||||||
|
"Text^^^^test": "Text^^^^test", # 4 carets preserved
|
||||||
|
"Text^^^^^test": "Texttest", # 5 carets removed
|
||||||
|
"Dots....here": "Dots....here", # 4 dots preserved
|
||||||
|
"Dots.....here": "Dotshere", # 5 dots removed
|
||||||
|
}
|
||||||
|
|
||||||
|
for input_text, expected in test_cases.items():
|
||||||
|
result = self.filter.filter(input_text)
|
||||||
|
self.assertEqual(
|
||||||
|
result,
|
||||||
|
expected,
|
||||||
|
f"Failed to handle repeated characters in: '{input_text}'\nExpected: '{expected}'\nGot: '{result}'",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_numbered_list_preservation(self):
|
||||||
|
"""Test that numbered lists are preserved correctly."""
|
||||||
|
input_text = """1. First item
|
||||||
|
2. Second item
|
||||||
|
3. Third item with **bold**"""
|
||||||
|
|
||||||
|
expected = """1. First item
|
||||||
|
2. Second item
|
||||||
|
3. Third item with bold"""
|
||||||
|
|
||||||
|
result = self.filter.filter(input_text)
|
||||||
|
self.assertEqual(
|
||||||
|
result.strip(),
|
||||||
|
expected.strip(),
|
||||||
|
f"Numbered list preservation failed.\nExpected:\n{expected}\nGot:\n{result}",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_html_entity_conversion(self):
|
||||||
|
"""Test conversion of HTML entities to their plain text equivalents."""
|
||||||
|
test_cases = {
|
||||||
|
"This & that": "This & that",
|
||||||
|
"1 < 2": "1 < 2",
|
||||||
|
"2 > 1": "2 > 1",
|
||||||
|
"Line break": "Line break",
|
||||||
|
"Mixed & <entities>": "Mixed & <entities>",
|
||||||
|
}
|
||||||
|
|
||||||
|
for input_text, expected in test_cases.items():
|
||||||
|
result = self.filter.filter(input_text)
|
||||||
|
self.assertEqual(result, expected, f"HTML entity conversion failed for: '{input_text}'")
|
||||||
|
|
||||||
|
async def test_asterisk_removal(self):
|
||||||
|
"""Test removal of Markdown asterisk formatting."""
|
||||||
|
test_cases = {
|
||||||
|
"**bold text**": "bold text", # Double asterisks
|
||||||
|
"*italic text*": "italic text", # Single asterisks
|
||||||
|
"**bold** and *italic*": "bold and italic", # Mixed
|
||||||
|
"multiple**bold**words": "multipleboldwords", # No spaces
|
||||||
|
"edge**cases***here*": "edgecaseshere", # Adjacent asterisks
|
||||||
|
}
|
||||||
|
|
||||||
|
for input_text, expected in test_cases.items():
|
||||||
|
result = self.filter.filter(input_text)
|
||||||
|
self.assertEqual(result, expected, f"Asterisk removal failed for: '{input_text}'")
|
||||||
|
|
||||||
|
async def test_newline_handling(self):
|
||||||
|
"""Test handling of empty and whitespace-only lines."""
|
||||||
|
test_cases = {
|
||||||
|
"Line 1\n\nLine 2": "Line 1\n Line 2", # Empty line becomes space
|
||||||
|
"Line 1\n \nLine 2": "Line 1\n Line 2", # Whitespace line becomes single space
|
||||||
|
"Text\n\n\nMore": "Text\n More", # Multiple empty lines become spaces
|
||||||
|
}
|
||||||
|
|
||||||
|
for input_text, expected in test_cases.items():
|
||||||
|
result = self.filter.filter(input_text)
|
||||||
|
self.assertEqual(
|
||||||
|
result, expected, f"Newline handling failed for:\n{input_text}\nGot:\n{result}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_numbered_list_marker_handling(self):
|
||||||
|
"""Test handling of numbered lists with the special §NUM§ marker."""
|
||||||
|
test_cases = {
|
||||||
|
"1. First\n2. Second": "1. First\n2. Second", # Basic numbered list
|
||||||
|
" 1. Indented": " 1. Indented", # Indented numbered list
|
||||||
|
"1. Item\nText\n2. Item": "1. Item\nText\n2. Item", # Text between items
|
||||||
|
"1.No space": "1.No space", # Not a list item (no space)
|
||||||
|
"12. Large number": "12. Large number", # Multi-digit numbers
|
||||||
|
}
|
||||||
|
|
||||||
|
for input_text, expected in test_cases.items():
|
||||||
|
result = self.filter.filter(input_text)
|
||||||
|
self.assertEqual(
|
||||||
|
result,
|
||||||
|
expected,
|
||||||
|
f"Numbered list handling failed for:\n{input_text}\nGot:\n{result}",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_inline_code_handling(self):
|
||||||
|
"""Test handling of inline code with backticks."""
|
||||||
|
test_cases = {
|
||||||
|
"`code`": "code", # Basic inline code
|
||||||
|
"Text `code` more": "Text code more", # Inline code within text
|
||||||
|
"``nested`code``": "nested`code", # Nested backticks
|
||||||
|
"`code1` and `code2`": "code1 and code2", # Multiple inline codes
|
||||||
|
"No``space``between": "Nospacebetween", # No spaces around backticks
|
||||||
|
}
|
||||||
|
|
||||||
|
for input_text, expected in test_cases.items():
|
||||||
|
result = self.filter.filter(input_text)
|
||||||
|
self.assertEqual(result, expected, f"Inline code handling failed for: '{input_text}'")
|
||||||
|
|
||||||
|
async def test_simple_table_removal(self):
|
||||||
|
"""Test removal of a simple markdown table."""
|
||||||
|
filter = MarkdownTextFilter(params=MarkdownTextFilter.InputParams(filter_tables=True))
|
||||||
|
|
||||||
|
input_text = "| Column 1 | Column 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |"
|
||||||
|
|
||||||
|
expected = ""
|
||||||
|
|
||||||
|
result = filter.filter(input_text)
|
||||||
|
self.assertEqual(
|
||||||
|
result.strip(),
|
||||||
|
expected.strip(),
|
||||||
|
f"Simple table removal failed.\nExpected:\n{expected}\nGot:\n{result}",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_feature_toggles(self):
|
||||||
|
"""Test enabling/disabling specific filter features."""
|
||||||
|
# Create a filter with all features disabled
|
||||||
|
filter = MarkdownTextFilter(
|
||||||
|
params=MarkdownTextFilter.InputParams(
|
||||||
|
enable_text_filter=False,
|
||||||
|
filter_code=False,
|
||||||
|
filter_tables=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test with text filtering disabled
|
||||||
|
text_with_markdown = "**bold** and *italic* with `code`"
|
||||||
|
self.assertEqual(
|
||||||
|
filter.filter(text_with_markdown),
|
||||||
|
text_with_markdown,
|
||||||
|
"Disabled filter should not modify text",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Enable just text filtering
|
||||||
|
filter.update_settings({"enable_text_filter": True})
|
||||||
|
self.assertEqual(
|
||||||
|
filter.filter(text_with_markdown),
|
||||||
|
"bold and italic with code",
|
||||||
|
"Enabled filter should remove markdown",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_settings_update(self):
|
||||||
|
"""Test that filter settings can be updated at runtime."""
|
||||||
|
filter = MarkdownTextFilter()
|
||||||
|
|
||||||
|
# Initial state - formatting should be removed
|
||||||
|
input_text = "**bold** and *italic*"
|
||||||
|
self.assertEqual(filter.filter(input_text), "bold and italic")
|
||||||
|
|
||||||
|
# Disable text filtering
|
||||||
|
filter.update_settings({"enable_text_filter": False})
|
||||||
|
self.assertEqual(filter.filter(input_text), input_text, "Text filtering should be disabled")
|
||||||
|
|
||||||
|
# Re-enable text filtering
|
||||||
|
filter.update_settings({"enable_text_filter": True})
|
||||||
|
self.assertEqual(
|
||||||
|
filter.filter(input_text), "bold and italic", "Text filtering should be re-enabled"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user