test_roller.py (12864B)
1 # This Source Code Form is subject to the terms of the Mozilla Public 2 # License, v. 2.0. If a copy of the MPL was not distributed with this 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 import os 6 import platform 7 import signal 8 import subprocess 9 import sys 10 import time 11 from itertools import chain 12 13 import mozunit 14 import pytest 15 16 from mozlint.errors import LintersNotConfigured, NoValidLinter 17 from mozlint.result import Issue, ResultSummary 18 from mozlint.roller import LintRoller 19 20 here = os.path.abspath(os.path.dirname(__file__)) 21 22 23 def test_roll_no_linters_configured(lint, files): 24 with pytest.raises(LintersNotConfigured): 25 lint.roll(files) 26 27 28 def test_roll_successful(lint, linters, files): 29 lint.read(linters("string", "regex", "external")) 30 31 result = lint.roll(files) 32 assert len(result.issues) == 1 33 assert result.failed == set([]) 34 35 path = list(result.issues.keys())[0] 36 assert os.path.basename(path) == "foobar.js" 37 38 errors = result.issues[path] 39 assert isinstance(errors, list) 40 assert len(errors) == 6 41 42 container = errors[0] 43 assert isinstance(container, Issue) 44 assert container.rule == "no-foobar" 45 46 47 def test_roll_from_subdir(lint, linters): 48 lint.read(linters("string", "regex", "external")) 49 50 oldcwd = os.getcwd() 51 try: 52 os.chdir(os.path.join(lint.root, "files")) 53 54 # Path relative to cwd works 55 result = lint.roll("foobar.js") 56 assert len(result.issues) == 1 57 assert len(result.failed) == 0 58 assert result.returncode == 1 59 60 # Path relative to root doesn't work 61 result = lint.roll(os.path.join("files", "foobar.js")) 62 assert len(result.issues) == 0 63 assert len(result.failed) == 0 64 assert result.returncode == 0 65 66 # Paths from vcs are always joined to root instead of cwd 67 lint.mock_vcs([os.path.join("files", "foobar.js")]) 68 result = lint.roll(outgoing=True) 69 assert len(result.issues) == 1 70 assert len(result.failed) == 0 71 assert result.returncode == 1 72 73 result = lint.roll(workdir=True) 74 assert len(result.issues) == 1 75 assert len(result.failed) == 0 76 assert result.returncode == 1 77 78 result = lint.roll(rev='not public() and keyword("dummy revset expression")') 79 assert len(result.issues) == 1 80 assert len(result.failed) == 0 81 assert result.returncode == 1 82 finally: 83 os.chdir(oldcwd) 84 85 86 def test_roll_catch_exception(lint, linters, files, capfd): 87 lint.read(linters("raises")) 88 89 lint.roll(files) # assert not raises 90 out, err = capfd.readouterr() 91 assert "LintException" in err 92 93 94 def test_roll_with_global_excluded_path(lint, linters, files): 95 lint.exclude = ["**/foobar.js"] 96 lint.read(linters("string", "regex", "external")) 97 result = lint.roll(files) 98 99 assert len(result.issues) == 0 100 assert result.failed == set([]) 101 102 103 def test_roll_with_local_excluded_path(lint, linters, files): 104 lint.read(linters("excludes")) 105 result = lint.roll(files) 106 107 assert "**/foobar.js" in lint.linters[0]["local_exclude"] 108 assert len(result.issues) == 0 109 assert result.failed == set([]) 110 111 112 def test_roll_with_no_files_to_lint(lint, linters, capfd): 113 lint.read(linters("string", "regex", "external")) 114 lint.mock_vcs([]) 115 result = lint.roll([], workdir=True) 116 assert isinstance(result, ResultSummary) 117 assert len(result.issues) == 0 118 assert len(result.failed) == 0 119 120 out, err = capfd.readouterr() 121 assert "WARNING: no files linted" in err 122 123 124 def test_roll_with_invalid_extension(lint, linters, filedir): 125 lint.read(linters("external")) 126 result = lint.roll(os.path.join(filedir, "foobar.py")) 127 assert len(result.issues) == 0 128 assert result.failed == set([]) 129 130 131 def test_roll_with_failure_code(lint, linters, files): 132 lint.read(linters("badreturncode")) 133 134 result = lint.roll(files, num_procs=1) 135 assert len(result.issues) == 0 136 assert result.failed == set(["BadReturnCodeLinter"]) 137 138 139 def test_roll_warnings(lint, linters, files): 140 lint.read(linters("warning")) 141 result = lint.roll(files) 142 assert len(result.issues) == 0 143 assert result.total_issues == 0 144 assert len(result.suppressed_warnings) == 1 145 assert result.total_suppressed_warnings == 2 146 147 lint.lintargs["show_warnings"] = True 148 result = lint.roll(files) 149 assert len(result.issues) == 1 150 assert result.total_issues == 2 151 assert len(result.suppressed_warnings) == 0 152 assert result.total_suppressed_warnings == 0 153 154 155 def test_roll_code_review(monkeypatch, linters, files): 156 monkeypatch.setenv("CODE_REVIEW", "1") 157 lint = LintRoller(root=here, show_warnings=False) 158 lint.read(linters("warning")) 159 result = lint.roll(files) 160 assert len(result.issues) == 1 161 assert result.total_issues == 2 162 assert len(result.suppressed_warnings) == 0 163 assert result.total_suppressed_warnings == 0 164 assert result.returncode == 1 165 166 167 def test_roll_code_review_warnings_disabled(monkeypatch, linters, files): 168 monkeypatch.setenv("CODE_REVIEW", "1") 169 lint = LintRoller(root=here, show_warnings=False) 170 lint.read(linters("warning_no_code_review")) 171 result = lint.roll(files) 172 assert len(result.issues) == 0 173 assert result.total_issues == 0 174 assert lint.result.fail_on_warnings is True 175 assert len(result.suppressed_warnings) == 1 176 assert result.total_suppressed_warnings == 2 177 assert result.returncode == 0 178 179 180 def test_roll_code_review_warnings_soft(linters, files): 181 lint = LintRoller(root=here, show_warnings="soft") 182 lint.read(linters("warning_no_code_review")) 183 result = lint.roll(files) 184 assert len(result.issues) == 1 185 assert result.total_issues == 2 186 assert lint.result.fail_on_warnings is False 187 assert len(result.suppressed_warnings) == 0 188 assert result.total_suppressed_warnings == 0 189 assert result.returncode == 0 190 191 192 def fake_run_worker(config, paths, **lintargs): 193 result = ResultSummary(lintargs["root"]) 194 result.issues["count"].append(1) 195 return result 196 197 198 @pytest.mark.skipif( 199 platform.system() == "Windows", 200 reason="monkeypatch issues with multiprocessing on Windows", 201 ) 202 @pytest.mark.parametrize("num_procs", [1, 4, 8, 16]) 203 def test_number_of_jobs(monkeypatch, lint, linters, files, num_procs): 204 monkeypatch.setattr(sys.modules[lint.__module__], "_run_worker", fake_run_worker) 205 206 linters = linters("string", "regex", "external") 207 lint.read(linters) 208 num_jobs = len(lint.roll(files, num_procs=num_procs).issues["count"]) 209 210 if len(files) >= num_procs: 211 assert num_jobs == num_procs * len(linters) 212 else: 213 assert num_jobs == len(files) * len(linters) 214 215 216 @pytest.mark.skipif( 217 platform.system() == "Windows", 218 reason="monkeypatch issues with multiprocessing on Windows", 219 ) 220 @pytest.mark.parametrize("max_paths,expected_jobs", [(1, 12), (4, 6), (16, 6)]) 221 def test_max_paths_per_job(monkeypatch, lint, linters, files, max_paths, expected_jobs): 222 monkeypatch.setattr(sys.modules[lint.__module__], "_run_worker", fake_run_worker) 223 224 files = files[:4] 225 assert len(files) == 4 226 227 linters = linters("string", "regex", "external")[:3] 228 assert len(linters) == 3 229 230 lint.MAX_PATHS_PER_JOB = max_paths 231 lint.read(linters) 232 num_jobs = len(lint.roll(files, num_procs=2).issues["count"]) 233 assert num_jobs == expected_jobs 234 235 236 @pytest.mark.skipif( 237 platform.system() == "Windows", 238 reason="monkeypatch issues with multiprocessing on Windows", 239 ) 240 @pytest.mark.parametrize("num_procs", [1, 4, 8, 16]) 241 def test_number_of_jobs_global(monkeypatch, lint, linters, files, num_procs): 242 monkeypatch.setattr(sys.modules[lint.__module__], "_run_worker", fake_run_worker) 243 244 linters = linters("global") 245 lint.read(linters) 246 num_jobs = len(lint.roll(files, num_procs=num_procs).issues["count"]) 247 248 assert num_jobs == 1 249 250 251 @pytest.mark.skipif( 252 platform.system() == "Windows", 253 reason="monkeypatch issues with multiprocessing on Windows", 254 ) 255 @pytest.mark.parametrize("max_paths", [1, 4, 16]) 256 def test_max_paths_per_job_global(monkeypatch, lint, linters, files, max_paths): 257 monkeypatch.setattr(sys.modules[lint.__module__], "_run_worker", fake_run_worker) 258 259 files = files[:4] 260 assert len(files) == 4 261 262 linters = linters("global")[:1] 263 assert len(linters) == 1 264 265 lint.MAX_PATHS_PER_JOB = max_paths 266 lint.read(linters) 267 num_jobs = len(lint.roll(files, num_procs=2).issues["count"]) 268 assert num_jobs == 1 269 270 271 @pytest.mark.skipif( 272 platform.system() == "Windows", 273 reason="signal.CTRL_C_EVENT isn't causing a KeyboardInterrupt on Windows", 274 ) 275 def test_keyboard_interrupt(): 276 # We use two linters so we'll have two jobs. One (string.yml) will complete 277 # quickly. The other (slow.yml) will run slowly. This way the first worker 278 # will be be stuck blocking on the ProcessPoolExecutor._call_queue when the 279 # signal arrives and the other still be doing work. 280 cmd = [sys.executable, "runcli.py", "-l=string", "-l=slow", "files/foobar.js"] 281 env = os.environ.copy() 282 env["PYTHONPATH"] = os.pathsep.join(sys.path) 283 proc = subprocess.Popen( 284 cmd, 285 stdout=subprocess.PIPE, 286 stderr=subprocess.STDOUT, 287 cwd=here, 288 env=env, 289 universal_newlines=True, 290 ) 291 time.sleep(1) 292 proc.send_signal(signal.SIGINT) 293 294 out = proc.communicate()[0] 295 print(out) 296 assert "WARNING: \nnot all files were linted" in out 297 assert "2 problems" in out 298 assert "Traceback" not in out 299 300 301 def test_support_files(lint, linters, filedir, monkeypatch, files): 302 jobs = [] 303 304 # Replace the original _generate_jobs with a new one that simply 305 # adds jobs to a list (and then doesn't return anything). 306 orig_generate_jobs = lint._generate_jobs 307 308 def fake_generate_jobs(*args, **kwargs): 309 jobs.extend([job[1] for job in orig_generate_jobs(*args, **kwargs)]) 310 return [] 311 312 monkeypatch.setattr(lint, "_generate_jobs", fake_generate_jobs) 313 314 linter_path = linters("support_files")[0] 315 lint.read(linter_path) 316 lint.root = filedir 317 318 # Modified support files only lint entire root if --outgoing or --workdir 319 # are used. 320 path = os.path.join(filedir, "foobar.js") 321 vcs_path = os.path.join(filedir, "foobar.py") 322 323 lint.mock_vcs([vcs_path]) 324 lint.roll(path) 325 actual_files = sorted(chain(*jobs)) 326 assert actual_files == [path] 327 328 expected_files = sorted(files) 329 330 jobs = [] 331 lint.roll(path, workdir=True) 332 actual_files = sorted(chain(*jobs)) 333 assert actual_files == expected_files 334 335 jobs = [] 336 lint.roll(path, outgoing=True) 337 actual_files = sorted(chain(*jobs)) 338 assert actual_files == expected_files 339 340 jobs = [] 341 lint.roll(path, rev='draft() and keyword("dummy revset expression")') 342 actual_files = sorted(chain(*jobs)) 343 assert actual_files == expected_files 344 345 # Lint config file is implicitly added as a support file 346 lint.mock_vcs([linter_path]) 347 jobs = [] 348 lint.roll(path, outgoing=True, workdir=True) 349 actual_files = sorted(chain(*jobs)) 350 assert actual_files == expected_files 351 352 # Avoid linting the entire root when `--fix` is passed. 353 lint.mock_vcs([vcs_path]) 354 lint.lintargs["fix"] = True 355 356 jobs = [] 357 lint.roll(path, outgoing=True) 358 actual_files = sorted(chain(*jobs)) 359 assert actual_files == sorted([path, vcs_path]), ( 360 "`--fix` with `--outgoing` on a `support-files` change should " 361 "avoid linting the entire root." 362 ) 363 364 jobs = [] 365 lint.roll(path, workdir=True) 366 actual_files = sorted(chain(*jobs)) 367 assert actual_files == sorted([path, vcs_path]), ( 368 "`--fix` with `--workdir` on a `support-files` change should " 369 "avoid linting the entire root." 370 ) 371 372 jobs = [] 373 lint.roll(path, rev='draft() and keyword("dummy revset expression")') 374 actual_files = sorted(chain(*jobs)) 375 assert actual_files == sorted([path, vcs_path]), ( 376 "`--fix` with `--rev` on a `support-files` change should " 377 "avoid linting the entire root." 378 ) 379 380 381 def test_setup(lint, linters, filedir, capfd): 382 with pytest.raises(NoValidLinter): 383 lint.setup() 384 385 lint.read(linters("setup", "setupfailed", "setupraised", "setupskipped")) 386 ret = lint.setup() 387 assert ret == 1 388 389 out, err = capfd.readouterr() 390 assert "oh no setup failed" in err 391 assert "ERROR: problem with lint setup, skipping" in err 392 assert lint.result.failed_setup == set(["SetupFailedLinter", "SetupRaisedLinter"]) 393 394 395 def test_setup_all_skipped(lint, linters, filedir, capfd): 396 lint.read(linters("setupskipped")) 397 ret = lint.setup() 398 assert ret == 1 399 400 out, err = capfd.readouterr() 401 assert "ERROR: all linters were skipped due to setup, nothing to do!" in err 402 assert lint.result.failed_setup == set() 403 404 405 if __name__ == "__main__": 406 mozunit.main()