Update coverage badges [skip ci]

This commit is contained in:
Gitea Action
2026-08-08 20:35:15 +00:00
commit 735face593
312 changed files with 91102 additions and 0 deletions
View File
+189
View File
@@ -0,0 +1,189 @@
"""
Unit tests for pyWebLayout style objects.
Tests the Font class and style enums for proper functionality and immutability.
"""
import unittest
from pyWebLayout.style import Font, FontStyle, FontWeight, TextDecoration
from pyWebLayout.style import Alignment
class TestStyleObjects(unittest.TestCase):
"""Test cases for pyWebLayout style objects."""
def test_font_weight_enum(self):
"""Test FontWeight enum values."""
self.assertEqual(FontWeight.NORMAL.value, "normal")
self.assertEqual(FontWeight.BOLD.value, "bold")
# Test that all expected values exist
weights = [FontWeight.NORMAL, FontWeight.BOLD]
self.assertEqual(len(weights), 2)
def test_font_style_enum(self):
"""Test FontStyle enum values."""
self.assertEqual(FontStyle.NORMAL.value, "normal")
self.assertEqual(FontStyle.ITALIC.value, "italic")
# Test that all expected values exist
styles = [FontStyle.NORMAL, FontStyle.ITALIC]
self.assertEqual(len(styles), 2)
def test_text_decoration_enum(self):
"""Test TextDecoration enum values."""
self.assertEqual(TextDecoration.NONE.value, "none")
self.assertEqual(TextDecoration.UNDERLINE.value, "underline")
self.assertEqual(TextDecoration.STRIKETHROUGH.value, "strikethrough")
# Test that all expected values exist
decorations = [
TextDecoration.NONE,
TextDecoration.UNDERLINE,
TextDecoration.STRIKETHROUGH]
self.assertEqual(len(decorations), 3)
def test_alignment_enum(self):
"""Test Alignment enum values."""
self.assertEqual(Alignment.LEFT.value, "left")
self.assertEqual(Alignment.CENTER.value, "center")
self.assertEqual(Alignment.RIGHT.value, "right")
self.assertEqual(Alignment.TOP.value, "top")
self.assertEqual(Alignment.BOTTOM.value, "bottom")
self.assertEqual(Alignment.JUSTIFY.value, "justify")
self.assertEqual(Alignment.MIDDLE.value, "middle")
def test_font_initialization_defaults(self):
"""Test Font initialization with default values."""
font = Font()
self.assertIsNone(font._font_path)
self.assertEqual(font.font_size, 16)
self.assertEqual(font.colour, (0, 0, 0))
self.assertEqual(font.color, (0, 0, 0)) # Alias
self.assertEqual(font.weight, FontWeight.NORMAL)
self.assertEqual(font.style, FontStyle.NORMAL)
self.assertEqual(font.decoration, TextDecoration.NONE)
self.assertEqual(font.background, (255, 255, 255, 0)) # Transparent
self.assertEqual(font.language, "en_EN")
def test_font_initialization_custom(self):
"""Test Font initialization with custom values."""
font = Font(
font_path="/path/to/font.ttf",
font_size=16,
colour=(255, 0, 0),
weight=FontWeight.BOLD,
style=FontStyle.ITALIC,
decoration=TextDecoration.UNDERLINE,
background=(255, 255, 0, 255),
language="fr_FR"
)
self.assertEqual(font._font_path, "/path/to/font.ttf")
self.assertEqual(font.font_size, 16)
self.assertEqual(font.colour, (255, 0, 0))
self.assertEqual(font.weight, FontWeight.BOLD)
self.assertEqual(font.style, FontStyle.ITALIC)
self.assertEqual(font.decoration, TextDecoration.UNDERLINE)
self.assertEqual(font.background, (255, 255, 0, 255))
self.assertEqual(font.language, "fr_FR")
def test_font_with_methods(self):
"""Test Font immutable modification methods."""
original_font = Font(
font_size=12,
colour=(0, 0, 0),
weight=FontWeight.NORMAL,
style=FontStyle.NORMAL,
decoration=TextDecoration.NONE
)
# Test with_size
size_font = original_font.with_size(16)
self.assertEqual(size_font.font_size, 16)
self.assertEqual(original_font.font_size, 12) # Original unchanged
self.assertEqual(size_font.colour, (0, 0, 0)) # Other properties preserved
# Test with_colour
color_font = original_font.with_colour((255, 0, 0))
self.assertEqual(color_font.colour, (255, 0, 0))
self.assertEqual(original_font.colour, (0, 0, 0)) # Original unchanged
self.assertEqual(color_font.font_size, 12) # Other properties preserved
# Test with_weight
weight_font = original_font.with_weight(FontWeight.BOLD)
self.assertEqual(weight_font.weight, FontWeight.BOLD)
self.assertEqual(original_font.weight, FontWeight.NORMAL) # Original unchanged
# Test with_style
style_font = original_font.with_style(FontStyle.ITALIC)
self.assertEqual(style_font.style, FontStyle.ITALIC)
self.assertEqual(original_font.style, FontStyle.NORMAL) # Original unchanged
# Test with_decoration
decoration_font = original_font.with_decoration(TextDecoration.UNDERLINE)
self.assertEqual(decoration_font.decoration, TextDecoration.UNDERLINE)
self.assertEqual(
original_font.decoration,
TextDecoration.NONE) # Original unchanged
def test_font_property_access(self):
"""Test Font property access methods."""
font = Font(
font_size=20,
colour=(128, 128, 128),
weight=FontWeight.BOLD,
style=FontStyle.ITALIC,
decoration=TextDecoration.STRIKETHROUGH
)
# Test all property getters
self.assertEqual(font.font_size, 20)
self.assertEqual(font.colour, (128, 128, 128))
self.assertEqual(font.color, (128, 128, 128)) # Alias
self.assertEqual(font.weight, FontWeight.BOLD)
self.assertEqual(font.style, FontStyle.ITALIC)
self.assertEqual(font.decoration, TextDecoration.STRIKETHROUGH)
# Test that font object is accessible
self.assertIsNotNone(font.font)
def test_font_immutability(self):
"""Test that Font objects behave immutably."""
font1 = Font(font_size=12, colour=(0, 0, 0))
font2 = font1.with_size(16)
font3 = font2.with_colour((255, 0, 0))
# Each should be different objects
self.assertIsNot(font1, font2)
self.assertIsNot(font2, font3)
self.assertIsNot(font1, font3)
# Original properties should be unchanged
self.assertEqual(font1.font_size, 12)
self.assertEqual(font1.colour, (0, 0, 0))
self.assertEqual(font2.font_size, 16)
self.assertEqual(font2.colour, (0, 0, 0))
self.assertEqual(font3.font_size, 16)
self.assertEqual(font3.colour, (255, 0, 0))
def test_background_handling(self):
"""Test background color handling."""
# Test default transparent background
font1 = Font()
self.assertEqual(font1.background, (255, 255, 255, 0))
# Test explicit background
font2 = Font(background=(255, 0, 0, 128))
self.assertEqual(font2.background, (255, 0, 0, 128))
# Test None background becomes transparent
font3 = Font(background=None)
self.assertEqual(font3.background, (255, 255, 255, 0))
if __name__ == '__main__':
unittest.main()
+233
View File
@@ -0,0 +1,233 @@
"""
Test the new abstract/concrete style system.
This test demonstrates how the new style system addresses the memory efficiency
concerns by using abstract styles that can be resolved to concrete styles
based on user preferences.
"""
import pytest
from pyWebLayout.style.abstract_style import (
AbstractStyle, AbstractStyleRegistry, FontFamily, FontSize
)
from pyWebLayout.style.concrete_style import (
ConcreteStyleRegistry, RenderingContext, StyleResolver
)
from pyWebLayout.style.fonts import FontWeight
def test_abstract_style_is_hashable():
"""Test that AbstractStyle objects are hashable and can be used as dict keys."""
# Create two identical styles
style1 = AbstractStyle(
font_family=FontFamily.SERIF,
font_size=16,
font_weight=FontWeight.BOLD,
color="red"
)
style2 = AbstractStyle(
font_family=FontFamily.SERIF,
font_size=16,
font_weight=FontWeight.BOLD,
color="red"
)
# They should be equal and have the same hash
assert style1 == style2
assert hash(style1) == hash(style2)
# They should work as dictionary keys
style_dict = {style1: "first", style2: "second"}
assert len(style_dict) == 1 # Should be deduplicated
assert style_dict[style1] == "second" # Last value wins
def test_abstract_style_registry_deduplication():
"""Test that the registry prevents duplicate styles."""
registry = AbstractStyleRegistry()
# Create the same style twice
style1 = AbstractStyle(font_size=18, font_weight=FontWeight.BOLD)
style2 = AbstractStyle(font_size=18, font_weight=FontWeight.BOLD)
# Register both - should get same ID
id1, _ = registry.get_or_create_style(style1)
id2, _ = registry.get_or_create_style(style2)
assert id1 == id2 # Same style should get same ID
assert registry.get_style_count() == 2 # Only default + our style
def test_style_inheritance():
"""Test that style inheritance works properly."""
registry = AbstractStyleRegistry()
# Create base style
base_style = AbstractStyle(font_size=16, color="black")
base_id, _ = registry.get_or_create_style(base_style)
# Create derived style
derived_id, derived_style = registry.create_derived_style(
base_id,
font_weight=FontWeight.BOLD,
color="red"
)
# Resolve effective style
effective = registry.resolve_effective_style(derived_id)
assert effective.font_size == 16 # Inherited from base
assert effective.font_weight == FontWeight.BOLD # Overridden
assert effective.color == "red" # Overridden
def test_style_resolver_user_preferences():
"""Test that user preferences affect concrete style resolution."""
# Create rendering context with larger fonts
context = RenderingContext(
base_font_size=20, # Larger base size
font_scale_factor=1.5, # Additional scaling
large_text=True # Accessibility preference
)
resolver = StyleResolver(context)
# Create abstract style with medium size
abstract_style = AbstractStyle(font_size=FontSize.MEDIUM)
# Resolve to concrete style
concrete_style = resolver.resolve_style(abstract_style)
# Font size should be: 20 (base) * 1.0 (medium) * 1.5 (scale) * 1.2
# (large_text) = 36
expected_size = int(20 * 1.0 * 1.5 * 1.2)
assert concrete_style.font_size == expected_size
def test_style_resolver_color_resolution():
"""Test color name resolution."""
context = RenderingContext()
resolver = StyleResolver(context)
# Test named colors
red_style = AbstractStyle(color="red")
concrete_red = resolver.resolve_style(red_style)
assert concrete_red.color == (255, 0, 0)
# Test hex colors
hex_style = AbstractStyle(color="#ff0000")
concrete_hex = resolver.resolve_style(hex_style)
assert concrete_hex.color == (255, 0, 0)
# Test RGB tuple (should pass through)
rgb_style = AbstractStyle(color=(128, 64, 192))
concrete_rgb = resolver.resolve_style(rgb_style)
assert concrete_rgb.color == (128, 64, 192)
def test_concrete_style_caching():
"""Test that concrete styles are cached efficiently."""
context = RenderingContext()
registry = ConcreteStyleRegistry(StyleResolver(context))
# Create abstract style
abstract_style = AbstractStyle(font_size=16, color="blue")
# Get font twice - should be cached
font1 = registry.get_font(abstract_style)
font2 = registry.get_font(abstract_style)
# Should be the same object (cached)
assert font1 is font2
# Check cache stats
stats = registry.get_cache_stats()
assert stats["concrete_styles"] == 1
assert stats["fonts"] == 1
def test_global_font_scaling():
"""Test that global font scaling affects all text."""
# Create two contexts with different scaling
context_normal = RenderingContext(font_scale_factor=1.0)
context_large = RenderingContext(font_scale_factor=2.0)
resolver_normal = StyleResolver(context_normal)
resolver_large = StyleResolver(context_large)
# Same abstract style
abstract_style = AbstractStyle(font_size=16)
# Resolve with different contexts
concrete_normal = resolver_normal.resolve_style(abstract_style)
concrete_large = resolver_large.resolve_style(abstract_style)
# Large should be 2x the size
assert concrete_large.font_size == concrete_normal.font_size * 2
def test_memory_efficiency():
"""Test that the new system is more memory efficient."""
registry = AbstractStyleRegistry()
# Create many "different" styles that are actually the same
styles = []
for i in range(100):
# All these styles are identical
style = AbstractStyle(
font_size=16,
font_weight=FontWeight.NORMAL,
color="black"
)
style_id, _ = registry.get_or_create_style(style)
styles.append(style_id)
# All should reference the same style
assert len(set(styles)) == 1 # All IDs are the same
assert registry.get_style_count() == 2 # Only default + our style
# This demonstrates that we don't create duplicate styles
def test_word_style_reference_concept():
"""Demonstrate how words would reference styles instead of storing fonts."""
registry = AbstractStyleRegistry()
# Create paragraph style
para_style = AbstractStyle(font_size=16, color="black")
para_id, _ = registry.get_or_create_style(para_style)
# Create bold word style
bold_style = AbstractStyle(font_size=16, color="black", font_weight=FontWeight.BOLD)
bold_id, _ = registry.get_or_create_style(bold_style)
# Simulate words storing style IDs instead of full Font objects
words_data = [
{"text": "This", "style_id": para_id},
{"text": "is", "style_id": para_id},
{"text": "bold", "style_id": bold_id},
{"text": "text", "style_id": para_id},
]
# To get the actual font for rendering, we resolve through registry
context = RenderingContext()
concrete_registry = ConcreteStyleRegistry(StyleResolver(context))
for word_data in words_data:
abstract_style = registry.get_style_by_id(word_data["style_id"])
font = concrete_registry.get_font(abstract_style)
# Now we have the actual Font object for rendering
assert font is not None
assert hasattr(font, 'font_size')
# Bold word should have bold weight
if word_data["text"] == "bold":
assert font.weight == FontWeight.BOLD
else:
assert font.weight == FontWeight.NORMAL
if __name__ == "__main__":
pytest.main([__file__])
@@ -0,0 +1,149 @@
"""
Test file demonstrating word spacing constraints functionality.
This test shows how to use the new min/max word spacing constraints
in the style system.
"""
from pyWebLayout.style.abstract_style import AbstractStyle, AbstractStyleRegistry
from pyWebLayout.style.concrete_style import StyleResolver, RenderingContext
class TestWordSpacingConstraints:
"""Test cases for word spacing constraints feature."""
def test_abstract_style_with_word_spacing_constraints(self):
"""Test that AbstractStyle accepts word spacing constraint fields."""
style = AbstractStyle(
word_spacing=5.0,
word_spacing_min=2.0,
word_spacing_max=10.0
)
assert style.word_spacing == 5.0
assert style.word_spacing_min == 2.0
assert style.word_spacing_max == 10.0
def test_concrete_style_resolution_with_constraints(self):
"""Test that word spacing constraints are resolved correctly."""
# Create rendering context
context = RenderingContext(base_font_size=16)
resolver = StyleResolver(context)
# Create abstract style with constraints
abstract_style = AbstractStyle(
word_spacing=5.0,
word_spacing_min=2.0,
word_spacing_max=12.0
)
# Resolve to concrete style
concrete_style = resolver.resolve_style(abstract_style)
# Check that constraints are preserved
assert concrete_style.word_spacing == 5.0
assert concrete_style.word_spacing_min == 2.0
assert concrete_style.word_spacing_max == 12.0
def test_default_constraint_logic(self):
"""Test default constraint logic when not specified."""
context = RenderingContext(base_font_size=16)
resolver = StyleResolver(context)
# Style with only base word spacing
abstract_style = AbstractStyle(word_spacing=6.0)
concrete_style = resolver.resolve_style(abstract_style)
# Should apply default logic: min = base, max = base * 2
assert concrete_style.word_spacing == 6.0
assert concrete_style.word_spacing_min == 6.0
assert concrete_style.word_spacing_max == 12.0
def test_no_word_spacing_defaults(self):
"""Test defaults when no word spacing is specified."""
context = RenderingContext(base_font_size=16)
resolver = StyleResolver(context)
# Style with no word spacing specified
abstract_style = AbstractStyle()
concrete_style = resolver.resolve_style(abstract_style)
# Should apply font-based defaults
assert concrete_style.word_spacing == 0.0
assert concrete_style.word_spacing_min == 2.0 # Minimum default
assert concrete_style.word_spacing_max == 8.0 # 50% of font size (16 * 0.5)
def test_partial_constraints(self):
"""Test behavior when only min or max is specified."""
context = RenderingContext(base_font_size=16)
resolver = StyleResolver(context)
# Only min specified
abstract_style_min = AbstractStyle(
word_spacing=4.0,
word_spacing_min=3.0
)
concrete_style_min = resolver.resolve_style(abstract_style_min)
assert concrete_style_min.word_spacing_min == 3.0
assert concrete_style_min.word_spacing_max == 6.0 # 3.0 * 2
# Only max specified
abstract_style_max = AbstractStyle(
word_spacing=4.0,
word_spacing_max=8.0
)
concrete_style_max = resolver.resolve_style(abstract_style_max)
assert concrete_style_max.word_spacing_min == 4.0 # max(word_spacing, 2.0)
assert concrete_style_max.word_spacing_max == 8.0
def test_style_registry_with_constraints(self):
"""Test that style registry handles word spacing constraints."""
registry = AbstractStyleRegistry()
# Create style with constraints
style_id, style = registry.get_or_create_style(
word_spacing=5.0,
word_spacing_min=3.0,
word_spacing_max=10.0
)
# Verify the style was created correctly
retrieved_style = registry.get_style_by_id(style_id)
assert retrieved_style.word_spacing == 5.0
assert retrieved_style.word_spacing_min == 3.0
assert retrieved_style.word_spacing_max == 10.0
def test_em_units_in_constraints(self):
"""Test that em units work in word spacing constraints."""
context = RenderingContext(base_font_size=16)
resolver = StyleResolver(context)
# Use em units
abstract_style = AbstractStyle(
word_spacing="0.25em",
word_spacing_min="0.1em",
word_spacing_max="0.5em"
)
concrete_style = resolver.resolve_style(abstract_style)
# Should convert em to pixels based on font size (16px)
assert concrete_style.word_spacing == 4.0 # 0.25 * 16
assert concrete_style.word_spacing_min == 1.6 # 0.1 * 16
assert concrete_style.word_spacing_max == 8.0 # 0.5 * 16
if __name__ == "__main__":
# Run basic tests
test = TestWordSpacingConstraints()
test.test_abstract_style_with_word_spacing_constraints()
test.test_concrete_style_resolution_with_constraints()
test.test_default_constraint_logic()
test.test_no_word_spacing_defaults()
test.test_partial_constraints()
test.test_style_registry_with_constraints()
test.test_em_units_in_constraints()
print("All word spacing constraint tests passed!")