Skip to content

Commit 8a04389

Browse files
committed
Improved continuous integration: tests now have a threshold below which travis fails. Also, releases will now automatically be made.
1 parent 7daea37 commit 8a04389

10 files changed

+318
-52
lines changed

.travis.yml

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,58 @@
11
language: python
22

3-
cache: pip
3+
# DO NOT CACHE PIP: we rather use a fresh conda
4+
# cache: pip
45

56
python:
6-
# - "2.6"
77
- "2.7"
8-
# - "3.2"
9-
# - "3.3"
10-
# - "3.4"
118
- "3.5"
12-
# - "3.5-dev" # 3.5 development branch
139
- "3.6"
14-
# - "3.6-dev" # 3.6 development branch
1510
# - "3.7" NOT AVAILABLE ON TRAVIS YET..
1611
# - "3.7-dev" # 3.7 development branch
1712
# - "nightly" # currently points to 3.7-dev
1813
# PyPy versions
1914
# - "pypy" # PyPy2 2.5.0
2015
# - "pypy3" # Pypy3 2.4.0
2116
# - "pypy-5.3.1"
22-
#
2317

2418
env:
2519
global:
2620
- GH_REF: [email protected]:smarie/python-pytest-cases.git
21+
matrix:
22+
- PYTEST_VERSION="<3" PYTEST_HTML_VERSION="==1.9.0" # indeed recent pytest_html require pytest>=3
23+
- PYTEST_VERSION="<4" PYTEST_HTML_VERSION=""
2724

2825
before_install:
26+
# (a) linux dependencies
2927
- sudo apt-get install pandoc
3028
- sudo apt-get install ant
3129
- sudo apt-get install ant-optional
3230

31+
# ------------ USE CONDA BECAUSE OTHERWISE WE HAVE VERSION CONFLICTS WITH PYTEST AND ITS PLUGINS ---
32+
# (b) install conda - from https://conda.io/docs/user-guide/tasks/use-conda-with-travis-ci.html
33+
- echo "downloading miniconda"; if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then
34+
sudo wget -q https://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh -O miniconda.sh;
35+
else
36+
sudo wget -q https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh;
37+
fi
38+
- echo "installing miniconda to $HOME/miniconda"; bash miniconda.sh -b -p $HOME/miniconda; export PATH="$HOME/miniconda/bin:$PATH"
39+
- echo "configuring miniconda"; hash -r; conda config --set always_yes yes --set changeps1 no
40+
# - conda update -q conda NO !!!!
41+
42+
# (c) base conda environment
43+
- echo "creating conda environment"; conda create -q -y -n test-environment python=$TRAVIS_PYTHON_VERSION
44+
- echo "activating conda environment"; source activate test-environment
45+
3346
install:
34-
- pip install -r ci_tools/requirements-setup.txt
35-
- pip install -r ci_tools/requirements-test.txt
36-
- pip install -r ci_tools/requirements-report.txt
37-
- pip install -r ci_tools/requirements-doc.txt
38-
- pip install codecov # https://github.com/codecov/example-python. This is specific to travis integration
39-
# - pip install coveralls # this is an alternative to codecov
47+
- conda list
48+
- python ci_tools/py_install.py conda ci_tools/requirements-conda.txt
49+
- python ci_tools/py_install.py pip ci_tools/requirements-pip.txt
50+
# travis-specific installs
51+
- pip install PyGithub # for ci_tools/github_release.py
52+
- pip install codecov # See https://github.com/codecov/example-python.
53+
- conda list
54+
# WARNING to use the "true" pytest (or py.test) depending on version, "pytest" is NOT the way to go !
55+
- python -m pytest --version # - pytest --version
4056

4157
script:
4258
# - coverage run tests.py
@@ -48,12 +64,12 @@ script:
4864
# now done in a dedicated script to capture exit code 1 and transform it to 0
4965
- chmod a+x ./ci_tools/run_tests.sh
5066
- sh ./ci_tools/run_tests.sh
67+
- python ci_tools/generate-junit-badge.py 100 # generates the badge for the test results and fail build if less than x%
5168

5269
after_success:
5370
# ***reporting***
5471
# - junit2html junit.xml testrun.html output is really not nice
5572
- ant -f ci_tools/generate-junit-html.xml # generates the html for the test results. Actually we dont use it anymore
56-
- python ci_tools/generate-junit-badge.py # generates the badge for the test results
5773
- codecov
5874
- pylint pytest_cases # note that at the moment the report is simply lost, we dont transform the result into anything
5975
# ***documentation***
@@ -71,7 +87,7 @@ after_success:
7187
git config user.name "Automatic Publish"
7288
git config user.email "[email protected]"
7389
git remote add gh-remote "${GH_REF}";
74-
git fetch gh-remote && git fetch gh-remote gh-pages:gh-pages;
90+
git fetch gh-remote && git fetch gh-remote gh-pages:gh-pages; # make sure we have the latest gh-remote
7591
# push but only if this is not a build triggered by a pull request
7692
# note: here we use the --dirty flag so that mkdocs does not clean the additional reports that we copied in the site
7793
if [ "${TRAVIS_PULL_REQUEST}" = "false" ] && [ "${TRAVIS_PYTHON_VERSION}" = "3.5" ]; then echo "Pushing to github"; PYTHONPATH=pytest_cases/ mkdocs gh-deploy -v --dirty -f docs/mkdocs.yml --remote-name gh-remote; git push gh-remote gh-pages; fi;
@@ -80,15 +96,26 @@ after_success:
8096
fi
8197
8298
deploy:
83-
provider: pypi
84-
user: "smarie"
85-
password:
86-
secure: "iWtaX7rsW1e1dQGMEo4nAa6O9cv27rT7pZMrHe2sN/oODf1CErBLD5MarReA1XLXEcqdO/Qvsx6+djl0Z3daVa6Pk7FMt+5lKBuw1QPUNuU56/MAty36nnH06H4627GZK5gEFbV107BNqnt+1eR7QIlndFVtImdA1m61JyW/5ydCgCy4ppCTNGxwxpkPWimxXMVVwS/vMT/TNzTIUIJfAXObDoBra2bVvyymjPAIJoJWghE/FG1mbsLIhMUq/HWE5k22LwcFWNHRzVddfSCzh6Qw2NyFJaV4QjCLxK3Ia6AmrF7gjpC8GqTSnoClgiW1N9Cl6+h8099BLq46FSAw4eJjkD5BrWzKxtdHg1TBWhoqEPmP4gxTbk/3lT5nYl0Vo0xdXsvjIuiHmy3RGQVNutTUT7ms1w7It0ioX2wPLaTseafOWzf4y1CQceB6AKEXCAFKA0zsj5oxDXokVTSgLpvgFaKHFfy1zz60Ga8TqJY2GD70oxA31NgmGRO+Quamas8iIsFwTmKEjLRtRt/ShTG96wYvZNlOMU4DQ4X5h0BHc5HbZLl8CVWY3NNXISbtH48E+mdvVvw5fJMnan6aK3AGLmn3i+pPX9dfn25avQ2+ulPJbvrxK/x8Ys/ZD2zDlZUdZRS8ffqfeyYFVbMwlt60DkP98zZYibdkGYPcwno="
87-
on:
88-
tags: true
89-
python: 3.5 #only one of the builds have to be deployed
90-
# server: https://test.pypi.org/legacy/
91-
distributions: "sdist bdist_wheel"
99+
# Deploy on PyPI on tags
100+
- provider: pypi
101+
user: "smarie"
102+
password:
103+
secure: "iWtaX7rsW1e1dQGMEo4nAa6O9cv27rT7pZMrHe2sN/oODf1CErBLD5MarReA1XLXEcqdO/Qvsx6+djl0Z3daVa6Pk7FMt+5lKBuw1QPUNuU56/MAty36nnH06H4627GZK5gEFbV107BNqnt+1eR7QIlndFVtImdA1m61JyW/5ydCgCy4ppCTNGxwxpkPWimxXMVVwS/vMT/TNzTIUIJfAXObDoBra2bVvyymjPAIJoJWghE/FG1mbsLIhMUq/HWE5k22LwcFWNHRzVddfSCzh6Qw2NyFJaV4QjCLxK3Ia6AmrF7gjpC8GqTSnoClgiW1N9Cl6+h8099BLq46FSAw4eJjkD5BrWzKxtdHg1TBWhoqEPmP4gxTbk/3lT5nYl0Vo0xdXsvjIuiHmy3RGQVNutTUT7ms1w7It0ioX2wPLaTseafOWzf4y1CQceB6AKEXCAFKA0zsj5oxDXokVTSgLpvgFaKHFfy1zz60Ga8TqJY2GD70oxA31NgmGRO+Quamas8iIsFwTmKEjLRtRt/ShTG96wYvZNlOMU4DQ4X5h0BHc5HbZLl8CVWY3NNXISbtH48E+mdvVvw5fJMnan6aK3AGLmn3i+pPX9dfn25avQ2+ulPJbvrxK/x8Ys/ZD2zDlZUdZRS8ffqfeyYFVbMwlt60DkP98zZYibdkGYPcwno="
104+
on:
105+
tags: true
106+
python: 3.5 #only one of the builds have to be deployed
107+
condition: $PYTEST_VERSION = "<3"
108+
# server: https://test.pypi.org/legacy/
109+
distributions: "sdist bdist_wheel"
110+
111+
# Create a github release on tags
112+
- provider: script
113+
script: python ci_tools/github_release.py -s $GITHUB_TOKEN --repo-slug smarie/python-pytest-cases -cf ./docs/changelog.md -d https://smarie.github.io/python-pytest-cases/changelog/ $TRAVIS_TAG
114+
skip_cleanup: true
115+
on:
116+
tags: true
117+
python: 3.5 #only one of the builds have to be deployed
118+
condition: $PYTEST_VERSION = "<3"
92119

93120
matrix:
94121
fast_finish: true

ci_tools/generate-junit-badge.py

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,40 @@
1+
import sys
2+
13
import requests
24
import shutil
35
from os import makedirs, path
46
import xunitparser
57

68

7-
def download_badge(junit_xml: str='reports/junit/junit.xml', dest_folder: str='reports/junit'):
8-
9-
makedirs(dest_folder, exist_ok=True)
10-
11-
# read the junit test file
9+
def get_success_percentage(junit_xml='reports/junit/junit.xml' # type: str
10+
):
11+
# type: (...) -> int
12+
"""
13+
read the junit test file and extract the success percentage
14+
:param junit_xml: the junit xml file path
15+
:return: the success percentage (an int)
16+
"""
1217
ts, tr = xunitparser.parse(open(junit_xml))
1318
runned = tr.testsRun
1419
failed = len(tr.failures)
1520

1621
success_percentage = round((runned - failed) * 100 / runned)
22+
return success_percentage
23+
24+
25+
def download_badge(success_percentage, # type: int
26+
dest_folder='reports/junit' # type: str
27+
):
28+
"""
29+
Downloads the badge corresponding to the provided success percentage, from https://img.shields.io.
30+
31+
:param success_percentage:
32+
:param dest_folder:
33+
:return:
34+
"""
35+
if not path.exists(dest_folder):
36+
makedirs(dest_folder) # , exist_ok=True) not python 2 compliant
37+
1738
if success_percentage < 50:
1839
color = 'red'
1940
elif success_percentage < 75:
@@ -35,5 +56,19 @@ def download_badge(junit_xml: str='reports/junit/junit.xml', dest_folder: str='r
3556

3657

3758
if __name__ == "__main__":
38-
# execute only if run as a script
39-
download_badge()
59+
# Execute only if run as a script.
60+
# Check the arguments
61+
assert len(sys.argv[1:]) == 1, "a single mandatory argument is required: <threshold>"
62+
threshold = float(sys.argv[1])
63+
64+
# First retrieve the success percentage from the junit xml
65+
success_percentage = get_success_percentage()
66+
67+
# Validate against the threshold
68+
print("Success percentage is %s%%. Checking that it is >= %s" % (success_percentage, threshold))
69+
if success_percentage < threshold:
70+
raise Exception("Success percentage %s%% is strictly lower than required threshold %s%%"
71+
"" % (success_percentage, threshold))
72+
73+
# Download the badge
74+
download_badge(success_percentage)

ci_tools/github_release.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# a clone of the ruby example https://gist.github.com/valeriomazzeo/5491aee76f758f7352e2e6611ce87ec1
2+
import os
3+
import re
4+
5+
import click
6+
from github import Github, UnknownObjectException
7+
# from valid8 import validate not compliant with python 2.7
8+
9+
10+
@click.command()
11+
@click.option('-u', '--user', help='GitHub username')
12+
@click.option('-p', '--pwd', help='GitHub password')
13+
@click.option('-s', '--secret', help='GitHub access token')
14+
@click.option('-r', '--repo-slug', help='Repo slug. i.e.: apple/swift')
15+
@click.option('-cf', '--changelog-file', help='Changelog file path')
16+
@click.option('-d', '--doc-url', help='Documentation url')
17+
@click.argument('tag')
18+
def create_or_update_release(user, pwd, secret, repo_slug, changelog_file, doc_url, tag):
19+
"""
20+
Creates or updates (TODO)
21+
a github release corresponding to git tag <TAG>.
22+
"""
23+
# 1- AUTHENTICATION
24+
if user is not None and secret is None:
25+
# using username and password
26+
# validate('user', user, instance_of=str)
27+
assert isinstance(user, str)
28+
# validate('pwd', pwd, instance_of=str)
29+
assert isinstance(pwd, str)
30+
g = Github(user, pwd)
31+
elif user is None and secret is not None:
32+
# or using an access token
33+
# validate('secret', secret, instance_of=str)
34+
assert isinstance(secret, str)
35+
g = Github(secret)
36+
else:
37+
raise ValueError("You should either provide username/password OR an access token")
38+
click.echo("Logged in as {user_name}".format(user_name=g.get_user()))
39+
40+
# 2- CHANGELOG VALIDATION
41+
regex_pattern = "[\s\S]*[\n][#]+[\s]*(?P<title>[\S ]*%s[\S ]*)[\n]+(?P<body>[\s\S]*?)[\n]*(\n#|$)" % re.escape(tag)
42+
changelog_section = re.compile(regex_pattern)
43+
if changelog_file is not None:
44+
# validate('changelog_file', changelog_file, custom=os.path.exists,
45+
# help_msg="changelog file should be a valid file path")
46+
assert os.path.exists(changelog_file), "changelog file should be a valid file path"
47+
with open(changelog_file) as f:
48+
contents = f.read()
49+
50+
match = changelog_section.match(contents).groupdict()
51+
if match is None or len(match) != 2:
52+
raise ValueError("Unable to find changelog section matching regexp pattern in changelog file.")
53+
else:
54+
title = match['title']
55+
message = match['body']
56+
else:
57+
title = tag
58+
message = ''
59+
60+
# append footer if doc url is provided
61+
message += "\n\nSee [documentation page](%s) for details." % doc_url
62+
63+
# 3- REPOSITORY EXPLORATION
64+
# validate('repo_slug', repo_slug, instance_of=str, min_len=1, help_msg="repo_slug should be a non-empty string")
65+
assert isinstance(repo_slug, str) and len(repo_slug) > 0, "repo_slug should be a non-empty string"
66+
repo = g.get_repo(repo_slug)
67+
68+
# -- Is there a tag with that name ?
69+
try:
70+
tag_ref = repo.get_git_ref("tags/" + tag)
71+
except UnknownObjectException:
72+
raise ValueError("No tag with name %s exists in repository %s" % (tag, repo.name))
73+
74+
# -- Is there already a release with that tag name ?
75+
click.echo("Checking if release %s already exists in repository %s" % (tag, repo.name))
76+
try:
77+
release = repo.get_release(tag)
78+
if release is not None:
79+
raise ValueError("Release %s already exists in repository %s. Please set overwrite to True if you wish to "
80+
"update the release (Not yet supported)" % (tag, repo.name))
81+
except UnknownObjectException:
82+
# Release does not exist: we can safely create it.
83+
click.echo("Creating release %s on repo: %s" % (tag, repo.name))
84+
click.echo("Release title: '%s'" % title)
85+
click.echo("Release message:\n--\n%s\n--\n" % message)
86+
repo.create_git_release(tag=tag, name=title,
87+
message=message,
88+
draft=False, prerelease=False)
89+
90+
# --- Memo ---
91+
# release.target_commitish # 'master'
92+
# release.tag_name # '0.5.0'
93+
# release.title # 'First public release'
94+
# release.body # markdown body
95+
# release.draft # False
96+
# release.prerelease # False
97+
# #
98+
# release.author
99+
# release.created_at # datetime.datetime(2018, 11, 9, 17, 49, 56)
100+
# release.published_at # datetime.datetime(2018, 11, 9, 20, 11, 10)
101+
# release.last_modified # None
102+
# #
103+
# release.id # 13928525
104+
# release.etag # 'W/"dfab7a13086d1b44fe290d5d04125124"'
105+
# release.url # 'https://api.github.com/repos/smarie/python-pytest-harvest/releases/13928525'
106+
# release.html_url # 'https://github.com/smarie/python-pytest-harvest/releases/tag/0.5.0'
107+
# release.tarball_url # 'https://api.github.com/repos/smarie/python-pytest-harvest/tarball/0.5.0'
108+
# release.zipball_url # 'https://api.github.com/repos/smarie/python-pytest-harvest/zipball/0.5.0'
109+
# release.upload_url # 'https://uploads.github.com/repos/smarie/python-pytest-harvest/releases/13928525/assets{?name,label}'
110+
111+
112+
if __name__ == '__main__':
113+
create_or_update_release()

ci_tools/py_install.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""
2+
equivalent of pip install -r <file>
3+
- with environment variables replacement
4+
- and all dependencies are installed in one 'pip install' call (solving potential complex deps)
5+
"""
6+
import os
7+
8+
import re
9+
import sys
10+
import subprocess
11+
12+
13+
def check_cmd(cmd):
14+
assert isinstance(cmd, str), "cmd should be a string"
15+
assert cmd in {"pip", "conda"}, "cmd should be conda or pip. Unknown: " + str(cmd)
16+
17+
18+
def install(cmd, packages):
19+
"""
20+
Installs all packages provided at once
21+
:param packages:
22+
:return:
23+
"""
24+
check_cmd(cmd)
25+
26+
all_pkgs_str = " ".join(all_pkgs)
27+
print("INSTALLING: " + cmd + " install " + all_pkgs_str)
28+
subprocess.check_call([cmd, 'install'] + packages) # install pkg
29+
30+
31+
env_var_regexp = re.compile(".*\$(\S+).*")
32+
33+
34+
if __name__ == '__main__':
35+
assert len(sys.argv[1:]) >= 2, "at least two mandatory arguments are required: <cmd> <filename>"
36+
37+
cmd = sys.argv[1]
38+
check_cmd(cmd)
39+
40+
filenames = sys.argv[2:]
41+
42+
all_pkgs = []
43+
for filename in filenames:
44+
with open(filename) as f:
45+
for line in f.readlines():
46+
# First remove any comment on that line
47+
splitted = line.split('#', 1) # (maxsplit=1) but python 2 does not support it :)
48+
splitted = splitted[0].strip().rstrip()
49+
if splitted != '':
50+
# the replace env vars
51+
env_var_found=True
52+
while env_var_found:
53+
res = env_var_regexp.match(splitted)
54+
env_var_found = res is not None
55+
if env_var_found:
56+
env_var_name = res.groups()[0]
57+
try:
58+
env_var_val = os.environ[env_var_name]
59+
print("replacing $%s with %s" % (env_var_name, env_var_val))
60+
splitted = splitted.replace("$%s" % env_var_name, env_var_val)
61+
except KeyError:
62+
raise Exception("Environment variable does not exist in file %s: $%s"
63+
"" % (filename, env_var_name))
64+
else:
65+
all_pkgs.append(splitted)
66+
67+
install(cmd, all_pkgs)

0 commit comments

Comments
 (0)