feat: implement comprehensive asset shipping for md-render command
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Add automatic asset copying when rendering markdown to different output directories with intelligent defaults and full user control. Key Features: - Environment variable support: MARKITECT_OUTPUT_DIR sets default output directory - Smart defaults: auto-ship assets for directory output, disabled for file output - CLI control flags: --ship-assets and --no-ship-assets for explicit control - Timestamp-based copying: only copies when source newer than destination - Path preservation: maintains relative directory structure in output - Graceful error handling: missing assets logged as warnings, not failures Technical Implementation: - Enhanced asset discovery in markitect/assets/discovery.py with discover_assets_from_markdown() - Added environment variable priority: CLI --output > MARKITECT_OUTPUT_DIR > input directory - Comprehensive asset shipping logic with _ship_assets() function - Directory vs file output detection for intelligent default behavior Examples and Testing: - Added image-assets example directory with 6 sample images and comprehensive README - Created comprehensive TDD test suite with 10 tests covering all functionality - Tests validate environment variables, CLI flags, asset discovery, shipping logic, timestamp handling, missing assets, path preservation, and default behaviors Usage: markitect md-render file.md -o /output/dir/ # Auto-ships assets markitect md-render file.md --no-ship-assets # Suppresses shipping MARKITECT_OUTPUT_DIR=/docs markitect md-render file.md # Uses env var 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
240
tests/test_md_render_asset_shipping.py
Normal file
240
tests/test_md_render_asset_shipping.py
Normal file
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TDD tests for asset shipping in md-render command.
|
||||
|
||||
Tests the automatic copying of referenced assets when rendering markdown
|
||||
to different output directories.
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from markitect.plugins.builtin.markdown_commands import md_render_command
|
||||
from click.testing import CliRunner
|
||||
|
||||
|
||||
class TestAssetShippingMdRender:
|
||||
"""Test asset shipping functionality in md-render."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test environment."""
|
||||
self.runner = CliRunner()
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.test_dir = Path(self.temp_dir)
|
||||
|
||||
# Create test markdown with image references
|
||||
self.markdown_content = """# Test Document
|
||||
|
||||
## Images
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Links
|
||||
|
||||
[Documentation](docs/readme.md)
|
||||
"""
|
||||
|
||||
# Create test file structure
|
||||
self.md_file = self.test_dir / "test.md"
|
||||
self.md_file.write_text(self.markdown_content)
|
||||
|
||||
# Create asset directories and files
|
||||
(self.test_dir / "images").mkdir()
|
||||
(self.test_dir / "assets").mkdir()
|
||||
(self.test_dir / "diagrams").mkdir()
|
||||
(self.test_dir / "docs").mkdir()
|
||||
|
||||
# Create sample asset files
|
||||
(self.test_dir / "images" / "arch.png").write_bytes(b"fake png data")
|
||||
(self.test_dir / "assets" / "logo.jpg").write_bytes(b"fake jpg data")
|
||||
(self.test_dir / "diagrams" / "flow.svg").write_text("<svg>fake svg</svg>")
|
||||
(self.test_dir / "docs" / "readme.md").write_text("# README")
|
||||
|
||||
def teardown_method(self):
|
||||
"""Clean up test environment."""
|
||||
import shutil
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_environment_variable_output_directory(self):
|
||||
"""Test that MARKITECT_OUTPUT_DIR is used when no --output is specified."""
|
||||
output_dir = self.test_dir / "env_output"
|
||||
output_dir.mkdir()
|
||||
|
||||
with patch.dict(os.environ, {'MARKITECT_OUTPUT_DIR': str(output_dir)}):
|
||||
result = self.runner.invoke(md_render_command, [str(self.md_file)])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert (output_dir / "test.html").exists()
|
||||
|
||||
def test_cli_output_overrides_environment_variable(self):
|
||||
"""Test that CLI --output parameter overrides environment variable."""
|
||||
env_output = self.test_dir / "env_output"
|
||||
cli_output = self.test_dir / "cli_output"
|
||||
env_output.mkdir()
|
||||
cli_output.mkdir()
|
||||
|
||||
with patch.dict(os.environ, {'MARKITECT_OUTPUT_DIR': str(env_output)}):
|
||||
result = self.runner.invoke(md_render_command, [
|
||||
str(self.md_file),
|
||||
'--output', str(cli_output)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert (cli_output / "test.html").exists()
|
||||
assert not (env_output / "test.html").exists()
|
||||
|
||||
def test_asset_shipping_enabled_by_default_for_directory_output(self):
|
||||
"""Test that assets are shipped automatically when output is a directory."""
|
||||
output_dir = self.test_dir / "output"
|
||||
output_dir.mkdir()
|
||||
|
||||
result = self.runner.invoke(md_render_command, [
|
||||
str(self.md_file),
|
||||
'--output', str(output_dir)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert (output_dir / "test.html").exists()
|
||||
|
||||
# Check that assets were copied
|
||||
assert (output_dir / "images" / "arch.png").exists()
|
||||
assert (output_dir / "assets" / "logo.jpg").exists()
|
||||
assert (output_dir / "diagrams" / "flow.svg").exists()
|
||||
assert (output_dir / "docs" / "readme.md").exists()
|
||||
|
||||
def test_no_ship_assets_flag_suppresses_asset_copying(self):
|
||||
"""Test that --no-ship-assets flag prevents asset copying."""
|
||||
output_dir = self.test_dir / "output"
|
||||
output_dir.mkdir()
|
||||
|
||||
result = self.runner.invoke(md_render_command, [
|
||||
str(self.md_file),
|
||||
'--output', str(output_dir),
|
||||
'--no-ship-assets'
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert (output_dir / "test.html").exists()
|
||||
|
||||
# Check that assets were NOT copied
|
||||
assert not (output_dir / "images").exists()
|
||||
assert not (output_dir / "assets").exists()
|
||||
assert not (output_dir / "diagrams").exists()
|
||||
|
||||
def test_timestamp_based_asset_copying(self):
|
||||
"""Test that assets are only copied if source is newer than destination."""
|
||||
output_dir = self.test_dir / "output"
|
||||
output_dir.mkdir()
|
||||
|
||||
# First render - assets should be copied
|
||||
result = self.runner.invoke(md_render_command, [
|
||||
str(self.md_file),
|
||||
'--output', str(output_dir)
|
||||
])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Mark output asset as newer
|
||||
output_asset = output_dir / "images" / "arch.png"
|
||||
original_mtime = output_asset.stat().st_mtime
|
||||
output_asset.touch() # Update timestamp
|
||||
|
||||
# Second render - asset should not be overwritten
|
||||
result = self.runner.invoke(md_render_command, [
|
||||
str(self.md_file),
|
||||
'--output', str(output_dir)
|
||||
])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Check that the timestamp wasn't changed (asset wasn't overwritten)
|
||||
assert output_asset.stat().st_mtime > original_mtime
|
||||
|
||||
def test_ship_assets_flag_explicit_enable(self):
|
||||
"""Test that --ship-assets flag explicitly enables asset shipping."""
|
||||
output_dir = self.test_dir / "output"
|
||||
output_dir.mkdir()
|
||||
|
||||
result = self.runner.invoke(md_render_command, [
|
||||
str(self.md_file),
|
||||
'--output', str(output_dir),
|
||||
'--ship-assets'
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert (output_dir / "test.html").exists()
|
||||
assert (output_dir / "images" / "arch.png").exists()
|
||||
|
||||
def test_missing_assets_handled_gracefully(self):
|
||||
"""Test that missing assets are handled with warnings, not errors."""
|
||||
# Remove one of the assets
|
||||
(self.test_dir / "images" / "arch.png").unlink()
|
||||
|
||||
output_dir = self.test_dir / "output"
|
||||
output_dir.mkdir()
|
||||
|
||||
result = self.runner.invoke(md_render_command, [
|
||||
str(self.md_file),
|
||||
'--output', str(output_dir)
|
||||
])
|
||||
|
||||
# Should succeed despite missing asset
|
||||
assert result.exit_code == 0
|
||||
assert (output_dir / "test.html").exists()
|
||||
|
||||
# Other assets should still be copied
|
||||
assert (output_dir / "assets" / "logo.jpg").exists()
|
||||
|
||||
def test_asset_discovery_from_markdown_content(self):
|
||||
"""Test discovery of assets from markdown content."""
|
||||
from markitect.assets.discovery import discover_assets_from_markdown
|
||||
|
||||
assets = discover_assets_from_markdown(self.markdown_content, self.test_dir)
|
||||
|
||||
# Should find all asset references
|
||||
asset_paths = [asset.asset_path for asset in assets]
|
||||
assert "images/arch.png" in asset_paths
|
||||
assert "assets/logo.jpg" in asset_paths
|
||||
assert "./diagrams/flow.svg" in asset_paths
|
||||
assert "docs/readme.md" in asset_paths
|
||||
|
||||
def test_relative_path_preservation(self):
|
||||
"""Test that relative path structure is preserved in output."""
|
||||
output_dir = self.test_dir / "output"
|
||||
output_dir.mkdir()
|
||||
|
||||
result = self.runner.invoke(md_render_command, [
|
||||
str(self.md_file),
|
||||
'--output', str(output_dir)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Check that directory structure is preserved
|
||||
assert (output_dir / "images" / "arch.png").exists()
|
||||
assert (output_dir / "assets" / "logo.jpg").exists()
|
||||
assert (output_dir / "diagrams" / "flow.svg").exists()
|
||||
assert (output_dir / "docs" / "readme.md").exists()
|
||||
|
||||
def test_asset_shipping_disabled_for_file_output(self):
|
||||
"""Test that asset shipping is disabled when output is a specific file."""
|
||||
# Create a separate output directory
|
||||
output_dir = self.test_dir / "output_dir"
|
||||
output_dir.mkdir()
|
||||
output_file = output_dir / "specific_output.html"
|
||||
|
||||
result = self.runner.invoke(md_render_command, [
|
||||
str(self.md_file),
|
||||
'--output', str(output_file)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
# Assets should NOT be copied when output is a specific file
|
||||
# (they should not exist in the output directory)
|
||||
assert not (output_dir / "images").exists()
|
||||
assert not (output_dir / "assets").exists()
|
||||
Reference in New Issue
Block a user