# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
"""Homebrew-specific tests."""
from __future__ import annotations
import subprocess
from pathlib import Path
import pytest
from click_extra.testing import env_copy
from click_extra.tests.conftest import unless_macos
[docs]@pytest.fixture()
def install_cask():
"""A fixture to install a Cask from a specific commit."""
packages = set()
"""List of installed packages."""
def get_cask_tap_path():
"""Default location of Homebrew Cask formulas on macOS.
This is supposed to be a `shallow copy of the official cask repository
<https://github.com/Homebrew/homebrew-cask>`_.
The later can be obtained with:
.. code-block:: shell-session
$ export HOMEBREW_NO_INSTALL_FROM_API="1"
$ brew tap homebrew/cask
"""
process = subprocess.run(
("brew", "--repository"),
capture_output=True,
encoding="utf-8",
)
assert process.returncode == 0
assert not process.stderr
assert process.stdout
brew_root = Path(process.stdout.strip())
assert brew_root.is_absolute()
assert brew_root.exists()
assert brew_root.is_dir()
cask_repo = brew_root.joinpath("Library/Taps/homebrew/homebrew-cask/Casks/")
assert cask_repo.is_absolute()
assert cask_repo.exists()
assert cask_repo.is_dir()
return cask_repo
def get_cask_folder(package_id: str):
"""Get the folder where a Cask is located.
..warning::
As of `aa46114 <https://github.com/Homebrew/homebrew-cask/commit/aa46114>`_,
casks have been moved to a subfolder named after their first letter.
"""
cask_folder = get_cask_tap_path().joinpath(package_id[0])
assert cask_folder.is_absolute()
assert cask_folder.exists()
assert cask_folder.is_dir()
return cask_folder
def git_checkout(package_id: str, commit: str):
process = subprocess.run(
(
"git",
"-C",
get_cask_folder(package_id),
"checkout",
commit,
f"{package_id}.rb",
),
)
assert not process.stderr
assert process.returncode == 0
def brew_cleanup():
"""Remove all downloads cached by Homebrew.
.. note::
``brew`` does not cleanup ``~/Library/Caches/Homebrew``, see:
https://github.com/Homebrew/brew/issues/3784#issuecomment-364675767
We might need to force it with:
.. code-block:: shell-session
$ rm -rf $(brew --cache)
"""
process = subprocess.run(
("brew", "cleanup", "-s", "--prune=all"),
env=env_copy({"HOMEBREW_NO_AUTO_UPDATE": "1"}),
)
assert not process.stderr
assert process.returncode == 0
def brew_uninstall(package_id):
process = subprocess.run(("brew", "uninstall", "--cask", "--force", package_id))
assert not process.stderr
assert process.returncode == 0
def _install_cask(package_id, commit):
packages.add(package_id)
# Fetch locally the old version of the Cask's formula.
git_checkout(package_id, commit)
brew_cleanup()
# XXX Work around the bad old ubersicht cask formula.
# Replace ":yosemite" by ":el_capitan" like the last commit did at:
# https://github.com/Homebrew/homebrew-cask/commit/5cddbd6
# The day we have a new version of ubersicht after this commit, we will be able
# to remove that hack, and have the install_cask() invocation from the
# test_autoupdate_unicode_name() test below checkout ubersicht from that commit.
if package_id == "ubersicht":
cask_path = get_cask_tap_path().joinpath(f"{package_id}.rb")
content = cask_path.read_text()
cask_path.write_text(content.replace(":yosemite", ":el_capitan"))
# Install the cask but bypass its local cache auto-update: we want to
# force brew to rely on our formula from the past.
process = subprocess.run(
("brew", "reinstall", "--cask", package_id),
capture_output=True,
encoding="utf-8",
check=True,
env=env_copy(
{
# Do not let brew use its live API to fetch the latest version.
# This variable forces brew to use the local repository instead:
# https://github.com/Homebrew/brew/blob/10845a1/Library/Homebrew/env_config.rb#L314-L317
"HOMEBREW_NO_INSTALL_FROM_API": "1",
"HOMEBREW_NO_AUTO_UPDATE": "1",
"HOMEBREW_NO_ENV_HINTS": "1",
},
),
)
# Restore old formula to its most recent version.
git_checkout(package_id, "HEAD")
brew_cleanup()
# Check the cask has been properly installed.
if process.stderr:
assert "is already installed" not in process.stderr
assert f"{package_id} was successfully installed!" in process.stdout
return process.stdout
yield _install_cask
# Remove all installed packages.
for package_id in packages:
brew_uninstall(package_id)
[docs]@pytest.mark.destructive()
@unless_macos
# XXX The layout of the cask repository has changed, so we cannot go back in time too
# much and checkout old versions of casks. We need to wait for a couple of new releases
# of the casks we use in our tests, so we can have enough commit depth to test the
# upgrade process.
@pytest.mark.skip(
reason="Cask repository structure changed too much to go back in time."
)
class TestCask:
[docs] @pytest.mark.xdist_group(name="avoid_concurrent_git_tweaks")
def test_autoupdate_unicode_name(self, invoke, install_cask):
"""See #16."""
# Install an old version of a package with a unicode name.
# Old Cask formula for ubersicht 1.6.69.
# XXX Update this commit when a new version of ubersicht is released so we can
# get rid of the hack above.
output = install_cask("ubersicht", "9870e8ffaa8dc83403973580415b2c56dc760f15")
assert "Uebersicht-1.6.69.app.zip" in output
assert "Übersicht.app" in output
assert "Übersicht.app" not in output
# Ubersicht is not reported as outdated because is tagged as
# auto-update.
result = invoke("--cask", "outdated")
assert result.exit_code == 0
assert "ubersicht" not in result.stdout
assert "Übersicht" not in result.stdout
# Try with explicit option.
result = invoke("--ignore-auto-updates", "--cask", "outdated")
assert result.exit_code == 0
assert "ubersicht" not in result.stdout
assert "Übersicht" not in result.stdout
# Look for reported available upgrade.
result = invoke("--include-auto-updates", "--cask", "outdated")
assert result.exit_code == 0
assert "ubersicht" in result.stdout
# Outdated subcommand does not fetch the unicode name by default.
assert "Übersicht" not in result.stdout
[docs] @pytest.mark.xdist_group(name="avoid_concurrent_git_tweaks")
def test_multiple_names(self, invoke, install_cask):
"""See #26."""
# Install an old version of a package with multiple names.
# Old Cask formula for xld 20210101,153.1.
output = install_cask("xld", "6be43816ea729c18219322f656c62a702e1c2440")
assert "xld-20210101.dmg" in output
assert "XLD.app" in output
# Look for reported available upgrade.
result = invoke("--include-auto-updates", "--cask", "outdated")
assert result.exit_code == 0
assert "xld" in result.stdout
# Outdated subcommand does not fetch the unicode name by default.
assert "X Lossless Decoder" not in result.stdout