From da0ef2b7a2026aa93b37c47af188666daf0e6b28 Mon Sep 17 00:00:00 2001 From: csh Date: Thu, 8 Jan 2026 11:29:44 +0800 Subject: [PATCH] :white_check_mark: test: add automated tests and ci workflow --- .gitea/workflows/test.yml | 826 +++++++++++++++++++ docs/tsl/syntax_book/function.md | 4 +- docs/tsl/syntax_book/function/index.md | 3 +- scripts/vendor_playbook.sh | 2 +- tests/README.md | 435 ++++++++++ tests/integration/check_doc_links.sh | 220 +++++ tests/scripts/test_install_codex_skills.bats | 116 +++ tests/scripts/test_sync_standards.bats | 251 ++++++ tests/scripts/test_vendor_playbook.bats | 284 +++++++ tests/templates/validate_ci_templates.sh | 332 ++++++++ tests/templates/validate_cpp_templates.sh | 353 ++++++++ tests/templates/validate_python_templates.sh | 329 ++++++++ 12 files changed, 3149 insertions(+), 6 deletions(-) create mode 100644 .gitea/workflows/test.yml create mode 100644 tests/README.md create mode 100644 tests/integration/check_doc_links.sh create mode 100644 tests/scripts/test_install_codex_skills.bats create mode 100644 tests/scripts/test_sync_standards.bats create mode 100644 tests/scripts/test_vendor_playbook.bats create mode 100644 tests/templates/validate_ci_templates.sh create mode 100644 tests/templates/validate_cpp_templates.sh create mode 100644 tests/templates/validate_python_templates.sh diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml new file mode 100644 index 0000000..8048919 --- /dev/null +++ b/.gitea/workflows/test.yml @@ -0,0 +1,826 @@ +name: 🧪 Playbook 测试套件 + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: # 允许手动触发 + +concurrency: + group: test-${{ github.repository }}-${{ github.ref }} + cancel-in-progress: true + +# ========================================== +# 🔧 配置区域 - 测试参数 +# ========================================== +env: + # ===== 测试环境配置 ===== + # 测试工作目录 + WORKSPACE_DIR: "/home/workspace" + TEST_WORKSPACE: "/home/workspace/playbook-test" + + # ===== 测试覆盖率目标 ===== + # Shell 脚本测试覆盖率目标(百分比) + SHELL_COVERAGE_TARGET: "80" + # 模板验证通过率目标(百分比) + TEMPLATE_VALIDATION_TARGET: "100" + + # ===== 测试输出配置 ===== + # 是否生成详细测试报告 + VERBOSE_OUTPUT: "true" + # 测试报告格式(tap/junit/markdown) + REPORT_FORMAT: "markdown" + + # ===== 颜色输出配置 ===== + # 启用彩色输出 + FORCE_COLOR: "1" + TERM: "xterm-256color" + +jobs: + # ========================================== + # Job 1: 环境检查与准备 + # ========================================== + setup: + name: 🔍 环境检查 + runs-on: ubuntu-22.04 + + outputs: + scripts-changed: ${{ steps.changes.outputs.scripts }} + templates-changed: ${{ steps.changes.outputs.templates }} + docs-changed: ${{ steps.changes.outputs.docs }} + + steps: + - name: 📥 准备仓库 + run: | + echo "========================================" + echo "📥 准备仓库到 WORKSPACE_DIR" + echo "========================================" + + REPO_NAME="${{ github.event.repository.name }}" + REPO_DIR="${{ env.WORKSPACE_DIR }}/$REPO_NAME" + TOKEN="${{ secrets.WORKFLOW }}" + if [ -n "$TOKEN" ]; then + REPO_URL="https://oauth2:${TOKEN}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git" + else + REPO_URL="${GITHUB_SERVER_URL}/${{ github.repository }}.git" + fi + + if [ -d "$REPO_DIR" ]; then + if [ -d "$REPO_DIR/.git" ]; then + cd "$REPO_DIR" + git clean -fdx + git reset --hard + git fetch --all --tags --force --prune --prune-tags + else + rm -rf "$REPO_DIR" + fi + fi + + if [ ! -d "$REPO_DIR/.git" ]; then + mkdir -p "${{ env.WORKSPACE_DIR }}" + git clone "$REPO_URL" "$REPO_DIR" + cd "$REPO_DIR" + fi + + TARGET_SHA="${{ github.sha }}" + TARGET_REF="${{ github.ref }}" + if git cat-file -e "$TARGET_SHA^{commit}" 2>/dev/null; then + git checkout -f "$TARGET_SHA" + else + if [ -n "$TARGET_REF" ]; then + git fetch origin "$TARGET_REF" + git checkout -f FETCH_HEAD + else + git checkout -f "${{ github.ref_name }}" + fi + fi + + git config --global --add safe.directory "$REPO_DIR" + echo "REPO_DIR=$REPO_DIR" >> $GITHUB_ENV + + - name: 🔍 检测变更文件 + id: changes + run: | + echo "========================================" + echo "🔍 检测变更文件" + echo "========================================" + + cd "$REPO_DIR" + + # 如果是手动触发或主分支push,测试所有内容 + if [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "push" ]; then + echo "scripts=true" >> $GITHUB_OUTPUT + echo "templates=true" >> $GITHUB_OUTPUT + echo "docs=true" >> $GITHUB_OUTPUT + echo "✓ 手动触发或主分支push,测试所有内容" + else + # PR:只测试变更的部分 + SCRIPTS_CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -E '^scripts/' || echo "") + TEMPLATES_CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -E '^templates/' || echo "") + DOCS_CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -E '^(docs/|.agents/)' || echo "") + + if [ -n "$SCRIPTS_CHANGED" ]; then + echo "scripts=true" >> $GITHUB_OUTPUT + echo "✓ 检测到脚本变更" + else + echo "scripts=false" >> $GITHUB_OUTPUT + fi + + if [ -n "$TEMPLATES_CHANGED" ]; then + echo "templates=true" >> $GITHUB_OUTPUT + echo "✓ 检测到模板变更" + else + echo "templates=false" >> $GITHUB_OUTPUT + fi + + if [ -n "$DOCS_CHANGED" ]; then + echo "docs=true" >> $GITHUB_OUTPUT + echo "✓ 检测到文档变更" + else + echo "docs=false" >> $GITHUB_OUTPUT + fi + fi + + echo "========================================" + + - name: 📊 显示测试环境信息 + run: | + echo "========================================" + echo "📊 测试环境信息" + echo "========================================" + echo "🖥️ 操作系统: $(lsb_release -ds)" + echo "🐚 Shell: $SHELL" + echo "🐍 Python: $(python3 --version)" + echo "📦 Git: $(git --version)" + echo "========================================" + + # ========================================== + # Job 2: Shell 脚本测试 + # ========================================== + test-scripts: + name: 🐚 Shell 脚本测试 + runs-on: ubuntu-22.04 + needs: setup + if: needs.setup.outputs.scripts-changed == 'true' + + strategy: + fail-fast: false + matrix: + script-group: + - name: sync_standards + scripts: "sync_standards.sh" + - name: vendor_playbook + scripts: "vendor_playbook.sh" + - name: install_codex_skills + scripts: "install_codex_skills.sh" + + steps: + - name: 📥 准备仓库 + run: | + echo "========================================" + echo "📥 准备仓库到 WORKSPACE_DIR" + echo "========================================" + + REPO_NAME="${{ github.event.repository.name }}" + REPO_DIR="${{ env.WORKSPACE_DIR }}/$REPO_NAME" + TOKEN="${{ secrets.WORKFLOW }}" + if [ -n "$TOKEN" ]; then + REPO_URL="https://oauth2:${TOKEN}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git" + else + REPO_URL="${GITHUB_SERVER_URL}/${{ github.repository }}.git" + fi + + if [ -d "$REPO_DIR" ]; then + if [ -d "$REPO_DIR/.git" ]; then + cd "$REPO_DIR" + git clean -fdx + git reset --hard + git fetch --all --tags --force --prune --prune-tags + else + rm -rf "$REPO_DIR" + fi + fi + + if [ ! -d "$REPO_DIR/.git" ]; then + mkdir -p "${{ env.WORKSPACE_DIR }}" + git clone "$REPO_URL" "$REPO_DIR" + cd "$REPO_DIR" + fi + + TARGET_SHA="${{ github.sha }}" + TARGET_REF="${{ github.ref }}" + if git cat-file -e "$TARGET_SHA^{commit}" 2>/dev/null; then + git checkout -f "$TARGET_SHA" + else + if [ -n "$TARGET_REF" ]; then + git fetch origin "$TARGET_REF" + git checkout -f FETCH_HEAD + else + git checkout -f "${{ github.ref_name }}" + fi + fi + + git config --global --add safe.directory "$REPO_DIR" + echo "REPO_DIR=$REPO_DIR" >> $GITHUB_ENV + + - name: 🔧 安装 bats-core + run: | + echo "========================================" + echo "🔧 安装 bats-core 测试框架" + echo "========================================" + + sudo apt-get update + sudo apt-get install -y bats + + echo "" + echo "✓ bats 版本: $(bats --version)" + echo "========================================" + + - name: 🧪 运行 ${{ matrix.script-group.name }} 测试 + run: | + echo "========================================" + echo "🧪 测试脚本组: ${{ matrix.script-group.name }}" + echo "========================================" + + cd "$REPO_DIR/tests/scripts" + + # 运行对应的测试文件 + if [ -f "test_${{ matrix.script-group.name }}.bats" ]; then + bats --formatter tap "test_${{ matrix.script-group.name }}.bats" | tee "${{ matrix.script-group.name }}_test_results.tap" + TEST_EXIT_CODE=${PIPESTATUS[0]} + + echo "" + if [ $TEST_EXIT_CODE -eq 0 ]; then + echo "✅ ${{ matrix.script-group.name }} 测试通过" + else + echo "❌ ${{ matrix.script-group.name }} 测试失败" + exit 1 + fi + else + echo "⚠️ 未找到测试文件: test_${{ matrix.script-group.name }}.bats" + exit 1 + fi + + echo "========================================" + + - name: 📊 上传测试结果 + if: always() + uses: actions/upload-artifact@v4 + with: + name: script-test-results-${{ matrix.script-group.name }} + path: ${{ env.REPO_DIR }}/tests/scripts/*_test_results.tap + retention-days: 30 + + # ========================================== + # Job 3: 模板验证测试 + # ========================================== + test-templates: + name: 📄 模板验证测试 + runs-on: ubuntu-22.04 + needs: setup + if: needs.setup.outputs.templates-changed == 'true' + + strategy: + fail-fast: false + matrix: + template-type: + - name: python + validator: validate_python_templates.sh + - name: cpp + validator: validate_cpp_templates.sh + - name: ci + validator: validate_ci_templates.sh + + steps: + - name: 📥 准备仓库 + run: | + echo "========================================" + echo "📥 准备仓库到 WORKSPACE_DIR" + echo "========================================" + + REPO_NAME="${{ github.event.repository.name }}" + REPO_DIR="${{ env.WORKSPACE_DIR }}/$REPO_NAME" + TOKEN="${{ secrets.WORKFLOW }}" + if [ -n "$TOKEN" ]; then + REPO_URL="https://oauth2:${TOKEN}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git" + else + REPO_URL="${GITHUB_SERVER_URL}/${{ github.repository }}.git" + fi + + if [ -d "$REPO_DIR" ]; then + if [ -d "$REPO_DIR/.git" ]; then + cd "$REPO_DIR" + git clean -fdx + git reset --hard + git fetch --all --tags --force --prune --prune-tags + else + rm -rf "$REPO_DIR" + fi + fi + + if [ ! -d "$REPO_DIR/.git" ]; then + mkdir -p "${{ env.WORKSPACE_DIR }}" + git clone "$REPO_URL" "$REPO_DIR" + cd "$REPO_DIR" + fi + + TARGET_SHA="${{ github.sha }}" + TARGET_REF="${{ github.ref }}" + if git cat-file -e "$TARGET_SHA^{commit}" 2>/dev/null; then + git checkout -f "$TARGET_SHA" + else + if [ -n "$TARGET_REF" ]; then + git fetch origin "$TARGET_REF" + git checkout -f FETCH_HEAD + else + git checkout -f "${{ github.ref_name }}" + fi + fi + + git config --global --add safe.directory "$REPO_DIR" + echo "REPO_DIR=$REPO_DIR" >> $GITHUB_ENV + + - name: 🔧 安装验证工具 + run: | + echo "========================================" + echo "🔧 安装 ${{ matrix.template-type.name }} 模板验证工具" + echo "========================================" + + case "${{ matrix.template-type.name }}" in + python) + python3 -m pip install --upgrade pip + pip install toml tomli jsonschema yamllint + echo "✓ Python 验证工具已安装" + ;; + cpp) + sudo apt-get update + sudo apt-get install -y cmake clang-format + echo "✓ C++ 验证工具已安装" + ;; + ci) + pip install yamllint + echo "✓ CI 验证工具已安装" + ;; + esac + + echo "========================================" + + - name: 🧪 验证 ${{ matrix.template-type.name }} 模板 + run: | + echo "========================================" + echo "🧪 验证模板类型: ${{ matrix.template-type.name }}" + echo "========================================" + + cd "$REPO_DIR/tests/templates" + + if [ -f "${{ matrix.template-type.validator }}" ]; then + chmod +x "${{ matrix.template-type.validator }}" + ./"${{ matrix.template-type.validator }}" + + if [ $? -eq 0 ]; then + echo "✅ ${{ matrix.template-type.name }} 模板验证通过" + else + echo "❌ ${{ matrix.template-type.name }} 模板验证失败" + exit 1 + fi + else + echo "⚠️ 未找到验证脚本: ${{ matrix.template-type.validator }}" + exit 1 + fi + + echo "========================================" + + - name: 📊 上传验证报告 + if: always() + uses: actions/upload-artifact@v4 + with: + name: template-validation-${{ matrix.template-type.name }} + path: ${{ env.REPO_DIR }}/tests/templates/*_validation_report.txt + retention-days: 30 + + # ========================================== + # Job 4: 集成测试 + # ========================================== + test-integration: + name: 🔗 集成测试 + runs-on: ubuntu-22.04 + needs: [setup, test-scripts, test-templates] + if: | + always() && + (needs.test-scripts.result == 'success' || needs.test-scripts.result == 'skipped') && + (needs.test-templates.result == 'success' || needs.test-templates.result == 'skipped') + + steps: + - name: 📥 准备仓库 + run: | + echo "========================================" + echo "📥 准备仓库到 WORKSPACE_DIR" + echo "========================================" + + REPO_NAME="${{ github.event.repository.name }}" + REPO_DIR="${{ env.WORKSPACE_DIR }}/$REPO_NAME" + TOKEN="${{ secrets.WORKFLOW }}" + if [ -n "$TOKEN" ]; then + REPO_URL="https://oauth2:${TOKEN}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git" + else + REPO_URL="${GITHUB_SERVER_URL}/${{ github.repository }}.git" + fi + + if [ -d "$REPO_DIR" ]; then + if [ -d "$REPO_DIR/.git" ]; then + cd "$REPO_DIR" + git clean -fdx + git reset --hard + git fetch --all --tags --force --prune --prune-tags + else + rm -rf "$REPO_DIR" + fi + fi + + if [ ! -d "$REPO_DIR/.git" ]; then + mkdir -p "${{ env.WORKSPACE_DIR }}" + git clone "$REPO_URL" "$REPO_DIR" + cd "$REPO_DIR" + fi + + TARGET_SHA="${{ github.sha }}" + TARGET_REF="${{ github.ref }}" + if git cat-file -e "$TARGET_SHA^{commit}" 2>/dev/null; then + git checkout -f "$TARGET_SHA" + else + if [ -n "$TARGET_REF" ]; then + git fetch origin "$TARGET_REF" + git checkout -f FETCH_HEAD + else + git checkout -f "${{ github.ref_name }}" + fi + fi + + git config --global --add safe.directory "$REPO_DIR" + echo "REPO_DIR=$REPO_DIR" >> $GITHUB_ENV + + - name: 🔧 准备测试环境 + run: | + echo "========================================" + echo "🔧 准备集成测试环境" + echo "========================================" + + mkdir -p "${{ env.TEST_WORKSPACE }}" + cd "${{ env.TEST_WORKSPACE }}" + + # 创建测试项目目录 + mkdir -p test-project-tsl + mkdir -p test-project-cpp + mkdir -p test-project-multi + + echo "✓ 测试环境已准备" + echo "========================================" + + - name: 🧪 测试场景1: TSL 项目标准同步 + run: | + echo "========================================" + echo "🧪 测试场景1: TSL 项目标准同步" + echo "========================================" + + cd "${{ env.TEST_WORKSPACE }}/test-project-tsl" + + # 初始化 git 仓库 + git init + git config user.name "Test User" + git config user.email "test@example.com" + + # 模拟 subtree add(包含 .agents 等点目录,排除 .git) + mkdir -p docs/standards/playbook + tar -C "$REPO_DIR" --exclude .git -cf - . | tar -C docs/standards/playbook -xf - + + # 运行同步脚本 + echo "▶ 运行 sync_standards.sh tsl" + sh docs/standards/playbook/scripts/sync_standards.sh tsl + + # 验证结果 + if [ -d ".agents/tsl" ] && [ -f ".agents/tsl/index.md" ]; then + echo "✅ TSL 规则集同步成功" + else + echo "❌ TSL 规则集同步失败" + exit 1 + fi + + if grep -q "# BEGIN playbook .gitattributes" .gitattributes 2>/dev/null; then + echo "✅ .gitattributes 更新成功" + else + echo "❌ .gitattributes 更新失败" + exit 1 + fi + + echo "========================================" + + - name: 🧪 测试场景2: C++ 项目标准同步 + run: | + echo "========================================" + echo "🧪 测试场景2: C++ 项目标准同步" + echo "========================================" + + cd "${{ env.TEST_WORKSPACE }}/test-project-cpp" + + git init + git config user.name "Test User" + git config user.email "test@example.com" + + mkdir -p docs/standards/playbook + tar -C "$REPO_DIR" --exclude .git -cf - . | tar -C docs/standards/playbook -xf - + + echo "▶ 运行 sync_standards.sh cpp" + sh docs/standards/playbook/scripts/sync_standards.sh cpp + + if [ -d ".agents/cpp" ] && [ -f ".agents/cpp/index.md" ]; then + echo "✅ C++ 规则集同步成功" + else + echo "❌ C++ 规则集同步失败" + exit 1 + fi + + echo "========================================" + + - name: 🧪 测试场景3: 多语言项目标准同步 + run: | + echo "========================================" + echo "🧪 测试场景3: 多语言项目标准同步" + echo "========================================" + + cd "${{ env.TEST_WORKSPACE }}/test-project-multi" + + git init + git config user.name "Test User" + git config user.email "test@example.com" + + mkdir -p docs/standards/playbook + tar -C "$REPO_DIR" --exclude .git -cf - . | tar -C docs/standards/playbook -xf - + + echo "▶ 运行 sync_standards.sh tsl cpp" + sh docs/standards/playbook/scripts/sync_standards.sh tsl cpp + + if [ -d ".agents/tsl" ] && [ -d ".agents/cpp" ] && [ -f ".agents/index.md" ]; then + echo "✅ 多语言规则集同步成功" + else + echo "❌ 多语言规则集同步失败" + exit 1 + fi + + echo "========================================" + + - name: 🧪 测试场景4: vendor_playbook 脚本 + run: | + echo "========================================" + echo "🧪 测试场景4: vendor_playbook 脚本" + echo "========================================" + + cd "${{ env.TEST_WORKSPACE }}" + mkdir -p test-project-vendor + cd test-project-vendor + + git init + git config user.name "Test User" + git config user.email "test@example.com" + + echo "▶ 运行 vendor_playbook.sh" + sh "$REPO_DIR/scripts/vendor_playbook.sh" . tsl + + if [ -d "docs/standards/playbook" ] && [ -d ".agents/tsl" ]; then + echo "✅ vendor_playbook 脚本执行成功" + else + echo "❌ vendor_playbook 脚本执行失败" + exit 1 + fi + + echo "========================================" + + - name: 🧹 清理测试环境 + if: always() + run: | + echo "🧹 清理测试环境..." + chmod -R u+w "${{ env.TEST_WORKSPACE }}" 2>/dev/null || true + rm -rf "${{ env.TEST_WORKSPACE }}" + echo "✓ 清理完成" + + # ========================================== + # Job 5: 文档一致性检查 + # ========================================== + test-docs: + name: 📚 文档一致性检查 + runs-on: ubuntu-22.04 + needs: setup + if: needs.setup.outputs.docs-changed == 'true' + + steps: + - name: 📥 准备仓库 + run: | + echo "========================================" + echo "📥 准备仓库到 WORKSPACE_DIR" + echo "========================================" + + REPO_NAME="${{ github.event.repository.name }}" + REPO_DIR="${{ env.WORKSPACE_DIR }}/$REPO_NAME" + TOKEN="${{ secrets.WORKFLOW }}" + if [ -n "$TOKEN" ]; then + REPO_URL="https://oauth2:${TOKEN}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git" + else + REPO_URL="${GITHUB_SERVER_URL}/${{ github.repository }}.git" + fi + + if [ -d "$REPO_DIR" ]; then + if [ -d "$REPO_DIR/.git" ]; then + cd "$REPO_DIR" + git clean -fdx + git reset --hard + git fetch --all --tags --force --prune --prune-tags + else + rm -rf "$REPO_DIR" + fi + fi + + if [ ! -d "$REPO_DIR/.git" ]; then + mkdir -p "${{ env.WORKSPACE_DIR }}" + git clone "$REPO_URL" "$REPO_DIR" + cd "$REPO_DIR" + fi + + TARGET_SHA="${{ github.sha }}" + TARGET_REF="${{ github.ref }}" + if git cat-file -e "$TARGET_SHA^{commit}" 2>/dev/null; then + git checkout -f "$TARGET_SHA" + else + if [ -n "$TARGET_REF" ]; then + git fetch origin "$TARGET_REF" + git checkout -f FETCH_HEAD + else + git checkout -f "${{ github.ref_name }}" + fi + fi + + git config --global --add safe.directory "$REPO_DIR" + echo "REPO_DIR=$REPO_DIR" >> $GITHUB_ENV + + - name: 🔍 检查文档链接有效性 + run: | + echo "========================================" + echo "🔍 检查文档链接有效性" + echo "========================================" + + cd "$REPO_DIR/tests/integration" + + if [ -f "check_doc_links.sh" ]; then + chmod +x check_doc_links.sh + ./check_doc_links.sh + + if [ $? -eq 0 ]; then + echo "✅ 文档链接检查通过" + else + echo "❌ 发现无效链接" + exit 1 + fi + else + echo "⚠️ 未找到链接检查脚本,跳过" + fi + + echo "========================================" + + - name: 🔍 检查代理规则一致性 + run: | + echo "========================================" + echo "🔍 检查代理规则一致性" + echo "========================================" + + cd "$REPO_DIR" + + # 检查 .agents/ 和 docs/ 中的规则是否一致 + python3 << 'EOF' + import os + import sys + from pathlib import Path + + errors = [] + + # 检查各语言的 .agents/ 目录 + agents_base = Path(".agents") + for lang_dir in ["tsl", "cpp", "python"]: + agents_lang = agents_base / lang_dir + if not agents_lang.exists(): + continue + + # 检查必须存在的文件 + required_files = ["index.md", "auth.md", "code_quality.md"] + for req_file in required_files: + file_path = agents_lang / req_file + if not file_path.exists(): + errors.append(f"❌ 缺少文件: {file_path}") + elif file_path.stat().st_size == 0: + errors.append(f"❌ 文件为空: {file_path}") + + if errors: + print("\n".join(errors)) + sys.exit(1) + else: + print("✅ 代理规则一致性检查通过") + EOF + + echo "========================================" + + # ========================================== + # Job 6: 生成测试报告 + # ========================================== + report: + name: 📊 生成测试报告 + runs-on: ubuntu-22.04 + needs: [setup, test-scripts, test-templates, test-integration, test-docs] + if: always() + + steps: + - name: 📁 准备报告目录 + run: | + mkdir -p "${{ env.WORKSPACE_DIR }}/test-results" + + - name: 📥 下载所有测试结果 + uses: actions/download-artifact@v4 + with: + path: ${{ env.WORKSPACE_DIR }}/test-results + + - name: 📊 生成综合报告 + if: always() + run: | + echo "========================================" + echo "📊 生成测试综合报告" + echo "========================================" + + cat >> $GITHUB_STEP_SUMMARY << 'EOFSUMMARY' + # 🧪 Playbook 测试报告 + + ## 📋 测试执行摘要 + + | 测试类型 | 状态 | + |---------|------| + EOFSUMMARY + + # Shell 脚本测试 + if [ "${{ needs.test-scripts.result }}" = "success" ]; then + echo "| 🐚 Shell 脚本测试 | ✅ 通过 |" >> $GITHUB_STEP_SUMMARY + elif [ "${{ needs.test-scripts.result }}" = "failure" ]; then + echo "| 🐚 Shell 脚本测试 | ❌ 失败 |" >> $GITHUB_STEP_SUMMARY + elif [ "${{ needs.test-scripts.result }}" = "skipped" ]; then + echo "| 🐚 Shell 脚本测试 | ⏭️ 跳过 |" >> $GITHUB_STEP_SUMMARY + fi + + # 模板验证测试 + if [ "${{ needs.test-templates.result }}" = "success" ]; then + echo "| 📄 模板验证测试 | ✅ 通过 |" >> $GITHUB_STEP_SUMMARY + elif [ "${{ needs.test-templates.result }}" = "failure" ]; then + echo "| 📄 模板验证测试 | ❌ 失败 |" >> $GITHUB_STEP_SUMMARY + elif [ "${{ needs.test-templates.result }}" = "skipped" ]; then + echo "| 📄 模板验证测试 | ⏭️ 跳过 |" >> $GITHUB_STEP_SUMMARY + fi + + # 集成测试 + if [ "${{ needs.test-integration.result }}" = "success" ]; then + echo "| 🔗 集成测试 | ✅ 通过 |" >> $GITHUB_STEP_SUMMARY + elif [ "${{ needs.test-integration.result }}" = "failure" ]; then + echo "| 🔗 集成测试 | ❌ 失败 |" >> $GITHUB_STEP_SUMMARY + elif [ "${{ needs.test-integration.result }}" = "skipped" ]; then + echo "| 🔗 集成测试 | ⏭️ 跳过 |" >> $GITHUB_STEP_SUMMARY + fi + + # 文档检查 + if [ "${{ needs.test-docs.result }}" = "success" ]; then + echo "| 📚 文档一致性检查 | ✅ 通过 |" >> $GITHUB_STEP_SUMMARY + elif [ "${{ needs.test-docs.result }}" = "failure" ]; then + echo "| 📚 文档一致性检查 | ❌ 失败 |" >> $GITHUB_STEP_SUMMARY + elif [ "${{ needs.test-docs.result }}" = "skipped" ]; then + echo "| 📚 文档一致性检查 | ⏭️ 跳过 |" >> $GITHUB_STEP_SUMMARY + fi + + cat >> $GITHUB_STEP_SUMMARY << 'EOFEND' + + --- + + ## 🔗 相关链接 + + - 📝 [测试文档](tests/README.md) + - 🐛 [问题反馈](../../issues) + - 📖 [开发指南](docs/index.md) + + --- + +
+ + *🤖 由 [Gitea Actions](../../actions) 自动生成* + + EOFEND + + echo "*📅 生成时间: $(date -u '+%Y-%m-%d %H:%M:%S UTC')*" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + + echo "========================================" + echo "✅ 测试报告已生成" + echo "========================================" diff --git a/docs/tsl/syntax_book/function.md b/docs/tsl/syntax_book/function.md index 5053315..83ae17d 100644 --- a/docs/tsl/syntax_book/function.md +++ b/docs/tsl/syntax_book/function.md @@ -63,9 +63,7 @@ function/ ### 方法 3:查看历史备份 -如果需要查看原始完整文件: - -- 备份文件:[function.md.backup](./function.md.backup)(保留完整内容) +如需查看原始完整文件,请从历史版本获取(当前仓库不保留备份文件)。 ## 💡 拆分的好处 diff --git a/docs/tsl/syntax_book/function/index.md b/docs/tsl/syntax_book/function/index.md index 1cd79ab..455b2d6 100644 --- a/docs/tsl/syntax_book/function/index.md +++ b/docs/tsl/syntax_book/function/index.md @@ -77,5 +77,4 @@ --- -**原始文档**:本文档由 221,389 行的 `function.md` 拆分而来,原文件已保留为 -[`../function.md.backup`](../function.md.backup) +**原始文档**:本文档由 221,389 行的 `function.md` 拆分而来,原始大文件不在当前仓库保留;如需查看,请从历史版本获取。 diff --git a/scripts/vendor_playbook.sh b/scripts/vendor_playbook.sh index 428c03e..ad7316c 100644 --- a/scripts/vendor_playbook.sh +++ b/scripts/vendor_playbook.sh @@ -210,7 +210,7 @@ sh docs/standards/playbook/scripts/install_codex_skills.sh ## CI templates(可选) -目标项目可复制启用的 CI 示例模板(如 Gitea Actions):`templates/ci/`。 +目标项目可复制启用的 CI 示例模板(如 Gitea Actions):\`templates/ci/\`。 EOF cat >"$DEST_PREFIX/SOURCE.md" <.bats`: + +```bash +#!/usr/bin/env bats + +setup() { + # 测试前准备 + export TEST_DIR="$(mktemp -d)" +} + +teardown() { + # 测试后清理 + rm -rf "$TEST_DIR" +} + +@test "描述测试内容" { + # 测试代码 + [ -f "some_file" ] +} +``` + +#### 添加新的模板验证 + +在 `tests/templates/` 创建新文件 `validate__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 diff --git a/tests/integration/check_doc_links.sh b/tests/integration/check_doc_links.sh new file mode 100644 index 0000000..1803222 --- /dev/null +++ b/tests/integration/check_doc_links.sh @@ -0,0 +1,220 @@ +#!/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, /\[[^]]+\]\(([^)]+)\)/, m)) { + print NR "\t" m[1] + line = substr(line, RSTART + RLENGTH) + } + + if (match(line, /^\[[^]]+\]:[[:space:]]*(.+)/, ref)) { + print NR "\t" ref[1] + } + } + ' "$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 diff --git a/tests/scripts/test_install_codex_skills.bats b/tests/scripts/test_install_codex_skills.bats new file mode 100644 index 0000000..9e93aa2 --- /dev/null +++ b/tests/scripts/test_install_codex_skills.bats @@ -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" ] +} diff --git a/tests/scripts/test_sync_standards.bats b/tests/scripts/test_sync_standards.bats new file mode 100644 index 0000000..0b864c0 --- /dev/null +++ b/tests/scripts/test_sync_standards.bats @@ -0,0 +1,251 @@ +#!/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 + 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 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 - 默认模式创建 managed block" { + cd "$TEST_DIR" + [ ! -f ".gitattributes" ] + + sh "$SCRIPT_PATH" tsl + + [ -f ".gitattributes" ] + grep -q "# BEGIN playbook .gitattributes" .gitattributes + grep -q "# END playbook .gitattributes" .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 + + 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 "幂等性 - 多次执行结果一致" { + 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" ] +} diff --git a/tests/scripts/test_vendor_playbook.bats b/tests/scripts/test_vendor_playbook.bats new file mode 100644 index 0000000..7b11684 --- /dev/null +++ b/tests/scripts/test_vendor_playbook.bats @@ -0,0 +1,284 @@ +#!/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 hash(40个十六进制字符) + 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" + + run sh "$SCRIPT_PATH" /nonexistent/path 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" ] +} diff --git a/tests/templates/validate_ci_templates.sh b/tests/templates/validate_ci_templates.sh new file mode 100644 index 0000000..c696353 --- /dev/null +++ b/tests/templates/validate_ci_templates.sh @@ -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 diff --git a/tests/templates/validate_cpp_templates.sh b/tests/templates/validate_cpp_templates.sh new file mode 100644 index 0000000..9e1f9c4 --- /dev/null +++ b/tests/templates/validate_cpp_templates.sh @@ -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 diff --git a/tests/templates/validate_python_templates.sh b/tests/templates/validate_python_templates.sh new file mode 100644 index 0000000..f97a7d1 --- /dev/null +++ b/tests/templates/validate_python_templates.sh @@ -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