Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Initial commit

  • Loading branch information...
commit e656c4e5ad712f658b554b77e3d33329b5f74213 0 parents
David Cramer authored February 03, 2012
4  .gitignore
... ...
@@ -0,0 +1,4 @@
  1
+*.pyc
  2
+/dist
  3
+/build
  4
+*.egg*
202  LICENSE
... ...
@@ -0,0 +1,202 @@
  1
+
  2
+                              Apache License
  3
+                        Version 2.0, January 2004
  4
+                     http://www.apache.org/licenses/
  5
+
  6
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
  7
+
  8
+1. Definitions.
  9
+
  10
+   "License" shall mean the terms and conditions for use, reproduction,
  11
+   and distribution as defined by Sections 1 through 9 of this document.
  12
+
  13
+   "Licensor" shall mean the copyright owner or entity authorized by
  14
+   the copyright owner that is granting the License.
  15
+
  16
+   "Legal Entity" shall mean the union of the acting entity and all
  17
+   other entities that control, are controlled by, or are under common
  18
+   control with that entity. For the purposes of this definition,
  19
+   "control" means (i) the power, direct or indirect, to cause the
  20
+   direction or management of such entity, whether by contract or
  21
+   otherwise, or (ii) ownership of fifty percent (50%) or more of the
  22
+   outstanding shares, or (iii) beneficial ownership of such entity.
  23
+
  24
+   "You" (or "Your") shall mean an individual or Legal Entity
  25
+   exercising permissions granted by this License.
  26
+
  27
+   "Source" form shall mean the preferred form for making modifications,
  28
+   including but not limited to software source code, documentation
  29
+   source, and configuration files.
  30
+
  31
+   "Object" form shall mean any form resulting from mechanical
  32
+   transformation or translation of a Source form, including but
  33
+   not limited to compiled object code, generated documentation,
  34
+   and conversions to other media types.
  35
+
  36
+   "Work" shall mean the work of authorship, whether in Source or
  37
+   Object form, made available under the License, as indicated by a
  38
+   copyright notice that is included in or attached to the work
  39
+   (an example is provided in the Appendix below).
  40
+
  41
+   "Derivative Works" shall mean any work, whether in Source or Object
  42
+   form, that is based on (or derived from) the Work and for which the
  43
+   editorial revisions, annotations, elaborations, or other modifications
  44
+   represent, as a whole, an original work of authorship. For the purposes
  45
+   of this License, Derivative Works shall not include works that remain
  46
+   separable from, or merely link (or bind by name) to the interfaces of,
  47
+   the Work and Derivative Works thereof.
  48
+
  49
+   "Contribution" shall mean any work of authorship, including
  50
+   the original version of the Work and any modifications or additions
  51
+   to that Work or Derivative Works thereof, that is intentionally
  52
+   submitted to Licensor for inclusion in the Work by the copyright owner
  53
+   or by an individual or Legal Entity authorized to submit on behalf of
  54
+   the copyright owner. For the purposes of this definition, "submitted"
  55
+   means any form of electronic, verbal, or written communication sent
  56
+   to the Licensor or its representatives, including but not limited to
  57
+   communication on electronic mailing lists, source code control systems,
  58
+   and issue tracking systems that are managed by, or on behalf of, the
  59
+   Licensor for the purpose of discussing and improving the Work, but
  60
+   excluding communication that is conspicuously marked or otherwise
  61
+   designated in writing by the copyright owner as "Not a Contribution."
  62
+
  63
+   "Contributor" shall mean Licensor and any individual or Legal Entity
  64
+   on behalf of whom a Contribution has been received by Licensor and
  65
+   subsequently incorporated within the Work.
  66
+
  67
+2. Grant of Copyright License. Subject to the terms and conditions of
  68
+   this License, each Contributor hereby grants to You a perpetual,
  69
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
  70
+   copyright license to reproduce, prepare Derivative Works of,
  71
+   publicly display, publicly perform, sublicense, and distribute the
  72
+   Work and such Derivative Works in Source or Object form.
  73
+
  74
+3. Grant of Patent License. Subject to the terms and conditions of
  75
+   this License, each Contributor hereby grants to You a perpetual,
  76
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
  77
+   (except as stated in this section) patent license to make, have made,
  78
+   use, offer to sell, sell, import, and otherwise transfer the Work,
  79
+   where such license applies only to those patent claims licensable
  80
+   by such Contributor that are necessarily infringed by their
  81
+   Contribution(s) alone or by combination of their Contribution(s)
  82
+   with the Work to which such Contribution(s) was submitted. If You
  83
+   institute patent litigation against any entity (including a
  84
+   cross-claim or counterclaim in a lawsuit) alleging that the Work
  85
+   or a Contribution incorporated within the Work constitutes direct
  86
+   or contributory patent infringement, then any patent licenses
  87
+   granted to You under this License for that Work shall terminate
  88
+   as of the date such litigation is filed.
  89
+
  90
+4. Redistribution. You may reproduce and distribute copies of the
  91
+   Work or Derivative Works thereof in any medium, with or without
  92
+   modifications, and in Source or Object form, provided that You
  93
+   meet the following conditions:
  94
+
  95
+   (a) You must give any other recipients of the Work or
  96
+       Derivative Works a copy of this License; and
  97
+
  98
+   (b) You must cause any modified files to carry prominent notices
  99
+       stating that You changed the files; and
  100
+
  101
+   (c) You must retain, in the Source form of any Derivative Works
  102
+       that You distribute, all copyright, patent, trademark, and
  103
+       attribution notices from the Source form of the Work,
  104
+       excluding those notices that do not pertain to any part of
  105
+       the Derivative Works; and
  106
+
  107
+   (d) If the Work includes a "NOTICE" text file as part of its
  108
+       distribution, then any Derivative Works that You distribute must
  109
+       include a readable copy of the attribution notices contained
  110
+       within such NOTICE file, excluding those notices that do not
  111
+       pertain to any part of the Derivative Works, in at least one
  112
+       of the following places: within a NOTICE text file distributed
  113
+       as part of the Derivative Works; within the Source form or
  114
+       documentation, if provided along with the Derivative Works; or,
  115
+       within a display generated by the Derivative Works, if and
  116
+       wherever such third-party notices normally appear. The contents
  117
+       of the NOTICE file are for informational purposes only and
  118
+       do not modify the License. You may add Your own attribution
  119
+       notices within Derivative Works that You distribute, alongside
  120
+       or as an addendum to the NOTICE text from the Work, provided
  121
+       that such additional attribution notices cannot be construed
  122
+       as modifying the License.
  123
+
  124
+   You may add Your own copyright statement to Your modifications and
  125
+   may provide additional or different license terms and conditions
  126
+   for use, reproduction, or distribution of Your modifications, or
  127
+   for any such Derivative Works as a whole, provided Your use,
  128
+   reproduction, and distribution of the Work otherwise complies with
  129
+   the conditions stated in this License.
  130
+
  131
+5. Submission of Contributions. Unless You explicitly state otherwise,
  132
+   any Contribution intentionally submitted for inclusion in the Work
  133
+   by You to the Licensor shall be under the terms and conditions of
  134
+   this License, without any additional terms or conditions.
  135
+   Notwithstanding the above, nothing herein shall supersede or modify
  136
+   the terms of any separate license agreement you may have executed
  137
+   with Licensor regarding such Contributions.
  138
+
  139
+6. Trademarks. This License does not grant permission to use the trade
  140
+   names, trademarks, service marks, or product names of the Licensor,
  141
+   except as required for reasonable and customary use in describing the
  142
+   origin of the Work and reproducing the content of the NOTICE file.
  143
+
  144
+7. Disclaimer of Warranty. Unless required by applicable law or
  145
+   agreed to in writing, Licensor provides the Work (and each
  146
+   Contributor provides its Contributions) on an "AS IS" BASIS,
  147
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  148
+   implied, including, without limitation, any warranties or conditions
  149
+   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
  150
+   PARTICULAR PURPOSE. You are solely responsible for determining the
  151
+   appropriateness of using or redistributing the Work and assume any
  152
+   risks associated with Your exercise of permissions under this License.
  153
+
  154
+8. Limitation of Liability. In no event and under no legal theory,
  155
+   whether in tort (including negligence), contract, or otherwise,
  156
+   unless required by applicable law (such as deliberate and grossly
  157
+   negligent acts) or agreed to in writing, shall any Contributor be
  158
+   liable to You for damages, including any direct, indirect, special,
  159
+   incidental, or consequential damages of any character arising as a
  160
+   result of this License or out of the use or inability to use the
  161
+   Work (including but not limited to damages for loss of goodwill,
  162
+   work stoppage, computer failure or malfunction, or any and all
  163
+   other commercial damages or losses), even if such Contributor
  164
+   has been advised of the possibility of such damages.
  165
+
  166
+9. Accepting Warranty or Additional Liability. While redistributing
  167
+   the Work or Derivative Works thereof, You may choose to offer,
  168
+   and charge a fee for, acceptance of support, warranty, indemnity,
  169
+   or other liability obligations and/or rights consistent with this
  170
+   License. However, in accepting such obligations, You may act only
  171
+   on Your own behalf and on Your sole responsibility, not on behalf
  172
+   of any other Contributor, and only if You agree to indemnify,
  173
+   defend, and hold each Contributor harmless for any liability
  174
+   incurred by, or claims asserted against, such Contributor by reason
  175
+   of your accepting any such warranty or additional liability.
  176
+
  177
+END OF TERMS AND CONDITIONS
  178
+
  179
+APPENDIX: How to apply the Apache License to your work.
  180
+
  181
+   To apply the Apache License to your work, attach the following
  182
+   boilerplate notice, with the fields enclosed by brackets "[]"
  183
+   replaced with your own identifying information. (Don't include
  184
+   the brackets!)  The text should be enclosed in the appropriate
  185
+   comment syntax for the file format. We also recommend that a
  186
+   file or class name and description of purpose be included on the
  187
+   same "printed page" as the copyright notice for easier
  188
+   identification within third-party archives.
  189
+
  190
+Copyright 2010 DISQUS
  191
+
  192
+Licensed under the Apache License, Version 2.0 (the "License");
  193
+you may not use this file except in compliance with the License.
  194
+You may obtain a copy of the License at
  195
+
  196
+    http://www.apache.org/licenses/LICENSE-2.0
  197
+
  198
+Unless required by applicable law or agreed to in writing, software
  199
+distributed under the License is distributed on an "AS IS" BASIS,
  200
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  201
+See the License for the specific language governing permissions and
  202
+limitations under the License.
2  README.rst
Source Rendered
... ...
@@ -0,0 +1,2 @@
  1
+nose-quickunit
  2
+==============
0  quickunit/__init__.py
No changes.
114  quickunit/diff.py
... ...
@@ -0,0 +1,114 @@
  1
+"""
  2
+quickunit.diff
  3
+~~~~~~~~~~~~~~
  4
+
  5
+This is an adaptation from lodgeit's lib/diff.py
  6
+
  7
+:copyright: 2007 by Armin Ronacher.
  8
+:license: BSD
  9
+"""
  10
+
  11
+import re
  12
+
  13
+
  14
+class DiffParser(object):
  15
+    """
  16
+    This is based on code from the open source project, "lodgeit".
  17
+    """
  18
+    _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
  19
+
  20
+    def __init__(self, udiff):
  21
+        """:param udiff:   a text in udiff format"""
  22
+        self.lines = udiff.splitlines()
  23
+
  24
+    def _extract_rev(self, line1, line2):
  25
+        def _extract(line):
  26
+            parts = line.split(None, 1)
  27
+            return parts[0], (len(parts) == 2 and parts[1] or None)
  28
+        try:
  29
+            if line1.startswith('--- ') and line2.startswith('+++ '):
  30
+                return _extract(line1[4:]), _extract(line2[4:])
  31
+        except (ValueError, IndexError):
  32
+            pass
  33
+        return (None, None), (None, None)
  34
+
  35
+    def parse(self):
  36
+        in_header = True
  37
+        header = []
  38
+        lineiter = iter(self.lines)
  39
+        files = []
  40
+        try:
  41
+            line = lineiter.next()
  42
+            while 1:
  43
+                # continue until we found the old file
  44
+                if not line.startswith('--- '):
  45
+                    if in_header:
  46
+                        header.append(line)
  47
+                    line = lineiter.next()
  48
+                    continue
  49
+
  50
+                if header and all(x.strip() for x in header):
  51
+                    # files.append({'is_header': True, 'lines': header})
  52
+                    header = []
  53
+
  54
+                in_header = False
  55
+                chunks = []
  56
+                old, new = self._extract_rev(line, lineiter.next())
  57
+                files.append({
  58
+                    'is_header':        False,
  59
+                    'old_filename':     old[0],
  60
+                    'old_revision':     old[1],
  61
+                    'new_filename':     new[0],
  62
+                    'new_revision':     new[1],
  63
+                    'chunks':           chunks
  64
+                })
  65
+
  66
+                line = lineiter.next()
  67
+                while line:
  68
+                    match = self._chunk_re.match(line)
  69
+                    if not match:
  70
+                        in_header = True
  71
+                        break
  72
+
  73
+                    lines = []
  74
+                    chunks.append(lines)
  75
+
  76
+                    old_line, old_end, new_line, new_end = \
  77
+                        [int(x or 1) for x in match.groups()]
  78
+                    old_line -= 1
  79
+                    new_line -= 1
  80
+                    old_end += old_line
  81
+                    new_end += new_line
  82
+                    line = lineiter.next()
  83
+
  84
+                    while old_line < old_end or new_line < new_end:
  85
+                        if line:
  86
+                            command, line = line[0], line[1:]
  87
+                        else:
  88
+                            command = ' '
  89
+                        affects_old = affects_new = False
  90
+
  91
+                        if command == '+':
  92
+                            affects_new = True
  93
+                            action = 'add'
  94
+                        elif command == '-':
  95
+                            affects_old = True
  96
+                            action = 'del'
  97
+                        else:
  98
+                            affects_old = affects_new = True
  99
+                            action = 'unmod'
  100
+
  101
+                        old_line += affects_old
  102
+                        new_line += affects_new
  103
+                        lines.append({
  104
+                            'old_lineno':   affects_old and old_line or u'',
  105
+                            'new_lineno':   affects_new and new_line or u'',
  106
+                            'action':       action,
  107
+                            'line':         line
  108
+                        })
  109
+                        line = lineiter.next()
  110
+
  111
+        except StopIteration:
  112
+            pass
  113
+
  114
+        return files
137  quickunit/plugin.py
... ...
@@ -0,0 +1,137 @@
  1
+"""
  2
+quickunit.plugin
  3
+~~~~~~~~~~~~~~~~
  4
+
  5
+:copyright: 2011 DISQUS.
  6
+:license: BSD
  7
+"""
  8
+
  9
+from __future__ import absolute_import
  10
+
  11
+import inspect
  12
+import logging
  13
+import os
  14
+import sys
  15
+
  16
+from collections import defaultdict
  17
+from nose.plugins.base import Plugin
  18
+from subprocess import Popen, PIPE, STDOUT
  19
+
  20
+from quickunit.diff import DiffParser
  21
+from quickunit.utils import is_py_script
  22
+
  23
+
  24
+class QuickUnitPlugin(Plugin):
  25
+    """
  26
+    We find the diff with the parent revision for diff-tests with::
  27
+
  28
+        git diff origin/master
  29
+
  30
+    If you run with the discover flag, it will attempt to discovery
  31
+    any tests that are required to run to test the changes in your current
  32
+    branch, against those of origin/master.
  33
+
  34
+    """
  35
+    score = 1000
  36
+    name = 'quickunit'
  37
+
  38
+    def options(self, parser, env):
  39
+        Plugin.options(self, parser, env)
  40
+        parser.add_option("--quickunit-prefix", dest="quickunit_prefix", default="tests/unit/")
  41
+
  42
+    def configure(self, options, config):
  43
+        Plugin.configure(self, options, config)
  44
+        if not self.enabled:
  45
+            return
  46
+
  47
+        self.prefix = options.quickunit_prefix
  48
+        self.parent = 'master'
  49
+
  50
+        self.logger = logging.getLogger(__name__)
  51
+
  52
+        self.pending_files = set()
  53
+
  54
+        # diff is a mapping of filename->set(linenos)
  55
+        self.diff_data = defaultdict(set)
  56
+
  57
+        # the root directory of our diff (this is basically cwd)
  58
+        self.root = None
  59
+
  60
+    def begin(self):
  61
+        # XXX: this is pretty hacky
  62
+        proc = Popen(['git', 'merge-base', 'HEAD', self.parent], stdout=PIPE, stderr=STDOUT)
  63
+        self.parent_revision = proc.stdout.read().strip()
  64
+
  65
+        # pull in our diff
  66
+        # git diff `git merge-base HEAD master`
  67
+        proc = Popen(['git', 'diff', self.parent_revision], stdout=PIPE, stderr=STDOUT)
  68
+        diff = proc.stdout.read().strip()
  69
+
  70
+        parser = DiffParser(diff)
  71
+        files = list(parser.parse())
  72
+
  73
+        diff = self.diff_data
  74
+        for file in files:
  75
+            # we dont care about headers
  76
+            if file['is_header']:
  77
+                continue
  78
+
  79
+            # file was removed
  80
+            if file['new_filename'] == '/dev/null':
  81
+                continue
  82
+
  83
+            is_new_file = (file['old_filename'] == '/dev/null')
  84
+            if is_new_file:
  85
+                filename = file['new_filename']
  86
+                if not filename.startswith('b/'):
  87
+                    continue
  88
+            else:
  89
+                filename = file['old_filename']
  90
+                if not filename.startswith('a/'):
  91
+                    continue  # ??
  92
+
  93
+            filename = filename[2:]
  94
+
  95
+            if self.root is None:
  96
+                self.root = os.path.abspath(filename)[:-len(filename)]
  97
+
  98
+            # Ignore non python files
  99
+            if not is_py_script(filename):
  100
+                continue
  101
+
  102
+            new_filename = file['new_filename'][2:]
  103
+
  104
+            # file is new, only record diff state
  105
+            for chunk in file['chunks']:
  106
+                linenos = filter(bool, (l['new_lineno'] for l in chunk if l['line']))
  107
+                diff[new_filename].update(linenos)
  108
+
  109
+            # we dont care about missing coverage for new code, and there
  110
+            # wont be any "existing coverage" to check for
  111
+            if is_new_file:
  112
+                continue
  113
+
  114
+            self.pending_files.add(os.path.join(self.prefix, new_filename.rsplit('.', 1)[0]))
  115
+
  116
+    def wantMethod(self, method):
  117
+        # only works with unittest compatible functions currently
  118
+        method = getattr(sys.modules[method.im_class.__module__], method.im_class.__name__)
  119
+
  120
+        # check if this test was modified (e.g. added/changed)
  121
+        filename = inspect.getfile(method)
  122
+        if self.root and filename.startswith(self.root):
  123
+            filename = filename[len(self.root):]
  124
+
  125
+        diff_data = self.diff_data[filename]
  126
+        if diff_data:
  127
+            lines, startlineno = inspect.getsourcelines(method)
  128
+            for lineno in xrange(startlineno, len(lines) + startlineno):
  129
+                if lineno in diff_data:
  130
+                    return True
  131
+
  132
+        if filename.startswith(self.prefix):
  133
+            for pending in self.pending_files:
  134
+                if filename.startwith(pending):
  135
+                    return True
  136
+
  137
+        return False
17  quickunit/utils.py
... ...
@@ -0,0 +1,17 @@
  1
+import os
  2
+import os.path
  3
+
  4
+
  5
+def is_py_script(filename):
  6
+    "Returns True if a file is a python executable."
  7
+    if filename.endswith(".py") and os.path.exists(filename):
  8
+        return True
  9
+    elif not os.access(filename, os.X_OK):
  10
+        return False
  11
+    else:
  12
+        try:
  13
+            with open(filename, "r") as fp:
  14
+                first_line = fp.readline().strip()
  15
+            return "#!" in first_line and "python" in first_line
  16
+        except StopIteration:
  17
+            return False
36  setup.py
... ...
@@ -0,0 +1,36 @@
  1
+#!/usr/bin/env python
  2
+
  3
+from setuptools import setup, find_packages
  4
+
  5
+tests_require = [
  6
+]
  7
+
  8
+setup(
  9
+    name='nose-quickunit',
  10
+    version='0.1.0',
  11
+    author='David Cramer',
  12
+    author_email='dcramer@gmail.com',
  13
+    description='A discovery plugin for Nose which relies on sane structure.',
  14
+    url='http://github.com/dcramer/nose-quickunit',
  15
+    packages=find_packages(exclude=["tests"]),
  16
+    zip_safe=False,
  17
+    install_requires=[
  18
+        'nose>=0.9',
  19
+        'simplejson',
  20
+    ],
  21
+    entry_points={
  22
+       'nose.plugins.0.10': [
  23
+            'quickunit = quickunit.plugin:QuickUnitPlugin'
  24
+        ]
  25
+    },
  26
+    license='Apache License 2.0',
  27
+    tests_require=tests_require,
  28
+    extras_require={'test': tests_require},
  29
+    include_package_data=True,
  30
+    classifiers=[
  31
+        'Intended Audience :: Developers',
  32
+        'Intended Audience :: System Administrators',
  33
+        'Operating System :: OS Independent',
  34
+        'Topic :: Software Development'
  35
+    ],
  36
+)

0 notes on commit e656c4e

Please sign in to comment.
Something went wrong with that request. Please try again.