Michael Johnson | a507425 | 2020-06-30 10:28:09 -0700 | [diff] [blame] | 1 | # Copyright (c) 2014 OpenStack Foundation. |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 4 | # not use this file except in compliance with the License. You may obtain |
| 5 | # a copy of the License at |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 12 | # License for the specific language governing permissions and limitations |
| 13 | # under the License. |
| 14 | |
| 15 | |
| 16 | """ |
| 17 | Guidelines for writing new hacking checks |
| 18 | |
| 19 | - Use only for Octavia specific tests. OpenStack general tests |
| 20 | should be submitted to the common 'hacking' module. |
| 21 | - Pick numbers in the range O3xx. Find the current test with |
| 22 | the highest allocated number and then pick the next value. |
| 23 | - Keep the test method code in the source file ordered based |
| 24 | on the O3xx value. |
| 25 | - List the new rule in the top level HACKING.rst file |
| 26 | - Add test cases for each new rule to |
| 27 | octavia/tests/unit/test_hacking.py |
| 28 | |
| 29 | """ |
| 30 | |
| 31 | import re |
| 32 | |
| 33 | from hacking import core |
| 34 | |
| 35 | |
| 36 | _all_log_levels = {'critical', 'error', 'exception', 'info', 'warning'} |
| 37 | _all_hints = {'_LC', '_LE', '_LI', '_', '_LW'} |
| 38 | |
| 39 | _log_translation_hint = re.compile( |
| 40 | r".*LOG\.(%(levels)s)\(\s*(%(hints)s)\(" % { |
| 41 | 'levels': '|'.join(_all_log_levels), |
| 42 | 'hints': '|'.join(_all_hints), |
| 43 | }) |
| 44 | |
| 45 | assert_trueinst_re = re.compile( |
| 46 | r"(.)*assertTrue\(isinstance\((\w|\.|\'|\"|\[|\])+, " |
| 47 | r"(\w|\.|\'|\"|\[|\])+\)\)") |
| 48 | assert_equal_in_end_with_true_or_false_re = re.compile( |
| 49 | r"assertEqual\((\w|[][.'\"])+ in (\w|[][.'\", ])+, (True|False)\)") |
| 50 | assert_equal_in_start_with_true_or_false_re = re.compile( |
| 51 | r"assertEqual\((True|False), (\w|[][.'\"])+ in (\w|[][.'\", ])+\)") |
| 52 | assert_equal_with_true_re = re.compile( |
| 53 | r"assertEqual\(True,") |
| 54 | assert_equal_with_false_re = re.compile( |
| 55 | r"assertEqual\(False,") |
| 56 | mutable_default_args = re.compile(r"^\s*def .+\((.+=\{\}|.+=\[\])") |
| 57 | assert_equal_end_with_none_re = re.compile(r"(.)*assertEqual\(.+, None\)") |
| 58 | assert_equal_start_with_none_re = re.compile(r".*assertEqual\(None, .+\)") |
| 59 | assert_not_equal_end_with_none_re = re.compile( |
| 60 | r"(.)*assertNotEqual\(.+, None\)") |
| 61 | assert_not_equal_start_with_none_re = re.compile( |
| 62 | r"(.)*assertNotEqual\(None, .+\)") |
| 63 | revert_must_have_kwargs_re = re.compile( |
| 64 | r'[ ]*def revert\(.+,[ ](?!\*\*kwargs)\w+\):') |
| 65 | untranslated_exception_re = re.compile(r"raise (?:\w*)\((.*)\)") |
| 66 | no_eventlet_re = re.compile(r'(import|from)\s+[(]?eventlet') |
| 67 | no_line_continuation_backslash_re = re.compile(r'.*(\\)\n') |
| 68 | no_logging_re = re.compile(r'(import|from)\s+[(]?logging') |
| 69 | import_mock_re = re.compile(r"\bimport[\s]+mock\b") |
| 70 | import_from_mock_re = re.compile(r"\bfrom[\s]+mock[\s]+import\b") |
| 71 | |
| 72 | |
| 73 | def _translation_checks_not_enforced(filename): |
| 74 | # Do not do these validations on tests |
| 75 | return any(pat in filename for pat in ["/tests/", "rally-jobs/plugins/"]) |
| 76 | |
| 77 | |
| 78 | @core.flake8ext |
| 79 | def assert_true_instance(logical_line): |
| 80 | """Check for assertTrue(isinstance(a, b)) sentences |
| 81 | |
| 82 | O316 |
| 83 | """ |
| 84 | if assert_trueinst_re.match(logical_line): |
| 85 | yield (0, "O316: assertTrue(isinstance(a, b)) sentences not allowed. " |
| 86 | "Use assertIsInstance instead.") |
| 87 | |
| 88 | |
| 89 | @core.flake8ext |
| 90 | def assert_equal_or_not_none(logical_line): |
| 91 | """Check for assertEqual(A, None) or assertEqual(None, A) sentences, |
| 92 | |
| 93 | assertNotEqual(A, None) or assertNotEqual(None, A) sentences |
| 94 | |
| 95 | O318 |
| 96 | """ |
| 97 | msg = ("O318: assertEqual/assertNotEqual(A, None) or " |
| 98 | "assertEqual/assertNotEqual(None, A) sentences not allowed") |
| 99 | res = (assert_equal_start_with_none_re.match(logical_line) or |
| 100 | assert_equal_end_with_none_re.match(logical_line) or |
| 101 | assert_not_equal_start_with_none_re.match(logical_line) or |
| 102 | assert_not_equal_end_with_none_re.match(logical_line)) |
| 103 | if res: |
| 104 | yield (0, msg) |
| 105 | |
| 106 | |
| 107 | @core.flake8ext |
| 108 | def assert_equal_true_or_false(logical_line): |
| 109 | """Check for assertEqual(True, A) or assertEqual(False, A) sentences |
| 110 | |
| 111 | O323 |
| 112 | """ |
| 113 | res = (assert_equal_with_true_re.search(logical_line) or |
| 114 | assert_equal_with_false_re.search(logical_line)) |
| 115 | if res: |
| 116 | yield (0, "O323: assertEqual(True, A) or assertEqual(False, A) " |
| 117 | "sentences not allowed") |
| 118 | |
| 119 | |
| 120 | @core.flake8ext |
| 121 | def no_mutable_default_args(logical_line): |
| 122 | msg = "O324: Method's default argument shouldn't be mutable!" |
| 123 | if mutable_default_args.match(logical_line): |
| 124 | yield (0, msg) |
| 125 | |
| 126 | |
| 127 | @core.flake8ext |
| 128 | def assert_equal_in(logical_line): |
| 129 | """Check for assertEqual(A in B, True), assertEqual(True, A in B), |
| 130 | |
| 131 | assertEqual(A in B, False) or assertEqual(False, A in B) sentences |
| 132 | |
| 133 | O338 |
| 134 | """ |
| 135 | res = (assert_equal_in_start_with_true_or_false_re.search(logical_line) or |
| 136 | assert_equal_in_end_with_true_or_false_re.search(logical_line)) |
| 137 | if res: |
| 138 | yield (0, "O338: Use assertIn/NotIn(A, B) rather than " |
| 139 | "assertEqual(A in B, True/False) when checking collection " |
| 140 | "contents.") |
| 141 | |
| 142 | |
| 143 | @core.flake8ext |
| 144 | def no_log_warn(logical_line): |
| 145 | """Disallow 'LOG.warn(' |
| 146 | |
| 147 | O339 |
| 148 | """ |
| 149 | if logical_line.startswith('LOG.warn('): |
| 150 | yield(0, "O339:Use LOG.warning() rather than LOG.warn()") |
| 151 | |
| 152 | |
| 153 | @core.flake8ext |
| 154 | def no_translate_logs(logical_line, filename): |
| 155 | """O341 - Don't translate logs. |
| 156 | |
| 157 | Check for 'LOG.*(_(' and 'LOG.*(_Lx(' |
| 158 | |
| 159 | Translators don't provide translations for log messages, and operators |
| 160 | asked not to translate them. |
| 161 | |
| 162 | * This check assumes that 'LOG' is a logger. |
| 163 | |
| 164 | :param logical_line: The logical line to check. |
| 165 | :param filename: The file name where the logical line exists. |
| 166 | :returns: None if the logical line passes the check, otherwise a tuple |
| 167 | is yielded that contains the offending index in logical line |
| 168 | and a message describe the check validation failure. |
| 169 | """ |
| 170 | if _translation_checks_not_enforced(filename): |
| 171 | return |
| 172 | |
| 173 | msg = "O341: Log messages should not be translated!" |
| 174 | match = _log_translation_hint.match(logical_line) |
| 175 | if match: |
| 176 | yield (logical_line.index(match.group()), msg) |
| 177 | |
| 178 | |
| 179 | @core.flake8ext |
| 180 | def check_raised_localized_exceptions(logical_line, filename): |
| 181 | """O342 - Untranslated exception message. |
| 182 | |
| 183 | :param logical_line: The logical line to check. |
| 184 | :param filename: The file name where the logical line exists. |
| 185 | :returns: None if the logical line passes the check, otherwise a tuple |
| 186 | is yielded that contains the offending index in logical line |
| 187 | and a message describe the check validation failure. |
| 188 | """ |
| 189 | if _translation_checks_not_enforced(filename): |
| 190 | return |
| 191 | |
| 192 | logical_line = logical_line.strip() |
| 193 | raised_search = untranslated_exception_re.match(logical_line) |
| 194 | if raised_search: |
| 195 | exception_msg = raised_search.groups()[0] |
| 196 | if exception_msg.startswith("\"") or exception_msg.startswith("\'"): |
| 197 | msg = "O342: Untranslated exception message." |
| 198 | yield (logical_line.index(exception_msg), msg) |
| 199 | |
| 200 | |
| 201 | @core.flake8ext |
| 202 | def check_no_eventlet_imports(logical_line): |
| 203 | """O345 - Usage of Python eventlet module not allowed. |
| 204 | |
| 205 | :param logical_line: The logical line to check. |
| 206 | :returns: None if the logical line passes the check, otherwise a tuple |
| 207 | is yielded that contains the offending index in logical line |
| 208 | and a message describe the check validation failure. |
| 209 | """ |
| 210 | if no_eventlet_re.match(logical_line): |
| 211 | msg = 'O345 Usage of Python eventlet module not allowed' |
| 212 | yield logical_line.index('eventlet'), msg |
| 213 | |
| 214 | |
| 215 | @core.flake8ext |
| 216 | def check_line_continuation_no_backslash(logical_line, tokens): |
| 217 | """O346 - Don't use backslashes for line continuation. |
| 218 | |
| 219 | :param logical_line: The logical line to check. Not actually used. |
| 220 | :param tokens: List of tokens to check. |
| 221 | :returns: None if the tokens don't contain any issues, otherwise a tuple |
| 222 | is yielded that contains the offending index in the logical |
| 223 | line and a message describe the check validation failure. |
| 224 | """ |
| 225 | backslash = None |
| 226 | for token_type, text, start, end, orig_line in tokens: |
| 227 | m = no_line_continuation_backslash_re.match(orig_line) |
| 228 | if m: |
| 229 | backslash = (start[0], m.start(1)) |
| 230 | break |
| 231 | |
| 232 | if backslash is not None: |
| 233 | msg = 'O346 Backslash line continuations not allowed' |
| 234 | yield backslash, msg |
| 235 | |
| 236 | |
| 237 | @core.flake8ext |
| 238 | def revert_must_have_kwargs(logical_line): |
| 239 | """O347 - Taskflow revert methods must have \\*\\*kwargs. |
| 240 | |
| 241 | :param logical_line: The logical line to check. |
| 242 | :returns: None if the logical line passes the check, otherwise a tuple |
| 243 | is yielded that contains the offending index in logical line |
| 244 | and a message describe the check validation failure. |
| 245 | """ |
| 246 | if revert_must_have_kwargs_re.match(logical_line): |
| 247 | msg = 'O347 Taskflow revert methods must have **kwargs' |
| 248 | yield 0, msg |
| 249 | |
| 250 | |
| 251 | @core.flake8ext |
| 252 | def check_no_logging_imports(logical_line): |
| 253 | """O348 - Usage of Python logging module not allowed. |
| 254 | |
| 255 | :param logical_line: The logical line to check. |
| 256 | :returns: None if the logical line passes the check, otherwise a tuple |
| 257 | is yielded that contains the offending index in logical line |
| 258 | and a message describe the check validation failure. |
| 259 | """ |
| 260 | if no_logging_re.match(logical_line): |
| 261 | msg = 'O348 Usage of Python logging module not allowed, use oslo_log' |
| 262 | yield logical_line.index('logging'), msg |
| 263 | |
| 264 | |
| 265 | @core.flake8ext |
| 266 | def check_no_import_mock(logical_line): |
| 267 | """O349 - Test code must not import mock library. |
| 268 | |
| 269 | :param logical_line: The logical line to check. |
| 270 | :returns: None if the logical line passes the check, otherwise a tuple |
| 271 | is yielded that contains the offending index in logical line |
| 272 | and a message describe the check validation failure. |
| 273 | """ |
| 274 | if (import_mock_re.match(logical_line) or |
| 275 | import_from_mock_re.match(logical_line)): |
| 276 | msg = 'O349 Test code must not import mock library, use unittest.mock' |
| 277 | yield 0, msg |