diff --git a/.gitignore b/.gitignore index 9c43ee52..98aa82a4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,6 @@ .coverage *.pyc reports +tests/media diff --git a/django_jenkins/management/commands/__init__.py b/django_jenkins/management/commands/__init__.py index 0668a8d6..5900c1e3 100644 --- a/django_jenkins/management/commands/__init__.py +++ b/django_jenkins/management/commands/__init__.py @@ -47,7 +47,7 @@ def __init__(self): super(TaskListCommand, self).__init__() self.tasks_cls = [import_module(module_name).Task for module_name in self.get_task_list()] - def handle(self, *test_labels, **options): + def initialize(self, *test_labels, **options): # instantiate tasks self.tasks = self.get_tasks(*test_labels, **options) @@ -58,16 +58,18 @@ def handle(self, *test_labels, **options): if signal_handler: signal.connect(signal_handler) - # run + # setup test runner test_runner_cls = get_runner() - test_runner = test_runner_cls( + self.test_runner = test_runner_cls( output_dir=options['output_dir'], interactive=options['interactive'], debug=options['debug'], verbosity=int(options.get('verbosity', 1)), with_reports=options.get('with_reports', True)) - if test_runner.run_tests(test_labels): + def handle(self, *test_labels, **options): + self.initialize(*test_labels, **options) + if self.test_runner.run_tests(test_labels): sys.exit(1) def get_tasks(self, *test_labels, **options): diff --git a/django_jenkins/management/commands/ci.py b/django_jenkins/management/commands/ci.py new file mode 100644 index 00000000..0eae2a0b --- /dev/null +++ b/django_jenkins/management/commands/ci.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8; mode: django -*- +import os +import sys +from itertools import groupby +from django.utils.importlib import import_module +from django_jenkins.standalone.storage import Storage +from django_jenkins.management.commands.jenkins import Command as BaseCICommand + +def task_view(task): + return getattr(task, 'view', None) + + +class Command(BaseCICommand): + help = "Run CI process, and collect data for django_jenkins.standalon" + args = '[appname ...]' + + def handle(self, *test_labels, **options): + storage = Storage.open() + build_id = storage['last_build_id'] + 1 + + # substitute output_dir + options['output_dir'] = os.path.join(Storage.ci_root(), 'build-%d' % build_id) + + # run + self.initialize(*test_labels, **options) + result = self.test_runner.run_tests(test_labels) + + # store results and exit + build_data = {} + + # here is jenkins-task views come to play + build_data['views'] = [] + self.tasks.sort(key=task_view) + for view_name, tasks in groupby(self.tasks, task_view): + view = import_module(view_name) + build_data[view_name] = view.get_build_data(tasks) + build_data['views'].append(view_name) + + # tests + #test_result = self.test_runner.result + #build_data['tests-successes'] = len(test_result.successes) + #build_data['tests-failures'] = len(test_result.failures) + #build_data['tests-errors'] = len(test_result.errors) + + # End jenkins-task views + + storage['build-%d' % build_id] = build_data + storage['last_build_id'] = build_id + storage.close() + + if result: + sys.exit(1) diff --git a/django_jenkins/standalone/__init__.py b/django_jenkins/standalone/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/django_jenkins/standalone/storage.py b/django_jenkins/standalone/storage.py new file mode 100644 index 00000000..52fec15a --- /dev/null +++ b/django_jenkins/standalone/storage.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8; mode: django -*- +import os +import shelve +from django.conf import settings + + +class Storage(object): + def __init__(self, storage): + self.storage = storage + + @staticmethod + def ci_root(): + ci_root = getattr(settings, 'CI_ROOT', os.path.join(settings.MEDIA_ROOT, 'ci')) + if not os.path.exists(ci_root): + os.makedirs(ci_root) + return ci_root + + @staticmethod + def open(): + storage = shelve.open(os.path.join(Storage.ci_root(), 'cidata.shelve')) + if 'version' not in storage: + storage['version'] = '1.0' + if 'last_build_id' not in storage: + storage['last_build_id'] = 0 + return Storage(storage=storage) + + def __getitem__(self, key): + return self.storage[key] + + def __setitem__(self, key, value): + self.storage[key] = value + + def __contains__(self, item): + return item in self.storage + + def close(self): + self.storage.close() + diff --git a/django_jenkins/standalone/taskviews/__init__.py b/django_jenkins/standalone/taskviews/__init__.py new file mode 100644 index 00000000..ef85f2b7 --- /dev/null +++ b/django_jenkins/standalone/taskviews/__init__.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8; mode: django -*- +""" +Store and view task results data in standalone ci +""" +from django.views.generic import View + + +class BaseTaskDataExtract(object): + def extract_build_data(self, tasks): + """ + Extrats current build ci data + """ + return {} + + def extract_details_data(self, tasks): + """ + Extract details view data + """ + return None + + + +class BaseTaskDataView(object): + """ + Renters html part of ci index page + """ + def add_build_data(self, build_id, build_data): + pass + + def render_part(self, request): + pass + + +class TaskDetailView(View): + """ + Renders build task detail page + """ + def view(request, build_id, build_data): + pass diff --git a/django_jenkins/standalone/taskviews/tests_view.py b/django_jenkins/standalone/taskviews/tests_view.py new file mode 100644 index 00000000..16f2af03 --- /dev/null +++ b/django_jenkins/standalone/taskviews/tests_view.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8; mode: django -*- +""" +View for tasks with tests runner +""" +import math +import sys +from pygooglechart import Axis, SimpleLineChart +from django.template.loader import render_to_string +from django_jenkins.standalone import taskviews + + +class TaskDataExtract(taskviews.BaseTaskDataExtract): + """ + Extract data from test runner tasks. + + Each task should have test_runner and output_file attributes + """ + def extract_build_data(self, tasks): + """ + Extrats current build ci data + """ + result = {} + result['tests-successes'] = 0 + result['tests-failures'] = 0 + result['tests-errors'] = 0 + + for task in tasks: + test_result = task.test_runner.result + result['tests-successes'] += len(test_result.successes) + result['tests-failures'] += len(test_result.failures) + result['tests-errors'] += len(test_result.errors) + + return result + + def extract_details_data(self, tasks): + """ + Extract details view data + """ + output = [] + for task in tasks: + output.append(task.output_file) + return { 'output' : output } + + +class TaskDataView(taskviews.BaseTaskDataView): + """ + Renters html part of ci index page + """ + def __init__(self): + self.min_build_id, self.max_build_id = sys.max_int, 0 + self.successes = [] + self.failures = [] + self.errors = [] + + def add_build_data(self, build_id, build_data): + self.min_build_id = min(build_id, self.min_build_id) + self.max_build_id = max(build_id, self.max_build_id) + self.successes.append(build_data['tests-successes']) + self.failures.append(build_data['tests-failures']) + self.errors.append(build_data['tests-errors']) + + def build_chart(self): + tests_chart = SimpleLineChart(400, 250) + all_fails = [f+e for f,e in zip(self.failures, self.errors)] + all_results = [s+af for s,af in zip(self.successes, all_fails)] + + # axis + max_tests = max(all_results) + step = round(max_tests/10, 1-len(str(max_tests/10))) + total_steps = math.ceil(1.0 * max_tests/step) + tests_chart.set_axis_labels(Axis.LEFT, xrange(0, step*(total_steps+1), step)) + tests_chart.set_axis_labels(Axis.BOTTOM, xrange(self.min_build_id, self.max_build_id+1)) + tests_chart.set_grid(0, step/2, 5, 5) + + # First value - allowed maximum + tests_chart.add_data([step*total_steps] * 2) + # All tests, failures and errors, errors + tests_chart.add_data(all_results) + tests_chart.add_data(all_fails) + tests_chart.add_data(self.errors) + # Last value is the lowest in the Y axis. + tests_chart.add_data([0] * 2) + + # Fill colors + tests_chart.set_colours(['FFFFFF', '00FF00', 'FF0000', '0000FF']) + tests_chart.add_fill_range('00FF00', 1, 2) + tests_chart.add_fill_range('FF0000', 2, 3) + tests_chart.add_fill_range('0000FF', 3, 4) + + return tests_chart + + def render_part(self, request): + return render_to_string('django_jenkins/tests_part.html', + { 'chart' : self.build_chart(), + 'title' : 'Tests result' }) + + +class TaskDetailView(taskviews.BaseTaskDetailView): + """ + Renders build task detail page + """ + def view(request, build_id, build_data): + pass + diff --git a/django_jenkins/standalone/urls.py b/django_jenkins/standalone/urls.py new file mode 100644 index 00000000..52ed1f89 --- /dev/null +++ b/django_jenkins/standalone/urls.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8; mode: django -*- +from django.conf.urls.defaults import patterns, url + + +urlpatterns = patterns('django_jenkins.standalone.views', + url(r'^$', 'index', name="ci_index"), +) + diff --git a/django_jenkins/standalone/views.py b/django_jenkins/standalone/views.py new file mode 100644 index 00000000..5c2f5c91 --- /dev/null +++ b/django_jenkins/standalone/views.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8; mode: django -*- +import math +from pygooglechart import Axis, SimpleLineChart +from django.shortcuts import render +from django_jenkins.standalone.storage import Storage + + +def index(request): + try: + storage = Storage.open() + + # TODO Here jenkins-task views come to play + + # tests + last_build_id = storage['last_build_id'] + successes, failures, errors = [], [], [] + for build_id in xrange(1, last_build_id+1): + build_data = storage['build-%d' % build_id] + successes.append(build_data['tests-successes']) + failures.append(build_data['tests-failures']) + errors.append(build_data['tests-errors']) + + + tests_chart = SimpleLineChart(400, 250) + tests_chart.BASE_URL = 'http://chart.googleapis.com/chart?' + # axis + max_tests = max([s+f+e for s,f,e in zip(successes,failures,errors)]) + step = round(max_tests/10, 1-len(str(max_tests/10))) + total_steps = math.ceil(1.0 * max_tests/step) + tests_chart.set_axis_labels(Axis.LEFT, xrange(0, step*(total_steps+1), step)) + tests_chart.set_axis_labels(Axis.BOTTOM, xrange(1, last_build_id+1)) + tests_chart.set_grid(0, step/2, 5, 5) + + # First value - allowed maximum + tests_chart.add_data([step*total_steps] * 2) + # All tests, failures and errors, errors + tests_chart.add_data([s+f+e for s,f,e in zip(successes,failures,errors)]) + tests_chart.add_data([f+e for f,e in zip(failures,errors)]) + tests_chart.add_data(errors) + # Last value is the lowest in the Y axis. + tests_chart.add_data([0] * 2) + + # Fill colors + tests_chart.set_colours(['FFFFFF', '00FF00', 'FF0000', '0000FF']) + tests_chart.add_fill_range('00FF00', 1, 2) + tests_chart.add_fill_range('FF0000', 2, 3) + tests_chart.add_fill_range('0000FF', 3, 4) + + # End jenkins-task views + + return render(request, 'django_jenkins/index.html', + { 'tests_chart' : tests_chart }) + finally: + storage.close() + diff --git a/django_jenkins/tasks/__init__.py b/django_jenkins/tasks/__init__.py index 9ecbe682..32a38a18 100644 --- a/django_jenkins/tasks/__init__.py +++ b/django_jenkins/tasks/__init__.py @@ -8,6 +8,7 @@ class BaseTask(object): """ Base interface for ci tasks """ + view = None option_list = [] def __init__(self, test_labels, options): diff --git a/django_jenkins/tasks/csslint b/django_jenkins/tasks/csslint index 757301be..a90d8f2b 160000 --- a/django_jenkins/tasks/csslint +++ b/django_jenkins/tasks/csslint @@ -1 +1 @@ -Subproject commit 757301beb9ba2022aa9f140816c38127f9a2ade1 +Subproject commit a90d8f2b1fa6f67c69c3a9288cdd82bf5e8217a8 diff --git a/django_jenkins/tasks/dir_tests.py b/django_jenkins/tasks/dir_tests.py index 427cddd6..50304b46 100644 --- a/django_jenkins/tasks/dir_tests.py +++ b/django_jenkins/tasks/dir_tests.py @@ -20,6 +20,8 @@ def build_suite(app): class Task(BaseTask): + view = 'django_jenkins.standalone.taskviews.tests_view' + def __init__(self, test_labels, options): super(Task, self).__init__(test_labels, options) if not self.test_labels: diff --git a/django_jenkins/tasks/django_tests.py b/django_jenkins/tasks/django_tests.py index 9580913e..4bf2ee97 100644 --- a/django_jenkins/tasks/django_tests.py +++ b/django_jenkins/tasks/django_tests.py @@ -11,6 +11,8 @@ class Task(BaseTask): + view = 'django_jenkins.standalone.taskviews.tests_view' + def __init__(self, test_labels, options): super(Task, self).__init__(test_labels, options) if not self.test_labels: diff --git a/django_jenkins/tasks/jslint b/django_jenkins/tasks/jslint index 52769e7f..eec32d4b 160000 --- a/django_jenkins/tasks/jslint +++ b/django_jenkins/tasks/jslint @@ -1 +1 @@ -Subproject commit 52769e7f1ebf004d0f3272cb7159aa65360c109e +Subproject commit eec32d4b4bc7945672c49a5ffdf4dc6c87b7058b diff --git a/django_jenkins/tasks/lettuce_tests.py b/django_jenkins/tasks/lettuce_tests.py index 8ebf35e8..29985568 100644 --- a/django_jenkins/tasks/lettuce_tests.py +++ b/django_jenkins/tasks/lettuce_tests.py @@ -10,6 +10,8 @@ class Task(BaseTask): + view = 'django_jenkins.standalone.taskviews.tests_view' + option_list = [ make_option("--lettuce-server", dest="lettuce-server", diff --git a/django_jenkins/templates/django_jenkins/base_ci.html b/django_jenkins/templates/django_jenkins/base_ci.html new file mode 100644 index 00000000..1e9a08b3 --- /dev/null +++ b/django_jenkins/templates/django_jenkins/base_ci.html @@ -0,0 +1,10 @@ + + + Continous integration results + + +

CI results

+ {% block content %} + {% endblock %} + + diff --git a/django_jenkins/templates/django_jenkins/index.html b/django_jenkins/templates/django_jenkins/index.html new file mode 100644 index 00000000..ecbd9bec --- /dev/null +++ b/django_jenkins/templates/django_jenkins/index.html @@ -0,0 +1,5 @@ +{% extends 'django_jenkins/base_ci.html' %} + +{% block content %} + +{% endblock %}' diff --git a/django_jenkins/templates/django_jenkins/part.html b/django_jenkins/templates/django_jenkins/part.html new file mode 100644 index 00000000..e7c48f6c --- /dev/null +++ b/django_jenkins/templates/django_jenkins/part.html @@ -0,0 +1,4 @@ +{% block content %} +

{{ title }} + +{% endblock %}' diff --git a/django_jenkins/templates/django_jenkins/tests_part.html b/django_jenkins/templates/django_jenkins/tests_part.html new file mode 100644 index 00000000..5d3aa7bd --- /dev/null +++ b/django_jenkins/templates/django_jenkins/tests_part.html @@ -0,0 +1 @@ +{% extends 'django_jenkins/part.html' %} diff --git a/tests/requirements.pip b/tests/requirements.pip index f183c7a5..ae79f7e0 100644 --- a/tests/requirements.pip +++ b/tests/requirements.pip @@ -4,6 +4,7 @@ coverage>=3.4 pyflakes pep8 lettuce +pygooglechart # development only ipdb diff --git a/tests/settings.py b/tests/settings.py index a00b2902..38578f2d 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -29,6 +29,8 @@ } } +MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'media') + JENKINS_TASKS = ( 'django_jenkins.tasks.with_coverage', 'django_jenkins.tasks.django_tests', diff --git a/tests/test_app/urls.py b/tests/test_app/urls.py index d0107b0d..9aa2ac97 100644 --- a/tests/test_app/urls.py +++ b/tests/test_app/urls.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -from django.conf.urls.defaults import * +from django.conf.urls.defaults import patterns, url, include urlpatterns = patterns('', - url(r'^wm_test_click/$', 'django.views.generic.simple.direct_to_template', - {'template': 'test_app/wm_test_click.html'}, name='wm_test_click') + url(r'^ci/$', include('django_jenkins.standalone.urls')), ) +