From ffd99ffeb0a664528014bb8366e8a54ba2a9c89e Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 13 Jul 2026 12:14:02 +0800 Subject: [PATCH] =?UTF-8?q?ci:=20=E4=BC=98=E5=8C=96PR=E9=97=A8=E7=A6=81=20?= =?UTF-8?q?-=20Build=20Staging=E7=A7=BB=E5=87=BAPR=E6=B5=81=E7=A8=8B=20+?= =?UTF-8?q?=20Unit=20Tests=E7=8B=AC=E7=AB=8B=E5=B9=B6=E8=A1=8C=20(#245)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci-cd.yml | 129 +++++++++++-------- tests/unit/test_ffmpeg_timeout_protection.py | 2 +- 2 files changed, 73 insertions(+), 58 deletions(-) diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index dc732d20a..f3711fc55 100755 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -158,57 +158,72 @@ jobs: python3 scripts/check_migration_safety.py --allow-medium-risk fi - - name: Debug coverage paths - shell: sh - run: | - set +e - echo "=== PWD ===" - pwd - echo "=== check source dirs ===" - ls -d apps/api/app packages - echo "=== python import check ===" - python3 - <<'PY' - import sys, os - os.environ["PYTHONPATH"] = f"{os.getcwd()}/apps/api:{os.getcwd()}" - sys.path.insert(0, f"{os.getcwd()}/apps/api") - sys.path.insert(0, os.getcwd()) - print(f"cwd: {os.getcwd()}") - print(f"sys.path[:5]: {sys.path[:5]}") - try: - import app - print(f"app.__file__: {app.__file__}") - except Exception as e: - print(f"import app failed: {e}") - try: - import packages - print(f"packages.__file__: {packages.__file__}") - except Exception as e: - print(f"import packages failed: {e}") - PY - echo "=== coverage debug ===" - python3 - <<'PY' - import os, sys - sys.path.insert(0, f"{os.getcwd()}/apps/api") - sys.path.insert(0, os.getcwd()) - import coverage - cov = coverage.Coverage(source=["apps/api/app", "packages"]) - print(f"source: {cov.config.source}") - for src in cov.config.source or []: - abspath = os.path.abspath(src) - print(f" {src} -> {abspath} exists={os.path.exists(src)}") - if os.path.isdir(src): - pyfiles = [] - for root, dirs, files in os.walk(src): - for f in files: - if f.endswith('.py'): - pyfiles.append(os.path.join(root, f)) - print(f" .py files: {len(pyfiles)}") - PY + unit-tests: + name: Unit Tests + runs-on: host + timeout-minutes: 8 - - name: Run unit tests + env: + USE_IN_MEMORY_DB: "true" + + steps: + - name: Checkout code shell: sh env: - USE_IN_MEMORY_DB: "true" + GITHUB_TOKEN: ${{ github.token }} + run: | + set -eu + python3 - <<'PY' + import io, os, tarfile, time, urllib.request, urllib.error + url = f"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz" + request = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['GITHUB_TOKEN']}"}) + last_err = None + for attempt in range(5): + try: + with urllib.request.urlopen(request, timeout=120) as response: + archive = response.read() + break + except urllib.error.HTTPError as e: + last_err = e + if e.code >= 500 and attempt < 4: + wait = 2 ** attempt + print(f"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...") + time.sleep(wait) + continue + raise + except Exception as e: + last_err = e + if attempt < 4: + wait = 2 ** attempt + print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...") + time.sleep(wait) + continue + raise + else: + raise last_err + with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar: + root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/' + for member in tar.getmembers(): + name = member.name + if name == root_prefix[:-1]: + continue + if name.startswith(root_prefix): + member.name = name[len(root_prefix):] + if member.name: + tar.extract(member, '.') + PY + + - name: Install dependencies + shell: sh + run: | + set -eu + python3 -m pip install -q -r requirements-base.txt + python3 -m pip install -q -r requirements.txt + python3 -m pip install -q -r requirements-dev.txt + pytest --version + + - name: Run unit tests with coverage + shell: sh run: | set -eu PYTHONPATH="$PWD/apps/api:$PWD" python3 -m coverage run \ @@ -220,16 +235,16 @@ jobs: python3 -m coverage xml -o coverage.xml python3 -m coverage report --fail-under=60 > /dev/null - - name: Build summary - if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main' + - name: CI failure notification + if: failure() shell: sh + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + CI_WEBHOOK_URL: ${{ secrets.CI_WEBHOOK_URL }} run: | - set -eu - echo "Build completed successfully!" - echo "Branch: ${GITHUB_REF_NAME}" - echo "Commit: ${GITHUB_SHA}" - # 输出最终覆盖率 - python3 scripts/ci_coverage_summary.py + set +e + FAILED_JOB="Unit Tests" python3 scripts/ci_notify_failure.py + integration-tests: name: Integration Tests @@ -578,7 +593,7 @@ jobs: timeout-minutes: 30 needs: [validate, frontend-lint] - if: github.ref_name == 'main' || github.ref_name == 'develop' || startsWith(github.ref_name, 'feature/') + if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'develop') steps: - name: Checkout code diff --git a/tests/unit/test_ffmpeg_timeout_protection.py b/tests/unit/test_ffmpeg_timeout_protection.py index 04ecbac22..446d14fab 100644 --- a/tests/unit/test_ffmpeg_timeout_protection.py +++ b/tests/unit/test_ffmpeg_timeout_protection.py @@ -79,7 +79,7 @@ class TestProbeVideoInfoTimeout: """正常情况应解析 ffprobe JSON 输出。""" fake_output = """ { - "streams": [{"width": 1920, "height": 1080, "r_frame_rate": "30/1", "duration": "10.5"}], + "streams": [{"width": 1920, "height": 1080, "codec_type": "video", "r_frame_rate": "30/1", "duration": "10.5"}], "format": {"duration": "10.5"} } """