M .gitignore => .gitignore +2 -0
@@ 1,2 1,4 @@
+/dronecov.db
/reporter/dist/
/reporter/node_modules/
+/tests/tmp.db
A LICENSE => LICENSE +25 -0
@@ 0,0 1,25 @@
+BSD 2-Clause License
+
+Copyright (c) 2018, Johann Rudloff
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
A Pipfile => Pipfile +15 -0
@@ 0,0 1,15 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+flask = ">=1.0.2"
+Flask-SQLAlchemy = ">=2.3.2"
+gunicorn = "*"
+
+[dev-packages]
+tavern = ">=0.19"
+
+[requires]
+python_version = "3.7"
A Pipfile.lock => Pipfile.lock +269 -0
@@ 0,0 1,269 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "8f5482fe1915694a24c884eff2548d07eae97528b2a7277a07a4c6494663d716"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.7"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {
+ "click": {
+ "hashes": [
+ "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
+ "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
+ ],
+ "version": "==7.0"
+ },
+ "flask": {
+ "hashes": [
+ "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
+ "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
+ ],
+ "index": "pypi",
+ "version": "==1.0.2"
+ },
+ "flask-sqlalchemy": {
+ "hashes": [
+ "sha256:3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b",
+ "sha256:5971b9852b5888655f11db634e87725a9031e170f37c0ce7851cf83497f56e53"
+ ],
+ "index": "pypi",
+ "version": "==2.3.2"
+ },
+ "gunicorn": {
+ "hashes": [
+ "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471",
+ "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3"
+ ],
+ "index": "pypi",
+ "version": "==19.9.0"
+ },
+ "itsdangerous": {
+ "hashes": [
+ "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
+ "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
+ ],
+ "version": "==1.1.0"
+ },
+ "jinja2": {
+ "hashes": [
+ "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
+ "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
+ ],
+ "version": "==2.10"
+ },
+ "markupsafe": {
+ "hashes": [
+ "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
+ ],
+ "version": "==1.0"
+ },
+ "sqlalchemy": {
+ "hashes": [
+ "sha256:c5951d9ef1d5404ed04bae5a16b60a0779087378928f997a294d1229c6ca4d3e"
+ ],
+ "version": "==1.2.12"
+ },
+ "werkzeug": {
+ "hashes": [
+ "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
+ "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
+ ],
+ "version": "==0.14.1"
+ }
+ },
+ "develop": {
+ "atomicwrites": {
+ "hashes": [
+ "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
+ "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
+ ],
+ "version": "==1.2.1"
+ },
+ "attrs": {
+ "hashes": [
+ "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
+ "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
+ ],
+ "version": "==18.2.0"
+ },
+ "certifi": {
+ "hashes": [
+ "sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
+ "sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
+ ],
+ "version": "==2018.10.15"
+ },
+ "chardet": {
+ "hashes": [
+ "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
+ "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
+ ],
+ "version": "==3.0.4"
+ },
+ "contextlib2": {
+ "hashes": [
+ "sha256:509f9419ee91cdd00ba34443217d5ca51f5a364a404e1dce9e8979cea969ca48",
+ "sha256:f5260a6e679d2ff42ec91ec5252f4eeffdcf21053db9113bd0a8e4d953769c00"
+ ],
+ "version": "==0.5.5"
+ },
+ "docopt": {
+ "hashes": [
+ "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"
+ ],
+ "version": "==0.6.2"
+ },
+ "future": {
+ "hashes": [
+ "sha256:eb6d4df04f1fb538c99f69c9a28b255d1ee4e825d479b9c62fc38c0cf38065a4"
+ ],
+ "version": "==0.17.0"
+ },
+ "idna": {
+ "hashes": [
+ "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
+ "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
+ ],
+ "version": "==2.7"
+ },
+ "jmespath": {
+ "hashes": [
+ "sha256:6a81d4c9aa62caf061cb517b4d9ad1dd300374cd4706997aff9cd6aedd61fc64",
+ "sha256:f11b4461f425740a1d908e9a3f7365c3d2e569f6ca68a2ff8bc5bcd9676edd63"
+ ],
+ "version": "==0.9.3"
+ },
+ "more-itertools": {
+ "hashes": [
+ "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
+ "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
+ "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
+ ],
+ "version": "==4.3.0"
+ },
+ "paho-mqtt": {
+ "hashes": [
+ "sha256:31911f6031de306c27ed79dc77b690d7c55b0dcb0f0434ca34ec6361d0371122"
+ ],
+ "version": "==1.3.1"
+ },
+ "pbr": {
+ "hashes": [
+ "sha256:8fc938b1123902f5610b06756a31b1e6febf0d105ae393695b0c9d4244ed2910",
+ "sha256:f20ec0abbf132471b68963bb34d9c78e603a5cf9e24473f14358e66551d47475"
+ ],
+ "version": "==5.1.0"
+ },
+ "pluggy": {
+ "hashes": [
+ "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095",
+ "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f"
+ ],
+ "version": "==0.8.0"
+ },
+ "py": {
+ "hashes": [
+ "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
+ "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
+ ],
+ "version": "==1.7.0"
+ },
+ "pyjwt": {
+ "hashes": [
+ "sha256:30b1380ff43b55441283cc2b2676b755cca45693ae3097325dea01f3d110628c",
+ "sha256:4ee413b357d53fd3fb44704577afac88e72e878716116270d722723d65b42176"
+ ],
+ "version": "==1.6.4"
+ },
+ "pykwalify": {
+ "hashes": [
+ "sha256:428733907fe5c458fbea5de63a755f938edccd622c7a1d0b597806141976f00e",
+ "sha256:7e8b39c5a3a10bc176682b3bd9a7422c39ca247482df198b402e8015defcceb2"
+ ],
+ "version": "==1.7.0"
+ },
+ "pytest": {
+ "hashes": [
+ "sha256:212be78a6fa5352c392738a49b18f74ae9aeec1040f47c81cadbfd8d1233c310",
+ "sha256:6f6c1efc8d0ccc21f8f6c34d8330baca883cf109b66b3df954b0a117e5528fb4"
+ ],
+ "version": "==3.9.2"
+ },
+ "python-box": {
+ "hashes": [
+ "sha256:16ba64b0efabee84f08b1d6721c627ee8e53e6259be7211e84a4f3d3f9212c3c",
+ "sha256:b79b37b46d2b7067a956c97eb1d6176536f3c6a307cdb12136ade67ea4308b4a",
+ "sha256:c5499c733fd4447270b82aa7a8e369387fefe9a53990682229b6d667c4e5d633"
+ ],
+ "version": "==3.2.1"
+ },
+ "python-dateutil": {
+ "hashes": [
+ "sha256:2f13d3ea236aeb237e7258d5729c46eafe1506fd7f8507f34730734ed8b37454",
+ "sha256:f7cde3aecf8a797553d6ec49b65f0fbcffe7ffb971ccac452d181c28fd279936"
+ ],
+ "version": "==2.7.4"
+ },
+ "pyyaml": {
+ "hashes": [
+ "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b",
+ "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf",
+ "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a",
+ "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3",
+ "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1",
+ "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1",
+ "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613",
+ "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04",
+ "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
+ "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537",
+ "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"
+ ],
+ "version": "==3.13"
+ },
+ "requests": {
+ "hashes": [
+ "sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c",
+ "sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279"
+ ],
+ "version": "==2.20.0"
+ },
+ "six": {
+ "hashes": [
+ "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
+ "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
+ ],
+ "version": "==1.11.0"
+ },
+ "stevedore": {
+ "hashes": [
+ "sha256:b92bc7add1a53fb76c634a178978d113330aaf2006f9498d9e2414b31fbfc104",
+ "sha256:c58b7c231a9c4890cd3c2b5d2b23bd63fa807ff934d68579e3f6c3a1735e8a7c"
+ ],
+ "version": "==1.30.0"
+ },
+ "tavern": {
+ "hashes": [
+ "sha256:3564d0f059321e3699fd32d3d5c14970582f8502a6a2bc103fc81ff86f23d580"
+ ],
+ "index": "pypi",
+ "version": "==0.19.1"
+ },
+ "urllib3": {
+ "hashes": [
+ "sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae",
+ "sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59"
+ ],
+ "version": "==1.24"
+ }
+ }
+}
A README.md => README.md +30 -0
@@ 0,0 1,30 @@
+# Lighweight Coverage Tracking Server for Drone CI
+
+This is the target server, where the coverage reporter can post test results.
+
+
+# Running
+
+ pipenv install
+
+ # Optional, set database URI (default is ./dronecov.db)
+ export DRONECOV_DB_URI=sqlite:///./var/dronecov_data.db
+
+ pipenv run ./dronecov.py init
+ pipenv run gunicorn -b 127.0.0.1:5000 dronecov:app
+
+ # Generate access token
+ pipenv run ./dronecov.py token username "Token Name / Description"
+
+
+# Develpment
+
+Run development server:
+
+ pipenv install --dev
+
+ DRONECOV_DB_URI=sqlite:///./tests/tmp.db FLASK_DEBUG=1 FLASK_APP=dronecov.py pipenv run flask run
+
+Run tests:
+
+ ./runtests.sh
A dronecov.py => dronecov.py +178 -0
@@ 0,0 1,178 @@
+#!/usr/bin/env python3
+
+from flask import Flask, abort, json, render_template, request
+import flask
+from flask_sqlalchemy import SQLAlchemy
+
+import datetime
+import os
+
+MIME_TYPE_SVG = 'image/svg+xml;charset=utf-8'
+
+colormap = {
+ 'green': '#97ca00',
+ 'orange': '#fe7d37',
+ 'red': '#e05d44',
+}
+
+app = Flask(__name__)
+app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DRONECOV_DB_URI', 'sqlite:///./dronecov.db')
+app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
+db = SQLAlchemy(app)
+
+class CoverageInfo(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ username = db.Column(db.String(255), nullable=False)
+ reponame = db.Column(db.String(255), nullable=False)
+ branch = db.Column(db.String(255), nullable=False)
+ build_id = db.Column(db.String(8), nullable=False)
+ coverage = db.Column(db.Float(), nullable=False)
+ created_at = db.Column(db.DateTime(), nullable=False, default=datetime.datetime.utcnow)
+
+ def __repr__(self):
+ return '<Coverage %r/%r/%r@%r = %r>' % (self.username, self.reponame, self.branch, self.build_id,self.coverage)
+
+class AccessToken(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ token = db.Column(db.String(32), unique=True, nullable=False)
+ name = db.Column(db.String(255), nullable=False)
+ username = db.Column(db.String(255), nullable=False)
+ created_at = db.Column(db.DateTime(), nullable=False, default=datetime.datetime.utcnow)
+
+# Test support:
+# For the test DB, create all tables without asking
+if app.config['SQLALCHEMY_DATABASE_URI'] == 'sqlite:///./tests/tmp.db':
+ db.create_all()
+
+class UnauthorizedException(Exception):
+ pass
+
+class TokenUnauthorizedException(Exception):
+ pass
+
+def coverage_precision(cov):
+ if cov >= 99.95:
+ return "100"
+ if cov >= 9.995:
+ # return "%d.%01d" % (cov, ((cov-int(cov))*10))
+ return "%.1f" % cov
+ return "%.2f" % cov
+
+def format_coverage(cov):
+ return coverage_precision(cov) + " %"
+
+def render_color(cov: float, threshold_warn: float, threshold_error: float) -> str:
+ if cov <= threshold_error:
+ return "red"
+ if cov <= threshold_warn:
+ return "orange"
+ return "green"
+
+@app.errorhandler(UnauthorizedException)
+def handle_unauthorized(error):
+ return ("Unauthorized", 401, {})
+
+@app.errorhandler(TokenUnauthorizedException)
+def handle_unauthorized(error):
+ return ("Forbidden", 403, {})
+
+@app.route('/<user>/<repo>/<branch>/coverage.svg')
+def get_coverage_svg(user: str, repo: str, branch: str):
+ try:
+ threshold_error = float(request.args.get('error', 5))
+ threshold_warn = float(request.args.get('warn', 80))
+ except ValueError as e:
+ return (str(e), 400, {})
+
+ cov = db.session.query(CoverageInfo).filter_by(
+ username=user,
+ reponame=repo,
+ branch=branch).order_by(CoverageInfo.created_at.desc()).first()
+
+ if cov is not None:
+ if app.debug and 'cov' in request.args:
+ cov.coverage = float(request.args.get('cov'))
+ coverage_string = format_coverage(cov.coverage)
+ color = colormap[render_color(cov.coverage, threshold_warn, threshold_error)]
+ else:
+ coverage_string = 'N/A'
+ color = colormap['red']
+
+ return (render_template('badge-template.svg',
+ w1=60, w2=54, pad=4,
+ coverage=coverage_string,
+ color=color
+ ), 200, {
+ 'Content-Type': MIME_TYPE_SVG,
+ })
+
+AUTH_PREFIX = 'Bearer '
+
+def validate_coverage_report(user: str, repo: str, branch: str, cov_json) -> CoverageInfo:
+ cov_total = float(cov_json.get('coverage_total'))
+ build_number = int(cov_json.get('build_number'))
+
+ return CoverageInfo(coverage=cov_total,
+ build_id=build_number,
+ username=user,
+ reponame=repo,
+ branch=branch)
+
+def token_can_access(token: str, user: str, repo: str):
+ """Check that token can access "user/repo", otherwise throw an exception."""
+ tk = db.session.query(AccessToken).filter_by(username=user, token=token).first()
+ if tk is None:
+ raise TokenUnauthorizedException()
+
+def check_authorization(user: str, repo: str):
+ auth = request.headers.get('Authorization', '')
+ token = auth[len(AUTH_PREFIX):]
+ if not (auth.startswith(AUTH_PREFIX) and len(token) == 32):
+ raise UnauthorizedException()
+ return token_can_access(token, user, repo)
+
+@app.route('/<user>/<repo>/<branch>/coverage', methods=['POST'])
+def update_coverage(user: str, repo: str, branch: str):
+ check_authorization(user, repo)
+
+ try:
+ cov = validate_coverage_report(user, repo, branch, request.json)
+ except (TypeError, ValueError) as e:
+ return (str(e), 400, {})
+
+ db.session.add(cov)
+ db.session.commit()
+
+ return ('OK', 201, None)
+
+def generate_token() -> str:
+ import random
+ alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
+ return ''.join(random.choice(alphabet) for _ in range(32))
+
+if __name__ == '__main__':
+ import sys
+ if sys.argv[1] == 'init':
+ db.create_all()
+ print("DB created.")
+ elif sys.argv[1] in ['token', 'token-batch']:
+ user_repo = sys.argv[2]
+ if '/' not in user_repo:
+ user_repo += '/*'
+ user, repo = user_repo.split('/')
+ if repo not in ['', '*']:
+ print("warning: repo name is ignored, token is valid for all repos belonging to " + user)
+ t = AccessToken(username = user,
+ name = sys.argv[3])
+ t.token = generate_token()
+ db.session.add(t)
+ db.session.commit()
+
+ if sys.argv[1] == 'token':
+ print('Name: %s' % (t.name))
+ print('Access Token: %s' % (t.token))
+ print('Valid repos: %s/*' % (t.username))
+ else:
+ # Batch mode, print token and nothing else
+ print(t.token)
+
A runtests.sh => runtests.sh +12 -0
@@ 0,0 1,12 @@
+#!/bin/sh
+
+set -e
+set -u
+
+ACCESS_TOKEN=$(DRONECOV_DB_URI=sqlite:///./tests/tmp.db pipenv run ./dronecov.py token-batch testuser token-name)
+export ACCESS_TOKEN
+
+PYTHONPATH="$PWD:${PYTHONPATH:-}" \
+ pipenv run pytest \
+ --tavern-global-cfg tests/common.yaml \
+ tests/*.tavern.yaml -v
A templates/badge-template.svg => templates/badge-template.svg +21 -0
@@ 0,0 1,21 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{w1+w2}}" height="20">
+ <linearGradient id="b" x2="0" y2="100%">
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
+ <stop offset="1" stop-opacity=".1"/>
+ </linearGradient>
+ <clipPath id="a">
+ <rect width="{{w1+w2}}" height="20" rx="3" fill="#fff"/>
+ </clipPath>
+ <g clip-path="url(#a)">
+ <path fill="#555" d="M0 0h{{w1}}v20H0z"/>
+ <path fill="{{color}}" d="M{{w1}} 0h{{w2}}v20H{{w1}}z"/>
+ <path fill="url(#b)" d="M0 0h{{w1+w2}}v20H0z"/>
+ </g>
+ <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110">
+ <text x="{{w1*5}}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" >coverage</text>
+ <text x="{{w1*5}}" y="140" transform="scale(.1)" >coverage</text>
+ <text text-anchor="end" x="{{(w1+w2-pad) * 10}}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" >{{coverage}}</text>
+ <text text-anchor="end" x="{{(w1+w2-pad) * 10}}" y="140" transform="scale(.1)" >{{coverage}}</text>
+ </g>
+</svg>
A tests/common.yaml => tests/common.yaml +3 -0
@@ 0,0 1,3 @@
+---
+variables:
+ host: http://localhost:5000
A tests/test_auth.tavern.yaml => tests/test_auth.tavern.yaml +33 -0
@@ 0,0 1,33 @@
+---
+test_name: Access without token and with invalid token is rejectd
+
+stages:
+ - name: POST without token
+
+ request:
+ url: '{host}/testuser/testrepo/master/coverage'
+ method: POST
+ headers:
+ content-type: application/json
+ json:
+ coverage_total: 50
+ build_number: 50
+
+ response:
+ status_code: 401
+
+
+ - name: POST with invlaid token
+
+ request:
+ url: '{host}/testuser/testrepo/master/coverage'
+ method: POST
+ headers:
+ content-type: application/json
+ authorization: 'Bearer zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'
+ json:
+ coverage_total: 50
+ build_number: 50
+
+ response:
+ status_code: 403
A tests/test_minimal.tavern.yaml => tests/test_minimal.tavern.yaml +67 -0
@@ 0,0 1,67 @@
+---
+test_name: POST coverage report and verify returned SVG
+
+stages:
+ - name: post initial coverage
+
+ request:
+ url: '{host}/testuser/testrepo/master/coverage'
+ method: POST
+ headers:
+ content-type: application/json
+ authorization: 'Bearer {tavern.env_vars.ACCESS_TOKEN}'
+ json:
+ coverage_total: 42.3
+ build_number: 18
+
+ response:
+ status_code: 201
+
+ - name: verify returned coverage is correct
+
+ request:
+ url: '{host}/testuser/testrepo/master/coverage.svg'
+ method: GET
+
+ response:
+ status_code: 200
+ headers:
+ content-type: image/svg+xml;charset=utf-8
+ body:
+ $ext:
+ function: tests.utils:validate_svg
+ extra_kwargs:
+ coverage: "42.3"
+
+
+ - name: update coverage
+
+ request:
+ url: '{host}/testuser/testrepo/master/coverage'
+ method: POST
+ headers:
+ content-type: application/json
+ authorization: 'Bearer {tavern.env_vars.ACCESS_TOKEN}'
+ json:
+ coverage_total: 2.311
+ build_number: 19
+
+ response:
+ status_code: 201
+
+ - name: verify updated coverage is returned correctly
+
+ request:
+ url: '{host}/testuser/testrepo/master/coverage.svg'
+ method: GET
+
+ response:
+ status_code: 200
+ headers:
+ content-type: image/svg+xml;charset=utf-8
+ body:
+ $ext:
+ function: tests.utils:validate_svg
+ extra_kwargs:
+ coverage: "2.31"
+
A tests/utils.py => tests/utils.py +10 -0
@@ 0,0 1,10 @@
+import xml.etree.ElementTree as ET
+
+def validate_svg(response, coverage):
+ xml = ET.fromstring(response.text)
+ ns = {'svg': 'http://www.w3.org/2000/svg'}
+ cov = xml.find('./svg:g[2]/svg:text[3]', ns).text
+ actual = cov
+ expected = coverage + '\u2009%'
+ if actual != expected:
+ raise AssertionError(actual + " != " + expected)