Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
2e0eaead3This file is pinned to the selected release. Download the original to open it in your design tool.
#!/usr/bin/env python3
#
# This file is part of LiteX.
#
# Copyright (c) 2026 Florent Kermarrec <florent@enjoy-digital.fr>
# SPDX-License-Identifier: BSD-2-Clause
import os
import sys
import io
import unittest
import tempfile
import subprocess
import contextlib
from types import SimpleNamespace
from unittest import mock
import litex_setup
class TestLiteXSetup(unittest.TestCase):
def setUp(self):
self.tmpdir = tempfile.TemporaryDirectory()
self.workspace = self.tmpdir.name
self.old_cwd = os.getcwd()
self.old_path = litex_setup.current_path
self.old_git_repos = litex_setup.git_repos
self.old_install_configs = litex_setup.install_configs
self.old_stderr = sys.stderr
litex_setup.current_path = self.workspace
def tearDown(self):
os.chdir(self.old_cwd)
litex_setup.current_path = self.old_path
litex_setup.git_repos = self.old_git_repos
litex_setup.install_configs = self.old_install_configs
sys.stderr = self.old_stderr
self.tmpdir.cleanup()
def git(self, repo_path, *args):
subprocess.check_call(
["git"] + list(args),
cwd=repo_path,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
def git_output(self, repo_path, *args):
return subprocess.check_output(
["git"] + list(args),
cwd=repo_path,
stderr=subprocess.DEVNULL,
).decode("utf-8").strip()
def write_file(self, path, contents):
with open(path, "w", encoding="utf-8") as f:
f.write(contents)
def append_file(self, path, contents):
with open(path, "a", encoding="utf-8") as f:
f.write(contents)
def configure_user(self, repo_path):
self.git(repo_path, "config", "user.email", "setup-test@example.com")
self.git(repo_path, "config", "user.name", "Setup Test")
def create_repo(self):
remotes_path = os.path.join(self.workspace, "remotes")
os.makedirs(remotes_path, exist_ok=True)
remote_path = os.path.join(remotes_path, "litex.git")
upstream_path = os.path.join(self.workspace, "upstream")
repo_path = os.path.join(self.workspace, "litex")
self.git(self.workspace, "init", "--bare", remote_path)
os.makedirs(upstream_path)
self.git(upstream_path, "init", "-b", "master")
self.configure_user(upstream_path)
self.git(upstream_path, "remote", "add", "origin", remote_path)
self.write_file(os.path.join(upstream_path, "README.md"), "base\n")
self.git(upstream_path, "add", "README.md")
self.git(upstream_path, "commit", "-m", "Initial commit")
self.git(upstream_path, "push", "-u", "origin", "master")
self.git(self.workspace, "clone", remote_path, repo_path)
self.configure_user(repo_path)
litex_setup.git_repos = {
"litex": SimpleNamespace(
branch="master",
clone="regular",
tag=None,
sha1=None,
develop=True,
editable=True,
),
}
litex_setup.install_configs = {"minimal": ["litex"]}
return upstream_path, repo_path
def create_remote_only_repo(self, commit_count=2):
remotes_path = os.path.join(self.workspace, "remotes")
os.makedirs(remotes_path, exist_ok=True)
remote_path = os.path.join(remotes_path, "litex.git")
upstream_path = os.path.join(self.workspace, "upstream")
self.git(self.workspace, "init", "--bare", remote_path)
os.makedirs(upstream_path)
self.git(upstream_path, "init", "-b", "master")
self.configure_user(upstream_path)
self.git(upstream_path, "remote", "add", "origin", remote_path)
commits = []
for n in range(commit_count):
self.write_file(os.path.join(upstream_path, "README.md"), "commit-{}\n".format(n))
self.git(upstream_path, "add", "README.md")
self.git(upstream_path, "commit", "-m", "Commit {}".format(n))
commits.append(self.git_output(upstream_path, "rev-parse", "HEAD"))
self.git(upstream_path, "push", "-u", "origin", "master")
litex_setup.git_repos = {
"litex": SimpleNamespace(
url="file://" + remotes_path + "/",
branch="master",
clone="regular",
tag=None,
sha1=None,
develop=True,
editable=True,
),
}
litex_setup.install_configs = {"minimal": ["litex"]}
return upstream_path, remote_path, commits
def create_recursive_repo(self):
remotes_path = os.path.join(self.workspace, "remotes")
os.makedirs(remotes_path, exist_ok=True)
submodule_remote = os.path.join(remotes_path, "submodule.git")
submodule_upstream = os.path.join(self.workspace, "submodule-upstream")
self.git(self.workspace, "init", "--bare", submodule_remote)
os.makedirs(submodule_upstream)
self.git(submodule_upstream, "init", "-b", "master")
self.configure_user(submodule_upstream)
self.git(submodule_upstream, "remote", "add", "origin", submodule_remote)
self.write_file(os.path.join(submodule_upstream, "value.txt"), "submodule-v1\n")
self.git(submodule_upstream, "add", "value.txt")
self.git(submodule_upstream, "commit", "-m", "Submodule v1")
submodule_v1 = self.git_output(submodule_upstream, "rev-parse", "HEAD")
self.git(submodule_upstream, "push", "-u", "origin", "master")
self.write_file(os.path.join(submodule_upstream, "value.txt"), "submodule-v2\n")
self.git(submodule_upstream, "commit", "-am", "Submodule v2")
submodule_v2 = self.git_output(submodule_upstream, "rev-parse", "HEAD")
self.git(submodule_upstream, "push")
main_remote = os.path.join(remotes_path, "litex.git")
main_upstream = os.path.join(self.workspace, "main-upstream")
self.git(self.workspace, "init", "--bare", main_remote)
os.makedirs(main_upstream)
self.git(main_upstream, "init", "-b", "master")
self.configure_user(main_upstream)
self.git(main_upstream, "remote", "add", "origin", main_remote)
self.git(
main_upstream,
"-c", "protocol.file.allow=always",
"submodule", "add", submodule_remote, "deps/submodule",
)
submodule_path = os.path.join(main_upstream, "deps", "submodule")
self.git(submodule_path, "checkout", submodule_v1)
self.git(main_upstream, "add", ".gitmodules", "deps/submodule")
self.git(main_upstream, "commit", "-m", "Use submodule v1")
main_v1 = self.git_output(main_upstream, "rev-parse", "HEAD")
self.git(main_upstream, "push", "-u", "origin", "master")
self.git(submodule_path, "checkout", submodule_v2)
self.git(main_upstream, "add", "deps/submodule")
self.git(main_upstream, "commit", "-m", "Use submodule v2")
self.git(main_upstream, "push")
litex_setup.git_repos = {
"litex": SimpleNamespace(
url=os.path.join(remotes_path, ""),
branch="master",
clone="recursive",
tag=True,
sha1=main_v1,
develop=True,
editable=True,
),
}
litex_setup.install_configs = {"minimal": ["litex"]}
return main_remote, main_v1, submodule_v1
def assert_update_error(self):
output = io.StringIO()
with contextlib.redirect_stdout(output):
with self.assertRaises(litex_setup.SetupError):
litex_setup.litex_setup_update_repos(config="minimal")
sys.stderr = self.old_stderr
return output.getvalue()
def assert_setup_error(self, callback, *args, **kwargs):
stdout = io.StringIO()
stderr = io.StringIO()
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
with self.assertRaises(litex_setup.SetupError):
callback(*args, **kwargs)
return stdout.getvalue(), stderr.getvalue()
def test_checkout_preserves_leading_zeroes_in_string_sha(self):
sha1 = "0ddfb44723438d39636cfbf63e9e27609ec9bd43"
with mock.patch("litex_setup.subprocess_check_output", return_value="") as run:
litex_setup.git_checkout(sha1=sha1, cwd=self.workspace)
self.assertEqual(run.call_args[0][0][-1], sha1)
def test_checkout_keeps_integer_sha_abbreviated(self):
with mock.patch("litex_setup.subprocess_check_output", return_value="") as run:
litex_setup.git_checkout(sha1=0xc69953aff92, cwd=self.workspace)
self.assertEqual(run.call_args[0][0][-1], "c69953aff92")
def test_format_frozen_repo_keeps_full_sha_as_string(self):
repo = SimpleNamespace(
url="https://github.com/enjoy-digital/",
branch="master",
clone="regular",
tag=None,
develop=True,
editable=True,
)
formatted = litex_setup.litex_setup_format_frozen_repo(
"litex",
repo,
"https://github.com/enjoy-digital/",
"0ddfb44723438d39636cfbf63e9e27609ec9bd43",
)
self.assertIn('sha1="0ddfb44723438d39636cfbf63e9e27609ec9bd43"', formatted)
def test_update_can_be_cancelled_when_repo_has_local_changes(self):
_upstream_path, repo_path = self.create_repo()
self.append_file(os.path.join(repo_path, "README.md"), "local change\n")
with mock.patch("sys.stdin.isatty", return_value=True), \
mock.patch("builtins.input", return_value="n"):
output = self.assert_update_error()
self.assertIn("litex Git repository has local changes.", output)
self.assertIn("Update cancelled.", output)
def test_update_assume_yes_preserves_local_changes(self):
_upstream_path, repo_path = self.create_repo()
self.append_file(os.path.join(repo_path, "README.md"), "local change\n")
with mock.patch("sys.stdin.isatty", return_value=True), \
mock.patch("builtins.input") as input_prompt:
litex_setup.litex_setup_update_repos(config="minimal", assume_yes=True)
input_prompt.assert_not_called()
self.assertIn("README.md", self.git_output(repo_path, "status", "--short"))
def test_update_fast_forward_failure_has_actionable_error(self):
upstream_path, repo_path = self.create_repo()
self.append_file(os.path.join(upstream_path, "README.md"), "remote change\n")
self.git(upstream_path, "commit", "-am", "Remote change")
self.git(upstream_path, "push")
self.append_file(os.path.join(repo_path, "README.md"), "local commit\n")
self.git(repo_path, "commit", "-am", "Local change")
with mock.patch("sys.stdin.isatty", return_value=False):
output = self.assert_update_error()
self.assertIn("Could not fast-forward litex Git repository.", output)
self.assertIn("LiteX only performs fast-forward updates", output)
self.assertIn("git pull --ff-only", output)
self.assertNotIn("Traceback", output)
def test_init_clone_depth_creates_shallow_clone(self):
self.create_remote_only_repo()
litex_setup.litex_setup_init_repos(config="minimal", clone_depth=1)
repo_path = os.path.join(self.workspace, "litex")
self.assertEqual(self.git_output(repo_path, "rev-parse", "--is-shallow-repository"), "true")
self.assertEqual(self.git_output(repo_path, "rev-list", "--count", "HEAD"), "1")
def test_init_clone_depth_uses_full_clone_for_pinned_sha(self):
_upstream_path, _remote_path, commits = self.create_remote_only_repo()
litex_setup.git_repos["litex"].sha1 = commits[0]
litex_setup.litex_setup_init_repos(config="minimal", clone_depth=1)
repo_path = os.path.join(self.workspace, "litex")
self.assertEqual(self.git_output(repo_path, "rev-parse", "HEAD"), commits[0])
self.assertEqual(self.git_output(repo_path, "rev-parse", "--is-shallow-repository"), "false")
def test_init_clone_depth_uses_shallow_submodules_for_recursive_repo(self):
litex_setup.git_repos = {
"litex": SimpleNamespace(
url="https://github.com/enjoy-digital/",
branch="master",
clone="recursive",
tag=None,
sha1=None,
develop=True,
editable=True,
),
}
litex_setup.install_configs = {"minimal": ["litex"]}
with mock.patch("litex_setup.subprocess_check_output", return_value=b"") as run:
litex_setup.litex_setup_init_repos(config="minimal", clone_depth=1)
clone_cmd = run.call_args_list[0][0][0]
self.assertIn("--recursive", clone_cmd)
self.assertIn("--shallow-submodules", clone_cmd)
def test_init_clone_depth_rejects_non_positive_depth(self):
self.create_remote_only_repo()
output, stderr = self.assert_setup_error(
litex_setup.litex_setup_init_repos,
config = "minimal",
clone_depth = 0,
)
self.assertIn("--clone-depth must be a positive integer.", output)
self.assertNotIn("Traceback", output + stderr)
def test_init_updates_submodules_after_frozen_sha_checkout(self):
_main_remote, _main_v1, submodule_v1 = self.create_recursive_repo()
with mock.patch.dict(os.environ, {"GIT_ALLOW_PROTOCOL": "file"}):
litex_setup.litex_setup_init_repos(config="minimal")
submodule_path = os.path.join(self.workspace, "litex", "deps", "submodule")
self.assertEqual(self.git_output(submodule_path, "rev-parse", "HEAD"), submodule_v1)
def test_update_updates_submodules_after_frozen_sha_checkout(self):
main_remote, _main_v1, submodule_v1 = self.create_recursive_repo()
repo_path = os.path.join(self.workspace, "litex")
with mock.patch.dict(os.environ, {"GIT_ALLOW_PROTOCOL": "file"}):
self.git(
self.workspace,
"-c", "protocol.file.allow=always",
"clone", "--recursive", main_remote, repo_path,
)
litex_setup.litex_setup_update_repos(config="minimal")
submodule_path = os.path.join(repo_path, "deps", "submodule")
self.assertEqual(self.git_output(submodule_path, "rev-parse", "HEAD"), submodule_v1)
def test_init_rejects_existing_non_git_directory(self):
os.makedirs(os.path.join(self.workspace, "litex"))
litex_setup.git_repos = {
"litex": SimpleNamespace(
url=self.workspace + "/",
clone="regular",
branch="master",
tag=None,
sha1=None,
),
}
litex_setup.install_configs = {"minimal": ["litex"]}
output, stderr = self.assert_setup_error(litex_setup.litex_setup_init_repos, config="minimal")
self.assertIn("litex directory already exists but is not a Git repository.", output)
self.assertIn("Move or remove it, then retry --init.", output)
self.assertNotIn("Traceback", output + stderr)
def test_init_clone_failure_has_actionable_error(self):
litex_setup.git_repos = {
"litex": SimpleNamespace(
url=os.path.join(self.workspace, "missing") + "/",
clone="regular",
branch="master",
tag=None,
sha1=None,
),
}
litex_setup.install_configs = {"minimal": ["litex"]}
output, stderr = self.assert_setup_error(litex_setup.litex_setup_init_repos, config="minimal")
self.assertIn("Could not clone litex Git repository.", output)
self.assertIn("Check the remote URL, network/SSH access and local path, then retry --init.", output)
self.assertNotIn("Traceback", output + stderr)
def test_install_rejects_missing_repository(self):
litex_setup.git_repos = {
"litex": SimpleNamespace(develop=True, editable=True),
}
litex_setup.install_configs = {"minimal": ["litex"]}
output, stderr = self.assert_setup_error(litex_setup.litex_setup_install_repos, config="minimal")
self.assertIn("litex Git repository is not initialized, please run --init first.", output)
self.assertNotIn("Traceback", output + stderr)
def test_install_reports_all_uninitialized_repositories(self):
os.makedirs(os.path.join(self.workspace, "migen"))
litex_setup.git_repos = {
"migen": SimpleNamespace(develop=True, editable=True),
"litex": SimpleNamespace(develop=True, editable=True),
"optional": SimpleNamespace(develop=False, editable=True),
}
litex_setup.install_configs = {"standard": ["migen", "litex", "optional"]}
output, stderr = self.assert_setup_error(litex_setup.litex_setup_install_repos, config="standard")
self.assertIn("Some Git repositories are not initialized, please run --init first.", output)
self.assertIn("Missing repositories:", output)
self.assertIn("litex:", output)
self.assertIn("Paths that exist but are not Git repositories:", output)
self.assertIn("migen:", output)
self.assertIn("Move or remove these paths before running --init.", output)
self.assertIn("./litex_setup.py --init --config=standard", output)
self.assertIn("./litex_setup.py --install --config=standard", output)
self.assertNotIn("optional:", output)
self.assertNotIn("Traceback", output + stderr)
def test_install_pip_failure_has_actionable_error(self):
self.create_repo()
with mock.patch(
"litex_setup._pip_install",
side_effect=subprocess.CalledProcessError(1, [sys.executable, "-m", "pip"]),
), mock.patch("litex_setup.pip_install_build_dependencies"), \
mock.patch("litex_setup.pip_install_in_virtualenv", return_value=False), \
mock.patch("litex_setup.pip_install_externally_managed", return_value=False):
output, stderr = self.assert_setup_error(
litex_setup.litex_setup_install_repos,
config="minimal",
user_mode=True,
)
self.assertIn("litex Git repository could not be installed.", output)
self.assertIn("PYTHONPATH=", output)
self.assertIn("-m pip install --no-build-isolation --editable . --user", output)
self.assertNotIn("Traceback", output + stderr)
def test_install_externally_managed_python_has_actionable_error(self):
self.create_repo()
with mock.patch("litex_setup.pip_install_in_virtualenv", return_value=False), \
mock.patch("litex_setup.pip_install_externally_managed", return_value=True):
output, stderr = self.assert_setup_error(
litex_setup.litex_setup_install_repos,
config="minimal",
user_mode=True,
)
self.assertIn("externally managed", output)
self.assertIn("python3 -m venv ~/litex-venv", output)
self.assertIn("--break-system-packages", output)
self.assertIn("--user installs are also blocked", output)
self.assertNotIn("Traceback", output + stderr)
def test_install_break_system_packages_passes_pip_override(self):
self.create_repo()
with mock.patch("subprocess.check_call") as check_call, \
mock.patch("litex_setup.pip_install_externally_managed", return_value=True):
litex_setup.litex_setup_install_repos(
config="minimal",
break_system_packages=True,
)
cmd, _kwargs = check_call.call_args
self.assertIn("--break-system-packages", cmd[0])
def test_install_ignores_user_mode_inside_virtualenv(self):
self.create_repo()
output = io.StringIO()
with contextlib.redirect_stdout(output), \
mock.patch("subprocess.check_call") as check_call, \
mock.patch("litex_setup.pip_install_in_virtualenv", return_value=True):
litex_setup.litex_setup_install_repos(config="minimal", user_mode=True)
self.assertIn("--user ignored", output.getvalue())
for call in check_call.call_args_list:
cmd = call.args[0]
self.assertNotIn("--user", cmd)
def test_install_bootstraps_python_build_dependencies(self):
self.create_repo()
with mock.patch("subprocess.check_call") as check_call, \
mock.patch("litex_setup.pip_install_externally_managed", return_value=False):
litex_setup.litex_setup_install_repos(config="minimal")
cmd = check_call.call_args_list[0].args[0]
self.assertIn("setuptools>=65.5", cmd)
self.assertIn("wheel", cmd)
def test_build_dependencies_failure_has_actionable_error(self):
self.create_repo()
with mock.patch(
"subprocess.check_call",
side_effect=subprocess.CalledProcessError(1, [sys.executable, "-m", "pip"]),
), mock.patch("litex_setup.pip_install_externally_managed", return_value=False):
output, stderr = self.assert_setup_error(
litex_setup.litex_setup_install_repos,
config="minimal",
)
self.assertIn("Python build dependencies could not be installed.", output)
self.assertIn("setuptools>=65.5", output)
self.assertIn("wheel", output)
self.assertNotIn("Traceback", output + stderr)
def test_local_install_exposes_repo_to_pep517_backend(self):
self.create_repo()
with mock.patch("subprocess.check_call") as check_call, \
mock.patch("litex_setup.pip_install_externally_managed", return_value=False):
litex_setup.litex_setup_install_repos(config="minimal")
_cmd, kwargs = check_call.call_args
env = kwargs["env"]
pythonpath = env["PYTHONPATH"].split(os.pathsep)
self.assertEqual(pythonpath[0], os.path.join(self.workspace, "litex"))
def test_toolchain_command_failure_has_actionable_error(self):
with mock.patch("subprocess.check_call", side_effect=subprocess.CalledProcessError(1, ["apt"])), \
mock.patch.object(sys, "platform", "linux"):
output, stderr = self.assert_setup_error(
litex_setup.toolchain_install_cmd,
"RISC-V",
["apt", "install", "gcc-riscv64-unknown-elf"],
)
self.assertIn("RISC-V GCC toolchain could not be installed.", output)
self.assertIn("apt install gcc-riscv64-unknown-elf", output)
self.assertIn("sudo/root privileges", output)
self.assertNotIn("Traceback", output + stderr)
def test_toolchain_missing_package_manager_has_actionable_error(self):
with mock.patch("subprocess.check_call", side_effect=FileNotFoundError):
output, stderr = self.assert_setup_error(
litex_setup.toolchain_install_cmd,
"RISC-V",
["missing-package-manager", "install", "gcc"],
)
self.assertIn("missing-package-manager was not found", output)
self.assertIn("supported package manager", output)
self.assertNotIn("Traceback", output + stderr)
def test_toolchain_unsupported_platform_is_clean_error(self):
with mock.patch.object(sys, "platform", "unknown-os"):
output, stderr = self.assert_setup_error(litex_setup.riscv_gcc_install)
self.assertIn("RISC-V GCC requires manual installation on unknown-os.", output)
self.assertNotIn("Traceback", output + stderr)
def test_riscv_toolchain_uses_homebrew_package_on_macos(self):
with mock.patch.object(sys, "platform", "darwin"), \
mock.patch("subprocess.check_call") as check_call:
litex_setup.riscv_gcc_install()
check_call.assert_called_once_with(["brew", "install", "riscv64-elf-gcc"])
def test_mips_toolchain_uses_ubuntu_mipsel_package(self):
with mock.patch.object(sys, "platform", "linux"), \
mock.patch("litex_setup._read_os_release", return_value="ubuntu"), \
mock.patch("subprocess.check_call") as check_call:
litex_setup.mips_gcc_install()
check_call.assert_called_once_with(["apt", "install", "gcc-mipsel-linux-gnu"])
def test_mips_toolchain_uses_arch_mipsel_package(self):
with mock.patch.object(sys, "platform", "linux"), \
mock.patch("litex_setup._read_os_release", return_value="arch linux"), \
mock.patch("subprocess.check_call") as check_call:
litex_setup.mips_gcc_install()
check_call.assert_called_once_with(["yay", "-S", "mipsel-linux-gnu-gcc"])
def test_lm32_toolchain_uses_arch_package(self):
with mock.patch.object(sys, "platform", "linux"), \
mock.patch("litex_setup._read_os_release", return_value="arch linux"), \
mock.patch("subprocess.check_call") as check_call:
litex_setup.lm32_gcc_install()
check_call.assert_called_once_with(["pacman", "-S", "lm32-elf-gcc"])
def test_lm32_toolchain_uses_conda_fallback(self):
with mock.patch.object(sys, "platform", "linux"), \
mock.patch("litex_setup._read_os_release", return_value="ubuntu"), \
mock.patch("shutil.which", side_effect=[None, "/opt/conda/bin/conda"]), \
mock.patch("subprocess.check_call") as check_call:
litex_setup.lm32_gcc_install()
check_call.assert_called_once_with([
"/opt/conda/bin/conda",
"install",
"-y",
"-c",
"litex-hub",
"-c",
"conda-forge",
"gcc-lm32-elf-newlib",
])
def test_lm32_toolchain_without_package_manager_has_hint(self):
with mock.patch.object(sys, "platform", "linux"), \
mock.patch("litex_setup._read_os_release", return_value="ubuntu"), \
mock.patch("shutil.which", return_value=None):
output, stderr = self.assert_setup_error(litex_setup.lm32_gcc_install)
self.assertIn("LM32 GCC requires manual installation on linux.", output)
self.assertIn("conda install -c litex-hub -c conda-forge gcc-lm32-elf-newlib", output)
self.assertNotIn("Traceback", output + stderr)
def test_invalid_config_is_rejected_cleanly(self):
litex_setup.install_configs = {"minimal": [], "standard": [], "full": []}
output = io.StringIO()
with contextlib.redirect_stdout(output):
with self.assertRaises(litex_setup.SetupError):
litex_setup.litex_setup_validate_config("unknown")
self.assertIn("unknown is not a valid install config.", output.getvalue())
self.assertIn("Available configs: minimal, standard, full", output.getvalue())
def test_invalid_gcc_choice_is_rejected_by_argparse(self):
stdout = io.StringIO()
stderr = io.StringIO()
with mock.patch.object(sys, "argv", ["litex_setup.py", "--gcc", "invalid"]), \
contextlib.redirect_stdout(stdout), \
contextlib.redirect_stderr(stderr):
with self.assertRaises(SystemExit) as cm:
litex_setup.main()
self.assertEqual(cm.exception.code, 2)
self.assertIn("invalid choice", stderr.getvalue())
def test_run_exits_cleanly_on_setup_error(self):
with mock.patch("litex_setup.main", side_effect=litex_setup.SetupError):
with self.assertRaises(SystemExit) as cm:
litex_setup.run()
self.assertEqual(cm.exception.code, 1)
def test_run_reports_keyboard_interrupt(self):
output = io.StringIO()
with mock.patch("litex_setup.main", side_effect=KeyboardInterrupt), \
contextlib.redirect_stdout(output):
with self.assertRaises(SystemExit) as cm:
litex_setup.run()
self.assertEqual(cm.exception.code, 130)
self.assertIn("Cancelled.", output.getvalue())
if __name__ == "__main__":
unittest.main()