Commit 6351e9a2 authored by Georges Racinet's avatar Georges Racinet 🦑
Browse files

Heptapod 0.18 is the new stable

- merge default branch into stable branch
- updated base image in CI configuration

--HG--
branch : stable
Pipeline #16522 passed with stage
in 20 minutes and 6 seconds
variables:
TARGET_HEPTAPOD_IMAGE: "0-17-testing"
TARGET_HEPTAPOD_IMAGE: "0-18-testing"
HEPTAPOD_BASE_IMAGE: registry.heptapod.net/heptapod/omnibus-heptapod
stages:
......
......@@ -213,15 +213,9 @@ def test_implicit_move(test_project, tmpdir):
gl_branches = {
'branch/default': 'Commit on default',
'sliding': 'Commit 2',
'branch/other': 'Commit 2',
}
if not test_project.hg_native:
# not shadowing the branch is a divergence of state
# between hg and git that cannot sanely be reproduced
# in the native case. See heptapod#143 for the origin of this
# non-shadowing feature
gl_branches['branch/other'] = 'Commit 2'
assert test_project.api_branch_titles() == gl_branches
if test_project.hg_native:
......
# -*- coding: utf-8 -*-
from .utils import unique_name
from .utils.hg import LocalRepo
from .utils.hg import (
assert_matching_changesets,
LocalRepo,
)
from .utils.project import Project
def hg_log_set(repo, phase=True, bookmarks=False):
"""Return hg log of repo, as a set.
After an import, no assumption about ordering can be made, whence the set
"""
template = '{desc}:'
if phase:
template += '{phase}:'
template += '{bookmarks}' if bookmarks else '{topic}'
return set(repo.hg('log', '-T', template + '\n').splitlines())
def prepare_import_source(project, tmpdir, tweak_hgrc=True):
"""Prepare given project to be the import source for tests.
......@@ -70,8 +61,9 @@ def test_import_basic(public_project, tmpdir):
assert status == 'finished'
clone = LocalRepo.clone(project.owner_basic_auth_url,
tmpdir.join('repo'))
assert hg_log_set(clone, phase=False) == hg_log_set(src_repo,
phase=False)
assert_matching_changesets(clone, src_repo,
('node', 'desc', 'topic'),
ordered=False)
assert project.api_default_branch() == 'branch/default'
......@@ -98,7 +90,9 @@ def test_import_non_topic_draft(public_project, tmpdir):
assert clone.hg('log', '-T', '{phase}', '-r', 0) == 'draft', (
"draft non-topic changeset should not become public "
"in imported repo")
assert hg_log_set(clone) == hg_log_set(src_repo)
assert_matching_changesets(clone, src_repo,
('node', 'desc', 'topic', 'phase'),
ordered=False)
assert project.api_default_branch() == 'branch/default'
......@@ -128,7 +122,9 @@ def test_import_multiple_heads(public_project, tmpdir):
assert status == 'finished'
clone = LocalRepo.clone(project.owner_basic_auth_url,
tmpdir.join('repo'))
assert hg_log_set(clone) == hg_log_set(src_repo)
assert_matching_changesets(clone, src_repo,
('node', 'desc', 'topic', 'phase'),
ordered=False)
def test_import_bookmarks(public_project, tmpdir):
......@@ -156,5 +152,6 @@ def test_import_bookmarks(public_project, tmpdir):
assert status == 'finished'
clone = LocalRepo.clone(project.owner_basic_auth_url,
tmpdir.join('repo'))
assert (hg_log_set(clone, bookmarks=True)
== hg_log_set(src_repo, bookmarks=True))
assert_matching_changesets(clone, src_repo,
('node', 'desc', 'phase', 'bookmarks'),
ordered=False)
......@@ -4,7 +4,12 @@
# GNU General Public License version 2 or any later version.
#
# SPDX-License-Identifier: GPL-2.0-or-later
from .test_import import prepare_import_source, LocalRepo, hg_log_set
import pytest
from .utils.hg import LocalRepo, assert_matching_changesets
from .test_import import prepare_import_source
parametrize = pytest.mark.parametrize
def test_create(test_project):
......@@ -54,6 +59,47 @@ def test_pull(test_project, public_project, tmpdir):
clone = LocalRepo.clone(test_project.owner_basic_auth_url,
tmpdir.join('target-repo'))
assert_matching_changesets(clone, src_repo, ('node', 'desc'))
@parametrize('bookmarks', ('with', 'without'))
def test_pull_bookmarks(test_project, public_project, tmpdir, bookmarks):
"""Test hg-pull mirror"""
with_bookmarks = bookmarks == 'with'
if with_bookmarks:
test_project.owner_api_hgrc_set(inherit=True, allow_bookmarks=True)
src_repo, src_url = prepare_import_source(public_project, tmpdir,
tweak_hgrc=False)
public_project.owner_api_hgrc_set(inherit=True, allow_bookmarks=True)
src_repo.hg('bookmark', '-r', '0', 'book1')
src_repo.hg('push', '-B', 'book1', expected_return_code=1)
print("Graph of source project after push:")
print(src_repo.graphlog())
print("Url of the mirror target")
print(src_url)
mirror = test_project.api_create_mirror(url=src_url,
enabled=True,
hg_mirror_type='hg-pull')
res = mirror.api_trigger(check=False)
assert res.status_code == 204
# Check that Gitlab became aware of the changes done by the pull
test_project.wait_assert_api_branches(
lambda branches: "topic/default/antelope" in branches,
timeout=30,
)
assert ('book1' in test_project.api_branches()) is with_bookmarks
clone = LocalRepo.clone(test_project.owner_basic_auth_url,
tmpdir.join('target-repo'))
fields = ['node', 'desc']
if with_bookmarks:
fields.append('bookmarks')
else:
assert 'no bookmarks' in clone.hg('bookmarks')
expected_log_set = hg_log_set(src_repo, phase=False)
assert hg_log_set(clone, phase=False) == expected_log_set
assert_matching_changesets(clone, src_repo, fields)
......@@ -4,7 +4,10 @@ import pytest
import re
import requests
from .utils import needs
from .utils import (
assert_webdriver_not_error,
needs,
)
from .utils.reverse_calls import HttpListener
from .utils.hg import LocalRepo
from .utils.project import (
......@@ -44,8 +47,11 @@ def test_push_basic(test_project, tmpdir):
url = test_project.owner_basic_auth_url
clone = LocalRepo.clone(url, tmpdir.join('repo2'))
log = clone.hg('log', '-T', '{desc}:{phase}:{topic}\n')
assert log.splitlines() == ['Commit 1:draft:zetop', 'Commit 0:public:']
Extract, extracts = clone.changeset_extracts(('desc', 'phase', 'topic'))
assert extracts == (
Extract(desc='Commit 1', phase='draft', topic='zetop'),
Extract(desc='Commit 0', phase='public', topic=''),
)
def test_push_webdriver(test_project, tmpdir):
......@@ -62,10 +68,12 @@ def test_push_webdriver(test_project, tmpdir):
repo.hg('push', url)
clone.hg('pull', url)
log = clone.hg('log', '-T', '{desc}:{phase}:{topic}\n')
assert log.splitlines() == ['Commit 2:draft:zetop',
'Commit 1:draft:zetop',
'Commit 0:public:']
Extract, extracts = clone.changeset_extracts(('desc', 'phase', 'topic'))
assert extracts == (
Extract(desc='Commit 2', phase='draft', topic='zetop'),
Extract(desc='Commit 1', phase='draft', topic='zetop'),
Extract(desc='Commit 0', phase='public', topic=''),
)
# now GitLab side:
......@@ -77,18 +85,16 @@ def test_push_webdriver(test_project, tmpdir):
}
webdriver = test_project.owner_webdriver
webdriver.get(test_project.url)
assert_webdriver_not_error(webdriver)
webdriver.get('/'.join((test_project.url, 'commit',
branches['branch/default']['commit']['id'])))
# we don't have a reliable way to detect an error
# the title would be just 500 in production and the error class name in
# development mode.
assert "Error" not in webdriver.title
assert "(500)" not in webdriver.title
assert_webdriver_not_error(webdriver)
assert 'Commit 0' in webdriver.title
hgsha = repo.hg('log', '-T', '{node}', '-r', '0')
assert hgsha[:11] in webdriver.title
assert repo.node('0')[:11] in webdriver.title
@needs.reverse_call
......@@ -287,12 +293,13 @@ def test_protected_branch_refusal(test_project, tmpdir):
# the failed pushes had indeed no effect on the hg repo
clone = LocalRepo.clone(url, tmpdir.join('repo2'))
log = clone.hg('log', '-T', '{desc}:{branch}\n').splitlines()
assert set(l.strip() for l in log) == {
'Commit 0:default',
'Commit 1:default',
'Commit in branch:a_branch',
}
Extract, extracts = clone.changeset_extracts(('desc', 'branch'))
assert extracts == (
Extract(desc='Commit in branch', branch='a_branch'),
Extract(desc='Commit 1', branch='default'),
Extract(desc='Commit 0', branch='default'),
)
def test_protected_branch_ultimate(test_project, tmpdir):
......@@ -347,12 +354,12 @@ def test_protected_branch_ultimate(test_project, tmpdir):
# the failed pushes had indeed no effect on the hg repo
clone = LocalRepo.clone(url, tmpdir.join('repo2'))
log = clone.hg('log', '-T', '{desc}:{branch}\n').splitlines()
assert set(l.strip() for l in log) == {
'Commit 0:default',
'Commit 1:default',
'Commit in branch:a_branch',
}
Extract, extracts = clone.changeset_extracts(('desc', 'branch'))
assert extracts == (
Extract(desc='Commit in branch', branch='a_branch'),
Extract(desc='Commit 1', branch='default'),
Extract(desc='Commit 0', branch='default'),
)
# far from being completely blocked the newly appointed Maintainer
# can still accept MRs
......@@ -385,12 +392,15 @@ def test_protected_branch_ultimate(test_project, tmpdir):
assert resp.status_code < 300
assert resp.json()['state'] == 'merged'
clone.hg('pull')
log = clone.hg('log', '-T', '{desc}:{branch}:{phase}\n').splitlines()
assert set(l.strip() for l in log) == {
'Commit 0:default:public',
'Commit 1:default:public',
'Commit in branch:a_branch:public',
'New commit that even Owner cannot push:a_branch:public',
Extract, extracts = clone.changeset_extracts(('desc', 'branch', 'phase'),
collection=set)
assert extracts == {
Extract(desc='New commit that even Owner cannot push',
branch='a_branch',
phase='public'),
Extract(desc='Commit in branch', branch='a_branch', phase='public'),
Extract(desc='Commit 1', branch='default', phase='public'),
Extract(desc='Commit 0', branch='default', phase='public'),
}
......
"""Tests involving the python-gitlab API client library."""
from .test_push import prepare_simple_repo
from .utils.git import LocalRepo as GitRepo
def test_python_gitlab_hg_project(test_project, tmpdir):
proj = test_project
api_client = proj.owner_user.python_gitlab_client
gl_proj = api_client.projects.get(proj.id)
vcs_type = 'hg' if proj.hg_native else 'hg_git'
assert gl_proj.vcs_type == vcs_type
repo = prepare_simple_repo(proj, tmpdir / 'repo')
topic_hg_shas = dict(repo.changeset_extracts(('topic', 'node'))[1])
for topic, gl_branch_name in (('', 'branch/default'),
('zetop', 'topic/default/zetop')):
gl_commit = gl_proj.branches.get(gl_branch_name).commit
hg_sha = topic_hg_shas[topic]
assert gl_commit['hg_id'] == hg_sha
assert gl_commit['short_hg_id'] == hg_sha[:12]
def test_python_gitlab_git_project(git_project, tmpdir):
proj = git_project
api_client = proj.owner_user.python_gitlab_client
gl_proj = api_client.projects.get(proj.id)
assert gl_proj.vcs_type == 'git'
ssh_cmd, ssh_url = proj.owner_ssh_params
repo_path = tmpdir / 'repo.git'
repo = GitRepo.init(repo_path, default_url=ssh_url)
repo_path.join('foo').write("Hey this is in Git!\n")
repo.git('add', 'foo')
repo.git('commit', '-m', 'Commit 0')
repo.git('push', '--set-upstream', 'origin', 'master', ssh_cmd=ssh_cmd)
gl_commit = gl_proj.branches.get('master').commit
assert 'hg_id' not in gl_commit
assert 'short_hg_id' not in gl_commit
......@@ -33,3 +33,16 @@ def wait_assert(get_info, condition, timeout=10, retry_wait=0.2, msg=None):
def unique_name(name):
return '%s_%s' % (name, str(time.time()).replace('.', '_'))
def assert_webdriver_not_error(webdriver):
"""assert that the current page is not GitLab's error page rendering.
We don't have a reliable way to detect an error
The page title would be just 500 in production and the error class name in
# development mode.
"""
title = webdriver.title
assert "Error" not in title
assert not any(("(%d)" % code) in title
for code in (500, 502, 504, 403, 404))
from collections import namedtuple
import json
import logging
import os
......@@ -124,6 +125,42 @@ class LocalRepo(object):
cmd.append('--hidden')
return self.hg(*cmd)
def changeset_extracts(self, template_exprs,
collection=tuple,
revs=None,
val_sep='|:|', eol="\n"):
"""Extract information from changesets, as a collection of entries.
This doens't represent the graph information, but the ordering given
by `hg log`, on which this method is based, is preserved if using
an ordered collection.
:param template_exprs: an iterable of template keywords.
Examples: `['rev', 'phase', 'desc']`. Later on, for more complicated
expressions, we could allow ``template_exprs`` elements to be
``('key', 'expr')`` pairs, where the key would be used in the
as key of the resulting class:`dict` and `expr` would be evaluated.
as template expressions, and keys
:param collection: the wished collection class. Must be instantiable
by a generator expression, like `set` and `tuple` are.
:return: a named tuple class whose attributes are the elements of
``template_exprs`` and a set of instances of that class,
one for each changeset.
WARNING: there is no escaping. The command will fail utterly
if the rendering of one template expression contains ``val_sep`` or
` ``eol``.
"""
template = val_sep.join("{%s}" % expr for expr in template_exprs)
cmd = ['log', '-T', template + eol]
if revs is not None:
cmd.extend(('-r', revs))
Extract = namedtuple('Extract', template_exprs)
return Extract, collection(Extract(*line.split(val_sep))
for line in self.hg(*cmd).split(eol)
if line)
def branches(self):
"""Return `hg branches` as a set of names."""
return set(self.hg('branches', '-T', '{branch}\n').splitlines())
......@@ -146,3 +183,23 @@ class LocalRepo(object):
self.path.join('.gitlab-ci.yml').write(json.dumps(ci_config))
self.hg('add', '.gitlab-ci.yml')
self.hg('commit', '-m', message)
def assert_matching_changesets(repo1, repo2, fields, ordered=True, revs=None):
"""Assert changesets to have same information in the two given repos
Of course all structural information of the changesets can be checked
with just ``node``, but there is also outer information, like for example
``phases``, and ``bookmarks``, whose inclusion depends on the test case.
Also, adding some human-readable information such as `desc` or `topic`
can make matter more obvious to people engaged in debugging.
The ordering can be kept or not, according to the test case.
"""
collection = tuple if ordered else set
extract1, extract2 = [
repo.changeset_extracts(fields, collection=collection, revs=revs)[1]
for repo in (repo1, repo2)
]
assert extract1 == extract2
......@@ -3,6 +3,8 @@ import logging
import requests
import time
from gitlab import Gitlab # python-gitlab
logger = logging.getLogger(__name__)
......@@ -16,6 +18,7 @@ class User(object):
id = attr.ib()
heptapod = attr.ib(repr=False)
password = attr.ib(default=None, repr=False)
_python_gitlab_client = attr.ib(default=None, repr=False)
@classmethod
def create(cls, heptapod, name, password=None, fullname=None, **kw):
......@@ -241,6 +244,18 @@ class User(object):
logger.info("%r: access token generated in %.2f seconds",
self, time.time() - start)
@property
def python_gitlab_client(self):
"""Return the API client from the python-gitlab library
See https://python-gitlab.readthedocs.io
"""
cl = self._python_gitlab_client
if cl is None:
cl = self._python_gitlab_client = Gitlab(self.heptapod.url,
private_token=self.token)
return cl
def __enter__(self):
return self
......
......@@ -9,6 +9,7 @@ deps =
selenium
pytest-parallel
pytest-shard
python-gitlab
commands =
py.test {posargs}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment