diff --git a/src/fixit/cli.py b/src/fixit/cli.py index 84bd344a..dbe43167 100644 --- a/src/fixit/cli.py +++ b/src/fixit/cli.py @@ -218,27 +218,44 @@ def fix( visited.add(result.path) # for STDIN, we need STDOUT to equal the fixed content, so # move everything else to STDERR - if print_result( + is_dirty = print_result( result, show_diff=interactive or diff, stderr=is_stdin, output_format=config.output_format, output_template=config.output_template, - ): + ) + if is_dirty: dirty.add(result.path) - if autofix and result.violation and result.violation.autofixable: - autofixes += 1 - fixed += 1 - if interactive and result.violation and result.violation.autofixable: + + if result.error: + exit_code |= 2 + continue + + violation = result.violation + if not violation: + continue + + violation_fixed = False + if violation.autofixable: autofixes += 1 - answer = click.prompt( - "Apply autofix?", default="y", type=click.Choice("ynq", False) - ) - if answer == "y": - generator.respond(True) # noqa: B038 + if autofix: fixed += 1 - elif answer == "q": - break + violation_fixed = True + elif interactive: + answer = click.prompt( + "Apply autofix?", default="y", type=click.Choice("ynq", False) + ) + if answer == "y": + generator.respond(True) # noqa: B038 + fixed += 1 + violation_fixed = True + elif answer == "q": + exit_code |= 1 + break + + if not violation_fixed: + exit_code |= 1 splash(visited, dirty, autofixes, fixed) ctx.exit(exit_code) diff --git a/src/fixit/tests/smoke.py b/src/fixit/tests/smoke.py index 2c4afa34..82ae76c5 100644 --- a/src/fixit/tests/smoke.py +++ b/src/fixit/tests/smoke.py @@ -209,6 +209,53 @@ def test_directory_with_violations_and_errors(self) -> None: self.assertIn("broken.py: EXCEPTION: Syntax Error @ 1:", result.output) self.assertEqual(result.exit_code, 3) + def test_fix_exit_codes(self) -> None: + with self.subTest("unfixable violation"): + with TemporaryDirectory() as td: + tdp = Path(td).resolve() + dirty = tdp / "dirty.py" + dirty.write_text( + "try:\n pass\nexcept ValueError or KeyError:\n pass\n" + ) + + result = self.runner.invoke( + main, ["fix", "--automatic", dirty.as_posix()], catch_exceptions=False + ) + + self.assertIn("AvoidOrInExcept", result.output) + self.assertEqual(result.exit_code, 1) + + with self.subTest("syntax error"): + with TemporaryDirectory() as td: + tdp = Path(td).resolve() + broken = tdp / "broken.py" + broken.write_text("print)\n") + + result = self.runner.invoke( + main, + ["fix", "--automatic", broken.as_posix()], + catch_exceptions=False, + ) + + self.assertIn("EXCEPTION: Syntax Error", result.output) + self.assertEqual(result.exit_code, 2) + + with self.subTest("violations and errors together"): + with TemporaryDirectory() as td: + tdp = Path(td).resolve() + (tdp / "dirty.py").write_text( + "try:\n pass\nexcept ValueError or KeyError:\n pass\n" + ) + (tdp / "broken.py").write_text("print)\n") + + result = self.runner.invoke( + main, ["fix", "--automatic", td], catch_exceptions=False + ) + + self.assertIn("AvoidOrInExcept", result.output) + self.assertIn("EXCEPTION: Syntax Error", result.output) + self.assertEqual(result.exit_code, 3) + def test_directory_with_autofixes(self) -> None: with TemporaryDirectory() as td: tdp = Path(td).resolve()