├── .gitignore ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── django_jenkins ├── __init__.py ├── apps.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── jenkins.py ├── models.py ├── runner.py └── tasks │ ├── __init__.py │ ├── pylint.rc │ ├── run_flake8.py │ ├── run_pep8.py │ ├── run_pyflakes.py │ ├── run_pylint.py │ └── with_coverage.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── manage.py ├── runner.py ├── settings.py ├── static │ ├── css │ │ ├── test.css │ │ └── test.scss │ └── js │ │ └── test.js ├── test_app │ ├── __init__.py │ ├── features │ │ ├── example.feature │ │ └── example_steps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── not_for_coverage │ │ ├── __init__.py │ │ ├── one.py │ │ └── two.py │ ├── south_migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── static │ │ ├── css │ │ │ └── test_errors.css │ │ └── js │ │ │ └── test.js │ ├── templates │ │ ├── 404.html │ │ ├── 500.html │ │ └── test_app │ │ │ └── wm_test_click.html │ ├── tests.py │ ├── urls.py │ └── views.py ├── test_app_dirs │ ├── __init__.py │ ├── models │ │ ├── __init__.py │ │ └── sample.py │ ├── not_for_coverage │ │ ├── __init__.py │ │ ├── one.py │ │ └── two.py │ └── tests │ │ ├── __init__.py │ │ └── test_discovery_dir_tests.py └── test_non_bound.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .ve2? 3 | .ve3? 4 | .coverage 5 | *.pyc 6 | dist 7 | reports 8 | .reports_py?? 9 | .tox 10 | django_jenkins.egg-info/ 11 | MANIFEST 12 | geckodriver.log 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 0.110.0 2016-09-15 2 | ~~~~~~~~~~~~~~~~~~ 3 | 4 | * Flake8 >= 3.0 support 5 | * `scss-lint` task added 6 | 7 | 0.19.0 2016-06-15 8 | ~~~~~~~~~~~~~~~~~ 9 | 10 | * Flake8 >= 2.5.0 support 11 | * Drop django 1.7 support 12 | * Tested on django 1.10 13 | * Add suppport for `.add_arguments` from custom test runner for `jenkins` command 14 | 15 | 0.18.0 1985-10-26 16 | ~~~~~~~~~~~~~~~~~ 17 | 18 | * An exceptional release for the last 5 years issued not on 15th day of a month 19 | * Drop python 2.6 support 20 | * Drop django 1.6 support 21 | * Add django 1.9 compatibility 22 | * Drop scss-lint support (the tool no longer have xml output) 23 | 24 | 0.17.0 2015-04-15 25 | ~~~~~~~~~~~~~~~~~~ 26 | 27 | * Django 1.8 compatibility 28 | * Added support for excluding paths in the pyflakes runner 29 | * --coverage-html-report option removed 30 | * --coverage-format option added 31 | 32 | 0.16.4 2014-12-15 33 | ~~~~~~~~~~~~~~~~~ 34 | 35 | * New scss-lint task 36 | * Added support for pylint plugins 37 | * Include STATICFILES_DIRS to search path for csslint and jshint 38 | * Search pep8 config in setup.cfg and tox.ini. 39 | * Fix non-ascii tracback problem 40 | * Fix error handlin in case exception happens in fixture loading 41 | * Fix max-complexity option overriding 42 | 43 | 0.16.3 2014-08-15 44 | ~~~~~~~~~~~~~~~~~ 45 | 46 | * Python 2.6 compatibility returned 47 | * Added `--project-apps-tests` options to limit tests discovery by `PROJECT_APPS` setting value 48 | * Fix coverage for apps with separate models packages under django 1.6 49 | * Fix missing pep8 excludes option from pep8 config 50 | 51 | 52 | 0.16.0 2014-07-15 53 | ~~~~~~~~~~~~~~~~~ 54 | 55 | * Django 1.7 compatibility 56 | * Support for all standard django test runner options 57 | * Migrations now our friends and checked by all linters, `south_migrations` are ignored 58 | * ``django_jenkins.tasks.with_coverage`` depricated, use command line option instead `./manage.py jenkins --enable-coverage` 59 | * ``django_jenkins.tasks.run_graphmodels`` removed 60 | * ``django_jenkins.tasks.with_local_celery`` removed 61 | * Ability to run linters (pep8/pyflakes/pylint/csslint/jshint) from command line without tests removed (feel free to PR it back, if you need them) 62 | 63 | 64 | 0.15.0 2014-02-15 65 | ~~~~~~~~~~~~~~~~~ 66 | 67 | * Speed up and reduced memory usage for junit reports generation 68 | * django_tests and dir_tests test discovery tasks are replaced by directory discover test runner build-in in django 1.6 69 | * Removed unmaintained lettuce tests support 70 | * Removed unmaintained behave tests support 71 | * Fixed non-asci support in junit reports 72 | 73 | 74 | 0.14.1 2013-08-15 75 | ~~~~~~~~~~~~~~~~~ 76 | 77 | * Django 1.6 compatibility 78 | * Flake8 support 79 | * Pep8 file configuration support 80 | * CSSLint no longer shipped with django-jenkins. Install it with ``npm install csslint -g`` 81 | 82 | 83 | 0.14.0 2012-12-15 84 | ~~~~~~~~~~~~~~~~~ 85 | 86 | * Python 3 (with django 1.5) support 87 | * JSHint no longer shipped with django-jenkins. Install it with ``npm install jshint -g`` 88 | 89 | 90 | 0.13.0 2012-07-15 91 | ~~~~~~~~~~~~~~~~~ 92 | 93 | * unittest2 compatibility 94 | * **WARNING:** Junit test data now stored in one junit.xml file 95 | * Support for pep8 1.3 96 | * New in-directory test discovery task 97 | * Added --liveserver option 98 | * Fixes in jslint and csslint tasks 99 | 100 | 0.12.1 2012-03-15 101 | ~~~~~~~~~~~~~~~~~ 102 | 103 | * Added Celery task 104 | * Add nodejs support for jslint and csslint tasks 105 | * Improve js and css files selection 106 | * Bug fixes 107 | 108 | 0.12.0 2012-01-15 109 | ~~~~~~~~~~~~~~~~~ 110 | 111 | * Django 1.3 in requirements 112 | * Windmill support was removed (Django 1.4 has a better implementation) 113 | * Ignore South migrations by default 114 | * Added SLOCCount task 115 | * Added Lettuce testing task 116 | * Added CSS Lint task 117 | * Used xml output format for jslint 118 | * Used native pep8 output format 119 | 120 | 0.11.1 2010-06-15 121 | ~~~~~~~~~~~~~~~~~ 122 | 123 | * Do not produce file reports for jtest command by default 124 | * Ignore Django apps without models.py file, as in Django test command 125 | * Fix jslint_runner.js packaging 126 | * Fix coverage file filtering 127 | 128 | 0.11.0 2010-04-15 129 | ~~~~~~~~~~~~~~~~~ 130 | 131 | * Support pep8, Pyflakes, jslint tools 132 | * Added jtest command 133 | * Allow specify custom test runner 134 | * Various fixes, thnk githubbers :) 135 | 136 | 0.10.0 2010-02-15 137 | ~~~~~~~~~~~~~~~~~ 138 | 139 | * Pluggable ci tasks refactoring 140 | * Alpha support for windmill tests 141 | * Partial python 2.4 compatibility 142 | * Renamed to django-jenkins 143 | 144 | 0.9.1 2010-12-15 145 | ~~~~~~~~~~~~~~~~ 146 | 147 | * Python 2.5 compatibility 148 | * Make compatible with latest Pylint only 149 | 150 | 0.9.0 2010-10-15 151 | ~~~~~~~~~~~~~~~~ 152 | 153 | * Initial public release 154 | 155 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | 167 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include MANIFEST.in 4 | include django_jenkins/tasks/pylint.rc 5 | recursive-include tests * 6 | global-exclude __pycache__ 7 | global-exclude *.pyc 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-jenkins 2 | ============== 3 | 4 | Plug and play continuous integration with Django and Jenkins 5 | 6 | .. image:: https://img.shields.io/pypi/v/django-jenkins.svg 7 | :target: https://pypi.python.org/pypi/django-jenkins 8 | 9 | .. image:: https://requires.io/github/kmmbvnr/django-jenkins/requirements.png?branch=master 10 | :target: https://requires.io/github/kmmbvnr/django-jenkins/requirements/?branch=master 11 | 12 | .. image:: https://badges.gitter.im/Join%20Chat.svg 13 | :target: https://gitter.im/kmmbvnr/django-jenkins?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 14 | 15 | 16 | Installation 17 | ------------ 18 | 19 | From PyPI:: 20 | 21 | $ pip install django-jenkins 22 | 23 | Or by downloading the source and running:: 24 | 25 | $ python setup.py install 26 | 27 | Latest git version:: 28 | 29 | $ pip install -e git+git://github.com/kmmbvnr/django-jenkins.git#egg=django-jenkins 30 | $ pip install coverage 31 | 32 | Installation for Python 3:: 33 | 34 | Works out of the box 35 | 36 | Usage 37 | ----- 38 | 39 | Add ``'django_jenkins'`` to your ``INSTALLED_APPS`` list. 40 | Configure Jenkins to run the following command:: 41 | 42 | $ ./manage.py jenkins --enable-coverage 43 | 44 | This will create reports/ directory with junit xml, Coverage and Pylint 45 | reports. 46 | 47 | For more details see the generic tutorial: https://sites.google.com/site/kmmbvnr/home/django-jenkins-tutorial 48 | 49 | Settings 50 | -------- 51 | 52 | - ``PROJECT_APPS`` 53 | 54 | If present, it is supposed to be a list/tuple of django apps for Jenkins to run. 55 | Tests, reports, and coverage are generated only for the apps from this list. 56 | 57 | - ``JENKINS_TASKS`` 58 | 59 | List of Jenkins reporters executed by ``./manage.py jenkins`` command. 60 | 61 | Default value:: 62 | 63 | JENKINS_TASKS = () 64 | 65 | - ``JENKINS_TEST_RUNNER`` 66 | 67 | The name of the class to use for starting the test suite for ``jenkins`` command. 68 | Class should be inherited from 69 | ``django_jenkins.runner.CITestSuiteRunner`` 70 | 71 | 72 | Reporters 73 | --------- 74 | 75 | Here is the reporters prebuild with django-jenkins 76 | 77 | - ``django_jenkins.tasks.run_pylint`` 78 | 79 | Runs Pylint_ over selected Django apps. 80 | 81 | Task-specific settings: ``PYLINT_RCFILE`` 82 | 83 | .. _Pylint: http://www.logilab.org/project/pylint 84 | 85 | - ``django_jenkins.tasks.run_pep8`` 86 | 87 | Runs pep8 tool over selected Django apps. 88 | Creates Pylint compatible report for Jenkins 89 | 90 | You should have pep8_ python package (>=1.3) installed to run this task. 91 | 92 | Task-specific settings: ``PEP8_RCFILE`` 93 | 94 | .. _pep8: http://pypi.python.org/pypi/pep8 95 | 96 | - ``django_jenkins.tasks.run_pyflakes`` 97 | 98 | Runs Pyflakes tool over selected Django apps. 99 | Creates Pylint compatible report for Jenkins. 100 | 101 | You should have Pyflakes_ python package installed to run this task. 102 | 103 | .. _Pyflakes: http://pypi.python.org/pypi/pyflakes 104 | 105 | - ``django_jenkins.tasks.run_flake8`` 106 | 107 | Runs flake8 tool over selected Django apps. 108 | Creates pep8 compatible report for Jenkins. 109 | 110 | You should have flake8_ python package installed to run this task. 111 | 112 | .. _flake8: http://pypi.python.org/pypi/flake8 113 | 114 | 115 | Changelog 116 | --------- 117 | 118 | GIT Version 119 | ~~~~~~~~~~~~~~~~~~ 120 | 121 | * csslint/jshint/sccsLint/sloccount task removed (could be used as standalone tools) 122 | 123 | 124 | Contribution guide 125 | ~~~~~~~~~~~~~~~~~~ 126 | 127 | * Set up local jenkins 128 | * Set up django-jenkins:: 129 | 130 | npm install jshint 131 | npm install csslint 132 | PATH=$PATH:$WORKSPACE/node_modules/.bin 133 | tox 134 | 135 | * Ensure that everything works 136 | * Modify the *the only one thing* 137 | * Ensure that everything works again 138 | * Fix pep8/pyflakes errors and minimize pylint's warnings 139 | * Pull request! 140 | 141 | Authors 142 | ------- 143 | Created and maintained by Mikhail Podgurskiy 144 | 145 | Contributors: https://github.com/kmmbvnr/django-jenkins/graphs/contributors 146 | 147 | Special thanks, for all github forks authors for project extensions ideas and problem identifications. 148 | -------------------------------------------------------------------------------- /django_jenkins/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'django_jenkins.apps.JenkinsConfig' 2 | -------------------------------------------------------------------------------- /django_jenkins/apps.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from django.apps import AppConfig 3 | from django_jenkins.tasks.with_coverage import CoverageReporter 4 | 5 | 6 | class JenkinsConfig(AppConfig): 7 | """ 8 | Enable coverage measurement as soon as possible 9 | """ 10 | name = 'django_jenkins' 11 | 12 | def __init__(self, app_name, app_module): 13 | super(JenkinsConfig, self).__init__(app_name, app_module) 14 | 15 | self.coverage = None 16 | 17 | if 'jenkins' in sys.argv and '--enable-coverage' in sys.argv: 18 | """ 19 | Starting coverage as soon as possible 20 | """ 21 | self.coverage = CoverageReporter() 22 | -------------------------------------------------------------------------------- /django_jenkins/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmmbvnr/django-jenkins/f673c8ecded24eb56aeaef8d1c0caeb82208b595/django_jenkins/management/__init__.py -------------------------------------------------------------------------------- /django_jenkins/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmmbvnr/django-jenkins/f673c8ecded24eb56aeaef8d1c0caeb82208b595/django_jenkins/management/commands/__init__.py -------------------------------------------------------------------------------- /django_jenkins/management/commands/jenkins.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import warnings 4 | from importlib import import_module 5 | 6 | from django.apps import apps 7 | from django.conf import settings 8 | from django.core.management.commands.test import Command as TestCommand 9 | 10 | from django_jenkins.runner import CITestSuiteRunner 11 | 12 | 13 | def get_runner(settings, test_runner_class=None): 14 | if test_runner_class is None: 15 | test_runner_class = getattr( 16 | settings, 'JENKINS_TEST_RUNNER', 'django_jenkins.runner.CITestSuiteRunner') 17 | 18 | test_module_name, test_cls = test_runner_class.rsplit('.', 1) 19 | test_module = __import__(test_module_name, {}, {}, test_cls) 20 | test_runner = getattr(test_module, test_cls) 21 | 22 | if not issubclass(test_runner, CITestSuiteRunner): 23 | raise ValueError('Your custom TestRunner should extend ' 24 | 'the CITestSuiteRunner class.') 25 | return test_runner 26 | 27 | 28 | class Command(TestCommand): 29 | def __init__(self): 30 | self.test_runner = None 31 | self.tasks_cls = [import_module(module_name).Reporter 32 | for module_name in self.get_task_list()] 33 | self.tasks = [task_cls() for task_cls in self.tasks_cls] 34 | super(Command, self).__init__() 35 | 36 | def run_from_argv(self, argv): 37 | """ 38 | Pre-parse the command line to extract the value of the --testrunner 39 | option. This allows a test runner to define additional command line 40 | arguments. 41 | """ 42 | option = '--testrunner=' 43 | for arg in argv[2:]: 44 | if arg.startswith(option): 45 | self.test_runner = arg[len(option):] 46 | break 47 | super(Command, self).run_from_argv(argv) 48 | 49 | def get_task_list(self): 50 | return getattr(settings, 'JENKINS_TASKS', ()) 51 | 52 | def add_arguments(self, parser): 53 | super(Command, self).add_arguments(parser) 54 | parser.add_argument('--output-dir', dest='output_dir', default="reports", 55 | help='Report files directory'), 56 | parser.add_argument("--enable-coverage", 57 | action="store_true", default=False, 58 | help="Measure code coverage"), 59 | parser.add_argument('--debug', action='store_true', 60 | dest='debug', default=False, 61 | help='Do not intercept stdout and stderr, friendly for console debuggers'), 62 | parser.add_argument("--coverage-rcfile", 63 | dest="coverage_rcfile", 64 | default="", 65 | help="Specify configuration file."), 66 | parser.add_argument("--coverage-format", 67 | dest="coverage_format", 68 | default="xml", 69 | help="Specify coverage output formats html,xml,bin"), 70 | parser.add_argument("--coverage-exclude", action="append", 71 | default=[], dest="coverage_excludes", 72 | help="Module name to exclude"), 73 | parser.add_argument("--project-apps-tests", action="store_true", 74 | default=False, dest="project_apps_tests", 75 | help="Take tests only from project apps") 76 | 77 | parser._optionals.conflict_handler = 'resolve' 78 | for task in self.tasks: 79 | if hasattr(task, 'add_arguments'): 80 | task.add_arguments(parser) 81 | 82 | test_runner_class = get_runner(settings, self.test_runner) 83 | 84 | if hasattr(test_runner_class, 'add_arguments'): 85 | test_runner_class.add_arguments(parser) 86 | 87 | def handle(self, *test_labels, **options): 88 | TestRunner = get_runner(settings, options['testrunner']) 89 | options['verbosity'] = int(options.get('verbosity')) 90 | 91 | if options.get('liveserver') is not None: 92 | os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = options['liveserver'] 93 | del options['liveserver'] 94 | 95 | output_dir = options['output_dir'] 96 | if not os.path.exists(output_dir): 97 | os.makedirs(output_dir) 98 | 99 | test_runner = TestRunner(**options) 100 | 101 | if not test_labels and options['project_apps_tests']: 102 | test_labels = getattr(settings, 'PROJECT_APPS', []) 103 | 104 | failures = test_runner.run_tests(test_labels) 105 | 106 | if failures: 107 | sys.exit(bool(failures)) 108 | else: 109 | tested_locations = self.get_tested_locations(test_labels) 110 | 111 | coverage = apps.get_app_config('django_jenkins').coverage 112 | if coverage: 113 | if options['verbosity'] >= 1: 114 | print('Storing coverage info...') 115 | 116 | coverage.save(tested_locations, options) 117 | 118 | # run reporters 119 | for task in self.tasks: 120 | if options['verbosity'] >= 1: 121 | print('Executing {0}...'.format(task.__module__)) 122 | task.run(tested_locations, **options) 123 | 124 | if options['verbosity'] >= 1: 125 | print('Done') 126 | 127 | def get_tested_locations(self, test_labels): 128 | locations = [] 129 | 130 | coverage = apps.get_app_config('django_jenkins').coverage 131 | if test_labels: 132 | pass 133 | elif hasattr(settings, 'PROJECT_APPS'): 134 | test_labels = settings.PROJECT_APPS 135 | elif coverage and coverage.coverage.source: 136 | warnings.warn("No PROJECT_APPS settings, using 'source' config from rcfile") 137 | locations = coverage.coverage.source 138 | else: 139 | warnings.warn('No PROJECT_APPS settings, coverage gathered over all apps') 140 | test_labels = settings.INSTALLED_APPS 141 | 142 | for test_label in test_labels: 143 | app_config = apps.get_containing_app_config(test_label) 144 | if app_config is not None: 145 | locations.append(app_config.path) 146 | else: 147 | warnings.warn('No app found for test: {0}'.format(test_label)) 148 | 149 | return locations 150 | -------------------------------------------------------------------------------- /django_jenkins/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmmbvnr/django-jenkins/f673c8ecded24eb56aeaef8d1c0caeb82208b595/django_jenkins/models.py -------------------------------------------------------------------------------- /django_jenkins/runner.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | from unittest import TextTestResult 5 | 6 | from xml.etree import ElementTree as ET 7 | 8 | from django.test.runner import DiscoverRunner 9 | from django.utils.encoding import smart_text 10 | 11 | 12 | class EXMLTestResult(TextTestResult): 13 | def __init__(self, *args, **kwargs): 14 | self.case_start_time = time.time() 15 | self.run_start_time = None 16 | self.tree = None 17 | super(EXMLTestResult, self).__init__(*args, **kwargs) 18 | 19 | def startTest(self, test): 20 | self.case_start_time = time.time() 21 | super(EXMLTestResult, self).startTest(test) 22 | 23 | def startTestRun(self): 24 | self.tree = ET.Element('testsuite') 25 | self.run_start_time = time.time() 26 | super(EXMLTestResult, self).startTestRun() 27 | 28 | def addSuccess(self, test): 29 | self.testcase = self._make_testcase_element(test) 30 | super(EXMLTestResult, self).addSuccess(test) 31 | 32 | def addFailure(self, test, err): 33 | self.testcase = self._make_testcase_element(test) 34 | test_result = ET.SubElement(self.testcase, 'failure') 35 | self._add_tb_to_test(test, test_result, err) 36 | super(EXMLTestResult, self).addFailure(test, err) 37 | 38 | def addError(self, test, err): 39 | self.testcase = self._make_testcase_element(test) 40 | test_result = ET.SubElement(self.testcase, 'error') 41 | self._add_tb_to_test(test, test_result, err) 42 | super(EXMLTestResult, self).addError(test, err) 43 | 44 | def addUnexpectedSuccess(self, test): 45 | self.testcase = self._make_testcase_element(test) 46 | test_result = ET.SubElement(self.testcase, 'skipped') 47 | test_result.set('message', 'Test Skipped: Unexpected Success') 48 | super(EXMLTestResult, self).addUnexpectedSuccess(test) 49 | 50 | def addSkip(self, test, reason): 51 | self.testcase = self._make_testcase_element(test) 52 | test_result = ET.SubElement(self.testcase, 'skipped') 53 | test_result.set('message', 'Test Skipped: %s' % reason) 54 | super(EXMLTestResult, self).addSkip(test, reason) 55 | 56 | def addExpectedFailure(self, test, err): 57 | self.testcase = self._make_testcase_element(test) 58 | test_result = ET.SubElement(self.testcase, 'skipped') 59 | self._add_tb_to_test(test, test_result, err) 60 | super(EXMLTestResult, self).addExpectedFailure(test, err) 61 | 62 | def stopTest(self, test): 63 | if self.buffer: 64 | output = sys.stdout.getvalue() if hasattr(sys.stdout, 'getvalue') else '' 65 | if output: 66 | sysout = ET.SubElement(self.testcase, 'system-out') 67 | sysout.text = smart_text(output, errors='ignore') 68 | 69 | error = sys.stderr.getvalue() if hasattr(sys.stderr, 'getvalue') else '' 70 | if error: 71 | syserr = ET.SubElement(self.testcase, 'system-err') 72 | syserr.text = smart_text(error, errors='ignore') 73 | 74 | super(EXMLTestResult, self).stopTest(test) 75 | 76 | def stopTestRun(self): 77 | run_time_taken = time.time() - self.run_start_time 78 | self.tree.set('name', 'Django Project Tests') 79 | self.tree.set('errors', str(len(self.errors))) 80 | self.tree.set('failures', str(len(self.failures))) 81 | self.tree.set('skips', str(len(self.skipped))) 82 | self.tree.set('tests', str(self.testsRun)) 83 | self.tree.set('time', "%.3f" % run_time_taken) 84 | super(EXMLTestResult, self).stopTestRun() 85 | 86 | def _make_testcase_element(self, test): 87 | time_taken = time.time() - self.case_start_time 88 | classname = ('%s.%s' % (test.__module__, test.__class__.__name__)).split('.') 89 | testcase = ET.SubElement(self.tree, 'testcase') 90 | testcase.set('time', "%.6f" % time_taken) 91 | testcase.set('classname', '.'.join(classname)) 92 | testcase.set('name', getattr(test, '_testMethodName', 93 | getattr(test, 'description', 'UNKNOWN'))) 94 | return testcase 95 | 96 | def _restoreStdout(self): 97 | '''Disables buffering once the stdout/stderr are reset.''' 98 | super(EXMLTestResult, self)._restoreStdout() 99 | self.buffer = False 100 | 101 | def _add_tb_to_test(self, test, test_result, err): 102 | '''Add a traceback to the test result element''' 103 | exc_class, exc_value, tb = err 104 | tb_str = self._exc_info_to_string(err, test) 105 | test_result.set('type', '%s.%s' % (exc_class.__module__, exc_class.__name__)) 106 | test_result.set('message', smart_text(exc_value)) 107 | test_result.text = smart_text(tb_str) 108 | 109 | def dump_xml(self, output_dir): 110 | """ 111 | Dumps test result to xml 112 | """ 113 | if not os.path.exists(output_dir): 114 | os.makedirs(output_dir) 115 | 116 | output = ET.ElementTree(self.tree) 117 | output.write(os.path.join(output_dir, 'junit.xml'), encoding="utf-8") 118 | 119 | 120 | class CITestSuiteRunner(DiscoverRunner): 121 | def __init__(self, output_dir='reports', debug=False, **kwargs): 122 | self.output_dir = output_dir 123 | self.debug = debug 124 | super(CITestSuiteRunner, self).__init__(**kwargs) 125 | 126 | def run_suite(self, suite, **kwargs): 127 | result = self.test_runner( 128 | verbosity=self.verbosity, 129 | failfast=self.failfast, 130 | resultclass=EXMLTestResult, 131 | buffer=not self.debug 132 | ).run(suite) 133 | 134 | result.dump_xml(self.output_dir) 135 | 136 | return result 137 | -------------------------------------------------------------------------------- /django_jenkins/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import itertools 3 | 4 | from django.conf import settings 5 | from django.contrib.staticfiles import finders 6 | 7 | 8 | def static_files_iterator(tested_locations, extension, ignore_patterns=None, additional_settings_list=None): 9 | if ignore_patterns is None: 10 | ignore_patterns = [] 11 | 12 | source = (os.path.join(storage.location, path) 13 | for finder in finders.get_finders() 14 | for path, storage in finder.list(ignore_patterns)) 15 | 16 | if additional_settings_list and hasattr(settings, additional_settings_list): 17 | source = itertools.chain(source, getattr(settings, additional_settings_list)) 18 | 19 | return (path for path in source 20 | if path.endswith(extension) 21 | if any(path.startswith(location) for location in tested_locations)) 22 | 23 | 24 | def set_option(conf_dict, opt_name, opt_value, conf_file, default=None, split=None): 25 | if conf_file is None: 26 | if opt_value is None: 27 | opt_value = default 28 | 29 | if opt_value: 30 | if split: 31 | opt_value = opt_value.split(split) 32 | 33 | conf_dict[opt_name] = opt_value 34 | -------------------------------------------------------------------------------- /django_jenkins/tasks/pylint.rc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | persistent=yes 3 | ignore=south_migrations 4 | cache-size=500 5 | 6 | [MESSAGES CONTROL] 7 | # C0111 Missing docstring 8 | # I0011 Warning locally suppressed using disable-msg 9 | # I0012 Warning locally suppressed using disable-msg 10 | # W0704 Except doesn't do anything Used when an except clause does nothing but "pass" and there is no "else" clause 11 | # W0142 Used * or * magic* Used when a function or method is called using *args or **kwargs to dispatch arguments. 12 | # W0212 Access to a protected member %s of a client class 13 | # W0232 Class has no __init__ method Used when a class has no __init__ method, neither its parent classes. 14 | # W0613 Unused argument %r Used when a function or method argument is not used. 15 | # W0702 No exception's type specified Used when an except clause doesn't specify exceptions type to catch. 16 | # R0201 Method could be a function 17 | # C1001 Used when a class is defined that does not inherit from anotherclass and does not inherit explicitly from "object". 18 | # C0103 Invalid module name 19 | # R0901 Used when class has too many parent classes, try to reduce this to get a simpler (and so easier to use) class. 20 | disable=C0111,I0011,I0012,W0704,W0142,W0212,W0232,W0613,W0702,R0201,C1001,C0103,R0901 21 | 22 | [REPORTS] 23 | msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} 24 | 25 | 26 | [BASIC] 27 | no-docstring-rgx=__.*__|_.* 28 | class-rgx=[A-Z_][a-zA-Z0-9_]+$ 29 | function-rgx=[a-zA_][a-zA-Z0-9_]{2,70}$ 30 | method-rgx=[a-z_][a-zA-Z0-9_]{2,70}$ 31 | const-rgx=(([A-Z_][A-Z0-9_]*)|([a-z_][a-z0-9_]*)|(__.*__)|register|urlpatterns)$ 32 | good-names=_,i,j,k,e,qs,pk,setUp,tearDown 33 | 34 | [TYPECHECK] 35 | 36 | # Tells whether missing members accessed in mixin class should be ignored. A 37 | # mixin class is detected if its name ends with "mixin" (case insensitive). 38 | ignore-mixin-members=yes 39 | 40 | # List of classes names for which member attributes should not be checked 41 | # (useful for classes with attributes dynamically set). 42 | ignored-classes=SQLObject,WSGIRequest 43 | 44 | # List of members which are set dynamically and missed by pylint inference 45 | # system, and so shouldn't trigger E0201 when accessed. 46 | generated-members=objects,DoesNotExist,id,pk,_meta,base_fields,context 47 | 48 | # List of method names used to declare (i.e. assign) instance attributes 49 | defining-attr-methods=__init__,__new__,setUp 50 | 51 | 52 | [VARIABLES] 53 | init-import=no 54 | dummy-variables-rgx=_|dummy 55 | 56 | [SIMILARITIES] 57 | min-similarity-lines=6 58 | ignore-comments=yes 59 | ignore-docstrings=yes 60 | 61 | 62 | [MISCELLANEOUS] 63 | notes=FIXME,XXX,TODO 64 | 65 | 66 | [FORMAT] 67 | max-line-length=160 68 | max-module-lines=500 69 | indent-string=' ' 70 | 71 | 72 | [DESIGN] 73 | max-args=10 74 | max-locals=15 75 | max-returns=6 76 | max-branches=12 77 | max-statements=50 78 | max-parents=7 79 | max-attributes=7 80 | min-public-methods=0 81 | max-public-methods=50 82 | -------------------------------------------------------------------------------- /django_jenkins/tasks/run_flake8.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pep8 3 | import sys 4 | 5 | try: 6 | from StringIO import StringIO 7 | except ImportError: 8 | from io import StringIO 9 | 10 | 11 | from flake8.api.legacy import get_style_guide # Quck hack again, 3d time flake8 would be removed, if no volounters found 12 | from django.conf import settings 13 | 14 | from . import set_option 15 | 16 | 17 | class Reporter(object): 18 | """ 19 | Runs flake8 on python files. 20 | """ 21 | def add_arguments(self, parser): 22 | parser.add_argument('--max-complexity', 23 | dest='flake8-max-complexity', 24 | type=int, 25 | help='McCabe complexity treshold') 26 | parser.add_argument("--pep8-exclude", 27 | dest="pep8-exclude", 28 | help="exclude files or directories which match these " 29 | "comma separated patterns (default: %s)" % 30 | (pep8.DEFAULT_EXCLUDE + ",south_migrations")) 31 | parser.add_argument("--pep8-select", dest="pep8-select", 32 | help="select errors and warnings (e.g. E,W6)") 33 | parser.add_argument("--pep8-ignore", dest="pep8-ignore", 34 | help="skip errors and warnings (e.g. E4,W)") 35 | parser.add_argument("--pep8-max-line-length", 36 | dest="pep8-max-line-length", type=int, 37 | help="set maximum allowed line length (default: %d)" % 38 | pep8.MAX_LINE_LENGTH) 39 | parser.add_argument("--pep8-rcfile", dest="pep8-rcfile", 40 | help="PEP8 configuration file") 41 | 42 | def run(self, apps_locations, **options): 43 | output = open(os.path.join(options['output_dir'], 'flake8.report'), 'w') 44 | 45 | pep8_options = {} 46 | 47 | config_file = self.get_config_path(options) 48 | if config_file is not None: 49 | pep8_options['config_file'] = config_file 50 | 51 | set_option(pep8_options, 'exclude', options['pep8-exclude'], config_file, 52 | default=pep8.DEFAULT_EXCLUDE + ",south_migrations", split=',') 53 | 54 | set_option(pep8_options, 'select', options['pep8-select'], config_file, split=',') 55 | 56 | set_option(pep8_options, 'ignore', options['pep8-ignore'], config_file, split=',') 57 | 58 | set_option(pep8_options, 'max_line_length', options['pep8-max-line-length'], config_file, 59 | default=pep8.MAX_LINE_LENGTH) 60 | 61 | set_option(pep8_options, 'max_complexity', options['flake8-max-complexity'], config_file, 62 | default=-1) 63 | 64 | old_stdout, flake8_output = sys.stdout, StringIO() 65 | sys.stdout = flake8_output 66 | 67 | pep8style = get_style_guide( 68 | parse_argv=False, 69 | jobs='1', 70 | **pep8_options) 71 | 72 | try: 73 | for location in apps_locations: 74 | pep8style.input_file(os.path.relpath(location)) 75 | finally: 76 | sys.stdout = old_stdout 77 | 78 | flake8_output.seek(0) 79 | output.write(flake8_output.read()) 80 | output.close() 81 | 82 | def get_config_path(self, options): 83 | if options['pep8-rcfile']: 84 | return options['pep8-rcfile'] 85 | 86 | rcfile = getattr(settings, 'PEP8_RCFILE', None) 87 | if rcfile: 88 | return rcfile 89 | 90 | if os.path.exists('tox.ini'): 91 | return 'tox.ini' 92 | 93 | if os.path.exists('setup.cfg'): 94 | return 'setup.cfg' 95 | -------------------------------------------------------------------------------- /django_jenkins/tasks/run_pep8.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import pep8 3 | 4 | from django.conf import settings 5 | 6 | from . import set_option 7 | 8 | 9 | class Reporter(object): 10 | def add_arguments(self, parser): 11 | parser.add_argument("--pep8-exclude", 12 | dest="pep8-exclude", 13 | help="exclude files or directories which match these " 14 | "comma separated patterns (default: %s)" % 15 | (pep8.DEFAULT_EXCLUDE + ",south_migrations")) 16 | parser.add_argument("--pep8-select", dest="pep8-select", 17 | help="select errors and warnings (e.g. E,W6)") 18 | parser.add_argument("--pep8-ignore", dest="pep8-ignore", 19 | help="skip errors and warnings (e.g. E4,W)"), 20 | parser.add_argument("--pep8-max-line-length", 21 | dest="pep8-max-line-length", type=int, 22 | help="set maximum allowed line length (default: %d)" % pep8.MAX_LINE_LENGTH) 23 | parser.add_argument("--pep8-rcfile", dest="pep8-rcfile", 24 | help="PEP8 configuration file") 25 | 26 | def run(self, apps_locations, **options): 27 | output = open(os.path.join(options['output_dir'], 'pep8.report'), 'w') 28 | 29 | class JenkinsReport(pep8.BaseReport): 30 | def error(instance, line_number, offset, text, check): 31 | code = super(JenkinsReport, instance).error(line_number, offset, text, check) 32 | if code: 33 | sourceline = instance.line_offset + line_number 34 | output.write('%s:%s:%s: %s\n' % (instance.filename, sourceline, offset + 1, text)) 35 | 36 | pep8_options = {} 37 | config_file = self.get_config_path(options) 38 | if config_file is not None: 39 | pep8_options['config_file'] = config_file 40 | 41 | set_option(pep8_options, 'exclude', options['pep8-exclude'], config_file, 42 | default=pep8.DEFAULT_EXCLUDE + ",south_migrations", split=',') 43 | 44 | set_option(pep8_options, 'select', options['pep8-select'], config_file, split=',') 45 | 46 | set_option(pep8_options, 'ignore', options['pep8-ignore'], config_file, split=',') 47 | 48 | set_option(pep8_options, 'max_line_length', options['pep8-max-line-length'], config_file, 49 | default=pep8.MAX_LINE_LENGTH) 50 | 51 | pep8style = pep8.StyleGuide( 52 | parse_argv=False, 53 | reporter=JenkinsReport, 54 | **pep8_options) 55 | 56 | pep8style.options.report.start() 57 | for location in apps_locations: 58 | pep8style.input_dir(os.path.relpath(location)) 59 | pep8style.options.report.stop() 60 | 61 | output.close() 62 | 63 | def get_config_path(self, options): 64 | if options['pep8-rcfile']: 65 | return options['pep8-rcfile'] 66 | 67 | rcfile = getattr(settings, 'PEP8_RCFILE', None) 68 | if rcfile: 69 | return rcfile 70 | 71 | if os.path.exists('tox.ini'): 72 | return 'tox.ini' 73 | 74 | if os.path.exists('setup.cfg'): 75 | return 'setup.cfg' 76 | -------------------------------------------------------------------------------- /django_jenkins/tasks/run_pyflakes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import re 4 | import sys 5 | from pyflakes.scripts import pyflakes 6 | 7 | try: 8 | from StringIO import StringIO 9 | except ImportError: 10 | from io import StringIO 11 | 12 | 13 | class Reporter(object): 14 | def add_arguments(self, parser): 15 | parser.add_argument("--pyflakes-exclude-dir", 16 | action="append", 17 | default=['south_migrations'], 18 | dest="pyflakes_exclude_dirs", 19 | help="Path name to exclude") 20 | 21 | def run(self, apps_locations, **options): 22 | output = open(os.path.join(options['output_dir'], 'pyflakes.report'), 'w') 23 | 24 | # run pyflakes tool with captured output 25 | old_stdout, pyflakes_output = sys.stdout, StringIO() 26 | sys.stdout = pyflakes_output 27 | try: 28 | for location in apps_locations: 29 | if os.path.isdir(location): 30 | for dirpath, dirnames, filenames in os.walk(os.path.relpath(location)): 31 | if dirpath.endswith(tuple( 32 | ''.join([os.sep, exclude_dir]) for exclude_dir in options['pyflakes_exclude_dirs'])): 33 | continue 34 | 35 | for filename in filenames: 36 | if filename.endswith('.py'): 37 | pyflakes.checkPath(os.path.join(dirpath, filename)) 38 | else: 39 | pyflakes.checkPath(os.path.relpath(location)) 40 | finally: 41 | sys.stdout = old_stdout 42 | 43 | # save report 44 | pyflakes_output.seek(0) 45 | 46 | while True: 47 | line = pyflakes_output.readline() 48 | if not line: 49 | break 50 | message = re.sub(r': ', r': [E] PYFLAKES:', line) 51 | output.write(message) 52 | 53 | output.close() 54 | -------------------------------------------------------------------------------- /django_jenkins/tasks/run_pylint.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf import settings 4 | 5 | from pylint import lint 6 | from pylint.reporters.text import TextReporter 7 | 8 | 9 | class ParseableTextReporter(TextReporter): 10 | """ 11 | Outputs messages in a form recognized by jenkins 12 | 13 | :: 14 | """ 15 | name = 'parseable' 16 | line_format = '{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}' 17 | 18 | 19 | class Reporter(object): 20 | def add_arguments(self, parser): 21 | parser.add_argument("--pylint-rcfile", 22 | dest="pylint_rcfile", 23 | help="pylint configuration file") 24 | parser.add_argument("--pylint-errors-only", 25 | dest="pylint_errors_only", 26 | action="store_true", default=False, 27 | help="pylint output errors only mode") 28 | parser.add_argument("--pylint-load-plugins", 29 | dest="pylint_load_plugins", 30 | help="list of pylint plugins to load") 31 | 32 | def run(self, apps_locations, **options): 33 | output = open(os.path.join(options['output_dir'], 'pylint.report'), 'w') 34 | 35 | args = [] 36 | args.append("--rcfile=%s" % self.get_config_path(options)) 37 | if self.get_plugins(options): 38 | args.append('--load-plugins=%s' % self.get_plugins(options)) 39 | 40 | if options['pylint_errors_only']: 41 | args += ['--errors-only'] 42 | args += apps_locations 43 | 44 | lint.Run(args, reporter=ParseableTextReporter(output=output), exit=False) 45 | 46 | output.close() 47 | 48 | def get_plugins(self, options): 49 | if options.get('pylint_load_plugins', None): 50 | return options['pylint_load_plugins'] 51 | 52 | plugins = getattr(settings, 'PYLINT_LOAD_PLUGIN', None) 53 | if plugins: 54 | return ','.join(plugins) 55 | 56 | return None 57 | 58 | def get_config_path(self, options): 59 | if options['pylint_rcfile']: 60 | return options['pylint_rcfile'] 61 | 62 | rcfile = getattr(settings, 'PYLINT_RCFILE', 'pylint.rc') 63 | if os.path.exists(rcfile): 64 | return rcfile 65 | 66 | # use built-in 67 | root_dir = os.path.normpath(os.path.dirname(__file__)) 68 | return os.path.join(root_dir, 'pylint.rc') 69 | -------------------------------------------------------------------------------- /django_jenkins/tasks/with_coverage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from importlib import import_module 4 | 5 | from django.conf import settings 6 | 7 | 8 | class CoverageReporter(object): 9 | def __init__(self): 10 | try: 11 | import coverage 12 | except ImportError: 13 | raise ImportError('coverage is not installed') 14 | 15 | if coverage.__version__ < '4': 16 | raise ImportError('coverage>=4 required') 17 | 18 | coverage_config_file = None 19 | for argv in sys.argv: 20 | if argv.startswith('--coverage-rcfile='): 21 | _, coverage_config_file = argv.split('=') 22 | 23 | self.coverage = coverage.coverage( 24 | branch=True, 25 | config_file=coverage_config_file or self.default_coverage_config()) 26 | self.coverage.start() 27 | 28 | def save(self, apps_locations, options): 29 | self.coverage.stop() 30 | self.coverage.get_data() 31 | morfs = self.get_morfs(self.coverage, apps_locations, options) 32 | 33 | if 'xml' in options['coverage_format']: 34 | self.coverage.xml_report(morfs=morfs, outfile=os.path.join(options['output_dir'], 'coverage.xml')) 35 | if 'bin' in options['coverage_format']: 36 | self.coverage.save() 37 | if 'html' in options['coverage_format']: 38 | # Dump coverage html 39 | self.coverage.html_report(morfs=morfs, directory=os.path.join(options['output_dir'], 'coverage')) 40 | 41 | def get_morfs(self, coverage, tested_locations, options): 42 | excluded = [] 43 | 44 | # Exclude by module 45 | modnames = options.get('coverage_excludes') or getattr(settings, 'COVERAGE_EXCLUDES', []) 46 | for modname in modnames: 47 | try: 48 | excluded.append(os.path.dirname(import_module(modname).__file__)) 49 | except ImportError: 50 | pass 51 | 52 | # Exclude by directory 53 | excluded.extend(getattr(settings, 'COVERAGE_EXCLUDES_FOLDERS', [])) 54 | 55 | return [filename for filename in coverage.data.measured_files() 56 | if not (os.sep + 'migrations' + os.sep) in filename 57 | if not (os.sep + 'south_migrations' + os.sep) in filename 58 | if any(filename.startswith(location) for location in tested_locations) 59 | if not any(filename.startswith(location) for location in excluded)] 60 | 61 | def default_coverage_config(self): 62 | rcfile = getattr(settings, 'COVERAGE_RCFILE', 'coverage.rc') 63 | if os.path.exists(rcfile): 64 | return rcfile 65 | return None 66 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import codecs 5 | from os import path 6 | from setuptools import setup 7 | 8 | 9 | read = lambda filepath: codecs.open(filepath, 'r', 'utf-8').read() 10 | 11 | 12 | setup( 13 | name='django-jenkins', 14 | version='1.11.0', 15 | author='Mikhail Podgurskiy', 16 | author_email='kmmbvnr@gmail.com', 17 | description='Plug and play continuous integration with django and jenkins', 18 | long_description=read(path.abspath(path.join(path.dirname(__file__), 'README.rst'))), 19 | license='LGPL', 20 | platforms=['Any'], 21 | keywords=['pyunit', 'unittest', 'testrunner', 'hudson', 'jenkins', 22 | 'django', 'pylint', 'pep8', 'pyflakes', 'csslint', 'scsslint', 23 | 'jshint', 'coverage'], 24 | url='http://github.com/kmmbvnr/django-jenkins', 25 | classifiers=[ 26 | 'Development Status :: 5 - Production/Stable', 27 | 'Intended Audience :: Developers', 28 | 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)', 29 | 'Natural Language :: English', 30 | 'Operating System :: OS Independent', 31 | "Framework :: Django", 32 | "Framework :: Django :: 1.8", 33 | "Framework :: Django :: 1.9", 34 | 'Programming Language :: Python', 35 | 'Programming Language :: Python :: 2.7', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: 3.4', 38 | 'Programming Language :: Python :: 3.5', 39 | 'Topic :: Software Development :: Libraries :: Python Modules', 40 | 'Topic :: Software Development :: Testing' 41 | ], 42 | install_requires=[ 43 | 'Django>=1.8', 44 | ], 45 | packages=['django_jenkins', 'django_jenkins.management', 46 | 'django_jenkins.tasks', 'django_jenkins.management.commands'], 47 | package_data={'django_jenkins': ['tasks/pylint.rc']}, 48 | zip_safe=False, 49 | include_package_data=True 50 | ) 51 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmmbvnr/django-jenkins/f673c8ecded24eb56aeaef8d1c0caeb82208b595/tests/__init__.py -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os, sys 3 | from django.core.management import execute_from_command_line 4 | 5 | PROJECT_ROOT = os.path.dirname(os.path.abspath(os.path.dirname(__file__))) 6 | sys.path.insert(0, PROJECT_ROOT) 7 | 8 | if __name__ == "__main__": 9 | if len(sys.argv) == 1: 10 | sys.argv += ['test'] 11 | 12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") 13 | execute_from_command_line(sys.argv) 14 | -------------------------------------------------------------------------------- /tests/runner.py: -------------------------------------------------------------------------------- 1 | from django_jenkins.runner import CITestSuiteRunner 2 | 3 | 4 | class CustomTestRunner(CITestSuiteRunner): 5 | @classmethod 6 | def add_arguments(self, parser): 7 | parser.add_argument('--ok', default=False, action='store_true', help='Custom test runner option') 8 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) 4 | 5 | DEBUG = True 6 | TEMPLATE_DEBUG = DEBUG 7 | ROOT_URLCONF = 'test_app.urls' 8 | SECRET_KEY = 'nokey' 9 | MIDDLEWARE_CLASSES = () 10 | 11 | TEMPLATE_LOADERS = ( 12 | 'django.template.loaders.filesystem.Loader', 13 | 'django.template.loaders.app_directories.Loader', 14 | ) 15 | 16 | TEMPLATES = [ 17 | { 18 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 19 | 'DIRS': [ 20 | ], 21 | 'APP_DIRS': True, 22 | 'OPTIONS': { 23 | 'context_processors': [ 24 | 'django.contrib.auth.context_processors.auth', 25 | 'django.template.context_processors.debug', 26 | 'django.template.context_processors.i18n', 27 | 'django.template.context_processors.media', 28 | 'django.template.context_processors.static', 29 | 'django.template.context_processors.tz', 30 | 'django.contrib.messages.context_processors.messages', 31 | ], 32 | }, 33 | }, 34 | ] 35 | 36 | PROJECT_APPS = ( 37 | 'django.contrib.sessions', # just to ensure that dotted apps test works 38 | 'django_jenkins', 39 | 'tests.test_app', 40 | 'tests.test_app_dirs', 41 | ) 42 | 43 | INSTALLED_APPS = ( 44 | 'django.contrib.auth', 45 | 'django.contrib.contenttypes', 46 | ) + PROJECT_APPS 47 | 48 | 49 | DATABASE_ENGINE = 'sqlite3' 50 | DATABASES = { 51 | 'default': { 52 | 'ENGINE': 'django.db.backends.%s' % DATABASE_ENGINE, 53 | } 54 | } 55 | 56 | SOUTH_MIGRATION_MODULES = { 57 | 'test_app': 'test_app.south_migrations', 58 | } 59 | 60 | JENKINS_TASKS = ( 61 | 'django_jenkins.tasks.run_pylint', 62 | 'django_jenkins.tasks.run_pep8', 63 | 'django_jenkins.tasks.run_pyflakes', 64 | 'django_jenkins.tasks.run_flake8', 65 | ) 66 | 67 | COVERAGE_EXCLUDES = ['tests.test_app.not_for_coverage', ] 68 | COVERAGE_EXCLUDES_FOLDERS = [os.path.join(PROJECT_ROOT, 'test_app_dirs/not_for_coverage/'), ] 69 | 70 | # JSHINT_CHECKED_FILES = [os.path.join(PROJECT_ROOT, 'static/js/test.js')] 71 | # CSSLINT_CHECKED_FILES = [os.path.join(PROJECT_ROOT, 'static/css/test.css')] 72 | 73 | PYLINT_LOAD_PLUGIN = ( 74 | 'pylint_django', 75 | ) 76 | 77 | STATICFILES_DIRS = [ 78 | os.path.join(PROJECT_ROOT, 'static/'), 79 | ] 80 | 81 | STATIC_URL = '/media/' 82 | 83 | 84 | LOGGING = { 85 | 'version': 1, 86 | 'disable_existing_loggers': False, 87 | 'handlers': { 88 | 'console': { 89 | 'level': 'DEBUG', 90 | 'class': 'logging.StreamHandler', 91 | }, 92 | }, 93 | 'loggers': { 94 | 'django.request': { 95 | 'handlers': ['console'], 96 | 'level': 'ERROR', 97 | 'propagate': True, 98 | }, 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/static/css/test.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | /* empty ruleset, for testing csslint output*/ 3 | } -------------------------------------------------------------------------------- /tests/static/css/test.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | 3 | // empty rule and 4 spaces, for testing scsslint output 4 | .test { 5 | 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /tests/static/js/test.js: -------------------------------------------------------------------------------- 1 | function toggle() { 2 | "use strict"; 3 | var unused, x = true; 4 | if (x && y) { 5 | x = false; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/test_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmmbvnr/django-jenkins/f673c8ecded24eb56aeaef8d1c0caeb82208b595/tests/test_app/__init__.py -------------------------------------------------------------------------------- /tests/test_app/features/example.feature: -------------------------------------------------------------------------------- 1 | Feature: Addition 2 | 3 | Scenario: Simple addition between two numbers 4 | Given the number "1" 5 | When the number "1" is added to it 6 | Then the result should be "2" 7 | 8 | Scenario: Tricky addition between three numbers 9 | Given the number "1" 10 | And the number "7" 11 | When the number "6" is added to them 12 | Then the result should be "14" -------------------------------------------------------------------------------- /tests/test_app/features/example_steps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from lettuce import step, before 3 | 4 | @before.each_scenario 5 | def setup_scenario(scenario): 6 | scenario.numbers = [] 7 | 8 | @step('(?:Given|And|When) the number "(.*)"(?: is added to (?:it|them))?') 9 | def given_the_number(step, number): 10 | step.scenario.numbers.append(int(number)) 11 | 12 | @step('Then the result should be "(.*)"') 13 | def then_the_result_should_equal(step, result): 14 | actual = sum(step.scenario.numbers) 15 | assert int(result) == actual, "%s != %s" % (result, actual) 16 | -------------------------------------------------------------------------------- /tests/test_app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='TestModel', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 17 | ('test_text', models.CharField(max_length=250)), 18 | ], 19 | options={ 20 | }, 21 | bases=(models.Model,), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /tests/test_app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmmbvnr/django-jenkins/f673c8ecded24eb56aeaef8d1c0caeb82208b595/tests/test_app/migrations/__init__.py -------------------------------------------------------------------------------- /tests/test_app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from test_app.not_for_coverage import one, two # NOQA 3 | 4 | 5 | class TestModel(models.Model): 6 | test_text = models.CharField(max_length=250) 7 | -------------------------------------------------------------------------------- /tests/test_app/not_for_coverage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmmbvnr/django-jenkins/f673c8ecded24eb56aeaef8d1c0caeb82208b595/tests/test_app/not_for_coverage/__init__.py -------------------------------------------------------------------------------- /tests/test_app/not_for_coverage/one.py: -------------------------------------------------------------------------------- 1 | def test(): 2 | print('Not for coverage') -------------------------------------------------------------------------------- /tests/test_app/not_for_coverage/two.py: -------------------------------------------------------------------------------- 1 | def test(): 2 | print('Not for coverage') -------------------------------------------------------------------------------- /tests/test_app/south_migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from south.v2 import SchemaMigration 2 | 3 | class Migration(SchemaMigration): 4 | def forwards(self, orm): 5 | a = 1 # pyflakes/pylint violation 6 | pass 7 | 8 | def backwards(self, orm): 9 | pass 10 | -------------------------------------------------------------------------------- /tests/test_app/south_migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmmbvnr/django-jenkins/f673c8ecded24eb56aeaef8d1c0caeb82208b595/tests/test_app/south_migrations/__init__.py -------------------------------------------------------------------------------- /tests/test_app/static/css/test_errors.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | /* unclosed tag produce error forcsslint 3 | -------------------------------------------------------------------------------- /tests/test_app/static/js/test.js: -------------------------------------------------------------------------------- 1 | /*jshint unused:true */ 2 | 3 | function toggle() { 4 | var unused, x = true; 5 | if (x) { 6 | x = false; 7 | } 8 | } -------------------------------------------------------------------------------- /tests/test_app/templates/404.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmmbvnr/django-jenkins/f673c8ecded24eb56aeaef8d1c0caeb82208b595/tests/test_app/templates/404.html -------------------------------------------------------------------------------- /tests/test_app/templates/500.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmmbvnr/django-jenkins/f673c8ecded24eb56aeaef8d1c0caeb82208b595/tests/test_app/templates/500.html -------------------------------------------------------------------------------- /tests/test_app/templates/test_app/wm_test_click.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Windmill testing 5 | 6 | 7 | 8 |
9 |

Windmill testing

10 | 11 |
12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/test_app/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import io 3 | import sys 4 | from xml.etree import ElementTree as ET 5 | 6 | from django.core import mail 7 | from django.test import TestCase 8 | from unittest import skip 9 | 10 | from django.test import LiveServerTestCase 11 | from pyvirtualdisplay import Display 12 | from selenium import webdriver 13 | 14 | from django_jenkins.runner import EXMLTestResult 15 | 16 | 17 | class SaintyChecks(TestCase): 18 | # @classmethod 19 | # def setUpClass(cls): 20 | # raise Exception("Ups, should be disabled") 21 | 22 | def test_mailbox_stubs_not_broken(self): 23 | print("Testing mailbox django stubs") 24 | mail.send_mail('Test subject', 'Test message', 'nobody@kenkins.com', 25 | ['somewhere@nowhere.com']) 26 | self.assertTrue(1, len(mail.outbox)) 27 | 28 | @skip("Check skiped test") 29 | def test_is_skipped(self): 30 | print("This test should be skipped") 31 | 32 | def test_junit_xml_with_utf8_stdout_and_stderr(self): 33 | sys.stdout.write('\xc4\x85') 34 | sys.stderr.write('\xc4\x85') 35 | 36 | def test_junit_xml_with_invalid_stdout_and_stderr_encoding(self): 37 | sys.stdout.write('\xc4') 38 | sys.stderr.write('\xc4') 39 | 40 | # def test_failure(self): 41 | # raise Exception("Ups, should be disabled") 42 | 43 | 44 | class EXMLTestResultTests(TestCase): 45 | def setUp(self): 46 | self.exml_result = EXMLTestResult(None, None, 1) 47 | self.exml_result.startTestRun() 48 | self.result_element = ET.SubElement(self.exml_result.tree, 'result') 49 | 50 | def test_non_ascii_traceback(self): 51 | try: 52 | self.explode_with_unicode_traceback() 53 | except ValueError: 54 | err = sys.exc_info() 55 | else: 56 | self.fail() 57 | 58 | self.exml_result._add_tb_to_test(TestCase('fail'), self.result_element, err) 59 | 60 | output = self.write_element(self.result_element) 61 | 62 | self.assert_(output) 63 | 64 | def test_non_ascii_message(self): 65 | try: 66 | self.explode_with_unicode_message() 67 | except ValueError: 68 | err = sys.exc_info() 69 | else: 70 | self.fail() 71 | 72 | self.exml_result._add_tb_to_test(TestCase('fail'), self.result_element, err) 73 | 74 | output = self.write_element(self.result_element) 75 | 76 | self.assert_(output) 77 | 78 | def write_element(self, element): 79 | # write out the element the way that our TestResult.dump_xml does. 80 | # (except not actually to disk.) 81 | tree = ET.ElementTree(element) 82 | output = io.BytesIO() 83 | # this bit blows up if components of the output are byte-strings with non-ascii content. 84 | tree.write(output, encoding='utf-8') 85 | output_bytes = output.getvalue() 86 | return output_bytes 87 | 88 | def explode_with_unicode_traceback(self): 89 | # The following will result in an ascii error message, but the traceback will contain the 90 | # full source line, including the comment's non-ascii characters. 91 | raise ValueError("dead") # "⚠ Not enough ☕" 92 | 93 | def explode_with_unicode_message(self): 94 | # This source code has only ascii, the exception has a non-ascii message. 95 | raise ValueError(u"\N{BIOHAZARD SIGN} Too much \N{HOT BEVERAGE}") 96 | 97 | 98 | class SeleniumTests(LiveServerTestCase): 99 | @classmethod 100 | def setUpClass(cls): 101 | cls.display = Display(visible=0, size=(1024, 768)) 102 | cls.display.start() 103 | cls.selenium = webdriver.Firefox() 104 | super(SeleniumTests, cls).setUpClass() 105 | 106 | @classmethod 107 | def tearDownClass(cls): 108 | super(SeleniumTests, cls).tearDownClass() 109 | cls.selenium.quit() 110 | cls.display.stop() 111 | 112 | def test_login(self): 113 | self.selenium.get('%s%s' % (self.live_server_url, '/test_click/')) 114 | self.selenium.find_element_by_id("wm_click").click() 115 | self.assertEqual('Button clicked', self.selenium.find_element_by_id("wm_target").text) 116 | -------------------------------------------------------------------------------- /tests/test_app/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.conf.urls import url 3 | from django.views.generic.base import TemplateView 4 | 5 | urlpatterns = [ 6 | url(r'^test_click/$', TemplateView.as_view(template_name='test_app/wm_test_click.html'), 7 | name='wm_test_click') 8 | ] 9 | -------------------------------------------------------------------------------- /tests/test_app/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /tests/test_app_dirs/__init__.py: -------------------------------------------------------------------------------- 1 | from test_app_dirs.not_for_coverage import one, two -------------------------------------------------------------------------------- /tests/test_app_dirs/models/__init__.py: -------------------------------------------------------------------------------- 1 | from test_app_dirs.models.sample import TestDirModel # NOQA -------------------------------------------------------------------------------- /tests/test_app_dirs/models/sample.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class TestDirModel(models.Model): 5 | test_text = models.CharField(max_length=250) 6 | 7 | class Meta: 8 | app_label = 'test_app_dirs' -------------------------------------------------------------------------------- /tests/test_app_dirs/not_for_coverage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmmbvnr/django-jenkins/f673c8ecded24eb56aeaef8d1c0caeb82208b595/tests/test_app_dirs/not_for_coverage/__init__.py -------------------------------------------------------------------------------- /tests/test_app_dirs/not_for_coverage/one.py: -------------------------------------------------------------------------------- 1 | def test(): 2 | print('Not for coverage') -------------------------------------------------------------------------------- /tests/test_app_dirs/not_for_coverage/two.py: -------------------------------------------------------------------------------- 1 | def test(): 2 | print('Not for coverage') -------------------------------------------------------------------------------- /tests/test_app_dirs/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmmbvnr/django-jenkins/f673c8ecded24eb56aeaef8d1c0caeb82208b595/tests/test_app_dirs/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_app_dirs/tests/test_discovery_dir_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.test import TestCase 3 | 4 | 5 | class DirDiscoveryTest(TestCase): 6 | def test_should_be_dicoverd(self): 7 | """ 8 | Yep! 9 | """ 10 | -------------------------------------------------------------------------------- /tests/test_non_bound.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | 4 | class NonBoundTest(TestCase): 5 | def test_executed(self): 6 | """ 7 | This test executed only if no --project-apps-tests option provided 8 | """ 9 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27,34,35}-{dj18,dj19,dj110} 3 | skipsdist = True 4 | 5 | [testenv] 6 | basepython = 7 | py27: python2.7 8 | py34: python3.4 9 | py35: python3.5 10 | commands = python tests/manage.py {posargs:jenkins --enable-coverage --pep8-max-line-length=150 --liveserver=localhost:8090-8100 --output-dir=reports/{envname} --testrunner=tests.runner.CustomTestRunner --ok} 11 | deps = 12 | dj18: django==1.8.17 13 | dj19: django==1.9.12 14 | dj110: django==1.10.5 15 | pylint==1.6.5 16 | pylint-django==0.7.2 17 | coverage==4.3.4 18 | pyflakes==1.5.0 19 | pep8==1.7.0 20 | flake8==3.3.0 21 | selenium==3.0.2 22 | pyvirtualdisplay==0.2.1 23 | ipdb 24 | passenv=HOME 25 | --------------------------------------------------------------------------------