159 lines
7.7 KiB
Python
159 lines
7.7 KiB
Python
"""Check shipped skill links, current library signatures and candidate projects."""
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SKILL = ROOT / "skills"
|
|
COMPILER = None
|
|
|
|
|
|
def check_tutorial_sources(skill):
|
|
"""Keep tutorial excerpts tied to the complete projects checked by Rust."""
|
|
projects = [
|
|
path.resolve()
|
|
for path in [skill / "assets/template", *(skill / "assets/examples").iterdir()]
|
|
]
|
|
for document in sorted((skill / "references").glob("*.md")):
|
|
content = document.read_text()
|
|
blocks = list(re.finditer(
|
|
r"^```dever[ \t]*\n(.*?)^```[ \t]*$", content, re.MULTILINE | re.DOTALL
|
|
))
|
|
if len(blocks) != len(re.findall(r"^```dever\b", content, re.MULTILINE)):
|
|
raise ValueError(f"{document.name}: unclosed or unsupported dever fence")
|
|
for block in blocks:
|
|
binding = re.search(
|
|
r"源码:\[[^\]\n]+\]\(([^)\n]+)\)\n\n\Z", content[:block.start()]
|
|
)
|
|
if binding is None:
|
|
raise ValueError(f"{document.name}: dever block needs a source link")
|
|
source = (document.parent / binding.group(1)).resolve()
|
|
owners = [root for root in projects if source.is_relative_to(root)]
|
|
if len(owners) != 1 or not source.name.endswith(".dever"):
|
|
raise ValueError(f"{document.name}: source must belong to a checked skill project")
|
|
relative = source.relative_to(owners[0])
|
|
if relative.parts[0] not in ("module", "test") or not source.is_file():
|
|
raise ValueError(f"{document.name}: source must be a module or test file")
|
|
excerpt = block.group(1)
|
|
code = source.read_text()
|
|
if not excerpt.strip() or ("\n" + code).count("\n" + excerpt) != 1:
|
|
raise ValueError(f"{document.name}: excerpt differs from its source or is ambiguous")
|
|
|
|
|
|
class SkillExamples(unittest.TestCase):
|
|
def test_all_shipped_projects_pass_the_candidate_checker(self):
|
|
if COMPILER is None:
|
|
self.skipTest("pass --compiler <candidate-dever> to check shipped projects")
|
|
projects = [SKILL / "assets/template", *sorted((SKILL / "assets/examples").iterdir())]
|
|
for project in projects:
|
|
with self.subTest(project=project.name):
|
|
result = subprocess.run(
|
|
[str(COMPILER), "check", str(project)],
|
|
capture_output=True, text=True, timeout=30,
|
|
)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
|
|
def test_project_templates_do_not_pin_the_compiler(self):
|
|
for path in (SKILL / "assets").rglob("setting.json"):
|
|
with self.subTest(path=path):
|
|
settings = json.loads(path.read_text())
|
|
self.assertNotIn("version", settings.get("dever", {}))
|
|
|
|
def test_relocated_skill_links_and_release_file_limits(self):
|
|
with tempfile.TemporaryDirectory(prefix="dever-skill-") as directory:
|
|
installed = Path(directory) / "dever-language"
|
|
shutil.copytree(SKILL, installed)
|
|
files = [path for path in installed.rglob("*") if path.is_file()]
|
|
self.assertLessEqual(len(files), 256)
|
|
self.assertLessEqual(sum(path.stat().st_size for path in files), 8 * 1024 * 1024)
|
|
self.assertTrue((installed / "SKILL.md").is_file())
|
|
for path in files:
|
|
self.assertFalse(path.is_symlink())
|
|
self.assertLessEqual(path.stat().st_size, 1024 * 1024)
|
|
if path.suffix != ".md":
|
|
continue
|
|
for target in re.findall(r"\]\(([^)]+)\)", path.read_text()):
|
|
if "://" in target or target.startswith("#"):
|
|
continue
|
|
resolved = (path.parent / target.split("#", 1)[0]).resolve()
|
|
self.assertTrue(resolved.is_relative_to(installed), (path, target))
|
|
self.assertTrue(resolved.exists(), (path, target))
|
|
|
|
def test_tutorial_code_matches_relocated_checked_sources(self):
|
|
with tempfile.TemporaryDirectory(prefix="dever-tutorial-") as directory:
|
|
installed = Path(directory) / "dever-language"
|
|
shutil.copytree(SKILL, installed)
|
|
check_tutorial_sources(installed)
|
|
|
|
def test_tutorial_source_drift_and_unbound_code_are_rejected(self):
|
|
with tempfile.TemporaryDirectory(prefix="dever-tutorial-drift-") as directory:
|
|
installed = Path(directory) / "dever-language"
|
|
shutil.copytree(SKILL, installed)
|
|
document = installed / "references/language.md"
|
|
original = document.read_text()
|
|
source = installed / "assets/examples/collections/test/content/article/clauses.dever"
|
|
original_source = source.read_text()
|
|
mutations = {
|
|
"unbound": original.replace("源码:", "完整示例:", 1),
|
|
"changed_code": original.replace('"fail"', '"failed"', 1),
|
|
"unclosed": original + "\n```dever\nunclosed() () {}\n",
|
|
"outside_project": original.replace(
|
|
"../assets/template/module/hello/greeting/app.dever", "../SKILL.md", 1
|
|
),
|
|
}
|
|
for name, changed in mutations.items():
|
|
with self.subTest(name=name):
|
|
document.write_text(changed)
|
|
with self.assertRaises(ValueError):
|
|
check_tutorial_sources(installed)
|
|
document.write_text(original)
|
|
source.write_text(original_source.replace('"fail"', '"failed"'))
|
|
with self.assertRaisesRegex(ValueError, "excerpt differs"):
|
|
check_tutorial_sources(installed)
|
|
source.write_text(original_source + "\n" + original_source)
|
|
with self.assertRaisesRegex(ValueError, "ambiguous"):
|
|
check_tutorial_sources(installed)
|
|
|
|
def test_signature_index_matches_public_library_declarations(self):
|
|
project = ROOT / "rust/src/compiler/project/mod.rs"
|
|
libraries = {}
|
|
for package, relative in re.findall(r'\(\s*"([^"]+)",\s*"lib/[^"]+",\s*include_str!\("([^"]+)"\)', project.read_text()):
|
|
source = (project.parent / relative).read_text()
|
|
libraries[package] = libraries.get(package, "") + "\n" + source
|
|
index = (SKILL / "references/signatures.md").read_text()
|
|
blocks = re.findall(r"```signatures\n(.*?)```", index, re.DOTALL)
|
|
self.assertTrue(blocks, "the shipped index must contain concrete signatures")
|
|
seen = set()
|
|
for signature in "\n".join(blocks).splitlines():
|
|
if not signature:
|
|
continue
|
|
qualified = signature.split("(", 1)[0]
|
|
package, function = qualified.rsplit(".", 1)
|
|
self.assertIn(package, libraries, "signature package must be embedded by this candidate")
|
|
self.assertNotIn(qualified, seen)
|
|
seen.add(qualified)
|
|
declaration = re.search(
|
|
rf"^public ({re.escape(function)}\s*\([^{{]*?)\s*\{{",
|
|
libraries[package],
|
|
re.MULTILINE,
|
|
)
|
|
self.assertIsNotNone(declaration, qualified)
|
|
expected = package + "." + declaration.group(1)
|
|
self.assertEqual(re.sub(r"\s", "", signature), re.sub(r"\s", "", expected), qualified)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(add_help=False)
|
|
parser.add_argument("--compiler", type=Path)
|
|
options, remaining = parser.parse_known_args()
|
|
COMPILER = options.compiler.resolve() if options.compiler else None
|
|
unittest.main(argv=[sys.argv[0], *remaining])
|