mirror of
https://github.com/eunomia-bpf/bpf-developer-tutorial.git
synced 2026-10-28 19:24:04 +08:00
docs: add generated tutorial compatibility matrix (#212)
* docs: declare tutorial compatibility requirements * docs: generate tutorial compatibility matrix * fix: correct BPF Arena minimum kernel * refactor: simplify compatibility validation --------- Co-authored-by: LinuxDev9002 <i@littlefisher.me>
This commit is contained in:
363
scripts/generate_compatibility.py
Normal file
363
scripts/generate_compatibility.py
Normal file
@@ -0,0 +1,363 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate tutorial compatibility metadata and generate compatibility tables."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REQUIRED_FIELDS = (
|
||||
"kernel_min",
|
||||
"kernel_min_basis",
|
||||
"architectures",
|
||||
"btf",
|
||||
"kernel_config",
|
||||
"hardware",
|
||||
"root",
|
||||
"test_status",
|
||||
)
|
||||
ARCHITECTURES = {"all", "x86_64", "arm64"}
|
||||
BTF_VALUES = {"required", "optional", "not-required"}
|
||||
ROOT_VALUES = {"required", "not-required"}
|
||||
TEST_STATUSES = {"ci-runtime", "ci-build", "not-in-ci", "docs-only"}
|
||||
KERNEL_BASES = {
|
||||
"documented",
|
||||
"required-feature",
|
||||
"repository-baseline",
|
||||
"unverified",
|
||||
"not-applicable",
|
||||
}
|
||||
HARDWARE_VALUES = {
|
||||
"none",
|
||||
"android-device-or-emulator",
|
||||
"block-device",
|
||||
"drm-gpu",
|
||||
"gpu",
|
||||
"intel-npu",
|
||||
"network-interface",
|
||||
"nvidia-cuda-gpu",
|
||||
}
|
||||
KERNEL_VERSION = re.compile(r"^\d+\.\d+$")
|
||||
ARCH_KERNEL_VERSION = re.compile(r"^(x86_64|arm64):\d+\.\d+$")
|
||||
KERNEL_CONFIG = re.compile(r"^CONFIG_[A-Z0-9_]+=(?:y|m|y\|m)$")
|
||||
REPOSITORY_BASELINE = "4.8"
|
||||
|
||||
|
||||
def sort_key(path: Path) -> tuple:
|
||||
"""Match the natural ordering used by the tutorial indexes."""
|
||||
relative = path.parent.as_posix()
|
||||
numbers = tuple(map(int, re.findall(r"\d+", relative)))
|
||||
return (0, numbers, relative) if numbers else (1, (), relative)
|
||||
|
||||
|
||||
def parse_config(path: Path) -> dict[str, str]:
|
||||
metadata: dict[str, str] = {}
|
||||
for line_number, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if "=" not in line:
|
||||
raise ValueError(f"{path}:{line_number}: expected key=value")
|
||||
key, value = line.split("=", 1)
|
||||
if key in metadata:
|
||||
raise ValueError(f"{path}:{line_number}: duplicate field {key}")
|
||||
metadata[key] = value
|
||||
return metadata
|
||||
|
||||
|
||||
def _csv(value: str) -> list[str]:
|
||||
return [item.strip() for item in value.split(",")]
|
||||
|
||||
|
||||
def _validate_architectures(path: Path, value: str) -> list[str]:
|
||||
architectures = _csv(value)
|
||||
if (
|
||||
not architectures
|
||||
or len(architectures) != len(set(architectures))
|
||||
or not set(architectures) <= ARCHITECTURES
|
||||
or ("all" in architectures and len(architectures) != 1)
|
||||
):
|
||||
raise ValueError(f"{path}: invalid architectures={value}")
|
||||
return architectures
|
||||
|
||||
|
||||
def _validate_per_arch_kernel(path: Path, value: str, architectures: list[str]) -> None:
|
||||
per_arch = _csv(value)
|
||||
per_arch_names = [item.split(":", 1)[0] for item in per_arch]
|
||||
valid = (
|
||||
per_arch
|
||||
and all(ARCH_KERNEL_VERSION.fullmatch(item) for item in per_arch)
|
||||
and len(per_arch_names) == len(set(per_arch_names))
|
||||
and set(per_arch_names) == set(architectures)
|
||||
)
|
||||
if not valid:
|
||||
raise ValueError(f"{path}: invalid kernel_min={value}")
|
||||
|
||||
|
||||
def _validate_kernel(path: Path, kernel_min: str, basis: str, architectures: list[str]) -> None:
|
||||
special_values = {"not-applicable", "unknown"}
|
||||
if kernel_min not in special_values and not KERNEL_VERSION.fullmatch(kernel_min):
|
||||
_validate_per_arch_kernel(path, kernel_min, architectures)
|
||||
if basis not in KERNEL_BASES:
|
||||
raise ValueError(f"{path}: invalid kernel_min_basis={basis}")
|
||||
if (kernel_min == "not-applicable") != (basis == "not-applicable"):
|
||||
raise ValueError(f"{path}: kernel_min and kernel_min_basis disagree")
|
||||
if (kernel_min == "unknown") != (basis == "unverified"):
|
||||
raise ValueError(f"{path}: unknown kernel_min must use an unverified basis")
|
||||
if basis == "repository-baseline" and kernel_min != REPOSITORY_BASELINE:
|
||||
raise ValueError(
|
||||
f"{path}: repository-baseline must be {REPOSITORY_BASELINE}, got {kernel_min}"
|
||||
)
|
||||
|
||||
|
||||
def _validate_choice(path: Path, field: str, value: str, allowed: set[str]) -> None:
|
||||
if value not in allowed:
|
||||
raise ValueError(f"{path}: invalid {field}={value}")
|
||||
|
||||
|
||||
def _validate_kernel_config(path: Path, metadata: dict[str, str]) -> None:
|
||||
value = metadata["kernel_config"]
|
||||
configs = _csv(value)
|
||||
if value != "none" and (
|
||||
not configs
|
||||
or len(configs) != len(set(configs))
|
||||
or not all(KERNEL_CONFIG.fullmatch(item) for item in configs)
|
||||
):
|
||||
raise ValueError(f"{path}: invalid kernel_config={value}")
|
||||
if metadata["btf"] == "required" and "CONFIG_DEBUG_INFO_BTF=y" not in configs:
|
||||
raise ValueError(f"{path}: btf=required needs CONFIG_DEBUG_INFO_BTF=y")
|
||||
if metadata["kernel_min"] != "not-applicable" and not {
|
||||
"CONFIG_BPF=y",
|
||||
"CONFIG_BPF_SYSCALL=y",
|
||||
} <= set(configs):
|
||||
raise ValueError(f"{path}: runnable tutorials need CONFIG_BPF=y and CONFIG_BPF_SYSCALL=y")
|
||||
|
||||
|
||||
def _validate_docs_only(path: Path, metadata: dict[str, str]) -> None:
|
||||
is_not_applicable = metadata["kernel_min"] == "not-applicable"
|
||||
if is_not_applicable != (metadata["test_status"] == "docs-only"):
|
||||
raise ValueError(f"{path}: docs-only status and not-applicable kernel must agree")
|
||||
defaults = {
|
||||
"kernel_min_basis": "not-applicable",
|
||||
"architectures": "all",
|
||||
"btf": "not-required",
|
||||
"kernel_config": "none",
|
||||
"hardware": "none",
|
||||
"root": "not-required",
|
||||
"test_status": "docs-only",
|
||||
}
|
||||
if is_not_applicable and any(metadata[field] != value for field, value in defaults.items()):
|
||||
raise ValueError(f"{path}: not-applicable tutorials must use docs-only defaults")
|
||||
|
||||
|
||||
def validate_metadata(path: Path, metadata: dict[str, str]) -> None:
|
||||
missing = [field for field in REQUIRED_FIELDS if not metadata.get(field)]
|
||||
if missing:
|
||||
raise ValueError(f"{path}: missing required fields: {', '.join(missing)}")
|
||||
|
||||
architectures = _validate_architectures(path, metadata["architectures"])
|
||||
_validate_kernel(
|
||||
path, metadata["kernel_min"], metadata["kernel_min_basis"], architectures
|
||||
)
|
||||
_validate_choice(path, "btf", metadata["btf"], BTF_VALUES)
|
||||
_validate_choice(path, "root", metadata["root"], ROOT_VALUES)
|
||||
_validate_choice(path, "test_status", metadata["test_status"], TEST_STATUSES)
|
||||
_validate_choice(path, "hardware", metadata["hardware"], HARDWARE_VALUES)
|
||||
_validate_kernel_config(path, metadata)
|
||||
_validate_docs_only(path, metadata)
|
||||
|
||||
|
||||
def _title(path: Path) -> str:
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
if line.startswith("#"):
|
||||
return line.lstrip("#").strip()
|
||||
raise ValueError(f"{path}: missing Markdown title")
|
||||
|
||||
|
||||
def collect_tutorials(src_dir: Path) -> list[dict[str, object]]:
|
||||
tutorials = []
|
||||
for config_path in sorted(
|
||||
src_dir.rglob(".config"), key=lambda path: sort_key(path.relative_to(src_dir))
|
||||
):
|
||||
metadata = parse_config(config_path)
|
||||
validate_metadata(config_path, metadata)
|
||||
lesson_dir = config_path.parent
|
||||
english = lesson_dir / "README.md"
|
||||
chinese = lesson_dir / "README.zh.md"
|
||||
if not english.exists() or not chinese.exists():
|
||||
raise ValueError(f"{lesson_dir}: both English and Chinese READMEs are required")
|
||||
tutorials.append(
|
||||
{
|
||||
"relative": lesson_dir.relative_to(src_dir),
|
||||
"title_en": _title(english),
|
||||
"title_zh": _title(chinese),
|
||||
"metadata": metadata,
|
||||
}
|
||||
)
|
||||
return tutorials
|
||||
|
||||
|
||||
DISPLAY = {
|
||||
"required": "Required",
|
||||
"optional": "Optional",
|
||||
"not-required": "Not required",
|
||||
"not-applicable": "N/A",
|
||||
"ci-runtime": "CI runtime",
|
||||
"ci-build": "CI build",
|
||||
"not-in-ci": "Not in CI",
|
||||
"docs-only": "Docs only",
|
||||
"none": "None",
|
||||
"all": "All",
|
||||
"documented": "Tutorial docs",
|
||||
"required-feature": "Required feature",
|
||||
"repository-baseline": "Repository baseline",
|
||||
"unverified": "Unverified",
|
||||
"unknown": "Unknown",
|
||||
"android-device-or-emulator": "Android device or emulator",
|
||||
"block-device": "Block device",
|
||||
"drm-gpu": "DRM GPU",
|
||||
"gpu": "GPU",
|
||||
"intel-npu": "Intel NPU",
|
||||
"network-interface": "Network interface",
|
||||
"nvidia-cuda-gpu": "NVIDIA CUDA GPU",
|
||||
}
|
||||
DISPLAY_ZH = {
|
||||
"required": "需要",
|
||||
"optional": "可选",
|
||||
"not-required": "不需要",
|
||||
"not-applicable": "不适用",
|
||||
"ci-runtime": "CI 运行测试",
|
||||
"ci-build": "CI 构建测试",
|
||||
"not-in-ci": "未纳入 CI",
|
||||
"docs-only": "仅文档",
|
||||
"none": "无",
|
||||
"all": "全部",
|
||||
"documented": "教程文档",
|
||||
"required-feature": "必需特性",
|
||||
"repository-baseline": "仓库基线",
|
||||
"unverified": "尚未验证",
|
||||
"unknown": "未知",
|
||||
"android-device-or-emulator": "Android 设备或模拟器",
|
||||
"block-device": "块设备",
|
||||
"drm-gpu": "DRM GPU",
|
||||
"gpu": "GPU",
|
||||
"intel-npu": "Intel NPU",
|
||||
"network-interface": "网络接口",
|
||||
"nvidia-cuda-gpu": "NVIDIA CUDA GPU",
|
||||
}
|
||||
|
||||
|
||||
def _display(value: str, chinese: bool = False) -> str:
|
||||
mapping = DISPLAY_ZH if chinese else DISPLAY
|
||||
if "," in value:
|
||||
return ", ".join(_display(item, chinese) for item in _csv(value))
|
||||
return mapping.get(value, value.replace("-", " "))
|
||||
|
||||
|
||||
def render(tutorials: list[dict[str, object]], chinese: bool = False) -> str:
|
||||
if chinese:
|
||||
lines = [
|
||||
"# 教程兼容性矩阵",
|
||||
"",
|
||||
"<!-- Generated by scripts/generate_compatibility.py; do not edit directly. -->",
|
||||
"",
|
||||
"本表由各教程的 `.config` 元数据生成。`最低内核` 表示教程按当前写法声明支持的最低版本;`依据` 标明该值来自教程文档、必需特性首次出现的版本,还是仓库声明的基线。`未纳入 CI` 不表示已做过手工验证。内核发行版可能禁用所列配置,因此版本号本身并不能保证兼容。",
|
||||
"",
|
||||
"| 教程 | 最低内核 | 依据 | 架构 | BTF | 核心内核配置 | 硬件 | Root | 测试状态 |",
|
||||
"| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
|
||||
]
|
||||
else:
|
||||
lines = [
|
||||
"# Tutorial compatibility matrix",
|
||||
"",
|
||||
"<!-- Generated by scripts/generate_compatibility.py; do not edit directly. -->",
|
||||
"",
|
||||
"This table is generated from each tutorial's `.config` metadata. `Minimum kernel` means the oldest kernel that the tutorial currently declares support for. `Basis` says whether that value comes from the tutorial documentation, the first version of a required feature, or the repository's declared baseline. `Not in CI` does not imply that a manual test was performed. Distribution kernels may disable a listed option, so a version number alone does not guarantee compatibility.",
|
||||
"",
|
||||
"| Tutorial | Minimum kernel | Basis | Architectures | BTF | Core kernel config | Hardware | Root | Test status |",
|
||||
"| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
|
||||
]
|
||||
|
||||
for tutorial in tutorials:
|
||||
relative = tutorial["relative"]
|
||||
metadata = tutorial["metadata"]
|
||||
assert isinstance(relative, Path)
|
||||
assert isinstance(metadata, dict)
|
||||
readme = "README.zh.md" if chinese else "README.md"
|
||||
title_key = "title_zh" if chinese else "title_en"
|
||||
title = str(tutorial[title_key]).replace("|", "\\|")
|
||||
link = f"{relative.as_posix()}/{readme}"
|
||||
values = [
|
||||
metadata["kernel_min"],
|
||||
metadata["kernel_min_basis"],
|
||||
metadata["architectures"],
|
||||
metadata["btf"],
|
||||
metadata["kernel_config"],
|
||||
metadata["hardware"],
|
||||
metadata["root"],
|
||||
metadata["test_status"],
|
||||
]
|
||||
displayed = [_display(value, chinese).replace("|", "\\|") for value in values]
|
||||
lines.append(f"| [{title}]({link}) | {' | '.join(displayed)} |")
|
||||
|
||||
lines.extend(["", "## Metadata fields" if not chinese else "## 元数据字段", ""])
|
||||
if chinese:
|
||||
lines.extend(
|
||||
[
|
||||
"- `kernel_min` 与 `kernel_min_basis`:教程声明的最低内核及其依据;按架构不同时使用 `架构:版本`。",
|
||||
"- `architectures`:教程当前声明支持的架构。",
|
||||
"- `btf` 与 `kernel_config`:BTF 要求以及各实现路径共有的核心内核选项;供应商或挂载点特有的条件请参阅对应教程。`y|m` 表示内建或模块均可。",
|
||||
"- `hardware`:运行教程所需的专用硬件或环境;`none` 表示不需要专用硬件。",
|
||||
"- `root`:运行教程是否需要 root 权限。",
|
||||
"- `test_status`:`ci-runtime` 会在 CI 中实际运行,`ci-build` 仅验证构建,`not-in-ci` 表示没有 CI 证据,`docs-only` 表示纯文档。",
|
||||
"",
|
||||
"字段定义和允许值由 `scripts/generate_compatibility.py` 校验;修改教程要求时,请更新对应 `.config` 并重新运行生成脚本。",
|
||||
]
|
||||
)
|
||||
else:
|
||||
lines.extend(
|
||||
[
|
||||
"- `kernel_min` and `kernel_min_basis`: the declared minimum kernel and its evidence basis; use `architecture:version` when architectures differ.",
|
||||
"- `architectures`: architectures the tutorial currently declares support for.",
|
||||
"- `btf` and `kernel_config`: the BTF requirement and core kernel options common to the implementation paths; see the tutorial for vendor- or hook-specific conditions. `y|m` means built in or loaded as a module.",
|
||||
"- `hardware`: specialized hardware or environment required to run the tutorial; `none` means no specialized hardware.",
|
||||
"- `root`: whether running the tutorial requires root privileges.",
|
||||
"- `test_status`: `ci-runtime` executes in CI, `ci-build` checks only the build, `not-in-ci` means there is no CI evidence, and `docs-only` is documentation only.",
|
||||
"",
|
||||
"Field definitions and allowed values are validated by `scripts/generate_compatibility.py`. When a tutorial requirement changes, update its `.config` and rerun the generator.",
|
||||
]
|
||||
)
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate(project_root: Path, check: bool = False) -> None:
|
||||
tutorials = collect_tutorials(project_root / "src")
|
||||
outputs = {
|
||||
project_root / "src" / "compatibility.md": render(tutorials),
|
||||
project_root / "src" / "compatibility.zh.md": render(tutorials, chinese=True),
|
||||
}
|
||||
stale = []
|
||||
for path, content in outputs.items():
|
||||
if check:
|
||||
if not path.exists() or path.read_text(encoding="utf-8") != content:
|
||||
stale.append(path)
|
||||
else:
|
||||
path.write_text(content, encoding="utf-8")
|
||||
print(f"Generated: {path}")
|
||||
if stale:
|
||||
names = ", ".join(str(path.relative_to(project_root)) for path in stale)
|
||||
raise SystemExit(f"Generated compatibility documentation is stale: {names}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--check", action="store_true", help="validate without rewriting files")
|
||||
args = parser.parse_args()
|
||||
generate(Path(__file__).resolve().parent.parent, check=args.check)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -3,7 +3,8 @@ import re
|
||||
|
||||
# Define a function to walk through the directory and generate the TOC structure
|
||||
def generate_toc(base_dir, project_root, output_file_dir):
|
||||
toc = "## Table of Contents\n\n"
|
||||
compatibility = os.path.relpath(os.path.join(base_dir, "compatibility.md"), output_file_dir)
|
||||
toc = f"## Table of Contents\n\n- [Tutorial compatibility matrix]({compatibility})\n\n"
|
||||
section_headers = {
|
||||
"Basic": "### Getting Started Examples\n\nThis section contains simple eBPF program examples and introductions. It primarily utilizes the `eunomia-bpf` framework to simplify development and introduces the basic usage and development process of eBPF.\n\n",
|
||||
"Advance": "### Advanced Documents and Examples\n\nWe start to build complete eBPF projects mainly based on `libbpf` and combine them with various application scenarios for practical use.\n\n",
|
||||
@@ -135,7 +136,8 @@ def generate_toc(base_dir, project_root, output_file_dir):
|
||||
|
||||
# Define a function to walk through the directory and generate the TOC structure in Chinese
|
||||
def generate_toc_cn(base_dir, project_root, output_file_dir):
|
||||
toc = "## 目录\n\n"
|
||||
compatibility = os.path.relpath(os.path.join(base_dir, "compatibility.zh.md"), output_file_dir)
|
||||
toc = f"## 目录\n\n- [教程兼容性矩阵]({compatibility})\n\n"
|
||||
section_headers = {
|
||||
"Basic": "### 入门示例\n\n这一部分包含简单的 eBPF 程序示例和介绍。主要利用 `eunomia-bpf` 框架简化开发,介绍 eBPF 的基本用法和开发流程。\n\n",
|
||||
"Advance": "### 高级文档和示例\n\n我们开始构建完整的 eBPF 项目,主要基于 `libbpf`,并将其与各种应用场景结合起来,以便实际使用。\n\n",
|
||||
|
||||
129
scripts/test_generate_compatibility.py
Normal file
129
scripts/test_generate_compatibility.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.generate_compatibility import (
|
||||
REQUIRED_FIELDS,
|
||||
collect_tutorials,
|
||||
parse_config,
|
||||
render,
|
||||
validate_metadata,
|
||||
)
|
||||
|
||||
|
||||
class CompatibilityMetadataTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.project_root = Path(__file__).resolve().parent.parent
|
||||
cls.tutorials = collect_tutorials(cls.project_root / "src")
|
||||
|
||||
def test_every_tutorial_has_valid_metadata(self):
|
||||
config_paths = list((self.project_root / "src").rglob(".config"))
|
||||
self.assertTrue(config_paths)
|
||||
self.assertEqual(len(config_paths), len(self.tutorials))
|
||||
for path in config_paths:
|
||||
metadata = parse_config(path)
|
||||
self.assertTrue(set(REQUIRED_FIELDS) <= set(metadata), path)
|
||||
|
||||
def test_bilingual_tables_contain_every_tutorial(self):
|
||||
for chinese in (False, True):
|
||||
table = render(self.tutorials, chinese=chinese)
|
||||
self.assertEqual(len(self.tutorials), table.count("| ["))
|
||||
for tutorial in self.tutorials:
|
||||
readme = "README.zh.md" if chinese else "README.md"
|
||||
expected = f"{tutorial['relative'].as_posix()}/{readme}"
|
||||
self.assertIn(expected, table)
|
||||
|
||||
def test_missing_field_is_rejected(self):
|
||||
metadata = {
|
||||
"kernel_min": "5.15",
|
||||
"kernel_min_basis": "required-feature",
|
||||
"architectures": "x86_64",
|
||||
"btf": "optional",
|
||||
"kernel_config": "CONFIG_BPF=y",
|
||||
"hardware": "none",
|
||||
"root": "required",
|
||||
}
|
||||
with self.assertRaisesRegex(ValueError, "test_status"):
|
||||
validate_metadata(Path("example/.config"), metadata)
|
||||
|
||||
def test_required_btf_config_is_enforced(self):
|
||||
metadata = {
|
||||
"kernel_min": "5.15",
|
||||
"kernel_min_basis": "required-feature",
|
||||
"architectures": "x86_64",
|
||||
"btf": "required",
|
||||
"kernel_config": "CONFIG_BPF=y",
|
||||
"hardware": "none",
|
||||
"root": "required",
|
||||
"test_status": "not-in-ci",
|
||||
}
|
||||
with self.assertRaisesRegex(ValueError, "CONFIG_DEBUG_INFO_BTF"):
|
||||
validate_metadata(Path("example/.config"), metadata)
|
||||
|
||||
def test_duplicate_config_field_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / ".config"
|
||||
path.write_text("level=Basic\nlevel=Depth\n", encoding="utf-8")
|
||||
with self.assertRaisesRegex(ValueError, "duplicate field level"):
|
||||
parse_config(path)
|
||||
|
||||
def test_empty_csv_item_is_rejected(self):
|
||||
metadata = {
|
||||
"kernel_min": "5.15",
|
||||
"kernel_min_basis": "repository-baseline",
|
||||
"architectures": "x86_64,,arm64",
|
||||
"btf": "optional",
|
||||
"kernel_config": "CONFIG_BPF=y",
|
||||
"hardware": "none",
|
||||
"root": "required",
|
||||
"test_status": "not-in-ci",
|
||||
}
|
||||
with self.assertRaisesRegex(ValueError, "architectures"):
|
||||
validate_metadata(Path("example/.config"), metadata)
|
||||
|
||||
def test_per_arch_kernel_min_covers_all_architectures(self):
|
||||
metadata = {
|
||||
"kernel_min": "x86_64:5.5",
|
||||
"kernel_min_basis": "documented",
|
||||
"architectures": "x86_64,arm64",
|
||||
"btf": "optional",
|
||||
"kernel_config": "CONFIG_BPF=y",
|
||||
"hardware": "none",
|
||||
"root": "required",
|
||||
"test_status": "not-in-ci",
|
||||
}
|
||||
with self.assertRaisesRegex(ValueError, "kernel_min"):
|
||||
validate_metadata(Path("example/.config"), metadata)
|
||||
|
||||
def test_not_applicable_basis_requires_not_applicable_kernel(self):
|
||||
metadata = {
|
||||
"kernel_min": "5.15",
|
||||
"kernel_min_basis": "not-applicable",
|
||||
"architectures": "x86_64",
|
||||
"btf": "optional",
|
||||
"kernel_config": "CONFIG_BPF=y",
|
||||
"hardware": "none",
|
||||
"root": "required",
|
||||
"test_status": "not-in-ci",
|
||||
}
|
||||
with self.assertRaisesRegex(ValueError, "disagree"):
|
||||
validate_metadata(Path("example/.config"), metadata)
|
||||
|
||||
def test_repository_baseline_is_consistent(self):
|
||||
metadata = {
|
||||
"kernel_min": "5.15",
|
||||
"kernel_min_basis": "repository-baseline",
|
||||
"architectures": "x86_64",
|
||||
"btf": "optional",
|
||||
"kernel_config": "CONFIG_BPF=y,CONFIG_BPF_SYSCALL=y",
|
||||
"hardware": "none",
|
||||
"root": "required",
|
||||
"test_status": "not-in-ci",
|
||||
}
|
||||
with self.assertRaisesRegex(ValueError, "repository-baseline must be 4.8"):
|
||||
validate_metadata(Path("example/.config"), metadata)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user