chore: make work-item a reusable subproject
This commit is contained in:
519
work_item/tests/test_cli_e2e.py
Normal file
519
work_item/tests/test_cli_e2e.py
Normal file
@@ -0,0 +1,519 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SOURCE_ROOT = Path(__file__).resolve().parents[1]
|
||||
PACKAGE_ROOT = SOURCE_ROOT / "src"
|
||||
|
||||
|
||||
def run(args, *, cwd: Path, env=None, check=True):
|
||||
result = subprocess.run(args, cwd=cwd, env=env, text=True, capture_output=True)
|
||||
if check and result.returncode:
|
||||
raise AssertionError(f"{args}\nstdout={result.stdout}\nstderr={result.stderr}")
|
||||
return result
|
||||
|
||||
|
||||
FAKE_TEA = r"""#!/usr/bin/env python3
|
||||
import json, os, subprocess, sys
|
||||
from pathlib import Path
|
||||
|
||||
state_path = Path(os.environ["FAKE_TEA_STATE"])
|
||||
state = json.loads(state_path.read_text())
|
||||
args = sys.argv[1:]
|
||||
|
||||
def save():
|
||||
state_path.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n")
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value))
|
||||
|
||||
if args[0] == "api":
|
||||
endpoint = args[1]
|
||||
if endpoint.endswith("/issues?state=open&type=issues&limit=100"):
|
||||
out([issue for issue in state["issues"] if issue["state"] == "open"])
|
||||
elif "/dependencies" in endpoint:
|
||||
issue = int(endpoint.split("/issues/")[1].split("/")[0])
|
||||
blockers = state["dependencies"].get(str(issue), [])
|
||||
out([next(x for x in state["issues"] if x["number"] == number) for number in blockers])
|
||||
elif endpoint.endswith("/pulls?state=open&limit=50"):
|
||||
out([pr for pr in state["prs"].values() if not pr["merged"]])
|
||||
elif "/pulls/" in endpoint:
|
||||
number = int(endpoint.rsplit("/", 1)[1])
|
||||
out(state["prs"][str(number)])
|
||||
elif "/commits/" in endpoint and endpoint.endswith("/status"):
|
||||
out({"state": state.get("ci_state", "success")})
|
||||
else:
|
||||
raise SystemExit(f"unsupported api: {endpoint}")
|
||||
elif args[:2] == ["issues", "edit"]:
|
||||
issue = next(x for x in state["issues"] if x["number"] == int(args[2]))
|
||||
if "--add-assignees" in args:
|
||||
issue["assignees"] = [{"login": args[args.index("--add-assignees") + 1]}]
|
||||
labels = {x["name"] for x in issue.get("labels", [])}
|
||||
if "--remove-labels" in args:
|
||||
labels -= set(args[args.index("--remove-labels") + 1].split(","))
|
||||
if "--add-labels" in args:
|
||||
labels |= set(args[args.index("--add-labels") + 1].split(","))
|
||||
issue["labels"] = [{"name": x} for x in sorted(labels)]
|
||||
save(); out(issue)
|
||||
elif args[:2] == ["comments", "add"]:
|
||||
state["comments"].append({"issue": int(args[2]), "body": args[3]})
|
||||
save(); out(state["comments"][-1])
|
||||
elif args[:2] == ["pulls", "create"]:
|
||||
number = len(state["prs"]) + 1
|
||||
branch = args[args.index("--head") + 1]
|
||||
sha = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip()
|
||||
pr = {"number": number, "index": number, "url": f"https://example.test/pr/{number}",
|
||||
"merged": False, "head": {"sha": sha, "ref": branch}}
|
||||
state["prs"][str(number)] = pr
|
||||
save(); out(pr)
|
||||
elif args[:2] == ["pulls", "merge"]:
|
||||
number = int(args[2]); pr = state["prs"][str(number)]
|
||||
if state.get("merge_succeeds", True):
|
||||
subprocess.check_call(["git", "push", "origin", f"{pr['head']['ref']}:main"])
|
||||
pr["merged"] = True
|
||||
save(); out(pr)
|
||||
elif args[:2] == ["issues", "close"]:
|
||||
issue = next(x for x in state["issues"] if x["number"] == int(args[2]))
|
||||
issue["state"] = "closed"; save(); out(issue)
|
||||
else:
|
||||
raise SystemExit("unsupported tea command: " + repr(args))
|
||||
"""
|
||||
|
||||
|
||||
class CliEndToEndTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.base = Path(self.temp.name)
|
||||
remote_parent = self.base / "domverse"
|
||||
remote_parent.mkdir()
|
||||
seed = self.base / "seed"
|
||||
seed.mkdir()
|
||||
run(["git", "init", "-b", "main"], cwd=seed)
|
||||
run(["git", "config", "user.name", "Test Agent"], cwd=seed)
|
||||
run(["git", "config", "user.email", "agent@example.test"], cwd=seed)
|
||||
(seed / "README.md").write_text("seed\n")
|
||||
run(["git", "add", "README.md"], cwd=seed)
|
||||
run(["git", "commit", "-m", "seed"], cwd=seed)
|
||||
self.remote = remote_parent / "photoanalyzer.git"
|
||||
run(["git", "clone", "--bare", str(seed), str(self.remote)], cwd=self.base)
|
||||
self.repo = self.base / "work"
|
||||
run(["git", "clone", str(self.remote), str(self.repo)], cwd=self.base)
|
||||
run(["git", "config", "user.name", "Test Agent"], cwd=self.repo)
|
||||
run(["git", "config", "user.email", "agent@example.test"], cwd=self.repo)
|
||||
|
||||
config = textwrap.dedent("""\
|
||||
[repository]
|
||||
slug = "domverse/photoanalyzer"
|
||||
login = "fake"
|
||||
assignee = "agent"
|
||||
remote = "origin"
|
||||
main_branch = "main"
|
||||
[workflow]
|
||||
branch_prefix = "us"
|
||||
require_ci = true
|
||||
required_tests = []
|
||||
[safety]
|
||||
max_file_bytes = 100000
|
||||
allow = ["tests/fixtures/**"]
|
||||
deny = ["_IGNORE/**", "**/_IGNORE/**", "pictures/**", "*.env", "*.jpg"]
|
||||
""")
|
||||
(self.repo / ".work-item.toml").write_text(config)
|
||||
run(["git", "add", ".work-item.toml"], cwd=self.repo)
|
||||
run(["git", "commit", "-m", "config"], cwd=self.repo)
|
||||
run(["git", "push", "origin", "main"], cwd=self.repo)
|
||||
|
||||
bin_dir = self.base / "bin"
|
||||
bin_dir.mkdir()
|
||||
tea = bin_dir / "tea"
|
||||
tea.write_text(FAKE_TEA)
|
||||
tea.chmod(0o755)
|
||||
self.state_path = self.base / "tea-state.json"
|
||||
self.state_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"issues": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "US01-01 — Implement Safe Workflow",
|
||||
"state": "open",
|
||||
"labels": [{"name": "status/backlog"}],
|
||||
"assignees": [],
|
||||
}
|
||||
],
|
||||
"dependencies": {"1": []},
|
||||
"comments": [],
|
||||
"prs": {},
|
||||
}
|
||||
)
|
||||
)
|
||||
self.env = os.environ.copy()
|
||||
self.env["PATH"] = f"{bin_dir}{os.pathsep}{self.env['PATH']}"
|
||||
self.env["FAKE_TEA_STATE"] = str(self.state_path)
|
||||
self.env["PYTHONPATH"] = str(PACKAGE_ROOT)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.temp.cleanup()
|
||||
|
||||
def cli(self, *args, check=True):
|
||||
return run(
|
||||
[sys.executable, "-m", "work_item", *args], cwd=self.repo, env=self.env, check=check
|
||||
)
|
||||
|
||||
def state(self):
|
||||
return json.loads(self.state_path.read_text())
|
||||
|
||||
def save_state(self, state):
|
||||
self.state_path.write_text(json.dumps(state))
|
||||
|
||||
def test_claim_submit_merge_complete_lifecycle(self) -> None:
|
||||
next_result = self.cli("next")
|
||||
self.assertEqual(json.loads(next_result.stdout)["story_id"], "US01-01")
|
||||
|
||||
claim = json.loads(self.cli("claim").stdout)
|
||||
self.assertEqual(claim["status"], "in-progress")
|
||||
self.assertEqual(
|
||||
run(["git", "branch", "--show-current"], cwd=self.repo).stdout.strip(), claim["branch"]
|
||||
)
|
||||
issue = self.state()["issues"][0]
|
||||
self.assertEqual(issue["assignees"], [{"login": "agent"}])
|
||||
self.assertIn({"name": "status/in-progress"}, issue["labels"])
|
||||
|
||||
(self.repo / "private.env").write_text("not-a-real-secret\n")
|
||||
unsafe = self.cli("submit", "--test", "true", "--yes", check=False)
|
||||
self.assertEqual(unsafe.returncode, 2)
|
||||
self.assertIn("denied path", unsafe.stderr)
|
||||
(self.repo / "private.env").unlink()
|
||||
|
||||
(self.repo / "feature.txt").write_text("implemented\n")
|
||||
review_only = self.cli("submit", "--test", "true", check=False)
|
||||
self.assertEqual(review_only.returncode, 2)
|
||||
self.assertIn("rerun with --yes", review_only.stderr)
|
||||
self.assertEqual(
|
||||
run(["git", "status", "--porcelain"], cwd=self.repo).stdout.strip(), "?? feature.txt"
|
||||
)
|
||||
|
||||
submitted = json.loads(self.cli("submit", "--test", "true", "--yes").stdout)
|
||||
self.assertEqual(submitted["status"], "review")
|
||||
self.assertEqual(submitted["pr"], 1)
|
||||
self.assertIn("status/review", {x["name"] for x in self.state()["issues"][0]["labels"]})
|
||||
|
||||
not_merged = self.cli("complete", check=False)
|
||||
self.assertEqual(not_merged.returncode, 2)
|
||||
self.assertIn("not merged", not_merged.stderr)
|
||||
|
||||
completed = json.loads(self.cli("complete", "--merge").stdout)
|
||||
self.assertEqual(completed["status"], "done")
|
||||
self.assertEqual(
|
||||
run(["git", "branch", "--show-current"], cwd=self.repo).stdout.strip(), "main"
|
||||
)
|
||||
self.assertEqual(run(["git", "status", "--porcelain"], cwd=self.repo).stdout.strip(), "")
|
||||
issue = self.state()["issues"][0]
|
||||
self.assertEqual(issue["state"], "closed")
|
||||
self.assertIn("status/done", {x["name"] for x in issue["labels"]})
|
||||
self.assertFalse((self.repo / ".git" / "work-item-state.json").exists())
|
||||
|
||||
def test_dirty_tree_prevents_claim(self) -> None:
|
||||
(self.repo / "unexpected.txt").write_text("dirty\n")
|
||||
result = self.cli("claim", check=False)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn("clean", result.stderr)
|
||||
self.assertEqual(
|
||||
run(["git", "branch", "--show-current"], cwd=self.repo).stdout.strip(), "main"
|
||||
)
|
||||
|
||||
def test_status_reports_changes_and_claim_resumes_dirty_work(self) -> None:
|
||||
claimed = json.loads(self.cli("claim").stdout)
|
||||
(self.repo / "work.txt").write_text("pending\n")
|
||||
status = json.loads(self.cli("status").stdout)
|
||||
self.assertEqual(status["branch"], claimed["branch"])
|
||||
self.assertEqual(status["current_branch"], claimed["branch"])
|
||||
self.assertEqual(status["changes"], ["work.txt"])
|
||||
resumed = json.loads(self.cli("claim").stdout)
|
||||
self.assertEqual(resumed, claimed)
|
||||
self.assertEqual((self.repo / "work.txt").read_text(), "pending\n")
|
||||
|
||||
def test_claim_switches_back_to_claimed_branch_when_clean(self) -> None:
|
||||
claimed = json.loads(self.cli("claim").stdout)
|
||||
run(["git", "switch", "main"], cwd=self.repo)
|
||||
resumed = json.loads(self.cli("claim").stdout)
|
||||
self.assertEqual(resumed, claimed)
|
||||
self.assertEqual(
|
||||
run(["git", "branch", "--show-current"], cwd=self.repo).stdout.strip(),
|
||||
claimed["branch"],
|
||||
)
|
||||
|
||||
def test_next_selection_skips_ineligible_issues(self) -> None:
|
||||
state = self.state()
|
||||
state["issues"] = [
|
||||
{"number": 10, "title": "Maintenance", "state": "open", "labels": [], "assignees": []},
|
||||
{
|
||||
"number": 1,
|
||||
"title": "US01-01 — Busy",
|
||||
"state": "open",
|
||||
"labels": [{"name": "status/in-progress"}],
|
||||
"assignees": [],
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"title": "US01-02 — Assigned",
|
||||
"state": "open",
|
||||
"labels": [],
|
||||
"assignees": [{"login": "someone"}],
|
||||
},
|
||||
{
|
||||
"number": 3,
|
||||
"title": "US01-03 — Has dependency",
|
||||
"state": "open",
|
||||
"labels": [],
|
||||
"assignees": [],
|
||||
},
|
||||
{
|
||||
"number": 4,
|
||||
"title": "US01-04 — Ready",
|
||||
"state": "open",
|
||||
"labels": [{"name": "status/backlog"}],
|
||||
"assignees": [],
|
||||
},
|
||||
{
|
||||
"number": 5,
|
||||
"title": "US02-01 — Later epic",
|
||||
"state": "open",
|
||||
"labels": [],
|
||||
"assignees": [],
|
||||
},
|
||||
{"number": 99, "title": "Blocker", "state": "open", "labels": [], "assignees": []},
|
||||
]
|
||||
state["dependencies"] = {"3": [99], "4": [], "5": []}
|
||||
self.save_state(state)
|
||||
payload = json.loads(self.cli("next").stdout)
|
||||
self.assertEqual(payload["story_id"], "US01-04")
|
||||
|
||||
def test_next_fails_when_no_story_is_eligible(self) -> None:
|
||||
state = self.state()
|
||||
state["issues"][0]["labels"] = [{"name": "status/blocked"}]
|
||||
self.save_state(state)
|
||||
result = self.cli("next", check=False)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn("No eligible", result.stderr)
|
||||
|
||||
def test_commands_requiring_a_claim_fail_without_state(self) -> None:
|
||||
cases = (
|
||||
("status",),
|
||||
("submit", "--test", "true"),
|
||||
("complete",),
|
||||
("block", "--reason", "blocked"),
|
||||
)
|
||||
for args in cases:
|
||||
with self.subTest(command=args[0]):
|
||||
result = self.cli(*args, check=False)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn("No story is currently claimed", result.stderr)
|
||||
|
||||
def test_submit_requires_changes_and_passing_tests(self) -> None:
|
||||
self.cli("claim")
|
||||
no_changes = self.cli("submit", "--test", "true", "--yes", check=False)
|
||||
self.assertEqual(no_changes.returncode, 2)
|
||||
self.assertIn("No changes", no_changes.stderr)
|
||||
|
||||
(self.repo / "feature.txt").write_text("implemented\n")
|
||||
failed_test = self.cli("submit", "--test", "false", "--yes", check=False)
|
||||
self.assertEqual(failed_test.returncode, 2)
|
||||
self.assertIn("Test command failed", failed_test.stderr)
|
||||
self.assertEqual(
|
||||
run(["git", "status", "--porcelain"], cwd=self.repo).stdout.strip(), "?? feature.txt"
|
||||
)
|
||||
|
||||
def test_submit_rejects_wrong_branch(self) -> None:
|
||||
claimed = json.loads(self.cli("claim").stdout)
|
||||
run(["git", "switch", "main"], cwd=self.repo)
|
||||
result = self.cli("submit", "--test", "true", check=False)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn(f"Expected branch {claimed['branch']}", result.stderr)
|
||||
|
||||
def test_complete_requires_review_and_successful_ci(self) -> None:
|
||||
self.cli("claim")
|
||||
before_review = self.cli("complete", "--merge", check=False)
|
||||
self.assertEqual(before_review.returncode, 2)
|
||||
self.assertIn("not been submitted", before_review.stderr)
|
||||
|
||||
(self.repo / "feature.txt").write_text("implemented\n")
|
||||
self.cli("submit", "--test", "true", "--yes")
|
||||
state = self.state()
|
||||
state["ci_state"] = "pending"
|
||||
self.save_state(state)
|
||||
pending = self.cli("complete", "--merge", check=False)
|
||||
self.assertEqual(pending.returncode, 2)
|
||||
self.assertIn("CI is not successful: pending", pending.stderr)
|
||||
|
||||
def test_complete_detects_unverified_merge(self) -> None:
|
||||
self.cli("claim")
|
||||
(self.repo / "feature.txt").write_text("implemented\n")
|
||||
self.cli("submit", "--test", "true", "--yes")
|
||||
state = self.state()
|
||||
state["merge_succeeds"] = False
|
||||
self.save_state(state)
|
||||
result = self.cli("complete", "--merge", check=False)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn("merge could not be verified", result.stderr)
|
||||
|
||||
def test_complete_rejects_new_dirty_work_after_submission(self) -> None:
|
||||
self.cli("claim")
|
||||
(self.repo / "feature.txt").write_text("implemented\n")
|
||||
self.cli("submit", "--test", "true", "--yes")
|
||||
(self.repo / "unexpected.txt").write_text("do not carry to main\n")
|
||||
result = self.cli("complete", "--merge", check=False)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn("Working tree must be clean", result.stderr)
|
||||
self.assertTrue((self.repo / "unexpected.txt").is_file())
|
||||
|
||||
def test_whitespace_only_block_reason_is_rejected(self) -> None:
|
||||
self.cli("claim")
|
||||
result = self.cli("block", "--reason", " ", check=False)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn("non-empty blocking reason", result.stderr)
|
||||
|
||||
def test_blocked_story_can_be_resumed(self) -> None:
|
||||
claimed = json.loads(self.cli("claim").stdout)
|
||||
blocked = json.loads(self.cli("block", "--reason", "Waiting for a fixture").stdout)
|
||||
self.assertEqual(blocked["status"], "blocked")
|
||||
resumed = json.loads(self.cli("claim").stdout)
|
||||
self.assertEqual(resumed["status"], "in-progress")
|
||||
self.assertNotIn("reason", resumed)
|
||||
issue = self.state()["issues"][0]
|
||||
labels = {label["name"] for label in issue["labels"]}
|
||||
self.assertIn("status/in-progress", labels)
|
||||
self.assertNotIn("status/blocked", labels)
|
||||
self.assertIn(claimed["branch"], self.state()["comments"][-1]["body"])
|
||||
|
||||
def test_review_story_cannot_be_reclaimed(self) -> None:
|
||||
self.cli("claim")
|
||||
(self.repo / "feature.txt").write_text("implemented\n")
|
||||
self.cli("submit", "--test", "true", "--yes")
|
||||
result = self.cli("claim", check=False)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn("finish its review/completion", result.stderr)
|
||||
|
||||
def test_command_works_from_repository_subdirectory(self) -> None:
|
||||
subdirectory = self.repo / "nested"
|
||||
subdirectory.mkdir()
|
||||
result = run(
|
||||
[sys.executable, "-m", "work_item", "next"],
|
||||
cwd=subdirectory,
|
||||
env=self.env,
|
||||
)
|
||||
self.assertEqual(json.loads(result.stdout)["story_id"], "US01-01")
|
||||
|
||||
def test_cli_discovers_yaml_configuration(self) -> None:
|
||||
(self.repo / ".work-item.toml").unlink()
|
||||
(self.repo / ".work-item.yml").write_text(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
repository:
|
||||
slug: domverse/photoanalyzer
|
||||
login: fake
|
||||
assignee: agent
|
||||
remote: origin
|
||||
main_branch: main
|
||||
workflow:
|
||||
branch_prefix: story
|
||||
require_ci: false
|
||||
required_tests: []
|
||||
safety:
|
||||
max_file_bytes: 100000
|
||||
allow: ["tests/fixtures/**"]
|
||||
deny: ["*.env", "*.jpg"]
|
||||
"""
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
payload = json.loads(self.cli("next").stdout)
|
||||
self.assertEqual(payload["story_id"], "US01-01")
|
||||
|
||||
def test_cli_discovers_dotenv_and_honors_environment_override(self) -> None:
|
||||
(self.repo / ".work-item.toml").unlink()
|
||||
(self.repo / ".work-item.env").write_text(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
WORK_ITEM_REPO_SLUG=wrong/repository
|
||||
WORK_ITEM_LOGIN=fake
|
||||
WORK_ITEM_ASSIGNEE=agent
|
||||
WORK_ITEM_REQUIRED_TESTS=[]
|
||||
WORK_ITEM_DENY=[]
|
||||
WORK_ITEM_ALLOW=[]
|
||||
"""
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
self.env["WORK_ITEM_REPO_SLUG"] = "domverse/photoanalyzer"
|
||||
payload = json.loads(self.cli("next").stdout)
|
||||
self.assertEqual(payload["story_id"], "US01-01")
|
||||
|
||||
def test_block_requires_reason_and_records_state(self) -> None:
|
||||
self.cli("claim")
|
||||
result = self.cli("block", "--reason", "Provider fixture unavailable")
|
||||
payload = json.loads(result.stdout)
|
||||
self.assertEqual(payload["status"], "blocked")
|
||||
issue = self.state()["issues"][0]
|
||||
self.assertIn("status/blocked", {x["name"] for x in issue["labels"]})
|
||||
self.assertIn("Provider fixture unavailable", self.state()["comments"][-1]["body"])
|
||||
|
||||
|
||||
class InterpreterWrapperTests(unittest.TestCase):
|
||||
def test_wrapper_selects_supported_python(self) -> None:
|
||||
result = run(
|
||||
[
|
||||
str(SOURCE_ROOT / "scripts" / "python"),
|
||||
"-c",
|
||||
"import sys; print(sys.version_info[:2])",
|
||||
],
|
||||
cwd=SOURCE_ROOT,
|
||||
)
|
||||
major, minor = eval(result.stdout.strip(), {"__builtins__": {}})
|
||||
self.assertEqual(major, 3)
|
||||
self.assertGreaterEqual(minor, 12)
|
||||
|
||||
def test_wrapper_falls_back_when_conda_python_is_too_old(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
bin_dir = Path(directory)
|
||||
conda = bin_dir / "conda"
|
||||
conda.write_text("#!/bin/sh\necho 3.11\n")
|
||||
conda.chmod(0o755)
|
||||
fallback = bin_dir / "python3.13"
|
||||
fallback.symlink_to(sys.executable)
|
||||
env = os.environ.copy()
|
||||
env["PATH"] = f"{bin_dir}{os.pathsep}{env['PATH']}"
|
||||
result = run(
|
||||
[str(SOURCE_ROOT / "scripts" / "python"), "-c", "print('fallback')"],
|
||||
cwd=SOURCE_ROOT,
|
||||
env=env,
|
||||
)
|
||||
self.assertEqual(result.stdout.strip(), "fallback")
|
||||
|
||||
def test_wrapper_reports_when_no_supported_python_exists(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
bin_dir = Path(directory)
|
||||
conda = bin_dir / "conda"
|
||||
conda.write_text("#!/bin/sh\necho 3.11\n")
|
||||
conda.chmod(0o755)
|
||||
env = os.environ.copy()
|
||||
env["PATH"] = f"{bin_dir}{os.pathsep}/bin:/usr/bin"
|
||||
result = run(
|
||||
[str(SOURCE_ROOT / "scripts" / "python"), "-c", "print('never')"],
|
||||
cwd=SOURCE_ROOT,
|
||||
env=env,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn("Python 3.12 or newer is required", result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user