blob: 28c362233622cba0d41110052df182490ac9f434 [file] [edit]
import os
import sys
from unittest.mock import patch
import pytest
import mdformat
from mdformat._cli import get_plugin_info_str, run, wrap_paragraphs
from mdformat.plugins import CODEFORMATTERS, PARSER_EXTENSIONS
from tests.utils import (
FORMATTED_MARKDOWN,
UNFORMATTED_MARKDOWN,
ASTChangingPlugin,
PrefixPostprocessPlugin,
)
def test_no_files_passed():
assert run(()) == 0
def test_format(tmp_path):
file_path = tmp_path / "test_markdown.md"
file_path.write_text(UNFORMATTED_MARKDOWN)
assert run((str(file_path),)) == 0
assert file_path.read_text() == FORMATTED_MARKDOWN
def test_format__folder(tmp_path):
file_path_1 = tmp_path / "test_markdown1.md"
file_path_2 = tmp_path / "test_markdown2.md"
file_path_3 = tmp_path / "not_markdown3"
file_path_1.write_text(UNFORMATTED_MARKDOWN)
file_path_2.write_text(UNFORMATTED_MARKDOWN)
file_path_3.write_text(UNFORMATTED_MARKDOWN)
assert run((str(tmp_path),)) == 0
assert file_path_1.read_text() == FORMATTED_MARKDOWN
assert file_path_2.read_text() == FORMATTED_MARKDOWN
assert file_path_3.read_text() == UNFORMATTED_MARKDOWN
def test_format__folder_leads_to_invalid(tmp_path):
file_path_1 = tmp_path / "test_markdown1.md"
file_path_1.mkdir()
assert run((str(tmp_path),)) == 0
assert file_path_1.is_dir()
def test_format__symlinks(tmp_path):
# Create two MD files
file_path_1 = tmp_path / "test_markdown1.md"
file_path_2 = tmp_path / "test_markdown2.md"
file_path_1.write_text(UNFORMATTED_MARKDOWN)
file_path_2.write_text(UNFORMATTED_MARKDOWN)
# Create a symlink to both files: one in the root folder, one in a sub folder
subdir_path = tmp_path / "subdir"
subdir_path.mkdir()
symlink_1 = subdir_path / "symlink1.md"
symlink_1.symlink_to(file_path_1)
symlink_2 = tmp_path / "symlink2.md"
symlink_2.symlink_to(file_path_2)
# Format file 1 via directory of the symlink, and file 2 via symlink
assert run([str(subdir_path), str(symlink_2)]) == 0
# Assert that files are formatted and symlinks are not overwritten
assert file_path_1.read_text() == FORMATTED_MARKDOWN
assert file_path_2.read_text() == FORMATTED_MARKDOWN
assert symlink_1.is_symlink()
assert symlink_2.is_symlink()
def test_broken_symlink(tmp_path, capsys):
# Create a broken symlink
file_path = tmp_path / "test_markdown1.md"
symlink_path = tmp_path / "symlink"
symlink_path.symlink_to(file_path)
with pytest.raises(SystemExit) as exc_info:
run([str(symlink_path)])
assert exc_info.value.code == 2
captured = capsys.readouterr()
assert "does not exist" in captured.err
def test_invalid_file(capsys):
with pytest.raises(SystemExit) as exc_info:
run(("this is not a valid filepath?`=|><@{[]\\/,.%ยค#'",))
assert exc_info.value.code == 2
captured = capsys.readouterr()
assert "does not exist" in captured.err
@pytest.mark.skipif(os.name == "nt", reason="No os.mkfifo on windows")
def test_fifo(tmp_path, capsys):
fifo_path = tmp_path / "fifo1"
os.mkfifo(fifo_path)
with pytest.raises(SystemExit) as exc_info:
run((str(fifo_path),))
assert exc_info.value.code == 2
assert "does not exist" in capsys.readouterr().err
def test_check(tmp_path):
file_path = tmp_path / "test_markdown.md"
file_path.write_bytes(FORMATTED_MARKDOWN.encode())
assert run((str(file_path), "--check")) == 0
def test_check__fail(tmp_path):
file_path = tmp_path / "test_markdown.md"
file_path.write_text(UNFORMATTED_MARKDOWN)
assert run((str(file_path), "--check")) == 1
def test_check__multi_fail(capsys, tmp_path):
"""Test for --check flag when multiple files are unformatted.
Test that the names of all unformatted files are listed when using
--check.
"""
file_path1 = tmp_path / "test_markdown1.md"
file_path2 = tmp_path / "test_markdown2.md"
file_path1.write_text(UNFORMATTED_MARKDOWN)
file_path2.write_text(UNFORMATTED_MARKDOWN)
assert run((str(tmp_path), "--check")) == 1
captured = capsys.readouterr()
assert str(file_path1) in captured.err
assert str(file_path2) in captured.err
def example_formatter(code, info):
return "dummy\n"
def test_formatter_plugin(tmp_path, monkeypatch):
monkeypatch.setitem(CODEFORMATTERS, "lang", example_formatter)
file_path = tmp_path / "test_markdown.md"
file_path.write_text("```lang\nother\n```\n")
assert run((str(file_path),)) == 0
assert file_path.read_text() == "```lang\ndummy\n```\n"
def test_dash_stdin(capfd, patch_stdin):
patch_stdin(UNFORMATTED_MARKDOWN)
assert run(("-",)) == 0
captured = capfd.readouterr()
assert captured.out == FORMATTED_MARKDOWN
def test_wrap_paragraphs():
with patch("shutil.get_terminal_size", return_value=(72, 24)):
assert wrap_paragraphs(
[
'Error: Could not format "/home/user/file_name_longer_than_wrap_width--------------------------------------.md".', # noqa: E501
"The formatted Markdown renders to different HTML than the input Markdown. " # noqa: E501
"This is likely a bug in mdformat. "
"Please create an issue report here: "
"https://github.com/hukkin/mdformat/issues",
]
) == (
"Error: Could not format\n"
'"/home/user/file_name_longer_than_wrap_width--------------------------------------.md".\n' # noqa: E501
"\n"
"The formatted Markdown renders to different HTML than the input\n"
"Markdown. This is likely a bug in mdformat. Please create an issue\n"
"report here: https://github.com/hukkin/mdformat/issues\n"
)
def test_version(capsys):
with pytest.raises(SystemExit) as exc_info:
run(["--version"])
assert exc_info.value.code == 0
captured = capsys.readouterr()
assert captured.out.startswith(f"mdformat {mdformat.__version__}")
def test_no_wrap(tmp_path):
file_path = tmp_path / "test_markdown.md"
file_path.write_text(
"all\n"
"these newlines\n"
"except the one in this hardbreak \n"
"should be\n"
"removed because they are\n"
"in the same paragraph\n"
"\n"
"This however is the next\n"
"paragraph. Whitespace should be collapsed\n"
" \t here\n"
)
assert run([str(file_path), "--wrap=no"]) == 0
assert (
file_path.read_text()
== "all these newlines except the one in this hardbreak\\\n"
"should be removed because they are in the same paragraph\n"
"\n"
"This however is the next paragraph. Whitespace should be collapsed here\n"
)
def test_wrap(tmp_path):
file_path = tmp_path / "test_markdown.md"
file_path.write_text(
"This\n"
"text\n"
"should\n"
"be wrapped again so that wrap width is whatever is defined below. "
"Also whitespace should\t\tbe collapsed. "
"Next up a second paragraph:\n"
"\n"
"This paragraph should also be wrapped. "
"Here's some more text to wrap. "
"Here's some more text to wrap. "
"Here's some more text to wrap. "
)
assert run([str(file_path), "--wrap=60"]) == 0
assert (
file_path.read_text()
== "This text should be wrapped again so that wrap width is\n"
"whatever is defined below. Also whitespace should be\n"
"collapsed. Next up a second paragraph:\n"
"\n"
"This paragraph should also be wrapped. Here's some more text\n"
"to wrap. Here's some more text to wrap. Here's some more\n"
"text to wrap.\n"
)
def test_consecutive_wrap_width_lines(tmp_path):
"""Test a case where two consecutive lines' width equals wrap width.
There was a bug where this would cause an extra newline and split
the paragraph, hence the test.
"""
wrap_width = 20
file_path = tmp_path / "test_markdown.md"
text = "A" * wrap_width + "\n" + "A" * wrap_width + "\n"
file_path.write_text(text)
assert run([str(file_path), f"--wrap={wrap_width}"]) == 0
assert file_path.read_text() == text
def test_wrap__hard_break(tmp_path):
file_path = tmp_path / "test_markdown.md"
file_path.write_text(
"This\n"
"text\n"
"should\n"
"be wrapped again\\\n"
"so that wrap width is whatever is defined below. "
"Also whitespace should\t\tbe collapsed."
)
assert run([str(file_path), "--wrap=60"]) == 0
assert (
file_path.read_text() == "This text should be wrapped again\\\n"
"so that wrap width is whatever is defined below. Also\n"
"whitespace should be collapsed.\n"
)
def test_bad_wrap_width(capsys):
with pytest.raises(SystemExit) as exc_info:
run(["some-path.md", "--wrap=-1"])
assert exc_info.value.code == 2
captured = capsys.readouterr()
assert "error: argument --wrap" in captured.err
def test_eol__lf(tmp_path):
file_path = tmp_path / "test.md"
file_path.write_bytes(b"Oi\r\n")
assert run([str(file_path)]) == 0
assert file_path.read_bytes() == b"Oi\n"
def test_eol__crlf(tmp_path):
file_path = tmp_path / "test.md"
file_path.write_bytes(b"Oi\n")
assert run([str(file_path), "--end-of-line=crlf"]) == 0
assert file_path.read_bytes() == b"Oi\r\n"
def test_eol__keep_lf(tmp_path):
file_path = tmp_path / "test.md"
file_path.write_bytes(b"Oi\n")
assert run([str(file_path), "--end-of-line=keep"]) == 0
assert file_path.read_bytes() == b"Oi\n"
def test_eol__keep_crlf(tmp_path):
file_path = tmp_path / "test.md"
file_path.write_bytes(b"Oi\r\n")
assert run([str(file_path), "--end-of-line=keep"]) == 0
assert file_path.read_bytes() == b"Oi\r\n"
def test_eol__crlf_stdin(capfd, patch_stdin):
patch_stdin("Oi\n")
assert run(["-", "--end-of-line=crlf"]) == 0
captured = capfd.readouterr()
assert captured.out == "Oi\r\n"
def test_eol__check_lf(tmp_path):
file_path = tmp_path / "test.md"
file_path.write_bytes(b"lol\r\n")
assert run((str(file_path), "--check")) == 1
file_path.write_bytes(b"lol\n")
assert run((str(file_path), "--check")) == 0
def test_eol__check_crlf(tmp_path):
file_path = tmp_path / "test.md"
file_path.write_bytes(b"lol\n")
assert run((str(file_path), "--check", "--end-of-line=crlf")) == 1
file_path.write_bytes(b"lol\r\n")
assert run((str(file_path), "--check", "--end-of-line=crlf")) == 0
def test_eol__check_keep_lf(tmp_path):
file_path = tmp_path / "test.md"
file_path.write_bytes(b"lol\n")
assert run((str(file_path), "--check", "--end-of-line=keep")) == 0
file_path.write_bytes(b"mixed\nEOLs\r")
assert run((str(file_path), "--check", "--end-of-line=keep")) == 1
def test_eol__check_keep_crlf(tmp_path):
file_path = tmp_path / "test.md"
file_path.write_bytes(b"lol\r\n")
assert run((str(file_path), "--check", "--end-of-line=keep")) == 0
file_path.write_bytes(b"mixed\r\nEOLs\n")
assert run((str(file_path), "--check", "--end-of-line=keep")) == 1
def test_cli_no_validate(tmp_path):
file_path = tmp_path / "test.md"
content = "1. ordered"
file_path.write_text(content)
with patch("mdformat.renderer._context.get_list_marker_type", return_value="?"):
assert run((str(file_path),)) == 1
assert file_path.read_text() == content
assert run((str(file_path), "--no-validate")) == 0
assert file_path.read_text() == "1? ordered\n"
def test_get_plugin_info_str():
info = get_plugin_info_str(
{"mdformat-tables": ("0.1.0", ["tables"])},
{"mdformat-black": ("12.1.0", ["python"])},
)
assert (
info
== """\
installed codeformatters:
mdformat-black: python
installed extensions:
mdformat-tables: tables"""
)
def test_no_timestamp_modify(tmp_path):
file_path = tmp_path / "test.md"
file_path.write_bytes(b"lol\n")
initial_access_time = 0
initial_mod_time = 0
os.utime(file_path, (initial_access_time, initial_mod_time))
# Assert that modification time does not change when no changes are applied
assert run([str(file_path)]) == 0
assert os.path.getmtime(file_path) == initial_mod_time
@pytest.mark.skipif(
sys.version_info < (3, 13), reason="'exclude' only possible on 3.13+"
)
def test_exclude(tmp_path):
subdir_path_1 = tmp_path / "folder1"
subdir_path_2 = subdir_path_1 / "folder2"
file_path_1 = subdir_path_2 / "file1.md"
subdir_path_1.mkdir()
subdir_path_2.mkdir()
file_path_1.write_text(UNFORMATTED_MARKDOWN)
cwd = tmp_path
with patch("mdformat._cli.Path.cwd", return_value=cwd):
for good_pattern in [
"folder1/folder2/file1.md",
"**",
"**/*.md",
"**/file1.md",
"folder1/**",
]:
assert run([str(file_path_1), "--exclude", good_pattern]) == 0
assert file_path_1.read_text() == UNFORMATTED_MARKDOWN
for bad_pattern in [
"**file1.md",
"file1.md",
"folder1",
"*.md",
"*",
"folder1/*",
]:
file_path_1.write_text(UNFORMATTED_MARKDOWN)
assert run([str(file_path_1), "--exclude", bad_pattern]) == 0
assert file_path_1.read_text() == FORMATTED_MARKDOWN
def test_codeformatters(tmp_path, monkeypatch):
monkeypatch.setitem(CODEFORMATTERS, "enabled-lang", lambda code, info: "dumdum")
monkeypatch.setitem(CODEFORMATTERS, "disabled-lang", lambda code, info: "dumdum")
file_path = tmp_path / "test.md"
unformatted = """\
```disabled-lang
hey
```
```enabled-lang
hey
```
"""
formatted = """\
```disabled-lang
hey
```
```enabled-lang
dumdum
```
"""
file_path.write_text(unformatted)
assert run((str(file_path), "--codeformatters", "enabled-lang")) == 0
assert file_path.read_text() == formatted
def test_extensions(tmp_path, monkeypatch):
ast_plugin_name = "ast-plug"
prefix_plugin_name = "prefix-plug"
monkeypatch.setitem(PARSER_EXTENSIONS, ast_plugin_name, ASTChangingPlugin)
monkeypatch.setitem(PARSER_EXTENSIONS, prefix_plugin_name, PrefixPostprocessPlugin)
unformatted = "original text\n"
file_path = tmp_path / "test.md"
file_path.write_text(unformatted)
assert run((str(file_path), "--extensions", "prefix-plug")) == 0
assert file_path.read_text() == "Prefixed!original text\n"
file_path.write_text(unformatted)
assert run((str(file_path), "--extensions", "ast-plug")) == 0
assert file_path.read_text() == ASTChangingPlugin.TEXT_REPLACEMENT + "\n"
file_path.write_text(unformatted)
assert (
run((str(file_path), "--extensions", "ast-plug", "--extensions", "prefix-plug"))
== 0
)
assert (
file_path.read_text() == "Prefixed!" + ASTChangingPlugin.TEXT_REPLACEMENT + "\n"
)
def test_codeformatters__invalid(tmp_path, capsys):
file_path = tmp_path / "test.md"
file_path.write_text("")
assert run((str(file_path), "--codeformatters", "no-exists")) == 1
captured = capsys.readouterr()
assert "Error: Invalid code formatter required" in captured.err
def test_extensions__invalid(tmp_path, capsys):
file_path = tmp_path / "test.md"
file_path.write_text("")
assert run((str(file_path), "--extensions", "no-exists")) == 1
captured = capsys.readouterr()
assert "Error: Invalid extension required" in captured.err
def test_no_codeformatters(tmp_path, monkeypatch):
monkeypatch.setitem(CODEFORMATTERS, "lang", lambda code, info: "dumdum")
file_path = tmp_path / "test.md"
original_md = """\
```lang
original code
```
"""
file_path.write_text(original_md)
assert run((str(file_path), "--no-codeformatters")) == 0
assert file_path.read_text() == original_md
def test_no_extensions(tmp_path, monkeypatch):
plugin_name = "plug-name"
monkeypatch.setitem(PARSER_EXTENSIONS, plugin_name, ASTChangingPlugin)
file_path = tmp_path / "test.md"
original_md = "original md\n"
file_path.write_text(original_md)
assert run((str(file_path), "--no-extensions")) == 0
assert file_path.read_text() == original_md