Browse Source

Add server.

master
Johann Rudloff 1 year ago
parent
commit
faef64e04f
12 changed files with 665 additions and 0 deletions
  1. +2
    -0
      .gitignore
  2. +25
    -0
      LICENSE
  3. +15
    -0
      Pipfile
  4. +269
    -0
      Pipfile.lock
  5. +30
    -0
      README.md
  6. +178
    -0
      dronecov.py
  7. +12
    -0
      runtests.sh
  8. +21
    -0
      templates/badge-template.svg
  9. +3
    -0
      tests/common.yaml
  10. +33
    -0
      tests/test_auth.tavern.yaml
  11. +67
    -0
      tests/test_minimal.tavern.yaml
  12. +10
    -0
      tests/utils.py

+ 2
- 0
.gitignore View File

@@ -1,2 +1,4 @@
/dronecov.db
/reporter/dist/
/reporter/node_modules/
/tests/tmp.db

+ 25
- 0
LICENSE View File

@@ -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.

+ 15
- 0
Pipfile View File

@@ -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"

+ 269
- 0
Pipfile.lock View File

@@ -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"
}
}
}

+ 30
- 0
README.md View File

@@ -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

+ 178
- 0
dronecov.py View File

@@ -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)


+ 12
- 0
runtests.sh View File

@@ -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

+ 21
- 0
templates/badge-template.svg View File

@@ -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>

+ 3
- 0
tests/common.yaml View File

@@ -0,0 +1,3 @@
---
variables:
host: http://localhost:5000

+ 33
- 0
tests/test_auth.tavern.yaml View File

@@ -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

+ 67
- 0
tests/test_minimal.tavern.yaml View File

@@ -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"


+ 10
- 0
tests/utils.py View File

@@ -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)

Loading…
Cancel
Save