From 4ae05617f21fe8783050c6ffb4c8f171e5b2137c Mon Sep 17 00:00:00 2001 From: Makiras <22570332+Makiras@users.noreply.github.com> Date: Sat, 24 Oct 2026 22:52:43 +0800 Subject: [PATCH 1/2] ci: sync validated documentation to GitHub Wiki --- .github/workflows/docs.yml | 119 +++++++++++++++++++++++ CONTRIBUTING.md | 35 +++++++ scripts/docs.py | 152 ++++++++++++++++++++++++++++++ scripts/sync_wiki.py | 82 ++++++++++++++++ scripts/tests/test_sync_wiki.py | 102 ++++++++++++++++++++ scripts/tests/test_wiki_export.py | 68 +++++++++++++ 6 files changed, 558 insertions(+) create mode 100644 .github/workflows/docs.yml create mode 100644 scripts/docs.py create mode 100644 scripts/sync_wiki.py create mode 100644 scripts/tests/test_sync_wiki.py create mode 100644 scripts/tests/test_wiki_export.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..f8a3d9d --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,119 @@ +name: Documentation + +on: + pull_request: + branches: [main] + paths: + - "docs/**" + - "README*" + - "CONTRIBUTING.md" + - "mkdocs.yml" + - "pyproject.toml" + - "scripts/docs.py" + - "scripts/sync_wiki.py" + - "scripts/tests/**" + - ".github/workflows/docs.yml" + push: + branches: [main, "docs/**"] + paths: + - "docs/**" + - "README*" + - "CONTRIBUTING.md" + - "mkdocs.yml" + - "pyproject.toml" + - "scripts/docs.py" + - "scripts/sync_wiki.py" + - "scripts/tests/**" + - ".github/workflows/docs.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: documentation-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + documentation: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: pyproject.toml + - name: Install documentation tools + run: python -m pip install 'mkdocs>=1.6,<2' + - name: Test documentation tooling + run: python -m unittest discover -s scripts/tests -v + - name: Check documentation links + run: python scripts/docs.py check + - name: Build documentation site + run: python -m mkdocs build --strict --site-dir build/docs-site + - name: Export Wiki pages + run: python scripts/docs.py wiki --output build/wiki-export + - name: Upload documentation preview + uses: actions/upload-artifact@v4 + with: + name: toffee-documentation + if-no-files-found: error + path: | + build/docs-site + build/wiki-export + + sync-wiki: + name: Sync GitHub Wiki + needs: [documentation] + if: >- + github.repository == 'XS-MLVP/toffee' && + github.ref == 'refs/heads/main' && + (github.event_name == 'push' || github.event_name == 'workflow_dispatch') + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + concurrency: + group: wiki-sync + cancel-in-progress: false + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - uses: actions/download-artifact@v4 + with: + name: toffee-documentation + path: build + # GitHub requires an initial page created through the Wiki web UI. + # Specifying ref avoids querying the REST API for the separate Wiki repo. + - name: Checkout initialized Wiki + uses: actions/checkout@v4 + with: + repository: ${{ github.repository }}.wiki + ref: master + path: wiki-checkout + - name: Sync generated pages and push changes + env: + SOURCE_SHA: ${{ github.sha }} + run: | + set -euo pipefail + git fetch --no-tags --depth=1 origin main + if ! git diff --quiet "$SOURCE_SHA" FETCH_HEAD -- docs/ "README*" CONTRIBUTING.md mkdocs.yml pyproject.toml scripts/docs.py scripts/sync_wiki.py scripts/tests/ .github/workflows/docs.yml; then + echo "Documentation has advanced; skipping this outdated export." + exit 0 + fi + python scripts/sync_wiki.py --export build/wiki-export --checkout wiki-checkout + git -C wiki-checkout add --all + if git -C wiki-checkout diff --cached --quiet; then + echo "Wiki is already up to date." + exit 0 + fi + git -C wiki-checkout config user.name 'github-actions[bot]' + git -C wiki-checkout config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git -C wiki-checkout commit -m "Sync documentation from $SOURCE_SHA" + git -C wiki-checkout push origin HEAD:master diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04afd11..79ef788 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,6 +35,41 @@ Toffee 负责 Python API、仿真调度、验证组件和报告。 运行 `python3 -m pytest -q tests/integration/test_documentation.py` 和 `mkdocs build --strict`。 新增可运行示例时还要实际执行;已有覆盖模型片段由文档测试校验。 +## Wiki 自动同步 + +Wiki 的内容来自 `main` 的 `docs/`,导航来自 `mkdocs.yml`。只需编辑这些源文件。 +**Documentation** 工作流校验链接、构建网站,导出页面及侧边栏,并上传预览产物。 +文档修改进入 `main` 并通过检查后,会同步到 `toffee.wiki.git`;PR 和其他分支只生成预览。 +Legacy `master` 不参与同步。Wiki 工作流独立于软件包发布检查,避免扩大其写权限。 + +首次启用时,须在仓库设置中开启 Wiki,并在 GitHub Wiki 网页创建一次首页。 +同步使用自带的 `GITHUB_TOKEN`,只有发布任务拥有 `contents: write`,无需额外 secret。 +失败后可在 Actions 中选择 `main` 手动运行 **Documentation**。新文档已提交时,旧导出会跳过发布。 + +本地验证和导出不需要安装原生依赖: + +```bash +python3 -m pip install 'mkdocs>=1.6,<2' +python3 -m unittest discover -s scripts/tests -v +python3 scripts/docs.py check +python3 -m mkdocs build --strict +python3 scripts/docs.py wiki --output build/wiki-preview +``` + +导出目录须为空。页面、源码和图片链接转换为绝对 URL,代码示例保持原样。 +`manifest.json` 记录每个生成页面的 SHA-256。同步会更新生成页面,首次运行时替换初始化 +首页、侧边栏和页脚;其他手工内容会保留,同名冲突会报错。过期页面只有在仍符合旧清单时 +才被删除,存在手工改动时停止供检查。内容不变时不创建提交,推送不会强制覆盖历史。 +请将生成页面的修改提交到主仓库,避免下一次自动同步覆盖网页上的手工编辑。 + +本地检查同步差异时,先克隆已初始化的 Wiki,再执行: + +```bash +git clone https://github.com/XS-MLVP/toffee.wiki.git build/wiki-checkout +python3 scripts/sync_wiki.py --export build/wiki-preview --checkout build/wiki-checkout +git -C build/wiki-checkout diff +``` + ## 提交前检查 先准备 `uc-xcomm>=1.1,<2`,再安装开发依赖。开发跨仓库未发布修改时, diff --git a/scripts/docs.py b/scripts/docs.py new file mode 100644 index 0000000..68c609d --- /dev/null +++ b/scripts/docs.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Validate documentation links and export the main-branch manual to GitHub Wiki.""" +import argparse +import hashlib +from html.parser import HTMLParser +import json +from pathlib import Path +import re +from urllib.parse import quote, unquote, urlsplit + +import markdown +import yaml + +ROOT = Path(__file__).resolve().parents[1] +CONFIG = yaml.safe_load((ROOT / 'mkdocs.yml').read_text(encoding='utf-8')) +DOCS = ROOT / CONFIG.get('docs_dir', 'docs') +LINK = re.compile(r'(!?\[[^\]\n]*\]\()([^\s)]+)(\))') + + +class Links(HTMLParser): + def __init__(self, text): + super().__init__() + self.targets, self.ids = [], set() + self.feed(markdown.markdown(text, extensions=['toc', 'tables', 'fenced_code'])) + + def handle_starttag(self, tag, attrs): + attrs = dict(attrs) + if 'id' in attrs: + self.ids.add(attrs['id']) + key = 'src' if tag == 'img' else 'href' + if key in attrs: + self.targets.append(attrs[key]) + + +def nav_items(entries): + for entry in entries: + for title, value in entry.items(): + if isinstance(value, list): + yield from nav_items(value) + else: + yield title, value + + +def wiki_slug(page): + relative = page.relative_to(DOCS) + return 'Home' if relative == Path('index.md') else relative.with_suffix('').as_posix().replace('/', '-') + + +def wiki_url(slug): + return CONFIG['repo_url'].rstrip('/') + '/wiki/' + quote(slug) + + +def check(): + pages = sorted(DOCS.rglob('*.md')) + navigation = [Path(path) for _, path in nav_items(CONFIG['nav'])] + if len(navigation) != len(pages) or set(navigation) != {page.relative_to(DOCS) for page in pages}: + raise SystemExit('Navigation must include every documentation page exactly once') + parsed = {page.resolve(): Links(page.read_text(encoding='utf-8')) for page in pages} + errors = [] + for page, links in parsed.items(): + for target in links.targets: + url = urlsplit(target) + if url.scheme or url.netloc: + continue + destination = (page.parent / unquote(url.path)).resolve() if url.path else page + if not destination.is_relative_to(ROOT) or not destination.exists(): + errors.append(f'{page.relative_to(ROOT)}: missing or external path {target}') + elif url.fragment and destination in parsed and unquote(url.fragment) not in parsed[destination].ids: + errors.append(f'{page.relative_to(ROOT)}: missing anchor {target}') + if errors: + raise SystemExit('\n'.join(errors)) + print(f'Checked navigation and local links in {len(pages)} pages.') + return pages + + +def export_wiki(args): + pages = check() + output = Path(args.output).resolve() + if output.exists() and any(output.iterdir()): + raise SystemExit('Choose an empty output directory; Wiki export does not overwrite existing work.') + slugs = {page.resolve(): wiki_slug(page) for page in pages} + names = [*slugs.values(), '_Sidebar', '_Footer'] + if len({name.casefold() for name in names}) != len(names): + raise SystemExit('Wiki page names collide') + output.mkdir(parents=True, exist_ok=True) + repo = CONFIG['repo_url'].rstrip('/') + for page in pages: + def replace_link(match): + url = urlsplit(match[2]) + if url.scheme or url.netloc or not url.path: + return match[0] + target = (page.parent / unquote(url.path)).resolve() + if target in slugs: + destination = wiki_url(slugs[target]) + else: + route = '/raw/refs/heads/main/' if match[1].startswith('!') else '/blob/main/' + destination = repo + route + quote(target.relative_to(ROOT).as_posix()) + if url.query: + destination += '?' + url.query + if url.fragment: + destination += '#' + url.fragment + return match[1] + destination + match[3] + + chunks = re.split(r'(^```.*?^```[^\n]*$)', page.read_text(encoding='utf-8'), flags=re.M | re.S) + for index in range(0, len(chunks), 2): + chunks[index] = LINK.sub(replace_link, chunks[index]) + source = repo + '/blob/main/' + quote(page.relative_to(ROOT).as_posix()) + content = f'[源文档 / Source]({source})\n\n' + ''.join(chunks) + (output / (slugs[page.resolve()] + '.md')).write_text(content, encoding='utf-8') + + sidebar = [] + def add_navigation(entries, indent=''): + for entry in entries: + for title, value in entry.items(): + if isinstance(value, list): + sidebar.append(f'{indent}- **{title}**') + add_navigation(value, indent + ' ') + else: + sidebar.append(f'{indent}- [{title}]({wiki_url(slugs[(DOCS / value).resolve()])})') + add_navigation(CONFIG['nav']) + (output / '_Sidebar.md').write_text('\n'.join(sidebar) + '\n', encoding='utf-8') + (output / '_Footer.md').write_text( + f'内容在 [{CONFIG["site_name"]} 主仓库]({repo}/tree/main/docs)维护,由 CI 自动同步。\n', + encoding='utf-8', + ) + for page in output.glob('*.md'): + for target in Links(page.read_text(encoding='utf-8')).targets: + url = urlsplit(target) + if not (url.scheme or url.netloc) and url.path: + raise SystemExit(f'Relative exported Wiki link: {page.name}: {target}') + if target.startswith(wiki_url('')) and not (output / (unquote(url.path.rsplit('/', 1)[-1]) + '.md')).exists(): + raise SystemExit(f'Broken exported Wiki link: {page.name}: {target}') + manifest = {page.name: hashlib.sha256(page.read_bytes()).hexdigest() for page in sorted(output.glob('*.md'))} + (output / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n', encoding='utf-8') + print(f'Exported {len(manifest)} Wiki pages to {output}; no remote repository was modified.') + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + actions = parser.add_subparsers(dest='action', required=True) + actions.add_parser('check') + wiki = actions.add_parser('wiki') + wiki.add_argument('--output', required=True) + args = parser.parse_args() + if args.action == 'check': + check() + else: + export_wiki(args) + + +if __name__ == '__main__': + main() diff --git a/scripts/sync_wiki.py b/scripts/sync_wiki.py new file mode 100644 index 0000000..aa29fb2 --- /dev/null +++ b/scripts/sync_wiki.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Apply a verified Wiki export to a checkout, preserving unmanaged files.""" +import argparse +import hashlib +import json +from pathlib import Path +import re +import shutil + + +def digest(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def read_manifest(directory): + path = directory / 'manifest.json' + if path.is_symlink(): + raise ValueError('Manifest must not be a symlink') + data = json.loads(path.read_text(encoding='utf-8')) + if not isinstance(data, dict): + raise ValueError('Manifest must be an object') + folded = set() + for name, checksum in data.items(): + if not re.fullmatch(r'[\w-]+\.md', name) or name.casefold() in folded: + raise ValueError(f'Unsafe or colliding Wiki filename: {name}') + folded.add(name.casefold()) + if not isinstance(checksum, str) or not re.fullmatch(r'[0-9a-f]{64}', checksum): + raise ValueError(f'Invalid SHA-256 for {name}') + return data + + +def sync(export, checkout): + export, checkout = Path(export).resolve(), Path(checkout).resolve() + if not export.is_dir() or not checkout.is_dir(): + raise ValueError('Export and Wiki checkout must already exist') + if export == checkout or export in checkout.parents or checkout in export.parents: + raise ValueError('Export and checkout must be separate directories') + current = read_manifest(export) + if 'Home.md' not in current: + raise ValueError('Refusing an export without Home.md') + previous = read_manifest(checkout) if (checkout / 'manifest.json').exists() or (checkout / 'manifest.json').is_symlink() else {} + + # Validate everything before changing the checkout, including deleted pages. + for name in current.keys() | previous.keys(): + target = checkout / name + if target.is_symlink() or (target.exists() and not target.is_file()): + raise ValueError(f'Unsafe Wiki destination: {name}') + for name, checksum in current.items(): + source = export / name + if source.is_symlink() or not source.is_file() or digest(source) != checksum: + raise ValueError(f'Export checksum mismatch: {name}') + target = checkout / name + # The first export can replace GitHub's initial Home/navigation pages. + if name not in previous and name not in {'Home.md', '_Sidebar.md', '_Footer.md'} and target.exists() and digest(target) != checksum: + raise ValueError(f'Unmanaged Wiki page conflicts with export: {name}') + removed = previous.keys() - current.keys() + for name in removed: + target = checkout / name + if target.exists() and digest(target) != previous[name]: + raise ValueError(f'Obsolete generated page has manual edits: {name}') + + for name in removed: + (checkout / name).unlink(missing_ok=True) + for name in current: + shutil.copyfile(export / name, checkout / name) + shutil.copyfile(export / 'manifest.json', checkout / 'manifest.json') + print(f'Synced {len(current)} generated pages; removed {len(removed)} obsolete pages.') + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--export', required=True, type=Path) + parser.add_argument('--checkout', required=True, type=Path) + args = parser.parse_args() + try: + sync(args.export, args.checkout) + except (ValueError, OSError) as error: + raise SystemExit(str(error)) from error + + +if __name__ == '__main__': + main() diff --git a/scripts/tests/test_sync_wiki.py b/scripts/tests/test_sync_wiki.py new file mode 100644 index 0000000..8842b56 --- /dev/null +++ b/scripts/tests/test_sync_wiki.py @@ -0,0 +1,102 @@ +"""Verify Wiki synchronization cannot remove or overwrite unrelated content.""" +import contextlib +import hashlib +import importlib.util +import io +import json +from pathlib import Path +import tempfile +import unittest + + +SPEC = importlib.util.spec_from_file_location( + 'sync_wiki', Path(__file__).resolve().parents[2] / 'scripts/sync_wiki.py' +) +wiki = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(wiki) + + +class SyncWiki(unittest.TestCase): + def setUp(self): + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + self.root = Path(directory.name) + self.export = self.root / 'export' + self.checkout = self.root / 'checkout' + self.export.mkdir() + self.checkout.mkdir() + self.enterContext(contextlib.redirect_stdout(io.StringIO())) + + def write_export(self, directory, pages): + manifest = {} + for name, text in pages.items(): + (directory / name).write_text(text, encoding='utf-8') + manifest[name] = hashlib.sha256(text.encode()).hexdigest() + (directory / 'manifest.json').write_text(json.dumps(manifest), encoding='utf-8') + + def test_sync_deletes_only_obsolete_generated_pages_and_is_idempotent(self): + self.write_export(self.checkout, {'Home.md': 'old', 'old-page.md': 'obsolete'}) + (self.checkout / 'Manual.md').write_text('hand maintained') + self.write_export(self.export, {'Home.md': 'new', 'new-page.md': 'replacement'}) + wiki.sync(self.export, self.checkout) + snapshot = {p.name: p.read_bytes() for p in self.checkout.iterdir()} + self.assertNotIn('old-page.md', snapshot) + self.assertEqual(snapshot['Manual.md'], b'hand maintained') + self.assertEqual(snapshot['Home.md'], b'new') + wiki.sync(self.export, self.checkout) + self.assertEqual(snapshot, {p.name: p.read_bytes() for p in self.checkout.iterdir()}) + + def test_initial_sync_replaces_bootstrap_home(self): + (self.checkout / 'Home.md').write_text('Welcome') + self.write_export(self.export, {'Home.md': 'Generated'}) + wiki.sync(self.export, self.checkout) + self.assertEqual((self.checkout / 'Home.md').read_text(), 'Generated') + + def test_bad_checksum_causes_no_partial_update(self): + self.write_export(self.export, {'Home.md': 'new', 'guide.md': 'guide'}) + (self.export / 'guide.md').write_text('corrupted') + (self.checkout / 'Home.md').write_text('old') + with self.assertRaisesRegex(ValueError, 'checksum mismatch'): + wiki.sync(self.export, self.checkout) + self.assertEqual((self.checkout / 'Home.md').read_text(), 'old') + + def test_manifest_cannot_escape_checkout(self): + self.write_export(self.export, {'Home.md': 'new'}) + for name in ('../outside.md', '/outside.md', '.git/config'): + with self.subTest(name=name): + (self.checkout / 'manifest.json').write_text(json.dumps({name: '0' * 64})) + with self.assertRaisesRegex(ValueError, 'Unsafe'): + wiki.sync(self.export, self.checkout) + + def test_symlink_cannot_overwrite_external_file(self): + self.write_export(self.export, {'Home.md': 'new'}) + outside = self.root / 'outside' + outside.write_text('keep') + (self.checkout / 'Home.md').symlink_to(outside) + with self.assertRaisesRegex(ValueError, 'Unsafe Wiki destination'): + wiki.sync(self.export, self.checkout) + self.assertEqual(outside.read_text(), 'keep') + + def test_empty_export_cannot_delete_wiki(self): + self.write_export(self.export, {}) + with self.assertRaisesRegex(ValueError, 'without Home.md'): + wiki.sync(self.export, self.checkout) + + def test_manual_edits_to_obsolete_page_require_resolution(self): + self.write_export(self.checkout, {'Home.md': 'old', 'old.md': 'original'}) + (self.checkout / 'old.md').write_text('manual edits') + self.write_export(self.export, {'Home.md': 'new'}) + with self.assertRaisesRegex(ValueError, 'manual edits'): + wiki.sync(self.export, self.checkout) + self.assertEqual((self.checkout / 'Home.md').read_text(), 'old') + + def test_unmanaged_page_conflict_requires_resolution(self): + self.write_export(self.export, {'Home.md': 'new', 'guide.md': 'generated'}) + (self.checkout / 'guide.md').write_text('manual') + with self.assertRaisesRegex(ValueError, 'Unmanaged Wiki page conflicts'): + wiki.sync(self.export, self.checkout) + self.assertFalse((self.checkout / 'Home.md').exists()) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/tests/test_wiki_export.py b/scripts/tests/test_wiki_export.py new file mode 100644 index 0000000..73971b6 --- /dev/null +++ b/scripts/tests/test_wiki_export.py @@ -0,0 +1,68 @@ +"""Wiki links resolve to generated pages or main-branch source files.""" +import contextlib +import importlib.util +import io +import json +from pathlib import Path +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import patch + + +SPEC = importlib.util.spec_from_file_location( + 'docs', Path(__file__).resolve().parents[2] / 'scripts/docs.py' +) +docs = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(docs) + + +class WikiExport(unittest.TestCase): + def setUp(self): + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + self.root = Path(directory.name).resolve() + self.repo = 'https://github.com/XS-MLVP/toffee' + config = {'site_name': 'Toffee', 'repo_url': self.repo, + 'nav': [{'Home': 'index.md'}, {'Guide': 'guide.md'}]} + self.enterContext(patch.multiple(docs, ROOT=self.root, DOCS=self.root / 'docs', CONFIG=config)) + self.enterContext(contextlib.redirect_stdout(io.StringIO())) + (self.root / 'docs').mkdir() + (self.root / 'LICENSE').write_text('License') + (self.root / 'test image.png').write_bytes(b'image') + (self.root / 'docs/index.md').write_text( + '# Home\n\n[Guide](guide.md#install)\n\n[License](../LICENSE)\n\n' + '![Image](../test%20image.png)\n\n```text\n[Literal](missing.md)\n```\n' + ) + (self.root / 'docs/guide.md').write_text('# Guide\n\n## Install\n\n[Home](index.md)\n') + + def test_export_preserves_code_and_rewrites_links_images_and_navigation(self): + output = self.root / 'export' + docs.export_wiki(SimpleNamespace(output=output)) + home = (output / 'Home.md').read_text() + self.assertIn(f'[Guide]({self.repo}/wiki/guide#install)', home) + self.assertIn(f'[License]({self.repo}/blob/main/LICENSE)', home) + self.assertIn(f'![Image]({self.repo}/raw/refs/heads/main/test%20image.png)', home) + self.assertIn('```text\n[Literal](missing.md)\n```', home) + self.assertIn(f'[Guide]({self.repo}/wiki/guide)', (output / '_Sidebar.md').read_text()) + self.assertEqual(len(json.loads((output / 'manifest.json').read_text())), 4) + + def test_missing_links_fail_before_export(self): + (self.root / 'docs/guide.md').write_text('# Guide\n\n[Missing](missing.md)\n') + with self.assertRaisesRegex(SystemExit, 'missing or external path'): + docs.export_wiki(SimpleNamespace(output=self.root / 'export')) + self.assertFalse((self.root / 'export').exists()) + + def test_missing_anchor_is_rejected(self): + (self.root / 'docs/guide.md').write_text('# Guide\n\n[Missing](index.md#missing)\n') + with self.assertRaisesRegex(SystemExit, 'missing anchor'): + docs.check() + + def test_unconverted_reference_link_fails_instead_of_publishing_broken_page(self): + (self.root / 'docs/guide.md').write_text('# Guide\n\n## Install\n\n[Home][home]\n\n[home]: index.md\n') + with self.assertRaisesRegex(SystemExit, 'Relative exported Wiki link'): + docs.export_wiki(SimpleNamespace(output=self.root / 'export')) + + +if __name__ == '__main__': + unittest.main() From 02346aead8186bd4ebc89b0282dbd75c107866b2 Mon Sep 17 00:00:00 2001 From: Makiras <22570332+Makiras@users.noreply.github.com> Date: Fri, 30 Oct 2026 16:40:39 +0800 Subject: [PATCH 2/2] docs: add complete English manual and bilingual Wiki export Move the 48-page Chinese manual under docs/zh and provide matching English pages, navigation, README links, and Wiki language switching. Validate translation coverage, shared examples, and links in CI. Keep UNCERTAIN outcome documentation in its dependent feature PR. --- .github/workflows/ci.yml | 2 +- .github/workflows/docs.yml | 9 +- CONTRIBUTING.md | 27 +- MANIFEST.in | 2 +- README.en.md | 100 ++++ README.md | 22 +- docs/en/examples.md | 113 +++++ docs/en/explanation/coverage-results.md | 54 ++ docs/en/explanation/execution-model.md | 69 +++ docs/en/explanation/index.md | 10 + docs/en/getting-started/agent-test.md | 82 +++ docs/en/getting-started/clock-and-input.md | 86 ++++ docs/en/getting-started/coverage.md | 99 ++++ docs/en/getting-started/driver.md | 75 +++ docs/en/getting-started/failures.md | 104 ++++ docs/en/getting-started/first-test.md | 69 +++ docs/en/getting-started/index.md | 56 ++ docs/en/getting-started/installation.md | 54 ++ docs/en/getting-started/monitor.md | 98 ++++ docs/en/getting-started/more-cases.md | 61 +++ docs/en/getting-started/native-and-next.md | 120 +++++ docs/en/getting-started/out-of-order.md | 77 +++ docs/en/getting-started/pipeline.md | 133 +++++ docs/en/getting-started/reference-model.md | 103 ++++ docs/en/guides/agents-and-reference-models.md | 170 +++++++ docs/en/guides/asyncio-and-pytest.md | 127 +++++ docs/en/guides/compiled-triggers.md | 95 ++++ docs/en/guides/coverage-classes.md | 116 +++++ docs/en/guides/coverage-patterns.md | 106 ++++ docs/en/guides/coverage-transactions.md | 90 ++++ docs/en/guides/coverage.md | 479 ++++++++++++++++++ docs/en/guides/data.md | 115 +++++ docs/en/guides/drivers.md | 119 +++++ docs/en/guides/execution-and-backend.md | 84 +++ docs/en/guides/expressions-and-resources.md | 59 +++ docs/en/guides/functional-points.md | 129 +++++ docs/en/guides/index.md | 45 ++ docs/en/guides/monitors.md | 71 +++ docs/en/guides/pipeline-examples.md | 299 +++++++++++ docs/en/guides/protocols.md | 60 +++ docs/en/guides/runner.md | 107 ++++ docs/en/guides/scoreboard.md | 149 ++++++ docs/en/guides/subscriptions.md | 54 ++ docs/en/guides/toffee-v2-migration.md | 459 +++++++++++++++++ docs/en/guides/triggers.md | 107 ++++ docs/en/guides/verification-flow.md | 97 ++++ docs/en/guides/versioning-and-releases.md | 132 +++++ docs/en/index.md | 30 ++ docs/en/reference/api.md | 168 ++++++ docs/en/reference/execution.md | 66 +++ docs/en/reference/feature-map.md | 49 ++ docs/en/reference/index.md | 16 + docs/en/reference/runner.md | 76 +++ .../en/reference/semantics-and-limitations.md | 91 ++++ docs/{ => zh}/examples.md | 0 docs/{ => zh}/explanation/coverage-results.md | 0 docs/{ => zh}/explanation/execution-model.md | 0 docs/{ => zh}/explanation/index.md | 0 docs/{ => zh}/getting-started/agent-test.md | 0 .../getting-started/clock-and-input.md | 0 docs/{ => zh}/getting-started/coverage.md | 0 docs/{ => zh}/getting-started/driver.md | 0 docs/{ => zh}/getting-started/failures.md | 0 docs/{ => zh}/getting-started/first-test.md | 0 docs/{ => zh}/getting-started/index.md | 0 docs/{ => zh}/getting-started/installation.md | 0 docs/{ => zh}/getting-started/monitor.md | 0 docs/{ => zh}/getting-started/more-cases.md | 0 .../getting-started/native-and-next.md | 0 docs/{ => zh}/getting-started/out-of-order.md | 0 docs/{ => zh}/getting-started/pipeline.md | 0 .../getting-started/reference-model.md | 0 .../guides/agents-and-reference-models.md | 0 docs/{ => zh}/guides/asyncio-and-pytest.md | 0 docs/{ => zh}/guides/compiled-triggers.md | 0 docs/{ => zh}/guides/coverage-classes.md | 0 docs/{ => zh}/guides/coverage-patterns.md | 0 docs/{ => zh}/guides/coverage-transactions.md | 0 docs/{ => zh}/guides/coverage.md | 0 docs/{ => zh}/guides/data.md | 0 docs/{ => zh}/guides/drivers.md | 0 docs/{ => zh}/guides/execution-and-backend.md | 0 .../guides/expressions-and-resources.md | 0 docs/{ => zh}/guides/functional-points.md | 0 docs/{ => zh}/guides/index.md | 0 docs/{ => zh}/guides/monitors.md | 0 docs/{ => zh}/guides/pipeline-examples.md | 0 docs/{ => zh}/guides/protocols.md | 0 docs/{ => zh}/guides/runner.md | 0 docs/{ => zh}/guides/scoreboard.md | 0 docs/{ => zh}/guides/subscriptions.md | 0 docs/{ => zh}/guides/toffee-v2-migration.md | 6 +- docs/{ => zh}/guides/triggers.md | 0 docs/{ => zh}/guides/verification-flow.md | 0 .../guides/versioning-and-releases.md | 0 docs/{ => zh}/index.md | 0 docs/{ => zh}/reference/api.md | 0 docs/{ => zh}/reference/execution.md | 0 docs/{ => zh}/reference/feature-map.md | 0 docs/{ => zh}/reference/index.md | 0 docs/{ => zh}/reference/runner.md | 0 .../reference/semantics-and-limitations.md | 0 examples/README.md | 4 +- examples/getting_started/README.md | 2 +- examples/getting_started/test_accumulator.py | 2 +- mkdocs.yml | 196 ++++--- pyproject.toml | 5 +- requirements-docs.txt | 4 + scripts/docs.py | 73 ++- scripts/tests/test_wiki_export.py | 54 +- tests/README.md | 2 +- tests/integration/test_documentation.py | 7 +- tests/integration/test_quickstart_tutorial.py | 5 +- 113 files changed, 5623 insertions(+), 127 deletions(-) create mode 100644 README.en.md create mode 100644 docs/en/examples.md create mode 100644 docs/en/explanation/coverage-results.md create mode 100644 docs/en/explanation/execution-model.md create mode 100644 docs/en/explanation/index.md create mode 100644 docs/en/getting-started/agent-test.md create mode 100644 docs/en/getting-started/clock-and-input.md create mode 100644 docs/en/getting-started/coverage.md create mode 100644 docs/en/getting-started/driver.md create mode 100644 docs/en/getting-started/failures.md create mode 100644 docs/en/getting-started/first-test.md create mode 100644 docs/en/getting-started/index.md create mode 100644 docs/en/getting-started/installation.md create mode 100644 docs/en/getting-started/monitor.md create mode 100644 docs/en/getting-started/more-cases.md create mode 100644 docs/en/getting-started/native-and-next.md create mode 100644 docs/en/getting-started/out-of-order.md create mode 100644 docs/en/getting-started/pipeline.md create mode 100644 docs/en/getting-started/reference-model.md create mode 100644 docs/en/guides/agents-and-reference-models.md create mode 100644 docs/en/guides/asyncio-and-pytest.md create mode 100644 docs/en/guides/compiled-triggers.md create mode 100644 docs/en/guides/coverage-classes.md create mode 100644 docs/en/guides/coverage-patterns.md create mode 100644 docs/en/guides/coverage-transactions.md create mode 100644 docs/en/guides/coverage.md create mode 100644 docs/en/guides/data.md create mode 100644 docs/en/guides/drivers.md create mode 100644 docs/en/guides/execution-and-backend.md create mode 100644 docs/en/guides/expressions-and-resources.md create mode 100644 docs/en/guides/functional-points.md create mode 100644 docs/en/guides/index.md create mode 100644 docs/en/guides/monitors.md create mode 100644 docs/en/guides/pipeline-examples.md create mode 100644 docs/en/guides/protocols.md create mode 100644 docs/en/guides/runner.md create mode 100644 docs/en/guides/scoreboard.md create mode 100644 docs/en/guides/subscriptions.md create mode 100644 docs/en/guides/toffee-v2-migration.md create mode 100644 docs/en/guides/triggers.md create mode 100644 docs/en/guides/verification-flow.md create mode 100644 docs/en/guides/versioning-and-releases.md create mode 100644 docs/en/index.md create mode 100644 docs/en/reference/api.md create mode 100644 docs/en/reference/execution.md create mode 100644 docs/en/reference/feature-map.md create mode 100644 docs/en/reference/index.md create mode 100644 docs/en/reference/runner.md create mode 100644 docs/en/reference/semantics-and-limitations.md rename docs/{ => zh}/examples.md (100%) rename docs/{ => zh}/explanation/coverage-results.md (100%) rename docs/{ => zh}/explanation/execution-model.md (100%) rename docs/{ => zh}/explanation/index.md (100%) rename docs/{ => zh}/getting-started/agent-test.md (100%) rename docs/{ => zh}/getting-started/clock-and-input.md (100%) rename docs/{ => zh}/getting-started/coverage.md (100%) rename docs/{ => zh}/getting-started/driver.md (100%) rename docs/{ => zh}/getting-started/failures.md (100%) rename docs/{ => zh}/getting-started/first-test.md (100%) rename docs/{ => zh}/getting-started/index.md (100%) rename docs/{ => zh}/getting-started/installation.md (100%) rename docs/{ => zh}/getting-started/monitor.md (100%) rename docs/{ => zh}/getting-started/more-cases.md (100%) rename docs/{ => zh}/getting-started/native-and-next.md (100%) rename docs/{ => zh}/getting-started/out-of-order.md (100%) rename docs/{ => zh}/getting-started/pipeline.md (100%) rename docs/{ => zh}/getting-started/reference-model.md (100%) rename docs/{ => zh}/guides/agents-and-reference-models.md (100%) rename docs/{ => zh}/guides/asyncio-and-pytest.md (100%) rename docs/{ => zh}/guides/compiled-triggers.md (100%) rename docs/{ => zh}/guides/coverage-classes.md (100%) rename docs/{ => zh}/guides/coverage-patterns.md (100%) rename docs/{ => zh}/guides/coverage-transactions.md (100%) rename docs/{ => zh}/guides/coverage.md (100%) rename docs/{ => zh}/guides/data.md (100%) rename docs/{ => zh}/guides/drivers.md (100%) rename docs/{ => zh}/guides/execution-and-backend.md (100%) rename docs/{ => zh}/guides/expressions-and-resources.md (100%) rename docs/{ => zh}/guides/functional-points.md (100%) rename docs/{ => zh}/guides/index.md (100%) rename docs/{ => zh}/guides/monitors.md (100%) rename docs/{ => zh}/guides/pipeline-examples.md (100%) rename docs/{ => zh}/guides/protocols.md (100%) rename docs/{ => zh}/guides/runner.md (100%) rename docs/{ => zh}/guides/scoreboard.md (100%) rename docs/{ => zh}/guides/subscriptions.md (100%) rename docs/{ => zh}/guides/toffee-v2-migration.md (98%) rename docs/{ => zh}/guides/triggers.md (100%) rename docs/{ => zh}/guides/verification-flow.md (100%) rename docs/{ => zh}/guides/versioning-and-releases.md (100%) rename docs/{ => zh}/index.md (100%) rename docs/{ => zh}/reference/api.md (100%) rename docs/{ => zh}/reference/execution.md (100%) rename docs/{ => zh}/reference/feature-map.md (100%) rename docs/{ => zh}/reference/index.md (100%) rename docs/{ => zh}/reference/runner.md (100%) rename docs/{ => zh}/reference/semantics-and-limitations.md (100%) create mode 100644 requirements-docs.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c31fda..35b997e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,7 +122,7 @@ jobs: with: python-version: "3.12" cache: pip - - run: python -m pip install 'mkdocs>=1.6' + - run: python -m pip install -r requirements-docs.txt - run: mkdocs build --strict package: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f8a3d9d..d816c67 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,12 +2,12 @@ name: Documentation on: pull_request: - branches: [main] paths: - "docs/**" - "README*" - "CONTRIBUTING.md" - "mkdocs.yml" + - "requirements-docs.txt" - "pyproject.toml" - "scripts/docs.py" - "scripts/sync_wiki.py" @@ -20,6 +20,7 @@ on: - "README*" - "CONTRIBUTING.md" - "mkdocs.yml" + - "requirements-docs.txt" - "pyproject.toml" - "scripts/docs.py" - "scripts/sync_wiki.py" @@ -44,9 +45,9 @@ jobs: with: python-version: "3.12" cache: pip - cache-dependency-path: pyproject.toml + cache-dependency-path: requirements-docs.txt - name: Install documentation tools - run: python -m pip install 'mkdocs>=1.6,<2' + run: python -m pip install -r requirements-docs.txt - name: Test documentation tooling run: python -m unittest discover -s scripts/tests -v - name: Check documentation links @@ -103,7 +104,7 @@ jobs: run: | set -euo pipefail git fetch --no-tags --depth=1 origin main - if ! git diff --quiet "$SOURCE_SHA" FETCH_HEAD -- docs/ "README*" CONTRIBUTING.md mkdocs.yml pyproject.toml scripts/docs.py scripts/sync_wiki.py scripts/tests/ .github/workflows/docs.yml; then + if ! git diff --quiet "$SOURCE_SHA" FETCH_HEAD -- docs/ "README*" CONTRIBUTING.md mkdocs.yml requirements-docs.txt pyproject.toml scripts/docs.py scripts/sync_wiki.py scripts/tests/ .github/workflows/docs.yml; then echo "Documentation has advanced; skipping this outdated export." exit 0 fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 79ef788..0ff3f10 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,26 +18,35 @@ Toffee 负责 Python API、仿真调度、验证组件和报告。 ## 文档按读者目的组织 -文档采用 [Diátaxis](https://diataxis.fr/) 的四类划分: +中文源文档放在 `docs/zh/`,英文放在 `docs/en/`,相对路径一一对应。新增或修改 +公共能力时,同一个 PR 更新两种语言;共享的可执行代码块保持一致,避免示例漂移。 +导航使用语言无关路径,英文标题配置在 `mkdocs.yml` 的 `nav_translations` 中。 +站点默认中文,提供 English 切换;缺失翻译不回退到中文,检查会直接失败。 + +文档采用 [Diátaxis](https://diataxis.fr/) 的四类划分;下表以中文路径为例: | 类型 | 位置 | 写作要求 | | --- | --- | --- | -| 教程 | `docs/getting-started/` | 带读者完成可复现的练习,给出命令与预期结果;逐步引入概念,减少分支选择 | -| 操作指南 | `docs/guides/` | 开头说明目标和前提,围绕实际任务给步骤、验证方法和常见失败处理 | -| 参考 | `docs/reference/` | 准确列出签名、默认值、单位、返回值、异常与限制;与实现逐项核对 | -| 原理说明 | `docs/explanation/` | 解释原因、边界和取舍,通过具体场景建立概念之间的联系 | +| 教程 | `docs/zh/getting-started/` | 带读者完成可复现的练习,给出命令与预期结果;逐步引入概念,减少分支选择 | +| 操作指南 | `docs/zh/guides/` | 开头说明目标和前提,围绕实际任务给步骤、验证方法和常见失败处理 | +| 参考 | `docs/zh/reference/` | 准确列出签名、默认值、单位、返回值、异常与限制;与实现逐项核对 | +| 原理说明 | `docs/zh/explanation/` | 解释原因、边界和取舍,通过具体场景建立概念之间的联系 | 一页主要解决一种阅读需求;需要其他类型的内容时链接过去,避免在操作步骤中展开长篇 原理,或在教程中罗列所有参数。已有页面混合多种内容时逐步拆分,并尽量保留旧地址。 例如,配置步骤留在 Execution 指南,默认值放在 Execution 参考,调度原因放在原理说明。 新增页面须接入 `mkdocs.yml` 和相应目录入口。检查相对链接与页内锚点, -运行 `python3 -m pytest -q tests/integration/test_documentation.py` 和 `mkdocs build --strict`。 +运行 `python3 scripts/docs.py check`、 +`python3 -m pytest -q tests/integration/test_documentation.py tests/integration/test_quickstart_tutorial.py` +和 `mkdocs build --strict`。 新增可运行示例时还要实际执行;已有覆盖模型片段由文档测试校验。 ## Wiki 自动同步 -Wiki 的内容来自 `main` 的 `docs/`,导航来自 `mkdocs.yml`。只需编辑这些源文件。 +Wiki 的内容来自 `main` 的 `docs/zh/` 和 `docs/en/`,导航来自 `mkdocs.yml`。 +首页提供语言选择,页面名使用 `zh-` / `en-` 前缀,每页链接对应的另一语言版本。 +只需编辑这些源文件;功能与对应的双语文档应在同一个 PR 中提交。 **Documentation** 工作流校验链接、构建网站,导出页面及侧边栏,并上传预览产物。 文档修改进入 `main` 并通过检查后,会同步到 `toffee.wiki.git`;PR 和其他分支只生成预览。 Legacy `master` 不参与同步。Wiki 工作流独立于软件包发布检查,避免扩大其写权限。 @@ -49,7 +58,7 @@ Legacy `master` 不参与同步。Wiki 工作流独立于软件包发布检查 本地验证和导出不需要安装原生依赖: ```bash -python3 -m pip install 'mkdocs>=1.6,<2' +python3 -m pip install -r requirements-docs.txt python3 -m unittest discover -s scripts/tests -v python3 scripts/docs.py check python3 -m mkdocs build --strict @@ -115,7 +124,7 @@ BUILD_XSPCOMM_SWIG=python XSPCOMM_BUILD_WHEEL=1 \ 版本号由 Git tag 和 setuptools-scm 生成,不在源码中另写一份。 针对 main 的 PR 需选择一个版本标签:`release:patch`、`release:minor` 或 `release:major`。 -具体选择和发布失败后的处理见[版本与发布](docs/guides/versioning-and-releases.md)。 +具体选择和发布失败后的处理见[版本与发布](docs/zh/guides/versioning-and-releases.md)。 发布检查包括原生回归、严格文档构建、源码包重建和隔离安装。 验证通过后发布的是同一组已检查产物,不另行构建一份替代。 diff --git a/MANIFEST.in b/MANIFEST.in index 203d8bb..4149656 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include LICENSE README.md CONTRIBUTING.md pyproject.toml mkdocs.yml +include LICENSE README.md README.en.md CONTRIBUTING.md pyproject.toml mkdocs.yml requirements-docs.txt include .github/native-dependencies.json recursive-include src *.py *.html *.css recursive-include scripts *.py diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..3b50014 --- /dev/null +++ b/README.en.md @@ -0,0 +1,100 @@ +# Toffee + +[中文](https://github.com/XS-MLVP/toffee/blob/main/README.md) · English + +[![PyPI](https://img.shields.io/pypi/v/uc-toffee?style=flat-square&logo=pypi&logoColor=white)](https://pypi.org/project/uc-toffee/) +[![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-blue?style=flat-square&logo=python&logoColor=white)](https://github.com/XS-MLVP/toffee/blob/main/docs/en/getting-started/installation.md) +[![CI](https://img.shields.io/github/actions/workflow/status/XS-MLVP/toffee/ci.yml?branch=main&style=flat-square&label=CI&logo=github)](https://github.com/XS-MLVP/toffee/actions/workflows/ci.yml?query=branch%3Amain) +[![License](https://img.shields.io/badge/license-MulanPSL--2.0-blue?style=flat-square)](https://github.com/XS-MLVP/toffee/blob/main/LICENSE) +[![Docs](https://img.shields.io/badge/docs-Guide-0A7EA4?style=flat-square)](https://github.com/XS-MLVP/toffee/blob/main/docs/en/index.md) +[![GitHub Stars](https://img.shields.io/github/stars/XS-MLVP/toffee?style=social)](https://github.com/XS-MLVP/toffee/tree/main) + +Toffee is a Python hardware verification framework. Describe input timing and condition waits with +`async` / `await`, run tests with pytest, and build verification environments with reference models, +checking, and coverage. + +- **Clocks and events:** wait for clock edges, signal conditions, and multi-cycle events, with + supported conditions compiled for native evaluation. +- **Driving and checking:** organize reusable Drivers, Monitors, and Agents; check ordered or + reordered responses with Scoreboard. +- **Coverage and regressions:** define functional, cross, and temporal coverage, then aggregate + functional and native coverage after parallel regressions. + +## Quick start + +Requires **Python 3.11+**. The PyPI distribution is `uc-toffee`, imported as `toffee`; dependency +`uc-xcomm` supplies native bindings. Install with `python3 -m pip install "uc-toffee[runner]"`. +To run the repository examples, create an environment and install from source at the repository root: + +```bash +python3 -m venv .venv +source .venv/bin/activate +python3 -m pip install -e '.[test]' +``` + +This test uses the included 8-bit accumulator model and needs no RTL simulator. It supplies 3, +waits one clock cycle, and checks the accumulated result and idle behavior: + +```python +import pytest + +from toffee import ClockCycles, Execution +from examples.getting_started.toy_dut import TutorialDut + + +@pytest.mark.asyncio +async def test_one_input_and_idle(): + dut = TutorialDut() + try: + async with Execution(dut.backend): + assert dut.total.U() == 0 + dut.operand.Set(3) + dut.enable.Set(1) + await ClockCycles(dut.clock, 1) + dut.enable.Set(0) + assert dut.total.U() == 3 + + await ClockCycles(dut.clock, 2) + assert dut.total.U() == 3 + finally: + dut.close() +``` + +The test is already in the [example file](https://github.com/XS-MLVP/toffee/blob/main/examples/getting_started/test_accumulator.py). +Run it directly: + +```bash +python3 -m pytest -q examples/getting_started/test_accumulator.py::test_one_input_and_idle +``` + +Expect `1 passed`. Continue with the [accumulator tutorial](https://github.com/XS-MLVP/toffee/blob/main/docs/en/getting-started/index.md) +to add boundary tests, Drivers, Monitors, reference models, and coverage. + +## Use Toffee in a project + +For real RTL, use a Picker-generated DUT Python package and connect its xspcomm clock/signals +through `XCommClockBackend`. See [real DUT integration](https://github.com/XS-MLVP/toffee/blob/main/docs/en/getting-started/native-and-next.md) +for preparation and dependencies. + +Use `toffee run` to execute larger regressions in parallel and aggregate coverage. For example: + +```bash +toffee run examples/getting_started --workers 4 +``` + +The terminal prints the run's artifact directory, including `coverage.html` when coverage data +exists. Replace the example path with your project's test directory. See the +[regression guide](https://github.com/XS-MLVP/toffee/blob/main/docs/en/guides/runner.md) for options. + +## Documentation + +- [Tutorials](https://github.com/XS-MLVP/toffee/blob/main/docs/en/getting-started/index.md): from one assertion to a complete environment. +- [How-to guides](https://github.com/XS-MLVP/toffee/blob/main/docs/en/guides/index.md): connect DUTs, organize components, collect coverage, and run regressions. +- [Reference](https://github.com/XS-MLVP/toffee/blob/main/docs/en/reference/index.md): APIs, CLI options, and behavioral limits. +- [Explanation](https://github.com/XS-MLVP/toffee/blob/main/docs/en/explanation/index.md): scheduling, ownership, and coverage interpretation. +- [Runnable examples](https://github.com/XS-MLVP/toffee/blob/main/docs/en/examples.md) · [Contributing](https://github.com/XS-MLVP/toffee/blob/main/CONTRIBUTING.md) + +## Toffee Legacy + +Legacy Toffee remains on the [`master` branch](https://github.com/XS-MLVP/toffee/tree/master). +The current version is a **breaking change** and is incompatible with the Toffee Legacy API. diff --git a/README.md b/README.md index d080aa3..9ddd0b6 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ # Toffee +中文 · [English](https://github.com/XS-MLVP/toffee/blob/main/README.en.md) + [![PyPI](https://img.shields.io/pypi/v/uc-toffee?style=flat-square&logo=pypi&logoColor=white)](https://pypi.org/project/uc-toffee/) -[![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-blue?style=flat-square&logo=python&logoColor=white)](https://github.com/XS-MLVP/toffee/blob/main/docs/getting-started/installation.md) +[![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12-blue?style=flat-square&logo=python&logoColor=white)](https://github.com/XS-MLVP/toffee/blob/main/docs/zh/getting-started/installation.md) [![CI](https://img.shields.io/github/actions/workflow/status/XS-MLVP/toffee/ci.yml?branch=main&style=flat-square&label=CI&logo=github)](https://github.com/XS-MLVP/toffee/actions/workflows/ci.yml?query=branch%3Amain) [![License](https://img.shields.io/badge/license-MulanPSL--2.0-blue?style=flat-square)](https://github.com/XS-MLVP/toffee/blob/main/LICENSE) -[![Docs](https://img.shields.io/badge/docs-Guide-0A7EA4?style=flat-square)](https://github.com/XS-MLVP/toffee/blob/main/docs/index.md) +[![Docs](https://img.shields.io/badge/docs-Guide-0A7EA4?style=flat-square)](https://github.com/XS-MLVP/toffee/blob/main/docs/zh/index.md) [![GitHub Stars](https://img.shields.io/github/stars/XS-MLVP/toffee?style=social)](https://github.com/XS-MLVP/toffee/tree/main) Toffee 是一个 Python 硬件验证框架。用 `async` / `await` 描述输入时序与条件等待, @@ -60,13 +62,13 @@ async def test_one_input_and_idle(): python3 -m pytest -q examples/getting_started/test_accumulator.py::test_one_input_and_idle ``` -预期得到 `1 passed`。继续阅读[累加器教程](https://github.com/XS-MLVP/toffee/blob/main/docs/getting-started/index.md), +预期得到 `1 passed`。继续阅读[累加器教程](https://github.com/XS-MLVP/toffee/blob/main/docs/zh/getting-started/index.md), 逐步加入边界测试、Driver、Monitor、参考模型与覆盖率。 ## 用于项目验证 接入真实 RTL 时,使用 Picker 生成的 DUT Python 包,通过 `XCommClockBackend` -连接 xspcomm 的时钟与信号。具体准备和依赖要求见[真实 DUT 接入指南](https://github.com/XS-MLVP/toffee/blob/main/docs/getting-started/native-and-next.md)。 +连接 xspcomm 的时钟与信号。具体准备和依赖要求见[真实 DUT 接入指南](https://github.com/XS-MLVP/toffee/blob/main/docs/zh/getting-started/native-and-next.md)。 测试增多后,用 `toffee run` 并行执行并汇总覆盖率。例如,运行整个入门示例目录: @@ -75,15 +77,15 @@ toffee run examples/getting_started --workers 4 ``` 终端会显示本次运行的产物目录;有覆盖数据时会生成 `coverage.html`。 -将示例路径换成项目测试目录即可运行自己的回归,更多选项见[回归运行指南](https://github.com/XS-MLVP/toffee/blob/main/docs/guides/runner.md)。 +将示例路径换成项目测试目录即可运行自己的回归,更多选项见[回归运行指南](https://github.com/XS-MLVP/toffee/blob/main/docs/zh/guides/runner.md)。 ## 文档 -- [入门教程](https://github.com/XS-MLVP/toffee/blob/main/docs/getting-started/index.md):从一条断言到完整的验证环境。 -- [操作指南](https://github.com/XS-MLVP/toffee/blob/main/docs/guides/index.md):接入 DUT、组织组件、采集覆盖率和运行回归。 -- [接口参考](https://github.com/XS-MLVP/toffee/blob/main/docs/reference/index.md):API、命令行参数和行为限制。 -- [原理说明](https://github.com/XS-MLVP/toffee/blob/main/docs/explanation/index.md):仿真调度、资源归属与覆盖率含义。 -- [可运行示例](https://github.com/XS-MLVP/toffee/blob/main/docs/examples.md) · [参与开发](https://github.com/XS-MLVP/toffee/blob/main/CONTRIBUTING.md) +- [入门教程](https://github.com/XS-MLVP/toffee/blob/main/docs/zh/getting-started/index.md):从一条断言到完整的验证环境。 +- [操作指南](https://github.com/XS-MLVP/toffee/blob/main/docs/zh/guides/index.md):接入 DUT、组织组件、采集覆盖率和运行回归。 +- [接口参考](https://github.com/XS-MLVP/toffee/blob/main/docs/zh/reference/index.md):API、命令行参数和行为限制。 +- [原理说明](https://github.com/XS-MLVP/toffee/blob/main/docs/zh/explanation/index.md):仿真调度、资源归属与覆盖率含义。 +- [可运行示例](https://github.com/XS-MLVP/toffee/blob/main/docs/zh/examples.md) · [参与开发](https://github.com/XS-MLVP/toffee/blob/main/CONTRIBUTING.md) ## Toffee Legacy diff --git a/docs/en/examples.md b/docs/en/examples.md new file mode 100644 index 0000000..ca1c904 --- /dev/null +++ b/docs/en/examples.md @@ -0,0 +1,113 @@ +# Examples + +Run these commands from the repository root after `python3 -m pip install -e '.[test]'`. + +## Accumulator tutorial from first principles + +The [learning path](getting-started/index.md) starts with four assertion lines, then adds clocks, +boundaries, Drivers, Monitors, Agents, reference models, pipelining, failure diagnosis, and coverage. +Every lesson explains new code, expected results, and exercises. Mainline code lives in +`test_accumulator.py`; `toy_dut.py` supplies a teaching DUT without native bindings or RTL. + +```bash +python3 -m pytest -q examples/getting_started +``` + +The unmodified examples should report `16 passed`. Individual commands and file roles are in +the tutorial. `failure_lab.py` contains four explicitly invoked failure exercises. `native_lab.py` +is optional and requires xspcomm. Neither is automatically collected by the directory command above. + +## Triggers + +[basic_execution.py](https://github.com/XS-MLVP/toffee/blob/main/examples/triggers/basic_execution.py) +demonstrates Execution, RisingEdge, FallingEdge, Value, and `@xtrigger`. + +```bash +PYTHONPATH=src python3 examples/triggers/basic_execution.py +``` + +## Sampling and persistent subscriptions + +[subscription.py](https://github.com/XS-MLVP/toffee/blob/main/examples/triggers/subscription.py) +captures immutable data at sampling time before processing it in an async handler: + +```bash +python3 examples/triggers/subscription.py +``` + +## Transaction checking + +[scoreboard.py](https://github.com/XS-MLVP/toffee/blob/main/examples/transactions/scoreboard.py) +shows protocol-independent Drivers/Monitors, fixed-latency responses, and budgeted finish: + +```bash +python3 examples/transactions/scoreboard.py +``` + +Expect `checked=8, passed=8, completed=8`. CI runs these introductory, native, subscription, and +transaction examples. + +## Pipelined input, overlapping stages, and reordered responses + +The [complete example guide](guides/pipeline-examples.md) compares continuous issue, serial and +overlapping input stages, nonblocking acceptance-only submissions, sequential send, and +FIFO/fixed-latency/key association. It also provides four reproducible failure scenarios. + +```bash +python3 examples/transactions/pipeline.py +python3 examples/transactions/pipeline.py --backend native +python3 -m pytest -q tests/integration/test_pipeline_examples.py --require-xspcomm +``` + +The default command needs neither RTL nor native bindings; the second needs xspcomm. CI runs +successful scenarios on both Backends and uses pytest to check expected failures, sampled timing, +and cleanup. + +## Agents and reference models + +See [Agents and reference models](guides/agents-and-reference-models.md) for a stateful model, +Agent, and out-of-order response demonstration: + +```bash +python3 -m examples.transactions.reference_model +python3 -m examples.transactions.reference_model --backend native +``` + +The [component and artifact recipe](guides/verification-flow.md) connects Driver → Monitor → +Scoreboard → coverage → pytest artifacts, including saving on failure and isolated seeds: + +```bash +python3 -m pytest -q -s examples/transactions/test_pipeline_coverage.py +``` + +## Functional coverage + +[functional_coverage.py](https://github.com/XS-MLVP/toffee/blob/main/examples/coverage/functional_coverage.py) +demonstrates coverpoints, bins, crosses, JSON output, and coverage checking. + +```bash +python3 examples/coverage/functional_coverage.py --output output/functional-coverage.json +``` + +Continue with: + +- [Class declarations](guides/coverage-classes.md): reusable points/models, manual sampling, and signal binding. +- [Transaction coverage](guides/coverage-transactions.md): latency and completion order by request ID. +- [Temporal coverage](guides/coverage-patterns.md): Sequence and FSM completions. + +## Move from teaching substitutes to real DUTs + +After the tutorial, choose the environment relevant to your goal; there is no need to run every +example first. Real DUTs require Picker, Verilator, and native bindings, and are not part of default +tutorial collection. + +| Goal | Entry point | Framework features | +| --- | --- | --- | +| Expand inputs into downstream commands; check routing, addresses, data, and errors | [e203 build and use](https://github.com/XS-MLVP/toffee/blob/main/examples/integration/e203/README.md) | Bundle snapshots, acceptance-time model updates, Scoreboard context, coverage, pytest results, LCOV/HTML, failure artifacts | +| Verify refill, masks, replacement, MMIO, and probe/release | [Cache build and use](https://github.com/XS-MLVP/toffee/blob/main/examples/integration/cache/README.md) | Project SyncDriver, Monitor, multi-beat CoherenceAgent, Scoreboard, reference memory, coverage, background cleanup | +| Verify submit, pipelines, reordering, cancellation, and deadlines | [Pipeline scenarios](guides/pipeline-examples.md), [Agents/models](guides/agents-and-reference-models.md) | AsyncDriver, single-cycle templates, Agent, FIFO/key/fixed latency, finish, memory/native Backends | + +e203 and Cache READMEs walk through building, directed cases, additional seeds, and reports. +Choose Drivers to match DUT timing instead of changing the protocol to showcase an API. +Cache's independent overlap probe is still expected to fail; a passing functional campaign does +not establish that this known issue is fixed. diff --git a/docs/en/explanation/coverage-results.md b/docs/en/explanation/coverage-results.md new file mode 100644 index 0000000..e2bb016 --- /dev/null +++ b/docs/en/explanation/coverage-results.md @@ -0,0 +1,54 @@ +# Checking, coverage, and regression results + +A regression can produce a test verdict, functional coverage, native coverage, and a run manifest. +Each answers a different question and should be read separately. + +## Observing a scenario does not establish correctness + +Assertions and Scoreboards compare expected and actual behavior. Functional coverage records +whether defined scenarios were observed. Native coverage records simulator code or signal activity. +High coverage does not prove complete checking, and passing tests do not prove all required +scenarios have occurred. + +For example, a Monitor may sample coverage for a response before the Scoreboard finds incorrect +data. The response was observed and was wrong; both facts remain valid. Define the sampling +location precisely: input acceptance, DUT output, or successful checking. Different locations +make counts describe different facts. + +## Deliver and sample an observation + +Checking and coverage often need the same DUT observation. Two consumers independently calling +one Monitor's `recv()` can split the stream so each sees only part of it. + +A project can sample the same immutable Transfer once before delivering it to the checker. +This preserves a single consumption path and an explicit source. Undelivered observations after +checking stops do not count. Projects needing observation after failure must separately define +that lifecycle. See the [verification environment recipe](../guides/verification-flow.md). + +## Aggregate consistent measurements across cases + +The Runner collects groups created by each test after teardown, and shared groups once at process +exit. Counts can be added across processes only when definitions, instance identities, and sampling +rules are compatible. Zero-hit bins must remain present so unobserved scenarios cannot disappear +from the denominator. + +Unit tests for a coverage model may fill bins artificially to verify classification. Those samples +describe the model, not scenarios observed on a DUT. The Runner's exclusion marker and `capture=False` +keep model self-tests separate from DUT measurements. See [run tests and aggregate coverage](../guides/runner.md). + +Native counts merge by DUT build identity, never across different builds. Functional and native +coverage remain separate; neither becomes a combined score that replaces the test verdict. + +## Distinguish complete reports from passing tests + +`state="committed"` in `manifest.json` means artifacts were fully written and validated. It does +not mean every test passed. A completely executed regression containing assertion failures can +still produce useful diagnostic coverage, while preserving a nonzero test exit code. + +Crashes, incomplete collection, or corrupt inputs make aggregation incomplete. Available +intermediate inputs help diagnosis but do not constitute a complete regression. Cleanup failure +after commit is different: final artifacts remain, the manifest records `cleanup_error`, and the +command still reports failure. + +See [Runner reference](../reference/runner.md) for files, states, and exit behavior, and +[the operating guide](../guides/runner.md) for troubleshooting steps. diff --git a/docs/en/explanation/execution-model.md b/docs/en/explanation/execution-model.md new file mode 100644 index 0000000..9548ceb --- /dev/null +++ b/docs/en/explanation/execution-model.md @@ -0,0 +1,69 @@ +# Simulation scheduling and resource ownership + +Toffee has two kinds of progress: Python tasks execute on asyncio, and the DUT advances on a +simulation clock. Their relationship explains why the DUT may keep running during an HTTP wait +and why ending a test does not automatically destroy the DUT. + +## Ready Python work and clock advancement + +asyncio is the sole scheduler of Python tasks and callbacks. Execution observes immediate callbacks +belonging to this simulation. It allows the next clock advancement or DriveStable sampling only +after those callbacks and their cascading wakeups finish. This is phase settling. For example, +a response wakes a checker, which releases input capacity, allowing a Driver to submit another +input. That immediate chain must finish before the clock moves. + +The Backend advances simulation and returns hit events. Supported compiled conditions evaluate +natively, reducing Python scheduling on cycles without hits. Drivers, Monitors, or `@pytrigger` +functions that execute Python every cycle still incur those calls, so batching benefits depend +on the actual test structure. + +Waiting for an incomplete external Future is not ready simulation work. With a DUT trigger or +persistent subscription, the clock may continue; without simulation watchers, waiting only for +HTTP or a Queue does not advance time. Persistent Monitors usually retain watchers, so a test +waiting for configuration does not imply a paused DUT. + +`Execution.paused()` explicitly freezes simulation time while host asyncio continues. Awaiting +a trigger that requires clock progress during a pause cannot complete. See +[external asyncio integration](../guides/asyncio-and-pytest.md). + +## Three budgets constrain different work + +| Budget | What it limits | What it does not establish | +| --- | --- | --- | +| Transaction or finish cycle budget | Simulated response, drain, and trailing observation cycles | Completion within a specified number of real seconds | +| Wall-clock timeout | Real elapsed time, including external I/O | Compliance with DUT cycle latency | +| `max_settle_rounds` | Yields back to asyncio while settling one phase | Number of simulated cycles waited | + +An in-domain task looping forever on `await asyncio.sleep(0)` keeps ready work in the current +phase. `SimulationNotSettledError` diagnoses this; the clock does not skip that work. Raise the +budget only for finite but long chains. Move unrelated services outside the domain using +`external_task`. See [Execution reference](../reference/execution.md). + +## Scheduling membership and lifecycle ownership + +A task belonging to the simulation scheduling domain does not automatically transfer its lifetime +to Execution. Execution starts/stops explicitly registered Agents, and Agents manage their Drivers +and Monitors. Creators still own independently created background tasks. `external_task` removes +the implicit Reactor binding and runs a service outside the domain; it does not clean up that service. + +| Resource | Lifecycle owner | +| --- | --- | +| Current test's waits, subscriptions, compiled state, and advancement task | Execution | +| Registered Agents and their components | Execution and Agent component management | +| Backend | Creator or fixture, calling `close()` | +| DUT | Project creator or fixture, including initialization and release | +| Independent external task | Creator, awaiting completion or cancelling and joining | + +This boundary permits sequential Backend reuse across tests without implicitly resetting the DUT. +Projects needing a fresh state must define a reset sequence. Previous Execution exit does not +mean the design returned to its initial state. + +## One owner advances a clock + +Inside Execution, `XCommClockBackend` exclusively controls its XClock. Calling `dut.Step()` as well +would create two advancement paths without one sampling order, so it is rejected. Pausing retains +that ownership; manual advancement becomes available only after exit. + +Ownership covers that clock domain only. Multiple Executions do not automatically share one +timeline. See [Execution reference](../reference/execution.md) for manual-step restrictions and +[Execution configuration](../guides/execution-and-backend.md) for fixture examples. diff --git a/docs/en/explanation/index.md b/docs/en/explanation/index.md new file mode 100644 index 0000000..789ea88 --- /dev/null +++ b/docs/en/explanation/index.md @@ -0,0 +1,10 @@ +# Explanation + +These pages explain how Toffee organizes simulation and verification, helping you reason about +component boundaries and results. For tasks, use [how-to guides](../guides/index.md); +for exact contracts, use [reference](../reference/index.md). + +| Question | Page | +| --- | --- | +| How do asyncio and simulation time coordinate, and who closes resources? | [Scheduling and resource ownership](execution-model.md) | +| What do passing tests, coverage hits, and complete reports each establish? | [Checking, coverage, and regression results](coverage-results.md) | diff --git a/docs/en/getting-started/agent-test.md b/docs/en/getting-started/agent-test.md new file mode 100644 index 0000000..b59a5b7 --- /dev/null +++ b/docs/en/getting-started/agent-test.md @@ -0,0 +1,82 @@ +# 6. Encapsulate an interface in an Agent + +Combine the Driver and Monitor so tests no longer start and consume each component individually. + +## Assemble the interface + +Continue in `examples/getting_started/test_accumulator.py`, retaining previous definitions: + + +```python +from toffee import Agent + + +def make_agent(dut): + return Agent( + "accumulator", + driver=make_sync_driver(dut), + monitors={"response": make_response_monitor(dut)}, + response_monitor="response", + clock=dut.clock, + response_timeout_cycles=3, + ) +``` + +`make_agent(dut)` returns an Agent using the factories from the preceding lessons. +The Monitor is named `"response"`; `response_monitor="response"` connects it to the response checker. +Do not also consume that Monitor yourself: you would compete with the checker for observations. + +`clock` defines the timeout clock. `response_timeout_cycles=3` allows at most three cycles after each +input is accepted. The specified latency is two cycles; three is an explicit test budget, not a guessed default. +By default, responses match acceptance order (FIFO). + +```text +用例 --send(操作数)--> Agent 内的 Driver --> DUT 输入 +DUT 响应 --> Agent 内的 Monitor --> 检查器 <-- 用例给出的 expected +``` + +## Run an automatically checked test + + +```python +@pytest.mark.asyncio +async def test_agent_checks_responses(): + dut = TutorialDut() + agent = make_agent(dut) + try: + async with Execution(dut.backend, agents=[agent]): + await agent.send(3, expected=3) + await agent.send(5, expected=8) + await agent.finish(timeout_cycles=10, observe_cycles=1) + finally: + dut.close() +``` + +```bash +python3 -m pytest -q examples/getting_started/test_accumulator.py::test_agent_checks_responses +``` + +Expect `1 passed`. Constructing an Agent does not start its clock. `Execution(..., agents=[agent])` +starts its components and cleans them up on exit. There is no need for `async with agent` or for tests +to manipulate the internal Driver and Monitor. +`send()` still waits for acceptance, but the connected checker continues checking the later response. + +## Finish explicitly + +`finish(timeout_cycles=10, observe_cycles=1)` closes admission, waits for submitted requests to finish, +and observes one extra cycle for unexpected responses, within a total budget of ten cycles. +Environment cleanup does not wait indefinitely for unfinished responses. + +| Budget | Starts at | Purpose | +| --- | --- | --- | +| `response_timeout_cycles=3` | Each input's acceptance | Deadline for that response | +| `finish(timeout_cycles=10)` | Calling finish | Overall drain and observation budget | +| `observe_cycles=1` | Completion of submitted work | Detect extra output in the observation window | + +Choose the observation window from the design's contract. One quiet cycle cannot prove that output will never recur. + +## Trigger a mismatch + +Change the second send's `expected=8` to 9. Expect `ScoreboardMismatch` with expected 9 and actual 8. +Background checking fails pytest even without a handwritten assertion. Restore 8. +The Agent organizes the interface; the test still supplies expectations. Next, [derive them from a reference model](reference-model.md). diff --git a/docs/en/getting-started/clock-and-input.md b/docs/en/getting-started/clock-and-input.md new file mode 100644 index 0000000..26f490a --- /dev/null +++ b/docs/en/getting-started/clock-and-input.md @@ -0,0 +1,86 @@ +# 2. Drive inputs, wait for a clock, and check the result + +Replace the arithmetic expression with a clocked DUT. Check that adding 3 works and that the value stays constant while idle. + +## State the DUT specification + +The tutorial accumulator starts at 0 and examines its inputs on every rising edge: + +- With `enable=1`, accept `operand` and update `total = (total + operand) % 256`. +- With `enable=0`, accept nothing and retain `total`. +- Each accepted input also produces a response **two cycles later**. This lesson reads `total`; lesson 5 checks responses. + +There is no backpressure or ready/valid handshake here. These are this DUT's rules, not assumptions +made by Toffee. Holding `enable=1` across two rising edges accepts two inputs. +`TutorialDut()` from `examples/getting_started/toy_dut.py` is a Python teaching model, requiring no RTL build. +Each instance starts fresh; real designs need initialization specified by the project. + +| Interface | Meaning | +| --- | --- | +| `dut.operand.Set(3)` | Write an operand in the range 0–255 | +| `dut.enable.Set(1)` / `.Set(0)` | Enable or disable acceptance at the next rising edge | +| `dut.total.U()` | Read the current unsigned total | +| `dut.clock` | Clock used by waits | +| `dut.backend` | Backend supplied to Execution | +| `dut.close()` | Release the teaching environment | + +## Run the test + +This excerpt extends `examples/getting_started/test_accumulator.py`; retain earlier definitions: + + +```python +import pytest + +from examples.getting_started.toy_dut import TutorialDut +from toffee import ClockCycles, Execution + + +@pytest.mark.asyncio +async def test_one_input_and_idle(): + dut = TutorialDut() + try: + async with Execution(dut.backend): + assert dut.total.U() == 0 + dut.operand.Set(3) + dut.enable.Set(1) + await ClockCycles(dut.clock, 1) + dut.enable.Set(0) + assert dut.total.U() == 3 + + await ClockCycles(dut.clock, 2) + assert dut.total.U() == 3 + finally: + dut.close() +``` + +```bash +python3 -m pytest -q examples/getting_started/test_accumulator.py::test_one_input_and_idle +``` + +Expect `1 passed`. + +## Read the Python constructs + +`import` obtains names from modules. `@pytest.mark.asyncio` marks an asynchronous test; +`async def` permits `await`. Here `await` waits for a simulation condition, not a second of wall time. +`async with Execution(dut.backend):` starts the environment and closes it when the block exits. +`await ClockCycles(dut.clock, 1)` waits until sampling at the next rising edge has completed. +Ordinary Python assignments alone do not advance the clock. +`try ... finally` ensures that `dut.close()` runs even when an assertion fails. + +## Follow the cycles + +| Time | enable | Action | total | +| --- | --- | --- | --- | +| DUT creation | 0 | Initial state | 0 | +| Before first edge | 1 | Write operand 3 | 0 | +| First rising edge | 1 | Accept 3 | 3 | +| After first edge | 0 | Deassert enable | 3 | +| Second and third edges | 0 | No new input | 3 | + +Comment out `dut.enable.Set(0)` and rerun. The final assertion becomes `assert 9 == 3` because +3 was accepted three times. Restore the line. A simulation cycle is not a Python loop iteration. +This backend uses two half-ticks per cycle; a new clock's first rising edge is tick 2. + +Continue with [multiple data sets and boundaries](more-cases.md). diff --git a/docs/en/getting-started/coverage.md b/docs/en/getting-started/coverage.md new file mode 100644 index 0000000..db00ea8 --- /dev/null +++ b/docs/en/getting-started/coverage.md @@ -0,0 +1,99 @@ +# 10. Know which scenarios were exercised + +Separate “all comparisons performed were correct” from “all planned scenarios occurred.” +Tests using 3, 5, and 7 may all pass without observing a result of 0 or 255. + +## Define three coverage targets + +Classify **successfully checked responses** into bins: + +| Bin | Result range | Stimulus | +| --- | --- | --- | +| zero | 0 | Start at 0 and add 0 | +| maximum | 255 | Then add 255 | +| middle | 1–254 | Then add 2, overflowing to 1 | + +Hitting all three means 100% of this plan, not all inputs, timing combinations, or design behavior. +Add goals and tests when the specification requires more. + +## Collect and save coverage + +Continue in `examples/getting_started/test_accumulator.py`: + + +```python +from toffee import Bin, CoverGroupDef, CoverPointDef, CoverageDatabase + + +@pytest.mark.asyncio +async def test_result_coverage(tmp_path): + definition = CoverGroupDef("accumulator", ( + CoverPointDef("result", { + "zero": Bin.values(0), + "middle": Bin.range(1, 254), + "maximum": Bin.values(255), + }), + )) + coverage = definition.instantiate("tutorial") + dut = TutorialDut() + agent = make_pipeline_agent(dut) + agent.connect(AccumulatorModel()) + try: + async with Execution(dut.backend, agents=[agent]): + for operand in [0, 255, 2]: + transfer = agent.submit(operand) + result = await transfer + coverage.sample({"result": result}) + await agent.finish(timeout_cycles=10, observe_cycles=1) + coverage.assert_coverage(100) + print(f"Result bins: {coverage.coverage:.0f}%") + finally: + report = tmp_path / "functional.json" + try: + CoverageDatabase([coverage]).write_json(report) + print(f"Coverage JSON: {report}") + finally: + dut.close() +``` + +```bash +python3 -m pytest -q -s examples/getting_started/test_accumulator.py::test_result_coverage +``` + +Expected output: + +```text +Result bins: 100% +Coverage JSON: /.../test_result_coverage0/functional.json +... +1 passed in ... +``` + +`-s` displays print output. pytest supplies the `tmp_path` fixture; `/ "functional.json"` selects a file within it. +`CoverGroupDef` defines the plan; `CoverPointDef("result", ...)` defines classification. +`instantiate("tutorial")` creates counters and `sample({"result": result})` records a sample. +Count after `await transfer` has checked the response, rather than treating submitted input as completed work. + +This example waits for each response to make sampling explicit. For throughput tests use the submission +pattern from lesson 8. Do not consume the Agent's response Monitor separately. +The `finally` block saves observations even on a comparison or coverage failure, then closes the DUT. + +## Leave one bin uncovered + +Replace `[0, 255, 2]` with `[0, 255]`. Comparisons pass, but `coverage.assert_coverage(100)` fails at +about 66.67%, identifying the missing `middle` bin. Restore the list to reach 100%. + +## Generate HTML + +Replace the placeholder path with the JSON path printed by the previous command: + +```bash +toffee-coverage-report --functional '/实际打印的路径/functional.json' --output tutorial-coverage.html +``` + +Open `tutorial-coverage.html` in the repository root. Functional coverage needs no RTL line-coverage tools. +For richer bins, crosses, and merging, see [coverage](../guides/coverage.md). +Test outcomes and functional coverage answer different questions; see [coverage across test cases](../guides/functional-points.md). + +You can now drive, sample, model, finish, and save results. Continue with the optional +[out-of-order](out-of-order.md) or [native integration](native-and-next.md) lessons. diff --git a/docs/en/getting-started/driver.md b/docs/en/getting-started/driver.md new file mode 100644 index 0000000..86be3d4 --- /dev/null +++ b/docs/en/getting-started/driver.md @@ -0,0 +1,75 @@ +# 4. Move repeated input timing into a Driver + +Let tests say “add 3” while defining signal writes, hold times, and idle values in one place. +A Driver encapsulates the project's input protocol. + +## Choose a Driver that matches the timing + +This DUT accepts at the next rising edge, so `SyncSingleCycleDriver` fits. +Continue in `examples/getting_started/test_accumulator.py`: + + +```python +from toffee import Bundle, SyncSingleCycleDriver + + +def encode_operand(operand): + return {"enable": 1, "operand": operand} + + +def make_sync_driver(dut): + return SyncSingleCycleDriver( + dut.clock, + Bundle(enable=dut.enable, operand=dut.operand), + idle={"enable": 0, "operand": 0}, + encoder=encode_operand, + ) +``` + +`Bundle` groups `enable` and `operand`. A dictionary maps each field to its value. +`encode_operand(3)` returns `{"enable": 1, "operand": 3}`. Passing `encoder=encode_operand` without +parentheses gives the Driver a function to call for each request. `idle` supplies the values restored afterwards. +`make_sync_driver(dut)` is an ordinary project factory, not a special framework type. + +## Repeat the same test + + +```python +@pytest.mark.asyncio +async def test_driver_restores_idle(): + dut = TutorialDut() + driver = make_sync_driver(dut) + try: + async with Execution(dut.backend): + async with driver: + await driver.send(3) + await driver.send(5) + assert dut.total.U() == 8 + assert dut.enable.U() == 0 + + await ClockCycles(dut.clock, 2) + assert dut.total.U() == 8 + finally: + dut.close() +``` + +```bash +python3 -m pytest -q examples/getting_started/test_accumulator.py::test_driver_restores_idle +``` + +Expect `1 passed`. Two sends accept 3 and 5; after two idle cycles the total remains 8. +`async with driver` manages this standalone Driver and releases its input resources. +Lesson 6 places it inside an Agent managed by Execution. + +## What does send wait for? + +`await driver.send(3)` waits for **input acceptance** and returns its event, not the response two cycles later. +Sync means the calling coroutine waits for the input operation; it does not block the entire Python process. +Calling `driver.send(3)` without awaiting it does not complete the send. + +Change the encoder's enable value to 0: the test reads 0 instead of 8 because no valid input was driven. +Restore it to 1. Use SingleCycle only when the interface really accepts in one cycle. +For multicycle or staged protocols, implement the corresponding [SyncDriver or AsyncDriver](../guides/drivers.md) +and return the actual acceptance event. + +Continue with [capturing delayed outputs](monitor.md). diff --git a/docs/en/getting-started/failures.md b/docs/en/getting-started/failures.md new file mode 100644 index 0000000..695232f --- /dev/null +++ b/docs/en/getting-started/failures.md @@ -0,0 +1,104 @@ +# 9. Understand failure diagnostics + +Run four deliberate faults and learn what the checker reports. These commands are expected to fail. + +## Confirm the working example first + +```bash +python3 -m pytest -q examples/getting_started/test_accumulator.py::test_three_inputs_in_flight +``` + +After `1 passed`, use `failure_lab.py`. It keeps the same Driver, Monitor, and model while making the +teaching DUT return a wrong, missing, late, or extra response. Its filename does not start with `test_`, +so normal directory discovery does not run it. + +File: `examples/getting_started/failure_lab.py`. + + +```python +"""Intentionally failing exercises; run one case explicitly with pytest -k. + +This filename does not start with test_, so the regular suite skips it. +""" + +import pytest + +from examples.getting_started.test_accumulator import AccumulatorModel, make_pipeline_agent +from examples.getting_started.toy_dut import TutorialDut +from toffee import Execution + + +@pytest.mark.asyncio +@pytest.mark.parametrize("fault", ["wrong", "missing", "late", "extra"]) +async def test_find_the_fault(fault): + dut = TutorialDut(fault=fault) + agent = make_pipeline_agent(dut) + agent.connect(AccumulatorModel()) + try: + async with Execution(dut.backend, agents=[agent]): + agent.submit(3) + await agent.finish(timeout_cycles=10, observe_cycles=2) + finally: + dut.close() +``` + +The test does not retain or await the submitted handle. `finish()` must still report background failures. + +## Wrong result + +```bash +python3 -m pytest -q examples/getting_started/failure_lab.py -k wrong --tb=short +``` + +`-k wrong` selects that case; `--tb=short` shortens the traceback. +Expect `1 failed, 3 deselected`, with diagnostics like: + +```text +ScoreboardMismatch: accumulator mismatch + request: 3 + accepted_tick: 2 + observed_tick: 6 + expected: 3 + actual: 4 +``` + +Read request, expected, and actual first, then the sampling ticks. +The injected fault adds 1 to the response; a longer timeout cannot fix it. + +## Missing and late responses + +```bash +python3 -m pytest -q examples/getting_started/failure_lab.py -k missing --tb=short +python3 -m pytest -q examples/getting_started/failure_lab.py -k late --tb=short +``` + +Each command produces `1 failed, 3 deselected` and `ScoreboardTimeoutError`. +Acceptance is at tick 2; a three-cycle budget sets the deadline to tick 8. +The following half-cycle barrier reports `deadline exceeded at tick 9`; a correct response at tick 8 is accepted. + +The missing case never responds; the late case waits five cycles. A timeout alone cannot distinguish them. +Use waveforms or DUT logs to determine whether output eventually appears. +Increase the budget only when the specification permits it. + +## An extra response after a correct one + +```bash +python3 -m pytest -q examples/getting_started/failure_lab.py -k extra --tb=short +``` + +Expect `ScoreboardAssociationError` containing `1 unmatched observation(s)`. +The first response is correct; the extra response appears one cycle later and is caught by +`observe_cycles=2`. Waiting only for the last expected response would miss it. + +## Decide what to investigate + +| Symptom | Check first | Avoid assuming | +| --- | --- | --- | +| AssertionError / ScoreboardMismatch | Specification, expectation, input encoding, sampled snapshot | The DUT must be wrong | +| ScoreboardTimeoutError | Acceptance time, response budget, sampling condition | Any larger timeout is valid | +| ScoreboardAssociationError | Extra/duplicate output, keys, accidental idle sampling | Extra samples can simply be discarded | +| Unfinished work at exit | finish usage and bounded completion | Closing automatically means success | + +Run the file without `-k` to see `4 failed`. The normal tutorial regression, +`python3 -m pytest -q examples/getting_started`, should pass. +Continue with [functional coverage](coverage.md). diff --git a/docs/en/getting-started/first-test.md b/docs/en/getting-started/first-test.md new file mode 100644 index 0000000..fd307b4 --- /dev/null +++ b/docs/en/getting-started/first-test.md @@ -0,0 +1,69 @@ +# 1. Your first PASS and FAIL + +Learn how a test judges a result and where to look when it fails. This lesson does not introduce clocks or verification components. + +## A four-line test + +Open `examples/getting_started/test_accumulator.py` and find `test_addition` near the beginning. +The file already contains later lessons; focus on these lines: + + +```python +def test_addition(): + expected = 8 + actual = 3 + 5 + assert actual == expected +``` + +`def` defines a function. pytest discovers functions whose names begin with `test_`. +Use four spaces for indentation. `=` assigns a value; `==` compares values. +`assert` requires a condition to hold and fails the test otherwise. +Here `3 + 5` stands in for the computation under test; the next lesson reads the actual value from a DUT. + +Run in the terminal: + +```bash +python3 -m pytest -q examples/getting_started/test_accumulator.py::test_addition +``` + +Expect output similar to this, with a different duration: + +```text +. [100%] +1 passed in ... +``` + +`.` means a passing test and `-q` requests concise output. `::test_addition` selects just that function. +Running the file with Python alone does not automatically call its test functions. + +## Make it fail deliberately + +Change `expected = 8` to `expected = 9` and repeat the command: + +```text +E assert 8 == 9 +1 failed in ... +``` + +The actual result is 8, while the test expected 9. Check the specification and test data before +blaming the DUT. Restore 8 and confirm that the test passes again. + +## Add a check + +Append this function to the same file: + +```python +def test_eight_bit_wraparound(): + actual = (250 + 10) % 256 + assert actual == 4 +``` + +`%` computes a remainder. An unsigned 8-bit value holds 0–255; retaining the low 8 bits of 260 gives 4. +Run the new function: + +```bash +python3 -m pytest -q examples/getting_started/test_accumulator.py::test_eight_bit_wraparound +``` + +Expect `1 passed`. This still checks the specification without accessing a DUT. +Keep working in the same file and continue with [inputs and clocks](clock-and-input.md). diff --git a/docs/en/getting-started/index.md b/docs/en/getting-started/index.md new file mode 100644 index 0000000..f396b0e --- /dev/null +++ b/docs/en/getting-started/index.md @@ -0,0 +1,56 @@ +# Start with your first test + +Use an **8-bit accumulator** to progress from one assertion to pipelined inputs, a reference model, +and coverage. Run each example, then change one thing and observe the result. +The first lessons use a Python DUT, so no RTL simulator is needed. Real DUT integration comes later. + +## Find the code + +The two main files are: + +```text +examples/getting_started/ +├── test_accumulator.py 按教学顺序排列的测试、驱动、监视器和模型 +└── toy_dut.py 已写好的累加器 +``` + +Each lesson names the relevant test and gives a command that runs only that test. +When using the repository files, do not paste duplicate definitions into them. When starting from +an empty file, append each lesson in order and retain earlier imports and definitions. +`# region` and `# endregion` are editor-folding comments; they do not affect execution. + +## Follow the lessons + +| Lesson | Question it answers | +| --- | --- | +| [0. Prepare the environment](installation.md) | Where do commands run, and how are dependencies installed? | +| [1. Your first PASS and FAIL](first-test.md) | How does a test decide whether a result is correct? | +| [2. Inputs and clocks](clock-and-input.md) | Why has the result not changed after writing an input? | +| [3. Data sets and boundaries](more-cases.md) | What should be tested beyond one ordinary value? | +| [4. Extract a Driver](driver.md) | How can input timing be defined once and reused? | +| [5. Delayed outputs and Monitors](monitor.md) | How can a one-cycle output be captured reliably? | +| [6. Assemble an Agent](agent-test.md) | How do input, output, and checking fit together? | +| [7. Models and fixtures](reference-model.md) | How are expectations computed and test environments isolated? | +| [8. Pipelines, batches, and cancellation](pipeline.md) | Can the next request start before the previous response arrives? | +| [9. Understand failures](failures.md) | What do wrong, missing, late, and extra responses look like? | +| [10. Functional coverage](coverage.md) | Which planned scenarios remain untested? | + +You can stop after lesson 3 to practice. Driver, Monitor, and Agent organize code you have already +written; you do not need all three in your first test. Afterwards, try [out-of-order responses](out-of-order.md) +and [native clocks and project DUTs](native-and-next.md). + +## Four terms + +- **DUT**: the design under test, such as an accumulator, cache, bus interface, or arithmetic unit. +- **Stimulus**: an input sent to the DUT, such as “add 3.” +- **Expected and actual values**: the result derived from the specification and the result observed from the DUT. +- **Transaction**: a complete operation, such as adding 3 and receiving its result, possibly across several cycles. + +Run `bash` blocks in a terminal; `python` blocks belong in source files. Do not copy expected output as commands. +Continue with [installation](installation.md). + +## After the tutorial + +After lesson 3 you can write clocked tests with boundary assertions. After lesson 10 you can combine +components, models, pipeline checking, and coverage. Then use the [how-to guides](../guides/index.md), +[API reference](../reference/api.md), and [explanations](../explanation/index.md) for your own project. diff --git a/docs/en/getting-started/installation.md b/docs/en/getting-started/installation.md new file mode 100644 index 0000000..914ac74 --- /dev/null +++ b/docs/en/getting-started/installation.md @@ -0,0 +1,54 @@ +# 0. Prepare an environment for the examples + +By the end of this page, you should be able to import Toffee in a terminal. +The commands use a Linux/macOS shell and require **Python 3.11 or newer**. No hardware simulator is needed yet. + +## Enter the repository root + +Use the `toffee` directory containing `pyproject.toml`, `examples/`, and `docs/`. To obtain it: + +```bash +git clone --branch main https://github.com/XS-MLVP/toffee.git +cd toffee +``` + +Run all following commands from that directory, rather than from `src/` or `examples/`. +The distribution is named `uc-toffee`; the Python import is `toffee`. +`python3 -m pip install "uc-toffee[runner]"` also installs `uc-xcomm>=1.1,<2`, which supplies `xspcomm`. +The old `pytoffee` package belongs to Legacy Toffee. + +The tutorial installs from source so the repository examples are available. When developing against +unreleased native changes, prepare the pinned dependency described in the +[contribution guide](https://github.com/XS-MLVP/toffee/blob/main/CONTRIBUTING.md). + +## Install in a virtual environment + +```bash +python3 --version +python3 -m venv .venv +source .venv/bin/activate +python3 -m pip install -e '.[test]' +python3 -c 'import toffee; print(toffee.__file__)' +``` + +The last command should print this checkout's `src/toffee/__init__.py`. +`venv` creates `.venv`; `source` selects it in the current shell. The editable `-e` installation makes +source changes immediately visible. Keep the quotes around `.[test]`. +In a new terminal, enter the repository and run `source .venv/bin/activate` again. + +pytest discovers and runs tests; pytest-asyncio runs the later asynchronous tests. +You do not need to create an event loop or TaskGroup yourself. + +## Troubleshoot installation + +| Symptom | Check | +| --- | --- | +| Python is older than 3.11 | Install a supported interpreter and recreate `.venv` | +| Missing `venv` or `ensurepip` | Install the distribution's Python venv component | +| pip cannot download packages | Check network access and the configured package mirror | +| `No module named toffee` | Activate the environment and use the same interpreter for installation and execution | +| Missing `examples` or a test file | Return to the repository root and use `python3 -m pytest ...` | + +Continue with [your first PASS and FAIL](first-test.md). For native XClock usage, see +[native integration](native-and-next.md). A matching wheel needs no local compilation; +building `uc-xcomm` from source requires system C++20 tools, CMake, and Python development headers. diff --git a/docs/en/getting-started/monitor.md b/docs/en/getting-started/monitor.md new file mode 100644 index 0000000..7377bf3 --- /dev/null +++ b/docs/en/getting-started/monitor.md @@ -0,0 +1,98 @@ +# 5. Capture an output that appears two cycles later + +Distinguish the signal visible now from a result sampled earlier. +Real interfaces often expose a response for only one cycle. + +## Complete the response specification + +`TutorialDut(latency=2)` produces its response two cycles after acceptance. +An integer, including 0, means a response is present; `None` means no response. +This Python property is a teaching interface. Real DUT sampling must follow the actual ports and validity rules. + +| Rising edge | Input | response | +| --- | --- | --- | +| 1 | Accept 3 | None | +| 2 | Idle | None | +| 3 | Idle | 3 | +| 4 | Idle | None | + +Reading `dut.response` at edge 4 misses 3. A Monitor stores it at edge 3. + +## Specify when and what to sample + +Continue in `examples/getting_started/test_accumulator.py`: + + +```python +from toffee import PythonPredicateTrigger, RisingEdge, SamplingMonitor + + +def make_response_monitor(dut): + def response_present(): + return dut.response is not None + + def capture(event): + return dut.response + + return SamplingMonitor( + PythonPredicateTrigger( + "response-present", response_present, + sample=RisingEdge(dut.clock), mode="each_sample", + ), + capture=capture, + ) +``` + +`response_present()` determines validity. Use `is not None` so a valid zero is retained. +`capture(event)` immediately returns an immutable integer snapshot. +The nested functions access the `dut` passed to the factory. `PythonPredicateTrigger` checks each +`RisingEdge`; `mode="each_sample"` captures consecutive valid cycles without losing the second response. +Capture is synchronous. For structured interfaces, use `Bundle.sample()` rather than handing out a mutable DUT object. + +## Read late and verify the stored sample + + +```python +from toffee import Scoreboard + + +@pytest.mark.asyncio +async def test_delayed_response_snapshot(): + dut = TutorialDut(latency=2) + driver = make_sync_driver(dut) + monitor = make_response_monitor(dut) + checker = Scoreboard("accumulator") + try: + async with Execution(dut.backend) as execution: + async with driver, monitor.start(execution): + await driver.send(3) + assert dut.response is None + await ClockCycles(dut.clock, 3) + assert dut.response is None + + observed = await monitor.recv() + checker.check(expected=3, actual=observed.value) + assert observed.event.tick == 6 + finally: + dut.close() +``` + +```bash +python3 -m pytest -q examples/getting_started/test_accumulator.py::test_delayed_response_snapshot +``` + +Expect `1 passed`. Input is accepted at cycle 1 and the test reads after cycle 4. +Although `dut.response` is then None, `monitor.recv()` retrieves the cycle-3 sample. +`observed.value` is 3 and `observed.event.tick` is 6, the sampling time rather than retrieval time. +`as execution` names the running environment. The combined context starts Driver and Monitor, +then closes them in reverse order. Start the Monitor before sending to observe all responses. + +## What does Scoreboard do here? + +`checker.check(expected=3, actual=observed.value)` compares values with useful diagnostics. +This still retrieves each response manually; no automatic association or response timeout is configured. +A standalone `recv()` cannot infer a timeout from a missing response. + +Change latency from 2 to 1: the value check passes but the time check becomes `assert 4 == 6`. +Only change timing expectations if the specification has changed; restore the example before continuing. +[Assemble the Driver and Monitor into an Agent](agent-test.md). diff --git a/docs/en/getting-started/more-cases.md b/docs/en/getting-started/more-cases.md new file mode 100644 index 0000000..a2f3de0 --- /dev/null +++ b/docs/en/getting-started/more-cases.md @@ -0,0 +1,61 @@ +# 3. Test multiple data sets and boundaries + +A passing “add 3 and hold for two cycles” test covers one scenario. Now check accumulation and boundaries. + +## Write the cases first + +| Input sequence | Expected final total | Scenario | +| --- | --- | --- | +| 0 | 0 | Zero is valid input, not absence of input | +| 3, 5 | 8 | State accumulates across requests | +| 255 | 255 | Maximum input | +| 255, 1 | 0 | Overflow exactly to zero | +| 250, 10 | 4 | Keep the low eight bits after overflow | + +Derive expectations from the specification, not `dut.total.U()`, which would compare the DUT with itself. + +## Run each row with pytest + +Continue in `examples/getting_started/test_accumulator.py`: + + +```python +@pytest.mark.asyncio +@pytest.mark.parametrize("operands, expected", [ + ([0], 0), + ([3, 5], 8), + ([255], 255), + ([255, 1], 0), + ([250, 10], 4), +]) +async def test_accumulator_cases(operands, expected): + dut = TutorialDut() + try: + async with Execution(dut.backend): + for operand in operands: + dut.operand.Set(operand) + dut.enable.Set(1) + await ClockCycles(dut.clock, 1) + dut.enable.Set(0) + assert dut.total.U() == expected + finally: + dut.close() +``` + +```bash +python3 -m pytest -v examples/getting_started/test_accumulator.py::test_accumulator_cases +``` + +Expect five `PASSED` entries and `5 passed`. `-v` lists each case. +`[3, 5]` is a list; `for operand in operands:` visits its values in order. +Each iteration writes input, waits for an edge, and clears enable. +`@pytest.mark.parametrize("operands, expected", [...])` calls the test once per row. +Each invocation creates a fresh DUT, so cases do not inherit prior state. + +## Add a boundary case + +Add `([128, 128], 0)` and expect `6 passed`. Writing 256 as the expectation makes only this new case fail, +because an 8-bit register cannot hold 256. +These tests check final state, not yet every response or whether all responses arrive. +The repeated write/enable/wait/disable sequence is the next abstraction to extract: +[define a Driver](driver.md). diff --git a/docs/en/getting-started/native-and-next.md b/docs/en/getting-started/native-and-next.md new file mode 100644 index 0000000..4cc98f6 --- /dev/null +++ b/docs/en/getting-started/native-and-next.md @@ -0,0 +1,120 @@ +# Optional: native clocks and project DUTs + +Complete the main tutorial and prepare xcomm's Python bindings. This lesson checks that the same +verification components work with native XClock. + +## Verify the loaded binding + +Toffee depends on `uc-xcomm`, which provides `xspcomm`. The backend requires APIs including +`XClock.StepHalf`, `GetHalfTick`, `GetPhase`, `XData`, and `XTriggerEngine`. +Use the commit, package, ABI, and protocol versions from the +[dependency manifest](https://github.com/XS-MLVP/toffee/blob/main/.github/native-dependencies.json), not the package name alone. + +```bash +python3 examples/getting_started/check_native.py +``` + +For a binding built locally, add the **parent of the xspcomm package directory** to the search path. +Replace the placeholder with your build directory: + +```bash +export PYTHONPATH="/实际路径/xcomm/python${PYTHONPATH:+:$PYTHONPATH}" +python3 examples/getting_started/check_native.py +``` + +The script prints the loaded binding's path and should end with: + +```text +native clock: tick=2, phase=RISING_STABLE +``` + +| Symptom | Action | +| --- | --- | +| `No module named xspcomm` | Check installation in the selected interpreter or PYTHONPATH | +| Path points at an old build | Select the intended binding | +| Missing half-step APIs or XTriggerEngine | Update or rebuild xcomm with those APIs | + +Repository tests attempt to discover a sibling Picker build. Ordinary scripts and example-only runs +do not use `tests/conftest.py`, so xspcomm must be directly importable here. + +## Check overflow on a native clock + +File: `examples/getting_started/native_lab.py`. + + +```python +"""Optional native-clock exercise, run explicitly after installing xspcomm.""" + +import pytest + +from examples.getting_started.test_accumulator import AccumulatorModel, make_pipeline_agent +from examples.getting_started.toy_dut import TutorialDut +from toffee import Execution + + +@pytest.mark.asyncio +async def test_accumulator_with_native_clock(): + dut = TutorialDut(backend="native") + agent = make_pipeline_agent(dut) + agent.connect(AccumulatorModel()) + try: + async with Execution(dut.backend, agents=[agent]): + first = agent.submit(250) + second = agent.submit(10) + await agent.finish(timeout_cycles=10, observe_cycles=1) + assert await first == 250 + assert await second == 4 + finally: + dut.close() +``` + +```bash +python3 -m pytest -q examples/getting_started/native_lab.py +``` + +Expect `1 passed`. The environment changes to `TutorialDut(backend="native")`, using native XClock and XData. +The accumulator behavior is still implemented in Python: this verifies backend integration, not an RTL accumulator. +This optional file is run explicitly; the basic tutorial does not require native bindings to execute its teaching model. + +## Replace the teaching DUT with real RTL + +DUT generation, ports, and initialization are project-specific. Implement these mappings in the project environment: + +| Teaching component | Project responsibility | +| --- | --- | +| `TutorialDut()` | Build/create the generated DUT and initialize it | +| `dut.clock` / `dut.backend` | Actual XClock and `XCommClockBackend(clock)` | +| `Bundle(enable=..., operand=...)` | Ports, widths, and idle values | +| SingleCycleDriver | Real acceptance condition and input timing, or a custom Driver | +| `dut.response is not None` | Response validity and sampling point | +| Integer snapshot | Immutable project response record | +| `AccumulatorModel.accept()` | Functional specification, initial state, and updates | +| Three-cycle response budget | Allowed timing from the specification | +| `dut.close()` | Actual resource cleanup | + +Keep the input/wait/check structure, but define protocol, reset, and flush rules explicitly. +Do not copy the tutorial's one-cycle enable rule to unrelated interfaces. + +## Choose your next task + +| Task | Example or guide | +| --- | --- | +| Multicycle inputs and overlapping stages | [Pipeline examples](../guides/pipeline-examples.md) | +| Passive observation | [Monitors](../guides/monitors.md) and [Agents](../guides/agents-and-reference-models.md) | +| Conditions and timeouts | [Triggers](../guides/triggers.md) | +| Fixed-latency output association | [Scoreboards](../guides/scoreboard.md) | +| Seeds, coverage, and pytest reports | [Verification flow](../guides/verification-flow.md) | +| Existing generated cache DUT | [Cache environment](https://github.com/XS-MLVP/toffee/blob/main/examples/integration/cache/README.md), with extra dependencies | +| Full e203 RTL and coverage flow | [e203 environment](https://github.com/XS-MLVP/toffee/blob/main/examples/integration/e203/README.md) | + +For the repository's complete checks: + +```bash +python3 -m pytest -q --require-xspcomm +python3 -m pip install -e '.[docs]' +mkdocs build --strict +``` + +`--require-xspcomm` fails when native bindings are unavailable, preventing skipped native tests from +being mistaken for passing coverage. It is a repository test option, not required by the earlier tutorial commands. +Return to the [tutorial](index.md) or [feature map](../reference/feature-map.md). diff --git a/docs/en/getting-started/out-of-order.md b/docs/en/getting-started/out-of-order.md new file mode 100644 index 0000000..7094a7d --- /dev/null +++ b/docs/en/getting-started/out-of-order.md @@ -0,0 +1,77 @@ +# Optional: match responses that return out of order + +Complete [pipelined submission](pipeline.md) first. FIFO matching works only when responses follow +acceptance order. An interface that reorders responses must expose an association key, such as a tag; +the framework cannot infer association from response values. + +## Use the tagged accumulator variant + +`examples/transactions/reference_model.py` provides a separate variant without changing `TutorialDut`. +It accepts positive operands with tags 1, 2, and 3; tag 0 means idle. This example does not overflow. +Results are computed in **acceptance order**, with deliberately different response latencies: + +| tag | operand | Expected total | Acceptance cycle | Latency | Response cycle | +| --- | --- | --- | --- | --- | --- | +| 1 | 3 | 3 | 1 | 4 | 5 | +| 2 | 5 | 8 | 2 | 2 | 4 | +| 3 | 7 | 15 | 3 | 3 | 6 | + +Response tags arrive as 2, 1, 3, while expected cumulative totals remain 3, 8, 15. +Do not advance the model in completion order. +`Command(tag=1, operand=3)` and `Result(tag=1, total=3)` are immutable dataclass records; +they contain data and do not advance simulation. + +## Configure association + +The variant's Agent sets `request_key=lambda request: request.tag` and +`response_key=lambda response: response.tag`. These functions extract matching keys. +Two outstanding requests must not share the same key. +The Driver writes tag and operand and restores idle after acceptance; the Monitor captures tagged responses. +See the [complete source](https://github.com/XS-MLVP/toffee/blob/main/examples/transactions/reference_model.py). + +## Run the example + +File: `examples/getting_started/test_tutorial_out_of_order.py`. + + +```python +import pytest + +from examples.transactions.reference_model import ( + AccumulatorAgent, AccumulatorDut, AccumulatorModel, Command, Result, +) +from toffee import Execution + + +@pytest.mark.asyncio +async def test_tagged_responses_arrive_out_of_order(): + dut = AccumulatorDut("memory") + agent = AccumulatorAgent(dut, response_timeout_cycles=5) + agent.connect(AccumulatorModel()) + try: + async with Execution(dut.backend, agents=[agent]): + first = agent.submit(Command(tag=1, operand=3)) + second = agent.submit(Command(tag=2, operand=5)) + third = agent.submit(Command(tag=3, operand=7)) + await agent.finish(timeout_cycles=20, observe_cycles=1) + + assert dut.emitted_tags == [2, 1, 3] + assert await first == Result(tag=1, total=3) + assert await second == Result(tag=2, total=8) + assert await third == Result(tag=3, total=15) + finally: + dut.close() +``` + +```bash +python3 -m pytest -q examples/getting_started/test_tutorial_out_of_order.py +``` + +Expect `1 passed`. `first` retrieves tag 1, not whichever response arrived first. +Awaiting handles in submission order does not change the physical response order. +Change the expected emitted tags to `[1, 2, 3]` to confirm that the ordering assertion fails. +Restore it, then try an incorrect first result of `Result(tag=1, total=8)` to trigger a value mismatch. + +Use keys only when the interface carries observable association information. If untagged responses can +reorder, first establish a valid association rule; a Python-only key cannot invent missing hardware information. +See [native integration](native-and-next.md) or return to the [tutorial overview](index.md). diff --git a/docs/en/getting-started/pipeline.md b/docs/en/getting-started/pipeline.md new file mode 100644 index 0000000..cdea8fe --- /dev/null +++ b/docs/en/getting-started/pipeline.md @@ -0,0 +1,133 @@ +# 8. Submit pipelines, multiple batches, and cancellations + +Keep inputs moving while earlier requests wait for responses. Inputs take one cycle to accept and +responses arrive two cycles later, allowing several transactions to be outstanding. + +## Distinguish the waits + +Repeated `await agent.send(...)` already waits only for acceptance, not each response. +`submit()` instead queues work immediately and returns a response handle: + +| Operation | Completion means | Driver requirement | +| --- | --- | --- | +| `await agent.send(3)` | Input accepted; connected response checking continues | Sync or Async | +| `transfer = agent.submit(3)` | Handle returned immediately; acceptance is not guaranteed yet | Async | +| `result = await transfer` | This response arrived and passed checking | Handle from submit | + +Waiting for a handle again retrieves that transaction's result; it does not resend input. +An Agent with a Sync Driver does not support `submit()`. + +## Keep timing, change submission + +Continue in `examples/getting_started/test_accumulator.py`: + + +```python +from toffee import AsyncSingleCycleDriver + + +def make_pipeline_agent(dut, *, response_timeout_cycles=3): + driver = AsyncSingleCycleDriver( + dut.clock, + Bundle(enable=dut.enable, operand=dut.operand), + idle={"enable": 0, "operand": 0}, + encoder=encode_operand, + ) + return Agent( + "accumulator", + driver=driver, + monitors={"response": make_response_monitor(dut)}, + response_monitor="response", + clock=dut.clock, + response_timeout_cycles=response_timeout_cycles, + ) +``` + +`AsyncSingleCycleDriver` reuses the same Bundle, encoder, idle values, and Monitor. +It drives queued requests in order, without the test managing tasks, locks, or `parallel`. +Async does not permit simultaneous writes to the same signals: this interface still accepts one input per cycle. + +## Send three consecutive requests + + +```python +@pytest.mark.asyncio +async def test_three_inputs_in_flight(): + dut = TutorialDut(latency=2) + agent = make_pipeline_agent(dut) + agent.connect(AccumulatorModel()) + try: + async with Execution(dut.backend, agents=[agent]): + first = agent.submit(3) + second = agent.submit(5) + third = agent.submit(7) + + await agent.finish(timeout_cycles=10, observe_cycles=1) + assert await first == 3 + assert await second == 8 + assert await third == 15 + assert dut.accepted_cycles == [1, 2, 3] + assert dut.response_cycles == [3, 4, 5] + finally: + dut.close() +``` + +```bash +python3 -m pytest -q examples/getting_started/test_accumulator.py::test_three_inputs_in_flight +``` + +Expect `1 passed`. The test checks this timing: + +| Rising edge | Newly accepted operand | Response | Outstanding requests | +| --- | --- | --- | --- | +| 1 | 3 | None | 1 | +| 2 | 5 | None | 1, 2 | +| 3 | 7 | 3 | 2, 3 | +| 4 | None | 8 | 3 | +| 5 | None | 15 | None | + +`assert await first == 3` retrieves and checks the first request's result. These assertions appear +after finish to show the handle results; finish reports background failures even if handles are not awaited. + +Try inserting `assert await first == 3` immediately after the first submit. The second request then +waits for the first response, so the consecutive acceptance assertion `[1, 2, 3]` fails. +Restore the example. Waiting for every response after submitting serializes stimulus regardless of Driver type. + +## Reuse one environment for two batches + + +```python +@pytest.mark.asyncio +async def test_two_batches_and_withdrawn_input(): + dut = TutorialDut() + agent = make_pipeline_agent(dut) + agent.connect(AccumulatorModel()) + try: + async with Execution(dut.backend, agents=[agent]): + first = agent.submit(3) + withdrawn = agent.submit(99) + withdrawn.cancel() + await agent.drain(timeout_cycles=10) + assert await first == 3 + + second = agent.submit(5) + await agent.finish(timeout_cycles=10, observe_cycles=1) + assert await second == 8 + assert dut.operands == [3, 5] + finally: + dut.close() +``` + +```bash +python3 -m pytest -q examples/getting_started/test_accumulator.py::test_two_batches_and_withdrawn_input +``` + +Expect `1 passed`. The queued 99 is cancelled before acceptance, so it reaches neither DUT nor model. +`drain()` waits for work submitted before the call and permits a later batch. `finish()` finally closes admission. +The Monitor remains active throughout the environment. +Cancellation here occurs before any await, while the request is still queued. Once accepted, cancellation +raises an error; the response or timeout still needs handling. This does not implement a DUT-specific flush protocol. + +Continue with [failure diagnostics](failures.md), or try [out-of-order responses](out-of-order.md). +For multistage inputs, use the [stage overlap examples](../guides/pipeline-examples.md) and define stage +resources explicitly rather than increasing SingleCycle concurrency. diff --git a/docs/en/getting-started/reference-model.md b/docs/en/getting-started/reference-model.md new file mode 100644 index 0000000..0b88b97 --- /dev/null +++ b/docs/en/getting-started/reference-model.md @@ -0,0 +1,103 @@ +# 7. Compute expectations and share environment setup + +Replace manual arithmetic for long sequences and reuse environment creation, startup, and cleanup. +This lesson introduces a reference model and a pytest fixture. + +## Implement the specification + +Continue in `examples/getting_started/test_accumulator.py`: + + +```python +class AccumulatorModel: + def __init__(self): + self.total = 0 + + def accept(self, operand): + self.total = (self.total + operand) % 256 + return self.total +``` + +`class` defines an object type. `AccumulatorModel()` constructs an instance; `__init__` initializes it. +`self.total` belongs to the model. `accept(operand)` updates that state and returns the expected response. +The model neither reads the DUT nor waits for clocks, and can run independently: + +```python +from examples.getting_started.test_accumulator import AccumulatorModel + +model = AccumulatorModel() +assert model.accept(250) == 250 +assert model.accept(10) == 4 +``` + +Connect it with `agent.connect(model)` before starting the environment. +The Agent advances it **after actual input acceptance**, in acceptance order, not response order. +Queued but unaccepted requests must not advance the model. Tests can then call `send(3)` without an explicit expectation. + +## Reuse setup across independent tests + + +```python +import pytest_asyncio + + +@pytest_asyncio.fixture +async def accumulator(): + dut = TutorialDut() + agent = make_agent(dut) + agent.connect(AccumulatorModel()) + try: + async with Execution(dut.backend, agents=[agent]): + yield agent + finally: + dut.close() + + +@pytest.mark.asyncio +async def test_model_checks_a_sequence(accumulator): + for operand in [3, 5, 7]: + await accumulator.send(operand) + await accumulator.finish(timeout_cycles=10, observe_cycles=1) + + +@pytest.mark.asyncio +async def test_model_checks_wraparound(accumulator): + for operand in [250, 10]: + await accumulator.send(operand) + await accumulator.finish(timeout_cycles=10, observe_cycles=1) +``` + +```bash +python3 -m pytest -v examples/getting_started/test_accumulator.py -k test_model_checks +``` + +`-k test_model_checks` selects two tests; expect `2 passed` and the other tests deselected. +The first checks 3, 8, 15; the second checks 250, 4. A fresh fixture per test prevents the second +from inheriting the first test's total. + +## Understand the fixture lifecycle + +`@pytest_asyncio.fixture` registers `accumulator()` as setup code. A test argument named `accumulator` +receives the Agent supplied by `yield agent`. + +```text +创建 DUT、模型和 Agent + ↓ +进入 Execution + ↓ +yield agent → 执行一条测试 → 测试结束或抛出异常 + ↓ +退出 Execution,再 dut.close() +``` + +`yield` hands control to the test; execution resumes afterwards for cleanup. +Keep `finish` in the test because budgets and observation windows depend on the scenario. +Closed instances cannot be restarted; create a new environment for each test. + +## Introduce a model bug + +Temporarily remove `% 256`. The ordinary sequence still passes; the overflow case reports expected 260 +and actual 4. Here the model is wrong, not the DUT. Restore the remainder operation. +Validate models with short hand-calculated examples. The teaching DUT sums input history while the model +updates state incrementally; they do not call one another. +Continue with [pipelined submission](pipeline.md). diff --git a/docs/en/guides/agents-and-reference-models.md b/docs/en/guides/agents-and-reference-models.md new file mode 100644 index 0000000..a2426da --- /dev/null +++ b/docs/en/guides/agents-and-reference-models.md @@ -0,0 +1,170 @@ +# Agents and reference models + +An Agent groups an interface's Driver, Monitors, and checking configuration. +The environment constructs it, connects a model, and registers it with Execution. +Tests send requests and check results without rewiring components: + +```python +model = AccumulatorModel() +agent = AccumulatorAgent(dut, response_timeout_cycles=5) +agent.connect(model) + +async with Execution(dut.backend, agents=[agent]): + transfers = [agent.submit(command) for command in commands] + await agent.finish(timeout_cycles=30, observe_cycles=1) + results = [await transfer for transfer in transfers] +``` + +An environment or pytest fixture can own the single Execution context and expose started Agent instances. +`connect()` only wires objects: it neither starts simulation nor advances model state. + +## Responsibilities + +| Layer | Responsibility | +| --- | --- | +| Agent | Interface components, transaction operations, model connection, response configuration | +| Driver | Input timing and explicit acceptance events | +| Monitor | Immutable observations at specified sample events | +| Reference model | Functional state transitions and expected responses for accepted requests | +| Scoreboard | Expectations, association, comparison, timeouts, and residual checks | +| Execution | Registered Agent startup, checks, and resource cleanup | +| Project environment/fixture | DUT, model, initialization, seeds, configuration, and artifacts | + +Models are independently owned. An Agent does not create, close, or reset them; multiple Agents may share one. +Scoreboard remains independently usable for direct checks or special association. + +## Define an Agent + +The runnable [AccumulatorAgent](https://github.com/XS-MLVP/toffee/blob/main/examples/transactions/reference_model.py) +encapsulates its input, sampling, and association rules: + +```python +class AccumulatorAgent(Agent[Input]): + def __init__(self, dut, *, response_timeout_cycles): + monitor = SamplingMonitor( + PythonPredicateTrigger( + "response-present", lambda: dut.output is not None, + sample=RisingEdge(dut.clock), mode="each_sample", + ), + capture=lambda event: dut.output, + ) + super().__init__( + "accumulator", driver=Input(dut), + monitors={"response": monitor}, response_monitor="response", + clock=dut.clock, response_timeout_cycles=response_timeout_cycles, + request_key=lambda request: request.tag, + response_key=lambda response: response.tag, + ) +``` + +Response presence here is this DUT's rule. The framework does not infer ready/valid, reset/flush, acceptance, or latency. +Without key functions, association is FIFO. `latency_cycles` and `sampled` follow Scoreboard semantics. +`compare` customizes comparison; `capacity` limits active transactions and unmatched observations. + +## send, submit, and Driver type + +| Configuration | send | submit | +| --- | --- | --- | +| SyncDriver, no response checking | Drive in caller; return acceptance XEvent | Unsupported | +| SyncDriver with checking | Drive in caller; checker follows the accepted request | Unsupported | +| AsyncDriver, no checking | Submit immediately; return original acceptance handle | Unsupported without response checking | +| AsyncDriver with checking | Submit immediately; return acceptance view; checking continues | Return XTransfer for checked response | +| Passive Agent without Driver | Unsupported | Unsupported | + +SingleCycle variants inherit their corresponding behavior. Agent does not convert a SyncDriver into a background Driver. +Async driving continues to use its original worker. +To wait only for acceptance: + +```python +event = await agent.send(command) +await agent.finish(timeout_cycles=20) # 已配置响应检查时,仍须等响应并检查 +``` + +For responses or multiple outstanding transactions with an AsyncDriver: + +```python +transfers = [agent.submit(command) for command in commands] +await agent.drain(timeout_cycles=30) +# drain 允许后续批次继续提交;Monitor 持续工作。 +more = agent.submit(next_command) +await agent.finish(timeout_cycles=20) +result = await more +``` + +With checking enabled, send changes only the caller's wait endpoint. Its acceptance view is awaitable and +cancellable before acceptance, but does not promise every XTransfer field. Use submit for full transaction state. +Unawaited response failures still reach finish or Execution exit. +Accepted inputs cannot be withdrawn. Cancelling an AsyncDriver acceptance/response waiter does not cancel its transaction. +Cancelling SyncDriver.send interrupts driving in the caller. +With response checking, Driver and checker capacity remain occupied until response completion. +For input-only interfaces, omit `response_monitor`. + +## Advance models on acceptance + +A model is an ordinary Python object: + +```python +class AccumulatorModel: + def __init__(self): + self.total = 0 + + def accept(self, command): + self.total += command.operand + return Result(command.tag, self.total) +``` + +The automatic path consumes Driver acceptance records: AsyncDriver's stream or SyncDriver's returned event. +In notification order it calls `ref.accept(request)`, stores the expected snapshot, and associates the response. + +- Submission alone does not advance the model; cancelling an unaccepted request does not call it. +- Stateful models follow acceptance order, not submission or completion order. +- Each expected response must be an independent snapshot; keys associate reordered responses. +- Explicit `expected=` overrides this comparison value but **still advances the connected model**. It is also usable without a model. +- Model errors reach the transaction, finish, or Execution exit; already observed background errors are not repeated. + +Model entry points must be synchronous; async methods and awaitable returns fail explicitly. +Compute from specification and valid input, not the DUT output being checked. +Remote asynchronous models require project-defined synchronization and budgets. +Standalone `Scoreboard(expected=...)` differs: that predictor runs only when explicit expected is absent. + +## Lifecycle and exclusive consumption + +Execution starts Agents in registration order and closes them in reverse. +An Agent starts Monitors, acquires Driver ownership, then starts response checking. +On exit it checks/stops Scoreboard, closes Driver and Monitors, then releases Execution resources and backend lease. +Partial startup failure cleans up already started components without starting later Agents. + +finish closes admission, drains, observes for the requested window, and checks balances; it does not close the Agent or Monitor. +Execution exit checks and cleans up again without an unbounded drain. +Continued monitoring after the final response detects extra output. +Components must be unstarted and exclusively owned. Duplicate Agents or shared Driver/Monitor objects are rejected. +Closed Agents and built-in Monitors cannot restart. Drivers must also clean up failed internal initialization. + +The response Monitor belongs to the internal Scoreboard; `agent.recv(response_name)` rejects a second consumer. +Other named Monitors support `await agent.recv(name)`. Do not bypass an active Agent to submit checked work or consume its response stream. +There is no public Agent bind, async context, or independent close operation. +Normal exit, failure, and cancellation clean up components while preserving original and independent cleanup errors. +External tasks remain with their owners; callers must finish or cancel concurrent SyncDriver.send tasks before environment exit. + +## Passive and multi-interface models + +Omit Driver for a passive Agent. Automatic connect(ref) requires active Driver acceptance events. +For passive bus checking, consume an input Monitor and explicitly invoke the model and Scoreboard.check; +do not also advance the same model from Driver notifications. +Sharing a model does not define cross-interface hardware order. Specify same-tick arbitration and state updates +in the project's input aggregation layer. Independent commutative operations can connect directly. +Reset, flush, retirement, and commit semantics also belong to the project. +The automatic response path is one accepted request to one response. Zero/multiple/autonomous outputs need an explicit association contract. +None is a valid expected value, not an absence-of-response marker. + +## Run the example + +```bash +python3 -m examples.transactions.reference_model +python3 -m examples.transactions.reference_model --backend native +``` + +Inputs 3, 5, 7 expect 3, 8, 15, with response tags 2, 1, 3. +DUT and model do not share state or prediction functions, and the model runs independently of Execution. +Regressions cover Sync/Async inputs, batches, cancellation, expected overrides, model errors, shared models, and cleanup. +See [verification flow](verification-flow.md) for coverage and artifacts. diff --git a/docs/en/guides/asyncio-and-pytest.md b/docs/en/guides/asyncio-and-pytest.md new file mode 100644 index 0000000..99197a8 --- /dev/null +++ b/docs/en/guides/asyncio-and-pytest.md @@ -0,0 +1,127 @@ +# Integrate asyncio and pytest + +Toffee uses the running asyncio loop alongside HTTP clients, servers, queues, timers, and other tasks. +This guide focuses on host integration and component authors. Ordinary verification uses Driver submissions, +Monitor/Scoreboard lifecycles, and Trigger waits; [pipelines](pipeline-examples.md) need no manually created concurrency tasks. +There is no generic parallel interface. See the [execution model](../explanation/execution-model.md) for scheduling and ownership. + +## pytest + +```python +import pytest + +from toffee import Execution, RisingEdge + + +@pytest.mark.asyncio +async def test_counter(backend, dut): + async with Execution(backend): + await RisingEdge(dut.clock) + assert int(dut.count.U()) == 1 +``` + +pytest-asyncio provides the loop. Use await directly; see the [fixture tutorial](../getting-started/reference-model.md) for complete tests. + +## Task ownership + +Execution starts and stops registered Agents. AsyncDriver owns input tasks, Scoreboard owns response collection, +and each Monitor has one lifecycle owner. Tasks or TaskGroups created by external libraries remain owned by those libraries; +scheduling observation does not transfer cleanup responsibility. + +AsyncDriver.send submits immediately and returns XTransfer, which is awaitable but is not a coroutine. +Do not pass it to create_task or TaskGroup.create_task. Save the handle and await it where needed. +This does not change SyncDriver or protocol-specific interfaces that return coroutines. + +## External services + +Tasks created inside Execution normally inherit its scheduling domain. +Use `execution.external_task(coro, name=None)` for independent services or models: + +```python +async with Execution(backend) as execution: + task = execution.external_task(fetch_configuration(), name="configuration") + configuration = await task + await FallingEdge(clock) + apply_configuration(configuration) +``` + +It returns a native Task, retains user ContextVars, and clears simulation ownership and the implicit Reactor binding. +Such a task must deliver data back to simulation tasks instead of awaiting XTrigger or implicitly driving the DUT. +Normally inherited child tasks remain external. An existing task cannot be reclassified retrospectively. +Direct awaiting retains asyncio cancellation propagation. The creator must still clean up the task. +To freeze clocks while obtaining configuration: + +```python +async with execution.paused(): + configuration = await execution.external_task(fetch_configuration()) +``` + +External work does not count toward simulation barriers, but shares the loop's observation hooks and their overhead. +See [limitations](../reference/semantics-and-limitations.md). + +## Await external work + +An ordinary coroutine can be awaited directly: + +```python +response = await http_client.get(url) +await RisingEdge(dut.clock) +``` + +Combine DUT and external sources with AnyOf: + +```python +event = await AnyOf( + Value(dut.done, 1, sample=RisingEdge(dut.clock)), + QueueTrigger(control_queue), + WallTimeout(2.0), +) +if event.kind is XEventKind.TIMEOUT: + raise TimeoutError("no DUT result or control message within 2 seconds") +``` + +Simulation advances while DUT Triggers or subscriptions exist. Waiting only for HTTP, a Queue, or a plain Task +with no simulation watchers leaves simulation time unchanged. Persistent Monitors/Scoreboards are watchers, +so the clock can continue while the caller awaits HTTP; pause explicitly when required. + +## Protect an existing task + +TaskComplete wraps results as XEvent and shields the original task: + +```python +from toffee import AnyOf, TaskComplete, WallTimeout, XEventKind + +# reference_task 由外层创建并负责最终 await 或取消。 +event = await AnyOf(TaskComplete(reference_task), WallTimeout(2.0)) +if event.kind is XEventKind.TIMEOUT: + raise TimeoutError("reference task did not complete within 2 seconds") +result = event.value +``` + +Timeout or cancellation of the wait does not cancel `reference_task`; its owner still performs cleanup. +Direct `AnyOf(reference_task, ...)` cancels it when it loses. +Likewise, cancelling a waiter on XTransfer does not cancel the transaction. +Only an unaccepted request can be explicitly cancelled with `transfer.cancel()`; see [Scoreboard](scoreboard.md). + +## Background task cleanup + +```python +import asyncio +import contextlib + +task = asyncio.create_task(run_reference_server()) +try: + async with Execution(backend): + ... +finally: + task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await task +``` + +Execution cleans up its own pump, subscriptions, and registered Agents. Creators close independent background tasks. + +## Worker threads + +Use `asyncio.to_thread()` for blocking functions. Access Backend, XClock, and DUT signals from the Execution thread. +Deliver worker results through asyncio queues or events. diff --git a/docs/en/guides/compiled-triggers.md b/docs/en/guides/compiled-triggers.md new file mode 100644 index 0000000..f629f11 --- /dev/null +++ b/docs/en/guides/compiled-triggers.md @@ -0,0 +1,95 @@ +# Compile conditions, sequences, and state machines + +## Declare an xtrigger + +`@xtrigger` compiles a condition into an engine expression. With XCommClockBackend, the C++ engine +evaluates supported conditions without Python polling each cycle: + +```python +from toffee import RisingEdge, xtrigger + + +@xtrigger(sample=RisingEdge("clk")) +def handshake(dut): + return dut.valid & dut.ready & ~dut.flush + + +event = await handshake(dut) +``` + +Use `&`, `|`, and `~` for Boolean composition, not Python `and`, `or`, or `not`. +Signals, unsigned constants, and comparisons are supported; widths are checked when armed or compiled. +For arithmetic and signed values use explicit conversions and [expression rules](expressions-and-resources.md). +Data bitwise operations use `bit_and/or/xor/not()`; logical operators retain their condition-composition meaning. +The decorator declares a specification; `handshake(dut)` binds it. `sample` can be a phase Trigger or a DUT attribute-path string. + +## Declare a Python predicate + +```python +from toffee import pytrigger + +allowed_values = {1, 2, 3} + +@pytrigger(sample=RisingEdge("clk")) +def model_accepts(dut): + return dut.data.U() in allowed_values +``` + +`@pytrigger` calls a synchronous condition at its specified sample phase. +For asynchronous sources use QueueTrigger, AsyncioEventTrigger, or TaskComplete. + +## Recognize a non-overlapping sequence + +```python +from toffee import Hold, Next, Sequence, Wait, Within + +@xtrigger(sample=RisingEdge("clk")) +def request_then_ack(dut): + return Sequence( + Wait(dut.req & ~dut.flush), + Within(1, 8, dut.ack), + Hold(dut.valid, cycles=2), + ) +``` + +- `Wait(expr)` waits for a condition. +- `Next(expr)` requires the immediately following sample; a miss restarts the sequence. +- `Within(min, max, expr)` waits within an inclusive cycle window. +- `Hold(expr, cycles=N)` requires N consecutive matching samples. +- Each registration has one active match state; Sequence matching is non-overlapping. + +A Within timeout restarts the sequence. A Hold miss clears that step's consecutive count and keeps waiting +at the same Hold. Neither automatically raises an assertion: only a complete match emits an event. +Use an explicit [timeout](triggers.md) when a test requires a failure deadline. Stimulus sequencing can remain ordinary async code. + +## Use an explicit FSM + +```python +from toffee import FSM, State + +@xtrigger(sample=RisingEdge("clk")) +def request_fsm(dut): + return FSM( + start="WAIT_REQ", + states={ + "WAIT_REQ": State().when(dut.req).goto("WAIT_ACK"), + "WAIT_ACK": State() + .when(dut.flush).goto("WAIT_REQ") + .when(dut.ack).trigger("ACK"), + }, + ) +``` + +Branches in a state are evaluated in declaration order. A terminal emits FsmEvent with its name in `event.terminal_state`. +For continuous delivery see [subscriptions](subscriptions.md); for transaction records see [Monitors](monitors.md). + +Within includes both bounds; the first sample after entering the step has age 1. A sample completes at most one step. +The regressions cover early and upper-bound matches, restarts, and interrupted Hold runs: + +```bash +python3 -m pytest -q tests/integration/test_compiled_patterns.py --require-xspcomm +``` + +The same sample tables check match ticks on MemoryBackend and the native engine. +Conditions can also feed [bound coverage](coverage.md), counted continuously in C++. +Coverage registrations have independent match history and do not change ordinary await semantics. diff --git a/docs/en/guides/coverage-classes.md b/docs/en/guides/coverage-classes.md new file mode 100644 index 0000000..f322d2e --- /dev/null +++ b/docs/en/guides/coverage-classes.md @@ -0,0 +1,116 @@ +# Define coverage models with classes + +Small models can use `CoverGroupDef` directly, as shown in [functional coverage](coverage.md). +For larger models or shared binning rules, use class declarations from `toffee.declarative`. +Both forms share counting, merging, and reporting; no separate runner is needed. + +## Samples, coverpoints, and covergroups + +A sample stores one actual observation. A coverpoint classifies it into situations called bins. +A covergroup collects related points and can count their cross combinations. + +This example runs as written: + +```python +from dataclasses import dataclass +from toffee.declarative import Bin, CoverGroup, CoverPoint, Cross, covergroup, coverpoint + + +@dataclass(frozen=True) +class Sample: + state: int + taken: bool + + +@coverpoint +class StatePoint(CoverPoint[int]): + idle = Bin.values(0) + busy = Bin.values(1, at_least=2) + done = Bin.values(2) + path = Bin.transition(0, 1, 2) + + +@coverpoint +class TakenPoint(CoverPoint[bool]): + yes = Bin.values(True) + no = Bin.values(False) + + +@covergroup(schema_id="pipeline.activity", contract="observed-pipeline/v1") +class PipelineCoverage(CoverGroup[Sample]): + state = StatePoint() + taken = TakenPoint() + state_taken = Cross(state, taken) + + +coverage = PipelineCoverage(instance="pipeline0") +coverage.sample(Sample(state=0, taken=False)) +coverage.sample(Sample(state=1, taken=True)) +coverage.sample(Sample(state=2, taken=True)) + +assert coverage.state.count(StatePoint.busy) == 1 +assert coverage.state.count(StatePoint.path) == 1 +``` + +`busy` requires two hits but receives only one, so the model has not reached full coverage. +`path` requires three adjacent samples with values 0, 1, 2; it does not search for all three values +inside a single sample. + +## Where samples come from + +Manual `sample()` suits snapshots or completed transactions already captured by a Monitor. +Use actual observations: intending to test a busy state does not justify sampling `state=1`. + +Fields use matching names by default: `state` reads `Sample.state`. Select a different or nested +field explicitly with `Fields(Sample).select(lambda s: s.state)`. The entire sample is type-checked; +`bool` cannot substitute for an `int` field. + +## Sample signals automatically + +Given bound `dut.state`, `dut.taken`, and `clock` signals: + +```python +from toffee import RisingEdge +from toffee.declarative import Fields, wire + +fields = Fields(Sample) +coverage.bind( + trigger=RisingEdge(clock), + fields=( + wire(fields.select(lambda s: s.state), dut.state, source_id="dut.state"), + wire(fields.select(lambda s: s.taken), dut.taken, source_id="dut.taken"), + ), + strategy="native", +) +``` + +Pass `coverage.runtime` to `Execution(backend, coverage=[coverage.runtime])` to start sampling on +entry. `source_id` identifies each field's signal for binding review and report merging. +Automatic signal sampling supports `int` and `bool`; booleans require one-bit signals. + +`strategy="native"` requires native execution and rejects unsupported bindings. `"auto"` permits +whole-group Python fallback and records its reason in the report. + +## Reuse models and inspect definitions + +Subclasses can add bins to a coverpoint, and a covergroup can replace its original point with +that subclass. Existing instances and base classes remain unchanged. Inherited crosses expand +against the new model. Combine independent declarations with `CoverageFragment[Sample]`. + +After compilation, do not mutate class attributes or sample annotations; define a new class or +subclass instead. `PipelineCoverage.compile().explain()` lists fields, bins, targets, and crosses. +`diff()` compares compiled models. Changing definitions or sampling rules may make older reports +incompatible for merging. + +## Run the complete example + +From the repository root: + +```bash +python3 -m examples.coverage.declarative_pipeline --backend manual +python3 -m examples.coverage.declarative_pipeline --backend native --output-dir output/classes +``` + +See [declarative_pipeline.py](https://github.com/XS-MLVP/toffee/blob/main/examples/coverage/declarative_pipeline.py). +For latency and out-of-order behavior by request ID, see [transaction coverage](coverage-transactions.md). +For conditions spanning cycles, see [temporal coverage](coverage-patterns.md). diff --git a/docs/en/guides/coverage-patterns.md b/docs/en/guides/coverage-patterns.md new file mode 100644 index 0000000..791d550 --- /dev/null +++ b/docs/en/guides/coverage-patterns.md @@ -0,0 +1,106 @@ +# Count events across cycles + +Ordinary bins inspect one sample. Temporal bins recognize events over time, such as a response +within four cycles of acceptance or a condition held for two samples. Reuse `@xtrigger` for these +patterns. If Sequence and FSM are new to you, start with [compiled conditions and timing](compiled-triggers.md). + +## A request/response example + +This definition comes from the [complete example](https://github.com/XS-MLVP/toffee/blob/main/examples/coverage/trigger_bins.py). +`ProtocolBundle` groups already-bound request, response, and clock signals; it is not a new signal wrapper. + +```python +from toffee import RisingEdge, Sequence, Wait, Within, xtrigger +from toffee.declarative import Bin, SignalCoverGroup, TemporalCoverPoint, covergroup +from toffee.ir import signal_expr +from examples.coverage.declarative_protocol import ProtocolBundle + + +@xtrigger(sample=RisingEdge("clock")) +def roundtrip(pins: ProtocolBundle, *, tag: int = 1, maximum: int = 4): + return Sequence( + Wait(signal_expr(pins.request.valid) & signal_expr(pins.request.ready) + & (signal_expr(pins.request.tag) == tag)), + Within(1, maximum, + signal_expr(pins.response.valid) & signal_expr(pins.response.ready) + & (signal_expr(pins.response.tag) == tag)), + ) + + +class Completions(TemporalCoverPoint[ProtocolBundle]): + tag_one = roundtrip + tag_one_fast = Bin.pattern(roundtrip.with_args(maximum=2)) + + +@covergroup(schema_id="protocol.completions", contract="roundtrip/v1") +class ProtocolCoverage(SignalCoverGroup[ProtocolBundle]): + completions = Completions() +``` + +`tag_one` uses the default four-cycle window; `tag_one_fast` uses two cycles. A response arriving +three cycles later hits only the former. Counts use the declared class attributes, for example +`coverage.completions.count(Completions.tag_one)`. + +After instantiation, bind the project's pins and clock with +`bind(pins=..., sample=RisingEdge(clock), strategy="native")`, then give `coverage.runtime` to +Execution. For attributes calculated by a Monitor, such as latency or IDs, see +[transaction coverage](coverage-transactions.md). + +## Parameters and terminal states + +`roundtrip(pins, maximum=3)` creates an awaitable Trigger. +`roundtrip.with_args(maximum=3)` stores arguments for later signal binding. Required arguments +must be supplied this way when the definition is used before binding. + +An FSM may have terminal states such as `OK` and `BAD_DATA`. Select outcomes with +`Bin.pattern(definition, terminals=("OK",))`, and wrap erroneous outcomes in `Bin.illegal(...)`. +Reaching an unselected terminal still ends the match without incrementing that bin. + +Within one coverpoint, terminal bins using the same process can share execution state while +keeping separate counts. Different windows or parameters execute separately. +`compile().explain()` shows this sharing. + +## Track overlapping requests + +The default tracks one active match. Enable concurrency with explicit capacity: + +```python +Bin.pattern(roundtrip, overlap=True, max_active=8) +``` + +Three completions in one cycle count three times. Exhausting capacity raises an error and marks +collection incomplete instead of silently dropping events. This does not associate arbitrary +dynamic request IDs with their responses. + +You cannot directly cross `TemporalCoverPoint` instances because their processes may finish at +different times. Form a completed transaction in the Monitor and cross its fields instead. + +## Execution and shutdown + +`strategy="native"` requires native support and fails if capabilities are insufficient. +`"auto"` allows Python fallback, recording the strategy and reason. See +[expressions and resource management](expressions-and-resources.md) for native requirements and types. + +Cancelling an ordinary await does not clear the covergroup's own matching history. Exit its +Execution to stop sampling; use the group's history operations to clear history explicitly. + +## Run and interpret the results + +```bash +python3 -m examples.coverage.trigger_bins --engine memory --strategy python +python3 -m examples.coverage.trigger_bins --engine xcomm --strategy native +``` + +The example sends IDs 1, 2, 3 and receives responses in order 2, 1, 3. + +| Coverage item | Count | Reason | +| --- | ---: | --- | +| `requests.handshake` | 3 | Three requests accepted | +| `completions.tag_one` | 1 | ID 1 returned within four cycles | +| `completions.tag_one_fast` | 0 | ID 1 missed the two-cycle window | +| `completions.tag_two_ready` | 1 | Ready stayed high for two samples after ID 2's response | +| `completions.correct_data` | 1 | ID 1 returned correct data | +| `completions.bad_data` | 0 | No bad data observed | + +An uncovered item may simply mean its scenario did not occur in this run. Assertions and +checkers still determine whether the design behaves correctly. diff --git a/docs/en/guides/coverage-transactions.md b/docs/en/guides/coverage-transactions.md new file mode 100644 index 0000000..731b840 --- /dev/null +++ b/docs/en/guides/coverage-transactions.md @@ -0,0 +1,90 @@ +# Cover transaction latency and out-of-order completion + +“Ready stayed low for two cycles” is signal timing. “Request 7 returned after three cycles” is +a transaction property: it requires retaining the request and associating its response by ID. +Keep these measurements distinct. + +This page uses the [request/response example](https://github.com/XS-MLVP/toffee/blob/main/examples/coverage/declarative_protocol.py) +and the syntax introduced in [class declarations](coverage-classes.md). + +## Observe consecutive samples + +```python +from toffee.declarative import Bin, CoverPoint, OverlapPolicy, coverpoint + + +@coverpoint(overlap=OverlapPolicy.ALLOW) +class ReadyPoint(CoverPoint[bool]): + stalled = Bin.values(False) + accepted = Bin.values(True) + waited_two_then_accepted = Bin.transition(False, False, True) +``` + +Three consecutive valid samples of `False, False, True` hit the final bin once. The first two also +hit `stalled`, and the last hits `accepted`, so overlap is explicitly allowed. + +The example samples only valid requests. Invalid cycles break transition history; matches cannot +span idle gaps. This coverage does not establish that a request received a correct response. + +## Sample when a multi-cycle condition completes + +A group's sampling condition can be a Sequence instead of each clock edge: + +```python +from toffee import RisingEdge, Sequence, Wait, Within, xtrigger +from toffee.ir import signal_expr + + +@xtrigger(sample=RisingEdge("clock")) +def tag_one_roundtrip(pins, *, maximum=4): + return Sequence( + Wait(signal_expr(pins.request.valid) & signal_expr(pins.request.ready) + & (signal_expr(pins.request.tag) == 1)), + Within(1, maximum, + signal_expr(pins.response.valid) & signal_expr(pins.response.ready) + & (signal_expr(pins.response.tag) == 1)), + ) +``` + +This waits for acceptance of request 1, then acceptance of its response within the next 1–4 cycles. +When used as `coverage.bind(trigger=...)`, fields are sampled at response completion. Other IDs +cannot complete this Sequence, and responses outside the window do not hit. + +This matches a fixed ID; it does not remember arbitrary request IDs automatically. For bins with +their own Sequence or FSM, use [temporal coverpoints](coverage-patterns.md). + +## Associate arbitrary request IDs + +The example Monitor captures requests and responses at a stable phase. The Collector then: + +1. Saves ID, opcode, and acceptance time at the request handshake. +2. Uses the response ID to find its original request. +3. Computes latency, checks data, and identifies out-of-order completion. +4. Produces one `CompletedTransaction` and samples it once. + +`TransactionCoverage` bins opcode, latency, data correctness, and completion order, and can cross +opcode with latency. Association happens in Python, not automatically inside native bins. + +A Monitor has one consumer. To check and collect coverage together, dispatch the same record +from that consumer; do not let two tasks compete for `recv()`. Save a snapshot when sampling, +rather than rereading pins later during asynchronous processing. + +## Keep checking independent of coverage + +A missing response produces no completed transaction and no latency sample. A hit in the correct-data +bin does not prove that all requests returned correct results. Checkers or Scoreboards must still +report missing responses, wrong data, unknown IDs, and duplicate responses. + +Changing windows, sampling conditions, or field meanings prevents direct merging with old results. +Switching between Python and native execution alone does not change the meaning of the same data. + +## Run the example + +```bash +python3 -m examples.coverage.declarative_protocol --engine memory --strategy python +python3 -m examples.coverage.declarative_protocol --engine xcomm --strategy native \ + --output output/transactions +``` + +The example reports request, completion-time, and transaction coverage separately. Compare what +each counts before applying the structure to your project's Monitor and checkers. diff --git a/docs/en/guides/coverage.md b/docs/en/guides/coverage.md new file mode 100644 index 0000000..9b8d059 --- /dev/null +++ b/docs/en/guides/coverage.md @@ -0,0 +1,479 @@ +# Functional coverage and reports + +Functional coverage answers which situations have been observed and which are missing. +It does not establish design correctness. Start with situations required by the specification, +define bins, and sample when those events actually occur. + +Sample transactions captured by a Monitor manually, or bind signals and a clock for automatic +collection under Execution. This page uses direct definitions. For larger reusable models, see +[class declarations](coverage-classes.md); for events across cycles, see [temporal coverage](coverage-patterns.md); +for latency associated with request IDs, see [transaction coverage](coverage-transactions.md). + +## Define a covergroup + +```python +from toffee import Bin, CoverGroupDef, CoverPointDef, CrossDef + +definition = CoverGroupDef( + "fetch", + ( + CoverPointDef( + "target", + { + "local": Bin.values("local"), + "external": Bin.values("external"), + }, + description="Exercises both fetch routes.", + ), + CoverPointDef( + "response", + { + "ok": Bin.values("ok"), + "error": Bin.values("error"), + }, + description="Exercises success and error responses.", + ), + ), + ( + CrossDef( + "target_x_response", + ("target", "response"), + description="Checks responses on every route.", + ), + ), + description="End-to-end fetch coverage.", +) + +coverage = definition.instantiate("core0.ifu") +coverage.sample( + {"target": "local", "response": "ok"}, + metadata={"seed": 7, "tick": 42}, + details=False, +) +coverage.sample({"target": "local", "response": "error"}) +coverage.sample({"target": "external", "response": "ok"}) +coverage.sample({"target": "external", "response": "error"}) +coverage.assert_coverage(100.0) +``` + +`details=False` accumulates counts and run evidence without returning per-sample bin-hit details. + +The environment can provide run identity, context, and transaction sampling rules without +requiring functional labels on each test case: + +```python +coverage = definition.instantiate( + "core0.ifu", run_id="ci-job-42/shard-1/run-7", + run_metadata={"test": "test_fetch", "seed": 7, "rtl_build": "build-123"}, + contract="ifu.response-observed-before-scoreboard/v1", +) +``` + +Omitting `run_id` generates a unique identity. An explicit ID must identify an independent +collection, not merely a seed: the same seed can run repeatedly. `run_metadata` stores project +context but does not validate RTL/config compatibility. To prevent merging incompatible sampling +semantics, include their stable name and version in the contract. + +Ordinary point/cross hits store each origin's count and first/last metadata in +`provenance[bin_id][origin_id]`, not the complete transaction stream. `origins` associates run IDs, +instances, and run metadata. Manual sampling and Python collectors can retain actual hit locations. +Native counters currently retain run association and counts only: first/last metadata is null, +and synchronization time is never substituted for hit time. Hits without metadata also use null. + +## Bin types + +```python +Bin.values(0, 1) +Bin.transition(0, 1, 2) +Bin.range(0, 15) +Bin.ranges((0, 3), (8, 11)) +Bin.masked(value=0b1000, mask=0b1100, width=4) +Bin.array("bucket", 0, 255, count=16) +Bin.default() +Bin.ignore(Bin.values(7)) +Bin.illegal(Bin.range(12, 15)) +``` + +`at_least=N` requires N hits. `ignore` bins do not contribute to the coverage denominator. +`illegal` raises `IllegalBinError` immediately by default; an instance can instead select the +record policy. Descriptions appear in JSON and HTML without entering the sampling hot path. + +## Adjacent transitions + +`Bin.transition(0, 1, 2)` requires three consecutive samples with those values. For manual +`sample()`, adjacency means consecutive calls; with a clock binding, it means consecutive selected +sampling phases. An intervening state prevents the match. + +Sliding overlap is enabled by default: three samples of 1 hit `Bin.transition(1, 1)` twice. +With `overlap=False`, matching restarts at the sample after completion, giving one hit. +`at_least` always describes a cumulative hit target. A three-sample hold can be expressed as +`Bin.transition(1, 1, 1)`. + +A disabled gate or X/Z in a required field breaks that point's transition history. Ignore bins +change classification, not time: `1, 9, 2` cannot hit `1→2` even if 9 is ignored. Unknown data +appears in the point's `unknown` statistics. + +## Bind signals for Execution to manage + +```python +from toffee import Execution, RisingEdge + +states = CoverGroupDef("states", ( + CoverPointDef("state", { + "busy": Bin.values(1), + "complete": Bin.transition(0, 1, 2), + }), +)).instantiate("core0.pipeline") + +states.bind( + trigger=RisingEdge(clock), + fields={"state": dut.state}, + strategy="native", +) + +async with Execution(backend, coverage=[states]): + await run_case() + checkpoint = states.report() + +CoverageDatabase([states]).write_json("states.json") +``` + +`bind()` returns the same CoverGroup; it does not create another public component. Keys in +`fields` correspond to point sources and Iff field paths. Native sampling supports the full +unsigned width of XData, including values, ranges, masks, transitions, Iff, and same-sample +crosses above 64 bits. Counters remain checked 64-bit integers independently of signal width. +The execution strategy applies to the whole group: + +- `native`: reject unsupported inputs at startup. +- `python`: process each sample in Python explicitly. +- `auto`: prefer native execution, otherwise fall back for the entire group. Reports expose the + selected `sampling_backend` and the reason in `sampling_fallback`. + +Collection does not advance the clock. Tests must still await triggers or perform transaction +operations. Each group has its own counts and history. A group cannot belong to two Executions +at once, or be sampled/merged manually while running. Close the backend only after its Execution +has exited. A group sampled by Execution also cannot receive manual samples afterward; use a +separate instance for transaction sampling so one result does not mix cycle and transaction units. + +Wide values use the same definitions, for example the upper bits of a 128-bit payload: + +```python +high = 1 << 100 +payload_bins = CoverPointDef("payload", { + "exact": Bin.values(high + 1), + "range": Bin.range(high, high + 3), + "mask": Bin.masked(high + 1, high + 3, width=128), + "path": Bin.transition(high + 1, high + 2), +}, overlap="allow") +``` + +Matching and illegal-hit diagnostics preserve upper bits. X/Z in any bit increments `unknown`, +skips ordinary hits, and interrupts adjacent transitions, including mask don't-care bits. +Illegal values are captured at the hit; later signal changes or delayed `sync()` cannot alter +that evidence. Unsigned constants outside a signal's range are not truncated: unreachable +values/transitions do not hit, and ranges/masks retain integer semantics. Signed values and +arithmetic expressions over wide signals are outside this extension; native sampling still +requires direct XData bindings. + +Run the complete example; both runs should produce identical bins and counts: + +```bash +python3 examples/coverage/functional_coverage.py --wide +python3 examples/coverage/functional_coverage.py --wide --native --output wide.json +``` + +## Isolation and synchronization across Executions + +Entering an Execution clears group counts and matching history by default. To retain completed +hits from earlier runs, bind with `accumulate=True`: + +```python +states.bind(trigger=RisingEdge(clock), fields={"state": dut.state}, + strategy="native", accumulate=True) +for scenario in scenarios: + async with Execution(backend, coverage=[states]): + await scenario() +``` + +Matching history remains isolated even when counts accumulate. A trailing 0, 1 in one Execution +cannot combine with a leading 2 in another to hit `0→1→2`. When saving runs separately for later +merging, use default isolation; do not add cumulative snapshots as independent runs. + +| Operation | Counts | Incomplete matches | +| --- | --- | --- | +| `report()` / `sync()` | Synchronize native counts without double counting | Retained | +| `clear_history()` | Retained | Cleared | +| `reset()` | Cleared; native statistics epoch updated | Cleared | +| Execution exit | Final synchronization; counts retained | Cleared; native registrations released | +| Next Execution entry | Cleared by default; retained with `accumulate=True` | Always restart | + +During execution, call `sync()` before reading raw statistics such as `samples`. Reports, +coverage queries, and JSON export synchronize automatically. Synchronize on the Execution's +thread; after exit, the final report is safe to read. Normal exit, test exceptions, external +cancellation, and partial startup failure all release registrations. Snapshot failure does not +stop cleanup or discard the original test exception. + +## Recognize complex patterns with existing triggers + +```python +from toffee import Next, Sequence, Wait, xtrigger + +@xtrigger(sample=RisingEdge("clock")) +def start_then_done(dut): + return Sequence(Wait(dut.state == 1), Next(dut.state == 2)) + +states.bind(trigger=start_then_done(dut), fields={"state": dut.state}, + strategy="native") +``` + +The group samples once whenever the complete pattern finishes, using field values at completion. +The unit of `Bin.transition` becomes consecutive pattern-completion events. Within, Hold, and FSM +can also supply samples. Next requires success at the immediately following sample; otherwise it +waits for a new start. Sequence/FSM are non-overlapping, single-instance by default; coverage +bindings can explicitly enable bounded overlap. Ordinary `await trigger` semantics are unchanged. + +Expression triggers honor their declared `enter`, `each_sample`, or `change` policy. A condition +that stays true samples once on entry with `enter`, or at every valid sample with `each_sample`. + +A project may pass `abort=dut.abort_flag` explicitly and include it in the named fields mapping. +Abort takes precedence over same-sample completion, clearing history while retaining counts. +The framework does not infer signal protocol meanings. `@pytrigger` requires a contract such as +`contract="project-defined-name/v1"` to identify custom semantics and always samples in Python; +the framework does not manage custom closure state. Runs with different sampling rules cannot +be merged or accumulated together. + +Run the existing examples: + +```bash +python3 examples/coverage/functional_coverage.py --stateful +python3 examples/coverage/functional_coverage.py --stateful --native --output state.json +``` + +Native examples require xcomm Python bindings with `XTriggerEngine.CoverageVersion() == 5`. +Rebuild the bindings after changing the C++ backend. With an older version, `native` fails at +startup and `auto` falls back for the whole group. + +## Optional concurrent matching + +To observe new starts while an earlier pattern is still active, enable overlap on the binding: + +```python +from toffee import Sequence, Wait, Within, xtrigger + +@xtrigger(sample=RisingEdge("clock")) +def completion(dut): + return Sequence(Wait(dut.start), Within(1, 3, dut.done)) + +group.bind( + trigger=completion(dut), fields={"value": dut.value}, + overlap=True, max_active=8, + diagnostics="summary", # 独立开关;可以省略 +) +``` + +- `overlap=False` is the default. Active matches reject new starts, and a completion sample is + not reused as a new start. +- `overlap=True` requires an explicit positive `max_active` and supports only compiled + Sequence/FSM patterns. An overlapping Sequence starts with Wait; every sample satisfying its + start can create a match. This does not implicitly become edge detection. +- An FSM instance starts when it leaves the start state. A start-state self-loop creates none. + A direct transition from start to a terminal counts as completion. Returning to start without + reaching a terminal ends the attempt. +- Each instance advances at most one step per sample. Existing instances run before new starts, + so same-sample completion can release capacity. An old instance's completion does not let a new + instance advance a second step in that sample. +- A new start at full capacity raises an error and marks `collection_complete=False`; it is not + silently dropped. Existing counts remain, but `assert_coverage()` rejects incomplete collection. +- Change configuration only outside Execution. Concurrency policy is part of the sampling + contract; different policies cannot be accumulated or merged directly. + +For starts in cycles 1 and 2 followed by done in cycle 3 within the configured window, two +patterns complete and the group samples twice. Both use the same completion condition; this +is not automatic request/response association. Dynamic keys and captured start fields are not +provided here. Use project Monitor/Scoreboard association for those cases. Fields come from the +completion phase, and temporal bins count completion events, possibly several in one cycle. + +Run the full example: + +```bash +python3 examples/coverage/functional_coverage.py --overlap +python3 examples/coverage/functional_coverage.py --overlap --native --output overlap.json +``` + +## Optional diagnostics and live inspection + +`diagnostics="off"` retains only coverage counts and illegal-hit evidence. +`diagnostics="summary"` adds aggregates for complex sampling patterns and `Bin.transition`, +without storing a cycle-by-cycle trace: + +| Field | Meaning | +| --- | --- | +| `started` / `completed` | Matches started / recognized as complete | +| `failed` | Next mismatch, or active FSM returning to start without completion | +| `expired` | Within exceeded its upper bound; success at the bound takes precedence | +| `aborted` | Active matches terminated by the explicit abort condition | +| `cleared` | Active history removed by clear_history, a bin gate, or unknown samples | +| `unfinished_at_close` | Matches still incomplete immediately before Execution exit | +| `peak_active` | Highest active-match count | + +`completed` describes recognition by the matcher, not necessarily an ordinary bin hit: group +gates and ignore/illegal priority still apply. Diagnostics do not affect the coverage denominator; +incomplete or expired matches do not automatically fail a checker. HTML includes a collapsible +section only when summaries were collected. JSON stores them under `diagnostics.patterns`. + +Inspect current state while running, without enabling summaries: + +```python +async with Execution(backend, coverage=[group]): + await ClockCycles(clock, 2) + progress = group.inspect() + # {"tick": ..., "phase": ..., "trigger": [...], "bins": {...}} + # Sequence/transition: step(从 0 起)、age、held;FSM: state 名称。 +``` + +Call `inspect()` on the Execution thread. It returns independent data without advancing time or +changing matching history. It is available only while Execution is active; afterward, use report +summaries. Closing records unfinished counts before freeing state. Normal close and external +cancellation do not count as protocol-level `aborted` events. + +The next Execution resets statistics by default. `accumulate=True` retains counts and diagnostics, +never history. Merging sums event counts and takes the maximum peak. Enabling diagnostics does not +change sampling semantics. Mixed runs expose `collected_runs` / `uncollected_runs`; missing data +is not reported as zero. `clear_history()` retains statistics and records cleared matches; +`reset()` clears counts, diagnostics, and history. + +## Save and merge + +```python +from toffee import CoverageDatabase + +database = CoverageDatabase([coverage]) +database.write_json("functional-coverage.json") + +merged = CoverageDatabase.read_json("run-a.json") +merged.merge(CoverageDatabase.read_json("run-b.json")) +merged.write_json("merged.json") +``` + +Only same-name instances with compatible schema digests and sampling contracts can merge safely. +Each report carries collection origins and a `snapshot_id`. Duplicate origins, duplicate run IDs +for an instance, and overlapping cumulative snapshots raise `CoverageMergeError` without changing +counts. Merge independent final snapshots, not intermediate and final snapshots from one run. +`accumulate=True` retains origins for overlap detection. Reset starts a new origin; when using +explicit run IDs, instantiate a new run with a new ID. + +The database JSON format is version 2; schema versioning is independent. Cross counts use +unambiguous JSON-array encoding for IDs. `bins` stores ID, tuple, label, kind, and `at_least`; +labels are display-only. Readers accept the current format and do not convert older reports. + +## Unified HTML reports + +Convert Verilator coverage to LCOV first. Other simulators producing LCOV use the same entry point: + +```bash +toffee-coverage-report \ + --functional functional-coverage.json \ + --line-coverage Verilator=line-coverage.info \ + --site coverage-report +``` + +The output contains functional group/point/cross pages, each bin's target/count/description, +per-file and per-line RTL coverage generated by genhtml, and shared navigation between functional +and line coverage. See the full +[functional_coverage.py example](https://github.com/XS-MLVP/toffee/blob/main/examples/coverage/functional_coverage.py). + +## Sample transaction fields and apply gates + +`source` accepts dotted paths through mappings, dataclass/object attributes, and BundleValue. +Separate design field names from coverage names for reusable definitions. Iff controls when to count: + +```python +from toffee import Iff + +point = CoverPointDef( + "opcode", {"read": Bin.values(0), "write": Bin.values(1)}, + source="request.opcode", iff=Iff.equals("accepted", True), + weight=2, goal=100, +) +``` + +Iff can gate a CoverGroupDef, CoverPointDef, or CrossDef. `Iff.not_equals()` excludes a condition; +`Iff("mode", (1, 2))` accepts several values. Reports expose gated sample counts in `gated`. +Missing fields are sampling errors, not silently skipped samples. + +## Limit crosses and control overlap + +By default, crosses form the Cartesian product of normal bin IDs. Use include to restrict a large space: + +```python +cross = CrossDef( + "operation_result", ("operation", "result"), + include=(("read", "ok"), ("write", "ok")), + ignore=(("read", "retry"),), + illegal=(("write", "forbidden"),), + at_least=2, max_auto_bins=64, +) +``` + +These strings are declared bin names, not raw input values. `max_auto_bins` limits automatic normal +cross expansion; excessive size raises `CoverageSchemaError` at definition time. A point's +`overlap="warn"` diagnoses overlaps by default, `"error"` rejects them, and `"allow"` permits them. +With overlap allowed, one sample can hit multiple bins and all corresponding cross combinations. + +Priority is illegal, ignore, then normal; default bins record otherwise unmatched values. +Only normal bins enter the denominator. `IllegalHit` retains metadata, and `assert_coverage()` +still rejects illegal hits when `illegal_policy="record"` defers the exception. + +A normal bin entirely excluded by ignore/illegal rules remains in the schema and diagnostic +counts but leaves the denominator, uncovered list, and automatic cross space. Reports explain +this in `excluded_bins`. A partly excluded bin remains: excluding 1 from `{0,1}` leaves 0 as the +required hit. Combined range/mask exclusions need not enumerate values; overly complex analysis +fails at definition time. Transition exclusion follows completion-time priority, including a +statically excluded final value or full coverage by an identical pattern. This does not imply +support for all SystemVerilog transition operators. A point/cross with no eligible normal bin +after exclusion is invalid. + +## Check targets, find gaps, and reuse definitions + +- `point_coverage(name)` / `cross_coverage(name)` return local percentages. +- `coverage` is the weighted normal-bin percentage across points/crosses. Weight zero still + samples but contributes no weight. +- `goal_met` only compares the group's raw weighted percentage with its goal; each item also + reports its own goal result. +- `covered` requires the group target, every positive-weight item's target, complete collection, + and no illegal hits. This is coverage acceptance, not Scoreboard or whole-test success. +- `assert_coverage()` checks those same conditions. `minimum` overrides the group threshold; + child items retain their goals. `per_item=False` checks only the group threshold while still + rejecting incomplete collection and illegal hits. This differs from older default acceptance + behavior; percentages are not normalized by goals. +- `uncovered()` lists bins below `at_least`. Half the required hits do not earn half a bin's credit. +- `sample()` returns CoverageSample by default, including point_bins, cross_bins, and illegal_hits. + Use `details=False` when only accumulating counts on a hot path. +- `reset()` clears an instance's counts and illegal records but keeps its definition. Across runs, + creating a new group usually makes artifacts easier to distinguish. + +Same-name instance merges require matching definition digests and policies or raise +`CoverageMergeError`. Different instances keep separate results; `CoverageDatabase.type_reports()` +provides explicit type-level aggregation. JSON round trips retain definitions, counts, and +descriptions. Renaming is not a substitute for compatible coverage plans. + +## Single-file reports and Python integration + +To share one file: + +```bash +toffee-coverage-report --functional functional-coverage.json --output coverage.html +``` + +`generate_unified_coverage_report(functional_paths=[...], output=...)` also generates self-contained +HTML. `generate_unified_coverage_site(..., output_dir=...)` generates a site. Only sites needing +LCOV source-line pages require genhtml; single-file reports and functional-only sites do not. +The output directory must be empty or carry an existing Toffee site marker. Other nonempty +directories are protected. LCOV source files must be readable where the report is generated; +genhtml controls displayed paths, without forced prefix stripping that changes source lookup. + +Use `parse_lcov()`, `build_unified_coverage_model()`, and `render_unified_coverage_html()` to parse, +combine, and render existing data. Functional bins and simulator line coverage stay separate; +they are not averaged into one verification percentage. pytest retains native pass/fail results. +See [run artifacts](verification-flow.md) for a complete saving recipe and +[functional requirements across test cases](functional-points.md) for coverage planning. diff --git a/docs/en/guides/data.md b/docs/en/guides/data.md new file mode 100644 index 0000000..5f8e976 --- /dev/null +++ b/docs/en/guides/data.md @@ -0,0 +1,115 @@ +# Signals, Bundles, and packed data + +## XData + +DUT signals use XData. Inspect the underlying object with: + +```python +from toffee import as_xdata + +signal = as_xdata(dut.data) +``` + +Binding, width checks, reads, writes, and native conditions all use the same XData identity. + +## Bundle + +Group signals for named access, sampling, and driving: + +```python +from toffee import Bundle + +payload = Bundle( + opcode=dut.req_opcode, + address=dut.req_address, + data=Bundle(lo=dut.req_data_lo, hi=dut.req_data_hi), +) + +snapshot = payload.sample() +payload.drive({ + "opcode": 1, + "address": 0x8000, + "data": {"lo": 0x1122, "hi": 0x3344}, +}) +``` + +`sample()` returns immutable BundleValue snapshots for checking or coverage. +Leaves are LogicValue objects retaining width and X/Z. Convert explicitly when a model needs an integer, +for example `snapshot.opcode.as_int()`. Unknown bits raise instead of silently becoming valid data. +The live `payload` can change; pass `snapshot` into delayed processing rather than rereading pins later. + +## PackedArray and PackedLayout + +An equal-width one-dimensional bus: + +```python +from toffee import PackedArray + +lanes = PackedArray(dut.lanes, element_width=32, count=4) +lanes[2].Set(0x1234) +``` + +Nested and multidimensional layouts: + +```python +from toffee import PackedLayout, PackedView + +entry = PackedLayout.struct({ + "valid": PackedLayout.bits(1), + "tag": PackedLayout.bits(7), + "payload": PackedLayout.array(2, PackedLayout.bits(32)), +}) +entries = PackedView(dut.entries_bus, PackedLayout.array(4, entry)) + +entries[0].drive({ + "valid": 1, + "tag": 3, + "payload": [0x11223344, 0x55667788], +}) +``` + +Views access underlying signals directly. They neither copy simulator storage nor choose sampling time. + +## Bind names or Picker metadata + +Declare sources and expected widths with Field to catch interface changes during binding: + +```python +from toffee import Bundle, Field + +request = Bundle.bind(dut, { + "address": Field("req_addr", width=32), + "payload": {"data": Field("req_data", width=64)}, +}) +address_only = request.view_as({"addr": Field("address", width=32)}) +assert address_only.addr is dut.req_addr +``` + +`Bundle.bind_tree(dut, signal_tree, source_prefix="io")` accepts explicitly supplied Picker metadata, +preserves signal identity, and checks widths. Keywords such as `in` become `in_`; consecutive numeric indices become sequences. +Names do not imply a protocol. Components can enumerate `fields` or `leaves()`. +`Bundle.drive()` requires a complete shape without extra fields; omitted fields are not implicitly held or zeroed. + +## Bit ordering, padding, and four-state values + +`lsb_first=True` places the first field or element at the low bits; False places it at the high bits. +Each array or struct sets its own ordering. Use stride, structure width, and explicit offsets for gaps: + +```python +layout = PackedLayout.struct({ + "opcode": PackedLayout.bits(4, offset=0), + "payload": PackedLayout.array(2, PackedLayout.bits(8), stride=16, offset=8), +}, width=40) +packet = PackedView(dut.packet, layout) +packet.payload[1].Set(0xA5) +``` + +Do not mix automatic and explicit offsets within one struct. Overlap is rejected unless intentional aliasing +uses `allow_overlap=True`. Driving writes leaves and preserves padding. +`split_packed()` constructs an equal-width PackedArray. These views require native XData sub-signal support. + +`LogicValue(value, x_mask, width)` retains aval/bval. Check `is_known` before strict integer conversion. +`as_int(strict=False)` is for diagnostics that explicitly tolerate unknown bits, not a hardware truth value. +`BundleValue.as_dict()` returns a structure of snapshot leaves unaffected by later DUT changes. +Driver ownership checks underlying identity; another view cannot bypass conflicts. +See [Drivers](drivers.md) and [Monitors](monitors.md). diff --git a/docs/en/guides/drivers.md b/docs/en/guides/drivers.md new file mode 100644 index 0000000..9f35c6b --- /dev/null +++ b/docs/en/guides/drivers.md @@ -0,0 +1,119 @@ +# Turn requests into input timing with Drivers + +Tests express operations; Drivers implement signal writes and protocol timing and report the actual acceptance event. + +## Separate submission style from input duration + +Sync Drivers drive while the caller awaits send. Async Drivers submit immediately and return XTransfer handles. +Independently, an input can occupy one cycle or several fixed stages. Input duration is not response latency. + +| Submission | One-cycle input | Fixed multistage input | +| --- | --- | --- | +| Wait for acceptance | SyncSingleCycleDriver | SyncMultiCycleDriver | +| Submit now, await later | AsyncSingleCycleDriver | AsyncMultiCycleDriver | + +Sync does not block the whole event loop; Async is not determined by whether a function uses `async def`. +For dynamic timing, subclass SyncDriver or AsyncDriver and define handshake and acceptance in `_drive_one()`. + +## send and submit + +| Operation | SyncDriver | AsyncDriver | +| --- | --- | --- | +| `driver.send(request)` | Returns a coroutine that has not run | Submits immediately; returns an input handle | +| `await driver.send(request)` | Drives and waits for acceptance | Submits and waits for acceptance | +| `driver.submit(request)` | Unsupported | Submits a transaction whose handle awaits a response | + +send completion means acceptance, not output arrival. A response checker completes submit handles, normally through an Agent. +After complete acceptance, a transaction enters PROCESSING. `cancel()` only withdraws unaccepted work. +Closing with accepted but unfinished transactions raises DriverIncompleteError. + +## Overlap input stages + +`max_active=1` serializes complete input operations. Larger values allow requests to occupy distinct stages, +not to write the same signals simultaneously. +DriveStage(name, bits, idle, encode, cycles) describes each stage; stage signals must be disjoint. +Acceptance occurs on the last cycle of the last stage. +For backpressure or shared ports, use `async with self.resource_lock(name)` in a custom Driver. +Locks with the same name serialize use within that Driver; they do not infer hardware arbitration. +Input overlap and outstanding responses need separate configuration. + +## Restore input state on close + +SignalDriver enforces exclusive signal ownership, direction checks, and idle restoration. +Close active async Drivers with `await driver.aclose()` or an async context: + +```python +from toffee import AsyncSingleCycleDriver, Bundle, Execution + +async with Execution(backend): + async with AsyncSingleCycleDriver( + dut.clock, Bundle(data=dut.data), idle={"data": 0}, + ) as driver: + inputs = [driver.send({"data": value}) for value in (1, 2, 3)] + accepted = [await item for item in inputs] +``` + +This submits three inputs before waiting for acceptance, without waiting for responses. +Project tests normally use an [Agent](agents-and-reference-models.md); see [pipeline examples](pipeline-examples.md). + +## Implement multicycle input + +Use fixed-stage templates when the timing is known: + +```python +from toffee import AsyncMultiCycleDriver, DriveStage + +driver = AsyncMultiCycleDriver( + clock, + ( + DriveStage("address", address, 0, lambda request: request.address), + DriveStage("payload", payload, 0, lambda request: request.payload, cycles=2), + ), + max_active=2, # 两笔请求可以占用不同阶段;同一阶段始终互斥 +) +``` + +SyncMultiCycleDriver accepts the same stages and waits in send. +AsyncMultiCycleDriver.send completes at acceptance; submit delegates response completion to a Monitor/Scoreboard. +Templates drive initial idle and restore the current stage's idle on cancellation or failure. +`max_active` limits concurrent input-driving operations, not accepted requests awaiting output. + +For dynamic stages, define the actual timing; the zero idle values below belong only to this example: + +```python +from toffee import ClockCycles, SyncDriver + +class ThreeCycleInput(SyncDriver[int]): + def __init__(self, clock, data): + self.clock, self.data = clock, data + super().__init__((data,), name="three-cycle") + + async def _drive_one(self, value): + self.data.Set(value) + try: + return await ClockCycles(self.clock, 3) + finally: + self.data.Set(0) +``` + +`_drive_one()` must return the actual acceptance XEvent, not a response or None. +For background dynamic handshakes, subclass AsyncDriver and declare capacity/max_active; +there is no need for a separate pipeline base class. Protect shared signals with resource_lock. + +## SingleCycle options + +Both templates accept a clock, scalar/Bundle/packed/sequence data, idle, and optional synchronous encoder. +The encoder returns a value matching the driven shape. SyncSingleCycleDriver restores its explicit idle after driving or cancellation. +AsyncSingleCycleDriver additionally accepts `idle(last_request)`: None at startup, then the accepted request after each acceptance. +The callable must handle both. Close releases resources without replacing the already computed idle. +To restore initial idle, wait for acceptance and call `driver.quiesce()` before exit; callable idle receives None again. + +```python +driver = AsyncSingleCycleDriver( + clock, data, idle=lambda previous: 0 if previous is None else previous, + encoder=lambda request: request, capacity=8, +) +``` + +Holding the previous input is valid only if the protocol permits it. +Use an [Agent](agents-and-reference-models.md) or [Scoreboard](scoreboard.md) to own response completion for submit transactions. diff --git a/docs/en/guides/execution-and-backend.md b/docs/en/guides/execution-and-backend.md new file mode 100644 index 0000000..c07993d --- /dev/null +++ b/docs/en/guides/execution-and-backend.md @@ -0,0 +1,84 @@ +# Configure Execution and Backend + +Place an existing DUT within a test scope and release resources afterwards. +These snippets assume a project-provided `dut`, registered `dut.clock`, and compatible xspcomm bindings. +For first-time integration, see [native clocks and project DUTs](../getting-started/native-and-next.md). + +## Create and close the backend + +Within an async test, create the backend, enter Execution, and close the backend in `finally`: + +```python +from toffee import Execution, RisingEdge, XCommClockBackend + +backend = XCommClockBackend(dut.GetXClock()) +try: + async with Execution(backend, default_sample=RisingEdge(dut.clock)): + await RisingEdge(dut.clock) + # 在稳定上升沿读取并检查 DUT 输出。 +finally: + backend.close() +``` + +Release the DUT after the backend. Inside Execution use Trigger waits; do not call `dut.Step()` or +run another clock task, even while paused. Execution cleans up its waits and components, while backend and DUT state persist. + +## Reuse a backend in pytest + +```python +import pytest + +from toffee import Execution, XCommClockBackend + + +@pytest.fixture(scope="session") +def backend(dut): + backend = XCommClockBackend(dut.GetXClock()) + try: + yield backend + finally: + backend.close() + + +@pytest.mark.asyncio +async def test_a(backend): + async with Execution(backend): + ... + + +@pytest.mark.asyncio +async def test_b(backend): + async with Execution(backend): + ... +``` + +This requires a session-scoped DUT fixture as well. Match a function-scoped DUT with a function-scoped +backend; see the [fixture tutorial](../getting-started/reference-model.md). +Reusing the backend does not reset the clock or DUT. Run the project's reset sequence after entering Execution when needed. +Construct and connect Agents before registering them with `Execution(backend, agents=[agent])`. +Execution starts components and cleans them up in reverse order. Create fresh Agent instances after exit. +See [Agent wiring](agents-and-reference-models.md). + +## Submit inputs and pause explicitly + +Execution drains ready simulation work and its cascading wakeups before advancing the clock. +AsyncDriver manages background driving without `sleep(0)` handoffs: + +```python +async with Execution(backend), input_driver as driver: + inputs = [driver.send(request) for request in requests] + accepted = [await item for item in inputs] +``` + +Here `input_driver` is an AsyncDriver subclass. Its completion contract depends on the Driver; +see [pipeline examples](pipeline-examples.md). +Draining ready work does not complete external I/O. Explicitly await configuration or initialization. +Use `Execution.paused()` when the clock must remain frozen; do not await a simulation-dependent Trigger +inside that context, since it cannot complete until simulation resumes. + +## Tune scheduling only when needed + +Start with default budgets. For `SimulationNotSettledError`, inspect tick, phase, and pending callbacks, +including possible endless `sleep(0)` loops. Use `external_task` for independent services as described in +[asyncio integration](asyncio-and-pytest.md). Increase `max_settle_rounds` only for a known finite callback chain. +See [Execution reference](../reference/execution.md) and [execution model](../explanation/execution-model.md). diff --git a/docs/en/guides/expressions-and-resources.md b/docs/en/guides/expressions-and-resources.md new file mode 100644 index 0000000..3f450f9 --- /dev/null +++ b/docs/en/guides/expressions-and-resources.md @@ -0,0 +1,59 @@ +# Native expressions and resource management + +XCommClockBackend evaluates supported conditions in xcomm. For installation see +[native integration](../getting-started/native-and-next.md); required commit, package, ABI, and protocol versions +are in the [dependency manifest](https://github.com/XS-MLVP/toffee/blob/main/.github/native-dependencies.json). + +## Separate conditions from data operations + +In a Trigger, `&`, `|`, and `~` combine Boolean conditions. For operations on data bits use +`bit_and()`, `bit_or()`, `bit_xor()`, and `bit_not()`. Do not substitute Python and/or/not. + +```python +from toffee import bits +from toffee.ir import signal_expr + +data = signal_expr(pins.data) +negative = data.as_signed() < 0 +low_byte = data.bit_and(255) +extended = data.as_signed().resize(129) +wrapped = (bits(255, 8) + 1) == 0 +``` + +## Width and signedness + +Python integers do not automatically overflow; native bit vectors have explicit widths. + +| Operation | Semantics | +| --- | --- | +| `bits(value, width, signed=...)` | Declare width and signedness; reject constants outside the range | +| `as_signed()`, `as_unsigned()` | Reinterpret unchanged bits; signed 8-bit `0xff` is -1 | +| `resize(width)` | Extend according to original signedness, or truncate high bits | +| `resize(width, signed=...)` | Extend/truncate first, then apply target interpretation | +| `+`, `-`, `*`, unary minus | Truncate to result width; unsigned 8-bit 255 + 1 is 0 | +| `//`, `%` | Signed division truncates toward zero; remainder follows dividend sign; reject division by zero | +| `<<`, `>>` | Signed right shift extends the sign; reject negative shift counts | +| Comparisons | Typed operands need equal width and signedness; result is Boolean | + +Plain integer constants are checked against the other operand's type. +`data.as_signed() == -1` is meaningful; unsigned data cannot directly compare with -1. +Convert mismatched types explicitly rather than relying on implicit promotion. + +## Unknown values and support + +Arithmetic uses two-state bit vectors, not full SystemVerilog four-state propagation. +Do not use it as an X/Z propagation model. XData/LogicValue four-state access is a separate capability; +see [signals and Bundles](data.md). +Strict native execution rejects unsupported expressions. Coverage models permitting automatic fallback record the reason in reports. + +## Reclaim compiled programs + +The backend caches up to 256 recent programs by default. Configure +`XCommClockBackend(clock, program_cache_capacity=256)`; zero disables caching. +The capacity is neither an active Trigger limit nor a fixed expression-node limit. + +Cache eviction does not invalidate active waits or coverage registrations. +The backend reclaims unused expressions at safe points; `backend.collect_programs()` requests collection +without clearing active Sequence/FSM histories or coverage counters. +Execution releases its registrations but does not close the backend. Close the backend in its owner's `finally`, +and retain DUT signals until no programs reference them. diff --git a/docs/en/guides/functional-points.md b/docs/en/guides/functional-points.md new file mode 100644 index 0000000..583f529 --- /dev/null +++ b/docs/en/guides/functional-points.md @@ -0,0 +1,129 @@ +# Functional requirements, scenarios, and coverage across tests + +Requirements define functional points; test cases provide stimulus. Their relationship is many-to-many: +several cases/seeds can cover one point, and one case can contribute to many points. Toffee does +not require functional labels on tests. Actual observations in the environment produce coverage; +pytest executes tests and records pass/fail results. + +## Turn requirements into observable scenarios + +Define the required behavior, triggering conditions, observation location, and checking evidence +before designing stimulus. For a cache refill: + +| Requirement | Scenario that must occur | Checking evidence | +| --- | --- | --- | +| Start filling at the requested word | All eight starting words | Accepted request address and first array-write address | +| Advance only on acceptance | Acceptance, response bubbles, 7→0 wraparound | Acceptance ledger, counter before/after, and write address | +| Commit metadata only on the final beat | Intermediate beats, bubble before final beat, final acceptance | Metadata write enable and new tag/valid | + +Represent scenarios as CoverPoint bins, crosses, or timing events identified by project observers. +Hits must come from actual transactions or states, not case names, random constraints, or simply +setting a stall parameter. Distinguish data sources. Use an independent model or event ledger +for checking instead of copying RTL equations as the sole oracle. + +A bin hit records that a condition occurred; assertions and Scoreboards check the resulting +behavior. An untriggered condition with no error is not completed verification. A triggered +condition with a checking failure cannot be dismissed because coverage met its target. State +whether sampling happens at input acceptance, DUT output observation, or successful comparison. + +## Centralize sampling in the environment + +Attach collectors to the environment or Agent observation path so applicable cases share them. +Cases configure and generate stimulus without listing functional points they own. Project cycle +observers can sample internal mechanisms; generic Drivers and scheduling need not know protocols. + +The [component and artifact recipe](verification-flow.md) samples when the Monitor delivers a +record and passes that same observation into the Scoreboard, avoiding competing consumers. +Observations already made remain in that run's coverage file if checking fails; failure status +is retained separately. + +A timing scenario must be observed completely within one run. Observing request A in one run and +request B in another does not demonstrate simultaneous contention. Cross merging combines actual +observed tuples; it does not form a Cartesian product of individual hits from different runs. + +## Let several cases contribute + +The runnable [test_pipeline_coverage.py example](https://github.com/XS-MLVP/toffee/blob/main/examples/transactions/test_pipeline_coverage.py) +uses one environment and the same `pipeline.output` instance name: + +| Case | Observed response tags | Coverage | +| --- | --- | --- | +| first, seed 17 | 1, 2 | 2/3 | +| second, seed 23 | 2, 3 | 2/3 | +| Combined runs | 1, 2, 3 | 3/3 | + +```bash +python3 -m pytest -q -s examples/transactions/test_pipeline_coverage.py +``` + +Both cases should pass; neither must reach global 100% alone. Each prints its artifact directory. +Give both `functional.json` files to one reporting command, substituting actual directories for +run-a/run-b: + +```bash +toffee-coverage-report \ + --functional run-a/functional.json \ + --functional run-b/functional.json \ + --output output/coverage.html --site output/coverage-site +``` + +Tag 2 has two merged hits but still contributes one bin. With default `at_least=1`, coverage is +the union of hit sets. With a higher target, sum counts first, then check the target. Do not +average case percentages or use the number of tests as the denominator. + +Check the aggregate goal from Python: + +```python +from toffee import CoverageDatabase + +combined = CoverageDatabase.read_json("run-a/functional.json") +combined.merge(CoverageDatabase.read_json("run-b/functional.json")) +combined["pipeline.output"].assert_coverage(100.0) +combined.write_json("output/combined.json") +``` + +Use a stable instance name for the same DUT location across cases; do not append case names or +seeds. Distinct DUT instances retain separate results; type aggregation requires `type_reports()`. +Incompatible schemas for one instance raise an error. The project decides whether RTL versions, +configurations, and input files belong in one regression; these identities are not automatically +validated. Duplicate or overlapping collection origins are rejected so copied files and cumulative +snapshots cannot inflate counts. Directories, seeds, node IDs, and run status locate evidence; +they do not determine bin hits. + +The recipe records independent identity, test name/seed, and sampling location through instantiate's +`run_id`, `run_metadata`, and contract. Each ordinary merged bin retains origin counts and first/last +metadata, showing which runs contributed. Native counts without actual hit locations do not invent +timestamps. A seed alone is not a run identity, and putting only run_id in sample metadata does not +replace database identity. See [coverage](coverage.md) for formats and acceptance rules. + +## Adjust tests using aggregate gaps + +| Finding | Next step | +| --- | --- | +| Stimulus exists, but no matching collector | Add observation and measure; do not report 0% | +| A legal scenario has a collector but no hits | Adjust constraints or add a directed sequence; check reachability | +| Serial stimulus cannot create contention | Add overlapping inputs, resource contention, or module-level scenarios | +| Scenario hit but checking failed | Retain both coverage and failure evidence; investigate | +| Test skipped or not run | Record this in pytest; keep other runs' contributions | + +Apply acceptance goals to the merged results of the selected regression. A directed case may +also check that its intended scenario occurred; this checks stimulus effectiveness without +requiring every case to complete the entire coverage model. + +## What belongs in a project plan + +The [e203](https://github.com/XS-MLVP/toffee/blob/main/examples/integration/e203/verification_plan.json) +and [Cache](https://github.com/XS-MLVP/toffee/blob/main/examples/integration/cache/verification_plan.json) +plans keep `scope` and `requirements`: design scope, source baseline, stable IDs, required behavior, +observation locations, scenarios, and checking evidence. They support design review; they are not +pytest input and do not compute completion from test labels. Existing checker locations are reading +aids, not coverage ownership rules. + +Internal obligations include state transitions, register load/hold behavior, forwarding, arbitration, +counters, commit, and retirement. e203's 49 and Cache's 77 obligations are connected to project +property observers. Runs record actual activation in `internal-functional-coverage.json`, with +comparison and closure state separately in `internal-checks.json`. Unobserved scenarios remain +unmeasured, and hits do not replace property checking. Reports and evidence remain local, outside +Git. The plans and project checker code define their precise contracts. + +See [coverage](coverage.md) for bins, gates, crosses, and reporting APIs. diff --git a/docs/en/guides/index.md b/docs/en/guides/index.md new file mode 100644 index 0000000..cc97ab4 --- /dev/null +++ b/docs/en/guides/index.md @@ -0,0 +1,45 @@ +# How-to guides + +These guides are organized around project tasks. New users should start with the +[accumulator tutorial](../getting-started/index.md); defaults and constraints belong in the [reference](../reference/index.md). + +## Connect and run simulation + +| Task | Guide | +| --- | --- | +| Create a backend, start tests, and reuse fixtures | [Execution and Backend](execution-and-backend.md) | +| Wait for clocks, signals, timeouts, or external events | [Triggers](triggers.md) | +| Recognize conditions across cycles | [Compiled triggers, Sequence, and FSM](compiled-triggers.md) | +| Receive persistent notifications and capture snapshots | [Subscriptions](subscriptions.md) | +| Integrate HTTP, queues, and background services | [asyncio and pytest](asyncio-and-pytest.md) | + +## Organize verification components + +| Task | Guide | +| --- | --- | +| Bind DUT signals to structured objects | [Signals and Bundles](data.md) | +| Drive single-cycle, multicycle, or overlapping inputs | [Drivers](drivers.md) | +| Capture outputs without missing brief responses | [Monitors](monitors.md) | +| Connect components and stateful models | [Agents and reference models](agents-and-reference-models.md) | +| Compare and associate transactions | [Scoreboards](scoreboard.md) | +| Use ready/valid channel views | [Protocols](protocols.md) | +| Run consecutive and overlapping input stages | [Pipeline examples](pipeline-examples.md) | + +## Coverage and regression + +| Task | Guide | +| --- | --- | +| Define bins, crosses, and goals | [Functional coverage](coverage.md) | +| Reuse class-based coverage models | [Coverage classes](coverage-classes.md) | +| Measure latency and reordering | [Transaction coverage](coverage-transactions.md) | +| Count temporal events | [Temporal coverage](coverage-patterns.md) | +| Turn requirements into observable scenarios | [Functional points](functional-points.md) | +| Connect checks and coverage to an environment | [Verification flow](verification-flow.md) | +| Run parallel tests and diagnose reports | [Runner](runner.md) | + +## Migration and maintenance + +- [Migrate to Toffee 2](toffee-v2-migration.md) from XReactor or Toffee Legacy. +- [Versions, CI, and releases](versioning-and-releases.md) describes version selection and publication checks. + +For complete programs, use the [example index](../examples.md). diff --git a/docs/en/guides/monitors.md b/docs/en/guides/monitors.md new file mode 100644 index 0000000..fbd1176 --- /dev/null +++ b/docs/en/guides/monitors.md @@ -0,0 +1,71 @@ +# Sample and observe with Monitors + +A Monitor captures signals at a specified event and stores the result for later checking, including outputs +that exist for only one cycle. Start with SamplingMonitor and define validity from the design. +Each record is `Transfer(event=event, value=snapshot)`, an observation rather than the XTransfer completion handle returned by submission. + +## Capture and close + +SamplingMonitor synchronously calls `capture(event)` at the explicit Trigger match and retains its tick/phase. +Return Bundle.sample() or another immutable snapshot, not a mutable DUT. Capture cannot be async or return an awaitable. + +```python +from toffee import Bundle, Execution, RisingEdge, SamplingMonitor + +outputs = Bundle(data=dut.data) +monitor = SamplingMonitor( + RisingEdge(dut.clock), capture=lambda event: outputs.sample(), capacity=64, +) +async with Execution(backend) as execution: + async with monitor.start(execution): + first = await monitor.recv() + second = await monitor.recv() +``` + +Sampling every cycle does not mean every cycle has a valid response. Supply a validity Trigger, or for a +fixed-latency datapath use `Scoreboard.bind(latency_cycles=..., sampled=True)` to select target windows. +SamplingMonitor does not infer acceptance; ReadyValidMonitor has its own candidate/acceptance rules. +See [examples](../examples.md) for fixed-latency checking. + +Both built-in Monitors follow these rules: + +- `aclose()` removes subscriptions, watchers, and tasks and discards unread buffers; it does not drain. +- Pending recv callers wake with MonitorClosedError on normal close; async iteration ends normally. +- Capture, decoder, or delivery errors retain their original exception. Once observed through recv, the same error is not repeated on exit. Unobserved errors surface at close; independent cleanup errors are retained. +- Closed instances cannot restart. Multiple batches share one active lifecycle. +- Cancelling one recv waiter does not close the Monitor. Context cancellation cleans the component without cancelling the host task. + +By default, a bound Scoreboard owns Monitor startup, consumption, and close. Do not start or consume it separately. +An [Agent](agents-and-reference-models.md) wires this ownership automatically. +Call bounded finish before cleanup: discarding buffers is not strict completion checking. + +## Choose delivery policy + +| Use | Policy | At capacity | +| --- | --- | --- | +| Transaction checking or per-item coverage | `delivery="lossless"` (default) | Raise MonitorOverflowError | +| UI/telemetry needing only recent values | `delivery="latest"` | Drop oldest records; capacity=1 retains only the latest | + +Monitors are queues, not broadcasts: recv callers compete for records. +When coverage and checking use one observation, the single consumer samples coverage before checking; +see [verification flow](verification-flow.md). + +## Use a passive Agent + +```python +monitor = SamplingMonitor( + RisingEdge(clock), capture=lambda event: output.sample(), + delivery="latest", capacity=1, +) +agent = Agent("telemetry", monitors={"output": monitor}) +async with Execution(backend, agents=[agent]): + observation = await agent.recv("output") + snapshot, tick = observation.value, observation.event.tick +``` + +A known validity rule can use +`PythonPredicateTrigger("output-present", present, sample=RisingEdge(clock), mode="each_sample")`. +A cycle sample and a valid response are different contracts. +If response checking owns the stream, consume transaction results instead of calling recv on that stream. +For protocol-specific candidate sampling, see [protocols](protocols.md). +Custom Monitors implement start/recv/aclose or reuse SamplingMonitor's synchronous capture template. diff --git a/docs/en/guides/pipeline-examples.md b/docs/en/guides/pipeline-examples.md new file mode 100644 index 0000000..c4b7f9e --- /dev/null +++ b/docs/en/guides/pipeline-examples.md @@ -0,0 +1,299 @@ +# Pipelined input and overlapping stages: runnable examples + +This page distinguishes consecutive input submissions, overlapping input-driving processes, +and multiple outstanding responses. The examples use AsyncDriver: send tracks input acceptance, +while submit tracks the later response. Both submit immediately when called and return awaitable +handles. These are this template's capabilities, not requirements for every Driver. SyncDriver +still drives through await send; protocol Drivers define their own send/completion contracts. + +Concrete `_drive_one()` methods define all input timing without inheriting a single-cycle class. +The examples neither use ready/valid or reset/flush nor infer a DUT protocol. + +## Run and select scenarios + +From the repository root: + +```bash +python3 -m pip install -e '.[test]' +python3 examples/transactions/pipeline.py +``` + +The default runs six successful scenarios, printing acceptance, responses, and cycle samples. +[pipeline.py](https://github.com/XS-MLVP/toffee/blob/main/examples/transactions/pipeline.py) contains +the runnable substitute, Drivers, Monitor, Scoreboard wiring, and cleanup. + +| Behavior | Individual argument | Mechanism | +| --- | --- | --- | +| One input each cycle while earlier responses remain outstanding | `--case continuous` | submit, max_active=1, fixed latency | +| Ordered responses with variable latency | `--case fifo` | Default FIFO association | +| Complete inputs traverse two stages serially | `--case serial` | max_active=1 | +| Different requests occupy separate stages | `--case overlap` | max_active=2, per-stage locks | +| Submit each request only after previous acceptance | `--case send` | Sequential await send, serial even at max_active=2 | +| Responses return in reverse order | `--case out-of-order` | request_key / response_key | + +For example: + +```bash +python3 examples/transactions/pipeline.py --case overlap +``` + +With native bindings, the same code runs on real XClock: + +```bash +python3 examples/transactions/pipeline.py --backend native +``` + +This tests native scheduling without RTL. If bindings are in a build directory, configure +PYTHONPATH as described in [installation](../getting-started/installation.md); pytest's local-build +autodiscovery does not apply to ordinary scripts. + +## Explicit substitute behavior + +ToyPipeline samples actually driven signals at every rising edge, rather than treating Python +Driver logs as observations. Its conventions are: + +- Positive values 1, 2, 3 identify requests; 0 is this substitute's idle value. +- A positive ordinary input is accepted and produces `Response(tag=request, value=request * 10)`. +- For staged input, stage one occupies one cycle and stage two occupies three. Complete input + acceptance occurs at stage two's third sample. +- Printed ticks are half ticks. Starting at zero, rising edges occur at 2, 4, 6, and so on. + +The substitute and concrete Drivers define these rules explicitly. Replace them with your design's +acceptance point, output sampling, and idle policy when integrating a real project. + +## 1. Continuous input without waiting for earlier responses + +Input directly subclasses AsyncDriver with `max_active=1`: + +```python +class Input(AsyncDriver[int, Response]): + def __init__(self, toy, *, capacity=8): + self.clock, self.data = toy.clock, toy.first + super().__init__((self.data,), name="input", capacity=capacity, max_active=1) + + async def _drive_one(self, request): + self.data.Set(request) + try: + return await ClockCycles(self.clock, 1) + finally: + self.data.Set(0) +``` + +Each `_drive_one()` handles input acceptance only; Monitor and Scoreboard track the response. +`run_responses()` submits every input before waiting for completion: + +```python +transfers = [board.submit(request) for request in (1, 2, 3)] +status = await board.finish(timeout_cycles=30, observe_cycles=1) +values = [await transfer for transfer in transfers] +``` + +Do not await each `board.submit(request)` inside the submission loop: that would delay each new +input until the previous response. Here, finish drains transactions before handles are read. + +The fixed-latency case explicitly sets `latency_cycles=3`: + +| Cycle | 1 | 2 | 3 | 4 | 5 | 6 | +| --- | --- | --- | --- | --- | --- | --- | +| Input request | 1 | 2 | 3 | — | — | — | +| Response tag | — | — | — | 1 | 2 | 3 | + +Output: + +```text +accepted=[(1, 2), (2, 4), (3, 6)] +responses (tag, tick): [(1, 8), (2, 10), (3, 12)] +``` + +The third input is accepted before the first response returns. Thus max_active=1 does not limit +the design to one outstanding response. The generic serial worker reuses its driving coroutine; +these consecutive inputs have no extra idle cycle. + +## 2. Two input stages: serial and overlapping + +QueuedStages subclasses AsyncDriver with the following driving function. Separate stage locks +prevent different requests from writing the same signals simultaneously: + +```python +async def drive_stages(driver, request): + async with driver.resource_lock("first"): + driver.first.Set(request) + try: + await ClockCycles(driver.clock, 1) + finally: + driver.first.Set(0) + async with driver.resource_lock("second"): + driver.second.Set(request) + try: + return await ClockCycles(driver.clock, driver.second_cycles) + finally: + driver.second.Set(0) +``` + +`resource_lock("first")` protects only stage one; stage two has its own lock. A single lock around +the whole function would serialize complete inputs. + +Resource locks are reused by name within a Driver. The Driver declares resource scope; tests +simply submit inputs. Execution settles ready callbacks and cascading wakeups before advancing +or sampling DriveStable, so stage code needs no added sleep(0). Locks do not specify occupied +cycles or handoff timing; explicit waits and signal operations do. + +Compare: + +```bash +python3 examples/transactions/pipeline.py --case serial +python3 examples/transactions/pipeline.py --case overlap +``` + +| max_active | Request 1 acceptance cycle | Request 2 | Request 3 | +| --- | --- | --- | --- | +| 1 | 4 | 8 | 12 | +| 2 | 4 | 7 | 10 | + +Actual samples in overlap mode: + +| Cycle | Stage one signal | Stage two signal | Complete input acceptance | +| --- | --- | --- | --- | +| 1 | 1 | 0 | — | +| 2 | 2 | 1 | — | +| 3 | 0 | 1 | — | +| 4 | 0 | 1 | Request 1 | +| 5 | 3 | 2 | — | +| 6 | 0 | 2 | — | +| 7 | 0 | 2 | Request 2 | + +At cycle 2, request 2 occupies stage one while request 1 occupies stage two: actual sampled overlap. +Stage two's three-cycle occupancy limits sustained throughput. Increasing max_active does not +make every stage accept once per cycle. + +These cases demonstrate input acceptance only, using send without response-bearing transactions: + +```python +async with Execution(toy.backend), QueuedStages(toy, max_active=2) as driver: + inputs = [driver.send(request) for request in (1, 2, 3)] + accepted = [await item for item in inputs] +``` + +All inputs submit first, and the Driver schedules them by max_active and stage resources. Reading +handles in order afterward does not serialize already-submitted work. At acceptance, a handle +becomes COMPLETED; its result, accepted_event, and completed_event are the same event. Capacity +is released, and the input does not enter the recv_accepted response-association stream. Multiple +batches can share one Driver context. + +Inputs execute even without waiters. Repeatedly awaiting one handle retrieves its result without +resending. Full capacity or an unstarted Driver fails directly at send. To prepare data for later, +store requests and call send only when submission is intended. + +Explicit `item.cancel()` can cancel unaccepted input. Cancelling a waiter does not retract it. +Driving errors propagate through the handle or Driver context exit; already-observed errors are +not duplicated. Context exit cancels unaccepted inputs and reclaims workers without implicitly +waiting for every submission. Await required handles before exit. Response checking owns +completion for submit transactions. + +## 3. Sequential waits versus batch submission + +```bash +python3 examples/transactions/pipeline.py --case send +``` + +Even with max_active=2, this QueuedStages loop sends requests sequentially: + +```python +for request in (1, 2, 3): + await driver.send(request) +``` + +Each new request submits after the previous acceptance, giving ticks 8, 16, 24. Submitting first +and awaiting later gives 8, 14, 20. The Driver manages stage concurrency; callers need not create tasks. + +SyncDriver remains suitable for callers directly awaiting input acceptance and has no background +submission queue. Choose a Driver matching the project's timing and lifecycle. + +## 4. FIFO, fixed latency, and key association + +Only bind's association configuration changes across these `run_responses()` cases: + +| Mode | Association arguments | Actual response latencies | +| --- | --- | --- | +| continuous | `latency_cycles=3` | Three cycles each | +| fifo | None | 3, 3, 4 cycles, in order | +| out-of-order | request_key / response_key | 5, 3, 1 cycles | + +Every mode explicitly sets clock and response_timeout_cycles. Fixed-latency mode checks the target +cycle proactively, rather than merely matching a response if it eventually arrives. + +Out-of-order configuration: + +```python +board.bind( + execution, driver=driver, monitor=monitor, clock=clock, + response_timeout_cycles=6, + request_key=lambda request: request, + response_key=lambda response: response.tag, +) +``` + +Requests accept in order 1, 2, 3, while responses arrive 3, 2, 1: + +```text +accepted=[(1, 2), (2, 4), (3, 6)] +responses (tag, tick): [(3, 8), (2, 10), (1, 12)] +``` + +Each XTransfer receives its own tag's response. Reading handles in submission order after submitting +all requests does not change completion order. Outstanding keys must be unique; duplicate tags +cannot provide unambiguous association. + +## 5. Failure demonstrations + +These commands **are expected to exit nonzero and report exceptions**. They are excluded from +the default successful scenarios: + +```bash +python3 examples/transactions/pipeline.py --case missing +python3 examples/transactions/pipeline.py --case late +python3 examples/transactions/pipeline.py --case wrong-fifo +python3 examples/transactions/pipeline.py --case capacity +``` + +| Case | Deliberate setup | Expected failure | +| --- | --- | --- | +| missing | Accepted request never responds; two-cycle budget | ScoreboardTimeoutError, operation=response | +| late | Required latency 3, actual response scheduled at 4 | ScoreboardTimeoutError, operation=fixed_latency, immediately on missing the target window | +| wrong-fifo | Reverse response order with FIFO association | ScoreboardMismatch: expected tag 1, received tag 3 | +| capacity | Submit three requests with Driver capacity 2 | RuntimeError: submission capacity is full | + +Failure paths still exit Scoreboard, Driver, and Execution and close the Backend. Ignoring transfer +awaits cannot hide background errors: finish or context exit propagates them. + +## Keep parameters distinct + +| Parameter | What it limits | Example | +| --- | --- | --- | +| `max_active` | Concurrent `_drive_one()` input processes | 1 for continuous input, 2 for overlapping stages | +| Driver `capacity` | Submitted but unfinished work: queued, driving, or awaiting response | Default 8; full submission raises instead of waiting | +| Scoreboard `capacity` | Active transactions and unmatched observations | Default 64, checked independently of Driver capacity | +| `latency_cycles` | Fixed acceptance-to-response window | 3 for continuous | +| `response_timeout_cycles` | Response budget after acceptance | 6 for successful cases; an earlier fixed-latency window still applies | +| `finish(timeout_cycles=...)` | Total drain and trailing-observation budget | 30, including observe_cycles=1 | + +Capacity is not pipeline depth, and max_active is not the maximum outstanding response count. +See [Scoreboard](scoreboard.md) for cancellation and completion semantics. + +## Automated verification + +```bash +python3 -m pytest -q tests/integration/test_pipeline_examples.py --require-xspcomm +``` + +MemoryBackend and native XClock regressions check bubble-free input, three outstanding responses, +success at a fixed-latency deadline, sampled timings at max_active=1/2 for both example Drivers, +stage exclusion, variable-latency FIFO, reverse key association, and all four expected failures. +They also check tasks, watchers, backend leases, idle signals, and native callback cleanup. + +`tests/integration/test_driver_handoffs.py` additionally checks cascading lock handoffs without +manual yields, writes before DriveStable, concurrency-slot handoff, fairness, cancellation, errors, +and external tasks not blocking simulation. + +Tests reuse these Drivers and substitutes instead of maintaining approximations. Wall-clock +timeouts only prevent hangs; missing/late responses must fail through simulated cycle budgets. diff --git a/docs/en/guides/protocols.md b/docs/en/guides/protocols.md new file mode 100644 index 0000000..d7739e6 --- /dev/null +++ b/docs/en/guides/protocols.md @@ -0,0 +1,60 @@ +# Optional protocol views + +Generic Drivers, Monitors, and Agents do not require ready/valid. Use these helpers only for interfaces +that implement that protocol; other interfaces define acceptance in their own components. + +## ReadyValid + +```python +from toffee import Bundle, ReadyValid, Role + +producer = ReadyValid( + clock=dut.clock, + valid=dut.valid, + ready=dut.ready, + bits=Bundle(data=dut.data), + role=Role.PRODUCER, + name="request", +) +``` + +Roles are from the testbench's perspective: + +- PRODUCER drives valid/bits and observes ready. +- CONSUMER drives ready and observes valid/bits. +- MONITOR only observes. + +ReadyValid.fire is a compiled Trigger sampled at DriveStable and supports persistent subscriptions. + +## Capture and drive + +ReadyValidMonitor captures an immutable payload at a fire candidate and delivers Transfer after confirmation +at the corresponding rising edge. Its decoder receives a snapshot, not live pins. +This differs from SamplingMonitor's direct capture at its supplied Trigger. +See [Monitor](monitors.md) for lifecycle, delivery policy, and errors. +Use drive_ready_valid inside a project SyncDriver or AsyncDriver subclass: + +```python +async def _drive_one(self, request): + return await drive_ready_valid( + self.port.clock, self.port.valid, self.port.ready, + lambda: self.port.bits.drive(request), + bits=self.port.bits, + ) +``` + +The subclass still declares its driven signals. The helper drives payload/valid at falling-stable, +waits for ready at DriveStable after same-phase writes settle, and deasserts valid after acceptance. +The drive callback must synchronously return None. `bits=` commits Picker Rise-mode payload before sampling, +including when ready depends on that payload. The helper also commits/deasserts Rise-mode valid. +There is no built-in ReadyValidDriver and no implicit ready/valid timing in generic Drivers. + +Optional `admit=lambda: bool(...)` runs in the actual drive slot before encoding. +False performs no encoding or payload/valid write and retries next slot. +True is followed immediately by drive without an intervening await. +The callback must synchronously return bool; errors propagate to the send task. +Admission controls starting a request, not ready: once started, payload stays held until protocol acceptance. +The caller owns wait budgets and cancellation. + +Decoupled aliases ReadyValid. flipped() creates an opposite-role view without copying or reconnecting signals. +A role/direction mismatch fails during binding or acquisition of drive ownership. diff --git a/docs/en/guides/runner.md b/docs/en/guides/runner.md new file mode 100644 index 0000000..195a1a1 --- /dev/null +++ b/docs/en/guides/runner.md @@ -0,0 +1,107 @@ +# Run tests and aggregate coverage + +This guide runs a project regression: select tests, collect coverage, read results, and investigate +collection failures. Tests should already run independently under pytest. Install runner dependencies +in the source checkout first: + +```bash +python3 -m pip install -e '.[runner]' +toffee run tests/ --workers 8 --output output/regression-001 +``` + +Replace `tests/` with your project's test directory and choose a new `--output` directory. +The terminal prints the artifact directory at completion. When coverage exists, open `coverage.html` +and also check pytest's result and the command exit code. See [Runner reference](../reference/runner.md) +for all options and file definitions. + +## Select tests and worker count + +```bash +# 只运行名称中含有 prediction 的测试。 +toffee run tests/ --workers 8 --output output/prediction-001 -k prediction + +# 单进程诊断,仍然使用相同的收集和报告流程。 +toffee run tests/ --workers 1 --output output/serial-001 +``` + +Options such as `-k`, `-m`, `-q`, and `--tb=short` pass through to pytest. `--collect-only` lists tests +without creating a measurement directory. The `--output` path must not already exist, keeping runs separate. + +## Collect functional coverage + +Create covergroups following the [coverage guide](coverage.md), and sample observed events. +Tests do not need to create a collector: the Runner captures and merges data automatically. +To save the final functional coverage database, add: + +```bash +toffee run tests/ --workers 8 --output output/export-001 --export-coverage-json +``` + +Existing calls to `write_json()` still write their files. After adopting automatic collection, +remove manual exports used solely for cross-case aggregation to avoid redundant copies. + +### Exclude model self-tests from DUT coverage + +Coverage-model unit tests may fill bins artificially to test classification. Mark them explicitly: + +```python +import pytest + + +@pytest.mark.toffee_functional_coverage(False) +def test_coverage_definition(): + # 在这里测试分桶或合并规则,不计入正式功能覆盖率。 + ... +``` + +The marker covers fixture setup, the test body, and teardown, without disabling native DUT coverage. +Do not put artificial samples and real DUT samples into one session-scoped shared group. + +For a helper group used only by local assertions, call +`definition.instantiate("helper", capture=False)`. It still supports sampling, checking, and manual +export, but does not enter automatic aggregation. + +### Add a project check after merging + +Implement `pytest_toffee_coverage` in a project plugin to check aggregate coverage goals. Read the +merged results in the hook and raise on failure; the Runner retains available inputs and reports +failure. See [Runner reference](../reference/runner.md) for its signature and timing. Ordinary +regressions do not require this hook. + +## Collect native DUT coverage + +Let fixtures initialize and call `Finish()` on Picker/Verilator DUTs. Return the DUT, an Agent with +a `dut` attribute, or a factory retaining objects in `_instances`. Ensure the generated package +provides the [discovery interfaces](../reference/runner.md). + +If a factory closes and releases DUTs during the test, register them at creation through the +`toffee_register_dut` fixture callback so teardown can still locate them. A standalone clock +object cannot locate DUT export files. + +Native counts merge by DUT build identity. To select a particular coverage processor: + +```bash +toffee run tests/ --output output/native-001 \ + --native-coverage-tool /path/to/verilator_coverage +``` + +The Runner does not drive simulation clocks or close Backends. Retain project fixture cleanup. + +## Locate results and diagnose failures + +Start with pytest's errors, then inspect this run's `manifest.json`. + +| Symptom | Next step | +| --- | --- | +| Output directory already exists | Choose a new directory; preserve the earlier evidence | +| Manifest is `committed`, but tests FAIL | Follow assertion or Scoreboard diagnostics; use coverage to locate scenarios | +| Manifest is `incomplete` | Read `errors` and worker logs; check crashes, incomplete collection, or incompatible models; preserve inputs | +| `cleanup_error` exists | Check permissions and open files; committed final artifacts remain valid | +| Tests pass without `coverage.html` | Check that groups were created and sampled, or that native DUT discovery succeeded | + +To retain successful runs' per-process intermediate data for comparison, add `--keep-coverage-inputs` +to a new run. Aggregation failures already preserve available inputs without requiring a rerun. +See [Runner reference](../reference/runner.md) for file and state definitions. + +Report completeness and DUT checking success are separate questions; see +[checking, coverage, and regression results](../explanation/coverage-results.md). diff --git a/docs/en/guides/scoreboard.md b/docs/en/guides/scoreboard.md new file mode 100644 index 0000000..35fe511 --- /dev/null +++ b/docs/en/guides/scoreboard.md @@ -0,0 +1,149 @@ +# Scoreboards and pipelined transactions + +Scoreboard.check compares supplied values. Its asynchronous mode consumes acceptance and observation streams, +associates records, and reuses the same comparison. Handshake, reset, and flush rules remain project-defined. + +## Choose an operation + +| Goal | API | Completion | +| --- | --- | --- | +| Compare available values | `check(expected=..., actual=...)` | Immediate comparison without binding | +| Wait for input acceptance | `await driver.send(request)` | Acceptance event | +| Submit input only | `async_driver.send(request)` | Immediate handle completed at acceptance | +| Submit and check response | `scoreboard.submit(request)` | Immediate XTransfer completed by checker | +| Own response processing | `driver.submit(request)` | Caller associates and completes handle | +| Wait for one response | `await transfer` from submit | Checked response or failure | +| Finish a batch, allow another | `drain(timeout_cycles=...)` | Work submitted before the call | +| Finish submissions and check | `finish(timeout_cycles=..., observe_cycles=...)` | Drain, finite observation, residual checks | +| Exit or clean up | context / `aclose()` | Check current state and release resources; no DUT drain | + +Transfer is an observation; XTransfer tracks acceptance/completion. +SyncDriver.send runs in its caller; AsyncDriver.send and submit immediately submit input. +These are AsyncDriver guarantees, not universal requirements on Driver/SignalDriver subclasses. +Input-only handles complete at acceptance without a fabricated response or recv_accepted entry. +If DUT responses require checking, use scoreboard.submit. send does not suppress real output or hide unmatched observations. + +## Compare directly + +```python +from toffee import Scoreboard + +scoreboard = Scoreboard("prediction") +scoreboard.check(expected=wanted, actual=prediction) +``` + +Default comparison recursively handles dataclasses, mappings, sequences, BundleValue, and scalars. +ScoreboardMismatch derives from AssertionError and includes field paths, expected/actual values, and context. +Differences are computed once, retaining at most `max_differences=20` by default. +Supply `compare(expected, actual)` or override compare for custom rules; custom assertions remain the exception cause. + +## Bind a response path + +```python +async with ( + Execution(backend) as execution, + prediction_driver as driver, + Scoreboard( + "prediction", expected=lambda request, _: model.predict(request), + ).bind( + execution, driver=driver, monitor=prediction_monitor, + clock=clock, response_timeout_cycles=20, + ) as scoreboard, +): + for request in requests: + scoreboard.submit(request) + status = await scoreboard.finish(timeout_cycles=200, observe_cycles=2) +``` + +Run inside an active [Execution](execution-and-backend.md). Pass clock and response_timeout_cycles explicitly. +A Scoreboard binds once. The Driver supplies submit/recv_accepted and the Monitor supplies start/recv/aclose. +Scoreboard exclusively consumes and normally manages the Monitor; the outer scope manages Driver lifetime. +An [Agent](agents-and-reference-models.md) performs this wiring internally. +With `manage_monitor=False`, the outer scope starts the Monitor first and closes it after Scoreboard; +consumption remains exclusive, and two Scoreboards cannot share the stream. + +Monitoring starts on context entry or first submission and stays active across batches until aclose. +Matching the final transaction neither stops monitoring nor invokes Driver idle/reset operations. +Without a context, close in finally. +Acceptance and observation events must be rising-stable events of the specified single XClock. +Deliver captured records promptly, without waiting for unrelated HTTP, wall delay, or future simulation events. +Budgets use half-tick timestamps. At the falling barrier following the deadline's rising phase, the checker +pauses progress and drains ready source consumers, so correct deadline-cycle responses beat timeout. + +## Predict and await responses + +```python +# 向量给出预期值;优先于 expected source。 +transfer = scoreboard.submit(request, expected=wanted) + +# 只有控制流需要结果时才等待;逐次等待会使激励串行化。 +actual = await transfer +``` + +The expected source runs when acceptance records are consumed, in notification order. +CheckContext.accepted_event retains the real acceptance time. Missing both explicit expected and a source raises MissingExpectedError. +Do not mutate requests or expectations after submission. +The predictor must be synchronous. Explicit expected skips this standalone predictor, so do not mix that shortcut +with a predictor used as a stateful model. Agent.connect advances its model independently of overrides. + +Transactions progress PENDING → PROCESSING → COMPLETED, or FAILED on error. +Only PENDING work can be cancelled. Cancelling a waiter neither cancels the transaction nor observes its eventual failure. +Abnormal close marks accepted unfinished transactions FAILED, not CANCELLED. + +## Associate observations + +- FIFO matches valid responses in acceptance order. +- request_key/response_key permits reordering. Duplicate active keys raise ScoreboardAssociationError. +- `latency_cycles=N` matches exactly N cycles after acceptance, including N=0. Missing that window times out immediately; N must not exceed the response budget. + +By default each observation is valid output; extra, duplicate, or unassociated output fails strict finish. +For a Monitor sampling every cycle including idle, use `latency_cycles=N, sampled=True` to ignore samples outside target windows. +sampled requires fixed latency and cannot combine with key matching. +A custom synchronous, side-effect-free `associate(observation, pending)` returns an entry or None to buffer the observation. +It does not run models, compare, or await events, and can handle Python observation delivery before the matching acceptance notification. + +## Budgets and cleanup + +| Budget | Starts at | Covers | +| --- | --- | --- | +| response_timeout_cycles | Input acceptance | Response; fixed latency uses its earlier exact window | +| drain/finish timeout_cycles | Method call | Queued/unaccepted work and draining; finish also includes observation | +| asyncio.timeout(seconds) | Context entry | Caller-selected wall time | + +`drain` captures a submission watermark; later submissions do not extend that wait. +`finish` closes admission, drains, observes M cycles (default 0), and checks conservation. +Its total budget includes observation; no later submissions are allowed. +`aclose` processes ready notifications, checks state, and releases resources without waiting for the DUT. +Unfinished requests raise ScoreboardIncompleteError; unmatched output raises ScoreboardAssociationError. + +## Diagnose timeout and background failure + +ScoreboardTimeoutError derives from both ScoreboardError and TimeoutError. +Inspect structured attributes instead of parsing messages: + +| Attribute | Meaning | +| --- | --- | +| name, operation | Checker name; response / fixed_latency / drain / finish | +| budget_cycles, elapsed_cycles | Effective budget and elapsed time, possibly fractional | +| start_tick, deadline_tick, current_tick | Half-tick timestamps; a late response uses observation time | +| context | Response CheckContext with sequence_id, request, accepted_event; None for overall budgets | +| status | Diagnostic counter snapshot | +| reason | Deadline exceeded or insufficient remaining time for final observation | + +Acceptance at tick 20 with a four-cycle budget sets tick 28 as deadline. Detection at falling tick 29 +reports 4.5 elapsed cycles. An observation window that cannot fit the remaining budget fails immediately, +possibly before elapsed time reaches the budget. +Errors reach pytest through transfer waits, drain, finish, or close; an already observed error is not repeated. +Independent cleanup errors are grouped with the original exception. +`capacity=64` separately bounds active requests and unmatched observations; overflow fails explicitly. +Completed entries are removed while counters persist. Keep full request history separately if needed. +ScoreboardStatus is diagnostic, not a pytest verdict or coverage result. + +Run the MemoryBackend example without RTL: + +```bash +python3 examples/transactions/scoreboard.py +``` + +See [source](https://github.com/XS-MLVP/toffee/blob/main/examples/transactions/scoreboard.py) +and [pipeline scenarios](pipeline-examples.md). diff --git a/docs/en/guides/subscriptions.md b/docs/en/guides/subscriptions.md new file mode 100644 index 0000000..707aad5 --- /dev/null +++ b/docs/en/guides/subscriptions.md @@ -0,0 +1,54 @@ +# Subscribe persistently and capture snapshots + +## Declare an on subscription + +```python +import asyncio + +from toffee import Execution, RisingEdge, on + +snapshots = asyncio.Queue() + +# clock 与 payload 是项目已经绑定的时钟和 Bundle。 +@on(RisingEdge(clock), capture=lambda event: (event.tick, payload.sample())) +async def observe(snapshot): + snapshots.put_nowait(snapshot) + +async with Execution(backend) as execution: + execution.subscribe(observe.bind()) + tick, value = await snapshots.get() + # 消费命中 phase 保存的不可变 BundleValue。 + assert value == expected +``` + +`await trigger` creates a one-shot registration. `@on` uses the same compilation path but rearms after matches +and delivers results through a handler queue. + +- `lossless` fails explicitly when its queue fills. +- `latest` drops old queued items to retain recent values. +- `capture=` must synchronously create an immutable snapshot at the matching phase. +- The handler must be `async def`. + +A handler receives the captured value, or XEvent when capture is omitted. Handlers can run later; +rereading a live signal does not recover the original sample. +The example's trigger is already bound, so `.bind()` takes no arguments. +A subscription declared against a trigger factory, such as `@on(handshake)`, instead uses `.bind(dut)`. +Execution cancels and reclaims subscriptions on exit. + +Run the pure-Python example: + +```bash +python3 examples/triggers/subscription.py +``` + +## Choose a subscription or Monitor + +Use a [Monitor](monitors.md) when tests need `await recv()` or response checking. +Use `@on` for component handlers that persistently process samples or state events, without blocking the event loop. +Capture occurs at the match phase even if the handler is slow. A lossless overflow raises +SubscriptionOverflowError; capture or handler exceptions fail Execution, which still reclaims tasks. + +The decorator returns XSubscriptionSpec; bind returns BoundSubscriptionSpec; Execution.subscribe returns +an active Subscription. Components can cancel it early with `execution.reactor.cancel_subscription(subscription)`. +Ordinary tests rely on Execution cleanup. +Value modes remain enter/each_sample/change: persistent rearming does not convert enter into a notification every high cycle. diff --git a/docs/en/guides/toffee-v2-migration.md b/docs/en/guides/toffee-v2-migration.md new file mode 100644 index 0000000..3fb4f2f --- /dev/null +++ b/docs/en/guides/toffee-v2-migration.md @@ -0,0 +1,459 @@ +# Migrate to Toffee 2 + +Toffee 2 migrates the XReactor 1.1.0 implementation, providing driving, monitoring, reference models, +checking, and coverage. **Toffee Legacy's implementation and history remain on +[`master`](https://github.com/XS-MLVP/toffee/tree/master); the new implementation is on `main`.** +The old distribution is `pytoffee`; the new one is `uc-toffee`. Both import as `toffee`, but their +APIs and execution semantics are incompatible. Use separate virtual environments. Existing +verification environments need updates to execution scopes, signal binding, model connections, +and sampling semantics. + +The migration baselines are [XReactor 1.1.0 `3017293`](https://github.com/XS-MLVP/xreactor/tree/3017293a7597c39adbae883e15d63d727bcbf6ae) +and [Legacy Toffee `f57dd6f`](https://github.com/XS-MLVP/toffee/tree/f57dd6ffc295b9b8bad3e3b3ce040b8a849f5c91). +Legacy actually released in the `0.x` series, not 1.x. Toffee 2 starts at `2.0.0` under `uc-toffee`; +install with `python3 -m pip install "uc-toffee[runner]"`. + +## Choose a path for existing projects + +Projects staying on Legacy should retain dependency locks. Git installations should select +master or a validated old commit explicitly instead of following the changed default branch. +Old 0.x tags still refer to Legacy. + +To migrate: + +1. Create a separate Python 3.11+ environment and install `uc-toffee[runner]`. +2. Migrate test entry points, Bundle, Driver, Monitor, and model wiring using the tables below. +3. Review cycle boundaries, completion conditions, and cleanup against the sampling/waiting sections. +4. Run project regressions and validate results before updating dependencies. + +Use separate environments for comparison because both generations provide `import toffee`. + +## Move from XReactor 1.1 + +Replace Python imports `xreactor` with `toffee`; commands `xreactor` and `xreactor-coverage-report` +become `toffee` and `toffee-coverage-report`. The distribution is `uc-toffee`. The low-level public +class XReactor becomes Reactor. Rename the `pytest_xreactor_coverage` hook, +`xreactor_register_dut` fixture, and `xreactor_functional_coverage` marker to use the toffee prefix. +The bundled runner continues to use pytest, pytest-asyncio, and pytest-xdist; it needs no toffee-test. + +Coverage JSON's `xreactor-*` format identifiers and existing report-directory markers remain +compatible, so existing reports are readable. This naming migration does not change the native +dependency pin, ABI, or coverage protocol. + +## Main differences + +| Area | Toffee Legacy | Toffee 2 | +| --- | --- | --- | +| Advancement | Python clock task calls dut.Step(1), scans host-loop tasks, and waits for runnable coroutines | Execution observes this simulation's immediate callbacks; native Backend batches advancement until events/barriers | +| Conditions | Value/Condition wake and evaluate in Python each cycle | Compilable conditions, Sequence, and FSM evaluate in C++; Python predicates explicitly use @pytrigger | +| Sampling | Mainly follows DUT, Bundle, or pin cycle events | Explicit rising, falling, or drive-stable phase via sample/default_sample | +| Wait results | Coroutine triggers generally indicate completion only | Reusable specifications with independent registrations per await, returning XEvent tick/phase data | +| Signals | Bundle subclasses declare Signal/Signals and bind by prefix, regex, or mapping | Bundle combines live signals, explicit fields, trees, and packed views; sample returns immutable snapshots | +| Agents | Multiple decorated driver/monitor methods inside an Agent | One optional Driver plus named Monitors, with submission and response checking | +| Models | Hook/Port wiring through Env.attach path matching | Plain Python model connected with agent.connect(model); automatic response chain calls accept after actual acceptance | +| Checking | Compare driver returns with Hook results, or monitor/model ports | Independent Scoreboard: direct, FIFO, key, or fixed-latency checking, cycle budgets, and end checks | +| Concurrency | Executor groups, priorities, and model-first ordering | Driver manages inputs/resources; ordinary async scenarios and asyncio for independent concurrency | +| Lifecycle | Monitor tasks created during construction; standard shutdown cancels other loop tasks | Execution manages registered Agents/framework resources; creators own independent tasks | +| Functional coverage | CovGroup binds objects and Python predicates, with function labels | Separate definitions/instances, bins, crosses, transitions, native collection, provenance, and merge | +| Test entry | toffee.run and separate toffee-test plugin | Standard pytest/pytest-asyncio; fixtures or project recipes retain artifacts | + +See Legacy [asynchronous.py](https://github.com/XS-MLVP/toffee/blob/f57dd6ffc295b9b8bad3e3b3ce040b8a849f5c91/toffee/asynchronous.py) +and [triggers.py](https://github.com/XS-MLVP/toffee/blob/f57dd6ffc295b9b8bad3e3b3ce040b8a849f5c91/toffee/triggers.py). +For current responsibilities, see [Execution](execution-and-backend.md) and [Agents/models](agents-and-reference-models.md). + +Native batching reduces Python scheduling and cross-language calls on unmatched cycles. Per-cycle +Python Drivers, Monitors, and predicates limit that benefit. Compare actual projects with identical +inputs, sampling scope, and simulator options. Empty-clock measurements in the +[benchmark guide](https://github.com/XS-MLVP/toffee/blob/main/benchmarks/README.md) compare framework +overhead and cannot directly predict real DUT speedup. + +## Installation and execution scope + +Legacy requires Python 3.8+; Toffee 2 requires 3.11+. From the Toffee repository root: + +```bash +python3 -m pip install -e '.[test]' +``` + +Real DUTs need xcomm/xspcomm bindings with XTriggerEngine, half-step, and stable-sampling support. +Whether an existing Picker DUT can be reused depends on its exposed XClock/XData and native +binding capabilities. Installing Python code cannot add capabilities to old native bindings. +See [native clocks and project DUTs](../getting-started/native-and-next.md). + +Replace start_clock(dut) plus toffee.run with Execution inside an existing asyncio loop. Here the +project creates dut, and dut.clock is its registered clock pin: + +```python +from toffee import ClockCycles, Execution, RisingEdge, XCommClockBackend + +backend = XCommClockBackend(dut.GetXClock()) +try: + async with Execution(backend, default_sample=RisingEdge(dut.clock)): + await ClockCycles(dut.clock, 1) +finally: + backend.close() +``` + +Execution exit clears current registrations/components while retaining Backend and DUT state. +The Backend fixture closes it; the DUT fixture releases the DUT through its generated interface. +Define reset explicitly when reusing a Backend. Recreate closed Agents and built-in Monitors. +Never advance one DUT concurrently with dut.Step, a Legacy clock task, and Execution. + +## Common API mappings + +These mappings indicate how to rewrite code; identical names still require the semantic review below. + +| Legacy usage | Current usage and caveats | +| --- | --- | +| toffee.run(test), start_clock(dut) | Ordinary asyncio entry or pytest-asyncio, with async Execution scope | +| await bundle.step(n), await ClockCycles(dut, n) | await ClockCycles(clock, n), passing XClock or its registered clock pin; n must be positive | +| await Value(pin, value, delay=...) | await Value(signal, value, sample=...); handle Legacy delay separately | +| await Condition(item, predicate) | @xtrigger for compiled expressions; @pytrigger or PythonPredicateTrigger for arbitrary synchronous Python | +| await AllValid(*pins) | Explicit @xtrigger condition requiring all signals valid | +| await Change(pin) | await ValueChange(signal, sample=...) | +| Delayer(signal, delay) | Capture history at an explicit phase; use Scoreboard latency association for fixed-latency responses | +| RisingEdge(pin), FallingEdge(pin) | Clock stable phases; express sampled edges of arbitrary data signals separately | +| Event, Queue, sleep, create_task, gather | Native asyncio objects; adapters when composing with DUT triggers | +| Signal/Signals/Bundle subclass declarations | Bundle(...), Bundle.bind(..., Field(...)), or Bundle.bind_tree(...) | +| bundle.assign(...) | bundle.drive(...) for complete structures; explicit leaf writes for partial updates | +| bundle.as_dict() for current values | bundle.sample() snapshot, then snapshot.as_dict() if needed | +| @driver_method | Project Driver plus Agent send/submit; project helpers may wrap protocol operations | +| @monitor_method | Explicit sampling trigger and synchronous capture, usually SamplingMonitor | +| Env.attach(Model()), @driver_hook | Environment explicitly constructs Agent/plain model and calls agent.connect(model) | +| @monitor_hook, model Port | Explicit observation consumption/model/checker wiring; no same-name autowiring | +| CovGroup, add_watch_point | CoverGroupDef, CoverPointDef, Bin, then instantiate and sample | +| @toffee_test.testcase, @toffee_test.fixture | @pytest.mark.asyncio, @pytest.fixture, or @pytest_asyncio.fixture | + +## Changed sampling and waiting semantics + +### Select clock and phase explicitly + +Legacy RisingEdge/FallingEdge compared pin values read each cycle. Current triggers represent +stable half-cycle barriers of XClock; the native Backend requires its own XClock or a registered +clock pin. Legacy RisingEdge(dut.irq) cannot simply be retained. To detect a clock-sampled 0→1 irq, +use a stateful Python predicate or timing pattern. These observe changes between selected samples, +not glitches occurring between them. + +Use ClockCycles for ordinary cycle waits. A rising-edge acceptance protocol can encapsulate +falling-edge writes and next-rising acceptance in its Driver. DriveStable samples combinational +logic after falling-edge writes; see [triggers](triggers.md). + +### Review Legacy delay boundaries + +Legacy Value(..., delay=1) waited at least one cycle event before checking; delay=0 could check +immediately. Current Value first evaluates at the next explicit sample after registration, even +if its condition is already true. + +If delay=N meant checking from the Nth rising edge, wait N−1 edges before registering Value for +N>1. Waiting N edges first delays earliest checking until edge N+1. This mapping assumes the +Legacy cycle event and chosen rising sample mean the same thing. + +```python +# 在已有 Execution 内,从第三个后续上升沿开始判断。 +await ClockCycles(clock, 2) +event = await Value(dut.ready, 1, sample=RisingEdge(clock)) +``` + +Implement delay=0 by explicitly reading the current stable value and deciding whether to wait. +A one-shot Value defaults to enter mode with previous state false at registration. Persistent +observation on every valid cycle requires each_sample; otherwise consecutive valid=1 cycles +produce only one entry event. + +### Separate compiled and Python conditions + +Legacy Condition evaluates Python every cycle. Signal conditions can become: + +```python +from toffee import RisingEdge, xtrigger + +@xtrigger(sample=RisingEdge("clock")) +def accepted(dut): + return (dut.valid != 0) & (dut.ready != 0) + +# 在 Execution 内调用:event = await accepted(dut) +``` + +Use `&`, `|`, and `~` for Boolean composition. Unsupported Python operations raise rather than +silently falling back. Explicitly choose @pytrigger for lookups or complex object computation, +retaining its per-sample Python cost. Sequence/FSM recognize patterns; window mismatch does not +automatically fail a test. Configure a failure deadline separately. + +### Handle results and timeouts + +Awaiting a trigger returns XEvent describing hit tick, phase, and value. Live signals may change +before later async processing or composed waits complete; retain relevant values with synchronous +Monitor capture. + +SimTimeout/WallTimeout return TIMEOUT events. Callers must raise explicitly for test failure. +AnyOf is a general race; Scoreboard response budgets enforce strict transaction cycle deadlines. +See [timeout semantics](triggers.md) and [Scoreboard](scoreboard.md). + +## Migrate Bundle and data + +Legacy subclasses Bundle and binds using prefixes, regexes, or mappings. Current Bundle directly +organizes actual signals, or validates names and widths through explicit field mappings: + +```python +from toffee import Bundle, Field + +request = Bundle.bind(dut, { + "address": Field("io_req_addr", width=32), + "data": Field("io_req_data", width=64), +}) +request.drive({"address": 0x8000, "data": 3}) +snapshot = request.sample() +assert snapshot.data.as_int() == 3 +``` + +Bundle.drive requires a complete structure without extra fields. Rewrite Legacy partial assignment, +wildcards, and custom __bundle_assign__ behavior as project encoding or explicit leaf writes. +Bundle.bind_tree requires a project signal tree; bind explicitly if metadata is absent. Do not +apply Legacy prefix/regex binding code unchanged to the current class. + +Snapshot leaves are LogicValue objects preserving width and X/Z; as_int raises on unknown bits. +Keep integer model calculations, but convert explicitly at boundaries and define unknown-input +handling. Native expressions require explicit width and signedness where relevant; review negative +constants, truncation, and casts using [expressions and resources](expressions-and-resources.md). +See [data](data.md) for packed arrays and structures. + +## Migrate Drivers, Monitors, and models + +Legacy Agents contain driving/monitoring methods with Env-injected Hooks/Ports. Current components +separate driving, sampling, prediction, and comparison; Agent connects one interface's components. +Encode a Legacy Agent's operations as different request types for one Driver, or use separate +Agents for independent input resources. + +Legacy driver Hooks offered model_first/dut_first and priorities. The current automatic response +chain invokes synchronous model.accept **after actual input acceptance**, stores expected data, +and associates the response. Model state follows acceptance order; queued requests must not +advance it prematurely. Preserve functional calculations but rewrite decorators, path matching, +and scheduling configuration. + +accept returns this request's independent expected response synchronously. Explicit expected +overrides comparison data while still updating the connected model. Legacy None skipped comparison; +current None is a valid expected value and cannot mean “no response.” + +### A runnable migrated environment + +The following file uses the teaching DUT: operand accepts at a rising edge with enable=1, and an +8-bit accumulated result responds two cycles later. With no ready backpressure, a single-cycle +Driver suffices. Real protocols must define acceptance and idle behavior themselves. Save the +example under the filename used by the command below in the repository root: + +```python +import pytest +import pytest_asyncio + +from examples.getting_started.toy_dut import TutorialDut +from toffee import ( + Agent, Bundle, Execution, PythonPredicateTrigger, RisingEdge, + SamplingMonitor, SyncSingleCycleDriver, +) + + +class AccumulatorModel: + def __init__(self): + self.total = 0 + + def accept(self, operand): + self.total = (self.total + operand) % 256 + return self.total + + +@pytest_asyncio.fixture +async def accumulator(): + dut = TutorialDut(latency=2) + driver = SyncSingleCycleDriver( + dut.clock, + Bundle(enable=dut.enable, operand=dut.operand), + idle={"enable": 0, "operand": 0}, + encoder=lambda operand: {"enable": 1, "operand": operand}, + ) + monitor = SamplingMonitor( + PythonPredicateTrigger( + "response-present", lambda: dut.response is not None, + sample=RisingEdge(dut.clock), mode="each_sample", + ), + capture=lambda event: dut.response, + ) + agent = Agent( + "accumulator", driver=driver, monitors={"response": monitor}, + response_monitor="response", clock=dut.clock, + response_timeout_cycles=3, + ) + agent.connect(AccumulatorModel()) + try: + async with Execution(dut.backend, agents=[agent]): + yield agent + finally: + dut.close() + + +@pytest.mark.asyncio +async def test_accumulator(accumulator): + for operand in [250, 10]: + await accumulator.send(operand) + await accumulator.finish(timeout_cycles=10, observe_cycles=1) +``` + +```bash +python3 -m pytest -q test_toffee_新版_example.py +``` + +This uses a Python predicate for the teaching DUT's Python outputs. Real XData conditions can +compile. No Model base class, Hook decorators, or Env.attach are needed. + +Legacy Monitor queues logged errors and dropped new records when full. Current Monitors and +subscriptions default to bounded lossless delivery, propagating overflow as verification failure. +Use delivery="latest" only for telemetry explicitly permitting older data to be discarded. +Response checking needs every record. + +### send and submit wait for different endpoints + +| Call/configuration | Caller waits for | +| --- | --- | +| await agent.send(request) | Input acceptance; configured response checking continues in the background | +| transfer = agent.submit(request), with AsyncDriver and response checking | Immediate submission; await transfer returns the associated, checked response | +| await agent.drain(timeout_cycles=...) | Transactions submitted before the call; later submission remains allowed | +| await agent.finish(timeout_cycles=..., observe_cycles=...) | Seal, drain, trailing observation, and residual checks; Execution still closes Monitors | + +Code that previously used a driver method's DUT return value should use a response-checking +AsyncDriver Agent's submit or an explicit project response path. SyncDriver Agents have no submit. +AsyncDriver.send returns an awaitable handle, not a coroutine for asyncio.create_task; save and +await the handle directly. + +Returning from a test body does not mean responses have been checked. On success, call budgeted +finish after submission ends and choose an observation window for allowed latency and extra +responses. Unawaited background errors still propagate at finish or Execution exit. + +Cancelling a coroutine waiting on a handle does not cancel the transaction. Explicit cancellation +only applies before acceptance. The environment defines reset/flush and stale-response handling +after inputs have been accepted. + +### Wire passive models and complex responses explicitly + +Automatic connect currently pairs one actively accepted request with one response. Legacy +AgentPort, DriverPort, MonitorPort, multi-model Hooks, and multi-interface Hooks have no one-for-one +compatibility layer. For passive inputs, multiple responses, or DUT-initiated transactions, use +input/output Monitor records and project-defined model update/association rules. + +Agents can share a model, but the framework does not infer same-tick interface arbitration or +update order. Express hardware ordering formerly encoded in priority/model-first settings in the +environment. A checker-owned response Monitor cannot also be consumed by a test's recv; Queue +consumers split records. Broadcast immutable observations explicitly when needed. + +## Migrate Executor and external async tasks + +There is no direct replacement for Legacy Executor groups, priority, sche_order, or exit policies. +AsyncDriver queues one input interface. Ordinary async functions express sequential scenarios; +asyncio.TaskGroup can run independent interfaces concurrently. The project defines group ordering. + +Use native asyncio Task, Lock, Queue, and Event. XEvent is an already-observed simulation result; +replace Legacy toffee.Event() with asyncio.Event(). Use AsyncioEventTrigger, QueueTrigger, or +TaskComplete to compose external waits with DUT events. + +Long-lived services can run outside the simulation domain through execution.external_task(coro); +creators still own shutdown. Infinite in-domain sleep(0) loops prevent settling and may raise +SimulationNotSettledError. External tasks cannot directly await DUT triggers; deliver their results +to simulation tasks instead. + +HTTP or other external I/O does not automatically pause the clock while simulation watchers remain. +Use execution.paused() to freeze time during configuration, and leave the pause before waiting on +clock-dependent triggers. See [asyncio and pytest](asyncio-and-pytest.md). + +## Migrate coverage and reports + +Legacy CovGroup combines live objects and Python conditions. Current coverage separates static +definitions, runtime instances, and sampling. + +| Legacy | Current | +| --- | --- | +| Eq(value), In(values) | Bin.values(...) | +| IsInRange(low, high) | Bin.range(low, high) | +| add_watch_point(target, bins, name) | CoverPointDef(name, bins), with sampled fields or XData binding | +| Callable/dynamic bins | Compute transaction fields and define stable bins; no arbitrary callable-bin equivalent | +| is_all_covered() | covered or assert_coverage(...), after reviewing denominators and thresholds | +| mark_function(...) | Functional descriptions and actual hit provenance; old function labels do not become hits | +| as_dict() and Legacy reports | Current report()/CoverageDatabase schema; old JSON cannot be directly merged as current databases | + +For example, convert operand boundary coverage to transaction sampling: + +```python +from toffee import Bin, CoverGroupDef, CoverPointDef + +coverage = CoverGroupDef("input", ( + CoverPointDef("operand", { + "zero": Bin.values(0), + "maximum": Bin.values(255), + "middle": Bin.range(1, 254), + }), +)).instantiate("accumulator.input") +coverage.sample({"operand": 0}) +coverage.sample({"operand": 255}) +coverage.sample({"operand": 3}) +coverage.assert_coverage(100.0) +``` + +This manually samples supplied values. Real transaction coverage must consistently sample at its +defined submission, acceptance, or observation point. To record actual acceptance, attach sampling +after the acceptance event. Bind cycle-level signal coverage to a clock and register it through +Execution(coverage=[group]). + +Legacy CovGroup stopped sampling after full coverage by default. Current coverage keeps accumulating +and uses at_least for hit targets, so counts need not match. Cross-run merges require compatible +schemas and sampling rules; run IDs and metadata preserve origins. See [coverage](coverage.md) +for crosses, illegal/ignore bins, transitions, and native strategies. + +DUT creation, cycle sampling, and --toffee-report in [toffee-test](https://github.com/XS-MLVP/toffee-test) +belong to that independent plugin. Toffee 2 does not provide the same entry points. Use standard +pytest, with project fixtures owning DUTs, waveforms, RTL coverage, seeds, and cleanup. Current +unified reports combine functional coverage and LCOV; they are not the old plugin's complete pytest +execution report. pytest's pass/fail remains the verdict. See [run artifacts](verification-flow.md). + +## Current migration boundaries + +- A native Backend represents one XClock scheduling domain. Independent Executions can share a + host loop but have no common multi-clock timeline; review old multi-clock tests separately. +- Native expressions support arbitrary-width bit vectors with explicit signed/unsigned types + and supported arithmetic. Review width, overflow, and unknown-value rules; this is not complete + SystemVerilog expression compatibility. +- Ordinary Sequence/FSM waits are non-overlapping. Coverage's bounded overlap does not imply all + triggers overlap or provide a full temporal assertion system. +- Backend, XClock, and DUT signals belong to the Execution thread. One Backend cannot serve + simultaneous active Executions. +- Model Ports, Executor priorities, Bundle randomization, and bulk assignment lack compatibility + entry points. Random stimulus can remain request generators using local random.Random(seed). +- The scheduling observer targets asyncio BaseEventLoop; other loops require adaptation/validation. +- SystemRDL register support is not implemented and is unavailable for migration. + +See [semantics and limitations](../reference/semantics-and-limitations.md). + +## Migrate one environment at a time + +1. Fix input sequences, seeds, DUT configuration, and expected results; record key acceptance/response cycles. +2. Update Python/native bindings and verify reads, writes, and waits in one Execution. +3. Migrate Bundle bindings and check fields, widths, shape, partial updates, and unknown values. +4. Separate Driver and Monitor behavior; define acceptance and immutable response capture. +5. Retain model calculations, rewrite wiring/update timing, and select FIFO, key, or fixed latency. +6. Replace entries/fixtures, add budgeted finish, and assign component/DUT/external-task cleanup. +7. Rebuild coverage definitions/sampling rules, then review reporting and cross-run merging. + +Compare acceptance counts, response counts, keys, data, and protocol-permitted latency for identical +inputs, not only final values or passing-test counts. Existing backpressure, continuous valid, +reordering, missing/extra responses, cancellation, and reset scenarios should retain their expected behavior. + +Run tutorials and the reordered-model example to confirm API usage: + +```bash +python3 -m pytest -q examples/getting_started/test_accumulator.py +python3 -m examples.transactions.reference_model +``` + +With native bindings, run native regressions as well: + +```bash +python3 -m pytest -q --require-xspcomm +python3 -m examples.transactions.reference_model --backend native +``` + +Framework and teaching-example checks do not replace project DUT comparisons between Legacy and Toffee 2. diff --git a/docs/en/guides/triggers.md b/docs/en/guides/triggers.md new file mode 100644 index 0000000..00afd4b --- /dev/null +++ b/docs/en/guides/triggers.md @@ -0,0 +1,107 @@ +# Use Triggers + +A Trigger describes a wait condition. `await trigger` returns an `XEvent`. +These snippets run inside an active Execution with project-provided clocks and signals. +For a complete test, see the [clock tutorial](../getting-started/clock-and-input.md). + +## Clocks + +Express ordinary test waits in cycles; let Drivers and Monitors encapsulate precise drive and sample timing: + +```python +await ClockCycles(dut.clock, 1) +await ClockCycles(dut.clock, 10) +``` + +`ClockCycles(clock, n)` waits for n rising edges after registration and returns the last stable event. +The first wait may begin partway through a cycle. Backpressured protocols must wait for actual acceptance; +elapsed time alone does not prove a handshake. +For phase-sensitive components or fault injection: + +```python +await FallingEdge(dut.clock) +await RisingEdge(dut.clock) +await DriveStable(dut.clock) +``` + +FallingEdge and RisingEdge return at stable half-steps. For an interface that samples inputs on rising edges: + +```python +await FallingEdge(dut.clock) +dut.valid.Set(1) +dut.data.Set(payload) +await RisingEdge(dut.clock) +``` + +## Signal conditions + +```python +await Value(dut.ready, 1, sample=RisingEdge(dut.clock)) +await ValueChange(dut.response, sample=RisingEdge(dut.clock)) +``` + +An omitted `sample=` uses `Execution(default_sample=...)`. Evaluation starts at the next sample after registration. + +| Value mode | Match | +| --- | --- | +| `"enter"` | False-to-true transition; default | +| `"each_sample"` | Every sample where the condition is true | +| `"change"` | Any change in the Boolean condition | + +Value and comparisons support arbitrary-width unsigned XData. ValueChange compares the full aval/bval representation. + +## Timeouts + +SimTimeout measures simulation cycles; WallTimeout measures seconds. Both return +`XEventKind.TIMEOUT` rather than raising automatically. Handle timeout explicitly when it should fail a test: + +```python +from toffee import AnyOf, RisingEdge, SimTimeout, Value, XEventKind + +event = await AnyOf( + Value(dut.result, wanted, sample=RisingEdge(clock)), + SimTimeout(100, clock=clock), +) +if event.kind is XEventKind.TIMEOUT: + raise TimeoutError("result was not observed within 100 cycles") +assert event.value == wanted +``` + +Provide clocks, signals, expectations, and budgets from the test. By the time a combined wait resumes, +simulation may have advanced. Use event values or synchronous capture snapshots instead of rereading live signals. +An outer `asyncio.timeout(seconds)` can bound the entire test's wall time. + +## asyncio sources + +```python +await AsyncioEventTrigger(stop_event) +item = (await QueueTrigger(queue)).value +result = (await TaskComplete(task)).value +``` + +## Combine waits + +```python +event = await AnyOf( + Value(dut.done, 1, sample=RisingEdge(dut.clock)), + AsyncioEventTrigger(stop_event), + WallTimeout(1.0), +) +if event.kind is XEventKind.TIMEOUT: + raise TimeoutError("neither DUT completion nor stop event arrived") + +event = await AllOf( + ClockCycles(dut.clock, 10), + TaskComplete(reference_task), +) +``` + +AnyOf selects a completed source and cancels the others. AllOf waits for every source and records child events in `event.causes`. +If several sources are already complete when the waiter resumes, AnyOf chooses argument order, not event tick order. +It does not drain all pending notifications in a simulation phase before choosing. +Therefore generic timeout races do not enforce exact transaction deadlines. Use +[Scoreboard response budgets](scoreboard.md) to accept deadline-cycle responses and reject late ones reliably. + +Wrap an existing task in `TaskComplete(task)` to produce an XEvent and protect the original task. +Passing a task directly lets AnyOf cancel a losing task, and lets AllOf cancel unfinished sources during failure cleanup. +See [asyncio ownership](asyncio-and-pytest.md). diff --git a/docs/en/guides/verification-flow.md b/docs/en/guides/verification-flow.md new file mode 100644 index 0000000..b2f9592 --- /dev/null +++ b/docs/en/guides/verification-flow.md @@ -0,0 +1,97 @@ +# Assemble components and save run artifacts + +This page assumes you already have a Driver, Monitor, and model, and want a runnable environment +that saves its results. For why sampling location changes counts, see +[checking, coverage, and regression results](../explanation/coverage-results.md). + +This recipe connects Driver, Monitor, Scoreboard, and coverage. Cases submit transactions and call +a budgeted finish. The environment constructs an Agent and connects an independent model; +Execution manages components, and the Agent connects its response Scoreboard. See +[Agents and reference models](agents-and-reference-models.md) for boundaries and stateful models. + +Runnable sources: [environment implementation](https://github.com/XS-MLVP/toffee/blob/main/examples/transactions/verification_flow.py) +and [pytest cases and fixture](https://github.com/XS-MLVP/toffee/blob/main/examples/transactions/test_pipeline_coverage.py). + +```bash +python3 -m pytest -q -s examples/transactions/test_pipeline_coverage.py +``` + +The fixture uses pytest's per-test temporary directory, with seed and run-number subdirectories, +and prints each artifact path. Existing run directories cannot be overwritten. Each run saves +`functional.json`, `coverage.html`, and `run.json`. The latter records node ID, seed, backend, +Python version, budgets, final tick, run state, and exception. The two cases contribute tags 1/2 +and 2/3 respectively: each reaches 2/3, and the combined result reaches 3/3. See +[cross-case coverage](functional-points.md) for merging; test execution uses native pytest reports. + +## Assign one lifecycle owner + +| Object | Creator and closer | Shutdown action | +| --- | --- | --- | +| DUT substitute and Backend | Project recipe | Close after components and Execution exit | +| Execution | Project context | Release advancement task, watchers, and backend lease | +| Driver | Agent inside Execution | Clean up input tasks and signal ownership | +| Monitor | Agent; consumed exclusively by its Scoreboard | Start and stop with Agent | +| Reference model | Project environment | Agent only calls accept; it does not reset or close a shared model | +| Coverage database | Project recipe | Save in finally, including failures and cancellation | + +Execution starts `agents=[agent]` and cleans them up in reverse order. Agent itself is not a +context manager. Startup failure also cleans already-started components. DUT constructors that +acquire resources in stages must clean up incomplete construction themselves. This example uses +the deterministic ToyPipeline substitute; it does not verify real RTL initialization, reset, or flush. + +## Sample each observation once + +`CoveredMonitor` is a small project adapter around the single recv entry point. It samples before returning: + +```python +async def recv(self): + observation = await self.source.recv() + self.coverage.sample( + {"tag": observation.value.tag}, + metadata={"tick": observation.event.tick}, + ) + return observation +``` + +The Scoreboard receives the same immutable Transfer. The adapter has no queue, background task, +or second consumer. This protocol-independent example reuses ToyPipeline's transaction substitute. +For real signals, replace its source with `SamplingMonitor(output_event, capture=...)` or a project +protocol Monitor. + +The chosen sampling location is the DUT observation before delivery to the checker. A response +still counts if its subsequent comparison fails. Observations not delivered after checking stops +do not count. These are therefore neither counts of successfully checked transactions nor a +complete record of DUT activity after failure. To count accepted inputs or successful comparisons, +choose that location explicitly instead of calling recv again on the same Monitor. + +Keep pytest outcomes, functional bins, and simulator line coverage separate. This example collects +functional bins only; project tooling exports simulator LCOV. `CoverageDatabase.merge()` combines +compatible schemas and rejects incompatible ones. + +## Configure three distinct budgets + +```python +async with pipeline(seed=17) as agent: + transfers = [agent.submit(value) for value in (1, 2, 3)] + await agent.finish(timeout_cycles=30, observe_cycles=1) + results = [await transfer for transfer in transfers] +``` + +The recipe explicitly sets a per-transaction response-cycle budget and an overall wall-clock limit. +Each case supplies its finish-cycle budget and trailing observation window. `Execution.max_settle_rounds` +only detects non-converging immediate work within a phase; it does not replace those timeouts. +The recipe implements its wall-clock limit with asyncio.timeout, so cases need no asyncio primitives. + +Cases use local `random.Random(seed)` instances without changing global random state. The recipe +records the seed but does not infer DUT initialization or manage a seed matrix. `run.json` describes +this simulation context; assertions outside it and other fixture teardown results remain governed +by pytest's native result. + +Checking failure, external cancellation, and cleanup failure all trigger artifact saving. Export +failure is retained together with an existing verification exception and cannot turn failure into +success. Unwritable directories cannot guarantee artifacts; an explicit exception reports saving failure. + +Regressions on MemoryBackend and native XClock cover success, mismatch, test-body exceptions, +external cancellation, startup failure, and cleanup failure. They check watchers, backend leases, +idle signals, and task release. HTML export failure has a separate regression. This recipe does +not aggregate pytest-xdist/reruns or manage simulator processes. diff --git a/docs/en/guides/versioning-and-releases.md b/docs/en/guides/versioning-and-releases.md new file mode 100644 index 0000000..b3deab4 --- /dev/null +++ b/docs/en/guides/versioning-and-releases.md @@ -0,0 +1,132 @@ +# Versions, CI, and releases + +Toffee derives versions from Git tags. Stable releases use `vX.Y.Z`; maintainers do not duplicate +version strings in source. For example, `v1.0.0` builds as `1.0.0`, and later untagged commits +build as development versions. `setuptools-scm` is a build dependency, not a requirement for using +an installed package. + +## Current and Legacy versions + +Toffee 2's `main` retains XReactor history. Its first merged-PR release is `v2.0.0`, distributed as +`uc-toffee==2.0.0`. Legacy `master` and `0.x` tags remain unchanged; XReactor's `1.x` tags stay in +its original repository. Legacy tags do not influence version calculation on the new `main`, +and the new workflow does not publish `pytoffee`. Later releases increment from `2.0.0` using +merged PR labels. + +## Choose a version increment + +A PR targeting `main` must select exactly one label: + +| Label | Applicable change | Next version from 1.0.0 | +| --- | --- | --- | +| `release:patch` | Fixes, documentation, or maintenance preserving existing usage | 1.0.1 | +| `release:minor` | New public capability preserving existing calls | 1.1.0 | +| `release:major` | Removed interfaces or changed existing semantics | 2.0.0 | + +Without a stable tag on main's first-parent history, the first version is fixed at `2.0.0`; +increments apply afterward. Every PR still needs one label. Missing or multiple increment labels +fail validation. Version workflows run against default branch `main`; `master` remains Legacy. + +After merge, Version policy verifies that the target is the actual PR merge result on main's +first-parent history, then creates an annotated tag. Side-branch tags, internal PR commits, and +direct pushes do not trigger automatic releases. Retrying the same commit reuses its tag. If a +newer commit already owns the latest version, the workflow refuses to version an older commit. +Version creation is queued to prevent duplicate selections. GitHub's `queue: max` preserves +multiple pending runs during consecutive merges; see [concurrency queues](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency). + +## What CI validates + +| Check | Acceptance criteria | +| --- | --- | +| Python 3.11 and 3.12 | Install and run the full regression; `--require-xspcomm` prevents native tests from silently skipping; run documented CLI examples | +| Native dependencies | Read full SHA and preview version from `.github/native-dependencies.json`, build xcomm, and check native ABI and coverage protocol separately | +| User documentation | `mkdocs build --strict` succeeds | +| Distributions | Wheel/sdist names, versions, and report templates agree; `twine check --strict` succeeds | +| Rebuild from sdist | Rebuild a wheel without Git metadata, preserving the same version | +| Isolated installation | Install wheel in a fresh venv; `pip check`, imports, and template access succeed | + +Native tests use real XClock, but default CI does not build external RTL such as Cache or e203. +Follow each project's README for DUT builds, line coverage, and functional checks. Framework CI +success does not close DUT coverage; retain known DUT defect reproductions separately. + +Pinned commits make verification reproducible; package versions define the user dependency contract. +When updating xcomm, change the pin, required capabilities, and relevant regressions together. +Picker generates actual DUT packages and is not a runtime dependency of the pure Python framework. +A generated DUT must still match its xcomm runtime. + +## Which build is published + +Tagged release accepts tag pushes, Version policy dispatches, or a tag selected manually by a +maintainer. It revalidates the tag's relationship to the merge result and invokes the same CI to +build and verify that commit. Only after checks pass does it publish a GitHub Release, uploading +the original CI artifacts without rebuilding in the publishing job. GitHub generates release +notes from merged PRs, so titles should describe observable changes. + +Release artifacts include: + +- One pure Python wheel and one source distribution. +- `RELEASE-MANIFEST.json`: version, source SHA, CI run ID, native pin, and each package's SHA-256. +- `SHA256SUMS`: checksums for distributions and the manifest. + +Before publishing, the workflow rechecks the manifest, tag, dependencies, run ID, and file set. +Existing assets are not overwritten. Identical existing files permit adding missing assets; +checksum differences prevent mixed releases. After interruption, prefer rerunning only the failed +publishing job to reuse the original artifact. Following GitHub Release success, the same wheel +and sdist publish to PyPI's `uc-toffee`. Manifests and native test wheels remain CI/GitHub assets, +not uploads to that PyPI project. Ordinary CI also retains distributions for review. Coverage +databases, logs, and source-coverage HTML remain local. + +Version policy invokes subsequent validation through `repository_dispatch`: tag pushes made with +`GITHUB_TOKEN` do not trigger other workflows, whereas dispatch supports this flow. See +[GitHub workflow triggers](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow). + +## Configure PyPI + +Publish a compatible `uc-xcomm` first, then `uc-toffee`. For initial setup, add a pending GitHub +publisher in [PyPI Publishing](https://pypi.org/manage/account/publishing/): + +| Field | Value | +| --- | --- | +| Project name | `uc-toffee` | +| Owner | `XS-MLVP` | +| Repository | `toffee` | +| Workflow filename | `release.yml` | +| Environment | `pypi` | + +Create the matching GitHub `pypi` environment and allow deployments from main or validated release +tags. No PyPI API token is needed. The first successful upload creates the project. If configuration +is incomplete, the PyPI job can fail and be retried after setup. An existing version cannot be +overwritten with different content. + +`repository_dispatch` uses workflows on the repository default branch, which must remain `main`. +Legacy master's source and old pytoffee publishing setup remain independent. + +## Initial enablement and recovery + +An administrator first creates the three `release:*` labels and requires both Python jobs under +`CI / test`, plus `CI / docs`, `CI / package`, and `Version policy / release-label` for merging. +Enable Actions and allow the write permissions declared by version and publishing workflows. +Use the exact check names shown by the first Actions run. + +Merge workflow files into the default branch before dispatch can find them. If the initial merge +does not start dispatch, or dispatch fails after tag creation, rerun Version policy on main with +the original full merge SHA and the same increment. It reuses an existing tag. If the tag exists +but validation or publication fails, run Tagged release manually with that tag. Manual runs still +require verified merge provenance and complete CI. + +Before a local commit, run: + +```bash +python3 -m pip install -e '.[test,docs]' 'build>=1,<2' 'twine>=6,<7' +python3 -m pytest -q --require-xspcomm +mkdocs build --strict +python3 -m build +python3 -m twine check --strict dist/*.whl dist/*.tar.gz +python3 scripts/release/verify_dist.py --manifest --verify-manifest +git diff --check +``` + +Read the installed version with `importlib.metadata.version("uc-toffee")`. Release CI needs full +Git history. An sdist includes its build-time version and rebuilds without the original repository. +Only plain source copies lacking both Git and sdist metadata use the `2.0.0.dev0` fallback. +See [setuptools-scm](https://setuptools-scm.readthedocs.io/en/v9.2.0/usage/) for configuration. diff --git a/docs/en/index.md b/docs/en/index.md new file mode 100644 index 0000000..829a4cf --- /dev/null +++ b/docs/en/index.md @@ -0,0 +1,30 @@ +# Toffee user manual + +This manual describes Toffee 2, migrated from XReactor. Legacy Toffee remains on +[`master`](https://github.com/XS-MLVP/toffee/tree/master); see the [migration guide](guides/toffee-v2-migration.md). + +A hardware test supplies inputs, waits for results, and checks them against the specification. +Toffee expresses these operations as Python tests and helps organize repeated behavior into reusable components. +Choose the section that matches your current task; each section can be read independently. + +| Your goal | Start here | What you will learn | +| --- | --- | --- | +| Follow a first exercise | [Tutorials](getting-started/index.md) | Test an 8-bit accumulator with assertions, drivers, automatic checking, and coverage | +| Complete a project task | [How-to guides](guides/index.md) | Connect a DUT, assemble verification components, run regressions, or migrate a project | +| Look up an interface | [Reference](reference/index.md) | Find APIs, defaults, CLI options, and supported behavior | +| Understand the design | [Explanation](explanation/index.md) | Understand simulation progress, resource ownership, and what coverage proves | + +## Write your first test + +Start with [installation](getting-started/installation.md) and [your first PASS and FAIL](getting-started/first-test.md). +The first exercises use Python without an RTL simulator. Every lesson identifies its source file, +command, and expected result, introducing concepts as the exercise needs them. + +## Common tasks + +- [Configure simulation](guides/execution-and-backend.md): create a backend and release its resources. +- [Drive inputs](guides/drivers.md) and [capture outputs](guides/monitors.md): reuse interface timing. +- [Connect a reference model](guides/agents-and-reference-models.md): advance it at acceptance and compare responses. +- [Run regressions](guides/runner.md): select tests, use multiple workers, and diagnose collection failures. + +See [runnable examples](examples.md) for complete programs and the [feature map](reference/feature-map.md) for implementation and regression links. diff --git a/docs/en/reference/api.md b/docs/en/reference/api.md new file mode 100644 index 0000000..992401a --- /dev/null +++ b/docs/en/reference/api.md @@ -0,0 +1,168 @@ +# Public API index + +Common objects are exported from top-level `toffee`. Start with the +[beginner tutorial](../getting-started/index.md) if you are new to the framework. + +| Need | Common entry points | +| --- | --- | +| Advance simulation and wait for conditions | Execution, ClockCycles, Value, AnyOf | +| Drive and sample structured data | Bundle, SignalDriver, Transfer | +| Track, compare, and finish transactions | XTransfer, Scoreboard; see [interface selection](../guides/scoreboard.md) | +| Functional coverage and reports | CoverGroupDef, CoverGroup, CoverageDatabase | + +Backend extension protocols and low-level advancement types appear later. Ordinary tests do not +need to implement them. + +## Execution and Backends + +See [Execution reference](execution.md) for parameters, defaults, and resource contracts. + +| API | Purpose | +| --- | --- | +| `Execution` | Async execution scope for one test | +| `Execution.external_task(coro, *, name=None)` | Create an out-of-domain native Task without implicit Reactor binding; caller owns cleanup | +| `Execution.paused()` | Freeze simulation time while host asyncio keeps running | +| `SimulationNotSettledError` | Ready work exceeded max_settle_rounds; the clock does not cross the barrier | +| `XCommClockBackend` | XClock/C++ trigger engine adapter | +| `MemoryBackend` | Pure Python Backend for semantic tests | + +## Triggers and events + +| API | Purpose | +| --- | --- | +| `RisingEdge`, `FallingEdge`, `DriveStable` | Stable-phase triggers | +| `ClockCycles` | Count rising-edge cycles | +| `Value`, `ValueChange` | Value conditions and changes at a selected phase | +| `SimTimeout`, `WallTimeout` | Return a TIMEOUT event on expiry; caller decides whether to raise | +| `AnyOf`, `AllOf` | Compose waits | +| `AsyncioEventTrigger`, `QueueTrigger`, `TaskComplete` | Adapt asyncio events into waits | +| `XEvent`, `EdgeEvent`, `ConditionEvent`, `FsmEvent` | Await results | +| `XPhase`, `XEventKind`, `LogicValue` | Phases, event kinds, and four-state values | + +## Compilation and subscriptions + +| API | Purpose | +| --- | --- | +| `@xtrigger` | Conditions/Sequence/FSM compiled into native IR | +| `@pytrigger` | Explicit Python predicate fallback | +| `Next(expr)` | Strictly adjacent Sequence sample; mismatch restarts at the beginning | +| `Bin.transition(*values, overlap=True)` | Adjacent-sample transition bin reusing Sequence matching semantics | +| `CoverGroup.bind(trigger=..., fields=..., strategy="auto", accumulate=False, overlap=False, max_active=None, diagnostics="off")` | Configure passive collection, optional bounded concurrent patterns and diagnostics, managed by Execution | +| `CoverGroup.sync()` / `clear_history()` | Synchronize native statistics / clear incomplete matches only | +| `CoverGroup.inspect()` | Inspect current steps, states, and progress inside Execution without traces or enabled diagnostics | +| `Sequence`, `Wait`, `Within`, `Hold` | Non-overlapping patterns by default; coverage may enable concurrency; missing a match does not automatically fail a test | +| `FSM`, `State` | Explicit branching state machines | +| `@on` | Declare a persistent subscription | +| `Execution.subscribe` | Start a bound subscription | + +## Data and protocols + +| API | Purpose | +| --- | --- | +| `as_xdata` | Obtain/validate native XData | +| `Bundle`, `BundleValue` | Structured live-signal view and immutable snapshot | +| `PackedArray`, `split_packed` | Split a packed bus into equal-width elements | +| `PackedLayout`, `PackedView` | Nested, customized packed layouts | +| `ReadyValid`, `Decoupled`, `Role` | Optional ready/valid channel views | +| `Driver`, `SignalDriver`, `Monitor` | Component interfaces and Bundle-independent signal ownership base | +| `Agent` | Interface instance combining Driver/Monitors; connect(ref), send/submit/recv, drain/finish; managed by Execution | +| `ReadyValidMonitor` | Passively observe ready/valid handshakes | +| `SamplingMonitor` | Generic explicit-trigger/synchronous-capture template retaining the triggering event | +| `MonitorClosedError`, `MonitorOverflowError` | Built-in Monitor termination and lossless-capacity errors | +| `SyncDriver`, `SyncSingleCycleDriver`, `SyncMultiCycleDriver` | Await send for acceptance; generic, single-cycle, and fixed-stage templates | +| `AsyncDriver`, `AsyncSingleCycleDriver`, `AsyncMultiCycleDriver` | Background submission base and timing templates; send/submit submit immediately when called | +| `DriveStage` | Static stage description: signals, idle values, encoding, and occupied cycles | +| `AsyncDriver.send(transaction) -> XTransfer[RequestT, XEvent]` | Complete automatically at acceptance; await returns acceptance event without a response collector | +| `AsyncDriver.submit(request) -> XTransfer[RequestT, ResponseT]` | Return a response handle completed by response association | +| `Transfer` | Monitor-delivered transaction and acceptance event | + +## Functional coverage + +| API | Purpose | +| --- | --- | +| `Bin`, `BinSpec`, `BinKind` | Bin definitions | +| `CoverPointDef`, `CrossDef`, `CoverGroupDef` | Serializable coverage schemas | +| `CoverGroup` | Runtime instance and counters | +| `CoverageDatabase` | Save, load, and merge multiple instances | +| `Iff` | Group/point/cross gates | +| `IllegalPolicy`, `OverlapPolicy` | Illegal-hit and overlap policies | +| `generate_unified_coverage_site` | Multi-page functional + LCOV report | +| `generate_unified_coverage_report` | Single-file unified report | + +Import these from `toffee`. For class-based models, import the following from `toffee.declarative`: + +| API | Purpose | +| --- | --- | +| `@coverpoint`, `CoverPoint`, `Bin` | Reusable classes declaring binning rules | +| `@covergroup`, `CoverGroup`, `Cross` | Sample types, points, and crosses | +| `Fields`, `wire` | Select field paths and bind existing signals | +| `TemporalCoverPoint`, `SignalCoverGroup` | Count events spanning cycles directly | + +See [class declarations](../guides/coverage-classes.md) and [temporal coverage](../guides/coverage-patterns.md). +The declarative CoverGroup is distinct from the top-level class. Its `runtime` integrates with +existing execution and reporting. + +## Transactions and Scoreboard + +| API | Purpose | +| --- | --- | +| `XTransfer`, `TransferState`, `MISSING` | Awaitable request handle from submission through acceptance to completion/failure; cancelling a waiter does not cancel the transaction | +| `Scoreboard` | Direct checking or asynchronous association with bound Driver/Monitor | +| `CheckContext`, `ScoreboardStatus`, `Difference` | Comparison context, diagnostics, and field differences | +| `ScoreboardMismatch` | Expected/actual mismatch; an AssertionError subclass | +| `ScoreboardAssociationError` | Association conflicts, unmatched output, or observation-capacity overflow | +| `ScoreboardIncompleteError` | Transactions remain incomplete at close | +| `ScoreboardTimeoutError` | Cycle-budget failure with operation, budget_cycles, elapsed_cycles, tick, context, and other structured attributes | +| `DriverIncompleteError` | Accepted requests remain incomplete when Driver closes | +| `MissingExpectedError` | Async transaction lacks explicit expected value or expected source | +| `structural_differences` | Recursive differences across dataclasses, mappings, sequences, and BundleValue | + +See [Scoreboard and pipelined transactions](../guides/scoreboard.md) for complete examples. + +## Backend and component extension + +| API | Purpose | +| --- | --- | +| `SimulationBackend` | Backend protocol | +| `BackendCapabilities` | Supported phases/capabilities | +| `RunLimit`, `RunResult`, `StopReason` | Batched Backend advancement interface | +| `AcceptedDriver`, `ObservedMonitor` | Structural component Protocols used by async Scoreboard | + +## Coverage and run reports + +See [Runner reference](runner.md) for test CLI, artifacts, and plugin interfaces. + +| API | Purpose | +| --- | --- | +| `CoverageSample`, `IllegalHit`, `IllegalBinError` | Sample hit details, illegal records, and failure | +| `CoverageSchemaError`, `CoverageMergeError` | Invalid definitions or incompatible merges | +| `CoverGroupDef.instantiate(..., run_id=None, run_metadata=None, contract=None)` | Run identity, context, and manual transaction sampling contract | +| `CoverGroup.goal_met`, `CoverGroup.covered` | Group percentage goal / acceptance including child goals, illegal hits, and completeness | +| `CoverGroup.assert_coverage(minimum=None, per_item=True)` | Check positive-weight child goals by default; optionally check only group threshold | +| `CoverGroup.from_report(report)`, `CoverageDatabase.read_json(path)` | Read current-format reports/databases | +| `parse_lcov` | Parse simulator line coverage and merge records for each file | +| `build_unified_coverage_model`, `render_unified_coverage_html` | Compose data explicitly and render self-contained HTML | + +See [coverage](../guides/coverage.md) for sampling, crosses, gates, saving, and merging. + +## Declaration types and low-level handles + +These types support component development and diagnostics. Ordinary tests should start with the +feature entry points above. + +| API | Purpose | +| --- | --- | +| `Field` | Source/width declarations for Bundle.bind/view_as; see [data](../guides/data.md) | +| `drive_ready_valid` | Optional handshake helper for project Drivers; see [protocol views](../guides/protocols.md) | +| `XTrigger`, `PhaseTrigger`, `ConditionMode` | Trigger contract, sample phase, and match mode | +| `PythonPredicateTrigger`, `CompiledTrigger` | Explicit Python predicate and IR condition instances | +| `PhaseEvent` | Events for phases such as DriveStable | +| `XExpr`, `SequenceSpec`, `FsmSpec` | Compiled expressions and timing specifications; prefer decorators/constructors for declarations | +| `XSubscriptionSpec`, `BoundSubscriptionSpec` | @on declarations and bound results; see [subscriptions](../guides/subscriptions.md) | +| `Subscription`, `SubscriptionOverflowError` | Running subscription handle and lossless queue overflow | +| `Reactor`, `Registration` | Component-level registration, dispatch, and cancellation managed by Execution | +| `BackendHandle`, `BackendHit` | Backend arm/disarm identity and hit records | +| `ScoreboardMode`, `ScoreboardError` | Direct/async checking modes and DUT checking exception base | + +The authoritative export list is `__all__` in `src/toffee/__init__.py`. See the [feature map](feature-map.md) +for examples/regressions and [semantics and limitations](semantics-and-limitations.md) for boundaries. diff --git a/docs/en/reference/execution.md b/docs/en/reference/execution.md new file mode 100644 index 0000000..6f64f01 --- /dev/null +++ b/docs/en/reference/execution.md @@ -0,0 +1,66 @@ +# Execution reference + +Import with `from toffee import Execution`. For setup, see +[Execution and Backend](../guides/execution-and-backend.md); for rationale, see +[scheduling and resource ownership](../explanation/execution-model.md). + +## Constructor parameters + +`backend` is required and accepts positional or keyword use. All other parameters are keyword-only. + +| Parameter | Default | Meaning and constraints | +| --- | --- | --- | +| `backend` | Required | SimulationBackend supporting half-step and stable sampling | +| `agents` | `()` | Iterable of distinct Agent instances; Agents cannot share components | +| `coverage` | `()` | Iterable of distinct CoverGroups whose sampling Execution starts/stops | +| `default_sample` | `None` | Default condition sampling phase; must be RisingEdge, FallingEdge, or DriveStable | +| `max_batch_ticks` | `4096` | Maximum half ticks per Backend call; positive | +| `quantum_ms` | `10.0` | Wall-time budget for continuous advancement, in milliseconds; positive | +| `budget_check_interval` | `64` | Native executor's time-budget checking interval; positive | +| `max_settle_rounds` | `100_000` | Maximum asyncio yields while settling one phase; positive integer, not bool | + +The three advancement budgets do not replace test timeouts or transaction response budgets. +`max_settle_rounds` is not a cycle count. Without `default_sample`, conditions requiring a sampling +phase must pass `sample` explicitly; see [triggers](../guides/triggers.md). + +## Scope and methods + +| Entry point | Contract | +| --- | --- | +| `async with Execution(...) as execution` | Acquire Backend control and start registered coverage/Agents; clean current resources and release control on exit | +| `execution.paused()` | Async context manager freezing simulation while host asyncio runs; resume after all nested pauses exit | +| `execution.external_task(coro, *, name=None)` | Create an out-of-domain native Task in the active Execution; retain user ContextVars and clear the implicit Reactor binding | +| `execution.subscribe(spec)` | Register a bound specification in the active Execution and return a Subscription | + +Normal exit, test exceptions, and startup failure all clean resources. Agents close in reverse +order, covergroups synchronize and stop, and current waits, subscriptions, and compiled state +are released. Execution does not close the Backend, reset the DUT, or terminate independently +created background tasks. Creators own `backend.close()`, DUT release, and external-task cleanup. + +A Backend can belong to only one active Execution. Reusing it does not reset clock or DUT state. +Create fresh instances of closed Agents and built-in Monitors. + +## XClock ownership + +| Period | Manual advancement | +| --- | --- | +| Backend constructed, before Execution entry | Control not acquired; `dut.Step(...)` can initialize the DUT | +| Inside Execution, including pauses | Native XEngine exclusively owns XClock; manual advancement and reset are prohibited | +| After Execution exits and releases control | Manual control resumes; release itself does not advance or reset the clock | + +Inside the scope, `dut.Step`, `clock.Step`, `RawStep`, `StepHalf`, and `Reset` raise errors. +Legacy `AStep`, `ANext`, `ACondition`, and `RunStep` also cannot be mixed in. Separate Backends +wrapping the same XClock cannot take control simultaneously. + +If exit occurs halfway through a cycle, finish the remaining half with `StepHalf` before calling +full-cycle `Step`. Calling Backend `arm` / `run_until` directly acquires control on first use and +retains it until `close()`. Remove directly registered watchers before entering Execution. + +## Settling and concurrency limits + +Exceeding `max_settle_rounds` raises `SimulationNotSettledError` with tick, phase, and unfinished +callbacks. It never skips ready work to advance. Do not await clock-dependent triggers inside a pause. + +Backend, XClock, and DUT signals do not support concurrent cross-thread access. Multiple Executions +do not provide a shared timeline. See [semantics and limitations](semantics-and-limitations.md) +for event-loop compatibility and external-event boundaries. diff --git a/docs/en/reference/feature-map.md b/docs/en/reference/feature-map.md new file mode 100644 index 0000000..3b4b196 --- /dev/null +++ b/docs/en/reference/feature-map.md @@ -0,0 +1,49 @@ +# Features, scenarios, and regressions + +This index connects user needs to guides, runnable examples, and behavior regressions. +Test links identify relevant groups and what they check; file existence and test counts are not +substitutes for behavioral verification. + +| Feature | Scenario and guide | Example / corresponding tests | +| --- | --- | --- | +| Getting started | Assertions, clocks, boundaries, components, models, pipelining, failures, coverage | [Tutorial](../getting-started/index.md); examples/getting_started joins default regression; test_quickstart_tutorial checks snippets and failure exercises; native_lab runs explicitly | +| Simulation lifecycle | [Execution](../guides/execution-and-backend.md): Backend reuse, pauses, exclusive lease, settling, cleanup | [runtime](https://github.com/XS-MLVP/toffee/blob/main/tests/runtime/test_runtime.py), [observer](https://github.com/XS-MLVP/toffee/blob/main/tests/integration/test_asyncio_observer.py): order, cancellation, external-task isolation, non-settling errors | +| Clocks and conditions | [Triggers](../guides/triggers.md): stable phases, default sampling, enter/change/each_sample, ValueChange, simulation/wall timeouts | [basic_execution](https://github.com/XS-MLVP/toffee/blob/main/examples/triggers/basic_execution.py), [native regression](https://github.com/XS-MLVP/toffee/blob/main/tests/integration/test_xcomm_backend.py): real XClock, wide signals, X/Z, registration baselines, phases | +| Composed events and external inputs | AnyOf/AllOf, Queue/Event/TaskComplete, [explicit HTTP/external-task synchronization](../guides/asyncio-and-pytest.md) | [adapters](https://github.com/XS-MLVP/toffee/blob/main/tests/integration/test_asyncio_adapters.py), [HTTP](https://github.com/XS-MLVP/toffee/blob/main/tests/integration/test_http_integration.py): composition, timeouts, shield ownership | +| Compiled patterns | [Conditions and Sequence/FSM](../guides/compiled-triggers.md): compilation, windows, holds, restart, branch priority | [runtime](https://github.com/XS-MLVP/toffee/blob/main/tests/runtime/test_runtime.py), [native](https://github.com/XS-MLVP/toffee/blob/main/tests/integration/test_xcomm_backend.py): matching, caching, native execution; [pattern boundaries](https://github.com/XS-MLVP/toffee/blob/main/tests/integration/test_compiled_patterns.py): windows, expiry restart, interrupted holds | +| Persistent subscriptions | [Synchronous capture and async handlers](../guides/subscriptions.md), no registration gaps, lossless/latest, errors | [example](https://github.com/XS-MLVP/toffee/blob/main/examples/triggers/subscription.py), [regression](https://github.com/XS-MLVP/toffee/blob/main/tests/runtime/test_subscriptions.py): snapshots, order, capacity, failure cleanup | +| Data and binding | [Bundle/Field/packed](../guides/data.md): snapshots, widths, metadata, dimensions, bit order, stride, offset, four-state data | [data regression](https://github.com/XS-MLVP/toffee/blob/main/tests/interfaces/test_data_interfaces.py): shape, direction, signal identity; native tests: live subviews, padding, widths | +| Drivers | [Templates](../guides/drivers.md): synchronous calls/immediate submission, single/fixed-stage timing, capacity/max_active, locks, idle, cancellation | [pipeline examples](../guides/pipeline-examples.md); [Sync](https://github.com/XS-MLVP/toffee/blob/main/tests/interfaces/test_sync_driver.py), [Async](https://github.com/XS-MLVP/toffee/blob/main/tests/interfaces/test_async_driver.py), [staged](https://github.com/XS-MLVP/toffee/blob/main/tests/interfaces/test_staged_driver.py), [handoffs](https://github.com/XS-MLVP/toffee/blob/main/tests/integration/test_driver_handoffs.py): acceptance ticks, overlap, exclusion, fairness, no bubbles, cleanup | +| Monitors | [Sampling and passive observations](../guides/monitors.md): explicit triggers, immutable records, capacity, closing, failures | [sampling](https://github.com/XS-MLVP/toffee/blob/main/tests/interfaces/test_sampling_monitor.py), [lifecycle](https://github.com/XS-MLVP/toffee/blob/main/tests/interfaces/test_monitor_lifecycle.py): both built-in Monitors, both Backends, all-waiter wakeup | +| Agents and models | [Instances and connect](../guides/agents-and-reference-models.md): Driver kinds, state updates, shared models, batches, one consumer, lifecycle | [stateful example](https://github.com/XS-MLVP/toffee/blob/main/examples/transactions/reference_model.py), [native integration](https://github.com/XS-MLVP/toffee/blob/main/tests/integration/test_agent_native.py), [unit cleanup](https://github.com/XS-MLVP/toffee/blob/main/tests/interfaces/test_agent.py): acceptance, expected overrides, unawaited errors, startup rollback | +| Scoreboard and transactions | [Direct and response checking](../guides/scoreboard.md): differences, FIFO/key/fixed latency, cycle windows, drain/finish, cancellation | [example](https://github.com/XS-MLVP/toffee/blob/main/examples/transactions/scoreboard.py), [association](https://github.com/XS-MLVP/toffee/blob/main/tests/runtime/test_scoreboard.py), [XTransfer](https://github.com/XS-MLVP/toffee/blob/main/tests/runtime/test_transfers.py), [native deadlines](https://github.com/XS-MLVP/toffee/blob/main/tests/integration/test_transaction_lifecycle_native.py): missing/late/duplicate responses, response before acceptance notification, deadline priority, active-table release | +| Optional protocol helpers | [ReadyValid/Decoupled](../guides/protocols.md), role views, ReadyValidMonitor, drive helper | [protocols](https://github.com/XS-MLVP/toffee/blob/main/tests/interfaces/test_protocols.py), data/native regressions: handshake acceptance, backpressure, candidate sampling, cleanup | +| Functional coverage | [Requirements across cases](../guides/functional-points.md), [bins/crosses/gates/goals](../guides/coverage.md), illegal hits, snapshot fields, atomic sampling, persistence/merge/reset | [example](https://github.com/XS-MLVP/toffee/blob/main/examples/coverage/functional_coverage.py), [regression](https://github.com/XS-MLVP/toffee/blob/main/tests/coverage/test_coverage.py): hit priority, gates, overlap, cross limits, weighting, schema, merge | +| Reports and artifacts | [Run recipe](../guides/verification-flow.md): seeds, budgets, one-consumer coverage, failure export, JSON/HTML/LCOV | [recipe tests](https://github.com/XS-MLVP/toffee/blob/main/examples/transactions/test_pipeline_coverage.py), [artifacts](https://github.com/XS-MLVP/toffee/blob/main/tests/integration/test_verification_flow.py), [reports](https://github.com/XS-MLVP/toffee/blob/main/tests/coverage/test_coverage_report.py): failure retention, counts, escaping, directory protection, source pages | + +## Run the corresponding regressions + +After installing the test extra from the repository root, `python3 -m pytest -q` runs framework +regressions, the beginner tutorial, and artifact examples. Native cases skip without xspcomm; +releases and complete acceptance must use: + +```bash +python3 -m pytest -q --require-xspcomm +mkdocs build --strict +``` + +To check a feature, pass its test file, for example: + +```bash +python3 -m pytest -q tests/interfaces/test_agent.py tests/integration/test_agent_native.py --require-xspcomm +python3 -m pytest -q tests/coverage +``` + +MemoryBackend tests check Python semantics; native XClock tests check actual Backend sampling +and scheduling. Neither replaces the other. Real DUT build/artifact dependencies such as e203/cache +are outside the default regression; see [project examples](../examples.md). This table does not +claim arbitrary RTL has been verified. + +New capabilities should update their guide, minimal runnable scenario, and behavior regression. +Prose-only edits do not need tests mirroring implementation. See [API](api.md) for exports and +[limitations](semantics-and-limitations.md) for supported boundaries. diff --git a/docs/en/reference/index.md b/docs/en/reference/index.md new file mode 100644 index 0000000..fdf5646 --- /dev/null +++ b/docs/en/reference/index.md @@ -0,0 +1,16 @@ +# Reference + +These pages define interfaces, parameters, and behavioral contracts. For tasks, see +[how-to guides](../guides/index.md); for design rationale, see [explanation](../explanation/index.md). + +| Look up | Page | +| --- | --- | +| Public types, methods, and imports | [API index](api.md) | +| Execution parameters, defaults, and lifecycle | [Execution reference](execution.md) | +| Regression commands, options, artifacts, and plugins | [Runner reference](runner.md) | +| Widths, signed expressions, and compiled resources | [Expressions and resources](../guides/expressions-and-resources.md) | +| Sampling, cancellation, association, and concurrency boundaries | [Semantics and limitations](semantics-and-limitations.md) | +| Examples and regressions for a capability | [Feature map](feature-map.md) | + +This manual describes Toffee 2. Legacy interfaces with the same names may behave differently; +see the [migration guide](../guides/toffee-v2-migration.md). diff --git a/docs/en/reference/runner.md b/docs/en/reference/runner.md new file mode 100644 index 0000000..8f13d8c --- /dev/null +++ b/docs/en/reference/runner.md @@ -0,0 +1,76 @@ +# Runner reference + +`toffee run` executes pytest tests and collects and merges coverage. For installation and operation, +see [run tests and aggregate coverage](../guides/runner.md). For interpretation, see +[checking, coverage, and regression results](../explanation/coverage-results.md). + +## Commands and options + +```text +toffee run [Toffee 选项] [pytest 参数] +python3 -m toffee run [Toffee 选项] [pytest 参数] +``` + +The entry points are equivalent. Execution requires pytest; multiple workers also require pytest-xdist. + +| Option | Default | Behavior | +| --- | --- | --- | +| `--workers N`, `-j N` | `1` | Positive integer; values above 1 use independent worker processes | +| `--output PATH` | `output/toffee--` | Results directory; must not exist | +| `--keep-coverage-inputs` | Off | Retain per-process intermediate inputs and raw exports after successful aggregation | +| `--export-coverage-json` | Off | Also save merged JSON when functional coverage exists | +| `--native-coverage-tool PATH` | Unspecified | Select the Verilator coverage processor explicitly; otherwise prefer the build provenance path, then PATH | + +Test paths and options such as `-k`, `-m`, `-q`, and `--tb=short` pass through to pytest. +`--collect-only` lists tests without creating a measurement directory; an existing `--output` +path is still rejected. Use `--` to separate subsequent pytest arguments explicitly. + +Set parallelism through `--workers`. Passing `-n`, `--numprocesses`, `--dist`, `--tx`, or +`--max-worker-restart` is prohibited. The Runner uses load scheduling and disables automatic worker +restart to identify incomplete regressions. + +## Final artifacts + +Paths below are relative to this run's `--output`. `manifest.json` records artifact inventory and identity. + +| Path | Creation conditions and contents | +| --- | --- | +| `manifest.json` | Run state, test verdict, artifacts, and completeness; failure details depend on the failing phase | +| `coverage.html` | Aggregate report when functional or native coverage exists; absent without coverage | +| `functional-coverage.json` | Functional data exists and `--export-coverage-json` is enabled | +| `native//coverage.dat` | Complete native counts merged by DUT build identity | +| `native//coverage.info` | Line and branch coverage when the native tool supports LCOV export | +| `_workers/` | Intermediate inputs; removed after successful commit by default, retained on request or aggregation failure | + +The default does not export JSON per test. Explicit `write_json()` calls are unaffected. +Native counts from different DUT builds never merge together; each build retains one final +`coverage.dat`. Merging retains zero-hit points, toggles, and other native counts, and verifies +that output counts equal the sum of inputs. + +## States and exit behavior + +| Situation | Result | +| --- | --- | +| Tests execute completely and all artifacts are written and validated | Manifest is `committed`; `pytest_exitstatus` and test counts still express the verdict | +| Assertion failure with complete execution and collection | Diagnostic reports can still commit; the command returns nonzero | +| Interrupted collection, corrupt inputs, incomplete collection, or worker crash | Manifest is `incomplete`; available inputs remain and final artifacts may be partial | +| Artifacts committed, but intermediate cleanup fails | State remains `committed`, `cleanup_error` is recorded, and exit is nonzero | + +The Runner normally returns pytest's exit code. Collection or cleanup failure changes an otherwise +successful run to nonzero. CLI argument errors return 2; a keyboard interrupt caught by the CLI +returns 130. Initialization failures may occur before a manifest exists; consult the exit status +and error output. Only this run's intermediate files are reclaimed, never historical run directories. + +## Project extension interfaces + +| Entry point | Contract | +| --- | --- | +| `@pytest.mark.toffee_functional_coverage(False)` | Exclude functional capture during this test and its fixture setup/teardown; native coverage stays enabled | +| `definition.instantiate("helper", capture=False)` | Exclude this helper from automatic aggregation; sampling, assertions, and manual export remain available | +| `toffee_register_dut` fixture | Callback registering DUTs that may close and be released during a test; registration neither advances nor closes them | +| `pytest_toffee_coverage(config, functional, native, pytest_exitstatus)` | Called once in the main process after merging and before commit; arguments are read-only; raising fails collection and preserves available inputs | + +Automatically discovered DUTs must provide `SetCoverage`, `GetCovMetrics`, `GetXClock`, and `Finish`. +A fixture can return the DUT directly, an Agent with `dut`, or a factory retaining instances in +`_instances`. Fixtures still own initialization and `Finish()`. The Runner neither takes over +clock advancement nor closes Backends. A standalone clock does not identify its simulator and exports. diff --git a/docs/en/reference/semantics-and-limitations.md b/docs/en/reference/semantics-and-limitations.md new file mode 100644 index 0000000..a0bf031 --- /dev/null +++ b/docs/en/reference/semantics-and-limitations.md @@ -0,0 +1,91 @@ +# Semantics and limitations + +This page defines current behavioral contracts. Parameters are in [Execution reference](execution.md); +for rationale, see [scheduling and resource ownership](../explanation/execution-model.md). + +## Sampling + +- RisingEdge and FallingEdge return at stable half steps. +- Value, ValueChange, and compiled conditions evaluate only at their selected sample phase. +- Evaluation begins at the next sample phase after registration. +- The Backend returns events hitting in the same phase as a batch. + +## Lifecycle + +- Awaiting a trigger creates a one-shot registration. +- Cancelling the wait unregisters its watcher. +- Closing a subscription or exiting Execution unregisters it. +- Execution cleans the current test's triggers and compiled state. +- The caller owns Backend, DUT state, and simulation time. + +## Driver submission + +- The Driver interface requires an awaitable from send; each implementation defines completion. +- AsyncDriver send/submit submit at the call, not when the caller awaits. +- send returns an input-acceptance handle, automatically completing and releasing capacity at + acceptance; submit returns a handle waiting for a response. +- Cancelling a waiter does not retract submitted work. Only unaccepted requests can be explicitly cancelled. +- Normal AsyncDriver exit cancels unaccepted inputs without implicitly draining. Await required handles first. +- SyncDriver still returns a driving coroutine without a background queue; protocol and single-cycle + implementations retain their own timing contracts. + +## Agents and transaction checking + +- Environments construct Agents and register them through `Execution(agents=[...])`; recreate them after exit. +- connect(ref) does not advance a model. Synchronous ref.accept runs after actual acceptance; + overriding expected does not skip model updates. +- A checked Agent's send waits only for acceptance while its checker tracks responses. Driver.send's + independent input-completion contract remains unchanged. +- drain waits for submissions made before the call. finish seals submissions, drains, and observes + the requested trailing window; it does not close Monitors. +- Responses and acceptance use an explicit single-clock rising-stable phase. Correct responses + at the deadline take precedence over timeout. +- FIFO/key extra or duplicate responses fail strict end checks. Sampled fixed-latency mode can + ignore samples outside target windows. +- Unawaited background failures still propagate through finish/exit. Cleanup does not report the + same already-observed error twice. +- Projects define shared-model ordering across interfaces, passive association, multiple responses, + and reset/flush behavior; these are not inferred. + +## asyncio phase settling + +- asyncio alone schedules Python; Execution observes immediate call_soon callbacks without a second queue. +- The current Execution's immediate callbacks and cascading wakeups settle before the next clock + advancement or DriveStable sample. +- Native Task, Lock, Queue, Event, Semaphore, gather, shield, and TaskGroup retain their semantics. +- An incomplete Future does not block the clock. A new in-domain task's first execution is ready work. +- Timers, I/O, and call_soon_threadsafe are external event entrances. Only in-domain work created + after delivery joins settling. call_later(0) is not phase-local sleep(0); external completion is + not promised at a deterministic simulation tick. +- external_task clears the implicit Reactor binding and runs outside the domain; its caller owns cleanup. +- Exceeding max_settle_rounds raises SimulationNotSettledError without skipping work. +- The adapter targets asyncio.BaseEventLoop and has been validated on CPython 3.12's default loop. + Other implementations need their own adaptation and verification. +- Replacing loop.call_soon during active Execution is unsupported and raises when detected. + Exit does not overwrite the host's newly installed entry point. +- Multiple Executions share observation infrastructure but settle independently, without a common + multi-clock timeline. + +External tasks still pass through the shared entry point's checks. Logical isolation does not +eliminate scheduling overhead. See [asyncio and pytest](../guides/asyncio-and-pytest.md). + +## Data + +- Native conditions support arbitrary-width XData with explicit signed or unsigned bit-vector interpretation. +- `bits`, `as_signed()`, `as_unsigned()`, and `resize()` specify types. Two typed operands must match width and signedness. +- Constants must fit the target type; arithmetic results truncate to its width. +- ValueChange compares four-state aval/bval values. +- Data operations use two-state bit-vector semantics, not full SystemVerilog four-state propagation. + +See [expressions and resources](../guides/expressions-and-resources.md) for operations, overflow, and division rules. + +## Current limitations + +- One XCommClockBackend represents one clock domain. +- Ordinary Sequence waits are non-overlapping. Coverage can enable `overlap=True` with a positive + `max_active`; see [temporal coverage](../guides/coverage-patterns.md). +- `@pytrigger` executes a Python predicate at every sample phase. +- One Backend cannot serve multiple active Executions simultaneously. +- Backend, XClock, and DUT signals do not support concurrent cross-thread access. +- Bundle and channel views require explicit binding; there is no generic Interface base class. +- Coverage JSON uses Toffee's schema, not UCIS. diff --git a/docs/examples.md b/docs/zh/examples.md similarity index 100% rename from docs/examples.md rename to docs/zh/examples.md diff --git a/docs/explanation/coverage-results.md b/docs/zh/explanation/coverage-results.md similarity index 100% rename from docs/explanation/coverage-results.md rename to docs/zh/explanation/coverage-results.md diff --git a/docs/explanation/execution-model.md b/docs/zh/explanation/execution-model.md similarity index 100% rename from docs/explanation/execution-model.md rename to docs/zh/explanation/execution-model.md diff --git a/docs/explanation/index.md b/docs/zh/explanation/index.md similarity index 100% rename from docs/explanation/index.md rename to docs/zh/explanation/index.md diff --git a/docs/getting-started/agent-test.md b/docs/zh/getting-started/agent-test.md similarity index 100% rename from docs/getting-started/agent-test.md rename to docs/zh/getting-started/agent-test.md diff --git a/docs/getting-started/clock-and-input.md b/docs/zh/getting-started/clock-and-input.md similarity index 100% rename from docs/getting-started/clock-and-input.md rename to docs/zh/getting-started/clock-and-input.md diff --git a/docs/getting-started/coverage.md b/docs/zh/getting-started/coverage.md similarity index 100% rename from docs/getting-started/coverage.md rename to docs/zh/getting-started/coverage.md diff --git a/docs/getting-started/driver.md b/docs/zh/getting-started/driver.md similarity index 100% rename from docs/getting-started/driver.md rename to docs/zh/getting-started/driver.md diff --git a/docs/getting-started/failures.md b/docs/zh/getting-started/failures.md similarity index 100% rename from docs/getting-started/failures.md rename to docs/zh/getting-started/failures.md diff --git a/docs/getting-started/first-test.md b/docs/zh/getting-started/first-test.md similarity index 100% rename from docs/getting-started/first-test.md rename to docs/zh/getting-started/first-test.md diff --git a/docs/getting-started/index.md b/docs/zh/getting-started/index.md similarity index 100% rename from docs/getting-started/index.md rename to docs/zh/getting-started/index.md diff --git a/docs/getting-started/installation.md b/docs/zh/getting-started/installation.md similarity index 100% rename from docs/getting-started/installation.md rename to docs/zh/getting-started/installation.md diff --git a/docs/getting-started/monitor.md b/docs/zh/getting-started/monitor.md similarity index 100% rename from docs/getting-started/monitor.md rename to docs/zh/getting-started/monitor.md diff --git a/docs/getting-started/more-cases.md b/docs/zh/getting-started/more-cases.md similarity index 100% rename from docs/getting-started/more-cases.md rename to docs/zh/getting-started/more-cases.md diff --git a/docs/getting-started/native-and-next.md b/docs/zh/getting-started/native-and-next.md similarity index 100% rename from docs/getting-started/native-and-next.md rename to docs/zh/getting-started/native-and-next.md diff --git a/docs/getting-started/out-of-order.md b/docs/zh/getting-started/out-of-order.md similarity index 100% rename from docs/getting-started/out-of-order.md rename to docs/zh/getting-started/out-of-order.md diff --git a/docs/getting-started/pipeline.md b/docs/zh/getting-started/pipeline.md similarity index 100% rename from docs/getting-started/pipeline.md rename to docs/zh/getting-started/pipeline.md diff --git a/docs/getting-started/reference-model.md b/docs/zh/getting-started/reference-model.md similarity index 100% rename from docs/getting-started/reference-model.md rename to docs/zh/getting-started/reference-model.md diff --git a/docs/guides/agents-and-reference-models.md b/docs/zh/guides/agents-and-reference-models.md similarity index 100% rename from docs/guides/agents-and-reference-models.md rename to docs/zh/guides/agents-and-reference-models.md diff --git a/docs/guides/asyncio-and-pytest.md b/docs/zh/guides/asyncio-and-pytest.md similarity index 100% rename from docs/guides/asyncio-and-pytest.md rename to docs/zh/guides/asyncio-and-pytest.md diff --git a/docs/guides/compiled-triggers.md b/docs/zh/guides/compiled-triggers.md similarity index 100% rename from docs/guides/compiled-triggers.md rename to docs/zh/guides/compiled-triggers.md diff --git a/docs/guides/coverage-classes.md b/docs/zh/guides/coverage-classes.md similarity index 100% rename from docs/guides/coverage-classes.md rename to docs/zh/guides/coverage-classes.md diff --git a/docs/guides/coverage-patterns.md b/docs/zh/guides/coverage-patterns.md similarity index 100% rename from docs/guides/coverage-patterns.md rename to docs/zh/guides/coverage-patterns.md diff --git a/docs/guides/coverage-transactions.md b/docs/zh/guides/coverage-transactions.md similarity index 100% rename from docs/guides/coverage-transactions.md rename to docs/zh/guides/coverage-transactions.md diff --git a/docs/guides/coverage.md b/docs/zh/guides/coverage.md similarity index 100% rename from docs/guides/coverage.md rename to docs/zh/guides/coverage.md diff --git a/docs/guides/data.md b/docs/zh/guides/data.md similarity index 100% rename from docs/guides/data.md rename to docs/zh/guides/data.md diff --git a/docs/guides/drivers.md b/docs/zh/guides/drivers.md similarity index 100% rename from docs/guides/drivers.md rename to docs/zh/guides/drivers.md diff --git a/docs/guides/execution-and-backend.md b/docs/zh/guides/execution-and-backend.md similarity index 100% rename from docs/guides/execution-and-backend.md rename to docs/zh/guides/execution-and-backend.md diff --git a/docs/guides/expressions-and-resources.md b/docs/zh/guides/expressions-and-resources.md similarity index 100% rename from docs/guides/expressions-and-resources.md rename to docs/zh/guides/expressions-and-resources.md diff --git a/docs/guides/functional-points.md b/docs/zh/guides/functional-points.md similarity index 100% rename from docs/guides/functional-points.md rename to docs/zh/guides/functional-points.md diff --git a/docs/guides/index.md b/docs/zh/guides/index.md similarity index 100% rename from docs/guides/index.md rename to docs/zh/guides/index.md diff --git a/docs/guides/monitors.md b/docs/zh/guides/monitors.md similarity index 100% rename from docs/guides/monitors.md rename to docs/zh/guides/monitors.md diff --git a/docs/guides/pipeline-examples.md b/docs/zh/guides/pipeline-examples.md similarity index 100% rename from docs/guides/pipeline-examples.md rename to docs/zh/guides/pipeline-examples.md diff --git a/docs/guides/protocols.md b/docs/zh/guides/protocols.md similarity index 100% rename from docs/guides/protocols.md rename to docs/zh/guides/protocols.md diff --git a/docs/guides/runner.md b/docs/zh/guides/runner.md similarity index 100% rename from docs/guides/runner.md rename to docs/zh/guides/runner.md diff --git a/docs/guides/scoreboard.md b/docs/zh/guides/scoreboard.md similarity index 100% rename from docs/guides/scoreboard.md rename to docs/zh/guides/scoreboard.md diff --git a/docs/guides/subscriptions.md b/docs/zh/guides/subscriptions.md similarity index 100% rename from docs/guides/subscriptions.md rename to docs/zh/guides/subscriptions.md diff --git a/docs/guides/toffee-v2-migration.md b/docs/zh/guides/toffee-v2-migration.md similarity index 98% rename from docs/guides/toffee-v2-migration.md rename to docs/zh/guides/toffee-v2-migration.md index 37168da..de547b7 100644 --- a/docs/guides/toffee-v2-migration.md +++ b/docs/zh/guides/toffee-v2-migration.md @@ -210,7 +210,8 @@ assert snapshot.data.as_int() == 3 快照叶子为保存位宽和 X/Z 的 `LogicValue`,`as_int()` 遇到未知位会报错。 模型中的整数算法可以保留,边界处需要显式转换,并定义未知输入如何处理。 -原生条件当前使用无符号语义,负常量、隐式截断和 signed cast 不能直接沿用。 +原生表达式支持显式位宽与有符号/无符号类型;负常量、截断和类型转换须按 +[表达式与资源](expressions-and-resources.md)中的规则核对,不能直接沿用旧隐式行为。 packed 数组和结构的迁移见[数据指南](data.md)。 ## Driver Monitor 与参考模型迁移 @@ -409,7 +410,8 @@ coverage.assert_coverage(100.0) - 一个原生 Backend 对应一个 XClock 调度域。多个独立 Execution 可以共享宿主 loop, 但不提供跨时钟域共同时间轴;旧多时钟用例须单独核对。 -- 原生 Expr 当前支持任意位宽无符号条件,不提供完整 signed 或算术表达式 IR。 +- 原生 Expr 支持任意位宽、显式有符号/无符号类型和已支持的算术运算;仍须核对 + 位宽、溢出及未知值规则,不等同于完整的 SystemVerilog 表达式兼容。 - 普通 Sequence 和 FSM 等待采用非重叠匹配。coverage 的有界重叠模式不等于所有 Trigger 都支持重叠,也不等于完整的时序断言系统。 - Backend、XClock 与 DUT 信号由 Execution 所在线程访问,同一 Backend 不能同时 diff --git a/docs/guides/triggers.md b/docs/zh/guides/triggers.md similarity index 100% rename from docs/guides/triggers.md rename to docs/zh/guides/triggers.md diff --git a/docs/guides/verification-flow.md b/docs/zh/guides/verification-flow.md similarity index 100% rename from docs/guides/verification-flow.md rename to docs/zh/guides/verification-flow.md diff --git a/docs/guides/versioning-and-releases.md b/docs/zh/guides/versioning-and-releases.md similarity index 100% rename from docs/guides/versioning-and-releases.md rename to docs/zh/guides/versioning-and-releases.md diff --git a/docs/index.md b/docs/zh/index.md similarity index 100% rename from docs/index.md rename to docs/zh/index.md diff --git a/docs/reference/api.md b/docs/zh/reference/api.md similarity index 100% rename from docs/reference/api.md rename to docs/zh/reference/api.md diff --git a/docs/reference/execution.md b/docs/zh/reference/execution.md similarity index 100% rename from docs/reference/execution.md rename to docs/zh/reference/execution.md diff --git a/docs/reference/feature-map.md b/docs/zh/reference/feature-map.md similarity index 100% rename from docs/reference/feature-map.md rename to docs/zh/reference/feature-map.md diff --git a/docs/reference/index.md b/docs/zh/reference/index.md similarity index 100% rename from docs/reference/index.md rename to docs/zh/reference/index.md diff --git a/docs/reference/runner.md b/docs/zh/reference/runner.md similarity index 100% rename from docs/reference/runner.md rename to docs/zh/reference/runner.md diff --git a/docs/reference/semantics-and-limitations.md b/docs/zh/reference/semantics-and-limitations.md similarity index 100% rename from docs/reference/semantics-and-limitations.md rename to docs/zh/reference/semantics-and-limitations.md diff --git a/examples/README.md b/examples/README.md index ae35f31..16588a6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,6 @@ # 可运行示例 -第一次使用,先看[入门教程](../docs/getting-started/index.md)。 +第一次使用,先看[入门教程](../docs/zh/getting-started/index.md)。 以下命令均在仓库根目录执行;先按 [README](../README.md) 安装测试依赖。 ## 先运行纯 Python 例子 @@ -49,4 +49,4 @@ python3 -m examples.coverage.trigger_bins --engine xcomm --strategy native 真实 DUT 的构建、端口绑定和初始化分别见 [Cache](integration/cache/README.md) 和 [e203](integration/e203/README.md),它们需要额外的项目依赖。 -[用户手册中的示例索引](../docs/examples.md)还列出了故障练习和报告用法。 +[用户手册中的示例索引](../docs/zh/examples.md)还列出了故障练习和报告用法。 diff --git a/examples/getting_started/README.md b/examples/getting_started/README.md index d6a37a8..fa4c91c 100644 --- a/examples/getting_started/README.md +++ b/examples/getting_started/README.md @@ -1,6 +1,6 @@ # 从零开始的累加器教程 -[学习路线](../../docs/getting-started/index.md) 配合一个持续扩展的主线文件阅读。 +[学习路线](../../docs/zh/getting-started/index.md) 配合一个持续扩展的主线文件阅读。 命令均在仓库根目录执行,先安装 `python3 -m pip install -e '.[test]'`。 ## 主线只看两个文件 diff --git a/examples/getting_started/test_accumulator.py b/examples/getting_started/test_accumulator.py index a6bb8bf..74cb967 100644 --- a/examples/getting_started/test_accumulator.py +++ b/examples/getting_started/test_accumulator.py @@ -1,4 +1,4 @@ -"""Read from top to bottom alongside docs/getting-started. +"""Read alongside docs/en/getting-started or docs/zh/getting-started. Each region is introduced by one lesson. Imports appear when first needed. Run one test with: python3 -m pytest -q :: diff --git a/mkdocs.yml b/mkdocs.yml index 2466434..a0597f3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,67 +2,141 @@ site_name: Toffee site_description: Asyncio-native Python hardware verification framework repo_url: https://github.com/XS-MLVP/toffee theme: - name: readthedocs + name: material + language: zh + font: false nav: - - 首页: index.md - - 教程: - - 学习路线: getting-started/index.md - - 0. 准备环境: getting-started/installation.md - - 1. 第一次 PASS 和 FAIL: getting-started/first-test.md - - 2. 输入与时钟: getting-started/clock-and-input.md - - 3. 多组数据与边界: getting-started/more-cases.md - - 4. 提取 Driver: getting-started/driver.md - - 5. 延迟输出与 Monitor: getting-started/monitor.md - - 6. 组装 Agent: getting-started/agent-test.md - - 7. 模型与测试环境: getting-started/reference-model.md - - 8. 流水、多批次与取消: getting-started/pipeline.md - - 9. 看懂失败: getting-started/failures.md - - 10. 功能覆盖率: getting-started/coverage.md - - 选读:乱序响应: getting-started/out-of-order.md - - 选读:原生时钟与项目 DUT: getting-started/native-and-next.md - - 操作指南: - - 按任务查找: guides/index.md - - 仿真与事件: - - 配置 Execution 与 Backend: guides/execution-and-backend.md - - 等待 Trigger 与超时: guides/triggers.md - - 编译条件、Sequence 与 FSM: guides/compiled-triggers.md - - 持续订阅事件: guides/subscriptions.md - - 集成 asyncio 与 pytest: guides/asyncio-and-pytest.md - - 验证组件: - - 绑定信号与结构化数据: guides/data.md - - 编写 Driver: guides/drivers.md - - 采集 Monitor 输出: guides/monitors.md - - 接入 Agent 与参考模型: guides/agents-and-reference-models.md - - 使用 Scoreboard 检查事务: guides/scoreboard.md - - 使用协议视图: guides/protocols.md - - 流水输入与阶段重叠: guides/pipeline-examples.md - - 覆盖率与回归: - - 定义功能覆盖率: guides/coverage.md - - 用类定义覆盖模型: guides/coverage-classes.md - - 统计事务覆盖: guides/coverage-transactions.md - - 统计时序覆盖: guides/coverage-patterns.md - - 功能点与跨用例覆盖: guides/functional-points.md - - 组合验证环境: guides/verification-flow.md - - 运行测试与汇总覆盖率: guides/runner.md - - 迁移与维护: - - 迁移到 Toffee 2: guides/toffee-v2-migration.md - - 版本、CI 与发布: guides/versioning-and-releases.md - - 可运行示例: examples.md - - 参考: - - 参考目录: reference/index.md - - API 索引: reference/api.md - - Execution 参数与生命周期: reference/execution.md - - Runner 命令与产物: reference/runner.md - - 原生表达式与资源: guides/expressions-and-resources.md - - 行为与限制: reference/semantics-and-limitations.md - - 功能与回归索引: reference/feature-map.md - - 原理说明: - - 理解设计: explanation/index.md - - 仿真调度与资源归属: explanation/execution-model.md - - 检查、覆盖率与回归结果: explanation/coverage-results.md +- 首页: index.md +- 教程: + - 学习路线: getting-started/index.md + - 0. 准备环境: getting-started/installation.md + - 1. 第一次 PASS 和 FAIL: getting-started/first-test.md + - 2. 输入与时钟: getting-started/clock-and-input.md + - 3. 多组数据与边界: getting-started/more-cases.md + - 4. 提取 Driver: getting-started/driver.md + - 5. 延迟输出与 Monitor: getting-started/monitor.md + - 6. 组装 Agent: getting-started/agent-test.md + - 7. 模型与测试环境: getting-started/reference-model.md + - 8. 流水、多批次与取消: getting-started/pipeline.md + - 9. 看懂失败: getting-started/failures.md + - 10. 功能覆盖率: getting-started/coverage.md + - 选读:乱序响应: getting-started/out-of-order.md + - 选读:原生时钟与项目 DUT: getting-started/native-and-next.md +- 操作指南: + - 按任务查找: guides/index.md + - 仿真与事件: + - 配置 Execution 与 Backend: guides/execution-and-backend.md + - 等待 Trigger 与超时: guides/triggers.md + - 编译条件、Sequence 与 FSM: guides/compiled-triggers.md + - 持续订阅事件: guides/subscriptions.md + - 集成 asyncio 与 pytest: guides/asyncio-and-pytest.md + - 验证组件: + - 绑定信号与结构化数据: guides/data.md + - 编写 Driver: guides/drivers.md + - 采集 Monitor 输出: guides/monitors.md + - 接入 Agent 与参考模型: guides/agents-and-reference-models.md + - 使用 Scoreboard 检查事务: guides/scoreboard.md + - 使用协议视图: guides/protocols.md + - 流水输入与阶段重叠: guides/pipeline-examples.md + - 覆盖率与回归: + - 定义功能覆盖率: guides/coverage.md + - 用类定义覆盖模型: guides/coverage-classes.md + - 统计事务覆盖: guides/coverage-transactions.md + - 统计时序覆盖: guides/coverage-patterns.md + - 功能点与跨用例覆盖: guides/functional-points.md + - 组合验证环境: guides/verification-flow.md + - 运行测试与汇总覆盖率: guides/runner.md + - 迁移与维护: + - 迁移到 Toffee 2: guides/toffee-v2-migration.md + - 版本、CI 与发布: guides/versioning-and-releases.md + - 可运行示例: examples.md +- 参考: + - 参考目录: reference/index.md + - API 索引: reference/api.md + - Execution 参数与生命周期: reference/execution.md + - Runner 命令与产物: reference/runner.md + - 原生表达式与资源: guides/expressions-and-resources.md + - 行为与限制: reference/semantics-and-limitations.md + - 功能与回归索引: reference/feature-map.md +- 原理说明: + - 理解设计: explanation/index.md + - 仿真调度与资源归属: explanation/execution-model.md + - 检查、覆盖率与回归结果: explanation/coverage-results.md markdown_extensions: - - tables - - fenced_code - - toc: - permalink: true +- tables +- fenced_code +- toc: + permalink: true strict: true +docs_dir: docs +site_dir: build/docs-site +plugins: +- i18n: + docs_structure: folder + fallback_to_default: false + languages: + - locale: zh + default: true + name: 中文 + build: true + - locale: en + name: English + build: true + nav_translations: + 首页: Home + 教程: Tutorials + 学习路线: Learning path + 0. 准备环境: 0. Prepare your environment + 1. 第一次 PASS 和 FAIL: 1. Your first PASS and FAIL + 2. 输入与时钟: 2. Inputs and clocks + 3. 多组数据与边界: 3. More cases and boundaries + 4. 提取 Driver: 4. Extract a Driver + 5. 延迟输出与 Monitor: 5. Delayed output and a Monitor + 6. 组装 Agent: 6. Assemble an Agent + 7. 模型与测试环境: 7. Models and the test environment + 8. 流水、多批次与取消: 8. Pipelines, batches, and cancellation + 9. 看懂失败: 9. Understand failures + 10. 功能覆盖率: 10. Functional coverage + 选读:乱序响应: 'Optional: out-of-order responses' + 选读:原生时钟与项目 DUT: 'Optional: native clocks and project DUTs' + 操作指南: How-to guides + 按任务查找: Find your task + 仿真与事件: Simulation and events + 配置 Execution 与 Backend: Configure Execution and Backend + 等待 Trigger 与超时: Wait for triggers and timeouts + 编译条件、Sequence 与 FSM: Compile conditions, Sequence, and FSM + 持续订阅事件: Subscribe to events + 集成 asyncio 与 pytest: Integrate asyncio and pytest + 验证组件: Verification components + 绑定信号与结构化数据: Bind signals and structured data + 编写 Driver: Write a Driver + 采集 Monitor 输出: Capture Monitor output + 接入 Agent 与参考模型: Connect Agents and models + 使用 Scoreboard 检查事务: Check transactions with Scoreboard + 使用协议视图: Use protocol views + 流水输入与阶段重叠: Pipeline input and overlap stages + 覆盖率与回归: Coverage and regressions + 定义功能覆盖率: Define functional coverage + 用类定义覆盖模型: Declare coverage with classes + 统计事务覆盖: Collect transaction coverage + 统计时序覆盖: Collect temporal coverage + 功能点与跨用例覆盖: Requirements and cross-case coverage + 组合验证环境: Assemble a verification environment + 运行测试与汇总覆盖率: Run tests and aggregate coverage + 迁移与维护: Migration and maintenance + 迁移到 Toffee 2: Migrate to Toffee 2 + 版本、CI 与发布: Versions, CI, and releases + 可运行示例: Runnable examples + 参考: Reference + 参考目录: Reference index + API 索引: API index + Execution 参数与生命周期: Execution parameters and lifecycle + Runner 命令与产物: Runner commands and artifacts + 原生表达式与资源: Native expressions and resources + 行为与限制: Semantics and limitations + 功能与回归索引: Features and regressions + 原理说明: Explanation + 理解设计: Understand the design + 仿真调度与资源归属: Scheduling and resource ownership + 检查、覆盖率与回归结果: Checking, coverage, and regression results +- search: {} diff --git a/pyproject.toml b/pyproject.toml index c60227d..9a14b91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,10 @@ test = [ "pytest-cov>=5", ] docs = [ - "mkdocs>=1.6", + "mkdocs==1.6.1", + "mkdocs-material==9.7.7", + "mkdocs-static-i18n==1.3.1", + "jieba==0.42.1", ] [project.urls] diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 0000000..5344595 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,4 @@ +mkdocs==1.6.1 +mkdocs-material==9.7.7 +mkdocs-static-i18n==1.3.1 +jieba==0.42.1 diff --git a/scripts/docs.py b/scripts/docs.py index 68c609d..3484e4d 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -15,6 +15,7 @@ CONFIG = yaml.safe_load((ROOT / 'mkdocs.yml').read_text(encoding='utf-8')) DOCS = ROOT / CONFIG.get('docs_dir', 'docs') LINK = re.compile(r'(!?\[[^\]\n]*\]\()([^\s)]+)(\))') +FENCED = re.compile(r'(^```.*?^```[^\n]*$)', re.M | re.S) class Links(HTMLParser): @@ -41,9 +42,19 @@ def nav_items(entries): yield title, value +def languages(): + for plugin in CONFIG['plugins']: + if isinstance(plugin, dict) and 'i18n' in plugin: + return plugin['i18n']['languages'] + raise SystemExit('Configure the documentation languages in mkdocs.yml') + + def wiki_slug(page): relative = page.relative_to(DOCS) - return 'Home' if relative == Path('index.md') else relative.with_suffix('').as_posix().replace('/', '-') + locale, *parts = relative.parts + path = Path(*parts) + suffix = 'Home' if path == Path('index.md') else path.with_suffix('').as_posix().replace('/', '-') + return f'{locale}-{suffix}' def wiki_url(slug): @@ -53,10 +64,29 @@ def wiki_url(slug): def check(): pages = sorted(DOCS.rglob('*.md')) navigation = [Path(path) for _, path in nav_items(CONFIG['nav'])] - if len(navigation) != len(pages) or set(navigation) != {page.relative_to(DOCS) for page in pages}: - raise SystemExit('Navigation must include every documentation page exactly once') - parsed = {page.resolve(): Links(page.read_text(encoding='utf-8')) for page in pages} + locales = languages() + expected = {Path(lang['locale']) / path for lang in locales for path in navigation} + if len(navigation) != len(set(navigation)) or expected != {page.relative_to(DOCS) for page in pages}: + raise SystemExit('Navigation and translations must include every page exactly once in each language') + baseline = next(lang['locale'] for lang in locales if lang.get('default')) errors = [] + for path in navigation: + original = FENCED.findall((DOCS / baseline / path).read_text(encoding='utf-8')) + for lang in locales: + translated = DOCS / lang['locale'] / path + if FENCED.findall(translated.read_text(encoding='utf-8')) != original: + errors.append(f'{translated.relative_to(ROOT)}: translated code examples differ from {baseline}') + def check_titles(entries, translations, locale): + for entry in entries: + for title, value in entry.items(): + if title not in translations: + errors.append(f'{locale}: missing navigation translation for {title}') + if isinstance(value, list): + check_titles(value, translations, locale) + for lang in locales: + if not lang.get('default'): + check_titles(CONFIG['nav'], lang.get('nav_translations', {}), lang['locale']) + parsed = {page.resolve(): Links(page.read_text(encoding='utf-8')) for page in pages} for page, links in parsed.items(): for target in links.targets: url = urlsplit(target) @@ -69,7 +99,7 @@ def check(): errors.append(f'{page.relative_to(ROOT)}: missing anchor {target}') if errors: raise SystemExit('\n'.join(errors)) - print(f'Checked navigation and local links in {len(pages)} pages.') + print(f'Checked translations, shared examples, navigation, and local links in {len(pages)} pages.') return pages @@ -79,7 +109,7 @@ def export_wiki(args): if output.exists() and any(output.iterdir()): raise SystemExit('Choose an empty output directory; Wiki export does not overwrite existing work.') slugs = {page.resolve(): wiki_slug(page) for page in pages} - names = [*slugs.values(), '_Sidebar', '_Footer'] + names = [*slugs.values(), 'Home', '_Sidebar', '_Footer'] if len({name.casefold() for name in names}) != len(names): raise SystemExit('Wiki page names collide') output.mkdir(parents=True, exist_ok=True) @@ -101,26 +131,41 @@ def replace_link(match): destination += '#' + url.fragment return match[1] + destination + match[3] - chunks = re.split(r'(^```.*?^```[^\n]*$)', page.read_text(encoding='utf-8'), flags=re.M | re.S) + chunks = FENCED.split(page.read_text(encoding='utf-8')) for index in range(0, len(chunks), 2): chunks[index] = LINK.sub(replace_link, chunks[index]) source = repo + '/blob/main/' + quote(page.relative_to(ROOT).as_posix()) - content = f'[源文档 / Source]({source})\n\n' + ''.join(chunks) + relative = page.relative_to(DOCS) + switches = [f'[源文档 / Source]({source})'] + for lang in languages(): + if lang['locale'] != relative.parts[0]: + counterpart = DOCS / lang['locale'] / Path(*relative.parts[1:]) + switches.append(f'[{lang["name"]}]({wiki_url(slugs[counterpart.resolve()])})') + content = ' · '.join(switches) + '\n\n' + ''.join(chunks) (output / (slugs[page.resolve()] + '.md')).write_text(content, encoding='utf-8') - sidebar = [] - def add_navigation(entries, indent=''): + home = [f'# {CONFIG["site_name"]}', '', '选择语言 / Choose a language:', ''] + sidebar = [f'[首页 / Home]({wiki_url("Home")})', ''] + def add_navigation(entries, lang, indent=''): for entry in entries: for title, value in entry.items(): + title = lang.get('nav_translations', {}).get(title, title) if isinstance(value, list): sidebar.append(f'{indent}- **{title}**') - add_navigation(value, indent + ' ') + add_navigation(value, lang, indent + ' ') else: - sidebar.append(f'{indent}- [{title}]({wiki_url(slugs[(DOCS / value).resolve()])})') - add_navigation(CONFIG['nav']) + slug = slugs[(DOCS / lang['locale'] / value).resolve()] + sidebar.append(f'{indent}- [{title}]({wiki_url(slug)})') + for lang in languages(): + home.append(f'- [{lang["name"]}]({wiki_url(wiki_slug(DOCS / lang["locale"] / "index.md"))})') + sidebar.extend([f'## {lang["name"]}', '']) + add_navigation(CONFIG['nav'], lang) + sidebar.append('') + (output / 'Home.md').write_text('\n'.join(home) + '\n', encoding='utf-8') (output / '_Sidebar.md').write_text('\n'.join(sidebar) + '\n', encoding='utf-8') (output / '_Footer.md').write_text( - f'内容在 [{CONFIG["site_name"]} 主仓库]({repo}/tree/main/docs)维护,由 CI 自动同步。\n', + f'内容由 CI 自动同步;请编辑[主仓库文档]({repo}/tree/main/docs)。 ' + f'Automatically synced by CI; edit the [source documentation]({repo}/tree/main/docs).\n', encoding='utf-8', ) for page in output.glob('*.md'): diff --git a/scripts/tests/test_wiki_export.py b/scripts/tests/test_wiki_export.py index 73971b6..2c6ba86 100644 --- a/scripts/tests/test_wiki_export.py +++ b/scripts/tests/test_wiki_export.py @@ -24,45 +24,71 @@ def setUp(self): self.root = Path(directory.name).resolve() self.repo = 'https://github.com/XS-MLVP/toffee' config = {'site_name': 'Toffee', 'repo_url': self.repo, - 'nav': [{'Home': 'index.md'}, {'Guide': 'guide.md'}]} + 'nav': [{'首页': 'index.md'}, {'指南': 'guide.md'}], + 'plugins': [{'i18n': {'languages': [ + {'locale': 'zh', 'default': True, 'name': '中文'}, + {'locale': 'en', 'name': 'English', + 'nav_translations': {'首页': 'Home', '指南': 'Guide'}}, + ]}}]} self.enterContext(patch.multiple(docs, ROOT=self.root, DOCS=self.root / 'docs', CONFIG=config)) self.enterContext(contextlib.redirect_stdout(io.StringIO())) - (self.root / 'docs').mkdir() (self.root / 'LICENSE').write_text('License') (self.root / 'test image.png').write_bytes(b'image') - (self.root / 'docs/index.md').write_text( - '# Home\n\n[Guide](guide.md#install)\n\n[License](../LICENSE)\n\n' - '![Image](../test%20image.png)\n\n```text\n[Literal](missing.md)\n```\n' - ) - (self.root / 'docs/guide.md').write_text('# Guide\n\n## Install\n\n[Home](index.md)\n') + for locale in ('zh', 'en'): + directory = self.root / 'docs' / locale + directory.mkdir(parents=True) + (directory / 'index.md').write_text( + '# Home\n\n[Guide](guide.md#install)\n\n[License](../../LICENSE)\n\n' + '![Image](../../test%20image.png)\n\n```text\n[Literal](missing.md)\n```\n' + ) + (directory / 'guide.md').write_text('# Guide\n\n## Install\n\n[Home](index.md)\n') def test_export_preserves_code_and_rewrites_links_images_and_navigation(self): output = self.root / 'export' docs.export_wiki(SimpleNamespace(output=output)) - home = (output / 'Home.md').read_text() - self.assertIn(f'[Guide]({self.repo}/wiki/guide#install)', home) + home = (output / 'en-Home.md').read_text() + self.assertIn(f'[Guide]({self.repo}/wiki/en-guide#install)', home) + self.assertIn(f'[中文]({self.repo}/wiki/zh-Home)', home) + self.assertIn(f'[English]({self.repo}/wiki/en-Home)', (output / 'Home.md').read_text()) self.assertIn(f'[License]({self.repo}/blob/main/LICENSE)', home) self.assertIn(f'![Image]({self.repo}/raw/refs/heads/main/test%20image.png)', home) self.assertIn('```text\n[Literal](missing.md)\n```', home) - self.assertIn(f'[Guide]({self.repo}/wiki/guide)', (output / '_Sidebar.md').read_text()) - self.assertEqual(len(json.loads((output / 'manifest.json').read_text())), 4) + self.assertIn(f'[Guide]({self.repo}/wiki/en-guide)', (output / '_Sidebar.md').read_text()) + self.assertIn(f'[指南]({self.repo}/wiki/zh-guide)', (output / '_Sidebar.md').read_text()) + self.assertEqual(len(json.loads((output / 'manifest.json').read_text())), 7) def test_missing_links_fail_before_export(self): - (self.root / 'docs/guide.md').write_text('# Guide\n\n[Missing](missing.md)\n') + (self.root / 'docs/en/guide.md').write_text('# Guide\n\n[Missing](missing.md)\n') with self.assertRaisesRegex(SystemExit, 'missing or external path'): docs.export_wiki(SimpleNamespace(output=self.root / 'export')) self.assertFalse((self.root / 'export').exists()) def test_missing_anchor_is_rejected(self): - (self.root / 'docs/guide.md').write_text('# Guide\n\n[Missing](index.md#missing)\n') + (self.root / 'docs/en/guide.md').write_text('# Guide\n\n[Missing](index.md#missing)\n') with self.assertRaisesRegex(SystemExit, 'missing anchor'): docs.check() def test_unconverted_reference_link_fails_instead_of_publishing_broken_page(self): - (self.root / 'docs/guide.md').write_text('# Guide\n\n## Install\n\n[Home][home]\n\n[home]: index.md\n') + (self.root / 'docs/en/guide.md').write_text('# Guide\n\n## Install\n\n[Home][home]\n\n[home]: index.md\n') with self.assertRaisesRegex(SystemExit, 'Relative exported Wiki link'): docs.export_wiki(SimpleNamespace(output=self.root / 'export')) + def test_missing_translation_cannot_fall_back_to_default_language(self): + (self.root / 'docs/en/guide.md').unlink() + with self.assertRaisesRegex(SystemExit, 'translations must include every page'): + docs.check() + + def test_changed_translated_example_is_rejected(self): + page = self.root / 'docs/en/index.md' + page.write_text(page.read_text().replace('[Literal](missing.md)', 'different example')) + with self.assertRaisesRegex(SystemExit, 'translated code examples differ'): + docs.check() + + def test_missing_navigation_translation_is_rejected(self): + del docs.CONFIG['plugins'][0]['i18n']['languages'][1]['nav_translations']['指南'] + with self.assertRaisesRegex(SystemExit, 'missing navigation translation'): + docs.check() + if __name__ == '__main__': unittest.main() diff --git a/tests/README.md b/tests/README.md index a043836..54e836d 100644 --- a/tests/README.md +++ b/tests/README.md @@ -24,7 +24,7 @@ mkdocs build --strict | `typing/` | 公共 API 的类型正例和错误用法 | 入门示例和流水线覆盖示例也在默认测试范围内。 -[功能索引](../docs/reference/feature-map.md)列出指南、示例和对应回归。 +[功能索引](../docs/zh/reference/feature-map.md)列出指南、示例和对应回归。 ## 新增测试 diff --git a/tests/integration/test_documentation.py b/tests/integration/test_documentation.py index d2fa7b0..df1b823 100644 --- a/tests/integration/test_documentation.py +++ b/tests/integration/test_documentation.py @@ -13,7 +13,7 @@ def user_pages(): return [ROOT / name for name in ( - 'README.md', 'CONTRIBUTING.md', 'tests/README.md', + 'README.md', 'README.en.md', 'CONTRIBUTING.md', 'tests/README.md', 'examples/README.md', 'benchmarks/README.md', )] + sorted((ROOT / 'docs').rglob('*.md')) @@ -21,8 +21,9 @@ def user_pages(): @pytest.mark.parametrize('name', ( 'coverage-classes.md', 'coverage-transactions.md', 'coverage-patterns.md', )) -def test_first_coverage_definition_uses_the_real_public_api(name): - path = ROOT / 'docs/guides' / name +@pytest.mark.parametrize('language', ('zh', 'en')) +def test_first_coverage_definition_uses_the_real_public_api(name, language): + path = ROOT / 'docs' / language / 'guides' / name code = PYTHON.search(path.read_text()) assert code is not None, path namespace = {'__name__': '__main__'} diff --git a/tests/integration/test_quickstart_tutorial.py b/tests/integration/test_quickstart_tutorial.py index 5d2d458..a8eab82 100644 --- a/tests/integration/test_quickstart_tutorial.py +++ b/tests/integration/test_quickstart_tutorial.py @@ -16,10 +16,11 @@ ) -def test_tutorial_excerpts_match_the_executed_source(): +@pytest.mark.parametrize("language", ("zh", "en")) +def test_tutorial_excerpts_match_the_executed_source(language): included = set() regions = set() - for page in (ROOT / "docs/getting-started").glob("*.md"): + for page in (ROOT / "docs" / language / "getting-started").glob("*.md"): for name, region, code in BLOCK.findall(page.read_text()): path = ROOT / name assert path.is_relative_to(EXAMPLES), (page, path)