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