Files
brave-core/updater/install_sh_mac_test.py
T

164 lines
6.1 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright (c) 2026 The Brave Authors. All rights reserved.
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at https://mozilla.org/MPL/2.0/.
"""
This script tests Brave's customizations to chrome/updater/mac/.install.sh.
You can run it with `python3 install_sh_mac_test.py`.
"""
from os import chmod, makedirs, mkdir, stat
from os.path import dirname, exists, join, realpath
from stat import S_IXUSR
from subprocess import run
from tempfile import TemporaryDirectory
import plistlib
import re
import sys
import unittest
SRC_ROOT = dirname(dirname(dirname(realpath(__file__))))
INSTALL_SH = join(SRC_ROOT, "chrome", "updater", "mac", ".install.sh")
PRODUCT_NAME = "Brave Browser"
CURRENT_VERSION = "1.0.0.0"
UPDATE_VERSION = "2.0.0.0"
@unittest.skipUnless(sys.platform == "darwin", "requires macOS")
class InstallShPatchTest(unittest.TestCase):
def setUp(self):
self.temp_dir = TemporaryDirectory()
self.dmg_dir = self._prepare_dmg_dir()
self.install_sh, self.rsync_log = self._prepare_install_sh()
def tearDown(self):
self.temp_dir.cleanup()
def test_nonstandard_app_dir_name(self):
app_dir = join(self.temp_dir.name, "User Renamed Brave.app")
self._make_app(app_dir, CURRENT_VERSION)
self._run_install_sh(app_dir)
self._check_app(app_dir, UPDATE_VERSION)
def test_nonroot_omits_perms_and_link_and_dir_times(self):
# See Chromium CL 5866112.
app_dir = join(self.temp_dir.name, f"{PRODUCT_NAME}.app")
self._make_app(app_dir, CURRENT_VERSION)
self._run_install_sh(app_dir)
rsync_args = self._get_rsync_args()
for arg in ('--ignore-times', '--links', '--no-perms',
'--executability', '--chmod=u=rwX,go=rX'):
self.assertIn(arg, rsync_args)
for arg in ('--perms', '--times'):
self.assertNotIn(arg, rsync_args)
def test_root_has_perms_and_link_and_dir_times(self):
app_dir = join(self.temp_dir.name, f"{PRODUCT_NAME}.app")
self._make_app(app_dir, CURRENT_VERSION)
self._run_install_sh(app_dir, is_root=True)
rsync_args = self._get_rsync_args()
for arg in ('--ignore-times', '--links', '--perms', '--times'):
self.assertIn(arg, rsync_args)
for arg in ('--no-perms', '--executability', '--chmod=u=rwX,go=rX'):
self.assertNotIn(arg, rsync_args)
def _prepare_dmg_dir(self):
dmg_dir = join(self.temp_dir.name, "dmg")
mkdir(dmg_dir)
self._make_app(join(dmg_dir, f"{PRODUCT_NAME}.app"), UPDATE_VERSION)
return dmg_dir
def _prepare_install_sh(self):
with open(INSTALL_SH, "r") as f:
source = f.read()
patched, count = re.subn(r"^UPDATE_VERSION=\s*$",
f'UPDATE_VERSION="{UPDATE_VERSION}"',
source,
count=1,
flags=re.MULTILINE)
self.assertEqual(1, count)
bin_dir = join(self.temp_dir.name, "bin")
mkdir(bin_dir)
rsync_wrapper = join(bin_dir, "rsync")
rsync_log_path = join(self.temp_dir.name, "rsync_args.log")
with open(rsync_wrapper, "w") as f:
f.write("#!/bin/bash\n"
f'printf "%s\\n" "$@" >> "{rsync_log_path}"\n'
'exec /usr/bin/rsync "$@"\n')
chmod(rsync_wrapper, stat(rsync_wrapper).st_mode | S_IXUSR)
# Add bin/ to PATH so tests can override commands like rsync.
patched, count = re.subn(r'^export PATH="',
f'export PATH="{bin_dir}:',
patched,
count=1,
flags=re.MULTILINE)
self.assertEqual(1, count)
install_sh_path = join(self.temp_dir.name, "install.sh")
with open(install_sh_path, "w") as f:
f.write(patched)
chmod(install_sh_path, stat(install_sh_path).st_mode | S_IXUSR)
return install_sh_path, rsync_log_path
def _make_app(self, bundle_path, version):
"""Create the minimum .app bundle that .install.sh's checks accept."""
contents = join(bundle_path, "Contents")
framework_dir = join(contents, "Frameworks",
f"{PRODUCT_NAME} Framework.framework")
makedirs(join(framework_dir, "Versions", version, "Resources"))
with open(join(contents, "Info.plist"), "wb") as f:
plistlib.dump(
{
"CFBundleDisplayName": PRODUCT_NAME,
"CFBundleShortVersionString": version,
"CFBundleExecutable": PRODUCT_NAME,
},
f,
)
def _check_app(self, bundle_path, version):
"""Assert a .app bundle has the structure _make_app would create."""
with open(join(bundle_path, "Contents", "Info.plist"), "rb") as f:
plist = plistlib.load(f)
self.assertEqual(version, plist["CFBundleShortVersionString"])
versioned_dir = join(
bundle_path,
"Contents",
"Frameworks",
f"{PRODUCT_NAME} Framework.framework",
"Versions",
version,
)
self.assertTrue(exists(versioned_dir), msg=versioned_dir)
def _run_install_sh(self, installed_app_dir, is_root=False):
env = {}
if is_root:
env["EUID"] = "0"
cp = run(
[
self.install_sh, self.dmg_dir, installed_app_dir,
CURRENT_VERSION
],
capture_output=True,
text=True,
timeout=60,
env=env,
check=False,
)
self.assertEqual(
0, cp.returncode,
f"Command {self.install_sh} failed with return code "
f"{cp.returncode}.\n\nStdout:\n{cp.stdout}\nStderr:\n{cp.stderr}")
def _get_rsync_args(self):
with open(self.rsync_log, "r") as f:
return re.split(r"\s+", f.read())
if __name__ == "__main__":
unittest.main()