# Docker-in-Docker Testing Mode Complete guide to running CWA tests inside Docker containers (Docker-in-Docker scenarios). ## 📋 Table of Contents - [The Problem](#the-problem) - [The Solution](#the-solution) - [Quick Usage](#quick-usage) - [How It Works](#how-it-works) - [VolumeHelper API](#volumehelper-api) - [Architecture Details](#architecture-details) - [Test Results](#test-results) - [Limitations](#limitations) - [Troubleshooting](#troubleshooting) --- ## The Problem When running CWA integration tests **inside a Docker container** (like a dev container), traditional bind mounts fail. ### Why Bind Mounts Fail in DinD ``` ┌─────────────────────────────────────┐ │ Dev Container (where tests run) │ │ │ │ Test creates bind mount: │ │ /tmp/pytest-123/ingest → │ │ test container:/cwa-book-ingest │ │ │ │ ❌ Problem: Docker daemon runs on │ │ the HOST, not in dev container │ │ /tmp/pytest-123 doesn't exist │ │ on host filesystem! │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ Host Machine (Docker daemon here) │ │ │ │ Docker daemon looks for: │ │ /tmp/pytest-123/ingest │ │ │ │ ❌ Path doesn't exist on host! │ │ ✅ Path only exists in dev │ │ container's filesystem │ └─────────────────────────────────────┘ ``` **Result**: Test container sees empty directories, tests fail. --- ## The Solution Use **Docker volumes** instead of bind mounts. Docker manages volumes on the host, making them accessible to all containers. ### Solution Architecture ``` ┌─────────────────────────────────────┐ │ Dev Container (where tests run) │ │ │ │ Test creates Docker volume: │ │ docker volume create cwa_test_xxx │ │ │ │ ✅ Volume managed by Docker daemon │ │ on the host │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ Host Machine (Docker daemon here) │ │ │ │ Docker daemon creates volume: │ │ /var/lib/docker/volumes/cwa_test_xxx│ │ │ │ ✅ Volume exists on host! │ │ ✅ Can mount into any container │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ Test Container │ │ │ │ Volume mounted: │ │ cwa_test_xxx → /cwa-book-ingest │ │ │ │ ✅ Files are accessible! │ └─────────────────────────────────────┘ ``` --- ## Quick Usage ### Method 1: Interactive Test Runner (Easiest) ```bash ./run_tests.sh ``` The script **auto-detects** if you're in a container and uses the right mode. **In a dev container, you'll see:** ``` Environment: docker Default mode: dind (Docker volume mode) ``` ### Method 2: Manual Execution ```bash # Set environment variable export USE_DOCKER_VOLUMES=true # Run tests pytest tests/integration/ -v ``` **Or as a one-liner:** ```bash USE_DOCKER_VOLUMES=true pytest tests/integration/ -v ``` ### Method 3: Make It Default (Optional) Add to your shell profile (`.bashrc`, `.zshrc`, etc.): ```bash # ~/.bashrc or ~/.zshrc export USE_DOCKER_VOLUMES=true ``` Then tests always use volume mode: ```bash pytest tests/integration/ -v ``` --- ## How It Works ### 1. Environment Variable Detection `tests/conftest.py` checks for the environment variable: ```python USE_DOCKER_VOLUMES = os.getenv('USE_DOCKER_VOLUMES', 'false').lower() == 'true' if USE_DOCKER_VOLUMES: # Import Docker volume fixtures from tests.conftest_volumes import * ``` ### 2. Fixture Override When `USE_DOCKER_VOLUMES=true`, fixtures from `conftest_volumes.py` override the default ones: | Fixture | Bind Mount Mode | Docker Volume Mode | |---------|----------------|-------------------| | `test_volumes` | Returns Path objects | Returns VolumeHelper objects | | `cwa_container` | Uses bind mounts | Uses volume mounts | | `ingest_folder` | Path to temp dir | VolumeHelper for volume | | `library_folder` | Path to temp dir | VolumeHelper for volume | ### 3. VolumeHelper Class Provides file operations using `docker cp`: ```python # Copy file into volume ingest_folder.copy_to(local_file_path) # Copy file from volume library_folder.copy_from("metadata.db", local_dest) # Check file existence if ingest_folder.file_exists("book.epub"): ... # List files files = library_folder.list_files("*.epub") ``` ### 4. Smart Container Startup Container readiness detection via log polling: ```python # Old way: Blind 60-second wait time.sleep(60) # New way: Poll logs until ready (~12 seconds) while not container_ready: logs = container.logs().decode('utf-8') if 'Starting Calibre Web' in logs: break time.sleep(1) ``` **Result**: Tests start ~48 seconds faster! --- ## VolumeHelper API The `VolumeHelper` class provides a Path-like interface for Docker volumes. ### File Operations ```python # Copy files to volume volume.copy_to(src_path, dest_name=None) volume.copy_to("/local/book.epub") # Copies to root of volume volume.copy_to("/local/book.epub", "renamed.epub") # Rename on copy # Copy files from volume volume.copy_from(src_name, dest_path) volume.copy_from("metadata.db", "/tmp/metadata.db") # Check existence volume.file_exists(filename) → bool volume.is_dir(path) → bool ``` ### Directory Operations ```python # Create subdirectories subfolder = volume / "subfolder" subfolder.mkdir() # List files files = volume.list_files() # All files files = volume.list_files("*.epub") # Pattern match # Iterate directory for item in volume.iterdir(): print(item.name) # Glob patterns epub_files = volume.glob("**/*.epub") ``` ### Path Operations ```python # Path concatenation subfolder = volume / "subfolder" file_path = subfolder / "file.epub" # VolumePath objects vol_path = volume / "book.epub" vol_path.exists() → bool vol_path.is_dir() → bool vol_path.name → "book.epub" vol_path.write_text("content") ``` ### Database Access ```python # Extract database to local temp file db_path = volume.read_to_temp("metadata.db") conn = sqlite3.connect(db_path) # Use database... # File automatically cleaned up ``` ### Helper Functions Available in test code: ```python # Universal copy (works in both modes) from tests.conftest import volume_copy volume_copy(src, dest) # Auto-detects mode # Database access (works in both modes) from tests.conftest import get_db_path db_path = get_db_path(folder_fixture, "cwa.db") conn = sqlite3.connect(db_path) ``` --- ## Architecture Details ### Dual-Mode Implementation ```python # tests/conftest.py - Main fixtures (bind mount mode) @pytest.fixture def test_volumes(tmp_path): """Standard mode: temp directories""" ingest = tmp_path / "ingest" library = tmp_path / "library" return ingest, library # tests/conftest_volumes.py - Volume mode @pytest.fixture def test_volumes_dind(): """DinD mode: Docker volumes""" ingest_vol = VolumeHelper("cwa_test_ingest_xxx") library_vol = VolumeHelper("cwa_test_library_xxx") yield ingest_vol, library_vol # Cleanup volumes ingest_vol.cleanup() library_vol.cleanup() ``` ### Conditional Loading ```python # tests/conftest.py USE_DOCKER_VOLUMES = os.getenv('USE_DOCKER_VOLUMES', 'false').lower() == 'true' if USE_DOCKER_VOLUMES: # Override fixtures with volume versions from tests.conftest_volumes import ( test_volumes as test_volumes_dind, cwa_container as cwa_container_dind, # ... other overrides ) # Re-export with original names test_volumes = test_volumes_dind cwa_container = cwa_container_dind ``` ### Test Compatibility Layer ```python # Helper function for universal copy def volume_copy(src, dest): """Copy file in either mode""" if USE_DOCKER_VOLUMES: # dest is VolumeHelper if isinstance(src, (str, Path)): dest.copy_to(src) else: # src is VolumeHelper src.copy_from(dest) else: # Standard shutil copy shutil.copy2(src, dest) ``` --- ## Test Results ### Docker Volume Mode Results ```bash USE_DOCKER_VOLUMES=true pytest tests/integration/ -v ``` **Output:** ``` tests/integration/test_ingest_pipeline.py::test_ingest_epub_already_target_format PASSED tests/integration/test_ingest_pipeline.py::test_ingest_empty_file PASSED tests/integration/test_ingest_pipeline.py::test_ingest_corrupted_file PASSED tests/integration/test_ingest_pipeline.py::test_txt_to_epub_conversion PASSED tests/integration/test_ingest_pipeline.py::test_filename_truncation_at_150_chars PASSED tests/integration/test_ingest_pipeline.py::test_book_appears_in_metadata_db PASSED tests/integration/test_ingest_pipeline.py::test_ingest_multiple_files PASSED tests/integration/test_ingest_pipeline.py::test_imported_files_backed_up PASSED tests/integration/test_ingest_pipeline.py::test_ingest_international_filename PASSED tests/integration/test_ingest_pipeline.py::test_mobi_to_epub_conversion PASSED tests/integration/test_ingest_pipeline.py::test_conversion_failure_moves_to_failed_folder PASSED tests/integration/test_ingest_pipeline.py::test_lock_released_after_processing PASSED tests/integration/test_ingest_pipeline.py::test_directory_import_processes_all_files PASSED tests/integration/test_ingest_pipeline.py::test_empty_folder_cleanup_after_processing PASSED tests/integration/test_ingest_pipeline.py::test_ignored_formats_not_deleted PASSED tests/integration/test_ingest_pipeline.py::test_processing_survives_multiple_files PASSED tests/integration/test_ingest_pipeline.py::test_zero_byte_file_doesnt_crash_ingest PASSED tests/integration/test_ingest_pipeline.py::test_cwa_db_tracks_import SKIPPED (requires config volume) tests/integration/test_ingest_pipeline.py::test_user_drops_book_and_it_appears_in_library PASSED tests/integration/test_ingest_pipeline.py::test_mixed_format_batch_import PASSED 19 passed, 1 skipped in 187.45s ``` ✅ **100% of runnable tests passing!** ### Bind Mount Mode Results (for comparison) ```bash pytest tests/integration/ -v ``` **Output:** ``` 20 passed in 165.32s ``` ✅ **All tests passing** ### Performance Comparison | Metric | Bind Mount | Docker Volume | Difference | |--------|------------|---------------|------------| | **Container startup** | Variable (30-60s) | Consistent (~12s) | ✅ Faster (log polling) | | **File processing** | 2-5 seconds | 2-5 seconds | Same | | **Full test suite** | ~3 minutes | ~3 minutes | Same | | **Tests passing** | 20/20 | 19/20 | 1 skip (expected) | **Conclusion**: Docker volume mode has equivalent performance! --- ## Limitations ### Test Skipped in Volume Mode **Test**: `test_cwa_db_tracks_import` **Reason**: Requires access to `/config/cwa.db` which is not mounted in test containers. **Workaround**: Would need to add config volume to test setup: ```python container.with_volume_mapping(config_vol, "/config") ``` **Status**: ⏭️ Intentionally skipped - not worth the complexity for 1 test. ### Glob Pattern Simplifications Complex multi-level glob patterns are simplified in volume mode: ```python # Bind mount mode (works) files = list(Path("/calibre-library").glob("**/metadata.db")) # Volume mode (simplified) files = [f for f in volume.list_files() if f.endswith("metadata.db")] ``` **Impact**: Minimal - tests adjusted to work in both modes. ### Performance Overhead `docker cp` operations add slight overhead: - Copy 1 MB file: +10-20ms - Copy 100 MB file: +200-500ms **Impact**: Negligible for tests (files are small, <10 MB). ### No Direct File System Access Can't use standard Python file operations directly: ```python # ❌ Doesn't work in volume mode with open(volume / "file.txt", "r") as f: content = f.read() # ✅ Use VolumeHelper methods content = volume.read_to_temp("file.txt") with open(content, "r") as f: data = f.read() ``` **Impact**: Minimal - tests use helper functions that work in both modes. --- ## Troubleshooting ### "Docker volume not found" **Cause**: Volume wasn't created or was already deleted. **Fix**: Check volume exists: ```bash docker volume ls | grep cwa_test ``` If missing, test cleanup may have failed. Remove stale volumes: ```bash docker volume prune -f ``` ### "Container can't access volume" **Cause**: Volume mount failed. **Fix**: Check container logs: ```bash docker logs temp-cwa-test-suite ``` Recreate container with correct volume mounts. ### "File not found in volume" **Cause**: File wasn't copied or copy failed silently. **Fix**: List volume contents: ```python files = volume.list_files() print(files) ``` Check `docker cp` command succeeded: ```bash docker cp file.epub volume_container:/cwa-book-ingest/ echo $? # Should be 0 ``` ### "Database is locked" **Cause**: Multiple processes accessing SQLite in volume. **Fix**: Use `get_db_path()` helper which extracts DB to temp file: ```python db_path = get_db_path(folder, "cwa.db") conn = sqlite3.connect(db_path) # Works on local file, no lock conflicts ``` ### Tests slower than expected **First run?** Docker needs to create volumes and download images. **Subsequent runs** should be ~3-4 minutes for integration tests. **Still slow?** - Check Docker Desktop resources (CPU/RAM) - Use SSD instead of HDD - Close other containers - Check network speed (volume creation requires Docker API calls) ### Environment variable not working **Check it's set:** ```bash echo $USE_DOCKER_VOLUMES # Should show: true ``` **Set it:** ```bash export USE_DOCKER_VOLUMES=true pytest tests/integration/ -v ``` **Or use one-liner:** ```bash USE_DOCKER_VOLUMES=true pytest tests/integration/ -v ``` ### CI tests failing but local tests pass **Cause**: CI uses bind mount mode by default. **Fix**: CI should NOT use `USE_DOCKER_VOLUMES=true` (runs on host, not in container). If CI is in a container (e.g., GitHub Actions with container job): ```yaml # .github/workflows/tests.yml - name: Run tests env: USE_DOCKER_VOLUMES: true # Only if running in container job run: pytest tests/integration/ -v ``` --- ## Advanced Topics ### Custom Volume Names Override volume names for debugging: ```python @pytest.fixture def test_volumes_dind(): ingest_vol = VolumeHelper("my-custom-ingest-vol") library_vol = VolumeHelper("my-custom-library-vol") # ... ``` Then inspect volumes: ```bash docker volume inspect my-custom-ingest-vol docker run --rm -v my-custom-ingest-vol:/data alpine ls -la /data ``` ### Accessing Volumes Manually Explore volume contents: ```bash # Create temp container with volume mounted docker run --rm -it -v cwa_test_ingest_xxx:/data alpine sh # Inside container cd /data ls -la cat book.epub exit ``` ### Debugging File Operations Enable debug logging in VolumeHelper: ```python class VolumeHelper: def __init__(self, volume_name, verbose=True): self.verbose = verbose # ... def copy_to(self, src, dest=None): if self.verbose: print(f"DEBUG: Copying {src} to volume {self.volume_name}") # ... ``` ### Preserving Volumes After Tests For debugging, keep volumes after test failure: ```python @pytest.fixture def test_volumes_dind(): ingest_vol = VolumeHelper("cwa_test_ingest_xxx") library_vol = VolumeHelper("cwa_test_library_xxx") yield ingest_vol, library_vol # Only cleanup on success if not hasattr(pytest, 'failed'): ingest_vol.cleanup() library_vol.cleanup() ``` Then inspect volumes after failed tests. --- ## Implementation Files **Core files:** - `tests/conftest.py` - Main fixtures + mode detection - `tests/conftest_volumes.py` - Docker volume implementation - `tests/integration/test_ingest_pipeline.py` - Tests using both modes **Helper functions:** - `volume_copy(src, dest)` - Universal copy function - `get_db_path(folder, db_name)` - Database access abstraction **Documentation:** - `DIND_MODE_COMPLETE.md` - Implementation summary - `HYBRID_DOCKER_IMPLEMENTATION.md` - Technical details - This guide - Usage documentation --- ## Next Steps - **[Testing Quick Start](Testing-Quick-Start.md)** - Get started with tests - **[Running Tests](Testing-Running-Tests.md)** - All execution modes - **[Writing Tests](Testing-Guide-for-Contributors.md)** - Contribute tests - **[Implementation Status](Testing-Implementation-Status.md)** - Progress tracking --- **Questions?** - Discord: https://discord.gg/EjgSeek94R - GitHub Issues: Tag with `testing` label --- **Docker-in-Docker testing works perfectly!** 🎉 100% of runnable tests passing with equivalent performance to bind mount mode.