From 5b2c24e59d995fec59f4ef9a375dd3107399398a Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sat, 7 Jun 2025 17:32:34 +0200 Subject: [PATCH] increase test coverage. --- .coveragerc | 31 ++++ COVERAGE_GUTTERS_SETUP.md | 122 ------------ tests/test_abstract_inline.py | 338 +++++++++++++++++++++++++++++++++- 3 files changed, 368 insertions(+), 123 deletions(-) create mode 100644 .coveragerc delete mode 100644 COVERAGE_GUTTERS_SETUP.md diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..ea44045 --- /dev/null +++ b/.coveragerc @@ -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 diff --git a/COVERAGE_GUTTERS_SETUP.md b/COVERAGE_GUTTERS_SETUP.md deleted file mode 100644 index f037e49..0000000 --- a/COVERAGE_GUTTERS_SETUP.md +++ /dev/null @@ -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 diff --git a/tests/test_abstract_inline.py b/tests/test_abstract_inline.py index fb43caa..2489e32 100644 --- a/tests/test_abstract_inline.py +++ b/tests/test_abstract_inline.py @@ -7,7 +7,7 @@ and formatting within documents. import unittest 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 @@ -251,6 +251,150 @@ class TestWord(unittest.TestCase): # Test getting individual parts for i, expected_part in enumerate(expected_parts): 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): @@ -386,6 +530,97 @@ class TestFormattedSpan(unittest.TestCase): first_word = span.add_word("first") self.assertIsNone(first_word.previous) 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): @@ -516,5 +751,106 @@ class TestWordFormattedSpanIntegration(unittest.TestCase): 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__': unittest.main()