428 lines
15 KiB
Python
428 lines
15 KiB
Python
|
|
"""
|
||
|
|
TDD8 Cycle 2: Frontmatter Commands Tests (RED Phase)
|
||
|
|
Issue #38 - MarkdownMatters CLI Implementation
|
||
|
|
|
||
|
|
This test file implements the RED phase tests for frontmatter command family:
|
||
|
|
- markitect frontmatter-get [key] [path] - Get specific frontmatter value
|
||
|
|
- markitect frontmatter-set key=value [path] - Set frontmatter value
|
||
|
|
- markitect frontmatter-keys [path] - List all frontmatter keys
|
||
|
|
- markitect frontmatter-stats [path] - Frontmatter statistics
|
||
|
|
|
||
|
|
Following TDD8 methodology, these tests MUST FAIL initially.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
import tempfile
|
||
|
|
import os
|
||
|
|
from pathlib import Path
|
||
|
|
from click.testing import CliRunner
|
||
|
|
|
||
|
|
from markitect.matter_frontmatter.parser import FrontmatterParser
|
||
|
|
from markitect.matter_frontmatter.stats import FrontmatterStats
|
||
|
|
from markitect.matter_frontmatter.commands import frontmatter_get, frontmatter_set, frontmatter_keys, frontmatter_stats
|
||
|
|
|
||
|
|
|
||
|
|
class TestFrontmatterExtraction:
|
||
|
|
"""Test frontmatter extraction and parsing."""
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def test_files_dir(self):
|
||
|
|
"""Path to frontmatter test fixture files."""
|
||
|
|
return Path(__file__).parent / "fixtures" / "frontmatter_test_files"
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def frontmatter_parser(self):
|
||
|
|
"""Frontmatter parser instance."""
|
||
|
|
return FrontmatterParser()
|
||
|
|
|
||
|
|
def test_frontmatter_parser_extracts_yaml_frontmatter(self, frontmatter_parser, test_files_dir):
|
||
|
|
"""Test that parser extracts YAML frontmatter correctly."""
|
||
|
|
file_path = test_files_dir / "yaml_frontmatter.md"
|
||
|
|
|
||
|
|
with open(file_path, 'r') as f:
|
||
|
|
text = f.read()
|
||
|
|
|
||
|
|
frontmatter = frontmatter_parser.extract_frontmatter(text)
|
||
|
|
|
||
|
|
# Should extract all YAML frontmatter fields
|
||
|
|
assert frontmatter["title"] == "YAML Frontmatter Test Document"
|
||
|
|
assert frontmatter["author"] == "Test Author"
|
||
|
|
assert str(frontmatter["date"]) == "2025-10-02"
|
||
|
|
assert frontmatter["tags"] == ["yaml", "frontmatter", "testing"]
|
||
|
|
assert frontmatter["version"] == 1.2
|
||
|
|
assert frontmatter["published"] is True
|
||
|
|
|
||
|
|
# Should handle nested objects
|
||
|
|
assert frontmatter["nested"]["category"] == "documentation"
|
||
|
|
assert frontmatter["nested"]["priority"] == "high"
|
||
|
|
assert frontmatter["nested"]["metadata"]["creation_date"] == "2025-10-02"
|
||
|
|
|
||
|
|
def test_frontmatter_parser_extracts_json_frontmatter(self, frontmatter_parser, test_files_dir):
|
||
|
|
"""Test that parser extracts JSON frontmatter correctly."""
|
||
|
|
file_path = test_files_dir / "json_frontmatter.md"
|
||
|
|
|
||
|
|
with open(file_path, 'r') as f:
|
||
|
|
text = f.read()
|
||
|
|
|
||
|
|
frontmatter = frontmatter_parser.extract_frontmatter(text)
|
||
|
|
|
||
|
|
# Should extract all JSON frontmatter fields
|
||
|
|
assert frontmatter["title"] == "JSON Frontmatter Test Document"
|
||
|
|
assert frontmatter["author"] == "Test Author"
|
||
|
|
assert frontmatter["tags"] == ["json", "frontmatter", "testing"]
|
||
|
|
assert frontmatter["version"] == 2.1
|
||
|
|
assert frontmatter["published"] is False
|
||
|
|
|
||
|
|
# Should handle nested objects
|
||
|
|
assert frontmatter["config"]["theme"] == "dark"
|
||
|
|
assert frontmatter["config"]["language"] == "en"
|
||
|
|
assert frontmatter["config"]["features"] == ["toc", "search", "navigation"]
|
||
|
|
|
||
|
|
def test_frontmatter_parser_handles_no_frontmatter(self, frontmatter_parser, test_files_dir):
|
||
|
|
"""Test that parser handles documents without frontmatter."""
|
||
|
|
file_path = test_files_dir / "no_frontmatter.md"
|
||
|
|
|
||
|
|
with open(file_path, 'r') as f:
|
||
|
|
text = f.read()
|
||
|
|
|
||
|
|
frontmatter = frontmatter_parser.extract_frontmatter(text)
|
||
|
|
|
||
|
|
# Should return empty dict for no frontmatter
|
||
|
|
assert frontmatter == {}
|
||
|
|
|
||
|
|
def test_frontmatter_parser_handles_empty_frontmatter(self, frontmatter_parser, test_files_dir):
|
||
|
|
"""Test that parser handles empty frontmatter blocks."""
|
||
|
|
file_path = test_files_dir / "empty_frontmatter.md"
|
||
|
|
|
||
|
|
with open(file_path, 'r') as f:
|
||
|
|
text = f.read()
|
||
|
|
|
||
|
|
frontmatter = frontmatter_parser.extract_frontmatter(text)
|
||
|
|
|
||
|
|
# Should return empty dict for empty frontmatter
|
||
|
|
assert frontmatter == {}
|
||
|
|
|
||
|
|
def test_frontmatter_parser_get_nested_value(self, frontmatter_parser, test_files_dir):
|
||
|
|
"""Test getting nested values using dot notation."""
|
||
|
|
file_path = test_files_dir / "yaml_frontmatter.md"
|
||
|
|
|
||
|
|
with open(file_path, 'r') as f:
|
||
|
|
text = f.read()
|
||
|
|
|
||
|
|
frontmatter = frontmatter_parser.extract_frontmatter(text)
|
||
|
|
|
||
|
|
# Should support dot notation for nested access
|
||
|
|
value = frontmatter_parser.get_nested_value(frontmatter, "nested.category")
|
||
|
|
assert value == "documentation"
|
||
|
|
|
||
|
|
value = frontmatter_parser.get_nested_value(frontmatter, "nested.metadata.creation_date")
|
||
|
|
assert value == "2025-10-02"
|
||
|
|
|
||
|
|
# Should return None for non-existent keys
|
||
|
|
value = frontmatter_parser.get_nested_value(frontmatter, "non.existent.key")
|
||
|
|
assert value is None
|
||
|
|
|
||
|
|
|
||
|
|
class TestFrontmatterModification:
|
||
|
|
"""Test frontmatter modification operations."""
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def frontmatter_parser(self):
|
||
|
|
"""Frontmatter parser instance."""
|
||
|
|
return FrontmatterParser()
|
||
|
|
|
||
|
|
def test_frontmatter_set_simple_value(self, frontmatter_parser):
|
||
|
|
"""Test setting simple frontmatter values."""
|
||
|
|
text = """---
|
||
|
|
title: "Original Title"
|
||
|
|
author: "Original Author"
|
||
|
|
---
|
||
|
|
|
||
|
|
# Content here"""
|
||
|
|
|
||
|
|
new_text = frontmatter_parser.set_frontmatter_value(text, "title", "New Title")
|
||
|
|
|
||
|
|
# Should update the title value
|
||
|
|
frontmatter = frontmatter_parser.extract_frontmatter(new_text)
|
||
|
|
assert frontmatter["title"] == "New Title"
|
||
|
|
assert frontmatter["author"] == "Original Author"
|
||
|
|
|
||
|
|
def test_frontmatter_set_new_value(self, frontmatter_parser):
|
||
|
|
"""Test adding new frontmatter values."""
|
||
|
|
text = """---
|
||
|
|
title: "Original Title"
|
||
|
|
---
|
||
|
|
|
||
|
|
# Content here"""
|
||
|
|
|
||
|
|
new_text = frontmatter_parser.set_frontmatter_value(text, "author", "New Author")
|
||
|
|
|
||
|
|
# Should add the new field
|
||
|
|
frontmatter = frontmatter_parser.extract_frontmatter(new_text)
|
||
|
|
assert frontmatter["title"] == "Original Title"
|
||
|
|
assert frontmatter["author"] == "New Author"
|
||
|
|
|
||
|
|
def test_frontmatter_set_nested_value(self, frontmatter_parser):
|
||
|
|
"""Test setting nested frontmatter values using dot notation."""
|
||
|
|
text = """---
|
||
|
|
title: "Test"
|
||
|
|
config:
|
||
|
|
theme: "light"
|
||
|
|
---
|
||
|
|
|
||
|
|
# Content here"""
|
||
|
|
|
||
|
|
new_text = frontmatter_parser.set_frontmatter_value(text, "config.theme", "dark")
|
||
|
|
|
||
|
|
# Should update nested value
|
||
|
|
frontmatter = frontmatter_parser.extract_frontmatter(new_text)
|
||
|
|
assert frontmatter["config"]["theme"] == "dark"
|
||
|
|
|
||
|
|
def test_frontmatter_add_to_empty_document(self, frontmatter_parser):
|
||
|
|
"""Test adding frontmatter to document without any."""
|
||
|
|
text = """# Content Without Frontmatter
|
||
|
|
|
||
|
|
Just some content here."""
|
||
|
|
|
||
|
|
new_text = frontmatter_parser.set_frontmatter_value(text, "title", "New Title")
|
||
|
|
|
||
|
|
# Should add frontmatter block
|
||
|
|
frontmatter = frontmatter_parser.extract_frontmatter(new_text)
|
||
|
|
assert frontmatter["title"] == "New Title"
|
||
|
|
|
||
|
|
# Should preserve content
|
||
|
|
assert "# Content Without Frontmatter" in new_text
|
||
|
|
|
||
|
|
|
||
|
|
class TestFrontmatterKeys:
|
||
|
|
"""Test frontmatter key listing functionality."""
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def test_files_dir(self):
|
||
|
|
"""Path to frontmatter test fixture files."""
|
||
|
|
return Path(__file__).parent / "fixtures" / "frontmatter_test_files"
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def frontmatter_parser(self):
|
||
|
|
"""Frontmatter parser instance."""
|
||
|
|
return FrontmatterParser()
|
||
|
|
|
||
|
|
def test_frontmatter_keys_yaml_document(self, frontmatter_parser, test_files_dir):
|
||
|
|
"""Test listing keys from YAML frontmatter."""
|
||
|
|
file_path = test_files_dir / "yaml_frontmatter.md"
|
||
|
|
|
||
|
|
with open(file_path, 'r') as f:
|
||
|
|
text = f.read()
|
||
|
|
|
||
|
|
keys = frontmatter_parser.get_frontmatter_keys(text)
|
||
|
|
|
||
|
|
# Should return all top-level keys
|
||
|
|
expected_keys = ["title", "author", "date", "tags", "version", "published", "description", "nested"]
|
||
|
|
assert set(keys) == set(expected_keys)
|
||
|
|
|
||
|
|
def test_frontmatter_keys_with_nested_option(self, frontmatter_parser, test_files_dir):
|
||
|
|
"""Test listing keys including nested keys with dot notation."""
|
||
|
|
file_path = test_files_dir / "yaml_frontmatter.md"
|
||
|
|
|
||
|
|
with open(file_path, 'r') as f:
|
||
|
|
text = f.read()
|
||
|
|
|
||
|
|
keys = frontmatter_parser.get_frontmatter_keys(text, include_nested=True)
|
||
|
|
|
||
|
|
# Should include nested keys with dot notation
|
||
|
|
assert "nested.category" in keys
|
||
|
|
assert "nested.priority" in keys
|
||
|
|
assert "nested.metadata.creation_date" in keys
|
||
|
|
assert "nested.metadata.last_modified" in keys
|
||
|
|
|
||
|
|
def test_frontmatter_keys_empty_document(self, frontmatter_parser, test_files_dir):
|
||
|
|
"""Test listing keys from document without frontmatter."""
|
||
|
|
file_path = test_files_dir / "no_frontmatter.md"
|
||
|
|
|
||
|
|
with open(file_path, 'r') as f:
|
||
|
|
text = f.read()
|
||
|
|
|
||
|
|
keys = frontmatter_parser.get_frontmatter_keys(text)
|
||
|
|
|
||
|
|
# Should return empty list
|
||
|
|
assert keys == []
|
||
|
|
|
||
|
|
|
||
|
|
class TestFrontmatterStatistics:
|
||
|
|
"""Test frontmatter statistics calculation."""
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def test_files_dir(self):
|
||
|
|
"""Path to frontmatter test fixture files."""
|
||
|
|
return Path(__file__).parent / "fixtures" / "frontmatter_test_files"
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def frontmatter_parser(self):
|
||
|
|
"""Frontmatter parser instance."""
|
||
|
|
return FrontmatterParser()
|
||
|
|
|
||
|
|
def test_frontmatter_stats_yaml_document(self, frontmatter_parser, test_files_dir):
|
||
|
|
"""Test statistics calculation for YAML frontmatter."""
|
||
|
|
file_path = test_files_dir / "yaml_frontmatter.md"
|
||
|
|
|
||
|
|
with open(file_path, 'r') as f:
|
||
|
|
text = f.read()
|
||
|
|
|
||
|
|
stats = frontmatter_parser.calculate_frontmatter_stats(text)
|
||
|
|
|
||
|
|
# Should count fields correctly
|
||
|
|
assert stats.total_fields == 8 # Top-level fields
|
||
|
|
assert stats.nested_fields == 5 # Nested fields (category, priority, creation_date, last_modified, metadata object)
|
||
|
|
assert stats.format == "yaml"
|
||
|
|
assert stats.has_frontmatter is True
|
||
|
|
|
||
|
|
# Should categorize field types
|
||
|
|
assert "string" in stats.field_types
|
||
|
|
assert "array" in stats.field_types
|
||
|
|
assert "number" in stats.field_types
|
||
|
|
assert "boolean" in stats.field_types
|
||
|
|
assert "object" in stats.field_types
|
||
|
|
|
||
|
|
def test_frontmatter_stats_json_document(self, frontmatter_parser, test_files_dir):
|
||
|
|
"""Test statistics calculation for JSON frontmatter."""
|
||
|
|
file_path = test_files_dir / "json_frontmatter.md"
|
||
|
|
|
||
|
|
with open(file_path, 'r') as f:
|
||
|
|
text = f.read()
|
||
|
|
|
||
|
|
stats = frontmatter_parser.calculate_frontmatter_stats(text)
|
||
|
|
|
||
|
|
# Should identify JSON format
|
||
|
|
assert stats.format == "json"
|
||
|
|
assert stats.has_frontmatter is True
|
||
|
|
assert stats.total_fields > 0
|
||
|
|
|
||
|
|
def test_frontmatter_stats_no_frontmatter(self, frontmatter_parser, test_files_dir):
|
||
|
|
"""Test statistics for document without frontmatter."""
|
||
|
|
file_path = test_files_dir / "no_frontmatter.md"
|
||
|
|
|
||
|
|
with open(file_path, 'r') as f:
|
||
|
|
text = f.read()
|
||
|
|
|
||
|
|
stats = frontmatter_parser.calculate_frontmatter_stats(text)
|
||
|
|
|
||
|
|
# Should indicate no frontmatter
|
||
|
|
assert stats.has_frontmatter is False
|
||
|
|
assert stats.total_fields == 0
|
||
|
|
assert stats.nested_fields == 0
|
||
|
|
assert stats.format is None
|
||
|
|
|
||
|
|
|
||
|
|
class TestFrontmatterCLICommands:
|
||
|
|
"""Test CLI command integration."""
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def runner(self):
|
||
|
|
"""CLI test runner."""
|
||
|
|
return CliRunner()
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def test_files_dir(self):
|
||
|
|
"""Path to frontmatter test fixture files."""
|
||
|
|
return Path(__file__).parent / "fixtures" / "frontmatter_test_files"
|
||
|
|
|
||
|
|
def test_frontmatter_get_command(self, runner, test_files_dir):
|
||
|
|
"""Test frontmatter-get CLI command."""
|
||
|
|
file_path = test_files_dir / "yaml_frontmatter.md"
|
||
|
|
|
||
|
|
# Test getting simple value
|
||
|
|
result = runner.invoke(frontmatter_get, ['title', '--file', str(file_path)])
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "YAML Frontmatter Test Document" in result.output
|
||
|
|
|
||
|
|
# Test getting nested value
|
||
|
|
result = runner.invoke(frontmatter_get, ['nested.category', '--file', str(file_path)])
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "documentation" in result.output
|
||
|
|
|
||
|
|
def test_frontmatter_keys_command(self, runner, test_files_dir):
|
||
|
|
"""Test frontmatter-keys CLI command."""
|
||
|
|
file_path = test_files_dir / "yaml_frontmatter.md"
|
||
|
|
|
||
|
|
result = runner.invoke(frontmatter_keys, ['--file', str(file_path)])
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "title" in result.output
|
||
|
|
assert "author" in result.output
|
||
|
|
assert "tags" in result.output
|
||
|
|
|
||
|
|
def test_frontmatter_stats_command(self, runner, test_files_dir):
|
||
|
|
"""Test frontmatter-stats CLI command."""
|
||
|
|
file_path = test_files_dir / "yaml_frontmatter.md"
|
||
|
|
|
||
|
|
result = runner.invoke(frontmatter_stats, ['--file', str(file_path)])
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "total_fields" in result.output
|
||
|
|
assert "format" in result.output
|
||
|
|
|
||
|
|
def test_frontmatter_set_command(self, runner, test_files_dir):
|
||
|
|
"""Test frontmatter-set CLI command."""
|
||
|
|
# Create temporary file for testing
|
||
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||
|
|
f.write("""---
|
||
|
|
title: "Original Title"
|
||
|
|
---
|
||
|
|
|
||
|
|
# Test Content""")
|
||
|
|
temp_file = f.name
|
||
|
|
|
||
|
|
try:
|
||
|
|
result = runner.invoke(frontmatter_set, ['title=New Title', '--file', temp_file])
|
||
|
|
assert result.exit_code == 0
|
||
|
|
|
||
|
|
# Verify the change was made
|
||
|
|
with open(temp_file, 'r') as f:
|
||
|
|
content = f.read()
|
||
|
|
assert "New Title" in content
|
||
|
|
|
||
|
|
finally:
|
||
|
|
os.unlink(temp_file)
|
||
|
|
|
||
|
|
def test_frontmatter_commands_help_text(self, runner):
|
||
|
|
"""Test that help text is available for all frontmatter commands."""
|
||
|
|
commands = [frontmatter_get, frontmatter_keys, frontmatter_stats, frontmatter_set]
|
||
|
|
|
||
|
|
for command in commands:
|
||
|
|
result = runner.invoke(command, ['--help'])
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "frontmatter" in result.output.lower()
|
||
|
|
|
||
|
|
|
||
|
|
class TestFrontmatterStats:
|
||
|
|
"""Test FrontmatterStats data class."""
|
||
|
|
|
||
|
|
def test_frontmatter_stats_creation(self):
|
||
|
|
"""Test FrontmatterStats object creation."""
|
||
|
|
stats = FrontmatterStats(
|
||
|
|
has_frontmatter=True,
|
||
|
|
total_fields=5,
|
||
|
|
nested_fields=2,
|
||
|
|
format="yaml",
|
||
|
|
field_types={"string": 3, "number": 1, "boolean": 1}
|
||
|
|
)
|
||
|
|
|
||
|
|
assert stats.has_frontmatter is True
|
||
|
|
assert stats.total_fields == 5
|
||
|
|
assert stats.nested_fields == 2
|
||
|
|
assert stats.format == "yaml"
|
||
|
|
assert stats.field_types["string"] == 3
|
||
|
|
|
||
|
|
def test_frontmatter_stats_to_dict(self):
|
||
|
|
"""Test FrontmatterStats conversion to dictionary."""
|
||
|
|
stats = FrontmatterStats(
|
||
|
|
has_frontmatter=True,
|
||
|
|
total_fields=5,
|
||
|
|
nested_fields=2,
|
||
|
|
format="yaml",
|
||
|
|
field_types={"string": 3}
|
||
|
|
)
|
||
|
|
|
||
|
|
stats_dict = stats.to_dict()
|
||
|
|
|
||
|
|
assert stats_dict["has_frontmatter"] is True
|
||
|
|
assert stats_dict["total_fields"] == 5
|
||
|
|
assert stats_dict["format"] == "yaml"
|