testing/vcttesting/environment.py (242 lines of code) (raw):
# 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 errno
import os
import shutil
import subprocess
import sys
HERE = os.path.abspath(os.path.dirname(__file__))
ROOT = os.path.normpath(os.path.join(HERE, "..", ".."))
CREATE_VIRTUALENV = os.path.join(ROOT, "testing", "create-virtualenv")
SITECUSTOMIZE = b"""
import os
if os.environ.get('CODE_COVERAGE', False):
import uuid
import coverage
covpath = os.path.join(os.environ['COVERAGE_DIR'], 'data',
'coverage.%s' % uuid.uuid1())
cov = coverage.Coverage(data_file=covpath, auto_data=True, branch=True)
cov._warn_no_data = False
cov._warn_unimported_source = False
cov.start()
"""
def create_virtualenv(name=None, python="python"):
path = os.path.join(ROOT, "venv")
env = dict(os.environ)
env["PYTHON_VERSION"] = python
if name:
path = os.path.join(path, name)
try:
os.makedirs(os.path.dirname(path))
except OSError as e:
if e.errno != errno.EEXIST:
raise
if os.name == "nt":
bin_dir = os.path.join(path, "Scripts")
pip = os.path.join(bin_dir, "pip.exe")
python = os.path.join(bin_dir, python + ".exe")
activate = os.path.join(bin_dir, "activate")
else:
bin_dir = os.path.join(path, "bin")
pip = os.path.join(bin_dir, "pip")
python = os.path.join(bin_dir, python)
activate = os.path.join(bin_dir, "activate")
res = {
"path": path,
"bin_dir": bin_dir,
"pip": pip,
"python": python,
"activate": activate,
"activate_this": os.path.join(bin_dir, "activate_this.py"),
}
env["ROOT"] = ROOT
env["VENV"] = path
if not os.path.exists(res["pip"]):
subprocess.check_call([CREATE_VIRTUALENV, path], env=env)
# Install a sitecustomize.py that starts code coverage if an environment
# variable is set.
with open(os.path.join(bin_dir, "sitecustomize.py"), "wb") as fh:
fh.write(SITECUSTOMIZE)
return res
def activate_virtualenv(venv):
"""Activate a virtualenv in the current Python process."""
with open(venv["activate_this"]) as f:
exec(f.read(), dict(__file__=venv["activate_this"]))
def process_pip_requirements(venv, requirements):
args = [
venv["pip"],
"install",
"--upgrade",
"--require-hashes",
"-r",
os.path.join(ROOT, requirements),
]
subprocess.check_call(args)
def install_editable(venv, relpath, extra_env=None):
args = [
venv["pip"],
"install",
"--no-deps",
"--editable",
os.path.join(ROOT, relpath),
]
env = dict(os.environ)
env.update(extra_env or {})
subprocess.check_call(args, env=env)
def install_mercurials(venv, hg="hg"):
"""Install supported Mercurial versions in a central location."""
VERSIONS = [
"6.1.4",
"6.2.3",
"6.3.2",
"6.4.3",
"6.5.2",
"6.6.3",
"6.7.4",
"6.8.2",
"6.9.5",
]
hg_dir = os.path.join("/app", "venv", "hg")
mercurials = os.path.join(venv["path"], "mercurials")
# Setting HGRCPATH to an empty value stops the global and user hgrc from
# being loaded. These could interfere with behavior we expect from
# vanilla Mercurial.
hg_env = dict(os.environ)
hg_env["HGRCPATH"] = ""
# Ensure a Mercurial clone is present and up to date.
if not os.path.isdir(hg_dir):
print("cloning Mercurial repository to %s" % hg_dir)
subprocess.check_call(
[hg, "clone", "https://repo.mercurial-scm.org/hg", hg_dir],
cwd="/",
env=hg_env,
)
subprocess.check_call([hg, "pull"], cwd=hg_dir, env=hg_env)
try:
os.makedirs(mercurials)
except OSError as e:
if e.errno != errno.EEXIST:
raise
# Remove old versions.
for p in os.listdir(mercurials):
if p in (".", ".."):
continue
if p not in VERSIONS:
print("removing old, unsupported Mercurial version: %s" % p)
shutil.rmtree(os.path.join(mercurials, p))
for v in VERSIONS:
dest = os.path.join(mercurials, v)
if os.path.exists(dest):
continue
print("installing Mercurial %s to %s" % (v, dest))
try:
subprocess.check_output(
[hg, "update", v], cwd=hg_dir, env=hg_env, stderr=subprocess.STDOUT
)
# We don't care about support files, which only slow down
# installation. So install-bin is a suitable target.
subprocess.check_output(
[
"make",
"install-bin",
"PREFIX=%s" % dest,
"PYTHON=%s" % venv["python"],
],
cwd=hg_dir,
env=hg_env,
stderr=subprocess.STDOUT,
)
subprocess.check_output(
[hg, "--config", "extensions.purge=", "purge", "--all"],
cwd=hg_dir,
env=hg_env,
stderr=subprocess.STDOUT,
)
except subprocess.CalledProcessError as e:
print("error installing: %s" % e.output)
raise Exception("could not install Mercurial")
def docker_client():
"""Attempt to obtain a Docker client.
Returns a client on success. None on failure.
"""
from .docker import (
Docker,
params_from_env,
)
docker_url, tls = params_from_env(os.environ)
d = Docker(docker_url, tls=tls)
return d if d.is_alive() else None
def create_docs():
"""Create environment used for building docs."""
venv = create_virtualenv("docs")
process_pip_requirements(venv, "docs-requirements.txt")
install_editable(venv, "hghooks")
install_editable(venv, "pylib/Bugsy")
install_editable(venv, "pylib/mozhg")
install_editable(venv, "pylib/mozhginfo")
install_editable(venv, "pylib/mozautomation")
install_editable(venv, "testing")
return venv
def create_hgdev():
"""Create an environment used for hacking on Mercurial extensions."""
venv = create_virtualenv("hgdev")
reqs = "testing/requirements-hgdev.txt"
process_pip_requirements(venv, reqs)
install_editable(venv, "hghooks")
install_editable(venv, "pylib/Bugsy")
install_editable(venv, "pylib/mozhg")
install_editable(venv, "pylib/mozhginfo")
install_editable(venv, "pylib/mozautomation")
install_editable(venv, "testing")
install_mercurials(venv, hg=os.path.join(venv["bin_dir"], "hg"))
return venv
def install_cinnabar(dest=None):
"""Install git-cinnabar"""
if not dest:
dest = os.path.join(ROOT, "venv", "git-cinnabar")
if not os.path.exists(dest):
subprocess.check_call(
[
"git",
"clone",
"--branch",
"release",
"https://github.com/glandium/git-cinnabar.git",
dest,
]
)
subprocess.check_call(["git", "pull"], cwd=dest)
subprocess.check_call(
[
"make",
"-j4",
"helper",
"NO_OPENSSL=1",
"NO_GETTEXT=1",
],
cwd=dest,
)
def create_global():
"""Create the global test environment virtualenv
This functions the same as ./create-test-environment
"""
from .hgmo import (
HgCluster,
)
# No `name` parameter since this will be the top-level venv
venv_py3 = create_virtualenv(name="py3", python="python3")
# Install third-party dependencies
process_pip_requirements(venv_py3, "test-requirements-3.txt")
# Install editable packages
editables = {
"hgserver/hgmolib",
"pylib/Bugsy",
"pylib/mozansible",
"pylib/mozhg",
"pylib/mozhginfo",
"pylib/mozautomation",
"pylib/vcsreplicator",
"hghooks",
"testing",
}
for package in editables:
install_editable(venv_py3, package)
install_mercurials(venv_py3)
cinnabar_dest = os.path.join(venv_py3["path"], "git-cinnabar")
install_cinnabar(dest=cinnabar_dest)
if os.getenv("NO_DOCKER"):
print("Not building Docker images because NO_DOCKER is set")
else:
print("Building Docker images.")
print("This could take a while and may consume a lot of internet bandwidth.")
print(
"If you don't want Docker images, it is safe to hit CTRL+c to abort this."
)
try:
HgCluster.build()
except subprocess.CalledProcessError:
print("You will not be able to run tests that require Docker.")
print(
"Please see https://docs.docker.com/installation/ for how to install Docker."
)
print("When Docker is installed, re-run this script")
print(
"To avoid re-building the Docker images next time, set the `NO_DOCKER` "
"environment variable to any value."
)
sys.exit(1)
return venv_py3
if __name__ == "__main__":
import sys
if sys.argv[1] != "install-mercurials":
sys.exit(1)
venv = {
"path": os.path.join("/app", "venv"),
"python": os.path.join("/app", "venv", "bin", "python"),
}
install_mercurials(venv, hg="hg")
sys.exit(0)