This is an automated email from the ASF dual-hosted git repository. tqchen pushed a commit to branch tvm-simplify-jenkins-pytest-sharding in repository https://gitbox.apache.org/repos/asf/tvm.git
commit 0992b4955b2fd5d8e6447fd18095ef9b90c07fab Author: Tianqi Chen <[email protected]> AuthorDate: Sat Jul 4 23:45:42 2026 +0000 [CI] Simplify Jenkins pytest execution Run each filtered suite once and rely on pytest-xdist inside the direct launchers. Remove obsolete sharding selectors and pytest helpers while preserving GPU filtering and Jenkins JUnit reporting. --- ci/jenkins/generated/arm_jenkinsfile.groovy | 4 +- ci/jenkins/generated/cpu_jenkinsfile.groovy | 79 ++------------ ci/jenkins/generated/docker_jenkinsfile.groovy | 4 +- ci/jenkins/generated/gpu_jenkinsfile.groovy | 98 +++-------------- ci/jenkins/generated/wasm_jenkinsfile.groovy | 4 +- ci/jenkins/templates/cpu_jenkinsfile.groovy.j2 | 3 +- ci/jenkins/templates/gpu_jenkinsfile.groovy.j2 | 8 +- ci/jenkins/templates/utils/base.groovy.j2 | 2 +- ci/jenkins/templates/utils/macros.j2 | 15 +-- ci/scripts/jenkins/pytest_ids.py | 43 -------- ci/scripts/jenkins/pytest_wrapper.py | 137 ------------------------ conftest.py | 54 ---------- docker/install/ubuntu_install_python_package.sh | 2 - tests/scripts/task_python_unittest.sh | 9 +- tests/scripts/task_python_unittest_gpuonly.sh | 5 +- 15 files changed, 40 insertions(+), 427 deletions(-) diff --git a/ci/jenkins/generated/arm_jenkinsfile.groovy b/ci/jenkins/generated/arm_jenkinsfile.groovy index d6826878bf..573885545a 100644 --- a/ci/jenkins/generated/arm_jenkinsfile.groovy +++ b/ci/jenkins/generated/arm_jenkinsfile.groovy @@ -60,7 +60,7 @@ // 'python3 jenkins/generate.py' // Note: This timestamp is here to ensure that updates to the Jenkinsfile are // always rebased on main before merging: -// Generated at 2026-06-23T03:28:14.520193 +// Generated at 2026-07-04T23:53:56.833713 import org.jenkinsci.plugins.pipeline.modeldefinition.Utils // These are set at runtime from data in ci/jenkins/docker-images.yml, update @@ -97,7 +97,7 @@ properties([ upstream_revision = null // command to start a docker container -docker_run = 'docker/bash.sh --env CI --env PLATFORM --env TVM_SHARD_INDEX --env TVM_NUM_SHARDS --env RUN_DISPLAY_URL --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' +docker_run = 'docker/bash.sh --env CI --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' docker_build = 'docker/build.sh' // timeout in minutes max_time = 180 diff --git a/ci/jenkins/generated/cpu_jenkinsfile.groovy b/ci/jenkins/generated/cpu_jenkinsfile.groovy index 879cd8b79f..dc944096ab 100644 --- a/ci/jenkins/generated/cpu_jenkinsfile.groovy +++ b/ci/jenkins/generated/cpu_jenkinsfile.groovy @@ -60,7 +60,7 @@ // 'python3 jenkins/generate.py' // Note: This timestamp is here to ensure that updates to the Jenkinsfile are // always rebased on main before merging: -// Generated at 2026-06-23T03:28:14.505504 +// Generated at 2026-07-04T23:53:56.855205 import org.jenkinsci.plugins.pipeline.modeldefinition.Utils // These are set at runtime from data in ci/jenkins/docker-images.yml, update @@ -97,7 +97,7 @@ properties([ upstream_revision = null // command to start a docker container -docker_run = 'docker/bash.sh --env CI --env PLATFORM --env TVM_SHARD_INDEX --env TVM_NUM_SHARDS --env RUN_DISPLAY_URL --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' +docker_run = 'docker/bash.sh --env CI --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' docker_build = 'docker/build.sh' // timeout in minutes max_time = 180 @@ -510,52 +510,7 @@ build() - -def shard_run_unittest_CPU_1_of_2(node_type) { - echo 'Begin running on node_type ' + node_type - if (!skip_ci && is_docs_only_build != 1) { - node(node_type) { - ws("workspace/exec_${env.EXECUTOR_NUMBER}/tvm/ut-python-cpu") { - // NOTE: if exception happens, it will be caught outside - init_git() - docker_init(ci_cpu) - timeout(time: max_time, unit: 'MINUTES') { - withEnv([ - 'PLATFORM=cpu', - 'TEST_STEP_NAME=unittest: CPU', - 'TVM_NUM_SHARDS=2', - 'TVM_SHARD_INDEX=0', - "SKIP_SLOW_TESTS=${skip_slow_tests}"], { - sh( - script: "./${jenkins_scripts_root}/s3.py --action download --bucket ${s3_bucket} --prefix ${s3_prefix}/cpu", - label: 'Download artifacts from S3', - ) - - ci_setup(ci_cpu) - cpp_unittest(ci_cpu) - python_unittest(ci_cpu) - }) - } - // only run upload if things are successful - try { - sh( - script: "./${jenkins_scripts_root}/s3.py --action upload --bucket ${s3_bucket} --prefix ${s3_prefix}/pytest-results/unittest_CPU --items build/pytest-results", - label: 'Upload JUnits to S3', - ) - - junit 'build/pytest-results/*.xml' - } catch (Exception e) { - echo 'Exception during JUnit upload: ' + e.toString() - } - } - } - echo 'End running on node_type ' + node_type - } else { - Utils.markStageSkippedForConditional('unittest: CPU 1 of 2') - } -} - -def shard_run_unittest_CPU_2_of_2(node_type) { +def run_unittest_CPU(node_type) { echo 'Begin running on node_type ' + node_type if (!skip_ci && is_docs_only_build != 1) { node(node_type) { @@ -567,8 +522,6 @@ def shard_run_unittest_CPU_2_of_2(node_type) { withEnv([ 'PLATFORM=cpu', 'TEST_STEP_NAME=unittest: CPU', - 'TVM_NUM_SHARDS=2', - 'TVM_SHARD_INDEX=1', "SKIP_SLOW_TESTS=${skip_slow_tests}"], { sh( script: "./${jenkins_scripts_root}/s3.py --action download --bucket ${s3_bucket} --prefix ${s3_prefix}/cpu", @@ -595,7 +548,7 @@ def shard_run_unittest_CPU_2_of_2(node_type) { } echo 'End running on node_type ' + node_type } else { - Utils.markStageSkippedForConditional('unittest: CPU 2 of 2') + Utils.markStageSkippedForConditional('unittest: CPU') } } @@ -606,27 +559,9 @@ def test() { SKIP_SLOW_TESTS = "${skip_slow_tests}" } parallel( - 'unittest: CPU 1 of 2': { - try { - shard_run_unittest_CPU_1_of_2('CPU-SMALL-SPOT') - } catch (Throwable ex) { - echo 'Exception during SPOT run ' + ex.toString() - if (is_last_build()) { - // retry if at last build - // mark the current stage as success - // and try again via on demand node - echo 'Retry on-demand given it is last build' - currentBuild.result = 'SUCCESS' - shard_run_unittest_CPU_1_of_2('CPU-SMALL') - } else { - echo 'Exit since it is not last build' - throw ex - } - } - }, - 'unittest: CPU 2 of 2': { + 'unittest: CPU': { try { - shard_run_unittest_CPU_2_of_2('CPU-SMALL-SPOT') + run_unittest_CPU('CPU-SMALL-SPOT') } catch (Throwable ex) { echo 'Exception during SPOT run ' + ex.toString() if (is_last_build()) { @@ -635,7 +570,7 @@ def test() { // and try again via on demand node echo 'Retry on-demand given it is last build' currentBuild.result = 'SUCCESS' - shard_run_unittest_CPU_2_of_2('CPU-SMALL') + run_unittest_CPU('CPU-SMALL') } else { echo 'Exit since it is not last build' throw ex diff --git a/ci/jenkins/generated/docker_jenkinsfile.groovy b/ci/jenkins/generated/docker_jenkinsfile.groovy index f5409e23aa..95f2fd8dd6 100644 --- a/ci/jenkins/generated/docker_jenkinsfile.groovy +++ b/ci/jenkins/generated/docker_jenkinsfile.groovy @@ -60,7 +60,7 @@ // 'python3 jenkins/generate.py' // Note: This timestamp is here to ensure that updates to the Jenkinsfile are // always rebased on main before merging: -// Generated at 2026-06-23T03:28:14.487314 +// Generated at 2026-07-04T23:53:56.875051 import org.jenkinsci.plugins.pipeline.modeldefinition.Utils // These are set at runtime from data in ci/jenkins/docker-images.yml, update @@ -97,7 +97,7 @@ properties([ upstream_revision = null // command to start a docker container -docker_run = 'docker/bash.sh --env CI --env PLATFORM --env TVM_SHARD_INDEX --env TVM_NUM_SHARDS --env RUN_DISPLAY_URL --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' +docker_run = 'docker/bash.sh --env CI --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' docker_build = 'docker/build.sh' // timeout in minutes max_time = 180 diff --git a/ci/jenkins/generated/gpu_jenkinsfile.groovy b/ci/jenkins/generated/gpu_jenkinsfile.groovy index 28c9a6040e..d31160dd52 100644 --- a/ci/jenkins/generated/gpu_jenkinsfile.groovy +++ b/ci/jenkins/generated/gpu_jenkinsfile.groovy @@ -60,7 +60,7 @@ // 'python3 jenkins/generate.py' // Note: This timestamp is here to ensure that updates to the Jenkinsfile are // always rebased on main before merging: -// Generated at 2026-06-23T03:28:14.533751 +// Generated at 2026-07-04T23:53:56.896868 import org.jenkinsci.plugins.pipeline.modeldefinition.Utils // These are set at runtime from data in ci/jenkins/docker-images.yml, update @@ -97,7 +97,7 @@ properties([ upstream_revision = null // command to start a docker container -docker_run = 'docker/bash.sh --env CI --env PLATFORM --env TVM_SHARD_INDEX --env TVM_NUM_SHARDS --env RUN_DISPLAY_URL --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' +docker_run = 'docker/bash.sh --env CI --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' docker_build = 'docker/build.sh' // timeout in minutes max_time = 180 @@ -515,8 +515,7 @@ def build() { build() - -def shard_run_unittest_GPU_1_of_2(node_type) { +def run_unittest_GPU(node_type) { echo 'Begin running on node_type ' + node_type if (!skip_ci && is_docs_only_build != 1) { node(node_type) { @@ -528,58 +527,6 @@ def shard_run_unittest_GPU_1_of_2(node_type) { withEnv([ 'PLATFORM=gpu', 'TEST_STEP_NAME=unittest: GPU', - 'TVM_NUM_SHARDS=2', - 'TVM_SHARD_INDEX=0', - "SKIP_SLOW_TESTS=${skip_slow_tests}"], { - sh( - script: "./${jenkins_scripts_root}/s3.py --action download --bucket ${s3_bucket} --prefix ${s3_prefix}/gpu", - label: 'Download artifacts from S3', - ) - - ci_setup(ci_gpu) - sh ( - script: "${docker_run} ${ci_gpu} ./tests/scripts/task_python_unittest_gpuonly.sh", - label: 'Run Python GPU unit tests', - ) - sh ( - script: "${docker_run} ${ci_gpu} ./tests/scripts/task_python_integration_gpuonly.sh", - label: 'Run Python GPU integration tests', - ) - }) - } - // only run upload if things are successful - try { - sh( - script: "./${jenkins_scripts_root}/s3.py --action upload --bucket ${s3_bucket} --prefix ${s3_prefix}/pytest-results/unittest_GPU --items build/pytest-results", - label: 'Upload JUnits to S3', - ) - - junit 'build/pytest-results/*.xml' - } catch (Exception e) { - echo 'Exception during JUnit upload: ' + e.toString() - } - } - } - echo 'End running on node_type ' + node_type - } else { - Utils.markStageSkippedForConditional('unittest: GPU 1 of 2') - } -} - -def shard_run_unittest_GPU_2_of_2(node_type) { - echo 'Begin running on node_type ' + node_type - if (!skip_ci && is_docs_only_build != 1) { - node(node_type) { - ws("workspace/exec_${env.EXECUTOR_NUMBER}/tvm/ut-python-gpu") { - // NOTE: if exception happens, it will be caught outside - init_git() - docker_init(ci_gpu) - timeout(time: max_time, unit: 'MINUTES') { - withEnv([ - 'PLATFORM=gpu', - 'TEST_STEP_NAME=unittest: GPU', - 'TVM_NUM_SHARDS=2', - 'TVM_SHARD_INDEX=1', "SKIP_SLOW_TESTS=${skip_slow_tests}"], { sh( script: "./${jenkins_scripts_root}/s3.py --action download --bucket ${s3_bucket} --prefix ${s3_prefix}/gpu", @@ -616,14 +563,13 @@ def shard_run_unittest_GPU_2_of_2(node_type) { } echo 'End running on node_type ' + node_type } else { - Utils.markStageSkippedForConditional('unittest: GPU 2 of 2') + Utils.markStageSkippedForConditional('unittest: GPU') } } - -def shard_run_docs_GPU_1_of_1(node_type) { +def run_docs_GPU(node_type) { echo 'Begin running on node_type ' + node_type if (!skip_ci) { node(node_type) { @@ -635,8 +581,6 @@ def shard_run_docs_GPU_1_of_1(node_type) { withEnv([ 'PLATFORM=gpu', 'TEST_STEP_NAME=docs: GPU', - 'TVM_NUM_SHARDS=1', - 'TVM_SHARD_INDEX=0', "SKIP_SLOW_TESTS=${skip_slow_tests}"], { sh( script: "./${jenkins_scripts_root}/s3.py --action download --bucket ${s3_bucket} --prefix ${s3_prefix}/gpu", @@ -674,7 +618,7 @@ def shard_run_docs_GPU_1_of_1(node_type) { } echo 'End running on node_type ' + node_type } else { - Utils.markStageSkippedForConditional('docs: GPU 1 of 1') + Utils.markStageSkippedForConditional('docs: GPU') } } @@ -686,27 +630,9 @@ def test() { SKIP_SLOW_TESTS = "${skip_slow_tests}" } parallel( - 'unittest: GPU 1 of 2': { - try { - shard_run_unittest_GPU_1_of_2('GPU-SPOT') - } catch (Throwable ex) { - echo 'Exception during SPOT run ' + ex.toString() - if (is_last_build()) { - // retry if at last build - // mark the current stage as success - // and try again via on demand node - echo 'Retry on-demand given it is last build' - currentBuild.result = 'SUCCESS' - shard_run_unittest_GPU_1_of_2('GPU') - } else { - echo 'Exit since it is not last build' - throw ex - } - } - }, - 'unittest: GPU 2 of 2': { + 'unittest: GPU': { try { - shard_run_unittest_GPU_2_of_2('GPU-SPOT') + run_unittest_GPU('GPU-SPOT') } catch (Throwable ex) { echo 'Exception during SPOT run ' + ex.toString() if (is_last_build()) { @@ -715,16 +641,16 @@ def test() { // and try again via on demand node echo 'Retry on-demand given it is last build' currentBuild.result = 'SUCCESS' - shard_run_unittest_GPU_2_of_2('GPU') + run_unittest_GPU('GPU') } else { echo 'Exit since it is not last build' throw ex } } }, - 'docs: GPU 1 of 1': { + 'docs: GPU': { try { - shard_run_docs_GPU_1_of_1('GPU-SPOT') + run_docs_GPU('GPU-SPOT') } catch (Throwable ex) { echo 'Exception during SPOT run ' + ex.toString() if (is_last_build()) { @@ -733,7 +659,7 @@ def test() { // and try again via on demand node echo 'Retry on-demand given it is last build' currentBuild.result = 'SUCCESS' - shard_run_docs_GPU_1_of_1('GPU') + run_docs_GPU('GPU') } else { echo 'Exit since it is not last build' throw ex diff --git a/ci/jenkins/generated/wasm_jenkinsfile.groovy b/ci/jenkins/generated/wasm_jenkinsfile.groovy index ab64444e4a..b6946a40fe 100644 --- a/ci/jenkins/generated/wasm_jenkinsfile.groovy +++ b/ci/jenkins/generated/wasm_jenkinsfile.groovy @@ -60,7 +60,7 @@ // 'python3 jenkins/generate.py' // Note: This timestamp is here to ensure that updates to the Jenkinsfile are // always rebased on main before merging: -// Generated at 2026-06-23T03:28:14.550555 +// Generated at 2026-07-04T23:53:56.918767 import org.jenkinsci.plugins.pipeline.modeldefinition.Utils // These are set at runtime from data in ci/jenkins/docker-images.yml, update @@ -97,7 +97,7 @@ properties([ upstream_revision = null // command to start a docker container -docker_run = 'docker/bash.sh --env CI --env PLATFORM --env TVM_SHARD_INDEX --env TVM_NUM_SHARDS --env RUN_DISPLAY_URL --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' +docker_run = 'docker/bash.sh --env CI --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' docker_build = 'docker/build.sh' // timeout in minutes max_time = 180 diff --git a/ci/jenkins/templates/cpu_jenkinsfile.groovy.j2 b/ci/jenkins/templates/cpu_jenkinsfile.groovy.j2 index d2e479d5e8..75b43c52a8 100644 --- a/ci/jenkins/templates/cpu_jenkinsfile.groovy.j2 +++ b/ci/jenkins/templates/cpu_jenkinsfile.groovy.j2 @@ -37,11 +37,10 @@ {% set test_method_names = [] %} -{% call(shard_index, num_shards) m.sharded_test_step( +{% call m.test_step( name="unittest: CPU", ws="tvm/ut-python-cpu", platform="cpu", - num_shards=2, docker_image="ci_cpu", test_method_names=test_method_names, ) %} diff --git a/ci/jenkins/templates/gpu_jenkinsfile.groovy.j2 b/ci/jenkins/templates/gpu_jenkinsfile.groovy.j2 index 7ab5256419..33e5a8aed5 100644 --- a/ci/jenkins/templates/gpu_jenkinsfile.groovy.j2 +++ b/ci/jenkins/templates/gpu_jenkinsfile.groovy.j2 @@ -38,9 +38,8 @@ {% set test_method_names = [] %} -{% call(shard_index, num_shards) m.sharded_test_step( +{% call m.test_step( name="unittest: GPU", - num_shards=2, ws="tvm/ut-python-gpu", platform="gpu", docker_image="ci_gpu", @@ -48,12 +47,10 @@ ) %} {{ m.download_artifacts(tag='gpu') }} ci_setup(ci_gpu) - {% if shard_index == 2 or num_shards < 2 %} sh ( script: "${docker_run} ${ci_gpu} ./tests/scripts/task_java_unittest.sh", label: 'Run Java unit tests', ) - {% endif %} sh ( script: "${docker_run} ${ci_gpu} ./tests/scripts/task_python_unittest_gpuonly.sh", label: 'Run Python GPU unit tests', @@ -65,9 +62,8 @@ {% endcall %} -{% call(shard_index, num_shards) m.sharded_test_step( +{% call m.test_step( name="docs: GPU", - num_shards=1, ws="tvm/docs-python-gpu", platform="gpu", docker_image="ci_gpu", diff --git a/ci/jenkins/templates/utils/base.groovy.j2 b/ci/jenkins/templates/utils/base.groovy.j2 index 813ceee886..a8fda460b7 100644 --- a/ci/jenkins/templates/utils/base.groovy.j2 +++ b/ci/jenkins/templates/utils/base.groovy.j2 @@ -82,7 +82,7 @@ properties([ upstream_revision = null // command to start a docker container -docker_run = 'docker/bash.sh --env CI --env PLATFORM --env TVM_SHARD_INDEX --env TVM_NUM_SHARDS --env RUN_DISPLAY_URL --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' +docker_run = 'docker/bash.sh --env CI --env PLATFORM --env SKIP_SLOW_TESTS --env TEST_STEP_NAME' docker_build = 'docker/build.sh' // timeout in minutes max_time = 180 diff --git a/ci/jenkins/templates/utils/macros.j2 b/ci/jenkins/templates/utils/macros.j2 index b1bd3679ac..bd0e0d4ee4 100644 --- a/ci/jenkins/templates/utils/macros.j2 +++ b/ci/jenkins/templates/utils/macros.j2 @@ -26,10 +26,8 @@ sh( ) {% endmacro %} -{% macro sharded_test_step(name, num_shards, ws, docker_image, platform, test_method_names, condition="!skip_ci && is_docs_only_build != 1") %} - -{% for shard_index in range(1, num_shards + 1) %} -{% set method_name = "shard_run_" + name.replace(":", "").replace(" ", "-").replace("-", "_") + "_" + shard_index|string + "_of_" + num_shards|string %} +{% macro test_step(name, ws, docker_image, platform, test_method_names, condition="!skip_ci && is_docs_only_build != 1") %} +{% set method_name = "run_" + name.replace(":", "").replace(" ", "-").replace("-", "_") %} {% set test_dir_name = name.replace(":", "").replace(" ", "-").replace("-", "_")|string %} def {{ method_name }}(node_type) { echo 'Begin running on node_type ' + node_type @@ -43,10 +41,8 @@ def {{ method_name }}(node_type) { withEnv([ 'PLATFORM={{ platform }}', 'TEST_STEP_NAME={{ name }}', - 'TVM_NUM_SHARDS={{ num_shards }}', - 'TVM_SHARD_INDEX={{ shard_index - 1 }}', "SKIP_SLOW_TESTS=${skip_slow_tests}"], { - {{ caller(shard_index, num_shards) | trim | indent(width=12) }} + {{ caller() | trim | indent(width=12) }} }) } // only run upload if things are successful @@ -60,12 +56,11 @@ def {{ method_name }}(node_type) { } echo 'End running on node_type ' + node_type } else { - Utils.markStageSkippedForConditional('{{ name }} {{ shard_index }} of {{ num_shards }}') + Utils.markStageSkippedForConditional('{{ name }}') } } -{% set _ = test_method_names.append((name + " " + shard_index|string + " of " + num_shards|string, method_name)) %} +{% set _ = test_method_names.append((name, method_name)) %} -{% endfor %} {% endmacro %} {% macro invoke_build(name, condition, node, docker_image, ws, platform) %} diff --git a/ci/scripts/jenkins/pytest_ids.py b/ci/scripts/jenkins/pytest_ids.py deleted file mode 100755 index 548069e855..0000000000 --- a/ci/scripts/jenkins/pytest_ids.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -import argparse -import io -from contextlib import redirect_stdout - -import pytest - - -class NodeidsCollector: - def pytest_collection_modifyitems(self, items): - self.nodeids = [item.nodeid for item in items] - - -def main(folder): - collector = NodeidsCollector() - f = io.StringIO() - with redirect_stdout(f): - pytest.main(["-qq", "--collect-only", folder], plugins=[collector]) - for nodeid in collector.nodeids: - print(nodeid) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="List pytest nodeids for a folder") - parser.add_argument("--folder", required=True, help="test folder to inspect") - args = parser.parse_args() - main(args.folder) diff --git a/ci/scripts/jenkins/pytest_wrapper.py b/ci/scripts/jenkins/pytest_wrapper.py deleted file mode 100755 index a667810293..0000000000 --- a/ci/scripts/jenkins/pytest_wrapper.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python3 -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ruff: noqa: E501 -import argparse -import logging -import os -import textwrap -import urllib.parse -from pathlib import Path - -import junitparser -from cmd_utils import init_log - -REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent - - -def lstrip(s: str, prefix: str) -> str: - if s.startswith(prefix): - s = s[len(prefix) :] - return s - - -def classname_to_file(classname: str) -> str: - classname = lstrip(classname, "cython.") - classname = lstrip(classname, "ctypes.") - return classname.replace(".", "/") + ".py" - - -def failed_test_ids() -> list[str]: - FAILURE_TYPES = (junitparser.Failure, junitparser.Error) - junit_dir = REPO_ROOT / "build" / "pytest-results" - failed_node_ids = [] - for junit in junit_dir.glob("*.xml"): - xml = junitparser.JUnitXml.fromfile(str(junit)) - for suite in xml: - # handle suites - for case in suite: - if case.result is None: - logging.warn(f"Incorrectly formatted JUnit found, result was None on {case}") - continue - - if len(case.result) > 0 and isinstance(case.result[0], FAILURE_TYPES): - node_id = classname_to_file(case.classname) + "::" + case.name - failed_node_ids.append(node_id) - - return list(set(failed_node_ids)) - - -def repro_command(build_type: str, failed_node_ids: list[str]) -> str | None: - """ - Parse available JUnit XML files and output a command that users can run to - reproduce CI failures locally - """ - test_args = [f"--tests {node_id}" for node_id in failed_node_ids] - test_args_str = " ".join(test_args) - return f"python3 tests/scripts/ci.py {build_type} {test_args_str}" - - -def make_issue_url(failed_node_ids: list[str]) -> str: - names = [f"`{node_id}`" for node_id in failed_node_ids] - run_url = os.getenv("RUN_DISPLAY_URL", "<insert run URL>") - test_bullets = [f" - `{node_id}`" for node_id in failed_node_ids] - params = { - "labels": "test: flaky", - "title": "[Flaky Test] " + ", ".join(names), - "body": textwrap.dedent( - """ - These tests were found to be flaky (intermittently failing on `main` or failed in a PR with unrelated changes). See [the docs](https://github.com/apache/tvm/blob/main/docs/contribute/ci.rst#handling-flaky-failures) for details. - - ### Tests(s)\n - """ - ) - + "\n".join(test_bullets) - + f"\n\n### Jenkins Links\n\n - {run_url}", - } - return "https://github.com/apache/tvm/issues/new?" + urllib.parse.urlencode(params) - - -def show_failure_help(failed_suites: list[str]) -> None: - failed_node_ids = failed_test_ids() - - if len(failed_node_ids) == 0: - return - - build_type = os.getenv("PLATFORM") - - if build_type is None: - raise RuntimeError("build type was None, cannot show command") - - repro = repro_command(build_type=build_type, failed_node_ids=failed_node_ids) - if repro is None: - print("No test failures detected") - return - - print(f"Report flaky test shortcut: {make_issue_url(failed_node_ids)}") - print("=============================== PYTEST FAILURES ================================") - print( - "These pytest suites failed to execute. The results can be found in the " - "Jenkins 'Tests' tab or by scrolling up through the raw logs here. " - "If there is no test listed below, the failure likely came from a segmentation " - "fault which you can find in the logs above.\n" - ) - if failed_suites is not None and len(failed_suites) > 0: - print("\n".join([f" - {suite}" for suite in failed_suites])) - print("") - - print("You can reproduce these specific failures locally with this command:\n") - print(textwrap.indent(repro, prefix=" ")) - print("") - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Print information about a failed pytest run") - args, other = parser.parse_known_args() - init_log() - - try: - show_failure_help(failed_suites=other) - except Exception as e: - # This script shouldn't ever introduce failures since it's just there to - # add extra information, so ignore any errors - logging.exception(e) diff --git a/conftest.py b/conftest.py index dd32937620..dd294ea548 100644 --- a/conftest.py +++ b/conftest.py @@ -14,8 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -# ruff: noqa: E501 -import hashlib import os import sys from pathlib import Path @@ -25,58 +23,6 @@ IS_IN_CI = os.getenv("CI", "") == "true" REPO_ROOT = Path(__file__).resolve().parent -# These are long running tests (manually curated and extracted from CI logs) -# that should be allocated to test shards in a round-robin fashion. These are -# taken from the 20 (arbitrary number) of tests as from -# https://ci.tlcpack.ai/job/tvm/job/main/2907/testReport -_slowest_tests = [] -HARDCODED_ALLOCATIONS = {} -for idx, test in enumerate(_slowest_tests): - HARDCODED_ALLOCATIONS[test] = idx - -# These rely on running on the same node to pass successfully -FIXED_ALLOCATION_PREFIXES = { - "tests/python/testing/test_tvm_testing_features.py": 0, -} - - -def find_shard_index(nodeid: str, num_shards: int) -> int: - """ - Return the index of the shard that should run this test - """ - for prefix, target_shard_idx in FIXED_ALLOCATION_PREFIXES.items(): - if nodeid.startswith(prefix): - if target_shard_idx >= num_shards: - raise RuntimeError( - f"Cannot collect sharded tests, {nodeid} has hardcoded shard index {target_shard_idx} among only {num_shards} shards" - ) - return target_shard_idx - - if nodeid in HARDCODED_ALLOCATIONS: - hash = HARDCODED_ALLOCATIONS[nodeid] - else: - hash = hashlib.md5(nodeid.encode()) - hash = int(hash.hexdigest(), 16) - - return hash % num_shards - - -def pytest_collection_modifyitems(config, items): - if not all(k in os.environ for k in ["CI", "TVM_NUM_SHARDS", "TVM_SHARD_INDEX"]): - # Only apportion tests if in CI and in a job that is set up for it - return - - num_shards = int(os.environ["TVM_NUM_SHARDS"]) - shard_index = int(os.environ["TVM_SHARD_INDEX"]) - - print(f"Marking tests for shard {shard_index} of {num_shards}") - items_copy = list(items) - for item in items_copy: - item_shard_index = find_shard_index(item.nodeid, num_shards=num_shards) - if item_shard_index != shard_index: - items.remove(item) - - def pytest_sessionstart(): if IS_IN_CI: hook_script_dir = REPO_ROOT / "tests" / "scripts" / "request_hook" diff --git a/docker/install/ubuntu_install_python_package.sh b/docker/install/ubuntu_install_python_package.sh index d07f19ed3c..4445dfd83e 100755 --- a/docker/install/ubuntu_install_python_package.sh +++ b/docker/install/ubuntu_install_python_package.sh @@ -32,13 +32,11 @@ uv pip install --upgrade \ Pillow==12.1.1 \ "psutil~=7.0" \ "pytest~=8.3" \ - "pytest-profiling~=1.8" \ "pytest-xdist~=3.6" \ pytest-rerunfailures==16.1 \ "requests~=2.32" \ "scipy~=1.13" \ "Jinja2~=3.1" \ - junitparser==4.0.2 \ "six~=1.17" \ "tornado~=6.4" \ "ml_dtypes~=0.5" \ diff --git a/tests/scripts/task_python_unittest.sh b/tests/scripts/task_python_unittest.sh index 14675bf8a0..30e5bb2bf3 100755 --- a/tests/scripts/task_python_unittest.sh +++ b/tests/scripts/task_python_unittest.sh @@ -32,12 +32,11 @@ uv pip install -v --target=python ./3rdparty/tvm-ffi/ if [ -z "${TVM_UNITTEST_TESTSUITE_NAME:-}" ]; then TVM_UNITTEST_TESTSUITE_NAME=python-unittest fi -PYTEST_SHARD_SUFFIX="${TVM_SHARD_INDEX:+-shard-${TVM_SHARD_INDEX}}" # First run the minimal platform test. A GPU-only run can select no tests here. python3 -m pytest -n auto \ - -o "junit_suite_name=${TVM_UNITTEST_TESTSUITE_NAME}-platform-minimal-test${PYTEST_SHARD_SUFFIX}" \ - "--junit-xml=build/pytest-results/${TVM_UNITTEST_TESTSUITE_NAME}-platform-minimal-test${PYTEST_SHARD_SUFFIX}.xml" \ + -o "junit_suite_name=${TVM_UNITTEST_TESTSUITE_NAME}-platform-minimal-test" \ + "--junit-xml=build/pytest-results/${TVM_UNITTEST_TESTSUITE_NAME}-platform-minimal-test.xml" \ --junit-prefix=cython \ tests/python/all-platform-minimal-test || [ "$?" -eq 5 ] @@ -73,7 +72,7 @@ for TEST_FILE in "${TEST_FILES[@]}"; do done python3 -m pytest -n auto --dist=loadgroup \ - -o "junit_suite_name=${TVM_UNITTEST_TESTSUITE_NAME}${PYTEST_SHARD_SUFFIX}" \ - "--junit-xml=build/pytest-results/${TVM_UNITTEST_TESTSUITE_NAME}${PYTEST_SHARD_SUFFIX}.xml" \ + -o "junit_suite_name=${TVM_UNITTEST_TESTSUITE_NAME}" \ + "--junit-xml=build/pytest-results/${TVM_UNITTEST_TESTSUITE_NAME}.xml" \ --junit-prefix=cython \ "${PYTEST_TARGETS[@]}" diff --git a/tests/scripts/task_python_unittest_gpuonly.sh b/tests/scripts/task_python_unittest_gpuonly.sh index 9753a1a8a4..6bbd85dc10 100755 --- a/tests/scripts/task_python_unittest_gpuonly.sh +++ b/tests/scripts/task_python_unittest_gpuonly.sh @@ -36,10 +36,9 @@ export TVM_UNITTEST_TESTSUITE_NAME=python-codegen-vulkan export PYTHONPATH="$(pwd)/python" export PYTEST_ADDOPTS="-s -vv ${CI_PYTEST_ADD_OPTIONS:-} ${PYTEST_ADDOPTS:-}" mkdir -p build/pytest-results -PYTEST_SHARD_SUFFIX="${TVM_SHARD_INDEX:+-shard-${TVM_SHARD_INDEX}}" python3 -m pytest -n auto \ - -o "junit_suite_name=${TVM_UNITTEST_TESTSUITE_NAME}${PYTEST_SHARD_SUFFIX}" \ - "--junit-xml=build/pytest-results/${TVM_UNITTEST_TESTSUITE_NAME}${PYTEST_SHARD_SUFFIX}.xml" \ + -o "junit_suite_name=${TVM_UNITTEST_TESTSUITE_NAME}" \ + "--junit-xml=build/pytest-results/${TVM_UNITTEST_TESTSUITE_NAME}.xml" \ --junit-prefix=cython \ tests/python/codegen/test_target_codegen_vulkan.py
