579 lines
23 KiB
Python
579 lines
23 KiB
Python
"""
|
|
Unit tests for the recursive position system.
|
|
Tests the hierarchical position tracking that can reference any nested content structure.
|
|
"""
|
|
|
|
import unittest
|
|
import tempfile
|
|
import shutil
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from pyWebLayout.layout.recursive_position import (
|
|
ContentType, LocationNode, RecursivePosition, PositionBuilder, PositionStorage,
|
|
create_word_position, create_image_position, create_table_cell_position, create_list_item_position
|
|
)
|
|
|
|
|
|
class TestLocationNode(unittest.TestCase):
|
|
"""Test cases for LocationNode"""
|
|
|
|
def test_node_creation(self):
|
|
"""Test basic node creation"""
|
|
node = LocationNode(ContentType.WORD, 5, 3, {"text": "hello"})
|
|
|
|
self.assertEqual(node.content_type, ContentType.WORD)
|
|
self.assertEqual(node.index, 5)
|
|
self.assertEqual(node.offset, 3)
|
|
self.assertEqual(node.metadata["text"], "hello")
|
|
|
|
def test_node_serialization(self):
|
|
"""Test node serialization to/from dict"""
|
|
node = LocationNode(ContentType.TABLE_CELL, 2, 0, {"colspan": 2})
|
|
|
|
# Serialize
|
|
data = node.to_dict()
|
|
expected = {
|
|
'content_type': 'table_cell',
|
|
'index': 2,
|
|
'offset': 0,
|
|
'metadata': {'colspan': 2}
|
|
}
|
|
self.assertEqual(data, expected)
|
|
|
|
# Deserialize
|
|
restored = LocationNode.from_dict(data)
|
|
self.assertEqual(restored.content_type, ContentType.TABLE_CELL)
|
|
self.assertEqual(restored.index, 2)
|
|
self.assertEqual(restored.offset, 0)
|
|
self.assertEqual(restored.metadata, {'colspan': 2})
|
|
|
|
def test_node_string_representation(self):
|
|
"""Test string representation of nodes"""
|
|
node1 = LocationNode(ContentType.PARAGRAPH, 3)
|
|
self.assertEqual(str(node1), "paragraph[3]")
|
|
|
|
node2 = LocationNode(ContentType.WORD, 5, 2)
|
|
self.assertEqual(str(node2), "word[5]+2")
|
|
|
|
|
|
class TestRecursivePosition(unittest.TestCase):
|
|
"""Test cases for RecursivePosition"""
|
|
|
|
def test_position_creation(self):
|
|
"""Test basic position creation"""
|
|
pos = RecursivePosition()
|
|
|
|
# Should have document root by default
|
|
self.assertEqual(len(pos.path), 1)
|
|
self.assertEqual(pos.path[0].content_type, ContentType.DOCUMENT)
|
|
|
|
def test_position_building(self):
|
|
"""Test building complex positions"""
|
|
pos = RecursivePosition()
|
|
pos.add_node(LocationNode(ContentType.CHAPTER, 2))
|
|
pos.add_node(LocationNode(ContentType.BLOCK, 5))
|
|
pos.add_node(LocationNode(ContentType.PARAGRAPH, 0))
|
|
pos.add_node(LocationNode(ContentType.WORD, 12, 3))
|
|
|
|
self.assertEqual(len(pos.path), 5) # Including document root
|
|
self.assertEqual(pos.path[1].content_type, ContentType.CHAPTER)
|
|
self.assertEqual(pos.path[1].index, 2)
|
|
self.assertEqual(pos.path[-1].content_type, ContentType.WORD)
|
|
self.assertEqual(pos.path[-1].index, 12)
|
|
self.assertEqual(pos.path[-1].offset, 3)
|
|
|
|
def test_position_copy(self):
|
|
"""Test position copying"""
|
|
original = RecursivePosition()
|
|
original.add_node(LocationNode(ContentType.CHAPTER, 1))
|
|
original.add_node(LocationNode(ContentType.WORD, 5, 2, {"text": "test"}))
|
|
original.rendering_metadata = {"font_scale": 1.5}
|
|
|
|
copy = original.copy()
|
|
|
|
# Should be equal but not the same object
|
|
self.assertEqual(original, copy)
|
|
self.assertIsNot(original, copy)
|
|
self.assertIsNot(original.path, copy.path)
|
|
self.assertIsNot(original.rendering_metadata, copy.rendering_metadata)
|
|
|
|
# Modifying copy shouldn't affect original
|
|
copy.add_node(LocationNode(ContentType.IMAGE, 0))
|
|
self.assertNotEqual(len(original.path), len(copy.path))
|
|
|
|
def test_node_queries(self):
|
|
"""Test querying nodes by type"""
|
|
pos = RecursivePosition()
|
|
pos.add_node(LocationNode(ContentType.CHAPTER, 2))
|
|
pos.add_node(LocationNode(ContentType.BLOCK, 5))
|
|
pos.add_node(LocationNode(ContentType.TABLE, 0))
|
|
pos.add_node(LocationNode(ContentType.TABLE_ROW, 1))
|
|
pos.add_node(LocationNode(ContentType.TABLE_CELL, 2))
|
|
|
|
# Get single node
|
|
chapter_node = pos.get_node(ContentType.CHAPTER)
|
|
self.assertIsNotNone(chapter_node)
|
|
self.assertEqual(chapter_node.index, 2)
|
|
|
|
# Get non-existent node
|
|
word_node = pos.get_node(ContentType.WORD)
|
|
self.assertIsNone(word_node)
|
|
|
|
# Get multiple nodes (if there were multiple)
|
|
table_nodes = pos.get_nodes(ContentType.TABLE_ROW)
|
|
self.assertEqual(len(table_nodes), 1)
|
|
self.assertEqual(table_nodes[0].index, 1)
|
|
|
|
def test_position_hierarchy_operations(self):
|
|
"""Test ancestor/descendant relationships"""
|
|
# Create ancestor position: document -> chapter[1] -> block[2]
|
|
ancestor = RecursivePosition()
|
|
ancestor.add_node(LocationNode(ContentType.CHAPTER, 1))
|
|
ancestor.add_node(LocationNode(ContentType.BLOCK, 2))
|
|
|
|
# Create descendant position: document -> chapter[1] -> block[2] -> paragraph -> word[5]
|
|
descendant = ancestor.copy()
|
|
descendant.add_node(LocationNode(ContentType.PARAGRAPH, 0))
|
|
descendant.add_node(LocationNode(ContentType.WORD, 5))
|
|
|
|
# Create unrelated position: document -> chapter[2] -> block[1]
|
|
unrelated = RecursivePosition()
|
|
unrelated.add_node(LocationNode(ContentType.CHAPTER, 2))
|
|
unrelated.add_node(LocationNode(ContentType.BLOCK, 1))
|
|
|
|
# Test relationships
|
|
self.assertTrue(ancestor.is_ancestor_of(descendant))
|
|
self.assertTrue(descendant.is_descendant_of(ancestor))
|
|
self.assertFalse(ancestor.is_ancestor_of(unrelated))
|
|
self.assertFalse(unrelated.is_descendant_of(ancestor))
|
|
|
|
# Test common ancestor
|
|
common = ancestor.get_common_ancestor(descendant)
|
|
self.assertEqual(len(common.path), 3) # document + chapter + block
|
|
|
|
common_unrelated = ancestor.get_common_ancestor(unrelated)
|
|
self.assertEqual(len(common_unrelated.path), 1) # Only document root
|
|
|
|
def test_position_truncation(self):
|
|
"""Test truncating position to specific content type"""
|
|
pos = RecursivePosition()
|
|
pos.add_node(LocationNode(ContentType.CHAPTER, 1))
|
|
pos.add_node(LocationNode(ContentType.BLOCK, 2))
|
|
pos.add_node(LocationNode(ContentType.PARAGRAPH, 0))
|
|
pos.add_node(LocationNode(ContentType.WORD, 5))
|
|
|
|
# Truncate to block level
|
|
truncated = pos.copy().truncate_to_type(ContentType.BLOCK)
|
|
self.assertEqual(len(truncated.path), 3) # document + chapter + block
|
|
self.assertEqual(truncated.path[-1].content_type, ContentType.BLOCK)
|
|
|
|
def test_position_serialization(self):
|
|
"""Test position serialization to/from dict and JSON"""
|
|
pos = RecursivePosition()
|
|
pos.add_node(LocationNode(ContentType.CHAPTER, 2))
|
|
pos.add_node(LocationNode(ContentType.WORD, 5, 3, {"text": "hello"}))
|
|
pos.rendering_metadata = {"font_scale": 1.5, "page_size": [800, 600]}
|
|
|
|
# Test dict serialization
|
|
data = pos.to_dict()
|
|
restored = RecursivePosition.from_dict(data)
|
|
self.assertEqual(pos, restored)
|
|
|
|
# Test JSON serialization
|
|
json_str = pos.to_json()
|
|
restored_json = RecursivePosition.from_json(json_str)
|
|
self.assertEqual(pos, restored_json)
|
|
|
|
def test_position_equality_and_hashing(self):
|
|
"""Test position equality and hashing"""
|
|
pos1 = RecursivePosition()
|
|
pos1.add_node(LocationNode(ContentType.CHAPTER, 1))
|
|
pos1.add_node(LocationNode(ContentType.WORD, 5))
|
|
|
|
pos2 = RecursivePosition()
|
|
pos2.add_node(LocationNode(ContentType.CHAPTER, 1))
|
|
pos2.add_node(LocationNode(ContentType.WORD, 5))
|
|
|
|
pos3 = RecursivePosition()
|
|
pos3.add_node(LocationNode(ContentType.CHAPTER, 1))
|
|
pos3.add_node(LocationNode(ContentType.WORD, 6)) # Different word
|
|
|
|
# Test equality
|
|
self.assertEqual(pos1, pos2)
|
|
self.assertNotEqual(pos1, pos3)
|
|
|
|
# Test hashing (should be able to use as dict keys)
|
|
position_dict = {pos1: "value1", pos3: "value2"}
|
|
self.assertEqual(position_dict[pos2], "value1") # pos2 should hash same as pos1
|
|
|
|
def test_string_representation(self):
|
|
"""Test human-readable string representation"""
|
|
pos = RecursivePosition()
|
|
pos.add_node(LocationNode(ContentType.CHAPTER, 2))
|
|
pos.add_node(LocationNode(ContentType.BLOCK, 5))
|
|
pos.add_node(LocationNode(ContentType.WORD, 12, 3))
|
|
|
|
expected = "document[0] -> chapter[2] -> block[5] -> word[12]+3"
|
|
self.assertEqual(str(pos), expected)
|
|
|
|
|
|
class TestPositionBuilder(unittest.TestCase):
|
|
"""Test cases for PositionBuilder"""
|
|
|
|
def test_fluent_building(self):
|
|
"""Test fluent interface for building positions"""
|
|
pos = (PositionBuilder()
|
|
.chapter(2)
|
|
.block(5)
|
|
.paragraph()
|
|
.word(12, offset=3)
|
|
.with_rendering_metadata(font_scale=1.5, page_size=[800, 600])
|
|
.build())
|
|
|
|
# Check path structure
|
|
self.assertEqual(len(pos.path), 5) # document + chapter + block + paragraph + word
|
|
self.assertEqual(pos.path[1].content_type, ContentType.CHAPTER)
|
|
self.assertEqual(pos.path[1].index, 2)
|
|
self.assertEqual(pos.path[-1].content_type, ContentType.WORD)
|
|
self.assertEqual(pos.path[-1].index, 12)
|
|
self.assertEqual(pos.path[-1].offset, 3)
|
|
|
|
# Check rendering metadata
|
|
self.assertEqual(pos.rendering_metadata["font_scale"], 1.5)
|
|
self.assertEqual(pos.rendering_metadata["page_size"], [800, 600])
|
|
|
|
def test_table_building(self):
|
|
"""Test building table cell positions"""
|
|
pos = (PositionBuilder()
|
|
.chapter(1)
|
|
.block(3)
|
|
.table()
|
|
.table_row(2)
|
|
.table_cell(1)
|
|
.word(0)
|
|
.build())
|
|
|
|
# Verify table structure
|
|
table_node = pos.get_node(ContentType.TABLE)
|
|
row_node = pos.get_node(ContentType.TABLE_ROW)
|
|
cell_node = pos.get_node(ContentType.TABLE_CELL)
|
|
|
|
self.assertIsNotNone(table_node)
|
|
self.assertIsNotNone(row_node)
|
|
self.assertIsNotNone(cell_node)
|
|
self.assertEqual(row_node.index, 2)
|
|
self.assertEqual(cell_node.index, 1)
|
|
|
|
def test_list_building(self):
|
|
"""Test building list item positions"""
|
|
pos = (PositionBuilder()
|
|
.chapter(0)
|
|
.block(2)
|
|
.list()
|
|
.list_item(3)
|
|
.word(1)
|
|
.build())
|
|
|
|
# Verify list structure
|
|
list_node = pos.get_node(ContentType.LIST)
|
|
item_node = pos.get_node(ContentType.LIST_ITEM)
|
|
|
|
self.assertIsNotNone(list_node)
|
|
self.assertIsNotNone(item_node)
|
|
self.assertEqual(item_node.index, 3)
|
|
|
|
def test_image_building(self):
|
|
"""Test building image positions"""
|
|
pos = (PositionBuilder()
|
|
.chapter(1)
|
|
.block(4)
|
|
.image(0, alt_text="Test image", width=300, height=200)
|
|
.build())
|
|
|
|
image_node = pos.get_node(ContentType.IMAGE)
|
|
self.assertIsNotNone(image_node)
|
|
self.assertEqual(image_node.metadata["alt_text"], "Test image")
|
|
self.assertEqual(image_node.metadata["width"], 300)
|
|
|
|
|
|
class TestPositionStorage(unittest.TestCase):
|
|
"""Test cases for PositionStorage"""
|
|
|
|
def setUp(self):
|
|
"""Set up temporary directory for testing"""
|
|
self.temp_dir = tempfile.mkdtemp()
|
|
self.storage_json = PositionStorage(self.temp_dir, use_shelf=False)
|
|
self.storage_shelf = PositionStorage(self.temp_dir, use_shelf=True)
|
|
|
|
def tearDown(self):
|
|
"""Clean up temporary directory"""
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
def test_json_storage(self):
|
|
"""Test JSON-based position storage"""
|
|
# Create test position
|
|
pos = (PositionBuilder()
|
|
.chapter(2)
|
|
.block(5)
|
|
.word(12, offset=3)
|
|
.with_rendering_metadata(font_scale=1.5)
|
|
.build())
|
|
|
|
# Save position
|
|
self.storage_json.save_position("test_doc", "bookmark1", pos)
|
|
|
|
# Load position
|
|
loaded = self.storage_json.load_position("test_doc", "bookmark1")
|
|
self.assertIsNotNone(loaded)
|
|
self.assertEqual(pos, loaded)
|
|
|
|
# List positions
|
|
positions = self.storage_json.list_positions("test_doc")
|
|
self.assertIn("bookmark1", positions)
|
|
|
|
# Delete position
|
|
success = self.storage_json.delete_position("test_doc", "bookmark1")
|
|
self.assertTrue(success)
|
|
|
|
# Verify deletion
|
|
loaded_after_delete = self.storage_json.load_position("test_doc", "bookmark1")
|
|
self.assertIsNone(loaded_after_delete)
|
|
|
|
def test_shelf_storage(self):
|
|
"""Test shelf-based position storage"""
|
|
# Create test position
|
|
pos = (PositionBuilder()
|
|
.chapter(1)
|
|
.block(3)
|
|
.table()
|
|
.table_row(2)
|
|
.table_cell(1)
|
|
.build())
|
|
|
|
# Save position
|
|
self.storage_shelf.save_position("test_doc", "table_pos", pos)
|
|
|
|
# Load position
|
|
loaded = self.storage_shelf.load_position("test_doc", "table_pos")
|
|
self.assertIsNotNone(loaded)
|
|
self.assertEqual(pos, loaded)
|
|
|
|
# List positions
|
|
positions = self.storage_shelf.list_positions("test_doc")
|
|
self.assertIn("table_pos", positions)
|
|
|
|
# Delete position
|
|
success = self.storage_shelf.delete_position("test_doc", "table_pos")
|
|
self.assertTrue(success)
|
|
|
|
def test_multiple_positions(self):
|
|
"""Test storing multiple positions for same document"""
|
|
pos1 = create_word_position(0, 1, 5)
|
|
pos2 = create_image_position(1, 2)
|
|
pos3 = create_table_cell_position(2, 3, 1, 2, 0)
|
|
|
|
# Save multiple positions
|
|
self.storage_json.save_position("multi_doc", "pos1", pos1)
|
|
self.storage_json.save_position("multi_doc", "pos2", pos2)
|
|
self.storage_json.save_position("multi_doc", "pos3", pos3)
|
|
|
|
# List all positions
|
|
positions = self.storage_json.list_positions("multi_doc")
|
|
self.assertEqual(len(positions), 3)
|
|
self.assertIn("pos1", positions)
|
|
self.assertIn("pos2", positions)
|
|
self.assertIn("pos3", positions)
|
|
|
|
# Load and verify each position
|
|
loaded1 = self.storage_json.load_position("multi_doc", "pos1")
|
|
loaded2 = self.storage_json.load_position("multi_doc", "pos2")
|
|
loaded3 = self.storage_json.load_position("multi_doc", "pos3")
|
|
|
|
self.assertEqual(pos1, loaded1)
|
|
self.assertEqual(pos2, loaded2)
|
|
self.assertEqual(pos3, loaded3)
|
|
|
|
|
|
class TestConvenienceFunctions(unittest.TestCase):
|
|
"""Test cases for convenience functions"""
|
|
|
|
def test_create_word_position(self):
|
|
"""Test word position creation"""
|
|
pos = create_word_position(2, 5, 12, 3)
|
|
|
|
chapter_node = pos.get_node(ContentType.CHAPTER)
|
|
block_node = pos.get_node(ContentType.BLOCK)
|
|
word_node = pos.get_node(ContentType.WORD)
|
|
|
|
self.assertEqual(chapter_node.index, 2)
|
|
self.assertEqual(block_node.index, 5)
|
|
self.assertEqual(word_node.index, 12)
|
|
self.assertEqual(word_node.offset, 3)
|
|
|
|
def test_create_image_position(self):
|
|
"""Test image position creation"""
|
|
pos = create_image_position(1, 3, 0)
|
|
|
|
chapter_node = pos.get_node(ContentType.CHAPTER)
|
|
block_node = pos.get_node(ContentType.BLOCK)
|
|
image_node = pos.get_node(ContentType.IMAGE)
|
|
|
|
self.assertEqual(chapter_node.index, 1)
|
|
self.assertEqual(block_node.index, 3)
|
|
self.assertEqual(image_node.index, 0)
|
|
|
|
def test_create_table_cell_position(self):
|
|
"""Test table cell position creation"""
|
|
pos = create_table_cell_position(0, 2, 1, 3, 5)
|
|
|
|
chapter_node = pos.get_node(ContentType.CHAPTER)
|
|
block_node = pos.get_node(ContentType.BLOCK)
|
|
table_node = pos.get_node(ContentType.TABLE)
|
|
row_node = pos.get_node(ContentType.TABLE_ROW)
|
|
cell_node = pos.get_node(ContentType.TABLE_CELL)
|
|
word_node = pos.get_node(ContentType.WORD)
|
|
|
|
self.assertEqual(chapter_node.index, 0)
|
|
self.assertEqual(block_node.index, 2)
|
|
self.assertEqual(row_node.index, 1)
|
|
self.assertEqual(cell_node.index, 3)
|
|
self.assertEqual(word_node.index, 5)
|
|
|
|
def test_create_list_item_position(self):
|
|
"""Test list item position creation"""
|
|
pos = create_list_item_position(1, 4, 2, 7)
|
|
|
|
chapter_node = pos.get_node(ContentType.CHAPTER)
|
|
block_node = pos.get_node(ContentType.BLOCK)
|
|
list_node = pos.get_node(ContentType.LIST)
|
|
item_node = pos.get_node(ContentType.LIST_ITEM)
|
|
word_node = pos.get_node(ContentType.WORD)
|
|
|
|
self.assertEqual(chapter_node.index, 1)
|
|
self.assertEqual(block_node.index, 4)
|
|
self.assertEqual(item_node.index, 2)
|
|
self.assertEqual(word_node.index, 7)
|
|
|
|
|
|
class TestRealWorldScenarios(unittest.TestCase):
|
|
"""Test cases for real-world usage scenarios"""
|
|
|
|
def test_ereader_bookmark_scenario(self):
|
|
"""Test typical ereader bookmark usage"""
|
|
# User is reading chapter 3, paragraph 2, word 15, character 5
|
|
reading_pos = (PositionBuilder()
|
|
.chapter(3)
|
|
.block(8) # Block 8 in chapter 3
|
|
.paragraph()
|
|
.word(15, offset=5)
|
|
.with_rendering_metadata(
|
|
font_scale=1.2,
|
|
page_size=[600, 800],
|
|
theme="dark"
|
|
)
|
|
.build())
|
|
|
|
# Save as bookmark
|
|
storage = PositionStorage(use_shelf=False)
|
|
storage.save_position("my_novel", "chapter3_climax", reading_pos)
|
|
|
|
# Later, load bookmark
|
|
loaded_pos = storage.load_position("my_novel", "chapter3_climax")
|
|
self.assertEqual(reading_pos, loaded_pos)
|
|
|
|
# Verify we can extract the reading context
|
|
chapter_node = loaded_pos.get_node(ContentType.CHAPTER)
|
|
word_node = loaded_pos.get_node(ContentType.WORD)
|
|
|
|
self.assertEqual(chapter_node.index, 3)
|
|
self.assertEqual(word_node.index, 15)
|
|
self.assertEqual(word_node.offset, 5)
|
|
self.assertEqual(loaded_pos.rendering_metadata["font_scale"], 1.2)
|
|
|
|
def test_table_navigation_scenario(self):
|
|
"""Test navigating within a complex table"""
|
|
# User is in a table: chapter 2, table block 5, row 3, cell 2, word 1
|
|
table_pos = (PositionBuilder()
|
|
.chapter(2)
|
|
.block(5)
|
|
.table(0, table_type="data", columns=4, rows=10)
|
|
.table_row(3, row_type="data")
|
|
.table_cell(2, cell_type="data", colspan=1)
|
|
.word(1)
|
|
.build())
|
|
|
|
# Navigate to next cell (same row, next column)
|
|
next_cell_pos = table_pos.copy()
|
|
cell_node = next_cell_pos.get_node(ContentType.TABLE_CELL)
|
|
cell_node.index = 3 # Move to next column
|
|
word_node = next_cell_pos.get_node(ContentType.WORD)
|
|
word_node.index = 0 # Reset to first word in new cell
|
|
|
|
# Verify positions are different but related
|
|
self.assertNotEqual(table_pos, next_cell_pos)
|
|
|
|
# They should share common ancestor up to table row level
|
|
common = table_pos.get_common_ancestor(next_cell_pos)
|
|
row_node = common.get_node(ContentType.TABLE_ROW)
|
|
self.assertIsNotNone(row_node)
|
|
self.assertEqual(row_node.index, 3)
|
|
|
|
def test_multi_level_list_scenario(self):
|
|
"""Test navigating nested lists"""
|
|
# Position in nested list: chapter 1, list block 3, item 2, sub-list, sub-item 1, word 3
|
|
nested_pos = (PositionBuilder()
|
|
.chapter(1)
|
|
.block(3)
|
|
.list(0, list_type="ordered")
|
|
.list_item(2)
|
|
.list(1, list_type="unordered") # Nested list
|
|
.list_item(1)
|
|
.word(3)
|
|
.build())
|
|
|
|
# Verify we can distinguish between the two list levels
|
|
list_nodes = nested_pos.get_nodes(ContentType.LIST)
|
|
self.assertEqual(len(list_nodes), 2)
|
|
self.assertEqual(list_nodes[0].index, 0) # Outer list
|
|
self.assertEqual(list_nodes[1].index, 1) # Inner list
|
|
|
|
# Verify list item hierarchy
|
|
item_nodes = nested_pos.get_nodes(ContentType.LIST_ITEM)
|
|
self.assertEqual(len(item_nodes), 2)
|
|
self.assertEqual(item_nodes[0].index, 2) # Outer item
|
|
self.assertEqual(item_nodes[1].index, 1) # Inner item
|
|
|
|
def test_position_comparison_and_sorting(self):
|
|
"""Test comparing positions for sorting/ordering"""
|
|
# Create positions at different locations
|
|
pos1 = create_word_position(1, 2, 5) # Chapter 1, block 2, word 5
|
|
pos2 = create_word_position(1, 2, 10) # Chapter 1, block 2, word 10
|
|
pos3 = create_word_position(1, 3, 1) # Chapter 1, block 3, word 1
|
|
pos4 = create_word_position(2, 1, 1) # Chapter 2, block 1, word 1
|
|
|
|
positions = [pos4, pos2, pos1, pos3] # Unsorted
|
|
|
|
# For proper sorting, we'd need to implement comparison operators
|
|
# For now, we can test that positions are distinguishable
|
|
unique_positions = set(positions)
|
|
self.assertEqual(len(unique_positions), 4)
|
|
|
|
# Test that we can find common ancestors
|
|
common_12 = pos1.get_common_ancestor(pos2)
|
|
common_13 = pos1.get_common_ancestor(pos3)
|
|
common_14 = pos1.get_common_ancestor(pos4)
|
|
|
|
# pos1 and pos2 share paragraph-level ancestor (same chapter, block, paragraph)
|
|
self.assertEqual(len(common_12.path), 4) # document + chapter + block + paragraph
|
|
|
|
# pos1 and pos3 share chapter-level ancestor (same chapter, different blocks)
|
|
self.assertEqual(len(common_13.path), 2) # document + chapter
|
|
|
|
# pos1 and pos4 share only document-level ancestor (different chapters)
|
|
self.assertEqual(len(common_14.path), 1) # document only
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|