Commits (5)
......@@ -16,18 +16,16 @@ instance
### Heptapod install
*Changed for Heptapod 0.8 (SSH support)*
It's currently not possible to configure details about the heptapod install
(URL, passwords etc)
- Make sure the `heptapod` host name resolves to something (typically a
host-only IP address such as from the system running the tests.
- Run a fresh Docker container named `heptapod`, answering on
`http://heptapod:81` and on `ssh://heptapod:2022`.
The tests will issue `docker exec` commands, HTTP requests and SSH
- The Gitlab root password will be initialized by the test.
- Run `ssh -p 2022 heptapod` once, to accept the server key. This will
be relaxed in future versions of these tests.
- Run a fresh Docker container named `heptapod`, answering on
`http://heptapod:81` and on `ssh://heptapod:2022` (see `Configuration` below
if you want to change these defaults).
- The tests will issue `docker exec` commands, HTTP requests and SSH
- The Gitlab root password will be initialized by the first test to run.
The tests will fail if the Gitlab root password is already set
and does not match the expected one.
## Running the tests
......@@ -36,6 +34,31 @@ issue Docker commands.
This will test the current state of your Heptapod container.
### Configuration
It is possible to pass down options to the underlying `pytest` command:
tox -- --heptapod-url URL
Here are the custom options for these tests:
- `--heptapod-url` (default `http://heptapod:81`): HTTP URL of the tested
Heptapod instance. It must be a resolvable host *name*, not an IP address.
It does not have to be resolved through DNS, an `/etc/host` entry pointing
to the loopback interface is fine. **Never, ever run these tests on an
Heptapod instance that's not entirely meant for this**
- `--heptapod-ssh-port` (default 2022): SSH port of the tested Heptapod
instance. The same host name will be used as for HTTP. If the host name
resolves to the loopback interface, it is advised to tie it to a dedicated
address, such as ``, to minimize risks with your SSH
`known_hosts` file.
- `--heptapod-container-name` (default `heptapod`)
- `--heptapod-root-password` (default `5iveL!fe`). The password to use and maybe
set for the `root` user. The default value is the same as with the GitLab
Development Kit (GDK).
### Choosing the version to test
The versions installed in the Docker image you're using are specified by the
......@@ -12,6 +12,7 @@ import time
from tests.utils.project import Project
from tests.utils.user import User
from tests.utils import docker
from tests.utils import session
INITIAL_TIMEOUT = 300 # seconds
......@@ -30,6 +31,8 @@ def pytest_addoption(parser):
"""Add command-line options for Heptapod host."""
parser.addoption('--heptapod-url', default='http://heptapod:81')
parser.addoption('--heptapod-ssh-port', type=int, default=2022)
parser.addoption('--heptapod-docker-container', default='heptapod')
parser.addoption('--heptapod-root-password', default='5iveL!fe')
......@@ -37,6 +40,7 @@ class Heptapod(object):
url = attr.ib()
parsed_url = attr.ib()
ssh_port = attr.ib()
docker_container = attr.ib()
users = attr.ib()
dead = attr.ib(default=None) # None means we don't know yet
......@@ -69,7 +73,7 @@ class Heptapod(object):
def basic_user_token_headers(self):
return {'Private-Token': self.users['test_basic']['token']}
def prepare(self):
def prepare(self, root_password):
"""Make all preparations for the Heptapod instance to be testable.
This currently amounts to
......@@ -113,7 +117,7 @@ class Heptapod(object):
driver = selenium.webdriver.Chrome(options=options)
# guaranteeing driver to be available for teardown as soon as created
root['webdriver'] = driver
session.login_as_root(driver, self)
session.login_as_root(driver, self, root_password)
basic, driver = session.ensure_user(self, 'test_basic',
......@@ -166,6 +170,22 @@ class Heptapod(object):
if driver is not None:
def execute(self, command, user='root'):
return docker.heptapod_exec(self.docker_container, command, user=user)
def run_shell(self, command):
return docker.heptapod_run_shell(self.docker_container, command)
def put_archive(self, dest, path):
return docker.heptapod_put_archive(self.docker_container, dest, path)
def put_archive_bin(self, dest, fobj):
return docker.heptapod_put_archive_bin(
self.docker_container, dest, fobj)
def get_archive(self, path, tarf):
return docker.heptapod_get_archive(self.docker_container, path, tarf)
def heptapod(pytestconfig):
......@@ -174,9 +194,10 @@ def heptapod(pytestconfig):
yield heptapod
import requests
from .utils import docker
from .utils.project import Project
from .utils.group import Group
from .utils.hg import LocalRepo
......@@ -14,11 +13,12 @@ def test_webdriver_destroy(test_project):
exit_code = docker.heptapod_exec(
'ls -d ' + test_project.docker_fs_common_path + '.hg')[0]
heptapod = test_project.heptapod
exit_code = heptapod.execute(
'ls -d ' + test_project.fs_common_path + '.hg')[0]
assert exit_code == 2
exit_code = docker.heptapod_exec(
'ls -d ' + test_project.docker_fs_common_path + '.wiki.hg')[0]
exit_code = heptapod.execute(
'ls -d ' + test_project.fs_common_path + '.wiki.hg')[0]
assert exit_code == 2
......@@ -4,8 +4,8 @@ import sys
_client = docker.from_env()
def heptapod_exec(command, user='root'):
container = _client.containers.get('heptapod')
def heptapod_exec(ct, command, user='root'):
container = _client.containers.get(ct)
exit_code, output = container.exec_run(command, tty=True, demux=True,
sys.stdout.write('+ docker exec heptapod {command}\n'.format(
......@@ -20,8 +20,8 @@ def heptapod_exec(command, user='root'):
return exit_code, out
def heptapod_run_shell(command):
exit_code, output = heptapod_exec(command)
def heptapod_run_shell(ct, command):
exit_code, output = heptapod_exec(ct, command)
if exit_code:
raise RuntimeError(('Heptapod command {command} returned a non-zero '
'exit code {exit_code}').format(
......@@ -30,19 +30,19 @@ def heptapod_run_shell(command):
def heptapod_put_archive(dest, path):
def heptapod_put_archive(ct, dest, path):
"""Put the tar archive at path at given dest in container."""
container = _client.containers.get('heptapod')
container = _client.containers.get(ct)
with open(path, 'rb') as arf:
container.put_archive(dest, arf.read())
def heptapod_put_archive_bin(dest, fobj):
def heptapod_put_archive_bin(ct, dest, fobj):
"""Put the tar archive of file-like `fobj` at given dest in container."""
_client.containers.get('heptapod').put_archive(dest, fobj.read())
_client.containers.get(ct).put_archive(dest, fobj.read())
def heptapod_get_archive(path, tarf):
def heptapod_get_archive(ct, path, tarf):
"""Get the file or directory at path as a tar archive.
The tar binary contents is written to the tarf file-like object and
......@@ -52,7 +52,7 @@ def heptapod_get_archive(path, tarf):
:param archive_path: path to retrieve inside the Heptapod container
:returns: dict of stats.
container = _client.containers.get('heptapod')
container = _client.containers.get(ct)
bits, stats = container.get_archive(path)
for chunk in bits:
......@@ -4,8 +4,6 @@ import tarfile
from io import BytesIO
from . import docker
class UserNameSpace:
......@@ -96,7 +94,7 @@ class Group:
The lines have to include LF, same as with `writelines()`.
# TODO factorize a Docker method to put a file from lines.
# TODO factorize a method to put a file from lines.
inner_path = '/'.join((self.heptapod.repositories_root,
tar_buf = BytesIO()
......@@ -111,4 +109,4 @@ class Group:
tarf.addfile(tinfo, fileobj=contents_buf)
docker.heptapod_put_archive_bin(inner_path, tar_buf)
self.heptapod.put_archive_bin(inner_path, tar_buf)
......@@ -13,7 +13,6 @@ from selenium.common.exceptions import (
from selenium.webdriver.support.ui import WebDriverWait
from . import docker
from .constants import DATA_DIR
from .hg import LocalRepo
from .group import UserNameSpace
......@@ -188,11 +187,16 @@ class Project(object):
def docker_fs_common_path(self):
"""Common path on Docker FS (not ending with .hg nor .git)
def fs_common_path(self):
"""Common path on Heptapod server FS (not ending with .hg nor .git)
return '/'.join((self.heptapod.repositories_root,
self.group.full_path, self.name + '.hg'))
self.group.full_path, self.name))
def fs_path(self):
"""Path to the Mercurial repo on Heptapod server file system."""
return self.fs_common_path + '.hg'
def api_branches(self):
"""Retrieve and pre-sort branches info through the REST API."""
......@@ -311,22 +315,19 @@ class Project(object):
group_path = '/'.join((self.heptapod.repositories_root,
heptapod = self.heptapod
group_path = '/'.join((heptapod.repositories_root,
'rm -rf {group}/{project.name}.hg'.format(
group=group_path, project=self))
'rm -rf {group}/{project.name}.git'.format(
group=group_path, project=self))
docker.heptapod_put_archive(group_path, tarball_path)
docker.heptapod_run_shell('chown -R git:root ' + group_path)
heptapod.run_shell('rm -rf ' + self.fs_path)
heptapod.run_shell('rm -rf ' + self.fs_common_path + '.git')
heptapod.put_archive(group_path, tarball_path)
heptapod.run_shell('chown -R git:root ' + group_path)
def get_hgrc(self):
"""Return repo's server-side HGRC, as lines, uid and gid"""
hgrc_path = '/'.join((self.docker_fs_common_path, '.hg', 'hgrc'))
hgrc_path = '/'.join((self.fs_path, '.hg', 'hgrc'))
buf = BytesIO()
docker.heptapod_get_archive(hgrc_path, buf)
self.heptapod.get_archive(hgrc_path, buf)
tarf = tarfile.open(mode='r:', fileobj=buf)
tinfo = tarf.getmember('hgrc')
......@@ -338,7 +339,7 @@ class Project(object):
The lines have to include LF, same as with `writelines()`.
repo_inner_path = '/'.join((self.docker_fs_common_path, '.hg'))
repo_inner_path = '/'.join((self.fs_path, '.hg'))
tar_buf = BytesIO()
tarf = tarfile.open(mode='w:', fileobj=tar_buf)
......@@ -352,7 +353,7 @@ class Project(object):
tarf.addfile(tinfo, fileobj=contents_buf)
docker.heptapod_put_archive_bin(repo_inner_path, tar_buf)
self.heptapod.put_archive_bin(repo_inner_path, tar_buf)
def extend_hgrc(self, *lines):
"""Append given lines to repo's server-side HGRC
......@@ -374,15 +375,16 @@ class Project(object):
:return: if ``section`` is passed, a simple ``dict``, otherwise a
``dict`` of ``dicts``. End values are always strings.
cmd = ['hg', '-R', self.docker_fs_common_path,
cmd = ['hg', '-R', self.fs_path,
'--pager', 'false',
if section is not None:
code, out = docker.heptapod_exec(cmd, user='git')
code, out = self.heptapod.execute(cmd, user='git')
config = {}
for l in out.splitlines():
fullkey, val = l.split('=', 1)
section, key = fullkey.split('.', 1)
if section is not None:
......@@ -464,9 +466,9 @@ class Project(object):
This is to ensure that other tests can proceed even in face of failure
of Mercurial specific filesystem removal.
repo_path = self.docker_fs_common_path
repo_path = self.fs_path
wiki_path = repo_path[:-3] + '.wiki.hg'
docker.heptapod_run_shell(('rm', '-rf', repo_path, wiki_path))
self.heptapod.run_shell(('rm', '-rf', repo_path, wiki_path))
def webdriver_create(cls, heptapod, user_name, project_name):
......@@ -524,7 +526,7 @@ class Project(object):
"WHERE source_type='Project' "
" AND path='%s'" % route_path)
print("Cleaning up leftover route at %r" % route_path)
'gitlab-psql gitlabhq_production -c "%s"' % sql)
resp = requests.post(url, headers=headers, data=data)
......@@ -4,18 +4,16 @@ from selenium.webdriver.common.keys import Keys
from .selenium import webdriver_wait_get
from .user import default_password, User
ROOT_PASSWORD = 'p4s5w0rd'
def initialize_root(driver, heptapod):
def initialize_root(driver, heptapod, password):
# Create initial password
elem = driver.find_element_by_name('user[password]')
elem = driver.find_element_by_name('user[password_confirmation]')
sign_in_page_login(driver, heptapod, 'root', password=ROOT_PASSWORD)
sign_in_page_login(driver, heptapod, 'root', password=password)
generate_private_token(driver, heptapod, 'root')
......@@ -60,15 +58,15 @@ def sign_in_page_login(driver, heptapod, user, password=None):
def login_as_root(driver, heptapod):
def login_as_root(driver, heptapod, password):
webdriver_wait_get(heptapod, driver)
assert 'GitLab' in driver.title
html = driver.find_element_by_tag_name('html').get_attribute('innerHTML')
if 'Please create a password for your new account.' in html:
return initialize_root(driver, heptapod)
return initialize_root(driver, heptapod, password)
sign_in_page_login(driver, heptapod, 'root', password=ROOT_PASSWORD)
sign_in_page_login(driver, heptapod, 'root', password)
generate_private_token(driver, heptapod, 'root')
......@@ -8,4 +8,4 @@ deps =
commands =
py.test {posargs}