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>
240 lines
8.4 KiB
Python
240 lines
8.4 KiB
Python
#!/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() |