test_safe_editor_comprehensive.pyā¢21.4 kB
"""
Corrected comprehensive tests for SafeMarkdownEditor with better coverage.
"""
import pytest
import threading
from quantalogic_markdown_mcp import (
SafeMarkdownEditor,
ValidationLevel,
EditOperation,
ErrorCategory,
SectionReference,
EditResult,
SafeParseError
)
class TestSafeMarkdownEditorFixed:
"""Fixed test suite for SafeMarkdownEditor."""
@pytest.fixture
def sample_markdown(self):
"""Sample markdown for testing."""
return """# Main Document
This is the main document content.
## Section A
Content for section A.
### Subsection A1
Content for subsection A1.
### Subsection A2
Content for subsection A2.
## Section B
Content for section B.
## Section C
Content for section C.
"""
@pytest.fixture
def editor(self, sample_markdown):
"""Create SafeMarkdownEditor instance."""
return SafeMarkdownEditor(sample_markdown, ValidationLevel.NORMAL)
def test_constructor_all_validation_levels(self, sample_markdown):
"""Test constructor with all validation levels."""
# Test all validation levels
for level in [ValidationLevel.STRICT, ValidationLevel.NORMAL, ValidationLevel.PERMISSIVE]:
editor = SafeMarkdownEditor(sample_markdown, level)
assert editor._validation_level == level
def test_constructor_with_empty_content(self):
"""Test constructor with empty content."""
editor = SafeMarkdownEditor("", ValidationLevel.NORMAL)
sections = editor.get_sections()
assert len(sections) == 0
def test_get_sections_comprehensive(self, editor):
"""Test get_sections method thoroughly."""
sections = editor.get_sections()
# Should have 6 sections total
assert len(sections) == 6
# Check all sections have valid human-readable IDs
for section in sections:
assert section.id # Should not be empty
assert isinstance(section.id, str) # Should be a string
# Human-readable IDs should not contain spaces and should be reasonable length
assert ' ' not in section.id
assert len(section.id) > 0
assert len(section.id) <= 60
assert len(section.id) > 8 # Should have hash part
def test_get_section_by_id(self, editor):
"""Test get_section_by_id method."""
sections = editor.get_sections()
if sections:
first_section = sections[0]
# Should find existing section
found_section = editor.get_section_by_id(first_section.id)
assert found_section is not None
assert found_section.id == first_section.id
# Should return None for non-existent ID
not_found = editor.get_section_by_id("nonexistent_id")
assert not_found is None
def test_get_sections_by_level(self, editor):
"""Test get_sections_by_level method."""
# Test level 1
level_1 = editor.get_sections_by_level(1)
assert len(level_1) >= 1
# Test invalid level (should raise error)
with pytest.raises(ValueError):
editor.get_sections_by_level(7)
with pytest.raises(ValueError):
editor.get_sections_by_level(0)
def test_get_child_sections(self, editor):
"""Test get_child_sections method."""
sections = editor.get_sections()
if len(sections) > 0:
main_doc = sections[0]
children = editor.get_child_sections(main_doc)
assert isinstance(children, list)
def test_preview_operation_update_section(self, editor):
"""Test preview_operation for UPDATE_SECTION."""
sections = editor.get_sections()
if len(sections) > 1:
section_a = sections[1] # "Section A"
# Test valid preview
result = editor.preview_operation(
EditOperation.UPDATE_SECTION,
section_ref=section_a,
content="New content for section A"
)
assert result.success is True
assert result.operation == EditOperation.UPDATE_SECTION
def test_update_section_content(self, editor):
"""Test update_section_content method."""
sections = editor.get_sections()
if len(sections) > 4:
section_b = sections[4] # "Section B"
original_version = editor._version
# Update content
result = editor.update_section_content(
section_b,
"Updated content for section B with **formatting**."
)
# Should succeed or have clear error
assert isinstance(result, EditResult)
assert result.operation == EditOperation.UPDATE_SECTION
if result.success:
# Check version incremented
assert editor._version == original_version + 1
def test_insert_section_after(self, editor):
"""Test insert_section_after method."""
sections = editor.get_sections()
if len(sections) > 1:
section_a = sections[1]
original_count = len(sections)
result = editor.insert_section_after(
section_a,
level=3,
title="New Subsection",
content="Content for the new subsection."
)
assert isinstance(result, EditResult)
assert result.operation == EditOperation.INSERT_SECTION
def test_delete_section(self, editor):
"""Test delete_section method."""
sections = editor.get_sections()
if len(sections) > 2:
subsection = sections[2] # Should be a subsection
result = editor.delete_section(subsection)
assert isinstance(result, EditResult)
assert result.operation == EditOperation.DELETE_SECTION
def test_change_heading_level(self, editor):
"""Test change_heading_level method."""
sections = editor.get_sections()
if len(sections) > 4:
section_b = sections[4] # "Section B"
result = editor.change_heading_level(section_b, 3)
assert isinstance(result, EditResult)
assert result.operation == EditOperation.CHANGE_HEADING_LEVEL
def test_change_heading_level_invalid_level(self, editor):
"""Test change_heading_level with invalid level."""
sections = editor.get_sections()
if sections:
section = sections[0]
# Test level too high
result = editor.change_heading_level(section, 7)
assert result.success is False
# Test level too low
result = editor.change_heading_level(section, 0)
assert result.success is False
def test_move_section(self, editor):
"""Test move_section method."""
sections = editor.get_sections()
if len(sections) > 5:
section_c = sections[5]
section_a = sections[1]
result = editor.move_section(section_c, section_a, "before")
assert isinstance(result, EditResult)
assert result.operation == EditOperation.MOVE_SECTION
def test_get_transaction_history(self, editor):
"""Test get_transaction_history method."""
# Should return a list
history = editor.get_transaction_history()
assert isinstance(history, list)
# Test with limit
limited_history = editor.get_transaction_history(limit=1)
assert isinstance(limited_history, list)
assert len(limited_history) <= 1
def test_rollback_transaction_no_history(self):
"""Test rollback_transaction with no history."""
editor = SafeMarkdownEditor("# Test", ValidationLevel.NORMAL)
result = editor.rollback_transaction()
assert result.success is False
assert len(result.errors) > 0
def test_validate_document(self, editor):
"""Test validate_document method."""
errors = editor.validate_document()
assert isinstance(errors, list)
def test_to_markdown(self, editor):
"""Test to_markdown export."""
markdown = editor.to_markdown()
assert isinstance(markdown, str)
assert len(markdown) > 0
def test_to_html(self, editor):
"""Test to_html export."""
html = editor.to_html()
assert isinstance(html, str)
assert len(html) > 0
def test_to_json(self, editor):
"""Test to_json export."""
json_str = editor.to_json()
assert isinstance(json_str, str)
assert len(json_str) > 0
def test_get_statistics(self, editor):
"""Test get_statistics method."""
stats = editor.get_statistics()
assert stats.total_sections >= 0
assert stats.word_count >= 0
assert stats.character_count >= 0
assert stats.line_count >= 0
assert stats.max_heading_depth >= 0
assert stats.edit_count >= 0
assert isinstance(stats.section_distribution, dict)
def test_thread_safety_concurrent_reads(self, editor):
"""Test thread safety with concurrent reads."""
results = []
errors = []
def read_sections(thread_id):
try:
sections = editor.get_sections()
stats = editor.get_statistics()
results.append({
'thread_id': thread_id,
'section_count': len(sections),
'word_count': stats.word_count
})
except Exception as e:
errors.append(f"Thread {thread_id}: {e}")
# Create multiple threads
threads = []
for i in range(3):
thread = threading.Thread(target=read_sections, args=(i,))
threads.append(thread)
# Start all threads
for thread in threads:
thread.start()
# Wait for completion
for thread in threads:
thread.join()
# Verify results
assert len(errors) == 0, f"Thread errors: {errors}"
assert len(results) == 3
def test_error_handling_edge_cases(self, editor):
"""Test various error handling scenarios."""
# Test with empty strings
result = editor.get_section_by_id("")
assert result is None
# Test update with empty content - should succeed
sections = editor.get_sections()
if sections:
result = editor.update_section_content(sections[0], "")
assert isinstance(result, EditResult)
def test_document_state_consistency(self, editor):
"""Test that document state remains consistent across operations."""
# Get initial state
initial_sections = editor.get_sections()
initial_stats = editor.get_statistics()
# Perform operation
if len(initial_sections) > 1:
section = initial_sections[1]
result = editor.update_section_content(section, "Modified content")
if result.success:
# Try rollback if we have transaction history
rollback_result = editor.rollback_transaction()
if rollback_result.success:
# State should be restored
restored_sections = editor.get_sections()
assert len(restored_sections) == len(initial_sections)
class TestSafeMarkdownEditorTypesFixed:
"""Test SafeMarkdownEditor data types."""
def test_section_reference_immutability(self):
"""Test that SectionReference is properly immutable."""
section = SectionReference(
id="test_id",
title="Test Section",
level=2,
line_start=1,
line_end=5,
path=["parent"]
)
# Should not be able to modify fields
with pytest.raises((AttributeError, TypeError)):
section.title = "Modified"
def test_edit_result_creation(self):
"""Test EditResult creation and properties."""
result = EditResult(
success=True,
operation=EditOperation.UPDATE_SECTION,
modified_sections=[],
errors=[],
warnings=[]
)
assert result.success is True
assert result.operation == EditOperation.UPDATE_SECTION
assert isinstance(result.modified_sections, list)
assert isinstance(result.errors, list)
assert isinstance(result.warnings, list)
def test_safe_parse_error_with_suggestions(self):
"""Test SafeParseError with suggestions."""
error = SafeParseError(
message="Test error",
error_code="TEST_ERROR",
category=ErrorCategory.VALIDATION,
suggestions=["Try this", "Or this"]
)
assert error.message == "Test error"
assert error.error_code == "TEST_ERROR"
assert error.category == ErrorCategory.VALIDATION
assert len(error.suggestions) == 2
class TestSafeMarkdownEditorEdgeCasesFixed:
"""Edge case tests for SafeMarkdownEditor."""
def test_empty_document_operations(self):
"""Test operations on empty document."""
editor = SafeMarkdownEditor("", ValidationLevel.NORMAL)
# Should handle empty document gracefully
sections = editor.get_sections()
assert len(sections) == 0
stats = editor.get_statistics()
assert stats.total_sections == 0
assert stats.word_count == 0
# Validation should work on empty document
errors = editor.validate_document()
assert isinstance(errors, list)
def test_single_heading_document(self):
"""Test document with single heading."""
editor = SafeMarkdownEditor("# Single Heading", ValidationLevel.NORMAL)
sections = editor.get_sections()
assert len(sections) == 1
assert sections[0].title == "Single Heading"
assert sections[0].level == 1
def test_document_with_only_content_no_headings(self):
"""Test document with content but no headings."""
editor = SafeMarkdownEditor("Just some content\nwith multiple lines.", ValidationLevel.NORMAL)
sections = editor.get_sections()
assert len(sections) == 0
stats = editor.get_statistics()
assert stats.total_sections == 0
assert stats.word_count > 0 # Should still count words
def test_very_deep_heading_hierarchy(self):
"""Test with maximum heading depth."""
deep_markdown = """# Level 1
## Level 2
### Level 3
#### Level 4
##### Level 5
###### Level 6"""
editor = SafeMarkdownEditor(deep_markdown, ValidationLevel.NORMAL)
sections = editor.get_sections()
assert len(sections) == 6
levels = [s.level for s in sections]
assert levels == [1, 2, 3, 4, 5, 6]
stats = editor.get_statistics()
assert stats.max_heading_depth == 6
def test_sections_with_special_characters(self):
"""Test sections with special characters in titles."""
special_chars = """# Section with "quotes"
## Section with *asterisks*
### Section with [brackets]"""
editor = SafeMarkdownEditor(special_chars, ValidationLevel.NORMAL)
sections = editor.get_sections()
assert len(sections) == 3
# Should have sections with special characters
titles = [s.title for s in sections]
assert len(titles) == 3
def test_transaction_history_operations(self):
"""Test transaction history functionality."""
editor = SafeMarkdownEditor("# Test\n\nContent.", ValidationLevel.NORMAL)
# Get initial history
initial_history = editor.get_transaction_history()
initial_count = len(initial_history)
# Make a change
sections = editor.get_sections()
if sections:
result = editor.update_section_content(sections[0], "Modified content")
if result.success:
# Should have more transactions
new_history = editor.get_transaction_history()
assert len(new_history) == initial_count + 1
def test_validation_permissive_vs_strict(self):
"""Test different validation levels."""
# Content with potential issues
content = """# Heading 1
## Heading 2
#### Heading 4""" # Skips heading 3
# Permissive should allow it
editor_permissive = SafeMarkdownEditor(content, ValidationLevel.PERMISSIVE)
assert editor_permissive is not None
# Normal should allow it
editor_normal = SafeMarkdownEditor(content, ValidationLevel.NORMAL)
assert editor_normal is not None
# Strict might reject it
try:
editor_strict = SafeMarkdownEditor(content, ValidationLevel.STRICT)
assert editor_strict is not None
except Exception:
# Expected for strict validation
pass
def test_statistics_after_modifications(self):
"""Test that statistics update correctly after modifications."""
editor = SafeMarkdownEditor("# Test\n\nInitial content.", ValidationLevel.NORMAL)
initial_stats = editor.get_statistics()
assert initial_stats.edit_count == 0
# Make an edit
sections = editor.get_sections()
if sections:
result = editor.update_section_content(sections[0], "Modified content with more words.")
if result.success:
updated_stats = editor.get_statistics()
assert updated_stats.edit_count == 1
def test_export_methods_with_modified_content(self):
"""Test export methods after content modifications."""
editor = SafeMarkdownEditor("# Original\n\nOriginal content.", ValidationLevel.NORMAL)
# Test all export methods work
markdown = editor.to_markdown()
assert isinstance(markdown, str)
html = editor.to_html()
assert isinstance(html, str)
json_str = editor.to_json()
assert isinstance(json_str, str)
def test_helper_method_coverage(self):
"""Test various helper methods indirectly."""
markdown = """# Main Document
Content here.
## Section A
### Subsection A1
Content for A1."""
editor = SafeMarkdownEditor(markdown, ValidationLevel.NORMAL)
# Test _build_section_references through get_sections
sections = editor.get_sections()
assert len(sections) > 0
# All sections should have proper human-readable IDs
for section in sections:
assert section.id # Should not be empty
assert isinstance(section.id, str) # Should be a string
assert ' ' not in section.id # No spaces
assert len(section.id) <= 60 # Reasonable length
assert len(section.id) > 8
def test_section_boundary_detection(self):
"""Test section boundary detection in complex documents."""
complex_markdown = """# Main
Content before section A.
## Section A
Content for section A.
### Subsection A1
Content for A1."""
editor = SafeMarkdownEditor(complex_markdown, ValidationLevel.NORMAL)
sections = editor.get_sections()
# Verify line boundaries make sense
for i, section in enumerate(sections):
assert section.line_start >= 0 # Allow 0-based indexing
assert section.line_end >= section.line_start
def test_concurrent_access_safety(self):
"""Test concurrent access safety."""
editor = SafeMarkdownEditor("# Test\n\nContent.", ValidationLevel.NORMAL)
results = []
errors = []
def concurrent_operations(thread_id):
try:
# Different threads do different operations
if thread_id % 2 == 0:
sections = editor.get_sections()
results.append(('read', len(sections)))
else:
stats = editor.get_statistics()
results.append(('stats', stats.word_count))
except Exception as e:
errors.append(f"Thread {thread_id}: {e}")
# Run concurrent operations
threads = []
for i in range(4):
thread = threading.Thread(target=concurrent_operations, args=(i,))
threads.append(thread)
for thread in threads:
thread.start()
for thread in threads:
thread.join()
# Should not have any errors
assert len(errors) == 0
assert len(results) == 4