From 51efe128b873113086c66006ee25e0d8e2c8aad1 Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Tue, 21 Jul 2026 14:34:03 -0500 Subject: [PATCH 01/20] test: add golden compatibility coverage --- MANIFEST.in | 1 + .../golden/python311_compatibility.json | 257 ++++++++++++++++++ tests/test_golden_compatibility.py | 116 ++++++++ 3 files changed, 374 insertions(+) create mode 100644 MANIFEST.in create mode 100644 tests/fixtures/golden/python311_compatibility.json create mode 100644 tests/test_golden_compatibility.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..10b4fbc --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-include tests/fixtures *.json *.md diff --git a/tests/fixtures/golden/python311_compatibility.json b/tests/fixtures/golden/python311_compatibility.json new file mode 100644 index 0000000..4b5e4fc --- /dev/null +++ b/tests/fixtures/golden/python311_compatibility.json @@ -0,0 +1,257 @@ +{ + "block_html_and_comment": { + "errors": [], + "html": [ + "
\n\n\n
\nEnglish\n
\n
", + "
\n\n\n
\nDeutsch\n
\n
" + ], + "left_structure": "xx", + "link_errors": [], + "right_structure": "xx", + "text": [ + "\n\n
\nEnglish\n
", + "\n\n
\nDeutsch\n
" + ] + }, + "extra_paragraph": { + "errors": [ + { + "message": "There is a missing element `paragraph`.", + "style": "ins", + "symbol": "p", + "type": "InsertError" + }, + { + "message": "There is a missing element `text`.", + "style": "ins", + "symbol": "t", + "type": "InsertError" + } + ], + "html": [ + "
\none\n
", + "
\neins\n\nzwei\n\n\n
" + ], + "left_structure": "pt", + "link_errors": [ + { + "message": "There is a missing element `paragraph`.", + "style": "ins", + "symbol": "p", + "type": "InsertError" + } + ], + "right_structure": "ptpt", + "text": [ + "one", + "eins\n\nzwei" + ] + }, + "hard_break_and_direction_marks": { + "errors": [], + "html": [ + "
\none  \u00b6\ntwo\n\nthree\n
", + "
\neins  \u00b6\nzwei\n\ndrei\n
" + ], + "left_structure": "ptntpt", + "link_errors": [], + "right_structure": "ptntpt", + "text": [ + "one \u00b6\ntwo\n\nthree", + "eins \u00b6\nzwei\n\ndrei" + ] + }, + "heading_styles": { + "errors": [], + "html": [ + "
\n#Heading\n\n##Setext heading\n
", + "
\n#Ueberschrift\n\n##Setext Ueberschrift\n
" + ], + "left_structure": "1t2t", + "link_errors": [], + "right_structure": "1t2t", + "text": [ + "#Heading\n\n##Setext heading", + "#Ueberschrift\n\n##Setext Ueberschrift" + ] + }, + "image_and_reference_link": { + "errors": [], + "html": [ + "
\n![English alt](image.png)\n\nRead [the guide][docs].\n\n[docs]: https://example.test/docs\n
", + "
\n![Deutscher Alt-Text](image.png)\n\nLies [die Anleitung][docs].\n\n[docs]: https://example.test/docs\n
" + ], + "left_structure": "piptatpt", + "link_errors": [], + "right_structure": "piptatpt", + "text": [ + "![English alt](image.png)\n\nRead [the guide][docs].\n\n[docs]: https://example.test/docs", + "![Deutscher Alt-Text](image.png)\n\nLies [die Anleitung][docs].\n\n[docs]: https://example.test/docs" + ] + }, + "inline_html": { + "errors": [], + "html": [ + "
\nbefore <sub>one</sub> after\n
", + "
\nvor <sub>eins</sub> danach\n
" + ], + "left_structure": "pt", + "link_errors": [], + "right_structure": "pt", + "text": [ + "before <sub>one</sub> after", + "vor <sub>eins</sub> danach" + ] + }, + "link_count_change": { + "errors": [], + "html": [ + "
\nRead [one](https://example.test/one).\n
", + "
\nLies [eins](https://example.test/one) und [zwei](https://example.test/two).\n
" + ], + "left_structure": "ptat", + "link_errors": [ + { + "message": "There is a missing element `link`.", + "style": "ins", + "symbol": "a", + "type": "InsertError" + } + ], + "right_structure": "ptatat", + "text": [ + "Read [one](https://example.test/one).", + "Lies [eins](https://example.test/one) und [zwei](https://example.test/two)." + ] + }, + "list_variants": { + "errors": [], + "html": [ + "
\n* one\n* two\n\n\n0. first\n1. second\n
", + "
\n* eins\n* zwei\n\n\n0. erste\n1. zweite\n
" + ], + "left_structure": "lmtmtlmtmt", + "link_errors": [], + "right_structure": "lmtmtlmtmt", + "text": [ + "* one\n* two\n\n\n0. first\n1. second", + "* eins\n* zwei\n\n\n0. erste\n1. zweite" + ] + }, + "removed_paragraph": { + "errors": [ + { + "message": "There is an additional element `paragraph`.", + "style": "del", + "symbol": "p", + "type": "DeleteError" + }, + { + "message": "There is an additional element `text`.", + "style": "del", + "symbol": "t", + "type": "DeleteError" + } + ], + "html": [ + "
\none\n\ntwo\n\n\n
", + "
\neins\n
" + ], + "left_structure": "ptpt", + "link_errors": [ + { + "message": "There is an additional element `paragraph`.", + "style": "del", + "symbol": "p", + "type": "DeleteError" + } + ], + "right_structure": "pt", + "text": [ + "one\n\ntwo", + "eins" + ] + }, + "same_links": { + "errors": [], + "html": [ + "
\nA [link](https://example.test)\n
", + "
\nB [translated](https://example.test)\n
" + ], + "left_structure": "pta", + "link_errors": [], + "right_structure": "pta", + "text": [ + "A [link](https://example.test)", + "B [translated](https://example.test)" + ] + }, + "zendesk_callout_style": { + "errors": [ + { + "message": "There is an additional element `callout`.", + "style": "del", + "symbol": "C", + "type": "DeleteError" + }, + { + "message": "There is a missing element `callout`.", + "style": "ins", + "symbol": "C", + "type": "InsertError" + } + ], + "html": [ + "
\n\n\n#Title\n\nbody\n\n\n\n
", + "
\n\n\n#Titel\n\ninhalt\n\n\n\n
" + ], + "left_structure": "C1tpt", + "link_errors": [ + { + "message": "There is an additional element `callout`.", + "style": "del", + "symbol": "C", + "type": "DeleteError" + }, + { + "message": "There is a missing element `callout`.", + "style": "ins", + "symbol": "C", + "type": "InsertError" + } + ], + "right_structure": "C1tpt", + "text": [ + "\n\n#Title\n\nbody\n\n", + "\n\n#Titel\n\ninhalt\n\n" + ] + }, + "zendesk_steps_in_tabs": { + "errors": [], + "html": [ + "
\n\n\n#Steps\n\n\n\n0. one\n1. two\n\n\n\n\n
", + "
\n\n\n#Schritte\n\n\n\n0. eins\n1. zwei\n\n\n\n\n
" + ], + "left_structure": "T1tSlmtmt", + "link_errors": [], + "right_structure": "T1tSlmtmt", + "text": [ + "\n\n#Steps\n\n\n\n0. one\n1. two\n\n\n\n", + "\n\n#Schritte\n\n\n\n0. eins\n1. zwei\n\n\n\n" + ] + }, + "zendesk_unstyled_callout_in_tabs": { + "errors": [], + "html": [ + "
\n\n\n#Topic\n\n\n\nbody\n\n\n\n
", + "
\n\n\n#Thema\n\n\n\ninhalt\n\n\n\n
" + ], + "left_structure": "T1tCpt", + "link_errors": [], + "right_structure": "T1tCpt", + "text": [ + "\n\n#Topic\n\n\n\nbody\n\n\n", + "\n\n#Thema\n\n\n\ninhalt\n\n\n" + ] + } +} diff --git a/tests/test_golden_compatibility.py b/tests/test_golden_compatibility.py new file mode 100644 index 0000000..dca31fa --- /dev/null +++ b/tests/test_golden_compatibility.py @@ -0,0 +1,116 @@ +import json +from pathlib import Path +from unittest import TestCase + +import sdiff +from sdiff import parser +from sdiff.renderer import HtmlRenderer, TextRenderer + + +GOLDEN_PATH = Path(__file__).parent / 'fixtures' / 'golden' / 'python311_compatibility.json' + +CASES = { + 'block_html_and_comment': ( + '\n\n
\nEnglish\n
', + '\n\n
\nDeutsch\n
', + sdiff.MdParser, + ), + 'extra_paragraph': ('one', 'eins\n\nzwei', sdiff.MdParser), + 'hard_break_and_direction_marks': ( + '\u200eone \ntwo\n \nthree\u200f', + '\u200eeins \nzwei\n \ndrei\u200f', + sdiff.MdParser, + ), + 'heading_styles': ( + '# Heading\n\nSetext heading\n--------------', + '# Ueberschrift\n\nSetext Ueberschrift\n-------------------', + sdiff.MdParser, + ), + 'image_and_reference_link': ( + '![English alt](image.png)\n\nRead [the guide][docs].\n\n[docs]: https://example.test/docs', + '![Deutscher Alt-Text](image.png)\n\nLies [die Anleitung][docs].\n\n[docs]: https://example.test/docs', + sdiff.MdParser, + ), + 'inline_html': ('before one after', 'vor eins danach', sdiff.MdParser), + 'link_count_change': ( + 'Read [one](https://example.test/one).', + 'Lies [eins](https://example.test/one) und [zwei](https://example.test/two).', + sdiff.MdParser, + ), + 'list_variants': ( + '* one\n* two\n\n1. first\n2. second', + '* eins\n* zwei\n\n1. erste\n2. zweite', + sdiff.MdParser, + ), + 'removed_paragraph': ('one\n\ntwo', 'eins', sdiff.MdParser), + 'same_links': ( + 'A [link](https://example.test)', + 'B [translated](https://example.test)', + sdiff.MdParser, + ), + 'zendesk_callout_style': ( + '\n# Title\nbody\n', + '\n# Titel\ninhalt\n', + sdiff.ZendeskHelpMdParser, + ), + 'zendesk_steps_in_tabs': ( + '\n# Steps\n\n1. one\n2. two\n\n', + '\n# Schritte\n\n1. eins\n2. zwei\n\n', + sdiff.ZendeskHelpMdParser, + ), + 'zendesk_unstyled_callout_in_tabs': ( + '\n# Topic\n\nbody\n\n', + '\n# Thema\n\ninhalt\n\n', + sdiff.ZendeskHelpMdParser, + ), +} + + +def _serialize_errors(errors): + return [ + { + 'type': type(error).__name__, + 'symbol': error.node.symbol, + 'style': error.node.meta.get('style'), + 'message': error.message, + } + for error in errors + ] + + +def _snapshot(left, right, parser_cls): + left_tree = parser.parse(left, parser_cls=parser_cls) + right_tree = parser.parse(right, parser_cls=parser_cls) + _, _, link_errors = sdiff.diff_links(left_tree, right_tree) + left_text, right_text, errors = sdiff.diff( + left, + right, + renderer=TextRenderer(), + parser_cls=parser_cls, + ) + left_html, right_html, _ = sdiff.diff( + left, + right, + renderer=HtmlRenderer(), + parser_cls=parser_cls, + ) + return { + 'left_structure': left_tree.print_all(), + 'right_structure': right_tree.print_all(), + 'text': [left_text, right_text], + 'html': [left_html, right_html], + 'errors': _serialize_errors(errors), + 'link_errors': _serialize_errors(link_errors), + } + + +class TestGoldenCompatibility(TestCase): + maxDiff = None + + def test_parser_renderer_and_diff_outputs_match_python311_baseline(self): + expected = json.loads(GOLDEN_PATH.read_text(encoding='utf-8')) + actual = { + name: _snapshot(left, right, parser_cls) + for name, (left, right, parser_cls) in CASES.items() + } + self.assertEqual(expected, actual) From 7c78ad35a71e7aadf413c21bb179a1bab7283f02 Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Tue, 21 Jul 2026 14:35:07 -0500 Subject: [PATCH 02/20] refactor: apply Python syntax upgrades --- sdiff/model.py | 2 +- sdiff/parser.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdiff/model.py b/sdiff/model.py index 734ef8d..98499b2 100644 --- a/sdiff/model.py +++ b/sdiff/model.py @@ -138,7 +138,7 @@ def original(self, renderer): result = '' for idx, node in enumerate(self.nodes): if self.ordered: - result += '{}. {}'.format(idx, node.original(renderer)) + result += f'{idx}. {node.original(renderer)}' else: result += '* ' + node.original(renderer) result = result + '\n\n' diff --git a/sdiff/parser.py b/sdiff/parser.py index 93a4736..3d8d2e3 100644 --- a/sdiff/parser.py +++ b/sdiff/parser.py @@ -78,8 +78,8 @@ def __init__(self): self.grammar_class.block_html = re.compile( r'^\s* *(?:{}|{}|{}) *(?:\n{{1,}}|\s*$)'.format( r'', - r'<({})((?:{})*?)>([\s\S]+?)<\/\1>'.format(mistune._block_tag, mistune._valid_attr), - r'<{}(?:{})*?>'.format(mistune._block_tag, mistune._valid_attr), + fr'<({mistune._block_tag})((?:{mistune._valid_attr})*?)>([\s\S]+?)<\/\1>', + fr'<{mistune._block_tag}(?:{mistune._valid_attr})*?>', ) ) From 73a4f4b467696e211439bf7d32c2f6f439e8a182 Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Tue, 21 Jul 2026 14:36:15 -0500 Subject: [PATCH 03/20] chore: migrate packaging to pyproject --- .python-version | 2 +- pyproject.toml | 65 ++++++++++++++++++++++++++++++++++++++++++++ requirements-dev.txt | 11 ++++++-- requirements.txt | 2 +- setup.cfg | 12 -------- setup.py | 49 --------------------------------- 6 files changed, 75 insertions(+), 66 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.python-version b/.python-version index 2c07333..c70edfa 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.11 +3.11.13 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6e6d516 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,65 @@ +[build-system] +requires = ["setuptools>=82.0.1", "wheel>=0.47.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "sdiff" +version = "1.0.0" +description = "Compare Markdown documents by structure while ignoring translated content" +readme = "README.md" +requires-python = ">=3.11,<3.12" +license = "Apache-2.0" +authors = [ + { name = "Keepsafe", email = "support@getkeepsafe.com" }, +] +classifiers = [ + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", +] +dependencies = [ + "mistune==0.8.4", +] + +[project.optional-dependencies] +dev = [ + "build==1.5.0", + "coverage==7.6.1", + "flake8==7.1.1", + "flake8-pyproject==1.2.4", + "pipdeptree==2.35.2", + "pytest==9.1.1", + "pyupgrade==3.21.2", + "twine==6.2.0", +] +tests = [ + "coverage==7.6.1", + "flake8==7.1.1", + "flake8-pyproject==1.2.4", + "pytest==9.1.1", +] +devtools = [ + "build==1.5.0", + "twine==6.2.0", +] + +[project.urls] +Homepage = "https://github.com/KeepSafe/html-structure-diff/" + +[tool.setuptools.packages.find] +include = ["sdiff*"] +exclude = ["tests*"] + +[tool.flake8] +max-line-length = 120 +ignore = ["F403", "F405"] + +[tool.coverage.run] +branch = true + +[tool.coverage.report] +fail_under = 96 + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/requirements-dev.txt b/requirements-dev.txt index 86d13ec..7ed568f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,9 @@ -r requirements.txt -nose -flake8 -autopep8 \ No newline at end of file +build==1.5.0 +coverage==7.6.1 +flake8==7.1.1 +flake8-pyproject==1.2.4 +pipdeptree==2.35.2 +pytest==9.1.1 +pyupgrade==3.21.2 +twine==6.2.0 diff --git a/requirements.txt b/requirements.txt index 1f202e5..f0c8b7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -mistune==0.8.1 +mistune==0.8.4 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index ed862e5..0000000 --- a/setup.cfg +++ /dev/null @@ -1,12 +0,0 @@ -[flake8] -max-line-length = 120 -ignore = F403, F405 - -[pep8] -max-line-length = 120 - -[coverage:run] -branch = True - -[coverage:report] -fail_under = 96 diff --git a/setup.py b/setup.py deleted file mode 100644 index 8cc3c15..0000000 --- a/setup.py +++ /dev/null @@ -1,49 +0,0 @@ -import os -from setuptools import setup, find_packages - - -version = '1.0.0' - - -def read(f): - return open(os.path.join(os.path.dirname(__file__), f)).read().strip() - - -install_requires = [ - 'mistune <= 1', -] - -tests_require = [ - 'pytest >= 8', - 'coverage==7.6.1', - 'flake8==7.1.1', - 'autopep8', -] - -devtools_require = [ - 'twine', - 'build', -] - -setup(name='sdiff', - version=version, - python_requires='>=3.11', - description=('sdiff compares the structure of two markdown texts'), - classifiers=[ - 'License :: OSI Approved :: BSD License', - 'Intended Audience :: Developers', - 'Programming Language :: Python'], - author='Keepsafe', - author_email='support@getkeepsafe.com', - url='https://github.com/KeepSafe/html-structure-diff/', - license='Apache', - packages=find_packages(exclude=['tests']), - package_data={}, - namespace_packages=[], - install_requires=install_requires, - tests_require=tests_require, - extras_require={ - 'tests': tests_require, - 'devtools': devtools_require, - }, - include_package_data=False) From 23e2e6f07c1103fa775ca7ce350bdec14fea3dff Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Tue, 21 Jul 2026 14:37:02 -0500 Subject: [PATCH 04/20] chore: normalize Python 3.11 workflows --- .circleci/config.yml | 94 ++++++++++++++++++++++++++++++ Makefile | 133 +++++++++++++++++++++++++++++++------------ README.md | 31 ++++++++++ git_hooks/pre-push | 5 ++ 4 files changed, 226 insertions(+), 37 deletions(-) create mode 100644 .circleci/config.yml create mode 100755 git_hooks/pre-push diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..3678aca --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,94 @@ +version: 2 + +job_options: + - &default_job + docker: + - image: cimg/python:3.11.13 + +step_options: + - &pip_cache + key: v3-pip-{{ arch }}-{{ checksum "pyproject.toml" }} + keys: + - v3-pip-{{ arch }}-{{ checksum "pyproject.toml" }} + - v3-pip-{{ arch }} + - v3-pip- + paths: + - ~/.local + - ~/.cache + - &venv_cache + key: v3-venv-{{ arch }}-{{ checksum "pyproject.toml" }} + keys: + - v3-venv-{{ arch }}-{{ checksum "pyproject.toml" }} + - v3-venv-{{ arch }} + - v3-venv- + paths: + - /home/circleci/project/venv + +steps: + - &restore_pip_cache + restore_cache: + <<: *pip_cache + - &save_pip_cache + save_cache: + <<: *pip_cache + - &restore_venv_cache + restore_cache: + <<: *venv_cache + - &save_venv_cache + save_cache: + <<: *venv_cache + +jobs: + prepare_cache: + <<: *default_job + steps: + - checkout + - *restore_pip_cache + - *restore_venv_cache + - run: + name: Install dependencies + command: make ci-dev-install + - *save_pip_cache + - *save_venv_cache + + lint: + <<: *default_job + steps: + - checkout + - *restore_pip_cache + - *restore_venv_cache + - run: + name: Run the linter + command: make lint + - store_test_results: + path: build/test + + test: + <<: *default_job + steps: + - checkout + - *restore_pip_cache + - *restore_venv_cache + - run: + name: Run unit and golden fixture tests + command: make test-only + - store_test_results: + path: build/test + - store_artifacts: + path: build/coverage/coverage.xml + destination: coverage + - run: + name: Upload coverage to Codecov + command: bash <(curl -s https://codecov.io/bash) -t ${CODECOV_TOKEN} -f build/coverage/coverage.xml || echo "Codecov did not collect coverage reports" + +workflows: + version: 2 + test: + jobs: + - prepare_cache + - lint: + requires: + - prepare_cache + - test: + requires: + - prepare_cache diff --git a/Makefile b/Makefile index 6eeb1e2..c62eb92 100644 --- a/Makefile +++ b/Makefile @@ -1,60 +1,119 @@ -# Some simple testing tasks (sorry, UNIX only). +# Package development and verification tasks (UNIX only). -PYTHON=venv/bin/python3 +PYTHON=venv/bin/python PIP=venv/bin/pip COVERAGE=venv/bin/coverage -TEST_RUNNER=venv/bin/pytest -TEST_RUNNER_FLAGS=-s --durations=3 --durations-min=0.005 +PYTEST=venv/bin/pytest FLAKE=venv/bin/flake8 -FLAGS= +PIPDEPTREE=venv/bin/pipdeptree +PYUPGRADE=venv/bin/pyupgrade +RG=rg + +PYTHON_FILES=$(shell rg --files sdiff tests -g '*.py') +PYTEST_SHARED_FLAGS=-s --durations=3 --durations-min=0.005 +PYTEST_FLAGS=$(PYTEST_SHARED_FLAGS) +CI_COVERAGE_REPORT= + PYPICLOUD_HOST=pypicloud.getkeepsafe.local -TWINE=./venv/bin/twine +TWINE=venv/bin/twine +HOOK_PATH=$(shell git rev-parse --git-path hooks/pre-push) -update: - $(PIP) install -U pip - $(PIP) install -U . +ifdef CI +PYTEST_FLAGS += --junitxml=build/test/results.xml +CI_COVERAGE_REPORT=$(COVERAGE) xml -o build/coverage/coverage.xml +endif + +build-dir: + mkdir -p build/test build/coverage env: - test -d venv || python3 -m venv venv + test -d venv || python3.11 -m venv venv + $(PIP) install -U pip "setuptools>=82.0.1" "wheel>=0.47.0" + $(PIP) install -e . + +dev: env + $(PIP) install -e '.[dev]' -dev: env update - $(PIP) install .[tests,devtools] +update: + $(PIP) install -U . -install: env update +install: env -publish: - rm -rf dist - $(PYTHON) -m build . - $(TWINE) upload --verbose --sign --username developer --repository-url http://$(PYPICLOUD_HOST)/simple/ dist/*.whl +ci-env: + @if [ -d "venv" ] && $(PIP) --version >/dev/null 2>&1; then \ + echo "Reusing cached CI venv, no need to recreate when it has not changed"; \ + else \ + echo "No valid cached venv found, creating a fresh venv"; \ + if [ -d "venv" ]; then rm -rf venv; fi; \ + python3.11 -m venv venv; \ + $(PIP) install -U pip "setuptools>=82.0.1" "wheel>=0.47.0"; \ + fi + +ci-dev-install: ci-env + $(PIP) install -e '.[dev]' flake: $(FLAKE) sdiff tests -test: flake - $(COVERAGE) run -m pytest $(TEST_RUNNER_FLAGS) +check-msgpack: + @echo "Checking for direct msgpack imports..." + @! $(RG) -n --glob '*.py' '^(import msgpack|from msgpack)' sdiff tests \ + || (echo "ERROR: Unexpected direct msgpack import found." && exit 1) + +lint: build-dir flake check-msgpack + +test-only: build-dir + $(COVERAGE) erase + $(COVERAGE) run -m pytest $(PYTEST_FLAGS) + $(CI_COVERAGE_REPORT) -vtest: - $(COVERAGE) run -m pytest -v $(TEST_RUNNER_FLAGS) +test: lint test-only -testloop: - while sleep 1; do $(TEST_RUNNER) -s --lf $(TEST_RUNNER_FLAGS); done +vtest vtests: build-dir + $(COVERAGE) erase + $(COVERAGE) run -m pytest -v $(PYTEST_FLAGS) + $(CI_COVERAGE_REPORT) -cov cover coverage: +fixture-smoke: + $(PYTEST) -q tests/test_golden_compatibility.py tests/test_sdiff.py + +import-smoke: + $(PYTHON) -c 'import importlib.metadata as m; import sdiff; from sdiff import MdParser, ZendeskHelpMdParser, diff, diff_links, diff_struct, renderer; print(m.version("sdiff"), MdParser.__name__, ZendeskHelpMdParser.__name__, renderer.TextRenderer.__name__)' + +smoke: fixture-smoke import-smoke + +depcheck: + $(PIP) check + $(PIPDEPTREE) --warn fail + +pyupgrade: + $(PYUPGRADE) --py311-plus --keep-percent-format $(PYTHON_FILES) + +coverage: $(COVERAGE) report -m +cov cover: + $(COVERAGE) html --directory coverage + @echo "Coverage HTML written to coverage/index.html" + +package: + $(PYTHON) -m build + +publish: package + $(TWINE) upload --verbose --sign --username developer --repository-url http://$(PYPICLOUD_HOST)/simple/ dist/*.whl + +hooks: + cp git_hooks/pre-push $(HOOK_PATH) + chmod +x $(HOOK_PATH) + +unhooks: + rm -f $(HOOK_PATH) + clean: - rm -rf `find . -name __pycache__` - rm -f `find . -type f -name '*.py[co]' ` - rm -f `find . -type f -name '*~' ` - rm -f `find . -type f -name '.*~' ` - rm -f `find . -type f -name '@*' ` - rm -f `find . -type f -name '#*#' ` - rm -f `find . -type f -name '*.orig' ` - rm -f `find . -type f -name '*.rej' ` + find . -name __pycache__ -type d -prune -exec rm -rf {} + + find . -type f \( -name '*.py[co]' -o -name '*~' -o -name '.*~' -o -name '*.orig' -o -name '*.rej' \) -delete rm -f .coverage - rm -rf coverage - rm -rf build - rm -rf venv - + rm -rf build coverage dist sdiff.egg-info venv -.PHONY: all build env linux run pep test vtest testloop cov clean +.PHONY: build-dir check-msgpack ci-dev-install ci-env clean cov cover coverage depcheck dev env fixture-smoke \ + flake hooks import-smoke install lint package publish pyupgrade smoke test test-only unhooks update vtest vtests diff --git a/README.md b/README.md index b8bb2a8..3bffa58 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,33 @@ # md-sdiff + Diffs to markdown texts only based on their structure. Ignores content. Helpful to diff 2 files that contain the same content in different languages. + +## Python API + +```python +import sdiff +from sdiff import ZendeskHelpMdParser + +left, right, errors = sdiff.diff( + '# English title', + '# Deutscher Titel', + parser_cls=ZendeskHelpMdParser, +) +``` + +The package intentionally has no command-line or service entry point. Public behavior is exposed through +`sdiff.diff`, the parser classes, renderer classes, and the model/error objects returned by comparisons. + +## Python 3.11 development + +```sh +make dev +make lint +make test +make fixture-smoke +make depcheck +``` + +Exact parser, renderer, and diff behavior is protected by +`tests/fixtures/golden/python311_compatibility.json`. Migration scope and proof results are recorded in +`docs/python311-migration-contract.md`. diff --git a/git_hooks/pre-push b/git_hooks/pre-push new file mode 100755 index 0000000..04ed983 --- /dev/null +++ b/git_hooks/pre-push @@ -0,0 +1,5 @@ +#!/bin/sh +set -e + +make lint +make test From 5519cd741598204fdcaf7c75f5493f503471ac6c Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Tue, 21 Jul 2026 14:40:19 -0500 Subject: [PATCH 05/20] ci(item 1): migrate CircleCI config to 2.1 --- .circleci/config.yml | 85 +++++++++++++++++++------------------------- Makefile | 9 +++-- 2 files changed, 42 insertions(+), 52 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3678aca..ab714e8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,62 +1,51 @@ -version: 2 +version: 2.1 -job_options: - - &default_job - docker: - - image: cimg/python:3.11.13 +executors: + python311: + docker: + - image: cimg/python:3.11.13 -step_options: - - &pip_cache - key: v3-pip-{{ arch }}-{{ checksum "pyproject.toml" }} - keys: - - v3-pip-{{ arch }}-{{ checksum "pyproject.toml" }} - - v3-pip-{{ arch }} - - v3-pip- - paths: - - ~/.local - - ~/.cache - - &venv_cache - key: v3-venv-{{ arch }}-{{ checksum "pyproject.toml" }} - keys: - - v3-venv-{{ arch }}-{{ checksum "pyproject.toml" }} - - v3-venv-{{ arch }} - - v3-venv- - paths: - - /home/circleci/project/venv - -steps: - - &restore_pip_cache - restore_cache: - <<: *pip_cache - - &save_pip_cache - save_cache: - <<: *pip_cache - - &restore_venv_cache - restore_cache: - <<: *venv_cache - - &save_venv_cache - save_cache: - <<: *venv_cache +commands: + restore_dependency_caches: + steps: + - restore_cache: + keys: + - v3-pip-{{ arch }}-{{ checksum "pyproject.toml" }} + - v3-pip-{{ arch }} + - v3-pip- + - restore_cache: + keys: + - v3-venv-{{ arch }}-{{ checksum "pyproject.toml" }} + - v3-venv-{{ arch }} + - v3-venv- + save_dependency_caches: + steps: + - save_cache: + key: v3-pip-{{ arch }}-{{ checksum "pyproject.toml" }} + paths: + - ~/.local + - ~/.cache + - save_cache: + key: v3-venv-{{ arch }}-{{ checksum "pyproject.toml" }} + paths: + - /home/circleci/project/venv jobs: prepare_cache: - <<: *default_job + executor: python311 steps: - checkout - - *restore_pip_cache - - *restore_venv_cache + - restore_dependency_caches - run: name: Install dependencies command: make ci-dev-install - - *save_pip_cache - - *save_venv_cache + - save_dependency_caches lint: - <<: *default_job + executor: python311 steps: - checkout - - *restore_pip_cache - - *restore_venv_cache + - restore_dependency_caches - run: name: Run the linter command: make lint @@ -64,11 +53,10 @@ jobs: path: build/test test: - <<: *default_job + executor: python311 steps: - checkout - - *restore_pip_cache - - *restore_venv_cache + - restore_dependency_caches - run: name: Run unit and golden fixture tests command: make test-only @@ -82,7 +70,6 @@ jobs: command: bash <(curl -s https://codecov.io/bash) -t ${CODECOV_TOKEN} -f build/coverage/coverage.xml || echo "Codecov did not collect coverage reports" workflows: - version: 2 test: jobs: - prepare_cache diff --git a/Makefile b/Makefile index c62eb92..7069f00 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ # Package development and verification tasks (UNIX only). +PYTHON_VERSION=3.11.13 +BOOTSTRAP_PYTHON?=python3.11 PYTHON=venv/bin/python PIP=venv/bin/pip COVERAGE=venv/bin/coverage @@ -27,7 +29,7 @@ build-dir: mkdir -p build/test build/coverage env: - test -d venv || python3.11 -m venv venv + test -d venv || $(BOOTSTRAP_PYTHON) -m venv venv $(PIP) install -U pip "setuptools>=82.0.1" "wheel>=0.47.0" $(PIP) install -e . @@ -40,12 +42,13 @@ update: install: env ci-env: - @if [ -d "venv" ] && $(PIP) --version >/dev/null 2>&1; then \ + @if [ -d "venv" ] && $(PIP) --version >/dev/null 2>&1 \ + && $(PYTHON) -c 'import platform, sys; sys.exit(platform.python_version() != "$(PYTHON_VERSION)")'; then \ echo "Reusing cached CI venv, no need to recreate when it has not changed"; \ else \ echo "No valid cached venv found, creating a fresh venv"; \ if [ -d "venv" ]; then rm -rf venv; fi; \ - python3.11 -m venv venv; \ + $(BOOTSTRAP_PYTHON) -m venv venv; \ $(PIP) install -U pip "setuptools>=82.0.1" "wheel>=0.47.0"; \ fi From abfd52c1238a1f5b3c575311f1581ae3b075089c Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Tue, 21 Jul 2026 14:41:54 -0500 Subject: [PATCH 06/20] chore(item 2): remove redundant dependency tree tooling --- Makefile | 2 -- pyproject.toml | 1 - requirements-dev.txt | 1 - 3 files changed, 4 deletions(-) diff --git a/Makefile b/Makefile index 7069f00..f1e178b 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,6 @@ PIP=venv/bin/pip COVERAGE=venv/bin/coverage PYTEST=venv/bin/pytest FLAKE=venv/bin/flake8 -PIPDEPTREE=venv/bin/pipdeptree PYUPGRADE=venv/bin/pyupgrade RG=rg @@ -87,7 +86,6 @@ smoke: fixture-smoke import-smoke depcheck: $(PIP) check - $(PIPDEPTREE) --warn fail pyupgrade: $(PYUPGRADE) --py311-plus --keep-percent-format $(PYTHON_FILES) diff --git a/pyproject.toml b/pyproject.toml index 6e6d516..17a2561 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,6 @@ dev = [ "coverage==7.6.1", "flake8==7.1.1", "flake8-pyproject==1.2.4", - "pipdeptree==2.35.2", "pytest==9.1.1", "pyupgrade==3.21.2", "twine==6.2.0", diff --git a/requirements-dev.txt b/requirements-dev.txt index 7ed568f..bb68470 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,6 @@ build==1.5.0 coverage==7.6.1 flake8==7.1.1 flake8-pyproject==1.2.4 -pipdeptree==2.35.2 pytest==9.1.1 pyupgrade==3.21.2 twine==6.2.0 From 7207600f5357ae37b3fa31ccffed91f57d973a71 Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Tue, 21 Jul 2026 14:42:18 -0500 Subject: [PATCH 07/20] chore(item 3): bump package version to 1.1.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 17a2561..5c7d1b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "sdiff" -version = "1.0.0" +version = "1.1.0" description = "Compare Markdown documents by structure while ignoring translated content" readme = "README.md" requires-python = ">=3.11,<3.12" From 5b6bb3dd95d857722c4564370c10f03f1f41865b Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Tue, 21 Jul 2026 14:51:38 -0500 Subject: [PATCH 08/20] build(item 4): generate requirements with pip-compile --- Makefile | 12 ++++++--- README.md | 4 +++ pyproject.toml | 1 + requirements-dev.txt | 60 ++++++++++++++++++++++++++++++++++++++------ requirements.txt | 8 +++++- 5 files changed, 73 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index f1e178b..e5b2fba 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ PIP=venv/bin/pip COVERAGE=venv/bin/coverage PYTEST=venv/bin/pytest FLAKE=venv/bin/flake8 +PIP_COMPILE=venv/bin/pip-compile PYUPGRADE=venv/bin/pyupgrade RG=rg @@ -29,7 +30,7 @@ build-dir: env: test -d venv || $(BOOTSTRAP_PYTHON) -m venv venv - $(PIP) install -U pip "setuptools>=82.0.1" "wheel>=0.47.0" + $(PIP) install -U "pip<26" "setuptools>=82.0.1" "wheel>=0.47.0" $(PIP) install -e . dev: env @@ -48,7 +49,7 @@ ci-env: echo "No valid cached venv found, creating a fresh venv"; \ if [ -d "venv" ]; then rm -rf venv; fi; \ $(BOOTSTRAP_PYTHON) -m venv venv; \ - $(PIP) install -U pip "setuptools>=82.0.1" "wheel>=0.47.0"; \ + $(PIP) install -U "pip<26" "setuptools>=82.0.1" "wheel>=0.47.0"; \ fi ci-dev-install: ci-env @@ -87,6 +88,10 @@ smoke: fixture-smoke import-smoke depcheck: $(PIP) check +requirements: dev + $(PIP_COMPILE) --annotation-style=line --output-file=requirements.txt pyproject.toml + $(PIP_COMPILE) --annotation-style=line --output-file=requirements-dev.txt --extra=dev pyproject.toml + pyupgrade: $(PYUPGRADE) --py311-plus --keep-percent-format $(PYTHON_FILES) @@ -117,4 +122,5 @@ clean: rm -rf build coverage dist sdiff.egg-info venv .PHONY: build-dir check-msgpack ci-dev-install ci-env clean cov cover coverage depcheck dev env fixture-smoke \ - flake hooks import-smoke install lint package publish pyupgrade smoke test test-only unhooks update vtest vtests + flake hooks import-smoke install lint package publish pyupgrade requirements smoke test test-only unhooks update \ + vtest vtests diff --git a/README.md b/README.md index 3bffa58..b44bfdf 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,12 @@ make lint make test make fixture-smoke make depcheck +make requirements ``` +`make requirements` uses `pip-compile` to regenerate `requirements.txt` and `requirements-dev.txt` from +`pyproject.toml`. + Exact parser, renderer, and diff behavior is protected by `tests/fixtures/golden/python311_compatibility.json`. Migration scope and proof results are recorded in `docs/python311-migration-contract.md`. diff --git a/pyproject.toml b/pyproject.toml index 5c7d1b6..10fcd7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dev = [ "coverage==7.6.1", "flake8==7.1.1", "flake8-pyproject==1.2.4", + "pip-tools==7.5.3", "pytest==9.1.1", "pyupgrade==3.21.2", "twine==6.2.0", diff --git a/requirements-dev.txt b/requirements-dev.txt index bb68470..e004d47 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,8 +1,52 @@ --r requirements.txt -build==1.5.0 -coverage==7.6.1 -flake8==7.1.1 -flake8-pyproject==1.2.4 -pytest==9.1.1 -pyupgrade==3.21.2 -twine==6.2.0 +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --annotation-style=line --extra=dev --output-file=requirements-dev.txt pyproject.toml +# +backports-tarfile==1.2.0 # via jaraco-context +build==1.5.0 # via pip-tools, sdiff (pyproject.toml) +certifi==2026.6.17 # via requests +charset-normalizer==3.4.9 # via requests +click==8.4.2 # via pip-tools +coverage==7.6.1 # via sdiff (pyproject.toml) +docutils==0.23 # via readme-renderer +flake8==7.1.1 # via flake8-pyproject, sdiff (pyproject.toml) +flake8-pyproject==1.2.4 # via sdiff (pyproject.toml) +id==1.6.1 # via twine +idna==3.18 # via requests +importlib-metadata==9.0.0 # via keyring +iniconfig==2.3.0 # via pytest +jaraco-classes==3.4.0 # via keyring +jaraco-context==6.1.2 # via keyring +jaraco-functools==4.6.0 # via keyring +keyring==25.7.0 # via twine +markdown-it-py==4.2.0 # via rich +mccabe==0.7.0 # via flake8 +mdurl==0.1.2 # via markdown-it-py +mistune==0.8.4 # via sdiff (pyproject.toml) +more-itertools==11.1.0 # via jaraco-classes, jaraco-functools +nh3==0.3.6 # via readme-renderer +packaging==26.2 # via build, pytest, twine, wheel +pip-tools==7.5.3 # via sdiff (pyproject.toml) +pluggy==1.6.0 # via pytest +pycodestyle==2.12.1 # via flake8 +pyflakes==3.2.0 # via flake8 +pygments==2.20.0 # via pytest, readme-renderer, rich +pyproject-hooks==1.2.0 # via build, pip-tools +pytest==9.1.1 # via sdiff (pyproject.toml) +pyupgrade==3.21.2 # via sdiff (pyproject.toml) +readme-renderer==45.0 # via twine +requests==2.34.2 # via requests-toolbelt, twine +requests-toolbelt==1.0.0 # via twine +rfc3986==2.0.0 # via twine +rich==15.0.0 # via twine +tokenize-rt==6.2.0 # via pyupgrade +twine==6.2.0 # via sdiff (pyproject.toml) +urllib3==2.7.0 # via id, requests, twine +wheel==0.47.0 # via pip-tools +zipp==4.1.0 # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/requirements.txt b/requirements.txt index f0c8b7d..748a8a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,7 @@ -mistune==0.8.4 +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --annotation-style=line --output-file=requirements.txt pyproject.toml +# +mistune==0.8.4 # via sdiff (pyproject.toml) From 3f7c7cc1fb0d563e17665b20810032bb7c576069 Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Tue, 21 Jul 2026 14:54:00 -0500 Subject: [PATCH 09/20] chore(item 6): remove completed pyupgrade tooling --- Makefile | 8 +------- pyproject.toml | 1 - requirements-dev.txt | 2 -- 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Makefile b/Makefile index e5b2fba..c58d1aa 100644 --- a/Makefile +++ b/Makefile @@ -8,10 +8,8 @@ COVERAGE=venv/bin/coverage PYTEST=venv/bin/pytest FLAKE=venv/bin/flake8 PIP_COMPILE=venv/bin/pip-compile -PYUPGRADE=venv/bin/pyupgrade RG=rg -PYTHON_FILES=$(shell rg --files sdiff tests -g '*.py') PYTEST_SHARED_FLAGS=-s --durations=3 --durations-min=0.005 PYTEST_FLAGS=$(PYTEST_SHARED_FLAGS) CI_COVERAGE_REPORT= @@ -92,9 +90,6 @@ requirements: dev $(PIP_COMPILE) --annotation-style=line --output-file=requirements.txt pyproject.toml $(PIP_COMPILE) --annotation-style=line --output-file=requirements-dev.txt --extra=dev pyproject.toml -pyupgrade: - $(PYUPGRADE) --py311-plus --keep-percent-format $(PYTHON_FILES) - coverage: $(COVERAGE) report -m @@ -122,5 +117,4 @@ clean: rm -rf build coverage dist sdiff.egg-info venv .PHONY: build-dir check-msgpack ci-dev-install ci-env clean cov cover coverage depcheck dev env fixture-smoke \ - flake hooks import-smoke install lint package publish pyupgrade requirements smoke test test-only unhooks update \ - vtest vtests + flake hooks import-smoke install lint package publish requirements smoke test test-only unhooks update vtest vtests diff --git a/pyproject.toml b/pyproject.toml index 10fcd7c..5077249 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,6 @@ dev = [ "flake8-pyproject==1.2.4", "pip-tools==7.5.3", "pytest==9.1.1", - "pyupgrade==3.21.2", "twine==6.2.0", ] tests = [ diff --git a/requirements-dev.txt b/requirements-dev.txt index e004d47..912b286 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -35,13 +35,11 @@ pyflakes==3.2.0 # via flake8 pygments==2.20.0 # via pytest, readme-renderer, rich pyproject-hooks==1.2.0 # via build, pip-tools pytest==9.1.1 # via sdiff (pyproject.toml) -pyupgrade==3.21.2 # via sdiff (pyproject.toml) readme-renderer==45.0 # via twine requests==2.34.2 # via requests-toolbelt, twine requests-toolbelt==1.0.0 # via twine rfc3986==2.0.0 # via twine rich==15.0.0 # via twine -tokenize-rt==6.2.0 # via pyupgrade twine==6.2.0 # via sdiff (pyproject.toml) urllib3==2.7.0 # via id, requests, twine wheel==0.47.0 # via pip-tools From 7cac22038c90708296789197d8492d8aa16087be Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Tue, 21 Jul 2026 16:00:11 -0500 Subject: [PATCH 10/20] docs(item 8): record clean migration proof --- docs/python311-migration-contract.md | 243 +++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 docs/python311-migration-contract.md diff --git a/docs/python311-migration-contract.md b/docs/python311-migration-contract.md new file mode 100644 index 0000000..17a0904 --- /dev/null +++ b/docs/python311-migration-contract.md @@ -0,0 +1,243 @@ +# html-structure-diff Python 3.11 Migration Contract + +## Scope and Repo Shape + +`html-structure-diff` is a no-stack Python library, not a service. + +- Distribution: `sdiff` +- Import package: `sdiff` +- CLI surface: none +- Service/runtime surfaces: none; the audit found no PasteDeploy app factory, Gunicorn configuration, service INI, + health endpoint, worker, Docker runtime, backing-service dependency, or network client +- Migration lane: one lightweight `python311-upgrade` branch, not the six-PR service stack + +The `python311-service-upgrade-stack` skill is the convention source for the applicable packaging, dependency, +workflow, and verification work. Service-only tasks are explicitly excluded below. + +## Branch and Write Scope + +- Source checkout: `/Users/olmos/keepsafe/repos/html-structure-diff` +- Edit root: `/Users/olmos/keepsafe/repos/worktrees/html-structure-diff-python-upgrade` +- Base: clean `master` at `12e7782208e4b458c8c4242882fda2377d9cba6b` +- Branch: `python311-upgrade` +- First write scope: this contract and golden parser/renderer/diff compatibility proof +- Follow-on scope: packaging metadata, exact dependency pins, Makefile/CI workflow, and proof documentation + +## Public and Behavior-Sensitive Surfaces + +Public imports exercised by this migration: + +- `sdiff.diff` +- `sdiff.diff_struct` +- `sdiff.diff_links` +- `sdiff.MdParser` +- `sdiff.ZendeskHelpMdParser` +- `sdiff.renderer.TextRenderer` +- `sdiff.renderer.HtmlRenderer` +- model and error objects exposed through `sdiff.model` and diff results + +Behavior that must remain stable: + +- Mistune 0.8 Markdown tokenization and structural symbols +- translated documents with matching structure +- link structure independent of translated link text +- inline HTML escaping +- Zendesk ``, ``, and styled `` parsing/rendering +- insert/delete error ordering, node symbols, styles, and user-facing messages + +The existing Markdown fixture pairs under `tests/fixtures/same` and `tests/fixtures/different` remain the broad +classification proof. `tests/fixtures/golden/python311_compatibility.json` records exact parser, renderer, +structural-diff, and link-diff outputs for 12 compact Mistune 0.8.4 scenarios. The oracle covers block and inline +HTML, heading styles, lists, images and reference links, hard breaks and directional marks, link-count changes, +insert/delete symmetry, and nested Zendesk constructs. + +## Downstream Consumers + +Local source and requirements scans found: + +- `content-validator` directly imports `diff`, `renderer`, and `MdParser`. +- The migrated `content-validator` branch targets + `sdiff @ git+https://github.com/KeepSafe/html-structure-diff.git@1.0.0`. +- `email-service` and `translation-real-time-validaton` consume `sdiff` transitively through + `content-validator`; their legacy requirements still reference tag `0.4.1`. + +This library migration unblocks downstream requirement refresh and service proof. It does not claim those service +flows are safe until each downstream repo reruns its own integration proof with the migrated package. + +## Python and Packaging Target + +- Interpreter target: CPython 3.11, with `.python-version` normalized to `3.11.13` +- Packaging source of truth: `pyproject.toml` +- Package version: `1.1.0` +- Runtime dependencies: exact compatible pins +- Build/test/dev dependencies: exact pins in a `dev` optional dependency group +- Requirements artifacts: generated with `pip-compile` from `pyproject.toml` + +`master` already contains the prior Python 3.11 work and is tagged `1.0.0`. This compatibility-focused follow-up +therefore uses a minor release, `1.1.0`, rather than treating the already-established runtime baseline as another +major-version break. + +## Baseline Audit + +Before migration edits: + +- `.python-version` was `3.11`; local resolution in the worktree was Python 3.11.9. +- `setup.py` and `setup.cfg` were the packaging/config sources. +- Package metadata already declared `python_requires='>=3.11'` and version `1.0.0`. +- Runtime metadata allowed `mistune <= 1`; `requirements.txt` separately pinned `mistune==0.8.1`. +- The untouched dev install resolved `mistune==0.8.4`. +- The repo already used `pytest`, `coverage`, and Python 3.11 Travis. +- There was no CircleCI config on `master`, no pre-push hook, no explicit import smoke target, and no exact-output + golden fixture. +- Source history already included a pyupgrade-to-Python-3.11 pass; the final branch reran the complete ladder. + +Untouched baseline proof on 2026-07-16: + +| Command | Result | +| --- | --- | +| `make dev` | Pass; installed `sdiff==1.0.0` and `mistune==0.8.4`. | +| `make test` | Pass; 53 tests on Python 3.11.9. | +| `make coverage` | Pass; 96% total coverage. | +| `venv/bin/pip check` | Pass; no broken requirements. | +| public import smoke | Pass; `sdiff==1.0.0` and documented imports loaded. | +| `venv/bin/python -m compileall -q sdiff tests` | Pass. | + +## Task Mapping from python311-service-upgrade-stack + +| Skill task | Status | Repo mapping | +| --- | --- | --- | +| Task 1: pyproject, Python 3.11, dependency audit, pyupgrade | Applicable: complete | Replaced legacy packaging, normalized the interpreter target, hard-pinned Mistune, completed and reconfirmed every pyupgrade ladder stage, then removed the one-time tool. | +| Task 2a: formatting and Flake8 alignment | Applicable: complete | Moved Flake8 and coverage config into `pyproject.toml`, added `flake8-pyproject`, and retained the 120-character convention. | +| Task 2b: hooks, CI, Makefile, README | Applicable: complete | Normalized package-oriented Makefile targets, retained Python 3.11 Travis, added native CircleCI 2.1 package CI and a pre-push hook. Service launch targets are excluded. | +| Task 2c: mypy stabilization | Not applicable | The repo has no mypy baseline; introducing a typing program is not required to preserve this library's Python 3.11 behavior. | +| Task 3: msgpack, redis, asynctest, and nose compatibility | Not applicable | The repo uses pytest and has no msgpack, redis, aioredis, asynctest, or nose dependency/call site. | +| Task 4: asyncio/aiohttp modernization | Not applicable | No asyncio or aiohttp code exists. | +| Task 4c: async test harness modernization | Not applicable | No async tests or custom loop harness exists. | +| Task 5: Gunicorn and Docker local infrastructure | Not applicable | The audit proves library-only shape with no runtime service or local dependencies. | +| Task 6: deterministic requirements pipeline | Applicable: complete | `make requirements` uses `pip-tools==7.5.3` with `pip<26` to compile runtime and dev artifacts from `pyproject.toml`; architecture service lockfiles and Ansible artifacts do not apply. | + +## Service-Only Tasks Intentionally Skipped + +- Docker daemon preflight, Docker Compose dependencies, and runtime smoke +- Gunicorn configuration and PasteDeploy launcher wiring +- service INI, health endpoint, worker, or queue startup proof +- `ks-local-e2e` profile and production-egress guard +- local backing-service fixtures or paid-provider fakes +- `libks==1.0.0`, `LIBKS_VERSION`, and private-index handling +- architecture-specific service requirements and Ansible deployment artifacts +- service contract checker and migration gate, whose assertions require service-template files + +No external or paid service call is needed by this package. Dependency installation may use package indexes; all +behavior proof is local and fixture-backed. + +## Required Proof and Artifacts + +Required commands on the final branch: + +- `make clean` +- `make env` +- `make dev` +- `make ci-dev-install` +- `make requirements` +- the skill's full `--py36-plus` through `--py311-plus` pyupgrade ladder with `--keep-percent-format` +- `make lint` +- `make test` +- `make fixture-smoke` +- `CI=1 make test-only` +- public import/API smoke +- `make depcheck` +- `venv/bin/python -m compileall -q sdiff tests` +- `make package` +- built-wheel install and import smoke in an isolated CPython 3.11.13 venv +- `circleci config validate .circleci/config.yml` when the local CircleCI CLI can validate without a paid service +- `git diff --check` + +Expected evidence: + +- exact golden snapshot at `tests/fixtures/golden/python311_compatibility.json` +- golden compatibility test at `tests/test_golden_compatibility.py` +- coverage XML at `build/coverage/coverage.xml` in CI mode +- xUnit output at `build/test/results.xml` in CI mode +- local sdist and wheel under ignored `dist/` +- source distribution containing all Markdown and JSON compatibility fixtures +- command results in this contract and the umbrella ExecPlan + +## Dependency and Package Notes + +- Runtime metadata and `requirements.txt` now agree on `mistune==0.8.4`. The untouched editable-install baseline + already resolved 0.8.4 from the old `mistune <= 1` range, so this is a deterministic pin of proven behavior rather + than a broad parser upgrade. +- Mistune 3.3.3 is intentionally deferred to a separate, approval-gated commit. Its API/changelog audit and any + compatibility implementation will be performed against the committed golden oracle. +- Existing compatible hard pins are retained: `coverage==7.6.1` and `flake8==7.1.1`. +- The previously ranged `pytest >= 8` dependency is pinned to the proven environment's `pytest==9.1.1`. +- Added exact workflow pins: `build==1.5.0`, `flake8-pyproject==1.2.4`, `pip-tools==7.5.3`, and `twine==6.2.0`. +- `pipdeptree` was removed because `pip check` is the required integrity gate for this one-runtime-dependency + library. `pyupgrade` was removed after the official six-stage ladder was rerun with zero remaining changes. +- `make requirements` generated stable runtime/dev outputs on two consecutive runs. The compile environment uses + `pip==25.3` to satisfy the skill's `pip<26` compatibility guardrail. +- Removed unused legacy `nose` and `autopep8` entries from `requirements-dev.txt`; this repo uses pytest and does + not have an autopep8 target. +- The built wheel metadata declares `Requires-Python: >=3.11,<3.12` and `Requires-Dist: mistune==0.8.4`. +- `MANIFEST.in` narrowly includes Markdown and JSON fixture data in the sdist; wheel contents remain package-only. +- No direct or transitive `msgpack` or `libks` dependency is present. + +## Migration Result and Proof Log + +Date: 2026-07-21 + +Branch: `python311-upgrade` + +Ten reviewable commits, including this evidence record, were created on `python311-upgrade`; no pull request or push +was created. + +Completed applicable work: + +- Replaced `setup.py` and `setup.cfg` with `pyproject.toml`. +- Normalized `.python-version` and package/CI policy to Python 3.11.13. +- Bumped the package from `1.0.0` to the compatible minor release `1.1.0` and preserved public behavior. +- Hard-pinned the runtime dependency and generated deterministic requirements artifacts with `pip-compile`. +- Moved Flake8 and coverage configuration into `pyproject.toml`. +- Added package-oriented `env`, `dev`, `lint`, `test-only`, `fixture-smoke`, `import-smoke`, `depcheck`, + `requirements`, package-build, CI-install, and hook targets. +- Added native CircleCI 2.1 package jobs with reusable executors/commands for dependency preparation, lint, tests, + xUnit, and coverage XML. +- Added exact golden parser/renderer/structural-diff/link-diff snapshots for 12 behavior-sensitive scenarios. +- Ran the full skill ladder. The `--py36-plus` stage converted one list-rendering format call and two parser regex + format calls to f-strings; Python 3.7 through 3.11 stages were no-ops. Golden outputs remained unchanged. +- Review found that the first sdist omitted fixture data; `MANIFEST.in` fixed the package artifact before handoff. +- Removed `pipdeptree` and the completed one-time `pyupgrade` tool from the final developer environment. + +Proof results: + +| Command | Result | Evidence | +| --- | --- | --- | +| `make clean`; `make env`; `make dev` | Pass | Fresh venv is CPython 3.11.13 with editable `sdiff==1.1.0`, `mistune==0.8.4`, and exact dev tooling. | +| `make ci-dev-install` | Pass | CI bootstrap installed/reused the exact-version venv and final `.[dev]` dependency shape. | +| full skill pyupgrade ladder over `sdiff` and `tests` | Pass | All six stages were reconfirmed live with zero remaining changes; the earlier Python 3.6 stage changes are isolated in commit `7c78ad3`. | +| `make requirements` twice | Pass | Both pip-compile outputs were byte-stable; compile tooling is `pip-tools==7.5.3` with `pip==25.3`. | +| `make test` | Pass | Flake8 and msgpack guard passed; 54 tests passed on Python 3.11.13. | +| `make coverage` | Pass | Total branch-aware coverage remained 96%. | +| `make fixture-smoke` | Pass | 3 test methods and 12 fixture subtests passed, including the exact golden snapshot. | +| `make import-smoke` | Pass | Imported documented public API and printed `1.1.0 MdParser ZendeskHelpMdParser TextRenderer`. | +| `make depcheck` | Pass | `pip check` found no broken requirements. | +| `CI=1 make test-only` | Pass | 54 tests passed and wrote `build/test/results.xml` plus `build/coverage/coverage.xml`. | +| `venv/bin/python -m compileall -q sdiff tests` | Pass | Source and tests compiled on Python 3.11.13. | +| `make package` | Pass | Built isolated `sdiff-1.1.0.tar.gz` and `sdiff-1.1.0-py3-none-any.whl`. | +| `venv/bin/twine check dist/*` | Pass | Both distribution artifacts passed metadata/README validation. | +| sdist fixture listing | Pass | Archive contains all same/different Markdown fixtures and `python311_compatibility.json`. | +| isolated wheel install/import/diff smoke | Pass | A separate CPython 3.11.13 venv installed the wheel with `mistune==0.8.4` and ran `sdiff.diff()`. | +| CircleCI validate, `--next`, and config process | Pass | CircleCI accepted the native source `version: 2.1`, strict upcoming-compiler validation, and reusable-config expansion. | +| `make hooks`, installed-file comparison, `make unhooks` | Pass | Executable pre-push hook installed exactly and was removed after verification. | +| `git diff --check` | Pass | No whitespace errors. | + +An additional pre-final run passed all 54 tests on Python 3.11.9 before the exact 3.11.13 environment was selected. + +## Known Gaps + +- Remote CI is not exercised by default local proof. +- Downstream `content-validator`, `email-service`, and `translation-real-time-validaton` still need their own + requirements refresh and integration proof after consuming the merged package state. +- Mistune 3.3.3 research and implementation have not started; they require explicit user approval and a separate + commit after this checkpoint. +- No package release, tag, push, or pull request is part of this local checkpoint. From a73f7122421a08c380b87c38fccfe8b54d169cb9 Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Wed, 26 Aug 2026 12:37:42 -0500 Subject: [PATCH 11/20] bump version in pyproject.toml to 2.0.0 since 1.x already exists pre-py311 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5077249..f986c2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "sdiff" -version = "1.1.0" +version = "2.0.0" description = "Compare Markdown documents by structure while ignoring translated content" readme = "README.md" requires-python = ">=3.11,<3.12" From ebb7109261cdddcf72f5ae9907501236ef067767 Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Wed, 26 Aug 2026 15:16:47 -0500 Subject: [PATCH 12/20] Upgrade Mistune to 3.3.4 with compatibility fixtures --- CHANGELOG | 9 + MANIFEST.in | 3 +- Makefile | 18 +- README.md | 43 +- docs/python311-migration-contract.md | 106 +- pyproject.toml | 2 +- requirements-dev.txt | 2 +- requirements.txt | 2 +- scripts/mistune_compat_cases.py | 56 + scripts/mistune_compat_reporter.py | 70 ++ scripts/mistune_compat_signatures.py | 239 ++++ scripts/run_mistune_compat.py | 224 ++++ sdiff/parser.py | 496 ++++++-- .../golden_mistune_084_fixtures.json | 1100 +++++++++++++++++ .../fixtures/compatibility/mistune_cases.json | 363 ++++++ tests/test_mistune_compatibility.py | 234 ++++ 16 files changed, 2796 insertions(+), 171 deletions(-) create mode 100644 scripts/mistune_compat_cases.py create mode 100644 scripts/mistune_compat_reporter.py create mode 100644 scripts/mistune_compat_signatures.py create mode 100644 scripts/run_mistune_compat.py create mode 100644 tests/fixtures/compatibility/golden_mistune_084_fixtures.json create mode 100644 tests/fixtures/compatibility/mistune_cases.json create mode 100644 tests/test_mistune_compatibility.py diff --git a/CHANGELOG b/CHANGELOG index fc0b2a9..3dad7c3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,12 @@ +2.0.0 (unreleased) +------------------ + +- Require Python 3.11. +- Upgrade Mistune from 0.8.4 to 3.3.4 behind a compatibility parser that preserves the existing sdiff tree, + rendering, diff, HTML, list, link/image, and Zendesk behavior. +- Add self-contained Golden Mistune 0.8.4 Fixtures, a dual-version compatibility harness, + deterministic fuzz cases, and linear-time malformed-link regression coverage. + 0.2.0 (2018-08-21) ------------------ diff --git a/MANIFEST.in b/MANIFEST.in index 10b4fbc..4db4a67 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ -recursive-include tests/fixtures *.json *.md +recursive-include scripts *.py +recursive-include tests *.json *.md *.py diff --git a/Makefile b/Makefile index c58d1aa..3aa31b7 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,8 @@ PYTEST=venv/bin/pytest FLAKE=venv/bin/flake8 PIP_COMPILE=venv/bin/pip-compile RG=rg +MISTUNE_ORACLE_REV?=a73f7122421a08c380b87c38fccfe8b54d169cb9 +MISTUNE_GOLDEN_FIXTURES=tests/fixtures/compatibility/golden_mistune_084_fixtures.json PYTEST_SHARED_FLAGS=-s --durations=3 --durations-min=0.005 PYTEST_FLAGS=$(PYTEST_SHARED_FLAGS) @@ -32,6 +34,7 @@ env: $(PIP) install -e . dev: env + $(PIP) install -r requirements-dev.txt $(PIP) install -e '.[dev]' update: @@ -54,7 +57,7 @@ ci-dev-install: ci-env $(PIP) install -e '.[dev]' flake: - $(FLAKE) sdiff tests + $(FLAKE) sdiff tests scripts check-msgpack: @echo "Checking for direct msgpack imports..." @@ -86,6 +89,15 @@ smoke: fixture-smoke import-smoke depcheck: $(PIP) check +mistune-compat: + $(PYTHON) scripts/run_mistune_compat.py --oracle-revision "$(MISTUNE_ORACLE_REV)" \ + --bootstrap-python "$(BOOTSTRAP_PYTHON)" --target . + +mistune-compat-refresh: + $(PYTHON) scripts/run_mistune_compat.py --oracle-revision "$(MISTUNE_ORACLE_REV)" \ + --bootstrap-python "$(BOOTSTRAP_PYTHON)" --target . \ + --write-golden-fixtures $(MISTUNE_GOLDEN_FIXTURES) + requirements: dev $(PIP_COMPILE) --annotation-style=line --output-file=requirements.txt pyproject.toml $(PIP_COMPILE) --annotation-style=line --output-file=requirements-dev.txt --extra=dev pyproject.toml @@ -117,4 +129,6 @@ clean: rm -rf build coverage dist sdiff.egg-info venv .PHONY: build-dir check-msgpack ci-dev-install ci-env clean cov cover coverage depcheck dev env fixture-smoke \ - flake hooks import-smoke install lint package publish requirements smoke test test-only unhooks update vtest vtests + flake hooks import-smoke install lint mistune-compat mistune-compat-refresh package publish requirements smoke \ + test test-only unhooks update \ + vtest vtests diff --git a/README.md b/README.md index b44bfdf..1a22f93 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ make dev make lint make test make fixture-smoke +make mistune-compat make depcheck make requirements ``` @@ -32,6 +33,46 @@ make requirements `make requirements` uses `pip-compile` to regenerate `requirements.txt` and `requirements-dev.txt` from `pyproject.toml`. +### Golden Mistune 0.8.4 Fixtures + +Normal development and `make test` do not need an oracle checkout. The regular test suite checks every curated, +golden, fixture, exhaustive-matrix, and deterministic-fuzz result against the committed Golden Mistune 0.8.4 +Fixtures. + +For a live two-version comparison, prepare the target with `make dev` and run: + +```sh +make mistune-compat +``` + +The Make target always starts fresh. It removes any stale oracle state at +`/tmp/html-structure-diff-mistune-084-oracle`, creates a detached pre-port checkout there, builds an isolated Python +3.11 environment with Mistune 0.8.4, runs the comparison, and removes the oracle again. No reusable oracle setup is +required, and a partially cleaned `/tmp` directory is never trusted. + +Success ends with: + +```text +Mistune 0.8.4 oracle vs 3.3.4 target: 1086 cases, 0 mismatches +``` + +The command runs each checkout with its own `venv`, compares normalized parser, renderer, and diff results, and exits +nonzero with unified result diffs when behavior differs. + +Only refresh the committed Golden Mistune 0.8.4 Fixtures after deliberately changing and reviewing the compatibility +corpus. The refresh is generated from the 0.8.4 worktree, not from the target implementation, and refuses any other +oracle Mistune version: + +```sh +make mistune-compat-refresh +git diff -- tests/fixtures/compatibility/golden_mistune_084_fixtures.json +make test +``` + +Do not refresh the golden fixtures merely to hide a mismatch; investigate and either restore parity or document an +explicitly approved behavior change first. + Exact parser, renderer, and diff behavior is protected by -`tests/fixtures/golden/python311_compatibility.json`. Migration scope and proof results are recorded in +`tests/fixtures/golden/python311_compatibility.json` plus the Mistune compatibility corpus under +`tests/fixtures/compatibility`. Migration scope and proof results are recorded in `docs/python311-migration-contract.md`. diff --git a/docs/python311-migration-contract.md b/docs/python311-migration-contract.md index 17a0904..108c408 100644 --- a/docs/python311-migration-contract.md +++ b/docs/python311-migration-contract.md @@ -36,7 +36,7 @@ Public imports exercised by this migration: - `sdiff.renderer.HtmlRenderer` - model and error objects exposed through `sdiff.model` and diff results -Behavior that must remain stable: +Behavior that must remain stable across the Mistune 0.8.4-to-3.3.4 adapter: - Mistune 0.8 Markdown tokenization and structural symbols - translated documents with matching structure @@ -47,17 +47,30 @@ Behavior that must remain stable: The existing Markdown fixture pairs under `tests/fixtures/same` and `tests/fixtures/different` remain the broad classification proof. `tests/fixtures/golden/python311_compatibility.json` records exact parser, renderer, -structural-diff, and link-diff outputs for 12 compact Mistune 0.8.4 scenarios. The oracle covers block and inline +structural-diff, and link-diff outputs for 13 compact Mistune 0.8.4 scenarios. The oracle covers block and inline HTML, heading styles, lists, images and reference links, hard breaks and directional marks, link-count changes, insert/delete symmetry, and nested Zendesk constructs. +`scripts/run_mistune_compat.py` runs the frozen 0.8.4 worktree and this 3.3.4 worktree in isolated virtual +environments. The runner expands 61 committed corpus entries, all 13 golden cases, all 12 automatically discovered +Markdown fixture pairs, and 1,000 fixed-seed structured fuzz pairs for 1,086 named comparisons. Two aggregate corpus +entries exhaustively check another 98,334 short link/image label and destination combinations. The runner compares +recursive ASTs, exact text/HTML rendering, structural and link diff results, mutated metadata, and phase-specific +exception signatures. + +Normal `make test` recomputes every target result against the Golden Mistune 0.8.4 Fixtures at +`tests/fixtures/compatibility/golden_mistune_084_fixtures.json`. This compact expected-results file is generated only +from the frozen 0.8.4 implementation, so the normal suite retains the complete parity proof without requiring another +worktree. `make mistune-compat` provides readable full-result diffs, and `make mistune-compat-refresh` deliberately +refreshes the golden fixtures after an approved corpus change. + ## Downstream Consumers Local source and requirements scans found: - `content-validator` directly imports `diff`, `renderer`, and `MdParser`. -- The migrated `content-validator` branch targets - `sdiff @ git+https://github.com/KeepSafe/html-structure-diff.git@1.0.0`. +- The migrated `content-validator` branch currently targets immutable commit `7cac220`; it must move to the final + reviewed 2.x tag/commit to receive Mistune 3.3.4. - `email-service` and `translation-real-time-validaton` consume `sdiff` transitively through `content-validator`; their legacy requirements still reference tag `0.4.1`. @@ -68,14 +81,13 @@ flows are safe until each downstream repo reruns its own integration proof with - Interpreter target: CPython 3.11, with `.python-version` normalized to `3.11.13` - Packaging source of truth: `pyproject.toml` -- Package version: `1.1.0` +- Package version: `2.0.0` - Runtime dependencies: exact compatible pins - Build/test/dev dependencies: exact pins in a `dev` optional dependency group - Requirements artifacts: generated with `pip-compile` from `pyproject.toml` -`master` already contains the prior Python 3.11 work and is tagged `1.0.0`. This compatibility-focused follow-up -therefore uses a minor release, `1.1.0`, rather than treating the already-established runtime baseline as another -major-version break. +The historical repository already uses 1.x tags. The Python 3.11 migration branch therefore starts a 2.x release +line. Only downstream consumers that have migrated to Python 3.11 should move to those tags. ## Baseline Audit @@ -143,6 +155,7 @@ Required commands on the final branch: - `make lint` - `make test` - `make fixture-smoke` +- `make mistune-compat` - `CI=1 make test-only` - public import/API smoke - `make depcheck` @@ -156,19 +169,31 @@ Expected evidence: - exact golden snapshot at `tests/fixtures/golden/python311_compatibility.json` - golden compatibility test at `tests/test_golden_compatibility.py` +- target-side parity/regression tests at `tests/test_mistune_compatibility.py` +- dual-version reporter and runner under `scripts/` +- curated/fuzz corpus configuration at `tests/fixtures/compatibility/mistune_cases.json` +- Golden Mistune 0.8.4 Fixtures at + `tests/fixtures/compatibility/golden_mistune_084_fixtures.json` - coverage XML at `build/coverage/coverage.xml` in CI mode - xUnit output at `build/test/results.xml` in CI mode - local sdist and wheel under ignored `dist/` -- source distribution containing all Markdown and JSON compatibility fixtures +- source distribution containing the full test suite, compatibility helpers, and Markdown/JSON fixtures - command results in this contract and the umbrella ExecPlan ## Dependency and Package Notes -- Runtime metadata and `requirements.txt` now agree on `mistune==0.8.4`. The untouched editable-install baseline - already resolved 0.8.4 from the old `mistune <= 1` range, so this is a deterministic pin of proven behavior rather - than a broad parser upgrade. -- Mistune 3.3.3 is intentionally deferred to a separate, approval-gated commit. Its API/changelog audit and any - compatibility implementation will be performed against the committed golden oracle. +- Runtime metadata and both generated requirement files now agree on `mistune==3.3.4`. +- Mistune 3 removed the `BlockLexer`, `InlineLexer`, and private grammar constants used by the old implementation. + The adapter owns the intentionally narrow sdiff block/list/text grammar locally, uses Mistune 3's hardened inline + link parser surface, and emits the existing sdiff model directly. +- The adapter preserves smart entity escaping, literal unsupported Markdown, raw link/image source, unresolved + reference links, legacy list shapes, inline-vs-block HTML classification, and recursive Zendesk tags. +- The adapter honors direct `InlineLexer.parse(..., rules=...)` rule ordering and reused-parser token-list identity, + matching the exposed 0.8.4 facade behavior. +- The legacy nested-label and destination grammar is compiled into a linear-time index. This preserves 0.8.4's + unusual greedy/backtracking boundaries while eliminating repeated suffix scans on malformed nested openers. +- The port removes the class-level Zendesk rule mutation that could make a later plain `MdParser` call fail. This is + an intentional target-only defect fix; document outputs remain oracle-identical. - Existing compatible hard pins are retained: `coverage==7.6.1` and `flake8==7.1.1`. - The previously ranged `pytest >= 8` dependency is pinned to the proven environment's `pytest==9.1.1`. - Added exact workflow pins: `build==1.5.0`, `flake8-pyproject==1.2.4`, `pip-tools==7.5.3`, and `twine==6.2.0`. @@ -178,66 +203,77 @@ Expected evidence: `pip==25.3` to satisfy the skill's `pip<26` compatibility guardrail. - Removed unused legacy `nose` and `autopep8` entries from `requirements-dev.txt`; this repo uses pytest and does not have an autopep8 target. -- The built wheel metadata declares `Requires-Python: >=3.11,<3.12` and `Requires-Dist: mistune==0.8.4`. -- `MANIFEST.in` narrowly includes Markdown and JSON fixture data in the sdist; wheel contents remain package-only. +- The built wheel metadata declares `Requires-Python: >=3.11,<3.12` and `Requires-Dist: mistune==3.3.4`. +- `MANIFEST.in` includes the test modules, compatibility helper scripts, and Markdown/JSON fixture data needed to run + the complete suite from an extracted sdist; wheel contents remain package-only. - No direct or transitive `msgpack` or `libks` dependency is present. ## Migration Result and Proof Log -Date: 2026-07-21 +Date: 2026-08-26 Branch: `python311-upgrade` -Ten reviewable commits, including this evidence record, were created on `python311-upgrade`; no pull request or push -was created. +The Python migration commits and draft pull request already exist on `python311-upgrade`. The Mistune 3.3.4 port is +being prepared as one additional reviewable commit; all changes remain uncommitted until explicit user approval. Completed applicable work: - Replaced `setup.py` and `setup.cfg` with `pyproject.toml`. - Normalized `.python-version` and package/CI policy to Python 3.11.13. -- Bumped the package from `1.0.0` to the compatible minor release `1.1.0` and preserved public behavior. +- Started the Python 3.11-only 2.x release line and preserved public behavior. - Hard-pinned the runtime dependency and generated deterministic requirements artifacts with `pip-compile`. - Moved Flake8 and coverage configuration into `pyproject.toml`. - Added package-oriented `env`, `dev`, `lint`, `test-only`, `fixture-smoke`, `import-smoke`, `depcheck`, `requirements`, package-build, CI-install, and hook targets. - Added native CircleCI 2.1 package jobs with reusable executors/commands for dependency preparation, lint, tests, xUnit, and coverage XML. -- Added exact golden parser/renderer/structural-diff/link-diff snapshots for 12 behavior-sensitive scenarios. +- Added exact golden parser/renderer/structural-diff/link-diff snapshots for 13 behavior-sensitive scenarios. +- Ported the parser from removed Mistune 0.8 lexer APIs to a Mistune 3.3.4 compatibility adapter. +- Added a dual-worktree exact-signature harness, self-contained Golden Mistune 0.8.4 Fixtures, expanded + curated/fuzz/exhaustive link matrices, and target-side parity unit tests. +- Fixed a review-discovered `InlineLexer` explicit-rule mismatch and replaced the first nested-label scanner after + exhaustive testing exposed Mistune 0.8 greedy-label edge cases. - Ran the full skill ladder. The `--py36-plus` stage converted one list-rendering format call and two parser regex format calls to f-strings; Python 3.7 through 3.11 stages were no-ops. Golden outputs remained unchanged. -- Review found that the first sdist omitted fixture data; `MANIFEST.in` fixed the package artifact before handoff. +- Review found that the first sdist omitted fixture data, and the expanded parity suite exposed missing test helper + modules in the archive. `MANIFEST.in` now makes the complete extracted-sdist suite runnable before handoff. - Removed `pipdeptree` and the completed one-time `pyupgrade` tool from the final developer environment. Proof results: | Command | Result | Evidence | | --- | --- | --- | -| `make clean`; `make env`; `make dev` | Pass | Fresh venv is CPython 3.11.13 with editable `sdiff==1.1.0`, `mistune==0.8.4`, and exact dev tooling. | +| `make clean`; `make env`; `make dev` | Pass | Fresh venv is CPython 3.11.13 with editable `sdiff==2.0.0`, `mistune==3.3.4`, and the locked dev toolchain. | | `make ci-dev-install` | Pass | CI bootstrap installed/reused the exact-version venv and final `.[dev]` dependency shape. | | full skill pyupgrade ladder over `sdiff` and `tests` | Pass | All six stages were reconfirmed live with zero remaining changes; the earlier Python 3.6 stage changes are isolated in commit `7c78ad3`. | | `make requirements` twice | Pass | Both pip-compile outputs were byte-stable; compile tooling is `pip-tools==7.5.3` with `pip==25.3`. | -| `make test` | Pass | Flake8 and msgpack guard passed; 54 tests passed on Python 3.11.13. | -| `make coverage` | Pass | Total branch-aware coverage remained 96%. | +| `make test` | Pass | Flake8 and msgpack guard passed; 77 tests passed on Python 3.11.13, including the Golden Mistune 0.8.4 Fixtures. | +| `make coverage` | Pass | Total branch-aware coverage is 99%; `sdiff/parser.py` has 100% statement and 99% branch coverage. | | `make fixture-smoke` | Pass | 3 test methods and 12 fixture subtests passed, including the exact golden snapshot. | -| `make import-smoke` | Pass | Imported documented public API and printed `1.1.0 MdParser ZendeskHelpMdParser TextRenderer`. | +| `make mistune-compat` | Pass | Mistune 0.8.4 oracle vs 3.3.4 target: 1,086 named cases, 0 mismatches; aggregate matrix cases cover 98,334 additional link/image combinations. | +| malformed-link bounded-time regression | Pass | 16,000 nested link and image openers complete in about 0.03 seconds each; the test ceiling is 2 seconds. | +| `make import-smoke` | Pass | Imported documented public API and printed `2.0.0 MdParser ZendeskHelpMdParser TextRenderer`. | | `make depcheck` | Pass | `pip check` found no broken requirements. | -| `CI=1 make test-only` | Pass | 54 tests passed and wrote `build/test/results.xml` plus `build/coverage/coverage.xml`. | -| `venv/bin/python -m compileall -q sdiff tests` | Pass | Source and tests compiled on Python 3.11.13. | -| `make package` | Pass | Built isolated `sdiff-1.1.0.tar.gz` and `sdiff-1.1.0-py3-none-any.whl`. | +| `CI=1 make test-only` | Pass | 77 tests passed and wrote `build/test/results.xml` plus `build/coverage/coverage.xml`. | +| `venv/bin/python -m compileall -q sdiff tests scripts` | Pass | Source, tests, and compatibility scripts compiled on Python 3.11.13. | +| `make package` | Pass | Built isolated `sdiff-2.0.0.tar.gz` and `sdiff-2.0.0-py3-none-any.whl`. | | `venv/bin/twine check dist/*` | Pass | Both distribution artifacts passed metadata/README validation. | -| sdist fixture listing | Pass | Archive contains all same/different Markdown fixtures and `python311_compatibility.json`. | -| isolated wheel install/import/diff smoke | Pass | A separate CPython 3.11.13 venv installed the wheel with `mistune==0.8.4` and ran `sdiff.diff()`. | +| sdist content listing | Pass | Archive contains all tests, compatibility helpers, same/different Markdown fixtures, golden JSON, and Golden Mistune 0.8.4 Fixtures. | +| extracted-sdist test suite | Pass | All 77 tests and 1,120 subtests passed directly from the unpacked source archive. | +| isolated wheel install/import/diff smoke | Pass | A separate CPython 3.11.13 venv installed `sdiff==2.0.0` with `mistune==3.3.4` and ran `sdiff.diff()`. | | CircleCI validate, `--next`, and config process | Pass | CircleCI accepted the native source `version: 2.1`, strict upcoming-compiler validation, and reusable-config expansion. | +| downstream `content-validator` test suite | Pass | 65 tests passed with one expected skip against the local target and Mistune 3.3.4; HTTP was mocked. | | `make hooks`, installed-file comparison, `make unhooks` | Pass | Executable pre-push hook installed exactly and was removed after verification. | | `git diff --check` | Pass | No whitespace errors. | -An additional pre-final run passed all 54 tests on Python 3.11.9 before the exact 3.11.13 environment was selected. +An additional clean run passed all 71 tests on Python 3.11.9 before the exact 3.11.13 environment was selected. ## Known Gaps - Remote CI is not exercised by default local proof. - Downstream `content-validator`, `email-service`, and `translation-real-time-validaton` still need their own requirements refresh and integration proof after consuming the merged package state. -- Mistune 3.3.3 research and implementation have not started; they require explicit user approval and a separate - commit after this checkpoint. -- No package release, tag, push, or pull request is part of this local checkpoint. +- No 2.x package tag or release has been created. Downstreams must pin the final reviewed immutable tag/commit, + never the moving branch. +- The Mistune change has not been committed or pushed; review approval is still required. diff --git a/pyproject.toml b/pyproject.toml index f986c2c..0da9e68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] dependencies = [ - "mistune==0.8.4", + "mistune==3.3.4", ] [project.optional-dependencies] diff --git a/requirements-dev.txt b/requirements-dev.txt index 912b286..69c4498 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -24,7 +24,7 @@ keyring==25.7.0 # via twine markdown-it-py==4.2.0 # via rich mccabe==0.7.0 # via flake8 mdurl==0.1.2 # via markdown-it-py -mistune==0.8.4 # via sdiff (pyproject.toml) +mistune==3.3.4 # via sdiff (pyproject.toml) more-itertools==11.1.0 # via jaraco-classes, jaraco-functools nh3==0.3.6 # via readme-renderer packaging==26.2 # via build, pytest, twine, wheel diff --git a/requirements.txt b/requirements.txt index 748a8a3..a885504 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,4 @@ # # pip-compile --annotation-style=line --output-file=requirements.txt pyproject.toml # -mistune==0.8.4 # via sdiff (pyproject.toml) +mistune==3.3.4 # via sdiff (pyproject.toml) diff --git a/scripts/mistune_compat_cases.py b/scripts/mistune_compat_cases.py new file mode 100644 index 0000000..bb3d07c --- /dev/null +++ b/scripts/mistune_compat_cases.py @@ -0,0 +1,56 @@ +"""Expand the committed Mistune compatibility corpus deterministically.""" + +import json +import random +import runpy + + +SEED = 311_084_334 +FUZZ_CASES = 200 +FUZZ_FRAGMENTS = ( + '', 'plain', ' ', '\n', '\n\n', ' \n', '# heading', '###heading', + 'setext\n---', '* item', '1. item', '[label](url)', '![alt](image.png)', + '[label][ref]', '[ref]: https://example.test', 'x', '', + '*em*', '**strong**', '`code`', '---', '>', '&', '&', '\u200e', '\u200f', + '', '', '', '', '\\[escaped]', '(', ')', +) + + +def expand_cases(corpus_path, repo): + corpus = json.loads(corpus_path.read_text(encoding='utf-8')) + cases = [] + for case in corpus['cases']: + cases.append(dict(case)) + + golden_namespace = runpy.run_path(str(repo / 'tests/test_golden_compatibility.py')) + for name, (left, right, parser_cls) in golden_namespace['CASES'].items(): + cases.append({ + 'name': f'golden_{name}', + 'parser': parser_cls.__name__, + 'left': left, + 'right': right, + }) + + for english_path in sorted((repo / 'tests/fixtures').glob('*/*.en.md')): + german_path = english_path.with_name(english_path.name.replace('.en.md', '.de.md')) + relative = english_path.relative_to(repo) + cases.append({ + 'name': 'fixture_' + str(relative).replace('/', '_').replace('.en.md', ''), + 'parser': 'ZendeskHelpMdParser' if 'zendesk' in english_path.name else 'MdParser', + 'left': english_path.read_text(encoding='utf-8'), + 'right': german_path.read_text(encoding='utf-8'), + }) + + rng = random.Random(corpus.get('fuzz_seed', SEED)) + fuzz_count = corpus.get('fuzz_cases', FUZZ_CASES) + for index in range(fuzz_count): + fragment_count = rng.randint(1, 7) + left = ''.join(rng.choice(FUZZ_FRAGMENTS) for _ in range(fragment_count)) + right = ''.join(rng.choice(FUZZ_FRAGMENTS) for _ in range(fragment_count)) + cases.append({ + 'name': f'fuzz_{index:04d}', + 'parser': rng.choice(('MdParser', 'ZendeskHelpMdParser')), + 'left': left, + 'right': right, + }) + return cases diff --git a/scripts/mistune_compat_reporter.py b/scripts/mistune_compat_reporter.py new file mode 100644 index 0000000..d216012 --- /dev/null +++ b/scripts/mistune_compat_reporter.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Emit deterministic sdiff behavior signatures for one Python environment.""" + +import argparse +import importlib.metadata +import json +import sys +from pathlib import Path + +from mistune_compat_signatures import run_case + + +def _load_sdiff(repo: Path): + sys.path.insert(0, str(repo)) + import mistune + import sdiff + from sdiff import parser + from sdiff.renderer import HtmlRenderer, TextRenderer + + return mistune, sdiff, parser, HtmlRenderer, TextRenderer + + +def _run_isolated_cases(cases, sdiff, parser, HtmlRenderer, TextRenderer): + """Keep the Mistune 0.8 Zendesk class-rule leak outside corpus cases. + + The target intentionally fixes that process-global mutation and covers it + with a target-only regression. Resetting the oracle class list here makes + every differential corpus case represent an independent public API call. + """ + default_rules = list(sdiff.MdParser.default_rules) + results = {} + for case in cases: + sdiff.MdParser.default_rules[:] = default_rules + try: + results[case['name']] = run_case( + case, + sdiff, + parser, + HtmlRenderer, + TextRenderer, + ) + finally: + sdiff.MdParser.default_rules[:] = default_rules + return results + + +def main(): + argument_parser = argparse.ArgumentParser() + argument_parser.add_argument('--repo', type=Path, required=True) + argument_parser.add_argument('--cases', type=Path, required=True) + args = argument_parser.parse_args() + + repo = args.repo.resolve() + mistune, sdiff, parser, HtmlRenderer, TextRenderer = _load_sdiff(repo) + cases = json.loads(args.cases.read_text(encoding='utf-8')) + results = _run_isolated_cases(cases, sdiff, parser, HtmlRenderer, TextRenderer) + report = { + 'environment': { + 'python': '.'.join(map(str, sys.version_info[:3])), + 'sdiff': importlib.metadata.version('sdiff'), + 'mistune': importlib.metadata.version('mistune'), + }, + 'cases': results, + } + json.dump(report, sys.stdout, ensure_ascii=False, indent=2, sort_keys=True) + sys.stdout.write('\n') + + +if __name__ == '__main__': + main() diff --git a/scripts/mistune_compat_signatures.py b/scripts/mistune_compat_signatures.py new file mode 100644 index 0000000..deccab7 --- /dev/null +++ b/scripts/mistune_compat_signatures.py @@ -0,0 +1,239 @@ +"""Build deterministic, exact sdiff behavior signatures. + +The signature records both parsed trees, rendered output, diff output, and +exception details. Canonical SHA-256 digests make the complete oracle result +small enough to run as part of the normal test suite. +""" + +import hashlib +import itertools +import json + + +def node_signature(node): + if isinstance(node, dict): + return { + 'type': 'non-node-dict', + 'value': node, + } + signature = { + 'type': type(node).__name__, + 'name': node.name, + 'symbol': node.symbol, + 'string': str(node), + 'meta': dict(sorted(node.meta.items())), + 'nodes': [node_signature(child) for child in node.nodes], + } + for attribute in ('level', 'ordered', 'style', 'text'): + if hasattr(node, attribute): + signature[attribute] = getattr(node, attribute) + return signature + + +def error_signature(error): + return { + 'type': type(error).__name__, + 'message': error.message, + 'node': node_signature(error.node), + } + + +def capture(function): + try: + return { + 'status': 'ok', + 'value': function(), + } + except Exception as error: # pragma: no cover - exercised through signatures + return { + 'status': 'error', + 'error_type': type(error).__name__, + 'error_message': str(error), + } + + +def parse_signature(source, parser_cls, parser): + tree = parser.parse(source, parser_cls=parser_cls) + return { + 'tree': node_signature(tree), + 'structure': tree.print_all(), + } + + +def render_signature(source, parser_cls, parser, renderer): + tree = parser.parse(source, parser_cls=parser_cls) + return renderer.render(tree) + + +def diff_signature(left, right, parser_cls, sdiff, renderer): + left_output, right_output, errors = sdiff.diff( + left, + right, + renderer=renderer, + parser_cls=parser_cls, + ) + return { + 'left': left_output, + 'right': right_output, + 'errors': [error_signature(error) for error in errors], + } + + +def tree_diff_signature(left, right, parser_cls, parser, diff_function): + left_tree = parser.parse(left, parser_cls=parser_cls) + right_tree = parser.parse(right, parser_cls=parser_cls) + left_tree, right_tree, errors = diff_function(left_tree, right_tree) + return { + 'left_tree': node_signature(left_tree), + 'right_tree': node_signature(right_tree), + 'errors': [error_signature(error) for error in errors], + } + + +def _document_signature(case, sdiff, parser, HtmlRenderer, TextRenderer): + parser_cls = getattr(sdiff, case['parser']) + left = case['left'] + right = case.get('right', left) + return { + 'left_parse': capture(lambda: parse_signature(left, parser_cls, parser)), + 'right_parse': capture(lambda: parse_signature(right, parser_cls, parser)), + 'left_text_render': capture( + lambda: render_signature(left, parser_cls, parser, TextRenderer()) + ), + 'right_text_render': capture( + lambda: render_signature(right, parser_cls, parser, TextRenderer()) + ), + 'left_html_render': capture( + lambda: render_signature(left, parser_cls, parser, HtmlRenderer()) + ), + 'right_html_render': capture( + lambda: render_signature(right, parser_cls, parser, HtmlRenderer()) + ), + 'text_diff': capture( + lambda: diff_signature(left, right, parser_cls, sdiff, TextRenderer()) + ), + 'html_diff': capture( + lambda: diff_signature(left, right, parser_cls, sdiff, HtmlRenderer()) + ), + 'struct_diff': capture( + lambda: tree_diff_signature(left, right, parser_cls, parser, sdiff.diff_struct) + ), + 'link_diff': capture( + lambda: tree_diff_signature(left, right, parser_cls, parser, sdiff.diff_links) + ), + } + + +def _reused_parser_signature(case, sdiff, parser): + parser_cls = getattr(sdiff, case['parser']) + if case['scenario'] == 'reuse_inline_parser': + instance = parser.InlineLexer() + else: + instance = parser_cls() + + first_result = instance(case['left']) + first_signature = [node_signature(node) for node in first_result] + second_result = instance(case['right']) + return { + 'first_result_before_reuse': first_signature, + 'second_result': [node_signature(node) for node in second_result], + 'same_result_object': first_result is second_result, + 'tokens_are_result': instance.tokens is second_result, + } + + +def _rule_selection_signature(case, sdiff, parser): + results = {} + for operation in case['operations']: + if case['scenario'] == 'inline_rule_selection': + instance = parser.InlineLexer() + else: + instance = getattr(sdiff, case['parser'])() + parse_method = instance if operation.get('call') else instance.parse + results[operation['name']] = capture( + lambda method=parse_method, item=operation: [ + node_signature(node) + for node in method(item['source'], item.get('rules')) + ] + ) + return results + + +def _preprocessing_signature(case, sdiff, parser): + parser_cls = getattr(sdiff, case['parser']) + direct_nodes = parser_cls().parse(case['left']) + public_tree = parser.parse(case['left'], parser_cls=parser_cls) + return { + 'direct_nodes': [node_signature(node) for node in direct_nodes], + 'public_tree': node_signature(public_tree), + 'lexer_type': type(parser_cls.get_lexer()).__name__, + } + + +def _matrix_signature(sources, sdiff, parser): + digest = hashlib.sha256() + count = 0 + for source in sources: + signature = capture(lambda item=source: parse_signature(item, sdiff.MdParser, parser)) + payload = json.dumps( + [source, signature], + ensure_ascii=False, + separators=(',', ':'), + sort_keys=True, + ).encode('utf-8') + digest.update(len(payload).to_bytes(8, byteorder='big')) + digest.update(payload) + count += 1 + return { + 'case_count': count, + 'sha256': digest.hexdigest(), + } + + +def _link_label_matrix_sources(): + for length in range(6): + for characters in itertools.product('[]^a\\', repeat=length): + label = ''.join(characters) + yield f'[{label}](u)' + yield f'![{label}](u)' + yield f'[{label}][r]' + yield f'![{label}][r]' + + +def _link_tail_matrix_sources(): + for length in range(6): + for characters in itertools.product('<>)\'" a', repeat=length): + suffix = ''.join(characters) + yield '[x](' + suffix + yield '![x](' + suffix + for characters in itertools.product('[]^ a', repeat=length): + suffix = ''.join(characters) + yield '[x]' + suffix + yield '![x]' + suffix + + +def run_case(case, sdiff, parser, HtmlRenderer, TextRenderer): + scenario = case.get('scenario', 'documents') + if scenario in ('reuse_block_parser', 'reuse_inline_parser'): + return capture(lambda: _reused_parser_signature(case, sdiff, parser)) + if scenario in ('block_rule_selection', 'inline_rule_selection'): + return capture(lambda: _rule_selection_signature(case, sdiff, parser)) + if scenario == 'preprocessing_boundary': + return capture(lambda: _preprocessing_signature(case, sdiff, parser)) + if scenario == 'link_label_matrix': + return _matrix_signature(_link_label_matrix_sources(), sdiff, parser) + if scenario == 'link_tail_matrix': + return _matrix_signature(_link_tail_matrix_sources(), sdiff, parser) + if scenario != 'documents': + raise ValueError(f"Unknown compatibility scenario: {scenario}") + return _document_signature(case, sdiff, parser, HtmlRenderer, TextRenderer) + + +def canonical_hash(value): + payload = json.dumps( + value, + ensure_ascii=False, + separators=(',', ':'), + sort_keys=True, + ).encode('utf-8') + return hashlib.sha256(payload).hexdigest() diff --git a/scripts/run_mistune_compat.py b/scripts/run_mistune_compat.py new file mode 100644 index 0000000..1f396c3 --- /dev/null +++ b/scripts/run_mistune_compat.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +"""Compare exact sdiff signatures between the Mistune 0.8.4 oracle and target.""" + +import argparse +from contextlib import contextmanager +import difflib +import json +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +if __package__: + from .mistune_compat_cases import SEED, expand_cases + from .mistune_compat_signatures import canonical_hash +else: + from mistune_compat_cases import SEED, expand_cases + from mistune_compat_signatures import canonical_hash + + +ORACLE_DIR = Path('/tmp/html-structure-diff-mistune-084-oracle') + + +def _run_report(python: Path, reporter: Path, repo: Path, cases: Path): + completed = subprocess.run( + [str(python), str(reporter), '--repo', str(repo), '--cases', str(cases)], + check=True, + capture_output=True, + text=True, + ) + return json.loads(completed.stdout) + + +def _git_revision(repo): + completed = subprocess.run( + ['git', 'rev-parse', 'HEAD'], + cwd=repo, + check=True, + capture_output=True, + text=True, + ) + return completed.stdout.strip() + + +def _git_status(repo): + completed = subprocess.run( + ['git', 'status', '--porcelain'], + cwd=repo, + check=True, + capture_output=True, + text=True, + ) + return completed.stdout.strip() + + +def _validate_oracle(oracle, revision): + python = oracle / 'venv/bin/python' + if not python.is_file(): + raise RuntimeError(f'Temporary oracle has no Python executable: {python}') + actual_revision = _git_revision(oracle) + if actual_revision != revision: + raise RuntimeError( + f'Temporary oracle is at {actual_revision}, expected {revision}' + ) + if _git_status(oracle): + raise RuntimeError(f'Oracle worktree must be clean: {oracle}') + + +def _remove_oracle(target): + subprocess.run( + ['git', 'worktree', 'remove', '--force', str(ORACLE_DIR)], + cwd=target, + check=False, + capture_output=True, + text=True, + ) + if ORACLE_DIR.is_symlink() or ORACLE_DIR.is_file(): + ORACLE_DIR.unlink() + elif ORACLE_DIR.is_dir(): + shutil.rmtree(ORACLE_DIR) + subprocess.run(['git', 'worktree', 'prune'], cwd=target, check=True) + + +@contextmanager +def _prepared_oracle(target, revision, bootstrap_python): + _remove_oracle(target) + print(f'Creating fresh Mistune oracle at {ORACLE_DIR}', flush=True) + try: + subprocess.run( + ['git', 'worktree', 'add', '--detach', str(ORACLE_DIR), revision], + cwd=target, + check=True, + ) + subprocess.run( + ['make', '-C', str(ORACLE_DIR), 'env', f'BOOTSTRAP_PYTHON={bootstrap_python}'], + check=True, + ) + _validate_oracle(ORACLE_DIR, revision) + yield ORACLE_DIR + finally: + _remove_oracle(target) + print(f'Removed temporary Mistune oracle {ORACLE_DIR}', flush=True) + + +def _write_golden_fixtures(path, oracle_report, oracle_revision, cases, corpus): + if oracle_report['environment']['mistune'] != '0.8.4': + raise ValueError('Golden fixtures may only be generated from Mistune 0.8.4') + oracle_environment = dict(oracle_report['environment']) + oracle_environment['revision'] = oracle_revision + manifest = { + 'schema_version': 1, + 'oracle': oracle_environment, + 'case_count': len(cases), + 'fuzz_seed': corpus.get('fuzz_seed', SEED), + 'expanded_cases_sha256': canonical_hash(cases), + 'cases': { + name: canonical_hash(signature) + for name, signature in sorted(oracle_report['cases'].items()) + }, + } + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + '\n', encoding='utf-8') + + +def _format_diff(name, oracle_case, target_case): + oracle = json.dumps(oracle_case, ensure_ascii=False, indent=2, sort_keys=True).splitlines() + target = json.dumps(target_case, ensure_ascii=False, indent=2, sort_keys=True).splitlines() + lines = difflib.unified_diff( + oracle, + target, + fromfile=f'oracle/{name}.json', + tofile=f'target/{name}.json', + lineterm='', + ) + return '\n'.join(lines) + + +def main(): + default_target = Path(__file__).resolve().parents[1] + argument_parser = argparse.ArgumentParser() + argument_parser.add_argument('--oracle-revision', required=True) + argument_parser.add_argument('--bootstrap-python', default='python3.11') + argument_parser.add_argument('--target', type=Path, default=default_target) + argument_parser.add_argument( + '--corpus', + type=Path, + default=default_target / 'tests/fixtures/compatibility/mistune_cases.json', + ) + argument_parser.add_argument('--report', type=Path) + argument_parser.add_argument('--write-golden-fixtures', type=Path) + args = argument_parser.parse_args() + + target = args.target.resolve() + reporter = target / 'scripts/mistune_compat_reporter.py' + corpus = json.loads(args.corpus.read_text(encoding='utf-8')) + cases = expand_cases(args.corpus, target) + + with _prepared_oracle( + target, + args.oracle_revision, + args.bootstrap_python, + ) as prepared_oracle: + with tempfile.TemporaryDirectory(prefix='sdiff-mistune-compat-') as temp_dir: + expanded_cases = Path(temp_dir) / 'cases.json' + expanded_cases.write_text(json.dumps(cases, ensure_ascii=False), encoding='utf-8') + oracle_report = _run_report( + prepared_oracle / 'venv/bin/python', + reporter, + prepared_oracle, + expanded_cases, + ) + target_report = _run_report(target / 'venv/bin/python', reporter, target, expanded_cases) + + if oracle_report['environment']['mistune'] != '0.8.4': + raise RuntimeError( + 'Oracle environment must use Mistune 0.8.4; found ' + f'{oracle_report["environment"]["mistune"]}' + ) + + if args.write_golden_fixtures: + _write_golden_fixtures( + args.write_golden_fixtures, + oracle_report, + _git_revision(prepared_oracle), + cases, + corpus, + ) + + mismatches = [] + for name, oracle_case in oracle_report['cases'].items(): + target_case = target_report['cases'].get(name) + if oracle_case != target_case: + mismatches.append({ + 'name': name, + 'diff': _format_diff(name, oracle_case, target_case), + }) + + result = { + 'oracle_environment': oracle_report['environment'], + 'target_environment': target_report['environment'], + 'fuzz_seed': corpus.get('fuzz_seed', SEED), + 'case_count': len(cases), + 'mismatch_count': len(mismatches), + 'mismatches': mismatches, + } + if args.report: + args.report.parent.mkdir(parents=True, exist_ok=True) + args.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') + + print( + f"Mistune {oracle_report['environment']['mistune']} oracle vs " + f"{target_report['environment']['mistune']} target: " + f"{len(cases)} cases, {len(mismatches)} mismatches" + ) + for mismatch in mismatches[:10]: + print(f"\n{mismatch['diff']}") + if len(mismatches) > 10: + print(f'\n{len(mismatches) - 10} additional mismatches omitted from stdout') + return 1 if mismatches else 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/sdiff/parser.py b/sdiff/parser.py index 3d8d2e3..2a4a7e5 100644 --- a/sdiff/parser.py +++ b/sdiff/parser.py @@ -1,69 +1,342 @@ +import re from re import Match import mistune -import re +from mistune import InlineState from .model import * -class InlineLexer(mistune.BlockLexer): - grammar_class = mistune.InlineGrammar +# Mistune 3 intentionally removed the lexer/grammar APIs used by sdiff with +# Mistune 0.8. Keep sdiff's narrow Markdown dialect local so its public tree +# remains stable without importing private Mistune constants. +_INLINE_TAGS = ( + 'a', 'em', 'strong', 'small', 's', 'cite', 'q', 'dfn', 'abbr', 'data', + 'time', 'code', 'var', 'samp', 'kbd', 'sub', 'sup', 'i', 'b', 'u', + 'mark', 'ruby', 'rt', 'rp', 'bdi', 'bdo', 'span', 'br', 'wbr', 'ins', + 'del', 'img', 'font', +) +_BLOCK_TAG = r'(?!(?:{})\b)\w+(?!:/|[^\w\s@]*@)\b'.format('|'.join(_INLINE_TAGS)) +_VALID_ATTR = r'''\s*[a-zA-Z\-](?:\s*\=\s*(?:"[^"]*"|'[^']*'|[^\s'">]+))?''' +_SMART_AMP = re.compile(r'&(?!#?\w+;)') +_LINK_OPENER = re.compile(r'!?\[') - default_rules = [ - 'linebreak', 'link', - 'reflink', 'text', - ] - def __init__(self): - self.links = {} - self.grammar_class.text = re.compile(r'^ {1,}\n|^[\s\S]+?(?=[\[`~]| {2,}\n|$)') - super().__init__() +def _pure_pattern(pattern): + value = pattern.pattern + return value[1:] if value.startswith('^') else value - def parse_autolink(self, m): - self.tokens.append(Link(m.group(0))) - def parse_url(self, m): - self.tokens.append(Link(m.group(0))) +def _legacy_escape(text): + text = _SMART_AMP.sub('&', text) + return text.replace('<', '<').replace('>', '>') - def parse_link(self, m): - return self._process_link(m) - def parse_reflink(self, m): - # TODO skip this check for now - # key = mistune._keyify(m.group(2) or m.group(1)) - # if key not in self.links: - # return None - # ret = self.links[key] - return self._process_link(m) +class _InlineGrammar: + linebreak = re.compile(r' {2,}\n(?!\s*$)') + text = re.compile(r' {1,}\n|[\s\S]+?(?=[\[`~]| {2,}\n|$)') - def _process_link(self, m): - line = m.group(0) - if line[0] == '!': - node = Image(line) - else: - node = Link(line) - - self.tokens.append(node) - def parse_linebreak(self, m): - node = NewLine() - self.tokens.append(node) +class _BlockGrammar: + def_links = re.compile( + r'^ *\[([^^\]]+)\]: *' + r']+)>?' + r'(?: +["(]([^\n]+)[")])? *(?:\n+|$)' + ) + def_footnotes = re.compile( + r'^\[\^([^\]]+)\]: *(' + r'[^\n]*(?:\n+|$)' + r'(?: {1,}[^\n]*(?:\n+|$))*' + r')' + ) + newline = re.compile(r'^\n+') + fences = re.compile( + r'^ *(`{3,}|~{3,}) *([^`\s]+)? *\n' + r'([\s\S]+?)\s*' + r'\1 *(?:\n+|$)' + ) + hrule = re.compile(r'^ {0,3}[-*_](?: *[-*_]){2,} *(?:\n+|$)') + heading = re.compile(r'^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)') + lheading = re.compile(r'^([^\n]+)\n *(=|-)+ *(?:\n+|$)') + block_quote = re.compile(r'^( *>[^\n]+(\n[^\n]+)*\n*)+') + list_block = re.compile( + r'^( *)(?=[*+-]|\d+\.)(([*+-])?(?:\d+\.)?) [\s\S]+?' + r'(?:' + r'\n+(?=\1?(?:[-*_] *){3,}(?:\n+|$))' + r'|\n+(?=%s)' + r'|\n+(?=%s)' + r'|\n+(?=\1(?(3)\d+\.|[*+-]) )' + r'|\n{2,}' + r'(?! )' + r'(?!\1(?:[*+-]|\d+\.) )\n*' + r'|' + r'\s*$)' % ( + _pure_pattern(def_links), + _pure_pattern(def_footnotes), + ) + ) + list_item = re.compile( + r'^(( *)(?:[*+-]|\d+\.) [^\n]*' + r'(?:\n(?!\2(?:[*+-]|\d+\.) )[^\n]*)*)', + flags=re.M, + ) + list_bullet = re.compile(r'^ *(?:[*+-]|\d+\.) +') + paragraph = re.compile( + r'^((?:[^\n]+\n?(?!' + r'%s|%s|%s|%s|%s|%s|%s|%s|%s' + r'))+)\n*' % ( + _pure_pattern(fences).replace(r'\1', r'\2'), + _pure_pattern(list_block).replace(r'\1', r'\3'), + _pure_pattern(hrule), + _pure_pattern(heading), + _pure_pattern(lheading), + _pure_pattern(block_quote), + _pure_pattern(def_links), + _pure_pattern(def_footnotes), + '<' + _BLOCK_TAG, + ) + ) + block_html = re.compile( + r'^\s* *(?:{}|{}|{}) *(?:\n{{1,}}|\s*$)'.format( + r'', + fr'<({_BLOCK_TAG})((?:{_VALID_ATTR})*?)>([\s\S]+?)<\/\1>', + fr'<{_BLOCK_TAG}(?:{_VALID_ATTR})*?>', + ) + ) + text = re.compile(r'^[^\n]+') + + +class _HardenedLinkParser(mistune.InlineParser): + """Use Mistune 3's bounded link scanner without its rendering semantics.""" + + def precedence_scan(self, match, state, end_pos, rules=None): + return None + + +def _next_character_indexes(source, character): + indexes = [None] * (len(source) + 1) + next_index = None + for position in range(len(source) - 1, -1, -1): + if source[position] == character: + next_index = position + indexes[position] = next_index + return indexes + + +def _next_nonspace_indexes(source): + indexes = [len(source)] * (len(source) + 1) + for position in range(len(source) - 1, -1, -1): + indexes[position] = indexes[position + 1] if source[position].isspace() else position + return indexes + + +def _build_direct_tail_ends(source, next_parenthesis, next_nonspace): + """Return the Mistune 0.8 direct-link tail end for each possible label close.""" + quote_tail_ends = [None] * len(source) + next_valid_quote = [None] * (len(source) + 1) + next_quote = None + for position in range(len(source) - 1, -1, -1): + if source[position] in ('\'', '"'): + close_parenthesis = next_nonspace[position + 1] + if close_parenthesis < len(source) and source[close_parenthesis] == ')': + quote_tail_ends[position] = close_parenthesis + 1 + next_quote = position + next_valid_quote[position] = next_quote + + angle_tail_ends = [None] * len(source) + next_valid_angle = [None] * (len(source) + 1) + next_angle = None + for position in range(len(source) - 1, -1, -1): + if source[position] == '>': + tail_start = next_nonspace[position + 1] + if tail_start < len(source) and source[tail_start] == ')': + angle_tail_ends[position] = tail_start + 1 + elif tail_start > position + 1 and tail_start < len(source) and source[tail_start] in ('\'', '"'): + closing_quote = next_valid_quote[tail_start + 1] + if closing_quote is not None: + angle_tail_ends[position] = quote_tail_ends[closing_quote] + if angle_tail_ends[position] is not None: + next_angle = position + next_valid_angle[position] = next_angle + + tail_ends = [None] * len(source) + for position in range(len(source) - 1): + if source[position:position + 2] != '](': + continue + destination_start = next_nonspace[position + 2] + fallback_parenthesis = next_parenthesis[destination_start] + if fallback_parenthesis is None: + continue + if destination_start < len(source) and source[destination_start] == '<': + closing_angle = next_valid_angle[destination_start + 1] + if closing_angle is not None: + tail_ends[position] = angle_tail_ends[closing_angle] + continue + tail_ends[position] = fallback_parenthesis + 1 + return tail_ends + + +def _build_reference_tail_ends(source, next_bracket, next_caret, next_nonspace): + """Return the Mistune 0.8 reference-link tail end for each label close.""" + tail_ends = [None] * len(source) + for position, char in enumerate(source): + if char != ']': + continue + reference_start = next_nonspace[position + 1] + if reference_start >= len(source) or source[reference_start] != '[': + continue + reference_end = next_bracket[reference_start + 1] + caret = next_caret[reference_start + 1] + if reference_end is not None and (caret is None or caret > reference_end): + tail_ends[position] = reference_end + 1 + return tail_ends + + +def _build_link_end_index(source, tail_ends, next_open, next_bracket, next_caret): + """Compile Mistune 0.8's greedy nested-label expression into a linear index.""" + link_ends = [None] * (len(source) + 1) + for position in range(len(source) - 1, -1, -1): + char = source[position] + if char == '[': + nested_end = next_bracket[position + 1] + caret = next_caret[position + 1] + if nested_end is not None and (caret is None or caret > nested_end): + link_ends[position] = link_ends[nested_end + 1] + continue + if char == ']': + later_bracket = next_bracket[position + 1] + later_open = next_open[position + 1] + can_extend = later_bracket is not None and ( + later_open is None or later_bracket < later_open + ) + if can_extend and link_ends[position + 1] is not None: + link_ends[position] = link_ends[position + 1] + else: + link_ends[position] = tail_ends[position] + continue + link_ends[position] = link_ends[position + 1] + return link_ends + + +def _build_link_indexes(source): + next_open = _next_character_indexes(source, '[') + next_bracket = _next_character_indexes(source, ']') + next_caret = _next_character_indexes(source, '^') + next_parenthesis = _next_character_indexes(source, ')') + next_nonspace = _next_nonspace_indexes(source) + direct_tails = _build_direct_tail_ends(source, next_parenthesis, next_nonspace) + reference_tails = _build_reference_tail_ends( + source, + next_bracket, + next_caret, + next_nonspace, + ) + return ( + _build_link_end_index(source, direct_tails, next_open, next_bracket, next_caret), + _build_link_end_index(source, reference_tails, next_open, next_bracket, next_caret), + ) - def parse_text(self, m): - text = m.group(0) - if text.strip(): - escaped_text = mistune.escape(text) - node = Text(escaped_text) - self.tokens.append(node) +class InlineLexer: + default_rules = [ + 'linebreak', 'link', + 'reflink', 'text', + ] -class MdParser(mistune.BlockLexer): + def __init__(self): + self.links = {} + self.tokens = [] + self.rules = _InlineGrammar() + self._link_parser = _HardenedLinkParser() + + def __call__(self, text, rules=None): + return self.parse(text, rules) + + def parse(self, text, rules=None): + text = text.rstrip('\n') + active_rules = rules or self.default_rules + position = 0 + direct_link_ends, reference_link_ends = _build_link_indexes(text) + modern_state = InlineState({'ref_links': {}}) + modern_state.src = text + + while position < len(text): + for rule in active_rules: + if rule == 'linebreak': + linebreak = self.rules.linebreak.match(text, position) + if linebreak: + self.tokens.append(NewLine()) + position += len(linebreak.group(0)) + break + continue + + if rule in ('link', 'reflink') and text.startswith(('![', '['), position): + marker_length = 2 if text.startswith('![', position) else 1 + opener_end = position + marker_length + if rule == 'link': + end_position = direct_link_ends[opener_end] + else: + end_position = reference_link_ends[opener_end] + if end_position is not None: + opener = _LINK_OPENER.match(text, position) + # Exercise Mistune 3's hardened parser for syntax it + # accepts; the local boundary remains authoritative for + # legacy raw-source compatibility. + self._link_parser.parse_link(opener, modern_state) + source = text[position:end_position] + self.tokens.append(Image(source) if source[0] == '!' else Link(source)) + position = end_position + break + continue + + if rule == 'text': + text_match = self.rules.text.match(text, position) + if text_match: + raw = text_match.group(0) + if raw.strip(): + self.tokens.append(Text(_legacy_escape(raw))) + position += len(raw) + break + else: # pragma: no cover - default rules end with the text fallback + raise RuntimeError(f'Infinite loop at: {text[position:]}') + + return self.tokens + + +class _CompatibilityBlockLexer: + grammar_class = _BlockGrammar + default_rules = () + + def _configure_compatibility_lexer(self): + self.tokens = [] + self.rules = self.grammar_class() + self._active_rules = list(self.default_rules) + + def __call__(self, text, rules=None): + return self.parse(text, rules) + + def parse(self, text, rules=None): + text = text.rstrip('\n') + active_rules = rules or self._active_rules + + while text: + for name in active_rules: + match = getattr(self.rules, name).match(text) + if match: + getattr(self, f'parse_{name}')(match) + text = text[len(match.group(0)):] + break + else: # pragma: no cover - every grammar ends with a text rule + raise RuntimeError(f'Infinite loop at: {text}') + return self.tokens + + +class MdParser(_CompatibilityBlockLexer): default_rules = [ 'newline', 'list_block', 'block_html', 'heading', 'lheading', 'paragraph', 'text', ] - list_rules = ( 'newline', 'heading', 'lheading', 'hrule', 'list_block', 'text', @@ -74,97 +347,63 @@ def get_lexer(cls): return cls() def __init__(self): - super().__init__() - self.grammar_class.block_html = re.compile( - r'^\s* *(?:{}|{}|{}) *(?:\n{{1,}}|\s*$)'.format( - r'', - fr'<({mistune._block_tag})((?:{mistune._valid_attr})*?)>([\s\S]+?)<\/\1>', - fr'<{mistune._block_tag}(?:{mistune._valid_attr})*?>', - ) - ) + self._configure_compatibility_lexer() def _parse_inline(self, text): - inline = InlineLexer() - return inline.parse(text) + return InlineLexer().parse(text) - def parse_newline(self, m): - length = len(m.group(0)) - if length > 1: + def parse_newline(self, match): + if len(match.group(0)) > 1: self.tokens.append(NewLine()) - def parse_heading(self, m): - level = len(m.group(1)) - node = Header(level) - node.add_nodes(self._parse_inline(m.group(2))) + def parse_heading(self, match): + node = Header(len(match.group(1))) + node.add_nodes(self._parse_inline(match.group(2))) self.tokens.append(node) - def parse_lheading(self, m): - level = 1 if m.group(2) == '=' else 2 - text = m.group(1) + def parse_lheading(self, match): + level = 1 if match.group(2) == '=' else 2 node = Header(level) - node.add_nodes(self._parse_inline(text)) + node.add_nodes(self._parse_inline(match.group(1))) self.tokens.append(node) - def parse_block_html(self, m): - text = m.group(0) - html = Html(text) - self.tokens.append(html) + def parse_block_html(self, match): + self.tokens.append(Html(match.group(0))) - def parse_paragraph(self, m): - text = m.group(1).rstrip('\n') + def parse_paragraph(self, match): node = Paragraph() - node.add_nodes(self._parse_inline(text)) + node.add_nodes(self._parse_inline(match.group(1).rstrip('\n'))) self.tokens.append(node) - def parse_text(self, m): - text = m.group(0) - escaped_text = mistune.escape(text) - node = Text(escaped_text) - self.tokens.append(node) + def parse_text(self, match): + self.tokens.append(Text(_legacy_escape(match.group(0)))) - def parse_list_block(self, m): - bull = m.group(2) - cap = m.group(0) - ordered = '.' in bull - node = List(ordered) - node.add_nodes(self._process_list_item(cap, bull)) + def parse_hrule(self, match): + # Preserve Mistune 0.8's inherited behavior for a thematic break + # nested in a list. The compatibility reporter records the exception. + self.tokens.append({'type': 'hrule'}) + + def parse_list_block(self, match): + bullet = match.group(2) + node = List('.' in bullet) + node.add_nodes(self._process_list_item(match.group(0))) self.tokens.append(node) - def _process_list_item(self, cap, bull): + def _process_list_item(self, captured): result = [] - cap = self.rules.list_item.findall(cap) - - _next = False - length = len(cap) - - for i in range(length): - item = cap[i][0] + items = self.rules.list_item.findall(captured) - # remove the bullet + for captured_item in items: + item = captured_item[0] space = len(item) item = self.rules.list_bullet.sub('', item) - # outdent if '\n ' in item: - space = space - len(item) - pattern = re.compile(r'^ {1,%d}' % space, flags=re.M) - item = pattern.sub('', item) - - # determine whether item is loose or not - loose = _next - if not loose and re.search(r'\n\n(?!\s*$)', item): - loose = True - - rest = len(item) - if i != length - 1 and rest: - _next = item[rest - 1] == '\n' - if not loose: - loose = _next + space -= len(item) + item = re.compile(r'^ {1,%d}' % space, flags=re.M).sub('', item) node = ListItem() - block_lexer = self.get_lexer() - nodes = block_lexer.parse(item, self.list_rules) - node.add_nodes(nodes) + node.add_nodes(self.get_lexer().parse(item, self.list_rules)) result.append(node) return result @@ -177,29 +416,30 @@ class ZendeskHelpMdParser(MdParser): def __init__(self): super().__init__() - self.grammar_class.callout = re.compile(self.TAG_PATTERN.format(tag_name='callout', - attr_re=self.CALLOUT_ATTR_PATTERN)) - self.default_rules.insert(0, 'callout') + self.rules.callout = re.compile(self.TAG_PATTERN.format( + tag_name='callout', + attr_re=self.CALLOUT_ATTR_PATTERN, + )) + self._active_rules.insert(0, 'callout') - self.grammar_class.steps = re.compile(self.TAG_PATTERN.format(tag_name='steps', attr_re='')) - self.default_rules.insert(0, 'steps') + self.rules.steps = re.compile(self.TAG_PATTERN.format(tag_name='steps', attr_re='')) + self._active_rules.insert(0, 'steps') - self.grammar_class.tabs = re.compile(self.TAG_PATTERN.format(tag_name='tabs', attr_re='')) - self.default_rules.insert(0, 'tabs') + self.rules.tabs = re.compile(self.TAG_PATTERN.format(tag_name='tabs', attr_re='')) + self._active_rules.insert(0, 'tabs') - def parse_callout(self, m: Match[str]) -> None: - style = m.group(self.CALLOUT_STYLE_GROUP) - self._parse_nested(ZendeskHelpCallout(style), m) + def parse_callout(self, match: Match[str]) -> None: + style = match.group(self.CALLOUT_STYLE_GROUP) + self._parse_nested(ZendeskHelpCallout(style), match) - def parse_steps(self, m: Match[str]) -> None: - self._parse_nested(ZendeskHelpSteps(), m) + def parse_steps(self, match: Match[str]) -> None: + self._parse_nested(ZendeskHelpSteps(), match) - def parse_tabs(self, m: Match[str]) -> None: - self._parse_nested(ZendeskHelpTabs(), m) + def parse_tabs(self, match: Match[str]) -> None: + self._parse_nested(ZendeskHelpTabs(), match) - def _parse_nested(self, node: Node, m: Match[str]) -> None: - nested_content = m.group(self.TAG_CONTENT_GROUP) - nested_nodes = self.get_lexer().parse(nested_content) + def _parse_nested(self, node: Node, match: Match[str]) -> None: + nested_nodes = self.get_lexer().parse(match.group(self.TAG_CONTENT_GROUP)) node.add_nodes(nested_nodes) self.tokens.append(node) @@ -213,8 +453,6 @@ def _remove_ltr_rtl_marks(text): def parse(text, parser_cls: type[MdParser] = MdParser): - # HACK dirty hack to be consistent with Markdown list_block text = _remove_spaces_from_empty_lines(text) text = _remove_ltr_rtl_marks(text) - block_lexer = parser_cls() - return Root(block_lexer.parse(text)) + return Root(parser_cls().parse(text)) diff --git a/tests/fixtures/compatibility/golden_mistune_084_fixtures.json b/tests/fixtures/compatibility/golden_mistune_084_fixtures.json new file mode 100644 index 0000000..8d44035 --- /dev/null +++ b/tests/fixtures/compatibility/golden_mistune_084_fixtures.json @@ -0,0 +1,1100 @@ +{ + "case_count": 1086, + "cases": { + "atx_headings": "f7be5be83689cf22dd77489d41ab7881d1f92fdcce915578c47bbd31932410e3", + "autolinks_and_bare_urls_are_text": "f01bbeae321bf5357c645c236c0b81689c320f22111ebc28e7419984925f4a4e", + "block_html_and_comment": "f65d31151146abb7f3810f92f3412ea2d725239ac7ee78920778cb7f8991bbc8", + "block_html_template_attributes": "44cf0cdb2c9d5e484ae8b9713d4b845c9c3cdfd0eba0dec86d82db9f5ac1bb0b", + "block_parser_reuse": "43af3a2315aec3bb6aed336165dfdaad94fe24b7f2321db215ed06b700e1349a", + "block_rule_selection": "e37c103226a6c9865cb87c6718df6fc8cc5c890f32386467d22c80f1557d1f8e", + "block_syntax_stays_text": "5a859a8c09aac078ddaa24064cb1df7c81e073b3bc9f1b689aa228521250214e", + "closing_and_excess_heading_markers": "13d72f13ddaefe0298892e8da20477f2c439f3b0145ddac1362d704cd71259a1", + "deep_unmatched_link_openers": "b6e7eceddc289682d737b51cfd6f79afd57ce3733222b767296cc234516b0659", + "direction_marks_and_line_endings": "9d0c9ca015ae9554010d3863afcd9dcf63f4e99ebf8926872356ef25f8c7e179", + "empty": "031a3819af28b5a3ad407b04f08ea58e647f1520252a1d42b5c2ec7b3378cfdb", + "empty_and_unclosed_images": "9e1a345d0d80b3dec00dfd66e77195c1d3aaa0768f2c9f40f72f353baf7dfb93", + "exhaustive_link_label_matrix": "f891cf17d584b865562e6a30af596854701f680c0d3809f1956b508ab01d9286", + "exhaustive_link_tail_matrix": "819c8d053459be2e7c3038f87120bead4995df315eb6052a2897118e716e08f7", + "fixture_tests_fixtures_different_extra_paragraph": "4f4044ad3694fac516bb4fb8697b29efde888dbb0aae6b87b370255a460bf4f1", + "fixture_tests_fixtures_different_zendesk_callout_in_tabs": "4d63ca718729a84c5d63bc618d7379a88ee528366863bec242a598c1b95d0c93", + "fixture_tests_fixtures_different_zendesk_steps": "30dfb15eb7a6be8f4dd84f684725bc54d254b5401f826ade737600fb7c3dc3cb", + "fixture_tests_fixtures_same_extra_end_space": "a2985e8d6b1795d5300f76da5f05a1270198a92ff4e39ab51ba2047e6d9cf4d8", + "fixture_tests_fixtures_same_inline_html": "bf00a64b305149863ae25c44c06bd32d6cb812bc0e3c5282cee1c86eb4307c73", + "fixture_tests_fixtures_same_link_text_order": "253ed930dea3ae2e989eaa81b33975a1b48711f591b721a546ef57220ead0cf9", + "fixture_tests_fixtures_same_long": "49a24c3ff94f22c865523ea3cbfcca4ccbe5966969b1a096dba8e76e78408562", + "fixture_tests_fixtures_same_simple": "49eb1cd27e7adb02dd61dd0254256116eca08124910d5da0de6c1bb602d73794", + "fixture_tests_fixtures_same_single_line": "ee861924bfaaeb207905e5368f5fa6fd16848d6c253967db0f3915d4867c0777", + "fixture_tests_fixtures_same_spaces_text": "352063f2efd0db9c7eda0e46e748c3ceb97697132c1c92682e41f4bd7b4a6f5b", + "fixture_tests_fixtures_same_special_chars": "9e758d40935fa48f69af99e3dc70be0ca37ae73ae283665b4cd2b3b353bb33c9", + "fixture_tests_fixtures_same_zendesk_steps_in_tabs": "32f7e604f6eeb0568e2a36b9fe76f60dd0ded06871fb9b709f13caeae4baab37", + "fuzz_0000": "6797748c4c412b01940de979b380ef8d8f90e670533a4c52abe8ee24867b46a1", + "fuzz_0001": "031a3819af28b5a3ad407b04f08ea58e647f1520252a1d42b5c2ec7b3378cfdb", + "fuzz_0002": "b8a2946c56b6c3480d1e069e62b5a9d17be1615a45fcfa79312be08e7ec5c35d", + "fuzz_0003": "be8e4442db27f488d94401a437f91236826b519972beefcf3f6f401240ca6a4f", + "fuzz_0004": "e87835928b388375dd103507e0e7c7b9645de0a24dfbc5eb687f9aa04c5b3655", + "fuzz_0005": "1da5d1b53ea450a7e278de97aeba596360c7025a4cbc6661cd95f4a6d73570f6", + "fuzz_0006": "4360d477ab048ec001c3e00c58b6678138bb08148fb4cf8ddf22ba203fc4551e", + "fuzz_0007": "0e43248030d0b5a4f67d4731fee53967755fd2174e1ea86059aca723e59c276a", + "fuzz_0008": "aa29ee5ff549c06092e77a3cd2df05917fb0570e205ce9da435c5e8abda7e91a", + "fuzz_0009": "dc4a4a990a2fb1a5d0833e64602cadafcafa6e41016f3b73ad69c073aef60fce", + "fuzz_0010": "c896348519a173c33d2fd1a3a77c97aab0e26ca9be33350aa980e1fde2c6dabc", + "fuzz_0011": "ccaad5f6f431f5e74f4667aca7bbc8484e27347a545ad3d74bb989bad0e0c517", + "fuzz_0012": "0c0ccf4d81c0a41241aa5f1964faa53a75a78275784b87be29060ffc9c5c9d84", + "fuzz_0013": "18accd4b925a8b0268cdd03f4e36559fc6dcdc9e4144dbe26bba60bb9def7af3", + "fuzz_0014": "b15069c14d640dd44d53854578c4dfd9eae9e406e388dcc1f0f390483f75ac31", + "fuzz_0015": "a0af454a290c0ad0018322eb966276d82ce4146d7cc2b1a77d0d42ed989e7497", + "fuzz_0016": "0a0b5e5ebee93ad9998e8d82d46f594842a304ea533bfe27e6e25c41ef3abcea", + "fuzz_0017": "45a2d8ee69dbcf40dcadec05b948e846ac56cfecbde8672cb9d39fe35aee43b5", + "fuzz_0018": "28ba7c2598853c648822034cdef85ad15c0cf5dd867ce547b2f453e0752e1d89", + "fuzz_0019": "89c13022821ffa1275e993a959aa088557a8b3852902049dbb5e95fd61aae2ee", + "fuzz_0020": "3a58a5811e8147561912027ff284746e40b2fa82387069438c776a5ec9a0f81e", + "fuzz_0021": "7397fbac183c8868dc16857a59791dc143db2429ed690090d3c7c9fe777964d8", + "fuzz_0022": "30cefe6b8ef2aaa10361a2b09373db0655ff96921219b340082ca965f8b5b2b0", + "fuzz_0023": "c6b6c6559249f2a36f271593bda7c0c912a1503895e4f1b08df690506537f5b4", + "fuzz_0024": "be01db6031d71caf91d03515a800cac423db9dfe7a17eab0688d8ab5e52856b9", + "fuzz_0025": "e88147418091a2583260d364c4f832cfe6bb0dc5904c7463fb7872e5f038414f", + "fuzz_0026": "92802b415e9f1fe4827b36cada889a27c72e3cf87a36c83d212f881cecc18891", + "fuzz_0027": "ffb9a08b32880a1ccd296d165e1f882897421ada12f0f860fcaf50ee51704613", + "fuzz_0028": "7b931fda55bdd90bb6afb0d69ea5d918e55d85d5bb471c1c8686f80a71ff8a4c", + "fuzz_0029": "b340d4be0bbeac7cb76df6436c4cd6b1a82949adaab121f036f45d750e5fa2e1", + "fuzz_0030": "ff0187213c5e6f4705cb20a6dd48077359a57e863c0086379d21555e6d14dd62", + "fuzz_0031": "a13fde7b421460f4ab8800269b034f3b66b13db10ed6b5deb0324e2acc8b1000", + "fuzz_0032": "87289fadfb005fd3a533836f8d6a3f800fc6ddceb8de9d2c72192699b9a9ddf3", + "fuzz_0033": "c26b8de6cc86ea0f3252f5471aa3e9fac0beaf30135fb092f2dd5e1d6fad75de", + "fuzz_0034": "2d1ac02ec43d9b89575cbe5b08fafa9bdbdd4422578c50e914cadef7997a8756", + "fuzz_0035": "efd6722ab52e1b15ab09d19cada07da523b166ce63b3cd92d062f19473e7ab46", + "fuzz_0036": "25f409499ccbdde41211dd948187ea79a9156671140b55a5551868669b609cd8", + "fuzz_0037": "4e95e1343898de4b66c921a9aaada6d5c76962c0b047a1572d510f78368027df", + "fuzz_0038": "a9cbd6adcc4613fac6e4ef42264e473e1c945c52bbcdfe8551c9d8e437b21ba8", + "fuzz_0039": "d16890eee92c1e9949f11d9b67e36a7fff85f33d86efb6003862d3b1c93d1aee", + "fuzz_0040": "8236112afd4363b51560b9ea79b6807dc0d4397243012731a42d4425a10c7716", + "fuzz_0041": "bbfed59e1779669a77288364fd45eadd3fe9aa65fac27365ec49195548bbd7a7", + "fuzz_0042": "7496cf2692bdf63817b7554083e4751275559b3dba012d455f53086102e78e00", + "fuzz_0043": "0c3dd8bd675f6ca69e1e85af6e00f61b1d9abe99782c0576f52729fd9ef6ae1f", + "fuzz_0044": "4c62f2d7e8541878dc06c541e64b3a087eece291fdf5804345241f1cdfaee94f", + "fuzz_0045": "5e3a38a3bc06b2b0e307d159c83066933c6de2ae3b9355db8b30e9abd1537df1", + "fuzz_0046": "fb549b96c772cdf0a29ffb4cd6f875b483e6efb60cb3d5137d35209866ba7572", + "fuzz_0047": "bdb0dc59db3432297d9d9b121c5435be0ceb7956670ca61639c9e2eaf5802d4b", + "fuzz_0048": "cb67d8878051789d2c9a610a68686c4dacabdc550f0c6b8c8db8be962d185f4f", + "fuzz_0049": "dc78277aebc8dff8be1e57c79a3dfd8948f9eded7f268606a8f8c08dee3fa01c", + "fuzz_0050": "b692d0be9c09026cd4328fb265921266f49b8da624cf092946ecafae00bf823a", + "fuzz_0051": "c05529370bcbfdf51ebb21d5fafdfb0a68020d3c3398c7614da40250fb428547", + "fuzz_0052": "8e51f439903174e72015e47c726de4bc1d6e1041b8dad066d6dfee2c6d04fc40", + "fuzz_0053": "cd4369a7686e41c54113dab7d5ce5616ce3279483220870364c47c15655cba6b", + "fuzz_0054": "698848bb49121cbaef95901bd053f9093cf3305cfde323e9b2972364f6471e0a", + "fuzz_0055": "aa3365f174b8376e8bee68deae870326f954871ac51548094be445b69e0cad1b", + "fuzz_0056": "6f895ee569b5b6b3dabfca18ef21091fba6216c68fed486b8c198502411b5ae4", + "fuzz_0057": "f1c89e24b1f26f59305725e55967d7f2a1d0719b38e335b04b096f16d8b7c4be", + "fuzz_0058": "48894d3c7f1863a027a8ce826e1887e9afd9aae8a1a306e1d2aa39543dbd5e5e", + "fuzz_0059": "3068eb4420ea7025e94ab554f823744f7b47a14427b9e1a41fbb7b8b209c538c", + "fuzz_0060": "e56735ced94c3f5e94a9d5d3c8a7f3981fe75a98fa0aea47f7f49f0963621bbe", + "fuzz_0061": "eee1154a4764c779c223d816c9d633b8dc24b135233d02eaba1cc75312262ffb", + "fuzz_0062": "38171b799ee6601618349774b71d63060adbddec9cc1f417e65e2d55b60391bb", + "fuzz_0063": "3018c85ea8f81dee0a125a79c2faf046f39336dbe734e1c267ada63d510d82b0", + "fuzz_0064": "d18eef4a85e2acac6346dd2234ccfb6e7f959795284fc135c7fff09843437b94", + "fuzz_0065": "3d25a30d88422d41e3f15560bb2fbfa97c7a2f4aa58feac49da538fc2d6fee59", + "fuzz_0066": "13301c949bb78fb70e6cca2ed3648b8d2b5b63597e25114899aba49f2e02e65b", + "fuzz_0067": "8690f1d46764e3dba641ed50518c45d410bb35548320647538d425d5ca4e4aa9", + "fuzz_0068": "bbb8cb472168bf86f06811593600867ef6333d50f1598d9ad6de6e1e58ef9170", + "fuzz_0069": "3baf87d0d6e5cec6794287c9e761224dd02f7dc22bcdbbc1073cdbccbc1cad89", + "fuzz_0070": "0c5dc52982c2fd771731a74ef34e8894102554aa60348e7c76d308c2763469ab", + "fuzz_0071": "759791e50bc7ccdbb21abe5187a3a4af80f357d17387eea56f2091b94c71d0b6", + "fuzz_0072": "6262cb747d1596f08473162e291fc4eab0484a5dde450e3b7d6249eb78329164", + "fuzz_0073": "03608c99da46a97b43a1068d71578abc221382c0f4884a276f21b6a8b73b67e7", + "fuzz_0074": "18cea210123c070f97be9130b83a8b70c0e8252514a19df4fb803561e407c99a", + "fuzz_0075": "e8bc90791a728347ef20b7b124cb817a1bc70df201e54f63d5eb6fa183c7f399", + "fuzz_0076": "045aeaddb920ce29e7df993530e6852e566ca40d9c7e6ec7539e522d3546dc18", + "fuzz_0077": "f377b04c232d1ce9ca17593f45a2e34ddfec8b8129f00da0f5a9c391c629beb8", + "fuzz_0078": "3315572b97afa2c86620da593dbb9ea73b33be59213e122d1566b4388e270037", + "fuzz_0079": "178b5a959615aab261eb6af2b94b29985c8a942a4114043e3448c3c7b59d62f0", + "fuzz_0080": "7d4c6ae854c753359e967408cfaa3101d49971d501241474c31457d2de7c208a", + "fuzz_0081": "eff59f92c54f98d27f26b5d9197601e1dc7c31e69b3f59643c5fce0e2ba11487", + "fuzz_0082": "391b754d17b032cd00bf84e19eb1b1bf1e5acf499057ae13bf9730d00a78c15b", + "fuzz_0083": "e33075e0cc24bfcc4db8fc9e5d5e8c78129aa7ca0905be6f23309c36bfedcba1", + "fuzz_0084": "87913d282dd682a79c1a4c305b57a1582e491bb1c18f9c18064adc811857e22d", + "fuzz_0085": "712635c6375042d5c0af3bc288d0e5ae579073fb40b4ba23305dca87b7125afb", + "fuzz_0086": "0232e04c8805ec6176f1304741a3073f31f56dae06984b16256473c6748bf7ff", + "fuzz_0087": "e86351f17fdb8fc9140f74c6a67aa88ee5114c2b08e7566ce5ab8b0eb66028fc", + "fuzz_0088": "ef9c3f94688b693981e4cc9a099f46ff88daca7a624040e32b4fc9feced1384d", + "fuzz_0089": "ad29e075e49a1ca071c4f2ee642165d05fe5c72523c8ae45a0ff35724319f454", + "fuzz_0090": "12ae9596b6af4118fabd54bb51d4ce747eb241bfca45cb9c5596e3f47c748828", + "fuzz_0091": "c08f0ef3ee78deed7f1f0753ddbb11e5713e80fd8c6a519a8a8ad757b4d7da55", + "fuzz_0092": "57fa1cf6999c9b7f6a29078528457119199224adfecb4b532ab38d7273641140", + "fuzz_0093": "c9b850bc5ebb25cf2e8346647908102ebf7ead9eaee410688d05a4bef290277b", + "fuzz_0094": "bbf7141ec376b0fa372299e3e39c756f4d56e2ba0811644c531357b7d6d7fa96", + "fuzz_0095": "83f436ff1f387dd4135a933a182328a6ceb655bebf403d65d274ac022a76031d", + "fuzz_0096": "a13f984fb020674ed1dbc7489462274d82efe7562ed179a7ab3bbfbd2861ac2e", + "fuzz_0097": "5c588b2fa97b0defbb0a6457a70a700fa9ba23f6811ae788fcd2addb44d7da6b", + "fuzz_0098": "7e0f34ec80f867f1c4f5e3c26517a7d2f1b0e27e37193ca9317d162cd3f8aef4", + "fuzz_0099": "6b6949915c4e14ca19d8bbefb53aff16499094ab1431ec22d1b8bf00e5e6c351", + "fuzz_0100": "761f7f2be22630ffda05828ec1bfd1b0dfcd0efe302e5efe7feb5f1292116c94", + "fuzz_0101": "8660fa1b858d0a53cb5f0164555d834277e90fadbbebff00d66aa8a21822f9a6", + "fuzz_0102": "0b3566e9b6a6dbc60ffbc5caf5fda2ed16b698759ee5aa334d054cfc3cb3901a", + "fuzz_0103": "d2d83117edeac8bbc595ab6d16560abfa9934f7959df8df6851d384f70ffb4d4", + "fuzz_0104": "cac5874824d93104b5b8f6d92c915ac135349a220c9cf7f6b17e564c457b6227", + "fuzz_0105": "3de5259bd7eae1ccb19218597538836b93efa0d0d5c5e7b91460e1c03f502886", + "fuzz_0106": "f01b12fc06387a543d7161c1954d379222a95ca4713748511e24079c240c0335", + "fuzz_0107": "b2ae022626d414e6a9e5c5c5281ffa0b6d6d8ca98d44a45329174f1969783aa6", + "fuzz_0108": "de22d2f3b22bf05b87b13136ab741b685661903cf41472fb6819a5fb88c74767", + "fuzz_0109": "83f3c80d12840cb3fa170e3a0ba0846a3fd44c9be586966917c4cd9aa819ae26", + "fuzz_0110": "08c907a7726e8489b16e2c4db31c4ebd5de5fe1c2b2a8d6e832c6ca593164ad6", + "fuzz_0111": "2d39af9bd70dfc0812c9d74b35b53dee892b9494c6636a32220ec979d6cb0570", + "fuzz_0112": "d541abede59b3b099582ccb3473bfdca739e981cfae990952c884bd323bfad43", + "fuzz_0113": "4f124082fa3b2967185ab90e03a92de17e5b28be29645472b1191ba68d96fb7f", + "fuzz_0114": "5bed16e84b28dfde7581ba6176b26892c7b9df131820e5ba4698368bbfeb41bf", + "fuzz_0115": "e2f3756cee4392a3403f46673e3531eeac8a802484222771284919a3818eeda9", + "fuzz_0116": "c837cfbacbb6975858d474bc631fcaa149d1e51c5208b69ef1bde2cd924a0cda", + "fuzz_0117": "943172a4adb89e83a90484651c8525feadd3c7578335480f3743ce04edfd14b9", + "fuzz_0118": "15af2960d339aa0687fc169c0eaddf00490d12340b24ccd1ae0d4ef8fa09be3d", + "fuzz_0119": "33f32f949e0edd599ea0472d86bc579ed47080ff5341b17435f465d65752a321", + "fuzz_0120": "3ea0fa1ef513c3db77a99228b5e4ac6f5dab24acde5ca1ff0afb27f56fb02afd", + "fuzz_0121": "6f817224e633c7965377745b5d6cc06964bf50a1700894855f5d69f9fa07e10e", + "fuzz_0122": "8e5a4babd5a63c889f65682ceaaab491bcbbb191eb1ffb69d246eaf459b15321", + "fuzz_0123": "0ca77ae5341916df1b80ecdd6e6bd61438c07bc31a055d31a969dff01ae69625", + "fuzz_0124": "285d513bb5b268912a68734f719861523300d61781d69fa6535dce4f29ce416c", + "fuzz_0125": "e0c70afa137d014748cc89094b8d3c00b0303812a12b8def55edce3b84d8aa2d", + "fuzz_0126": "8a34e1bbc4a85e29cad521d99c7e9559df213878080b87366cfae9e80fce6170", + "fuzz_0127": "aca985b6093b10fcc2be7ee674499282e6b54fc47baafdc7b604b6560d94edc3", + "fuzz_0128": "0566c1ff428b8a625aca2353369d66422113fca9281e721bb86a6ed5798b0840", + "fuzz_0129": "8b9182a256003fb2f2ed6788ee6f128f38f834015b0e7e7d46ec09e7302f621c", + "fuzz_0130": "66b8f71641bcce17ac537a8f65d6d654e13aaecef23243afa46fec6303fd595e", + "fuzz_0131": "da3572da1aca5f6df7d18aeb27bc370f13fd9df40c3c13e77f61f52a3a0e80fa", + "fuzz_0132": "5d2ed09ec296ff44dba69919fa5969592cfac8b507607e1543c56867134bdd62", + "fuzz_0133": "c66f0565127d5ccb4d3d75f1c8aaf03130d69070cc5fc5833725581fe2d3248c", + "fuzz_0134": "7140f8b8f28456b73b2deded0685e19aa5a4ee68163b643f4080831f14d61312", + "fuzz_0135": "043212bfbfc03f12a10d54337fc03b5a1d7e3b06ad02b45786795c9d1cafac5c", + "fuzz_0136": "5d4ab22b783a7aa2c69e514991ca98260b3d987baed6f324db46f6cad1cf30e8", + "fuzz_0137": "d7dae084c0c36830e3961d10f6d91b94e67034928480e7efcae464c944f96d7c", + "fuzz_0138": "6190f1da98788d9fc1a3714a02a33ffed68c327f2ae7b611c3461d53af79bdeb", + "fuzz_0139": "9408c697e16d47b448f83614feee0f8bfd787b75e8fda5f8c70fadf2580c260d", + "fuzz_0140": "9811ae36e79d9c8367646890d68a62a1eec17c19f1f45feec14950d05de5014b", + "fuzz_0141": "db1fbee35f031a8fde847a1983bb991cf3e1d9003ae2e9c3032d076676f83590", + "fuzz_0142": "3ae9b265ec784c2b77d8a58c516560ddc00c41eaaeda8b5301fc4ae80116b1e4", + "fuzz_0143": "5127e51dbab442071c594d84f4a70adba5809c7372f37d732a55c3d23016dee7", + "fuzz_0144": "1f43eaf4287bca44fb72c98a565ebe366db9b9e62a8e94c97c2772c1d9c7ee10", + "fuzz_0145": "4af56171828b5e58c83fc346d454bde4ba7fa530123ed9bdefb170bcd1286c18", + "fuzz_0146": "b844210b7ff5a9b3759833d35c3ff5846f4088975b5dffe258915460e8cd64cc", + "fuzz_0147": "4500007abeec8a4b3637b0ec9daec75310d5fb0eebce555873b9940406d0a0d2", + "fuzz_0148": "2c1861ebb1d14fc5a37f79fa32bdfa651b68acbab11960b25427a3cd08ea8f49", + "fuzz_0149": "fc6961790868a00be2ad37f622995aff5da271f9bebd12f98edec834bab7d4f8", + "fuzz_0150": "28fac1111ef8c63649bc168de4f990fb1f3f63f4d4048fef15a9f80f18f6b588", + "fuzz_0151": "d032e3a46419ca8e17ac69e895ba79f2971c64044ac2cfe03bcf6e1fb3c91b5e", + "fuzz_0152": "9931ccf0eb025a259a8c750e1f0f9d23399611211cda77654997bc3e358e574b", + "fuzz_0153": "aac54b405d8ee1fe1bee5282d2c8f1f91759e8e20e1cc75914deaed590ca4f88", + "fuzz_0154": "51d51a25eecfd8a5cd318933dd8c63cbf9dfd7de4f57ab4dddfe1a0c11a7786f", + "fuzz_0155": "b867702f37d7f6d3b934d4fdb86f41dcad7987826e8860395235c01352d12a2b", + "fuzz_0156": "6208f37a66c331db8ee8f61336438da912bb9bb11ca2c5a0ee64dd1e2a38210a", + "fuzz_0157": "6e40316adf2cb821c035619a7df42786f5771d97f3283feec1919d7fd1ce6ef2", + "fuzz_0158": "79ae877517ed55be150f1a5db0e8cec7ad5fef59604f83cbf1ef33c917e7411c", + "fuzz_0159": "008a086f6ada2434c08e20cd47ac5f2fd9ef7be2050f50e542d9706c6d594a27", + "fuzz_0160": "73c77f758c47b0a321132bdc8f224798f08ea675294ee0831aae31d6b13b941e", + "fuzz_0161": "30be63e76cbc8c6192e525fbdc382f66840cfeef8b15d330e00a053232b2732c", + "fuzz_0162": "d227b8f149e862dee2891f4685aec8735b7df3bfb07337ff34e87eba4a549544", + "fuzz_0163": "7b956eec6ad8cba808ef061466a4e20e144b2180518a165627657bd958dda4ea", + "fuzz_0164": "151e2066f1be0053be2521ab3917aa2ac5aa52b76d0eabeeef41e402a2637ca8", + "fuzz_0165": "362c894c5f1e310af45929b827195b18e18da8cae20d67d7816689d2a6c272ff", + "fuzz_0166": "6d0c8becc550c1a9c5cd8b2bc2a6513970cd6198d7c45842619cf829b82c3454", + "fuzz_0167": "fd2ed4e421479a3d3c094584cb2dc07f79f16a0a13cbccd0622ccd57bfd2eb7b", + "fuzz_0168": "b917c9c2085977243b3ec4b5641f8d24c1c2d571fac902d27432f827d33f0742", + "fuzz_0169": "94ff634cdd6545bea6217d7bd3948ce537f6840840c087953b4904a884b10c91", + "fuzz_0170": "58443b06c2011b969f8448aa63d4ea477da3773b808e541c17619832f0494229", + "fuzz_0171": "553e6712b2814da7df1a9b3bf07b58dfe79d24331347e8352fa0fb2596e457b2", + "fuzz_0172": "d93adaefeaba5ddb2b335062e64c92cb84d6789c17501be10ac55e84f1a6728f", + "fuzz_0173": "5250b603393038ea64bedd273796774103ad851650901d66193223b8a760b1b8", + "fuzz_0174": "9217ff04f7e569635a121cbf561380fc7788cc8e5a81271ad9c54f1399eb2389", + "fuzz_0175": "427a5cc7787b966400bdbceb72a1a7466d5c16a7a1d807d7437e8a21d5aa9a16", + "fuzz_0176": "6395ff0f0daee60cde71134423ecd5e79da782008b9e7f7f448de72090024eb3", + "fuzz_0177": "b43b8d56dbda827f8535642aac1774f94951777fa802c80ea3390c6e583d179b", + "fuzz_0178": "62d57b33f8d591cce79ae1df5b0ebcea93b45403e2930c216f3d3f53485a3409", + "fuzz_0179": "23e8d2ced6a5ee127d33b654c50d55375c72b08460b4ec9ca06e6f6c25c0b8c4", + "fuzz_0180": "05394e748bd3f1a8058de819babb1b5005b69b6e32ba7811fee3d0e776d03565", + "fuzz_0181": "577ff8b28c4f7c4a346655be5614ed5ab37de9edb35a7aba87dde88295108404", + "fuzz_0182": "bd0f0db8596a74b20ffe24551d4c6625fb3b2f386fc4ec6e062820d1291d4fed", + "fuzz_0183": "c32a3a76c41e779d3a39dacb5bb20160f24133dccfa8d1847c795a4b3f71fe4d", + "fuzz_0184": "cc2d5976377ec10acd684827ce67f98414a5180bcb262a2f5a39e1432dfaa8b3", + "fuzz_0185": "cf1f57f9fb3dbd51297b846c58b697dc02065ae73f061f7376a2b1d6eef5f8d1", + "fuzz_0186": "ddab9b1d3f90a6bdc41f17c6120b0a37569176ee589a63e5f554c0587f2ba7b9", + "fuzz_0187": "9d93421cdf4fdf695f598d2fea7fe3280e465d67752bf161c472075a6116e922", + "fuzz_0188": "5bf325c7c8eb7dbf00e025ac9f1cc5a4444517ed3f5cd282bf37d9087ffc79a0", + "fuzz_0189": "3a790a0bfd2d326faa4fed5ca80c3f6cd73ade220c00ef0db3fda17fe9133fb8", + "fuzz_0190": "328a0037eb5700bea303f5acff4801260cf8f4131cac1611c93ee4d7a74ae30d", + "fuzz_0191": "cf0da33921b58a5bbc6772e72800a3388fadc5c087b4f58259f9d1fd3911d197", + "fuzz_0192": "42a10fb87cda3c06bd56c0e28f8ce87c173b05c33e1854909e731f55c48671aa", + "fuzz_0193": "8a56ed655021f54005a07bd7634d1113d2adcdafb39f03a4799863d33a4d95be", + "fuzz_0194": "1e4c0a23d05bc497da8a5d3f4a8dc3a79be5dd1be49c411058342746043b6143", + "fuzz_0195": "c837cfbacbb6975858d474bc631fcaa149d1e51c5208b69ef1bde2cd924a0cda", + "fuzz_0196": "2a30b66dc78bac125789550217609e225f33e1dee5e7708d71c9239d6461d655", + "fuzz_0197": "5ae320efbd06dfc7a58f373a6e08a697d57ce7eae31db3f059bcac40da067e52", + "fuzz_0198": "31931baf6299b5ef8a501c2c87e795feb1fcbc419133f026dfd43b10a6c6f0ff", + "fuzz_0199": "7ffcd3e4123f8ed741a703aac6a3ca85f1779f2c4f75d68ee3488be8c3899ef3", + "fuzz_0200": "1973ffe0e114f23fbd2d3ae5cee3f46700dbda2439faad73fd1e645c7555be1f", + "fuzz_0201": "8cf7480d0488f6dcee67422ee3c9e9f201c1b0cbeb68e60fd4b3f14349c5ea3a", + "fuzz_0202": "f232742a7b3125758e60d88d9dad8d23f92038f7eee92848cf26bed2e3a51661", + "fuzz_0203": "ece22f0fa24ba5dcb9a9e59e4097e90c0aa97718c551229fd97a19051b9c4ef9", + "fuzz_0204": "df02093907f00e068f1e5faf6dfb5e3ec3e801cd0df843bad848c6f3bf0f3f59", + "fuzz_0205": "2001260c78eabc55e169f2a0aa92ef83c3e740122ed527e2551e249e7528b13e", + "fuzz_0206": "fc4333131c114e88ac99d37706ee1f94048c7907c238055728cb3ae10429a2d1", + "fuzz_0207": "49d13212c3cb92309a6629db1d285da96ea5dc4e66a2d5f56c1555b0624c274e", + "fuzz_0208": "771fc9d81ee09c30632694f248b338fbaf221491be856b41f57eea9d5bd75265", + "fuzz_0209": "6f895ee569b5b6b3dabfca18ef21091fba6216c68fed486b8c198502411b5ae4", + "fuzz_0210": "1814a456a8ff7772f1a322fb0ae521d5419aec353b3f79323c72fa018038dfe6", + "fuzz_0211": "53e97d5176b708066c83cfe1de2eb5973b2706afff0269bc15e32bd78b8ad60b", + "fuzz_0212": "f7703cb8d55da981236ce534bf3fb727c5dba1a703358bb818d259486ff32cb1", + "fuzz_0213": "06af4562f34f7f926b9192a97e285292aa75fe2eaacc9f62c1f26d64a7a087f3", + "fuzz_0214": "f55b024d9cf691ce932ec51f5a9e84d87fff2272878c58872cac0fe21e30874f", + "fuzz_0215": "aac572045843369f1b8c24af5af2b4c42578b65b59d93d8bd6fe3c64454620af", + "fuzz_0216": "56b7acb7bbba7cc4a41553d782ff238709098eb16ef0e8c85a489e9cddc8dcc6", + "fuzz_0217": "69224b3928133e7ad8c6864a7e0886f3274119c5ee72318a3823eb2beb3f5f2f", + "fuzz_0218": "7dd0d8253f32437ed1671da1e7bc2ac29ddb10be067f9ba4e31e3d957ecd137b", + "fuzz_0219": "21990831fa1d417693aa3c1c20962a739792e5e26aed141fa7addfe666e81248", + "fuzz_0220": "5cdd461846de8f080922d29347f5a55965fda21c4bdc78c7df449ce66ae395fd", + "fuzz_0221": "08d4a58c8e55011636a01865e1fb66ed14eee4ddfe5d31d3d9708df14f402405", + "fuzz_0222": "fd525e445af0a332b73ee7cc26703d1e798fa8c58d5e6747527cd5772633a2f0", + "fuzz_0223": "404954f301bf16ca627cf62922d2aff4f5dc4dc6c35e870a0a49b00ee86b2d93", + "fuzz_0224": "c9543154e481804771e16be3eeb674a45de64391f9c62bd2cdc965de4e25e689", + "fuzz_0225": "cc0cec7ee48aa8b75d1e846a3e91bf0e36a5b0b341cc9fd157043fc5bdf262e0", + "fuzz_0226": "dfea7d93174204fb1617253c2493b07af074f749dd741a509d101acaea4c0ab1", + "fuzz_0227": "4f1030a62ef00188b51cfe8dc84261912e91af938e3a0d4fcb9e8c01827fc1e4", + "fuzz_0228": "3944897340c892baeebf811945afe62c49ec2a2a6e820b215d9098d9b4883d6f", + "fuzz_0229": "92aa6bc288a1ccea812ed91f64c87490e2ef1b2fccb6868dded49e841e7a2189", + "fuzz_0230": "224bba8e3466171c14bd0b3df1c6be1c6e70eef4140619629a68c9139ff5f982", + "fuzz_0231": "dd33081c72335d663db5d39ea5cccaa55e75d221a082921dcd13022447afc539", + "fuzz_0232": "224127e5ea04a32e9d3d7e7fd5fe9203536c23789a59784f491e0830e382bed0", + "fuzz_0233": "8f8a1a7aee106a95b83defe188bf3a2e8d761d46c6773145fe5b522a3041afb8", + "fuzz_0234": "f9fd3afff215b2731a7ddfbca36dc84b2de448115961a5bc887bc94ee4f7e3f0", + "fuzz_0235": "824ae8629f3eae4b8775137b79d009cc1b9be46e6050fb03a5a2cf4f61bdf814", + "fuzz_0236": "d6eb4e5c04ff09b00036cdea4f262880d7d1249da08bc4eea2ae5b15d4094f21", + "fuzz_0237": "c03406d0ef7784d76ddbe8f6cd1430a28bc637b090cd25075b88cc29cca78d39", + "fuzz_0238": "7db495e6011b072d8a958b138a4f954db21e7059e560dc05424bd1459c22efb5", + "fuzz_0239": "73c6c9ab7a950b957d0192abc52fbab5934a7a43929e0bec7b71bab645c05d8f", + "fuzz_0240": "07b1ebd616343e37cea496d9eb3f50c3ff4776e6c4cde77a7dbdbd492c093e7e", + "fuzz_0241": "f54a62ca6f9d88913a1b24b72aa2da6ab650b65b83461de87af0abe42a70e610", + "fuzz_0242": "9fe88b1f0056ba28497fa8596f8bdcd5fe996c4d5b884f1a9dea21defa6260a6", + "fuzz_0243": "725c351aeda897f4b84009a5f8db730ce97705a06fe5d410ec2a9ca5bdd4d0c8", + "fuzz_0244": "7acd7ef7e9ae500494b449df5dcde24960ee41c3fc1a4f0131d826324be788e3", + "fuzz_0245": "9bd80efbe1fa55224fe021830b01f75c7c87d52933c6439896fe598a6fbb3708", + "fuzz_0246": "d270292de7db95f3dfdd108e946127bba9d5cb65535a3c04413bdf54b7d7c381", + "fuzz_0247": "da5086ba48db19a5cb3c2a1c61baec743d56846ce2a6cf1381ea4e8aa4c439ea", + "fuzz_0248": "1d75a450e21639687d07e13b7713504be9cac79324e3badc8a380356a3a2be4f", + "fuzz_0249": "2999f6ce90b731fb4c678389c39fd34211222e57973bfdd80a359efc3a2b8bc1", + "fuzz_0250": "f021ea45177d4cad5214e7bd3f4d986f33a8ee98295da973e83f1d1e7123aa43", + "fuzz_0251": "3158af628f0fa22f3d2996df4b6854bcfd5054625d202651952bfe742570e6eb", + "fuzz_0252": "b9665c93124d9d8860e8b2cc1c7b2ad5724e5f5ffe5f1a4b234fcf69d9e91444", + "fuzz_0253": "071d8a72f3b21c2cc74d2012a04218f6b8e6f9dca8c361c6e699207f47a5d1ca", + "fuzz_0254": "f153dc27b9eb4969a303007cac8ffac61471cb5361cfdd86c686128c62428f09", + "fuzz_0255": "fb39fa5f35c8f5197e98bd71d21150c1bfa638409dbf9db6e0d0d340f56d487a", + "fuzz_0256": "cf2e3726e04e915adf1079cc3140947564459a210e1b29481e4be6bc01bb8c94", + "fuzz_0257": "071abeb6efdf052a2fc3bd6ca40804e948de3c272283b41135df19030c721539", + "fuzz_0258": "c6c0bed8e61aa6efa848c8071b5215b26fac5d2fc68ea7c3b4d937618068f5dd", + "fuzz_0259": "62f961d926cd750c068bd7c50208bbe39637db74496cecfc4167f9605c17251a", + "fuzz_0260": "5dd86efb745ca3a0545cc4e41a0d1bee15a308d574a941d4aa409a6b4cdeddeb", + "fuzz_0261": "dd0e8e7fc6f3bd939e51f075ca09f0cdeb3a4c725a2ff5abffb46588fabc111e", + "fuzz_0262": "5f1f0757b2612db6db0017d65c0bd5001b51b9ed8a51da1de26ffd6b3fe736e4", + "fuzz_0263": "8660fa1b858d0a53cb5f0164555d834277e90fadbbebff00d66aa8a21822f9a6", + "fuzz_0264": "0834392c068829c6d0270a9e3afef5b4a96913424698058f000cae372fddce81", + "fuzz_0265": "a6e0b12a2fb033f0360939e9fd264f864da2932d3bbdcafb09b92dabb35bea3a", + "fuzz_0266": "574d14aa32a56919134bc3a522fb7e922943aeab7762cde0c2b82495e5c7304a", + "fuzz_0267": "93e2d26231009caedf969c8c2f5514a5ada0a33050f609d1bb6775de6d55e20c", + "fuzz_0268": "331a3d8292e74f5221e2f442697675488bff2a48a7e9d2959effdc4eb8e2d7e4", + "fuzz_0269": "eea6da7e4258d3dab25d56b050242a0f4ef20b9ac803e22e4f3db7ddcfc29d90", + "fuzz_0270": "4f735f2208a93a8127dbf884f94a7b540f0453c175d43d20129c9d79e14a1c15", + "fuzz_0271": "5156aa5e688bbace46017e45c61827a440745bd46bca874560de4e13f5c90a08", + "fuzz_0272": "bf6e2b1418614f47a7b0978767b0f6700bdc90202136c3f22510ec0f5624246f", + "fuzz_0273": "8c527c89e71b60a40e0e49d6283b5f7490fb25f0cb7fcb9c9cccb64010e88cae", + "fuzz_0274": "efbd15ceea61a9c00cb1dc3357cbc0dd9d4641f328ef028215c2af5d680ee2aa", + "fuzz_0275": "8bca99561534a1a5bade91ea10b39c9069c7958e80e144e586e46a007c483626", + "fuzz_0276": "9596bf108d724f454beb5dc36d50d85c19e7606f47bd4800a4fa7d4deeb24534", + "fuzz_0277": "2a9de442ee336711b6dd7eb194097d73651866330025c12dceb73bc3d68d2227", + "fuzz_0278": "c08f0ef3ee78deed7f1f0753ddbb11e5713e80fd8c6a519a8a8ad757b4d7da55", + "fuzz_0279": "89688bfb199a7149a4d4663be6f05fdaf0c5cdba155af7182f2fb9a0583d0232", + "fuzz_0280": "8a2830a82536dae13f0a21f296f84e204c3c52b0dbcb2b564073e0dbf51ecd2a", + "fuzz_0281": "712d22ad85005bdd5eae9cd62aada5e58dec26c81d39e1824dfcf998d06793e9", + "fuzz_0282": "94748b8ec7414caf3a2bebcac6b08171d2ab579c8836909932d1fb316f3ec617", + "fuzz_0283": "4c8c3fdb5cfacbe5eef09b685fa5b1ff9fc36e91482b107dbf2d55ba2ecdb3d9", + "fuzz_0284": "12aded7b9f5d29473937a110e815f11552ef623561b067fdced2b7cf207860bd", + "fuzz_0285": "5cf81ae77f559ac4ad45d67cb13e65c9a8f7c2ae7f399ab03edc3ba52795f5c9", + "fuzz_0286": "a154816dd537ddfff3792dec7982a1a46a575feb6e5836b9544a35dd717f4503", + "fuzz_0287": "c51c59ef1e318c68802edc177d9850934d35a468dcd59e804ea47cb65e915fc3", + "fuzz_0288": "e5d463a0f305f15902ac6374675fe8e7b9dc9d438c9805a542a0ff3016f99908", + "fuzz_0289": "ad40f57ea051e6ec6fcfd83695457938dd7841e8b4585d8093f248e06de0697a", + "fuzz_0290": "3fc09724703b88ccd344c5907b30e0125f0097dbb97296fd7398a1954b7ac349", + "fuzz_0291": "7013f629b324b090ef4b80321a656c72f8aead8944f8577b668b52b088094cdd", + "fuzz_0292": "9f6e161503ac99b41344ad571358d01f1924bc29a9a160483aabc7f8a9cf2eb1", + "fuzz_0293": "a0747a6faf21eacff19e84ee1269d24d5fcb91acf714c0e5dfc6af93fa5f2923", + "fuzz_0294": "28330b906d57a6fba74b1c6a783e5e36c9191d4d3c9fb0712e408bda58967083", + "fuzz_0295": "6dfae31f1eb63f8350e73729591cd32d59e29dc1227bfa5ff8feaa39cb941a79", + "fuzz_0296": "6fdd6a21e540b62ac6ab56fe7a4a0bc21087c58b723cf78eb239f236c301af03", + "fuzz_0297": "900467f8421d0e5eda3be7738d6fae488ae5e0762e3941c644d9c6faa147d400", + "fuzz_0298": "eee425a5e18ab84231fbff891f3b532b8008d60208ff26c869a5b6c723705ea2", + "fuzz_0299": "c137b593a3bb216a5742538f162d324bf39950dfe00f3c7473f960b093c12388", + "fuzz_0300": "7fec7b2ce64bb8c8ecc27db7b4dfb4e2ec382fdc9b691eb485cbb8a8d43c6c06", + "fuzz_0301": "ad6335cb28f11c235b96ac5767ce5dce311790c90ba4f4d3435361ccfa29f9aa", + "fuzz_0302": "c98ab85f6f00df8d5010ab27731a05c0ea8ba7381263cf3ce41874ef8525458c", + "fuzz_0303": "83b30f23b1076439f64ae5c2b3727b79ddba833f3020134c94357831d08d6dcb", + "fuzz_0304": "2e1713f4d4192b8765ec37170849091a0b7c8517fedfee5d07ac3a853bf004d2", + "fuzz_0305": "304a6aece5b12e18ef6ab1d1d271c00901fa016cf170b6573ccca1afabe508bd", + "fuzz_0306": "1197163e82b5084b88032ae4167c94d80f706630783d0d05de7c464e15be76ca", + "fuzz_0307": "5816fd84d9cd0e780ac46b2909cf3fd60f360db097e057cf3ef2cb3f37a2db4f", + "fuzz_0308": "ce15e7984a1b18b212aee59478e46ac2fd942f82257b9f43ec569c45bace38a1", + "fuzz_0309": "49cacdea62d9ca65ba6047a09036e2bbb099df0acd30ff454f58f26584266c65", + "fuzz_0310": "dd8a10f07de12c0a4bf854563fd302d38bdfa84f342aa68a513418a4f517c7b1", + "fuzz_0311": "373b4c2298ab27212b5ad450da25c6d26dd89d70bcdf222b34adf03721934a5e", + "fuzz_0312": "22ce01e0b71a0c8bc32bc776695c758140b6339a2db469b5b76ec8d8dd2e67b8", + "fuzz_0313": "ad7842d2e7f53ddf4b456076d60c8a9f58db3cb4392a71bcb00ceeed250a8119", + "fuzz_0314": "44ee88e4da2740e2a360ad8a8e590e56f4a780ae75ee50ee62dc3a4b6a66ab2c", + "fuzz_0315": "0beed347624dcfdd81648db315b7343fea3f7160265d61d1058f104ec3fb4ae9", + "fuzz_0316": "58a00ccfbb2ae7464763770f8af83c6020a6486467e29933d3075db5c59d7b4b", + "fuzz_0317": "41be2619993ebb6cac66e32c9383fc82f714033d35cf3da042bab2fd15fa8436", + "fuzz_0318": "c51da6a5dad4bcb59e0c5475088147945bdb35fd683630ba67b7b7c94255a7d8", + "fuzz_0319": "6f55c428e605d0bd6b5705a7a257e0419e541ec5299dbeabc6414828b9da01e8", + "fuzz_0320": "f557982bc839afed467d6bc1903a847d4a8f64fe67891907281df7c293747374", + "fuzz_0321": "2d27962329aa249425dc632aed8824812bce6b888e913233009be2afe85dcb91", + "fuzz_0322": "8960c7cab00fa34f48cf74f343b5f76819b7d1f1a3aa7137ba8a2edf66bea03d", + "fuzz_0323": "686e928302d42092498fdff0480003be46ba5c0b10b1cd299c9c961ac3b4437e", + "fuzz_0324": "fff2eed49815f7e54c7ed1ec7849e7a92ae306082e4711f4fd8b5991fc42e298", + "fuzz_0325": "14309e66d59f9e02c9e7a5636aa7f150c04aa297f7a5cbd622dcd103d571f663", + "fuzz_0326": "aefd5c8a38774d34efa445097b7715d73f7c453f10db3631fe339ef726f7936c", + "fuzz_0327": "0ff01b7be5e88783a3e761169b7f9829edc9ec0ca8e8432cb0b96fa3e8401ffb", + "fuzz_0328": "8c4f6629e51dd5224b64159becc531f50d6495047a3e25649cc9f09be7cf9bfb", + "fuzz_0329": "ec2c018f6d3475a97cca7044e7fb549f1bb50356bf7ff238e8b5b2cd17ef929b", + "fuzz_0330": "799d46fda297fff43f641b6ff5a5c51de530292fa13d4c58c3650154fddb9b1d", + "fuzz_0331": "a425544eed2c6d9d6dc996553d99c93a3a641e56c0c408a11728bd1a9bff4d1d", + "fuzz_0332": "5547044e605482866f88f15dd929791f797608bdcbc81857e543b82bb210100b", + "fuzz_0333": "bcdf97c6136747c96870f762315667116e34d6e2df370ca90dd0f82a634f4c93", + "fuzz_0334": "4ee327344a7083dea6fce576b05ee97f5d1b163146fad1a88208b126ab251392", + "fuzz_0335": "33a67ef7352f31e1f6b4612448030569c7bb3c0d4d44b791f5a8c5721976f3bf", + "fuzz_0336": "0086ae843cba5cad3758b16e20f162b25cd91cc75409df23f39d1e86edc3d939", + "fuzz_0337": "4d5d98755fab8da6a73b5fc38184c380f3a47f6d3f83e0dba74ebaad9347bc72", + "fuzz_0338": "dbe956b26c6f1f1127f80719f8660c951824d7eeffd131603218df22bdd01817", + "fuzz_0339": "0f568762532791bf0170ab31c9b13fb56501466c2d6b54deba012e321aec41b8", + "fuzz_0340": "75942f0039509b8362b6c9a4476e70bcfba6d37814703e18bec61a4d5d810ec2", + "fuzz_0341": "cac2b2845dd85ca0192861e3a7007c3d76a40e262c90b7858ff44d5c1f0b5e51", + "fuzz_0342": "b115aa6c8b731735d53502e701f1d8afbb420bd5e547bc2a74448d40053b944f", + "fuzz_0343": "117c0adead79723ffe0786c8e279ea51ff383955815781ae5ecf3594148afdc2", + "fuzz_0344": "43be753247ab03297ab22fc5cf9c29d377bcdadabb00f34b01ca14ad1642c9cb", + "fuzz_0345": "0e6800919fba6883ca26420afd83b7ac8c66460fcdd80f227dc357515cceb2a2", + "fuzz_0346": "4256caeb04f8d670ca1e935bb49c3b01670dee5819dc556148521918bdce848b", + "fuzz_0347": "930aa544843490348d27c2f1cd79611d0498598509c7710175b8960f42215e22", + "fuzz_0348": "d9d055b2586dacfa73801622d006e87bdab83be1724054d0b7ae08b8323aeddd", + "fuzz_0349": "56a09301d39a09d0d55e79b7b84ae92538a4f18fa469acf3de03e90b6335d784", + "fuzz_0350": "cbd989bc0d5b2ef8d643f027bdc32cee331cebb38aa78bba26ddcd90a5c5d374", + "fuzz_0351": "bde89a9fb78fc379ece395b1e861139c3bc5e1606329ffa822430da5e4b20032", + "fuzz_0352": "bd42db7ebc9b2aafca773a7665ccf1a5c6d1a0b192ca514db3c9a0c34ae871ed", + "fuzz_0353": "e87835928b388375dd103507e0e7c7b9645de0a24dfbc5eb687f9aa04c5b3655", + "fuzz_0354": "aff5fda310cd547cf71b486dd19ecdd36eaa113842c0e0c8bf77750f33250fc6", + "fuzz_0355": "715a71c40cb7ed1de1fc4b5ef2f0e95ced8b80c9f7695d516395f0b4a058a878", + "fuzz_0356": "43807907cb19dbe79839473b5fe5d87c9ba0713112e13817e2c77643c6cd622b", + "fuzz_0357": "768c4a55f3e6dee247f699b51bbac3b1421c5e5313c63dec76cc910c45bf7e90", + "fuzz_0358": "cc5c616f4d8be28c3252fa148dfbfeca3dec627857b8d0447ab8540e972dd5e0", + "fuzz_0359": "d284aef930956988cf3e6d363fcde4658be4c2d051e992cca456785232b3e24f", + "fuzz_0360": "85c8acacf4226c4f8ac45f7f30aa24f038335a560e42039fa34fdfc6c567f19e", + "fuzz_0361": "da8c2d306e74a15f04830a888372d616bee3d31673e76b1270f2715782f3e1ac", + "fuzz_0362": "fd5c176609e752dace1f837c9eeb1dc9d232544632f7d51913d1599a41a6fdca", + "fuzz_0363": "df48bfdd20825548fe5844242ccb37cadff90f8d8755d12e171ca470f8d63b50", + "fuzz_0364": "f739b2b4effb3e50d093859f26805381cd0e06be5948da14b652c62421e85f09", + "fuzz_0365": "6f04cb6d550d5a169b265b905af0547395e179b8df61362679d9f560a5f4b03b", + "fuzz_0366": "b3150651b7e1bfd32e3119041d3c7378a7b8095ef8de8fd71ddf8cfe9c20cd54", + "fuzz_0367": "c9d664b0bcd4ab4237760a45f6e6aea5fdab4479bd6cdde744827c8219dda333", + "fuzz_0368": "024cfa3634a3aa9f8e06876e82e3c5b18062b07321969db3c823cc5956a5187a", + "fuzz_0369": "169593c6c5241ae66c373039aac0ef6988b846dc009e853ea3363fd7bf2377f4", + "fuzz_0370": "45ef48ad9e5e501a48c22df887cd9599151bf8b8acce7b5d226fca2e2abf631d", + "fuzz_0371": "92b980cf39f998029c45fa630cc6814c6fd9a92231fce8f832be7bef52d2bdea", + "fuzz_0372": "86f0520db72ccfcc5d821ab7e7240e064e6f3cd6d3edfbb986f623343319434b", + "fuzz_0373": "6a349aa99dde8490104067a279132d94faec71adc12666bd93104e45f42ad7c1", + "fuzz_0374": "5c5eaef5035782d6b1732c2b2a19588a7b746a2a9dc727a8b3cf1477ce5d9d4b", + "fuzz_0375": "87b6ad087d7e8c245baa9803b33eba8801e99b4ac454bf7cd75ac61dc0dbe10e", + "fuzz_0376": "0d28ea96048091f22bdb1096e364f40fc6053e163d90d64a0570b8e8e90ddf73", + "fuzz_0377": "2c6f6e698e1f142953b7685246b31ab6558e52571954ff23b7663ef7808a82f1", + "fuzz_0378": "2ba0cb1b1d6882d2ed71929e8621db2e8a611468527ef4024783f62793f49dae", + "fuzz_0379": "080f35ee428b2dafe27b590101551362ce090760350b2e419f9ab7ff1c4a0759", + "fuzz_0380": "fa7e96ce256af2ff4e832bfd23b7fc6ce38a4dd22776df989752dc814cb26d01", + "fuzz_0381": "ab36b0849f6c474e58f321bd2c0f311204633a4d1841018277132348ca214684", + "fuzz_0382": "09f82a88741f0df2a6f276b55312dd641740975129c0f9d24bba4712d195ea0c", + "fuzz_0383": "46b26f3a84994507524992f2e1ea0c38340a14841a0c818ccf7b3f7ffc488fb7", + "fuzz_0384": "cdd901a6d945143db67967a2f2086432c924083632eae438ad71de47a1451035", + "fuzz_0385": "04d7b163ac83e7cbee2ed867c29234acd61b670a6cd3afdd485f4f9d3cc0050d", + "fuzz_0386": "8fc7794759c4217ee36624aa7f1f0536b8d9dda0764637e1217ded66e53a7bd3", + "fuzz_0387": "a84af67edfa1785ce0303fdfad316dfeed5ae43eb563502f73a05f491beec0d0", + "fuzz_0388": "8ea823c6d69e2f5c7ff6c96e7c212bcf96e5808db405b02a8d63509acce4ffd8", + "fuzz_0389": "4686cc827bdc21081903247c5a96ee49c3b75e1e1a213aa5b3733b62e625cb4e", + "fuzz_0390": "c48ed3a8d247b8bc9cecd2724cd2590c445f82eaa6068b18ea66266f6a6fb5b4", + "fuzz_0391": "9eb1a2681f7e128bedb97ae9b146230d6fb63e24832868b19b6eece5dfe6c7bc", + "fuzz_0392": "f869704f6e5584ddc4d3bd3c3e8bf906f07c2189d76715a73529122c3e21f7a5", + "fuzz_0393": "acd8134e8a7d264b5a9b9903bed5800ba682092816c38e71bc072cdb19818bcc", + "fuzz_0394": "e23fcbb05b501a25f2f15d5b0c92696d96a081bded8d3adca38274ada2f631fe", + "fuzz_0395": "eb6a63039ede73c5d770c762b1025364b24890ab9818bad84343a957722b630f", + "fuzz_0396": "d7c7f92d85061abd965d2a4303a79e17e11e63c41672c98532e83724697c4205", + "fuzz_0397": "50dd62ab27cf9be50fa7835a0e0505cc0e09670d35efc8325e1cf35f9a2fcbc0", + "fuzz_0398": "ad05ceaacd248cf7867f7041f6377f27b48b83cf5d1e58a2c053565b830ebb29", + "fuzz_0399": "a00505b2b68ac766dfa60d978694285c2765805f44820dd41965936a74f10b0c", + "fuzz_0400": "271f4927bbb390cd2c21c1ae1bc266ecacd7633003b8fa78de60e03b3dfcb14a", + "fuzz_0401": "fc0658a3639a175380261c6165cefddf43f3daa4786b647cbd79460813891ca7", + "fuzz_0402": "7b349808a21c2fd29b8a1aec8f8ca58702dcbc066027e14f6f0daf58a96e07f7", + "fuzz_0403": "b986bbc886d4a26118a34987828b7deab5343ed44bfe545ed9061a9d4254017a", + "fuzz_0404": "031a3819af28b5a3ad407b04f08ea58e647f1520252a1d42b5c2ec7b3378cfdb", + "fuzz_0405": "786555c52797f5ad6f0f0941aa74413b8c229d529f74b165202bee8e60aeb21e", + "fuzz_0406": "194bcaa87c67a69e6130694da6f51a7e68869fe80fad28111bc8d762a3417edb", + "fuzz_0407": "e65dd55ae1065b2398f4e3bad763bbae7da15e8b1d4079f6c67c6583a6779a19", + "fuzz_0408": "c8e401c0e6ed19440ef472e3a2a08adcbef561a4926c7106e169848997ddbd44", + "fuzz_0409": "2554a72408b8c3ed1385702c6d110fad084ac0cdc1a868eb85f0a09401751a5c", + "fuzz_0410": "50b600c8515760fd23defb5f1bab4f8b4e3f65f86ac95167b7cf3af2ed29afbc", + "fuzz_0411": "ba2ba052533d995bfa79f24776081e5cefec2ea4a914a30bd77e88d44502f739", + "fuzz_0412": "a2760357cade56fd81f039ed73f8167c68c0eaf3ad66577d4af79fe772e95d11", + "fuzz_0413": "031a3819af28b5a3ad407b04f08ea58e647f1520252a1d42b5c2ec7b3378cfdb", + "fuzz_0414": "c6c1cee8020f3b886514cd8b54860cce3bc0c0b8ff82d0c936e9317c24352d7f", + "fuzz_0415": "cfeeeeeef4aa20818b4b89550556b09583e4aa97e518fe62c974665223d13156", + "fuzz_0416": "7d1912c03ac2baeec19d0f31d735fd2502efb4a132c1ef720117576306e564ca", + "fuzz_0417": "719b99d8b3625a2b192aecb3f139d7af0aa398a6d63cfa1300a1704cadbec37a", + "fuzz_0418": "75e278f827e4ef3caf5256a998afc34a1951f79ab7f9ec507c9495a44348efa2", + "fuzz_0419": "ea160184c6c4375e5ae1e47b8dc12c2d093db8a4e3a3945a32b101445cf4b2af", + "fuzz_0420": "087718c6aa4d32c723d60d59328a72a958b4063f0ae5c99f14d309a878afb907", + "fuzz_0421": "e72e774b55599dad3c8fe103acbfd719d7e4c02af64e85f9ebf40876c1aa0c83", + "fuzz_0422": "1b371ac2e13ab69f025d8d7fc872b101db18b3404a85d1e2131dfe1c4808d4e3", + "fuzz_0423": "f180c8c45d53fc2ac55b940095be152969e148da4f5644b21fb98c99e259b829", + "fuzz_0424": "46b7e08802260f58f36e13f5a64bcfcae11e1191cdd26f0e2f0434514ed3da32", + "fuzz_0425": "cc22712591d43ba512d415ae211fc2dadb8dc9d55e55b9f162ca11371f3ea893", + "fuzz_0426": "2f121d9cff8a4157bd11fcb77bcc8c96a791eefe7c6037894b24118547f9d0d7", + "fuzz_0427": "26091fb09ddebecdbe164accc91a54fdacc1e9e7b305a04a40d0e231d7eca52c", + "fuzz_0428": "02b232ec432039c828f0fd60c6a949a1224281fba2c93a7de635d19cd10826e0", + "fuzz_0429": "922ea9b8b463ba069fcba55dc149ab6ab4cd0162a90ac9623255c0fcbf90bcb7", + "fuzz_0430": "e87835928b388375dd103507e0e7c7b9645de0a24dfbc5eb687f9aa04c5b3655", + "fuzz_0431": "d6e719b92a7be2960e9b39d927c3f288d89940fee0618ff9b8432882da09183a", + "fuzz_0432": "ec56a8f5432ee44bf0db42fd0257976be2dfb3d82194cb912d7b69c2614a1336", + "fuzz_0433": "4939db3bf9a1f84a7fc3a40ced7778143d1a262a738cf58bfb2c79708960523d", + "fuzz_0434": "7e6dee684b04182e3e2659842d970a67d9374318e7e9a08900658e1720257f95", + "fuzz_0435": "b774c74fdca82774007a7b52f56b98546ea5f6b7132ad9d9d71639921ac9ba7a", + "fuzz_0436": "4d512dc3d41373f54e88c71c02b768196b68f7eec97b56b34294d24b22f7880c", + "fuzz_0437": "fcf8018ff7a751e421ece0e44e49a8371ae8a7f7476cb0f8ec0b7c0a964adcaf", + "fuzz_0438": "a88d8cf02b1f757e5e479eb57a04440fa229bf7e2726f39c1a93e9d9ae98c433", + "fuzz_0439": "7c399067482bb147e13713296b49863d337d3995072af4a3ad279f4fb629afdc", + "fuzz_0440": "5b7c493d7b60cbb5c4bb60c765b565882993a51e715c8626caec451140ea025c", + "fuzz_0441": "937e678d3731cca5e9bd3e688b6eaf4a179dda28733d711f6efeaeddcd6f503e", + "fuzz_0442": "a64d64f06942f7c4d351a8ba0fb9aca9e6ee6018d27c63dbf112378deccbf744", + "fuzz_0443": "b6469959c181db45d032aa791050b08379bedab300a69ad8bc9433e28c133422", + "fuzz_0444": "24a1fe3873eceda9c7ea4acabc1cf21b57dd1d703d5c5056b949c52fdd94ec11", + "fuzz_0445": "37cf8e131475e1da1774257daaf1f5b808b3b193136b3e6067a4bb006bbfa80d", + "fuzz_0446": "c3364a310a466fb6954aca752f7e88eadcfaac331c7c918d298a9324aba1df72", + "fuzz_0447": "9367ba6e3b69da31b523ff542089454eb0f0df9a9a7410c53e5fedd783e5da46", + "fuzz_0448": "cbfcf2d1ddece631b97be5477457916c32c49164e8202b8bb0809df2048d1c5f", + "fuzz_0449": "4887a2c704a32cd1c4b0724e6f483abadc3ec00254dae5e310030f5a206ac3b1", + "fuzz_0450": "1b5e78d91291784312854f4db506aa8a2d09ef20039e7e326b50f3a4bd52e593", + "fuzz_0451": "95a96db672df2759d21619d47a478fe049c2944ba03a5620dbdf597c6fe01d57", + "fuzz_0452": "b85f2bd4dc3dd6ef0ec8a3b54f69d04956bde110ad8f8d80e621ad748c6d05e7", + "fuzz_0453": "2432d9a983449dda44f68e8a342bd6d8c2cc426287af6e22dfe9cbe302bfe438", + "fuzz_0454": "d020af0429b7d0cff7a4cea954440c3b9b14878cdd21039c10a6217c26f25ef5", + "fuzz_0455": "f093142a25dd83d527ecdb2f3a708347687b0a36cb0ccb2f4c8211f2b0cc7f6a", + "fuzz_0456": "d4388853b13f38c640948d950123e33265d9617e777fc8b07d376ec7fe87d532", + "fuzz_0457": "816316c3e2ac3e7cddfef0ac30beeb8833784d3a136fe038877cced7f05c7dfc", + "fuzz_0458": "97de170f64243c9b9c051910fe0b21e093c27358c33aa28af531f627dd9f6749", + "fuzz_0459": "1fffbc18925b2075e33707bfdb9c5c446e093d00099a34c48a2e9b5226120fe2", + "fuzz_0460": "ef84daaf98ecdbf7bc099c552281d04619b4dc2836545513d05de0e64ce8aac9", + "fuzz_0461": "e720fb57cdf94e6073f9526accaebd56436f932ecb5ec3e5f578b0b9fc680c8d", + "fuzz_0462": "eac41f2e96454a5abdbe77ab5940e913f0a074c8999bfd9ec8d34e41ae907147", + "fuzz_0463": "d69c07b43ef842beb735c99312ef832922c1324d616ff725d35d3a980b517247", + "fuzz_0464": "dac1b9f62c653d59970b9401a5afb68f320993b47dc1558064d973c55cd6810f", + "fuzz_0465": "1782ada72b2e2d95cc092470b0517ce1324184cc21f07455ddbdd5e1278c82e7", + "fuzz_0466": "f1fed59ea58fedbe1ef08dc8f3267099b009885a09be7b0da3a29fa50628875c", + "fuzz_0467": "3a1f46a82d46a6a8a9b9050ae67494898ea5e7d4d9d4734f808d7b693b3f490d", + "fuzz_0468": "f863160643718f75276679daf54e311951e842392cef31382e3de9414fb9b224", + "fuzz_0469": "e717765a69636473a1986c5e429e6edff5827521386f428fa5686aad015720c5", + "fuzz_0470": "92296390e87b2d65513a93293ef02ee7d5288481e5ba6b37e1cec00104aa1eae", + "fuzz_0471": "48774c10b1b927054d534a6cc19dd4812d092bb8c2762ba1181988d977b59126", + "fuzz_0472": "f97933a8738dc44938fec471da0a66ac12504c5fceac163eb233363507890214", + "fuzz_0473": "e6f8401d06b13f27b1a73823e40880f79846f893ab3c6ad556999ff7461fdebe", + "fuzz_0474": "eee8de8df10f6eed373d2fe129633cf46f90b3654d23d0efb987e778f282b9ce", + "fuzz_0475": "06e7db5f1b452a6d81b9b9229ae6d5770cdca12a4784f782929fc90846b7a822", + "fuzz_0476": "2cef3069a16efd8b7a726efbbb77691735edbb88bcbaa4cd4756f3278b7a08d5", + "fuzz_0477": "1807fdfab8e5de9142abcd1b1b1133b5ef9cfab395293e7054ca1097b80a5db2", + "fuzz_0478": "7faa4834b7aabae853ac0922530224a3f5afdc37d96ef873439211bab0a5c096", + "fuzz_0479": "61ad91b782a81ac754085eee37cfcfaf9b270b4f24e11d71c76fe92bfd81374c", + "fuzz_0480": "a0172621ea29e0bc2a0087aaeb46bee7607250e9814d368dd3bc5432231ca922", + "fuzz_0481": "32449ee384b056df21fc49d9ecd2b5d7faec8f91d9426f96e48edfc54209ccee", + "fuzz_0482": "0f2e55d82b62cd49c2200d0b027c8aaa2dcd0a6068bac893e8d2b0adb4bac9d5", + "fuzz_0483": "6f0f0453243c804b208c1b23c8d5131543e5c97e0e08af8b7fc6ce0bad426d68", + "fuzz_0484": "a49ba1a29be4a0ce32f38771ea097042a32b5a6275810529b4bc10e85faaef34", + "fuzz_0485": "cd6329abd7faa08f77a0eb1948025186db70712933951d5631e24c60d5b69442", + "fuzz_0486": "031a3819af28b5a3ad407b04f08ea58e647f1520252a1d42b5c2ec7b3378cfdb", + "fuzz_0487": "5dae1d6f4e01569c63780a8159c294d0363e10f0dbc8a792bfe1f8080e80902c", + "fuzz_0488": "6054f3d7a1fdf6847d837ad62e4020fd90808b385a1bb2d0f5448abfb43b6369", + "fuzz_0489": "95bf70ab878ae5aee9d4d65819b1e0f4469487ba34822c31ca96306bab40a7bb", + "fuzz_0490": "b9ea6a0faec526d5772faa5557ad43efceb57a52e355447c90ac7a1394c87319", + "fuzz_0491": "ca6b1b3922270531400cc98026fa822061430db2fb9013cde7a78310f1dad690", + "fuzz_0492": "8cac9d44ee03e048f7d8c6007c705f6e688fb1e450ac763ddb1228e012e5e242", + "fuzz_0493": "6da420f9965a2e333a41009eada3585ecb53f58869d1d0a33078957c1b706efd", + "fuzz_0494": "de27414f9747cb20bee5baad547f101a93a8f35d959d208585ab52d8b2d7bdd5", + "fuzz_0495": "c68dad1f1d5637c06b07b81da6a7750286c561430ea6ec0f808e28cefaa93657", + "fuzz_0496": "dbd30efce822b3b4e6bcd6f3304fd4c12d23009ee6fb3876ab7bc65108ed5170", + "fuzz_0497": "ff3fda794ae74663737700f76912fe4277c64b6099325999b938a3491f1dc04b", + "fuzz_0498": "553e6712b2814da7df1a9b3bf07b58dfe79d24331347e8352fa0fb2596e457b2", + "fuzz_0499": "cc22712591d43ba512d415ae211fc2dadb8dc9d55e55b9f162ca11371f3ea893", + "fuzz_0500": "d97d8442c9726932e56d5e44fa9f3d11f122b1223fe96aabe26e8cdc6e26a738", + "fuzz_0501": "0b3566e9b6a6dbc60ffbc5caf5fda2ed16b698759ee5aa334d054cfc3cb3901a", + "fuzz_0502": "fb2108ace1d07f77dbfb85a814abbe824e30784140ad6269a0efd427a45944e5", + "fuzz_0503": "2fcdbe5512f6d2924139bf5ca8f0c19720075f0278741a1b122ee47700030294", + "fuzz_0504": "c29bd71fd01ea9f658fcbeca5902117c4eafe72ffbf8e55beb8ebf4de89d1b50", + "fuzz_0505": "c24b0277758f3f2cd33679559fd9b918b232ca0b1ffd2744e0d8fd9e5b57875c", + "fuzz_0506": "48c2ca0a8c2fabd4d82a88c5833d31c1f5232772a5b8aba1bbeafd43eec89a48", + "fuzz_0507": "910d76c9e2f457415c03af44c5fe0b46cd39447acbf711666319281f44fca781", + "fuzz_0508": "b3ab23f6ae4dfe969d67d7e99d123b2a0ee69f50848e4aead6bf43e1a3fb6a5e", + "fuzz_0509": "36ecf81504164bf181ebcc25f74d5c11ece3bc99b412dfee10182d4bf5f9287a", + "fuzz_0510": "0c12cd297bb1436ecfb9810c52aef8368edba58468947c0bc49ca51bf3bfbfb4", + "fuzz_0511": "4a55b5b0091311c425e234759f1bf36eeff36b02104e99a1e2947bb9eff732ef", + "fuzz_0512": "4c79bb8a6577b039198d3aaebe188cdcbf94fa1739d9df71d91134927d9ef7d7", + "fuzz_0513": "da59ebc2d2d5e51727b46ff8ca9b14c4df6cd5ee12e70859e9d70776bbe680f6", + "fuzz_0514": "354cdc35b04278141f602b6be3348c7f6c5be38e3411d3f43c483d12257f97ae", + "fuzz_0515": "879f48300870620272961bde2d8df7c446717267b716a2a28ddc9dbfd966ce93", + "fuzz_0516": "f38524fef312182d900cbf78d9756de3fce4c9f7f65d88b28a5b5eb7eda80c0e", + "fuzz_0517": "27dc9e54f3653be67e1e3d205280b0296bb9adcbee7f81c90f89b5d1c1d472b0", + "fuzz_0518": "031a3819af28b5a3ad407b04f08ea58e647f1520252a1d42b5c2ec7b3378cfdb", + "fuzz_0519": "4e70a7842d8e870b2cff68b3068f856b1f0d97ed21501bab0f6a9c7af51c1b52", + "fuzz_0520": "938fe877215d2be35d388840e8cf3ba3d74529fbc2f1ccf03297c6011f7a83fc", + "fuzz_0521": "c278feb0b22c6c7faedb8c8672758180ddde64ab13c8bb0f0157849fe487a244", + "fuzz_0522": "1c9f7a3c84fbf73865534e09722c55042f451cb680b34cbae687587ceec49e72", + "fuzz_0523": "6425f9bbcc11076147c1236bac4b2243c8d6e32d268420eb76975a81b40848c8", + "fuzz_0524": "4af56171828b5e58c83fc346d454bde4ba7fa530123ed9bdefb170bcd1286c18", + "fuzz_0525": "c359ecbff185801654b2fcd3559fe6dae75942ace00b415a65ea61b8365e780d", + "fuzz_0526": "08507427b179159713892f71745cb718a00fdedbc42be20fea79aecc358c77c2", + "fuzz_0527": "9d07ad45d290a526e398df9fb94af28d4b022a3e6db448ae11c942d36ad274f4", + "fuzz_0528": "d42ffebd10a397a9fe9ba7dcc99bb281f6cf7c59a2b4f9ea97b93345b45ec82c", + "fuzz_0529": "6c2f18a8d2fa9db858abb26fa4f7aa950c18f5de1e1490bd9f12cea9378238f6", + "fuzz_0530": "af00d73098fba0890fcb5d0526690455996fdb8da837ab2e16321e1bd9058745", + "fuzz_0531": "dc78277aebc8dff8be1e57c79a3dfd8948f9eded7f268606a8f8c08dee3fa01c", + "fuzz_0532": "679458bf88fe4e562127e500c2c5d4b947e33142d811dd2049ef524f8d846b55", + "fuzz_0533": "59adf766ccc7cf16d65e279b52f9914488697a3e83ad72fddcdd8f89a7dad169", + "fuzz_0534": "2b244bdd287ad152d25bc9be6f59f2a05e940412611789f7496e3249da52f563", + "fuzz_0535": "b64727a7bd7e53c7dabaaa4859c787a017029a766a77a8442a145d1027d6c909", + "fuzz_0536": "a1279f05cce1539acff185d3afe3b254e6726dbdae77862c730444cd03248ac1", + "fuzz_0537": "96c4881503f8c518ba057a4c84de35cffd847e689bae9e5dc575cc0417ce866f", + "fuzz_0538": "11b84a64709c42ae8c4174681a0c8aa31d38e03966b9b3a8c2b4f02a59f1fed3", + "fuzz_0539": "ed497b8071a9cb04657fde06e093f33057a34b1ff54084b30022a765045429ed", + "fuzz_0540": "ff5b51a3e6668c91b0b069d59c9a8c40954801fde4ec28ae1814fcdf4eeb8804", + "fuzz_0541": "116ca945c590710f341d22a4d1b59cfebf6ba37b2d6cde30d8b52a22cce319a8", + "fuzz_0542": "0422fb4c9f149317d439f74a7143a80c4ce5b7e654f98bdde49d25c303fd505d", + "fuzz_0543": "489dc054c83a6bcd5e5f0cad166f34fced80db6948c4039a5fe0cf4e68de1847", + "fuzz_0544": "bafaabe8bdbd5f4e4c00f49f7b59c68bfa24148d99af2299adcc35524821b011", + "fuzz_0545": "373becea802abe95d733a017723750d8bb0a31f75a87994b54d8861a74d00ffd", + "fuzz_0546": "9f1a8b5ebafdb22013c81ceefff69e5268622328b119dd4ca0f84185009d71b5", + "fuzz_0547": "031a3819af28b5a3ad407b04f08ea58e647f1520252a1d42b5c2ec7b3378cfdb", + "fuzz_0548": "d338bc9cfc5f40f1b7be7f6fa01abd88c42b41d6fe4afb9c173af0598d71b87a", + "fuzz_0549": "94c764bcb229bb8961cfd008f1371fc8a2fe3a7a563ab0ae4cefca5b3d48b0a6", + "fuzz_0550": "99c569b15a429f3794f2e5d738bde7d68529b5512aafa81ad8500cd328c3d971", + "fuzz_0551": "49108b3cda15304cea0086c29b88e3073c10e18c19e24bde75e1ddb5e1057af5", + "fuzz_0552": "62e2918870c962c588b0f0256f47a913e8cd9af61a9486d0d4b00d7d4a4e916a", + "fuzz_0553": "3bac424c188d86c0c330b3befbc88601b85224ac5484aa579c9a8c295ae8a234", + "fuzz_0554": "fea9d9da379dac700964048d3574d287f70cdcd153b735d261bd925a78553b75", + "fuzz_0555": "5fb4469cf9a6fc25363b224938962ba424bc1ef328bbbec91828723bec6b7c65", + "fuzz_0556": "c1f73f2307abc613fc665d53566ef0447ee0f92be492e1db88c377052d927162", + "fuzz_0557": "427feb9c715d82e68f31d7d8faf1692b7014cac8dc225f451a0fa1966709c29e", + "fuzz_0558": "330c6285bba3e1dd46dada70593507fa1586559b989fd61d3be1c7c57c034f59", + "fuzz_0559": "45008280c8e2ff0139289c3e6ff8bf3b9e710e54def729796e3abe6126363315", + "fuzz_0560": "d803391f17f8c55bbf8e6b9f6651c90f592cd4a0e1424a439bf144c239ad8c9f", + "fuzz_0561": "9fae99c4b615e1f3d0badb54494003c6e2dbe8ed0f6d71d9a09a3e9d7a671075", + "fuzz_0562": "ba306748af362a1d499cc513279b18862ca72bbdb9d8d8f8266aa460594ed7f4", + "fuzz_0563": "9abc4653aec2fef678013b6d9f04938314eeb4a0e6058546eb33d9da589615e4", + "fuzz_0564": "e2d82763d7d6d5c30bd70918905642c4440a0b61dfc072d22d153646a7945713", + "fuzz_0565": "5991623e362a2a87209725d40db74024e9ba07422a9a98e1c15f1cab5cd0f33e", + "fuzz_0566": "78646946fb2ca279cf92350615803f8185e326b50289f63e9605680b1ae47690", + "fuzz_0567": "1dec20733c804bdff51d1dce54bda23c6cf97b95759246648227e9ce96c43d2e", + "fuzz_0568": "a63d8507e56ccb9ed9543dd6c1d2c6be1a37d25f69093d1a9b3a40f9fb111ef9", + "fuzz_0569": "71990c0f00c276cac5b397b6ba81f292c795424cf021432a1710457db50c3600", + "fuzz_0570": "225b54a9235d35afc6e9e9271df29411cd8e97147819a4b659d76571b440a7e5", + "fuzz_0571": "504ca45c3a1f36adb5e037306de8541a5b1d269ce4c2b8dc370b0fbf5f2d6cff", + "fuzz_0572": "d1f9b2a8bc0e5e4852ed52673c863a486ad217be72e8232d3e7cd4dc51628a22", + "fuzz_0573": "f9172dedf0929d46b39023d5d827a7f9c9a8d97a6300dfb5f59136059012ab46", + "fuzz_0574": "3b3dc185a80f27f2254241309994cae14b055dcc56c32e7b9f6eddf7d0f94b35", + "fuzz_0575": "79dc75a098b9d9a42f013520cebf45e20c77a3347ad7a8f2d571c4cd5528b489", + "fuzz_0576": "227c3c0cb61648cca730433d4b2e825c52143fe5ea4579a6c330165ffe013205", + "fuzz_0577": "478056ec7204f53f1316b4a4d880e5612f00ed9b425e987f98f96bfa39562bda", + "fuzz_0578": "d2157faf1dd3d80de171877c5ddcfd439159e6d282629f64c308dc4c1895ad11", + "fuzz_0579": "de0d0119cccf4a8a13755791cf0a1ae5c633a4c4c32484ca328c6602cc1384e0", + "fuzz_0580": "dd85320a31ab2fd1c64aa009624c17af2c9bb2eee7b01a65a75da5a8d8b04ca6", + "fuzz_0581": "b439c6cfeedd3d8c63acebb6fe7b1886ba4e013ff2b99527ed717a4ce8d20161", + "fuzz_0582": "86e78af8e0150bf1b6f3da94684d9bd6352fc7b18a603578551797dfd239f489", + "fuzz_0583": "aeb8a57aa2006ee943e4b7efc7f244afdbfd103bacefaf93e35dba4390506f9f", + "fuzz_0584": "8660fa1b858d0a53cb5f0164555d834277e90fadbbebff00d66aa8a21822f9a6", + "fuzz_0585": "ebaf255bcf5063eedb335801dac263da210a3e00e82950106bbdb7fb8968e212", + "fuzz_0586": "c5fb8ec6f289d8d02eb4059ce5347c893a1230aab028fba024605a626c10c248", + "fuzz_0587": "d4698de2a3a355c77840932d28aa5db4922855591b9f3ba3bdeda0bce30118f4", + "fuzz_0588": "ee4eac7d320f66ee67786d31356307f5a04ebc5513e5e6a9913f789b2d3dc665", + "fuzz_0589": "ceaf2e469673adbc42036c6daa7e5df38b576e0a7cc614f25a9cff58316b4afa", + "fuzz_0590": "02ab443baeeac6934ee97bfb066962230b32c8928c96c9ed76ceb64962a4636b", + "fuzz_0591": "abcb961891808117f92074f0d4c09245e70a0f92e26d318a4718077ea6f48379", + "fuzz_0592": "309d99a75db26a59ce804c4ef4e97d9223f3ec3555bc5cae3eac072c4e0234b1", + "fuzz_0593": "2fe6b4f353af42346e45bcb0e14b97cb5b78bfdafac933219d3b4a68ff163070", + "fuzz_0594": "50df39b02134c60b01066802500812e9458d0784867da8acdcf8e8e6296e069e", + "fuzz_0595": "4b75aa3159dd4fe3700740fd8abca83d9f7733abefde6d827c9fed33b173cae9", + "fuzz_0596": "269d299baebf6cf407466e42e7370a99583280e1703c88bac020f6f9db5dca7d", + "fuzz_0597": "423a47abae54c97691a961034f63a4646ff9de1bf39876d422a1e24da7e8dad7", + "fuzz_0598": "1b1e00a898e48a994ad0134888fd40bd643a10302bb80019b98ac9edacd5d613", + "fuzz_0599": "67ad45e0beea3b429423b24507a15576764f3ffc593ce5a4c5976aea877ef720", + "fuzz_0600": "8e2edbb61f88c7c82c542cdf0d491f0b186ae42a97d8fb391cd348cb22d0a530", + "fuzz_0601": "0bde369e9c7474abc74290242a9424b6169d5ff561ff2bcfac1df1f526fce690", + "fuzz_0602": "dc3edecd675180a49601ca859f11d03eb5b8baa7f96528ce6f9700e5a036946b", + "fuzz_0603": "c4dd774513806d7b7e4849ba1577ba019ae1c0a9c833fe657a90d85e8ee197d9", + "fuzz_0604": "553e6712b2814da7df1a9b3bf07b58dfe79d24331347e8352fa0fb2596e457b2", + "fuzz_0605": "9a6f2d7063676b05d6b9df87e3ffb287a2a2d2549a658169c3db59c69b3b7f97", + "fuzz_0606": "ccaedb1ae67d381bb7e9a47b622166153931b8503f703b88f83da8ee8f905b9b", + "fuzz_0607": "99f8ed0a85e893c57811b21a843cd31605014716ca23e559ad9935acfe39a0a8", + "fuzz_0608": "03ecd016a4bff9c613a52c9848e0d11094a9b8dd316a89da2e527b64b67a9a8f", + "fuzz_0609": "4294627abd42039e1e4687d3cb708f4f46b1930e4e7660c89eb3a170edfa2672", + "fuzz_0610": "db23a251394c2b8aa7b0f3d7bb6578408aab91d5e8656a0be761bb107ab2a449", + "fuzz_0611": "65997733b4847d876f5caa666c3bba025ee7d47042ba34c7225a6ffd532a915c", + "fuzz_0612": "50cef25ff85ddc06b9089c1469551ef3607db79526ebbfe24741d93c64136beb", + "fuzz_0613": "01d1bf42ba081fe9a342832b8d39071f2cceabe1d46a494abc758e09e2684791", + "fuzz_0614": "916bc567eba7f2acd58abf8614a5cfecaa0c0566dab6d2ded83a912455bdb9a5", + "fuzz_0615": "6a80744e71034181713d8adec7f7cbf4dc23f642aa064b2108436180ab225b2b", + "fuzz_0616": "724f7892705297beb558bfea599a5731cfb10ca7d8afdfc89f6a1cf540ec2ade", + "fuzz_0617": "19af5b57fb78d74a9ea752aed05335cd33cc89e60bc3e9d213c506dea9a9d4ee", + "fuzz_0618": "2fce96262e22dd2e8a0060c71a4efb95aa755ea4c6befea76baf577709d54857", + "fuzz_0619": "4c38dbf46ec3e44ca33b2fc2687b17938546f8a42a32dedad185bacdf2d8bff3", + "fuzz_0620": "a451d2bcdec4a3e6d9808d6bdbd83d1524848dff482f96613b09d122cf657115", + "fuzz_0621": "65fd36ddacd627eab2a12125b02470c44d234d76315a4e589052fc87ee8f0a6f", + "fuzz_0622": "f7703cb8d55da981236ce534bf3fb727c5dba1a703358bb818d259486ff32cb1", + "fuzz_0623": "6e4faa6ed9c76d91b5ff03acd86e05e3ed5a4f51c72d27865446d0c8f541f14d", + "fuzz_0624": "e3b8912a6f4890d3e211ef343e5a34c0059c5b383309e9e824cd3c79d14dcbdb", + "fuzz_0625": "13d0ca00e5d5199d95bdc9ecfb1102711df6e9f87ae2bd747fe539fc67e5c245", + "fuzz_0626": "493210b429e72b2fc9ed8129431965a2892b8a73dd42c7a2ca2276e6628c23fc", + "fuzz_0627": "c4dc53c9d80f605cd6c28d48ade1b69b99ce63333b1a57eeec0b01dc11a8e839", + "fuzz_0628": "626b332f0c002721d2fb7d263cbb0a7d0737ff6100cc542e2fdc18e9ab594966", + "fuzz_0629": "a59f85fd8a7dc7d5ae18de67d8a005312dacb11fc3a4a4b1c226ebc3ef9f9a54", + "fuzz_0630": "147899d6ce1f91b344004053fb2dc1b63a5a9749d44335a6fb9e06cdf7ba53eb", + "fuzz_0631": "5ae6ccffac58c9ad62c68bde84db44c1954b83c61eef5a897faa3057d4ba9b75", + "fuzz_0632": "e56da716ca90055a60e3448c1c499ec7be41a846ec0be7988448babf60dc58d4", + "fuzz_0633": "b82d6ee87cbc0b8621af5662f4c04f9fe91f1f54514f8c4594835e645b7515d3", + "fuzz_0634": "d5f4ceb2528c0573aa4dff917620a8cf8999073bbbac8c5180e9110d889607c1", + "fuzz_0635": "6fee0a6b0d944561b652f6a49d1363e63af7c70d008df135cd6af8dac8855980", + "fuzz_0636": "23aeba2b6299d98e46891c31ecd4043ea1bac252f4ac146c425f7618faaff94e", + "fuzz_0637": "42bedf252b711ecaacd28c03b03e0ed6f953bdf76cbd3e5aa509bacfe3aec168", + "fuzz_0638": "ffaa125254e9227f20a32fd5d7de8418b9a73864e2a34e2bafe146b9ed577ab8", + "fuzz_0639": "45a22938aa13588352c6d4a275f2529f933820510acd062b5ae614e2c7b93b52", + "fuzz_0640": "ddc010a51d01182ffc2a2c0692bff6284c54e386deb03a342cf54eaea988df31", + "fuzz_0641": "efbc7a0a29301ac049185ce0ea81a204a29b28ca07db67935b73a3b148862100", + "fuzz_0642": "7a92885842f4284b73bdb95facbf7512837152d9d37fb4492ed9f59ca521f728", + "fuzz_0643": "5f968caa20c84f68a4e8405b4b529b60fa830eb615e9a5581aec68b46661aa1b", + "fuzz_0644": "43b37a9b5b73f5444290f0fefc15d03594384f68d036a62bd3cf1e74008bc5f9", + "fuzz_0645": "2329902646f84a611c3a40d64b9c4826000676b876bd03af96af892054789411", + "fuzz_0646": "8c3f607bd196fbd2ebb65b0d4c969635146540fd40424d11ca48512e44b4310c", + "fuzz_0647": "73db19ab99f0b31ecb7d7614ad8e9af135448cd8babe8e05fe71164a8bd4afa7", + "fuzz_0648": "bb0e175a86e1314da4f6650be2a3e5a4c7fa535a482bfee12181432588c835d2", + "fuzz_0649": "3196ebf9262930c8c014bfa6591ef605d5f6e4df02d53e099e1cef949d604a79", + "fuzz_0650": "886be6829a085f45c19f967a7b97ccb87e351d7450e80a4fcee369c1c02f6434", + "fuzz_0651": "fa9d686caf70c3a5213eb9f59d6e971658f7bdf1c2fc069f78a03c9484f74eea", + "fuzz_0652": "20453f4dd167c1439f67df6a75b0ec1fdb3de3d6b3fc7954c34a5e9f24f0b382", + "fuzz_0653": "0828300f65a6f0154ad3cdcc59f388cc7348211ea55a93142b3788ebe9fb98e2", + "fuzz_0654": "d2066326a5ac25a6c952f5a90625a171eaf638de6e39bde92354c2a102d6ae3f", + "fuzz_0655": "1f0e5b2c9b00862edf22355f2b2cfb38a7dd5b96cc803189928dfcb3db442b24", + "fuzz_0656": "5e92ba6fcbb48f181380218ca640c49e090f92696238eebb1cbecfb8a1890c13", + "fuzz_0657": "5225f5878eab7130800f9d1c3cb2bef51d215e3f2dedf62ca1b46b3b6f40dd34", + "fuzz_0658": "7579586ca13a1b159c20366b568261ca5a15583ebae8ecfd9e2183d9453ed7d5", + "fuzz_0659": "d7dae084c0c36830e3961d10f6d91b94e67034928480e7efcae464c944f96d7c", + "fuzz_0660": "559a7046f194889dd3cce50d19eca13a6d9172136194822725302a5c0a4b7879", + "fuzz_0661": "0bcf63bcbffd781469b36946687107050db7a62a06c25b6ea1df9c18ee4f572c", + "fuzz_0662": "c163b92bf5a4d5946589b15ece5f8f12a76384ea297e4403563919972adc16eb", + "fuzz_0663": "3e4520b3c2d18e9b66573b19fbdaf6d460ff3b2f65905d656b277f9becd91635", + "fuzz_0664": "0b68ba2cde3874dca424bedfa533da11cc58fae5fe37001bb0c38c25144e646c", + "fuzz_0665": "608798521ed9f28e4df737b6d9b818f1c8252c31e7d2c5b2e33223270989bf86", + "fuzz_0666": "767700d4fad1e6511f16d21530500607c71554a834d19a7b72998a406ff80341", + "fuzz_0667": "a999547362eb61ab8b558f24150df114489c5cf8fa88d3f6df799c87f154d9d8", + "fuzz_0668": "11278172fd637fe645602fb6336849207337c60362199d58f5b841486c512967", + "fuzz_0669": "4fbf99255cb783bf5f3a4cc4fb60df348d185113dd62b8b750c0ee37d4a2a13d", + "fuzz_0670": "26889282773779d074befaa6e3a8228b113881b404d625160919e1baaabc212c", + "fuzz_0671": "2aafc088ea0ff3f58d940a155f73ee7e92f504d88b29c16c8845080446e3611b", + "fuzz_0672": "ea17f4af6de076918314c54667d0a664f5c63c57326bd25f01cdbc63c7426a03", + "fuzz_0673": "9f7b2e5b4cccc5502cf7499f3496105a258e5305ff7b683ce9bdf740f78361f1", + "fuzz_0674": "e268913c0bca800b1e9af3fc1b0eba0fe58a7906da09296417d45ef6af02bab2", + "fuzz_0675": "5f0a8faf08f27e86605b68846ab7fcef6ec236aa429bd49eafb6e9515f4422e0", + "fuzz_0676": "0289c1e9ed44ba1cbb408c8426a01add7bd13b739dcab772de18ec855e692dfd", + "fuzz_0677": "de598448b9a486c9cae609444b132d71de88eb2c06ef493ea8a714d497a631e5", + "fuzz_0678": "d0b32578626a876a3d6cd53bca7a954f9d45bf660ba7f8b61df921ab0a1dc288", + "fuzz_0679": "c01d60b4d63baddb9cb9d61162a89ffda72186a7429b0b2ec231f8b691015f46", + "fuzz_0680": "865d5c2754f87bd413ec1abced1937904a475fce059ee11b87aa72b6c48c4807", + "fuzz_0681": "1b390cef74b2c7eec73738c7754ee0ba1be0c39cb4a9d682305aeca34b060be6", + "fuzz_0682": "3c590bb9a707b4416c4debc62bce0b0b0c6ce19ca6e9a99fa0f3f741447f0a09", + "fuzz_0683": "a0dc4af67f799bd8fc4b2ac3972319eb25f338668324609ea2cd860e74949e7e", + "fuzz_0684": "6a818b235684c4a7481b844add4b5510b90ceeec5a527deaaed35f6557ee7d82", + "fuzz_0685": "14d0d0d78e3eae5da63529e283305c8f66ccd342f7fc377540cac03a296c24d7", + "fuzz_0686": "c1c213c441af05e0ba4eb1a11ef56ea3f57965cfbb7836929608154dc0048ea3", + "fuzz_0687": "1f110408f8f68a835228081cde9318b067c82ba0c11cab749861ecc2b161af4b", + "fuzz_0688": "7d4198b2b21cb1cf662e5f3cd2b36d1894782849c2712bfe68011d3833fa3b93", + "fuzz_0689": "90096d292fde898911ac2ddfe85510ed8d91bd789cab797642ce312a4154865e", + "fuzz_0690": "256b2c6c3a716bd026aa9130167c80f6a0c7db265de1fbebca15adb5f5c62683", + "fuzz_0691": "1b0822abef40db65703ee8bc5c6013b92bb0bec5604a35d530585f317c553eb9", + "fuzz_0692": "f2dccbac6424ea6e73bf04ec4707fa4d3b7dda0cc20dbe6675c17e2bc8a23b93", + "fuzz_0693": "2aeebafc6fbd419a5dfe7a005a2c135d460468a2f7813a9ed6af67e105958513", + "fuzz_0694": "a93880b5fbfc2b9eb0ef299913a5550b6092f77f5b61f969820d76fa2474d3ff", + "fuzz_0695": "8ea68168e6d52671011d71df391ee3330b6c71ea06ad5c887be9477e72a6afcc", + "fuzz_0696": "0c3bc954dccb2cceda697c0800e73269d2878aa35cda553f2da0ab63c47fed88", + "fuzz_0697": "68f0f220a36cf6ee0b862c38e3ad5b6a951c001a8f4c876ccd3ebeab29cc0342", + "fuzz_0698": "8192bb351b8035ce78b69917888afa09cd7a2e61f227b8c8be8ed3e09961a54b", + "fuzz_0699": "40ac34c3a36bc95fe0ddedfd01f4fd2303a7ff8a574d963e0fa106a314e0e67d", + "fuzz_0700": "3a32efd65c9bcc8864dbe83bd2a1cf3968660d577f0e46ad325c26f9f85a3136", + "fuzz_0701": "6007ec1b59f040191ef0617068622f6bae44115fff70f7e2d2fcf8c56eae8802", + "fuzz_0702": "0f12fd31f08468721a841a2305c2045968f32486539a08bded7c721dbd4dad9c", + "fuzz_0703": "06fde8b6f6a7741d9cb468ce299f5978a59c462ecaea1920f0adbeac9c02fc38", + "fuzz_0704": "c5f03c735ca1a2875518236c7c57d864dd802dfe6cb3b770eda56a96a77abfbc", + "fuzz_0705": "f41b99107562038db95db5dd5f188f798e93cca36cd8d39c53b53e2655459cc6", + "fuzz_0706": "815b2b56b01579e938e7900d19ca04858a7bfce2b83d0a12f518ba0488eff5bc", + "fuzz_0707": "2fd95c978c994a5b3d73cacceb06187b0ba5c655fb47c3599bac8d962ec5da4e", + "fuzz_0708": "828245460dfca09971e7798b8dee8790c6cd74c8fc5981d95f13c1b32aec9c6f", + "fuzz_0709": "f7361c2c8d7f24c76f9f11212676e5699ffc05a0a8fd3339b7a04e2e8a80fb32", + "fuzz_0710": "65890b4506599ac34552d505f2e7e901753ae658d72ad0ed9916b0cc797d6da3", + "fuzz_0711": "3f64e684cb8fded72a933325ffb1dc7b3f1fc69ea2351aab946cc0536ad3e09d", + "fuzz_0712": "3bb421b34a6bd613347538e3e6c7ed153f7679376ea6b0251cfa96e744ee770b", + "fuzz_0713": "1275369235fe5bc75830947a03adb1de2fbb27f504c3024a068cfc4a6a767c4d", + "fuzz_0714": "2dd644f4828e5bf5349c73d16bbdd03b443d2b37f6860fe2c706f54d88e539bf", + "fuzz_0715": "389c25a930c9709cd24ac9417ea60f646742f647a92b912f93a0a99d52e1e7e2", + "fuzz_0716": "2dab3c2ce62082855390591f03a05c6d57d565052268100e937d694e11255d67", + "fuzz_0717": "751185c222ecfea637df9fa831d7f88cd8e69b3832bd5b8c846dd8dff340a542", + "fuzz_0718": "ad955616e6210a0c9da41bddbb1660eabeb10d2a3001969783ff4cebc654598c", + "fuzz_0719": "4dcdb5f76e0798c7dc5f04a685112d14a88776bd715298c6d5b8110946130d57", + "fuzz_0720": "dfea095f2c9fda6a064fa87d58e14cb63957ff407732cf72768bc752e36bbf70", + "fuzz_0721": "c56970856ac3a63bd4d7c51f40f4abd0e941b117bca7d70f3206b53652c59eae", + "fuzz_0722": "88b2d962b7c13ee5227abd63348bbd7cb99e10cc81e4ffcf4cc7b90786eb3e3e", + "fuzz_0723": "d736c35b5c166df24ab132f9f23314afeb84ec63c5f43e2da66cde3ce7df6903", + "fuzz_0724": "1ec1c12cd05d8e3d6031e716a65752eb5c1d5c6d76853b14571a44ce3be7b583", + "fuzz_0725": "3e8ddf258825610b971999f5f5765ecba696d30191eeb771616bfa8fa9fadda3", + "fuzz_0726": "0ba9239172ea5bc2717a7c9d92c254d34c429b6228403e1968ae1599d84a5ad8", + "fuzz_0727": "4824f33e80447b518389fcac5214544eed4f6f11a48dfde229740de0d311645e", + "fuzz_0728": "3b3f7d84b075823f7f0cfbc5146e425133359358de3cc149e3f7109bb059ac8b", + "fuzz_0729": "04aeee4fe4d31a8b56e4c4ed6ef19a84988b1a9dd6781421c0a6ba1746326062", + "fuzz_0730": "5f797fbe711966c587c09cf1f67209cae40ed5eaf60e64b321831b533615fc87", + "fuzz_0731": "8b3591c2ac9afe7c251d3238133e000bd6201a416d0be57e82bd695caf409209", + "fuzz_0732": "99763ab46dd172f24d7b724a1ccd25a747d5512c0336a1b87af031dd4b499841", + "fuzz_0733": "d99f296cf78535ab9aa2039c5d5043334adb0f83ac75e711ad1f853afce1ca62", + "fuzz_0734": "27855a7fdc1b685746e7ff9ed995c45355e1c49569528c38cbbb446a1240812e", + "fuzz_0735": "df20e9e55d3deaa09edd68da8285a5ca3cc246bf55cc668f4c887109aced6fa9", + "fuzz_0736": "32136b3c6abcc1cedb2ec680596f169bce95c3ad8e9f5eddf5e0a8a547501c9a", + "fuzz_0737": "23ce0e24c673606b1c2d37044ad1fcef4d5bba8866797c3aeeb13e0ced2a121c", + "fuzz_0738": "63cf12be51a3bcba89d6e7856d978b4b45e2b245ef950858a4eb8bdb71dd4be9", + "fuzz_0739": "dc61085998d2ac83abf706f1e2d569b32d9cb253825a22856b9f419d37fdc5d8", + "fuzz_0740": "2f0c7ad60debe8db1ea5eb15d60c5c4be7d24385722b9e6f412e6b4c79a51c24", + "fuzz_0741": "53ddf25b449002c098abaf9a7ec683f3cc04bd3294cc5c550630051e843547d9", + "fuzz_0742": "1cd763d103aa260b16f7ff2bbd209f604c9101e534f8d90607125329f4c200c2", + "fuzz_0743": "2e9e8ccb719a64b3fcece7c2f6699bda5d789b5247ccb0b6eaebecd2b063bf08", + "fuzz_0744": "9c499d1103d232090cd00ad39f23bf3769d62d50bf95a0d710ad2ceea969c546", + "fuzz_0745": "cac87b846c63913fc0b7bb3f94608939f470c0edef7249f1996eca701b096c4e", + "fuzz_0746": "5b21509510d1f5999c5acac2db0e17f1d012b187306288432f6b1f1b58c86a6d", + "fuzz_0747": "36fd6e58a61ecf957a8dc2fd47ed44f16cfe3c6dd02994365d80216b4da3203c", + "fuzz_0748": "bf01d87b4fcb303720a5bf63b25cc2bd264c233d55cccb44a718fea6bb52ae21", + "fuzz_0749": "4a05a5be2b282b5a73f44a1a8891e45b616e8c8e86701abf2c62600c0c9509b9", + "fuzz_0750": "a49ba1a29be4a0ce32f38771ea097042a32b5a6275810529b4bc10e85faaef34", + "fuzz_0751": "5e7f5a51c396705fccdcef3463a3e0a1197c82b2896cab82be930fd3f563271c", + "fuzz_0752": "3b0713f2a29e8e46ee0ae024860afb73fad69d001b09ed0f2b6318939a706de5", + "fuzz_0753": "02bbd52bc0c230a4ad77258c0649cbecbbb1cfa68719a185767d021350073072", + "fuzz_0754": "ca42fdd13848aa2ca5130b13c464dbee8c9c1647e348abba9405daecc5b65220", + "fuzz_0755": "373becea802abe95d733a017723750d8bb0a31f75a87994b54d8861a74d00ffd", + "fuzz_0756": "69d302c290f312fd4d697b77f266461d8e22fcde888b105b2981fb21b33f441d", + "fuzz_0757": "79c5f50618554ed6db7b12da4614741b40fe0860468ff4733aaa82fc313fe218", + "fuzz_0758": "ce0d89aa6c5f2d8df92f9d7b1aa21610f923d7e01ac71dfd668d802d5119c9c0", + "fuzz_0759": "7b7d8bcbaee0f1e4d0128802e1d1f00da3f42e2b5babd28599d19f2e11bb9d9a", + "fuzz_0760": "911fe511dfdef969ea66f636b9c7f939d139f25b650614b05b7bc84478298d5d", + "fuzz_0761": "f073ac047959ade26177abefad059f69dca387e4b36f1ac05c7006cee4eb5b8e", + "fuzz_0762": "5962c66d2aeffa5b5768d4dfeec1fd0d16b97441f24d20f1b9ac42adedb248ba", + "fuzz_0763": "0c9f3029a184ffda46cf8b67294abcdd4e71dcfaa1e6b5a2d7203d25b62f1192", + "fuzz_0764": "bbaef5e67e7e525c7b0d5aa81f17e6fa35b9e28e824788a6b0b5c3102637ade2", + "fuzz_0765": "6af30d6a1b54fc53ddb62f60fcbb51ae4e2728dedb302e237732b421dec51095", + "fuzz_0766": "2f3e2fb6e27e7879081516748f5a0eeafcc59c3394b8b648749d88e8092a9cf8", + "fuzz_0767": "1dcb28a866e4398d045d2a73b9eac27bb1171a14fab2275ab3075e6f93f3b0ab", + "fuzz_0768": "e9d93303183e081396e54965d77d6d53880e69be42dd9bae68dd183bca0dbed4", + "fuzz_0769": "b103c278ac401a191859e3b48b8da64f329161148266cd90997b628b66166beb", + "fuzz_0770": "2f9c1e650e24c8d3cd1cb6a689657b3875cb85639a4f46febbdcf56e1d35fda8", + "fuzz_0771": "72e7a83ad0d47b6c38cc13d84161c67150ebf523cc904b7859554168d18a46b6", + "fuzz_0772": "937e03c5a2518b98a391de5023207d0ae09840163e13658df29adf062df5f369", + "fuzz_0773": "792c47e34e0829cf23af714fabc4695c43b07ffe97929f43c52c2dca3f60cf10", + "fuzz_0774": "d4d648bbdbfc5d758967a6be16c44f584e0b73a79db1b874f246ccd68429d9c5", + "fuzz_0775": "3a8ddcc1464aefc3c597a55555d92a51657748b55ba2178f0afae64e54d7291b", + "fuzz_0776": "06e5489db254c1fdf5f0958753dedf9703fe5e2262490a314f375d5538a9ffe7", + "fuzz_0777": "21fecce56b4b401aa7b01a58fe5434c72954c5eb938b83662b0630335c636a93", + "fuzz_0778": "00b079cf72d527d5dc645a8aff56fadfde3aa049a4df7dd2ed8603c46e5fd8d7", + "fuzz_0779": "aeaaaed0bbaf24295ea8476c8121a9e7858fd88f1417a9de938ad63bdfe1f92f", + "fuzz_0780": "33b30e98b0a60fed114d09f6b7a31afeb040225d980f6ac660fb8cd69d848f3e", + "fuzz_0781": "373becea802abe95d733a017723750d8bb0a31f75a87994b54d8861a74d00ffd", + "fuzz_0782": "c4ffeae99258e73784bcd8f71f540ce64fdf22bff84dbb5ae14842a0d3563510", + "fuzz_0783": "ff523d27d118f46d8285da0610080b72ab4b618b1d24f3af5cc8ab1acaafc39a", + "fuzz_0784": "039d49911e602dc6e0656be8b2991f14f3ef9a6fdabc7c9117a4d6d898ada922", + "fuzz_0785": "7e00d1a8f73ae16795be952871c518f5c8003ab7febe6971911fa8d672092209", + "fuzz_0786": "a24402e57c6b443458eb20fd0a304ba91131fb7109d610187d5262c81b8f610f", + "fuzz_0787": "561735bce22bd942a88f0b49edc6c49cd9efd9fb35269e7a04959dc538191717", + "fuzz_0788": "253446e78c845ba96ac3b39158e3df159d3aa532513607a682f82462c42dd268", + "fuzz_0789": "cc22712591d43ba512d415ae211fc2dadb8dc9d55e55b9f162ca11371f3ea893", + "fuzz_0790": "6c427801547fd0f7d9efee11435aa8a6645202c8dad9844f4678c7e527d11add", + "fuzz_0791": "4c25f0d34c044bd29947190dace29010e0cb1642e88725ed1fb49a5ef7541899", + "fuzz_0792": "d7c61fc16b142547ed041e1b4dc497f9676240c65fb56dab45633baf940cdb89", + "fuzz_0793": "d81648f66782f33f6f1e72e863bb7a2cd5bb69da59eda34743e9b5d304ef991e", + "fuzz_0794": "cbd371168478ba857d3711a37a9f4cf28a08e86af0ac946e03430cb62a3435e8", + "fuzz_0795": "d6f8432135474990dc8970aba2bb1521cac42a1624c0dd35c1ceff2489745699", + "fuzz_0796": "0168d360c7afbc67cc2a7fff6eebd433261985c4355246dbe8ffc701c70d1a5d", + "fuzz_0797": "dc7216424877184a345cf10436e36e9ba6fa084097b44d35183ad21032631cbc", + "fuzz_0798": "8bdba0ed444116641f77135c61640777886eef0aa8db65cf1c7f4719d9d0652f", + "fuzz_0799": "5dce02ddf9f3dc640675e30a30596946feea62553ad5c766f2144070a32e6fda", + "fuzz_0800": "de93fb61c1ea8e9345039f3b7970a808adbb7d08066e0e90aeccc93ca4aeddc3", + "fuzz_0801": "bb70e2b006034dc666ba1fe87339d0c0aed4065aad9021a6c3678b073c41809a", + "fuzz_0802": "6dd9fd85e969239ad9994de15eea6dd8d325676c94df214145a2a0cc1fd8f088", + "fuzz_0803": "e2544c97b8723703bd3f9aa8a8ec433d567afb6aa637712f5c0f5643ea305357", + "fuzz_0804": "ab015a4407fb03c688d06e14bfd04d7ff641e8af612453b8f6fd32b1ee445949", + "fuzz_0805": "0ac9bf869bd1ee95a8b29494cad00a0959fded33043448f9baf3e5a4a51d945b", + "fuzz_0806": "2c55d4bdd4bc12138733bcc0ae6fdfe5eb9fc52ba406b1f8f10148e42aaefc22", + "fuzz_0807": "176fd3584e4919b2a044e9fcb8103c6ebfb524614f2ce699c31262078e3db256", + "fuzz_0808": "3b40fd888e2a31660ec810027cc28382ab13576f58c0b69c7e575c95ecc8b228", + "fuzz_0809": "56fa6805c47b22d186f09d1ed028d0a02d262fb8f516e531ed7b30b1b71f5fde", + "fuzz_0810": "6968aa44e46eff815c770271e577dea6a37343e930a6a31d1612215fd3bb658c", + "fuzz_0811": "121d16ac1fa70adb1e8776eff7e2d7c477313f8c9ce0256ed351e7fb74f155c7", + "fuzz_0812": "48ffea4a2b75076a950945079e805c25d314b20836751feedee4bf875e53d392", + "fuzz_0813": "a88a2963ff1018a92937ccbcf3047fe01cd6015133eb4937c3a6e574e0272c65", + "fuzz_0814": "f2a961162b9003b7c70b004ede9bed0fdbf278ff8e8094ed1489ca858b3cecf6", + "fuzz_0815": "fb09a481ed3db7e995f3b49975f16605d8038b4e13d726643f1edcb00b69e75d", + "fuzz_0816": "c8da4114b1011887d6a9c74f41ad0c333ae9756ec81a6cc1382d13c3bcde8669", + "fuzz_0817": "167386ead4dc11c1069c2963923327c793e9268be78a30732bcbc3e1d710d7d1", + "fuzz_0818": "4522b047e98a54040343bf80058274234b6ec7f57ac48ab9a274be6fb04d4999", + "fuzz_0819": "3df3b8eb58a6f47a0caca27ddf5adb49f6f7c8bfa58578dac3c7deef1fba8461", + "fuzz_0820": "78885a3579c41437546bbbf16cd222412c98c54029077af50316ee05e62d3aa5", + "fuzz_0821": "0f91d878d32bc2d0a0d5b333249304bdeb003a4f44e57449f83bed31f51b4788", + "fuzz_0822": "40d45e636c3776db2e2c18d2d3632835a27300f0177d897e46c377ebaac8fd64", + "fuzz_0823": "8b7e249a06820708b46eb35a773e1219dd6213de551d1610555a8ca4d138e548", + "fuzz_0824": "6283e07e2c08eb3c7ca685f7642f239dd5433472073778cf7363fbca7892af4b", + "fuzz_0825": "d19f60cdbf406ce40977cbe9eb2dfe37ef8bf68963698cf6a29859f3beabbd4d", + "fuzz_0826": "f46c93f4315ba02f8f562030f40244173f45839a581402c9ecfdfb5c1d1c1356", + "fuzz_0827": "92829096fde8428d09ec6c40af10bf11fa74ace7c11e51c50480c74795999f7e", + "fuzz_0828": "373becea802abe95d733a017723750d8bb0a31f75a87994b54d8861a74d00ffd", + "fuzz_0829": "1e123abe4e68b5b289be4970658e56195ec76e300766d4c991043a0e33c2f3be", + "fuzz_0830": "9a00531cb66a8950b67f423c2719a79d23773ab59a99868842d3d3c66d1560d2", + "fuzz_0831": "f932d1ea9527e98ce6bf59760e6c820225f5e8e8ba486faed571b79ddf969171", + "fuzz_0832": "e6edf7ee400dd83078cbf6a1152e93f93d4ea74ceb6dd3fa45a4201d292f9670", + "fuzz_0833": "9c63678fafeafdb447b00eb83e065bc49cb445e577252319cb505deb70f024fb", + "fuzz_0834": "45ef48ad9e5e501a48c22df887cd9599151bf8b8acce7b5d226fca2e2abf631d", + "fuzz_0835": "f53368931d4d84effecf04f35aaeb09b3a7f2ff10dfa98e6c8407e826a9ef5c0", + "fuzz_0836": "ba8b19d2d6c8e62478a28f907e80cb3fc072830317a120fd26251492c676766a", + "fuzz_0837": "bec8bade53daf8aa8a6b8e40ee09f79351e08f12979a9b9cce5571d1bf0361ab", + "fuzz_0838": "e6bfa4722b3581c3eb799f007890908190cd8e70ff96cf4b6d817e0f900d84bc", + "fuzz_0839": "516a5a871e377d338a452af2c7a69d49aa8a773c7a8fb168d8b0164073c4fdb9", + "fuzz_0840": "6daf767ab6bacb731c5f4dee420cfdaf7181426d80f3f92338c5c4a89438a971", + "fuzz_0841": "cfcac0fb6c1efc6555e08802d6702b4574245d5c1f5b5a8b46a478a6f7b40847", + "fuzz_0842": "eab9e762c9bb525e0345ddb79b8d4a1cd33cef0cfffb2f3006e6dfab494acdee", + "fuzz_0843": "b61b6fca164d550047f0b0fe2bf57b61a86d311f2d51d3ce1020a073b39aac9c", + "fuzz_0844": "12c8b987e55b6790c51dc0efc4c06ce61cc9f4ccd73c93bd634174740f4889ae", + "fuzz_0845": "4d0999684709b087197dbc1e4e20649f5a97a4af6f52c375da1b02930fdd5e2b", + "fuzz_0846": "9755fb2f89568a44cf143cd303102848648a2a63ad88d28ae5a72e28dc80f966", + "fuzz_0847": "035e828cc65ead0afbca2b6fbdb0c589e281176899f4f36a27df1f8b6df040a4", + "fuzz_0848": "e9dc598dd0eb52a3527ea4ed5e1d29fcfbec40b19bb81a983d2b2cf08561cb42", + "fuzz_0849": "7f6e2a30b5db5ac767ac9d617f295b4cd8feace1e19f642cc2881db8815e1ef2", + "fuzz_0850": "eef0a01d7499955e5918e31b8455b0d7be634d25c6f2260f32b6a6beb1da83ea", + "fuzz_0851": "7c5773c5fe2013199cee68987020c9cb59c61cd0c0844b26d0b63d954f12081a", + "fuzz_0852": "a61cecf60c41834daf6bceb6a4beabe254a55e8e14259d4505b3dd6d750c9df5", + "fuzz_0853": "6053de6da47a1004daf62fb386669423e7793b7d5190397575b01ce8a14bbe4e", + "fuzz_0854": "17e66dfd2c6b2dd6b7feafc2fd16e083d3420b1159a41008b42640803665fc8f", + "fuzz_0855": "4ce9c9fba4ffb8826bf25b2cddb82d0156741786d3afb72c5347670510b39206", + "fuzz_0856": "02dd1c937d9db30163fba75767a1b117cae32119c2a8e4417d189115480a1761", + "fuzz_0857": "f50ba891a6a58e25fef8c95c455f0f39db9a2aa5823c1dc8af8b1d4f195c3ef2", + "fuzz_0858": "9f0c1c39bd2e840cb661379f24beb84389b9783391afd5b508010dc3ee99311c", + "fuzz_0859": "44f0fa91935168a5ff3974734d1062912112ea8771cfc56dc124d495727010d7", + "fuzz_0860": "55a0ea6b2fce04be8ca4e1e6266f09d1b9e8596d27b7ea5689e2a46e17ce3790", + "fuzz_0861": "39009f7e4afbf13774c5cd330b8cffcdce8e8ec6f6c36071de9eda9ab2b46e61", + "fuzz_0862": "1b32513c12ec2fcd63bcf4cf73e40ea1938377143921efe67cc2e8cfc8f8fdcb", + "fuzz_0863": "0039570a56dd1f14e31b98eadcbc6093014331d36218d618e9476462199bb9a5", + "fuzz_0864": "1f60f63aaae863958fcb7f4d2ad6f73787bbf6865bcfcc8b2916547bfe98bb1a", + "fuzz_0865": "739f296ccfc5197ad4f4c22a2031b8412ff240a4d696e88acdd7560babb3c773", + "fuzz_0866": "bf012f886ad3422b535e0b772cac7721e31737efbfa4d4040ee2a1962900154b", + "fuzz_0867": "f354198091919b5326f03afe07741cd47ee598200e54c73244dfcd51b318f599", + "fuzz_0868": "5016e98b2fbeacef6632b52f19f15d2bf39e75e5573ff49c666861641b7a3aed", + "fuzz_0869": "4824f33e80447b518389fcac5214544eed4f6f11a48dfde229740de0d311645e", + "fuzz_0870": "25239824f08f7c1d719457fb87b45b3a585645da5af36719a581867ad1b2993f", + "fuzz_0871": "974c9be26f9b8b81871df7cd74f4b5093400fead61bdd5c87c30fc2f3ad34e57", + "fuzz_0872": "fd4644f644718f2247c481620e6ae03b39cef1886c3a0ca5928461caa0b3f13a", + "fuzz_0873": "238b94ff36e28fb82391a7816fcbaa2775bff727d58abae3c116b043331261df", + "fuzz_0874": "db80a041e579024e231edd2b533feb97334dbb455d85fb5bdcd93ed080d403e8", + "fuzz_0875": "ed234d6e53ba19d60ea3d0dada913872ff2768ffd171a957b8813fe7f597a11c", + "fuzz_0876": "5d83a9dfd8af8c96bf8c7c562f84d112b296da25677ce1f6011457b87a168301", + "fuzz_0877": "d896cbad561e6e18d9e83f401e5a43079c097364d45074bcb54e820d4a5be9be", + "fuzz_0878": "e7e2f228d7236f5d52398535d3bbdcbbb6928bfb58b4494a359b53639a2f5a8f", + "fuzz_0879": "4dab1c91c71e343bdfce944c3adc3d837575fc52709b58909621cc7d3fc045d4", + "fuzz_0880": "51eff3c7cda0c4d7c90fae861ac6ff36c901317a0c51b2e47678a142e50093b3", + "fuzz_0881": "631076a65ccac4ab5eef27a5de8afe9a73dd72aac49dcae16cd4f25f099c7257", + "fuzz_0882": "65faba14c9647fa21dc1c6dc53965350be15ce80772743bda7c00391f418b832", + "fuzz_0883": "b0f4f789666c6c8dea968b187c274bb27768381e7dff28a63fac96cb836cbb29", + "fuzz_0884": "2b3d38679100c6bc9dc12f9c95994dc60460e8cdf2201ac2a015ea7fddcf9f4a", + "fuzz_0885": "cc22712591d43ba512d415ae211fc2dadb8dc9d55e55b9f162ca11371f3ea893", + "fuzz_0886": "ccc8c078d34d34842d3195252e6ea500734776001e1363cef69fa58155a0b449", + "fuzz_0887": "d3ebdc13939ad3e165cb4fc6249161ba9270af2d1ab4a9c6953144adceb2d82b", + "fuzz_0888": "be25c4920562ea94a281c593e9cbdf95e8dc3de2edd1d22bd3abd8bc715966fe", + "fuzz_0889": "777e36c577d31c78fbb2b34286f1d9532f003c4faa452f9ca8de101fa4efe63d", + "fuzz_0890": "dca271f112695e563a7ed77c98868805b434e3b072398064ffcd14fa727f0a3f", + "fuzz_0891": "f650e3fd9ae89500aa445eb696d33ebbf62a08f6852d0666abeb748fba91a0cb", + "fuzz_0892": "ae5adfd0e7e1e2435f071babe16dd2df06fd7010109fcd78b34fd4323a086bc0", + "fuzz_0893": "48ffea4a2b75076a950945079e805c25d314b20836751feedee4bf875e53d392", + "fuzz_0894": "a11170d0c00d38ef8f475a73082eef53565e9472d93692ac18bd6cdd9085624b", + "fuzz_0895": "b56509c30de035680a21ad829651d12395bdcd3d1ee58e592550f8a96c7ea008", + "fuzz_0896": "79f21ce4b83a2c67ebb451ea7d24862329cd8020d063b0182382ae187f4a1ad5", + "fuzz_0897": "b3186ce08fcec000a3ae3c3f0435861f3151cd4cf4f2573744af4a0009871974", + "fuzz_0898": "a3b60afdb8663a283270de9019b6c767e7597971fc716d50ec9b23a95e06862d", + "fuzz_0899": "c54e19181ff6fa730be52084a5943513ca756b9e541134ab02c8c44b3da976e9", + "fuzz_0900": "92ffe8a9f11424bb1eb9159bdfd6f74aa3291fb46a2f6573c7ec386a603530fa", + "fuzz_0901": "2562203d853f9e94d8a498a20a743d01182a491d89130ac6d62c376b96a31066", + "fuzz_0902": "4a9a706cfb9909480de2fcf2883afa849229f07ec4ddae02dbeddebf276fc2b1", + "fuzz_0903": "03c9072923f03385bdf91e2ba05568c137850ca54483563c81798bce0e75ed71", + "fuzz_0904": "5d0b482a6d4210f4569610188760d429192163d594cdb5bc2b7dea9d555acf50", + "fuzz_0905": "6262cb747d1596f08473162e291fc4eab0484a5dde450e3b7d6249eb78329164", + "fuzz_0906": "7fe39861dc182eceb310a2272d392346e4e17f1266ad69aa04c5026c4e888880", + "fuzz_0907": "be8e4618609a3f53e35da4068f69b09fe32578b0a6045500d1b9034fa84f7215", + "fuzz_0908": "130fca2be6f39649ef5e6010fb6a99ce21e149249374096f6712957092572689", + "fuzz_0909": "84e1937a2b77fb86154ae473ccc53182aceed68708eef9a1e5550d9f49a16638", + "fuzz_0910": "033e823a82d71b55afe3847cc32ad6e7b6facf6ecbc051602799adb0f915d196", + "fuzz_0911": "e9e5ce4038ad1215045d860cc03787ecf00a3f438d486bb62c3647264a70cea8", + "fuzz_0912": "2537784553b87ef8c358e1caabd33022e6689aead33c3e32b44d25954e1bb9e3", + "fuzz_0913": "c22f0b0a6ab9193fb5c6d60fb3de8670f3b028384e2a1fc069714df53f9a84b3", + "fuzz_0914": "c2e5725e266536cbe04c116341239f0131cd8d2941b225e7c33c309e8d8e88ab", + "fuzz_0915": "60c416c9a255b74eeae1f130685073f507b91b3e82ec5caf96ee55974a7d1596", + "fuzz_0916": "d029f26539abf7247fa75e53b12e6b200d85c64fedd8ca09f06ba7f2bbe98d39", + "fuzz_0917": "4fca094d0bccfdee0a5ec38b61bfcca289b8ec5b1743aa1f3cf49ace82df61ed", + "fuzz_0918": "4c2e3cb092a64355e63b167328041aea00b656c6438bf50047a0438b1c7a56e7", + "fuzz_0919": "7bec95c29ea72a83e66c110e9c15bceefb156f6c7d669d3f7bdbdf30a2e51003", + "fuzz_0920": "40170b0176e702e23f333c0b3755e69fd3fc980401966d7b23ab571fd589acc8", + "fuzz_0921": "431589d6273d9429c6b7d670359fd5b270574b46bbd79a72c67c1653f444b164", + "fuzz_0922": "120da170a63458bff68ebc4999641966c25aa784fa1b506574f83dc5dc04c56f", + "fuzz_0923": "3455d1faad1a4765ebaa2c341267f9aa0a1c2d15f63a580204b2ac595a54d1e7", + "fuzz_0924": "33eef6b6dbe25fff7b86d4ab94f0f289bb1da757fb9c4fd12e97a0a948f3e788", + "fuzz_0925": "fd7bc430aa64402d277a9d241e814c4211a00e6b77129713d51c1c9ec381f8a6", + "fuzz_0926": "dd3e4995db68f08c7becb611dc5f18461fc5b2acf9e161a42be7c7e0a53f2ba0", + "fuzz_0927": "db090202228c6e639ed2de729f48ee8283b8c2db1fd6e9dd1d8b1a4458e575b7", + "fuzz_0928": "8f0b318fb7c6f403ad88a30b48fa0313e027295650ccfd3b4d4fa1b842cefd08", + "fuzz_0929": "bc24fccaadfbd6e3c1a4321bd90ce9e9bb4ca91ad60e4455c8e965f4d84230b3", + "fuzz_0930": "66aa94bfc10ea738fe12870a8a398a51625b27126624f57e8de64d8ba9f08084", + "fuzz_0931": "82f5993126fd87a6d01af30287a7e4ff0a13fc8415b9cbe470ffcaeeb5de4b09", + "fuzz_0932": "51736e746b1c1be6658cf1a85906144c8842a7b1a0237d79053142c54f9bab50", + "fuzz_0933": "e3798737ca4812bc4a1975fdd429fef5b6ac0cb6777c3ec3bacc301307e497a3", + "fuzz_0934": "ce34bfa7bed288d05940a0cc97759ff96ad80bc7459f8a06757156bfa571d4e7", + "fuzz_0935": "c9e4bfa09505059b688ed4b6acc51dc1c42ef50a7cbd52eb3dc6d6aa756173fc", + "fuzz_0936": "1f20fb2f73cf99d1caff33dfd1a4acf6c9083f827885070b503abc2727c22fb5", + "fuzz_0937": "ef0705a07ae93bc4e03c594548d4db9c447647071f7f8077dc07446fbd4ef2d5", + "fuzz_0938": "4025c7e7a14f7b37a13b939861bf964e15171a8d45aed375d1149a78eaa2dabb", + "fuzz_0939": "7407faa6e1faaba62cfbc1f174a398cb29d9acef5aaaa1ec436f94bea9ba730f", + "fuzz_0940": "a0a8f69f84150ebde2195338f47ae20ab8171dedd54462996908c829358fc03d", + "fuzz_0941": "925f728e51e4fe9761c2b8bebc66b929aa0827be9b6c4583f9675b44dfbadd94", + "fuzz_0942": "460766d2002a5aedd38194abeaf681f00372c15b649fb1995c804b72af926761", + "fuzz_0943": "8d0ea65d168ad724b6d0c4567116180225e359527e56702f281dd92ce08743da", + "fuzz_0944": "cdd5fc45d84a03b8da8a7b134d4fab1633cdcaef85274dc919f72e5fed157ab3", + "fuzz_0945": "0c5dc52982c2fd771731a74ef34e8894102554aa60348e7c76d308c2763469ab", + "fuzz_0946": "cf924e43738c76902354b3278acf16abd94665c58b9440f4ccee1a5976d134d5", + "fuzz_0947": "5552a372385581a2e4b1b6d5e00a5cc9b2c0b7fdd07a028438298ccae3af079a", + "fuzz_0948": "6794a1660f46ecd7a1dfb891ecb3258ab0be3261c451ee7a3ba112c56484ec92", + "fuzz_0949": "ce79bc2909245312d275d9204d8541dc133387745cb55e78a5035549ef971c4f", + "fuzz_0950": "be3a9e1a1f3a0f0bbe6a5696d877e91ca9d9c7092b280f24632c4e333c09eb15", + "fuzz_0951": "0db803b89813fe00ba81c59b53c15c846d7265968f2ff4bb4412225cd3024e41", + "fuzz_0952": "92d0450c7af46b68cb8c4942947b3d1c9cb63926f0af830101a8b6753fc7e7fc", + "fuzz_0953": "a62143d9214a1cd4a5c193c568063a2ec4a10c2e5ed6ea6ebf943345fbfa8b65", + "fuzz_0954": "f303b02dee4425baaf482b297018d1396db50cbc99fc3c910031989d0d3eab32", + "fuzz_0955": "6ad567993d461a04f8c936a71f373b07e1bd5e8c6698cfeb32e899aa4014d259", + "fuzz_0956": "5f36dd21451dc7455532f09534869826f9ffe8d50bb78569c0238db17ef5e431", + "fuzz_0957": "8d8213b025b14eb751fa62685751f4a45cfc49b18a05a692c9d33195529fb78c", + "fuzz_0958": "468be1790d9f771f2b1f31b1b8a320625cf5e80cd90003dbdb2538d17be1ec85", + "fuzz_0959": "cf0b94672adca216c876ce44215bafbdc53188fd3bc771d096ca2daa16c2177c", + "fuzz_0960": "a6b8d6dc7ca137907acb922d4073df77049f024fd7e912ddb4071da34784f7d4", + "fuzz_0961": "f92c7b375623d36a1dbf4f2caafdc556d1a3abd82b70b83fcb66b6eab0a558b4", + "fuzz_0962": "4fca77ba083d70d76993591dce55c94e8f33e83d40efc7fb8bb430c621d731ca", + "fuzz_0963": "2f94302d13ae9b3fa4714f41f1b8b5ca76f5f5c07224dc6e4d0744beaf00782b", + "fuzz_0964": "98071f04eee77e1256ea7a355ee582acf0f4c858a2ca1e2847afc13da8280ad7", + "fuzz_0965": "915be3ca334d48d7e645bebaea22e959c85c19c8cba08962ca003bb60f017fe4", + "fuzz_0966": "8c4936ab1bd39d67a003ce5d6502c7d5f2d859495a6ecc46230d9eb034db2607", + "fuzz_0967": "c46e81dd4a8d29dd991fd9d8bbaa72b2f3e6c8258fb607e7b6990a68ad495f89", + "fuzz_0968": "c352e00489a93974aa391bb87a35bf725c1111f9b5668b6498ce508850ec6d6d", + "fuzz_0969": "6f0e06dfc0820a65da879be0b58de2afd221190540481cdb39da5898fe0b4216", + "fuzz_0970": "e1ebe7545489ab67ddc51b074ef08fbd4b527319fec8436ffe0391627e83137c", + "fuzz_0971": "05a4d8689c5e3ab3477b0398f65f6cc31d7100cb616539f12e0e02fa051b7478", + "fuzz_0972": "787ef2640a6593050cd9d9727a8ea4e459e9c9f00647de2223c641b3ea04d5fa", + "fuzz_0973": "468fec37c91299055e25744a5a2c9bb1522db8dfde10c6546d57c72448ca794d", + "fuzz_0974": "f112121ea93b460e906210f1e2486079b03fa4c435599db2025f0eb878eba754", + "fuzz_0975": "db9ca449dc70c07b3fc30a41d8efe74bf8ffd49c365600f8d2b28ad6657de137", + "fuzz_0976": "2bbacc5395f750f61f232f91c65ef8272b09f9175e8c0fe1e3983cf67c047819", + "fuzz_0977": "5a2abc70ae0144ef7510333384138470392b51878ffbd09da64c6a52da6120c7", + "fuzz_0978": "7a892d8c80e95d65d5807b0b2abaa4df5b860bcc62a746514b0c999f3958c4d5", + "fuzz_0979": "bd5e26c0dd64dc6235abf1cc9edb884eb60e895fe17364bb28ef6436f6ff8a6e", + "fuzz_0980": "50087e7f1ae644194e72d851bb9ae06c60aeea253355959e824b28c6ef163c20", + "fuzz_0981": "52f180de534f450ba0cd6ada0dbc8e0d32db555aa2423365091d7e0301c44ab0", + "fuzz_0982": "c945130054d7d026cac2b049b29c2bfb0f7df29c693690a33b00aeb3d3ec4422", + "fuzz_0983": "b8218d12ad121f22d9a1e9c7be849a3fe967e2bc99576c1ecf3965f94d133c95", + "fuzz_0984": "e6585103c626c3e5340c288737def36beef9fb790e241784bc3a87fa3e0acfbf", + "fuzz_0985": "676c5d2f9a8a5117ff68f8ba3604f759251cf9e4a363a65def5d1043555a3f02", + "fuzz_0986": "7d6941d45a774da65d837612262a453db062457273b7701643ad0165a747b21a", + "fuzz_0987": "cabd8b4ca072e5d00380a3e656fc477225659521e78565d99ec66f65de5a89f7", + "fuzz_0988": "f447067d15d08b309302fa185241f33555394c855d0ce1578ae27cd41a3ac671", + "fuzz_0989": "0828aab5dc73524ea23916f309471514243027ccb197387afd972fed0ecabd98", + "fuzz_0990": "5d0c476fd56e24e1f1a042920e5fd37ee331c435352ce726aae944291a8eaf7a", + "fuzz_0991": "72281733d360fc726929fcc88d101333a077af5801f3c8414d74ea7205f0e093", + "fuzz_0992": "32699316e87ecca070417f685e2906b369cdfe611d5568ea888600b5229823fe", + "fuzz_0993": "366b50560e1f7c0c1831b8f3ec83516a146e64814f3cf6fd2ffafe310779046d", + "fuzz_0994": "544b7ea279d8b3fdc94192a537162ca21c8ebbced0738b94469b598b4168626f", + "fuzz_0995": "9ce22fae7418f146d224c3a71356fe815e617db0e8aac23252bb5bc12737ab76", + "fuzz_0996": "82300c0d0a02115117b1eed201a52a62298fa802b54dd2c9a239fb3a459f27fc", + "fuzz_0997": "b5a8992423add069acfbc6c00e4ef028016fb0b6aa35a87a2d39cd0ad31e740e", + "fuzz_0998": "d6b8a8401932cb272d5cc08eb08cff33bc486358084bd37260f553c3b4da58fa", + "fuzz_0999": "2c9d2fcb02975bcbe0af6012811e6c6f819b46aa15f4c1fc2dd15ff8ce6d6e00", + "golden_block_html_and_comment": "996a3ce052c37bdddf190e528927456e02466c9e7204ca26fe280c6002bed1e0", + "golden_extra_paragraph": "f10ff40c9d3e85ec5b517e0ef2538359c3c72b4dc3fc4e11017c6d810d9f949f", + "golden_hard_break_and_direction_marks": "6398d17ec26727db57f2f9e4c3a56815d3074f9fd5ddf16fe538c504b76414c7", + "golden_heading_styles": "9efed81e625cd2cb8dd3c09a53cbb262fa96a0bef68d28711ec5a3a1af620213", + "golden_image_and_reference_link": "7d48ac4ac242f2039f92613f56c3073fb84b088ee975e1ca568eb69df0c1e5b6", + "golden_inline_html": "1814c119ac9080105cc4f19c134c014410d131a896e9e1e69fd8d868d12af146", + "golden_link_count_change": "e879fc2cab9d6f335b57d6a3e04f5aa398cad6c0adcce1c412a0abdf2ac7636a", + "golden_list_variants": "7dd71fa26f6b1e0d3482296f94198a1773875b0d09b8b24cfcb608c371436569", + "golden_removed_paragraph": "8c5aa653212db2597f8b289aef4e43ff8547faa0009a8fa2e5dd4f950d06c4e3", + "golden_same_links": "a9c2358f3342cd8fa04a2f21a6e07bdc88776d57ee1b053ad9c109a082d5f50f", + "golden_zendesk_callout_style": "70e8cad20e83096249a430f1bb2fb003d1514637030cfa17cfb21aab5de2b691", + "golden_zendesk_steps_in_tabs": "6bb786a9ba21be0d096e37706f9f75e457c5503c303645de3e58a9f1d42c0336", + "golden_zendesk_unstyled_callout_in_tabs": "28c2590d7c70a15add77f8ae0366de645651d7d88edac32ba3ee48bf2e36f1de", + "greedy_label_closing_boundary": "9591db56c68a7958cec9ee16ee804be74110265dadd60abc0809e260b771a5c3", + "hard_soft_and_blank_lines": "6f531c8d04e17558f3b9d7db8616883d7913aa247b739a2746d01e0ec1ece1c6", + "headings_in_lists": "cd3362f7efb89a330560500b6a31b5cacba5c606b9833a1cde6fa6eca3ff552b", + "html_empty_and_self_closing": "2118bd89e5e791ffda68e849e7de684a45c15272f0dbbd4ef88feb46c77528d8", + "html_inline_comment": "e078b0c7f6ce7a6509917ab936d913b3a8952046f914381c1c880f5d30c98046", + "html_mismatched_case_and_attributes": "371e8e82c834c84d9cec5fee778be8a54750607db821edfe3688c408e2a8a252", + "inline_html_is_escaped": "bf38be2ddbcbaac36438d275330de8ce00bf921059a145728b3a614d6d809e56", + "inline_parser_reuse": "179af2c54a9197e53172b31170b39bde737cd3c4b145ba3160f228f94c1ce85e", + "inline_rule_selection": "8ace84d3afff2f3c1c2a06d080fe62941116c2429a6bc4f321c5cfe2c0653b15", + "inline_syntax_stays_text": "836724aea14b615d69c6497533e7125ec924a44b8ae95f26a12bf9c3f9e6b6a8", + "leading_and_trailing_blank_lines": "3c33bce6eac82926642c41374ee24a9bdf961f755ad209c47297cf26965ba0ae", + "legacy_nested_label_atom": "fce97d21836a21e9d8da9d5fd898fb8bb29faaea44ffc440825ddef9cb77b7c2", + "link_angle_fallback_boundaries": "3d466a560ca1ac1bde6053a08ba0a7048b4f285acd5a5cc256b171022ae836cc", + "link_backticks_and_nested_labels": "d7bafc5a17aac29a7410506564bdfcf143d62102e07a56d51d1fac6dc145202e", + "link_deep_and_unbalanced_labels": "5ffff731ad3ad2b5b571e1eb10deb38e6a9bd8db4d4b0937b37b8ee5c54899d7", + "link_destination_forms": "2334165975b2593c3296aec4a31fa970de2555be00870ddeb42516c8b5f40cb2", + "link_multiline_destination": "d850ad3f67655636cfe8bd26ffba99759f2a0cec42785cf08d2065e0654faffb", + "link_title_forms": "7b69c2bb8fd4f872ebb7311a664762d0e667e000d17d26494d3613b070d3dcf5", + "link_unmatched_closing_boundaries": "7539db8e34dda790422b11bee6e32094177caf380ebe6b31bd6cb3963f505bde", + "links_and_images": "2d5d25bc284366db81632c66fe9eb4fb6a75c9b5237df0720e420bbc9fbf8e54", + "list_thematic_break_exception": "31930d90b3ec6b0235522fe9d0053d76f7f1ed1dbffbfdc518afa08057482bac", + "list_with_hard_break_and_html": "d70ba3d993b683f6d6f2d64d98092eb654b3f61f6bd376c792f89ac0c5f50c00", + "literal_table": "348083ae4e90b920919d0f97df16eefa3ba4c95552645b0da17ebc77f5653bd6", + "malformed_links": "a819dc73b15e2a76b9e24ef1c98c1bff8dba2211dc423e81a162011c18281f47", + "mixed_direct_reference_precedence": "59074984579359980b85fa92b62e05e8bdf669cd5f1314f53cc17d27bec1c228", + "mixed_list_boundaries": "77015584853a936cd3e1cae6fcffb5d9d1cb563468811bfa18ba149217bb8ad3", + "nested_and_escaped_links": "e745d930d63b35507e36c35421b6e422f58689ce87f7f19655dba4f82f45bacf", + "ordered_and_nested_lists": "e37c8490f6aa752ad0bd03b6dc8721983f9a067be2a002a7b63aaccd1ecb37ea", + "ordered_list_nonstandard_starts": "6c0234e9760931f318b44ab45f42920720d6b4078c1b5a6834973f9e914d7bd3", + "parenthesized_list_marker_is_text": "174e4b8533ece00c34f781389ae4f699c48adf51fce41347417b37f261b4fd4c", + "plain_and_entities": "41368d1d7a497a35456a308b62fcc77be854c955e96361471335ab4a8f179546", + "preprocessing_boundary": "5d8fa3d177a1fadf56fca87d8bd0516cf23e1f005e831594ea52753aa2493962", + "reference_link_forms": "3344af2dde3f221490b51cf8826e2b075962c875d9bab2954618d2bb11f07ac8", + "reference_links": "17d3a54af09f12108738cf17c2ab4e75d8c57f2ecf6cdab94a2b18f9708d26ee", + "setext_and_thematic_break": "fd6ee32a139aed0453c52ba97c0bc738468bb4635f7de27edf6f73fb95da2747", + "setext_equals_and_underscores": "75e8b288bd6210d3df7109c17fea8d40311b1eb29f3fe07980e5736b27354b48", + "unicode_heading_space": "14e83b6ada24f9c001f2f62249adcb7f7843fcf6278f1cf096822e1973c6acea", + "unordered_lists": "c7d8c5b49811fc4bb0ef4e1593ca27e7f591c57bf3892b44321bc32b35bed13c", + "whitespace_only": "031a3819af28b5a3ad407b04f08ea58e647f1520252a1d42b5c2ec7b3378cfdb", + "zendesk_callout_all_styles": "8edb59a497ff6d163db810c72f670ddac0d5349de63704bf6472207faf3256e7", + "zendesk_callouts": "6b66c42678e2cc9a196e8fa32daa6b89dc1f04dc586f9cf97284c8e824942959", + "zendesk_empty_and_trailing_content": "5d7bcc3fb2ed750db7547b45f798fce0756dad72bce86839f328b1e6eb89e89a", + "zendesk_invalid_closing": "1f0a8958ee66fd87f2cb5bba1fac53d417140776330d84af2f8f8cff65a18ab3", + "zendesk_nested": "ead5922df2ca29b5b47ef95d051217bae193bf05edfff4838e4bf9d6df23afaf", + "zendesk_same_tag_nesting": "52cd4dda77dcd387e605583ed6145309751728ea4850bf801f077d089f8b9661", + "zendesk_sibling_custom_tags": "5b667bf654ec935f86e14d65150370cb2df867b52ae3965ed59c0b6caf27443b", + "zendesk_uppercase_and_multiple_styles": "b3983c67f1153e42a6952bdb466654d534a6fad0e2960f4c5455a54949462fe2" + }, + "expanded_cases_sha256": "2173c20d71bc273aa41c4793bac3a9f5a10adb581ba9cbfed738e7a34ae66671", + "fuzz_seed": 311084334, + "oracle": { + "mistune": "0.8.4", + "python": "3.11.9", + "revision": "a73f7122421a08c380b87c38fccfe8b54d169cb9", + "sdiff": "2.0.0" + }, + "schema_version": 1 +} diff --git a/tests/fixtures/compatibility/mistune_cases.json b/tests/fixtures/compatibility/mistune_cases.json new file mode 100644 index 0000000..c7645f0 --- /dev/null +++ b/tests/fixtures/compatibility/mistune_cases.json @@ -0,0 +1,363 @@ +{ + "fuzz_seed": 311084334, + "fuzz_cases": 1000, + "cases": [ + { + "name": "empty", + "parser": "MdParser", + "left": "" + }, + { + "name": "plain_and_entities", + "parser": "MdParser", + "left": "a & b & © &bogus; < > \"quoted\"" + }, + { + "name": "atx_headings", + "parser": "MdParser", + "left": "# heading\n\n###heading\n\n###### six\n\n####### seven" + }, + { + "name": "setext_and_thematic_break", + "parser": "MdParser", + "left": "heading\n===\n\nsubheading\n---\n\n---\n\n***" + }, + { + "name": "unicode_heading_space", + "parser": "MdParser", + "left": "#\u2003heading\n\n#\theading" + }, + { + "name": "inline_syntax_stays_text", + "parser": "MdParser", + "left": "*em* **strong** ~~strike~~ `code` \\*escaped*" + }, + { + "name": "block_syntax_stays_text", + "parser": "MdParser", + "left": "> quote\n\n```python\nprint('x')\n```\n\n indented" + }, + { + "name": "inline_html_is_escaped", + "parser": "MdParser", + "left": "before one after" + }, + { + "name": "block_html_and_comment", + "parser": "MdParser", + "left": "\n\n
\nbody\n
\n\n
" + }, + { + "name": "block_html_template_attributes", + "parser": "MdParser", + "left": "
\n{{ body }}\n
" + }, + { + "name": "links_and_images", + "parser": "MdParser", + "left": "[label](https://example.test/a_(b) \"title\") ![alt](image.png 'title')" + }, + { + "name": "reference_links", + "parser": "MdParser", + "left": "[resolved][docs] [collapsed][] [shortcut]\n\n[docs]: https://example.test/docs\n[collapsed]: /local" + }, + { + "name": "nested_and_escaped_links", + "parser": "MdParser", + "left": "[outer [inner]](url) [escaped \\] label](url) ![a [b]](img)" + }, + { + "name": "malformed_links", + "parser": "MdParser", + "left": "[open]( [missing](url ![image]( [label] (url)" + }, + { + "name": "hard_soft_and_blank_lines", + "parser": "MdParser", + "left": "one \ntwo\nthree\n\n\n\nfour\n \nfive" + }, + { + "name": "direction_marks_and_line_endings", + "parser": "MdParser", + "left": "\u200eone\r\ntwo\u200f\r\n" + }, + { + "name": "unordered_lists", + "parser": "MdParser", + "left": "* one\n- two\n+ three\n\n* loose one\n\n continuation\n\n* loose two" + }, + { + "name": "ordered_and_nested_lists", + "parser": "MdParser", + "left": "1. first\n2. second\n * nested\n * nested two\n3. third" + }, + { + "name": "headings_in_lists", + "parser": "MdParser", + "left": "1. ###header\n2. setext\n ---\n3. text" + }, + { + "name": "zendesk_callouts", + "parser": "ZendeskHelpMdParser", + "left": "\n# Title\nbody\n\n\nred\n\nbad" + }, + { + "name": "zendesk_nested", + "parser": "ZendeskHelpMdParser", + "left": "\n# Topic\n\n1. one\n2. two\n\n\nbody\n\n" + }, + { + "name": "zendesk_invalid_closing", + "parser": "ZendeskHelpMdParser", + "left": "\n1. one\n\n\none" + }, + { + "name": "leading_and_trailing_blank_lines", + "parser": "MdParser", + "left": "\n\nleading\n\n\n\ntrailing\n\n" + }, + { + "name": "whitespace_only", + "parser": "MdParser", + "left": " \n\t\n \n" + }, + { + "name": "closing_and_excess_heading_markers", + "parser": "MdParser", + "left": "### heading ###\n\n####### heading\n\n # deeply indented" + }, + { + "name": "setext_equals_and_underscores", + "parser": "MdParser", + "left": "level one\n===\n\n___\n\n***" + }, + { + "name": "literal_table", + "parser": "MdParser", + "left": "| a | b |\n|---|---|\n| c | d |" + }, + { + "name": "html_empty_and_self_closing", + "parser": "MdParser", + "left": "
\n\n
\n\n
\n\n" + }, + { + "name": "html_mismatched_case_and_attributes", + "parser": "MdParser", + "left": "
\nbody\n
\n\n
body
" + }, + { + "name": "html_inline_comment", + "parser": "MdParser", + "left": "before after" + }, + { + "name": "link_destination_forms", + "parser": "MdParser", + "left": "[plain](path) [angle]() [percent](a%20b) [empty]()" + }, + { + "name": "link_title_forms", + "parser": "MdParser", + "left": "[double](url \"title\") [single](url 'title') [parenthesized](url (title)) [angle]( \"title\")" + }, + { + "name": "link_multiline_destination", + "parser": "MdParser", + "left": "[label](https://example.test/\npath)" + }, + { + "name": "link_backticks_and_nested_labels", + "parser": "MdParser", + "left": "[a `b`](url) [outer [inner]](url)" + }, + { + "name": "link_deep_and_unbalanced_labels", + "parser": "MdParser", + "left": "[[[label]]](url) [[label](url)" + }, + { + "name": "legacy_nested_label_atom", + "parser": "MdParser", + "left": "[[[]](u) ![[[]](u) [[[]][r] ![[[]][r]" + }, + { + "name": "greedy_label_closing_boundary", + "parser": "MdParser", + "left": "[a](u) x ](v)\n\n[a](u)](v)" + }, + { + "name": "link_angle_fallback_boundaries", + "parser": "MdParser", + "left": "[missing-close-angle]()" + }, + { + "name": "link_unmatched_closing_boundaries", + "parser": "MdParser", + "left": "[a]b](u) [a]](u) [[a]](u) [[[a]]](u)" + }, + { + "name": "mixed_direct_reference_precedence", + "parser": "MdParser", + "left": "[a][ref](u) [a](u)[ref] [[a][ref]](u) [a] [ref](u)" + }, + { + "name": "reference_link_forms", + "parser": "MdParser", + "left": "[full][label] [empty][] [space] [label] ![image][missing] [shortcut]" + }, + { + "name": "autolinks_and_bare_urls_are_text", + "parser": "MdParser", + "left": " https://example.test person@example.test" + }, + { + "name": "empty_and_unclosed_images", + "parser": "MdParser", + "left": "![]() ![alt](url ![alt][missing]" + }, + { + "name": "ordered_list_nonstandard_starts", + "parser": "MdParser", + "left": "text\n2. two\n\n7. seven\n8. eight\n\n12345678901. large" + }, + { + "name": "parenthesized_list_marker_is_text", + "parser": "MdParser", + "left": "1) one\n2) two" + }, + { + "name": "mixed_list_boundaries", + "parser": "MdParser", + "left": "* star\n- dash\n+ plus\n\n1. ordered\n* unordered" + }, + { + "name": "list_with_hard_break_and_html", + "parser": "MdParser", + "left": "* one \n continued\n*
\n body\n
" + }, + { + "name": "list_thematic_break_exception", + "parser": "MdParser", + "left": "* one\n\n ---\n* two" + }, + { + "name": "zendesk_callout_all_styles", + "parser": "ZendeskHelpMdParser", + "left": "green\n\nred\n\nyellow" + }, + { + "name": "zendesk_uppercase_and_multiple_styles", + "parser": "ZendeskHelpMdParser", + "left": "upper\n\nmultiple" + }, + { + "name": "zendesk_empty_and_trailing_content", + "parser": "ZendeskHelpMdParser", + "left": "\n\none trailing" + }, + { + "name": "zendesk_same_tag_nesting", + "parser": "ZendeskHelpMdParser", + "left": "\ninner\n" + }, + { + "name": "zendesk_sibling_custom_tags", + "parser": "ZendeskHelpMdParser", + "left": "1. one\n\nwarning" + }, + { + "name": "deep_unmatched_link_openers", + "parser": "MdParser", + "left": "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[x](url)" + }, + { + "name": "inline_rule_selection", + "scenario": "inline_rule_selection", + "operations": [ + { + "name": "text_only_direct", + "source": "[x](url)", + "rules": ["text"] + }, + { + "name": "text_only_reference_call", + "source": "[x][ref]", + "rules": ["text"], + "call": true + }, + { + "name": "direct_rule", + "source": "[x](url)", + "rules": ["link", "text"] + }, + { + "name": "reference_rule", + "source": "[x][ref]", + "rules": ["reflink", "text"] + }, + { + "name": "empty_rules_use_defaults", + "source": "[x](url)", + "rules": [] + }, + { + "name": "none_uses_defaults", + "source": "[x][ref]" + } + ] + }, + { + "name": "block_rule_selection", + "scenario": "block_rule_selection", + "parser": "MdParser", + "operations": [ + { + "name": "text_only_heading", + "source": "# heading", + "rules": ["text"] + }, + { + "name": "heading_then_text", + "source": "# heading", + "rules": ["heading", "text"], + "call": true + }, + { + "name": "empty_rules_use_defaults", + "source": "# heading", + "rules": [] + } + ] + }, + { + "name": "block_parser_reuse", + "scenario": "reuse_block_parser", + "parser": "MdParser", + "left": "# first", + "right": "second" + }, + { + "name": "inline_parser_reuse", + "scenario": "reuse_inline_parser", + "parser": "MdParser", + "left": "first [one](url)", + "right": "second [two][ref]" + }, + { + "name": "preprocessing_boundary", + "scenario": "preprocessing_boundary", + "parser": "MdParser", + "left": "\u200eone \n\u200ftwo\n \nthree" + }, + { + "name": "exhaustive_link_label_matrix", + "scenario": "link_label_matrix" + }, + { + "name": "exhaustive_link_tail_matrix", + "scenario": "link_tail_matrix" + } + ] +} diff --git a/tests/test_mistune_compatibility.py b/tests/test_mistune_compatibility.py new file mode 100644 index 0000000..2afeb8b --- /dev/null +++ b/tests/test_mistune_compatibility.py @@ -0,0 +1,234 @@ +import json +from time import perf_counter +from pathlib import Path +from unittest import TestCase + +import mistune +import sdiff + +from sdiff import MdParser, ZendeskHelpMdParser, parser +from sdiff.model import Html, Image, Link, Text, ZendeskHelpCallout +from sdiff.renderer import TextRenderer +from sdiff.renderer import HtmlRenderer +from scripts.mistune_compat_cases import expand_cases +from scripts.mistune_compat_signatures import canonical_hash, run_case + + +REPO_ROOT = Path(__file__).resolve().parents[1] +COMPATIBILITY_CORPUS = REPO_ROOT / 'tests/fixtures/compatibility/mistune_cases.json' +GOLDEN_MISTUNE_084_FIXTURES = ( + REPO_ROOT / 'tests/fixtures/compatibility/golden_mistune_084_fixtures.json' +) + + +class TestLegacyMistuneBehaviorContract(TestCase): + def test_existing_entities_are_not_double_escaped(self): + tree = parser.parse('a & b & © &bogus; < >') + self.assertEqual('a & b & © &bogus; < >', tree.nodes[0].nodes[0].text) + + def test_nonstructural_markdown_remains_text(self): + source = '*em* **strong** `code` ~~strike~~\n\n> quote\n\n```python\npass\n```' + tree = parser.parse(source) + self.assertNotIn('code', {node.name for node in tree.nodes}) + self.assertEqual(source.replace('> quote', '> quote'), TextRenderer().render(tree)) + + def test_link_and_image_keep_the_exact_markdown_source(self): + link_tree = parser.parse('[label](https://example.test/path "title")') + image_tree = parser.parse("![alt](image.png 'title')") + link = link_tree.nodes[0].nodes[0] + image = image_tree.nodes[0].nodes[0] + self.assertIsInstance(link, Link) + self.assertIsInstance(image, Image) + self.assertEqual('[label](https://example.test/path "title")', link.text) + self.assertEqual("![alt](image.png 'title')", image.text) + + def test_inline_image_after_text_keeps_legacy_link_classification(self): + tree = parser.parse('text ![alt](image.png)') + self.assertEqual('pta', tree.print_all()) + self.assertEqual('text !', tree.nodes[0].nodes[0].text) + self.assertEqual('[alt](image.png)', tree.nodes[0].nodes[1].text) + + def test_nested_parenthesis_link_keeps_legacy_truncated_structure(self): + tree = parser.parse('[label](https://example.test/a_(b) "title")') + self.assertEqual('pat', tree.print_all()) + self.assertEqual('[label](https://example.test/a_(b)', tree.nodes[0].nodes[0].text) + self.assertEqual(' "title")', tree.nodes[0].nodes[1].text) + + def test_escaped_bracket_label_keeps_legacy_link_boundary(self): + tree = parser.parse(r'[escaped \] label](url)') + self.assertEqual('pa', tree.print_all()) + self.assertEqual(r'[escaped \] label](url)', tree.nodes[0].nodes[0].text) + + def test_invalid_link_boundaries_remain_text(self): + sources = ( + '[label][unclosed', + '[label][bad^reference]', + ) + for source in sources: + with self.subTest(source=source): + tree = parser.parse(source) + self.assertNotIn('a', tree.print_all()) + + def test_nested_and_angle_boundaries_keep_legacy_link_shapes(self): + expectations = { + '[[[label]]](url)': 'pa', + '[[label](url)': 'pta', + '[label]()': 'pa', + } + for source, expected in expectations.items(): + with self.subTest(source=source): + self.assertEqual(expected, parser.parse(source).print_all()) + + def test_nested_label_atoms_and_greedy_closing_match_legacy_regex(self): + expectations = { + '[[[]](u)': ('pa', ['[[[]](u)']), + '![[[]](u)': ('pi', ['![[[]](u)']), + '[[[]][r]': ('pa', ['[[[]][r]']), + '![[[]][r]': ('pi', ['![[[]][r]']), + '[a](u)](v)': ('pa', ['[a](u)](v)']), + '[a](u)](': ('pat', ['[a](u)', '](']), + '[a]()': ('pa', ['[a]()']), + '[a](one after') + block = parser.parse('
\none\n
') + self.assertIsInstance(inline.nodes[0].nodes[0], Text) + self.assertEqual('before <sub>one</sub> after', inline.nodes[0].nodes[0].text) + self.assertIsInstance(block.nodes[0], Html) + self.assertEqual('
\none\n
', block.nodes[0].text) + + def test_bare_thematic_break_is_not_a_structural_node(self): + tree = parser.parse('---') + self.assertEqual('pt', tree.print_all()) + self.assertEqual('---', tree.nodes[0].nodes[0].text) + + def test_zendesk_callout_recursively_uses_compatibility_parser(self): + tree = parser.parse( + '\n# Heading\n[label][missing]\n', + parser_cls=ZendeskHelpMdParser, + ) + self.assertIsInstance(tree.nodes[0], ZendeskHelpCallout) + self.assertEqual('red', tree.nodes[0].style) + self.assertEqual('C1tpa', tree.print_all()) + + def test_parser_facades_remain_constructible(self): + nodes = MdParser()('plain') + self.assertEqual('paragraph', nodes[0].name) + self.assertEqual([], parser.InlineLexer()(' ')) + self.assertEqual('ZendeskHelpMdParser', ZendeskHelpMdParser.__name__) + self.assertEqual('3.3.4', mistune.__version__) + + def test_inline_parser_honors_explicit_rule_selection(self): + text_only = parser.InlineLexer().parse('[x](url)', ['text']) + direct_only = parser.InlineLexer().parse('[x](url)', ['link', 'text']) + reference_only = parser.InlineLexer()('[x][ref]', ['reflink', 'text']) + self.assertEqual([('[x](url)', Text)], [(node.text, type(node)) for node in text_only]) + self.assertEqual([('[x](url)', Link)], [(node.text, type(node)) for node in direct_only]) + self.assertEqual([('[x][ref]', Link)], [(node.text, type(node)) for node in reference_only]) + + def test_parser_instances_reuse_the_legacy_token_list(self): + block_parser = MdParser() + first_block_result = block_parser.parse('# first') + second_block_result = block_parser.parse('second') + self.assertIs(first_block_result, second_block_result) + self.assertIs(block_parser.tokens, second_block_result) + self.assertEqual(['header', 'paragraph'], [node.name for node in second_block_result]) + + inline_parser = parser.InlineLexer() + first_inline_result = inline_parser.parse('first') + second_inline_result = inline_parser.parse('second') + self.assertIs(first_inline_result, second_inline_result) + self.assertIs(inline_parser.tokens, second_inline_result) + self.assertEqual(['first', 'second'], [node.text for node in second_inline_result]) + + def test_parser_factory_and_preprocessing_boundaries_match_legacy_api(self): + source = '\u200eone\u200f' + direct = MdParser().parse(source) + public = parser.parse(source) + self.assertEqual(source, direct[0].nodes[0].text) + self.assertEqual('one', public.nodes[0].nodes[0].text) + self.assertIs(type(MdParser.get_lexer()), MdParser) + self.assertIs(type(ZendeskHelpMdParser.get_lexer()), ZendeskHelpMdParser) + + def test_legacy_list_thematic_break_value_is_retained(self): + nodes = MdParser().parse('* one\n\n ---\n* two') + thematic_break = nodes[0].nodes[0].nodes[2] + self.assertEqual({'type': 'hrule'}, thematic_break) + + def test_zendesk_rules_do_not_leak_into_plain_parser_instances(self): + original_rules = list(MdParser.default_rules) + first = ZendeskHelpMdParser() + second = ZendeskHelpMdParser() + self.assertEqual(original_rules, MdParser.default_rules) + self.assertIsNot(first._active_rules, second._active_rules) + self.assertEqual(['tabs', 'steps', 'callout'], first._active_rules[:3]) + parser.parse('one', parser_cls=ZendeskHelpMdParser) + plain = parser.parse('one', parser_cls=MdParser) + self.assertNotEqual('tabs', plain.nodes[0].name) + + def test_malformed_links_are_scanned_in_bounded_time(self): + cases = ( + '[label](' * 8_000, + '[' * 16_000 + 'x](url)', + '![' * 16_000 + 'x](url)', + ) + for source in cases: + with self.subTest(prefix=source[:16], length=len(source)): + started = perf_counter() + tree = parser.parse(source) + elapsed = perf_counter() - started + self.assertEqual('paragraph', tree.nodes[0].name) + self.assertLess(elapsed, 2.0) + + +class TestGoldenMistune084Fixtures(TestCase): + maxDiff = None + + @classmethod + def setUpClass(cls): + cls.expected = json.loads(GOLDEN_MISTUNE_084_FIXTURES.read_text(encoding='utf-8')) + cls.cases = expand_cases(COMPATIBILITY_CORPUS, REPO_ROOT) + + def test_expanded_corpus_matches_golden_fixture_inventory(self): + case_names = [case['name'] for case in self.cases] + self.assertEqual(1, self.expected['schema_version']) + self.assertEqual('0.8.4', self.expected['oracle']['mistune']) + self.assertEqual(self.expected['case_count'], len(self.cases)) + self.assertEqual(len(case_names), len(set(case_names))) + self.assertEqual(set(self.expected['cases']), set(case_names)) + self.assertEqual(self.expected['expanded_cases_sha256'], canonical_hash(self.cases)) + + def test_every_target_result_matches_golden_mistune_084_fixture(self): + for case in self.cases: + with self.subTest(case=case['name']): + signature = run_case(case, sdiff, parser, HtmlRenderer, TextRenderer) + actual_hash = canonical_hash(signature) + expected_hash = self.expected['cases'][case['name']] + if actual_hash != expected_hash: + details = { + 'case': case, + 'expected_hash': expected_hash, + 'actual_hash': actual_hash, + 'actual_signature': signature, + } + self.fail(json.dumps(details, ensure_ascii=False, indent=2, sort_keys=True)) From 5ebec5545ba6df34942403c9afd73fc87b471c49 Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Wed, 26 Aug 2026 16:21:51 -0500 Subject: [PATCH 13/20] chore: finalize 2.0.0 release workflow --- CHANGELOG | 2 +- Makefile | 8 +++++--- README.md | 20 ++++++++++++++++++++ docs/python311-migration-contract.md | 3 +-- tests/test_sdiff.py | 8 +++----- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3dad7c3..8154da2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -2.0.0 (unreleased) +2.0.0 (2026-08-26) ------------------ - Require Python 3.11. diff --git a/Makefile b/Makefile index 3aa31b7..dcdaf08 100644 --- a/Makefile +++ b/Makefile @@ -109,10 +109,12 @@ cov cover: $(COVERAGE) html --directory coverage @echo "Coverage HTML written to coverage/index.html" -package: - $(PYTHON) -m build +package: publish -publish: package +publish: + $(PIP) install -U twine build + rm -rf dist + $(PYTHON) -m build . $(TWINE) upload --verbose --sign --username developer --repository-url http://$(PYPICLOUD_HOST)/simple/ dist/*.whl hooks: diff --git a/README.md b/README.md index 1a22f93..b721960 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,26 @@ make requirements `make requirements` uses `pip-compile` to regenerate `requirements.txt` and `requirements-dev.txt` from `pyproject.toml`. +### New release + +#### Release + +1. Bump the version in `pyproject.toml` and add the release date to `CHANGELOG`. +2. Commit the changes and merge them to `master`. +3. `git tag ` +4. `git push origin ` +5. `make publish` +6. Go to http://10.10.1.143:8080/#/admin, log in with the admin credentials from BE 1Password, select + **Rebuild package list**, and wait for completion. +7. Go to http://10.10.2.107:8080/#/admin, log in with the admin credentials from BE 1Password, select + **Rebuild package list**, and wait for completion. + +Step 5 builds the wheel and source distribution, then uploads the wheel to the internal PyPI. If `sdiff` code changed, +a new version must be assigned or the upload will fail. Internal PyPI credentials are required and are available in +[1Password](https://start.1password.com/open/i?a=RAREY3D7KJDHPFPS5BGKXGA5TI&h=keepsafe.1password.com&i=vsqhjp66yb7r5mg2bb3whwiqg4&v=7frtpxbxiu4bkuqxh5rnechwhi). +[Twine keyring support](https://twine.readthedocs.io/en/stable/#keyring-support) may also be used. +`make package` is an alias of `make publish` and performs the same upload workflow. + ### Golden Mistune 0.8.4 Fixtures Normal development and `make test` do not need an oracle checkout. The regular test suite checks every curated, diff --git a/docs/python311-migration-contract.md b/docs/python311-migration-contract.md index 108c408..70cfb26 100644 --- a/docs/python311-migration-contract.md +++ b/docs/python311-migration-contract.md @@ -263,6 +263,7 @@ Proof results: | extracted-sdist test suite | Pass | All 77 tests and 1,120 subtests passed directly from the unpacked source archive. | | isolated wheel install/import/diff smoke | Pass | A separate CPython 3.11.13 venv installed `sdiff==2.0.0` with `mistune==3.3.4` and ran `sdiff.diff()`. | | CircleCI validate, `--next`, and config process | Pass | CircleCI accepted the native source `version: 2.1`, strict upcoming-compiler validation, and reusable-config expansion. | +| PR #14 remote CI | Pass | CircleCI `prepare_cache`, `lint`, and `test`, plus Travis CI branch and pull-request builds, passed at `ebb7109261cdddcf72f5ae9907501236ef067767`. | | downstream `content-validator` test suite | Pass | 65 tests passed with one expected skip against the local target and Mistune 3.3.4; HTTP was mocked. | | `make hooks`, installed-file comparison, `make unhooks` | Pass | Executable pre-push hook installed exactly and was removed after verification. | | `git diff --check` | Pass | No whitespace errors. | @@ -271,9 +272,7 @@ An additional clean run passed all 71 tests on Python 3.11.9 before the exact 3. ## Known Gaps -- Remote CI is not exercised by default local proof. - Downstream `content-validator`, `email-service`, and `translation-real-time-validaton` still need their own requirements refresh and integration proof after consuming the merged package state. - No 2.x package tag or release has been created. Downstreams must pin the final reviewed immutable tag/commit, never the moving branch. -- The Mistune change has not been committed or pushed; review approval is still required. diff --git a/tests/test_sdiff.py b/tests/test_sdiff.py index a132509..69ac666 100644 --- a/tests/test_sdiff.py +++ b/tests/test_sdiff.py @@ -1,18 +1,16 @@ +from pathlib import Path from unittest import TestCase -import os import sdiff -from pathlib import Path - from sdiff import ZendeskHelpMdParser def _load_fixture(*path): - return open(os.path.join('tests/fixtures', *path)).read() + return Path('tests/fixtures', *path).read_text(encoding='utf-8') def _read_test_files(dirpath): - path = Path(os.path.join('tests/fixtures', dirpath)) + path = Path('tests/fixtures', dirpath) filenames = {f.name.split('.')[0] for f in path.glob('*.md')} return [('%s.en.md' % fn, '%s.de.md' % fn) for fn in filenames] From 5fa63c82692d366acad283994295b5cbf5124f02 Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Wed, 26 Aug 2026 16:40:30 -0500 Subject: [PATCH 14/20] docs: finalize migration release proof --- docs/python311-migration-contract.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/python311-migration-contract.md b/docs/python311-migration-contract.md index 70cfb26..cc8e70c 100644 --- a/docs/python311-migration-contract.md +++ b/docs/python311-migration-contract.md @@ -159,8 +159,8 @@ Required commands on the final branch: - `CI=1 make test-only` - public import/API smoke - `make depcheck` -- `venv/bin/python -m compileall -q sdiff tests` -- `make package` +- `venv/bin/python -m compileall -q sdiff tests scripts` +- `rm -rf dist && venv/bin/python -m build .` - built-wheel install and import smoke in an isolated CPython 3.11.13 venv - `circleci config validate .circleci/config.yml` when the local CircleCI CLI can validate without a paid service - `git diff --check` @@ -214,8 +214,8 @@ Date: 2026-08-26 Branch: `python311-upgrade` -The Python migration commits and draft pull request already exist on `python311-upgrade`. The Mistune 3.3.4 port is -being prepared as one additional reviewable commit; all changes remain uncommitted until explicit user approval. +Draft PR #14 contains the committed Python migration, Mistune 3.3.4 compatibility port, and 2.0.0 release workflow +on `python311-upgrade`. The final 2.0.0 tag and internal package publication remain pending. Completed applicable work: @@ -225,7 +225,8 @@ Completed applicable work: - Hard-pinned the runtime dependency and generated deterministic requirements artifacts with `pip-compile`. - Moved Flake8 and coverage configuration into `pyproject.toml`. - Added package-oriented `env`, `dev`, `lint`, `test-only`, `fixture-smoke`, `import-smoke`, `depcheck`, - `requirements`, package-build, CI-install, and hook targets. + `requirements`, CI-install, release, and hook targets. `package` intentionally aliases the publishing workflow; + build-only verification uses `venv/bin/python -m build .` directly. - Added native CircleCI 2.1 package jobs with reusable executors/commands for dependency preparation, lint, tests, xUnit, and coverage XML. - Added exact golden parser/renderer/structural-diff/link-diff snapshots for 13 behavior-sensitive scenarios. @@ -257,13 +258,13 @@ Proof results: | `make depcheck` | Pass | `pip check` found no broken requirements. | | `CI=1 make test-only` | Pass | 77 tests passed and wrote `build/test/results.xml` plus `build/coverage/coverage.xml`. | | `venv/bin/python -m compileall -q sdiff tests scripts` | Pass | Source, tests, and compatibility scripts compiled on Python 3.11.13. | -| `make package` | Pass | Built isolated `sdiff-2.0.0.tar.gz` and `sdiff-2.0.0-py3-none-any.whl`. | +| `rm -rf dist && venv/bin/python -m build .` | Pass | Built isolated `sdiff-2.0.0.tar.gz` and `sdiff-2.0.0-py3-none-any.whl` without invoking the publishing targets. | | `venv/bin/twine check dist/*` | Pass | Both distribution artifacts passed metadata/README validation. | | sdist content listing | Pass | Archive contains all tests, compatibility helpers, same/different Markdown fixtures, golden JSON, and Golden Mistune 0.8.4 Fixtures. | | extracted-sdist test suite | Pass | All 77 tests and 1,120 subtests passed directly from the unpacked source archive. | | isolated wheel install/import/diff smoke | Pass | A separate CPython 3.11.13 venv installed `sdiff==2.0.0` with `mistune==3.3.4` and ran `sdiff.diff()`. | | CircleCI validate, `--next`, and config process | Pass | CircleCI accepted the native source `version: 2.1`, strict upcoming-compiler validation, and reusable-config expansion. | -| PR #14 remote CI | Pass | CircleCI `prepare_cache`, `lint`, and `test`, plus Travis CI branch and pull-request builds, passed at `ebb7109261cdddcf72f5ae9907501236ef067767`. | +| PR #14 remote CI | Pass | CircleCI `prepare_cache`, `lint`, and `test`, plus Travis CI branch and pull-request builds, passed at `5ebec5545ba6df34942403c9afd73fc87b471c49`. | | downstream `content-validator` test suite | Pass | 65 tests passed with one expected skip against the local target and Mistune 3.3.4; HTTP was mocked. | | `make hooks`, installed-file comparison, `make unhooks` | Pass | Executable pre-push hook installed exactly and was removed after verification. | | `git diff --check` | Pass | No whitespace errors. | From f503dcf75b786897f93d07b13ab8c707b433f524 Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Wed, 26 Aug 2026 17:06:44 -0500 Subject: [PATCH 15/20] fix: preserve quoted Mistune link titles --- README.md | 2 +- docs/python311-migration-contract.md | 18 +++-- scripts/mistune_compat_signatures.py | 2 +- sdiff/parser.py | 79 ++++++++++++------- .../golden_mistune_084_fixtures.json | 11 ++- .../fixtures/compatibility/mistune_cases.json | 15 ++++ tests/test_mistune_compatibility.py | 22 ++++++ 7 files changed, 107 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index b721960..114416e 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ required, and a partially cleaned `/tmp` directory is never trusted. Success ends with: ```text -Mistune 0.8.4 oracle vs 3.3.4 target: 1086 cases, 0 mismatches +Mistune 0.8.4 oracle vs 3.3.4 target: 1089 cases, 0 mismatches ``` The command runs each checkout with its own `venv`, compares normalized parser, renderer, and diff results, and exits diff --git a/docs/python311-migration-contract.md b/docs/python311-migration-contract.md index cc8e70c..637673a 100644 --- a/docs/python311-migration-contract.md +++ b/docs/python311-migration-contract.md @@ -52,9 +52,9 @@ HTML, heading styles, lists, images and reference links, hard breaks and directi insert/delete symmetry, and nested Zendesk constructs. `scripts/run_mistune_compat.py` runs the frozen 0.8.4 worktree and this 3.3.4 worktree in isolated virtual -environments. The runner expands 61 committed corpus entries, all 13 golden cases, all 12 automatically discovered -Markdown fixture pairs, and 1,000 fixed-seed structured fuzz pairs for 1,086 named comparisons. Two aggregate corpus -entries exhaustively check another 98,334 short link/image label and destination combinations. The runner compares +environments. The runner expands 64 committed corpus entries, all 13 golden cases, all 12 automatically discovered +Markdown fixture pairs, and 1,000 fixed-seed structured fuzz pairs for 1,089 named comparisons. Two aggregate corpus +entries exhaustively check another 329,200 short link/image label and destination combinations. The runner compares recursive ASTs, exact text/HTML rendering, structural and link diff results, mutated metadata, and phase-specific exception signatures. @@ -191,7 +191,8 @@ Expected evidence: - The adapter honors direct `InlineLexer.parse(..., rules=...)` rule ordering and reused-parser token-list identity, matching the exposed 0.8.4 facade behavior. - The legacy nested-label and destination grammar is compiled into a linear-time index. This preserves 0.8.4's - unusual greedy/backtracking boundaries while eliminating repeated suffix scans on malformed nested openers. + unusual greedy/backtracking boundaries, including quoted titles containing `)` and angle-destination fallback, + while eliminating repeated suffix scans on malformed nested openers. - The port removes the class-level Zendesk rule mutation that could make a later plain `MdParser` call fail. This is an intentional target-only defect fix; document outputs remain oracle-identical. - Existing compatible hard pins are retained: `coverage==7.6.1` and `flake8==7.1.1`. @@ -249,19 +250,20 @@ Proof results: | `make ci-dev-install` | Pass | CI bootstrap installed/reused the exact-version venv and final `.[dev]` dependency shape. | | full skill pyupgrade ladder over `sdiff` and `tests` | Pass | All six stages were reconfirmed live with zero remaining changes; the earlier Python 3.6 stage changes are isolated in commit `7c78ad3`. | | `make requirements` twice | Pass | Both pip-compile outputs were byte-stable; compile tooling is `pip-tools==7.5.3` with `pip==25.3`. | -| `make test` | Pass | Flake8 and msgpack guard passed; 77 tests passed on Python 3.11.13, including the Golden Mistune 0.8.4 Fixtures. | +| `make test` | Pass | Flake8 and msgpack guard passed; 78 tests passed on Python 3.11.13, including the Golden Mistune 0.8.4 Fixtures. | | `make coverage` | Pass | Total branch-aware coverage is 99%; `sdiff/parser.py` has 100% statement and 99% branch coverage. | | `make fixture-smoke` | Pass | 3 test methods and 12 fixture subtests passed, including the exact golden snapshot. | -| `make mistune-compat` | Pass | Mistune 0.8.4 oracle vs 3.3.4 target: 1,086 named cases, 0 mismatches; aggregate matrix cases cover 98,334 additional link/image combinations. | +| `make mistune-compat` | Pass | Mistune 0.8.4 oracle vs 3.3.4 target: 1,089 named cases, 0 mismatches; aggregate matrix cases cover 329,200 additional link/image combinations. | +| direct-link endpoint oracle sweep | Pass | 1,921,600 exhaustive tails through length seven plus 500,000 deterministic randomized inputs matched Mistune 0.8.4 with zero mismatches. | | malformed-link bounded-time regression | Pass | 16,000 nested link and image openers complete in about 0.03 seconds each; the test ceiling is 2 seconds. | | `make import-smoke` | Pass | Imported documented public API and printed `2.0.0 MdParser ZendeskHelpMdParser TextRenderer`. | | `make depcheck` | Pass | `pip check` found no broken requirements. | -| `CI=1 make test-only` | Pass | 77 tests passed and wrote `build/test/results.xml` plus `build/coverage/coverage.xml`. | +| `CI=1 make test-only` | Pass | 78 tests passed and wrote `build/test/results.xml` plus `build/coverage/coverage.xml`. | | `venv/bin/python -m compileall -q sdiff tests scripts` | Pass | Source, tests, and compatibility scripts compiled on Python 3.11.13. | | `rm -rf dist && venv/bin/python -m build .` | Pass | Built isolated `sdiff-2.0.0.tar.gz` and `sdiff-2.0.0-py3-none-any.whl` without invoking the publishing targets. | | `venv/bin/twine check dist/*` | Pass | Both distribution artifacts passed metadata/README validation. | | sdist content listing | Pass | Archive contains all tests, compatibility helpers, same/different Markdown fixtures, golden JSON, and Golden Mistune 0.8.4 Fixtures. | -| extracted-sdist test suite | Pass | All 77 tests and 1,120 subtests passed directly from the unpacked source archive. | +| extracted-sdist test suite | Pass | All 78 tests and 1,129 subtests passed directly from the unpacked source archive. | | isolated wheel install/import/diff smoke | Pass | A separate CPython 3.11.13 venv installed `sdiff==2.0.0` with `mistune==3.3.4` and ran `sdiff.diff()`. | | CircleCI validate, `--next`, and config process | Pass | CircleCI accepted the native source `version: 2.1`, strict upcoming-compiler validation, and reusable-config expansion. | | PR #14 remote CI | Pass | CircleCI `prepare_cache`, `lint`, and `test`, plus Travis CI branch and pull-request builds, passed at `5ebec5545ba6df34942403c9afd73fc87b471c49`. | diff --git a/scripts/mistune_compat_signatures.py b/scripts/mistune_compat_signatures.py index deccab7..fff5a79 100644 --- a/scripts/mistune_compat_signatures.py +++ b/scripts/mistune_compat_signatures.py @@ -201,7 +201,7 @@ def _link_label_matrix_sources(): def _link_tail_matrix_sources(): - for length in range(6): + for length in range(7): for characters in itertools.product('<>)\'" a', repeat=length): suffix = ''.join(characters) yield '[x](' + suffix diff --git a/sdiff/parser.py b/sdiff/parser.py index 2a4a7e5..3efa4cc 100644 --- a/sdiff/parser.py +++ b/sdiff/parser.py @@ -130,49 +130,73 @@ def _next_nonspace_indexes(source): return indexes -def _build_direct_tail_ends(source, next_parenthesis, next_nonspace): +def _build_direct_tail_ends(source, next_nonspace): """Return the Mistune 0.8 direct-link tail end for each possible label close.""" - quote_tail_ends = [None] * len(source) - next_valid_quote = [None] * (len(source) + 1) + source_length = len(source) + + # Mistune 0.8 treats either quote character as a valid title terminator; + # the opening and closing quotes do not need to use the same character. + quote_tail_ends = [None] * source_length + next_valid_quote = [None] * (source_length + 1) next_quote = None - for position in range(len(source) - 1, -1, -1): + for position in range(source_length - 1, -1, -1): if source[position] in ('\'', '"'): close_parenthesis = next_nonspace[position + 1] - if close_parenthesis < len(source) and source[close_parenthesis] == ')': + if close_parenthesis < source_length and source[close_parenthesis] == ')': quote_tail_ends[position] = close_parenthesis + 1 next_quote = position next_valid_quote[position] = next_quote - angle_tail_ends = [None] * len(source) - next_valid_angle = [None] * (len(source) + 1) - next_angle = None - for position in range(len(source) - 1, -1, -1): - if source[position] == '>': - tail_start = next_nonspace[position + 1] - if tail_start < len(source) and source[tail_start] == ')': - angle_tail_ends[position] = tail_start + 1 - elif tail_start > position + 1 and tail_start < len(source) and source[tail_start] in ('\'', '"'): - closing_quote = next_valid_quote[tail_start + 1] + # Exact matches of the suffix after Mistune 0.8's non-greedy destination: + # (?:\s+['"][\s\S]*?['"])?\s*\) + # The quoted-title branch is attempted before the no-title branch. + suffix_ends = [None] * (source_length + 1) + for position, char in enumerate(source): + if char == ')': + suffix_ends[position] = position + 1 + elif char.isspace(): + suffix_start = next_nonspace[position] + if suffix_start < source_length and source[suffix_start] in ('\'', '"'): + closing_quote = next_valid_quote[suffix_start + 1] if closing_quote is not None: - angle_tail_ends[position] = quote_tail_ends[closing_quote] - if angle_tail_ends[position] is not None: - next_angle = position + suffix_ends[position] = quote_tail_ends[closing_quote] + elif suffix_start < source_length and source[suffix_start] == ')': + suffix_ends[position] = suffix_start + 1 + + # The destination is non-greedy, so use the earliest body endpoint whose + # suffix matches. Angle mode similarly uses the earliest `>` immediately + # followed by a valid suffix. + next_valid_suffix = [None] * (source_length + 1) + next_valid_angle = [None] * (source_length + 1) + next_suffix = None + next_angle = None + for position in range(source_length - 1, -1, -1): + if suffix_ends[position] is not None: + next_suffix = position + next_valid_suffix[position] = next_suffix + + if source[position] == '>' and suffix_ends[position + 1] is not None: + next_angle = position next_valid_angle[position] = next_angle - tail_ends = [None] * len(source) - for position in range(len(source) - 1): + tail_ends = [None] * source_length + for position in range(source_length - 1): if source[position:position + 2] != '](': continue + destination_start = next_nonspace[position + 2] - fallback_parenthesis = next_parenthesis[destination_start] - if fallback_parenthesis is None: - continue - if destination_start < len(source) and source[destination_start] == '<': + + # Optional angle mode is attempted first. If it cannot finish, the + # regex backtracks and treats `<` as ordinary destination content. + if destination_start < source_length and source[destination_start] == '<': closing_angle = next_valid_angle[destination_start + 1] if closing_angle is not None: - tail_ends[position] = angle_tail_ends[closing_angle] + tail_ends[position] = suffix_ends[closing_angle + 1] continue - tail_ends[position] = fallback_parenthesis + 1 + + suffix_start = next_valid_suffix[destination_start] + if suffix_start is not None: + tail_ends[position] = suffix_ends[suffix_start] return tail_ends @@ -222,9 +246,8 @@ def _build_link_indexes(source): next_open = _next_character_indexes(source, '[') next_bracket = _next_character_indexes(source, ']') next_caret = _next_character_indexes(source, '^') - next_parenthesis = _next_character_indexes(source, ')') next_nonspace = _next_nonspace_indexes(source) - direct_tails = _build_direct_tail_ends(source, next_parenthesis, next_nonspace) + direct_tails = _build_direct_tail_ends(source, next_nonspace) reference_tails = _build_reference_tail_ends( source, next_bracket, diff --git a/tests/fixtures/compatibility/golden_mistune_084_fixtures.json b/tests/fixtures/compatibility/golden_mistune_084_fixtures.json index 8d44035..0df9021 100644 --- a/tests/fixtures/compatibility/golden_mistune_084_fixtures.json +++ b/tests/fixtures/compatibility/golden_mistune_084_fixtures.json @@ -1,5 +1,5 @@ { - "case_count": 1086, + "case_count": 1089, "cases": { "atx_headings": "f7be5be83689cf22dd77489d41ab7881d1f92fdcce915578c47bbd31932410e3", "autolinks_and_bare_urls_are_text": "f01bbeae321bf5357c645c236c0b81689c320f22111ebc28e7419984925f4a4e", @@ -14,7 +14,7 @@ "empty": "031a3819af28b5a3ad407b04f08ea58e647f1520252a1d42b5c2ec7b3378cfdb", "empty_and_unclosed_images": "9e1a345d0d80b3dec00dfd66e77195c1d3aaa0768f2c9f40f72f353baf7dfb93", "exhaustive_link_label_matrix": "f891cf17d584b865562e6a30af596854701f680c0d3809f1956b508ab01d9286", - "exhaustive_link_tail_matrix": "819c8d053459be2e7c3038f87120bead4995df315eb6052a2897118e716e08f7", + "exhaustive_link_tail_matrix": "d708a48837c65b76b9d59b013c8620268b600fe0da6ef04d80ae179709f686bc", "fixture_tests_fixtures_different_extra_paragraph": "4f4044ad3694fac516bb4fb8697b29efde888dbb0aae6b87b370255a460bf4f1", "fixture_tests_fixtures_different_zendesk_callout_in_tabs": "4d63ca718729a84c5d63bc618d7379a88ee528366863bec242a598c1b95d0c93", "fixture_tests_fixtures_different_zendesk_steps": "30dfb15eb7a6be8f4dd84f684725bc54d254b5401f826ade737600fb7c3dc3cb", @@ -1046,6 +1046,7 @@ "html_empty_and_self_closing": "2118bd89e5e791ffda68e849e7de684a45c15272f0dbbd4ef88feb46c77528d8", "html_inline_comment": "e078b0c7f6ce7a6509917ab936d913b3a8952046f914381c1c880f5d30c98046", "html_mismatched_case_and_attributes": "371e8e82c834c84d9cec5fee778be8a54750607db821edfe3688c408e2a8a252", + "image_titles_with_closing_parentheses": "8e330e422c141af716981538d092cb7cfa914ee1db1d6c1429189d34bcd7085b", "inline_html_is_escaped": "bf38be2ddbcbaac36438d275330de8ce00bf921059a145728b3a614d6d809e56", "inline_parser_reuse": "179af2c54a9197e53172b31170b39bde737cd3c4b145ba3160f228f94c1ce85e", "inline_rule_selection": "8ace84d3afff2f3c1c2a06d080fe62941116c2429a6bc4f321c5cfe2c0653b15", @@ -1057,7 +1058,9 @@ "link_deep_and_unbalanced_labels": "5ffff731ad3ad2b5b571e1eb10deb38e6a9bd8db4d4b0937b37b8ee5c54899d7", "link_destination_forms": "2334165975b2593c3296aec4a31fa970de2555be00870ddeb42516c8b5f40cb2", "link_multiline_destination": "d850ad3f67655636cfe8bd26ffba99759f2a0cec42785cf08d2065e0654faffb", + "link_title_angle_and_fallback_boundaries": "2b217d60dd080a3d9da14a9086a58ebd444bbbd425c4c14aca55d4a044043c2f", "link_title_forms": "7b69c2bb8fd4f872ebb7311a664762d0e667e000d17d26494d3613b070d3dcf5", + "link_titles_with_closing_parentheses": "fb75c6f8537b156c5d12e8bd9f5f38674e42d966cf7110a4f72e9559cbe2e38c", "link_unmatched_closing_boundaries": "7539db8e34dda790422b11bee6e32094177caf380ebe6b31bd6cb3963f505bde", "links_and_images": "2d5d25bc284366db81632c66fe9eb4fb6a75c9b5237df0720e420bbc9fbf8e54", "list_thematic_break_exception": "31930d90b3ec6b0235522fe9d0053d76f7f1ed1dbffbfdc518afa08057482bac", @@ -1088,11 +1091,11 @@ "zendesk_sibling_custom_tags": "5b667bf654ec935f86e14d65150370cb2df867b52ae3965ed59c0b6caf27443b", "zendesk_uppercase_and_multiple_styles": "b3983c67f1153e42a6952bdb466654d534a6fad0e2960f4c5455a54949462fe2" }, - "expanded_cases_sha256": "2173c20d71bc273aa41c4793bac3a9f5a10adb581ba9cbfed738e7a34ae66671", + "expanded_cases_sha256": "4d46f648a0b58e7e0d5db234bf6ef36d234ca249cdb6945c807ca6c917bc5237", "fuzz_seed": 311084334, "oracle": { "mistune": "0.8.4", - "python": "3.11.9", + "python": "3.11.13", "revision": "a73f7122421a08c380b87c38fccfe8b54d169cb9", "sdiff": "2.0.0" }, diff --git a/tests/fixtures/compatibility/mistune_cases.json b/tests/fixtures/compatibility/mistune_cases.json index c7645f0..d094037 100644 --- a/tests/fixtures/compatibility/mistune_cases.json +++ b/tests/fixtures/compatibility/mistune_cases.json @@ -162,6 +162,21 @@ "parser": "MdParser", "left": "[double](url \"title\") [single](url 'title') [parenthesized](url (title)) [angle]( \"title\")" }, + { + "name": "link_titles_with_closing_parentheses", + "parser": "MdParser", + "left": "[double](a \"x)\") [single](a 'x)')" + }, + { + "name": "image_titles_with_closing_parentheses", + "parser": "MdParser", + "left": "![double](a \"x)\") ![single](a 'x)')" + }, + { + "name": "link_title_angle_and_fallback_boundaries", + "parser": "MdParser", + "left": "[fallback](< \")\") [angle]( \"x)\")" + }, { "name": "link_multiline_destination", "parser": "MdParser", diff --git a/tests/test_mistune_compatibility.py b/tests/test_mistune_compatibility.py index 2afeb8b..cb3ce90 100644 --- a/tests/test_mistune_compatibility.py +++ b/tests/test_mistune_compatibility.py @@ -42,6 +42,22 @@ def test_link_and_image_keep_the_exact_markdown_source(self): self.assertEqual('[label](https://example.test/path "title")', link.text) self.assertEqual("![alt](image.png 'title')", image.text) + def test_parentheses_inside_quoted_titles_keep_the_complete_link_or_image(self): + expectations = { + '[](a "x)")': Link, + "[](a 'x)')": Link, + '![](^ ")")': Image, + "![](^ ')')": Image, + '[x](< ")")': Link, + '[x]( "x)")': Link, + } + for source, node_type in expectations.items(): + with self.subTest(source=source): + tree = parser.parse(source) + self.assertEqual('p' + node_type.symbol, tree.print_all()) + self.assertIsInstance(tree.nodes[0].nodes[0], node_type) + self.assertEqual(source, tree.nodes[0].nodes[0].text) + def test_inline_image_after_text_keeps_legacy_link_classification(self): tree = parser.parse('text ![alt](image.png)') self.assertEqual('pta', tree.print_all()) @@ -219,9 +235,15 @@ def test_expanded_corpus_matches_golden_fixture_inventory(self): self.assertEqual(self.expected['expanded_cases_sha256'], canonical_hash(self.cases)) def test_every_target_result_matches_golden_mistune_084_fixture(self): + matrix_counts = { + 'exhaustive_link_label_matrix': 15_624, + 'exhaustive_link_tail_matrix': 313_576, + } for case in self.cases: with self.subTest(case=case['name']): signature = run_case(case, sdiff, parser, HtmlRenderer, TextRenderer) + if case['name'] in matrix_counts: + self.assertEqual(matrix_counts[case['name']], signature['case_count']) actual_hash = canonical_hash(signature) expected_hash = self.expected['cases'][case['name']] if actual_hash != expected_hash: From 4769d846890d8890d5e27bb577159aeaccbcc439 Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Wed, 26 Aug 2026 17:12:29 -0500 Subject: [PATCH 16/20] perf: skip unused Mistune link indexes --- sdiff/parser.py | 12 +++++++++--- tests/test_mistune_compatibility.py | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/sdiff/parser.py b/sdiff/parser.py index 3efa4cc..4a31feb 100644 --- a/sdiff/parser.py +++ b/sdiff/parser.py @@ -279,9 +279,12 @@ def parse(self, text, rules=None): text = text.rstrip('\n') active_rules = rules or self.default_rules position = 0 - direct_link_ends, reference_link_ends = _build_link_indexes(text) - modern_state = InlineState({'ref_links': {}}) - modern_state.src = text + direct_link_ends = reference_link_ends = None + modern_state = None + if '[' in text and any(rule in ('link', 'reflink') for rule in active_rules): + direct_link_ends, reference_link_ends = _build_link_indexes(text) + modern_state = InlineState({'ref_links': {}}) + modern_state.src = text while position < len(text): for rule in active_rules: @@ -296,6 +299,8 @@ def parse(self, text, rules=None): if rule in ('link', 'reflink') and text.startswith(('![', '['), position): marker_length = 2 if text.startswith('![', position) else 1 opener_end = position + marker_length + assert direct_link_ends is not None + assert reference_link_ends is not None if rule == 'link': end_position = direct_link_ends[opener_end] else: @@ -305,6 +310,7 @@ def parse(self, text, rules=None): # Exercise Mistune 3's hardened parser for syntax it # accepts; the local boundary remains authoritative for # legacy raw-source compatibility. + assert modern_state is not None self._link_parser.parse_link(opener, modern_state) source = text[position:end_position] self.tokens.append(Image(source) if source[0] == '!' else Link(source)) diff --git a/tests/test_mistune_compatibility.py b/tests/test_mistune_compatibility.py index cb3ce90..45d3a8e 100644 --- a/tests/test_mistune_compatibility.py +++ b/tests/test_mistune_compatibility.py @@ -2,6 +2,7 @@ from time import perf_counter from pathlib import Path from unittest import TestCase +from unittest.mock import patch import mistune import sdiff @@ -162,6 +163,27 @@ def test_inline_parser_honors_explicit_rule_selection(self): self.assertEqual([('[x](url)', Link)], [(node.text, type(node)) for node in direct_only]) self.assertEqual([('[x][ref]', Link)], [(node.text, type(node)) for node in reference_only]) + def test_inline_parser_builds_link_indexes_only_when_link_rules_can_use_them(self): + skip_cases = ( + ('plain text without a link opener', None), + ('[x](url)', ['text']), + ) + for source, rules in skip_cases: + with self.subTest(source=source, rules=rules): + with patch.object(parser, '_build_link_indexes') as build_indexes: + nodes = parser.InlineLexer().parse(source, rules) + build_indexes.assert_not_called() + self.assertEqual(source, nodes[0].text) + + with patch.object( + parser, + '_build_link_indexes', + wraps=parser._build_link_indexes, + ) as build_indexes: + nodes = parser.InlineLexer().parse('[x](url)', ['link', 'text']) + build_indexes.assert_called_once_with('[x](url)') + self.assertEqual([('[x](url)', Link)], [(node.text, type(node)) for node in nodes]) + def test_parser_instances_reuse_the_legacy_token_list(self): block_parser = MdParser() first_block_result = block_parser.parse('# first') From 1fe95958d0865a9ce4a21ec85c7958cfdf91b382 Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Wed, 26 Aug 2026 17:51:04 -0500 Subject: [PATCH 17/20] fix: restore Mistune parser facade parity --- README.md | 2 +- docs/python311-migration-contract.md | 31 +-- scripts/mistune_compat_signatures.py | 197 +++++++++++++++++- sdiff/parser.py | 139 +++++++++++- .../golden_mistune_084_fixtures.json | 10 +- .../fixtures/compatibility/mistune_cases.json | 135 ++++++++++++ tests/test_mistune_compatibility.py | 111 +++++++++- 7 files changed, 591 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 114416e..be0298c 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ required, and a partially cleaned `/tmp` directory is never trusted. Success ends with: ```text -Mistune 0.8.4 oracle vs 3.3.4 target: 1089 cases, 0 mismatches +Mistune 0.8.4 oracle vs 3.3.4 target: 1091 cases, 0 mismatches ``` The command runs each checkout with its own `venv`, compares normalized parser, renderer, and diff results, and exits diff --git a/docs/python311-migration-contract.md b/docs/python311-migration-contract.md index 637673a..e23fcbc 100644 --- a/docs/python311-migration-contract.md +++ b/docs/python311-migration-contract.md @@ -52,11 +52,11 @@ HTML, heading styles, lists, images and reference links, hard breaks and directi insert/delete symmetry, and nested Zendesk constructs. `scripts/run_mistune_compat.py` runs the frozen 0.8.4 worktree and this 3.3.4 worktree in isolated virtual -environments. The runner expands 64 committed corpus entries, all 13 golden cases, all 12 automatically discovered -Markdown fixture pairs, and 1,000 fixed-seed structured fuzz pairs for 1,089 named comparisons. Two aggregate corpus -entries exhaustively check another 329,200 short link/image label and destination combinations. The runner compares -recursive ASTs, exact text/HTML rendering, structural and link diff results, mutated metadata, and phase-specific -exception signatures. +environments. The runner expands 66 committed corpus entries, all 13 golden cases, all 12 automatically discovered +Markdown fixture pairs, and 1,000 fixed-seed structured fuzz pairs for 1,091 named comparisons. Four aggregate corpus +entries check another 329,200 short link/image label and destination combinations, 52,272 generated inline-rule +operations, and 643 generated block-facade operations. The runner compares recursive ASTs, exact text/HTML rendering, +structural and link diff results, mutated parser state, and phase-specific exception signatures. Normal `make test` recomputes every target result against the Golden Mistune 0.8.4 Fixtures at `tests/fixtures/compatibility/golden_mistune_084_fixtures.json`. This compact expected-results file is generated only @@ -184,12 +184,17 @@ Expected evidence: - Runtime metadata and both generated requirement files now agree on `mistune==3.3.4`. - Mistune 3 removed the `BlockLexer`, `InlineLexer`, and private grammar constants used by the old implementation. - The adapter owns the intentionally narrow sdiff block/list/text grammar locally, uses Mistune 3's hardened inline - link parser surface, and emits the existing sdiff model directly. + The adapter owns sdiff's intentionally narrow default block/list/text grammar locally, retains the explicitly + callable legacy rule facade, uses Mistune 3's hardened inline link parser surface, and emits the existing sdiff + model directly. - The adapter preserves smart entity escaping, literal unsupported Markdown, raw link/image source, unresolved reference links, legacy list shapes, inline-vs-block HTML classification, and recursive Zendesk tags. -- The adapter honors direct `InlineLexer.parse(..., rules=...)` rule ordering and reused-parser token-list identity, - matching the exposed 0.8.4 facade behavior. +- The adapter honors direct `InlineLexer.parse(..., rules=...)` rule ordering for links, reference links, autolinks, + and bare URLs. Explicit block rules retain 0.8.4 block-quote/code/table token shapes, definition maps, footnote + recursion, post-construction `default_rules` changes, and reused-parser token-list identity. +- Mistune 0.8's class hierarchy and unrelated private lexer internals are intentionally not recreated. The 2.x + compatibility contract covers sdiff's callable parser facade and observable results, not subclassing removed + Mistune implementation classes; no indexed downstream consumer relies on that private inheritance surface. - The legacy nested-label and destination grammar is compiled into a linear-time index. This preserves 0.8.4's unusual greedy/backtracking boundaries, including quoted titles containing `)` and angle-destination fallback, while eliminating repeated suffix scans on malformed nested openers. @@ -236,6 +241,8 @@ Completed applicable work: curated/fuzz/exhaustive link matrices, and target-side parity unit tests. - Fixed a review-discovered `InlineLexer` explicit-rule mismatch and replaced the first nested-label scanner after exhaustive testing exposed Mistune 0.8 greedy-label edge cases. +- Restored the remaining sdiff-authored autolink/URL rules and the inherited callable block-rule facade, including + exact raw token dictionaries, definition state, footnote recursion, and mutable default-rule behavior. - Ran the full skill ladder. The `--py36-plus` stage converted one list-rendering format call and two parser regex format calls to f-strings; Python 3.7 through 3.11 stages were no-ops. Golden outputs remained unchanged. - Review found that the first sdist omitted fixture data, and the expanded parity suite exposed missing test helper @@ -250,15 +257,15 @@ Proof results: | `make ci-dev-install` | Pass | CI bootstrap installed/reused the exact-version venv and final `.[dev]` dependency shape. | | full skill pyupgrade ladder over `sdiff` and `tests` | Pass | All six stages were reconfirmed live with zero remaining changes; the earlier Python 3.6 stage changes are isolated in commit `7c78ad3`. | | `make requirements` twice | Pass | Both pip-compile outputs were byte-stable; compile tooling is `pip-tools==7.5.3` with `pip==25.3`. | -| `make test` | Pass | Flake8 and msgpack guard passed; 78 tests passed on Python 3.11.13, including the Golden Mistune 0.8.4 Fixtures. | +| `make test` | Pass | Flake8 and msgpack guard passed; 83 tests passed on Python 3.11.13, including the Golden Mistune 0.8.4 Fixtures. | | `make coverage` | Pass | Total branch-aware coverage is 99%; `sdiff/parser.py` has 100% statement and 99% branch coverage. | | `make fixture-smoke` | Pass | 3 test methods and 12 fixture subtests passed, including the exact golden snapshot. | -| `make mistune-compat` | Pass | Mistune 0.8.4 oracle vs 3.3.4 target: 1,089 named cases, 0 mismatches; aggregate matrix cases cover 329,200 additional link/image combinations. | +| `make mistune-compat` | Pass | Mistune 0.8.4 oracle vs 3.3.4 target: 1,091 named cases, 0 mismatches; matrices add 329,200 link/image inputs, 52,272 inline-rule operations, and 643 block-facade operations. | | direct-link endpoint oracle sweep | Pass | 1,921,600 exhaustive tails through length seven plus 500,000 deterministic randomized inputs matched Mistune 0.8.4 with zero mismatches. | | malformed-link bounded-time regression | Pass | 16,000 nested link and image openers complete in about 0.03 seconds each; the test ceiling is 2 seconds. | | `make import-smoke` | Pass | Imported documented public API and printed `2.0.0 MdParser ZendeskHelpMdParser TextRenderer`. | | `make depcheck` | Pass | `pip check` found no broken requirements. | -| `CI=1 make test-only` | Pass | 78 tests passed and wrote `build/test/results.xml` plus `build/coverage/coverage.xml`. | +| `CI=1 make test-only` | Pass | 83 tests passed and wrote `build/test/results.xml` plus `build/coverage/coverage.xml`. | | `venv/bin/python -m compileall -q sdiff tests scripts` | Pass | Source, tests, and compatibility scripts compiled on Python 3.11.13. | | `rm -rf dist && venv/bin/python -m build .` | Pass | Built isolated `sdiff-2.0.0.tar.gz` and `sdiff-2.0.0-py3-none-any.whl` without invoking the publishing targets. | | `venv/bin/twine check dist/*` | Pass | Both distribution artifacts passed metadata/README validation. | diff --git a/scripts/mistune_compat_signatures.py b/scripts/mistune_compat_signatures.py index fff5a79..eec7808 100644 --- a/scripts/mistune_compat_signatures.py +++ b/scripts/mistune_compat_signatures.py @@ -142,23 +142,186 @@ def _reused_parser_signature(case, sdiff, parser): } +def _inline_rule_operation_signature(operation, parser): + instance = parser.InlineLexer() + parse_method = instance if operation.get('call') else instance.parse + return [ + node_signature(node) + for node in parse_method(operation['source'], operation.get('rules')) + ] + + +def _block_rule_operation_signature(operation, sdiff, parser_cls_name): + instance = getattr(sdiff, parser_cls_name)() + if 'default_rules' in operation: + instance.default_rules = list(operation['default_rules']) + if 'blockquote_depth' in operation: + instance._blockquote_depth = operation['blockquote_depth'] + parse_method = instance if operation.get('call') else instance.parse + nodes = [ + node_signature(node) + for node in parse_method(operation['source'], operation.get('rules')) + ] + return { + 'nodes': nodes, + 'def_links': instance.def_links, + 'def_footnotes': instance.def_footnotes, + 'default_rules': list(instance.default_rules), + } + + def _rule_selection_signature(case, sdiff, parser): results = {} + inline_rules = case['scenario'] == 'inline_rule_selection' for operation in case['operations']: - if case['scenario'] == 'inline_rule_selection': - instance = parser.InlineLexer() + if inline_rules: + results[operation['name']] = capture( + lambda item=operation: _inline_rule_operation_signature(item, parser) + ) else: - instance = getattr(sdiff, case['parser'])() - parse_method = instance if operation.get('call') else instance.parse - results[operation['name']] = capture( - lambda method=parse_method, item=operation: [ - node_signature(node) - for node in method(item['source'], item.get('rules')) - ] - ) + results[operation['name']] = capture( + lambda item=operation: _block_rule_operation_signature( + item, + sdiff, + case['parser'], + ) + ) return results +def _operation_matrix_signature(operations, operation_signature): + digest = hashlib.sha256() + count = 0 + for operation in operations: + signature = capture(lambda item=operation: operation_signature(item)) + payload = json.dumps( + [operation, signature], + ensure_ascii=False, + separators=(',', ':'), + sort_keys=True, + ).encode('utf-8') + digest.update(len(payload).to_bytes(8, byteorder='big')) + digest.update(payload) + count += 1 + return { + 'case_count': count, + 'sha256': digest.hexdigest(), + } + + +def _inline_rule_matrix_operations(): + autolink_rules = ( + ['autolink', 'text'], + ['text', 'autolink'], + ['autolink'], + ) + for length in range(5): + for characters in itertools.product('a@:< >', repeat=length): + body = ''.join(characters) + sources = ( + f'<{body}>', + f'<{body}>x', + f'<{body}>', + f'x<{body}>', + ) + for source in sources: + for rules in autolink_rules: + yield {'source': source, 'rules': rules} + + url_rules = ( + ['url', 'text'], + ['text', 'url'], + ['url'], + ) + for length in range(5): + for characters in itertools.product('a.(),< ', repeat=length): + suffix = ''.join(characters) + for scheme in ('http://', 'https://'): + url = scheme + suffix + for source in (url, 'x ' + url): + for rules in url_rules: + yield {'source': source, 'rules': rules} + + +def _block_rule_matrix_operations(): + def rule_orders(rule): + return ([rule, 'text'], ['text', rule]) + + for indentation in (' ', ' '): + for body in ('code', 'a\nb', 'code\n\n'): + for rules in rule_orders('block_code'): + yield {'source': indentation + body, 'rules': rules} + + for marker in ('```', '~~~', '````'): + for language in ('', 'python', 'x-y'): + for body in ('x', 'a\nb', ' x '): + for leading in ('', ' ', ' '): + source = f'{leading}{marker} {language}\n{body}\n{marker}' + for rules in rule_orders('fences'): + yield {'source': source, 'rules': rules} + + for depth in range(1, 9): + for body in ('quote', '# heading', '> nested'): + source = '> ' * depth + body + for blockquote_depth in (0, 6): + for rules in rule_orders('block_quote'): + yield { + 'source': source, + 'rules': rules, + 'blockquote_depth': blockquote_depth, + } + + keys = ('A', 'A B', 'A\tB', 'A&B', 'A"B', "A'B") + for key in keys: + for link in ('/one', 'https://example.test', 'a&b', 'a%20b'): + for title in ('', ' "Title"', ' (Title)', ' (Mixed"'): + source = f'[{key}]: <{link}>{title}\n' + for rules in rule_orders('def_links'): + yield {'source': source, 'rules': rules} + yield { + 'source': f'[{key}]: /one\n[{key.lower()}]: /two\n', + 'rules': ['def_links', 'text'], + } + + footnote_bodies = ( + 'body\n', + 'first\n second\n', + '\n ```python\n print(1)\n ```\n', + '\n | a | b |\n |---|:---:|\n | c | d |\n', + '\n > quote\n', + '\n # heading\n', + '\n * one\n * two\n', + '\n
body
\n', + ) + for key in keys: + for body in footnote_bodies: + source = f'[^{key}]: {body}' + for rules in rule_orders('def_footnotes'): + yield {'source': source, 'rules': rules} + yield { + 'source': f'[^{key}]: one\n[^{key.lower()}]: two\n', + 'rules': ['def_footnotes', 'text'], + } + + alignments = ('---', ':---', '---:', ':---:') + for first_alignment in alignments: + for second_alignment in alignments: + for leading_pipe, rule in ((True, 'table'), (False, 'nptable')): + prefix = '| ' if leading_pipe else '' + row_prefix = '| ' if leading_pipe else '' + source = ( + f'{prefix}a | b |\n' + f'{prefix}{first_alignment}|{second_alignment}|\n' + f'{row_prefix}c\\|d | e |\n' + ) + for rules in rule_orders(rule): + yield {'source': source, 'rules': rules} + + for source in ('# heading', '> quote', '```\ncode\n```'): + for default_rules in (['text'], ['heading', 'text'], ['fences', 'text']): + yield {'source': source, 'default_rules': default_rules} + + def _preprocessing_signature(case, sdiff, parser): parser_cls = getattr(sdiff, case['parser']) direct_nodes = parser_cls().parse(case['left']) @@ -224,6 +387,20 @@ def run_case(case, sdiff, parser, HtmlRenderer, TextRenderer): return _matrix_signature(_link_label_matrix_sources(), sdiff, parser) if scenario == 'link_tail_matrix': return _matrix_signature(_link_tail_matrix_sources(), sdiff, parser) + if scenario == 'inline_rule_matrix': + return _operation_matrix_signature( + _inline_rule_matrix_operations(), + lambda operation: _inline_rule_operation_signature(operation, parser), + ) + if scenario == 'block_rule_matrix': + return _operation_matrix_signature( + _block_rule_matrix_operations(), + lambda operation: _block_rule_operation_signature( + operation, + sdiff, + case['parser'], + ), + ) if scenario != 'documents': raise ValueError(f"Unknown compatibility scenario: {scenario}") return _document_signature(case, sdiff, parser, HtmlRenderer, TextRenderer) diff --git a/sdiff/parser.py b/sdiff/parser.py index 4a31feb..e933679 100644 --- a/sdiff/parser.py +++ b/sdiff/parser.py @@ -20,6 +20,9 @@ _VALID_ATTR = r'''\s*[a-zA-Z\-](?:\s*\=\s*(?:"[^"]*"|'[^']*'|[^\s'">]+))?''' _SMART_AMP = re.compile(r'&(?!#?\w+;)') _LINK_OPENER = re.compile(r'!?\[') +_BLOCK_CODE_LEADING = re.compile(r'^ {4}', flags=re.M) +_BLOCK_QUOTE_LEADING = re.compile(r'^ *> ?', flags=re.M) +_KEY_WHITESPACE = re.compile(r'\s+') def _pure_pattern(pattern): @@ -32,7 +35,15 @@ def _legacy_escape(text): return text.replace('<', '<').replace('>', '>') +def _legacy_keyify(key): + key = _legacy_escape(key.lower()) + key = key.replace('"', '"').replace("'", ''') + return _KEY_WHITESPACE.sub(' ', key) + + class _InlineGrammar: + autolink = re.compile(r'<([^ >]+(@|:)[^ >]+)>') + url = re.compile(r'''(https?:\/\/[^\s<]+[^<.,:;"')\]\s])''') linebreak = re.compile(r' {2,}\n(?!\s*$)') text = re.compile(r' {1,}\n|[\s\S]+?(?=[\[`~]| {2,}\n|$)') @@ -50,6 +61,7 @@ class _BlockGrammar: r')' ) newline = re.compile(r'^\n+') + block_code = re.compile(r'^( {4}[^\n]+\n*)+') fences = re.compile( r'^ *(`{3,}|~{3,}) *([^`\s]+)? *\n' r'([\s\S]+?)\s*' @@ -103,6 +115,12 @@ class _BlockGrammar: fr'<{_BLOCK_TAG}(?:{_VALID_ATTR})*?>', ) ) + table = re.compile( + r'^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*' + ) + nptable = re.compile( + r'^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*' + ) text = re.compile(r'^[^\n]+') @@ -288,6 +306,14 @@ def parse(self, text, rules=None): while position < len(text): for rule in active_rules: + if rule in ('autolink', 'url'): + link = getattr(self.rules, rule).match(text, position) + if link: + self.tokens.append(Link(link.group(0))) + position = link.end() + break + continue + if rule == 'linebreak': linebreak = self.rules.linebreak.match(text, position) if linebreak: @@ -338,15 +364,19 @@ class _CompatibilityBlockLexer: def _configure_compatibility_lexer(self): self.tokens = [] + self.def_links = {} + self.def_footnotes = {} self.rules = self.grammar_class() - self._active_rules = list(self.default_rules) + self._max_recursive_depth = 6 + self._list_depth = 0 + self._blockquote_depth = 0 def __call__(self, text, rules=None): return self.parse(text, rules) def parse(self, text, rules=None): text = text.rstrip('\n') - active_rules = rules or self._active_rules + active_rules = rules or self.default_rules while text: for name in active_rules: @@ -370,6 +400,11 @@ class MdParser(_CompatibilityBlockLexer): 'newline', 'heading', 'lheading', 'hrule', 'list_block', 'text', ) + footnote_rules = ( + 'newline', 'block_code', 'fences', 'heading', + 'nptable', 'lheading', 'hrule', 'block_quote', + 'list_block', 'block_html', 'table', 'paragraph', 'text', + ) @classmethod def get_lexer(cls): @@ -407,11 +442,63 @@ def parse_paragraph(self, match): def parse_text(self, match): self.tokens.append(Text(_legacy_escape(match.group(0)))) + def parse_block_code(self, match): + self.tokens.append({ + 'type': 'code', + 'lang': None, + 'text': _BLOCK_CODE_LEADING.sub('', match.group(0)), + }) + + def parse_fences(self, match): + self.tokens.append({ + 'type': 'code', + 'lang': match.group(2), + 'text': match.group(3), + }) + def parse_hrule(self, match): # Preserve Mistune 0.8's inherited behavior for a thematic break # nested in a list. The compatibility reporter records the exception. self.tokens.append({'type': 'hrule'}) + def parse_block_quote(self, match): + self.tokens.append({'type': 'block_quote_start'}) + self._blockquote_depth += 1 + if self._blockquote_depth > self._max_recursive_depth: + self.parse_text(match) + else: + captured = _BLOCK_QUOTE_LEADING.sub('', match.group(0)) + self.parse(captured) + self.tokens.append({'type': 'block_quote_end'}) + self._blockquote_depth -= 1 + + def parse_def_links(self, match): + self.def_links[_legacy_keyify(match.group(1))] = { + 'link': match.group(2), + 'title': match.group(3), + } + + def parse_def_footnotes(self, match): + key = _legacy_keyify(match.group(1)) + if key in self.def_footnotes: + return + + self.def_footnotes[key] = 0 + self.tokens.append({'type': 'footnote_start', 'key': key}) + + text = match.group(2) + if '\n' in text: + lines = text.split('\n') + whitespace = None + for line in lines[1:]: + space = len(line) - len(line.lstrip()) + if space and (not whitespace or space < whitespace): + whitespace = space + text = '\n'.join([lines[0]] + [line[whitespace:] for line in lines[1:]]) + + self.parse(text, self.footnote_rules) + self.tokens.append({'type': 'footnote_end', 'key': key}) + def parse_list_block(self, match): bullet = match.group(2) node = List('.' in bullet) @@ -436,6 +523,47 @@ def _process_list_item(self, captured): result.append(node) return result + def parse_table(self, match): + item = self._process_table(match) + cells = re.sub(r'(?: *\| *)?\n$', '', match.group(3)).split('\n') + for index, value in enumerate(cells): + value = re.sub(r'^ *\| *| *\| *$', '', value) + cells[index] = re.split(r' *(? None: style = match.group(self.CALLOUT_STYLE_GROUP) diff --git a/tests/fixtures/compatibility/golden_mistune_084_fixtures.json b/tests/fixtures/compatibility/golden_mistune_084_fixtures.json index 0df9021..4f4398d 100644 --- a/tests/fixtures/compatibility/golden_mistune_084_fixtures.json +++ b/tests/fixtures/compatibility/golden_mistune_084_fixtures.json @@ -1,12 +1,12 @@ { - "case_count": 1089, + "case_count": 1091, "cases": { "atx_headings": "f7be5be83689cf22dd77489d41ab7881d1f92fdcce915578c47bbd31932410e3", "autolinks_and_bare_urls_are_text": "f01bbeae321bf5357c645c236c0b81689c320f22111ebc28e7419984925f4a4e", "block_html_and_comment": "f65d31151146abb7f3810f92f3412ea2d725239ac7ee78920778cb7f8991bbc8", "block_html_template_attributes": "44cf0cdb2c9d5e484ae8b9713d4b845c9c3cdfd0eba0dec86d82db9f5ac1bb0b", "block_parser_reuse": "43af3a2315aec3bb6aed336165dfdaad94fe24b7f2321db215ed06b700e1349a", - "block_rule_selection": "e37c103226a6c9865cb87c6718df6fc8cc5c890f32386467d22c80f1557d1f8e", + "block_rule_selection": "f99d2fe0e7429302a843227b0807ac0da3d1682b3cfcf79bba74d9750342be89", "block_syntax_stays_text": "5a859a8c09aac078ddaa24064cb1df7c81e073b3bc9f1b689aa228521250214e", "closing_and_excess_heading_markers": "13d72f13ddaefe0298892e8da20477f2c439f3b0145ddac1362d704cd71259a1", "deep_unmatched_link_openers": "b6e7eceddc289682d737b51cfd6f79afd57ce3733222b767296cc234516b0659", @@ -15,6 +15,8 @@ "empty_and_unclosed_images": "9e1a345d0d80b3dec00dfd66e77195c1d3aaa0768f2c9f40f72f353baf7dfb93", "exhaustive_link_label_matrix": "f891cf17d584b865562e6a30af596854701f680c0d3809f1956b508ab01d9286", "exhaustive_link_tail_matrix": "d708a48837c65b76b9d59b013c8620268b600fe0da6ef04d80ae179709f686bc", + "explicit_block_rule_matrix": "342521a5d4d5bf0eb2b07f7c67a4eec62c2932c5e610ec6112e7bffe0e7409c6", + "explicit_inline_rule_matrix": "01eb613f714ec496c394252d1e86f497e84e25b3d20efb2e4a39bd25e2b1f817", "fixture_tests_fixtures_different_extra_paragraph": "4f4044ad3694fac516bb4fb8697b29efde888dbb0aae6b87b370255a460bf4f1", "fixture_tests_fixtures_different_zendesk_callout_in_tabs": "4d63ca718729a84c5d63bc618d7379a88ee528366863bec242a598c1b95d0c93", "fixture_tests_fixtures_different_zendesk_steps": "30dfb15eb7a6be8f4dd84f684725bc54d254b5401f826ade737600fb7c3dc3cb", @@ -1049,7 +1051,7 @@ "image_titles_with_closing_parentheses": "8e330e422c141af716981538d092cb7cfa914ee1db1d6c1429189d34bcd7085b", "inline_html_is_escaped": "bf38be2ddbcbaac36438d275330de8ce00bf921059a145728b3a614d6d809e56", "inline_parser_reuse": "179af2c54a9197e53172b31170b39bde737cd3c4b145ba3160f228f94c1ce85e", - "inline_rule_selection": "8ace84d3afff2f3c1c2a06d080fe62941116c2429a6bc4f321c5cfe2c0653b15", + "inline_rule_selection": "6001500b4000c39785721fb48dac5f2fa16b282fe201b2d2aa3df6007ff1cbd4", "inline_syntax_stays_text": "836724aea14b615d69c6497533e7125ec924a44b8ae95f26a12bf9c3f9e6b6a8", "leading_and_trailing_blank_lines": "3c33bce6eac82926642c41374ee24a9bdf961f755ad209c47297cf26965ba0ae", "legacy_nested_label_atom": "fce97d21836a21e9d8da9d5fd898fb8bb29faaea44ffc440825ddef9cb77b7c2", @@ -1091,7 +1093,7 @@ "zendesk_sibling_custom_tags": "5b667bf654ec935f86e14d65150370cb2df867b52ae3965ed59c0b6caf27443b", "zendesk_uppercase_and_multiple_styles": "b3983c67f1153e42a6952bdb466654d534a6fad0e2960f4c5455a54949462fe2" }, - "expanded_cases_sha256": "4d46f648a0b58e7e0d5db234bf6ef36d234ca249cdb6945c807ca6c917bc5237", + "expanded_cases_sha256": "a30b6f703a4a45039ac9d616a84a7830ba7caac1506f9d4b125214cf3d355152", "fuzz_seed": 311084334, "oracle": { "mistune": "0.8.4", diff --git a/tests/fixtures/compatibility/mistune_cases.json b/tests/fixtures/compatibility/mistune_cases.json index d094037..84dad71 100644 --- a/tests/fixtures/compatibility/mistune_cases.json +++ b/tests/fixtures/compatibility/mistune_cases.json @@ -312,6 +312,51 @@ "source": "[x][ref]", "rules": ["reflink", "text"] }, + { + "name": "autolink_rule", + "source": "", + "rules": ["autolink", "text"] + }, + { + "name": "email_autolink_rule", + "source": "", + "rules": ["autolink", "text"] + }, + { + "name": "url_rule", + "source": "https://example.test/path", + "rules": ["url", "text"] + }, + { + "name": "url_trailing_punctuation", + "source": "https://example.test/path).", + "rules": ["url", "text"] + }, + { + "name": "sequential_autolinks", + "source": "", + "rules": ["autolink", "text"] + }, + { + "name": "text_precedes_autolink", + "source": "", + "rules": ["text", "autolink"] + }, + { + "name": "text_precedes_url", + "source": "https://example.test", + "rules": ["text", "url"] + }, + { + "name": "prefixed_autolink_remains_text", + "source": "before ", + "rules": ["autolink", "text"] + }, + { + "name": "prefixed_url_remains_text", + "source": "before https://example.test", + "rules": ["url", "text"] + }, { "name": "empty_rules_use_defaults", "source": "[x](url)", @@ -339,6 +384,87 @@ "rules": ["heading", "text"], "call": true }, + { + "name": "block_quote", + "source": "> quote", + "rules": ["block_quote", "text"] + }, + { + "name": "nested_block_quote", + "source": "> > deep", + "rules": ["block_quote", "text"] + }, + { + "name": "block_quote_recursion_limit", + "source": "> quote", + "rules": ["block_quote", "text"], + "blockquote_depth": 6 + }, + { + "name": "fenced_code", + "source": "```python\nprint(1)\n```", + "rules": ["fences", "text"] + }, + { + "name": "indented_code", + "source": " code\n", + "rules": ["block_code", "text"] + }, + { + "name": "pipe_table", + "source": "| a | b | c | d |\n|---|:---|---:|:---:|\n| w | x | y | z |\n", + "rules": ["table", "text"] + }, + { + "name": "no_leading_pipe_table", + "source": "a | b\n---|:---:\nc | d\n", + "rules": ["nptable", "text"] + }, + { + "name": "definition_link", + "source": "[Docs]: \"Title\"\n", + "rules": ["def_links", "text"] + }, + { + "name": "duplicate_definition_link", + "source": "[Docs]: /one\n[docs]: /two\n", + "rules": ["def_links", "text"] + }, + { + "name": "definition_footnote", + "source": "[^Note]: body\n", + "rules": ["def_footnotes", "text"] + }, + { + "name": "multiline_definition_footnote", + "source": "[^n]: first\n second\n", + "rules": ["def_footnotes", "text"] + }, + { + "name": "fenced_definition_footnote", + "source": "[^code]:\n ```python\n print(1)\n ```\n", + "rules": ["def_footnotes", "text"] + }, + { + "name": "table_definition_footnote", + "source": "[^table]:\n | a | b |\n |---|:---:|\n | c | d |\n", + "rules": ["def_footnotes", "text"] + }, + { + "name": "quote_definition_footnote", + "source": "[^quote]:\n > quote\n", + "rules": ["def_footnotes", "text"] + }, + { + "name": "duplicate_definition_footnote", + "source": "[^n]: one\n[^N]: two\n", + "rules": ["def_footnotes", "text"] + }, + { + "name": "mutated_default_rules", + "source": "# heading", + "default_rules": ["text"] + }, { "name": "empty_rules_use_defaults", "source": "# heading", @@ -373,6 +499,15 @@ { "name": "exhaustive_link_tail_matrix", "scenario": "link_tail_matrix" + }, + { + "name": "explicit_inline_rule_matrix", + "scenario": "inline_rule_matrix" + }, + { + "name": "explicit_block_rule_matrix", + "scenario": "block_rule_matrix", + "parser": "MdParser" } ] } diff --git a/tests/test_mistune_compatibility.py b/tests/test_mistune_compatibility.py index 45d3a8e..56004b9 100644 --- a/tests/test_mistune_compatibility.py +++ b/tests/test_mistune_compatibility.py @@ -163,6 +163,111 @@ def test_inline_parser_honors_explicit_rule_selection(self): self.assertEqual([('[x](url)', Link)], [(node.text, type(node)) for node in direct_only]) self.assertEqual([('[x][ref]', Link)], [(node.text, type(node)) for node in reference_only]) + def test_inline_parser_honors_legacy_autolink_and_url_rules(self): + cases = ( + ( + '', + ['autolink', 'text'], + [ + ('', Link), + ('', Link), + ], + ), + ( + 'https://example.test/path).', + ['url', 'text'], + [ + ('https://example.test/path', Link), + (').', Text), + ], + ), + ( + 'before ', + ['autolink', 'text'], + [('before <https://example.test>', Text)], + ), + ( + 'before https://example.test', + ['url', 'text'], + [('before https://example.test', Text)], + ), + ( + '', + ['text', 'autolink'], + [('<https://example.test>', Text)], + ), + ( + 'https://example.test', + ['text', 'url'], + [('https://example.test', Text)], + ), + ) + for source, rules, expected in cases: + with self.subTest(source=source, rules=rules): + nodes = parser.InlineLexer().parse(source, rules) + self.assertEqual(expected, [(node.text, type(node)) for node in nodes]) + + def test_block_parser_honors_legacy_explicit_structural_rules(self): + block_quote = MdParser().parse('> quote', ['block_quote', 'text']) + nested_quote = MdParser().parse('> > deep', ['block_quote', 'text']) + limited_parser = MdParser() + limited_parser._blockquote_depth = limited_parser._max_recursive_depth + limited_quote = limited_parser.parse('> quote', ['block_quote', 'text']) + fenced = MdParser().parse('```python\nprint(1)\n```', ['fences', 'text']) + indented = MdParser().parse(' code\n', ['block_code', 'text']) + table = MdParser().parse( + '| a | b | c | d |\n|---|:---|---:|:---:|\n| w | x | y | z |\n', + ['table', 'text'], + ) + + self.assertEqual({'type': 'block_quote_start'}, block_quote[0]) + self.assertEqual('quote', block_quote[1].nodes[0].text) + self.assertEqual({'type': 'block_quote_end'}, block_quote[2]) + self.assertEqual('> deep', nested_quote[1].nodes[0].text) + self.assertEqual('> quote', limited_quote[1].text) + self.assertEqual( + {'type': 'code', 'lang': 'python', 'text': 'print(1)'}, + fenced[0], + ) + self.assertEqual({'type': 'code', 'lang': None, 'text': 'code'}, indented[0]) + self.assertEqual( + { + 'type': 'table', + 'header': ['a', 'b', 'c', 'd'], + 'align': [None, 'left', 'right', 'center'], + 'cells': [['w', 'x', 'y', 'z']], + }, + table[0], + ) + + def test_block_parser_preserves_legacy_definition_state(self): + link_parser = MdParser() + link_nodes = link_parser.parse( + '[Docs]: /one\n[docs]: /two\n', + ['def_links', 'text'], + ) + footnote_parser = MdParser() + footnote_nodes = footnote_parser.parse( + '[^Note]: first\n second\n', + ['def_footnotes', 'text'], + ) + + self.assertEqual([], link_nodes) + self.assertEqual( + {'docs': {'link': '/two', 'title': None}}, + link_parser.def_links, + ) + self.assertEqual({'note': 0}, footnote_parser.def_footnotes) + self.assertEqual({'type': 'footnote_start', 'key': 'note'}, footnote_nodes[0]) + self.assertEqual('first\nsecond', footnote_nodes[1].nodes[0].text) + self.assertEqual({'type': 'footnote_end', 'key': 'note'}, footnote_nodes[2]) + + def test_block_parser_honors_post_construction_default_rule_changes(self): + block_parser = MdParser() + block_parser.default_rules = ['text'] + nodes = block_parser.parse('# heading') + self.assertEqual([('# heading', Text)], [(node.text, type(node)) for node in nodes]) + def test_inline_parser_builds_link_indexes_only_when_link_rules_can_use_them(self): skip_cases = ( ('plain text without a link opener', None), @@ -218,8 +323,8 @@ def test_zendesk_rules_do_not_leak_into_plain_parser_instances(self): first = ZendeskHelpMdParser() second = ZendeskHelpMdParser() self.assertEqual(original_rules, MdParser.default_rules) - self.assertIsNot(first._active_rules, second._active_rules) - self.assertEqual(['tabs', 'steps', 'callout'], first._active_rules[:3]) + self.assertIsNot(first.default_rules, second.default_rules) + self.assertEqual(['tabs', 'steps', 'callout'], first.default_rules[:3]) parser.parse('one', parser_cls=ZendeskHelpMdParser) plain = parser.parse('one', parser_cls=MdParser) self.assertNotEqual('tabs', plain.nodes[0].name) @@ -260,6 +365,8 @@ def test_every_target_result_matches_golden_mistune_084_fixture(self): matrix_counts = { 'exhaustive_link_label_matrix': 15_624, 'exhaustive_link_tail_matrix': 313_576, + 'explicit_inline_rule_matrix': 52_272, + 'explicit_block_rule_matrix': 643, } for case in self.cases: with self.subTest(case=case['name']): From 4c3f16b287f53399be2c493253b10574f7888cde Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Wed, 26 Aug 2026 18:12:33 -0500 Subject: [PATCH 18/20] test: harden Mistune compatibility oracle --- Makefile | 11 +- README.md | 17 +- docs/python311-migration-contract.md | 33 +- scripts/run_mistune_compat.py | 137 +++++- .../golden_mistune_084_fixtures.json | 4 +- tests/test_mistune_compat_runner.py | 445 ++++++++++++++++++ tests/test_mistune_compatibility.py | 6 + 7 files changed, 611 insertions(+), 42 deletions(-) create mode 100644 tests/test_mistune_compat_runner.py diff --git a/Makefile b/Makefile index dcdaf08..31487e6 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,8 @@ PYTEST=venv/bin/pytest FLAKE=venv/bin/flake8 PIP_COMPILE=venv/bin/pip-compile RG=rg -MISTUNE_ORACLE_REV?=a73f7122421a08c380b87c38fccfe8b54d169cb9 +SDIFF_MISTUNE_084_ORACLE_REV?=12e7782208e4b458c8c4242882fda2377d9cba6b +MISTUNE_ORACLE_PYTHON?=$(abspath $(PYTHON)) MISTUNE_GOLDEN_FIXTURES=tests/fixtures/compatibility/golden_mistune_084_fixtures.json PYTEST_SHARED_FLAGS=-s --durations=3 --durations-min=0.005 @@ -90,12 +91,12 @@ depcheck: $(PIP) check mistune-compat: - $(PYTHON) scripts/run_mistune_compat.py --oracle-revision "$(MISTUNE_ORACLE_REV)" \ - --bootstrap-python "$(BOOTSTRAP_PYTHON)" --target . + $(PYTHON) scripts/run_mistune_compat.py --oracle-revision "$(SDIFF_MISTUNE_084_ORACLE_REV)" \ + --bootstrap-python "$(MISTUNE_ORACLE_PYTHON)" --expected-python "$(PYTHON_VERSION)" --target . mistune-compat-refresh: - $(PYTHON) scripts/run_mistune_compat.py --oracle-revision "$(MISTUNE_ORACLE_REV)" \ - --bootstrap-python "$(BOOTSTRAP_PYTHON)" --target . \ + $(PYTHON) scripts/run_mistune_compat.py --oracle-revision "$(SDIFF_MISTUNE_084_ORACLE_REV)" \ + --bootstrap-python "$(MISTUNE_ORACLE_PYTHON)" --expected-python "$(PYTHON_VERSION)" --target . \ --write-golden-fixtures $(MISTUNE_GOLDEN_FIXTURES) requirements: dev diff --git a/README.md b/README.md index be0298c..5758df6 100644 --- a/README.md +++ b/README.md @@ -66,9 +66,15 @@ make mistune-compat ``` The Make target always starts fresh. It removes any stale oracle state at -`/tmp/html-structure-diff-mistune-084-oracle`, creates a detached pre-port checkout there, builds an isolated Python -3.11 environment with Mistune 0.8.4, runs the comparison, and removes the oracle again. No reusable oracle setup is -required, and a partially cleaned `/tmp` directory is never trusted. +`/tmp/html-structure-diff-mistune-084-oracle`, creates a detached checkout of the permanent pre-port master commit +`12e7782208e4b458c8c4242882fda2377d9cba6b`, and removes the oracle again after the comparison. The runner uses the +target's absolute Python 3.11.13 interpreter to create a fresh oracle venv, then independently installs exactly +`mistune==0.8.4` and that checkout's `sdiff==1.0.0`; it does not trust the historical Makefile or any partially +cleaned `/tmp` state. + +Before comparing results, the runner verifies the exact oracle Git revision, a clean oracle worktree, Python +3.11.13, Mistune 0.8.4, and sdiff 1.0.0. It also verifies that the target report was produced with Python 3.11.13. +No reusable oracle setup is required. Success ends with: @@ -80,8 +86,9 @@ The command runs each checkout with its own `venv`, compares normalized parser, nonzero with unified result diffs when behavior differs. Only refresh the committed Golden Mistune 0.8.4 Fixtures after deliberately changing and reviewing the compatibility -corpus. The refresh is generated from the 0.8.4 worktree, not from the target implementation, and refuses any other -oracle Mistune version: +corpus. The refresh is generated from the verified permanent master oracle, not from the target implementation. The +regular test suite asserts the fixture's oracle revision, Python, Mistune, and sdiff versions so provenance drift is +visible: ```sh make mistune-compat-refresh diff --git a/docs/python311-migration-contract.md b/docs/python311-migration-contract.md index e23fcbc..60f963c 100644 --- a/docs/python311-migration-contract.md +++ b/docs/python311-migration-contract.md @@ -51,18 +51,23 @@ structural-diff, and link-diff outputs for 13 compact Mistune 0.8.4 scenarios. T HTML, heading styles, lists, images and reference links, hard breaks and directional marks, link-count changes, insert/delete symmetry, and nested Zendesk constructs. -`scripts/run_mistune_compat.py` runs the frozen 0.8.4 worktree and this 3.3.4 worktree in isolated virtual -environments. The runner expands 66 committed corpus entries, all 13 golden cases, all 12 automatically discovered -Markdown fixture pairs, and 1,000 fixed-seed structured fuzz pairs for 1,091 named comparisons. Four aggregate corpus -entries check another 329,200 short link/image label and destination combinations, 52,272 generated inline-rule -operations, and 643 generated block-facade operations. The runner compares recursive ASTs, exact text/HTML rendering, -structural and link diff results, mutated parser state, and phase-specific exception signatures. +`scripts/run_mistune_compat.py` runs permanent pre-port master commit +`12e7782208e4b458c8c4242882fda2377d9cba6b` and this 3.3.4 worktree in isolated virtual environments. The runner +creates the oracle from scratch under `/tmp` with the target's absolute Python 3.11.13 interpreter, installs exactly +Mistune 0.8.4 plus the historical sdiff 1.0.0 checkout independently of its Makefile, and verifies the Git revision, +clean worktree, Python, Mistune, and sdiff provenance before accepting results. It expands 66 committed corpus +entries, all 13 golden cases, all 12 automatically discovered Markdown fixture pairs, and 1,000 fixed-seed structured +fuzz pairs for 1,091 named comparisons. Four aggregate corpus entries check another 329,200 short link/image label and +destination combinations, 52,272 generated inline-rule operations, and 643 generated block-facade operations. The +runner compares recursive ASTs, exact text/HTML rendering, structural and link diff results, mutated parser state, and +phase-specific exception signatures. Normal `make test` recomputes every target result against the Golden Mistune 0.8.4 Fixtures at `tests/fixtures/compatibility/golden_mistune_084_fixtures.json`. This compact expected-results file is generated only -from the frozen 0.8.4 implementation, so the normal suite retains the complete parity proof without requiring another -worktree. `make mistune-compat` provides readable full-result diffs, and `make mistune-compat-refresh` deliberately -refreshes the golden fixtures after an approved corpus change. +from that permanent 0.8.4 oracle, and the test suite pins its revision and environment metadata, so the normal suite +retains the complete parity proof without requiring another worktree. `make mistune-compat` provides readable +full-result diffs, and `make mistune-compat-refresh` deliberately refreshes the golden fixtures after an approved +corpus change. ## Downstream Consumers @@ -239,6 +244,8 @@ Completed applicable work: - Ported the parser from removed Mistune 0.8 lexer APIs to a Mistune 3.3.4 compatibility adapter. - Added a dual-worktree exact-signature harness, self-contained Golden Mistune 0.8.4 Fixtures, expanded curated/fuzz/exhaustive link matrices, and target-side parity unit tests. +- Anchored the live oracle to the permanent pre-port master commit, made its Python 3.11.13 environment entirely + runner-owned, and added exact revision/runtime/package provenance plus stale `/tmp` cleanup checks. - Fixed a review-discovered `InlineLexer` explicit-rule mismatch and replaced the first nested-label scanner after exhaustive testing exposed Mistune 0.8 greedy-label edge cases. - Restored the remaining sdiff-authored autolink/URL rules and the inherited callable block-rule facade, including @@ -257,10 +264,10 @@ Proof results: | `make ci-dev-install` | Pass | CI bootstrap installed/reused the exact-version venv and final `.[dev]` dependency shape. | | full skill pyupgrade ladder over `sdiff` and `tests` | Pass | All six stages were reconfirmed live with zero remaining changes; the earlier Python 3.6 stage changes are isolated in commit `7c78ad3`. | | `make requirements` twice | Pass | Both pip-compile outputs were byte-stable; compile tooling is `pip-tools==7.5.3` with `pip==25.3`. | -| `make test` | Pass | Flake8 and msgpack guard passed; 83 tests passed on Python 3.11.13, including the Golden Mistune 0.8.4 Fixtures. | -| `make coverage` | Pass | Total branch-aware coverage is 99%; `sdiff/parser.py` has 100% statement and 99% branch coverage. | +| `make test` | Pass | Flake8 and msgpack guard passed; 95 tests passed on Python 3.11.13, including the Golden Mistune 0.8.4 Fixtures and oracle-runner hardening tests. | +| `make coverage` | Pass | Total branch-aware coverage is 99%; `sdiff/parser.py` has 100% statement and 99% branch coverage, and the oracle runner has 96%. | | `make fixture-smoke` | Pass | 3 test methods and 12 fixture subtests passed, including the exact golden snapshot. | -| `make mistune-compat` | Pass | Mistune 0.8.4 oracle vs 3.3.4 target: 1,091 named cases, 0 mismatches; matrices add 329,200 link/image inputs, 52,272 inline-rule operations, and 643 block-facade operations. | +| `make mistune-compat` | Pass | A fresh `/tmp` oracle at permanent master commit `12e7782`, with Python 3.11.13, `sdiff==1.0.0`, and `mistune==0.8.4`, matched the 3.3.4 target across 1,091 named cases with 0 mismatches; post-run cleanup succeeded. | | direct-link endpoint oracle sweep | Pass | 1,921,600 exhaustive tails through length seven plus 500,000 deterministic randomized inputs matched Mistune 0.8.4 with zero mismatches. | | malformed-link bounded-time regression | Pass | 16,000 nested link and image openers complete in about 0.03 seconds each; the test ceiling is 2 seconds. | | `make import-smoke` | Pass | Imported documented public API and printed `2.0.0 MdParser ZendeskHelpMdParser TextRenderer`. | @@ -270,7 +277,7 @@ Proof results: | `rm -rf dist && venv/bin/python -m build .` | Pass | Built isolated `sdiff-2.0.0.tar.gz` and `sdiff-2.0.0-py3-none-any.whl` without invoking the publishing targets. | | `venv/bin/twine check dist/*` | Pass | Both distribution artifacts passed metadata/README validation. | | sdist content listing | Pass | Archive contains all tests, compatibility helpers, same/different Markdown fixtures, golden JSON, and Golden Mistune 0.8.4 Fixtures. | -| extracted-sdist test suite | Pass | All 78 tests and 1,129 subtests passed directly from the unpacked source archive. | +| extracted-sdist test suite | Pass | All 95 tests and 1,146 subtests passed directly from the unpacked source archive, including runner tests that create their own disposable Git fixtures. | | isolated wheel install/import/diff smoke | Pass | A separate CPython 3.11.13 venv installed `sdiff==2.0.0` with `mistune==3.3.4` and ran `sdiff.diff()`. | | CircleCI validate, `--next`, and config process | Pass | CircleCI accepted the native source `version: 2.1`, strict upcoming-compiler validation, and reusable-config expansion. | | PR #14 remote CI | Pass | CircleCI `prepare_cache`, `lint`, and `test`, plus Travis CI branch and pull-request builds, passed at `5ebec5545ba6df34942403c9afd73fc87b471c49`. | diff --git a/scripts/run_mistune_compat.py b/scripts/run_mistune_compat.py index 1f396c3..bc8c64f 100644 --- a/scripts/run_mistune_compat.py +++ b/scripts/run_mistune_compat.py @@ -20,6 +20,8 @@ ORACLE_DIR = Path('/tmp/html-structure-diff-mistune-084-oracle') +ORACLE_MISTUNE_VERSION = '0.8.4' +ORACLE_SDIFF_VERSION = '1.0.0' def _run_report(python: Path, reporter: Path, repo: Path, cases: Path): @@ -43,6 +45,17 @@ def _git_revision(repo): return completed.stdout.strip() +def _resolve_revision(repo, revision): + completed = subprocess.run( + ['git', 'rev-parse', '--verify', f'{revision}^{{commit}}'], + cwd=repo, + check=True, + capture_output=True, + text=True, + ) + return completed.stdout.strip() + + def _git_status(repo): completed = subprocess.run( ['git', 'status', '--porcelain'], @@ -54,7 +67,31 @@ def _git_status(repo): return completed.stdout.strip() -def _validate_oracle(oracle, revision): +def _python_version(python): + completed = subprocess.run( + [str(python), '-c', 'import platform; print(platform.python_version())'], + check=True, + capture_output=True, + text=True, + ) + return completed.stdout.strip() + + +def _resolve_python(executable): + candidate = Path(executable).expanduser() + if candidate.is_absolute() or candidate.parent != Path('.'): + resolved = candidate.absolute() + else: + discovered = shutil.which(executable) + if discovered is None: + raise RuntimeError(f'Bootstrap Python does not exist: {executable}') + resolved = Path(discovered).absolute() + if not resolved.is_file(): + raise RuntimeError(f'Bootstrap Python does not exist: {resolved}') + return resolved + + +def _validate_oracle(oracle, revision, expected_python): python = oracle / 'venv/bin/python' if not python.is_file(): raise RuntimeError(f'Temporary oracle has no Python executable: {python}') @@ -65,6 +102,33 @@ def _validate_oracle(oracle, revision): ) if _git_status(oracle): raise RuntimeError(f'Oracle worktree must be clean: {oracle}') + actual_python = _python_version(python) + if actual_python != expected_python: + raise RuntimeError( + f'Temporary oracle uses Python {actual_python}, expected {expected_python}' + ) + + +def _validate_report_environments(oracle_report, target_report, expected_python): + expected_oracle = { + 'mistune': ORACLE_MISTUNE_VERSION, + 'python': expected_python, + 'sdiff': ORACLE_SDIFF_VERSION, + } + actual_oracle = oracle_report['environment'] + for key, expected in expected_oracle.items(): + actual = actual_oracle.get(key) + if actual != expected: + raise RuntimeError( + f'Oracle environment {key} must be {expected}; found {actual}' + ) + + actual_target_python = target_report['environment'].get('python') + if actual_target_python != expected_python: + raise RuntimeError( + f'Target environment must use Python {expected_python}; ' + f'found {actual_target_python}' + ) def _remove_oracle(target): @@ -82,8 +146,32 @@ def _remove_oracle(target): subprocess.run(['git', 'worktree', 'prune'], cwd=target, check=True) +def _create_oracle_environment(oracle, bootstrap_python): + subprocess.run( + [str(bootstrap_python), '-m', 'venv', str(oracle / 'venv')], + check=True, + ) + oracle_python = oracle / 'venv/bin/python' + subprocess.run( + [ + str(oracle_python), + '-m', + 'pip', + 'install', + f'mistune=={ORACLE_MISTUNE_VERSION}', + ], + cwd=oracle, + check=True, + ) + subprocess.run( + [str(oracle_python), '-m', 'pip', 'install', '--no-deps', '-e', '.'], + cwd=oracle, + check=True, + ) + + @contextmanager -def _prepared_oracle(target, revision, bootstrap_python): +def _prepared_oracle(target, revision, bootstrap_python, expected_python): _remove_oracle(target) print(f'Creating fresh Mistune oracle at {ORACLE_DIR}', flush=True) try: @@ -92,11 +180,8 @@ def _prepared_oracle(target, revision, bootstrap_python): cwd=target, check=True, ) - subprocess.run( - ['make', '-C', str(ORACLE_DIR), 'env', f'BOOTSTRAP_PYTHON={bootstrap_python}'], - check=True, - ) - _validate_oracle(ORACLE_DIR, revision) + _create_oracle_environment(ORACLE_DIR, bootstrap_python) + _validate_oracle(ORACLE_DIR, revision, expected_python) yield ORACLE_DIR finally: _remove_oracle(target) @@ -104,8 +189,11 @@ def _prepared_oracle(target, revision, bootstrap_python): def _write_golden_fixtures(path, oracle_report, oracle_revision, cases, corpus): - if oracle_report['environment']['mistune'] != '0.8.4': - raise ValueError('Golden fixtures may only be generated from Mistune 0.8.4') + if oracle_report['environment']['mistune'] != ORACLE_MISTUNE_VERSION: + raise ValueError( + f'Golden fixtures may only be generated from Mistune ' + f'{ORACLE_MISTUNE_VERSION}' + ) oracle_environment = dict(oracle_report['environment']) oracle_environment['revision'] = oracle_revision manifest = { @@ -141,6 +229,7 @@ def main(): argument_parser = argparse.ArgumentParser() argument_parser.add_argument('--oracle-revision', required=True) argument_parser.add_argument('--bootstrap-python', default='python3.11') + argument_parser.add_argument('--expected-python', required=True) argument_parser.add_argument('--target', type=Path, default=default_target) argument_parser.add_argument( '--corpus', @@ -152,14 +241,23 @@ def main(): args = argument_parser.parse_args() target = args.target.resolve() + resolved_oracle_revision = _resolve_revision(target, args.oracle_revision) + bootstrap_python = _resolve_python(args.bootstrap_python) + bootstrap_version = _python_version(bootstrap_python) + if bootstrap_version != args.expected_python: + raise RuntimeError( + f'Bootstrap Python is {bootstrap_version}, expected {args.expected_python}: ' + f'{bootstrap_python}' + ) reporter = target / 'scripts/mistune_compat_reporter.py' corpus = json.loads(args.corpus.read_text(encoding='utf-8')) cases = expand_cases(args.corpus, target) with _prepared_oracle( target, - args.oracle_revision, - args.bootstrap_python, + resolved_oracle_revision, + bootstrap_python, + args.expected_python, ) as prepared_oracle: with tempfile.TemporaryDirectory(prefix='sdiff-mistune-compat-') as temp_dir: expanded_cases = Path(temp_dir) / 'cases.json' @@ -172,17 +270,22 @@ def main(): ) target_report = _run_report(target / 'venv/bin/python', reporter, target, expanded_cases) - if oracle_report['environment']['mistune'] != '0.8.4': - raise RuntimeError( - 'Oracle environment must use Mistune 0.8.4; found ' - f'{oracle_report["environment"]["mistune"]}' - ) + _validate_oracle( + prepared_oracle, + resolved_oracle_revision, + args.expected_python, + ) + _validate_report_environments( + oracle_report, + target_report, + args.expected_python, + ) if args.write_golden_fixtures: _write_golden_fixtures( args.write_golden_fixtures, oracle_report, - _git_revision(prepared_oracle), + resolved_oracle_revision, cases, corpus, ) diff --git a/tests/fixtures/compatibility/golden_mistune_084_fixtures.json b/tests/fixtures/compatibility/golden_mistune_084_fixtures.json index 4f4398d..455da20 100644 --- a/tests/fixtures/compatibility/golden_mistune_084_fixtures.json +++ b/tests/fixtures/compatibility/golden_mistune_084_fixtures.json @@ -1098,8 +1098,8 @@ "oracle": { "mistune": "0.8.4", "python": "3.11.13", - "revision": "a73f7122421a08c380b87c38fccfe8b54d169cb9", - "sdiff": "2.0.0" + "revision": "12e7782208e4b458c8c4242882fda2377d9cba6b", + "sdiff": "1.0.0" }, "schema_version": 1 } diff --git a/tests/test_mistune_compat_runner.py b/tests/test_mistune_compat_runner.py new file mode 100644 index 0000000..12d2ab2 --- /dev/null +++ b/tests/test_mistune_compat_runner.py @@ -0,0 +1,445 @@ +import json +import subprocess +import sys +import tempfile +from contextlib import nullcontext +from io import StringIO +from pathlib import Path +from unittest import TestCase +from unittest.mock import call, patch + +from scripts import run_mistune_compat + + +REPO_ROOT = Path(__file__).resolve().parents[1] +MISTUNE_ORACLE_REVISION = '12e7782208e4b458c8c4242882fda2377d9cba6b' + + +class TestMistuneCompatibilityRunner(TestCase): + @staticmethod + def _create_git_repo(path): + subprocess.run(['git', 'init', '--quiet', str(path)], check=True) + (path / 'README').write_text('test repository\n', encoding='utf-8') + subprocess.run(['git', '-C', str(path), 'add', 'README'], check=True) + subprocess.run( + [ + 'git', + '-C', + str(path), + '-c', + 'user.name=Compatibility Test', + '-c', + 'user.email=compatibility@example.test', + 'commit', + '--quiet', + '-m', + 'test fixture', + ], + check=True, + ) + + def test_command_helpers_decode_subprocess_results(self): + completed = type('Completed', (), {'stdout': '{"value": 1}\n'})() + with patch.object( + run_mistune_compat.subprocess, + 'run', + return_value=completed, + ) as run: + report = run_mistune_compat._run_report( + Path('/python'), + Path('/reporter'), + Path('/repo'), + Path('/cases'), + ) + revision = run_mistune_compat._git_revision(Path('/repo')) + status = run_mistune_compat._git_status(Path('/repo')) + version = run_mistune_compat._python_version(Path('/python')) + + self.assertEqual({'value': 1}, report) + self.assertEqual('{"value": 1}', revision) + self.assertEqual('{"value": 1}', status) + self.assertEqual('{"value": 1}', version) + self.assertEqual(4, run.call_count) + + def test_revision_resolver_expands_an_abbreviated_commit(self): + with tempfile.TemporaryDirectory(prefix='sdiff-revision-test-') as temp_dir: + repo = Path(temp_dir) + self._create_git_repo(repo) + revision = run_mistune_compat._git_revision(repo) + resolved = run_mistune_compat._resolve_revision(repo, revision[:12]) + self.assertEqual(revision, resolved) + + def test_remove_oracle_replaces_stale_state_and_tolerates_missing_state(self): + with tempfile.TemporaryDirectory(prefix='sdiff-oracle-cleanup-test-') as temp_dir: + repo = Path(temp_dir) / 'repo' + repo.mkdir() + self._create_git_repo(repo) + oracle = Path(temp_dir) / 'mistune-oracle' + oracle.mkdir() + (oracle / 'stale').write_text('not a usable worktree', encoding='utf-8') + + with patch.object(run_mistune_compat, 'ORACLE_DIR', oracle): + run_mistune_compat._remove_oracle(repo) + self.assertFalse(oracle.exists()) + oracle.write_text('stale file', encoding='utf-8') + run_mistune_compat._remove_oracle(repo) + self.assertFalse(oracle.exists()) + run_mistune_compat._remove_oracle(repo) + self.assertFalse(oracle.exists()) + + def test_python_resolution_accepts_paths_and_path_commands(self): + target_python = Path(sys.executable).absolute() + self.assertEqual( + target_python, + run_mistune_compat._resolve_python(str(target_python)), + ) + with patch.object( + run_mistune_compat.shutil, + 'which', + return_value=str(target_python), + ): + self.assertEqual( + target_python, + run_mistune_compat._resolve_python('python-for-test'), + ) + with patch.object(run_mistune_compat.shutil, 'which', return_value=None): + with self.assertRaisesRegex(RuntimeError, 'does not exist'): + run_mistune_compat._resolve_python('missing-python-for-test') + with self.assertRaisesRegex(RuntimeError, 'does not exist'): + run_mistune_compat._resolve_python('/missing/python-for-test') + + def test_oracle_validation_rejects_each_invalid_state(self): + with tempfile.TemporaryDirectory(prefix='sdiff-oracle-validate-test-') as temp_dir: + oracle = Path(temp_dir) + python = oracle / 'venv/bin/python' + + with self.assertRaisesRegex(RuntimeError, 'no Python executable'): + run_mistune_compat._validate_oracle(oracle, 'revision', '3.11.13') + + python.parent.mkdir(parents=True) + python.symlink_to(sys.executable) + with ( + patch.object(run_mistune_compat, '_git_revision', return_value='revision'), + patch.object(run_mistune_compat, '_git_status', return_value=''), + patch.object(run_mistune_compat, '_python_version', return_value='3.11.13'), + ): + run_mistune_compat._validate_oracle(oracle, 'revision', '3.11.13') + + invalid_states = ( + ('other-revision', '', '3.11.13', 'expected revision'), + ('revision', 'dirty', '3.11.13', 'must be clean'), + ('revision', '', '3.11.9', 'uses Python 3.11.9'), + ) + for revision, status, version, message in invalid_states: + with self.subTest(message=message): + with ( + patch.object( + run_mistune_compat, + '_git_revision', + return_value=revision, + ), + patch.object( + run_mistune_compat, + '_git_status', + return_value=status, + ), + patch.object( + run_mistune_compat, + '_python_version', + return_value=version, + ), + ): + with self.assertRaisesRegex(RuntimeError, message): + run_mistune_compat._validate_oracle( + oracle, + 'revision', + '3.11.13', + ) + + def test_runner_owns_oracle_environment_installation(self): + oracle = Path('/tmp/oracle-for-test') + bootstrap_python = Path('/target/venv/bin/python') + oracle_python = oracle / 'venv/bin/python' + with patch.object(run_mistune_compat.subprocess, 'run') as run: + run_mistune_compat._create_oracle_environment( + oracle, + bootstrap_python, + ) + self.assertEqual( + [ + call( + [str(bootstrap_python), '-m', 'venv', str(oracle / 'venv')], + check=True, + ), + call( + [ + str(oracle_python), + '-m', + 'pip', + 'install', + 'mistune==0.8.4', + ], + cwd=oracle, + check=True, + ), + call( + [ + str(oracle_python), + '-m', + 'pip', + 'install', + '--no-deps', + '-e', + '.', + ], + cwd=oracle, + check=True, + ), + ], + run.call_args_list, + ) + + def test_prepared_oracle_always_starts_and_finishes_with_cleanup(self): + target = Path('/target') + revision = 'revision' + bootstrap_python = Path('/python') + with ( + patch.object(run_mistune_compat, '_remove_oracle') as remove, + patch.object(run_mistune_compat, '_create_oracle_environment') as create, + patch.object(run_mistune_compat, '_validate_oracle') as validate, + patch.object(run_mistune_compat.subprocess, 'run') as run, + ): + with run_mistune_compat._prepared_oracle( + target, + revision, + bootstrap_python, + '3.11.13', + ) as oracle: + self.assertEqual(run_mistune_compat.ORACLE_DIR, oracle) + + self.assertEqual([call(target), call(target)], remove.call_args_list) + run.assert_called_once_with( + [ + 'git', + 'worktree', + 'add', + '--detach', + str(run_mistune_compat.ORACLE_DIR), + revision, + ], + cwd=target, + check=True, + ) + create.assert_called_once_with( + run_mistune_compat.ORACLE_DIR, + bootstrap_python, + ) + validate.assert_called_once_with( + run_mistune_compat.ORACLE_DIR, + revision, + '3.11.13', + ) + + def test_report_validation_requires_exact_oracle_provenance(self): + oracle = { + 'environment': { + 'mistune': '0.8.4', + 'python': '3.11.13', + 'sdiff': '1.0.0', + }, + } + target = { + 'environment': { + 'mistune': '3.3.4', + 'python': '3.11.13', + 'sdiff': '2.0.0', + }, + } + run_mistune_compat._validate_report_environments( + oracle, + target, + '3.11.13', + ) + + mismatches = ( + ('oracle', 'mistune', '3.3.4', 'mistune must be 0.8.4'), + ('oracle', 'python', '3.11.9', 'python must be 3.11.13'), + ('oracle', 'sdiff', '2.0.0', 'sdiff must be 1.0.0'), + ('target', 'python', '3.11.9', 'must use Python 3.11.13'), + ) + for report_name, field, value, message in mismatches: + with self.subTest(report=report_name, field=field): + invalid_oracle = json.loads(json.dumps(oracle)) + invalid_target = json.loads(json.dumps(target)) + report = invalid_oracle if report_name == 'oracle' else invalid_target + report['environment'][field] = value + with self.assertRaisesRegex(RuntimeError, message): + run_mistune_compat._validate_report_environments( + invalid_oracle, + invalid_target, + '3.11.13', + ) + + def test_golden_fixture_writer_records_verified_oracle_metadata(self): + report = { + 'environment': { + 'mistune': '0.8.4', + 'python': '3.11.13', + 'sdiff': '1.0.0', + }, + 'cases': {'case': {'value': 1}}, + } + cases = [{'name': 'case'}] + with tempfile.TemporaryDirectory(prefix='sdiff-golden-writer-test-') as temp_dir: + fixture = Path(temp_dir) / 'nested/golden.json' + run_mistune_compat._write_golden_fixtures( + fixture, + report, + MISTUNE_ORACLE_REVISION, + cases, + {}, + ) + written = json.loads(fixture.read_text(encoding='utf-8')) + + self.assertEqual(MISTUNE_ORACLE_REVISION, written['oracle']['revision']) + self.assertEqual(run_mistune_compat.SEED, written['fuzz_seed']) + self.assertEqual(1, written['case_count']) + self.assertEqual( + run_mistune_compat.canonical_hash(report['cases']['case']), + written['cases']['case'], + ) + + invalid_report = json.loads(json.dumps(report)) + invalid_report['environment']['mistune'] = '3.3.4' + with self.assertRaisesRegex(ValueError, 'Mistune 0.8.4'): + run_mistune_compat._write_golden_fixtures( + Path('/unused'), + invalid_report, + MISTUNE_ORACLE_REVISION, + cases, + {}, + ) + + def test_result_diff_is_labeled_and_readable(self): + difference = run_mistune_compat._format_diff( + 'sample', + {'value': 1}, + {'value': 2}, + ) + self.assertIn('--- oracle/sample.json', difference) + self.assertIn('+++ target/sample.json', difference) + self.assertIn('- "value": 1', difference) + self.assertIn('+ "value": 2', difference) + + def test_main_writes_report_and_returns_nonzero_for_mismatches(self): + case_names = [f'case-{index}' for index in range(11)] + cases = [{'name': name} for name in case_names] + oracle_report = { + 'environment': { + 'mistune': '0.8.4', + 'python': '3.11.13', + 'sdiff': '1.0.0', + }, + 'cases': {name: {'value': 'oracle'} for name in case_names}, + } + target_report = { + 'environment': { + 'mistune': '3.3.4', + 'python': '3.11.13', + 'sdiff': '2.0.0', + }, + 'cases': {name: {'value': 'target'} for name in case_names}, + } + with tempfile.TemporaryDirectory(prefix='sdiff-runner-main-test-') as temp_dir: + report_path = Path(temp_dir) / 'nested/report.json' + fixture_path = Path(temp_dir) / 'golden.json' + arguments = [ + 'run_mistune_compat.py', + '--oracle-revision', + MISTUNE_ORACLE_REVISION, + '--bootstrap-python', + sys.executable, + '--expected-python', + '3.11.13', + '--target', + str(REPO_ROOT), + '--corpus', + str(REPO_ROOT / 'tests/fixtures/compatibility/mistune_cases.json'), + '--report', + str(report_path), + '--write-golden-fixtures', + str(fixture_path), + ] + with ( + patch.object(run_mistune_compat.sys, 'argv', arguments), + patch.object( + run_mistune_compat, + '_resolve_revision', + return_value=MISTUNE_ORACLE_REVISION, + ), + patch.object( + run_mistune_compat, + '_resolve_python', + return_value=Path(sys.executable), + ), + patch.object( + run_mistune_compat, + '_python_version', + return_value='3.11.13', + ), + patch.object(run_mistune_compat, 'expand_cases', return_value=cases), + patch.object( + run_mistune_compat, + '_prepared_oracle', + return_value=nullcontext(Path('/oracle')), + ), + patch.object( + run_mistune_compat, + '_run_report', + side_effect=[oracle_report, target_report], + ), + patch.object(run_mistune_compat, '_validate_oracle'), + patch.object(run_mistune_compat, '_validate_report_environments'), + patch.object(run_mistune_compat, '_write_golden_fixtures') as write, + patch('sys.stdout', new_callable=StringIO) as stdout, + ): + result = run_mistune_compat.main() + + written_report = json.loads(report_path.read_text(encoding='utf-8')) + + self.assertEqual(1, result) + self.assertEqual(11, written_report['mismatch_count']) + self.assertEqual(11, len(written_report['mismatches'])) + self.assertIn('1 additional mismatches omitted', stdout.getvalue()) + write.assert_called_once() + + def test_main_rejects_wrong_bootstrap_python_version(self): + arguments = [ + 'run_mistune_compat.py', + '--oracle-revision', + MISTUNE_ORACLE_REVISION, + '--bootstrap-python', + sys.executable, + '--expected-python', + '3.11.13', + ] + with ( + patch.object(run_mistune_compat.sys, 'argv', arguments), + patch.object( + run_mistune_compat, + '_resolve_revision', + return_value=MISTUNE_ORACLE_REVISION, + ), + patch.object( + run_mistune_compat, + '_resolve_python', + return_value=Path(sys.executable), + ), + patch.object( + run_mistune_compat, + '_python_version', + return_value='3.11.9', + ), + ): + with self.assertRaisesRegex(RuntimeError, '3.11.9, expected 3.11.13'): + run_mistune_compat.main() diff --git a/tests/test_mistune_compatibility.py b/tests/test_mistune_compatibility.py index 56004b9..625561e 100644 --- a/tests/test_mistune_compatibility.py +++ b/tests/test_mistune_compatibility.py @@ -20,6 +20,9 @@ GOLDEN_MISTUNE_084_FIXTURES = ( REPO_ROOT / 'tests/fixtures/compatibility/golden_mistune_084_fixtures.json' ) +MISTUNE_ORACLE_REVISION = '12e7782208e4b458c8c4242882fda2377d9cba6b' +MISTUNE_ORACLE_PYTHON = '3.11.13' +MISTUNE_ORACLE_SDIFF = '1.0.0' class TestLegacyMistuneBehaviorContract(TestCase): @@ -356,6 +359,9 @@ def test_expanded_corpus_matches_golden_fixture_inventory(self): case_names = [case['name'] for case in self.cases] self.assertEqual(1, self.expected['schema_version']) self.assertEqual('0.8.4', self.expected['oracle']['mistune']) + self.assertEqual(MISTUNE_ORACLE_PYTHON, self.expected['oracle']['python']) + self.assertEqual(MISTUNE_ORACLE_REVISION, self.expected['oracle']['revision']) + self.assertEqual(MISTUNE_ORACLE_SDIFF, self.expected['oracle']['sdiff']) self.assertEqual(self.expected['case_count'], len(self.cases)) self.assertEqual(len(case_names), len(set(case_names))) self.assertEqual(set(self.expected['cases']), set(case_names)) From 7306ba0550df8467bb151082a3885ccd680f88ab Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Wed, 26 Aug 2026 18:18:06 -0500 Subject: [PATCH 19/20] refactor: clarify Mistune parser probe --- docs/python311-migration-contract.md | 11 ++++++----- sdiff/parser.py | 15 ++++++++------- tests/test_mistune_compatibility.py | 15 +++++++++++++++ 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/docs/python311-migration-contract.md b/docs/python311-migration-contract.md index 60f963c..baa9fc8 100644 --- a/docs/python311-migration-contract.md +++ b/docs/python311-migration-contract.md @@ -190,8 +190,8 @@ Expected evidence: - Runtime metadata and both generated requirement files now agree on `mistune==3.3.4`. - Mistune 3 removed the `BlockLexer`, `InlineLexer`, and private grammar constants used by the old implementation. The adapter owns sdiff's intentionally narrow default block/list/text grammar locally, retains the explicitly - callable legacy rule facade, uses Mistune 3's hardened inline link parser surface, and emits the existing sdiff - model directly. + callable legacy rule facade, and emits the existing sdiff model directly. It exercises Mistune 3's inline link + parser as a non-authoritative compatibility probe; the probe's return and tokens do not determine sdiff output. - The adapter preserves smart entity escaping, literal unsupported Markdown, raw link/image source, unresolved reference links, legacy list shapes, inline-vs-block HTML classification, and recursive Zendesk tags. - The adapter honors direct `InlineLexer.parse(..., rules=...)` rule ordering for links, reference links, autolinks, @@ -202,7 +202,8 @@ Expected evidence: Mistune implementation classes; no indexed downstream consumer relies on that private inheritance surface. - The legacy nested-label and destination grammar is compiled into a linear-time index. This preserves 0.8.4's unusual greedy/backtracking boundaries, including quoted titles containing `)` and angle-destination fallback, - while eliminating repeated suffix scans on malformed nested openers. + while eliminating repeated suffix scans on malformed nested openers. This local index—not Mistune 3's probe—is + authoritative for link and image boundaries. - The port removes the class-level Zendesk rule mutation that could make a later plain `MdParser` call fail. This is an intentional target-only defect fix; document outputs remain oracle-identical. - Existing compatible hard pins are retained: `coverage==7.6.1` and `flake8==7.1.1`. @@ -264,7 +265,7 @@ Proof results: | `make ci-dev-install` | Pass | CI bootstrap installed/reused the exact-version venv and final `.[dev]` dependency shape. | | full skill pyupgrade ladder over `sdiff` and `tests` | Pass | All six stages were reconfirmed live with zero remaining changes; the earlier Python 3.6 stage changes are isolated in commit `7c78ad3`. | | `make requirements` twice | Pass | Both pip-compile outputs were byte-stable; compile tooling is `pip-tools==7.5.3` with `pip==25.3`. | -| `make test` | Pass | Flake8 and msgpack guard passed; 95 tests passed on Python 3.11.13, including the Golden Mistune 0.8.4 Fixtures and oracle-runner hardening tests. | +| `make test` | Pass | Flake8 and msgpack guard passed; 96 tests passed on Python 3.11.13, including the Golden Mistune 0.8.4 Fixtures, oracle-runner hardening, and non-authoritative Mistune 3 probe coverage. | | `make coverage` | Pass | Total branch-aware coverage is 99%; `sdiff/parser.py` has 100% statement and 99% branch coverage, and the oracle runner has 96%. | | `make fixture-smoke` | Pass | 3 test methods and 12 fixture subtests passed, including the exact golden snapshot. | | `make mistune-compat` | Pass | A fresh `/tmp` oracle at permanent master commit `12e7782`, with Python 3.11.13, `sdiff==1.0.0`, and `mistune==0.8.4`, matched the 3.3.4 target across 1,091 named cases with 0 mismatches; post-run cleanup succeeded. | @@ -277,7 +278,7 @@ Proof results: | `rm -rf dist && venv/bin/python -m build .` | Pass | Built isolated `sdiff-2.0.0.tar.gz` and `sdiff-2.0.0-py3-none-any.whl` without invoking the publishing targets. | | `venv/bin/twine check dist/*` | Pass | Both distribution artifacts passed metadata/README validation. | | sdist content listing | Pass | Archive contains all tests, compatibility helpers, same/different Markdown fixtures, golden JSON, and Golden Mistune 0.8.4 Fixtures. | -| extracted-sdist test suite | Pass | All 95 tests and 1,146 subtests passed directly from the unpacked source archive, including runner tests that create their own disposable Git fixtures. | +| extracted-sdist test suite | Pass | All 96 tests and 1,149 subtests passed directly from the unpacked source archive, including runner tests that create their own disposable Git fixtures. | | isolated wheel install/import/diff smoke | Pass | A separate CPython 3.11.13 venv installed `sdiff==2.0.0` with `mistune==3.3.4` and ran `sdiff.diff()`. | | CircleCI validate, `--next`, and config process | Pass | CircleCI accepted the native source `version: 2.1`, strict upcoming-compiler validation, and reusable-config expansion. | | PR #14 remote CI | Pass | CircleCI `prepare_cache`, `lint`, and `test`, plus Travis CI branch and pull-request builds, passed at `5ebec5545ba6df34942403c9afd73fc87b471c49`. | diff --git a/sdiff/parser.py b/sdiff/parser.py index e933679..2d173cb 100644 --- a/sdiff/parser.py +++ b/sdiff/parser.py @@ -124,8 +124,8 @@ class _BlockGrammar: text = re.compile(r'^[^\n]+') -class _HardenedLinkParser(mistune.InlineParser): - """Use Mistune 3's bounded link scanner without its rendering semantics.""" +class _ModernLinkParserProbe(mistune.InlineParser): + """Exercise Mistune 3 without making it authoritative for legacy output.""" def precedence_scan(self, match, state, end_pos, rules=None): return None @@ -288,7 +288,7 @@ def __init__(self): self.links = {} self.tokens = [] self.rules = _InlineGrammar() - self._link_parser = _HardenedLinkParser() + self._link_parser_probe = _ModernLinkParserProbe() def __call__(self, text, rules=None): return self.parse(text, rules) @@ -333,11 +333,12 @@ def parse(self, text, rules=None): end_position = reference_link_ends[opener_end] if end_position is not None: opener = _LINK_OPENER.match(text, position) - # Exercise Mistune 3's hardened parser for syntax it - # accepts; the local boundary remains authoritative for - # legacy raw-source compatibility. + # Exercise the installed Mistune 3 parser as a + # non-authoritative compatibility probe. Its return and + # tokens are intentionally ignored: only the local + # linear-time index owns the Mistune 0.8 boundary. assert modern_state is not None - self._link_parser.parse_link(opener, modern_state) + self._link_parser_probe.parse_link(opener, modern_state) source = text[position:end_position] self.tokens.append(Image(source) if source[0] == '!' else Link(source)) position = end_position diff --git a/tests/test_mistune_compatibility.py b/tests/test_mistune_compatibility.py index 625561e..cf8058f 100644 --- a/tests/test_mistune_compatibility.py +++ b/tests/test_mistune_compatibility.py @@ -292,6 +292,21 @@ def test_inline_parser_builds_link_indexes_only_when_link_rules_can_use_them(sel build_indexes.assert_called_once_with('[x](url)') self.assertEqual([('[x](url)', Link)], [(node.text, type(node)) for node in nodes]) + def test_modern_link_parser_probe_cannot_change_legacy_link_boundaries(self): + source = '[label](url)' + for probe_result in (None, 1, len(source) + 100): + with self.subTest(probe_result=probe_result): + with patch.object( + parser._ModernLinkParserProbe, + 'parse_link', + return_value=probe_result, + ) as probe: + tree = parser.parse(source) + probe.assert_called_once() + self.assertEqual('pa', tree.print_all()) + self.assertIsInstance(tree.nodes[0].nodes[0], Link) + self.assertEqual(source, tree.nodes[0].nodes[0].text) + def test_parser_instances_reuse_the_legacy_token_list(self): block_parser = MdParser() first_block_result = block_parser.parse('# first') From 3bb941e9f1b209b17abe3b674d453ae829359665 Mon Sep 17 00:00:00 2001 From: Michael Olmos Date: Wed, 26 Aug 2026 21:29:04 -0500 Subject: [PATCH 20/20] ci: consume compiled development lock --- .circleci/config.yml | 18 ++++++++++-------- .travis.yml | 8 ++++++-- Makefile | 21 ++++++++++++++------- README.md | 7 +++++++ docs/python311-migration-contract.md | 24 ++++++++++++++++-------- 5 files changed, 53 insertions(+), 25 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ab714e8..0d8012f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,23 +10,25 @@ commands: steps: - restore_cache: keys: - - v3-pip-{{ arch }}-{{ checksum "pyproject.toml" }} - - v3-pip-{{ arch }} - - v3-pip- + - v4-pip-{{ arch }}-{{ checksum ".python-version" }}-{{ checksum "pyproject.toml" }}-{{ checksum "requirements-dev.txt" }} + - v4-pip-{{ arch }}-{{ checksum ".python-version" }}-{{ checksum "requirements-dev.txt" }} + - v4-pip-{{ arch }} + - v4-pip- - restore_cache: keys: - - v3-venv-{{ arch }}-{{ checksum "pyproject.toml" }} - - v3-venv-{{ arch }} - - v3-venv- + - v4-venv-{{ arch }}-{{ checksum ".python-version" }}-{{ checksum "pyproject.toml" }}-{{ checksum "requirements-dev.txt" }} + - v4-venv-{{ arch }}-{{ checksum ".python-version" }}-{{ checksum "requirements-dev.txt" }} + - v4-venv-{{ arch }} + - v4-venv- save_dependency_caches: steps: - save_cache: - key: v3-pip-{{ arch }}-{{ checksum "pyproject.toml" }} + key: v4-pip-{{ arch }}-{{ checksum ".python-version" }}-{{ checksum "pyproject.toml" }}-{{ checksum "requirements-dev.txt" }} paths: - ~/.local - ~/.cache - save_cache: - key: v3-venv-{{ arch }}-{{ checksum "pyproject.toml" }} + key: v4-venv-{{ arch }}-{{ checksum ".python-version" }}-{{ checksum "pyproject.toml" }}-{{ checksum "requirements-dev.txt" }} paths: - /home/circleci/project/venv diff --git a/.travis.yml b/.travis.yml index df31221..3f3e239 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,15 @@ language: python dist: jammy python: - - "3.11" + - 3.11.9 # command to install dependencies install: - - make dev + - python -m venv venv + - venv/bin/pip install -U "pip<26" "setuptools>=82.0.1" "wheel>=0.47.0" + - venv/bin/pip install -r requirements-dev.txt + - venv/bin/pip install --no-deps -e . # command to run tests script: + - venv/bin/python -VV - make test - make coverage diff --git a/Makefile b/Makefile index 31487e6..0510603 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Package development and verification tasks (UNIX only). -PYTHON_VERSION=3.11.13 -BOOTSTRAP_PYTHON?=python3.11 +PYTHON_VERSION_FILE=.python-version +PYTHON_VERSION=$(strip $(shell cat $(PYTHON_VERSION_FILE))) PYTHON=venv/bin/python PIP=venv/bin/pip COVERAGE=venv/bin/coverage @@ -30,13 +30,20 @@ build-dir: mkdir -p build/test build/coverage env: - test -d venv || $(BOOTSTRAP_PYTHON) -m venv venv + @if [ -d "venv" ] && $(PIP) --version >/dev/null 2>&1 \ + && $(PYTHON) -c 'import platform, sys; sys.exit(platform.python_version() != "$(PYTHON_VERSION)")'; then \ + echo "Reusing venv with Python $(PYTHON_VERSION)"; \ + else \ + echo "Creating venv with Python $(PYTHON_VERSION)"; \ + if [ -d "venv" ]; then rm -rf venv; fi; \ + python -m venv venv; \ + fi $(PIP) install -U "pip<26" "setuptools>=82.0.1" "wheel>=0.47.0" $(PIP) install -e . dev: env $(PIP) install -r requirements-dev.txt - $(PIP) install -e '.[dev]' + $(PIP) install --no-deps -e . update: $(PIP) install -U . @@ -50,12 +57,12 @@ ci-env: else \ echo "No valid cached venv found, creating a fresh venv"; \ if [ -d "venv" ]; then rm -rf venv; fi; \ - $(BOOTSTRAP_PYTHON) -m venv venv; \ + python -m venv venv; \ $(PIP) install -U "pip<26" "setuptools>=82.0.1" "wheel>=0.47.0"; \ fi - ci-dev-install: ci-env - $(PIP) install -e '.[dev]' + $(PIP) install -r requirements-dev.txt + $(PIP) install --no-deps -e . flake: $(FLAKE) sdiff tests scripts diff --git a/README.md b/README.md index 5758df6..8f6a8c9 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,13 @@ make requirements `make requirements` uses `pip-compile` to regenerate `requirements.txt` and `requirements-dev.txt` from `pyproject.toml`. +`make env`, `make dev`, and the CircleCI setup read the exact local/CI interpreter version from `.python-version`. +They use the active `python`; pyenv automatically selects the file's version during local development, and CircleCI +provides the same pinned version. `make dev`, Travis, and CircleCI install the compiled +`requirements-dev.txt` environment, then install this project editable with `--no-deps`. Direct dependency declarations +remain authoritative in `pyproject.toml`; regenerate the compiled files after changing them. Travis is intentionally a +Python 3.11.9 compatibility lane because that is the patch release available from its Jammy Python catalog. + ### New release #### Release diff --git a/docs/python311-migration-contract.md b/docs/python311-migration-contract.md index baa9fc8..8d8c12e 100644 --- a/docs/python311-migration-contract.md +++ b/docs/python311-migration-contract.md @@ -84,7 +84,8 @@ flows are safe until each downstream repo reruns its own integration proof with ## Python and Packaging Target -- Interpreter target: CPython 3.11, with `.python-version` normalized to `3.11.13` +- Local and CircleCI interpreter target: exact CPython 3.11.13 from `.python-version` +- Travis compatibility lane: exact CPython 3.11.9, the Python 3.11 patch release available in its Jammy catalog - Packaging source of truth: `pyproject.toml` - Package version: `2.0.0` - Runtime dependencies: exact compatible pins @@ -125,7 +126,7 @@ Untouched baseline proof on 2026-07-16: | --- | --- | --- | | Task 1: pyproject, Python 3.11, dependency audit, pyupgrade | Applicable: complete | Replaced legacy packaging, normalized the interpreter target, hard-pinned Mistune, completed and reconfirmed every pyupgrade ladder stage, then removed the one-time tool. | | Task 2a: formatting and Flake8 alignment | Applicable: complete | Moved Flake8 and coverage config into `pyproject.toml`, added `flake8-pyproject`, and retained the 120-character convention. | -| Task 2b: hooks, CI, Makefile, README | Applicable: complete | Normalized package-oriented Makefile targets, retained Python 3.11 Travis, added native CircleCI 2.1 package CI and a pre-push hook. Service launch targets are excluded. | +| Task 2b: hooks, CI, Makefile, README | Applicable: complete | Normalized package-oriented Makefile targets; made `.python-version` authoritative for exact local/CircleCI bootstrap; pinned the Travis compatibility lane to 3.11.9; made local, Travis, and CircleCI dev installs consume the compiled dev lock; added native CircleCI 2.1 package CI and a pre-push hook. Service launch targets are excluded. | | Task 2c: mypy stabilization | Not applicable | The repo has no mypy baseline; introducing a typing program is not required to preserve this library's Python 3.11 behavior. | | Task 3: msgpack, redis, asynctest, and nose compatibility | Not applicable | The repo uses pytest and has no msgpack, redis, aioredis, asynctest, or nose dependency/call site. | | Task 4: asyncio/aiohttp modernization | Not applicable | No asyncio or aiohttp code exists. | @@ -155,6 +156,7 @@ Required commands on the final branch: - `make env` - `make dev` - `make ci-dev-install` +- the `.travis.yml` locked-install sequence in a clean CPython 3.11.9 venv - `make requirements` - the skill's full `--py36-plus` through `--py311-plus` pyupgrade ladder with `--keep-percent-format` - `make lint` @@ -188,6 +190,9 @@ Expected evidence: ## Dependency and Package Notes - Runtime metadata and both generated requirement files now agree on `mistune==3.3.4`. +- `pyproject.toml` remains the authoritative direct-dependency declaration; `requirements-dev.txt` is the compiled, + transitive development lock consumed by local `make dev`, Travis, and CircleCI. Editable installation uses + `--no-deps` after the lock so those environments do not independently resolve `.[dev]`. - Mistune 3 removed the `BlockLexer`, `InlineLexer`, and private grammar constants used by the old implementation. The adapter owns sdiff's intentionally narrow default block/list/text grammar locally, retains the explicitly callable legacy rule facade, and emits the existing sdiff model directly. It exercises Mistune 3's inline link @@ -232,7 +237,8 @@ on `python311-upgrade`. The final 2.0.0 tag and internal package publication rem Completed applicable work: - Replaced `setup.py` and `setup.cfg` with `pyproject.toml`. -- Normalized `.python-version` and package/CI policy to Python 3.11.13. +- Normalized local and CircleCI policy to exact Python 3.11.13 from `.python-version`, with a separately proven + Travis compatibility lane on exact Python 3.11.9. - Started the Python 3.11-only 2.x release line and preserved public behavior. - Hard-pinned the runtime dependency and generated deterministic requirements artifacts with `pip-compile`. - Moved Flake8 and coverage configuration into `pyproject.toml`. @@ -240,7 +246,7 @@ Completed applicable work: `requirements`, CI-install, release, and hook targets. `package` intentionally aliases the publishing workflow; build-only verification uses `venv/bin/python -m build .` directly. - Added native CircleCI 2.1 package jobs with reusable executors/commands for dependency preparation, lint, tests, - xUnit, and coverage XML. + xUnit, and coverage XML; cache keys include `.python-version`, `pyproject.toml`, and `requirements-dev.txt`. - Added exact golden parser/renderer/structural-diff/link-diff snapshots for 13 behavior-sensitive scenarios. - Ported the parser from removed Mistune 0.8 lexer APIs to a Mistune 3.3.4 compatibility adapter. - Added a dual-worktree exact-signature harness, self-contained Golden Mistune 0.8.4 Fixtures, expanded @@ -261,8 +267,10 @@ Proof results: | Command | Result | Evidence | | --- | --- | --- | -| `make clean`; `make env`; `make dev` | Pass | Fresh venv is CPython 3.11.13 with editable `sdiff==2.0.0`, `mistune==3.3.4`, and the locked dev toolchain. | -| `make ci-dev-install` | Pass | CI bootstrap installed/reused the exact-version venv and final `.[dev]` dependency shape. | +| `make clean`; `make env`; `make dev` | Pass | The active pyenv-selected Python from `.python-version` created a fresh exact CPython 3.11.13 venv with editable `sdiff==2.0.0`, `mistune==3.3.4`, and the compiled dev lock. | +| `make ci-dev-install` | Pass | CI bootstrap installed/reused exact Python 3.11.13, consumed `requirements-dev.txt`, and installed the editable package with `--no-deps`. | +| `.travis.yml` install/test commands in a clean CPython 3.11.9 venv | Pass | The compiled dev lock and editable `--no-deps` install passed `pip check`, all 96 tests, xUnit/coverage XML generation, and the 99% coverage report on 3.11.9. | +| `make ci-env` with a pre-existing Python 3.11.9 venv | Pass | Make discarded the wrong-patch venv and recreated it as exact Python 3.11.13 from `.python-version`. | | full skill pyupgrade ladder over `sdiff` and `tests` | Pass | All six stages were reconfirmed live with zero remaining changes; the earlier Python 3.6 stage changes are isolated in commit `7c78ad3`. | | `make requirements` twice | Pass | Both pip-compile outputs were byte-stable; compile tooling is `pip-tools==7.5.3` with `pip==25.3`. | | `make test` | Pass | Flake8 and msgpack guard passed; 96 tests passed on Python 3.11.13, including the Golden Mistune 0.8.4 Fixtures, oracle-runner hardening, and non-authoritative Mistune 3 probe coverage. | @@ -273,14 +281,14 @@ Proof results: | malformed-link bounded-time regression | Pass | 16,000 nested link and image openers complete in about 0.03 seconds each; the test ceiling is 2 seconds. | | `make import-smoke` | Pass | Imported documented public API and printed `2.0.0 MdParser ZendeskHelpMdParser TextRenderer`. | | `make depcheck` | Pass | `pip check` found no broken requirements. | -| `CI=1 make test-only` | Pass | 83 tests passed and wrote `build/test/results.xml` plus `build/coverage/coverage.xml`. | +| `CI=1 make test-only` | Pass | 96 tests passed and wrote `build/test/results.xml` plus `build/coverage/coverage.xml`. | | `venv/bin/python -m compileall -q sdiff tests scripts` | Pass | Source, tests, and compatibility scripts compiled on Python 3.11.13. | | `rm -rf dist && venv/bin/python -m build .` | Pass | Built isolated `sdiff-2.0.0.tar.gz` and `sdiff-2.0.0-py3-none-any.whl` without invoking the publishing targets. | | `venv/bin/twine check dist/*` | Pass | Both distribution artifacts passed metadata/README validation. | | sdist content listing | Pass | Archive contains all tests, compatibility helpers, same/different Markdown fixtures, golden JSON, and Golden Mistune 0.8.4 Fixtures. | | extracted-sdist test suite | Pass | All 96 tests and 1,149 subtests passed directly from the unpacked source archive, including runner tests that create their own disposable Git fixtures. | | isolated wheel install/import/diff smoke | Pass | A separate CPython 3.11.13 venv installed `sdiff==2.0.0` with `mistune==3.3.4` and ran `sdiff.diff()`. | -| CircleCI validate, `--next`, and config process | Pass | CircleCI accepted the native source `version: 2.1`, strict upcoming-compiler validation, and reusable-config expansion. | +| CircleCI validate, `--next`, and config process | Pass | CircleCI accepted native source `version: 2.1`, strict upcoming-compiler validation, reusable-config expansion, and v4 cache keys derived from the Python target, dependency declarations, and compiled dev lock. | | PR #14 remote CI | Pass | CircleCI `prepare_cache`, `lint`, and `test`, plus Travis CI branch and pull-request builds, passed at `5ebec5545ba6df34942403c9afd73fc87b471c49`. | | downstream `content-validator` test suite | Pass | 65 tests passed with one expected skip against the local target and Mistune 3.3.4; HTTP was mocked. | | `make hooks`, installed-file comparison, `make unhooks` | Pass | Executable pre-push hook installed exactly and was removed after verification. |