This is an automated email from the ASF dual-hosted git repository. tqchen pushed a commit to branch jenkins-cleanup-trusted-main in repository https://gitbox.apache.org/repos/asf/tvm.git
commit 6e02972f256ca9f6f998ed64e5384038ab549a9e Author: Tianqi Chen <[email protected]> AuthorDate: Thu Oct 15 11:39:52 2026 +0000 [CI] Retain script and bot coverage with a minimal inline fixture --- tests/python/ci/jenkins_loader_test.groovy | 84 -------- tests/python/ci/sample_prs/pr10786.json | 130 ------------ tests/python/ci/test_ci.py | 55 ++++- tests/python/ci/test_jenkins_helpers.py | 322 ----------------------------- tests/python/ci/test_jenkins_loader.py | 37 ---- tests/python/ci/test_tvmbot.py | 106 ++++++++-- tests/python/ci/test_utils.py | 80 ------- 7 files changed, 144 insertions(+), 670 deletions(-) diff --git a/tests/python/ci/jenkins_loader_test.groovy b/tests/python/ci/jenkins_loader_test.groovy deleted file mode 100644 index 1b2fe16e2e..0000000000 --- a/tests/python/ci/jenkins_loader_test.groovy +++ /dev/null @@ -1,84 +0,0 @@ -// 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 org.codehaus.groovy.control.CompilerConfiguration - -class LoaderPipeline extends Script { - Object run() {} - Object evaluate(String source) { - state.evaluated << source - if (state.evaluationFailure) throw state.evaluationFailure - return 'loaded helper' - } - def readTrusted(String path) { - assert path == 'ci/jenkins/generated/common.groovy' - if (state.readFailure) throw state.readFailure - return 'selected revision helper' - } - def node(String label, Closure body) { - throw new AssertionError('loader allocated a node') - } - def sh(Map options) { - throw new AssertionError('loader ran a shell command') - } - -} - -def loader = new GroovyClassLoader(getClass().classLoader) -def abort = loader.parseClass('package hudson; class AbortException extends IOException { AbortException(String message) { super(message) } }') -def config = new CompilerConfiguration(scriptBaseClass: 'LoaderPipeline') -def helper = 'ci/jenkins/generated/common.groovy' -def modified = { abort.newInstance("‘${helper}’ has been modified in an untrusted revision".toString()) } -def missing = { new FileNotFoundException(helper) } -def cases = [ - [name: 'unchanged PR'], - [name: 'trusted named branch', branch: true], - [name: 'modified PR', readFailure: modified()], - [name: 'deleted PR lightweight', readFailure: missing()], - [name: 'deleted PR heavyweight', readFailure: abort.newInstance("/workspace/trusted/${helper} not found".toString())], - [name: 'release target PR', readFailure: modified(), target: 'release'], - [name: 'missing named branch', branch: true, readFailure: missing()], - [name: 'rejected named branch', branch: true, readFailure: modified()], - [name: 'unrelated abort', readFailure: abort.newInstance('controller offline')], - [name: 'unrelated missing file', readFailure: new FileNotFoundException('other/file')], - [name: 'read network failure', readFailure: new IOException('network')], - [name: 'interrupted read', readFailure: new InterruptedException('cancelled')], - [name: 'selected evaluation error', evaluationFailure: new IllegalStateException('bad helper')], -] -int count = 0 -args.each { filename -> - def fullSource = new File(filename).text - def source = fullSource.substring(0, fullSource.indexOf('\nci.start()')) - cases.each { scenario -> - def state = [nodes: 0, fetches: 0, evaluated: []] + scenario - def binding = new Binding([state: state, env: [CHANGE_ID: scenario.branch ? null : '123', CHANGE_TARGET: scenario.target ?: 'main']]) - Throwable failure - try { - new GroovyShell(loader, binding, config).evaluate(source) - } catch (Throwable caught) { - failure = caught - } - def expected = scenario.readFailure ?: scenario.evaluationFailure - assert failure.is(expected): "${filename}: ${scenario.name}: ${failure} != ${expected}" - assert state.nodes == 0: scenario.name - assert state.fetches == 0: scenario.name - def shouldEvaluate = !scenario.readFailure - assert state.evaluated == (shouldEvaluate ? ['selected revision helper'] : []): scenario.name - count++ - } -} -println("${count} native loader cases passed") diff --git a/tests/python/ci/sample_prs/pr10786.json b/tests/python/ci/sample_prs/pr10786.json deleted file mode 100644 index 5e978ffda1..0000000000 --- a/tests/python/ci/sample_prs/pr10786.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "title": "[Hexagon] 2-d allocation cleanup", - "body": - "- Added device validity check in allocation. HexagonDeviceAPI should only be called for CPU/Hexagon types.\r\n\r\n- Check for \"global.vtcm\" scope instead of \"vtcm\". The ccope of N-d allocations produced by `LowerVtcmAlloc` should be `\"global.vtcm\"`. The previous check allowed unsupported scope such as `\"local.vtcm\"`.\r\n\r\n- Remove `vtcmallocs` entry after calling free.\n\n\nThanks for contributing to TVM! Please refer to guideline https://tvm.apache.org/docs/contribu [...] - "state": "OPEN", - "author": { - "login": "abc" - }, - "comments": { - "pageInfo": { - "hasPreviousPage": false - }, - "nodes": [] - }, - "authorCommits": { - "nodes": [ - { - "commit": { - "authors": { - "nodes": [ - { - "name": "Eric Lunderberg", - "email": "[email protected]" - }, - { - "name": "Adam Straw", - "email": "[email protected]" - } - ] - } - } - } - ] - }, - "commits": { - "nodes": [ - { - "commit": { - "oid": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd", - "statusCheckRollup": { - "contexts": { - "pageInfo": { - "hasNextPage": false - }, - "nodes": [ - { - "name": "MacOS", - "checkSuite": { - "workflowRun": { - "workflow": { - "name": "CI" - } - } - }, - "status": "COMPLETED", - "conclusion": "SUCCESS", - "url": "https://github.com/apache/tvm/runs/5694945392" - }, - { - "name": "cc-reviewers", - "checkSuite": { - "workflowRun": { - "workflow": { - "name": "PR" - } - } - }, - "status": "COMPLETED", - "conclusion": "SUCCESS", - "url": "https://github.com/apache/tvm/runs/5694945029" - }, - { - "name": "tag-teams", - "checkSuite": { - "workflowRun": { - "workflow": { - "name": "Teams" - } - } - }, - "status": "COMPLETED", - "conclusion": "SUCCESS", - "url": "https://github.com/apache/tvm/runs/5694945030" - }, - { - "name": "Windows", - "checkSuite": { - "workflowRun": { - "workflow": { - "name": "CI" - } - } - }, - "status": "COMPLETED", - "conclusion": "SUCCESS", - "url": "https://github.com/apache/tvm/runs/5694945524" - }, - { - "state": "SUCCESS", - "context": "tvm-ci/pr-head", - "targetUrl": "https://ci.tlcpack.ai/job/tvm/job/PR-10786/1/display/redirect" - } - ] - } - } - } - } - ] - }, - "reviewDecision": "APPROVED", - "reviews": { - "pageInfo": { - "hasPreviousPage": false - }, - "nodes": [ - { - "body": "@tvm-bot merge", - "updatedAt": "2022-03-25T22:13:50Z", - "authorCanPushToRepository": true, - "commit": { - "oid": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd" - }, - "author": { - "login": "kparzysz-quic" - }, - "state": "APPROVED" - } - ] - } -} diff --git a/tests/python/ci/test_ci.py b/tests/python/ci/test_ci.py index 7144fee3d1..4c146dd974 100644 --- a/tests/python/ci/test_ci.py +++ b/tests/python/ci/test_ci.py @@ -18,12 +18,65 @@ """Test various CI scripts and GitHub Actions workflows""" import json +import os import subprocess from pathlib import Path +from typing import Any import pytest -from .test_utils import JENKINS_SCRIPT_ROOT, TempGit, run_script +JENKINS_SCRIPT_ROOT = Path(__file__).resolve().parents[3] / "ci" / "jenkins" / "scripts" + + +class TempGit: + """ + A wrapper to run commands in a directory (specifically for use in CI tests) + """ + + def __init__(self, cwd): + self.cwd = cwd + # Jenkins git is too old and doesn't have 'git init --initial-branch', + # so init and checkout need to be separate steps + self.run("init", stderr=subprocess.PIPE, stdout=subprocess.PIPE) + self.run("checkout", "-b", "main", stderr=subprocess.PIPE) + self.run("remote", "add", "origin", "https://github.com/apache/tvm.git") + + def run(self, *args, **kwargs): + """ + Run a git command based on *args + """ + proc = subprocess.run( + ["git"] + list(args), encoding="utf-8", cwd=self.cwd, check=False, **kwargs + ) + if proc.returncode != 0: + raise RuntimeError(f"git command failed: '{args}'") + + return proc + + +def run_script(command: list[Any], check: bool = True, **kwargs): + """ + Wrapper to run a script and print its output if there was an error + """ + command = [str(c) for c in command] + kwargs_to_send = { + "stdout": subprocess.PIPE, + "stderr": subprocess.PIPE, + "encoding": "utf-8", + } + env = kwargs.pop("env", None) + if env is not None: + kwargs_to_send["env"] = {**os.environ, **env} + kwargs_to_send.update(kwargs) + proc = subprocess.run( + command, + check=False, + **kwargs_to_send, + ) + if check and proc.returncode != 0: + raise RuntimeError(f"Process failed:\nstdout:\n{proc.stdout}\n\nstderr:\n{proc.stderr}") + + return proc @pytest.fixture diff --git a/tests/python/ci/test_jenkins_helpers.py b/tests/python/ci/test_jenkins_helpers.py deleted file mode 100644 index ae28ea97f7..0000000000 --- a/tests/python/ci/test_jenkins_helpers.py +++ /dev/null @@ -1,322 +0,0 @@ -# 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. -"""Host-side Jenkins commands preserve retries, paths and build configuration.""" - -import importlib.util -import json -import os -import shutil -import subprocess -import sys - -import pytest - -from .test_utils import JENKINS_SCRIPT_ROOT, REPO_ROOT, TempGit - - -def load_s3(script_root=JENKINS_SCRIPT_ROOT): - spec = importlib.util.spec_from_file_location("jenkins_s3", script_root / "s3.py") - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def executable(path, body): - path.write_text("#!/usr/bin/env python3\n" + body) - path.chmod(0o755) - - [email protected]("failures", [2, 3]) -def test_s3_transfer_retries(tmp_path, monkeypatch, failures): - script_root = JENKINS_SCRIPT_ROOT - # Run the real retry.sh with fake AWS and sleep, without network or delays. - executable( - tmp_path / "aws", - """ -import json, os, pathlib, sys -log = pathlib.Path(os.environ["AWS_LOG"]) -with log.open("a") as out: - out.write(json.dumps(sys.argv[1:]) + "\\n") -attempt = len(log.read_text().splitlines()) -print("download: s3://bucket/file to file name") -sys.exit(1 if attempt <= int(os.environ["FAILURES"]) else 0) -""", - ) - executable( - tmp_path / "sleep", - """ -import os, pathlib, sys -with pathlib.Path(os.environ["SLEEP_LOG"]).open("a") as out: - out.write(sys.argv[1] + "\\n") -""", - ) - monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}") - monkeypatch.setenv("FAILURES", str(failures)) - monkeypatch.setenv("SLEEP_LOG", str(tmp_path / "sleeps")) - monkeypatch.syspath_prepend(str(script_root)) - destination = "file name" - for transfer in range(2): - log = tmp_path / f"aws-{transfer}" - monkeypatch.setenv("AWS_LOG", str(log)) - s3 = load_s3(script_root) - if failures == 3: - with pytest.raises(subprocess.CalledProcessError): - s3.s3("s3://bucket/file", destination, False) - else: - assert s3.parse_output_files(s3.s3("s3://bucket/file", destination, False)) - commands = [json.loads(line) for line in log.read_text().splitlines()] - assert commands == [["s3", "cp", "--no-progress", "s3://bucket/file", destination]] * 3 - delays = (tmp_path / "sleeps").read_text().splitlines() - assert len(delays) == 4 - assert all(30 <= int(delay) <= 200 for delay in delays) - - -def test_build_environment_and_target(tmp_path): - # Run the entry point in a minimal checkout, capturing the tool boundary. - script = tmp_path / "tests/scripts/task_build.py" - script.parent.mkdir(parents=True) - shutil.copy(REPO_ROOT / "tests/scripts/task_build.py", script) - (tmp_path / "build").mkdir() - ffi_path = tmp_path / "3rdparty/tvm-ffi" - ffi_path.mkdir(parents=True) - ffi_repo = TempGit(ffi_path) - ffi_repo.run("config", "user.name", "CI Test") - ffi_repo.run("config", "user.email", "[email protected]") - ffi_repo.run("commit", "--allow-empty", "-m", "FFI fixture") - ffi_commit = ffi_repo.run("rev-parse", "HEAD", stdout=subprocess.PIPE).stdout.strip() - ffi_marker = tmp_path / "python/tvm_ffi/.source-commit" - ffi_marker.parent.mkdir(parents=True) - ffi_marker.write_text(f"{ffi_commit} {sys.implementation.cache_tag}\n") - executable( - tmp_path / "cmake", - """ -import json, os, pathlib, sys -with pathlib.Path(os.environ["COMMAND_LOG"]).open("a") as out: - entry = [sys.argv[1:], os.getcwd(), os.environ["SCCACHE_BUCKET"], os.environ["CC"]] - out.write(json.dumps(entry) + "\\n") -""", - ) - executable(tmp_path / "sccache", "pass\n") - log = tmp_path / "commands" - env = dict( - os.environ, - PATH=f"{tmp_path}:{os.environ['PATH']}", - COMMAND_LOG=str(log), - AWS_ACCESS_KEY_ID="test", - CI_NUM_EXECUTORS="2", - VERBOSE="false", - ) - subprocess.run( - [ - sys.executable, - script, - "--debug", - "--cmake-target", - "cpptest", - "--sccache-bucket", - "test-cache", - ], - cwd=tmp_path, - env=env, - check=True, - ) - configure, build = [json.loads(line) for line in log.read_text().splitlines()] - assert configure == [ - ["-GNinja", "-DCMAKE_BUILD_TYPE=Debug", ".."], - str(tmp_path / "build"), - "test-cache", - "/opt/sccache/cc", - ] - assert build[0][:4] == ["--build", ".", "--", "cpptest"] - assert build[0][4].startswith("-j") and int(build[0][4][2:]) >= 1 - assert len(build[0]) == 5 - assert build[1:] == configure[1:] - - [email protected] -def artifact_cli(tmp_path): - script_root = tmp_path / "ci/jenkins/scripts" - script_root.mkdir(parents=True) - for name in ("s3.py", "retry.sh"): - shutil.copy(JENKINS_SCRIPT_ROOT / name, script_root / name) - registry = { - "wheels": ["build/dist/*.whl", "build/dist/a.whl", "./build/dist/a.whl"], - "extra": ["build/exact.so", "build/dir", "build/missing.so", "build/no-match/*.so"], - } - (script_root.parent / "artifacts.py").write_text("ARTIFACTS = " + repr(registry)) - for name in ( - "build/dist/b space.whl", - "build/dist/a.whl", - "build/exact.so", - "build/dir/nested", - ): - path = tmp_path / name - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text("artifact") - binary_dir = tmp_path / "bin" - binary_dir.mkdir() - executable( - binary_dir / "aws", - """ -import json, os, pathlib, sys -with pathlib.Path(os.environ["AWS_LOG"]).open("a") as out: - out.write(json.dumps(sys.argv[1:]) + "\\n") -if sys.argv[-1] == ".": - pathlib.Path("downloaded file").write_text("downloaded") - print("download: s3://bucket/prefix/downloaded file to downloaded file") -""", - ) - log = tmp_path / "commands" - env = dict(os.environ, PATH=f"{binary_dir}:{os.environ['PATH']}", AWS_LOG=str(log)) - - def run(*options, action="upload"): - log.unlink(missing_ok=True) - result = subprocess.run( - [ - sys.executable, - script_root / "s3.py", - "--action", - action, - "--bucket", - "bucket", - "--prefix", - "/prefix/", - *options, - ], - cwd=tmp_path, - env=env, - text=True, - capture_output=True, - ) - commands = ( - [json.loads(line) for line in log.read_text().splitlines()] if log.exists() else [] - ) - return result, commands - - return run - - -def test_artifact_upload_aliases_and_globs(artifact_cli): - expected = [ - ["s3", "cp", "--no-progress", "--recursive", "build/dir", "s3://bucket/prefix/build/dir"], - ["s3", "cp", "--no-progress", "build/dist/a.whl", "s3://bucket/prefix/build/dist/a.whl"], - [ - "s3", - "cp", - "--no-progress", - "build/dist/b space.whl", - "s3://bucket/prefix/build/dist/b space.whl", - ], - ["s3", "cp", "--no-progress", "build/exact.so", "s3://bucket/prefix/build/exact.so"], - ] - for flags in ( - ["--artifacts", "wheels", "extra"], - ["--artifacts", "extra", "--artifacts", "wheels"], - ["--bundle", "wheels", "--bundle", "extra"], - ["--bundle", "extra", "wheels"], - ): - result, commands = artifact_cli(*flags) - assert result.returncode == 0, result.stderr - assert commands == expected - assert "No artifacts matched pattern: build/no-match/*.so" in result.stderr - assert "The path doesn't exist: build/missing.so" in result.stderr - - -def test_artifact_paths_alias_and_unmatched_pattern(artifact_cli): - for flag in ("--paths", "--items"): - result, commands = artifact_cli(flag, "build/dist/*.whl", "./build/dist/a.whl") - assert result.returncode == 0, result.stderr - assert [command[-2] for command in commands] == [ - "build/dist/a.whl", - "build/dist/b space.whl", - ] - result, commands = artifact_cli(flag, "absent/*.whl") - assert result.returncode == 0 - assert commands == [] - assert "No artifacts matched pattern: absent/*.whl" in result.stderr - - [email protected]("artifacts_flag", ["--artifacts", "--bundle"]) [email protected]("paths_flag", ["--paths", "--items"]) -def test_artifact_flags_are_mutually_exclusive(artifact_cli, artifacts_flag, paths_flag): - result, commands = artifact_cli(artifacts_flag, "wheels", paths_flag, "build/exact.so") - assert result.returncode == 2 - assert "not allowed with argument" in result.stderr - assert commands == [] - - -def test_artifact_download_copies_whole_prefix(artifact_cli, tmp_path): - result, commands = artifact_cli(action="download") - assert result.returncode == 0, result.stderr - assert commands == [["s3", "cp", "--no-progress", "--recursive", "s3://bucket/prefix", "."]] - downloaded = tmp_path / "downloaded file" - assert downloaded.read_text() == "downloaded" - assert downloaded.stat().st_mode & 0o111 - - [email protected]("absolute", [False, True]) -def test_legacy_artifact_literal_paths(artifact_cli, tmp_path, absolute): - outside = tmp_path.parent / "outside.so" - outside.write_text("artifact") - path = str(outside) if absolute else "../outside.so" - result, commands = artifact_cli("--items", path) - assert result.returncode == 0, result.stderr - assert commands == [["s3", "cp", "--no-progress", path, "s3://bucket/prefix/" + path]] - - -def test_recursive_artifact_overlap(artifact_cli): - result, commands = artifact_cli("--paths", "build/dir", "build/dir/*", "build/dir/nested") - assert result.returncode == 0, result.stderr - assert commands == [ - ["s3", "cp", "--no-progress", "--recursive", "build/dir", "s3://bucket/prefix/build/dir"] - ] - - -def test_artifact_empty_directory_and_unknown_name(artifact_cli, tmp_path): - (tmp_path / "empty").mkdir() - result, commands = artifact_cli("--paths", "empty") - assert result.returncode != 0 - assert "Cannot upload empty folder" in result.stderr - assert commands == [] - result, commands = artifact_cli("--artifacts", "unknown") - assert result.returncode != 0 - assert "Unknown artifact" in result.stderr - assert commands == [] - - -def test_current_pipeline_artifact_options(artifact_cli, tmp_path): - # c14bc80 passes repeated --bundle names; the new Jenkinsfiles use --artifacts. - registry = load_s3().load_artifacts() - (tmp_path / "ci/jenkins/artifacts.py").write_text("ARTIFACTS = " + repr(registry)) - for entries in registry.values(): - for item in entries: - path = tmp_path / item - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text("artifact") - for names in ( - ["tvm_lib", "cpptest"], - ["tvm_lib", "tvm_lib_gpu_extra", "cpptest"], - ["tvm_runtime"], - ): - old = [option for name in names for option in ("--bundle", name)] - old_result, old_commands = artifact_cli(*old) - new_result, new_commands = artifact_cli("--artifacts", *names) - assert old_result.returncode == new_result.returncode == 0 - assert old_commands == new_commands - assert [command[-2] for command in new_commands] == sorted( - {item for name in names for item in registry[name]} - ) diff --git a/tests/python/ci/test_jenkins_loader.py b/tests/python/ci/test_jenkins_loader.py deleted file mode 100644 index 8d73c97642..0000000000 --- a/tests/python/ci/test_jenkins_loader.py +++ /dev/null @@ -1,37 +0,0 @@ -# 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. -"""Exercise the native helper trust boundary without workspace fallback.""" - -import shutil -import subprocess -from pathlib import Path - -import pytest - -from .test_utils import REPO_ROOT - -PIPELINES = sorted((REPO_ROOT / "ci/jenkins/generated").glob("*_jenkinsfile.groovy")) - - -def test_native_loader_trust_boundary(): - groovy = shutil.which("groovy") - if groovy is None: - pytest.skip("Groovy is required for the executable pipeline loader checks") - subprocess.run( - [groovy, str(Path(__file__).with_name("jenkins_loader_test.groovy")), *PIPELINES], - check=True, - ) diff --git a/tests/python/ci/test_tvmbot.py b/tests/python/ci/test_tvmbot.py index 89ca2a1562..1e361d9438 100644 --- a/tests/python/ci/test_tvmbot.py +++ b/tests/python/ci/test_tvmbot.py @@ -14,11 +14,11 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -# ruff: noqa: E501 """ Test the @tvm-bot merge code """ +import copy import json import os from pathlib import Path @@ -26,20 +26,95 @@ from typing import Any import pytest -from .test_utils import GITHUB_SCRIPT_ROOT, TempGit, run_script - -SUCCESS_EXPECTED_OUTPUT = """ -Dry run, would have merged with url=pulls/10786/merge and data={ - "commit_title": "[Hexagon] 2-d allocation cleanup (#10786)", - "commit_message": "- Added device validity check in allocation. HexagonDeviceAPI should only be called for CPU/Hexagon types.\\n\\n- Check for \\"global.vtcm\\" scope instead of \\"vtcm\\". The ccope of N-d allocations produced by `LowerVtcmAlloc` should be `\\"global.vtcm\\"`. The previous check allowed unsupported scope such as `\\"local.vtcm\\"`.\\n\\n- Remove `vtcmallocs` entry after calling free.\\n\\nPreviously, the vtcm allocation map kept dangling pointers to `HexagonBuffer` [...] - "sha": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd", - "merge_method": "squash" +from .test_ci import TempGit, run_script + +GITHUB_SCRIPT_ROOT = Path(__file__).resolve().parents[3] / "ci" / "github" + +# Only the fields consumed by the bot and the mutations below are needed. +SAMPLE_PR = { + "title": "Example change", + "body": ( + "Example description.\r\n\r\ncc @example-user\n\n" + "Co-authored-by: Example Existing <[email protected]>" + ), + "state": "OPEN", + "author": {"login": "abc"}, + "authorCommits": { + "nodes": [ + { + "commit": { + "authors": { + "nodes": [ + {"name": "Example Author", "email": "[email protected]"}, + {"name": "Example Existing", "email": "[email protected]"}, + ] + } + } + } + ], + }, + "commits": { + "nodes": [ + { + "commit": { + "oid": "abc123", + "statusCheckRollup": { + "contexts": { + "nodes": [ + { + "context": "tvm-ci/pr-head", + "state": "SUCCESS", + "targetUrl": "https://example.com/build/1", + }, + { + "name": "Example check", + "conclusion": "SUCCESS", + "checkSuite": {"workflowRun": {"workflow": {"name": "CI"}}}, + "url": "https://example.com/check/1", + }, + { + "name": "Example automation", + "conclusion": "FAILURE", + "checkSuite": { + "workflowRun": { + "databaseId": 1, + "workflow": {"name": "Example automation"}, + } + }, + "url": "https://example.com/check/2", + }, + ] + } + }, + } + } + ], + }, + "reviews": { + "nodes": [ + { + "body": "Example review", + "authorCanPushToRepository": True, + "commit": {"oid": "abc123"}, + "state": "APPROVED", + } + ] + }, } -""".strip() + +SUCCESS_EXPECTED_OUTPUT = ( + "Dry run, would have merged with url=pulls/1/merge and data={\n" + ' "commit_title": "Example change (#1)",\n' + ' "commit_message": "Example description.\\n\\ncc example-user\\n\\n' + 'Co-authored-by: Example Existing <[email protected]>",\n' + ' "sha": "abc123",\n' + ' "merge_method": "squash"\n' + "}" +) class _TvmBotTest: - NUMBER = 10786 + NUMBER = 1 def preprocess_data(self, data: dict[str, Any]): """ @@ -57,14 +132,13 @@ class _TvmBotTest: Run the tvm-bot script using the data from preprocess_data """ mergebot_script = GITHUB_SCRIPT_ROOT / "github_tvmbot.py" - test_json_dir = Path(__file__).resolve().parent / "sample_prs" - with open(test_json_dir / f"pr{self.NUMBER}.json") as f: - test_data = json.load(f) + test_data = copy.deepcopy(SAMPLE_PR) # Update testing data with replacements / additions test_data = self.preprocess_data(test_data) git = TempGit(tmpdir_factory.mktemp("tmp_git_dir")) + git.run("remote", "set-url", "origin", "https://github.com/example/project.git") comment = { "body": self.COMMENT, @@ -232,11 +306,11 @@ class TestCoAuthors(_TvmBotTest): COMMENT = "@tvm-bot merge" USER = "abc" - EXPECTED = "Co-authored-by: Some One <[email protected]>" + EXPECTED = "Co-authored-by: Example Coauthor <[email protected]>" def preprocess_data(self, data: dict[str, Any]) -> dict[str, Any]: data["authorCommits"]["nodes"][0]["commit"]["authors"]["nodes"].append( - {"name": "Some One", "email": "[email protected]"} + {"name": "Example Coauthor", "email": "[email protected]"} ) return data diff --git a/tests/python/ci/test_utils.py b/tests/python/ci/test_utils.py deleted file mode 100644 index 946e72fa6b..0000000000 --- a/tests/python/ci/test_utils.py +++ /dev/null @@ -1,80 +0,0 @@ -# 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: RUF005 -""" -Constants used in various CI tests -""" - -import os -import pathlib -import subprocess -from typing import Any - -REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent.parent.parent -GITHUB_SCRIPT_ROOT = REPO_ROOT / "ci" / "github" -JENKINS_SCRIPT_ROOT = REPO_ROOT / "ci" / "jenkins" / "scripts" - - -class TempGit: - """ - A wrapper to run commands in a directory (specifically for use in CI tests) - """ - - def __init__(self, cwd): - self.cwd = cwd - # Jenkins git is too old and doesn't have 'git init --initial-branch', - # so init and checkout need to be separate steps - self.run("init", stderr=subprocess.PIPE, stdout=subprocess.PIPE) - self.run("checkout", "-b", "main", stderr=subprocess.PIPE) - self.run("remote", "add", "origin", "https://github.com/apache/tvm.git") - - def run(self, *args, **kwargs): - """ - Run a git command based on *args - """ - proc = subprocess.run( - ["git"] + list(args), encoding="utf-8", cwd=self.cwd, check=False, **kwargs - ) - if proc.returncode != 0: - raise RuntimeError(f"git command failed: '{args}'") - - return proc - - -def run_script(command: list[Any], check: bool = True, **kwargs): - """ - Wrapper to run a script and print its output if there was an error - """ - command = [str(c) for c in command] - kwargs_to_send = { - "stdout": subprocess.PIPE, - "stderr": subprocess.PIPE, - "encoding": "utf-8", - } - env = kwargs.pop("env", None) - if env is not None: - kwargs_to_send["env"] = {**os.environ, **env} - kwargs_to_send.update(kwargs) - proc = subprocess.run( - command, - check=False, - **kwargs_to_send, - ) - if check and proc.returncode != 0: - raise RuntimeError(f"Process failed:\nstdout:\n{proc.stdout}\n\nstderr:\n{proc.stderr}") - - return proc
