Keep public tutorial references self-contained

This commit is contained in:
LinuxDev9002
2026-07-18 23:28:18 -07:00
parent 496579074a
commit bb87059e0c
8 changed files with 188 additions and 369 deletions

View File

@@ -5,13 +5,13 @@ description: Repository-local prose and bilingual checklist for drafting, rewrit
# BPF Tutorial Writing Style
Use this Skill for writing quality only. Use `$write-bpf-production-tutorial` for build, KVM, source synchronization, and PR steps.
Use this Skill for writing quality only. Use `$write-bpf-production-tutorial` for build, KVM, exact-source checks, and PR steps.
Read all three references before drafting or reviewing:
1. [Advanced tutorial guidelines](references/advanced-tutorial-guidelines.md) contains the complete project-provided requirements and must remain intact.
2. [Repository house style](references/repository-house-style.md) explains how to learn from existing tutorials without copying their defects.
3. [Prose and bilingual checklist](references/prose-and-bilingual-checklist.md) carries the relevant eunomia.dev writing discipline into this repository.
1. [Advanced tutorial guidelines](https://github.com/eunomia-bpf/bpf-developer-tutorial/blob/main/.agents/skills/bpf-tutorial-writing-style/references/advanced-tutorial-guidelines.md) contains the complete project-provided requirements and must remain intact.
2. [Repository house style](https://github.com/eunomia-bpf/bpf-developer-tutorial/blob/main/.agents/skills/bpf-tutorial-writing-style/references/repository-house-style.md) explains how to learn from existing tutorials without copying their defects.
3. [Prose and bilingual checklist](https://github.com/eunomia-bpf/bpf-developer-tutorial/blob/main/.agents/skills/bpf-tutorial-writing-style/references/prose-and-bilingual-checklist.md) carries the relevant eunomia.dev writing discipline into this repository.
Also read the English and Chinese versions of `47-cuda-events` and `49-hid` in full. Existing lessons are examples of rhythm, depth, and teaching order, not templates. Verified code, real output, primary sources, and the user's requirements remain authoritative.
@@ -27,9 +27,11 @@ Do not write like a paper, specification, PR description, release note, test log
Include complete core source exactly as implemented. Teach one component at a time: introduce its role, present its source inline, and immediately explain the important logic before moving on. Do not hide source in `<details>` and do not separate a long source dump from all of its explanation.
Do not simplify code, insert ellipses, rewrite comments, or hand-edit synchronized source blocks. Link tests and build boilerplate unless they are part of the mechanism being taught.
Do not simplify code, insert ellipses, or rewrite comments. Present complete source in ordinary Markdown fenced code blocks without HTML synchronization markers, then verify it against the real file.
Link every other tutorial with its canonical absolute URL. Do not use relative cross-tutorial paths such as `../49-hid/README.md`, because those links break when the page is published outside the repository. Keep relative links only for files inside the current lesson.
Use canonical GitHub absolute URLs for every Markdown link, even when the target belongs to the current lesson. Never use `./`, `../`, or another host. If no stable GitHub URL exists, leave the text unlinked. This rule overrides link patterns in older lessons and the website call to action in the advanced guidelines.
Keep local infrastructure private. Do not name workspace paths, shared lab repositories, VM instances, copy routes, caches, or agent trace locations in a tutorial or PR description. Retain only public reproducibility facts such as architecture, kernel version and commit, configuration, commands, and captured output.
## Keep scope honest

View File

@@ -58,13 +58,12 @@ Provide instructions on compiling and running the eBPF programs, noting any adva
Include links to complete source code and resources:
- **Repository:** <https://github.com/eunomia-bpf/bpf-developer-tutorial>
- **Website:** <https://eunomia.dev/tutorials/>
## 6. Summary and Call to Action
Wrap up by summarizing the key points. Emphasize the advanced concepts covered and encourage readers to apply this knowledge. Invite them to explore more examples and tutorials, as one paragraph:
> If you'd like to dive deeper into eBPF, check out our tutorial repository at <https://github.com/eunomia-bpf/bpf-developer-tutorial> or visit our website at <https://eunomia.dev/tutorials/>.
> If you'd like to dive deeper into eBPF, check out our tutorial repository at <https://github.com/eunomia-bpf/bpf-developer-tutorial>.
## reference

View File

@@ -42,13 +42,14 @@ Use it as a reader-focused editing guide, not as a reason to add review rounds.
## Code and evidence presentation
- Present full core source exactly as it exists before excerpting it again for explanation. Mark complete-source blocks for the workflow Skill's synchronizer, then require its `--check` mode to pass against the complete source inventory. Never silently shorten functions, replace blocks with ellipses, translate comments, or “clean up” commands.
- Present full core source exactly as it exists before excerpting it again for explanation. Use ordinary Markdown fenced code blocks with no HTML markers, then require the workflow Skill's read-only `--check` mode to pass against the complete source inventory. Never silently shorten functions, replace blocks with ellipses, translate comments, or “clean up” commands.
- Teach one component at a time. Introduce the file's role, present its complete source inline, and immediately explain the parts that matter before starting the next component.
- Never hide source in `<details>` and never place several complete files in one uninterrupted dump followed by a distant walkthrough. Long examples need stronger component boundaries and local explanations, not collapsed code.
- Explain what advanced developers need to know: program type, attach point, context restrictions, helper or kfunc semantics, map ownership, state lifetime, concurrency, verifier constraints, event/control path, error handling, and cleanup.
- Keep build and run commands copyable. Explain prerequisites before the command and interpret real output after it.
- Label volatile fields and distinguish demonstrations, functional tests, and benchmarks.
- Use canonical absolute URLs for every link to another tutorial. Reject `../<lesson>/README.md` and `../<lesson>/README.zh.md`; reserve relative links for files inside the current lesson.
- Use canonical GitHub absolute URLs for every Markdown link, including links to the current lesson. Reject every `./`, `../`, or non-GitHub target. If no stable GitHub URL exists, do not add a link. This repository rule overrides link examples in precedents and the advanced guideline's website call to action.
- Keep local infrastructure and agent state private. Never publish workspace paths, shared lab repository or VM names, host-to-guest copy routes, cache locations, prompt stores, or trace paths. Public reproducibility may include architecture, kernel version and commit, configuration, commands, and captured output.
## Natural Chinese

View File

@@ -16,7 +16,7 @@ Present complete core source exactly as required by the advanced tutorial guidel
After the source, explain the BPF structure, attach point, state and ownership, relevant helpers or kfuncs, kernel interaction, user-space lifecycle, error handling, and cleanup. Spend words on advanced constraints and surprising behavior. Do not explain ordinary C syntax.
Compilation and execution should show copyable commands, representative real output, and what that output means. Put detailed KVM provenance, commit IDs, and compatibility data in a compact requirements or reproducibility paragraph. Do not narrate the PR's validation history.
Compilation and execution should show copyable commands, representative real output, and what that output means. Put public kernel provenance, commit IDs, and compatibility data in a compact requirements or reproducibility paragraph. Do not name local workspaces, shared lab repositories, VM instances, copy routes, caches, agent traces, or the PR's validation history.
End with an honest boundary, a short summary, the required invitation, and primary references.

View File

@@ -15,6 +15,7 @@ Compile tutorial code on the host, but run every command that loads, attaches, o
- Treat the tutorial directory as read-only in the guest by default. Use `--rwdir` only for a path that must return guest-generated files to the host.
- Use bounded commands. The wrapper applies a host-side timeout; add a shorter guest-side `timeout` to long-running loaders when useful.
- Report a KVM run as a functional smoke test, not as benchmark-quality evidence.
- Keep this infrastructure private in public prose. A tutorial or PR description may state the architecture, kernel version and commit, configuration, commands, and captured output, but must not name `bpf-benchmark`, local paths, VM instances, mount routes, caches, or agent-state locations.
## Default locations
@@ -82,7 +83,7 @@ The wrapper boots the built benchmark `bzImage` with KVM, verifies the guest rel
Use `--network user` only when the test needs an external/userspace-backed interface. Use `--network loop` for a guest-local loop network. Prefer `BPF_PROG_TEST_RUN` or deterministic guest-local traffic when either is sufficient.
Read [references/tutorial-patterns.md](references/tutorial-patterns.md) for tracer, network, writable-output, and failure-diagnosis patterns.
Read [the tutorial test patterns](https://github.com/eunomia-bpf/bpf-developer-tutorial/blob/main/.agents/skills/test-bpf-tutorial-kvm/references/tutorial-patterns.md) for tracer, network, writable-output, and failure-diagnosis patterns.
### 5. Verify the behavior, not only the exit code

View File

@@ -13,8 +13,8 @@ Invoke `$bpf-tutorial-writing-style` before writing or reviewing README prose.
Read the implementation, tests, Makefile, captured output, and primary upstream references. Read these bilingual precedents in full:
- `src/47-cuda-events/README.md` and `README.zh.md` for end-to-end depth;
- `src/49-hid/README.md` and `README.zh.md` for an approachable teaching voice;
- <https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/47-cuda-events> for end-to-end depth;
- <https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/49-hid> for an approachable teaching voice;
- at most one closer completed lesson when it contributes a subsystem-specific pattern.
Write down only two private working sentences: who the reader is, and what they should be able to explain or run at the end. Do not add this planning material to the repository.
@@ -35,7 +35,9 @@ Follow the selected precedent's component rhythm. Introduce one component, prese
Explain advanced eBPF behavior, not ordinary C syntax. Keep build, run, expected output, requirements, cleanup, limitations, summary, and primary references. State once what the evidence proves and once where the example stops.
Use canonical absolute URLs when linking to another tutorial. Never publish cross-tutorial links such as `../20-tc/README.md` or `../20-tc/README.zh.md`. Relative links are allowed only for files that belong to the current lesson, such as its `Makefile`, source files, and tests.
Use canonical GitHub absolute URLs for every Markdown link, including links to the current lesson's source, `Makefile`, and tests. Never publish a `./` or `../` target or a link to another host. If no stable GitHub URL exists, omit the link. This repository rule overrides link examples in older lessons and the advanced guideline's website call to action.
Do not publish local infrastructure details. Keep workspace paths, shared lab repository names, VM names, host-to-guest copy steps, cache locations, and agent trace paths out of the README and PR description. Public reproducibility text may state the architecture, kernel version and commit, required configuration, commands, and captured output.
## 4. Write Chinese from the same facts
@@ -61,7 +63,7 @@ Never delete, truncate, overwrite, or clean real conversation history, agent tra
## 6. Validate and publish
Synchronize and check every complete-source block in the English and Chinese pair:
Check every complete-source block in the English and Chinese pair:
```bash
python3 .agents/skills/write-bpf-production-tutorial/scripts/sync-source-blocks.py \
@@ -73,6 +75,6 @@ python3 .agents/skills/write-bpf-production-tutorial/scripts/sync-source-blocks.
--check
```
Repeat `--expected-source` for each complete shared header. Use `--write` only to restore source blocks from their real files; never hand-edit generated payloads.
Repeat `--expected-source` for each complete shared header. Use ordinary Markdown fenced code blocks without HTML markers. Copy complete source from the real file, then run the checker; it is deliberately read-only.
Run `git diff --check`, the lesson build, the relevant host tests, the KVM runtime test, and repository documentation checks. Inspect the final diff for unintended files. Commit and push the repository-local Skills and tutorial changes only after these checks pass. Do not merge unless the user asks.

View 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__":

View 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()