diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index dd4c590..063b817 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,3 +31,5 @@ jobs: pip install --upgrade hatch - name: Lint run: hatch fmt --check + - name: Check types + run: hatch run types:check diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 77779a2..c8cb779 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -13,7 +13,7 @@ env: FORCE_COLOR: "1" jobs: - deploy-test: + deploy: runs-on: ubuntu-latest @@ -27,6 +27,16 @@ jobs: run: | python -m pip install --upgrade pip pip install --upgrade hatch + - name: Check project version + run: | + PROJECT_VERSION=$(TERM=dumb hatch version) + + if [ "$PROJECT_VERSION" != "${TAG_NAME#v}" ]; then + echo "Wrong project version, cannot release: $PROJECT_VERSION, ${TAG_NAME#v} expected" + exit 1 + fi + env: + TAG_NAME: ${{ github.ref }} - name: Build package run: hatch build - name: Publish package diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14bc790..03fcc3f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,6 +17,12 @@ repos: language: system pass_filenames: false types: [ python ] + - id: check-types + name: run mypy + entry: hatch run types:check + language: system + pass_filenames: false + types: [ python ] - id: format name: run the formatter entry: hatch fmt -f diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..80778e8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +services: + kroki: + image: docker.io/yuzutech/kroki + depends_on: + - mermaid + - bpmn + - excalidraw + environment: + - KROKI_MERMAID_HOST=mermaid + - KROKI_BPMN_HOST=bpmn + - KROKI_EXCALIDRAW_HOST=excalidraw + ports: + - "127.0.0.1:8080:8000" + mermaid: + image: docker.io/yuzutech/kroki-mermaid + bpmn: + image: docker.io/yuzutech/kroki-bpmn + excalidraw: + image: docker.io/yuzutech/kroki-excalidraw diff --git a/kroki/client.py b/kroki/client.py index d0cc823..ee3893e 100644 --- a/kroki/client.py +++ b/kroki/client.py @@ -20,13 +20,13 @@ class DownloadedContent: def _ugly_temp_excalidraw_fix(self) -> None: """TODO: remove me, when excalidraw container works again.. ref: https://github.com/excalidraw/excalidraw/issues/7366""" - self.file_content = self.file_content.replace( + self.file_content: bytes = self.file_content.replace( b"https://unpkg.com/@excalidraw/excalidraw@undefined/dist", b"https://unpkg.com/@excalidraw/excalidraw@0.17.1/dist", ) def __init__(self, file_content: bytes, file_extension: str, additional_metadata: None | dict) -> None: - file_uuid = uuid3(NAMESPACE_OID, f"{additional_metadata}{file_content}") + file_uuid = uuid3(NAMESPACE_OID, f"{additional_metadata}{file_content!r}") self.file_name = f"{FILE_PREFIX}{file_uuid}.{file_extension}" self.file_content = file_content diff --git a/kroki/config.py b/kroki/config.py index 1077eb0..58278e8 100644 --- a/kroki/config.py +++ b/kroki/config.py @@ -7,8 +7,11 @@ class DeprecatedDownloadImagesCompat(config_options.Deprecated): - def pre_validation(self, config: "KrokiPluginConfig", key_name: str) -> None: + def pre_validation(self, config: MkDocsBaseConfig, key_name: str) -> None: """Set `HttpMethod: 'POST'`, if enabled""" + if not isinstance(config, KrokiPluginConfig): + return + if config.get(key_name) is None: return diff --git a/kroki/diagram_types.py b/kroki/diagram_types.py index a9b04f2..8e41cbc 100644 --- a/kroki/diagram_types.py +++ b/kroki/diagram_types.py @@ -1,11 +1,13 @@ from collections import ChainMap from typing import ClassVar +from mkdocs.exceptions import PluginError + from kroki.logging import log class KrokiDiagramTypes: - _kroki_base: ClassVar[dict[str, list[str]]] = { + _base_diagrams: ClassVar[dict[str, list[str]]] = { "bytefield": ["svg"], "ditaa": ["png", "svg"], "erd": ["png", "svg", "jpeg", "pdf"], @@ -27,7 +29,7 @@ class KrokiDiagramTypes: "wireviz": ["png", "svg"], } - _kroki_blockdiag: ClassVar[dict[str, list[str]]] = { + _blockdiag: ClassVar[dict[str, list[str]]] = { "blockdiag": ["png", "svg", "pdf"], "seqdiag": ["png", "svg", "pdf"], "actdiag": ["png", "svg", "pdf"], @@ -36,19 +38,19 @@ class KrokiDiagramTypes: "rackdiag": ["png", "svg", "pdf"], } - _kroki_bpmn: ClassVar[dict[str, list[str]]] = { + _bpmn: ClassVar[dict[str, list[str]]] = { "bpmn": ["svg"], } - _kroki_excalidraw: ClassVar[dict[str, list[str]]] = { + _excalidraw: ClassVar[dict[str, list[str]]] = { "excalidraw": ["svg"], } - _kroki_mermaid: ClassVar[dict[str, list[str]]] = { + _mermaid: ClassVar[dict[str, list[str]]] = { "mermaid": ["png", "svg"], } - _kroki_diagramsnet: ClassVar[dict[str, list[str]]] = { + _diagramsnet: ClassVar[dict[str, list[str]]] = { "diagramsnet": ["svg"], } @@ -66,39 +68,60 @@ def __init__( ): self._fence_prefix = fence_prefix - kroki_types = ChainMap( - self._kroki_base, - self._kroki_blockdiag if blockdiag_enabled else {}, - self._kroki_bpmn if bpmn_enabled else {}, - self._kroki_excalidraw if excalidraw_enabled else {}, - self._kroki_mermaid if mermaid_enabled else {}, - self._kroki_diagramsnet if diagramsnet_enabled else {}, + diagram_type_file_ext_map = ChainMap( + self._base_diagrams, + self._blockdiag if blockdiag_enabled else {}, + self._bpmn if bpmn_enabled else {}, + self._excalidraw if excalidraw_enabled else {}, + self._mermaid if mermaid_enabled else {}, + self._diagramsnet if diagramsnet_enabled else {}, + ) + + self._file_ext_mapping: dict[str, str] = self._get_file_ext_mapping( + diagram_type_file_ext_map, file_types, file_type_overrides ) - self._kroki_types_supporting_file = {} + log.debug("File and Diagram types configured: %s", self._file_ext_mapping) - for kroki_type, kroki_file_types in kroki_types.items(): - kroki_file_type = next(filter(lambda file: file in kroki_file_types, file_types), None) - if kroki_file_type is not None: - self._kroki_types_supporting_file[kroki_type] = next( - filter(lambda file: file in kroki_file_types, file_types), None + def _get_file_ext_mapping( + self, + diagram_type_file_ext_map: ChainMap[str, list[str]], + file_types: list[str], + file_type_overrides: dict[str, str], + ) -> dict[str, str]: + def get_file_type(diagram_type: str) -> str: + supported_file_types = diagram_type_file_ext_map[diagram_type] + file_type_override = file_type_overrides.get(diagram_type) + if file_type_override is not None: + if file_type_override not in supported_file_types: + err_msg = ( + f"{diagram_type}: {file_type_override} not in supported file types: " f"{supported_file_types}" + ) + raise PluginError(err_msg) + return file_type_override + + target_file_type = next((t for t in file_types if t in supported_file_types), None) + if target_file_type is None: + err_msg = ( + f"{diagram_type}: Not able to satisfy any of {file_types}, " + f"supported file types: {supported_file_types}" ) + raise PluginError(err_msg) - for kroki_type, kroki_file_type in file_type_overrides.items(): - self._kroki_types_supporting_file[kroki_type] = kroki_file_type + return target_file_type - log.debug("File and Diagram types configured: %s", self._kroki_types_supporting_file) + return {diagram_type: get_file_type(diagram_type) for diagram_type in diagram_type_file_ext_map} def get_file_ext(self, kroki_type: str) -> str: - return self._kroki_types_supporting_file[kroki_type] + return self._file_ext_mapping[kroki_type] - def get_kroki_type(self, block_type: None | str) -> None | str: + def get_kroki_type(self, block_type: None | str) -> str | None: if block_type is None: - return + return None if not block_type.startswith(self._fence_prefix): - return - kroki_type = block_type.removeprefix(self._fence_prefix).lower() - if kroki_type not in self._kroki_types_supporting_file: - return + return None + diagram_type = block_type.removeprefix(self._fence_prefix).lower() + if diagram_type not in self._file_ext_mapping: + return None - return kroki_type + return diagram_type diff --git a/kroki/render.py b/kroki/render.py index 05b97f8..fb06278 100644 --- a/kroki/render.py +++ b/kroki/render.py @@ -22,7 +22,8 @@ def _get_media_type(self, file_ext: str) -> str: case "pdf": return "application/pdf" case _: - raise NotImplementedError(file_ext) + err_msg = f"Not implemented: '{file_ext}" + raise PluginError(err_msg) def _image_response(self, image_src: ImageSrc) -> str: media_type = self._get_media_type(image_src.file_ext) diff --git a/pyproject.toml b/pyproject.toml index 73fb328..6a457a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,6 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -36,7 +35,7 @@ classifiers = [ dependencies = [ "mkdocs>=1.5.0", "requests>=2.27.0", - "result", + "result>=0.17.0", ] [project.entry-points."mkdocs.plugins"] @@ -68,6 +67,8 @@ include = [ [tool.hatch.envs.test] template = "hatch-test" [tool.hatch.envs.test.env-vars] +# start local test kroki: +# docker-compose up KROKI_SERVER_URL = "http://localhost:8080" [tool.hatch.envs.hatch-test] @@ -87,3 +88,15 @@ cov-report = [ "coverage xml", "coverage report --omit='tests/*'", ] + +[tool.hatch.envs.types] +template = "hatch-test" +extra-dependencies = [ + "mypy", + "types-PyYAML", + "types-beautifulsoup4", + "types-requests", + "types-babel", +] +[tool.hatch.envs.types.scripts] +check = "mypy --install-types --non-interactive {args:kroki tests}" diff --git a/tests/data/happy_path/docs/assets/diagram.plantuml b/tests/data/happy_path/docs/assets/diagram.plantuml new file mode 100644 index 0000000..a7b4074 --- /dev/null +++ b/tests/data/happy_path/docs/assets/diagram.plantuml @@ -0,0 +1,3 @@ +@startuml +Bob -> Alice : hello +@enduml diff --git a/tests/data/happy_path/docs/index.md b/tests/data/happy_path/docs/index.md index 400bcd1..1c33850 100644 --- a/tests/data/happy_path/docs/index.md +++ b/tests/data/happy_path/docs/index.md @@ -1,5 +1,7 @@ # Test +## inline + ```c4plantuml !include !include @@ -29,3 +31,9 @@ Rel(site_host, db, "read") Rel(site_host, reader, "serve") ``` + +## from file + +```plantuml +@from_file:assets/diagram.plantuml +``` diff --git a/tests/test_errors.py b/tests/test_errors.py index 52266a5..3c26584 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -4,56 +4,66 @@ from tests.utils import MkDocsHelper, get_expected_log_line +def _assert_error_block(err_msg: str, index_html: str): + index_soup = bs4.BeautifulSoup(index_html) + details_tag = index_soup.find("details") + assert isinstance(details_tag, bs4.Tag), "Error message container not in resulting HTML" + summary_tag = details_tag.summary + assert isinstance(summary_tag, bs4.Tag), "Error message container has no summary element" + assert err_msg in summary_tag.text + + @pytest.mark.usefixtures("kroki_timeout") def test_request_timeout() -> None: + # Arrange with MkDocsHelper("happy_path") as mkdocs_helper: mkdocs_helper.set_http_method("POST") - + # Act result = mkdocs_helper.invoke_build() - + # Assert assert result.exit_code == 0 assert get_expected_log_line("Request error") in result.output with open(mkdocs_helper.test_dir / "site/index.html") as index_html_file: - index_soup = bs4.BeautifulSoup(index_html_file.read()) - assert "Request error" in index_soup.find("details").summary.text + _assert_error_block("Request error", index_html_file.read()) @pytest.mark.usefixtures("kroki_bad_request") def test_request_bad_request() -> None: + # Arrange with MkDocsHelper("happy_path") as mkdocs_helper: mkdocs_helper.set_http_method("POST") - + # Act result = mkdocs_helper.invoke_build() - + # Assert assert result.exit_code == 0 assert get_expected_log_line("Diagram error!") in result.output with open(mkdocs_helper.test_dir / "site/index.html") as index_html_file: - index_soup = bs4.BeautifulSoup(index_html_file.read()) - assert "Diagram error!" in index_soup.find("details").summary.text + _assert_error_block("Diagram error!", index_html_file.read()) @pytest.mark.usefixtures("kroki_is_a_teapot") def test_request_other_error() -> None: + # Arrange with MkDocsHelper("happy_path") as mkdocs_helper: mkdocs_helper.set_http_method("POST") - + # Act result = mkdocs_helper.invoke_build() - + # Assert assert result.exit_code == 0 assert get_expected_log_line("Could not retrieve image data") in result.output with open(mkdocs_helper.test_dir / "site/index.html") as index_html_file: - index_soup = bs4.BeautifulSoup(index_html_file.read()) - assert "Could not retrieve image data" in index_soup.find("details").summary.text + _assert_error_block("Could not retrieve image data", index_html_file.read()) @pytest.mark.usefixtures("kroki_dummy") -def test_missing_file_from() -> None: +def test_missing_from_file() -> None: + # Arrange with MkDocsHelper("missing_from_file") as mkdocs_helper: mkdocs_helper.set_http_method("POST") + # Act result = mkdocs_helper.invoke_build() - + # Assert assert result.exit_code == 0 assert get_expected_log_line("Can't read file:") in result.output with open(mkdocs_helper.test_dir / "site/index.html") as index_html_file: - index_soup = bs4.BeautifulSoup(index_html_file.read()) - assert "Can't read file: " in index_soup.find("details").summary.text + _assert_error_block("Can't read file: ", index_html_file.read()) diff --git a/tests/test_errors_fail_fast.py b/tests/test_errors_fail_fast.py index 493facb..e3a8d01 100644 --- a/tests/test_errors_fail_fast.py +++ b/tests/test_errors_fail_fast.py @@ -5,12 +5,13 @@ @pytest.mark.usefixtures("kroki_timeout") def test_request_timeout() -> None: + # Arrange with MkDocsHelper("happy_path") as mkdocs_helper: mkdocs_helper.set_http_method("POST") mkdocs_helper.enable_fail_fast() - + # Act result = mkdocs_helper.invoke_build() - + # Assert assert result.exit_code == 1 assert get_expected_log_line("Request error") in result.output assert "Aborted with a BuildError!" in result.output @@ -18,12 +19,13 @@ def test_request_timeout() -> None: @pytest.mark.usefixtures("kroki_bad_request") def test_request_bad_request() -> None: + # Arrange with MkDocsHelper("happy_path") as mkdocs_helper: mkdocs_helper.set_http_method("POST") mkdocs_helper.enable_fail_fast() - + # Act result = mkdocs_helper.invoke_build() - + # Assert assert result.exit_code == 1 assert get_expected_log_line("Diagram error!") in result.output assert "Aborted with a BuildError!" in result.output @@ -31,24 +33,26 @@ def test_request_bad_request() -> None: @pytest.mark.usefixtures("kroki_is_a_teapot") def test_request_other_error() -> None: + # Arrange with MkDocsHelper("happy_path") as mkdocs_helper: mkdocs_helper.set_http_method("POST") mkdocs_helper.enable_fail_fast() - + # Act result = mkdocs_helper.invoke_build() - + # Assert assert result.exit_code == 1 assert get_expected_log_line("Could not retrieve image data") in result.output assert "Aborted with a BuildError!" in result.output @pytest.mark.usefixtures("kroki_dummy") -def test_missing_file_from() -> None: +def test_missing_from_file() -> None: + # Arrange with MkDocsHelper("missing_from_file") as mkdocs_helper: mkdocs_helper.enable_fail_fast() - + # Act result = mkdocs_helper.invoke_build() - + # Assert assert result.exit_code == 1 assert get_expected_log_line("Can't read file:") in result.output assert "Aborted with a BuildError!" in result.output diff --git a/tests/test_fences.py b/tests/test_fences.py index dceade2..5359d83 100644 --- a/tests/test_fences.py +++ b/tests/test_fences.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field import pytest from pytest_mock import MockerFixture @@ -11,9 +11,9 @@ @dataclass class StubInput: page_data: str - expected_code_block_data: str | None = None - epxected_options: dict | None = None - expected_kroki_type: str | None = None + expected_code_block_data: str = "" + epxected_options: dict = field(default_factory=dict) + expected_kroki_type: str = "" TEST_CASES = { @@ -26,7 +26,6 @@ class StubInput: """, expected_code_block_data="stuff containing ```\n\n", expected_kroki_type="plantuml", - epxected_options={}, ), "https://pandoc.org/MANUAL.html#fenced-code-blocks": StubInput( page_data="""~~~~~~~~~~~~~~~~ mermaid @@ -36,7 +35,6 @@ class StubInput: ~~~~~~~~~~~~~~~~""", expected_code_block_data="~~~~~~~~~~\ncode including tildes\n~~~~~~~~~~\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-119": StubInput( page_data=""" @@ -47,7 +45,6 @@ class StubInput: """, expected_code_block_data="<\n >\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-120": StubInput( page_data=""" @@ -58,7 +55,6 @@ class StubInput: """, expected_code_block_data="<\n >\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-122": StubInput( page_data=""" @@ -69,7 +65,6 @@ class StubInput: """, expected_code_block_data="aaa\n~~~\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-123": StubInput( page_data=""" @@ -80,7 +75,6 @@ class StubInput: """, expected_code_block_data="aaa\n```\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-125": StubInput( page_data=""" @@ -91,7 +85,6 @@ class StubInput: """, expected_code_block_data="aaa\n~~~\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-129": StubInput( page_data=""" @@ -102,7 +95,6 @@ class StubInput: """, expected_code_block_data="\n\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-130": StubInput( page_data=""" @@ -111,7 +103,6 @@ class StubInput: """, expected_code_block_data="", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-147": StubInput( page_data=""" @@ -121,7 +112,6 @@ class StubInput: """, expected_code_block_data="``` aaa\n", expected_kroki_type="mermaid", - epxected_options={}, ), } @@ -136,7 +126,6 @@ class StubInput: """, expected_code_block_data="aaa\n aaa\naaa\n", # "aaa\naaa\naaa\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-133": StubInput( page_data=""" @@ -148,7 +137,15 @@ class StubInput: """, expected_code_block_data=" aaa\n aaa\naaa\n", # "aaa\n aaa\naaa\n", expected_kroki_type="mermaid", - epxected_options={}, + ), + "https://spec.commonmark.org/0.31.2/#example-134": StubInput( + page_data=""" + ``` mermaid + aaa + ``` +""", + expected_code_block_data="aaa\n", # should not be replaced.. + expected_kroki_type="mermaid", ), } @@ -161,7 +158,6 @@ class StubInput: """, expected_code_block_data="foo\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-124": StubInput( page_data=""" @@ -172,7 +168,6 @@ class StubInput: """, expected_code_block_data="aaa\n```\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-126": StubInput( page_data=""" @@ -180,7 +175,6 @@ class StubInput: """, expected_code_block_data="\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-127": StubInput( page_data=""" @@ -191,7 +185,6 @@ class StubInput: """, expected_code_block_data="\n```\naaa\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-128": StubInput( page_data=""" @@ -202,7 +195,6 @@ class StubInput: """, expected_code_block_data="aaa\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-131": StubInput( page_data=""" @@ -213,17 +205,6 @@ class StubInput: """, expected_code_block_data="aaa\naaa\n", expected_kroki_type="mermaid", - epxected_options={}, - ), - "https://spec.commonmark.org/0.31.2/#example-134": StubInput( - page_data=""" - ``` mermaid - aaa - ```, - expected_code_block_data="aaa\n", # should not be replaced.. - expected_kroki_type="mermaid", - epxected_options={}, -""" ), "https://spec.commonmark.org/0.31.2/#example-135": StubInput( page_data=""" @@ -233,7 +214,6 @@ class StubInput: """, expected_code_block_data="aaa\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-136": StubInput( page_data=""" @@ -243,7 +223,6 @@ class StubInput: """, expected_code_block_data="aaa\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-140": StubInput( page_data=""" @@ -255,7 +234,6 @@ class StubInput: """, expected_code_block_data="bar\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-141": StubInput( page_data=""" @@ -268,7 +246,6 @@ class StubInput: """, expected_code_block_data="bar\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-146": StubInput( page_data=""" @@ -278,7 +255,6 @@ class StubInput: """, expected_code_block_data="foo\n", expected_kroki_type="mermaid", - epxected_options={}, ), } @@ -289,13 +265,13 @@ class StubInput: + [pytest.param(v, id=k) for k, v in TEST_CASES_NOT_COMPLYING.items()], ) def test_fences(test_data: StubInput, mock_kroki_diagram_types: KrokiDiagramTypes, mocker: MockerFixture) -> None: + # Arrange parser = MarkdownParser("", mock_kroki_diagram_types) - callback_stub = mocker.stub() context_stub = mocker.stub() - + # Act parser.replace_kroki_blocks(test_data.page_data, callback_stub, context_stub) - + # Assert callback_stub.assert_called_once_with( KrokiImageContext( kroki_type=test_data.expected_kroki_type, @@ -310,11 +286,11 @@ def test_fences(test_data: StubInput, mock_kroki_diagram_types: KrokiDiagramType def test_fences_not_supported( test_data: StubInput, mock_kroki_diagram_types: KrokiDiagramTypes, mocker: MockerFixture ) -> None: + # Arrange parser = MarkdownParser("", mock_kroki_diagram_types) - callback_stub = mocker.stub() context_stub = mocker.stub() - + # Act parser.replace_kroki_blocks(test_data.page_data, callback_stub, context_stub) - + # Assert callback_stub.assert_not_called() diff --git a/tests/test_happy_path.py b/tests/test_happy_path.py new file mode 100644 index 0000000..50bb6a6 --- /dev/null +++ b/tests/test_happy_path.py @@ -0,0 +1,22 @@ +import bs4 +import pytest + +from tests.utils import MkDocsHelper + + +@pytest.mark.usefixtures("kroki_dummy") +def test_happy_path() -> None: + # Arrange + with MkDocsHelper("happy_path") as mkdocs_helper: + mkdocs_helper.set_http_method("POST") + # Act + result = mkdocs_helper.invoke_build() + # Assert + assert result.exit_code == 0 + with open(mkdocs_helper.test_dir / "site/index.html") as index_html_file: + index_html = index_html_file.read() + + index_soup = bs4.BeautifulSoup(index_html) + img_tags = index_soup.find_all(attrs={"name": "Kroki"}) + + assert len(img_tags) == 2 diff --git a/tests/test_nested.py b/tests/test_nested.py index 9012d99..640e4a8 100644 --- a/tests/test_nested.py +++ b/tests/test_nested.py @@ -6,6 +6,7 @@ @pytest.mark.usefixtures("kroki_dummy") def test_block_inside_html() -> None: + # Arrange with MkDocsTemplateHelper("""
Show Sequence diagram... @@ -21,8 +22,9 @@ def test_block_inside_html() -> None: ``` """) as mkdocs_helper: mkdocs_helper.set_http_method("POST") + # Act result = mkdocs_helper.invoke_build() - + # Assert assert result.exit_code == 0, f"exit code {result.exit_code}, expected 0" with open(mkdocs_helper.test_dir / "site/index.html") as index_html: index_soup = bs4.BeautifulSoup(index_html.read()) diff --git a/tests/utils.py b/tests/utils.py index 664bf6e..a40f430 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -5,7 +5,7 @@ from contextlib import AbstractContextManager from pathlib import Path from string import Template -from typing import Literal +from typing import Final, Literal import yaml from click.testing import CliRunner, Result @@ -15,7 +15,7 @@ from tests.compat import chdir -def get_expected_log_line(log_msg) -> str: +def get_expected_log_line(log_msg: str) -> str: return f"{log.prefix}: {log_msg}" @@ -25,62 +25,67 @@ def __init__(self) -> None: class MkDocsHelper(AbstractContextManager): + class Context: + def __init__(self, test_dir: Path) -> None: + self.test_dir: Final[Path] = test_dir + self.config_file_path: Final[Path] = test_dir / "mkdocs.yml" + with open(self.test_dir / "mkdocs.yml") as file: + self.config_file = yaml.safe_load(file) + + def _dump_config(self) -> None: + with open(self.config_file_path, mode="w") as file: + yaml.safe_dump(self.config_file, file) + + def _get_plugin_config_entry(self) -> dict: + for plugin_entry in self.config_file["plugins"]: + if "kroki" in plugin_entry: + return plugin_entry["kroki"] + raise NoPluginEntryError + + def enable_fail_fast(self) -> None: + self._get_plugin_config_entry()["FailFast"] = True + + def set_http_method(self, method: Literal["GET", "POST"]) -> None: + self._get_plugin_config_entry()["HttpMethod"] = method + + def set_fence_prefix(self, fence_prefix: str) -> None: + self._get_plugin_config_entry()["FencePrefix"] = fence_prefix + + def invoke_build(self) -> Result: + self._dump_config() + runner = CliRunner() + with chdir(self.test_dir): + return runner.invoke(build_command) + def __init__(self, test_case: str) -> None: - self.config_file = None self.test_case = test_case self.test_dir = Path(tempfile.mkdtemp()) - self._copy_test_case() def _copy_test_case(self) -> None: # equals to `../data`, using current source file as a pin data_dir = pathlib.Path(os.path.realpath(__file__)).parent / "data" shutil.copytree(data_dir / self.test_case, self.test_dir, dirs_exist_ok=True) - def _load_config(self) -> None: - with open(self.test_dir / "mkdocs.yml") as file: - self.config_file = yaml.safe_load(file) - - def _dump_config(self) -> None: - with open(self.test_dir / "mkdocs.yml", mode="w") as file: - yaml.safe_dump(self.config_file, file) - - def __enter__(self) -> "MkDocsHelper": - self._load_config() - - return self + def __enter__(self) -> Context: + self._copy_test_case() + return MkDocsHelper.Context(self.test_dir) def __exit__(self, *_args) -> None: shutil.rmtree(self.test_dir) - def _get_plugin_config_entry(self) -> dict: - for plugin_entry in self.config_file["plugins"]: - if "kroki" in plugin_entry: - return plugin_entry["kroki"] - raise NoPluginEntryError - - def enable_fail_fast(self) -> None: - self._get_plugin_config_entry()["FailFast"] = True - - def set_http_method(self, method: Literal["GET", "POST"]) -> None: - self._get_plugin_config_entry()["HttpMethod"] = method - - def set_fence_prefix(self, fence_prefix: str) -> None: - self._get_plugin_config_entry()["FencePrefix"] = fence_prefix - - def invoke_build(self) -> Result: - self._dump_config() - runner = CliRunner() - with chdir(self.test_dir): - return runner.invoke(build_command) - class MkDocsTemplateHelper(MkDocsHelper): - def _substitute_code_block(self, code_block: str): + def _substitute_code_block(self): with open(self.test_dir / "docs/index.md") as in_file: file_content = Template(in_file.read()) with open(self.test_dir / "docs/index.md", "w") as out_file: - out_file.write(file_content.substitute(code_block=code_block)) + out_file.write(file_content.substitute(code_block=self.code_block)) def __init__(self, code_block: str) -> None: super().__init__("template") - self._substitute_code_block(code_block) + self.code_block = code_block + + def __enter__(self) -> MkDocsHelper.Context: + context = super().__enter__() + self._substitute_code_block() + return context