- tests/conftest.py redirects MYTOOLKIT_HOME to a temp dir before imports so the module-level Config singleton never touches the real ~/.mytoolkit - CLI smoke: every registered command answers --help with exit code 0 - unit tests: Config get/set/migrate, templates_registry override/fallback, md_to_pdf pure helpers (pandoc-dependent cases skipif-guarded) - pyproject: dev dependency group (pytest) + pytest config; Makefile: make test
96 lines
3.1 KiB
Python
96 lines
3.1 KiB
Python
"""Tests for pure helpers in mytoolkit.md_to_pdf (no pandoc/typst needed)."""
|
|
|
|
import shutil
|
|
|
|
import pytest
|
|
|
|
from mytoolkit.md_to_pdf import (
|
|
_consume_balanced,
|
|
_longest_backtick_run,
|
|
_odd_fenced_code_before,
|
|
_typst_raw_fence,
|
|
preprocess_typst_callouts,
|
|
resolve_markdown_image_paths,
|
|
)
|
|
|
|
|
|
class TestResolveMarkdownImagePaths:
|
|
def test_relative_path_resolved_when_file_exists(self, tmp_path):
|
|
(tmp_path / "figs").mkdir()
|
|
(tmp_path / "figs" / "a.png").write_bytes(b"png")
|
|
out = resolve_markdown_image_paths("", tmp_path)
|
|
assert out == f""
|
|
|
|
def test_missing_file_left_unchanged(self, tmp_path):
|
|
md = ""
|
|
assert resolve_markdown_image_paths(md, tmp_path) == md
|
|
|
|
@pytest.mark.parametrize(
|
|
"src",
|
|
["https://x.com/a.png", "http://x.com/a.png", "data:image/png;base64,AA", "#frag"],
|
|
)
|
|
def test_urls_and_anchors_untouched(self, tmp_path, src):
|
|
md = f""
|
|
assert resolve_markdown_image_paths(md, tmp_path) == md
|
|
|
|
def test_absolute_path_untouched(self, tmp_path):
|
|
md = ""
|
|
assert resolve_markdown_image_paths(md, tmp_path) == md
|
|
|
|
def test_html_img_tag_resolved(self, tmp_path):
|
|
(tmp_path / "a.png").write_bytes(b"png")
|
|
out = resolve_markdown_image_paths('<img src="a.png" width="50%">', tmp_path)
|
|
assert out == f'<img src="{tmp_path / "a.png"}" width="50%">'
|
|
|
|
|
|
class TestBacktickFenceHelpers:
|
|
@pytest.mark.parametrize(
|
|
("s", "expected"),
|
|
[("", 0), ("a`b", 1), ("``x```y", 3), ("````", 4)],
|
|
)
|
|
def test_longest_backtick_run(self, s, expected):
|
|
assert _longest_backtick_run(s) == expected
|
|
|
|
def test_raw_fence_at_least_three(self):
|
|
assert _typst_raw_fence("plain") == "```"
|
|
|
|
def test_raw_fence_exceeds_inner_run(self):
|
|
assert _typst_raw_fence("has ``` inside") == "````"
|
|
|
|
|
|
class TestBalancedConsume:
|
|
def test_simple(self):
|
|
assert _consume_balanced("(a(b)c)", 0, "(", ")") == 7
|
|
|
|
def test_not_open_char(self):
|
|
assert _consume_balanced("x()", 0, "(", ")") is None
|
|
|
|
def test_unbalanced(self):
|
|
assert _consume_balanced("(a(b)", 0, "(", ")") is None
|
|
|
|
def test_quoted_chars_ignored(self):
|
|
assert _consume_balanced('(")")x', 0, "(", ")") == 5
|
|
|
|
|
|
class TestOddFencedCode:
|
|
def test_inside_fence(self):
|
|
md = "```\ncode\n"
|
|
assert _odd_fenced_code_before(md, len(md)) is True
|
|
|
|
def test_outside_fence(self):
|
|
md = "```\ncode\n```\n"
|
|
assert _odd_fenced_code_before(md, len(md)) is False
|
|
|
|
|
|
@pytest.mark.skipif(not shutil.which("pandoc"), reason="pandoc not installed")
|
|
class TestPreprocessTypstCallouts:
|
|
def test_info_box_wrapped_in_raw_typst(self):
|
|
md = "#info-box[\nHello **world**\n]\n"
|
|
out = preprocess_typst_callouts(md)
|
|
assert "{=typst}" in out
|
|
assert "#info-box[" in out
|
|
|
|
def test_callout_inside_code_fence_untouched(self):
|
|
md = "```\n#info-box[\nx\n]\n```\n"
|
|
assert preprocess_typst_callouts(md) == md
|