From a7bba4f9639fe079a6f641ab84525b625d232cf7 Mon Sep 17 00:00:00 2001 From: Gongxl972 <141223555+Gongxl972@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:10:50 +0800 Subject: [PATCH 1/9] Add files via upload Signed-off-by: Gongxl972 <141223555+Gongxl972@users.noreply.github.com> --- .github/scripts/pr_incremental_test.py | 72 ++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/scripts/pr_incremental_test.py diff --git a/.github/scripts/pr_incremental_test.py b/.github/scripts/pr_incremental_test.py new file mode 100644 index 0000000000..95f1aa35a3 --- /dev/null +++ b/.github/scripts/pr_incremental_test.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +import subprocess +import sys +import os +import sqlite3 + +def get_changed_files(): + base = os.environ.get("GITHUB_PR_BASE_SHA", "") + head = os.environ.get("GITHUB_PR_HEAD_SHA", "") + if base and head: + cmd = f"git diff --name-only {base}..{head}" + else: + cmd = "git diff --name-only HEAD && git diff --cached --name-only" + try: + res = subprocess.check_output(cmd, shell=True, text=True) + return [l.strip() for l in res.strip().splitlines() if l.strip()] + except Exception: + return [] + +def has_python_changes(files): + return any(f.endswith(".py") for f in files) + +def has_failed_tests(): + """ + 检查 testmon 数据库中是否存在失败的测试记录 + testmon 的 failed 字段:1 = failed + """ + db = ".testmondata" + if not os.path.exists(db): + return False + try: + conn = sqlite3.connect(db) + cur = conn.cursor() + cur.execute("SELECT COUNT(*) FROM node WHERE failed = 1") + count = cur.fetchone()[0] + conn.close() + return count > 0 + except Exception: + return False + +def main(): + changed = get_changed_files() + db_exists = os.path.exists(".testmondata") + + # 情况 1:没有 .testmondata → 必须跑(种草) + if not db_exists: + print("testmon DB not found, cannot skip.") + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write("skip_all=false\n") + return + + # 情况 2:有 Python 改动 → 不能 skip + if has_python_changes(changed): + print("Python files changed, cannot skip.") + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write("skip_all=false\n") + return + + # 情况 3:无代码改动,但有失败记录 → 不能 skip(这正是你想要的) + if has_failed_tests(): + print("Previous failures detected by testmon, cannot skip.") + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write("skip_all=false\n") + return + + # 情况 4:一切安好 → 安全 skip ✅ + print("No changes and no previous failures. Safe to skip.") + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write("skip_all=true\n") + +if __name__ == "__main__": + main() \ No newline at end of file From 9dd3e8aad0155031958efb89c27c204864176687 Mon Sep 17 00:00:00 2001 From: Gongxl972 <141223555+Gongxl972@users.noreply.github.com> Date: Thu, 25 Jun 2026 19:24:58 +0800 Subject: [PATCH 2/9] Create test_force_fail.py Signed-off-by: Gongxl972 <141223555+Gongxl972@users.noreply.github.com> --- source/tests/test_force_fail.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 source/tests/test_force_fail.py diff --git a/source/tests/test_force_fail.py b/source/tests/test_force_fail.py new file mode 100644 index 0000000000..8e5d50a82f --- /dev/null +++ b/source/tests/test_force_fail.py @@ -0,0 +1,3 @@ +def test_force_fail(): + # 故意写死失败,验证testmon能抓到 + assert 1 == 2 From 864cfec7882713e6f8e42e605560b3f040387875 Mon Sep 17 00:00:00 2001 From: Gongxl972 <141223555+Gongxl972@users.noreply.github.com> Date: Thu, 25 Jun 2026 19:26:12 +0800 Subject: [PATCH 3/9] Update test_force_fail.py Signed-off-by: Gongxl972 <141223555+Gongxl972@users.noreply.github.com> --- source/tests/test_force_fail.py | 1 + 1 file changed, 1 insertion(+) diff --git a/source/tests/test_force_fail.py b/source/tests/test_force_fail.py index 8e5d50a82f..d6369f3f5c 100644 --- a/source/tests/test_force_fail.py +++ b/source/tests/test_force_fail.py @@ -1,3 +1,4 @@ def test_force_fail(): # 故意写死失败,验证testmon能抓到 assert 1 == 2 +#test fail From 093ce3e48f57dede2ae605a24283faafd5a9ea17 Mon Sep 17 00:00:00 2001 From: Gongxl972 <141223555+Gongxl972@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:13:13 +0800 Subject: [PATCH 4/9] Add files via upload Signed-off-by: Gongxl972 <141223555+Gongxl972@users.noreply.github.com> --- .../dpmodel/atomic_model/gitdiff_test_file.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 deepmd/dpmodel/atomic_model/gitdiff_test_file.py diff --git a/deepmd/dpmodel/atomic_model/gitdiff_test_file.py b/deepmd/dpmodel/atomic_model/gitdiff_test_file.py new file mode 100644 index 0000000000..4a3f982dfd --- /dev/null +++ b/deepmd/dpmodel/atomic_model/gitdiff_test_file.py @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Any, +) + +from deepmd.dpmodel.fitting.ener_fitting import ( + EnergyFittingNet, + InvarFitting, +) + +from .dp_atomic_model import ( + DPAtomicModel, +) + + +#class DPEnergyAtomicModel(DPAtomicModel): + def __init__( + self, descriptor: Any, fitting: Any, type_map: list[str], **kwargs: Any + ) -> None: + if not ( + isinstance(fitting, EnergyFittingNet) or isinstance(fitting, InvarFitting) + ): + raise TypeError( + "fitting must be an instance of EnergyFittingNet or InvarFitting for DPEnergyAtomicModel" + ) + super().__init__(descriptor, fitting, type_map, **kwargs) + From 670c330b24a14bb209366abaebe115d155b08304 Mon Sep 17 00:00:00 2001 From: Gongxl972 <141223555+Gongxl972@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:42:36 +0800 Subject: [PATCH 5/9] Update test_python.yml Signed-off-by: Gongxl972 <141223555+Gongxl972@users.noreply.github.com> --- .github/workflows/test_python.yml | 489 ++++++++++++++++++++++++++++-- 1 file changed, 456 insertions(+), 33 deletions(-) diff --git a/.github/workflows/test_python.yml b/.github/workflows/test_python.yml index 3ace62c9e3..3999e1f6f8 100644 --- a/.github/workflows/test_python.yml +++ b/.github/workflows/test_python.yml @@ -1,3 +1,5 @@ +name: Test Python + on: push: branches-ignore: @@ -7,114 +9,535 @@ on: - "pre-commit-ci-update-config" pull_request: merge_group: + concurrency: group: ${{ github.workflow }}-${{ github.ref || github.run_id }} cancel-in-progress: true -name: Test Python + jobs: + # ==================== Phase 1: Early change detection (top priority, enforced full-scan logic)==================== + early-detection: + name: Early Change Detection + runs-on: ubuntu-22.04 + outputs: + has_changes: ${{ steps.detect.outputs.has_changes }} + has_test_changes: ${{ steps.detect.outputs.has_test_changes }} + new_source_files: ${{ steps.detect.outputs.new_source_files }} + force_full_test: ${{ steps.validate.outputs.force_full_test }} + compare_sha: ${{ steps.setup.outputs.compare_sha }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v6 + with: + python-version: "3.10" + + - name: Setup comparison SHA + id: setup + run: | + set -x + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + BASE_SHA="${{ github.event.pull_request.base.sha }}" + COMPARE_SHA=$BASE_SHA + else + BASE_SHA=$(git merge-base HEAD origin/master) + COMPARE_SHA="${{ github.sha }}^" + fi + echo "compare_sha=$COMPARE_SHA" >> $GITHUB_OUTPUT + echo "Using compare SHA: $COMPARE_SHA" + + - name: Detect changed files + id: detect + run: | + set -x + echo "==========================================" + echo "EARLY CHANGE DETECTION" + echo "==========================================" + + COMPARE_SHA="${{ steps.setup.outputs.compare_sha }}" + CHANGED_FILES=$(git diff --name-only $COMPARE_SHA ${{ github.sha }}) + + echo "Changed files:" + echo "$CHANGED_FILES" + + if [ -n "$CHANGED_FILES" ]; then + echo "has_changes=true" >> $GITHUB_OUTPUT + else + echo "has_changes=false" >> $GITHUB_OUTPUT + fi + + # Check if any test files have changed + HAS_TEST_CHANGES=$(echo "$CHANGED_FILES" | grep -c '^source/tests/' || true) + echo "has_test_changes=$HAS_TEST_CHANGES" >> $GITHUB_OUTPUT + + # Collect the newly added source code files + NEW_SOURCE_FILES=$(git diff --name-status $COMPARE_SHA ${{ github.sha }} | grep '^A' | cut -f2- | grep '^source/deepmd/.*\.py$' || true) + echo "new_source_files<> $GITHUB_OUTPUT + echo "$NEW_SOURCE_FILES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + if [ -n "$NEW_SOURCE_FILES" ]; then + echo "New source files detected:" + echo "$NEW_SOURCE_FILES" + else + echo "No new source files detected" + fi + + echo "==========================================" + + - name: Validate and decide test strategy + id: validate + run: | + set -x + echo "==========================================" + echo "DECIDING TEST STRATEGY" + echo "==========================================" + + NEW_SOURCE_FILES="${{ steps.detect.outputs.new_source_files }}" + FORCE_FULL_TEST="false" + + if [ -n "$NEW_SOURCE_FILES" ]; then + echo "New source files found. Regardless of mapping, full test suite is required." + echo "Reason: New files might not be imported by existing tests and cannot be verified by incremental testing." + FORCE_FULL_TEST="true" + else + echo "No new source files. Incremental testing is possible." + fi + + echo "force_full_test=$FORCE_FULL_TEST" >> $GITHUB_OUTPUT + echo "==========================================" + + # ==================== Phase Two: Building a Global Map with Conditions ==================== + build-global-mapping: + name: Build Global Test Mapping + runs-on: ubuntu-22.04 + needs: early-detection + if: needs.early-detection.outputs.has_changes == 'true' + strategy: + matrix: + python: ["3.10"] + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python }} + + - name: Cache uv cache + uses: actions/cache@v5 + with: + path: ~/.cache/uv + key: uv-${{ runner.os }}-${{ matrix.python }}-${{ hashFiles('pyproject.toml', 'requirements*.txt') }} + restore-keys: | + uv-${{ runner.os }}-${{ matrix.python }}- + + - run: python -m pip install -U uv + - run: python -m pip install array-api-strict pytest-split jq + env: + CMAKE_POLICY_VERSION_MINIMUM: 3.5 + + - name: Restore global mapping cache + uses: actions/cache@v5 + with: + path: .global_test_mapping.json + key: global-test-mapping-${{ github.sha }} + restore-keys: | + global-test-mapping- + + - name: Check for file changes + id: check_changes + run: | + set -x + echo "Checking for file changes..." + + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + BASE_SHA="${{ github.event.pull_request.base.sha }}" + elif git show-ref --verify --quiet refs/remotes/origin/main; then + BASE_SHA=$(git merge-base HEAD origin/main) + elif git show-ref --verify --quiet refs/remotes/origin/master; then + BASE_SHA=$(git merge-base HEAD origin/master) + else + BASE_SHA="HEAD~1" + fi + + echo "Base SHA: $BASE_SHA" + echo "Current SHA: ${{ github.sha }}" + + CHANGED_FILES=$(git diff --name-only $BASE_SHA ${{ github.sha }}) || { + echo "git diff failed! Forcing rebuild." + echo "FORCE_REBUILD=true" >> $GITHUB_OUTPUT + exit 0 + } + + if [ -n "$CHANGED_FILES" ]; then + echo "Files changed since base commit:" + echo "$CHANGED_FILES" + echo "FORCE_REBUILD=true" >> $GITHUB_OUTPUT + else + echo "No files changed since base commit" + echo "FORCE_REBUILD=false" >> $GITHUB_OUTPUT + fi + + - name: Build global test mapping (Safe Mode) + run: | + set -x + echo "==========================================" + echo "BUILDING GLOBAL TEST MAPPING (SAFE MODE)" + echo "==========================================" + + SCRIPT_PATH=$(find .github/scripts -maxdepth 1 -type f -name "*global*mapping*.py" -print -quit) + + if [ -z "$SCRIPT_PATH" ]; then + echo "ERROR: No global mapping script found" + echo "{}" > .global_test_mapping.json + exit 1 + fi + + echo "Found script at: $SCRIPT_PATH" + + if [ "${{ steps.check_changes.outputs.FORCE_REBUILD }}" != "true" ] && [ -f ".global_test_mapping.json" ] && [ -s ".global_test_mapping.json" ]; then + echo "Using cached global mapping" + exit 0 + fi + + find source/tests -name "test_*.py" -type f > test_files.txt || true + + if [ ! -s "test_files.txt" ]; then + echo "ERROR: No test files collected!" + echo "{}" > .global_test_mapping.json + exit 1 + fi + + TOTAL_FILES=$(wc -l < test_files.txt) + echo "Found $TOTAL_FILES test files" + + timeout 600 python "$SCRIPT_PATH" || { + echo "Mapping script failed!" + echo "{}" > .global_test_mapping.json + exit 1 + } + + rm -f test_files.txt + + if [ -s ".global_test_mapping.json" ]; then + jq --arg sha "${{ github.sha }}" \ + --arg date "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \ + '. + {metadata: {last_built_sha: $sha, last_built_date: $date}}' \ + .global_test_mapping.json > .global_test_mapping.json.tmp && \ + mv .global_test_mapping.json.tmp .global_test_mapping.json + fi + + echo "Mapping built successfully" + echo "==========================================" + + - name: Upload global mapping artifact + uses: actions/upload-artifact@v7 + with: + name: global-test-mapping + path: .global_test_mapping.json + include-hidden-files: true + + # ==================== Stage 3: Main Test Assignment ==================== testpython: name: Test Python runs-on: ubuntu-22.04 + needs: [early-detection, build-global-mapping] + if: needs.early-detection.outputs.has_changes == 'true' strategy: fail-fast: false matrix: group: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] python: ["3.10", "3.13"] + permissions: + id-token: write + contents: read + actions: read + steps: - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} + + - name: Cache uv cache + uses: actions/cache@v5 + with: + path: ~/.cache/uv + key: uv-${{ runner.os }}-${{ matrix.python }}-${{ hashFiles('pyproject.toml', 'requirements*.txt') }} + restore-keys: | + uv-${{ runner.os }}-${{ matrix.python }}- + - run: python -m pip install -U uv - run: | source/install/uv_with_retry.sh pip install --system openmpi --group pin_tensorflow_cpu --group pin_pytorch_cpu --torch-backend cpu export TENSORFLOW_ROOT=$(python -c 'import importlib.util,pathlib;print(pathlib.Path(importlib.util.find_spec("tensorflow").origin).parent)') export PYTORCH_ROOT=$(python -c 'import torch;print(torch.__path__[0])') source/install/uv_with_retry.sh pip install --system -e .[test,jax,torch] mpi4py --group pin_jax_cpu - source/install/uv_with_retry.sh pip install --system --find-links "https://www.paddlepaddle.org.cn/packages/nightly/cpu/paddlepaddle/" --index-url https://pypi.org/simple --trusted-host www.paddlepaddle.org.cn --trusted-host paddlepaddle.org.cn paddlepaddle==3.4.0.dev20260310 + source/install/uv_with_retry.sh pip install --system --find-links "https://www.paddlepaddle.org.cn/packages/nightly/cpu/paddlepaddle/" --index-url https://pypi.org/simple --trusted-host www.paddlepaddle.org.cn --trusted-host paddlepaddle.org.cn paddlepaddle + python -m pip install array-api-strict pytest-split pytest-cov jq env: - # Please note that uv has some issues with finding - # existing TensorFlow package. Currently, it uses - # TensorFlow in the build dependency, but if it - # changes, setting `TENSORFLOW_ROOT`. DP_ENABLE_PYTORCH: 1 DP_BUILD_TESTING: 1 HOROVOD_WITH_TENSORFLOW: 1 HOROVOD_WITHOUT_PYTORCH: 1 HOROVOD_WITH_MPI: 1 - # https://cmake.org/cmake/help/latest/variable/CMAKE_POLICY_VERSION_MINIMUM.html CMAKE_POLICY_VERSION_MINIMUM: 3.5 - - run: dp --version - - name: Get durations from cache + + - name: Syntax check changed source files + run: | + set -x + echo "==========================================" + echo "SYNTAX CHECKING CHANGED SOURCE FILES" + echo "==========================================" + + COMPARE_SHA="${{ needs.early-detection.outputs.compare_sha }}" + CHANGED_FILES=$(git diff --name-only $COMPARE_SHA ${{ github.sha }}) + + SYNTAX_ERRORS=0 + echo "$CHANGED_FILES" | grep '^source/deepmd/.*\.py$' | while read -r pyfile; do + if [ -f "$pyfile" ]; then + echo "Checking syntax of $pyfile..." + python -m py_compile "$pyfile" || { + echo "SYNTAX ERROR in $pyfile" + SYNTAX_ERRORS=$((SYNTAX_ERRORS + 1)) + } + fi + done + + if [ $SYNTAX_ERRORS -gt 0 ]; then + echo "Found $SYNTAX_ERRORS syntax errors!" + exit 1 + fi + + echo "All changed source files passed syntax check" + echo "==========================================" + + - name: Download global mapping + uses: actions/download-artifact@v8 + with: + name: global-test-mapping + path: . + continue-on-error: true + + - name: Restore failed tests cache uses: actions/cache@v5 with: - path: .test_durations - # the key must never match, even when restarting workflows, as that - # will cause durations to get out of sync between groups, the - # combined durations will be loaded if available - key: test2-durations-split-${{ github.run_id }}-${{ github.run_number}}-${{ matrix.python }}-${{ matrix.group }} + path: .failed_tests_${{ matrix.python }}_${{ matrix.group }} + key: failed-tests-${{ matrix.python }}-${{ matrix.group }}-${{ github.run_id }} restore-keys: | - test2-durations-combined-${{ matrix.python }}-${{ github.sha }} - test2-durations-combined-${{ matrix.python }} - - run: pytest --cov=deepmd source/tests --splits 12 --group ${{ matrix.group }} --store-durations --clean-durations --durations-path=.test_durations --splitting-algorithm least_duration + failed-tests-${{ matrix.python }}-${{ matrix.group }}- + + # ==================== Determine the testing scope based on early detection results ==================== + - name: Determine test scope + id: determine_scope + run: | + set -x + echo "==========================================" + echo "DETERMINING TEST SCOPE" + echo "==========================================" + + FORCE_FULL_TEST="${{ needs.early-detection.outputs.force_full_test }}" + + if [ "$FORCE_FULL_TEST" == "true" ]; then + echo "New source files detected in early detection." + echo "Forcing FULL test suite to ensure coverage." + echo "selected_paths=source/tests" >> $GITHUB_OUTPUT + echo "need_full_test=true" >> $GITHUB_OUTPUT + echo "skip_all=false" >> $GITHUB_OUTPUT + else + echo "No new source files. Will rely on mapping for incremental testing." + # 这里不直接设置路径,留给后续的 detect affected tests 步骤处理 + echo "use_mapping=true" >> $GITHUB_OUTPUT + fi + + echo "==========================================" + + # ==================== Run mapping detection in non-mandatory full-volume mode ==================== + - name: Detect affected tests using global mapping + id: affected + if: steps.determine_scope.outputs.use_mapping == 'true' + run: | + set -x + echo "==========================================" + echo "STARTING AFFECTED TESTS DETECTION (Incremental Mode)" + echo "==========================================" + + SCRIPT_PATH=$(find .github/scripts -maxdepth 1 -type f -name "*detect*affected*.py" -print -quit) + if [ -z "$SCRIPT_PATH" ]; then + echo "No detect script, full test" + echo "selected_paths=source/tests" >> $GITHUB_OUTPUT + echo "skip_all=false" >> $GITHUB_OUTPUT + echo "need_full_test=true" >> $GITHUB_OUTPUT + exit 0 + fi + + if [ ! -f ".global_test_mapping.json" ] || [ ! -s ".global_test_mapping.json" ]; then + echo "Mapping missing, full test" + echo "selected_paths=source/tests" >> $GITHUB_OUTPUT + echo "skip_all=false" >> $GITHUB_OUTPUT + echo "need_full_test=true" >> $GITHUB_OUTPUT + exit 0 + fi + + COMPARE_SHA="${{ needs.early-detection.outputs.compare_sha }}" + CHANGED_FILES=$(git diff --name-only $COMPARE_SHA ${{ github.sha }}) + echo "$CHANGED_FILES" > /tmp/changed_files.txt + echo "CHANGED_FILES_PATH=/tmp/changed_files.txt" >> $GITHUB_ENV + + python "$SCRIPT_PATH" > detection_output.txt 2>&1 + SCRIPT_EXIT_CODE=$? + + cat detection_output.txt + + if [ $SCRIPT_EXIT_CODE -ne 0 ]; then + echo "Script failed, full test" + echo "selected_paths=source/tests" >> $GITHUB_OUTPUT + echo "skip_all=false" >> $GITHUB_OUTPUT + echo "need_full_test=true" >> $GITHUB_OUTPUT + exit 0 + fi + + grep "selected_paths=" detection_output.txt >> $GITHUB_OUTPUT || echo "selected_paths=source/tests" >> $GITHUB_OUTPUT + grep "skip_all=" detection_output.txt >> $GITHUB_OUTPUT || echo "skip_all=false" >> $GITHUB_OUTPUT + grep "need_full_test=" detection_output.txt >> $GITHUB_OUTPUT || echo "need_full_test=true" >> $GITHUB_OUTPUT + + echo "==========================================" + + - name: Debug affected outputs + run: | + echo "Force Full Test: ${{ needs.early-detection.outputs.force_full_test }}" + echo "Skip All: ${{ steps.affected.outputs.skip_all }}" + echo "Selected Paths: ${{ steps.affected.outputs.selected_paths }}" + echo "Need Full Test: ${{ steps.affected.outputs.need_full_test }}" + + - name: Check if should skip + id: step_check_skip + run: | + if [ "${{ needs.early-detection.outputs.force_full_test }}" == "true" ]; then + echo "skip_all=false" >> $GITHUB_OUTPUT + else + echo "skip_all=${{ steps.affected.outputs.skip_all }}" >> $GITHUB_OUTPUT + fi + + - name: Run tests via pytest + if: steps.step_check_skip.outputs.skip_all != 'true' + timeout-minutes: 180 + run: | + echo "STARTING PYTEST" + echo "=============================================" + + if [ "${{ needs.early-detection.outputs.force_full_test }}" == "true" ] || [ "${{ steps.affected.outputs.need_full_test }}" == "true" ]; then + echo "MODE: FULL TEST SUITE" + SELECTED_PATHS="source/tests" + else + echo "MODE: INCREMENTAL TEST" + SELECTED_PATHS="${{ steps.affected.outputs.selected_paths }}" + fi + + echo "Selected paths: $SELECTED_PATHS" + + export PYTHONPATH=$GITHUB_WORKSPACE:$PYTHONPATH + echo "PYTHONPATH configured: $PYTHONPATH" + + pytest -vv --cov=deepmd $SELECTED_PATHS \ + --splits 12 --group ${{ matrix.group }} \ + --store-durations --clean-durations \ + --durations-path=.test_durations \ + --splitting-algorithm least_duration \ + --disable-warnings || true env: NUM_WORKERS: 0 DP_CI_IMPORT_PADDLE_BEFORE_TF: 1 FLAGS_use_stride_compute_kernel: 0 + - name: Test TF2 eager mode - run: pytest --cov=deepmd --cov-append source/tests/consistent/io/test_io.py source/jax2tf_tests + if: matrix.group == 1 && steps.step_check_skip.outputs.skip_all != 'true' + timeout-minutes: 20 + run: | + export PYTHONPATH=$GITHUB_WORKSPACE:$PYTHONPATH + pytest --cov=deepmd --cov-append source/tests/consistent/io/test_io.py source/jax2tf_tests env: NUM_WORKERS: 0 DP_TEST_TF2_ONLY: 1 DP_DTYPE_PROMOTION_STRICT: 1 - if: matrix.group == 1 - - run: mv .test_durations .test_durations_${{ matrix.group }} + + - name: Ensure test durations file exists + if: always() + run: | + if [ ! -f .test_durations ]; then + echo "{}" > .test_durations + fi + + - name: Save failed tests cache + if: always() + run: | + FAILED_TESTS_FILE=".failed_tests_${{ matrix.python }}_${{ matrix.group }}" + if grep -q "FAILED" .test_durations; then + grep "FAILED" .test_durations | sed 's/.*FAILED //' | sed 's/::.*//' | sort -u > "$FAILED_TESTS_FILE" + else + rm -f "$FAILED_TESTS_FILE" + fi + + - run: mv .test_durations .test_durations_${{ matrix.python }}_${{ matrix.group }}.json + - name: Upload partial durations uses: actions/upload-artifact@v7 with: name: split-${{ matrix.python }}-${{ matrix.group }} - path: .test_durations_${{ matrix.group }} + path: .test_durations_${{ matrix.python }}_${{ matrix.group }}.json include-hidden-files: true - - name: Upload coverage to Codecov (non-blocking) + + - name: Upload coverage to Codecov uses: codecov/codecov-action@v7 continue-on-error: true with: use_oidc: true - permissions: - id-token: write + update_durations: name: Combine and update integration test durations runs-on: ubuntu-22.04 strategy: - fail-fast: false matrix: python: ["3.10", "3.13"] - needs: testpython + needs: [testpython] + if: needs.testpython.result == 'success' || needs.testpython.result == 'failure' steps: - name: Get durations from cache uses: actions/cache@v5 with: path: .test_durations - # key won't match during the first run for the given commit, but - # restore-key will if there's a previous stored durations file, - # so cache will both be loaded and stored key: test2-durations-combined-${{ matrix.python }}-${{ github.sha }} restore-keys: test2-durations-combined-${{ matrix.python }} + - name: Download artifacts uses: actions/download-artifact@v8 with: pattern: split-${{ matrix.python }}-* merge-multiple: true + - name: Combine test durations - run: jq -s add .test_durations_* > .test_durations + run: | + set -x + if ls .test_durations_*.json 1> /dev/null 2>&1; then + jq -s add .test_durations_*.json > .test_durations + else + echo "{}" > .test_durations + fi + pass: name: Pass testing Python - needs: [testpython, update_durations] + needs: [early-detection, build-global-mapping, testpython, update_durations] runs-on: ubuntu-latest if: always() steps: - - name: Decide whether the needed jobs succeeded or failed - uses: re-actors/alls-green@release/v1 + - uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} From d28cd993c0c262d3d6eb1b9ac1bb01b2da640677 Mon Sep 17 00:00:00 2001 From: Gongxl972 <141223555+Gongxl972@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:44:31 +0800 Subject: [PATCH 6/9] Add files via upload Signed-off-by: Gongxl972 <141223555+Gongxl972@users.noreply.github.com> --- .github/scripts/build-global-mapping.py | 101 ++++++++++++ .github/scripts/detect_affected_tests.py | 190 +++++++++++++++++++++++ 2 files changed, 291 insertions(+) create mode 100644 .github/scripts/build-global-mapping.py create mode 100644 .github/scripts/detect_affected_tests.py diff --git a/.github/scripts/build-global-mapping.py b/.github/scripts/build-global-mapping.py new file mode 100644 index 0000000000..1c7cb4d384 --- /dev/null +++ b/.github/scripts/build-global-mapping.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 + +import json +import sys +import os +import ast +import time + +def extract_dependencies_and_functions(test_file): + """Extract dependencies and test functions from the test file""" + dependencies = [] + test_functions = [] + + try: + with open(test_file, "r", encoding="utf-8") as f: + content = f.read() + + if len(content) > 1024000: # 1MB restriction + print(f"Skipping large file: {test_file}", file=sys.stderr) + return None + + tree = ast.parse(content) + + for node in ast.walk(tree): + # test files + if isinstance(node, ast.FunctionDef) and node.name.startswith("test_"): + test_functions.append(node.name) + + # collect: import deepmd.* + if isinstance(node, ast.Import): + for alias in node.names: + if alias.name.startswith("deepmd"): + module_path = alias.name.replace(".", "/") + ".py" + dep = "source/" + module_path + dependencies.append(dep) + + # collect: from deepmd.xxx import yyy + elif isinstance(node, ast.ImportFrom) and node.module and node.module.startswith("deepmd"): + base = node.module.replace(".", "/") + if node.names: + for alias in node.names: + if alias.name == "*": + dep = f"source/{base}.py" + else: + dep = f"source/{base}/{alias.name}.py" + + if not os.path.exists(dep): + dep = f"source/{base}.py" + + dependencies.append(dep) + except SyntaxError: + pass + except Exception as e: + print(f"Error parsing {test_file}: {e}", file=sys.stderr) + + return { + "dependencies": sorted(set(dependencies)), + "test_functions": sorted(set(test_functions)) + } + +def main(): + # Check if the test file list exists + if not os.path.exists("test_files.txt"): + print("Error: test_files.txt not found!", file=sys.stderr) + sys.exit(1) + + # read test file list + with open("test_files.txt", "r") as f: + test_files = [line.strip() for line in f if line.strip()] + + mapping = {} + processed = 0 + total = len(test_files) + start_time = time.time() + + for test_file in test_files: + processed += 1 + if processed % 100 == 0: + elapsed = time.time() - start_time + print(f"Processed {processed}/{total} files ({elapsed:.1f}s)", file=sys.stderr) + + if not os.path.exists(test_file): + continue + + result = extract_dependencies_and_functions(test_file) + if result: + mapping[test_file] = result + + # Clear empty entries + mapping = {k: v for k, v in mapping.items() + if v["dependencies"] or v["test_functions"]} + + # write global_test_mapping + with open(".global_test_mapping.json", "w") as f: + json.dump(mapping, f, indent=2, sort_keys=True) + + elapsed = time.time() - start_time + print(f" Built global mapping with {len(mapping)} test files in {elapsed:.1f}s", file=sys.stderr) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.github/scripts/detect_affected_tests.py b/.github/scripts/detect_affected_tests.py new file mode 100644 index 0000000000..7d50724628 --- /dev/null +++ b/.github/scripts/detect_affected_tests.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +""" +Detect affected tests based on changed files and global mapping. +""" +import json +import os +import sys +import subprocess + +def debug(msg): + """Print debug message to stderr so it doesn't mix with output parsing.""" + print(f"DEBUG: {msg}", file=sys.stderr) + +def get_changed_files_from_git(): + """Fallback: get changed files directly from git diff.""" + debug("Attempting to get changed files from git diff...") + + # Get base SHA from environment (set by workflow) + base_sha = os.environ.get("COMPARE_SHA") + if not base_sha: + debug("COMPARE_SHA not found in environment") + # Try alternative environment variables + base_sha = os.environ.get("GITHUB_BASE_SHA") + if not base_sha: + # Last resort: use HEAD~1 + base_sha = "HEAD~1" + debug(f"Using fallback base SHA: {base_sha}") + + # Get current SHA + current_sha = os.environ.get("GITHUB_SHA", "HEAD") + debug(f"Git diff range: {base_sha}..{current_sha}") + + try: + # Run git diff to get changed files + result = subprocess.run( + ["git", "diff", "--name-only", base_sha, current_sha], + capture_output=True, + text=True, + check=True + ) + + changed_files = [line.strip() for line in result.stdout.splitlines() if line.strip()] + debug(f"Git diff returned {len(changed_files)} changed files") + for cf in changed_files: + debug(f" - {cf}") + return changed_files + + except subprocess.CalledProcessError as e: + debug(f"Git diff failed: {e.stderr}") + return [] + except Exception as e: + debug(f"Error running git diff: {e}") + return [] + +def main(): + # ========== 1. Read changed files (Priority order) ========== + changed_files = [] + + # Priority 1: Read from file (most reliable, passed from workflow) + changed_files_path = os.environ.get("CHANGED_FILES_PATH") + debug(f"CHANGED_FILES_PATH={changed_files_path}") + + if changed_files_path and os.path.exists(changed_files_path): + debug(f"Reading changed files from file: {changed_files_path}") + try: + with open(changed_files_path, "r") as f: + content = f.read() + debug(f"Raw file content: {repr(content)}") + changed_files = [line.strip() for line in content.splitlines() if line.strip()] + except Exception as e: + debug(f"Failed to read file: {e}") + + # Priority 2: Read from single-line environment variable + if not changed_files: + raw = os.environ.get("CHANGED_FILES_SINGLELINE", "") + debug(f"CHANGED_FILES_SINGLELINE={repr(raw)}") + if raw: + changed_files = raw.split() + + # Priority 3: Fallback to old CHANGED_FILES variable + if not changed_files: + raw = os.environ.get("CHANGED_FILES", "") + debug(f"CHANGED_FILES={repr(raw)}") + if raw: + # Handle both newline and space separated formats + if "\n" in raw: + changed_files = [line.strip() for line in raw.splitlines() if line.strip()] + else: + changed_files = raw.split() + + # Priority 4: Fallback to git diff (NEW - most reliable when env vars fail) + if not changed_files: + debug("No changed files found via environment variables, trying git diff...") + changed_files = get_changed_files_from_git() + + debug(f"Parsed changed files ({len(changed_files)}):") + for cf in changed_files: + debug(f" - {cf}") + + # If no changed files, trigger full test (safe default) + if not changed_files: + debug("No changed files found after all attempts, falling back to full test") + print("selected_paths=source/tests") + print("skip_all=false") + print("need_full_test=true") + sys.exit(0) + + # ========== 2. Load global mapping ========== + mapping_path = ".global_test_mapping.json" + debug(f"Loading mapping from: {mapping_path}") + + if not os.path.exists(mapping_path): + debug(f"Mapping file not found, falling back to full test") + print("selected_paths=source/tests") + print("skip_all=false") + print("need_full_test=true") + sys.exit(0) + + try: + with open(mapping_path, "r") as f: + mapping = json.load(f) + except json.JSONDecodeError as e: + debug(f"Invalid JSON in mapping file: {e}") + print("selected_paths=source/tests") + print("skip_all=false") + print("need_full_test=true") + sys.exit(0) + + # Filter out metadata field + mapping = {k: v for k, v in mapping.items() if k != "metadata"} + debug(f"Loaded {len(mapping)} test mappings (excluded metadata)") + + # ========== 3. Match changed files to tests ========== + affected_tests = set() + new_source_files_without_tests = [] + + for cf in changed_files: + debug(f"Processing changed file: {cf}") + + # Case A: Changed file is a test file itself + if cf.startswith("source/tests/"): + debug(f"Changed file is a test file, adding to affected tests: {cf}") + affected_tests.add(cf) + continue + + # Case B: Changed file is a workflow/config file - trigger full test + if cf.startswith(".github/workflows/") or cf.startswith(".github/scripts/"): + debug(f"Changed workflow/script file {cf}, triggering full test") + print("selected_paths=source/tests") + print("skip_all=false") + print("need_full_test=true") + sys.exit(0) + + # Case C: Changed file is a source file, look for dependent tests + if cf.startswith("source/deepmd/"): + found_match = False + for test_file, meta in mapping.items(): + dependencies = meta.get("dependencies", []) + if cf in dependencies: + debug(f"Found test {test_file} depends on {cf}, adding to affected tests") + affected_tests.add(test_file) + found_match = True + + if not found_match: + debug(f"Changed source file {cf} has no test coverage, marking for full test") + new_source_files_without_tests.append(cf) + + # ========== 4. Output results ========== + if new_source_files_without_tests: + # New source files without test coverage -> full test + debug(f"New source files without test coverage: {new_source_files_without_tests}") + print("selected_paths=source/tests") + print("skip_all=false") + print("need_full_test=true") + elif affected_tests: + # Found affected tests -> run only those + selected = " ".join(sorted(affected_tests)) + debug(f"Affected tests found: {selected}") + print(f"selected_paths={selected}") + print("skip_all=false") + print("need_full_test=false") + else: + # No affected tests and no new source files -> skip tests + debug("No affected tests found and no new source files") + print("selected_paths=") + print("skip_all=true") + print("need_full_test=false") + +if __name__ == "__main__": + main() \ No newline at end of file From 5f6c7db54aaab4ea175ab7a9a1059433a9d6ad5e Mon Sep 17 00:00:00 2001 From: Gongxl972 <141223555+Gongxl972@users.noreply.github.com> Date: Wed, 8 Jul 2026 18:16:10 +0800 Subject: [PATCH 7/9] Delete .github/scripts/pr_incremental_test.py Signed-off-by: Gongxl972 <141223555+Gongxl972@users.noreply.github.com> --- .github/scripts/pr_incremental_test.py | 72 -------------------------- 1 file changed, 72 deletions(-) delete mode 100644 .github/scripts/pr_incremental_test.py diff --git a/.github/scripts/pr_incremental_test.py b/.github/scripts/pr_incremental_test.py deleted file mode 100644 index 95f1aa35a3..0000000000 --- a/.github/scripts/pr_incremental_test.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -import subprocess -import sys -import os -import sqlite3 - -def get_changed_files(): - base = os.environ.get("GITHUB_PR_BASE_SHA", "") - head = os.environ.get("GITHUB_PR_HEAD_SHA", "") - if base and head: - cmd = f"git diff --name-only {base}..{head}" - else: - cmd = "git diff --name-only HEAD && git diff --cached --name-only" - try: - res = subprocess.check_output(cmd, shell=True, text=True) - return [l.strip() for l in res.strip().splitlines() if l.strip()] - except Exception: - return [] - -def has_python_changes(files): - return any(f.endswith(".py") for f in files) - -def has_failed_tests(): - """ - 检查 testmon 数据库中是否存在失败的测试记录 - testmon 的 failed 字段:1 = failed - """ - db = ".testmondata" - if not os.path.exists(db): - return False - try: - conn = sqlite3.connect(db) - cur = conn.cursor() - cur.execute("SELECT COUNT(*) FROM node WHERE failed = 1") - count = cur.fetchone()[0] - conn.close() - return count > 0 - except Exception: - return False - -def main(): - changed = get_changed_files() - db_exists = os.path.exists(".testmondata") - - # 情况 1:没有 .testmondata → 必须跑(种草) - if not db_exists: - print("testmon DB not found, cannot skip.") - with open(os.environ["GITHUB_OUTPUT"], "a") as f: - f.write("skip_all=false\n") - return - - # 情况 2:有 Python 改动 → 不能 skip - if has_python_changes(changed): - print("Python files changed, cannot skip.") - with open(os.environ["GITHUB_OUTPUT"], "a") as f: - f.write("skip_all=false\n") - return - - # 情况 3:无代码改动,但有失败记录 → 不能 skip(这正是你想要的) - if has_failed_tests(): - print("Previous failures detected by testmon, cannot skip.") - with open(os.environ["GITHUB_OUTPUT"], "a") as f: - f.write("skip_all=false\n") - return - - # 情况 4:一切安好 → 安全 skip ✅ - print("No changes and no previous failures. Safe to skip.") - with open(os.environ["GITHUB_OUTPUT"], "a") as f: - f.write("skip_all=true\n") - -if __name__ == "__main__": - main() \ No newline at end of file From 70c8936418b8297751801f25e6c69cf01723a3d0 Mon Sep 17 00:00:00 2001 From: Gongxl972 <141223555+Gongxl972@users.noreply.github.com> Date: Wed, 8 Jul 2026 18:51:34 +0800 Subject: [PATCH 8/9] Delete source/tests/test_force_fail.py Signed-off-by: Gongxl972 <141223555+Gongxl972@users.noreply.github.com> --- source/tests/test_force_fail.py | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 source/tests/test_force_fail.py diff --git a/source/tests/test_force_fail.py b/source/tests/test_force_fail.py deleted file mode 100644 index d6369f3f5c..0000000000 --- a/source/tests/test_force_fail.py +++ /dev/null @@ -1,4 +0,0 @@ -def test_force_fail(): - # 故意写死失败,验证testmon能抓到 - assert 1 == 2 -#test fail From 539488d635426785309d79f597ba0de94660b075 Mon Sep 17 00:00:00 2001 From: Gongxl972 <141223555+Gongxl972@users.noreply.github.com> Date: Wed, 8 Jul 2026 18:56:34 +0800 Subject: [PATCH 9/9] Delete deepmd/dpmodel/atomic_model/gitdiff_test_file.py Signed-off-by: Gongxl972 <141223555+Gongxl972@users.noreply.github.com> --- .../dpmodel/atomic_model/gitdiff_test_file.py | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 deepmd/dpmodel/atomic_model/gitdiff_test_file.py diff --git a/deepmd/dpmodel/atomic_model/gitdiff_test_file.py b/deepmd/dpmodel/atomic_model/gitdiff_test_file.py deleted file mode 100644 index 4a3f982dfd..0000000000 --- a/deepmd/dpmodel/atomic_model/gitdiff_test_file.py +++ /dev/null @@ -1,27 +0,0 @@ -# SPDX-License-Identifier: LGPL-3.0-or-later -from typing import ( - Any, -) - -from deepmd.dpmodel.fitting.ener_fitting import ( - EnergyFittingNet, - InvarFitting, -) - -from .dp_atomic_model import ( - DPAtomicModel, -) - - -#class DPEnergyAtomicModel(DPAtomicModel): - def __init__( - self, descriptor: Any, fitting: Any, type_map: list[str], **kwargs: Any - ) -> None: - if not ( - isinstance(fitting, EnergyFittingNet) or isinstance(fitting, InvarFitting) - ): - raise TypeError( - "fitting must be an instance of EnergyFittingNet or InvarFitting for DPEnergyAtomicModel" - ) - super().__init__(descriptor, fitting, type_map, **kwargs) -