~cypheon/dronecov

faef64e04f38bfdbc5c5e0ba725aee1d677385c0 — Johann Rudloff 5 years ago c19bf3e
Add server.
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) + "&#8201;%"

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)