mirror of
https://github.com/eunomia-bpf/bpf-developer-tutorial.git
synced 2026-10-31 12:45:23 +08:00
Keep public tutorial references self-contained
This commit is contained in:
249
.agents/skills/write-bpf-production-tutorial/scripts/sync-source-blocks.py
Executable file → Normal file
249
.agents/skills/write-bpf-production-tutorial/scripts/sync-source-blocks.py
Executable file → Normal file
@@ -1,19 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Synchronize inventoried complete-source blocks in tutorial READMEs."""
|
||||
"""Check exact source files embedded in standard Markdown code fences."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import collections
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
BEGIN_RE = re.compile(r"^<!-- BEGIN FULL SOURCE: ([^ ]+) -->$")
|
||||
FENCE_RE = re.compile(r"^(`{3,})([A-Za-z0-9_+.-]*)$")
|
||||
END = "<!-- END FULL SOURCE -->"
|
||||
FENCE_RE = re.compile(r"^(`{3,})([A-Za-z0-9_+.-]*)[ \t]*$")
|
||||
LEGACY_MARKERS = ("<!-- BEGIN FULL SOURCE:", "<!-- END FULL SOURCE -->")
|
||||
LANGUAGES = {
|
||||
".c": "c",
|
||||
".cc": "cpp",
|
||||
@@ -35,101 +33,33 @@ def contained_path(root: pathlib.Path, value: str, label: str) -> pathlib.Path:
|
||||
return path
|
||||
|
||||
|
||||
def render(
|
||||
readme: pathlib.Path,
|
||||
root: pathlib.Path,
|
||||
forbidden_sources: set[pathlib.Path],
|
||||
) -> tuple[str, set[pathlib.Path]]:
|
||||
lines = readme.read_text(encoding="utf-8").splitlines()
|
||||
rendered: list[str] = []
|
||||
sources: set[pathlib.Path] = set()
|
||||
index = 0
|
||||
def parse_fenced_blocks(readme: pathlib.Path) -> list[tuple[str, str]]:
|
||||
text = readme.read_text(encoding="utf-8")
|
||||
for marker in LEGACY_MARKERS:
|
||||
if marker in text:
|
||||
raise ValueError(
|
||||
f"legacy complete-source HTML marker is not allowed in {readme.name}"
|
||||
)
|
||||
|
||||
lines = text.splitlines(keepends=True)
|
||||
blocks: list[tuple[str, str]] = []
|
||||
index = 0
|
||||
while index < len(lines):
|
||||
match = BEGIN_RE.match(lines[index])
|
||||
if not match:
|
||||
rendered.append(lines[index])
|
||||
opening = FENCE_RE.match(lines[index].rstrip("\r\n"))
|
||||
if not opening:
|
||||
index += 1
|
||||
continue
|
||||
|
||||
source_name = match.group(1)
|
||||
source = contained_path(root, source_name, "source path")
|
||||
if not source.is_file():
|
||||
raise ValueError(f"source file does not exist: {source_name}")
|
||||
if source in forbidden_sources:
|
||||
raise ValueError(
|
||||
f"complete-source marker cannot name a tutorial README: {source_name}"
|
||||
)
|
||||
if source.suffix not in LANGUAGES:
|
||||
raise ValueError(f"unsupported complete-source file type: {source_name}")
|
||||
if source in sources:
|
||||
raise ValueError(f"duplicate complete-source marker: {source_name}")
|
||||
|
||||
if index + 1 >= len(lines):
|
||||
raise ValueError(f"missing generated code fence after {source_name}")
|
||||
opening_fence = FENCE_RE.match(lines[index + 1])
|
||||
if not opening_fence:
|
||||
raise ValueError(f"missing generated code fence after {source_name}")
|
||||
closing_fence = opening_fence.group(1)
|
||||
end = index + 2
|
||||
while end < len(lines) and lines[end] != closing_fence:
|
||||
fence, language = opening.groups()
|
||||
end = index + 1
|
||||
while end < len(lines) and lines[end].rstrip("\r\n") != fence:
|
||||
end += 1
|
||||
if end == len(lines) or end + 1 == len(lines) or lines[end + 1] != END:
|
||||
raise ValueError(f"missing matching generated fence and {END!r} after {source_name}")
|
||||
if end == len(lines):
|
||||
raise ValueError(f"unclosed Markdown code fence in {readme.name}")
|
||||
blocks.append((language, "".join(lines[index + 1 : end])))
|
||||
index = end + 1
|
||||
|
||||
language = LANGUAGES[source.suffix]
|
||||
source_bytes = source.read_bytes()
|
||||
if not source_bytes:
|
||||
raise ValueError(f"complete source cannot be empty: {source_name}")
|
||||
if not source_bytes.endswith(b"\n"):
|
||||
raise ValueError(
|
||||
f"complete source must end with an LF byte for exact fenced inclusion: {source_name}"
|
||||
)
|
||||
source_text = source_bytes.decode("utf-8")
|
||||
if "\r" in source_text:
|
||||
raise ValueError(f"complete source must use LF line endings: {source_name}")
|
||||
max_backticks = max((len(run) for run in re.findall(r"`+", source_text)), default=0)
|
||||
generated_fence = "`" * max(3, max_backticks + 1)
|
||||
source_text = source_text[:-1]
|
||||
rendered.extend(
|
||||
[
|
||||
lines[index],
|
||||
f"{generated_fence}{language}",
|
||||
source_text,
|
||||
generated_fence,
|
||||
END,
|
||||
]
|
||||
)
|
||||
sources.add(source)
|
||||
index = end + 2
|
||||
|
||||
return "\n".join(rendered) + "\n", sources
|
||||
|
||||
|
||||
def display_paths(paths: set[pathlib.Path], root: pathlib.Path) -> str:
|
||||
return ", ".join(sorted(str(path.relative_to(root)) for path in paths))
|
||||
|
||||
|
||||
def atomic_write(path: pathlib.Path, content: str) -> None:
|
||||
temporary_name: str | None = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
dir=path.parent,
|
||||
prefix=f".{path.name}.",
|
||||
delete=False,
|
||||
) as temporary:
|
||||
temporary_name = temporary.name
|
||||
temporary.write(content)
|
||||
temporary.flush()
|
||||
os.fsync(temporary.fileno())
|
||||
os.chmod(temporary_name, path.stat().st_mode)
|
||||
os.replace(temporary_name, path)
|
||||
temporary_name = None
|
||||
finally:
|
||||
if temporary_name is not None:
|
||||
pathlib.Path(temporary_name).unlink(missing_ok=True)
|
||||
return blocks
|
||||
|
||||
|
||||
def parse_arguments() -> argparse.Namespace:
|
||||
@@ -144,9 +74,9 @@ def parse_arguments() -> argparse.Namespace:
|
||||
required=True,
|
||||
help="repository-relative complete core source; repeat for the full inventory",
|
||||
)
|
||||
mode = parser.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument("--write", action="store_true", help="replace marked blocks")
|
||||
mode.add_argument("--check", action="store_true", help="fail when blocks differ")
|
||||
parser.add_argument(
|
||||
"--check", action="store_true", required=True, help="check exact fenced source"
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -172,79 +102,42 @@ def resolve_readmes(
|
||||
return readmes
|
||||
|
||||
|
||||
def resolve_expected_sources(
|
||||
root: pathlib.Path,
|
||||
values: list[str],
|
||||
readme_paths: set[pathlib.Path],
|
||||
) -> set[pathlib.Path]:
|
||||
expected_sources: set[pathlib.Path] = set()
|
||||
for value in values:
|
||||
source = contained_path(root, value, "expected source path")
|
||||
if not source.is_file():
|
||||
raise ValueError(f"expected source file does not exist: {value}")
|
||||
if source in readme_paths:
|
||||
raise ValueError(f"expected source cannot be a tutorial README: {value}")
|
||||
if source.suffix not in LANGUAGES:
|
||||
raise ValueError(f"unsupported expected-source file type: {value}")
|
||||
if source in expected_sources:
|
||||
raise ValueError(f"duplicate expected source: {value}")
|
||||
expected_sources.add(source)
|
||||
return expected_sources
|
||||
def source_payload(
|
||||
root: pathlib.Path, value: str, readme_paths: set[pathlib.Path]
|
||||
) -> tuple[str, str]:
|
||||
source = contained_path(root, value, "expected source path")
|
||||
if not source.is_file():
|
||||
raise ValueError(f"expected source file does not exist: {value}")
|
||||
if source in readme_paths:
|
||||
raise ValueError(f"expected source cannot be a tutorial README: {value}")
|
||||
if source.suffix not in LANGUAGES:
|
||||
raise ValueError(f"unsupported expected-source file type: {value}")
|
||||
|
||||
source_bytes = source.read_bytes()
|
||||
if not source_bytes:
|
||||
raise ValueError(f"complete source cannot be empty: {value}")
|
||||
if not source_bytes.endswith(b"\n"):
|
||||
raise ValueError(f"complete source must end with an LF byte: {value}")
|
||||
source_text = source_bytes.decode("utf-8")
|
||||
if "\r" in source_text:
|
||||
raise ValueError(f"complete source must use LF line endings: {value}")
|
||||
return LANGUAGES[source.suffix], source_text
|
||||
|
||||
|
||||
def inventory_mismatch(
|
||||
def check_readme(
|
||||
value: str,
|
||||
root: pathlib.Path,
|
||||
expected_sources: set[pathlib.Path],
|
||||
sources: set[pathlib.Path],
|
||||
) -> ValueError | None:
|
||||
missing = expected_sources - sources
|
||||
extra = sources - expected_sources
|
||||
if not missing and not extra:
|
||||
return None
|
||||
details = []
|
||||
if missing:
|
||||
details.append(f"missing: {display_paths(missing, root)}")
|
||||
if extra:
|
||||
details.append(f"unexpected: {display_paths(extra, root)}")
|
||||
return ValueError(
|
||||
f"complete-source inventory mismatch in {value} ({'; '.join(details)})"
|
||||
)
|
||||
|
||||
|
||||
def plan_updates(
|
||||
root: pathlib.Path,
|
||||
readmes: list[tuple[str, pathlib.Path]],
|
||||
expected_sources: set[pathlib.Path],
|
||||
) -> list[tuple[str, pathlib.Path, str, str, int]]:
|
||||
readme_paths = {path for _, path in readmes}
|
||||
planned: list[tuple[str, pathlib.Path, str, str, int]] = []
|
||||
for value, readme in readmes:
|
||||
before = readme.read_text(encoding="utf-8")
|
||||
after, sources = render(readme, root, readme_paths)
|
||||
mismatch = inventory_mismatch(value, root, expected_sources, sources)
|
||||
if mismatch:
|
||||
raise mismatch
|
||||
planned.append((value, readme, before, after, len(sources)))
|
||||
return planned
|
||||
|
||||
|
||||
def execute_plan(
|
||||
planned: list[tuple[str, pathlib.Path, str, str, int]], write: bool
|
||||
) -> int:
|
||||
failed = False
|
||||
for value, readme, before, after, blocks in planned:
|
||||
if write:
|
||||
if before != after:
|
||||
atomic_write(readme, after)
|
||||
print(f"synced {blocks} source blocks in {value}")
|
||||
elif before != after:
|
||||
print(f"stale complete-source block in {value}", file=sys.stderr)
|
||||
failed = True
|
||||
else:
|
||||
print(f"checked {blocks} source blocks in {value}")
|
||||
|
||||
return 1 if failed else 0
|
||||
readme: pathlib.Path,
|
||||
expected: collections.Counter[tuple[str, str]],
|
||||
labels: dict[tuple[str, str], list[str]],
|
||||
) -> None:
|
||||
actual = collections.Counter(parse_fenced_blocks(readme))
|
||||
for payload, count in expected.items():
|
||||
found = actual[payload]
|
||||
names = ", ".join(labels[payload])
|
||||
if found < count:
|
||||
raise ValueError(f"missing exact fenced source in {value}: {names}")
|
||||
if found > count:
|
||||
raise ValueError(f"duplicate exact fenced source in {value}: {names}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
@@ -257,14 +150,24 @@ def main() -> int:
|
||||
try:
|
||||
readmes = resolve_readmes(root, args.readme)
|
||||
readme_paths = {path for _, path in readmes}
|
||||
expected_sources = resolve_expected_sources(
|
||||
root, args.expected_source, readme_paths
|
||||
)
|
||||
planned = plan_updates(root, readmes, expected_sources)
|
||||
except (OSError, ValueError) as error:
|
||||
labels: dict[tuple[str, str], list[str]] = collections.defaultdict(list)
|
||||
expected: collections.Counter[tuple[str, str]] = collections.Counter()
|
||||
seen_values: set[str] = set()
|
||||
for value in args.expected_source:
|
||||
if value in seen_values:
|
||||
raise ValueError(f"duplicate expected source: {value}")
|
||||
payload = source_payload(root, value, readme_paths)
|
||||
expected[payload] += 1
|
||||
labels[payload].append(value)
|
||||
seen_values.add(value)
|
||||
|
||||
for value, readme in readmes:
|
||||
check_readme(value, readme, expected, labels)
|
||||
print(f"checked {sum(expected.values())} fenced source blocks in {value}")
|
||||
except (OSError, UnicodeError, ValueError) as error:
|
||||
print(error, file=sys.stderr)
|
||||
return 1
|
||||
return execute_plan(planned, args.write)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
269
.agents/skills/write-bpf-production-tutorial/scripts/test_sync_source_blocks.py
Executable file → Normal file
269
.agents/skills/write-bpf-production-tutorial/scripts/test_sync_source_blocks.py
Executable file → Normal file
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for the tutorial workflow's source-block synchronizer."""
|
||||
"""Tests for marker-free tutorial source-block checks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -12,7 +12,7 @@ import unittest
|
||||
SCRIPT = pathlib.Path(__file__).with_name("sync-source-blocks.py")
|
||||
|
||||
|
||||
class SyncSourceBlocksTest(unittest.TestCase):
|
||||
class SourceBlocksTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
self.root = pathlib.Path(self.temporary.name)
|
||||
@@ -28,26 +28,32 @@ class SyncSourceBlocksTest(unittest.TestCase):
|
||||
def tearDown(self) -> None:
|
||||
self.temporary.cleanup()
|
||||
|
||||
def write_readme(self, name: str, source: str = "src/lesson/tool.c") -> pathlib.Path:
|
||||
def write_readme(
|
||||
self,
|
||||
name: str,
|
||||
payload: str | None = None,
|
||||
language: str = "c",
|
||||
fence: str = "```",
|
||||
) -> pathlib.Path:
|
||||
if payload is None:
|
||||
payload = (self.root / "src/lesson/tool.c").read_text(encoding="utf-8")
|
||||
path = self.root / name
|
||||
path.write_text(
|
||||
"# Lesson\n\n"
|
||||
f"<!-- BEGIN FULL SOURCE: {source} -->\n"
|
||||
"```c\nold\n```\n"
|
||||
"<!-- END FULL SOURCE -->\n",
|
||||
encoding="utf-8",
|
||||
f"# Lesson\n\n{fence}{language}\n{payload}{fence}\n", encoding="utf-8"
|
||||
)
|
||||
return path
|
||||
|
||||
def write_pair(self, **kwargs: str) -> None:
|
||||
self.write_readme("src/lesson/README.md", **kwargs)
|
||||
self.write_readme("src/lesson/README.zh.md", **kwargs)
|
||||
|
||||
def run_script(
|
||||
self,
|
||||
*arguments: str,
|
||||
expected: tuple[str, ...] = ("src/lesson/tool.c",),
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
expected_arguments = [
|
||||
item
|
||||
for source in expected
|
||||
for item in ("--expected-source", source)
|
||||
item for source in expected for item in ("--expected-source", source)
|
||||
]
|
||||
return subprocess.run(
|
||||
[
|
||||
@@ -63,201 +69,98 @@ class SyncSourceBlocksTest(unittest.TestCase):
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_write_then_check_bilingual_pair(self) -> None:
|
||||
english = self.write_readme("src/lesson/README.md")
|
||||
chinese = self.write_readme("src/lesson/README.zh.md")
|
||||
|
||||
written = self.run_script(
|
||||
"--readme", "src/lesson/README.md",
|
||||
"--readme", "src/lesson/README.zh.md",
|
||||
"--write",
|
||||
)
|
||||
self.assertEqual(0, written.returncode, written.stderr)
|
||||
self.assertIn("int main(void)", english.read_text(encoding="utf-8"))
|
||||
self.assertEqual(
|
||||
english.read_text(encoding="utf-8"), chinese.read_text(encoding="utf-8")
|
||||
)
|
||||
|
||||
checked = self.run_script(
|
||||
"--readme", "src/lesson/README.md",
|
||||
"--readme", "src/lesson/README.zh.md",
|
||||
"--check",
|
||||
)
|
||||
self.assertEqual(0, checked.returncode, checked.stderr)
|
||||
|
||||
before = english.read_bytes()
|
||||
repeated = self.run_script(
|
||||
"--readme", "src/lesson/README.md",
|
||||
"--readme", "src/lesson/README.zh.md",
|
||||
"--write",
|
||||
)
|
||||
self.assertEqual(0, repeated.returncode, repeated.stderr)
|
||||
self.assertEqual(before, english.read_bytes())
|
||||
|
||||
def test_check_rejects_stale_block(self) -> None:
|
||||
self.write_readme("src/lesson/README.md")
|
||||
self.write_readme("src/lesson/README.zh.md")
|
||||
checked = self.run_script(
|
||||
"--readme", "src/lesson/README.md",
|
||||
"--readme", "src/lesson/README.zh.md",
|
||||
"--check",
|
||||
)
|
||||
self.assertEqual(1, checked.returncode)
|
||||
self.assertIn("stale complete-source block", checked.stderr)
|
||||
|
||||
def test_write_validates_every_readme_before_changing_any(self) -> None:
|
||||
english = self.write_readme("src/lesson/README.md")
|
||||
original = english.read_text(encoding="utf-8")
|
||||
invalid = self.root / "src/lesson/README.zh.md"
|
||||
invalid.write_text("# No markers\n", encoding="utf-8")
|
||||
|
||||
result = self.run_script(
|
||||
"--readme", "src/lesson/README.md",
|
||||
"--readme", "src/lesson/README.zh.md",
|
||||
"--write",
|
||||
)
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertEqual(original, english.read_text(encoding="utf-8"))
|
||||
|
||||
def test_rejects_source_outside_repository(self) -> None:
|
||||
self.write_readme("src/lesson/README.md", "../outside.c")
|
||||
self.write_readme("src/lesson/README.zh.md", "../outside.c")
|
||||
result = self.run_script(
|
||||
"--readme", "src/lesson/README.md",
|
||||
"--readme", "src/lesson/README.zh.md",
|
||||
"--write", expected=("../outside.c",)
|
||||
)
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("escapes repository root", result.stderr)
|
||||
|
||||
def test_rejects_nested_markers(self) -> None:
|
||||
readme = self.write_readme("src/lesson/README.md")
|
||||
self.write_readme("src/lesson/README.zh.md")
|
||||
text = readme.read_text(encoding="utf-8").replace(
|
||||
"```c\nold\n```\n",
|
||||
"<!-- BEGIN FULL SOURCE: src/lesson/tool.c -->\n",
|
||||
)
|
||||
readme.write_text(text, encoding="utf-8")
|
||||
result = self.run_script(
|
||||
"--readme", "src/lesson/README.md",
|
||||
"--readme", "src/lesson/README.zh.md",
|
||||
"--write",
|
||||
)
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("missing generated code fence", result.stderr)
|
||||
|
||||
def test_rejects_readme_as_source(self) -> None:
|
||||
self.write_readme("src/lesson/README.md", "src/lesson/README.md")
|
||||
self.write_readme("src/lesson/README.zh.md", "src/lesson/README.md")
|
||||
result = self.run_script(
|
||||
def check_pair(self, expected: tuple[str, ...] = ("src/lesson/tool.c",)):
|
||||
return self.run_script(
|
||||
"--readme",
|
||||
"src/lesson/README.md",
|
||||
"--readme",
|
||||
"src/lesson/README.zh.md",
|
||||
"--write",
|
||||
expected=("src/lesson/README.md",),
|
||||
"--check",
|
||||
expected=expected,
|
||||
)
|
||||
|
||||
def test_accepts_exact_standard_markdown_fences(self) -> None:
|
||||
self.write_pair()
|
||||
result = self.check_pair()
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("checked 1 fenced source blocks", result.stdout)
|
||||
|
||||
def test_rejects_stale_source_block(self) -> None:
|
||||
self.write_pair(payload="int stale;\n")
|
||||
result = self.check_pair()
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("missing exact fenced source", result.stderr)
|
||||
|
||||
def test_rejects_duplicate_exact_source_block(self) -> None:
|
||||
source = (self.root / "src/lesson/tool.c").read_text(encoding="utf-8")
|
||||
payload = f"{source}```\n\n```c\n{source}"
|
||||
self.write_pair(payload=payload)
|
||||
result = self.check_pair()
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("duplicate exact fenced source", result.stderr)
|
||||
|
||||
def test_rejects_legacy_html_markers(self) -> None:
|
||||
self.write_pair()
|
||||
readme = self.root / "src/lesson/README.md"
|
||||
readme.write_text(
|
||||
readme.read_text(encoding="utf-8")
|
||||
+ "<!-- END FULL SOURCE -->\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = self.check_pair()
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("HTML marker is not allowed", result.stderr)
|
||||
|
||||
def test_rejects_source_outside_repository(self) -> None:
|
||||
self.write_pair()
|
||||
result = self.check_pair(expected=("../outside.c",))
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("escapes repository root", result.stderr)
|
||||
|
||||
def test_rejects_readme_as_source(self) -> None:
|
||||
self.write_pair()
|
||||
result = self.check_pair(expected=("src/lesson/README.md",))
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("expected source cannot be a tutorial README", result.stderr)
|
||||
|
||||
def test_rejects_incomplete_inventory(self) -> None:
|
||||
self.write_readme("src/lesson/README.md")
|
||||
self.write_readme("src/lesson/README.zh.md")
|
||||
result = self.run_script(
|
||||
"--readme",
|
||||
"src/lesson/README.md",
|
||||
"--readme",
|
||||
"src/lesson/README.zh.md",
|
||||
"--check",
|
||||
expected=("src/lesson/tool.c", "src/lesson/contract.h"),
|
||||
self.write_pair()
|
||||
result = self.check_pair(
|
||||
expected=("src/lesson/tool.c", "src/lesson/contract.h")
|
||||
)
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("missing: src/lesson/contract.h", result.stderr)
|
||||
self.assertIn("src/lesson/contract.h", result.stderr)
|
||||
|
||||
def test_rejects_unsupported_source_type(self) -> None:
|
||||
source = self.root / "src/lesson/source.txt"
|
||||
source.write_text("text\n", encoding="utf-8")
|
||||
self.write_readme("src/lesson/README.md", "src/lesson/source.txt")
|
||||
self.write_readme("src/lesson/README.zh.md", "src/lesson/source.txt")
|
||||
result = self.run_script(
|
||||
"--readme",
|
||||
"src/lesson/README.md",
|
||||
"--readme",
|
||||
"src/lesson/README.zh.md",
|
||||
"--write",
|
||||
expected=("src/lesson/source.txt",),
|
||||
)
|
||||
self.write_pair(payload="text\n", language="text")
|
||||
result = self.check_pair(expected=("src/lesson/source.txt",))
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("unsupported expected-source file type", result.stderr)
|
||||
|
||||
def test_source_marker_and_backtick_collisions_are_idempotent(self) -> None:
|
||||
def test_accepts_longer_fence_for_source_with_backticks(self) -> None:
|
||||
source = self.root / "src/lesson/tool.py"
|
||||
source.write_text(
|
||||
'payload = """\n<!-- END FULL SOURCE -->\n```\n"""\n\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
english = self.write_readme("src/lesson/README.md", "src/lesson/tool.py")
|
||||
chinese = self.write_readme("src/lesson/README.zh.md", "src/lesson/tool.py")
|
||||
arguments = (
|
||||
"--readme", "src/lesson/README.md",
|
||||
"--readme", "src/lesson/README.zh.md",
|
||||
"--write",
|
||||
)
|
||||
first = self.run_script(*arguments, expected=("src/lesson/tool.py",))
|
||||
self.assertEqual(0, first.returncode, first.stderr)
|
||||
first_english = english.read_bytes()
|
||||
first_chinese = chinese.read_bytes()
|
||||
self.assertIn(b"````python", first_english)
|
||||
payload = 'value = """\n```\n"""\n'
|
||||
source.write_text(payload, encoding="utf-8")
|
||||
self.write_pair(payload=payload, language="python", fence="````")
|
||||
result = self.check_pair(expected=("src/lesson/tool.py",))
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
|
||||
second = self.run_script(*arguments, expected=("src/lesson/tool.py",))
|
||||
self.assertEqual(0, second.returncode, second.stderr)
|
||||
self.assertEqual(first_english, english.read_bytes())
|
||||
self.assertEqual(first_chinese, chinese.read_bytes())
|
||||
|
||||
def test_rejects_empty_source_without_changing_either_readme(self) -> None:
|
||||
source = self.root / "src/lesson/tool.c"
|
||||
source.write_bytes(b"")
|
||||
english = self.write_readme("src/lesson/README.md")
|
||||
chinese = self.write_readme("src/lesson/README.zh.md")
|
||||
before = (english.read_bytes(), chinese.read_bytes())
|
||||
|
||||
result = self.run_script(
|
||||
"--readme", "src/lesson/README.md",
|
||||
"--readme", "src/lesson/README.zh.md",
|
||||
"--write",
|
||||
)
|
||||
def test_rejects_empty_source(self) -> None:
|
||||
(self.root / "src/lesson/tool.c").write_bytes(b"")
|
||||
self.write_pair(payload="")
|
||||
result = self.check_pair()
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("complete source cannot be empty", result.stderr)
|
||||
self.assertEqual(before, (english.read_bytes(), chinese.read_bytes()))
|
||||
|
||||
def test_rejects_source_without_final_lf(self) -> None:
|
||||
source = self.root / "src/lesson/tool.c"
|
||||
source.write_bytes(b"int value = 1;")
|
||||
self.write_readme("src/lesson/README.md")
|
||||
self.write_readme("src/lesson/README.zh.md")
|
||||
|
||||
result = self.run_script(
|
||||
"--readme", "src/lesson/README.md",
|
||||
"--readme", "src/lesson/README.zh.md",
|
||||
"--write",
|
||||
)
|
||||
(self.root / "src/lesson/tool.c").write_bytes(b"int value = 1;")
|
||||
self.write_pair(payload="int value = 1;\n")
|
||||
result = self.check_pair()
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("must end with an LF byte", result.stderr)
|
||||
|
||||
def test_preserves_multiple_trailing_lfs(self) -> None:
|
||||
source = self.root / "src/lesson/tool.c"
|
||||
source.write_bytes(b"int value = 1;\n\n\n")
|
||||
english = self.write_readme("src/lesson/README.md")
|
||||
self.write_readme("src/lesson/README.zh.md")
|
||||
|
||||
result = self.run_script(
|
||||
"--readme", "src/lesson/README.md",
|
||||
"--readme", "src/lesson/README.zh.md",
|
||||
"--write",
|
||||
)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("int value = 1;\n\n\n```", english.read_text(encoding="utf-8"))
|
||||
|
||||
def test_requires_bilingual_pair(self) -> None:
|
||||
self.write_readme("src/lesson/README.md")
|
||||
result = self.run_script(
|
||||
@@ -266,6 +169,14 @@ class SyncSourceBlocksTest(unittest.TestCase):
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("exactly the English and Chinese README pair", result.stderr)
|
||||
|
||||
def test_rejects_unclosed_fence(self) -> None:
|
||||
self.write_pair()
|
||||
readme = self.root / "src/lesson/README.zh.md"
|
||||
readme.write_text("# Lesson\n\n```c\nint value;\n", encoding="utf-8")
|
||||
result = self.check_pair()
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("unclosed Markdown code fence", result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user