# 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 """ Test the @tvm-bot merge code """ import json from pathlib import Path from typing import Any import tvm 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` objects after they had been freed.\\n\\n- Rename N-d alloc and free packed functions. Since most of the similar device functions use snake case, renaming `*.AllocND` to `*.alloc_nd` and `*.FreeND` to `*.free_nd`.\\n\\n\\ncc someone\\n\\n\\nCo-authored-by: Adam Straw ", "sha": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd", "merge_method": "squash" } """.strip() class _TvmBotTest: NUMBER = 10786 def preprocess_data(self, data: dict[str, Any]): """ Used to pre-process PR data before running the test. Override as necessary to edit data for specific test cases. """ return data @tvm.testing.skip_if_wheel_test def test(self, tmpdir_factory): """ 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) # Update testing data with replacements / additions test_data = self.preprocess_data(test_data) git = TempGit(tmpdir_factory.mktemp("tmp_git_dir")) comment = { "body": self.COMMENT, "id": 123, "user": { "login": self.USER, }, } allowed_users = [{"login": "abc"}, {"login": "other-abc"}] proc = run_script( [ mergebot_script, "--pr", self.NUMBER, "--dry-run", "--run-url", "https://example.com", "--testing-pr-json", json.dumps(test_data), "--testing-collaborators-json", json.dumps(allowed_users), "--testing-mentionable-users-json", json.dumps(allowed_users), "--trigger-comment-json", json.dumps(comment), ], env={ "TVM_BOT_JENKINS_TOKEN": "123", "GH_ACTIONS_TOKEN": "123", }, cwd=git.cwd, ) if self.EXPECTED not in proc.stderr: raise RuntimeError(f"{proc.stderr}\ndid not contain\n{self.EXPECTED}") class TestNoRequest(_TvmBotTest): """ A PR for which the mergebot runs but no merge is requested """ COMMENT = "@tvm-bot do something else" USER = "abc" EXPECTED = "Command 'do something else' did not match anything" def preprocess_data(self, data: dict[str, Any]): data["reviews"]["nodes"][0]["body"] = "nothing" return data class TestSuccessfulMerge(_TvmBotTest): """ Everything is fine so this PR will merge """ COMMENT = "@tvm-bot merge" USER = "abc" EXPECTED = SUCCESS_EXPECTED_OUTPUT class TestBadCI(_TvmBotTest): """ A PR which failed CI and cannot merge """ COMMENT = "@tvm-bot merge" USER = "abc" EXPECTED = "Cannot merge, these CI jobs are not successful on" def preprocess_data(self, data: dict[str, Any]): # Mark the Jenkins build as failed contexts = data["commits"]["nodes"][0]["commit"]["statusCheckRollup"]["contexts"]["nodes"] for context in contexts: if "context" in context and context["context"] == "tvm-ci/pr-head": context["state"] = "FAILED" return data class TestOldReview(_TvmBotTest): """ A PR with passing CI and approving reviews on an old commit so it cannot merge """ COMMENT = "@tvm-bot merge" USER = "abc" EXPECTED = "Cannot merge, did not find any approving reviews" def preprocess_data(self, data: dict[str, Any]) -> dict[str, Any]: data["reviews"]["nodes"][0]["commit"]["oid"] = "abc12345" return data class TestMissingJob(_TvmBotTest): """ PR missing an expected CI job and cannot merge """ COMMENT = "@tvm-bot merge" USER = "abc" EXPECTED = "Cannot merge, missing expected jobs" def preprocess_data(self, data: dict[str, Any]) -> dict[str, Any]: contexts = data["commits"]["nodes"][0]["commit"]["statusCheckRollup"]["contexts"]["nodes"] for context in contexts: if "context" in context and context["context"] == "tvm-ci/pr-head": context["context"] = "something" return data class TestInvalidAuthor(_TvmBotTest): """ Merge requester is not a committer and cannot merge """ COMMENT = "@tvm-bot merge" USER = "not-abc" EXPECTED = "Failed auth check 'collaborators', quitting" class TestUnauthorizedComment(_TvmBotTest): """ Check that a merge comment not from a CONTRIBUTOR is rejected """ COMMENT = "@tvm-bot merge" USER = "not-abc2" EXPECTED = "Failed auth check 'collaborators'" class TestNoReview(_TvmBotTest): """ Check that a merge request without any reviews is rejected """ COMMENT = "@tvm-bot merge" USER = "abc" EXPECTED = "Cannot merge, did not find any approving reviews from users with write access" def preprocess_data(self, data: dict[str, Any]) -> dict[str, Any]: data["reviews"]["nodes"] = [] return data class TestChangesRequested(_TvmBotTest): """ Check that a merge request with a 'Changes Requested' review is rejected """ COMMENT = "@tvm-bot merge" USER = "abc" EXPECTED = "Cannot merge, found [this review]" def preprocess_data(self, data: dict[str, Any]) -> dict[str, Any]: data["reviews"]["nodes"][0]["state"] = "CHANGES_REQUESTED" data["reviews"]["nodes"][0]["url"] = "http://example.com" return data class TestCoAuthors(_TvmBotTest): """ Check that a merge request with co-authors generates the correct commit message """ COMMENT = "@tvm-bot merge" USER = "abc" EXPECTED = "Co-authored-by: Some One " def preprocess_data(self, data: dict[str, Any]) -> dict[str, Any]: data["authorCommits"]["nodes"][0]["commit"]["authors"]["nodes"].append( {"name": "Some One", "email": "someone@email.com"} ) return data class TestRerunCI(_TvmBotTest): """ Start a new CI job """ COMMENT = "@tvm-bot rerun" USER = "abc" EXPECTED = "Rerunning ci with" class TestRerunPermissions(_TvmBotTest): """ Start a new CI job as an unauthorized user """ COMMENT = "@tvm-bot rerun" USER = "someone" EXPECTED = "Failed auth check 'mentionable_users', quitting" class TestRerunNonAuthor(_TvmBotTest): """ Start a new CI job as a mentionable user """ COMMENT = "@tvm-bot rerun" USER = "other-abc" EXPECTED = "Passed auth check 'mentionable_users', continuing" class TestIgnoreJobs(_TvmBotTest): """ Ignore GitHub Actions jobs that don't start with CI / """ COMMENT = "@tvm-bot merge" USER = "abc" EXPECTED = "Dry run, would have merged" if __name__ == "__main__": tvm.testing.main()