Merge commit 'ac794a6a7051497ff60b947642d44f0f8c04d6c6' into lsp-server

This commit is contained in:
csh
2026-01-08 15:56:37 +08:00
113 changed files with 254751 additions and 176 deletions
+435
View File
@@ -0,0 +1,435 @@
# 🧪 Playbook 测试套件
本目录包含 Playbook 项目的完整测试框架,用于验证脚本、模板和文档的正确性。
## 📋 目录结构
```txt
tests/
├── README.md # 本文件:测试文档
├── scripts/ # Shell 脚本测试(bats
│ ├── test_sync_standards.bats # sync_standards.sh 测试
│ ├── test_vendor_playbook.bats # vendor_playbook.sh 测试
│ └── test_install_codex_skills.bats # install_codex_skills.sh 测试
├── templates/ # 模板验证测试
│ ├── validate_python_templates.sh # Python 模板验证
│ ├── validate_cpp_templates.sh # C++ 模板验证
│ └── validate_ci_templates.sh # CI 模板验证
└── integration/ # 集成测试
└── check_doc_links.sh # 文档链接有效性检查
```
## 🚀 快速开始
### 本地运行所有测试
```bash
# 进入 playbook 根目录
cd /path/to/playbook
# 1. 运行 Shell 脚本测试(需要 bats)
sudo apt-get install bats # Ubuntu/Debian
cd tests/scripts
bats test_sync_standards.bats
bats test_vendor_playbook.bats
bats test_install_codex_skills.bats
# 2. 运行模板验证测试
cd tests/templates
sh validate_python_templates.sh
sh validate_cpp_templates.sh
sh validate_ci_templates.sh
# 3. 运行集成测试
cd tests/integration
sh check_doc_links.sh
```
### CI 自动化测试
测试套件通过 Gitea Actions 自动运行(见 `.gitea/workflows/test.yml`):
- **触发时机**
- 推送到 `main` 分支
- Pull Request 到 `main` 分支
- 手动触发(workflow_dispatch
- **运行平台**ubuntu-22.04
- **并行策略**:使用 matrix 策略并行运行多个测试组
## 📚 测试详解
### 1. Shell 脚本测试 (scripts/)
使用 [bats-core](https://github.com/bats-core/bats-core) 框架测试 shell 脚本。
#### test_sync_standards.bats
测试 `scripts/sync_standards.sh` 脚本的功能:
- **基础功能**
- 脚本存在且可执行
- 无参数时默认同步 tsl(已有 `.agents/<lang>/` 则自动识别)
- 单语言同步(tsl/cpp
- 多语言同步(tsl cpp
- **.gitattributes 同步**
- 默认模式追加缺失规则
- 保留现有内容
- 更新已存在的 managed block
- **AGENTS.md 处理**
- 不存在时自动创建
- 已存在时不覆盖
- **备份功能**
- 更新前创建备份
- **错误处理**
- 未找到 playbook 快照时报错
- 无效语言参数时报错
- **环境变量**
- `SYNC_GITATTR_MODE` 配置
- **幂等性**
- 多次执行结果一致
#### test_vendor_playbook.bats
测试 `scripts/vendor_playbook.sh` 脚本的功能:
- **基础功能**
- 单语言 vendoring
- 多语言 vendoring
- **自动同步**
- 自动执行 sync_standards
- **SOURCE.md 生成**
- 包含来源信息
- 包含 commit hash
- 包含时间戳
- **裁剪功能**
- 仅包含指定语言
- 始终包含 common 目录
- 包含对应模板文件
- **目标目录处理**
- 已存在时覆盖更新
- 创建必要的父目录
- **错误处理**
- 目标目录不存在时报错
- 无效语言参数时报错
- **完整性验证**
- 所有必要文件已复制
- 脚本可执行
- **幂等性**
- 多次 vendor 结果一致
#### test_install_codex_skills.bats
测试 `scripts/install_codex_skills.sh` 脚本的功能:
- **基础功能**
- 脚本存在且可执行
- **安装功能**
- 创建 skills 目录
- 复制 skill 目录(包含 `SKILL.md`
- 支持指定单个 skill 安装
- 同名目录安装前创建备份
- **错误处理**
- 指定不存在的 skill 报错
- **幂等性**
- 多次安装结果一致
### 2. 模板验证测试 (templates/)
验证项目模板文件的正确性和完整性。
#### validate_python_templates.sh
验证 `templates/python/` 目录下的 Python 模板:
- **pyproject.toml**
- 文件存在
- TOML 语法正确
- 包含必要配置节(tool.black, tool.isort, tool.pytest.ini_options
- black line-length 配置正确
- isort profile 配置正确
- **.flake8**
- 文件存在
- 包含 [flake8] 配置
- 配置了 max-line-length
- 配置了错误忽略规则
- **.pylintrc**
- 文件存在
- 包含必要配置节(MASTER, MESSAGES CONTROL, FORMAT
- 配置了 max-line-length
- **.pre-commit-config.yaml**
- 文件存在
- YAML 语法正确
- 包含 repos 配置
- 配置了常用 hooksblack, isort, flake8
- **.editorconfig**
- 文件存在
- 包含 root = true
- 包含 Python 文件配置
- **.vscode/settings.json**
- 文件存在
- JSON 语法正确
- 包含 Python 配置
#### validate_cpp_templates.sh
验证 `templates/cpp/` 目录下的 C++ 模板:
- **CMakeLists.txt**
- 文件存在
- CMake 基础语法正确
- 配置了 C++23 标准
- 启用了 C++ Modules 扫描(可选)
- 启用了 import std; 支持(可选)
- 启用了编译命令导出(用于 clangd)
- **.clang-format**
- 文件存在
- YAML 语法正确
- 配置了 Language: Cpp
- 基于某个风格(BasedOnStyle
- 配置了 C++ 标准
- 配置了缩进宽度
- **.clangd**
- 文件存在
- YAML 语法正确
- 包含 CompileFlags 配置
- 配置了 -std=c++23
- 配置了 CompilationDatabase 路径
- 配置了 Index 选项
- **conanfile.txt**
- 文件存在
- 包含 [requires] 配置(可选)
- 包含 [generators] 配置
- 配置了 CMakeDeps 生成器
- 配置了 CMakeToolchain 生成器
- 包含 [options] 配置
- **CMakeUserPresets.json**
- 文件存在
- JSON 语法正确
- 包含 version 字段
- 包含 configurePresets
- 包含 buildPresets
#### validate_ci_templates.sh
验证 `templates/ci/` 目录下的 CI 模板:
- **Gitea/GitHub workflow 文件**
- 文件存在
- YAML 语法正确
- 包含必要字段(name, on, jobs, runs-on, steps
- 包含中文注释(符合项目风格)
- 包含配置区域标记
- 包含环境变量配置
- **特定 workflow 模板**
- standards-check.yml:包含格式化检查和 lint 检查
- test.yml:包含测试步骤和测试矩阵
- **文档**
- 包含 README 说明文档
- 包含使用说明
### 3. 集成测试 (integration/)
端到端测试,验证整体功能。
#### check_doc_links.sh
检查所有 Markdown 文档中的链接有效性:
- **扫描范围**
- 所有 `*.md` 文件
- 排除 node_modules, .git, build, dist 等目录
- **链接类型**
- Markdown 链接:`[text](link)`
- 引用链接:`[text]: link`
- **验证逻辑**
- 检查相对路径链接是否指向存在的文件
- 跳过外部链接(http/https
- 跳过 mailto 链接
- 跳过代码块与行内代码中的链接样式文本
- 支持锚点(但只验证文件存在性)
- **报告内容**
- 总链接数
- 有效链接数
- 跳过链接数(外部/mailto
- 断开链接详情(文件、行号、链接、目标路径)
## 🔧 本地开发
### 安装测试依赖
**Ubuntu/Debian**
```bash
# bats-core (Shell 脚本测试)
sudo apt-get update
sudo apt-get install bats
# Python 工具(模板验证)
pip install toml tomli jsonschema yamllint
# C++ 工具(模板验证,可选)
sudo apt-get install cmake clang-format
# YAML 验证
pip install yamllint
```
**macOS**
```bash
# bats-core
brew install bats-core
# Python 工具
pip3 install toml tomli jsonschema yamllint
# C++ 工具(可选)
brew install cmake clang-format
# YAML 验证
pip3 install yamllint
```
### 运行单个测试
```bash
# Shell 脚本测试
cd tests/scripts
bats test_sync_standards.bats --tap # TAP 格式输出
bats test_vendor_playbook.bats --formatter junit # JUnit 格式
# 模板验证测试
cd tests/templates
sh validate_python_templates.sh
sh validate_cpp_templates.sh
sh validate_ci_templates.sh
# 集成测试
cd tests/integration
sh check_doc_links.sh
```
### 添加新测试
#### 添加新的 bats 测试
`tests/scripts/` 创建新文件 `test_<script_name>.bats`
```bash
#!/usr/bin/env bats
setup() {
# 测试前准备
export TEST_DIR="$(mktemp -d)"
}
teardown() {
# 测试后清理
rm -rf "$TEST_DIR"
}
@test "描述测试内容" {
# 测试代码
[ -f "some_file" ]
}
```
#### 添加新的模板验证
`tests/templates/` 创建新文件 `validate_<template_type>_templates.sh`,参考现有脚本结构。
#### 添加新的集成测试
`tests/integration/` 创建新脚本,确保:
1. 使用 `set -eu` 启用错误检测
2. 输出清晰的测试进度
3. 生成详细的报告文件
4. 返回正确的退出码(0 = 成功,非 0 = 失败)
## 📊 测试覆盖率目标
- **Shell 脚本测试**:目标覆盖率 ≥ 80%
- **模板验证测试**:目标通过率 = 100%
- **集成测试**:目标通过率 = 100%
- **文档链接有效性**:目标有效率 = 100%
## 🐛 故障排查
### bats 测试失败
```bash
# 使用 --verbose 查看详细输出
bats --verbose test_sync_standards.bats
# 使用 --trace 查看执行跟踪
bats --trace test_sync_standards.bats
```
### 模板验证失败
验证脚本会生成详细报告文件:
- `tests/templates/python_validation_report.txt`
- `tests/templates/cpp_validation_report.txt`
- `tests/templates/ci_validation_report.txt`
### 文档链接检查失败
查看详细报告:
```bash
cat /tmp/doc_links_report.txt
```
## 🤝 贡献指南
添加新功能时,请同步更新相应的测试:
1. **修改脚本**`scripts/`)→ 更新对应的 `.bats` 测试
2. **修改模板**`templates/`)→ 更新对应的验证脚本
3. **修改文档**`docs/`, `.agents/`)→ 运行文档链接检查
4. **修改 CI workflow**`.gitea/workflows/`)→ 验证 YAML 语法
## 📖 相关文档
- [开发规范索引](../docs/index.md)
- [提交信息规范](../docs/common/commit_message.md)
- [Gitea Actions 文档](https://docs.gitea.com/usage/actions/overview)
- [bats-core 文档](https://bats-core.readthedocs.io/)
## ❓ 常见问题
### Q: 为什么测试在 CI 通过,但本地失败?
A: 可能原因:
- 环境差异(工具版本、路径)
- 权限问题
- Git 配置差异
建议使用 Docker 容器复现 CI 环境。
### Q: 如何跳过某些测试?
A: bats 支持 `skip` 命令:
```bash
@test "某个测试" {
skip "原因说明"
# 测试代码
}
```
### Q: 测试运行很慢,如何加速?
A: 建议:
1. 使用 bats 的 `--jobs` 参数并行运行
2. 只运行变更相关的测试
3. 使用 CI 的缓存机制
---
**测试套件维护者**: Playbook 团队
**最后更新**: 2026-01-07
@@ -0,0 +1,226 @@
#!/usr/bin/env sh
# 文档链接有效性检查脚本
set -eu
echo "========================================"
echo "🔗 文档链接有效性检查"
echo "========================================"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLAYBOOK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
TOTAL_LINKS=0
VALID_LINKS=0
BROKEN_LINKS=0
SKIPPED_LINKS=0
BROKEN_LINKS_FILE="/tmp/broken_links.txt"
REPORT_FILE="/tmp/doc_links_report.txt"
> "$BROKEN_LINKS_FILE"
> "$REPORT_FILE"
echo "📁 Playbook 根目录: $PLAYBOOK_ROOT"
echo ""
# ============================================
# 辅助函数
# ============================================
check_file_link() {
local source_file="$1"
local link_path="$2"
local link_line="$3"
TOTAL_LINKS=$((TOTAL_LINKS + 1))
# 处理相对路径
local source_dir
source_dir="$(dirname "$source_file")"
# 解析链接路径
local target_path="$link_path"
# 移除锚点
target_path="${target_path%%#*}"
# 跳过空链接
if [ -z "$target_path" ]; then
SKIPPED_LINKS=$((SKIPPED_LINKS + 1))
return 0
fi
# 跳过外部链接(http/https
if echo "$target_path" | grep -qE "^https?://"; then
SKIPPED_LINKS=$((SKIPPED_LINKS + 1))
return 0
fi
# 跳过 mailto 链接
if echo "$target_path" | grep -q "^mailto:"; then
SKIPPED_LINKS=$((SKIPPED_LINKS + 1))
return 0
fi
# 构建绝对路径
local absolute_path
if echo "$target_path" | grep -q "^/"; then
# 绝对路径(从仓库根)
absolute_path="$PLAYBOOK_ROOT$target_path"
else
# 相对路径
absolute_path="$source_dir/$target_path"
fi
# 规范化路径
absolute_path="$(cd "$(dirname "$absolute_path")" 2>/dev/null && pwd)/$(basename "$absolute_path")" || absolute_path=""
# 检查文件是否存在
if [ -n "$absolute_path" ] && [ -e "$absolute_path" ]; then
VALID_LINKS=$((VALID_LINKS + 1))
return 0
else
BROKEN_LINKS=$((BROKEN_LINKS + 1))
echo "❌ 断链: $source_file:$link_line" >> "$BROKEN_LINKS_FILE"
echo " 链接: $link_path" >> "$BROKEN_LINKS_FILE"
echo " 目标: $absolute_path" >> "$BROKEN_LINKS_FILE"
echo "" >> "$BROKEN_LINKS_FILE"
return 1
fi
}
extract_links() {
awk '
BEGIN { in_code = 0 }
{
line = $0
if (line ~ /^```/) { in_code = !in_code; next }
if (in_code) next
gsub(/`[^`]*`/, "", line)
while (match(line, /\[[^]]+\]\([^)]*\)/)) {
link = substr(line, RSTART, RLENGTH)
sub(/^\[[^]]+\]\(/, "", link)
sub(/\)$/, "", link)
print NR "\t" link
line = substr(line, RSTART + RLENGTH)
}
if (match(line, /^\[[^]]+\]:[[:space:]]*[^[:space:]]+/)) {
link = substr(line, RSTART, RLENGTH)
sub(/^\[[^]]+\]:[[:space:]]*/, "", link)
sub(/[[:space:]].*$/, "", link)
print NR "\t" link
}
}
' "$1"
}
# ============================================
# 查找并检查所有 Markdown 文件
# ============================================
echo "🔍 扫描 Markdown 文件..."
cd "$PLAYBOOK_ROOT"
MD_FILES=$(find . -name "*.md" \
-not -path "*/node_modules/*" \
-not -path "*/.git/*" \
-not -path "*/build/*" \
-not -path "*/dist/*" \
2>/dev/null || true)
FILE_COUNT=$(echo "$MD_FILES" | grep -c "^" || echo 0)
echo "📄 找到 $FILE_COUNT 个 Markdown 文件"
echo ""
CURRENT_FILE_NUM=0
for md_file in $MD_FILES; do
CURRENT_FILE_NUM=$((CURRENT_FILE_NUM + 1))
# 显示进度
if [ "$CURRENT_FILE_NUM" -eq 1 ] || [ $((CURRENT_FILE_NUM % 10)) -eq 0 ] || [ "$CURRENT_FILE_NUM" -eq "$FILE_COUNT" ]; then
echo "📖 处理中... [$CURRENT_FILE_NUM/$FILE_COUNT] $md_file"
fi
links_file="$(mktemp)"
extract_links "$md_file" > "$links_file"
while IFS="$(printf '\t')" read -r line_num link; do
check_file_link "$md_file" "$link" "$line_num" || true
done < "$links_file"
rm -f "$links_file"
done
echo ""
echo "✅ 扫描完成"
echo ""
# ============================================
# 生成检查报告
# ============================================
echo "========================================"
echo "📊 链接检查结果统计"
echo "========================================"
echo "🔗 总链接数: $TOTAL_LINKS"
echo "✅ 有效链接: $VALID_LINKS"
echo "⏭️ 跳过链接: $SKIPPED_LINKS (外部/mailto)"
echo "❌ 断开链接: $BROKEN_LINKS"
if [ "$TOTAL_LINKS" -gt 0 ]; then
CHECKED_LINKS=$((TOTAL_LINKS - SKIPPED_LINKS))
if [ "$CHECKED_LINKS" -gt 0 ]; then
SUCCESS_RATE=$(awk "BEGIN {printf \"%.1f\", ($VALID_LINKS * 100.0) / $CHECKED_LINKS}")
echo "📈 有效率: $SUCCESS_RATE%"
fi
fi
echo ""
# 写入报告
{
echo "文档链接有效性检查报告"
echo "========================"
echo ""
echo "检查时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "检查目录: $PLAYBOOK_ROOT"
echo ""
echo "统计结果:"
echo " 总链接数: $TOTAL_LINKS"
echo " 有效链接: $VALID_LINKS"
echo " 跳过链接: $SKIPPED_LINKS"
echo " 断开链接: $BROKEN_LINKS"
echo ""
if [ "$BROKEN_LINKS" -gt 0 ]; then
echo "断开链接详情:"
echo "=============="
cat "$BROKEN_LINKS_FILE"
fi
} > "$REPORT_FILE"
if [ "$BROKEN_LINKS" -gt 0 ]; then
echo "❌ 发现 $BROKEN_LINKS 个断开的链接"
echo ""
echo "断开链接详情:"
cat "$BROKEN_LINKS_FILE"
echo ""
echo "📄 详细报告: $REPORT_FILE"
fi
echo "========================================"
# 清理临时文件(保留报告用于 CI
# rm -f "$BROKEN_LINKS_FILE"
# 返回结果
if [ "$BROKEN_LINKS" -eq 0 ]; then
echo "✅ 所有文档链接检查通过"
exit 0
else
echo "❌ 文档链接检查失败"
exit 1
fi
@@ -0,0 +1,116 @@
#!/usr/bin/env bats
# install_codex_skills.sh 测试套件
setup() {
export TEST_DIR="$(mktemp -d)"
export PLAYBOOK_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
export SCRIPT_PATH="$PLAYBOOK_ROOT/scripts/install_codex_skills.sh"
# 创建临时 HOME 避免污染真实环境
export ORIGINAL_HOME="$HOME"
export HOME="$TEST_DIR/home"
mkdir -p "$HOME"
export CODEX_HOME="$HOME/.codex"
export SKILLS_DST_ROOT="$CODEX_HOME/skills"
cd "$TEST_DIR"
}
teardown() {
export HOME="$ORIGINAL_HOME"
if [ -n "$TEST_DIR" ] && [ -d "$TEST_DIR" ]; then
chmod -R u+w "$TEST_DIR" 2>/dev/null || true
rm -rf "$TEST_DIR"
fi
}
skip_if_no_skills() {
if [ ! -d "$PLAYBOOK_ROOT/codex/skills" ]; then
skip "No codex/skills directory found"
fi
}
first_skill_name() {
ls -1 "$PLAYBOOK_ROOT/codex/skills" 2>/dev/null | head -n 1
}
# ==============================================
# 基础功能测试
# ==============================================
@test "install_codex_skills.sh 脚本存在且可执行" {
[ -f "$SCRIPT_PATH" ]
}
@test "安装 - 创建 skills 目录" {
skip_if_no_skills
[ ! -d "$SKILLS_DST_ROOT" ]
sh "$SCRIPT_PATH"
[ -d "$SKILLS_DST_ROOT" ]
}
@test "安装 - 复制 skill 目录" {
skip_if_no_skills
sh "$SCRIPT_PATH"
SKILL_DIRS=$(find "$SKILLS_DST_ROOT" -mindepth 1 -maxdepth 1 -type d)
[ -n "$SKILL_DIRS" ]
for dir in $SKILL_DIRS; do
[ -f "$dir/SKILL.md" ]
done
}
@test "安装 - 指定单个 skill" {
skip_if_no_skills
SKILL_NAME="$(first_skill_name)"
[ -n "$SKILL_NAME" ]
sh "$SCRIPT_PATH" "$SKILL_NAME"
[ -d "$SKILLS_DST_ROOT/$SKILL_NAME" ]
COUNT=$(find "$SKILLS_DST_ROOT" -mindepth 1 -maxdepth 1 -type d | wc -l)
[ "$COUNT" -eq 1 ]
}
@test "备份 - 同名 skill 目录会创建备份" {
skip_if_no_skills
SKILL_NAME="$(first_skill_name)"
[ -n "$SKILL_NAME" ]
mkdir -p "$SKILLS_DST_ROOT/$SKILL_NAME"
echo "marker" > "$SKILLS_DST_ROOT/$SKILL_NAME/marker.txt"
sh "$SCRIPT_PATH" "$SKILL_NAME"
BACKUP_DIRS=$(find "$SKILLS_DST_ROOT" -maxdepth 1 -type d -name "$SKILL_NAME.bak.*")
[ -n "$BACKUP_DIRS" ]
}
@test "错误处理 - 指定不存在 skill 报错" {
skip_if_no_skills
run sh "$SCRIPT_PATH" nonexistent-skill
[ "$status" -ne 0 ]
}
@test "幂等性 - 多次安装结果一致" {
skip_if_no_skills
sh "$SCRIPT_PATH"
CHECKSUM1=$(find "$SKILLS_DST_ROOT" -type f -name "SKILL.md" ! -path "$SKILLS_DST_ROOT"'/*.bak.*/*' -exec md5sum {} \; | sort | md5sum)
sh "$SCRIPT_PATH"
CHECKSUM2=$(find "$SKILLS_DST_ROOT" -type f -name "SKILL.md" ! -path "$SKILLS_DST_ROOT"'/*.bak.*/*' -exec md5sum {} \; | sort | md5sum)
[ "$CHECKSUM1" = "$CHECKSUM2" ]
}
@@ -0,0 +1,286 @@
#!/usr/bin/env bats
# sync_standards.sh 测试套件
# 测试辅助函数
setup() {
# 创建临时测试目录
export TEST_DIR="$(mktemp -d)"
export PLAYBOOK_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
# 初始化测试项目
cd "$TEST_DIR"
git init
git config user.name "Test User"
git config user.email "test@example.com"
# 模拟 playbook 快照目录
mkdir -p docs/standards/playbook
cp -r "$PLAYBOOK_ROOT"/{.agents,.gitattributes,docs,scripts} docs/standards/playbook/ 2>/dev/null || true
export SCRIPT_PATH="$TEST_DIR/docs/standards/playbook/scripts/sync_standards.sh"
}
teardown() {
# 清理测试目录
if [ -n "$TEST_DIR" ] && [ -d "$TEST_DIR" ]; then
chmod -R u+rwX "$TEST_DIR" 2>/dev/null || true
rm -rf "$TEST_DIR"
fi
}
# ==============================================
# 基础功能测试
# ==============================================
@test "sync_standards.sh 脚本存在且可执行" {
[ -f "$SCRIPT_PATH" ]
}
@test "sync_standards.sh 无参数时同步 tsl 规则集" {
cd "$TEST_DIR"
sh "$SCRIPT_PATH"
# 验证输出目录
[ -d ".agents/tsl" ]
[ -f ".agents/tsl/index.md" ]
}
@test "sync_standards.sh 无参数时自动识别已有语言" {
cd "$TEST_DIR"
mkdir -p .agents/cpp
echo "# placeholder" > .agents/cpp/index.md
sh "$SCRIPT_PATH"
[ -d ".agents/cpp" ]
[ -f ".agents/cpp/index.md" ]
[ ! -d ".agents/tsl" ]
}
@test "sync_standards.sh tsl - 同步 TSL 规则集" {
cd "$TEST_DIR"
sh "$SCRIPT_PATH" tsl
# 验证必须文件
[ -d ".agents/tsl" ]
[ -f ".agents/tsl/index.md" ]
[ -f ".agents/tsl/auth.md" ]
[ -f ".agents/tsl/code_quality.md" ]
[ -f ".agents/tsl/performance.md" ]
[ -f ".agents/tsl/testing.md" ]
}
@test "sync_standards.sh cpp - 同步 C++ 规则集" {
cd "$TEST_DIR"
sh "$SCRIPT_PATH" cpp
# 验证必须文件
[ -d ".agents/cpp" ]
[ -f ".agents/cpp/index.md" ]
[ -f ".agents/cpp/auth.md" ]
[ -f ".agents/cpp/code_quality.md" ]
}
@test "sync_standards.sh tsl cpp - 同步多个规则集" {
cd "$TEST_DIR"
sh "$SCRIPT_PATH" tsl cpp
# 验证两个规则集都存在
[ -d ".agents/tsl" ]
[ -d ".agents/cpp" ]
[ -f ".agents/index.md" ]
}
# ==============================================
# .gitattributes 同步测试
# ==============================================
@test ".gitattributes - 默认模式追加缺失规则" {
cd "$TEST_DIR"
[ ! -f ".gitattributes" ]
sh "$SCRIPT_PATH" tsl
[ -f ".gitattributes" ]
grep -q "Added from playbook .gitattributes" .gitattributes
grep -q "\\*.tsl" .gitattributes
}
@test ".gitattributes - 保留现有内容" {
cd "$TEST_DIR"
echo "# My custom rules" > .gitattributes
echo "*.custom binary" >> .gitattributes
sh "$SCRIPT_PATH" tsl
grep -q "# My custom rules" .gitattributes
grep -q "*.custom binary" .gitattributes
}
@test ".gitattributes - 更新已存在的 managed block" {
cd "$TEST_DIR"
cat > .gitattributes << 'EOF'
# BEGIN playbook .gitattributes
# Old content
# END playbook .gitattributes
EOF
export SYNC_GITATTR_MODE=block
sh "$SCRIPT_PATH" tsl
# 验证 block 已更新(不再包含 "Old content"
! grep -q "Old content" .gitattributes
}
# ==============================================
# AGENTS.md 自动生成测试
# ==============================================
@test "AGENTS.md - 不存在时自动创建" {
cd "$TEST_DIR"
[ ! -f "AGENTS.md" ]
sh "$SCRIPT_PATH" tsl
[ -f "AGENTS.md" ]
grep -q ".agents/" AGENTS.md
}
@test "AGENTS.md - 已存在时不覆盖" {
cd "$TEST_DIR"
echo "# My custom AGENTS.md" > AGENTS.md
sh "$SCRIPT_PATH" tsl
grep -q "# My custom AGENTS.md" AGENTS.md
}
# ==============================================
# 备份功能测试
# ==============================================
@test "备份 - .gitattributes 更新前创建备份" {
cd "$TEST_DIR"
echo "# Original content" > .gitattributes
sh "$SCRIPT_PATH" tsl
# 验证备份文件存在
[ -f ".gitattributes.bak."* ] || [ -f ".gitattributes.bak" ]
}
@test "备份 - .agents/ 更新前创建备份" {
cd "$TEST_DIR"
mkdir -p .agents/tsl
echo "# Old index" > .agents/tsl/index.md
sh "$SCRIPT_PATH" tsl
# 验证备份目录存在
[ -d ".agents/tsl.bak."* ] || [ -d ".agents/tsl.bak" ]
}
# ==============================================
# 多语言项目测试
# ==============================================
@test "多语言 - TSL + C++ + Python 规则集共存" {
cd "$TEST_DIR"
# 复制 Python 规则集(如果存在)
if [ -d "$PLAYBOOK_ROOT/.agents/python" ]; then
cp -r "$PLAYBOOK_ROOT/.agents/python" docs/standards/playbook/.agents/
fi
sh "$SCRIPT_PATH" tsl cpp
# 验证规则集不互相覆盖
[ -d ".agents/tsl" ]
[ -d ".agents/cpp" ]
# 验证索引文件正确引用
[ -f ".agents/index.md" ]
}
# ==============================================
# 错误处理测试
# ==============================================
@test "错误处理 - 未找到 playbook 快照时报错" {
cd "$TEST_DIR"
rm -rf docs/standards/playbook/.agents
run sh "$SCRIPT_PATH" tsl
[ "$status" -ne 0 ]
}
@test "错误处理 - 无效语言参数时报错" {
cd "$TEST_DIR"
run sh "$SCRIPT_PATH" invalid_lang
[ "$status" -ne 0 ]
}
# ==============================================
# 环境变量配置测试
# ==============================================
@test "环境变量 - SYNC_GITATTR_MODE=skip 跳过 .gitattributes" {
cd "$TEST_DIR"
export SYNC_GITATTR_MODE=skip
sh "$SCRIPT_PATH" tsl
[ ! -f ".gitattributes" ]
}
@test "环境变量 - SYNC_GITATTR_MODE=overwrite 覆盖 .gitattributes" {
cd "$TEST_DIR"
echo "# Custom content" > .gitattributes
export SYNC_GITATTR_MODE=overwrite
sh "$SCRIPT_PATH" tsl
# 验证自定义内容被覆盖
! grep -q "# Custom content" .gitattributes
}
@test "环境变量 - SYNC_GITATTR_MODE=append 追加缺失规则" {
cd "$TEST_DIR"
echo "# Custom rules only" > .gitattributes
export SYNC_GITATTR_MODE=append
sh "$SCRIPT_PATH" tsl
grep -q "Added from playbook .gitattributes" .gitattributes
grep -q "\\*.tsl" .gitattributes
}
@test "环境变量 - SYNC_GITATTR_MODE=append 无缺失规则不追加" {
cd "$TEST_DIR"
cp "$PLAYBOOK_ROOT/.gitattributes" .gitattributes
export SYNC_GITATTR_MODE=append
sh "$SCRIPT_PATH" tsl
! grep -q "Added from playbook .gitattributes" .gitattributes
}
# ==============================================
# 幂等性测试
# ==============================================
@test "幂等性 - 多次执行结果一致" {
cd "$TEST_DIR"
# 第一次同步
sh "$SCRIPT_PATH" tsl
CHECKSUM1=$(find .agents/tsl -type f -exec md5sum {} \; | sort | md5sum)
# 第二次同步
sh "$SCRIPT_PATH" tsl
CHECKSUM2=$(find .agents/tsl -type f -exec md5sum {} \; | sort | md5sum)
[ "$CHECKSUM1" = "$CHECKSUM2" ]
}
@@ -0,0 +1,287 @@
#!/usr/bin/env bats
# vendor_playbook.sh 测试套件
# 测试辅助函数
setup() {
# 创建临时测试目录
export TEST_DIR="$(mktemp -d)"
export PLAYBOOK_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
export SCRIPT_PATH="$PLAYBOOK_ROOT/scripts/vendor_playbook.sh"
# 创建目标项目目录
export TARGET_DIR="$(mktemp -d)"
cd "$TARGET_DIR"
git init
git config user.name "Test User"
git config user.email "test@example.com"
}
teardown() {
# 清理测试目录
if [ -n "$TEST_DIR" ] && [ -d "$TEST_DIR" ]; then
chmod -R u+w "$TEST_DIR" 2>/dev/null || true
rm -rf "$TEST_DIR"
fi
if [ -n "$TARGET_DIR" ] && [ -d "$TARGET_DIR" ]; then
chmod -R u+w "$TARGET_DIR" 2>/dev/null || true
rm -rf "$TARGET_DIR"
fi
}
# ==============================================
# 基础功能测试
# ==============================================
@test "vendor_playbook.sh 脚本存在且可执行" {
[ -f "$SCRIPT_PATH" ]
}
@test "vendor_playbook.sh - 无参数时显示用法" {
cd "$TARGET_DIR"
run sh "$SCRIPT_PATH"
[ "$status" -ne 0 ]
}
@test "vendor_playbook.sh - 单语言 vendoring (tsl)" {
cd "$TARGET_DIR"
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
# 验证快照目录结构
[ -d "docs/standards/playbook" ]
[ -d "docs/standards/playbook/docs/common" ]
[ -d "docs/standards/playbook/docs/tsl" ]
[ -d "docs/standards/playbook/.agents/tsl" ]
[ -f "docs/standards/playbook/scripts/sync_standards.sh" ]
}
@test "vendor_playbook.sh - 多语言 vendoring (tsl cpp)" {
cd "$TARGET_DIR"
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl cpp
# 验证包含两种语言的文档
[ -d "docs/standards/playbook/docs/tsl" ]
[ -d "docs/standards/playbook/docs/cpp" ]
[ -d "docs/standards/playbook/.agents/tsl" ]
[ -d "docs/standards/playbook/.agents/cpp" ]
}
# ==============================================
# 自动同步测试
# ==============================================
@test "vendor_playbook.sh - 自动执行 sync_standards" {
cd "$TARGET_DIR"
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
# 验证根目录已同步规则集
[ -d ".agents/tsl" ]
[ -f ".agents/tsl/index.md" ]
[ -f ".gitattributes" ]
}
@test "vendor_playbook.sh - 多语言自动同步" {
cd "$TARGET_DIR"
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl cpp
# 验证两个规则集都已同步
[ -d ".agents/tsl" ]
[ -d ".agents/cpp" ]
[ -f ".agents/index.md" ]
}
# ==============================================
# SOURCE.md 生成测试
# ==============================================
@test "vendor_playbook.sh - 生成 SOURCE.md" {
cd "$TARGET_DIR"
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
[ -f "docs/standards/playbook/SOURCE.md" ]
grep -q "Source:" docs/standards/playbook/SOURCE.md
grep -q "Commit:" docs/standards/playbook/SOURCE.md
}
@test "vendor_playbook.sh - SOURCE.md 包含 commit hash" {
cd "$TARGET_DIR"
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
# 验证包含 commit hash40个十六进制字符)
grep -E "[0-9a-f]{40}" docs/standards/playbook/SOURCE.md
}
@test "vendor_playbook.sh - SOURCE.md 包含时间戳" {
cd "$TARGET_DIR"
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
# 验证包含日期格式 YYYY-MM-DD
grep -E "[0-9]{4}-[0-9]{2}-[0-9]{2}" docs/standards/playbook/SOURCE.md
}
# ==============================================
# 裁剪功能测试
# ==============================================
@test "裁剪 - 仅包含指定语言的文档" {
cd "$TARGET_DIR"
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
# 验证包含 TSL 文档
[ -d "docs/standards/playbook/docs/tsl" ]
# 验证不包含其他语言(如果原本有 Python)
if [ -d "$PLAYBOOK_ROOT/docs/python" ]; then
[ ! -d "docs/standards/playbook/docs/python" ]
fi
}
@test "裁剪 - 始终包含 common 目录" {
cd "$TARGET_DIR"
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
[ -d "docs/standards/playbook/docs/common" ]
[ -f "docs/standards/playbook/docs/common/commit_message.md" ]
}
@test "裁剪 - 包含对应的模板文件" {
cd "$TARGET_DIR"
sh "$SCRIPT_PATH" "$TARGET_DIR" cpp
# 验证包含 C++ 模板
[ -d "docs/standards/playbook/templates/cpp" ]
# 验证不包含 Python 模板(如果指定了 cpp
if [ -d "$PLAYBOOK_ROOT/templates/python" ]; then
[ ! -d "docs/standards/playbook/templates/python" ]
fi
}
@test "裁剪 - 包含通用 CI 模板" {
cd "$TARGET_DIR"
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
[ -d "docs/standards/playbook/templates/ci" ]
}
# ==============================================
# 目标目录处理测试
# ==============================================
@test "目标目录 - 已存在时覆盖更新" {
cd "$TARGET_DIR"
# 首次 vendor
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
# 在快照中添加标记文件
touch docs/standards/playbook/OLD_MARKER
# 再次 vendor
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
# 验证旧标记不存在(已被覆盖)
[ ! -f "docs/standards/playbook/OLD_MARKER" ]
}
@test "目标目录 - 创建必要的父目录" {
cd "$TARGET_DIR"
# 确保 docs/standards 不存在
[ ! -d "docs/standards" ]
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
[ -d "docs/standards/playbook" ]
}
# ==============================================
# 错误处理测试
# ==============================================
@test "错误处理 - 目标目录不存在时报错" {
cd "$TARGET_DIR"
missing_dir="$TEST_DIR/missing-project"
rm -rf "$missing_dir"
run sh "$SCRIPT_PATH" "$missing_dir" tsl
[ "$status" -ne 0 ]
}
@test "错误处理 - 无效语言参数时报错" {
cd "$TARGET_DIR"
run sh "$SCRIPT_PATH" "$TARGET_DIR" invalid_lang
[ "$status" -ne 0 ]
}
@test "错误处理 - 目标目录不是 git 仓库时警告" {
cd "$TARGET_DIR"
rm -rf .git
run sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
# 应该给出警告但不失败
[ "$status" -eq 0 ]
}
# ==============================================
# 完整性测试
# ==============================================
@test "完整性 - 验证所有必要文件已复制" {
cd "$TARGET_DIR"
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
# 验证关键文件
[ -f "docs/standards/playbook/README.md" ]
[ -f "docs/standards/playbook/docs/index.md" ]
[ -f "docs/standards/playbook/.gitattributes" ]
[ -f "docs/standards/playbook/scripts/sync_standards.sh" ]
[ -f "docs/standards/playbook/SOURCE.md" ]
}
@test "完整性 - 验证脚本可执行性" {
cd "$TARGET_DIR"
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
# 验证同步脚本可执行
run sh docs/standards/playbook/scripts/sync_standards.sh tsl
[ "$status" -eq 0 ]
}
# ==============================================
# 多次执行测试
# ==============================================
@test "幂等性 - 多次 vendor 结果一致" {
cd "$TARGET_DIR"
# 第一次 vendor
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
CHECKSUM1=$(find docs/standards/playbook -type f -name "*.md" ! -name "SOURCE.md" -exec md5sum {} \; | sort | md5sum)
# 第二次 vendor
sh "$SCRIPT_PATH" "$TARGET_DIR" tsl
CHECKSUM2=$(find docs/standards/playbook -type f -name "*.md" ! -name "SOURCE.md" -exec md5sum {} \; | sort | md5sum)
[ "$CHECKSUM1" = "$CHECKSUM2" ]
}
@@ -0,0 +1,332 @@
#!/usr/bin/env sh
# CI 模板验证脚本
set -eu
echo "========================================"
echo "🔧 CI 模板验证"
echo "========================================"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLAYBOOK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
TEMPLATES_DIR="$PLAYBOOK_ROOT/templates/ci"
VALIDATION_PASSED=0
VALIDATION_FAILED=0
ERRORS_FILE="/tmp/ci_template_validation_errors.txt"
REPORT_FILE="$SCRIPT_DIR/ci_validation_report.txt"
> "$ERRORS_FILE"
> "$REPORT_FILE"
echo "📁 模板目录: $TEMPLATES_DIR"
echo ""
# ============================================
# 辅助函数
# ============================================
validate_file_exists() {
local file="$1"
local description="$2"
if [ -f "$file" ]; then
echo "$description: $(basename "$file")"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
return 0
else
echo "$description: $(basename "$file") - 文件不存在"
echo "文件不存在: $file" >> "$ERRORS_FILE"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
return 1
fi
}
validate_yaml_syntax() {
local file="$1"
local description="$2"
if ! command -v yamllint >/dev/null 2>&1; then
echo " ⚠️ $description: 跳过(yamllint 未安装)"
return 0
fi
if yamllint -d relaxed "$file" >/dev/null 2>&1; then
echo "$description: YAML 语法正确"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
return 0
else
echo "$description: YAML 语法错误"
echo "YAML 语法错误: $file" >> "$ERRORS_FILE"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
return 1
fi
}
validate_workflow_structure() {
local file="$1"
echo " 📋 检查 workflow 结构:"
# 检查必要字段
if grep -q "^name:" "$file"; then
echo " ✓ 包含 name 字段"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ✗ 缺少 name 字段"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
if grep -q "^on:" "$file"; then
echo " ✓ 包含 on 触发器"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ✗ 缺少 on 触发器"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
if grep -q "^jobs:" "$file"; then
echo " ✓ 包含 jobs 定义"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ✗ 缺少 jobs 定义"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
# 检查 runs-on
if grep -q "runs-on:" "$file"; then
runner=$(grep "runs-on:" "$file" | head -1 | awk '{print $2}')
echo " ✓ 配置了 runner: $runner"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ✗ 缺少 runs-on 配置"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
# 检查 steps
if grep -q "steps:" "$file"; then
step_count=$(grep -c "^ - name:" "$file" || echo 0)
echo " ✓ 包含 $step_count 个步骤"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ✗ 缺少 steps 定义"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
}
# ============================================
# 查找并验证 Gitea workflow 文件
# ============================================
echo "🔍 查找 Gitea workflow 文件"
GITEA_WORKFLOWS_DIR="$TEMPLATES_DIR/gitea/.gitea/workflows"
if [ ! -d "$GITEA_WORKFLOWS_DIR" ]; then
echo "⚠️ Gitea workflows 目录不存在: $GITEA_WORKFLOWS_DIR"
echo "跳过 Gitea workflow 验证"
else
WORKFLOW_FILES=$(find "$GITEA_WORKFLOWS_DIR" -name "*.yml" -o -name "*.yaml" 2>/dev/null || true)
if [ -z "$WORKFLOW_FILES" ]; then
echo "⚠️ 未找到 Gitea workflow 文件"
else
for workflow in $WORKFLOW_FILES; do
echo ""
echo "🔍 验证 $(basename "$workflow")"
if validate_file_exists "$workflow" "$(basename "$workflow")"; then
# 验证 YAML 语法
validate_yaml_syntax "$workflow" "$(basename "$workflow")"
# 验证 workflow 结构
validate_workflow_structure "$workflow"
# 检查是否包含中文注释(符合项目风格)
if grep -q "# .*[\u4e00-\u9fa5]" "$workflow" 2>/dev/null || grep -qP "[\x{4e00}-\x{9fa5}]" "$workflow" 2>/dev/null; then
echo " ✓ 包含中文注释(符合项目风格)"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
# 检查配置区域标记
if grep -q "# ====.*配置.*====" "$workflow"; then
echo " ✓ 包含配置区域标记"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
# 检查环境变量配置
if grep -q "^env:" "$workflow"; then
echo " ✓ 包含环境变量配置"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
fi
done
fi
fi
echo ""
# ============================================
# 查找并验证 GitHub Actions workflow 文件
# ============================================
echo "🔍 查找 GitHub Actions workflow 文件"
GITHUB_WORKFLOWS_DIR="$TEMPLATES_DIR/github/.github/workflows"
if [ -d "$GITHUB_WORKFLOWS_DIR" ]; then
WORKFLOW_FILES=$(find "$GITHUB_WORKFLOWS_DIR" -name "*.yml" -o -name "*.yaml" 2>/dev/null || true)
if [ -n "$WORKFLOW_FILES" ]; then
for workflow in $WORKFLOW_FILES; do
echo ""
echo "🔍 验证 $(basename "$workflow")"
if validate_file_exists "$workflow" "$(basename "$workflow")"; then
# 验证 YAML 语法
validate_yaml_syntax "$workflow" "$(basename "$workflow")"
# 验证 workflow 结构
validate_workflow_structure "$workflow"
fi
done
fi
else
echo "️ GitHub Actions workflows 目录不存在(可选)"
fi
echo ""
# ============================================
# 验证特定 workflow 模板
# ============================================
echo "🔍 验证特定 workflow 模板"
# 检查 standards-check workflow
STANDARDS_CHECK="$GITEA_WORKFLOWS_DIR/standards-check.yml"
if [ -f "$STANDARDS_CHECK" ]; then
echo ""
echo "📋 验证 standards-check.yml:"
# 检查是否包含格式化检查
if grep -q "格式化\|format" "$STANDARDS_CHECK"; then
echo " ✓ 包含格式化检查"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
# 检查是否包含 lint 检查
if grep -q "lint\|检查" "$STANDARDS_CHECK"; then
echo " ✓ 包含 lint 检查"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
else
echo "️ 未找到 standards-check.yml(可选)"
fi
# 检查 test workflow
TEST_WORKFLOW="$GITEA_WORKFLOWS_DIR/test.yml"
if [ -f "$TEST_WORKFLOW" ]; then
echo ""
echo "📋 验证 test.yml:"
# 检查是否包含测试步骤
if grep -q "测试\|test" "$TEST_WORKFLOW"; then
echo " ✓ 包含测试步骤"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
# 检查是否配置了测试矩阵
if grep -q "strategy:" "$TEST_WORKFLOW" && grep -q "matrix:" "$TEST_WORKFLOW"; then
echo " ✓ 配置了测试矩阵"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
else
echo "️ 未找到 test.yml(可选)"
fi
echo ""
# ============================================
# 验证 README 文档
# ============================================
echo "🔍 验证 CI 模板文档"
if [ -d "$TEMPLATES_DIR" ]; then
# 查找 README
README_FILE=""
for name in README.md readme.md README README.txt; do
if [ -f "$TEMPLATES_DIR/$name" ]; then
README_FILE="$TEMPLATES_DIR/$name"
break
fi
done
if [ -n "$README_FILE" ]; then
echo " ✅ 找到说明文档: $(basename "$README_FILE")"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
# 检查文档内容
if grep -qi "使用\|usage\|how to" "$README_FILE"; then
echo " ✓ 包含使用说明"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
else
echo " ⚠️ 未找到 README 文档(建议添加)"
fi
fi
echo ""
# ============================================
# 生成验证报告
# ============================================
echo "========================================"
echo "📊 验证结果统计"
echo "========================================"
echo "✅ 通过: $VALIDATION_PASSED"
echo "❌ 失败: $VALIDATION_FAILED"
if [ $((VALIDATION_PASSED + VALIDATION_FAILED)) -gt 0 ]; then
echo "📈 通过率: $(awk "BEGIN {printf \"%.1f\", ($VALIDATION_PASSED * 100.0) / ($VALIDATION_PASSED + $VALIDATION_FAILED)}")%"
else
echo "📈 通过率: N/A (无测试项)"
fi
echo ""
# 写入报告文件
{
echo "CI 模板验证报告"
echo "===================="
echo ""
echo "验证时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "模板目录: $TEMPLATES_DIR"
echo ""
echo "统计结果:"
echo " 通过: $VALIDATION_PASSED"
echo " 失败: $VALIDATION_FAILED"
if [ $((VALIDATION_PASSED + VALIDATION_FAILED)) -gt 0 ]; then
echo " 通过率: $(awk "BEGIN {printf \"%.1f\", ($VALIDATION_PASSED * 100.0) / ($VALIDATION_PASSED + $VALIDATION_FAILED)}")%"
fi
echo ""
if [ -s "$ERRORS_FILE" ]; then
echo "错误详情:"
cat "$ERRORS_FILE"
fi
} > "$REPORT_FILE"
echo "📄 详细报告: $REPORT_FILE"
echo "========================================"
# 清理临时文件
rm -f "$ERRORS_FILE"
# 返回结果
if [ "$VALIDATION_FAILED" -eq 0 ]; then
echo "✅ 所有 CI 模板验证通过"
exit 0
else
echo "❌ CI 模板验证失败 ($VALIDATION_FAILED 个错误)"
exit 1
fi
@@ -0,0 +1,353 @@
#!/usr/bin/env sh
# C++ 模板验证脚本
set -eu
echo "========================================"
echo "⚙️ C++ 模板验证"
echo "========================================"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLAYBOOK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
TEMPLATES_DIR="$PLAYBOOK_ROOT/templates/cpp"
VALIDATION_PASSED=0
VALIDATION_FAILED=0
ERRORS_FILE="/tmp/cpp_template_validation_errors.txt"
REPORT_FILE="$SCRIPT_DIR/cpp_validation_report.txt"
> "$ERRORS_FILE"
> "$REPORT_FILE"
echo "📁 模板目录: $TEMPLATES_DIR"
echo ""
# ============================================
# 辅助函数
# ============================================
validate_file_exists() {
local file="$1"
local description="$2"
if [ -f "$file" ]; then
echo "$description: $(basename "$file")"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
return 0
else
echo "$description: $(basename "$file") - 文件不存在"
echo "文件不存在: $file" >> "$ERRORS_FILE"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
return 1
fi
}
validate_cmake_syntax() {
local file="$1"
local description="$2"
# 基础语法检查
if [ ! -s "$file" ]; then
echo "$description: 文件为空"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
return 1
fi
# 检查必要的 CMake 命令
local has_errors=0
if ! grep -q "cmake_minimum_required" "$file"; then
echo " ✗ 缺少 cmake_minimum_required"
has_errors=1
fi
if ! grep -q "project(" "$file"; then
echo " ✗ 缺少 project()"
has_errors=1
fi
if [ $has_errors -eq 0 ]; then
echo "$description: CMake 基础语法正确"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
return 0
else
echo "$description: CMake 语法检查失败"
echo "CMake 语法错误: $file" >> "$ERRORS_FILE"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
return 1
fi
}
# ============================================
# 验证 CMakeLists.txt
# ============================================
echo "🔍 验证 CMakeLists.txt"
CMAKE_FILE="$TEMPLATES_DIR/CMakeLists.txt"
if validate_file_exists "$CMAKE_FILE" "CMakeLists.txt"; then
# 验证 CMake 语法
validate_cmake_syntax "$CMAKE_FILE" "CMakeLists.txt"
echo " 📋 检查 C++23 配置:"
# 检查 C++23 标准
if grep -q "CMAKE_CXX_STANDARD 23" "$CMAKE_FILE"; then
echo " ✓ 配置了 C++23 标准"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ✗ 未配置 C++23 标准"
echo "未配置 C++23: $CMAKE_FILE" >> "$ERRORS_FILE"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
# 检查 Modules 支持
if grep -q "CMAKE_CXX_SCAN_FOR_MODULES" "$CMAKE_FILE"; then
echo " ✓ 启用了 C++ Modules 扫描"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ⚠️ 未启用 C++ Modules 扫描(可选)"
fi
# 检查 import std; 支持
if grep -q "CMAKE_EXPERIMENTAL_CXX_IMPORT_STD" "$CMAKE_FILE"; then
echo " ✓ 启用了 import std; 支持"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ⚠️ 未启用 import std; 支持(可选)"
fi
# 检查编译命令导出
if grep -q "CMAKE_EXPORT_COMPILE_COMMANDS" "$CMAKE_FILE"; then
echo " ✓ 启用了编译命令导出(用于 clangd)"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ⚠️ 未启用编译命令导出"
fi
fi
echo ""
# ============================================
# 验证 .clang-format
# ============================================
echo "🔍 验证 .clang-format"
CLANG_FORMAT="$TEMPLATES_DIR/.clang-format"
if validate_file_exists "$CLANG_FORMAT" ".clang-format"; then
# 验证 YAML 语法
if command -v yamllint >/dev/null 2>&1; then
if yamllint -d relaxed "$CLANG_FORMAT" >/dev/null 2>&1; then
echo " ✓ YAML 语法正确"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ✗ YAML 语法错误"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
fi
# 验证关键配置
echo " 📋 检查格式化配置:"
if grep -q "^Language:" "$CLANG_FORMAT" && grep -q "Cpp" "$CLANG_FORMAT"; then
echo " ✓ 配置了 Language: Cpp"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
if grep -q "^BasedOnStyle:" "$CLANG_FORMAT"; then
style=$(grep "^BasedOnStyle:" "$CLANG_FORMAT" | awk '{print $2}')
echo " ✓ 基于风格: $style"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
if grep -q "^Standard:" "$CLANG_FORMAT"; then
std=$(grep "^Standard:" "$CLANG_FORMAT" | awk '{print $2}')
echo " ✓ C++ 标准: $std"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
# 检查缩进配置
if grep -q "^IndentWidth:" "$CLANG_FORMAT"; then
echo " ✓ 配置了缩进宽度"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
fi
echo ""
# ============================================
# 验证 .clangd
# ============================================
echo "🔍 验证 .clangd"
CLANGD="$TEMPLATES_DIR/.clangd"
if validate_file_exists "$CLANGD" ".clangd"; then
# 验证 YAML 语法
if command -v yamllint >/dev/null 2>&1; then
if yamllint -d relaxed "$CLANGD" >/dev/null 2>&1; then
echo " ✓ YAML 语法正确"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ✗ YAML 语法错误"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
fi
# 验证 CompileFlags
echo " 📋 检查 clangd 配置:"
if grep -q "CompileFlags:" "$CLANGD"; then
echo " ✓ 包含 CompileFlags 配置"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
# 检查 C++23 标准
if grep -q "std=c++23" "$CLANGD"; then
echo " ✓ 配置了 -std=c++23"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
fi
# 验证 CompilationDatabase
if grep -q "CompilationDatabase:" "$CLANGD"; then
echo " ✓ 配置了 CompilationDatabase 路径"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
# 验证 Index 配置
if grep -q "Index:" "$CLANGD"; then
echo " ✓ 配置了索引选项"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
fi
echo ""
# ============================================
# 验证 conanfile.txt
# ============================================
echo "🔍 验证 conanfile.txt"
CONANFILE="$TEMPLATES_DIR/conanfile.txt"
if validate_file_exists "$CONANFILE" "conanfile.txt"; then
echo " 📋 检查 Conan 配置:"
# 验证必要的节
if grep -q "^\[requires\]" "$CONANFILE"; then
echo " ✓ 包含 [requires] 配置"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ⚠️ 缺少 [requires] 配置(可选)"
fi
if grep -q "^\[generators\]" "$CONANFILE"; then
echo " ✓ 包含 [generators] 配置"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
# 检查 CMakeDeps 和 CMakeToolchain
if grep -q "CMakeDeps" "$CONANFILE"; then
echo " ✓ 配置了 CMakeDeps 生成器"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
if grep -q "CMakeToolchain" "$CONANFILE"; then
echo " ✓ 配置了 CMakeToolchain 生成器"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
fi
if grep -q "^\[options\]" "$CONANFILE"; then
echo " ✓ 包含 [options] 配置"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
fi
echo ""
# ============================================
# 验证 CMakeUserPresets.json
# ============================================
echo "🔍 验证 CMakeUserPresets.json"
CMAKE_PRESETS="$TEMPLATES_DIR/CMakeUserPresets.json"
if validate_file_exists "$CMAKE_PRESETS" "CMakeUserPresets.json"; then
# 验证 JSON 语法
if command -v python3 >/dev/null 2>&1; then
if python3 -m json.tool "$CMAKE_PRESETS" >/dev/null 2>&1; then
echo " ✓ JSON 语法正确"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ✗ JSON 语法错误"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
fi
# 验证配置项
echo " 📋 检查 CMake Presets:"
if grep -q "\"version\"" "$CMAKE_PRESETS"; then
echo " ✓ 包含 version 字段"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
if grep -q "\"configurePresets\"" "$CMAKE_PRESETS"; then
echo " ✓ 包含 configurePresets"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
if grep -q "\"buildPresets\"" "$CMAKE_PRESETS"; then
echo " ✓ 包含 buildPresets"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
fi
echo ""
# ============================================
# 生成验证报告
# ============================================
echo "========================================"
echo "📊 验证结果统计"
echo "========================================"
echo "✅ 通过: $VALIDATION_PASSED"
echo "❌ 失败: $VALIDATION_FAILED"
echo "📈 通过率: $(awk "BEGIN {printf \"%.1f\", ($VALIDATION_PASSED * 100.0) / ($VALIDATION_PASSED + $VALIDATION_FAILED)}")%"
echo ""
# 写入报告文件
{
echo "C++ 模板验证报告"
echo "===================="
echo ""
echo "验证时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "模板目录: $TEMPLATES_DIR"
echo ""
echo "统计结果:"
echo " 通过: $VALIDATION_PASSED"
echo " 失败: $VALIDATION_FAILED"
echo " 通过率: $(awk "BEGIN {printf \"%.1f\", ($VALIDATION_PASSED * 100.0) / ($VALIDATION_PASSED + $VALIDATION_FAILED)}")%"
echo ""
if [ -s "$ERRORS_FILE" ]; then
echo "错误详情:"
cat "$ERRORS_FILE"
fi
} > "$REPORT_FILE"
echo "📄 详细报告: $REPORT_FILE"
echo "========================================"
# 清理临时文件
rm -f "$ERRORS_FILE"
# 返回结果
if [ "$VALIDATION_FAILED" -eq 0 ]; then
echo "✅ 所有 C++ 模板验证通过"
exit 0
else
echo "❌ C++ 模板验证失败 ($VALIDATION_FAILED 个错误)"
exit 1
fi
@@ -0,0 +1,329 @@
#!/usr/bin/env sh
# Python 模板验证脚本
set -eu
echo "========================================"
echo "🐍 Python 模板验证"
echo "========================================"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLAYBOOK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
TEMPLATES_DIR="$PLAYBOOK_ROOT/templates/python"
VALIDATION_PASSED=0
VALIDATION_FAILED=0
ERRORS_FILE="/tmp/python_template_validation_errors.txt"
REPORT_FILE="$SCRIPT_DIR/python_validation_report.txt"
> "$ERRORS_FILE"
> "$REPORT_FILE"
echo "📁 模板目录: $TEMPLATES_DIR"
echo ""
# ============================================
# 辅助函数
# ============================================
validate_file_exists() {
local file="$1"
local description="$2"
if [ -f "$file" ]; then
echo "$description: $(basename "$file")"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
return 0
else
echo "$description: $(basename "$file") - 文件不存在"
echo "文件不存在: $file" >> "$ERRORS_FILE"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
return 1
fi
}
validate_toml_syntax() {
local file="$1"
local description="$2"
if ! command -v python3 >/dev/null 2>&1; then
echo " ⚠️ $description: 跳过(Python3 未安装)"
return 0
fi
if python3 << EOF
import sys
try:
import tomli
except ImportError:
try:
import tomllib as tomli
except ImportError:
import toml as tomli
try:
with open("$file", "rb") as f:
tomli.load(f)
sys.exit(0)
except Exception as e:
print(f"TOML 语法错误: {e}", file=sys.stderr)
sys.exit(1)
EOF
then
echo "$description: TOML 语法正确"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
return 0
else
echo "$description: TOML 语法错误"
echo "TOML 语法错误: $file" >> "$ERRORS_FILE"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
return 1
fi
}
validate_required_sections() {
local file="$1"
local sections="$2"
for section in $sections; do
if grep -q "^\[$section\]" "$file"; then
echo " ✓ 包含 [$section] 配置"
else
echo " ✗ 缺少 [$section] 配置"
echo "缺少配置节: $section in $file" >> "$ERRORS_FILE"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
done
}
# ============================================
# 验证 pyproject.toml
# ============================================
echo "🔍 验证 pyproject.toml"
PYPROJECT="$TEMPLATES_DIR/pyproject.toml"
if validate_file_exists "$PYPROJECT" "pyproject.toml"; then
# 验证 TOML 语法
validate_toml_syntax "$PYPROJECT" "pyproject.toml"
# 验证必要的配置节
echo " 📋 检查必要配置节:"
validate_required_sections "$PYPROJECT" "tool.black tool.isort tool.pytest.ini_options"
# 验证 black 配置
if grep -q "line-length = 80" "$PYPROJECT"; then
echo " ✓ black line-length 配置正确"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ✗ black line-length 配置缺失或不正确"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
# 验证 isort 配置
if grep -q "profile = \"google\"" "$PYPROJECT"; then
echo " ✓ isort profile 配置正确"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ✗ isort profile 配置缺失或不正确"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
fi
echo ""
# ============================================
# 验证 .flake8
# ============================================
echo "🔍 验证 .flake8"
FLAKE8="$TEMPLATES_DIR/.flake8"
if validate_file_exists "$FLAKE8" ".flake8"; then
# 验证 [flake8] 节
if grep -q "^\[flake8\]" "$FLAKE8"; then
echo " ✓ 包含 [flake8] 配置"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
# 验证 max-line-length
if grep -q "^max-line-length" "$FLAKE8"; then
echo " ✓ 配置了 max-line-length"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ✗ 缺少 max-line-length 配置"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
# 验证 extend-ignore
if grep -q "^extend-ignore" "$FLAKE8" || grep -q "^ignore" "$FLAKE8"; then
echo " ✓ 配置了错误忽略规则"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
else
echo " ✗ 缺少 [flake8] 配置节"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
fi
echo ""
# ============================================
# 验证 .pylintrc
# ============================================
echo "🔍 验证 .pylintrc"
PYLINTRC="$TEMPLATES_DIR/.pylintrc"
if validate_file_exists "$PYLINTRC" ".pylintrc"; then
# 验证关键配置节
for section in "MASTER" "MESSAGES CONTROL" "FORMAT"; do
if grep -qi "^\[$section\]" "$PYLINTRC"; then
echo " ✓ 包含 [$section] 配置"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ✗ 缺少 [$section] 配置"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
done
# 验证 max-line-length
if grep -q "^max-line-length" "$PYLINTRC"; then
echo " ✓ 配置了 max-line-length"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
fi
echo ""
# ============================================
# 验证 .pre-commit-config.yaml
# ============================================
echo "🔍 验证 .pre-commit-config.yaml"
PRECOMMIT="$TEMPLATES_DIR/.pre-commit-config.yaml"
if validate_file_exists "$PRECOMMIT" ".pre-commit-config.yaml"; then
# 验证 YAML 语法
if command -v yamllint >/dev/null 2>&1; then
if yamllint -d relaxed "$PRECOMMIT" >/dev/null 2>&1; then
echo " ✓ YAML 语法正确"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ✗ YAML 语法错误"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
fi
# 验证包含 pre-commit hooks
if grep -q "^repos:" "$PRECOMMIT"; then
echo " ✓ 包含 repos 配置"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
# 检查常用 hooks
for hook in "black" "isort" "flake8"; do
if grep -q "$hook" "$PRECOMMIT"; then
echo " ✓ 配置了 $hook hook"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
done
fi
fi
echo ""
# ============================================
# 验证 .editorconfig
# ============================================
echo "🔍 验证 .editorconfig"
EDITORCONFIG="$TEMPLATES_DIR/.editorconfig"
if validate_file_exists "$EDITORCONFIG" ".editorconfig"; then
# 验证包含 root 标记
if grep -q "^root = true" "$EDITORCONFIG"; then
echo " ✓ 包含 root = true"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
# 验证 Python 配置节
if grep -q "^\[\*.py\]" "$EDITORCONFIG" || grep -q "^\[*.py\]" "$EDITORCONFIG"; then
echo " ✓ 包含 Python 文件配置"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
fi
echo ""
# ============================================
# 验证 VSCode 配置
# ============================================
echo "🔍 验证 .vscode/settings.json"
VSCODE_SETTINGS="$TEMPLATES_DIR/.vscode/settings.json"
if validate_file_exists "$VSCODE_SETTINGS" "settings.json"; then
# 验证 JSON 语法
if command -v python3 >/dev/null 2>&1; then
if python3 -m json.tool "$VSCODE_SETTINGS" >/dev/null 2>&1; then
echo " ✓ JSON 语法正确"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo " ✗ JSON 语法错误"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
fi
# 验证 Python 相关配置
if grep -q "python" "$VSCODE_SETTINGS"; then
echo " ✓ 包含 Python 配置"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
fi
fi
echo ""
# ============================================
# 生成验证报告
# ============================================
echo "========================================"
echo "📊 验证结果统计"
echo "========================================"
echo "✅ 通过: $VALIDATION_PASSED"
echo "❌ 失败: $VALIDATION_FAILED"
echo "📈 通过率: $(awk "BEGIN {printf \"%.1f\", ($VALIDATION_PASSED * 100.0) / ($VALIDATION_PASSED + $VALIDATION_FAILED)}")%"
echo ""
# 写入报告文件
{
echo "Python 模板验证报告"
echo "===================="
echo ""
echo "验证时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "模板目录: $TEMPLATES_DIR"
echo ""
echo "统计结果:"
echo " 通过: $VALIDATION_PASSED"
echo " 失败: $VALIDATION_FAILED"
echo " 通过率: $(awk "BEGIN {printf \"%.1f\", ($VALIDATION_PASSED * 100.0) / ($VALIDATION_PASSED + $VALIDATION_FAILED)}")%"
echo ""
if [ -s "$ERRORS_FILE" ]; then
echo "错误详情:"
cat "$ERRORS_FILE"
fi
} > "$REPORT_FILE"
echo "📄 详细报告: $REPORT_FILE"
echo "========================================"
# 清理临时文件
rm -f "$ERRORS_FILE"
# 返回结果
if [ "$VALIDATION_FAILED" -eq 0 ]; then
echo "✅ 所有 Python 模板验证通过"
exit 0
else
echo "❌ Python 模板验证失败 ($VALIDATION_FAILED 个错误)"
exit 1
fi