increase test coverage.

This commit is contained in:
Duncan Tourolle 2025-06-07 17:32:34 +02:00
parent fa8ba244a8
commit 5b2c24e59d
3 changed files with 368 additions and 123 deletions

31
.coveragerc Normal file
View File

@ -0,0 +1,31 @@
[run]
source = pyWebLayout
branch = True
omit =
*/tests/*
*/test_*
setup.py
*/examples/*
*/__main__.py
[report]
exclude_lines =
pragma: no cover
def __repr__
if self.debug:
if settings.DEBUG
raise AssertionError
raise NotImplementedError
if 0:
if __name__ == .__main__.:
# Exclude docstrings
^\s*"""
^\s*'''
^\s*r"""
^\s*r'''
[xml]
output = coverage.xml
[html]
directory = htmlcov

View File

@ -1,122 +0,0 @@
# Coverage Gutters Setup Guide
This guide helps you set up Coverage Gutters in VSCode to visualize code coverage for the pyWebLayout project.
## Prerequisites
1. **Install VSCode Extension**: Make sure you have the "Coverage Gutters" extension installed in VSCode.
2. **Install Python packages**:
```bash
pip install pytest pytest-cov coverage
```
## Configuration Files
### 1. VSCode Settings (`.vscode/settings.json`)
Your VSCode settings are already configured with:
```json
{
"coverage-gutters.coverageBaseDir": "./",
"coverage-gutters.coverageFileNames": [
"coverage.xml",
"lcov.info",
"cov.xml",
"coverage.info"
],
"coverage-gutters.showGutterCoverage": true,
"coverage-gutters.showLineCoverage": true,
"coverage-gutters.showRulerCoverage": true,
"coverage-gutters.xmlname": "coverage.xml"
}
```
### 2. Coverage Configuration (`pyproject.toml`)
Coverage settings are configured in `pyproject.toml`:
```toml
[tool.coverage.run]
source = ["pyWebLayout"]
branch = true
omit = [
"*/tests/*",
"*/test_*",
"setup.py",
"*/examples/*",
"*/__main__.py"
]
[tool.coverage.xml]
output = "coverage.xml"
[tool.coverage.html]
directory = "htmlcov"
```
## How to Generate Coverage
### Option 1: Quick Coverage for Gutters
```bash
python run_coverage_gutters.py
```
### Option 2: Manual pytest command
```bash
python -m pytest tests/ --cov=pyWebLayout --cov-report=xml --cov-report=html --cov-report=term
```
### Option 3: Full coverage analysis
```bash
python scripts/run_coverage.py
```
## Using Coverage Gutters in VSCode
1. **Generate coverage data** using one of the options above
2. **Open VSCode** and navigate to your Python source files
3. **Enable Coverage Gutters**:
- Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on Mac)
- Type "Coverage Gutters: Display Coverage"
- Or click the "Watch" button in the status bar
## What You'll See
- **Green lines**: Code that is covered by tests
- **Red lines**: Code that is NOT covered by tests
- **Yellow lines**: Partially covered code (branches)
- **Coverage percentage** in the status bar
## Troubleshooting
1. **No coverage showing**:
- Make sure `coverage.xml` exists in the project root
- Check that the Coverage Gutters extension is enabled
- Try reloading VSCode window
2. **Coverage not updating**:
- Re-run the coverage command
- Click "Watch" in the status bar to refresh
3. **Tests not running**:
- Make sure you're in the project root directory
- Install missing dependencies: `pip install pytest pytest-cov`
## Coverage Files Generated
After running coverage, you should see:
- `coverage.xml` - XML format for Coverage Gutters
- `htmlcov/` - HTML coverage report directory
- `coverage.json` - JSON format for badges
- `.coverage` - Coverage database file
## Commands Summary
```bash
# Install dependencies
pip install pytest pytest-cov coverage
# Generate coverage for gutters
python run_coverage_gutters.py
# View HTML report
open htmlcov/index.html

View File

@ -7,7 +7,7 @@ and formatting within documents.
import unittest import unittest
from unittest.mock import Mock, patch, MagicMock from unittest.mock import Mock, patch, MagicMock
from pyWebLayout.abstract.inline import Word, FormattedSpan from pyWebLayout.abstract.inline import Word, FormattedSpan, LineBreak
from pyWebLayout.style import Font from pyWebLayout.style import Font
@ -251,6 +251,150 @@ class TestWord(unittest.TestCase):
# Test getting individual parts # Test getting individual parts
for i, expected_part in enumerate(expected_parts): for i, expected_part in enumerate(expected_parts):
self.assertEqual(word.get_hyphenated_part(i), expected_part) self.assertEqual(word.get_hyphenated_part(i), expected_part)
def test_word_create_and_add_to_with_container_style(self):
"""Test Word.create_and_add_to with container that has style property."""
# Create mock container with style and add_word method
mock_container = Mock()
mock_container.style = self.font
mock_container.add_word = Mock()
# Ensure _words and background don't interfere
del mock_container._words
del mock_container.background # Remove background so it inherits from font
# Create and add word
word = Word.create_and_add_to("hello", mock_container)
# Test that word was created with correct properties
self.assertIsInstance(word, Word)
self.assertEqual(word.text, "hello")
self.assertEqual(word.style, self.font)
self.assertEqual(word.background, self.font.background)
# Test that add_word was called
mock_container.add_word.assert_called_once_with(word)
def test_word_create_and_add_to_with_style_override(self):
"""Test Word.create_and_add_to with explicit style parameter."""
# Create alternate font
alt_font = Font()
# Create mock container
mock_container = Mock()
mock_container.style = self.font
mock_container.add_word = Mock()
# Ensure _words doesn't interfere
del mock_container._words
# Create word with style override
word = Word.create_and_add_to("test", mock_container, style=alt_font)
# Test that word uses the override style, not container style
self.assertEqual(word.style, alt_font)
self.assertNotEqual(word.style, self.font)
def test_word_create_and_add_to_with_background_override(self):
"""Test Word.create_and_add_to with explicit background parameter."""
# Create mock container
mock_container = Mock()
mock_container.style = self.font
mock_container.background = "container_bg"
mock_container.add_word = Mock()
# Ensure _words doesn't interfere
del mock_container._words
# Create word with background override
word = Word.create_and_add_to("test", mock_container, background="override_bg")
# Test that word uses the override background
self.assertEqual(word.background, "override_bg")
def test_word_create_and_add_to_inherit_container_background(self):
"""Test Word.create_and_add_to inheriting background from container."""
# Create mock container with background
mock_container = Mock()
mock_container.style = self.font
mock_container.background = "container_bg"
mock_container.add_word = Mock()
# Ensure _words doesn't interfere
del mock_container._words
# Create word without background override
word = Word.create_and_add_to("test", mock_container)
# Test that word inherits container background
self.assertEqual(word.background, "container_bg")
def test_word_create_and_add_to_with_words_list_previous(self):
"""Test Word.create_and_add_to linking with previous word from _words list."""
# Create mock container with _words list
mock_container = Mock()
mock_container.style = self.font
mock_container.add_word = Mock()
# Create existing word and add to container's _words list
existing_word = Word("previous", self.font)
mock_container._words = [existing_word]
# Create new word
word = Word.create_and_add_to("current", mock_container)
# Test that words are linked
self.assertEqual(word.previous, existing_word)
self.assertEqual(existing_word.next, word)
def test_word_create_and_add_to_with_words_method_previous(self):
"""Test Word.create_and_add_to linking with previous word from words() method."""
# Create a simple container that implements words() method
class SimpleContainer:
def __init__(self, font):
self.style = font
self.existing_word = Word("previous", font)
def words(self):
yield ("span1", self.existing_word)
def add_word(self, word):
pass # Simple implementation
container = SimpleContainer(self.font)
# Create new word
word = Word.create_and_add_to("current", container)
# Test that words are linked
self.assertEqual(word.previous, container.existing_word)
self.assertEqual(container.existing_word.next, word)
def test_word_create_and_add_to_no_style_error(self):
"""Test Word.create_and_add_to raises error when container has no style."""
# Create container without style
class BadContainer:
def add_word(self, word):
pass
container = BadContainer()
# Test that AttributeError is raised
with self.assertRaises(AttributeError) as context:
Word.create_and_add_to("test", container)
self.assertIn("must have a 'style' property", str(context.exception))
def test_word_create_and_add_to_no_add_word_error(self):
"""Test Word.create_and_add_to raises error when container has no add_word method."""
# Create container without add_word
class BadContainer:
def __init__(self, font):
self.style = font
container = BadContainer(self.font)
# Test that AttributeError is raised
with self.assertRaises(AttributeError) as context:
Word.create_and_add_to("test", container)
self.assertIn("must have an 'add_word' method", str(context.exception))
class TestFormattedSpan(unittest.TestCase): class TestFormattedSpan(unittest.TestCase):
@ -386,6 +530,97 @@ class TestFormattedSpan(unittest.TestCase):
first_word = span.add_word("first") first_word = span.add_word("first")
self.assertIsNone(first_word.previous) self.assertIsNone(first_word.previous)
self.assertIsNone(first_word.next) self.assertIsNone(first_word.next)
def test_formatted_span_create_and_add_to_with_container_style(self):
"""Test FormattedSpan.create_and_add_to with container that has style property."""
# Create mock container with style and add_span method
mock_container = Mock()
mock_container.style = self.font
mock_container.add_span = Mock()
# Remove background so it inherits from font
del mock_container.background
# Create and add span
span = FormattedSpan.create_and_add_to(mock_container)
# Test that span was created with correct properties
self.assertIsInstance(span, FormattedSpan)
self.assertEqual(span.style, self.font)
self.assertEqual(span.background, self.font.background)
# Test that add_span was called
mock_container.add_span.assert_called_once_with(span)
def test_formatted_span_create_and_add_to_with_style_override(self):
"""Test FormattedSpan.create_and_add_to with explicit style parameter."""
# Create alternate font
alt_font = Font()
# Create mock container
mock_container = Mock()
mock_container.style = self.font
mock_container.add_span = Mock()
# Create span with style override
span = FormattedSpan.create_and_add_to(mock_container, style=alt_font)
# Test that span uses the override style, not container style
self.assertEqual(span.style, alt_font)
self.assertNotEqual(span.style, self.font)
def test_formatted_span_create_and_add_to_with_background_override(self):
"""Test FormattedSpan.create_and_add_to with explicit background parameter."""
# Create mock container
mock_container = Mock()
mock_container.style = self.font
mock_container.background = "container_bg"
mock_container.add_span = Mock()
# Create span with background override
span = FormattedSpan.create_and_add_to(mock_container, background="override_bg")
# Test that span uses the override background
self.assertEqual(span.background, "override_bg")
def test_formatted_span_create_and_add_to_inherit_container_background(self):
"""Test FormattedSpan.create_and_add_to inheriting background from container."""
# Create mock container with background
mock_container = Mock()
mock_container.style = self.font
mock_container.background = "container_bg"
mock_container.add_span = Mock()
# Create span without background override
span = FormattedSpan.create_and_add_to(mock_container)
# Test that span inherits container background
self.assertEqual(span.background, "container_bg")
def test_formatted_span_create_and_add_to_no_style_error(self):
"""Test FormattedSpan.create_and_add_to raises error when container has no style."""
# Create mock container without style
mock_container = Mock()
del mock_container.style
mock_container.add_span = Mock()
# Test that AttributeError is raised
with self.assertRaises(AttributeError) as context:
FormattedSpan.create_and_add_to(mock_container)
self.assertIn("must have a 'style' property", str(context.exception))
def test_formatted_span_create_and_add_to_no_add_span_error(self):
"""Test FormattedSpan.create_and_add_to raises error when container has no add_span method."""
# Create mock container without add_span
mock_container = Mock()
mock_container.style = self.font
del mock_container.add_span
# Test that AttributeError is raised
with self.assertRaises(AttributeError) as context:
FormattedSpan.create_and_add_to(mock_container)
self.assertIn("must have an 'add_span' method", str(context.exception))
class TestWordFormattedSpanIntegration(unittest.TestCase): class TestWordFormattedSpanIntegration(unittest.TestCase):
@ -516,5 +751,106 @@ class TestWordFormattedSpanIntegration(unittest.TestCase):
self.assertEqual(span.words[0].text, "original") self.assertEqual(span.words[0].text, "original")
class TestLineBreak(unittest.TestCase):
"""Test cases for LineBreak class."""
def test_line_break_creation(self):
"""Test line break creation."""
line_break = LineBreak()
# Test initial state
self.assertIsNotNone(line_break.block_type)
self.assertIsNone(line_break.parent)
def test_line_break_block_type(self):
"""Test line break block type property."""
line_break = LineBreak()
# Import BlockType to verify the correct type
from pyWebLayout.abstract.block import BlockType
self.assertEqual(line_break.block_type, BlockType.LINE_BREAK)
def test_line_break_parent_property(self):
"""Test line break parent property getter and setter."""
line_break = LineBreak()
# Test initial state
self.assertIsNone(line_break.parent)
# Test setter
mock_parent = Mock()
line_break.parent = mock_parent
self.assertEqual(line_break.parent, mock_parent)
# Test setting to None
line_break.parent = None
self.assertIsNone(line_break.parent)
def test_line_break_create_and_add_to_with_add_line_break(self):
"""Test LineBreak.create_and_add_to with container that has add_line_break method."""
# Create mock container with add_line_break method
mock_container = Mock()
mock_container.add_line_break = Mock()
# Create and add line break
line_break = LineBreak.create_and_add_to(mock_container)
# Test that line break was created
self.assertIsInstance(line_break, LineBreak)
# Test that add_line_break was called
mock_container.add_line_break.assert_called_once_with(line_break)
def test_line_break_create_and_add_to_with_add_element(self):
"""Test LineBreak.create_and_add_to with container that has add_element method."""
# Create mock container without add_line_break but with add_element
mock_container = Mock()
del mock_container.add_line_break # Ensure no add_line_break method
mock_container.add_element = Mock()
# Create and add line break
line_break = LineBreak.create_and_add_to(mock_container)
# Test that line break was created
self.assertIsInstance(line_break, LineBreak)
# Test that add_element was called
mock_container.add_element.assert_called_once_with(line_break)
def test_line_break_create_and_add_to_with_add_word(self):
"""Test LineBreak.create_and_add_to with container that has add_word method."""
# Create mock container with only add_word method
mock_container = Mock()
del mock_container.add_line_break # Ensure no add_line_break method
del mock_container.add_element # Ensure no add_element method
mock_container.add_word = Mock()
# Create and add line break
line_break = LineBreak.create_and_add_to(mock_container)
# Test that line break was created
self.assertIsInstance(line_break, LineBreak)
# Test that add_word was called
mock_container.add_word.assert_called_once_with(line_break)
def test_line_break_create_and_add_to_fallback(self):
"""Test LineBreak.create_and_add_to fallback behavior when no add methods available."""
# Create mock container without any add methods
mock_container = Mock()
del mock_container.add_line_break
del mock_container.add_element
del mock_container.add_word
# Create and add line break
line_break = LineBreak.create_and_add_to(mock_container)
# Test that line break was created
self.assertIsInstance(line_break, LineBreak)
# Test that parent was set manually
self.assertEqual(line_break.parent, mock_container)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()