"""Unit tests for macro parser.""" import pytest from markitect.prompts.templates.parser import MacroParser, MacroParsingError from markitect.prompts.templates.models import MacroKind class TestMacroParser: """Tests for MacroParser.""" def setup_method(self): """Setup parser for each test.""" self.parser = MacroParser() def test_parse_required_macro(self): """Test parsing required macro.""" content = "Some text {{require:glossary}} more text" macros = self.parser.parse(content) assert len(macros) == 1 assert macros[0].kind == MacroKind.REQUIRED assert macros[0].target == "glossary" assert macros[0].parameters == {} def test_parse_optional_macro(self): """Test parsing optional macro.""" content = "Text {{optional:constraints}}" macros = self.parser.parse(content) assert len(macros) == 1 assert macros[0].kind == MacroKind.OPTIONAL assert macros[0].target == "constraints" def test_parse_generate_macro(self): """Test parsing generate macro.""" content = "{{generate:examples}}" macros = self.parser.parse(content) assert len(macros) == 1 assert macros[0].kind == MacroKind.GENERATE assert macros[0].target == "examples" def test_parse_macro_with_parameters(self): """Test parsing macro with parameters.""" content = "{{generate:code|language=python|framework=fastapi}}" macros = self.parser.parse(content) assert len(macros) == 1 assert macros[0].target == "code" assert macros[0].parameters == { "language": "python", "framework": "fastapi", } def test_parse_multiple_macros(self): """Test parsing multiple macros.""" content = """ {{require:glossary}} Some text here {{optional:notes}} {{generate:examples|lang=python}} """ macros = self.parser.parse(content) assert len(macros) == 3 assert macros[0].kind == MacroKind.REQUIRED assert macros[1].kind == MacroKind.OPTIONAL assert macros[2].kind == MacroKind.GENERATE def test_parse_with_line_numbers(self): """Test that line numbers are recorded.""" content = """Line 1 {{require:dep1}} Line 3 {{optional:dep2}} """ macros = self.parser.parse(content) assert macros[0].line_number == 2 assert macros[1].line_number == 4 def test_parse_case_insensitive(self): """Test macro kind is case insensitive.""" content = "{{REQUIRE:test}} {{Optional:test2}} {{Generate:test3}}" macros = self.parser.parse(content) assert len(macros) == 3 assert all(m.kind in MacroKind for m in macros) def test_parse_alias_gen_for_generate(self): """Test 'gen' alias for 'generate'.""" content = "{{gen:test}}" macros = self.parser.parse(content) assert len(macros) == 1 assert macros[0].kind == MacroKind.GENERATE def test_parse_alias_required_for_require(self): """Test 'required' alias for 'require'.""" content = "{{required:test}}" macros = self.parser.parse(content) assert len(macros) == 1 assert macros[0].kind == MacroKind.REQUIRED def test_parse_preserves_raw_text(self): """Test raw text is preserved.""" content = "{{require:test|a=1}}" macros = self.parser.parse(content) assert macros[0].raw_text == "{{require:test|a=1}}" def test_parse_empty_content(self): """Test parsing empty content.""" macros = self.parser.parse("") assert macros == [] def test_parse_no_macros(self): """Test parsing content without macros.""" content = "Just regular text without any macros" macros = self.parser.parse(content) assert macros == [] def test_parse_invalid_macro_kind_raises_error(self): """Test invalid macro kind raises error.""" content = "{{invalid:target}}" with pytest.raises(MacroParsingError, match="Invalid macro kind"): self.parser.parse(content) def test_parse_empty_target_raises_error(self): """Test empty target raises error.""" content = "{{require:}}" with pytest.raises(MacroParsingError, match="target cannot be empty"): self.parser.parse(content) def test_find_macro_positions(self): """Test finding macro positions.""" content = "Start {{require:dep1}} middle {{optional:dep2}} end" positions = self.parser.find_macro_positions(content) assert len(positions) == 2 assert content[positions[0][0]:positions[0][1]] == "{{require:dep1}}" assert content[positions[1][0]:positions[1][1]] == "{{optional:dep2}}" def test_count_macros(self): """Test macro counting.""" content = """ {{require:dep1}} {{require:dep2}} {{optional:dep3}} {{generate:gen1}} """ counts = self.parser.count_macros(content) assert counts['required'] == 2 assert counts['optional'] == 1 assert counts['generate'] == 1 def test_has_macros_true(self): """Test has_macros returns True when macros present.""" content = "Text {{require:test}} text" assert self.parser.has_macros(content) is True def test_has_macros_false(self): """Test has_macros returns False when no macros.""" content = "Just plain text" assert self.parser.has_macros(content) is False def test_parse_macro_with_spaces_in_target(self): """Test target with spaces is trimmed.""" content = "{{require: my-artifact }}" macros = self.parser.parse(content) assert macros[0].target == "my-artifact" def test_parse_parameter_with_spaces(self): """Test parameters with spaces are trimmed.""" content = "{{generate:test| key = value }}" macros = self.parser.parse(content) assert macros[0].parameters == {"key": "value"} # --- Shorthand @{target} syntax tests --- def test_parse_shorthand_macro(self): """Test parsing @{target} shorthand syntax.""" content = "Some text @{glossary} more text" macros = self.parser.parse(content) assert len(macros) == 1 assert macros[0].kind == MacroKind.REQUIRED assert macros[0].target == "glossary" assert macros[0].raw_text == "@{glossary}" assert macros[0].parameters == {} def test_parse_shorthand_with_line_numbers(self): """Test shorthand macros record line numbers.""" content = "Line 1\n@{dep1}\nLine 3\n@{dep2}" macros = self.parser.parse(content) assert len(macros) == 2 assert macros[0].line_number == 2 assert macros[1].line_number == 4 def test_parse_mixed_syntax(self): """Test parsing both {{kind:target}} and @{target} in same content.""" content = "{{require:dep1}} and @{dep2} and {{optional:dep3}}" macros = self.parser.parse(content) assert len(macros) == 3 assert macros[0].kind == MacroKind.REQUIRED assert macros[0].target == "dep1" assert macros[1].kind == MacroKind.OPTIONAL assert macros[1].target == "dep3" assert macros[2].kind == MacroKind.REQUIRED assert macros[2].target == "dep2" def test_shorthand_has_macros(self): """Test has_macros returns True for shorthand syntax.""" assert self.parser.has_macros("@{target}") is True assert self.parser.has_macros("no macros here") is False def test_shorthand_count_macros(self): """Test count_macros includes shorthand macros.""" content = "@{dep1} @{dep2} {{optional:dep3}}" counts = self.parser.count_macros(content) assert counts['required'] == 2 assert counts['optional'] == 1 def test_shorthand_find_macro_positions(self): """Test find_macro_positions includes shorthand macros.""" content = "Start @{dep1} middle {{require:dep2}} end" positions = self.parser.find_macro_positions(content) assert len(positions) == 2 # Should be sorted by position assert positions[0][2] == "@{dep1}" assert positions[1][2] == "{{require:dep2}}" def test_shorthand_target_trimmed(self): """Test shorthand target with spaces is trimmed.""" content = "@{ my-artifact }" macros = self.parser.parse(content) assert macros[0].target == "my-artifact"