#!/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 ![Architecture](images/arch.png) ![Logo](assets/logo.jpg) ![Diagram](./diagrams/flow.svg) ## 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("fake 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()