diff --git a/Vagrantfile b/Vagrantfile index a7f3b7c..97c9f19 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -43,13 +43,14 @@ Vagrant.configure(2) do |config| # backing providers for Vagrant. These expose provider-specific options. # Example for VirtualBox: # - # config.vm.provider "virtualbox" do |vb| + config.vm.provider "virtualbox" do |vb| # # Display the VirtualBox GUI when booting the machine # vb.gui = true # # # Customize the amount of memory on the VM: # vb.memory = "1024" - # end + vb.cpus = "2" + end # # View the documentation for the provider you are using for more # information on available options. @@ -77,6 +78,9 @@ Vagrant.configure(2) do |config| # Build tools sudo apt-get -y install build-essential git-buildpackage debhelper python-dev dh-systemd + wget -P /tmp/ \ + 'https://launchpad.net/ubuntu/+archive/primary/+files/dh-virtualenv_0.11-1_all.deb' + dpkg -i /tmp/dh-virtualenv_0.11-1_all.deb sudo pip install --upgrade pip sudo pip install sphinx sphinxcontrib-httpdomain diff --git a/debian/changelog b/debian/changelog index 2c90008..13cbd14 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,94 @@ +orlo (0.1.1) stable; urgency=medium + + * Cast package_rollback as a boolean + * Add log files for gunicorn + * Separate debug mode and debug logging + * Return 400 when getting /releases with no filter + * Remove skip from documentation, it was renamed to offset + * Fix tests, require filter on GET /releases + * Bump vagrant box cpus to 2 + * Implement stats by date/time + * Fix query in release stats to group by release + * Add boolean True to acceptable success values + * Bump version + * Cast package_rollback as a boolean + * Add log files for gunicorn + * Separate debug mode and debug logging + * Return 400 when getting /releases with no filter + * Remove skip from documentation, it was renamed to offset + * Fix tests, require filter on GET /releases + * Bump vagrant box cpus to 2 + * Implement stats by date/time + * Fix query in release stats to group by release + * Add boolean True to acceptable success values + * Bump version + + -- Alex Forbes Mon, 01 Feb 2016 18:19:03 +0000 + +orlo (0.1.0) stable; urgency=medium + + * Move orlo to /api/ in nginx + * Add ability to filter on rollback + * Add DB created to vagrant file + * Add more get_releases package filters and "latest" option + * Abstract filtering logic + + * Show easy to understand json message when filtering on invalid field + * Fix last/latest parameter name + * Use limit(1) instead of first for latest query + * Move helper functions from views.py to util.py + * Rename views.py to route_api.py + * Move filter function to util + * Start adding stats endpoints and functions + * Add index to stime field on Release + * Add __version__ attribute to package + * Add _version.py to gitignore + * Move cli interface into python page + * Add queries, for use by the stats and info routes + * Implement more /info endpoints, separate tests from stats + * Refactor non-endpoint tests to use internal methods rather than http + * Remove data.py + * Make the release counting function (now count_releases) more generic + * Add rollback filter to count_packages + * Comments, plus bump version + * Install requirements in vagrant file + * Fix platform name in log message + * Implement /stats endpoints + * Add /stats/package and consolidate the dictionary creation + * Handle poorly formatted time gracefully in stats endpoints + * Bump version + * Reverse Release-Package relationship + * Fix missing arguments passed to count_releases and implement /stats + * Fix to package_versions + * Rename /info urls + * Documentation updates + * Update Travis config to use Postgres + * Remove password from test postgres DB + * Fix postgres command + * Fix database setup + * Fix quotes in travis DB string + * Fix package_versions under postgres + * Set Flask packages to >= $version in requirements.txt + * Change /info/user endpoint + * Remove print statement, fix minor documentation bugs in /info + * Move platform in /info routes to query parameter + * Stream output of GET /releases to reduce memory usage + * Move /releases streaming json generator to util.py + * Add example curl to documentation for /import + * Abstract release logic away from the get_releases route + * Add status to get_releases parameters + * Remove "latest" filter option in favour of "desc" and "limit" + * Rename test_import + * Add offset to get_releases + * Ensure limit and offset are ints + * Implement time-based stats for charts + * Move orlo.conf to orlo/orlo.ini + * Rename package from python-orlo to orlo + * Deb packaging fixes + * Add tests for stop package + * Bump version to 0.1.0 + + -- Alex Forbes Tue, 19 Jan 2016 16:07:52 +0000 + python-orlo (0.0.4) stable; urgency=medium * Update debian description diff --git a/orlo/__init__.py b/orlo/__init__.py index 5846fd9..a5943cd 100644 --- a/orlo/__init__.py +++ b/orlo/__init__.py @@ -1,4 +1,5 @@ from flask import Flask +import logging from logging.handlers import RotatingFileHandler from orlo.config import config @@ -15,8 +16,15 @@ if config.getboolean('db', 'echo_queries'): app.config['SQLALCHEMY_ECHO'] = True -if config.getboolean('logging', 'debug'): +# Debug mode ignores all custom logging and should only be used in +# local testing... +if config.getboolean('main', 'debug_mode'): app.debug = True + +# ...as opposed to loglevel debug, which can be used anywhere +if config.getboolean('logging', 'debug'): + app.logger.setLevel(logging.DEBUG) + app.logger.debug('Debug enabled') if not config.getboolean('main', 'strict_slashes'): diff --git a/orlo/cli.py b/orlo/cli.py index ebe532e..cfcf600 100644 --- a/orlo/cli.py +++ b/orlo/cli.py @@ -19,8 +19,10 @@ def parse_args(): p_database = argparse.ArgumentParser(add_help=False) p_server = argparse.ArgumentParser(add_help=False) - p_server.add_argument('--host', '-H', dest='host', default='127.0.0.1', help="Address to listen on") - p_server.add_argument('--port', '-P', dest='port', type=int, default=5000, help="Port to listen on") + p_server.add_argument('--host', '-H', dest='host', default='127.0.0.1', + help="Address to listen on") + p_server.add_argument('--port', '-P', dest='port', type=int, default=5000, + help="Port to listen on") subparsers = parser.add_subparsers(dest='action') sp_config = subparsers.add_parser( diff --git a/orlo/config.py b/orlo/config.py index 59ed67c..922cb10 100644 --- a/orlo/config.py +++ b/orlo/config.py @@ -5,6 +5,7 @@ config = ConfigParser.ConfigParser() config.add_section('main') +config.set('main', 'debug_mode', 'false') config.set('main', 'propagate_exceptions', 'true') config.set('main', 'time_format', '%Y-%m-%dT%H:%M:%SZ') config.set('main', 'time_zone', 'UTC') diff --git a/orlo/exceptions.py b/orlo/exceptions.py index 73dd6c3..3602a2c 100644 --- a/orlo/exceptions.py +++ b/orlo/exceptions.py @@ -5,6 +5,7 @@ class OrloError(Exception): status_code = 500 + def __init__(self, message, status_code=None, payload=None): Exception.__init__(self) self.message = message @@ -28,4 +29,3 @@ class DatabaseError(OrloError): class OrloWorkflowError(OrloError): status_code = 400 - diff --git a/orlo/queries.py b/orlo/queries.py index 36dba1a..3b87e26 100644 --- a/orlo/queries.py +++ b/orlo/queries.py @@ -5,7 +5,6 @@ from orlo import app from orlo.orm import db, Release, Platform, Package, release_platform from orlo.exceptions import OrloError, InvalidUsage -from orlo.util import is_int from collections import OrderedDict __author__ = 'alforbes' @@ -15,7 +14,7 @@ """ -def _filter_release_status(query, status): +def filter_release_status(query, status): """ Filter the given query by the given release status @@ -43,7 +42,7 @@ def _filter_release_status(query, status): return query -def _filter_release_rollback(query, rollback): +def filter_release_rollback(query, rollback): """ Filter the given query by whether the releases are rollbacks or not @@ -81,12 +80,13 @@ def apply_filters(query, args): if field == 'latest': # this is not a comparison continue - # special logic for these ones, as they are package attributes + # special logic for these ones, as they are release attributes that + # are JIT calculated from package attributes if field == 'status': - query = _filter_release_status(query, value) + query = filter_release_status(query, value) continue if field == 'rollback': - query = _filter_release_rollback(query, value) + query = filter_release_rollback(query, value) continue if field.startswith('package_'): @@ -189,11 +189,15 @@ def releases(**kwargs): query = query.order_by(stime_field()) if limit: - if not is_int(limit): + try: + limit = int(limit) + except ValueError: raise InvalidUsage("limit must be a valid integer value") query = query.limit(limit) if offset: - if not is_int(offset): + try: + offset = int(offset) + except ValueError: raise InvalidUsage("offset must be a valid integer value") query = query.offset(offset) @@ -420,10 +424,10 @@ def count_releases(user=None, package=None, team=None, platform=None, status=Non query = query.filter(Release.stime <= ftime) if rollback is not None: - query = _filter_release_rollback(query, rollback) + query = filter_release_rollback(query, rollback) if status: - query = _filter_release_status(query, status) + query = filter_release_status(query, status) return query @@ -500,116 +504,3 @@ def platform_list(): return query -def stats_release_time(unit, summarize_by_unit=False, **kwargs): - """ - Return stats by time from the given arguments - - Functions in this file usually return a query object, but here we are - returning the result, as there are several queries in play. - - :param summarize_by_unit: Passed to add_release_by_time_to_dict() - :param unit: Passed to add_release_by_time_to_dict() - """ - - root_query = db.session.query(Release.id, Release.stime).join(Package) - root_query = apply_filters(root_query, kwargs) - - # Build queries for the individual stats - q_normal_successful = _filter_release_status( - _filter_release_rollback(root_query, rollback=False), 'SUCCESSFUL' - ) - q_normal_failed = _filter_release_status( - _filter_release_rollback(root_query, rollback=False), 'FAILED' - ) - q_rollback_successful = _filter_release_status( - _filter_release_rollback(root_query, rollback=True), 'SUCCESSFUL' - ) - q_rollback_failed = _filter_release_status( - _filter_release_rollback(root_query, rollback=True), 'FAILED' - ) - - output_dict = OrderedDict() - - add_releases_by_time_to_dict( - q_normal_successful, output_dict, ('normal', 'successful'), unit, summarize_by_unit) - add_releases_by_time_to_dict( - q_normal_failed, output_dict, ('normal', 'failed'), unit, summarize_by_unit) - add_releases_by_time_to_dict( - q_rollback_successful, output_dict, ('rollback', 'successful'), unit, - summarize_by_unit) - add_releases_by_time_to_dict( - q_rollback_failed, output_dict, ('rollback', 'failed'), unit, summarize_by_unit) - - return output_dict - - -def add_releases_by_time_to_dict(query, releases_dict, t_category, unit='month', - summarize_by_unit=False): - """ - Take a query and add each of its releases to a dictionary, broken down by time - - :param dict releases_dict: Dict to add to - :param tuple t_category: tuple of headings, i.e. (, ) - :param query query: Query object to retrieve releases from - :param string unit: Can be 'iso', 'hour', 'day', 'week', 'month', 'year', - :param boolean summarize_by_unit: Only break down releases by the given unit, i.e. only one - layer deep - :return: - """ - - for release in query: - if summarize_by_unit: - tree_args = [str(getattr(release.stime, unit))] - else: - if unit == 'year': - tree_args = [str(release.stime.year)] - elif unit == 'month': - tree_args = [str(release.stime.year), str(release.stime.month)] - elif unit == 'week': - # First two args of isocalendar(), year and week - tree_args = [str(i) for i in release.stime.isocalendar()][0:2] - elif unit == 'iso': - tree_args = [str(i) for i in release.stime.isocalendar()] - elif unit == 'day': - tree_args = [str(release.stime.year), str(release.stime.month), - str(release.stime.day)] - elif unit == 'hour': - tree_args = [str(release.stime.year), str(release.stime.month), - str(release.stime.day), str(release.stime.hour)] - else: - raise InvalidUsage( - 'Invalid unit "{}" specified for release breakdown'.format( - unit)) - # Append categories - print(tree_args) - tree_args += t_category - append_tree_recursive(releases_dict, tree_args[0], tree_args) - - -def append_tree_recursive(tree, parent, nodes): - """ - Recursively place the nodes under each other - - :param dict tree: The dictionary we are operating on - :param parent: The parent for this node - :param nodes: The list of nodes - :return: - """ - print('Called recursive function with args:\n{}, {}, {}'.format( - str(tree), str(parent), str(nodes))) - try: - # Get the child, one after the parent - child = nodes[nodes.index(parent) + 1] - except IndexError: - # Must be at end - if parent in tree: - tree[parent] += 1 - else: - tree[parent] = 1 - return tree - - # Otherwise recurse again - if parent not in tree: - tree[parent] = {} - # Child becomes the parent - append_tree_recursive(tree[parent], child, nodes) diff --git a/orlo/route_api.py b/orlo/route_api.py index fe2942f..18c2e3a 100644 --- a/orlo/route_api.py +++ b/orlo/route_api.py @@ -5,7 +5,8 @@ import datetime from orlo.orm import db, Release, Package, PackageResult, ReleaseNote, Platform from orlo.util import validate_request_json, create_release, validate_release_input, \ - validate_package_input, fetch_release, create_package, fetch_package, stream_json_list + validate_package_input, fetch_release, create_package, fetch_package, stream_json_list, \ + str_to_bool @app.route('/ping', methods=['GET']) @@ -194,7 +195,7 @@ def post_packages_stop(release_id, package_id): :param string release_id: Release UUID """ validate_request_json(request) - success = request.json.get('success') in ['True', 'true', '1'] + success = request.json.get('success') in [True, 'True', 'true', '1'] package = fetch_package(release_id, package_id) app.logger.info("Package stop, release {}, package {}, success {}".format( @@ -239,7 +240,6 @@ def get_releases(release_id=None): desc to true will reverse this and sort by stime descending :query int limit: Limit the results by int :query int offset: Offset the results by int - :query int skip: Skip this number of releases :query string package_name: Filter releases by package name :query string user: Filter releases by user the that performed the release :query string platform: Filter releases by platform @@ -271,16 +271,22 @@ def get_releases(release_id=None): """ + booleans = ('rollback', 'package_rollback', ) + if release_id: # Simple query = db.session.query(Release).filter(Release.id == release_id) + elif len(request.args.keys()) == 0: + raise InvalidUsage("Please specify a filter. See " + "http://orlo.readthedocs.org/en/latest/rest.html#get--releases for " + "more info") else: # Bit more complex # Flatten args, as the ImmutableDict puts some values in a list when expanded args = {} - for k, v in request.args.items(): - if type(v) is list: - args[k] = v[0] + for k in request.args.keys(): + if k in booleans: + args[k] = str_to_bool(request.args.get(k)) else: - args[k] = v + args[k] = request.args.get(k) query = queries.releases(**args) return Response(stream_json_list('releases', query), content_type='application/json') diff --git a/orlo/route_stats.py b/orlo/route_stats.py index 329869c..8d40230 100644 --- a/orlo/route_stats.py +++ b/orlo/route_stats.py @@ -1,6 +1,8 @@ from __future__ import print_function import arrow from flask import request, jsonify + +import stats from orlo import app from orlo.exceptions import InvalidUsage import orlo.queries as queries @@ -127,7 +129,7 @@ def build_all_stats_dict(stime=None, ftime=None): @app.route('/stats') -def stats(): +def stats_(): """ Return dictionary of global stats @@ -143,7 +145,7 @@ def stats(): stime = arrow.get(s_stime) if s_ftime: ftime = arrow.get(s_ftime) - except RuntimeError: # super-class to arrows ParserError, which is not importable + except RuntimeError: # super-class to arrow's ParserError, which is not importable raise InvalidUsage("A badly formatted datetime string was given") app.logger.debug("Building all_stats dict") @@ -301,11 +303,12 @@ def stats_package(package=None): return jsonify(package_stats) -@app.route('/stats/by_date') -def stats_by_date(): +@app.route('/stats/by_date/') +def stats_by_date(subject='release'): """ - Return release release_stats by date + Return stats by date + :param subject: Release or Package (default: release) :query string unit: Unit to group by, i.e. year, month, week, day, hour :query boolean summarize_by_unit: Don't build hierarchy, just summarize by the unit :return: @@ -318,12 +321,18 @@ def stats_by_date(): filters = dict((k, v) for k, v in request.args.items()) unit = filters.pop('unit', 'month') - summarize_by_unit = False if filters.pop('summarize_by_unit', False): summarize_by_unit = True + else: + summarize_by_unit = False # Returns releases and their time by rollback and status - release_stats = queries.stats_release_time(unit, summarize_by_unit, **filters) + if subject == 'release': + release_stats = stats.releases_by_time(unit, summarize_by_unit, **filters) + elif subject == 'package': + release_stats = stats.packages_by_time(unit, summarize_by_unit, **filters) + else: + raise InvalidUsage("subject must release or package, not '{}'".format()) return jsonify(release_stats) diff --git a/orlo/stats.py b/orlo/stats.py new file mode 100644 index 0000000..43c0450 --- /dev/null +++ b/orlo/stats.py @@ -0,0 +1,165 @@ +from __future__ import print_function +from orlo.queries import apply_filters, filter_release_rollback, filter_release_status +from orlo import app +from orlo.orm import db, Release, Platform, Package, release_platform +from orlo.exceptions import OrloError, InvalidUsage +from collections import OrderedDict + +__author__ = 'alforbes' + +""" +Functions related to building statistics +""" + + +def releases_by_time(unit, summarize_by_unit=False, **kwargs): + """ + Return stats by time from the given arguments + + :param summarize_by_unit: Passed to add_release_by_time_to_dict() + :param unit: Passed to add_release_by_time_to_dict() + """ + + query = db.session.query(Release.id, Release.stime).join(Package).group_by(Release) + query = apply_filters(query, kwargs) + + return get_dict_of_objects_by_time(query, unit, summarize_by_unit) + + +def packages_by_time(unit, summarize_by_unit=False, **kwargs): + """ + Count packages by time from the filters given + + :param summarize_by_unit: Passed to add_release_by_time_to_dict() + :param unit: Passed to add_release_by_time_to_dict() + """ + + query = db.session.query(Package.id, Package.name, Package.stime).join(Release) + query = apply_filters(query, kwargs) + + return get_dict_of_objects_by_time(query, unit, summarize_by_unit) + + +# TODO add stats_user_time and stats_team_time +# or generalise a stats_time function + + +def get_dict_of_objects_by_time(query, unit, summarize_by_unit=False): + """ + Build a dictionary which summarises the objects in the query given + + :param query: + :param unit: + :param summarize_by_unit: + :return: + """ + + # Build queries for the individual stats + q_normal_successful = filter_release_status( + filter_release_rollback(query, rollback=False), 'SUCCESSFUL' + ) + q_normal_failed = filter_release_status( + filter_release_rollback(query, rollback=False), 'FAILED' + ) + q_rollback_successful = filter_release_status( + filter_release_rollback(query, rollback=True), 'SUCCESSFUL' + ) + q_rollback_failed = filter_release_status( + filter_release_rollback(query, rollback=True), 'FAILED' + ) + + output_dict = OrderedDict() + + add_objects_by_time_to_dict( + q_normal_successful, output_dict, ('normal', 'successful'), unit, summarize_by_unit) + add_objects_by_time_to_dict( + q_normal_failed, output_dict, ('normal', 'failed'), unit, summarize_by_unit) + add_objects_by_time_to_dict( + q_rollback_successful, output_dict, ('rollback', 'successful'), unit, + summarize_by_unit) + add_objects_by_time_to_dict( + q_rollback_failed, output_dict, ('rollback', 'failed'), unit, summarize_by_unit) + + return output_dict + + +def add_objects_by_time_to_dict(query, releases_dict, t_category, unit='month', + summarize_by_unit=False): + """ + Take a query and add each of its objects to a dictionary, broken down by time + + If the query given has a 'name' column, that will be included in the dictionary path + above the categories (t_category). + + :param dict releases_dict: Dict to add to + :param tuple t_category: tuple of headings, i.e. (, ) + :param query query: Query object to retrieve releases from + :param string unit: Can be 'iso', 'hour', 'day', 'week', 'month', 'year', + :param boolean summarize_by_unit: Only break down releases by the given unit, i.e. only one + layer deep. For example, if "year" is the unit, we group all releases under the year + and do not add month etc underneath. + :return: + + **Note**: this can also be use for packages + """ + app.logger.debug("Entered add_objects_by_time_to_dict") + for object_ in query: + if summarize_by_unit: + tree_args = [str(getattr(object_.stime, unit))] + else: + if unit == 'year': + tree_args = [str(object_.stime.year)] + elif unit == 'month': + tree_args = [str(object_.stime.year), str(object_.stime.month)] + elif unit == 'week': + # First two args of isocalendar(), year and week + tree_args = [str(i) for i in object_.stime.isocalendar()][0:2] + elif unit == 'iso': + tree_args = [str(i) for i in object_.stime.isocalendar()] + elif unit == 'day': + tree_args = [str(object_.stime.year), str(object_.stime.month), + str(object_.stime.day)] + elif unit == 'hour': + tree_args = [str(object_.stime.year), str(object_.stime.month), + str(object_.stime.day), str(object_.stime.hour)] + else: + raise InvalidUsage( + 'Invalid unit "{}" specified for release breakdown'.format(unit)) + if hasattr(object_, 'name'): + # + tree_args.append(object_.name) + # Append categories + tree_args += t_category + append_tree_recursive(releases_dict, tree_args[0], tree_args) + + +def append_tree_recursive(tree, parent, nodes, node_index=0): + """ + Recursively place the nodes under each other + + :param dict tree: The dictionary we are operating on + :param parent: The parent for this node + :param nodes: The list of nodes + :param node_index: The position in the list list we are up to + :return: + """ + app.logger.debug('Called recursive function with args:\n{}, {}, {}'.format( + str(tree), str(parent), str(nodes))) + + child_index = node_index + 1 + try: + # Get the child, one after the parent + child = nodes[child_index] + except IndexError: + # Must be at end + if parent in tree: + tree[parent] += 1 + else: + tree[parent] = 1 + return tree + + # Otherwise recurse again + if parent not in tree: + tree[parent] = {} + # Child becomes the parent + append_tree_recursive(tree[parent], child, nodes, node_index=child_index) diff --git a/orlo/util.py b/orlo/util.py index be892aa..7d478c9 100644 --- a/orlo/util.py +++ b/orlo/util.py @@ -6,6 +6,7 @@ from orlo.orm import db, Release, Package, Platform from orlo.exceptions import InvalidUsage from sqlalchemy.orm import exc +from six import string_types __author__ = 'alforbes' @@ -21,7 +22,7 @@ def append_or_create_platforms(request_platforms): try: query = db.session.query(Platform).filter(Platform.name == p) platform = query.one() - app.logger.debug("Found platform {}".format(platform.name)) + # app.logger.debug("Found platform {}".format(platform.name)) except exc.NoResultFound: app.logger.info("Creating platform {}".format(p)) platform = Platform(p) @@ -177,9 +178,15 @@ def stream_json_list(heading, iterator): yield json.dumps(prev_release.to_dict()) + ']}' -def is_int(value): - try: - int(value) - return True - except ValueError: - return False +def str_to_bool(value): + if isinstance(value, string_types): + try: + value = int(value) + except ValueError: + if value.lower() in ('t', 'true'): + return True + elif value.lower() in ('f', 'false'): + return False + if isinstance(value, int): + return True if value > 0 else False + raise ValueError("Value {} can not be cast as boolean".format(value)) diff --git a/setup.py b/setup.py index 6ef5264..c931526 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ import multiprocessing # nopep8 -VERSION = '0.1.0' +VERSION = '0.1.1-1' version_file = open('./orlo/_version.py', 'w') version_file.write("__version__ = '{}'".format(VERSION)) version_file.close() diff --git a/systemd/orlo.service b/systemd/orlo.service index 91fa7e1..73ee3bd 100644 --- a/systemd/orlo.service +++ b/systemd/orlo.service @@ -9,7 +9,7 @@ ConditionPathExists=/usr/share/python/orlo/bin/gunicorn Type=simple User=orlo Group=orlo -ExecStart=/usr/share/python/orlo/bin/gunicorn -w 4 -b 127.0.0.1:8080 orlo:app +ExecStart=/usr/share/python/orlo/bin/gunicorn -w 4 -b 127.0.0.1:8080 orlo:app --access-logfile /var/log/orlo/gunicorn-access.log --log-level debug --error-logfile /var/log/orlo/gunicorn-error.log --log-file /var/log/orlo/gunicorn.log [Install] WantedBy=multi-user.target diff --git a/tests/test_contract.py b/tests/test_contract.py index a12dd6a..9d9d31e 100644 --- a/tests/test_contract.py +++ b/tests/test_contract.py @@ -31,15 +31,15 @@ def _create_release(self, """ response = self.client.post( - '/releases', - data=json.dumps({ - 'note': 'test note lorem ipsum', - 'platforms': platforms, - 'references': references, - 'team': team, - 'user': user, - }), - content_type='application/json', + '/releases', + data=json.dumps({ + 'note': 'test note lorem ipsum', + 'platforms': platforms, + 'references': references, + 'team': team, + 'user': user, + }), + content_type='application/json', ) self.assert200(response) return response.json['id'] @@ -71,9 +71,9 @@ def _create_package(self, release_id, doc['rollback'] = rollback response = self.client.post( - '/releases/{}/packages'.format(release_id), - data=json.dumps(doc), - content_type='application/json', + '/releases/{}/packages'.format(release_id), + data=json.dumps(doc), + content_type='application/json', ) self.assert200(response) return response.json['id'] @@ -87,8 +87,8 @@ def _start_package(self, release_id, package_id): """ response = self.client.post( - '/releases/{}/packages/{}/start'.format(release_id, package_id), - content_type='application/json', + '/releases/{}/packages/{}/start'.format(release_id, package_id), + content_type='application/json', ) self.assertEqual(response.status_code, 204) return response @@ -103,12 +103,12 @@ def _stop_package(self, release_id, package_id, """ response = self.client.post( - '/releases/{}/packages/{}/stop'.format(release_id, package_id), - data=json.dumps({ - 'success': str(success), - 'foo': 'bar', - }), - content_type='application/json', + '/releases/{}/packages/{}/stop'.format(release_id, package_id), + data=json.dumps({ + 'success': str(success), + 'foo': 'bar', + }), + content_type='application/json', ) self.assertEqual(response.status_code, 204) return response @@ -121,8 +121,8 @@ def _stop_release(self, release_id): :return: """ response = self.client.post( - '/releases/{}/stop'.format(release_id), - content_type='application/json', + '/releases/{}/stop'.format(release_id), + content_type='application/json', ) self.assertEqual(response.status_code, 204) @@ -149,9 +149,9 @@ def _post_releases_notes(self, release_id, text): doc = {'text': text} response = self.client.post( - '/releases/{}/notes'.format(release_id, text), - data=json.dumps(doc), - content_type='application/json', + '/releases/{}/notes'.format(release_id, text), + data=json.dumps(doc), + content_type='application/json', ) self.assertEqual(response.status_code, 204) return response @@ -185,13 +185,13 @@ def test_add_results(self): package_id = self._create_package(release_id) results_response = self.client.post( - '/releases/{}/packages/{}/results'.format( - release_id, package_id), - data=json.dumps({ - 'success': 'true', - 'foo': 'bar', - }), - content_type='application/json', + '/releases/{}/packages/{}/results'.format( + release_id, package_id), + data=json.dumps({ + 'success': 'true', + 'foo': 'bar', + }), + content_type='application/json', ) self.assertEqual(results_response.status_code, 204) @@ -238,12 +238,12 @@ def test_create_release_minimal(self): Create a release, omitting all optional parameters """ response = self.client.post('/releases', - data=json.dumps({ - 'platforms': ['test_platform'], - 'user': 'testuser', - }), - content_type='application/json', - ) + data=json.dumps({ + 'platforms': ['test_platform'], + 'user': 'testuser', + }), + content_type='application/json', + ) self.assert200(response) def test_diffurl_present(self): @@ -327,7 +327,7 @@ def _get_releases(self, release_id=None, filters=None, expected_status=200): path = '/releases' results_response = self.client.get( - path, content_type='application/json', + path, content_type='application/json', ) try: @@ -362,7 +362,9 @@ def test_get_releases(self): """ for _ in range(0, 3): self._create_finished_release() - results = self._get_releases() + results = self._get_releases( + filters=['limit=10'] + ) self.assertEqual(len(results['releases']), 3) def test_get_release_filter_package(self): @@ -376,7 +378,7 @@ def test_get_release_filter_package(self): package_id = self._create_package(release_id, name='specific-package') results = self._get_releases(filters=[ 'package_name=specific-package' - ]) + ]) for r in results['releases']: for p in r['packages']: @@ -475,7 +477,7 @@ def test_get_release_filter_ftime_before(self): Filter on releases that finished before a particular time """ r_yesterday, r_tomorrow = self._get_releases_time_filter( - 'ftime_before', finished=True) + 'ftime_before', finished=True) self.assertEqual(3, len(r_tomorrow['releases'])) self.assertEqual(0, len(r_yesterday['releases'])) @@ -485,7 +487,7 @@ def test_get_release_filter_ftime_after(self): Filter on releases that finished after a particular time """ r_yesterday, r_tomorrow = self._get_releases_time_filter( - 'ftime_after', finished=True) + 'ftime_after', finished=True) self.assertEqual(0, len(r_tomorrow['releases'])) self.assertEqual(3, len(r_yesterday['releases'])) @@ -555,9 +557,6 @@ def test_get_release_filter_rollback(self): first_results = self._get_releases(filters=['package_rollback=True']) second_results = self._get_releases(filters=['package_rollback=False']) - self.assertEqual(len(first_results['releases']), 3) - self.assertEqual(len(second_results['releases']), 2) - for r in first_results['releases']: for p in r['packages']: self.assertIs(p['rollback'], True) @@ -565,6 +564,9 @@ def test_get_release_filter_rollback(self): for p in r['packages']: self.assertIs(p['rollback'], False) + self.assertEqual(len(first_results['releases']), 3) + self.assertEqual(len(second_results['releases']), 2) + def test_get_release_limit_one(self): """ Should return only one release @@ -728,7 +730,7 @@ def test_get_release_filter_rollback_and_status(self): second_results = self._get_releases(filters=['package_rollback=False', 'package_status=NOT_STARTED']) # should be zero - third_results = self._get_releases(filters=['package_rollback=FALSE', + third_results = self._get_releases(filters=['package_rollback=False', 'package_status=SUCCESSFUL']) self.assertEqual(len(first_results['releases']), 3) @@ -777,10 +779,15 @@ def test_get_release_with_status_successful(self): def test_get_release_with_bad_status(self): """ - Tests get /releases?status=garbage give a helpful mesage + Tests get /releases?status=garbage give a helpful message """ self._create_finished_release() result = self._get_releases(filters=['status=garbage_boz'], expected_status=400) self.assertIn('message', result) + def test_get_releases_with_no_filters(self): + """ + Test get /releases without filters returns 400 + """ + self._get_releases(expected_status=400) diff --git a/tests/test_queries.py b/tests/test_queries.py index f3de4eb..db03c92 100644 --- a/tests/test_queries.py +++ b/tests/test_queries.py @@ -4,6 +4,7 @@ from tests.test_orm import OrloDbTest import orlo.queries import orlo.exceptions +import orlo.stats from time import sleep __author__ = 'alforbes' @@ -677,105 +678,3 @@ def test_releases_with_bad_offset(self): with self.assertRaises(orlo.exceptions.InvalidUsage): orlo.queries.releases(**args) - -class ReleaseTimeTest(OrloQueryTest): - """ - Test queries.stats_release_time - """ - ARGS = { - 'stime_gt': arrow.utcnow().replace(hours=-1), - 'stime_lt': arrow.utcnow().replace(hours=+1) - } - - def setUp(self): - super(OrloQueryTest, self).setUp() - - for r in range(0, 7): - self._create_finished_release() - - def test_append_tree_recursive(self): - """ - Test that append_tree_recursive returns a properly structured dictionary - """ - tree = {} - nodes = ['apple', 'orange'] - orlo.queries.append_tree_recursive(tree, nodes[0], nodes) - self.assertEqual(tree, {'apple': {'orange': 1}}) - - def test_append_tree_recursive_adds(self): - """ - Test that append_tree_recursive correctly adds one when called on the same path - """ - tree = {} - nodes = ['apple', 'orange'] - orlo.queries.append_tree_recursive(tree, nodes[0], nodes) - orlo.queries.append_tree_recursive(tree, nodes[0], nodes) - self.assertEqual(tree, {'apple': {'orange': 2}}) - - def test_release_time_month(self): - """ - Test queries.add_releases_by_time_to_dict by month - """ - result = orlo.queries.stats_release_time('month', **self.ARGS) - year = str(arrow.utcnow().year) - month = str(arrow.utcnow().month) - self.assertEqual(7, result[year][month]['normal']['successful']) - - def test_release_time_week(self): - """ - Test queries.add_releases_by_time_to_dict by week - """ - result = orlo.queries.stats_release_time('week', **self.ARGS) - year, week, day = arrow.utcnow().isocalendar() - self.assertEqual(7, result[str(year)][str(week)]['normal']['successful']) - - def test_release_time_year(self): - """ - Test queries.add_releases_by_time_to_dict by year - """ - result = orlo.queries.stats_release_time('year', **self.ARGS) - year = str(arrow.utcnow().year) - self.assertEqual(7, result[str(year)]['normal']['successful']) - - def test_release_time_day(self): - """ - Test queries.add_releases_by_time_to_dict by day - """ - result = orlo.queries.stats_release_time('day', **self.ARGS) - year = str(arrow.utcnow().year) - month = str(arrow.utcnow().month) - day = str(arrow.utcnow().day) - self.assertEqual( - 7, - result[year][month][day]['normal']['successful'], - ) - - def test_release_time_hour(self): - """ - Test queries.add_releases_by_time_to_dict by hour - """ - result = orlo.queries.stats_release_time('hour', **self.ARGS) - year = str(arrow.utcnow().year) - month = str(arrow.utcnow().month) - day = str(arrow.utcnow().day) - hour = str(arrow.utcnow().hour) - self.assertEqual( - 7, - result[year][month][day][hour]['normal']['successful'], - ) - - def test_release_time_with_only_this_unit(self): - """ - Test queries.add_releases_by_time_to_dict with only_this_unit - - Should break down by only the unit given - """ - result = orlo.queries.stats_release_time('hour', summarize_by_unit=True, **self.ARGS) - hour = str(arrow.utcnow().hour) - self.assertEqual( - 7, - result[hour]['normal']['successful'], - ) - - def test_release_time_with_unit_day(self): - pass \ No newline at end of file diff --git a/tests/test_route_import.py b/tests/test_route_import.py index 6332a28..a75c9e6 100644 --- a/tests/test_route_import.py +++ b/tests/test_route_import.py @@ -63,7 +63,7 @@ def test_import_get_releases(self): Crude. If only this test fails, consider adding a more specific test for the cause of the failure. """ - response = self.client.get('/releases') + response = self.client.get('/releases?limit=1') self.assert200(response) def test_import_param_platforms(self): diff --git a/tests/test_route_stats.py b/tests/test_route_stats.py index 1ee54ec..23c2642 100644 --- a/tests/test_route_stats.py +++ b/tests/test_route_stats.py @@ -129,8 +129,11 @@ def test_stats_package_returns_dict_with_package(self): self.assertIsInstance(response.json, dict) -class TimeBasedStatsTest(StatsTest): - ENDPOINT = '/stats/by_date' +class StatsByDateReleaseTest(StatsTest): + """ + Testing the "by_date" urls + """ + ENDPOINT = '/stats/by_date/release' def test_result_includes_normals(self): unittest.skip("Not suitable test for this endpoint") @@ -176,3 +179,55 @@ def test_stats_by_date_with_summarize_by_unit_day(self): """ response = self.client.get(self.ENDPOINT + '?unit=day&summarize_by_unit=1') self.assert200(response) + + def test_stats_by_date_with_platform_filter(self): + """ + Test /stats/by_date with a platform filter + """ + year = str(arrow.utcnow().year) + response = self.client.get(self.ENDPOINT + '?platform=test_platform') + self.assert200(response) + self.assertIn(year, response.json) + + def test_stats_by_date_with_platform_filter_negative(self): + """ + Test /stats/by_date with a bad platform filter returns nothing + """ + response = self.client.get(self.ENDPOINT + '?platform=bad_platform_foo') + self.assert200(response) + self.assertEqual({}, response.json) + + +class StatsByDatePackageTest(OrloDbTest): + """ + Testing the "by_date" urls + """ + ENDPOINT = '/stats/by_date/package' + + def setUp(self): + super(OrloDbTest, self).setUp() + for r in range(0, 3): + self._create_finished_release() + + def test_endpoint_200(self): + """ + Test self.ENDPOINT returns 200 + """ + response = self.client.get(self.ENDPOINT) + self.assert200(response) + + def test_endpoint_returns_dict(self): + """ + Test self.ENDPOINT returns a dictionary + """ + response = self.client.get(self.ENDPOINT) + self.assertIsInstance(response.json, dict) + + def test_package_name_in_dict(self): + """ + Test the package name is in the returned json + """ + response = self.client.get(self.ENDPOINT) + year = str(arrow.utcnow().year) + month = str(arrow.utcnow().month) + self.assertIn('test-package', response.json[year][month]) diff --git a/tests/test_stats.py b/tests/test_stats.py new file mode 100644 index 0000000..cf03a8e --- /dev/null +++ b/tests/test_stats.py @@ -0,0 +1,197 @@ +from __future__ import print_function, unicode_literals +import arrow +import orlo.queries +import orlo.exceptions +import orlo.stats +from tests.test_orm import OrloDbTest + +__author__ = 'alforbes' + + +class OrloStatsTest(OrloDbTest): + """ + Parent class for the stats tests + """ + ARGS = { + 'stime_gt': arrow.utcnow().replace(hours=-1), + 'stime_lt': arrow.utcnow().replace(hours=+1) + } + + def setUp(self): + super(OrloDbTest, self).setUp() + + for r in range(0, 7): + self._create_finished_release() + + +class GeneralTest(OrloStatsTest): + """ + Testing the shared stats functions + """ + + def test_append_tree_recursive(self): + """ + Test that append_tree_recursive returns a properly structured dictionary + """ + tree = {} + nodes = ['parent', 'child'] + orlo.stats.append_tree_recursive(tree, nodes[0], nodes) + self.assertEqual(tree, {'parent': {'child': 1}}) + + def test_append_tree_recursive_adds(self): + """ + Test that append_tree_recursive correctly adds one when called on the same path + """ + tree = {} + nodes = ['parent', 'child'] + orlo.stats.append_tree_recursive(tree, nodes[0], nodes) + orlo.stats.append_tree_recursive(tree, nodes[0], nodes) + self.assertEqual(tree, {'parent': {'child': 2}}) + + +class ReleaseTimeTest(OrloStatsTest): + """ + Test stats.releases_by_time + """ + + def test_release_time_month(self): + """ + Test stats.add_objects_by_time_to_dict by month + """ + result = orlo.stats.releases_by_time('month', **self.ARGS) + year = str(arrow.utcnow().year) + month = str(arrow.utcnow().month) + self.assertEqual(7, result[year][month]['normal']['successful']) + + def test_release_time_week(self): + """ + Test stats.add_objects_by_time_to_dict by week + """ + result = orlo.stats.releases_by_time('week', **self.ARGS) + year, week, day = arrow.utcnow().isocalendar() + self.assertEqual(7, result[str(year)][str(week)]['normal']['successful']) + + def test_release_time_year(self): + """ + Test stats.add_objects_by_time_to_dict by year + """ + result = orlo.stats.releases_by_time('year', **self.ARGS) + year = str(arrow.utcnow().year) + self.assertEqual(7, result[str(year)]['normal']['successful']) + + def test_release_time_day(self): + """ + Test stats.add_objects_by_time_to_dict by day + """ + result = orlo.stats.releases_by_time('day', **self.ARGS) + year = str(arrow.utcnow().year) + month = str(arrow.utcnow().month) + day = str(arrow.utcnow().day) + self.assertEqual( + 7, + result[year][month][day]['normal']['successful'], + ) + + def test_release_time_hour(self): + """ + Test stats.add_objects_by_time_to_dict by hour + """ + result = orlo.stats.releases_by_time('hour', **self.ARGS) + year = str(arrow.utcnow().year) + month = str(arrow.utcnow().month) + day = str(arrow.utcnow().day) + hour = str(arrow.utcnow().hour) + self.assertEqual( + 7, + result[year][month][day][hour]['normal']['successful'], + ) + + def test_release_time_with_only_this_unit(self): + """ + Test stats.add_objects_by_time_to_dict with only_this_unit + + Should break down by only the unit given + """ + result = orlo.stats.releases_by_time('hour', summarize_by_unit=True, **self.ARGS) + hour = str(arrow.utcnow().hour) + self.assertEqual( + 7, + result[hour]['normal']['successful'], + ) + + def test_release_time_with_unit_day(self): + pass + + +class PackageTimeTest(OrloStatsTest): + """ + Test stats.packages_by_time + """ + def test_package_time_month(self): + """ + Test stats.add_objects_by_time_to_dict by month + """ + result = orlo.stats.packages_by_time('month', **self.ARGS) + year = str(arrow.utcnow().year) + month = str(arrow.utcnow().month) + self.assertEqual(7, result[year][month]['test-package']['normal']['successful']) + print(result) + + def test_package_time_week(self): + """ + Test stats.add_objects_by_time_to_dict by week + """ + result = orlo.stats.packages_by_time('week', **self.ARGS) + year, week, day = arrow.utcnow().isocalendar() + self.assertEqual(7, result[str(year)][str(week)]['test-package']['normal']['successful']) + + def test_package_time_year(self): + """ + Test stats.add_objects_by_time_to_dict by year + """ + result = orlo.stats.packages_by_time('year', **self.ARGS) + year = str(arrow.utcnow().year) + self.assertEqual(7, result[str(year)]['test-package']['normal']['successful']) + + def test_package_time_day(self): + """ + Test stats.add_objects_by_time_to_dict by day + """ + result = orlo.stats.packages_by_time('day', **self.ARGS) + year = str(arrow.utcnow().year) + month = str(arrow.utcnow().month) + day = str(arrow.utcnow().day) + self.assertEqual( + 7, + result[year][month][day]['test-package']['normal']['successful'], + ) + + def test_package_time_hour(self): + """ + Test stats.add_objects_by_time_to_dict by hour + """ + result = orlo.stats.packages_by_time('hour', **self.ARGS) + year = str(arrow.utcnow().year) + month = str(arrow.utcnow().month) + day = str(arrow.utcnow().day) + hour = str(arrow.utcnow().hour) + self.assertEqual( + 7, + result[year][month][day][hour]['test-package']['normal']['successful'], + ) + + def test_package_time_with_only_this_unit(self): + """ + Test stats.add_objects_by_time_to_dict with only_this_unit + + Should break down by only the unit given + """ + result = orlo.stats.packages_by_time('hour', summarize_by_unit=True, **self.ARGS) + hour = str(arrow.utcnow().hour) + self.assertEqual( + 7, + result[hour]['test-package']['normal']['successful'], + ) + + def test_package_time_with_unit_day(self): + pass diff --git a/tests/test_stats_empirical.py b/tests/test_stats_empirical.py new file mode 100644 index 0000000..f6cfc01 --- /dev/null +++ b/tests/test_stats_empirical.py @@ -0,0 +1,238 @@ +from __future__ import print_function +from datetime import date, timedelta, datetime + +from orlo.util import append_or_create_platforms + +from tests.test_orm import OrloDbTest +from orlo.orm import db, Release, Package + +__author__ = 'alforbes' + +""" +This tests the stats functions by creating a known dataset, +making the results predictable +""" + + +def date_range(start, end): + # Courtesy of http://stackoverflow.com/questions/1060279 + for n in range(int((end - start).days)): + yield start + timedelta(n) + + +class StatsEmpiricalTest(OrloDbTest): + """ + Test release stats by creating a known data set + + Changing the dates or number of releases may impact tests + """ + + # Create releases for two days which span a year + start_date = datetime(2015, 12, 31) + end_date = datetime(2016, 1, 2) + + # Which means results for 2015-12-31 and 2016-01-01 should match below: + normal_successful_per_day = 2 + normal_failed_per_day = 1 + rollback_successful_per_day = 1 + rollback_failed_per_day = 1 + + package_list = ['p1', 'p2'] + + time_cursor = start_date + + def setUp(self): + self.releases = 0 + super(OrloDbTest, self).setUp() + + for day in date_range(self.start_date, self.end_date): + if day.weekday() > 5: + # weekend! don't release + continue + + # starting at 9am... + self.time_cursor = day + timedelta(hours=9) + + # do $per_day releases... + for i in range(0, self.normal_successful_per_day): + self.create_release(True, True) + for i in range(0, self.normal_failed_per_day): + self.create_release(True, False) + for i in range(0, self.rollback_successful_per_day): + self.create_release(False, True) + for i in range(0, self.rollback_failed_per_day): + self.create_release(False, False) + + db.session.commit() + print("Total releases created: {}".format(self.releases)) + + def create_release(self, normal, successful, user='test_user', team='test_team', + platform='test_platform'): + release = Release( + platforms=append_or_create_platforms([platform]), + user=user, + team=team, + # references=list_to_string(['test_reference']) + ) + release.stime = self.time_cursor + db.session.add(release) + + for package in self.package_list: + package = Package( + release_id=release.id, + name=package, + version='0.0.0' + ) + # which take 10 mins each... + package.stime = self.time_cursor + package.ftime = self.time_cursor = self.time_cursor + timedelta(minutes=10) + if normal: + package.rollback = False + else: + package.rollback = True + if successful: + package.status = 'SUCCESSFUL' + else: + package.status = 'FAILED' + db.session.add(package) + + release.ftime = self.time_cursor + release.duration = release.ftime - release.stime + self.releases += 1 + + def test_stats(self): + """ + Test our data against /stats + """ + response = self.client.get('/stats').json + total = response['global']['releases']['total'] + self.assertEqual(total['successful'] + total['failed'], self.releases) + + def test_stats_user(self): + """ + Test /stats/user with data + """ + # Add a release with a different user + self.create_release(False, False, user='bad_user') + + response = self.client.get('/stats/user/test_user').json + total = response['test_user']['releases']['total'] + self.assertEqual(total['successful'] + total['failed'], self.releases - 1) + + def test_stats_team(self): + """ + Test /stats/team with data + """ + # Add a release with a different team + self.create_release(False, False, team='bad_team') + + response = self.client.get('/stats/team/test_team').json + total = response['test_team']['releases']['total'] + self.assertEqual(total['successful'] + total['failed'], self.releases - 1) + + def test_stats_platform(self): + """ + Test /stats/platform with data + """ + # Add a release with a different team + self.create_release(False, False, platform='bad_platform') + + response = self.client.get('/stats/platform/test_platform').json + total = response['test_platform']['releases']['total'] + self.assertEqual(total['successful'] + total['failed'], self.releases - 1) + + def test_stats_package(self): + """ + Test /stats/package with data + """ + # All packages are in every release here, could be better tested + response = self.client.get('/stats/package/p1').json + total = response['p1']['releases']['total'] + self.assertEqual(total['successful'] + total['failed'], self.releases) + + def test_stats_by_date_release(self): + """ + Tests /stats/by_date/release + """ + s_year = self.start_date.year + s_month = self.start_date.month + response = self.client.get('/stats/by_date/release') + + self.assertEqual( + response.json[str(s_year)][str(s_month)]['normal']['successful'], + self.normal_successful_per_day, + ) + self.assertEqual( + response.json[str(s_year)][str(s_month)]['normal']['successful'], + self.normal_successful_per_day, + ) + self.assertEqual( + response.json[str(s_year + 1)]['1']['rollback']['failed'], + self.rollback_failed_per_day + ) + + def test_stats_by_date_package(self): + """ + Tests /stats/by_date/package + """ + s_year = self.start_date.year + s_month = self.start_date.month + response = self.client.get('/stats/by_date/package') + + # Assumes p1 is in every release + self.assertEqual( + response.json[str(s_year)][str(s_month)]['p1']['normal']['successful'], + self.normal_successful_per_day, + ) + self.assertEqual( + response.json[str(s_year)][str(s_month)]['p2']['normal']['successful'], + self.normal_successful_per_day, + ) + self.assertEqual( + response.json[str(s_year + 1)]['1']['p1']['rollback']['failed'], + self.rollback_failed_per_day + ) + + def test_stats_by_date_release_with_unit_day(self): + """ + Tests /stats/by_date/release + """ + s_year = self.start_date.year + s_month = self.start_date.month + response = self.client.get('/stats/by_date/release?unit=day') + + print(response.data) + self.assertEqual( + response.json[str(s_year)][str(s_month)]['31']['normal']['successful'], + self.normal_successful_per_day, + ) + self.assertEqual( + response.json[str(s_year)][str(s_month)]['31']['normal']['successful'], + self.normal_successful_per_day, + ) + self.assertEqual( + response.json[str(s_year + 1)]['1']['1']['rollback']['failed'], + self.rollback_failed_per_day + ) + + def test_stats_by_date_package_with_unit_day(self): + """ + Tests /stats/by_date/package + """ + s_year = self.start_date.year + s_month = self.start_date.month + response = self.client.get('/stats/by_date/package?unit=day') + + # Assumes p1 is in every release + self.assertEqual( + response.json[str(s_year)][str(s_month)]['31']['p1']['normal']['successful'], + self.normal_successful_per_day, + ) + self.assertEqual( + response.json[str(s_year)][str(s_month)]['31']['p2']['normal']['successful'], + self.normal_successful_per_day, + ) + self.assertEqual( + response.json[str(s_year + 1)]['1']['1']['p1']['rollback']['failed'], + self.rollback_failed_per_day + ) diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100644 index 0000000..c90af6d --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,21 @@ +from __future__ import print_function +from unittest import TestCase +import orlo.util + +__author__ = 'alforbes' + + +class UtilTest(TestCase): + def test_str_to_bool_true(self): + """ + Test some values for True + """ + for v in ['true', 'TrUE', '1', 't', '99', 1, 99]: + self.assertIs(orlo.util.str_to_bool(v), True) + + def test_str_to_bool_false(self): + """ + Test some values for True + """ + for v in ['false', 'FaLsE', 'f', '0', '-99', 0, -99]: + self.assertIs(orlo.util.str_to_bool(v), False)