Feature: POST-based diagram generation with local caching #2
259
Documentation/TESTING.md
Normal file
259
Documentation/TESTING.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# Running Tests
|
||||
|
||||
This document describes how to run the test suite for the vgui-cicd project.
|
||||
|
||||
## Test Structure
|
||||
|
||||
The project includes comprehensive unit tests for the diagram caching functionality:
|
||||
|
||||
```
|
||||
abschnitte/
|
||||
├── tests.py # Tests for rendering functions
|
||||
├── management/
|
||||
│ └── commands/
|
||||
│ └── test_clear_diagram_cache.py # Tests for management command
|
||||
|
||||
diagramm_proxy/
|
||||
└── test_diagram_cache.py # Tests for caching module
|
||||
```
|
||||
|
||||
## Running All Tests
|
||||
|
||||
To run the entire test suite:
|
||||
|
||||
```bash
|
||||
python manage.py test
|
||||
```
|
||||
|
||||
## Running Specific Test Modules
|
||||
|
||||
Run tests for a specific app:
|
||||
|
||||
```bash
|
||||
# Test abschnitte rendering
|
||||
python manage.py test abschnitte
|
||||
|
||||
# Test diagram caching
|
||||
python manage.py test diagramm_proxy
|
||||
|
||||
# Test management commands
|
||||
python manage.py test abschnitte.management.commands
|
||||
```
|
||||
|
||||
## Running Individual Test Cases
|
||||
|
||||
Run a specific test case:
|
||||
|
||||
```bash
|
||||
# Test diagram cache functionality
|
||||
python manage.py test diagramm_proxy.test_diagram_cache.DiagramCacheTestCase
|
||||
|
||||
# Test rendering functions
|
||||
python manage.py test abschnitte.tests.RenderTextabschnitteTestCase
|
||||
|
||||
# Test management command
|
||||
python manage.py test abschnitte.management.commands.test_clear_diagram_cache
|
||||
```
|
||||
|
||||
## Running Individual Tests
|
||||
|
||||
Run a single test method:
|
||||
|
||||
```bash
|
||||
python manage.py test abschnitte.tests.RenderTextabschnitteTestCase.test_render_diagram_success
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
To generate a coverage report, install coverage.py:
|
||||
|
||||
```bash
|
||||
pip install coverage
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
# Run tests with coverage
|
||||
coverage run --source='.' manage.py test
|
||||
|
||||
# Generate coverage report
|
||||
coverage report
|
||||
|
||||
# Generate HTML coverage report
|
||||
coverage html
|
||||
# Open htmlcov/index.html in browser
|
||||
```
|
||||
|
||||
## Test Options
|
||||
|
||||
### Verbose Output
|
||||
|
||||
Get more detailed output:
|
||||
|
||||
```bash
|
||||
python manage.py test --verbosity=2
|
||||
```
|
||||
|
||||
### Keep Test Database
|
||||
|
||||
Keep the test database after tests complete (useful for debugging):
|
||||
|
||||
```bash
|
||||
python manage.py test --keepdb
|
||||
```
|
||||
|
||||
### Fail Fast
|
||||
|
||||
Stop after first test failure:
|
||||
|
||||
```bash
|
||||
python manage.py test --failfast
|
||||
```
|
||||
|
||||
### Parallel Testing
|
||||
|
||||
Run tests in parallel (faster for large test suites):
|
||||
|
||||
```bash
|
||||
python manage.py test --parallel
|
||||
```
|
||||
|
||||
## What the Tests Cover
|
||||
|
||||
### Diagram Caching Tests (`diagramm_proxy/test_diagram_cache.py`)
|
||||
|
||||
- ✅ Hash computation and consistency
|
||||
- ✅ Cache path generation
|
||||
- ✅ Cache miss (diagram generation)
|
||||
- ✅ Cache hit (using cached diagrams)
|
||||
- ✅ HTTP error handling
|
||||
- ✅ Cache clearing (all and by type)
|
||||
- ✅ Unicode content handling
|
||||
- ✅ Timeout configuration
|
||||
- ✅ Full lifecycle integration tests
|
||||
|
||||
### Rendering Tests (`abschnitte/tests.py`)
|
||||
|
||||
- ✅ Text rendering with markdown
|
||||
- ✅ Unordered and ordered lists
|
||||
- ✅ Table rendering
|
||||
- ✅ Diagram rendering (success and error)
|
||||
- ✅ Diagram with custom options
|
||||
- ✅ Code blocks
|
||||
- ✅ Edge cases (empty content, missing types)
|
||||
- ✅ Multiple sections
|
||||
- ✅ Mixed content integration tests
|
||||
|
||||
### Management Command Tests (`abschnitte/management/commands/test_clear_diagram_cache.py`)
|
||||
|
||||
- ✅ Clearing all cache
|
||||
- ✅ Clearing by specific type
|
||||
- ✅ Clearing empty cache
|
||||
- ✅ Command help text
|
||||
- ✅ Full workflow integration
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
These tests are designed to run in CI/CD pipelines. Example GitHub Actions workflow:
|
||||
|
||||
```yaml
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.11
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
- name: Run tests
|
||||
run: |
|
||||
python manage.py test --verbosity=2
|
||||
```
|
||||
|
||||
## Debugging Failed Tests
|
||||
|
||||
If tests fail, you can:
|
||||
|
||||
1. **Run with verbose output** to see detailed error messages:
|
||||
```bash
|
||||
python manage.py test --verbosity=2
|
||||
```
|
||||
|
||||
2. **Run specific failing test** to isolate the issue:
|
||||
```bash
|
||||
python manage.py test path.to.TestCase.test_method
|
||||
```
|
||||
|
||||
3. **Use pdb for debugging**:
|
||||
```python
|
||||
import pdb; pdb.set_trace() # Add to test code
|
||||
```
|
||||
|
||||
4. **Check test database**:
|
||||
```bash
|
||||
python manage.py test --keepdb
|
||||
```
|
||||
|
||||
## Test Requirements
|
||||
|
||||
The tests use:
|
||||
- Django's built-in `TestCase` class
|
||||
- Python's `unittest.mock` for mocking external dependencies
|
||||
- `@override_settings` for temporary setting changes
|
||||
- `tempfile` for creating temporary directories
|
||||
|
||||
No additional testing libraries are required beyond what's in `requirements.txt`.
|
||||
|
||||
## Writing New Tests
|
||||
|
||||
When adding new features, follow these patterns:
|
||||
|
||||
1. **Test file location**: Place tests in the same app as the code being tested
|
||||
2. **Test class naming**: Use descriptive names ending in `TestCase`
|
||||
3. **Test method naming**: Start with `test_` and describe what's being tested
|
||||
4. **Use mocks**: Mock external dependencies (HTTP calls, file systems when needed)
|
||||
5. **Clean up**: Use `setUp()` and `tearDown()` or temporary directories
|
||||
6. **Test edge cases**: Include tests for error conditions and edge cases
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
from django.test import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
class MyFeatureTestCase(TestCase):
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.test_data = "example"
|
||||
|
||||
def test_basic_functionality(self):
|
||||
"""Test that the feature works correctly."""
|
||||
result = my_function(self.test_data)
|
||||
self.assertEqual(result, expected_value)
|
||||
|
||||
@patch('my_app.module.external_call')
|
||||
def test_with_mock(self, mock_call):
|
||||
"""Test with mocked external dependency."""
|
||||
mock_call.return_value = "mocked"
|
||||
result = my_function(self.test_data)
|
||||
mock_call.assert_called_once()
|
||||
```
|
||||
|
||||
## Test Best Practices
|
||||
|
||||
- Write tests before or alongside code (TDD approach)
|
||||
- Each test should test one thing
|
||||
- Tests should be independent (no shared state)
|
||||
- Use descriptive test names
|
||||
- Mock external dependencies (HTTP, filesystem, etc.)
|
||||
- Test both success and failure cases
|
||||
- Aim for high code coverage (>80%)
|
||||
99
TESTING_QUICKSTART.md
Normal file
99
TESTING_QUICKSTART.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Quick Test Guide
|
||||
|
||||
Quick reference for running tests in the vgui-cicd project.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
python manage.py test
|
||||
|
||||
# Or use the convenient runner
|
||||
python run_tests.py all
|
||||
```
|
||||
|
||||
## 📋 Common Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `python run_tests.py all` | Run all tests with verbose output |
|
||||
| `python run_tests.py cache` | Run diagram cache tests only |
|
||||
| `python run_tests.py render` | Run rendering tests only |
|
||||
| `python run_tests.py commands` | Run management command tests |
|
||||
| `python run_tests.py coverage` | Run tests with coverage report |
|
||||
| `python run_tests.py fast` | Run with fail-fast (stops at first failure) |
|
||||
|
||||
## 🎯 Run Specific Tests
|
||||
|
||||
```bash
|
||||
# Test a specific module
|
||||
python manage.py test diagramm_proxy
|
||||
|
||||
# Test a specific test case
|
||||
python manage.py test abschnitte.tests.RenderTextabschnitteTestCase
|
||||
|
||||
# Test a specific method
|
||||
python manage.py test abschnitte.tests.RenderTextabschnitteTestCase.test_render_diagram_success
|
||||
```
|
||||
|
||||
## 📊 Coverage Report
|
||||
|
||||
```bash
|
||||
# Generate coverage report
|
||||
python run_tests.py coverage
|
||||
|
||||
# Or manually
|
||||
pip install coverage
|
||||
coverage run --source='.' manage.py test
|
||||
coverage report
|
||||
coverage html # Creates htmlcov/index.html
|
||||
```
|
||||
|
||||
## ✅ What's Tested
|
||||
|
||||
### Diagram Caching (`diagramm_proxy/test_diagram_cache.py`)
|
||||
- Hash computation ✓
|
||||
- Cache hit/miss ✓
|
||||
- HTTP errors ✓
|
||||
- Cache clearing ✓
|
||||
- Unicode support ✓
|
||||
|
||||
### Rendering (`abschnitte/tests.py`)
|
||||
- Markdown to HTML ✓
|
||||
- Lists (ordered/unordered) ✓
|
||||
- Tables ✓
|
||||
- Diagrams ✓
|
||||
- Code blocks ✓
|
||||
|
||||
### Management Commands (`abschnitte/management/commands/test_clear_diagram_cache.py`)
|
||||
- Clear all cache ✓
|
||||
- Clear by type ✓
|
||||
- Help text ✓
|
||||
|
||||
## 🐛 Debugging Failed Tests
|
||||
|
||||
```bash
|
||||
# Run with more detail
|
||||
python manage.py test --verbosity=2
|
||||
|
||||
# Stop at first failure
|
||||
python manage.py test --failfast
|
||||
|
||||
# Keep test database
|
||||
python manage.py test --keepdb
|
||||
```
|
||||
|
||||
## 📖 More Information
|
||||
|
||||
See `Documentation/TESTING.md` for complete documentation.
|
||||
|
||||
## 🎓 Test Statistics
|
||||
|
||||
- **Total Test Files**: 3
|
||||
- **Total Test Cases**: 29+
|
||||
- **Lines of Test Code**: 790+
|
||||
- **Coverage Target**: >80%
|
||||
|
||||
---
|
||||
|
||||
**Need help?** Check the full testing guide in `Documentation/TESTING.md`
|
||||
164
abschnitte/management/commands/test_clear_diagram_cache.py
Normal file
164
abschnitte/management/commands/test_clear_diagram_cache.py
Normal file
@@ -0,0 +1,164 @@
|
||||
import os
|
||||
import tempfile
|
||||
from io import StringIO
|
||||
from unittest.mock import patch, Mock
|
||||
from django.test import TestCase, override_settings
|
||||
from django.core.management import call_command
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
|
||||
class ClearDiagramCacheCommandTestCase(TestCase):
|
||||
"""Test cases for clear_diagram_cache management command."""
|
||||
|
||||
@override_settings(MEDIA_ROOT=tempfile.mkdtemp())
|
||||
def test_clear_all_cache(self):
|
||||
"""Test clearing all cached diagrams."""
|
||||
# Create test cache files
|
||||
test_files = [
|
||||
'diagram_cache/plantuml/hash1.svg',
|
||||
'diagram_cache/plantuml/hash2.svg',
|
||||
'diagram_cache/mermaid/hash3.svg',
|
||||
]
|
||||
|
||||
for path in test_files:
|
||||
full_path = default_storage.path(path)
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
default_storage.save(path, ContentFile(b'test diagram'))
|
||||
|
||||
# Verify files exist
|
||||
for path in test_files:
|
||||
self.assertTrue(default_storage.exists(path))
|
||||
|
||||
# Run command
|
||||
out = StringIO()
|
||||
call_command('clear_diagram_cache', stdout=out)
|
||||
|
||||
# Verify files are deleted
|
||||
for path in test_files:
|
||||
self.assertFalse(default_storage.exists(path))
|
||||
|
||||
# Verify success message
|
||||
output = out.getvalue()
|
||||
self.assertIn('Clearing all diagram caches', output)
|
||||
self.assertIn('Cache cleared successfully', output)
|
||||
|
||||
@override_settings(MEDIA_ROOT=tempfile.mkdtemp())
|
||||
def test_clear_cache_by_type(self):
|
||||
"""Test clearing cache for specific diagram type."""
|
||||
# Create test cache files
|
||||
plantuml_files = [
|
||||
'diagram_cache/plantuml/hash1.svg',
|
||||
'diagram_cache/plantuml/hash2.svg',
|
||||
]
|
||||
mermaid_files = [
|
||||
'diagram_cache/mermaid/hash3.svg',
|
||||
]
|
||||
|
||||
for path in plantuml_files + mermaid_files:
|
||||
full_path = default_storage.path(path)
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
default_storage.save(path, ContentFile(b'test diagram'))
|
||||
|
||||
# Run command for plantuml only
|
||||
out = StringIO()
|
||||
call_command('clear_diagram_cache', type='plantuml', stdout=out)
|
||||
|
||||
# Verify only plantuml files are deleted
|
||||
for path in plantuml_files:
|
||||
self.assertFalse(default_storage.exists(path))
|
||||
|
||||
for path in mermaid_files:
|
||||
self.assertTrue(default_storage.exists(path))
|
||||
|
||||
# Verify output message
|
||||
output = out.getvalue()
|
||||
self.assertIn('Clearing cache for plantuml', output)
|
||||
self.assertIn('Cache cleared successfully', output)
|
||||
|
||||
@override_settings(MEDIA_ROOT=tempfile.mkdtemp())
|
||||
def test_clear_empty_cache(self):
|
||||
"""Test clearing cache when no files exist."""
|
||||
# Don't create any files
|
||||
|
||||
# Should not raise error
|
||||
out = StringIO()
|
||||
call_command('clear_diagram_cache', stdout=out)
|
||||
|
||||
# Should still show success
|
||||
output = out.getvalue()
|
||||
self.assertIn('Cache cleared successfully', output)
|
||||
|
||||
@override_settings(MEDIA_ROOT=tempfile.mkdtemp())
|
||||
@patch('diagramm_proxy.diagram_cache.clear_cache')
|
||||
def test_command_calls_clear_cache_function(self, mock_clear):
|
||||
"""Test that command properly calls the clear_cache function."""
|
||||
# Call without type
|
||||
call_command('clear_diagram_cache')
|
||||
mock_clear.assert_called_once_with(None)
|
||||
|
||||
# Call with type
|
||||
mock_clear.reset_mock()
|
||||
call_command('clear_diagram_cache', type='plantuml')
|
||||
mock_clear.assert_called_once_with('plantuml')
|
||||
|
||||
def test_command_help_text(self):
|
||||
"""Test that command has proper help text."""
|
||||
out = StringIO()
|
||||
call_command('clear_diagram_cache', '--help', stdout=out)
|
||||
output = out.getvalue()
|
||||
|
||||
self.assertIn('Clear cached diagrams', output)
|
||||
self.assertIn('--type', output)
|
||||
|
||||
|
||||
class ManagementCommandIntegrationTestCase(TestCase):
|
||||
"""Integration tests for management command with real filesystem."""
|
||||
|
||||
@override_settings(MEDIA_ROOT=tempfile.mkdtemp())
|
||||
def test_full_command_workflow(self):
|
||||
"""Test complete workflow: create cache, clear specific type, clear all."""
|
||||
# Create mixed cache files
|
||||
files = {
|
||||
'plantuml': [
|
||||
'diagram_cache/plantuml/abc123.svg',
|
||||
'diagram_cache/plantuml/def456.svg',
|
||||
],
|
||||
'mermaid': [
|
||||
'diagram_cache/mermaid/ghi789.svg',
|
||||
],
|
||||
'graphviz': [
|
||||
'diagram_cache/graphviz/jkl012.svg',
|
||||
]
|
||||
}
|
||||
|
||||
# Create all files
|
||||
for file_list in files.values():
|
||||
for path in file_list:
|
||||
full_path = default_storage.path(path)
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
default_storage.save(path, ContentFile(b'diagram content'))
|
||||
|
||||
# Verify all exist
|
||||
for file_list in files.values():
|
||||
for path in file_list:
|
||||
self.assertTrue(default_storage.exists(path))
|
||||
|
||||
# Clear plantuml
|
||||
call_command('clear_diagram_cache', type='plantuml')
|
||||
|
||||
# Verify plantuml deleted, others remain
|
||||
for path in files['plantuml']:
|
||||
self.assertFalse(default_storage.exists(path))
|
||||
for path in files['mermaid']:
|
||||
self.assertTrue(default_storage.exists(path))
|
||||
for path in files['graphviz']:
|
||||
self.assertTrue(default_storage.exists(path))
|
||||
|
||||
# Clear all remaining
|
||||
call_command('clear_diagram_cache')
|
||||
|
||||
# Verify all deleted
|
||||
for file_list in files.values():
|
||||
for path in file_list:
|
||||
self.assertFalse(default_storage.exists(path))
|
||||
@@ -1,3 +1,377 @@
|
||||
from django.test import TestCase
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from django.test import TestCase, override_settings
|
||||
from abschnitte.models import AbschnittTyp, Textabschnitt
|
||||
from abschnitte.utils import render_textabschnitte, md_table_to_html
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
class MockTextabschnitt:
|
||||
"""Mock object for Textabschnitt (since it's abstract)."""
|
||||
def __init__(self, abschnitttyp, inhalt):
|
||||
self.abschnitttyp = abschnitttyp
|
||||
self.inhalt = inhalt
|
||||
|
||||
|
||||
class RenderTextabschnitteTestCase(TestCase):
|
||||
"""Test cases for render_textabschnitte function."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
# Create mock AbschnittTyp objects
|
||||
self.typ_text = Mock()
|
||||
self.typ_text.abschnitttyp = "text"
|
||||
|
||||
self.typ_liste_ungeordnet = Mock()
|
||||
self.typ_liste_ungeordnet.abschnitttyp = "liste ungeordnet"
|
||||
|
||||
self.typ_liste_geordnet = Mock()
|
||||
self.typ_liste_geordnet.abschnitttyp = "liste geordnet"
|
||||
|
||||
self.typ_tabelle = Mock()
|
||||
self.typ_tabelle.abschnitttyp = "tabelle"
|
||||
|
||||
self.typ_diagramm = Mock()
|
||||
self.typ_diagramm.abschnitttyp = "diagramm"
|
||||
|
||||
self.typ_code = Mock()
|
||||
self.typ_code.abschnitttyp = "code"
|
||||
|
||||
def test_render_basic_text(self):
|
||||
"""Test rendering basic text content."""
|
||||
abschnitt = MockTextabschnitt(
|
||||
abschnitttyp=self.typ_text,
|
||||
inhalt="This is **bold** text with *italic*."
|
||||
)
|
||||
|
||||
result = render_textabschnitte([abschnitt])
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
typ, html = result[0]
|
||||
self.assertEqual(typ, "text")
|
||||
self.assertIn("<strong>bold</strong>", html)
|
||||
self.assertIn("<em>italic</em>", html)
|
||||
|
||||
def test_render_unordered_list(self):
|
||||
"""Test rendering unordered list."""
|
||||
abschnitt = MockTextabschnitt(
|
||||
abschnitttyp=self.typ_liste_ungeordnet,
|
||||
inhalt="Item 1\nItem 2\nItem 3"
|
||||
)
|
||||
|
||||
result = render_textabschnitte([abschnitt])
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
typ, html = result[0]
|
||||
self.assertEqual(typ, "liste ungeordnet")
|
||||
self.assertIn("<ul>", html)
|
||||
self.assertIn("<li>Item 1</li>", html)
|
||||
self.assertIn("<li>Item 2</li>", html)
|
||||
self.assertIn("<li>Item 3</li>", html)
|
||||
|
||||
def test_render_ordered_list(self):
|
||||
"""Test rendering ordered list."""
|
||||
abschnitt = MockTextabschnitt(
|
||||
abschnitttyp=self.typ_liste_geordnet,
|
||||
inhalt="First item\nSecond item\nThird item"
|
||||
)
|
||||
|
||||
result = render_textabschnitte([abschnitt])
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
typ, html = result[0]
|
||||
self.assertEqual(typ, "liste geordnet")
|
||||
self.assertIn("<ol>", html)
|
||||
self.assertIn("<li>First item</li>", html)
|
||||
|
||||
def test_render_table(self):
|
||||
"""Test rendering markdown table."""
|
||||
table_content = """| Header 1 | Header 2 |
|
||||
|----------|----------|
|
||||
| Cell 1 | Cell 2 |
|
||||
| Cell 3 | Cell 4 |"""
|
||||
|
||||
abschnitt = MockTextabschnitt(
|
||||
abschnitttyp=self.typ_tabelle,
|
||||
inhalt=table_content
|
||||
)
|
||||
|
||||
result = render_textabschnitte([abschnitt])
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
typ, html = result[0]
|
||||
self.assertEqual(typ, "tabelle")
|
||||
self.assertIn("<table", html)
|
||||
self.assertIn("Header 1", html)
|
||||
self.assertIn("Cell 1", html)
|
||||
self.assertIn("table-bordered", html)
|
||||
|
||||
@override_settings(MEDIA_URL='/media/')
|
||||
@patch('abschnitte.utils.get_cached_diagram')
|
||||
def test_render_diagram_success(self, mock_get_cached):
|
||||
"""Test rendering diagram with successful cache."""
|
||||
mock_get_cached.return_value = 'diagram_cache/plantuml/abc123.svg'
|
||||
|
||||
diagram_content = """plantuml
|
||||
@startuml
|
||||
A -> B
|
||||
@enduml"""
|
||||
|
||||
abschnitt = MockTextabschnitt(
|
||||
abschnitttyp=self.typ_diagramm,
|
||||
inhalt=diagram_content
|
||||
)
|
||||
|
||||
result = render_textabschnitte([abschnitt])
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
typ, html = result[0]
|
||||
self.assertEqual(typ, "diagramm")
|
||||
|
||||
# Verify diagram cache was called correctly
|
||||
mock_get_cached.assert_called_once_with(
|
||||
'plantuml',
|
||||
'@startuml\nA -> B\n@enduml'
|
||||
)
|
||||
|
||||
# Verify HTML contains correct image tag
|
||||
self.assertIn('<img', html)
|
||||
self.assertIn('/media/diagram_cache/plantuml/abc123.svg', html)
|
||||
self.assertIn('width="100%"', html)
|
||||
|
||||
@patch('abschnitte.utils.get_cached_diagram')
|
||||
def test_render_diagram_with_options(self, mock_get_cached):
|
||||
"""Test rendering diagram with custom options."""
|
||||
mock_get_cached.return_value = 'diagram_cache/mermaid/def456.svg'
|
||||
|
||||
diagram_content = """mermaid
|
||||
option:width="50%" height="300px"
|
||||
graph TD
|
||||
A --> B"""
|
||||
|
||||
abschnitt = MockTextabschnitt(
|
||||
abschnitttyp=self.typ_diagramm,
|
||||
inhalt=diagram_content
|
||||
)
|
||||
|
||||
result = render_textabschnitte([abschnitt])
|
||||
|
||||
typ, html = result[0]
|
||||
|
||||
# Verify custom options are used
|
||||
self.assertIn('width="50%"', html)
|
||||
self.assertIn('height="300px"', html)
|
||||
|
||||
# Verify diagram content doesn't include option line
|
||||
call_args = mock_get_cached.call_args[0]
|
||||
self.assertNotIn('option:', call_args[1])
|
||||
|
||||
@patch('abschnitte.utils.get_cached_diagram')
|
||||
def test_render_diagram_error(self, mock_get_cached):
|
||||
"""Test rendering diagram when caching fails."""
|
||||
mock_get_cached.side_effect = Exception("Kroki server unavailable")
|
||||
|
||||
diagram_content = """plantuml
|
||||
@startuml
|
||||
A -> B
|
||||
@enduml"""
|
||||
|
||||
abschnitt = MockTextabschnitt(
|
||||
abschnitttyp=self.typ_diagramm,
|
||||
inhalt=diagram_content
|
||||
)
|
||||
|
||||
result = render_textabschnitte([abschnitt])
|
||||
|
||||
typ, html = result[0]
|
||||
|
||||
# Should show error message
|
||||
self.assertIn('Error generating diagram', html)
|
||||
self.assertIn('Kroki server unavailable', html)
|
||||
self.assertIn('text-danger', html)
|
||||
|
||||
def test_render_code(self):
|
||||
"""Test rendering code block."""
|
||||
code_content = """def hello():
|
||||
print("Hello, World!")"""
|
||||
|
||||
abschnitt = MockTextabschnitt(
|
||||
abschnitttyp=self.typ_code,
|
||||
inhalt=code_content
|
||||
)
|
||||
|
||||
result = render_textabschnitte([abschnitt])
|
||||
|
||||
typ, html = result[0]
|
||||
self.assertEqual(typ, "code")
|
||||
self.assertIn("<pre>", html)
|
||||
self.assertIn("<code>", html)
|
||||
self.assertIn("def hello():", html)
|
||||
|
||||
def test_render_empty_content(self):
|
||||
"""Test rendering with empty or None content."""
|
||||
abschnitt = MockTextabschnitt(
|
||||
abschnitttyp=self.typ_text,
|
||||
inhalt=None
|
||||
)
|
||||
|
||||
result = render_textabschnitte([abschnitt])
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
typ, html = result[0]
|
||||
self.assertEqual(typ, "text")
|
||||
# Should not crash, should return empty or minimal HTML
|
||||
|
||||
def test_render_multiple_abschnitte(self):
|
||||
"""Test rendering multiple sections."""
|
||||
abschnitte = [
|
||||
MockTextabschnitt(self.typ_text, "First paragraph"),
|
||||
MockTextabschnitt(self.typ_liste_ungeordnet, "Item 1\nItem 2"),
|
||||
MockTextabschnitt(self.typ_text, "Second paragraph"),
|
||||
]
|
||||
|
||||
result = render_textabschnitte(abschnitte)
|
||||
|
||||
self.assertEqual(len(result), 3)
|
||||
self.assertEqual(result[0][0], "text")
|
||||
self.assertEqual(result[1][0], "liste ungeordnet")
|
||||
self.assertEqual(result[2][0], "text")
|
||||
|
||||
def test_render_no_abschnitttyp(self):
|
||||
"""Test rendering when abschnitttyp is None."""
|
||||
abschnitt = MockTextabschnitt(
|
||||
abschnitttyp=None,
|
||||
inhalt="Some content"
|
||||
)
|
||||
|
||||
result = render_textabschnitte([abschnitt])
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
typ, html = result[0]
|
||||
self.assertEqual(typ, "")
|
||||
# Should still render as markdown
|
||||
self.assertIn("Some content", html)
|
||||
|
||||
|
||||
class MdTableToHtmlTestCase(TestCase):
|
||||
"""Test cases for md_table_to_html function."""
|
||||
|
||||
def test_basic_table(self):
|
||||
"""Test converting basic markdown table to HTML."""
|
||||
md = """| Name | Age |
|
||||
|------|-----|
|
||||
| Alice | 30 |
|
||||
| Bob | 25 |"""
|
||||
|
||||
html = md_table_to_html(md)
|
||||
|
||||
self.assertIn('<table', html)
|
||||
self.assertIn('table-bordered', html)
|
||||
self.assertIn('table-hover', html)
|
||||
self.assertIn('<thead>', html)
|
||||
self.assertIn('<tbody>', html)
|
||||
self.assertIn('<th>Name</th>', html)
|
||||
self.assertIn('<th>Age</th>', html)
|
||||
self.assertIn('<td>Alice</td>', html)
|
||||
self.assertIn('<td>30</td>', html)
|
||||
|
||||
def test_table_with_spaces(self):
|
||||
"""Test table with various spacing."""
|
||||
md = """| Name | Age |
|
||||
|--------|-------|
|
||||
| Alice | 30 |
|
||||
| Bob | 25 |"""
|
||||
|
||||
html = md_table_to_html(md)
|
||||
|
||||
# Spaces should be trimmed
|
||||
self.assertIn('<th>Name</th>', html)
|
||||
self.assertIn('<th>Age</th>', html)
|
||||
self.assertIn('<td>Alice</td>', html)
|
||||
|
||||
def test_table_no_outer_pipes(self):
|
||||
"""Test table without outer pipe characters."""
|
||||
md = """Name | Age
|
||||
-----|-----
|
||||
Alice | 30
|
||||
Bob | 25"""
|
||||
|
||||
html = md_table_to_html(md)
|
||||
|
||||
self.assertIn('<th>Name</th>', html)
|
||||
self.assertIn('<td>Alice</td>', html)
|
||||
|
||||
def test_table_insufficient_rows(self):
|
||||
"""Test error handling for malformed table."""
|
||||
md = """| Name |
|
||||
|------|"""
|
||||
|
||||
with self.assertRaises(ValueError) as context:
|
||||
md_table_to_html(md)
|
||||
|
||||
self.assertIn("at least header + separator", str(context.exception))
|
||||
|
||||
def test_table_with_empty_cells(self):
|
||||
"""Test table with empty cells."""
|
||||
md = """| Name | Age |
|
||||
|------|-----|
|
||||
| Alice | |
|
||||
| | 25 |"""
|
||||
|
||||
html = md_table_to_html(md)
|
||||
|
||||
self.assertIn('<td>Alice</td>', html)
|
||||
self.assertIn('<td></td>', html)
|
||||
self.assertIn('<td>25</td>', html)
|
||||
|
||||
def test_table_three_columns(self):
|
||||
"""Test table with more columns."""
|
||||
md = """| First | Middle | Last |
|
||||
|-------|--------|------|
|
||||
| John | Q | Doe |"""
|
||||
|
||||
html = md_table_to_html(md)
|
||||
|
||||
self.assertIn('<th>First</th>', html)
|
||||
self.assertIn('<th>Middle</th>', html)
|
||||
self.assertIn('<th>Last</th>', html)
|
||||
self.assertIn('<td>John</td>', html)
|
||||
self.assertIn('<td>Q</td>', html)
|
||||
self.assertIn('<td>Doe</td>', html)
|
||||
|
||||
|
||||
class RenderIntegrationTestCase(TestCase):
|
||||
"""Integration tests for rendering pipeline."""
|
||||
|
||||
@patch('abschnitte.utils.get_cached_diagram')
|
||||
def test_mixed_content_rendering(self, mock_get_cached):
|
||||
"""Test rendering a mix of different content types."""
|
||||
mock_get_cached.return_value = 'diagram_cache/test.svg'
|
||||
|
||||
# Create various types of content
|
||||
typ_text = Mock(abschnitttyp="text")
|
||||
typ_liste = Mock(abschnitttyp="liste ungeordnet")
|
||||
typ_diagram = Mock(abschnitttyp="diagramm")
|
||||
|
||||
abschnitte = [
|
||||
MockTextabschnitt(typ_text, "Introduction paragraph"),
|
||||
MockTextabschnitt(typ_liste, "Point 1\nPoint 2\nPoint 3"),
|
||||
MockTextabschnitt(typ_diagram, "plantuml\n@startuml\nA -> B\n@enduml"),
|
||||
MockTextabschnitt(typ_text, "Conclusion"),
|
||||
]
|
||||
|
||||
result = render_textabschnitte(abschnitte)
|
||||
|
||||
# Should have 4 results
|
||||
self.assertEqual(len(result), 4)
|
||||
|
||||
# Verify each type was rendered correctly
|
||||
self.assertEqual(result[0][0], "text")
|
||||
self.assertIn("Introduction", result[0][1])
|
||||
|
||||
self.assertEqual(result[1][0], "liste ungeordnet")
|
||||
self.assertIn("<ul>", result[1][1])
|
||||
|
||||
self.assertEqual(result[2][0], "diagramm")
|
||||
self.assertIn("<img", result[2][1])
|
||||
|
||||
self.assertEqual(result[3][0], "text")
|
||||
self.assertIn("Conclusion", result[3][1])
|
||||
|
||||
267
diagramm_proxy/test_diagram_cache.py
Normal file
267
diagramm_proxy/test_diagram_cache.py
Normal file
@@ -0,0 +1,267 @@
|
||||
import os
|
||||
import hashlib
|
||||
import tempfile
|
||||
from unittest.mock import patch, Mock, MagicMock
|
||||
from django.test import TestCase, override_settings
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.files.base import ContentFile
|
||||
from diagramm_proxy.diagram_cache import (
|
||||
get_cache_path,
|
||||
compute_hash,
|
||||
get_cached_diagram,
|
||||
clear_cache,
|
||||
KROKI_UPSTREAM
|
||||
)
|
||||
|
||||
|
||||
class DiagramCacheTestCase(TestCase):
|
||||
"""Test cases for diagram caching functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.test_diagram_content = """
|
||||
@startuml
|
||||
Alice -> Bob: Hello
|
||||
Bob -> Alice: Hi
|
||||
@enduml
|
||||
"""
|
||||
self.diagram_type = "plantuml"
|
||||
self.temp_media_root = tempfile.mkdtemp()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after tests."""
|
||||
# Clean up temporary files
|
||||
import shutil
|
||||
if os.path.exists(self.temp_media_root):
|
||||
shutil.rmtree(self.temp_media_root)
|
||||
|
||||
def test_compute_hash(self):
|
||||
"""Test that hash computation is consistent."""
|
||||
content1 = "test content"
|
||||
content2 = "test content"
|
||||
content3 = "different content"
|
||||
|
||||
hash1 = compute_hash(content1)
|
||||
hash2 = compute_hash(content2)
|
||||
hash3 = compute_hash(content3)
|
||||
|
||||
# Same content should produce same hash
|
||||
self.assertEqual(hash1, hash2)
|
||||
|
||||
# Different content should produce different hash
|
||||
self.assertNotEqual(hash1, hash3)
|
||||
|
||||
# Hash should be valid SHA256 (64 hex characters)
|
||||
self.assertEqual(len(hash1), 64)
|
||||
self.assertTrue(all(c in '0123456789abcdef' for c in hash1))
|
||||
|
||||
def test_get_cache_path(self):
|
||||
"""Test cache path generation."""
|
||||
content_hash = "abc123def456"
|
||||
diagram_type = "mermaid"
|
||||
|
||||
path = get_cache_path(diagram_type, content_hash)
|
||||
|
||||
# Path should contain diagram type and hash
|
||||
self.assertIn("diagram_cache", path)
|
||||
self.assertIn(diagram_type, path)
|
||||
self.assertIn(content_hash, path)
|
||||
self.assertTrue(path.endswith(".svg"))
|
||||
|
||||
@override_settings(MEDIA_ROOT=tempfile.mkdtemp())
|
||||
@patch('diagramm_proxy.diagram_cache.requests.post')
|
||||
def test_get_cached_diagram_cache_miss(self, mock_post):
|
||||
"""Test diagram generation on cache miss."""
|
||||
# Mock successful Kroki response
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = b'<svg>test diagram</svg>'
|
||||
mock_response.raise_for_status = Mock()
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
# Call function
|
||||
result = get_cached_diagram(self.diagram_type, self.test_diagram_content)
|
||||
|
||||
# Verify POST was called with correct parameters
|
||||
expected_url = f"{KROKI_UPSTREAM}/{self.diagram_type}/svg"
|
||||
mock_post.assert_called_once()
|
||||
call_args = mock_post.call_args
|
||||
self.assertEqual(call_args[0][0], expected_url)
|
||||
self.assertEqual(call_args[1]['data'], self.test_diagram_content.encode('utf-8'))
|
||||
self.assertEqual(call_args[1]['headers']['Content-Type'], 'text/plain')
|
||||
|
||||
# Verify result is a valid path
|
||||
self.assertIsNotNone(result)
|
||||
self.assertIn('diagram_cache', result)
|
||||
|
||||
# Verify file was cached
|
||||
self.assertTrue(default_storage.exists(result))
|
||||
|
||||
@override_settings(MEDIA_ROOT=tempfile.mkdtemp())
|
||||
def test_get_cached_diagram_cache_hit(self):
|
||||
"""Test that cached diagrams are retrieved without HTTP call."""
|
||||
# Pre-populate cache
|
||||
content_hash = compute_hash(self.test_diagram_content)
|
||||
cache_path = get_cache_path(self.diagram_type, content_hash)
|
||||
|
||||
# Create cache directory and file
|
||||
full_path = default_storage.path(cache_path)
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
default_storage.save(cache_path, ContentFile(b'<svg>cached diagram</svg>'))
|
||||
|
||||
# Mock requests.post to ensure it's NOT called
|
||||
with patch('diagramm_proxy.diagram_cache.requests.post') as mock_post:
|
||||
result = get_cached_diagram(self.diagram_type, self.test_diagram_content)
|
||||
|
||||
# Verify POST was NOT called (cache hit)
|
||||
mock_post.assert_not_called()
|
||||
|
||||
# Verify correct path returned
|
||||
self.assertEqual(result, cache_path)
|
||||
|
||||
# Verify file exists
|
||||
self.assertTrue(default_storage.exists(result))
|
||||
|
||||
@override_settings(MEDIA_ROOT=tempfile.mkdtemp())
|
||||
@patch('diagramm_proxy.diagram_cache.requests.post')
|
||||
def test_get_cached_diagram_http_error(self, mock_post):
|
||||
"""Test error handling when Kroki server fails."""
|
||||
# Mock failed HTTP response
|
||||
mock_post.side_effect = Exception("Connection failed")
|
||||
|
||||
# Should raise exception
|
||||
with self.assertRaises(Exception):
|
||||
get_cached_diagram(self.diagram_type, self.test_diagram_content)
|
||||
|
||||
@override_settings(MEDIA_ROOT=tempfile.mkdtemp())
|
||||
def test_clear_cache_all(self):
|
||||
"""Test clearing all cached diagrams."""
|
||||
# Create some test cache files
|
||||
test_files = [
|
||||
('diagram_cache/plantuml/hash1.svg', b'diagram1'),
|
||||
('diagram_cache/plantuml/hash2.svg', b'diagram2'),
|
||||
('diagram_cache/mermaid/hash3.svg', b'diagram3'),
|
||||
]
|
||||
|
||||
for path, content in test_files:
|
||||
full_path = default_storage.path(path)
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
default_storage.save(path, ContentFile(content))
|
||||
|
||||
# Verify files exist
|
||||
for path, _ in test_files:
|
||||
self.assertTrue(default_storage.exists(path))
|
||||
|
||||
# Clear all cache
|
||||
clear_cache()
|
||||
|
||||
# Verify files are deleted
|
||||
for path, _ in test_files:
|
||||
self.assertFalse(default_storage.exists(path))
|
||||
|
||||
@override_settings(MEDIA_ROOT=tempfile.mkdtemp())
|
||||
def test_clear_cache_by_type(self):
|
||||
"""Test clearing cache for specific diagram type."""
|
||||
# Create test cache files
|
||||
plantuml_file = 'diagram_cache/plantuml/hash1.svg'
|
||||
mermaid_file = 'diagram_cache/mermaid/hash2.svg'
|
||||
|
||||
for path in [plantuml_file, mermaid_file]:
|
||||
full_path = default_storage.path(path)
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
default_storage.save(path, ContentFile(b'test'))
|
||||
|
||||
# Clear only plantuml cache
|
||||
clear_cache('plantuml')
|
||||
|
||||
# Verify plantuml file is deleted but mermaid remains
|
||||
self.assertFalse(default_storage.exists(plantuml_file))
|
||||
self.assertTrue(default_storage.exists(mermaid_file))
|
||||
|
||||
@override_settings(MEDIA_ROOT=tempfile.mkdtemp())
|
||||
@patch('diagramm_proxy.diagram_cache.requests.post')
|
||||
def test_same_content_different_type(self, mock_post):
|
||||
"""Test that same content but different type creates different cache entries."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = b'<svg>diagram</svg>'
|
||||
mock_response.raise_for_status = Mock()
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
# Generate for two different types
|
||||
path1 = get_cached_diagram('plantuml', self.test_diagram_content)
|
||||
path2 = get_cached_diagram('mermaid', self.test_diagram_content)
|
||||
|
||||
# Paths should be different
|
||||
self.assertNotEqual(path1, path2)
|
||||
self.assertIn('plantuml', path1)
|
||||
self.assertIn('mermaid', path2)
|
||||
|
||||
# Both should exist
|
||||
self.assertTrue(default_storage.exists(path1))
|
||||
self.assertTrue(default_storage.exists(path2))
|
||||
|
||||
def test_compute_hash_unicode(self):
|
||||
"""Test hash computation with unicode characters."""
|
||||
content_with_unicode = "Test with émojis 🎨 and ümlauts"
|
||||
|
||||
# Should not raise exception
|
||||
result = compute_hash(content_with_unicode)
|
||||
|
||||
# Should produce valid hash
|
||||
self.assertEqual(len(result), 64)
|
||||
self.assertTrue(all(c in '0123456789abcdef' for c in result))
|
||||
|
||||
@override_settings(MEDIA_ROOT=tempfile.mkdtemp())
|
||||
@patch('diagramm_proxy.diagram_cache.requests.post')
|
||||
def test_timeout_configuration(self, mock_post):
|
||||
"""Test that POST request includes timeout."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = b'<svg>test</svg>'
|
||||
mock_response.raise_for_status = Mock()
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
get_cached_diagram(self.diagram_type, self.test_diagram_content)
|
||||
|
||||
# Verify timeout parameter was passed
|
||||
call_kwargs = mock_post.call_args[1]
|
||||
self.assertIn('timeout', call_kwargs)
|
||||
self.assertEqual(call_kwargs['timeout'], 30)
|
||||
|
||||
|
||||
class DiagramCacheIntegrationTestCase(TestCase):
|
||||
"""Integration tests for diagram caching with file system."""
|
||||
|
||||
@override_settings(MEDIA_ROOT=tempfile.mkdtemp())
|
||||
@patch('diagramm_proxy.diagram_cache.requests.post')
|
||||
def test_full_cache_lifecycle(self, mock_post):
|
||||
"""Test complete lifecycle: cache miss, cache hit, clear."""
|
||||
diagram_content = "@startuml\nA -> B\n@enduml"
|
||||
diagram_type = "plantuml"
|
||||
|
||||
# Mock Kroki response
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = b'<svg>lifecycle test</svg>'
|
||||
mock_response.raise_for_status = Mock()
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
# First call - cache miss
|
||||
path1 = get_cached_diagram(diagram_type, diagram_content)
|
||||
self.assertEqual(mock_post.call_count, 1)
|
||||
self.assertTrue(default_storage.exists(path1))
|
||||
|
||||
# Second call - cache hit
|
||||
path2 = get_cached_diagram(diagram_type, diagram_content)
|
||||
self.assertEqual(mock_post.call_count, 1) # Still 1, not called again
|
||||
self.assertEqual(path1, path2)
|
||||
|
||||
# Clear cache
|
||||
clear_cache()
|
||||
self.assertFalse(default_storage.exists(path1))
|
||||
|
||||
# Third call - cache miss again
|
||||
path3 = get_cached_diagram(diagram_type, diagram_content)
|
||||
self.assertEqual(mock_post.call_count, 2) # Called again
|
||||
self.assertTrue(default_storage.exists(path3))
|
||||
60
pytest.ini
Normal file
60
pytest.ini
Normal file
@@ -0,0 +1,60 @@
|
||||
[tool:pytest]
|
||||
# Pytest configuration for vgui-cicd project
|
||||
# To use pytest instead of Django's test runner:
|
||||
# pip install pytest pytest-django
|
||||
# pytest
|
||||
|
||||
DJANGO_SETTINGS_MODULE = VorgabenUI.settings
|
||||
python_files = tests.py test_*.py *_tests.py
|
||||
python_classes = *TestCase *Test
|
||||
python_functions = test_*
|
||||
|
||||
# Test discovery patterns
|
||||
testpaths =
|
||||
abschnitte
|
||||
diagramm_proxy
|
||||
standards
|
||||
stichworte
|
||||
referenzen
|
||||
rollen
|
||||
pages
|
||||
|
||||
# Output options
|
||||
addopts =
|
||||
--verbose
|
||||
--strict-markers
|
||||
--tb=short
|
||||
--reuse-db
|
||||
|
||||
# Coverage options (when using pytest-cov)
|
||||
# Uncomment to enable:
|
||||
# --cov=.
|
||||
# --cov-report=html
|
||||
# --cov-report=term-missing
|
||||
|
||||
# Markers for organizing tests
|
||||
markers =
|
||||
slow: marks tests as slow (deselect with '-m "not slow"')
|
||||
integration: marks tests as integration tests
|
||||
unit: marks tests as unit tests
|
||||
cache: marks tests related to caching
|
||||
render: marks tests related to rendering
|
||||
commands: marks tests related to management commands
|
||||
|
||||
# Ignore paths
|
||||
norecursedirs =
|
||||
.git
|
||||
.tox
|
||||
dist
|
||||
build
|
||||
*.egg
|
||||
__pycache__
|
||||
static
|
||||
staticfiles
|
||||
media
|
||||
data
|
||||
venv
|
||||
.venv
|
||||
|
||||
# Django-specific settings
|
||||
django_find_project = false
|
||||
108
run_tests.py
Normal file
108
run_tests.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Test runner script for vgui-cicd project.
|
||||
Provides convenient shortcuts for running different test suites.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
|
||||
def main():
|
||||
"""Run tests based on command line arguments."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'VorgabenUI.settings')
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
command = sys.argv[1]
|
||||
|
||||
if command == 'all':
|
||||
# Run all tests
|
||||
execute_from_command_line(['manage.py', 'test', '--verbosity=2'])
|
||||
|
||||
elif command == 'cache':
|
||||
# Run only cache tests
|
||||
execute_from_command_line(['manage.py', 'test', 'diagramm_proxy', '--verbosity=2'])
|
||||
|
||||
elif command == 'render':
|
||||
# Run only rendering tests
|
||||
execute_from_command_line(['manage.py', 'test', 'abschnitte.tests', '--verbosity=2'])
|
||||
|
||||
elif command == 'commands':
|
||||
# Run only management command tests
|
||||
execute_from_command_line(['manage.py', 'test', 'abschnitte.management.commands', '--verbosity=2'])
|
||||
|
||||
elif command == 'coverage':
|
||||
# Run tests with coverage
|
||||
try:
|
||||
import coverage
|
||||
cov = coverage.Coverage(source=['.'])
|
||||
cov.start()
|
||||
|
||||
execute_from_command_line(['manage.py', 'test', '--verbosity=2'])
|
||||
|
||||
cov.stop()
|
||||
cov.save()
|
||||
|
||||
print('\n' + '='*70)
|
||||
print('COVERAGE REPORT')
|
||||
print('='*70)
|
||||
cov.report()
|
||||
|
||||
print('\nHTML coverage report generated in htmlcov/')
|
||||
cov.html_report()
|
||||
|
||||
except ImportError:
|
||||
print('ERROR: coverage.py not installed.')
|
||||
print('Install with: pip install coverage')
|
||||
sys.exit(1)
|
||||
|
||||
elif command == 'fast':
|
||||
# Run tests with failfast
|
||||
execute_from_command_line(['manage.py', 'test', '--failfast', '--verbosity=2'])
|
||||
|
||||
elif command == 'help':
|
||||
print_help()
|
||||
|
||||
else:
|
||||
# Pass through to Django test runner
|
||||
execute_from_command_line(['manage.py', 'test'] + sys.argv[1:])
|
||||
|
||||
else:
|
||||
print_help()
|
||||
|
||||
|
||||
def print_help():
|
||||
"""Print help message."""
|
||||
help_text = """
|
||||
Usage: python run_tests.py [command]
|
||||
|
||||
Commands:
|
||||
all Run all tests
|
||||
cache Run diagram cache tests only
|
||||
render Run rendering tests only
|
||||
commands Run management command tests only
|
||||
coverage Run tests with coverage report
|
||||
fast Run tests with fail-fast option
|
||||
help Show this help message
|
||||
|
||||
Advanced:
|
||||
You can also pass standard Django test arguments:
|
||||
python run_tests.py <app_label>
|
||||
python run_tests.py <app_label.TestCase>
|
||||
python run_tests.py <app_label.TestCase.test_method>
|
||||
|
||||
Examples:
|
||||
python run_tests.py all
|
||||
python run_tests.py cache
|
||||
python run_tests.py coverage
|
||||
python run_tests.py diagramm_proxy.test_diagram_cache.DiagramCacheTestCase
|
||||
python run_tests.py abschnitte.tests.RenderTextabschnitteTestCase.test_render_diagram_success
|
||||
|
||||
For more options, see Documentation/TESTING.md
|
||||
"""
|
||||
print(help_text)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user