Bug 1651624 - Add a macOS layer r=sparky

Adds a macOS layer that provides the ability to
mount DMGs on the fly. For instance Firefox's distribution.

Differential Revision: https://phabricator.services.mozilla.com/D84162
This commit is contained in:
Tarek Ziadé 2020-07-21 12:51:58 +00:00
parent 6094408d95
commit cb88926dd6
7 changed files with 226 additions and 11 deletions

View File

@ -82,21 +82,25 @@ class PerftestTests(MachCommandBase):
reimporting modules and produce wrong coverage info.
"""
display = kw.pop("display", False)
verbose = kw.pop("verbose", False)
args = [self.virtualenv_manager.python_path, "-m", module] + list(args)
sys.stdout.write("=> %s " % kw.pop("label", module))
sys.stdout.flush()
try:
if verbose:
sys.stdout.write("\nRunning %s\n" % " ".join(args))
sys.stdout.flush()
output = subprocess.check_output(args, stderr=subprocess.STDOUT)
if display:
print()
sys.stdout.write("\n")
for line in output.split(b"\n"):
print(line.decode("utf8"))
sys.stdout.write(line.decode("utf8") + "\n")
sys.stdout.write("[OK]\n")
sys.stdout.flush()
return True
except subprocess.CalledProcessError as e:
for line in e.output.split(b"\n"):
print(line.decode("utf8"))
sys.stdout.write(line.decode("utf8") + "\n")
sys.stdout.write("[FAILED]\n")
sys.stdout.flush()
return False
@ -125,6 +129,7 @@ class PerftestTests(MachCommandBase):
from mozperftest.utils import install_package, temporary_env
skip_linters = kwargs.get("skip_linters", False)
verbose = kwargs.get("verbose", False)
# include in sys.path all deps
_setup_path()
@ -198,7 +203,9 @@ class PerftestTests(MachCommandBase):
options,
tests,
]
assert self._run_python_script("coverage", *args, label="running tests")
assert self._run_python_script(
"coverage", *args, label="running tests", verbose=verbose
)
if run_coverage_check and not self._run_python_script(
"coverage", "report", display=True
):

View File

@ -5,15 +5,16 @@ from mozperftest.layers import Layers
from mozperftest.system.proxy import ProxyRunner
from mozperftest.system.android import AndroidDevice
from mozperftest.system.profile import Profile
from mozperftest.system.macos import MacosDevice
def get_layers():
return Profile, ProxyRunner, AndroidDevice
return Profile, ProxyRunner, AndroidDevice, MacosDevice
def pick_system(env, flavor, mach_cmd):
if flavor in ("desktop-browser", "xpcshell"):
return Layers(env, mach_cmd, (Profile, ProxyRunner,))
return Layers(env, mach_cmd, (MacosDevice, Profile, ProxyRunner,))
if flavor == "mobile-browser":
return Layers(env, mach_cmd, (Profile, ProxyRunner, AndroidDevice))
raise NotImplementedError(flavor)

View File

@ -0,0 +1,116 @@
# 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 http://mozilla.org/MPL/2.0/.
import platform
import tempfile
import subprocess
from pathlib import Path
import shutil
import os
from mozperftest.layers import Layer
# Add here any option that might point to a DMG file we want to extract. The key
# is name of the option and the value, the file in the DMG we want to use for
# the option.
POTENTIAL_DMGS = {"browsertime-binary": "Contents/MacOS/firefox"}
class MacosDevice(Layer):
"""Runs on macOS to mount DMGs if we see one.
"""
name = "macos"
activated = platform.system() == "Darwin"
def __init__(self, env, mach_cmd):
super(MacosDevice, self).__init__(env, mach_cmd)
self._tmp_dirs = []
def _run_process(self, args):
p = subprocess.Popen(
args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
stdout, stderr = p.communicate(timeout=15)
if p.returncode != 0:
raise subprocess.CalledProcessError(
stdout=stdout, stderr=stderr, returncode=p.returncode
)
return stdout
def extract_app(self, dmg, target):
mount = Path(tempfile.mkdtemp())
# mounting the DMG with hdiutil
cmd = f"hdiutil attach -nobrowse -mountpoint {str(mount)} {dmg}"
try:
self._run_process(cmd.split())
except subprocess.CalledProcessError:
self.error(f"Can't mount {dmg}")
if mount.exists():
shutil.rmtree(str(mount))
raise
# browse the mounted volume, to look for the app.
found = False
try:
for f in os.listdir(str(mount)):
if not f.endswith(".app"):
continue
app = mount / f
shutil.copytree(str(app), str(target))
found = True
break
finally:
try:
self._run_process(f"hdiutil detach {str(mount)}".split())
except subprocess.CalledProcessError as e: # noqa
self.warning("Detach failed {e.stdout}")
finally:
if mount.exists():
shutil.rmtree(str(mount))
if not found:
self.error(f"No app file found in {dmg}")
raise IOError(dmg)
def run(self, metadata):
# Each DMG is mounted, then we look for the .app
# directory in it, which is copied in a directory
# alongside the .dmg file. That directory
# is removed during teardown.
for option, path_in_dmg in POTENTIAL_DMGS.items():
value = self.get_arg(option)
if value is None or not value.endswith(".dmg"):
continue
self.info(f"Mounting {value}")
dmg_file = Path(value)
if not dmg_file.exists():
raise FileNotFoundError(str(dmg_file))
# let's unpack the DMG in place...
target = dmg_file.parent / dmg_file.name.split(".")[0]
self._tmp_dirs.append(target)
self.extract_app(dmg_file, target)
# ... find a specific file if needed ...
path = target / path_in_dmg
if not path.exists():
raise FileNotFoundError(str(path))
# ... and swap the browsertime argument
self.info(f"Using {path} for {option}")
self.env.set_arg(option, str(path))
return metadata
def teardown(self):
for dir in self._tmp_dirs:
if dir.exists():
shutil.rmtree(str(dir))

Binary file not shown.

View File

@ -15,6 +15,7 @@ EXAMPLE_TESTS_DIR = os.path.join(HERE, "data", "samples")
EXAMPLE_TEST = os.path.join(EXAMPLE_TESTS_DIR, "perftest_example.js")
EXAMPLE_XPCSHELL_TEST = Path(EXAMPLE_TESTS_DIR, "test_xpcshell.js")
BT_DATA = Path(HERE, "data", "browsertime-results", "browsertime.json")
DMG = Path(HERE, "data", "firefox.dmg")
@contextlib.contextmanager

View File

@ -0,0 +1,93 @@
#!/usr/bin/env python
import mozunit
from unittest import mock
import platform
import subprocess
import pytest
from pathlib import Path
import os
from mozperftest.tests.support import get_running_env, DMG
from mozperftest.system.macos import MacosDevice
def run_proc(*args, **kw):
if args[0][1] == "attach":
where = args[0][4]
bindir = Path(where, "firefox.app", "Contents", "MacOS")
os.makedirs(str(bindir))
firefox_bin = bindir / "firefox"
with firefox_bin.open("w") as f:
f.write("OK")
def mock_calls(test):
# on macOS we don't mock the system calls
# so we're mounting for real using hdiutil
if platform.system() == "Darwin":
return test
# on other platforms, we're unsing run_proc
@mock.patch("mozperftest.system.macos.MacosDevice._run_process", new=run_proc)
def wrapped(*args, **kw):
return test(*args, **kw)
@mock_calls
def test_mount_dmg():
mach_cmd, metadata, env = get_running_env(browsertime_binary=str(DMG))
device = MacosDevice(env, mach_cmd)
try:
device.run(metadata)
finally:
device.teardown()
target = Path(DMG.parent, "firefox", "Contents", "MacOS", "firefox")
assert env.get_arg("browsertime-binary") == str(target)
def run_fail(cmd):
def _run_fail(self, args):
run_cmd = " ".join(args)
if cmd not in run_cmd:
run_proc(args)
return
raise subprocess.CalledProcessError(returncode=2, cmd=" ".join(args))
return _run_fail
@mock.patch("mozperftest.system.macos.MacosDevice._run_process", new=run_fail("attach"))
def test_attach_fails():
mach_cmd, metadata, env = get_running_env(browsertime_binary=str(DMG))
device = MacosDevice(env, mach_cmd)
with pytest.raises(subprocess.CalledProcessError):
try:
device.run(metadata)
finally:
device.teardown()
@mock.patch("mozperftest.system.macos.MacosDevice._run_process", new=run_fail("detach"))
def test_detach_fails():
mach_cmd, metadata, env = get_running_env(browsertime_binary=str(DMG))
device = MacosDevice(env, mach_cmd)
# detaching will be swallowed
try:
device.run(metadata)
finally:
device.teardown()
target = Path(DMG.parent, "firefox", "Contents", "MacOS", "firefox")
assert env.get_arg("browsertime-binary") == str(target)
def test_no_op():
mach_cmd, metadata, env = get_running_env(browsertime_binary="notadmg")
device = MacosDevice(env, mach_cmd)
device.run(metadata)
if __name__ == "__main__":
mozunit.main()

View File

@ -25,18 +25,15 @@ domcount:
cron: true
run:
command: >-
mkdir -p ${MOZ_FETCHES_DIR}/firefox &&
hdiutil attach -nobrowse -noautoopen -mountpoint ${MOZ_FETCHES_DIR}/firefox ${MOZ_FETCHES_DIR}/target.dmg &&
mkdir -p $MOZ_FETCHES_DIR/../artifacts &&
cd $MOZ_FETCHES_DIR &&
python3 -m venv . &&
bin/python3 python/mozperftest/mozperftest/runner.py
browser/base/content/test/perftest_browser_xhtml_dom.js
--browsertime-binary ${MOZ_FETCHES_DIR}/firefox/Firefox\ Nightly.app/Contents/MacOS/firefox
--browsertime-binary ${MOZ_FETCHES_DIR}/target.dmg
--browsertime-node ${MOZ_FETCHES_DIR}/node/bin/node
--flavor desktop-browser
--perfherder
--perfherder-metrics name:totalDOMCount,unit:count name:panelMenuCount,unit:count name:lightDOMCount,unit:count name:lightDOMDetails,unit:count
--browsertime-geckodriver ${MOZ_FETCHES_DIR}/geckodriver
--output $MOZ_FETCHES_DIR/../artifacts &&
hdiutil detach ${MOZ_FETCHES_DIR}/firefox -force
--output $MOZ_FETCHES_DIR/../artifacts