Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 |
| 3 | |
| 4 | # Copyright (c) 2012, Cloudscaling |
| 5 | # All Rights Reserved. |
| 6 | # |
| 7 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 8 | # not use this file except in compliance with the License. You may obtain |
| 9 | # a copy of the License at |
| 10 | # |
| 11 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 12 | # |
| 13 | # Unless required by applicable law or agreed to in writing, software |
| 14 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 15 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 16 | # License for the specific language governing permissions and limitations |
| 17 | # under the License. |
| 18 | |
| 19 | """tempest HACKING file compliance testing |
| 20 | |
| 21 | built on top of pep8.py |
| 22 | """ |
| 23 | |
| 24 | import fnmatch |
| 25 | import inspect |
| 26 | import logging |
| 27 | import os |
| 28 | import re |
| 29 | import subprocess |
| 30 | import sys |
| 31 | import tokenize |
| 32 | import warnings |
| 33 | |
| 34 | import pep8 |
| 35 | |
| 36 | # Don't need this for testing |
| 37 | logging.disable('LOG') |
| 38 | |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 39 | #T1xx comments |
| 40 | #T2xx except |
| 41 | #T3xx imports |
| 42 | #T4xx docstrings |
| 43 | #T5xx dictionaries/lists |
| 44 | #T6xx calling methods |
| 45 | #T7xx localization |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 46 | #N8xx git commit messages |
| 47 | |
| 48 | IMPORT_EXCEPTIONS = ['sqlalchemy', 'migrate'] |
| 49 | DOCSTRING_TRIPLE = ['"""', "'''"] |
| 50 | VERBOSE_MISSING_IMPORT = os.getenv('HACKING_VERBOSE_MISSING_IMPORT', 'False') |
| 51 | |
| 52 | |
| 53 | # Monkey patch broken excluded filter in pep8 |
| 54 | # See https://github.com/jcrocholl/pep8/pull/111 |
| 55 | def excluded(self, filename): |
| 56 | """ |
| 57 | Check if options.exclude contains a pattern that matches filename. |
| 58 | """ |
| 59 | basename = os.path.basename(filename) |
| 60 | return any((pep8.filename_match(filename, self.options.exclude, |
| 61 | default=False), |
| 62 | pep8.filename_match(basename, self.options.exclude, |
| 63 | default=False))) |
| 64 | |
| 65 | |
| 66 | def input_dir(self, dirname): |
| 67 | """Check all files in this directory and all subdirectories.""" |
| 68 | dirname = dirname.rstrip('/') |
| 69 | if self.excluded(dirname): |
| 70 | return 0 |
| 71 | counters = self.options.report.counters |
| 72 | verbose = self.options.verbose |
| 73 | filepatterns = self.options.filename |
| 74 | runner = self.runner |
| 75 | for root, dirs, files in os.walk(dirname): |
| 76 | if verbose: |
| 77 | print('directory ' + root) |
| 78 | counters['directories'] += 1 |
| 79 | for subdir in sorted(dirs): |
| 80 | if self.excluded(os.path.join(root, subdir)): |
| 81 | dirs.remove(subdir) |
| 82 | for filename in sorted(files): |
| 83 | # contain a pattern that matches? |
| 84 | if ((pep8.filename_match(filename, filepatterns) and |
| 85 | not self.excluded(filename))): |
| 86 | runner(os.path.join(root, filename)) |
| 87 | |
| 88 | |
| 89 | def is_import_exception(mod): |
| 90 | return (mod in IMPORT_EXCEPTIONS or |
| 91 | any(mod.startswith(m + '.') for m in IMPORT_EXCEPTIONS)) |
| 92 | |
| 93 | |
| 94 | def import_normalize(line): |
| 95 | # convert "from x import y" to "import x.y" |
| 96 | # handle "from x import y as z" to "import x.y as z" |
| 97 | split_line = line.split() |
| 98 | if ("import" in line and line.startswith("from ") and "," not in line and |
| 99 | split_line[2] == "import" and split_line[3] != "*" and |
| 100 | split_line[1] != "__future__" and |
| 101 | (len(split_line) == 4 or |
| 102 | (len(split_line) == 6 and split_line[4] == "as"))): |
| 103 | return "import %s.%s" % (split_line[1], split_line[3]) |
| 104 | else: |
| 105 | return line |
| 106 | |
| 107 | |
| 108 | def tempest_todo_format(physical_line): |
| 109 | """Check for 'TODO()'. |
| 110 | |
| 111 | tempest HACKING guide recommendation for TODO: |
| 112 | Include your name with TODOs as in "#TODO(termie)" |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 113 | T101 |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 114 | """ |
| 115 | pos = physical_line.find('TODO') |
| 116 | pos1 = physical_line.find('TODO(') |
| 117 | pos2 = physical_line.find('#') # make sure it's a comment |
| 118 | if (pos != pos1 and pos2 >= 0 and pos2 < pos): |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 119 | return pos, "T101: Use TODO(NAME)" |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 120 | |
| 121 | |
| 122 | def tempest_except_format(logical_line): |
| 123 | """Check for 'except:'. |
| 124 | |
| 125 | tempest HACKING guide recommends not using except: |
| 126 | Do not write "except:", use "except Exception:" at the very least |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 127 | T201 |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 128 | """ |
| 129 | if logical_line.startswith("except:"): |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 130 | yield 6, "T201: no 'except:' at least use 'except Exception:'" |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 131 | |
| 132 | |
| 133 | def tempest_except_format_assert(logical_line): |
| 134 | """Check for 'assertRaises(Exception'. |
| 135 | |
| 136 | tempest HACKING guide recommends not using assertRaises(Exception...): |
| 137 | Do not use overly broad Exception type |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 138 | T202 |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 139 | """ |
| 140 | if logical_line.startswith("self.assertRaises(Exception"): |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 141 | yield 1, "T202: assertRaises Exception too broad" |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 142 | |
| 143 | |
| 144 | def tempest_one_import_per_line(logical_line): |
| 145 | """Check for import format. |
| 146 | |
| 147 | tempest HACKING guide recommends one import per line: |
| 148 | Do not import more than one module per line |
| 149 | |
| 150 | Examples: |
| 151 | BAD: from tempest.common.rest_client import RestClient, RestClientXML |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 152 | T301 |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 153 | """ |
| 154 | pos = logical_line.find(',') |
| 155 | parts = logical_line.split() |
| 156 | if (pos > -1 and (parts[0] == "import" or |
| 157 | parts[0] == "from" and parts[2] == "import") and |
| 158 | not is_import_exception(parts[1])): |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 159 | yield pos, "T301: one import per line" |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 160 | |
| 161 | _missingImport = set([]) |
| 162 | |
| 163 | |
| 164 | def tempest_import_module_only(logical_line): |
| 165 | """Check for import module only. |
| 166 | |
| 167 | tempest HACKING guide recommends importing only modules: |
| 168 | Do not import objects, only modules |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 169 | T302 import only modules |
| 170 | T303 Invalid Import |
| 171 | T304 Relative Import |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 172 | """ |
| 173 | def importModuleCheck(mod, parent=None, added=False): |
| 174 | """ |
| 175 | If can't find module on first try, recursively check for relative |
| 176 | imports |
| 177 | """ |
| 178 | current_path = os.path.dirname(pep8.current_file) |
| 179 | try: |
| 180 | with warnings.catch_warnings(): |
| 181 | warnings.simplefilter('ignore', DeprecationWarning) |
| 182 | valid = True |
| 183 | if parent: |
| 184 | if is_import_exception(parent): |
| 185 | return |
| 186 | parent_mod = __import__(parent, globals(), locals(), |
| 187 | [mod], -1) |
| 188 | valid = inspect.ismodule(getattr(parent_mod, mod)) |
| 189 | else: |
| 190 | __import__(mod, globals(), locals(), [], -1) |
| 191 | valid = inspect.ismodule(sys.modules[mod]) |
| 192 | if not valid: |
| 193 | if added: |
| 194 | sys.path.pop() |
| 195 | added = False |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 196 | return logical_line.find(mod), ("T304: No " |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 197 | "relative imports. " |
| 198 | "'%s' is a relative " |
| 199 | "import" |
| 200 | % logical_line) |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 201 | return logical_line.find(mod), ("T302: import only" |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 202 | " modules. '%s' does not " |
| 203 | "import a module" |
| 204 | % logical_line) |
| 205 | |
| 206 | except (ImportError, NameError) as exc: |
| 207 | if not added: |
| 208 | added = True |
| 209 | sys.path.append(current_path) |
| 210 | return importModuleCheck(mod, parent, added) |
| 211 | else: |
| 212 | name = logical_line.split()[1] |
| 213 | if name not in _missingImport: |
| 214 | if VERBOSE_MISSING_IMPORT != 'False': |
| 215 | print >> sys.stderr, ("ERROR: import '%s' in %s " |
| 216 | "failed: %s" % |
| 217 | (name, pep8.current_file, exc)) |
| 218 | _missingImport.add(name) |
| 219 | added = False |
| 220 | sys.path.pop() |
| 221 | return |
| 222 | |
| 223 | except AttributeError: |
| 224 | # Invalid import |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 225 | return logical_line.find(mod), ("T303: Invalid import, " |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 226 | "AttributeError raised") |
| 227 | |
| 228 | # convert "from x import y" to " import x.y" |
| 229 | # convert "from x import y as z" to " import x.y" |
| 230 | import_normalize(logical_line) |
| 231 | split_line = logical_line.split() |
| 232 | |
| 233 | if (logical_line.startswith("import ") and "," not in logical_line and |
| 234 | (len(split_line) == 2 or |
| 235 | (len(split_line) == 4 and split_line[2] == "as"))): |
| 236 | mod = split_line[1] |
| 237 | rval = importModuleCheck(mod) |
| 238 | if rval is not None: |
| 239 | yield rval |
| 240 | |
| 241 | # TODO(jogo) handle "from x import *" |
| 242 | |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 243 | #TODO(jogo): import template: T305 |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 244 | |
| 245 | |
| 246 | def tempest_import_alphabetical(logical_line, line_number, lines): |
| 247 | """Check for imports in alphabetical order. |
| 248 | |
| 249 | Tempest HACKING guide recommendation for imports: |
| 250 | imports in human alphabetical order |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 251 | T306 |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 252 | """ |
| 253 | # handle import x |
| 254 | # use .lower since capitalization shouldn't dictate order |
| 255 | split_line = import_normalize(logical_line.strip()).lower().split() |
| 256 | split_previous = import_normalize(lines[ |
| 257 | line_number - 2]).strip().lower().split() |
| 258 | # with or without "as y" |
| 259 | length = [2, 4] |
| 260 | if (len(split_line) in length and len(split_previous) in length and |
| 261 | split_line[0] == "import" and split_previous[0] == "import"): |
| 262 | if split_line[1] < split_previous[1]: |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 263 | yield (0, "T306: imports not in alphabetical order" |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 264 | " (%s, %s)" |
| 265 | % (split_previous[1], split_line[1])) |
| 266 | |
| 267 | |
| 268 | def tempest_docstring_start_space(physical_line): |
| 269 | """Check for docstring not start with space. |
| 270 | |
| 271 | tempest HACKING guide recommendation for docstring: |
| 272 | Docstring should not start with space |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 273 | T401 |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 274 | """ |
| 275 | pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE]) # start |
Sean Dague | f237ccb | 2013-01-04 15:19:14 -0500 | [diff] [blame^] | 276 | end = max([physical_line[-4:-1] == i for i in DOCSTRING_TRIPLE]) # end |
| 277 | if (pos != -1 and end and len(physical_line) > pos + 4): |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 278 | if (physical_line[pos + 3] == ' '): |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 279 | return (pos, "T401: one line docstring should not start" |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 280 | " with a space") |
| 281 | |
| 282 | |
| 283 | def tempest_docstring_one_line(physical_line): |
| 284 | """Check one line docstring end. |
| 285 | |
| 286 | tempest HACKING guide recommendation for one line docstring: |
| 287 | A one line docstring looks like this and ends in a period. |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 288 | T402 |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 289 | """ |
| 290 | pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE]) # start |
| 291 | end = max([physical_line[-4:-1] == i for i in DOCSTRING_TRIPLE]) # end |
| 292 | if (pos != -1 and end and len(physical_line) > pos + 4): |
| 293 | if (physical_line[-5] != '.'): |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 294 | return pos, "T402: one line docstring needs a period" |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 295 | |
| 296 | |
| 297 | def tempest_docstring_multiline_end(physical_line): |
| 298 | """Check multi line docstring end. |
| 299 | |
| 300 | Tempest HACKING guide recommendation for docstring: |
| 301 | Docstring should end on a new line |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 302 | T403 |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 303 | """ |
| 304 | pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE]) # start |
| 305 | if (pos != -1 and len(physical_line) == pos): |
| 306 | if (physical_line[pos + 3] == ' '): |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 307 | return (pos, "T403: multi line docstring end on new line") |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 308 | |
| 309 | |
Sean Dague | 97449cc | 2013-01-04 14:38:26 -0500 | [diff] [blame] | 310 | def tempest_no_test_docstring(physical_line, previous_logical, filename): |
| 311 | """Check that test_ functions don't have docstrings |
| 312 | |
| 313 | This ensure we get better results out of tempest, instead |
| 314 | of them being hidden behind generic descriptions of the |
| 315 | functions. |
| 316 | |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 317 | T404 |
Sean Dague | 97449cc | 2013-01-04 14:38:26 -0500 | [diff] [blame] | 318 | """ |
| 319 | if "tempest/test" in filename: |
| 320 | pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE]) |
| 321 | if pos != -1: |
| 322 | if previous_logical.startswith("def test_"): |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 323 | return (pos, "T404: test functions must " |
Sean Dague | 97449cc | 2013-01-04 14:38:26 -0500 | [diff] [blame] | 324 | "not have doc strings") |
| 325 | |
| 326 | |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 327 | FORMAT_RE = re.compile("%(?:" |
| 328 | "%|" # Ignore plain percents |
| 329 | "(\(\w+\))?" # mapping key |
| 330 | "([#0 +-]?" # flag |
| 331 | "(?:\d+|\*)?" # width |
| 332 | "(?:\.\d+)?" # precision |
| 333 | "[hlL]?" # length mod |
| 334 | "\w))") # type |
| 335 | |
| 336 | |
| 337 | class LocalizationError(Exception): |
| 338 | pass |
| 339 | |
| 340 | |
| 341 | def check_i18n(): |
| 342 | """Generator that checks token stream for localization errors. |
| 343 | |
| 344 | Expects tokens to be ``send``ed one by one. |
| 345 | Raises LocalizationError if some error is found. |
| 346 | """ |
| 347 | while True: |
| 348 | try: |
| 349 | token_type, text, _, _, line = yield |
| 350 | except GeneratorExit: |
| 351 | return |
| 352 | if (token_type == tokenize.NAME and text == "_" and |
| 353 | not line.startswith('def _(msg):')): |
| 354 | |
| 355 | while True: |
| 356 | token_type, text, start, _, _ = yield |
| 357 | if token_type != tokenize.NL: |
| 358 | break |
| 359 | if token_type != tokenize.OP or text != "(": |
| 360 | continue # not a localization call |
| 361 | |
| 362 | format_string = '' |
| 363 | while True: |
| 364 | token_type, text, start, _, _ = yield |
| 365 | if token_type == tokenize.STRING: |
| 366 | format_string += eval(text) |
| 367 | elif token_type == tokenize.NL: |
| 368 | pass |
| 369 | else: |
| 370 | break |
| 371 | |
| 372 | if not format_string: |
| 373 | raise LocalizationError(start, |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 374 | "T701: Empty localization " |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 375 | "string") |
| 376 | if token_type != tokenize.OP: |
| 377 | raise LocalizationError(start, |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 378 | "T701: Invalid localization " |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 379 | "call") |
| 380 | if text != ")": |
| 381 | if text == "%": |
| 382 | raise LocalizationError(start, |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 383 | "T702: Formatting " |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 384 | "operation should be outside" |
| 385 | " of localization method call") |
| 386 | elif text == "+": |
| 387 | raise LocalizationError(start, |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 388 | "T702: Use bare string " |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 389 | "concatenation instead of +") |
| 390 | else: |
| 391 | raise LocalizationError(start, |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 392 | "T702: Argument to _ must" |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 393 | " be just a string") |
| 394 | |
| 395 | format_specs = FORMAT_RE.findall(format_string) |
| 396 | positional_specs = [(key, spec) for key, spec in format_specs |
| 397 | if not key and spec] |
| 398 | # not spec means %%, key means %(smth)s |
| 399 | if len(positional_specs) > 1: |
| 400 | raise LocalizationError(start, |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 401 | "T703: Multiple positional " |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 402 | "placeholders") |
| 403 | |
| 404 | |
| 405 | def tempest_localization_strings(logical_line, tokens): |
| 406 | """Check localization in line. |
| 407 | |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 408 | T701: bad localization call |
| 409 | T702: complex expression instead of string as argument to _() |
| 410 | T703: multiple positional placeholders |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 411 | """ |
| 412 | |
| 413 | gen = check_i18n() |
| 414 | next(gen) |
| 415 | try: |
| 416 | map(gen.send, tokens) |
| 417 | gen.close() |
| 418 | except LocalizationError as e: |
| 419 | yield e.args |
| 420 | |
| 421 | #TODO(jogo) Dict and list objects |
| 422 | |
| 423 | current_file = "" |
| 424 | |
| 425 | |
| 426 | def readlines(filename): |
| 427 | """Record the current file being tested.""" |
| 428 | pep8.current_file = filename |
| 429 | return open(filename).readlines() |
| 430 | |
| 431 | |
| 432 | def add_tempest(): |
| 433 | """Monkey patch in tempest guidelines. |
| 434 | |
| 435 | Look for functions that start with tempest_ and have arguments |
| 436 | and add them to pep8 module |
| 437 | Assumes you know how to write pep8.py checks |
| 438 | """ |
| 439 | for name, function in globals().items(): |
| 440 | if not inspect.isfunction(function): |
| 441 | continue |
| 442 | args = inspect.getargspec(function)[0] |
| 443 | if args and name.startswith("tempest"): |
| 444 | exec("pep8.%s = %s" % (name, name)) |
| 445 | |
| 446 | |
| 447 | def once_git_check_commit_title(): |
| 448 | """Check git commit messages. |
| 449 | |
| 450 | tempest HACKING recommends not referencing a bug or blueprint |
| 451 | in first line, it should provide an accurate description of the change |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 452 | T801 |
| 453 | T802 Title limited to 50 chars |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 454 | """ |
| 455 | #Get title of most recent commit |
| 456 | |
| 457 | subp = subprocess.Popen(['git', 'log', '--no-merges', '--pretty=%s', '-1'], |
| 458 | stdout=subprocess.PIPE) |
| 459 | title = subp.communicate()[0] |
| 460 | if subp.returncode: |
| 461 | raise Exception("git log failed with code %s" % subp.returncode) |
| 462 | |
| 463 | #From https://github.com/openstack/openstack-ci-puppet |
| 464 | # /blob/master/modules/gerrit/manifests/init.pp#L74 |
| 465 | #Changeid|bug|blueprint |
| 466 | git_keywords = (r'(I[0-9a-f]{8,40})|' |
| 467 | '([Bb]ug|[Ll][Pp])[\s\#:]*(\d+)|' |
| 468 | '([Bb]lue[Pp]rint|[Bb][Pp])[\s\#:]*([A-Za-z0-9\\-]+)') |
| 469 | GIT_REGEX = re.compile(git_keywords) |
| 470 | |
| 471 | error = False |
| 472 | #NOTE(jogo) if match regex but over 3 words, acceptable title |
| 473 | if GIT_REGEX.search(title) is not None and len(title.split()) <= 3: |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 474 | print ("T801: git commit title ('%s') should provide an accurate " |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 475 | "description of the change, not just a reference to a bug " |
| 476 | "or blueprint" % title.strip()) |
| 477 | error = True |
| 478 | if len(title.decode('utf-8')) > 72: |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 479 | print ("T802: git commit title ('%s') should be under 50 chars" |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 480 | % title.strip()) |
| 481 | error = True |
| 482 | return error |
| 483 | |
| 484 | if __name__ == "__main__": |
| 485 | #include tempest path |
| 486 | sys.path.append(os.getcwd()) |
| 487 | #Run once tests (not per line) |
| 488 | once_error = once_git_check_commit_title() |
Sean Dague | d18cfe5 | 2013-01-04 14:53:00 -0500 | [diff] [blame] | 489 | #TEMPEST error codes start with a T |
| 490 | pep8.ERRORCODE_REGEX = re.compile(r'[EWT]\d{3}') |
Matthew Treinish | 8b37289 | 2012-12-07 17:13:16 -0500 | [diff] [blame] | 491 | add_tempest() |
| 492 | pep8.current_file = current_file |
| 493 | pep8.readlines = readlines |
| 494 | pep8.StyleGuide.excluded = excluded |
| 495 | pep8.StyleGuide.input_dir = input_dir |
| 496 | try: |
| 497 | pep8._main() |
| 498 | sys.exit(once_error) |
| 499 | finally: |
| 500 | if len(_missingImport) > 0: |
| 501 | print >> sys.stderr, ("%i imports missing in this test environment" |
| 502 | % len(_missingImport)) |