diff --git a/.github/workflows/sycl-linux-build.yml b/.github/workflows/sycl-linux-build.yml index 19a80f14d5ecd..45612131b7869 100644 --- a/.github/workflows/sycl-linux-build.yml +++ b/.github/workflows/sycl-linux-build.yml @@ -179,6 +179,7 @@ jobs: with: sparse-checkout: | devops/actions + devops/scripts # Cleanup will be run after all actions are completed. - name: Register cleanup after job is finished uses: ./devops/actions/cleanup @@ -332,6 +333,7 @@ jobs: testing_mode: build-only target_devices: all binaries_artifact: ${{ inputs.e2e_binaries_artifact }} + job_name: ${{ inputs.e2e_binaries_artifact }} sycl_compiler: $GITHUB_WORKSPACE/toolchain/bin/dpclang++ extra_lit_opts: --param sycl_build_targets="spir;nvidia" --param gpu_aot_target=${{ env.GPU_AOT_TARGET }} @@ -347,6 +349,7 @@ jobs: testing_mode: build-only target_devices: all binaries_artifact: ${{ inputs.e2e_binaries_new_offload_model_artifact }} + job_name: ${{ inputs.e2e_binaries_new_offload_model_artifact }} sycl_compiler: $GITHUB_WORKSPACE/toolchain/bin/dpclang++ extra_lit_opts: --param sycl_build_targets="spir;nvidia" --param enable_new_offload_model=True --param gpu_aot_target=${{ env.GPU_AOT_TARGET }} @@ -358,6 +361,7 @@ jobs: testing_mode: build-only target_devices: all binaries_artifact: ${{ inputs.e2e_binaries_spirv_backend_artifact }} + job_name: ${{ inputs.e2e_binaries_spirv_backend_artifact }} sycl_compiler: $GITHUB_WORKSPACE/toolchain/bin/dpclang++ extra_lit_opts: --param spirv-backend=True --param gpu_aot_target=${{ env.GPU_AOT_TARGET }} @@ -369,5 +373,6 @@ jobs: testing_mode: build-only target_devices: all binaries_artifact: ${{ inputs.e2e_binaries_preview_artifact }} + job_name: ${{ inputs.e2e_binaries_preview_artifact }} sycl_compiler: $GITHUB_WORKSPACE/toolchain/bin/dpclang++ extra_lit_opts: --param test-preview-mode=True --param gpu_aot_target=${{ env.GPU_AOT_TARGET }} diff --git a/.github/workflows/sycl-linux-precommit.yml b/.github/workflows/sycl-linux-precommit.yml index c473b3830140d..b676b79272595 100644 --- a/.github/workflows/sycl-linux-precommit.yml +++ b/.github/workflows/sycl-linux-precommit.yml @@ -81,7 +81,8 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: sparse-checkout: | - devops/ + devops/actions + devops/scripts # download build artefact - name: Download toolchain @@ -100,6 +101,7 @@ jobs: ref: ${{ github.ref || github.sha }} testing_mode: full target_devices: native_cpu:cpu + job_name: ${{ github.job }} sycl_compiler: $GITHUB_WORKSPACE/toolchain/bin/dpclang++ extra_lit_opts: --param sycl_build_targets="native_cpu" extra_cmake_args: -DSYCL_TEST_E2E_TARGETS="native_cpu:cpu" -DSYCL_TEST_E2E_STANDALONE=ON diff --git a/.github/workflows/sycl-linux-run-tests.yml b/.github/workflows/sycl-linux-run-tests.yml index 35badd106674c..fcfd32c4f8ec9 100644 --- a/.github/workflows/sycl-linux-run-tests.yml +++ b/.github/workflows/sycl-linux-run-tests.yml @@ -261,7 +261,8 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: sparse-checkout: | - devops + devops/actions + devops/scripts sycl/cts_exclude_filter - name: Register cleanup after job is finished uses: ./devops/actions/cleanup @@ -366,6 +367,7 @@ jobs: target_devices: ${{ inputs.target_devices }} extra_lit_opts: ${{ inputs.extra_lit_opts }} retention-days: ${{ inputs.retention-days }} + job_name: ${{ inputs.name }} - name: Run SYCL CTS Tests if: inputs.tests_selector == 'cts' diff --git a/.github/workflows/sycl-prebuilt-e2e-container.yml b/.github/workflows/sycl-prebuilt-e2e-container.yml index 15891fc59f6f2..b5fdfe3210eb0 100644 --- a/.github/workflows/sycl-prebuilt-e2e-container.yml +++ b/.github/workflows/sycl-prebuilt-e2e-container.yml @@ -102,7 +102,8 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: sparse-checkout: | - devops + devops/actions + devops/scripts - run: | mkdir toolchain tar -I 'zstd' -xf /sycl-prebuilt/toolchain.tar.zst -C toolchain @@ -116,3 +117,4 @@ jobs: with: testing_mode: run-only target_devices: level_zero:gpu + job_name: ${{ github.job }} diff --git a/.github/workflows/sycl-windows-build.yml b/.github/workflows/sycl-windows-build.yml index da370d946fe8a..44cc212935dcb 100644 --- a/.github/workflows/sycl-windows-build.yml +++ b/.github/workflows/sycl-windows-build.yml @@ -123,6 +123,7 @@ jobs: with: sparse-checkout: | devops/actions + devops/scripts - uses: TheMrMilchmann/setup-msvc-dev@368ef7d1ee4d1171b31d4a7f67f4d954f903f5a9 # v4.1.0 with: arch: x64 @@ -262,6 +263,7 @@ jobs: testing_mode: build-only target_devices: all binaries_artifact: ${{ inputs.e2e_binaries_artifact }} + job_name: ${{ inputs.e2e_binaries_artifact }} extra_lit_opts: --param sycl_build_targets="spir" --param gpu_aot_target=${{ env.GPU_AOT_TARGET }} cxx: ${{ inputs.cxx }} @@ -277,6 +279,7 @@ jobs: testing_mode: build-only target_devices: all binaries_artifact: ${{ inputs.e2e_binaries_new_offload_model_artifact }} + job_name: ${{ inputs.e2e_binaries_new_offload_model_artifact }} extra_lit_opts: --param sycl_build_targets="spir" --param enable_new_offload_model=True --param gpu_aot_target=${{ env.GPU_AOT_TARGET }} cxx: ${{ inputs.cxx }} diff --git a/.github/workflows/sycl-windows-run-tests.yml b/.github/workflows/sycl-windows-run-tests.yml index a9ebcaf58fbe6..5b683be76c596 100644 --- a/.github/workflows/sycl-windows-run-tests.yml +++ b/.github/workflows/sycl-windows-run-tests.yml @@ -98,6 +98,7 @@ jobs: with: sparse-checkout: | devops/actions + devops/scripts sycl/cts_exclude_filter - uses: TheMrMilchmann/setup-msvc-dev@368ef7d1ee4d1171b31d4a7f67f4d954f903f5a9 # v4.1.0 with: @@ -152,6 +153,7 @@ jobs: extra_lit_opts: ${{ inputs.extra_lit_opts }} retention-days: ${{ inputs.artifact_retention_days }} cxx: ${{ inputs.cxx }} + job_name: ${{ inputs.name }} - name: Run SYCL CTS Tests if: inputs.tests_selector == 'cts' diff --git a/.github/workflows/ur-build-hw.yml b/.github/workflows/ur-build-hw.yml index 7870cb85f1b34..d6cf13633ffd2 100644 --- a/.github/workflows/ur-build-hw.yml +++ b/.github/workflows/ur-build-hw.yml @@ -184,7 +184,7 @@ jobs: env: # Enable Level Zero loader debug trace for L0 adapters only ZE_ENABLE_LOADER_DEBUG_TRACE: ${{ startsWith(matrix.adapter.name, 'L0') && '1' || '' }} - uses: ./devops/actions/run-tests/ur + uses: ./devops/actions/run-tests/lit with: test_type: 'adapter-specific' build_dir: build @@ -194,7 +194,7 @@ jobs: env: # Enable Level Zero loader debug trace for L0 adapters only ZE_ENABLE_LOADER_DEBUG_TRACE: ${{ startsWith(matrix.adapter.name, 'L0') && '1' || '' }} - uses: ./devops/actions/run-tests/ur + uses: ./devops/actions/run-tests/lit with: test_type: 'conformance' build_dir: build diff --git a/devops/actions/run-tests/linux/e2e/action.yml b/devops/actions/run-tests/linux/e2e/action.yml index c9f11c5324694..d8d40b3560ce9 100644 --- a/devops/actions/run-tests/linux/e2e/action.yml +++ b/devops/actions/run-tests/linux/e2e/action.yml @@ -18,6 +18,10 @@ inputs: required: false sycl_compiler: required: false + job_name: + description: 'Name of the calling job, used to build a unique artifact name' + required: false + default: '' runs: @@ -43,6 +47,7 @@ runs: run: | mkdir build-e2e tar -I 'zstd' -xf e2e_binaries.tar.zst -C build-e2e + devops/scripts/validate-e2e-binaries build-e2e - name: Extract E2E tests from container image if: ${{ inputs.testing_mode == 'run-only' && inputs.binaries_artifact == 'in-container' }} @@ -51,6 +56,7 @@ runs: mkdir build-e2e llvm tar -I 'zstd' -xf /sycl-prebuilt/e2e_binaries.tar.zst -C build-e2e tar -I 'zstd' -xf /sycl-prebuilt/e2e_sources.tar.zst -C llvm + devops/scripts/validate-e2e-binaries build-e2e - name: Deduce E2E CMake options if: inputs.testing_mode != 'run-only' @@ -67,32 +73,32 @@ runs: shell: bash run: | cmake -GNinja -B./build-e2e -S./llvm/sycl/test-e2e -DCMAKE_CXX_COMPILER="${{ inputs.sycl_compiler || '$(which dpclang++)'}}" -DLLVM_LIT="$PWD/llvm/llvm/utils/lit/lit.py" ${{ steps.cmake_opts.outputs.opts }} - - name: SYCL End-to-end tests - id: run_e2e - continue-on-error: true + + # Composed here, not in ur-test, since these depend on caller-only workflow inputs. + - name: Compose E2E LIT options + id: lit_opts shell: bash env: - LIT_OPTS: -v --no-progress-bar --show-unsupported --show-pass --show-xfail --max-time 3600 --time-tests --param print_features=True --param test-mode=${{ inputs.testing_mode }} --param sycl_devices=${{ inputs.target_devices }} ${{ inputs.extra_lit_opts }} + TESTING_MODE: ${{ inputs.testing_mode }} + TARGET_DEVICES: ${{ inputs.target_devices }} + USER_LIT_OPTS: ${{ inputs.extra_lit_opts }} run: | - ninja -C build-e2e check-sycl-e2e > e2e.log 2>&1 - # Two steps below are duplicated between Lin/Win actions, updates must change both - - name: E2E logs - if: ${{ always() }} - shell: bash - run: | - echo "::group::Show Full E2E Log" - cat e2e.log - echo "::endgroup::" - - name: Report E2E Failures - if: steps.run_e2e.outcome != 'success' + # --timeout kills an individual hung test (e.g. a GPU driver hang) instead of + # letting it block the whole suite until the job's own timeout-minutes fires. + echo "opts=--max-time 3600 --timeout 600 --param print_features=True --param test-mode=${TESTING_MODE} --param sycl_devices=${TARGET_DEVICES} ${USER_LIT_OPTS}" >> "$GITHUB_OUTPUT" + + - name: Run E2E tests + uses: ./devops/actions/run-tests/lit + with: + test_type: 'e2e' + build_dir: 'build-e2e' + extra_lit_opts: ${{ steps.lit_opts.outputs.opts }} + job_name: ${{ inputs.job_name }} + + - name: Verify E2E binaries were built + if: ${{ !cancelled() && inputs.binaries_artifact != '' && inputs.testing_mode != 'run-only' }} shell: bash - # For some reason Github uses the first line from the `run: |` section for - # the folded entry when displaying instead of this step's name. - run: | - # Report E2E Failures - awk '/^Failed Tests|Unexpectedly Passed Tests|Unresolved tests|Timed Out Tests|Testing Time/{flag=1}/FAILED: CMakeFiles/{flag=0}flag' e2e.log >> $GITHUB_STEP_SUMMARY - awk '/^Failed Tests|Unexpectedly Passed Tests|Unresolved tests|Timed Out Tests|Testing Time/{flag=1}/FAILED: CMakeFiles/{flag=0}flag' e2e.log - exit 1 + run: devops/scripts/validate-e2e-binaries build-e2e - name: Pack E2E binaries if: ${{ !cancelled() && inputs.binaries_artifact != '' && inputs.testing_mode != 'run-only' }} diff --git a/devops/actions/run-tests/ur/action.yml b/devops/actions/run-tests/lit/action.yml similarity index 54% rename from devops/actions/run-tests/ur/action.yml rename to devops/actions/run-tests/lit/action.yml index 0e161f6970846..eee68160d3bb9 100644 --- a/devops/actions/run-tests/ur/action.yml +++ b/devops/actions/run-tests/lit/action.yml @@ -1,20 +1,47 @@ -name: 'Run UR Tests' +name: 'Run LIT Tests' inputs: test_type: - description: 'Type of tests to run (adapter-specific or conformance)' + description: 'Type of tests to run (adapter-specific, conformance or e2e)' required: true build_dir: required: false default: 'build' artifact_name: - description: 'Name for uploaded test artifacts (logs, XML)' + description: 'Name for uploaded test artifacts (logs, XML). If empty and job_name is set, a name is derived from test_type and job_name.' + required: false + default: '' + job_name: + description: 'Human-readable job identifier used to derive artifact_name when it is not given explicitly.' + required: false + default: '' + extra_lit_opts: + description: 'Extra options appended to LIT_OPTS (e.g. E2E --param/--max-time flags)' required: false default: '' runs: using: "composite" steps: + - name: Set up ur-test + id: ur_test + uses: ./devops/actions/setup-ur-test + + - name: Resolve artifact name + id: artifact + shell: bash + env: + ARTIFACT_NAME: ${{ inputs.artifact_name }} + JOB_NAME: ${{ inputs.job_name }} + TEST_TYPE: ${{ inputs.test_type }} + run: | + name="$ARTIFACT_NAME" + if [ -z "$name" ] && [ -n "$JOB_NAME" ]; then + safe_name=$(echo "$JOB_NAME" | tr -c '[:alnum:]._-' '-' | sed 's/-\+/-/g; s/^-//; s/-$//') + name="${TEST_TYPE}-${safe_name}" + fi + echo "name=$name" >> "$GITHUB_OUTPUT" + - name: Run ${{ inputs.test_type }} tests id: run_tests continue-on-error: true @@ -22,14 +49,21 @@ runs: env: BUILD_DIR: ${{ inputs.build_dir }} TEST_TYPE: ${{ inputs.test_type }} + EXTRA_LIT_OPTS: ${{ inputs.extra_lit_opts }} + PY: ${{ steps.ur_test.outputs.python }} + # Needed by lit (spawned via cmake/ninja's system Python) to see psutil, + # and by ur-test itself under the --target fallback; set on every step below too. + PYTHONPATH: ${{ steps.ur_test.outputs['site-packages'] }} run: | - devops/scripts/ur-test run "$TEST_TYPE" "$BUILD_DIR" "$GITHUB_WORKSPACE" + "$PY" devops/scripts/ur-test run "$TEST_TYPE" "$BUILD_DIR" "$GITHUB_WORKSPACE" - name: Filter test log if: ${{ always() && steps.run_tests.outputs['skip-artifacts'] != '1' }} shell: bash env: LOG_FILE: ${{ steps.run_tests.outputs['log-file'] }} + PY: ${{ steps.ur_test.outputs.python }} + PYTHONPATH: ${{ steps.ur_test.outputs['site-packages'] }} run: | if [ -z "$LOG_FILE" ]; then echo "::warning::Test log path not provided by runner; skipping log filtering." @@ -42,7 +76,7 @@ runs: fi echo "::group::Test Log" - devops/scripts/ur-test filter-log "$LOG_FILE" || { echo "::error::Filter failed"; head -1000 "$LOG_FILE"; } + "$PY" devops/scripts/ur-test filter-log "$LOG_FILE" || { echo "::error::Filter failed"; head -1000 "$LOG_FILE"; } echo "::endgroup::" - name: Report test failures @@ -50,13 +84,15 @@ runs: shell: bash env: LOG_FILE: ${{ steps.run_tests.outputs['log-file'] }} + PY: ${{ steps.ur_test.outputs.python }} + PYTHONPATH: ${{ steps.ur_test.outputs['site-packages'] }} run: | if [ -z "$LOG_FILE" ] || [ ! -f "$LOG_FILE" ]; then echo "::warning::Test execution failed, but no test log is available." exit 1 fi - devops/scripts/ur-test extract-errors "$LOG_FILE" + "$PY" devops/scripts/ur-test extract-errors "$LOG_FILE" exit 1 - name: Test summary @@ -65,9 +101,11 @@ runs: env: LOG_FILE: ${{ steps.run_tests.outputs['log-file'] }} XML_FILE: ${{ steps.run_tests.outputs['xml-file'] }} - ARTIFACT_NAME: ${{ inputs.artifact_name }} + ARTIFACT_NAME: ${{ steps.artifact.outputs.name }} TEST_TYPE: ${{ inputs.test_type }} WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + PY: ${{ steps.ur_test.outputs.python }} + PYTHONPATH: ${{ steps.ur_test.outputs['site-packages'] }} run: | if [ -z "$LOG_FILE" ]; then echo "::warning::Test log path not provided by runner; skipping summary." @@ -84,7 +122,7 @@ runs: exit 0 fi - devops/scripts/ur-test summary "$LOG_FILE" "${XML_FILE:-}" + "$PY" devops/scripts/ur-test summary "$LOG_FILE" "${XML_FILE:-}" if [ -n "$ARTIFACT_NAME" ]; then echo "" @@ -96,11 +134,11 @@ runs: if [ -n "$GITHUB_STEP_SUMMARY" ]; then echo "### ${TEST_TYPE} tests" >> "$GITHUB_STEP_SUMMARY" - devops/scripts/ur-test step-summary "$LOG_FILE" "${XML_FILE:-}" >> "$GITHUB_STEP_SUMMARY" + "$PY" devops/scripts/ur-test step-summary "$LOG_FILE" "${XML_FILE:-}" >> "$GITHUB_STEP_SUMMARY" fi - name: Copy artifacts - if: ${{ always() && inputs.artifact_name != '' && (inputs.test_type != 'adapter-specific' || steps.run_tests.outputs['skip-artifacts'] != '1') }} + if: ${{ always() && steps.artifact.outputs.name != '' && (inputs.test_type != 'adapter-specific' || steps.run_tests.outputs['skip-artifacts'] != '1') }} shell: bash env: LOG_FILE: ${{ steps.run_tests.outputs['log-file'] }} @@ -120,10 +158,10 @@ runs: fi - name: Upload test artifacts - if: ${{ always() && inputs.artifact_name != '' && (inputs.test_type != 'adapter-specific' || steps.run_tests.outputs['skip-artifacts'] != '1') }} + if: ${{ always() && steps.artifact.outputs.name != '' && (inputs.test_type != 'adapter-specific' || steps.run_tests.outputs['skip-artifacts'] != '1') }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: ${{ inputs.artifact_name }} + name: ${{ steps.artifact.outputs.name }} path: test_artifacts/ retention-days: 7 if-no-files-found: warn diff --git a/devops/actions/run-tests/windows/e2e/action.yml b/devops/actions/run-tests/windows/e2e/action.yml index f22bb0f65185c..3304253dc6e3b 100644 --- a/devops/actions/run-tests/windows/e2e/action.yml +++ b/devops/actions/run-tests/windows/e2e/action.yml @@ -21,6 +21,10 @@ inputs: cxx: required: false default: "cl" + job_name: + description: 'Name of the calling job, used to build a unique artifact name' + required: false + default: '' runs: using: "composite" @@ -44,6 +48,7 @@ runs: run: | mkdir build-e2e tar -xf e2e_bin.tar.gz -C build-e2e + devops/scripts/validate-e2e-binaries build-e2e - name: Deduce E2E CMake options id: cmake_opts @@ -71,32 +76,27 @@ runs: shell: bash run: ls build-e2e > e2econf_files.txt - - name: Run End-to-End tests - id: run_e2e - continue-on-error: true + # Composed here, not in ur-test, since these depend on caller-only workflow inputs. + - name: Compose E2E LIT options + id: lit_opts shell: bash env: - LIT_OPTS: -v --no-progress-bar --show-unsupported --show-pass --show-xfail --max-time ${{ inputs.testing_mode == 'run-only' && 1200 || 3600 }} --time-tests --param print_features=True --param test-mode=${{ inputs.testing_mode }} --param sycl_devices=${{ inputs.target_devices }} ${{ inputs.extra_lit_opts }} - run: | - cmake --build build-e2e --target check-sycl-e2e > e2e.log 2>&1 - # Two steps below are duplicated between Lin/Win actions, updates must change both - - name: E2E logs - if: ${{ always() }} - shell: bash - run: | - echo "::group::Show Full E2E Log" - cat e2e.log - echo "::endgroup::" - - name: Report E2E Failures - if: steps.run_e2e.outcome != 'success' - shell: bash - # For some reason Github uses the first line from the `run: |` section for - # the folded entry when displaying instead of this step's name. + TESTING_MODE: ${{ inputs.testing_mode }} + TARGET_DEVICES: ${{ inputs.target_devices }} + USER_LIT_OPTS: ${{ inputs.extra_lit_opts }} + MAX_TIME: ${{ inputs.testing_mode == 'run-only' && 1200 || 3600 }} run: | - # Report E2E Failures - awk '/^Failed Tests|Unexpectedly Passed Tests|Unresolved tests|Timed Out Tests|Testing Time/{flag=1}/FAILED: CMakeFiles/{flag=0}flag' e2e.log >> $GITHUB_STEP_SUMMARY - awk '/^Failed Tests|Unexpectedly Passed Tests|Unresolved tests|Timed Out Tests|Testing Time/{flag=1}/FAILED: CMakeFiles/{flag=0}flag' e2e.log - exit 1 + # --timeout kills an individual hung test (e.g. a GPU driver hang) instead of + # letting it block the whole suite until the job's own timeout-minutes fires. + echo "opts=--max-time ${MAX_TIME} --timeout 600 --param print_features=True --param test-mode=${TESTING_MODE} --param sycl_devices=${TARGET_DEVICES} ${USER_LIT_OPTS}" >> "$GITHUB_OUTPUT" + + - name: Run E2E tests + uses: ./devops/actions/run-tests/lit + with: + test_type: 'e2e' + build_dir: 'build-e2e' + extra_lit_opts: ${{ steps.lit_opts.outputs.opts }} + job_name: ${{ inputs.job_name }} # Github CI doesn't support containers on Windows, so we cannot guarantee # that paths are the same between building and running systems. To avoid @@ -109,6 +109,11 @@ runs: for FILE in $(cat e2econf_files.txt); do rm -r build-e2e/$FILE; done rm e2econf_files.txt + - name: Verify E2E binaries were built + if: ${{ !cancelled() && inputs.binaries_artifact != '' && inputs.testing_mode != 'run-only' }} + shell: bash + run: devops/scripts/validate-e2e-binaries build-e2e + - name: Pack E2E test binaries if: ${{ !cancelled() && inputs.binaries_artifact != '' && inputs.testing_mode != 'run-only' }} shell: bash diff --git a/devops/actions/setup-ur-test/action.yml b/devops/actions/setup-ur-test/action.yml new file mode 100644 index 0000000000000..fadb94f343e6b --- /dev/null +++ b/devops/actions/setup-ur-test/action.yml @@ -0,0 +1,64 @@ +name: 'Set up ur-test' +description: > + Provision the Python dependencies required by UR test reporting (ur-test) + and LIT execution (devops/scripts/ur_test_tools/requirements.txt), without + touching the system/base Python installation. + +outputs: + python: + description: 'Path to the Python interpreter with ur-test dependencies installed.' + value: ${{ steps.setup.outputs.python }} + site-packages: + description: > + Path to the directory containing ur-test's dependencies. Needed on + PYTHONPATH only by other processes (e.g. lit, run via a different, + system Python) that must see these packages but do not use the + interpreter above directly. + value: ${{ steps.setup.outputs.site-packages }} + +runs: + using: "composite" + steps: + - name: Provision ur-test Python environment + id: setup + shell: bash + run: | + # `python3` may not be on PATH on Windows runners. + if command -v python3 >/dev/null 2>&1; then + base_py=python3 + else + base_py=python + fi + + # Already satisfied (e.g. a caller-provisioned venv already on PATH)? + # Avoid provisioning a redundant nested environment. + if "$base_py" -c "import defusedxml, psutil" >/dev/null 2>&1; then + echo "python=$base_py" >> "$GITHUB_OUTPUT" + echo "site-packages=" >> "$GITHUB_OUTPUT" + exit 0 + fi + + venv_dir="${RUNNER_TEMP:-$GITHUB_WORKSPACE}/ur-test-venv" + rm -rf "$venv_dir" + if "$base_py" -m venv "$venv_dir" >/dev/null 2>&1; then + venv_py="$venv_dir/bin/python" + [ -f "$venv_py" ] || venv_py="$venv_dir/Scripts/python.exe" + "$venv_py" -m pip install --disable-pip-version-check --quiet \ + -r devops/scripts/ur_test_tools/requirements.txt + site_packages="$("$venv_py" -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')" + echo "python=$venv_py" >> "$GITHUB_OUTPUT" + echo "site-packages=$site_packages" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Some containers (nightly images) have no venv/ensurepip and broken apt; + # fall back to a workspace-local --target install on the base interpreter. + echo "::warning::'python -m venv' unavailable; falling back to a --target install." + deps_dir="${RUNNER_TEMP:-$GITHUB_WORKSPACE}/ur-test-deps" + rm -rf "$deps_dir" + mkdir -p "$deps_dir" + # Same --break-system-packages fallback as run-tests/benchmark for nightly images. + "$base_py" -m pip install --disable-pip-version-check --quiet --break-system-packages --target "$deps_dir" -r devops/scripts/ur_test_tools/requirements.txt \ + || "$base_py" -m pip install --disable-pip-version-check --quiet --target "$deps_dir" -r devops/scripts/ur_test_tools/requirements.txt + echo "python=$base_py" >> "$GITHUB_OUTPUT" + echo "site-packages=$deps_dir" >> "$GITHUB_OUTPUT" diff --git a/devops/scripts/ur_test_tools/cli.py b/devops/scripts/ur_test_tools/cli.py index 53ffcfc943526..c94c6531ae721 100644 --- a/devops/scripts/ur_test_tools/cli.py +++ b/devops/scripts/ur_test_tools/cli.py @@ -120,6 +120,8 @@ def _run_tests_command() -> int: github_actions.print_error(str(e)) return 1 + config.extra_lit_opts = os.environ.get("EXTRA_LIT_OPTS", "") + workspace_path = Path(workspace).resolve() build_dir_path = workspace_path / build_dir diff --git a/devops/scripts/ur_test_tools/constants.py b/devops/scripts/ur_test_tools/constants.py index 396574d62516c..f7d831c6e8d69 100644 --- a/devops/scripts/ur_test_tools/constants.py +++ b/devops/scripts/ur_test_tools/constants.py @@ -29,6 +29,7 @@ TEST_TYPE_ADAPTER_SPECIFIC = "adapter-specific" TEST_TYPE_CONFORMANCE = "conformance" +TEST_TYPE_E2E = "e2e" # These tests cause timeouts on CI and are excluded from adapter-specific runs LIT_FILTER_OUT_ADAPTER_SPECIFIC = ( @@ -37,8 +38,9 @@ ) TEST_NOT_SELECTED_MSG = "Test not selected" -SLOWEST_TESTS_HEADER = "Slowest Tests:" -TEST_TIMES_HEADERS = ("Tests Times:", "Test Times:") +# No trailing colon: lit appends a dynamic "(N of M):"/"(N):" suffix to these. +SLOWEST_TESTS_HEADER = "Slowest Tests" +TEST_TIMES_HEADERS = ("Tests Times", "Test Times") FAIL_TIMEOUT_PATTERN = re.compile(r"^(FAIL|TIMEOUT):") TEST_CATEGORY_PATTERN = re.compile(r"^([A-Za-z]+(?: [A-Za-z]+)*) Tests \((\d+)\):") diff --git a/devops/scripts/ur_test_tools/models/config.py b/devops/scripts/ur_test_tools/models/config.py index 0c640fa06751c..4bc038d9fac65 100644 --- a/devops/scripts/ur_test_tools/models/config.py +++ b/devops/scripts/ur_test_tools/models/config.py @@ -8,6 +8,9 @@ class TestConfig: target: str log_file: str lit_filter_out: Optional[str] = None + lit_timeout: Optional[int] = None + lit_jobs: Optional[int] = None + extra_lit_opts: str = "" def __post_init__(self): if not all([self.target, self.log_file]): diff --git a/devops/scripts/ur_test_tools/outputs/console.py b/devops/scripts/ur_test_tools/outputs/console.py index 2d88e3a29ca0c..70e124c92a2bf 100644 --- a/devops/scripts/ur_test_tools/outputs/console.py +++ b/devops/scripts/ur_test_tools/outputs/console.py @@ -27,7 +27,9 @@ def filter_log_for_display(lines: List[str]) -> List[str]: skip_until_empty = True continue - if stripped == SLOWEST_TESTS_HEADER or stripped in TEST_TIMES_HEADERS: + if stripped.startswith(SLOWEST_TESTS_HEADER) or any( + stripped.startswith(h) for h in TEST_TIMES_HEADERS + ): in_timing = True continue diff --git a/devops/scripts/ur_test_tools/parsers/log_parser.py b/devops/scripts/ur_test_tools/parsers/log_parser.py index d312c0438b4ff..227c5cb63b690 100644 --- a/devops/scripts/ur_test_tools/parsers/log_parser.py +++ b/devops/scripts/ur_test_tools/parsers/log_parser.py @@ -67,8 +67,8 @@ def extract_error_details(self) -> List[str]: if in_error and ( TEST_CATEGORY_PATTERN.match(line) - or line.strip() == SLOWEST_TESTS_HEADER - or line.strip() in TEST_TIMES_HEADERS + or line.strip().startswith(SLOWEST_TESTS_HEADER) + or any(line.strip().startswith(h) for h in TEST_TIMES_HEADERS) ): break @@ -101,11 +101,11 @@ def extract_time_summary(self) -> Tuple[List[str], List[str]]: for line in self.lines: stripped = line.strip() - if stripped == SLOWEST_TESTS_HEADER: + if stripped.startswith(SLOWEST_TESTS_HEADER): current_section = "slowest" skip_next_hr = True continue - elif stripped in TEST_TIMES_HEADERS: + elif any(stripped.startswith(h) for h in TEST_TIMES_HEADERS): current_section = "histogram" skip_next_hr = True continue diff --git a/devops/scripts/ur_test_tools/requirements.txt b/devops/scripts/ur_test_tools/requirements.txt new file mode 100644 index 0000000000000..6c631f784380b --- /dev/null +++ b/devops/scripts/ur_test_tools/requirements.txt @@ -0,0 +1,3 @@ +defusedxml==0.7.1 +# Required by lit's --timeout (kills hung test processes). +psutil>=7.0.0 diff --git a/devops/scripts/ur_test_tools/summary_generator.py b/devops/scripts/ur_test_tools/summary_generator.py index ed02323e3e23c..f4ccbb6852341 100644 --- a/devops/scripts/ur_test_tools/summary_generator.py +++ b/devops/scripts/ur_test_tools/summary_generator.py @@ -19,16 +19,6 @@ TestStatus.PASS, ] -# Statuses worth listing by name in the compact GitHub step summary. -_STEP_SUMMARY_LISTED_STATUSES = [ - TestStatus.FAIL, - TestStatus.XPASS, - TestStatus.TIMEOUT, - TestStatus.UNRESOLVED, -] - -_STEP_SUMMARY_MAX_LISTED_TESTS = 50 - class SummaryReporter: def __init__(self, result: TestRunResult): @@ -59,12 +49,9 @@ def _display_test_groups(self) -> None: def _print_test_group(self, status: TestStatus, tests: List[TestResult]) -> None: print(f"::group::{status.display_label} ({len(tests)})") - for test in tests[:100]: + for test in tests: print(test.name) - if len(tests) > 100: - print(f"... and {len(tests) - 100} more") - print("::endgroup::") def _display_timing(self) -> None: @@ -96,29 +83,26 @@ def _display_timing(self) -> None: print("::endgroup::") def generate_github_step_summary(self) -> None: - """Print a compact summary for $GITHUB_STEP_SUMMARY. + """Print a full summary for $GITHUB_STEP_SUMMARY. - Unlike generate(), this lists only statuses that need attention - (Failed, Unexpectedly Passed, Timed Out, Unresolved) and shows - all other statuses as counts only, to keep the workflow run page - readable regardless of how many tests were discovered. + Every status with at least one test gets its own collapsible section + listing every one of its test names, with no truncation. """ result = self.result grouped = result.group_by_status() - print("```") - - for status in _STEP_SUMMARY_LISTED_STATUSES: + for status in _STATUS_DISPLAY_ORDER: tests = grouped.get(status, []) if not tests: continue - print(f"{status.display_label} Tests ({len(tests)}):") - for test in tests[:_STEP_SUMMARY_MAX_LISTED_TESTS]: + print(f"
{status.display_label} ({len(tests)})") + print() + print("```") + for test in tests: print(test.name) - if len(tests) > _STEP_SUMMARY_MAX_LISTED_TESTS: - remaining = len(tests) - _STEP_SUMMARY_MAX_LISTED_TESTS - print(f"... and {remaining} more") + print("```") + print("
") print() if result.testing_time_ms is not None: @@ -129,6 +113,8 @@ def generate_github_step_summary(self) -> None: counted_statuses = [s for s in _STATUS_DISPLAY_ORDER if grouped.get(s)] if total and counted_statuses: print(f"Total Discovered Tests: {total}") + print() + print("```") label_width = max(len(s.display_label) for s in counted_statuses) for status in counted_statuses: count = len(grouped[status]) @@ -137,5 +123,4 @@ def generate_github_step_summary(self) -> None: f"{status.display_label:<{label_width}} : " f"{count} ({percentage:.2f}%)" ) - - print("```") + print("```") diff --git a/devops/scripts/ur_test_tools/test_runner.py b/devops/scripts/ur_test_tools/test_runner.py index f46f3101b0998..b6a7953874c3b 100644 --- a/devops/scripts/ur_test_tools/test_runner.py +++ b/devops/scripts/ur_test_tools/test_runner.py @@ -13,6 +13,7 @@ LIT_CI_OPTIONS, TEST_TYPE_ADAPTER_SPECIFIC, TEST_TYPE_CONFORMANCE, + TEST_TYPE_E2E, LIT_FILTER_OUT_ADAPTER_SPECIFIC, MAX_LINES_TO_SCAN, ) @@ -27,11 +28,21 @@ def get_test_config(test_type: str) -> TestConfig: target="check-unified-runtime-adapter", log_file="adapter_tests.log", lit_filter_out=LIT_FILTER_OUT_ADAPTER_SPECIFIC, + lit_timeout=DEFAULT_LIT_TIMEOUT, + lit_jobs=DEFAULT_LIT_JOBS, ) elif test_type == TEST_TYPE_CONFORMANCE: return TestConfig( target="check-unified-runtime-conformance", log_file="conformance_tests.log", + lit_timeout=DEFAULT_LIT_TIMEOUT, + lit_jobs=DEFAULT_LIT_JOBS, + ) + elif test_type == TEST_TYPE_E2E: + # Timeout/parallelism come from the caller via extra_lit_opts (e.g. --max-time). + return TestConfig( + target="check-sycl-e2e", + log_file="e2e_tests.log", ) else: raise ValueError(f"Invalid test_type: {test_type}") @@ -81,20 +92,33 @@ def run(self) -> int: return result.returncode def _setup_environment(self) -> None: + config = self.context.config lit_opts_parts = [ *LIT_COMMON_REPORTING_OPTIONS, *LIT_CI_OPTIONS, - "--timeout", - str(DEFAULT_LIT_TIMEOUT), - "-j", - str(DEFAULT_LIT_JOBS), + ] + + if config.lit_timeout is not None: + lit_opts_parts += ["--timeout", str(config.lit_timeout)] + + if config.lit_jobs is not None: + lit_opts_parts += ["-j", str(config.lit_jobs)] + + # Use forward slashes: LIT_OPTS is re-split with shlex (POSIX mode) on + # the lit side, which eats backslashes as escapes and mangles Windows + # paths (e.g. "D:\...\e2e_results.xml" -> "D:...e2e_resultsxml"). + lit_opts_parts += [ "--xunit-xml-output", - str(self.context.xml_output_path), + self.context.xml_output_path.as_posix(), ] + + if config.extra_lit_opts: + lit_opts_parts.append(config.extra_lit_opts) + self.context.env["LIT_OPTS"] = " ".join(lit_opts_parts) - if self.context.config.lit_filter_out: - self.context.env["LIT_FILTER_OUT"] = self.context.config.lit_filter_out + if config.lit_filter_out: + self.context.env["LIT_FILTER_OUT"] = config.lit_filter_out def _build_cmake_command(self) -> List[str]: return [ diff --git a/devops/scripts/ur_test_tools/tests/test_result_reconciliation.py b/devops/scripts/ur_test_tools/tests/test_result_reconciliation.py index 65326233c091c..79efbade49c29 100644 --- a/devops/scripts/ur_test_tools/tests/test_result_reconciliation.py +++ b/devops/scripts/ur_test_tools/tests/test_result_reconciliation.py @@ -179,6 +179,41 @@ def test_sums_repeated_statistics_labels(self): self.assertEqual(result["Passed"], 5) + def test_extracts_time_summary_with_lit_count_suffix(self): + # lit appends a dynamic "(N of M)"/"(N)" suffix to these headers. + lines = [ + "Slowest Tests (20 of 2022):\n", + "-" * 70 + "\n", + "41.86s: SYCL :: WorkGroupMemory/basic_usage.cpp\n", + "\n", + "Test Times (2022):\n", + "-" * 70 + "\n", + "[40.0s,42.0s) :: [] :: [ 1/2022]\n", + "-" * 70 + "\n", + ] + + slowest, histogram = LITLogParser(lines).extract_time_summary() + + self.assertEqual(slowest, ["41.86s: SYCL :: WorkGroupMemory/basic_usage.cpp"]) + self.assertEqual( + histogram, + ["[40.0s,42.0s) :: [] :: [ 1/2022]", "-" * 70], + ) + + def test_error_details_stop_at_lit_count_suffixed_slowest_header(self): + lines = [ + "FAIL: Suite :: test.cpp (1 of 1)\n", + "some failure output\n", + "Slowest Tests (1 of 1):\n", + "0.10s: Suite :: test.cpp\n", + ] + + details = LITLogParser(lines).extract_error_details() + + self.assertEqual( + details, ["FAIL: Suite :: test.cpp (1 of 1)\n", "some failure output\n"] + ) + def test_sums_repeated_category_counts(self): lines = [ "Passed Tests (1):\n", @@ -380,7 +415,7 @@ def test_displays_skipped_and_unsupported_separately(self): self.assertIn(" Unsupported: 3", output) self.assertIn("Testing Time: 1.25s", output) - def test_step_summary_lists_only_statuses_needing_attention(self): + def test_step_summary_lists_every_status_in_full(self): lines = [ "Failed Tests (1):\n", "suite :: fail.cpp\n", @@ -405,16 +440,19 @@ def test_step_summary_lists_only_statuses_needing_attention(self): SummaryReporter(result).generate_github_step_summary() output = stdout.getvalue() - self.assertIn("Failed Tests (1):", output) + self.assertIn("Failed (1)", output) self.assertIn("suite :: fail.cpp", output) - self.assertNotIn("suite :: pass-1.cpp", output) - self.assertNotIn("suite :: unsupported.cpp", output) + self.assertIn("Passed (2)", output) + self.assertIn("suite :: pass-1.cpp", output) + self.assertIn("suite :: pass-2.cpp", output) + self.assertIn("Unsupported (1)", output) + self.assertIn("suite :: unsupported.cpp", output) self.assertIn("Testing Time: 1.50s", output) self.assertIn("Total Discovered Tests: 4", output) self.assertIn("(25.00%)", output) self.assertIn("(50.00%)", output) - def test_step_summary_caps_listed_tests(self): + def test_step_summary_does_not_truncate_long_lists(self): lines = ["Failed Tests (60):\n"] lines.extend(f"suite :: fail-{i}.cpp\n" for i in range(60)) lines.append("\n") @@ -427,8 +465,8 @@ def test_step_summary_caps_listed_tests(self): output = stdout.getvalue() self.assertIn("suite :: fail-49.cpp", output) - self.assertNotIn("suite :: fail-50.cpp", output) - self.assertIn("... and 10 more", output) + self.assertIn("suite :: fail-59.cpp", output) + self.assertNotIn("more", output) class PathValidatorTest(unittest.TestCase): diff --git a/devops/scripts/validate-e2e-binaries b/devops/scripts/validate-e2e-binaries new file mode 100755 index 0000000000000..4162ea3339a91 --- /dev/null +++ b/devops/scripts/validate-e2e-binaries @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +build_dir="${1:?Usage: validate-e2e-binaries }" + +if [ -z "$(find "$build_dir" -path '*/Output/*' -type f -print -quit)" ]; then + echo "::error::No E2E test binaries found under $build_dir/**/Output/" + exit 1 +fi