diff --git a/.gitignore b/.gitignore index e1e3ab0b..29f338e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,11 @@ *.pyc +*.swp +*.swo +.DS_Store *.db -example/repos/* +sample_project/repos/* override +build +dist +codespeed.egg-info/ + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..3d80fa22 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +language: python +branches: + only: + - master +env: + global: + - DJANGO_SETTINGS_MODULE=sample_project.settings +matrix: + include: + - python: "2.7" + env: DJANGO_VERSION=1.11 + - python: "3.5" + env: DJANGO_VERSION=1.11 + - python: "3.5" + env: DJANGO_VERSION=2.1 +install: + - pip install flake8 + - pip install -q Django==$DJANGO_VERSION + - python setup.py install +before_script: + flake8 codespeed +script: + - python manage.py test codespeed diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..0a1845c9 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,34 @@ +Codespeed is written and maintained by Miquel Torres Barceló and various contributors: + + Frank Becker + Chris Adams + Stefan Marr + Reiner Gerecke + daniloaf + Joachim Breitner + Rayan Chikhi + str4d + Alexey Palazhchenko + Javier Honduvilla Coto + Octavian Moraru + Phil Opaola + Laurent Raufaste + Nicolas Cornu + Elliot Saba + Jean-Paul Calderone + Malcolm Parsons + Rayan + Catalin Gabriel Manciu + Clément MATHIEU + Dave Collins + David Fraser + Kevin Modzelewski + Matt Haggard + Michael Thompson + Reiner + Zachary Ware + Mark Watts + Catalin G. Manciu + Octavian Moraru + Iskander (Alex) Sharipov + Tim (Timmmm) diff --git a/CHANGELOG b/CHANGELOG index 550a9a69..661a6ca2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,12 +1,70 @@ == Change Log == +=== Version 0.13.0, February 23, 2019 === +* NEW #263: Added support for Django 2.1, drop support for Django 1.8 +* NEW #255: Allow partial requests of timeline grid +* FIX #233: Ensure missing data points don’t lead to infinite loop +* FIX #242: Revision.tag field is not trimmed before insert +* FIX #250: All doesn't work after clicking None in comparison + + +=== Version 0.12.0, November 11, 2017 === +* NEW #230: Added support for Django 1.11 +* NEW #226: Added previous and next commit navigation to the changes view +* NEW #228: Customise the meta description to describe the URL +* NEW #224: Default banch is now configured in db, per project +* NEW #221: Added support for Django 1.9 +* NEW #214: Added optional http basic authentication to results API calls +* NEW #209: Addeds the 'makeimage' service +* NEW #207: Commit tag saving features +* FIX #219: Made git tag identification more conservative +* FIX #212: Improved py3 compat by adding b'' literals +* FIX #212: Fix git tag error + + +=== Version 0.11.0, July 31, 2016 === +* NEW #191: Django 1.7 and 1.8 support +* NEW #192: Initial Python 3.4 support +* NEW #199: str4d upgraded all javascript libraries. Most importantly jqPlot to 1.0.9 and jQuery to 1.12.3 +* NEW #203: str4d implemented displaying of quartile and extrema bands on median benchmarks +* NEW #205: javierhonduco gave the application a more modern look + + +=== Version 0.10.1, November 1, 2015 === +* NEW #169: nomeata added admin action to recalculate reports +* NEW #169: nomeata increased allowed length for benchmark names to 100 chars +* FIX #186: pepsiman fixed getting commit logs for non-default branches on mercurial +* FIX #170: Show infinite increase instead of "-" +* FIX: various admin fixes + + +=== Version 0.10.0, December 27, 2013 === +* NEW #108: AlekSi made executable names uniques per project instead of globally +* NEW #148: staticfloat added the ability to set benchmark fields when saving results for a new benchmark +* NEW #149, #161: rchikhi and others fixed Django compatibility with versions 1.4-1.6 +* NEW #158: smarr added initial travis configuration. Continuous integration is now enabled for Codespeed +* NEW #32: squiddy added option to control display of authors email address +* OPTIMIZATION #98: squiddy greatly reduced the number of necessary queries for getting comparison data, getting down the response time by an order of magnitude +* FIX #86: a8 fixed timeline plot failing when more than 4 series were selected +* FIX #130: alkino fixed migrations 0006 and 0012 +* FIX #151: philangist migrated Github integration to API v3 +* FIX #85: a8 fixed timeline view on IE + + +=== Version 0.9.1, September 11, 2011 === +* NEW: Stefan added the possibility to name the default branch other than "default" +* NEW: Danilo added executable grouping by project +* NEW: Made default number of data points in the timeline plots configurable +* FIX: Frank fixed a lot of issues with the deploy examples +* FIX: Other numerous fixes + === Version 0.9.0, July 17, 2011 === * NEW: Frank refactored the Django project and app. Now Codespeed is a proper, installable Django app, and there is a sample project in the "example" dir * NEW: Upgraded to jQuery 1.6 * NEW #44: Stefan added an option to the timeline view for equidistant data point plotting * NEW: Permalinks are now id based for Environment, to shorten the length and avoid problems related to url encoding, as well as to allow changing the name without beaking the permalink -* FIX: Frank added a check for revision_date = "None" (as string), caused by the +* FIX: Frank added a check for revision_date = "None" (as string), caused by the save_single_result.py sample script @@ -17,7 +75,7 @@ * NEW #45: Stefan Marr added support for browser-button navigation on timeline * FIX: Stefan Marr improved the GitHub plugin * FIX: Improvements in the Changes View layout that prevent overlapping in some cases -* FIX: Stefan Marr fixed a bug where links to a changes view revision from the +* FIX: Stefan Marr fixed a bug where links to a changes view revision from the timeline view would not work because they used short commit ids * FIX: Stefan Marr prevented some infinite loops caused when results were equal or less than 0 @@ -75,7 +133,7 @@ Bug fix release - Fix benchmark/legend ordering - Improve error messages - Issue correctness warning for normalized stacked bars. - + * Known bugs: - There are several bugs in the comparison view related to the plotting library jqplot - Bad autoscaling under some circumstancies. Issue#199 (http://bitbucket.org/cleonello/jqplot/issue/199/autoscale-breaks-negative-values) diff --git a/LICENSE b/LICENSE index b0e0b3c4..4a42cc14 100644 --- a/LICENSE +++ b/LICENSE @@ -1,8 +1,8 @@ All files in this work, are now covered by the following copyright notice. Please note that included libraries in the media/ directory may have their own license. - -Copyright (c) 2009-2010 Miquel Torres - + +Copyright (c) 2009-2016 Miquel Torres and other contributors, see AUTHORS file. + This file is part of Codespeed. Codespeed is free software; you can redistribute it and/or diff --git a/MANIFEST.in b/MANIFEST.in index a5cbc425..70e9ce36 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include README.md include LICENSE -recursive-include codespeed/templates/codespeed/ * -recursive-include codespeed/static/ * +recursive-include codespeed/fixtures timeline_tests.json +recursive-include codespeed/templates/codespeed * +recursive-include codespeed/static * diff --git a/README.md b/README.md index fdc02d8c..bbcb6f9e 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,20 @@ # Codespeed +[![Build Status](https://travis-ci.org/tobami/codespeed.png?branch=master)](https://travis-ci.org/tobami/codespeed) +[![PyPI version](https://img.shields.io/pypi/v/codespeed.svg)](https://pypi.python.org/pypi/codespeed) -A web application to monitor and analyze the performance of your code. +Codespeed is a web application to monitor and analyze the performance of your code. -Known to be used by [PyPy](http://speed.pypy.org), -[Twisted](http://speed.twistedmatrix.com). -and many more. +Known to be used by [CPython](https://speed.python.org), [PyPy](http://speed.pypy.org), [Twisted](http://speed.twistedmatrix.com) and others. For an overview of some application concepts see the [wiki page](https://github.com/tobami/codespeed/wiki/Overview) -# Requirements +## Installation -You will need Python 2.6+ and Django 1.1+ with South. +You will need Python 2.7 or 3.5+. -In Debian and Ubuntu, they can be installed with: +To install dependencies and the codespeed Django app: - sudo apt-get install python-django python-django-south - -Instead of using distribution packages, you can use pip: - - sudo pip install django - sudo pip install South + pip install codespeed If you want version control integration, there are additional requirements: @@ -32,22 +27,18 @@ If you want version control integration, there are additional requirements: Codespeed will try to clone the repo, which depending on the size of the project can take a long time. Please be patient. -# Installation - * Download the last stable release from - [http://github.com/tobami/codespeed/downloads](http://github.com/tobami/codespeed/downloads) - and unpack it -* To get started, you can use the `example` directory as a starting point for your Django project, which can be normally configured by editing `example/settings.py`. + [github.com/tobami/codespeed/tags](https://github.com/tobami/codespeed/tags), unpack it and install it with `python setup.py install`. +* To get started, you can use the `sample_project` directory as a starting point for your Django project, which can be normally configured by editing `sample_project/settings.py`. * For simplicity, you can use the default sqlite configuration, which will save - the data to a database named `example/data.db` -* Create the DB by changing to the `example/` directory and running: + the data to a database named `data.db` +* Create the DB by typing from the root directory: - python manage.py syncdb + python manage.py migrate -* Create an admin user in the process. -* Migrate to the new DB Schema: +* Create an admin user: - python manage.py migrate + python manage.py createsuperuser * For testing purposes, you can now start the development server: @@ -56,9 +47,20 @@ can take a long time. Please be patient. The codespeed installation can now be accessed by navigating to `http://localhost:8000/`. **Note**: for production, you should configure a real server like Apache or nginx (refer to the [Django docs](http://docs.djangoproject.com/en/dev/howto/deployment/)). You should also -modify `example/settings.py` and set `DEBUG = False`. +modify `sample_project/settings.py` and set `DEBUG = False`. +[`sample_project/README.md`](https://github.com/tobami/codespeed/tree/master/sample_project/README.md) also describes some production settings. + +## Codespeed configuration -# Codespeed configuration +### Using the provided test data + +If you want to test drive Codespeed, you can use the testdata.json fixtures to have a working data set to browse. + +* From the root directory, type: + + ./manage.py loaddata codespeed/fixtures/testdata.json + +### Starting from scratch Before you can start saving (and displaying) data, you need to first create an environment and define a default project. @@ -74,9 +76,9 @@ integration, configure the relevant fields. **Note**: Only executables associated to projects with a checked "track changes" field will be shown in the Changes and Timeline views. -**Note**: Git and Mercurial need to locally clone the repository. That means that your `example/repos` directory will need to be owned by the server. In the case of a typical Apache installation, you'll need to type `sudo chown www-data:www-data example/repos` +**Note**: Git and Mercurial need to locally clone the repository. That means that your `sample_project/repos` directory will need to be owned by the server. In the case of a typical Apache installation, you'll need to type `sudo chown www-data:www-data sample_project/repos` -# Saving data +## Saving data Data is saved POSTing to `http://localhost:8000/result/add/`. @@ -93,12 +95,12 @@ environment. It must always exist or the data won't be saved (that is the reason it is described as a necessary step in the previous "Codespeed configuration" section). -# Further customization +## Further customization -## Custom Settings +### Custom Settings -You may override any of the default settings by creating the file -`example/override/settings.py`. It is strongly recommended that you only override the +You may override any of the default settings by setting them in +`sample_project/settings.py`. It is strongly recommended that you only override the settings you need by importing the default settings and replacing only the values needed for your customizations: @@ -108,44 +110,45 @@ values needed for your customizations: ### Site-wide Changes -All pages inherit from the `site_base.html` template, which -extends `base.html`. To change every page on the site simply edit (`example/templates/site_base.html`) which extends `base.html` and override +All pages inherit from the `base.html` template. To change every page on the site +simply edit (`sample_project/templates/codespeed/base_site.html`) and override the appropriate block: * Custom title: you may replace the default "My Speed Center" for the title block with your prefered value: - {% block title} + {% block title %} My Project's Speed Center {% endblock %} -* Custom logo: Place your logo in `example/override/media/img` and add a block like - this: +* Replacing logo.png: Place your logo in `sample_project/static/images/logo.png` +* Logo with custom filename: Place your logo in `sample_project/static/images/` and add a block like + this to `base_site.html`: {% block logo %} - My Project + My Project {% endblock logo %} n.b. the layout will stay exactly the same for any image with a height of 48px (any width will do) -* Custom JavaScript or CSS: add your files to the `example/override/media` directory +* Custom JavaScript or CSS: add your files to the `sample_project/static/js` directory and extend the `extra_head` template block: {% block extra_head %} {{ block.super }} - + + {% block extra_script %}{% endblock %} + + diff --git a/codespeed/templates/codespeed/base_site.html b/codespeed/templates/codespeed/base_site.html new file mode 100644 index 00000000..87740917 --- /dev/null +++ b/codespeed/templates/codespeed/base_site.html @@ -0,0 +1,3 @@ +{% extends "codespeed/base.html" %} + +{# This exists only to be overriden #} diff --git a/codespeed/templates/codespeed/changes.html b/codespeed/templates/codespeed/changes.html index c9d31373..4403b913 100644 --- a/codespeed/templates/codespeed/changes.html +++ b/codespeed/templates/codespeed/changes.html @@ -1,126 +1,14 @@ -{% extends "base.html" %} -{% block title %}{{ block.super }}: Changes{% endblock %} -{% block extra_head %} -{{ block.super }} - - - -{% endblock %} +{% block title %}{{ block.super }}: Changes{% endblock %} +{% block description %}{% if pagedesc %}{{ pagedesc }}{% else %}{{ block.super }}{% endif %}{% endblock %} {% block navigation %} {{ block.super }} {% endblock %} -{% block nav-changes %}class="current">Changes{% endblock %} +{% block nav-changes %}class="current">Changes{% endblock %} {% block body %}
{% endblock %} + +{% block extra_script %} +{{ block.super }} + + + + + + + + + + + + +{% endblock %} diff --git a/example/__init__.py b/codespeed/templatetags/__init__.py similarity index 100% rename from example/__init__.py rename to codespeed/templatetags/__init__.py diff --git a/codespeed/templatetags/percentages.py b/codespeed/templatetags/percentages.py new file mode 100644 index 00000000..33739432 --- /dev/null +++ b/codespeed/templatetags/percentages.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +from django import template + +register = template.Library() + + +@register.filter +def percentage(value): + if value == "-": + return "-" + elif value == float("inf"): + return "+∞%" + else: + return "%.2f" % value + + +@register.filter +def fix_infinity(value): + """Python’s ∞ prints 'inf', but JavaScript wants 'Infinity'""" + if value == float("inf"): + return "Infinity" + elif value == float("-inf"): + return "-Infinity" + else: + return value diff --git a/example/repos/clonerepos b/codespeed/tests/__init__.py similarity index 100% rename from example/repos/clonerepos rename to codespeed/tests/__init__.py diff --git a/codespeed/tests/test_models.py b/codespeed/tests/test_models.py new file mode 100644 index 00000000..5d116c73 --- /dev/null +++ b/codespeed/tests/test_models.py @@ -0,0 +1,297 @@ +# -*- coding: utf-8 -*- +import os + +from django.conf import settings +from django.test import TestCase, override_settings + +from codespeed.models import (Project, Report, Revision, Branch, Environment, + Benchmark, Executable, Result) +from datetime import timedelta, datetime + + +@override_settings(CHANGE_THRESHOLD=3.0, TREND_THRESHOLD=5.0) +class TestReport(TestCase): + + def setUp(self): + self.days = 0 + self.starttime = datetime.now() + timedelta(days=-100) + + Project(repo_type='G', name='pro', + repo_path='/home/foo/codespeed').save() + self.pro = Project.objects.get(name='pro') + + Branch(project=self.pro, name='branch').save() + self.b = Branch.objects.get(name='branch') + + Environment(name='Walden Pond').save() + Executable(name='walden', project=self.pro).save() + Benchmark(name='TestBench').save() + + self.env = Environment.objects.get(name='Walden Pond') + self.exe = Executable.objects.get(name='walden') + self.bench = Benchmark.objects.get(name='TestBench') + + def test_average_change_bad(self): + self.make_result(12) + s2 = self.make_result(15) + rep = self.make_report(s2) + self.assertEqual(rep.colorcode, 'red') + + def test_average_change_good(self): + self.make_result(15) + s2 = self.make_result(12) + rep = self.make_report(s2) + self.assertEqual(rep.colorcode, 'green') + + def test_within_threshold_none(self): + self.make_result(15) + s2 = self.make_result(15.2) + rep = self.make_report(s2) + self.assertEqual(rep.colorcode, 'none') + + def test_initial_revision_none(self): + s2 = self.make_result(15) + rep = self.make_report(s2) + self.assertEqual(rep.colorcode, 'none') + + def test_bench_change_good(self): + b1 = self.make_bench('b1') + + s1 = self.make_result(15) + self.make_result(15, rev=s1, benchmark=b1) + + s2 = self.make_result(14.54) + self.make_result(15, rev=s2, benchmark=b1) + + rep = self.make_report(s2) + self.assertEqual(rep.colorcode, 'green') + self.assertIn(self.bench.name, rep.summary) + + def test_bench_change_bad(self): + b1 = self.make_bench('b1') + + s1 = self.make_result(15) + self.make_result(15, rev=s1, benchmark=b1) + + s2 = self.make_result(15.46) + self.make_result(15, rev=s2, benchmark=b1) + + rep = self.make_report(s2) + self.assertEqual(rep.colorcode, 'red') + self.assertIn(self.bench.name, rep.summary) + + # NOTE: Don't need to test with multiple projects since the calculation of + # urgency doesn't take projects into account + + def test_average_change_beats_bench_change(self): + b1 = self.make_bench('b1') + + s1 = self.make_result(15) + self.make_result(15, rev=s1, benchmark=b1) + + s2 = self.make_result(14) + self.make_result(15, rev=s2, benchmark=b1) + + rep = self.make_report(s2) + self.assertIn('Average', rep.summary) + + def test_good_benchmark_change_beats_bad_average_trend(self): + changes = self.make_bad_trend() + b1 = self.make_bench('b1') + for x in changes: + s1 = self.make_result(x) + if x != changes[-1]: + self.make_result(x, rev=s1, benchmark=b1) + self.make_result(changes[-2] * .97, rev=s1, benchmark=b1) + rep = self.make_report(s1) + self.assertEquals('green', rep.colorcode) + self.assertIn('b1', rep.summary) + + def test_good_average_change_beats_bad_average_trend(self): + changes = self.make_bad_trend() + b1 = self.make_bench('b1') + for x in changes: + s1 = self.make_result(x) + if x != changes[-1]: + self.make_result(x, rev=s1, benchmark=b1) + self.make_result(changes[-2] * .92, rev=s1, benchmark=b1) + rep = self.make_report(s1) + self.assertEquals('green', rep.colorcode) + self.assertIn('Average', rep.summary) + + def test_good_change_beats_good_trend(self): + changes = self.make_good_trend() + b1 = self.make_bench('b1') + for x in changes: + s1 = self.make_result(x) + if x != changes[-1]: + self.make_result(x, rev=s1, benchmark=b1) + self.make_result(changes[-2] * .95, rev=s1, benchmark=b1) + rep = self.make_report(s1) + self.assertIn('b1', rep.summary) + self.assertNotIn('trend', rep.summary) + + def test_bad_trend_beats_good_trend(self): + good_changes = self.make_good_trend() + bad_changes = self.make_bad_trend() + + b1 = self.make_bench('b1') + for i in range(len(good_changes)): + s1 = self.make_result(good_changes[i]) + self.make_result(bad_changes[i], rev=s1, benchmark=b1) + + rep = self.make_report(s1) + self.assertIn('trend', rep.summary) + self.assertIn('b1', rep.summary) + self.assertIn('yellow', rep.colorcode) + + def test_bad_change_beats_good_trend(self): + changes = self.make_good_trend() + b1 = self.make_bench('b1') + for x in changes: + s1 = self.make_result(x) + if x != changes[-1]: + self.make_result(x, rev=s1, benchmark=b1) + self.make_result(changes[-2] * 1.05, rev=s1, benchmark=b1) + rep = self.make_report(s1) + self.assertIn('b1', rep.summary) + self.assertNotIn('trend', rep.summary) + self.assertEquals('red', rep.colorcode) + + def test_bad_beats_good_change(self): + b1 = self.make_bench('b1') + + s1 = self.make_result(12) + self.make_result(12, rev=s1, benchmark=b1) + + s2 = self.make_result(15) + self.make_result(9, rev=s2, benchmark=b1) + + rep = self.make_report(s2) + self.assertEqual(rep.colorcode, 'red') + + def test_bigger_bad_beats_smaller_bad(self): + b1 = self.make_bench('b1') + b2 = self.make_bench('b2') + + s1 = self.make_result(1.0) + self.make_result(1.0, rev=s1, benchmark=b1) + self.make_result(1.0, rev=s1, benchmark=b2) + + s2 = self.make_result(1.0) + self.make_result(1.04, rev=s2, benchmark=b1) + self.make_result(1.03, rev=s2, benchmark=b2) + + rep = self.make_report(s2) + self.assertIn('b1', rep.summary) + self.assertEquals('red', rep.colorcode) + + def test_multiple_quantities(self): + b1 = self.make_bench('b1', quantity='Space', units='bytes') + s1 = self.make_result(1.0) + self.make_result(1.0, rev=s1, benchmark=b1) + + s2 = self.make_result(1.4) + self.make_result(1.5, rev=s2, benchmark=b1) + + rep = self.make_report(s2) + self.assertRegexpMatches(rep.summary, '[sS]pace') + self.assertEquals('red', rep.colorcode) + + def make_result(self, value, rev=None, benchmark=None): + from uuid import uuid4 + + if not benchmark: + benchmark = self.bench + + if not rev: + commitdate = self.starttime + timedelta(days=self.days) + cid = str(uuid4()) + Revision(commitid=cid, date=commitdate, branch=self.b, + project=self.pro).save() + rev = Revision.objects.get(commitid=cid) + + Result(value=value, revision=rev, executable=self.exe, + environment=self.env, benchmark=benchmark).save() + self.days += 1 + return rev + + def make_report(self, revision): + Report(revision=revision, environment=self.env, + executable=self.exe).save() + return Report.objects.get(revision=revision) + + def make_bench(self, name, quantity='Time', units='seconds'): + Benchmark(name=name, units_title=quantity, units=units).save() + return Benchmark.objects.get(name=name) + + def make_bad_trend(self): + return self.make_trend(1) + + def make_good_trend(self): + return self.make_trend(-1) + + def make_trend(self, direction): + return [1 + direction * x * 1.25 * + settings.TREND_THRESHOLD / 100 / settings.TREND + for x in range(settings.TREND)] + + +class TestProject(TestCase): + + def setUp(self): + self.github_project = Project( + name='Some Project', + repo_type='H', + repo_path='https://github.com/tobami/codespeed.git' + ) + self.git_project = Project( + repo_type='G', + repo_path='/home/foo/codespeed' + ) + + def test_str(self): + self.assertEqual(str(self.github_project), 'Some Project') + + def test_repo_name(self): + """Test that only projects with local repositories have a repo_name attribute + """ + self.assertEqual(self.git_project.repo_name, 'codespeed') + + self.assertRaises(AttributeError, getattr, + self.github_project, 'repo_name') + + def test_working_copy(self): + """Test that only projects with local repositories have a working_copy + attribute + + """ + self.assertEqual(self.git_project.working_copy, + os.path.join(settings.REPOSITORY_BASE_PATH, + self.git_project.repo_name)) + + self.assertRaises( + AttributeError, getattr, self.github_project, 'working_copy') + + def test_github_browsing_url(self): + """If empty, the commit browsing url will be filled in with a default + value when using github repository. + """ + + # It should work with https:// as well as git:// urls + self.github_project.save() + self.assertEquals(self.github_project.commit_browsing_url, + 'https://github.com/tobami/codespeed.git/' + 'commit/{commitid}') + + self.github_project.repo_path = 'git://github.com/tobami/codespeed.git' + self.github_project.save() + self.assertEquals(self.github_project.commit_browsing_url, + 'https://github.com/tobami/codespeed.git/' + 'commit/{commitid}') + + # If filled in, commit browsing url should not change + self.github_project.commit_browsing_url = 'https://example.com/{commitid}' + self.github_project.save() + self.assertEquals(self.github_project.commit_browsing_url, + 'https://example.com/{commitid}') diff --git a/codespeed/tests/test_settings.py b/codespeed/tests/test_settings.py new file mode 100644 index 00000000..f525bd22 --- /dev/null +++ b/codespeed/tests/test_settings.py @@ -0,0 +1,34 @@ +from django.conf import settings +from django.test import TestCase + +from codespeed import settings as default_settings + + +class TestCodespeedSettings(TestCase): + """Test codespeed.settings + """ + + def setUp(self): + self.cs_setting_keys = [key for key in dir(default_settings) if key.isupper()] + + def test_website_name(self): + """See if WEBSITENAME is set + """ + self.assertTrue(default_settings.WEBSITE_NAME) + self.assertEqual(default_settings.WEBSITE_NAME, 'MySpeedSite', + "Change codespeed settings in project.settings") + + def test_keys_in_settings(self): + """Check that all settings attributes from codespeed.settings exist + in django.conf.settings + """ + for k in self.cs_setting_keys: + self.assertTrue(hasattr(settings, k), + "Key {0} is missing in settings.py.".format(k)) + + def test_settings_attributes(self): + """Check if all settings from codespeed.settings equals + django.conf.settings + """ + for k in self.cs_setting_keys: + self.assertEqual(getattr(settings, k), getattr(default_settings, k)) diff --git a/codespeed/tests.py b/codespeed/tests/test_views.py similarity index 60% rename from codespeed/tests.py rename to codespeed/tests/test_views.py index ee83f47b..c56132be 100644 --- a/codespeed/tests.py +++ b/codespeed/tests/test_views.py @@ -1,45 +1,44 @@ # -*- coding: utf-8 -*- from datetime import datetime, timedelta -from time import sleep -import copy, json +import copy +import json -from django.test import TestCase -from django.test.client import Client -from django.core.urlresolvers import reverse -from django.conf import settings -from codespeed.models import (Project, Benchmark, Revision, Branch, - Executable, Environment, Result, Report) -from codespeed import settings as default_settings +from django.test import TestCase, override_settings +from django.urls import reverse +from codespeed.models import (Project, Benchmark, Revision, Branch, Executable, + Environment, Result, Report) -class AddResult(TestCase): + +@override_settings(ALLOW_ANONYMOUS_POST=True) +class TestAddResult(TestCase): def setUp(self): - self.path = reverse('codespeed.views.add_result') - self.client = Client() - self.e = Environment(name='Dual Core', cpu='Core 2 Duo 8200') - self.e.save() + self.path = reverse('add-result') + self.e = Environment.objects.create(name='Dual Core', + cpu='Core 2 Duo 8200') temp = datetime.today() self.cdate = datetime( - temp.year, temp.month, temp.day, temp.hour, temp.minute, temp.second) + temp.year, temp.month, temp.day, + temp.hour, temp.minute, temp.second) self.data = { - 'commitid': '23', - 'branch': 'default', - 'project': 'MyProject', - 'executable': 'myexe O3 64bits', - 'benchmark': 'float', - 'environment': 'Dual Core', - 'result_value': 456, + 'commitid': '23', + 'branch': 'default', + 'project': 'MyProject', + 'executable': 'myexe O3 64bits', + 'benchmark': 'float', + 'environment': 'Dual Core', + 'result_value': 456, } def test_add_correct_result(self): """Add correct result data""" response = self.client.post(self.path, self.data) - + # Check that we get a success response self.assertEquals(response.status_code, 202) - self.assertEquals(response.content, "Result data saved succesfully") - + self.assertEquals(response.content.decode(), "Result data saved successfully") + # Check that the data was correctly saved e = Environment.objects.get(name='Dual Core') b = Benchmark.objects.get(name='float') @@ -65,12 +64,12 @@ def test_add_non_default_result(self): modified_data['revision_date'] = revision_date result_date = self.cdate + timedelta(minutes=2) modified_data['result_date'] = result_date - modified_data['std_dev'] = 1.11111 - modified_data['max'] = 2 - modified_data['min'] = 1.0 + modified_data['std_dev'] = 1.11111 + modified_data['max'] = 2 + modified_data['min'] = 1.0 response = self.client.post(self.path, modified_data) self.assertEquals(response.status_code, 202) - self.assertEquals(response.content, "Result data saved succesfully") + self.assertEquals(response.content.decode(), "Result data saved successfully") e = Environment.objects.get(name='Dual Core') p = Project.objects.get(name='MyProject') branch = Branch.objects.get(name='default', project=p) @@ -98,7 +97,8 @@ def test_bad_environment(self): self.data['environment'] = bad_name response = self.client.post(self.path, self.data) self.assertEquals(response.status_code, 400) - self.assertEquals(response.content, "Environment " + bad_name + " not found") + self.assertEquals(response.content.decode(), + "Environment " + bad_name + " not found") self.data['environment'] = 'Dual Core' def test_empty_argument(self): @@ -109,7 +109,8 @@ def test_empty_argument(self): response = self.client.post(self.path, self.data) self.assertEquals(response.status_code, 400) self.assertEquals( - response.content, 'Value for key "' + key + '" empty in request') + response.content.decode(), + 'Value for key "' + key + '" empty in request') self.data[key] = backup def test_missing_argument(self): @@ -120,25 +121,27 @@ def test_missing_argument(self): response = self.client.post(self.path, self.data) self.assertEquals(response.status_code, 400) self.assertEquals( - response.content, 'Key "' + key + '" missing from request') + response.content.decode(), + 'Key "' + key + '" missing from request') self.data[key] = backup def test_report_is_not_created(self): - '''Should not create a report when adding a single result''' - response = self.client.post(self.path, self.data) + """Should not create a report when adding a single result""" + self.client.post(self.path, self.data) number_of_reports = len(Report.objects.all()) # After adding one result for one revision, there should be no reports self.assertEquals(number_of_reports, 0) def test_report_is_created(self): """Should create a report when adding a result for two revisions""" - response = self.client.post(self.path, self.data) + # First result does not create report + self.client.post(self.path, self.data) modified_data = copy.deepcopy(self.data) modified_data['commitid'] = "23233" - response = self.client.post(self.path, modified_data) + # Second result should trigger report creation + self.client.post(self.path, modified_data) number_of_reports = len(Report.objects.all()) - # After adding a result for a second revision, a report should be created self.assertEquals(number_of_reports, 1) def test_submit_data_with_none_timestamp(self): @@ -156,57 +159,68 @@ def test_add_result_with_no_project(self): modified_data['executable'] = "My new executable" response = self.client.post(self.path, modified_data) self.assertEquals(response.status_code, 202) - self.assertEquals(response.content, "Result data saved succesfully") + self.assertEquals( + response.content.decode(), "Result data saved successfully") + +@override_settings(ALLOW_ANONYMOUS_POST=True) +class TestAddJSONResults(TestCase): -class AddJSONResults(TestCase): def setUp(self): - self.path = reverse('codespeed.views.add_json_results') - self.client = Client() + self.path = reverse('add-json-results') self.e = Environment(name='bigdog', cpu='Core 2 Duo 8200') self.e.save() temp = datetime.today() self.cdate = datetime( - temp.year, temp.month, temp.day, temp.hour, temp.minute, temp.second) - + temp.year, temp.month, temp.day, temp.hour, temp.minute, + temp.second) + self.data = [ {'commitid': '123', - 'project': 'pypy', - 'branch': 'default', - 'executable': 'pypy-c', - 'benchmark': 'Richards', - 'environment': 'bigdog', - 'result_value': 456,}, + 'project': 'pypy', + 'branch': 'default', + 'executable': 'pypy-c', + 'benchmark': 'Richards', + 'environment': 'bigdog', + 'result_value': 456}, {'commitid': '456', - 'project': 'pypy', - 'branch': 'default', - 'executable': 'pypy-c', - 'benchmark': 'Richards', - 'environment': 'bigdog', - 'result_value': 457,}, + 'project': 'pypy', + 'branch': 'default', + 'executable': 'pypy-c', + 'benchmark': 'Richards', + 'environment': 'bigdog', + 'result_value': 457}, {'commitid': '456', - 'project': 'pypy', - 'branch': 'default', - 'executable': 'pypy-c', - 'benchmark': 'Richards2', - 'environment': 'bigdog', - 'result_value': 34,}, + 'project': 'pypy', + 'branch': 'default', + 'executable': 'pypy-c', + 'benchmark': 'Richards2', + 'environment': 'bigdog', + 'result_value': 34}, {'commitid': '789', - 'project': 'pypy', - 'branch': 'default', - 'executable': 'pypy-c', - 'benchmark': 'Richards', - 'environment': 'bigdog', - 'result_value': 458,}, + 'project': 'pypy', + 'branch': 'default', + 'executable': 'pypy-c', + 'benchmark': 'Richards', + 'environment': 'bigdog', + 'result_value': 458}, ] + def test_get_returns_405(self): + response = self.client.get(self.path, + {'json': json.dumps(self.data)}) + + self.assertEquals(response.status_code, 405) + def test_add_correct_results(self): """Should add all results when the request data is valid""" - response = self.client.post(self.path, {'json' : json.dumps(self.data)}) + response = self.client.post(self.path, + {'json': json.dumps(self.data)}) # Check that we get a success response self.assertEquals(response.status_code, 202) - self.assertEquals(response.content, "All result data saved successfully") + self.assertEquals(response.content.decode(), + "All result data saved successfully") # Check that the data was correctly saved e = Environment.objects.get(name='bigdog') @@ -254,10 +268,12 @@ def test_bad_environment(self): data = self.data[0] bad_name = 'bigdog1' data['environment'] = bad_name - response = self.client.post(self.path, {'json' : json.dumps(self.data)}) + response = self.client.post(self.path, + {'json': json.dumps(self.data)}) self.assertEquals(response.status_code, 400) - self.assertEquals(response.content, "Environment " + bad_name + " not found") + self.assertEquals( + response.content.decode(), "Environment " + bad_name + " not found") data['environment'] = 'bigdog' def test_empty_argument(self): @@ -267,9 +283,11 @@ def test_empty_argument(self): backup = data[key] data[key] = "" response = self.client.post(self.path, - {'json' : json.dumps(self.data)}) + {'json': json.dumps(self.data)}) self.assertEquals(response.status_code, 400) - self.assertEquals(response.content, 'Value for key "' + key + '" empty in request') + self.assertEquals( + response.content.decode(), + 'Value for key "' + key + '" empty in request') data[key] = backup def test_missing_argument(self): @@ -279,15 +297,17 @@ def test_missing_argument(self): backup = data[key] del(data[key]) response = self.client.post(self.path, - {'json' : json.dumps(self.data)}) + {'json': json.dumps(self.data)}) self.assertEquals(response.status_code, 400) - self.assertEquals(response.content, 'Key "' + key + '" missing from request') + self.assertEquals( + response.content.decode(), 'Key "' + key + '" missing from request') data[key] = backup def test_report_is_created(self): '''Should create a report when adding json results for two revisions plus a third revision with one result less than the last one''' - response = self.client.post(self.path, {'json' : json.dumps(self.data)}) + response = self.client.post(self.path, + {'json': json.dumps(self.data)}) # Check that we get a success response self.assertEquals(response.status_code, 202) @@ -299,11 +319,8 @@ def test_report_is_created(self): self.assertEquals(number_of_reports, 1) -class Timeline(TestCase): - fixtures = ["gettimeline_unittest.json"] - - def setUp(self): - self.client = Client() +class TestTimeline(TestCase): + fixtures = ["timeline_tests.json"] def test_fixture(self): """Test the loaded fixture data @@ -316,63 +333,91 @@ def test_fixture(self): results = benchmarks[0].results.all() self.assertEquals(len(results), 8) + def test_timeline(self): + path = reverse('timeline') + response = self.client.get(path) + self.assertEquals(response.status_code, 200) + responsedata = response.content.decode() + self.assertIn('My Own Title\n: Timeline', responsedata) + def test_gettimelinedata(self): """Test that gettimelinedata returns correct timeline data """ - path = reverse('codespeed.views.gettimelinedata') + path = reverse('gettimelinedata') data = { - "exe": "1,2", + "exe": "1,2", "base": "2+4", - "ben": "float", - "env": "1", - "revs": 2 + "ben": "float", + "env": "1", + "revs": "2" } response = self.client.get(path, data) self.assertEquals(response.status_code, 200) - responsedata = json.loads(response.content) + responsedata = json.loads(response.getvalue().decode()) + self.assertEquals( responsedata['error'], "None", "there should be no errors") self.assertEquals( len(responsedata['timelines']), 1, "there should be 1 benchmark") self.assertEquals( - len(responsedata['timelines'][0]['branches']['default']), + len(responsedata['timelines'][0]['branches']), 2, - "there should be 2 timelines") + "there should be 2 branches") + self.assertEquals( + len(responsedata['timelines'][0]['branches']['default']), + 1, + "there should be 1 timeline for master") self.assertEquals( - len(responsedata['timelines'][0]['branches']['default']['1']), + len(responsedata['timelines'][0]['branches']['master']['1']), 2, "There are 2 datapoints") self.assertEquals( - responsedata['timelines'][0]['branches']['default']['1'][1], - [u'2011-04-13 17:04:22', 2000.0, 1.11111, u'2', u'default'], - "Wrong data returned: ") + responsedata['timelines'][0]['branches']['master']['1'][1], + [u'2011/04/13 17:04:22 ', 2000.0, 1.11111, u'2', u'', u'master']) -class CodespeedSettings(TestCase): - """Test codespeed.settings - """ +@override_settings(ALLOW_ANONYMOUS_POST=True) +class TestReports(TestCase): def setUp(self): - self.cs_setting_keys = [key for key in dir(default_settings) if key.isupper()] + Environment.objects.create(name='Dual Core', cpu='Core 2 Duo 8200') + self.data = { + 'commitid': 'abcd1', + 'branch': 'master', + 'project': 'MyProject', + 'executable': 'myexe O3 64bits', + 'benchmark': 'float', + 'environment': 'Dual Core', + 'result_value': 200, + } + resp = self.client.post(reverse('add-result'), + self.data) + self.assertEqual(resp.status_code, 202) + self.data['commitid'] = "abcd2" + self.data['result_value'] = 150 + self.client.post(reverse('add-result'), self.data) + self.assertEqual(resp.status_code, 202) - def test_website_name(self): - """See if WEBSITENAME is set - """ - self.assertTrue(default_settings.WEBSITE_NAME) - self.assertEqual(default_settings.WEBSITE_NAME, 'MySpeedSite', - "Change codespeed settings in project.settings") + def test_reports(self): + response = self.client.get(reverse('reports')) - def test_keys_in_settings(self): - """Check that all settings attributes from codespeed.settings exist - in django.conf.settings - """ - for k in self.cs_setting_keys: - self.assertTrue(hasattr(settings, k)) + self.assertEqual(response.status_code, 200) + content = response.content.decode() + self.assertIn('Latest Results', content) + self.assertIn('Latest Significant Results', content) + self.assertIn(self.data['commitid'], content) - def test_settings_attributes(self): - """Check if all settings from codespeed.settings equals - django.conf.settings - """ - for k in self.cs_setting_keys: - self.assertEqual(getattr(settings, k), getattr(default_settings, k)) + def test_reports_post_returns_405(self): + response = self.client.post(reverse('reports'), {}) + + self.assertEqual(response.status_code, 405) + + +class TestFeeds(TestCase): + + def test_latest_result_feed(self): + response = self.client.get(reverse('latest-results')) + self.assertEqual(response.status_code, 200) + content = response.content.decode() + self.assertIn('.*)/$', LatestEntries()), -) + url(r'^feeds/latest/$', LatestEntries(), name='latest-results'), + url(r'^feeds/latest_significant/$', LatestSignificantEntries(), + name='latest-significant-results'), +] -urlpatterns += patterns('codespeed.views', - url(r'^reports/$', 'reports', name='reports'), - url(r'^changes/$', 'changes', name='changes'), - url(r'^changes/table/$', 'getchangestable', name='getchangestable'), - url(r'^changes/logs/$', 'displaylogs', name='displaylogs'), - url(r'^timeline/$', 'timeline', name='timeline'), - url(r'^timeline/json/$', 'gettimelinedata', name='gettimelinedata'), - url(r'^comparison/$', 'comparison', name='comparison'), - url(r'^comparison/json/$', 'getcomparisondata', name='getcomparisondata'), -) +urlpatterns += [ + url(r'^historical/json/$', views.gethistoricaldata, name='gethistoricaldata'), + url(r'^reports/$', views.reports, name='reports'), + url(r'^changes/$', views.changes, name='changes'), + url(r'^changes/table/$', views.getchangestable, name='getchangestable'), + url(r'^changes/logs/$', views.displaylogs, name='displaylogs'), + url(r'^timeline/$', views.timeline, name='timeline'), + url(r'^timeline/json/$', views.gettimelinedata, name='gettimelinedata'), + url(r'^comparison/$', views.comparison, name='comparison'), + url(r'^comparison/json/$', views.getcomparisondata, name='getcomparisondata'), + url(r'^makeimage/$', views.makeimage, name='makeimage'), +] -urlpatterns += patterns('codespeed.views', +urlpatterns += [ # URLs for adding results - (r'^result/add/json/$', 'add_json_results'), - (r'^result/add/$', 'add_result'), -) + url(r'^result/add/json/$', views.add_json_results, name='add-json-results'), + url(r'^result/add/$', views.add_result, name='add-result'), +] diff --git a/codespeed/validators.py b/codespeed/validators.py new file mode 100644 index 00000000..32b22fb1 --- /dev/null +++ b/codespeed/validators.py @@ -0,0 +1,46 @@ +from django.core.exceptions import ValidationError + + +def validate_results_request(data): + """ + Validates that a result request dictionary has all needed parameters + and their type is correct. + + Throws ValidationError on error. + """ + mandatory_data = [ + 'env', + 'proj', + 'branch', + 'exe', + 'ben', + ] + + for key in mandatory_data: + if key not in data: + raise ValidationError('Key "' + key + + '" missing from GET request!') + elif data[key] == '': + raise ValidationError('Value for key "' + key + + '" empty in GET request!') + + integer_data = [ + 'revs', + 'width', + 'height' + ] + + """ + Check that the items in integer_data are the correct format, + if they exist + """ + for key in integer_data: + if key in data: + try: + rev_value = int(data[key]) + except ValueError: + raise ValidationError('Value for "' + key + + '" is not an integer!') + if rev_value <= 0: + raise ValidationError('Value for "' + key + '" should be a' + ' strictly positive integer!') diff --git a/codespeed/views.py b/codespeed/views.py index 2e457907..7cbdfccd 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -1,208 +1,197 @@ # -*- coding: utf-8 -*- -from datetime import datetime -from itertools import chain +from __future__ import absolute_import, unicode_literals + import json import logging -from django.http import (HttpResponse, Http404, HttpResponseNotAllowed, - HttpResponseBadRequest) -from django.shortcuts import get_object_or_404, render_to_response -from django.template import RequestContext -from django.core.exceptions import ValidationError +import django from django.conf import settings +from django.urls import reverse +from django.core.exceptions import ValidationError, ObjectDoesNotExist +from django.http import HttpResponse, Http404, HttpResponseBadRequest, \ + HttpResponseNotFound, StreamingHttpResponse +from django.db.models import F +from django.shortcuts import get_object_or_404, render_to_response +from django.views.decorators.http import require_GET, require_POST +from django.views.decorators.csrf import csrf_exempt +from django.views.generic.base import TemplateView -from codespeed.models import (Environment, Report, Project, Revision, Result, - Executable, Benchmark, Branch) +from .auth import basic_auth_required +from .models import (Environment, Report, Project, Revision, Result, + Executable, Benchmark, Branch) +from .views_data import (get_default_environment, getbaselineexecutables, + getdefaultexecutable, getcomparisonexes, + get_benchmark_results, get_num_revs_and_benchmarks, + get_stats_with_defaults) +from .results import save_result, create_report_if_enough_data +from . import commits +from .validators import validate_results_request +from .images import gen_image_from_results + +logger = logging.getLogger(__name__) def no_environment_error(request): + admin_url = reverse('admin:codespeed_environment_changelist') return render_to_response('codespeed/nodata.html', { - 'message': 'You need to configure at least one Environment. Please go to the admin interface' - }, context_instance=RequestContext(request)) + 'message': ('You need to configure at least one Environment. ' + 'Please go to the ' + 'admin interface' % admin_url) + }) + def no_default_project_error(request): + admin_url = reverse('admin:codespeed_project_changelist') return render_to_response('codespeed/nodata.html', { - 'message': 'You need to configure at least one one Project as default (checked "Track changes" field).
Please go to the admin interface' - }, context_instance=RequestContext(request)) + 'message': ('You need to configure at least one one Project as ' + 'default (checked "Track changes" field).
' + 'Please go to the ' + 'admin interface' % admin_url) + }) + def no_executables_error(request): return render_to_response('codespeed/nodata.html', { 'message': 'There needs to be at least one executable' - }, context_instance=RequestContext(request)) + }) + def no_data_found(request): return render_to_response('codespeed/nodata.html', { 'message': 'No data found' - }, context_instance=RequestContext(request)) - - -def getbaselineexecutables(): - baseline = [{'key': "none", 'name': "None", 'executable': "none", 'revision': "none"}] - revs = Revision.objects.exclude(tag="") - maxlen = 22 - for rev in revs: - #add executables that correspond to each tagged revision. - for exe in Executable.objects.filter(project=rev.branch.project): - exestring = str(exe) - if len(exestring) > maxlen: exestring = str(exe)[0:maxlen] + "..." - name = exestring + " " + rev.tag - key = str(exe.id) + "+" + str(rev.id) - baseline.append({ - 'key': key, - 'executable': exe, - 'revision': rev, - 'name': name, - }) - # move default to first place - if hasattr(settings, 'DEF_BASELINE') and settings.DEF_BASELINE != None: - try: - for base in baseline: - if base['key'] == "none": - continue - exename = settings.DEF_BASELINE['executable'] - commitid = settings.DEF_BASELINE['revision'] - if base['executable'].name == exename and base['revision'].commitid == commitid: - baseline.remove(base) - baseline.insert(1, base) - break - except KeyError: - # TODO: write to server logs - #error in settings.DEF_BASELINE - pass - return baseline - - -def get_default_environment(enviros, data, multi=False): - """Returns the default environment. Preference level is: - * Present in URL parameters (permalinks) - * Value in settings.py - * First Environment ID - - """ - defaultenviros = [] - # Use permalink values - if 'env' in data: - for env_value in data['env'].split(","): - for env in enviros: - try: - env_id = int(env_value) - except ValueError: - # Not an int - continue - for env in enviros: - if env_id == env.id: - defaultenviros.append(env) - if not multi: - break - # Use settings.py value - if not defaultenviros and not multi: - if hasattr(settings, 'DEF_ENVIRONMENT') and \ - settings.DEF_ENVIRONMENT != None: - for env in enviros: - if settings.DEF_ENVIRONMENT == env.name: - defaultenviros.append(env) - break - # Last fallback - if not defaultenviros: - defaultenviros = enviros - if multi: - return defaultenviros - else: - return defaultenviros[0] + }) -def getdefaultexecutable(): - default = None - if hasattr(settings, 'DEF_EXECUTABLE') and settings.DEF_EXECUTABLE != None: - try: - default = Executable.objects.get(name=settings.DEF_EXECUTABLE) - except Executable.DoesNotExist: - pass - if default == None: - execquery = Executable.objects.filter(project__track=True) - if len(execquery): - default = execquery[0] - - return default - - -def getcomparisonexes(): - all_executables = {} - exekeys = [] - baselines = getbaselineexecutables() - for proj in Project.objects.all(): - executables = [] - executablekeys = [] - maxlen = 20 - # add all tagged revs for any project - for exe in baselines: - if exe['key'] != "none" and exe['executable'].project == proj: - executablekeys.append(exe['key']) - executables.append(exe) - - # add latest revs of the project - branches = Branch.objects.filter(project=proj) - for branch in branches: - try: - rev = Revision.objects.filter(branch=branch).latest('date') - except Revision.DoesNotExist: - continue - # Now only append when tag == "", - # because we already added tagged revisions - if rev.tag == "": - for exe in Executable.objects.filter(project=proj): - exestring = str(exe) - if len(exestring) > maxlen: - exestring = str(exe)[0:maxlen] + "..." - name = exestring + " latest" - if branch.name != 'default': - name += " in branch '" + branch.name + "'" - key = str(exe.id) + "+L+" + branch.name - executablekeys.append(key) - executables.append({ - 'key': key, - 'executable': exe, - 'revision': rev, - 'name': name, - }) - all_executables[proj] = executables - exekeys += executablekeys - return all_executables, exekeys +class HomeView(TemplateView): + template_name = "home.html" + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['show_reports'] = settings.SHOW_REPORTS + context['show_historical'] = settings.SHOW_HISTORICAL + historical_settings = ['SHOW_HISTORICAL', 'DEF_BASELINE', 'DEF_EXECUTABLE'] + if not all(getattr(settings, var) for var in historical_settings): + context['show_historical'] = False + return context -def getcomparisondata(request): - if request.method != 'GET': - return HttpResponseNotAllowed('GET') - data = request.GET + baseline_exe = Executable.objects.get( + name=settings.DEF_BASELINE['executable']) + context['baseline'] = baseline_exe + default_exe = Executable.objects.get(name=settings.DEF_EXECUTABLE) + context['default_exe'] = default_exe + return context + +@require_GET +def gethistoricaldata(request): + data = {'results': {}, 'benchmarks': []} + env = Environment.objects.all() + if settings.DEF_ENVIRONMENT: + env = env.get(name=settings.DEF_ENVIRONMENT) + else: + env = env.first() + + # Fetch Baseline data + baseline_exe = Executable.objects.get( + name=settings.DEF_BASELINE['executable']) + baseline_revs = Revision.objects.filter( + branch__project=baseline_exe.project).order_by('-date') + baseline_lastrev = baseline_revs[0] + for rev in baseline_revs: + baseline_results = Result.objects.filter( + executable=baseline_exe, revision=rev, environment=env) + if baseline_results: + baseline_lastrev = rev + break + if len(baseline_results) == 0: + logger.error('Could not find results for {} rev="{}" env="{}"'.format( + baseline_exe, baseline_lastrev, env)) + data['baseline'] = '{} {}'.format( + settings.DEF_BASELINE['executable'], baseline_lastrev.tag) + + default_exe = Executable.objects.get(name=settings.DEF_EXECUTABLE) + default_branch = Branch.objects.get( + name=default_exe.project.default_branch, + project=default_exe.project) + + # Fetch tagged revisions for default executable + default_taggedrevs = Revision.objects.filter( + branch=default_branch + ).exclude(tag="").order_by('date') + default_results = {} + for rev in default_taggedrevs: + res = Result.objects.filter( + executable=default_exe, revision=rev, environment=env) + if not res: + logger.info('no results for %s %s %s' % (str(default_exe), str(rev), str(env))) + continue + default_results[rev.tag] = res + data['tagged_revs'] = [rev.tag for rev in default_taggedrevs if rev.tag in default_results] + # Fetch data for latest results + revs = Revision.objects.filter( + branch=default_branch).order_by('-date')[:5] + default_lastrev = None + for i in range(4): + default_lastrev = revs[i] + if default_lastrev.results.filter(executable=default_exe, environment=env): + break + default_lastrev = None + if default_lastrev is None: + return HttpResponse(json.dumps(data)) + default_results['latest'] = Result.objects.filter( + executable=default_exe, revision=default_lastrev, environment=env) + + # Collect data + benchmarks = [] + for res in baseline_results: + if res == 0: + continue + benchmarks.append(res.benchmark.name) + data['results'][res.benchmark.name] = {data['baseline']: res.value} + for rev_name in default_results: + val = 0 + for default_res in default_results[rev_name]: + if default_res.benchmark.name == res.benchmark.name: + val = default_res.value + data['results'][res.benchmark.name][rev_name] = val + benchmarks.sort() + data['benchmarks'] = benchmarks + return HttpResponse(json.dumps(data)) + + +@require_GET +def getcomparisondata(request): executables, exekeys = getcomparisonexes() + benchmarks = Benchmark.objects.all() + environments = Environment.objects.all() compdata = {} compdata['error'] = "Unknown error" for proj in executables: for exe in executables[proj]: compdata[exe['key']] = {} - for env in Environment.objects.all(): + for env in environments: compdata[exe['key']][env.id] = {} - for bench in Benchmark.objects.all().order_by('name'): - try: - value = Result.objects.get( - environment=env, - executable=exe['executable'], - revision=exe['revision'], - benchmark=bench - ).value - except Result.DoesNotExist: - value = None - compdata[exe['key']][env.id][bench.id] = value + + # Load all results for this env/executable/revision in a + # dict for fast lookup + results = dict(Result.objects.filter( + environment=env, + executable=exe['executable'], + revision=exe['revision'], + ).values_list('benchmark', 'value')) + + for bench in benchmarks: + compdata[exe['key']][env.id][bench.id] = results.get( + bench.id, None) + compdata['error'] = "None" - return HttpResponse(json.dumps( compdata )) + return HttpResponse(json.dumps(compdata)) +@require_GET def comparison(request): - if request.method != 'GET': - return HttpResponseNotAllowed('GET') data = request.GET # Configuration of default parameters @@ -211,12 +200,12 @@ def comparison(request): return no_environment_error(request) checkedenviros = get_default_environment(enviros, data, multi=True) - if not len(Project.objects.all()): + if not len(Project.objects.filter(track=True)): return no_default_project_error(request) # Check whether there exist appropiate executables if not getdefaultexecutable(): - return no_executables_error() + return no_executables_error(request) executables, exekeys = getcomparisonexes() checkedexecutables = [] @@ -226,8 +215,7 @@ def comparison(request): continue if i in exekeys: checkedexecutables.append(i) - elif hasattr(settings, 'COMP_EXECUTABLES') and\ - settings.COMP_EXECUTABLES: + elif hasattr(settings, 'COMP_EXECUTABLES') and settings.COMP_EXECUTABLES: for exe, rev in settings.COMP_EXECUTABLES: try: exe = Executable.objects.get(name=exe) @@ -264,25 +252,31 @@ def comparison(request): benchmark_type="C" ).filter(units_title=unit) units = benchmarks[unit][0].units - lessisbetter = benchmarks[unit][0].lessisbetter and ' (less is better)' or ' (more is better)' - bench_units[unit] = [[b.id for b in benchmarks[unit]], lessisbetter, units] + lessisbetter = (benchmarks[unit][0].lessisbetter and + ' (less is better)' or ' (more is better)') + bench_units[unit] = [ + [b.id for b in benchmarks[unit]], lessisbetter, units + ] checkedbenchmarks = [] if 'ben' in data: checkedbenchmarks = [] for i in data['ben'].split(","): - if not i: continue + if not i: + continue try: checkedbenchmarks.append(Benchmark.objects.get(id=int(i))) except Benchmark.DoesNotExist: pass if not checkedbenchmarks: # Only include benchmarks marked as cross-project - checkedbenchmarks = Benchmark.objects.filter(benchmark_type="C") + checkedbenchmarks = Benchmark.objects.filter( + benchmark_type="C", default_on_comparison=True) charts = ['normal bars', 'stacked bars', 'relative bars'] # Don't show relative charts as an option if there is only one executable # Relative charts need normalization - if len(executables) == 1: charts.remove('relative bars') + if len(executables) == 1: + charts.remove('relative bars') selectedchart = charts[0] if 'chart' in data and data['chart'] in charts: @@ -296,12 +290,12 @@ def comparison(request): elif 'bas' in data: # bas is present but is none pass - elif len(exekeys) > 1 and hasattr(settings, 'NORMALIZATION') and\ - settings.NORMALIZATION: + elif (len(exekeys) > 1 and hasattr(settings, 'NORMALIZATION') and + settings.NORMALIZATION): try: # TODO: Avoid calling twice getbaselineexecutables selectedbaseline = getbaselineexecutables()[1]['key'] - # Uncheck exe used for normalization + # Uncheck exe used for normalization try: checkedexecutables.remove(selectedbaseline) except ValueError: @@ -310,8 +304,9 @@ def comparison(request): pass # Keep "none" as default baseline selecteddirection = False - if 'hor' in data and data['hor'] == "true" or\ - hasattr(settings, 'CHART_ORIENTATION') and settings.CHART_ORIENTATION == 'horizontal': + if ('hor' in data and data['hor'] == "true" or + hasattr(settings, 'CHART_ORIENTATION') and + settings.CHART_ORIENTATION == 'horizontal'): selecteddirection = True return render_to_response('codespeed/comparison.html', { @@ -326,125 +321,202 @@ def comparison(request): 'selectedbaseline': selectedbaseline, 'selectedchart': selectedchart, 'selecteddirection': selecteddirection - }, context_instance=RequestContext(request)) + }) + +def get_setting(name, default = None): + if hasattr(settings, name): + return getattr(settings, name) + else: + return default +@require_GET def gettimelinedata(request): - if request.method != 'GET': - return HttpResponseNotAllowed('GET') data = request.GET timeline_list = {'error': 'None', 'timelines': []} - executables = data.get('exe', "").split(",") - if not filter(None, executables): + executable_ids = data.get('exe', '').split(',') + + executables = [] + for i in executable_ids: + if not i: + continue + try: + executables.append(Executable.objects.get(id=int(i))) + except Executable.DoesNotExist: + pass + + if not executables: timeline_list['error'] = "No executables selected" - return HttpResponse(json.dumps( timeline_list )) + return HttpResponse(json.dumps(timeline_list)) environment = None try: environment = get_object_or_404(Environment, id=data.get('env')) except ValueError: Http404() - benchmarks = [] - number_of_revs = data.get('revs', 10) + number_of_revs, benchmarks = get_num_revs_and_benchmarks(data) - if data['ben'] == 'grid': - benchmarks = Benchmark.objects.all().order_by('name') - number_of_revs = 15 - elif data['ben'] == 'show_none': - benchmarks = [] - else: - benchmarks = [get_object_or_404(Benchmark, name=data['ben'])] - - baselinerev = None - baselineexe = None + baseline_rev = None + baseline_exe = None if data.get('base') not in (None, 'none', 'undefined'): - exeid, revid = data['base'].split("+") - baselinerev = Revision.objects.get(id=revid) - baselineexe = Executable.objects.get(id=exeid) + exe_id, rev_id = data['base'].split("+") + baseline_rev = Revision.objects.get(id=rev_id) + baseline_exe = Executable.objects.get(id=exe_id) + + next_benchmarks = data.get('nextBenchmarks', False) + if next_benchmarks is not False: + next_benchmarks = int(next_benchmarks) + + resp = StreamingHttpResponse(stream_timeline(baseline_exe, baseline_rev, benchmarks, data, + environment, executables, number_of_revs, + next_benchmarks), + content_type='application/json') + return resp + + +def stream_timeline(baseline_exe, baseline_rev, benchmarks, data, environment, executables, + number_of_revs, next_benchmarks): + yield '{"timelines": [' + num_results = {"results": 0} + num_benchmark = 0 + transmitted_benchmarks = 0 + timeline_grid_paging = get_setting('TIMELINE_GRID_PAGING', 10) + for bench in benchmarks: - lessisbetter = bench.lessisbetter and ' (less is better)' or ' (more is better)' - timeline = { - 'benchmark': bench.name, - 'benchmark_id': bench.id, - 'benchmark_description': bench.description, - 'units': bench.units, - 'lessisbetter': lessisbetter, - 'branches': {}, - 'baseline': "None", - } - # Temporary - trunks = [] - if Branch.objects.filter(name=settings.DEF_BRANCH): - trunks.append(settings.DEF_BRANCH) - #for branch in data2.get('bran', '').split(','): #-- For now, we'll only work with trunk branches - append = False - for branch in trunks: - append = False - timeline['branches'][branch] = {} - for executable in executables: - resultquery = Result.objects.filter( - benchmark=bench - ).filter( - environment=environment - ).filter( - executable=executable - ).filter( - revision__branch__name=branch - ).select_related( - "revision" - ).order_by('-revision__date')[:number_of_revs] - if not len(resultquery): - continue - - results = [] - for res in resultquery: + if transmitted_benchmarks + 1 > timeline_grid_paging: + # don't send more results than configured + break + + num_benchmark += 1 + + if not next_benchmarks or num_benchmark > next_benchmarks: + result = get_timeline_for_benchmark(baseline_exe, baseline_rev, bench, environment, + executables, number_of_revs, num_results) + if result != "": + transmitted_benchmarks += 1 + yield result + + if not next_benchmarks or (next_benchmarks < len(benchmarks) + and transmitted_benchmarks > 0): + next_page = ', "nextBenchmarks": ' + str(num_benchmark) + else: + next_page = ', "nextBenchmarks": false' + + if next_benchmarks: + not_first = ', "first": false' + else: + not_first = ', "first": true' + + if num_results['results'] == 0 and data['ben'] != 'show_none' and not next_benchmarks: + yield ']' + not_first + next_page + ', "error":"No data found for the selected options"}\n' + else: + yield ']' + not_first + next_page + ', "error":"None"}\n' + + +def get_timeline_for_benchmark(baseline_exe, baseline_rev, bench, environment, executables, + number_of_revs, num_results): + lessisbetter = bench.lessisbetter and ' (less is better)' or ' (more is better)' + timeline = { + 'benchmark': bench.name, + 'benchmark_id': bench.id, + 'benchmark_description': bench.description, + 'data_type': bench.data_type, + 'units': bench.units, + 'lessisbetter': lessisbetter, + 'branches': {}, + 'baseline': "None", + } + append = False + for branch in Branch.objects.filter( + project__track=True, name=F('project__default_branch')): + # For now, we'll only work with default branches + for executable in executables: + if executable.project != branch.project: + continue + + resultquery = Result.objects.filter( + benchmark=bench + ).filter( + environment=environment + ).filter( + executable=executable + ).filter( + revision__branch=branch + ).select_related( + "revision" + ).order_by('-revision__date')[:number_of_revs] + if not len(resultquery): + continue + timeline['branches'].setdefault(branch.name, {}) + + results = [] + for res in resultquery: + if bench.data_type == 'M': + q1, q3, val_max, val_min = get_stats_with_defaults(res) + results.append( + [ + res.revision.date.strftime('%Y/%m/%d %H:%M:%S %z'), + res.value, val_max, q3, q1, val_min, + res.revision.get_short_commitid(), res.revision.tag, branch.name + ] + ) + else: std_dev = "" - if res.std_dev != None: + if res.std_dev is not None: std_dev = res.std_dev results.append( - [str(res.revision.date), res.value, std_dev, res.revision.get_short_commitid(), branch] + [ + res.revision.date.strftime('%Y/%m/%d %H:%M:%S %z'), + res.value, std_dev, + res.revision.get_short_commitid(), res.revision.tag, branch.name + ] ) - timeline['branches'][branch][executable] = results - append = True - if baselinerev != None and append: - try: - baselinevalue = Result.objects.get( - executable=baselineexe, - benchmark=bench, - revision=baselinerev, - environment=environment - ).value - except Result.DoesNotExist: - timeline['baseline'] = "None" - else: - # determine start and end revision (x axis) from longest data series - results = [] - for exe in timeline['branches'][branch]: - if len(timeline['branches'][branch][exe]) > len(results): - results = timeline['branches'][branch][exe] - end = results[0][0] - start = results[len(results)-1][0] - timeline['baseline'] = [ - [str(start), baselinevalue], - [str(end), baselinevalue] - ] - if append: - timeline_list['timelines'].append(timeline) - - if not len(timeline_list['timelines']) and data['ben'] != 'show_none': - response = 'No data found for the selected options' - timeline_list['error'] = response - return HttpResponse(json.dumps( timeline_list )) + timeline['branches'][branch.name][executable.id] = results + append = True + if baseline_rev is not None and append: + try: + baselinevalue = Result.objects.get( + executable=baseline_exe, + benchmark=bench, + revision=baseline_rev, + environment=environment + ).value + except Result.DoesNotExist: + timeline['baseline'] = "None" + else: + # determine start and end revision (x axis) + # from longest data series + results = [] + for branch in timeline['branches']: + for exe in timeline['branches'][branch]: + if len(timeline['branches'][branch][exe]) > len(results): + results = timeline['branches'][branch][exe] + end = results[0][0] + start = results[len(results) - 1][0] + timeline['baseline'] = [ + [str(start), baselinevalue], + [str(end), baselinevalue] + ] + if append: + old_num_results = num_results['results'] + json_str = json.dumps(timeline) + num_results['results'] = old_num_results + len(timeline) + + if old_num_results > 0: + return "," + json_str + else: + return json_str + else: + return "" +@require_GET def timeline(request): - if request.method != 'GET': - return HttpResponseNotAllowed('GET') data = request.GET - ## Configuration of default parameters ## + # Configuration of default parameters # # Default Environment enviros = Environment.objects.all() if not enviros: @@ -461,7 +533,8 @@ def timeline(request): checkedexecutables = [] if 'exe' in data: for i in data['exe'].split(","): - if not i: continue + if not i: + continue try: checkedexecutables.append(Executable.objects.get(id=int(i))) except Executable.DoesNotExist: @@ -479,8 +552,8 @@ def timeline(request): branch_list.sort() defaultbranch = "" - if "default" in branch_list: - defaultbranch = settings.DEF_BRANCH + if defaultproject.default_branch in branch_list: + defaultbranch = defaultproject.default_branch if data.get('bran') in branch_list: defaultbranch = data.get('bran') @@ -496,29 +569,29 @@ def timeline(request): pass lastrevisions = [10, 50, 200, 1000] - defaultlast = 200 + defaultlast = settings.DEF_TIMELINE_LIMIT if 'revs' in data: if int(data['revs']) not in lastrevisions: lastrevisions.append(data['revs']) defaultlast = data['revs'] benchmarks = Benchmark.objects.all() - grid_limit = 30 + defaultbenchmark = "grid" if not len(benchmarks): return no_data_found(request) elif len(benchmarks) == 1: defaultbenchmark = benchmarks[0] - elif hasattr(settings, 'DEF_BENCHMARK') and settings.DEF_BENCHMARK != None: + elif hasattr(settings, 'DEF_BENCHMARK') and settings.DEF_BENCHMARK is not None: if settings.DEF_BENCHMARK in ['grid', 'show_none']: defaultbenchmark = settings.DEF_BENCHMARK else: try: defaultbenchmark = Benchmark.objects.get( - name=settings.DEF_BENCHMARK) + name=settings.DEF_BENCHMARK) except Benchmark.DoesNotExist: pass - elif len(benchmarks) >= grid_limit: + elif len(benchmarks) >= get_setting('TIMELINE_GRID_LIMIT', 30): defaultbenchmark = 'show_none' if 'ben' in data and data['ben'] != defaultbenchmark: @@ -526,17 +599,32 @@ def timeline(request): defaultbenchmark = data['ben'] else: defaultbenchmark = get_object_or_404(Benchmark, name=data['ben']) - + if 'equid' in data: defaultequid = data['equid'] else: defaultequid = "off" + if 'quarts' in data: + defaultquarts = data['quarts'] + else: + defaultquarts = "on" + if 'extr' in data: + defaultextr = data['extr'] + else: + defaultextr = "on" # Information for template + if defaultbenchmark in ['grid', 'show_none']: + pagedesc = None + else: + pagedesc = "Results timeline for the '%s' benchmark (project %s)" % \ + (defaultbenchmark, defaultproject) executables = {} for proj in Project.objects.filter(track=True): executables[proj] = Executable.objects.filter(project=proj) + use_median_bands = hasattr(settings, 'USE_MEDIAN_BANDS') and settings.USE_MEDIAN_BANDS return render_to_response('codespeed/timeline.html', { + 'pagedesc': pagedesc, 'checkedexecutables': checkedexecutables, 'defaultbaseline': defaultbaseline, 'baseline': baseline, @@ -549,10 +637,14 @@ def timeline(request): 'environments': enviros, 'branch_list': branch_list, 'defaultbranch': defaultbranch, - 'defaultequid' : defaultequid - }, context_instance=RequestContext(request)) + 'defaultequid': defaultequid, + 'defaultquarts': defaultquarts, + 'defaultextr': defaultextr, + 'use_median_bands': use_median_bands, + }) +@require_GET def getchangestable(request): executable = get_object_or_404(Executable, pk=request.GET.get('exe')) environment = get_object_or_404(Environment, pk=request.GET.get('env')) @@ -562,6 +654,47 @@ def getchangestable(request): raise Http404() selectedrev = get_object_or_404(Revision, commitid=request.GET.get('rev'), branch__project=executable.project) + prevrev = Revision.objects.filter( + branch=selectedrev.branch, + date__lt=selectedrev.date, + ).order_by('-date').first() + if prevrev: + try: + summary = Report.objects.get( + revision=prevrev, + executable=executable, + environment=environment).item_description + except Report.DoesNotExist: + summary = '' + prevrev = { + 'desc': str(prevrev), + 'rev': prevrev.commitid, + 'short_rev': prevrev.get_short_commitid(), + 'summary': summary, + } + else: + prevrev = None + + nextrev = Revision.objects.filter( + branch=selectedrev.branch, + date__gt=selectedrev.date, + ).order_by('date').first() + if nextrev: + try: + summary = Report.objects.get( + revision=nextrev, + executable=executable, + environment=environment).item_description + except Report.DoesNotExist: + summary = '' + nextrev = { + 'desc': str(nextrev), + 'rev': nextrev.commitid, + 'short_rev': nextrev.get_short_commitid(), + 'summary': summary, + } + else: + nextrev = None report, created = Report.objects.get_or_create( executable=executable, environment=environment, revision=selectedrev @@ -569,29 +702,34 @@ def getchangestable(request): tablelist = report.get_changes_table(trendconfig) if not len(tablelist): - return HttpResponse('

No results for this parameters

') + return HttpResponse('
' + '

No results for this ' + 'parameters

') - return render_to_response('codespeed/changes_table.html', { + return render_to_response('codespeed/changes_data.html', { 'tablelist': tablelist, 'trendconfig': trendconfig, 'rev': selectedrev, 'exe': executable, 'env': environment, - }, context_instance=RequestContext(request)) + 'prev': prevrev, + 'next': nextrev, + }) +@require_GET def changes(request): - if request.method != 'GET': return HttpResponseNotAllowed('GET') data = request.GET # Configuration of default parameters defaultchangethres = 3.0 defaulttrendthres = 4.0 - if hasattr(settings, 'CHANGE_THRESHOLD') and \ - settings.CHANGE_THRESHOLD != None: + if (hasattr(settings, 'CHANGE_THRESHOLD') and + settings.CHANGE_THRESHOLD is not None): defaultchangethres = settings.CHANGE_THRESHOLD - if hasattr(settings, 'TREND_THRESHOLD') and \ - settings.TREND_THRESHOLD != None: + if (hasattr(settings, 'TREND_THRESHOLD') and + settings.TREND_THRESHOLD is not None): defaulttrendthres = settings.TREND_THRESHOLD defaulttrend = 10 @@ -604,9 +742,12 @@ def changes(request): return no_environment_error(request) defaultenv = get_default_environment(enviros, data) + if not len(Project.objects.filter(track=True)): + return no_default_project_error(request) + defaultexecutable = getdefaultexecutable() if not defaultexecutable: - return no_executables_error() + return no_executables_error(request) if "exe" in data: try: @@ -635,10 +776,10 @@ def changes(request): for proj in Project.objects.filter(track=True): executables[proj] = Executable.objects.filter(project=proj) projectlist.append(proj) - branch = Branch.objects.filter(name=settings.DEF_BRANCH, project=proj) - revisionlists[proj.name] = Revision.objects.filter( + branch = Branch.objects.get(name=proj.default_branch, project=proj) + revisionlists[proj.name] = list(Revision.objects.filter( branch=branch - ).order_by('-date')[:revlimit] + ).order_by('-date')[:revlimit]) # Get lastest revisions for this project and it's "default" branch lastrevisions = revisionlists.get(defaultexecutable.project.name) if not len(lastrevisions): @@ -651,9 +792,8 @@ def changes(request): selectedrevision = Revision.objects.get( commitid__startswith=commitid, branch=branch ) - if not selectedrevision in lastrevisions: - lastrevisions = list(chain(lastrevisions)) - lastrevisions.append(selectedrevision) + if selectedrevision not in revisionlists[selectedrevision.project.name]: + revisionlists[selectedrevision.project.name].append(selectedrevision) except Revision.DoesNotExist: selectedrevision = lastrevisions[0] # This variable is used to know when the newly selected executable @@ -665,7 +805,16 @@ def changes(request): projectmatrix[e.id] = e.project.name projectmatrix = json.dumps(projectmatrix) + for project, revisions in revisionlists.items(): + revisionlists[project] = [ + (str(rev), rev.commitid) for rev in revisions + ] + revisionlists = json.dumps(revisionlists) + + pagedesc = "Report of %s performance changes for commit %s on branch %s" % \ + (defaultexecutable, selectedrevision.commitid, selectedrevision.branch) return render_to_response('codespeed/changes.html', { + 'pagedesc': pagedesc, 'defaultenvironment': defaultenv, 'defaultexecutable': defaultexecutable, 'selectedrevision': selectedrevision, @@ -675,26 +824,37 @@ def changes(request): 'environments': enviros, 'executables': executables, 'projectmatrix': projectmatrix, - 'revisionboxes': revisionlists, + 'revisionlists': revisionlists, 'trends': trends, - }, context_instance=RequestContext(request)) + }) +@require_GET def reports(request): - if request.method != 'GET': - return HttpResponseNotAllowed('GET') + context = {} + + context['reports'] = \ + Report.objects.order_by('-revision__date')[:10] - return render_to_response('codespeed/reports.html', { - 'reports': Report.objects.filter( - revision__branch__name=settings.DEF_BRANCH - ).order_by('-revision__date')[:10], - }, context_instance=RequestContext(request)) + context['significant_reports'] = Report.objects.filter( + colorcode__in=('red', 'green') + ).order_by('-revision__date')[:10] + return render_to_response('codespeed/reports.html', context) + +@require_GET def displaylogs(request): rev = get_object_or_404(Revision, pk=request.GET.get('revisionid')) logs = [] - logs.append(rev) + logs.append( + { + 'date': str(rev.date), 'author': rev.author, + 'author_email': '', 'message': rev.message, + 'short_commit_id': rev.get_short_commitid(), + 'commitid': rev.commitid + } + ) error = False try: startrev = Revision.objects.filter( @@ -705,278 +865,103 @@ def displaylogs(request): else: startrev = startrev[0] - remotelogs = getcommitlogs(rev, startrev) + remotelogs = commits.get_logs(rev, startrev) if len(remotelogs): try: if remotelogs[0]['error']: error = remotelogs[0]['message'] except KeyError: - pass#no errors + pass # no errors logs = remotelogs else: - error = 'no logs found' - except StandardError, e: - logging.error("Unhandled exception displaying logs for %s: %s", rev, e, exc_info=True) - error = repr(e) - - return render_to_response('codespeed/changes_logs.html', - {'error': error, 'logs': logs }, - context_instance=RequestContext(request)) - - -def getcommitlogs(rev, startrev, update=False): - logs = [] - - if rev.branch.project.repo_type == 'S': - from subversion import getlogs, updaterepo - elif rev.branch.project.repo_type == 'M': - from mercurial import getlogs, updaterepo - elif rev.branch.project.repo_type == 'G': - from git import getlogs, updaterepo - elif rev.branch.project.repo_type == 'H': - from github import getlogs, updaterepo - else: - if rev.branch.project.repo_type not in ("N", ""): - logging.warning("Don't know how to retrieve logs from %s project", - rev.branch.project.get_repo_type_display()) - return logs - - if update: - updaterepo(rev.branch.project) - - logs = getlogs(rev, startrev) - - # Remove last log because the startrev log shouldn't be shown - if len(logs) > 1 and logs[-1].get('commitid') == startrev.commitid: - logs.pop() - - return logs - - -def saverevisioninfo(rev): - log = getcommitlogs(rev, rev, update=True) - - if log: - log = log[0] - rev.author = log['author'] - rev.date = log['date'] - rev.message = log['message'] - - -def validate_result(item): - """ - Validates that a result dictionary has all needed parameters - - It returns a tuple - Environment, False when no errors where found - Errormessage, True when there is an error - """ - mandatory_data = [ - 'commitid', - 'branch', - 'project', - 'executable', - 'benchmark', - 'environment', - 'result_value', - ] - - response = {} - error = True - for key in mandatory_data: - if not key in item: - return 'Key "' + key + '" missing from request', error - elif key in item and item[key] == "": - return 'Value for key "' + key + '" empty in request', error - - # Check that the Environment exists - try: - e = Environment.objects.get(name=item['environment']) - error = False - return e, error - except Environment.DoesNotExist: - return "Environment %(environment)s not found" % item, error - - -def create_report_if_enough_data(rev, exe, e): - """Triggers Report creation when there are enough results""" - last_revs = Revision.objects.filter( - branch=rev.branch - ).order_by('-date')[:2] - if len(last_revs) > 1: - current_results = rev.results.filter(executable=exe, environment=e) - last_results = last_revs[1].results.filter(executable=exe,environment=e) - # If there is are at least as many results as in the last revision, - # create new report - if len(current_results) >= len(last_results): - logging.debug("create_report_if_enough_data: About to create new report") - report, created = Report.objects.get_or_create( - executable=exe, environment=e, revision=rev - ) - report.full_clean() - report.save() - logging.debug("create_report_if_enough_data: Created new report.") - - -def save_result(data): - res, error = validate_result(data) - if error: - return res, True - else: - assert(isinstance(res, Environment)) - env = res - - p, created = Project.objects.get_or_create(name=data["project"]) - branch, created = Branch.objects.get_or_create(name=data["branch"], - project=p) - b, created = Benchmark.objects.get_or_create(name=data["benchmark"]) - try: - rev = branch.revisions.get(commitid=data['commitid']) - except Revision.DoesNotExist: - rev_date = data.get("revision_date") - # "None" (as string) can happen when we urlencode the POST data - if not rev_date or rev_date in ["", "None"]: - rev_date = datetime.today() - rev = Revision(branch=branch, project=p, commitid=data['commitid'], - date=rev_date) - try: - rev.full_clean() - except ValidationError as e: - return str(e), True - if p.repo_type not in ("N", ""): - try: - saverevisioninfo(rev) - except RuntimeError as e: - logging.warning("unable to save revision %s info: %s", rev, e, - exc_info=True) - rev.save() - - exe, created = Executable.objects.get_or_create( - name=data['executable'], - project=p - ) - - try: - r = Result.objects.get( - revision=rev,executable=exe,benchmark=b,environment=env) - except Result.DoesNotExist: - r = Result(revision=rev,executable=exe,benchmark=b,environment=env) - - r.value = data["result_value"] - if 'result_date' in data: - r.date = data["result_date"] - elif rev.date: - r.date = rev.date - else: - r.date = datetime.now() - - r.std_dev = data.get('std_dev') - r.val_min = data.get('min') - r.val_max = data.get('max') - - r.full_clean() - r.save() - - return (rev, exe, env), False - - + error = 'No logs found' + except commits.exceptions.CommitLogError as e: + logger.error('Unhandled exception displaying logs for %s: %s', + rev, e, exc_info=True) + error = str(e) + + # Add commit browsing url to logs + project = rev.branch.project + for log in logs: + log['commit_browse_url'] = project.commit_browsing_url.format(**log) + + return render_to_response( + 'codespeed/changes_logs.html', + { + 'error': error, 'logs': logs, + 'show_email_address': settings.SHOW_AUTHOR_EMAIL_ADDRESS + }) + + +@csrf_exempt +@require_POST +@basic_auth_required('results') def add_result(request): - if request.method != 'POST': - return HttpResponseNotAllowed('POST') - data = request.POST - - response, error = save_result(data) + response, error = save_result(request.POST) if error: - logging.error("Could not save result: " + response) + logger.error("Could not save result: " + response) return HttpResponseBadRequest(response) else: create_report_if_enough_data(response[0], response[1], response[2]) - logging.debug("add_result: completed") - return HttpResponse("Result data saved succesfully", status=202) + return HttpResponse("Result data saved successfully", status=202) +@csrf_exempt +@require_POST +@basic_auth_required('results') def add_json_results(request): - if request.method != 'POST': - return HttpResponseNotAllowed('POST') if not request.POST.get('json'): return HttpResponseBadRequest("No key 'json' in POST payload") data = json.loads(request.POST['json']) - logging.info("add_json_results request with %d entries." % len(data)) + logger.info("add_json_results request with %d entries." % len(data)) unique_reports = set() - i = 0 - for result in data: - i += 1 - logging.debug("add_json_results: save item %d." % i) - response, error = save_result(result) + for (i, result) in enumerate(data): + logger.debug("add_json_results: save item %d." % i) + response, error = save_result(result, update_repo=(i==0)) if error: - logging.debug( + logger.debug( "add_json_results: could not save item %d because %s" % ( - i, response)) + i, response)) return HttpResponseBadRequest(response) else: unique_reports.add(response) - logging.debug("add_json_results: about to create reports") for rep in unique_reports: create_report_if_enough_data(rep[0], rep[1], rep[2]) - logging.debug("add_json_results: completed") - return HttpResponse("All result data saved successfully", status=202) +def django_has_content_type(): + return (django.VERSION[0] > 1 or + (django.VERSION[0] == 1 and django.VERSION[1] >= 6)) -def get_home_data(request): - if request.method != 'GET': - return HttpResponseNotAllowed('GET') - data = {'results': {}, 'benchmarks': []} - env = Environment.objects.get(name='tannit') - # Fetch CPython data - cp_exe = Executable.objects.get(name="cpython") - cp_lastrev = Revision.objects.filter( - branch__project=cp_exe.project).order_by('-date')[0] - data['baseline'] = 'CPython ' + cp_lastrev.tag - cp_results = Result.objects.filter( - executable=cp_exe, revision=cp_lastrev, environment=env) - - pp_exe = Executable.objects.get(name="pypy-c-jit") - pp_branch = Branch.objects.get(name="default", project=pp_exe.project) - # Fetch PyPy tagged revisions - pp_taggedrevs = Revision.objects.filter( - branch=pp_branch - ).exclude(tag="").order_by('date') - data['tagged_revs'] = [rev.tag for rev in pp_taggedrevs] - pp_results = {} - for rev in pp_taggedrevs: - pp_results[rev.tag] = Result.objects.filter( - executable=pp_exe, revision=rev, environment=env) - - # Fetch PyPy trunk data - revs = Revision.objects.filter(branch=pp_branch).order_by('-date')[:5] - pp_lastrev = None - for i in range(4): - pp_lastrev = revs[i] - if pp_lastrev.results.filter(executable=pp_exe): - break - pp_lastrev = None - if pp_lastrev is None: - return HttpResponse(json.dumps( data )) - pp_results['PyPy trunk'] = Result.objects.filter( - executable=pp_exe, revision=pp_lastrev, environment=env) - # Save data - benchmarks = [] - for res in cp_results: - if res == 0: - continue - benchmarks.append(res.benchmark.name) - data['results'][res.benchmark.name] = {data['baseline']: res.value} - for rev_name in pp_results: - val = 0 - for pp_res in pp_results[rev_name]: - if pp_res.benchmark.name == res.benchmark.name: - val = pp_res.value - data['results'][res.benchmark.name][rev_name] = val - benchmarks.sort() - data['benchmarks'] = benchmarks - return HttpResponse(json.dumps( data )) +@require_GET +def makeimage(request): + data = request.GET + + try: + validate_results_request(data) + except ValidationError as err: + return HttpResponseBadRequest(str(err)) + + try: + result_data = get_benchmark_results(data) + except ObjectDoesNotExist as err: + return HttpResponseNotFound(str(err)) + + image_data = gen_image_from_results( + result_data, + int(data['width']) if 'width' in data else None, + int(data['height']) if 'height' in data else None) + + if django_has_content_type(): + response = HttpResponse(content=image_data, content_type='image/png') + else: + response = HttpResponse(content=image_data, mimetype='image/png') + + response['Content-Length'] = len(image_data) + response['Content-Disposition'] = 'attachment; filename=image.png' + + return response diff --git a/codespeed/views_data.py b/codespeed/views_data.py new file mode 100644 index 00000000..8ae6902c --- /dev/null +++ b/codespeed/views_data.py @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from django.shortcuts import get_object_or_404 + +from codespeed.models import ( + Executable, Revision, Project, Branch, + Environment, Benchmark, Result) + + +def get_default_environment(enviros, data, multi=False): + """Returns the default environment. Preference level is: + * Present in URL parameters (permalinks) + * Value in settings.py + * First Environment ID + + """ + defaultenviros = [] + # Use permalink values + if 'env' in data: + for env_value in data['env'].split(","): + for env in enviros: + try: + env_id = int(env_value) + except ValueError: + # Not an int + continue + for env in enviros: + if env_id == env.id: + defaultenviros.append(env) + if not multi: + break + # Use settings.py value + if not defaultenviros and not multi: + if (hasattr(settings, 'DEF_ENVIRONMENT') and + settings.DEF_ENVIRONMENT is not None): + for env in enviros: + if settings.DEF_ENVIRONMENT == env.name: + defaultenviros.append(env) + break + # Last fallback + if not defaultenviros: + defaultenviros = enviros + if multi: + return defaultenviros + else: + return defaultenviros[0] + + +def getbaselineexecutables(): + baseline = [{ + 'key': "none", + 'name': "None", + 'executable': "none", + 'revision': "none", + }] + executables = Executable.objects.select_related('project') + revs = Revision.objects.exclude(tag="").select_related('branch__project') + maxlen = 22 + for rev in revs: + # Add executables that correspond to each tagged revision. + for exe in [e for e in executables if e.project == rev.branch.project]: + exestring = str(exe) + if len(exestring) > maxlen: + exestring = str(exe)[0:maxlen] + "..." + name = exestring + " " + rev.tag + key = str(exe.id) + "+" + str(rev.id) + baseline.append({ + 'key': key, + 'executable': exe, + 'revision': rev, + 'name': name, + }) + # move default to first place + if hasattr(settings, 'DEF_BASELINE') and settings.DEF_BASELINE is not None: + try: + exename = settings.DEF_BASELINE['executable'] + commitid = settings.DEF_BASELINE['revision'] + for base in baseline: + if base['key'] == "none": + continue + if (base['executable'].name == exename and + base['revision'].commitid == commitid): + baseline.remove(base) + baseline.insert(1, base) + break + except KeyError: + # TODO: write to server logs + # error in settings.DEF_BASELINE + pass + return baseline + + +def getdefaultexecutable(): + default = None + if (hasattr(settings, 'DEF_EXECUTABLE') and + settings.DEF_EXECUTABLE is not None): + try: + default = Executable.objects.get(name=settings.DEF_EXECUTABLE) + except Executable.DoesNotExist: + pass + if default is None: + execquery = Executable.objects.filter(project__track=True) + if len(execquery): + default = execquery[0] + + return default + + +def getcomparisonexes(): + all_executables = {} + exekeys = [] + baselines = getbaselineexecutables() + for proj in Project.objects.all(): + executables = [] + executablekeys = [] + maxlen = 20 + # add all tagged revs for any project + for exe in baselines: + if exe['key'] != "none" and exe['executable'].project == proj: + executablekeys.append(exe['key']) + executables.append(exe) + + # add latest revs of the project + branches = Branch.objects.filter(project=proj) + for branch in branches: + try: + rev = Revision.objects.filter(branch=branch).latest('date') + except Revision.DoesNotExist: + continue + # Now only append when tag == "", + # because we already added tagged revisions + if rev.tag == "": + for exe in Executable.objects.filter(project=proj): + exestring = str(exe) + if len(exestring) > maxlen: + exestring = str(exe)[0:maxlen] + "..." + name = exestring + " latest" + if branch.name != 'default': + name += " in branch '" + branch.name + "'" + key = str(exe.id) + "+L+" + branch.name + executablekeys.append(key) + executables.append({ + 'key': key, + 'executable': exe, + 'revision': rev, + 'name': name, + }) + all_executables[proj] = executables + exekeys += executablekeys + return all_executables, exekeys + + +def get_benchmark_results(data): + environment = Environment.objects.get(name=data['env']) + project = Project.objects.get(name=data['proj']) + executable = Executable.objects.get(name=data['exe'], project=project) + branch = Branch.objects.get(name=data['branch'], project=project) + benchmark = Benchmark.objects.get(name=data['ben']) + + number_of_revs = int(data.get('revs', 10)) + + baseline_commit_name = (data['base_commit'] if 'base_commit' in data + else None) + relative_results = ( + ('relative' in data and data['relative'] in ['1', 'yes']) or + baseline_commit_name is not None) + + result_query = Result.objects.filter( + benchmark=benchmark + ).filter( + environment=environment + ).filter( + executable=executable + ).filter( + revision__project=project + ).filter( + revision__branch=branch + ).select_related( + "revision" + ).order_by('-date')[:number_of_revs] + + if len(result_query) == 0: + raise ObjectDoesNotExist("No results were found!") + + result_list = [item for item in result_query] + result_list.reverse() + + if relative_results: + ref_value = result_list[0].value + + if baseline_commit_name is not None: + baseline_env = environment + baseline_proj = project + baseline_exe = executable + baseline_branch = branch + + if 'base_env' in data: + baseline_env = Environment.objects.get(name=data['base_env']) + if 'base_proj' in data: + baseline_proj = Project.objects.get(name=data['base_proj']) + if 'base_exe' in data: + baseline_exe = Executable.objects.get(name=data['base_exe'], + project=baseline_proj) + if 'base_branch' in data: + baseline_branch = Branch.objects.get(name=data['base_branch'], + project=baseline_proj) + + base_data = Result.objects.get( + benchmark=benchmark, + environment=baseline_env, + executable=baseline_exe, + revision__project=baseline_proj, + revision__branch=baseline_branch, + revision__commitid=baseline_commit_name) + + ref_value = base_data.value + + if relative_results: + for element in result_list: + element.value = (100 * (element.value - ref_value)) / ref_value + + return { + 'environment': environment, + 'project': project, + 'executable': executable, + 'branch': branch, + 'benchmark': benchmark, + 'results': result_list, + 'relative': relative_results, + } + + +def get_num_revs_and_benchmarks(data): + if data['ben'] == 'grid': + benchmarks = Benchmark.objects.all().order_by('name') + number_of_revs = 15 + elif data['ben'] == 'show_none': + benchmarks = [] + number_of_revs = int(data.get('revs', 10)) + else: + benchmarks = [get_object_or_404(Benchmark, name=data['ben'])] + number_of_revs = int(data.get('revs', 10)) + return number_of_revs, benchmarks + + +def get_stats_with_defaults(res): + val_min = "" + if res.val_min is not None: + val_min = res.val_min + val_max = "" + if res.val_max is not None: + val_max = res.val_max + q1 = "" + if res.q1 is not None: + q1 = res.q1 + q3 = "" + if res.q3 is not None: + q3 = res.q3 + return q1, q3, val_max, val_min diff --git a/example/manage.py b/example/manage.py deleted file mode 100644 index 5e78ea97..00000000 --- a/example/manage.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python -from django.core.management import execute_manager -try: - import settings # Assumed to be in the same directory. -except ImportError: - import sys - sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) - sys.exit(1) - -if __name__ == "__main__": - execute_manager(settings) diff --git a/example/requirements.txt b/example/requirements.txt deleted file mode 100644 index 34cd48c0..00000000 --- a/example/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -Django>=1.3 -South>=0.7.3 -#-e git://github.com/robhudson/django-debug-toolbar.git#egg=django-debug-toolbar diff --git a/example/settings.py b/example/settings.py index 956319d5..f63cf7ae 100644 --- a/example/settings.py +++ b/example/settings.py @@ -55,7 +55,7 @@ # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. # Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = '/admin_media/' +ADMIN_MEDIA_PREFIX = '/static/admin/' # Make this unique, and don't share it with anybody. SECRET_KEY = 'as%n_m#)^vee2pe91^^@c))sl7^c6t-9r8n)_69%)2yt+(la2&' @@ -176,8 +176,5 @@ def process_exception(self, request, exception): # ('myexe', '21df2423ra'), # ('myexe', 'L'),] -#DEF_BRANCH = "default" # Defines the default branch to be used. - # In git projects, this branch is usually be calles - # "master" +from .local_settings import * -from local_settings import * diff --git a/example/templates/base.html b/example/templates/base.html index af7d6b92..cc60dfb4 100644 --- a/example/templates/base.html +++ b/example/templates/base.html @@ -49,6 +49,16 @@

{% block page_title %}SPEED CENTER{% endblock page_title %}

{% block footer %} + + {% endblock %} - - - - - - +{% if show_historical %} + + + + + + + + +{% endif %} {% endblock %} -{% block navigation %} -{% endblock navigation %} - -{% block body %} -
- - - -
- -
-

How fast is PyPy?

-
-

Plot 1: The above plot represents PyPy trunk (with JIT) benchmark times normalized to CPython. Smaller is better.

-

It depends greatly on the type of task being performed. The geometric average of all benchmarks is or times faster than CPython

-

How has PyPy performance evolved over time?

- -
-

Plot 2: Speedup compared to CPython, using the inverse of the geometric average of normalized times, out of benchmarks (see paper on why the geometric mean is better for normalized results).

-
-
-{% endblock body %} diff --git a/speed_pypy/urls.py b/speed_pypy/urls.py new file mode 100644 index 00000000..ed416c79 --- /dev/null +++ b/speed_pypy/urls.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +from django.conf import settings +from django.conf.urls import include, url +from django.contrib import admin + +urlpatterns = [ + url(r'^admin/', admin.site.urls), + url(r'^', include('codespeed.urls')) +] + +if settings.DEBUG: + # needed for development server + from django.contrib.staticfiles.urls import staticfiles_urlpatterns + urlpatterns += staticfiles_urlpatterns() diff --git a/example/wsgi.py b/speed_pypy/wsgi.py similarity index 100% rename from example/wsgi.py rename to speed_pypy/wsgi.py diff --git a/tools/create_trunks.py b/tools/create_trunks.py index 785163b0..5514e3b9 100644 --- a/tools/create_trunks.py +++ b/tools/create_trunks.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- - """ Create the default branch for all existing projects Starting v 0.8.0 that is mandatory. @@ -8,30 +7,32 @@ as the project settings.py. Otherwise you have to set the shell environment DJANGO_SETTINGS_MODULE """ - import sys import os + ## Setup to import models from Django app ## def import_from_string(name): """helper to import module from a given string""" - components = name.split('.')[1:] return reduce(lambda mod, y: getattr(mod, y), components, __import__(name)) + sys.path.append(os.path.abspath('..')) -if os.environ.has_key('DJANGO_SETTINGS_MODULE'): + + +if 'DJANGO_SETTINGS_MODULE' in os.environ: settings = import_from_string(os.environ['DJANGO_SETTINGS_MODULE']) else: try: - import settings # Assumed to be in the same directory. + import settings # Assumed to be in the same directory. except ImportError: import sys sys.stderr.write( "Error: Can't find the file 'settings.py' in the directory " "containing %r. It appears you've customized things.\nYou'll have " - "to run django-admin.py, passing it your settings module.\n(If the " - "file settings.py does indeed exist, it's causing an ImportError " + "to run django-admin.py, passing it your settings module.\n(If the" + " file settings.py does indeed exist, it's causing an ImportError " "somehow.)\n" % __file__) sys.exit(1) @@ -46,7 +47,7 @@ def main(): for proj in projects: if not proj.branches.filter(name='default'): - trunk = Branch(name='default', project = proj) + trunk = Branch(name='default', project=proj) trunk.save() print "Created branch 'default' for project {0}".format(proj) diff --git a/tools/migrate_script.py b/tools/migrate_script.py index b1e28884..e87379e9 100644 --- a/tools/migrate_script.py +++ b/tools/migrate_script.py @@ -1,14 +1,13 @@ # -*- coding: utf-8 -*- -import sys -import os - -""" -Add the default branch to all existing revisions +"""Adds the default branch to all existing revisions Note: This file is assumed to be in the same directory as the project settings.py. Otherwise you have to set the shell environment DJANGO_SETTINGS_MODULE """ +import sys +import os + ## Setup to import models from Django app ## def import_from_string(name): @@ -18,18 +17,19 @@ def import_from_string(name): return reduce(lambda mod, y: getattr(mod, y), components, __import__(name)) sys.path.append(os.path.abspath('..')) -if os.environ.has_key('DJANGO_SETTINGS_MODULE'): + +if 'DJANGO_SETTINGS_MODULE' in os.environ: settings = import_from_string(os.environ['DJANGO_SETTINGS_MODULE']) else: try: - import settings # Assumed to be in the same directory. + import settings # Assumed to be in the same directory. except ImportError: import sys sys.stderr.write( "Error: Can't find the file 'settings.py' in the directory " "containing %r. It appears you've customized things.\nYou'll have " - "to run django-admin.py, passing it your settings module.\n(If the " - "file settings.py does indeed exist, it's causing an ImportError " + "to run django-admin.py, passing it your settings module.\n(If the" + " file settings.py does indeed exist, it's causing an ImportError " "somehow.)\n" % __file__) sys.exit(1) @@ -50,4 +50,3 @@ def main(): if __name__ == '__main__': main() - diff --git a/tools/pypy/import_from_json.py b/tools/pypy/import_from_json.py index bd68e8db..5d3eb21d 100644 --- a/tools/pypy/import_from_json.py +++ b/tools/pypy/import_from_json.py @@ -55,7 +55,8 @@ interpreter = INTERP int_options = "" options = "" - if result.has_key('options'): options = result['options'] + if 'options' in result: + options = result['options'] host = 'tannit' #saveresults.save(proj, revision, result['results'], options, interpreter, host) diff --git a/tools/pypy/saveresults.py b/tools/pypy/saveresults.py index 4909dc78..8ca8ee75 100644 --- a/tools/pypy/saveresults.py +++ b/tools/pypy/saveresults.py @@ -54,7 +54,7 @@ def send(data): f = urllib2.urlopen(SPEEDURL + 'result/add/', params) response = f.read() f.close() - except urllib2.URLError, e: + except urllib2.URLError as e: if hasattr(e, 'reason'): response = '\n We failed to reach a server\n' response += ' Reason: ' + str(e.reason) diff --git a/tools/save_multiple_results.py b/tools/save_multiple_results.py index ad6121a8..bc262e1a 100644 --- a/tools/save_multiple_results.py +++ b/tools/save_multiple_results.py @@ -2,7 +2,10 @@ #################################################### # Sample script that shows how to save result data using json # #################################################### -import urllib, urllib2, json +import urllib +import urllib2 +import json + # You need to enter the real URL and have the server running CODESPEED_URL = 'http://localhost:8000/' @@ -28,6 +31,7 @@ } ] + def add(data): #params = urllib.urlencode(data) response = "None" diff --git a/tools/save_single_result.py b/tools/save_single_result.py index 913cbd6d..830858e7 100644 --- a/tools/save_single_result.py +++ b/tools/save_single_result.py @@ -3,7 +3,8 @@ # Sample script that shows how to save result data # #################################################### from datetime import datetime -import urllib, urllib2 +import urllib +import urllib2 # You need to enter the real URL and have the server running CODESPEED_URL = 'http://localhost:8000/' @@ -13,7 +14,7 @@ # Mandatory fields data = { 'commitid': '14', - 'branch': 'default', # Always use default for trunk/master/tip + 'branch': 'default', # Always use default for trunk/master/tip 'project': 'MyProject', 'executable': 'myexe O3 64bits', 'benchmark': 'float', @@ -23,14 +24,15 @@ # Optional fields data.update({ - 'revision_date': current_date, # Optional. Default is taken either - # from VCS integration or from current date - 'result_date': current_date, # Optional, default is current date - 'std_dev': 1.11111, # Optional. Default is blank - 'max': 4001.6, # Optional. Default is blank - 'min': 3995.1, # Optional. Default is blank + 'revision_date': current_date, # Optional. Default is taken either + # from VCS integration or from current date + 'result_date': current_date, # Optional, default is current date + 'std_dev': 1.11111, # Optional. Default is blank + 'max': 4001.6, # Optional. Default is blank + 'min': 3995.1, # Optional. Default is blank }) + def add(data): params = urllib.urlencode(data) response = "None" diff --git a/tools/test_performance.py b/tools/test_performance.py index 90c6cb75..20408da4 100644 --- a/tools/test_performance.py +++ b/tools/test_performance.py @@ -1,9 +1,13 @@ -import timeit, urllib, sys +import timeit +import urllib +import sys + SPEEDURL = 'http://localhost:8000/' benchmarks = ['ai', 'django', 'spambayes', 'grid'] + def test_overview(): data = { "trend": 10, @@ -13,13 +17,14 @@ def test_overview(): "host": "bigdog", } params = urllib.urlencode(data) - page= urllib.urlopen(SPEEDURL + 'overview/table/?' + params) + page = urllib.urlopen(SPEEDURL + 'overview/table/?' + params) jsonstring = page.read() page.close() if not '' in jsonstring: print "bad overview response" sys.exit(1) + def test_timeline(bench): data = { "executables": "1,2,6", @@ -29,7 +34,7 @@ def test_timeline(bench): "revisions": 200 } params = urllib.urlencode(data) - page= urllib.urlopen(SPEEDURL + 'timeline/json/?' + params) + page = urllib.urlopen(SPEEDURL + 'timeline/json/?' + params) jsonstring = page.read() #print jsonstring page.close() @@ -44,13 +49,14 @@ def test_timeline(bench): print print "OVERVIEW RESULTS" print "min:", min(results) - print "avg:", sum(results)/len(results) + print "avg:", sum(results) / len(results) print print "TIMELINE RESULTS" for bench in benchmarks: - t = timeit.Timer('test_timeline("'+bench+'")', 'from __main__ import test_timeline') + t = timeit.Timer('test_timeline("' + bench + '")', + 'from __main__ import test_timeline') results = t.repeat(20, 1) print "benchmark =", bench print "min:", min(results) - print "avg:", sum(results)/len(results) + print "avg:", sum(results) / len(results) print