diff --git a/integration_tests/test_program.py b/integration_tests/test_program.py index 038b3193..af6383c2 100644 --- a/integration_tests/test_program.py +++ b/integration_tests/test_program.py @@ -14,7 +14,7 @@ def test_codemods_include_exclude_conflict(self): completed_process = subprocess.run( [ "codemodder", - "tests/samples/", + "some/path", "--output", "doesntmatter.txt", "--codemod-exclude", diff --git a/tests/test_cli.py b/tests/test_cli.py index 58380a94..a8937efb 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -30,7 +30,7 @@ def test_no_args(self, error_logger, mocker): [ ["--help"], [ - "tests/samples/", + "some/path", "--output", "here.txt", "--codemod-include=url-sandbox", @@ -51,7 +51,7 @@ def test_help_is_printed(self, mock_print_help, cli_args): [ ["--version"], [ - "tests/samples/", + "some/path", "--output", "here.txt", "--codemod-include=url-sandbox", @@ -72,7 +72,7 @@ def test_version_is_printed(self, mock_print_msg, cli_args): [ ["--list"], [ - "tests/samples/", + "some/path", "--output", "here.txt", "--list", @@ -112,7 +112,7 @@ def test_bad_output_format(self, error_logger): with pytest.raises(SystemExit) as err: parse_args( [ - "tests/samples/", + "some/path", "--output", "here.txt", "--output-format", @@ -132,7 +132,7 @@ def test_bad_option(self, error_logger): with pytest.raises(SystemExit) as err: parse_args( [ - "tests/samples/", + "some/path", "--output", "here.txt", "--codemod=url-sandbox", @@ -152,7 +152,7 @@ def test_bad_option(self, error_logger): def test_codemod_name_or_id(self, codemod): parse_args( [ - "tests/samples/", + "some/path", "--output", "here.txt", f"--codemod-include={codemod}", diff --git a/tests/test_codemodder.py b/tests/test_codemodder.py index ff910717..2cef0bbb 100644 --- a/tests/test_codemodder.py +++ b/tests/test_codemodder.py @@ -6,7 +6,6 @@ from codemodder.diff import create_diff_from_tree from codemodder.registry import load_registered_codemods from codemodder.result import ResultSet -from codemodder.semgrep import run as semgrep_run @pytest.fixture(autouse=True, scope="module") @@ -21,19 +20,44 @@ def disable_codemod_apply(mocker, request): run all of codemodder but we most often don't need to actually apply codemods. """ # Skip mocking only for specific tests that need to apply codemods. - if request.function.__name__ == "test_cst_parsing_fails": + if request.function.__name__ in ( + "test_cst_parsing_fails", + "test_dry_run", + "test_run_codemod_name_or_id", + ): return mocker.patch("codemodder.codemods.base_codemod.BaseCodemod.apply") +@pytest.fixture(scope="function") +def dir_structure(tmp_path_factory): + code_dir = tmp_path_factory.mktemp("code") + (code_dir / "test_request.py").write_text( + """ + from test_sources import untrusted_data + import requests + + url = untrusted_data() + requests.get(url) + var = "hello" + """ + ) + (code_dir / "test_random.py").write_text( + """ + import random + def func(foo=[]): + return random.random() + """ + ) + codetf = code_dir / "result.codetf" + assert not codetf.exists() + return code_dir, codetf + + class TestRun: @mock.patch("libcst.parse_module") - def test_no_files_matched(self, mock_parse, tmpdir): - codetf = tmpdir / "result.codetf" - code_dir = tmpdir.mkdir("code") - code_dir.join("code.py").write("# anything") - assert not codetf.exists() - + def test_no_files_matched(self, mock_parse, dir_structure): + code_dir, codetf = dir_structure args = [ str(code_dir), "--output", @@ -50,15 +74,12 @@ def test_no_files_matched(self, mock_parse, tmpdir): @mock.patch("libcst.parse_module", side_effect=Exception) @mock.patch("codemodder.codetf.CodeTF.build") - def test_cst_parsing_fails(self, build_report, mock_parse, tmpdir): - code_dir = tmpdir.mkdir("code") - code_file = code_dir.join("test_request.py") - code_file.write("# anything") - + def test_cst_parsing_fails(self, build_report, mock_parse, dir_structure): + code_dir, codetf = dir_structure args = [ str(code_dir), "--output", - str(tmpdir / "result.codetf"), + str(codetf), "--codemod-include", "fix-assert-tuple", "--path-include", @@ -80,17 +101,21 @@ def test_cst_parsing_fails(self, build_report, mock_parse, tmpdir): assert requests_report.changeset == [] assert len(requests_report.failedFiles) == 1 assert sorted(requests_report.failedFiles) == [ - str(code_file), + str(code_dir / "test_request.py"), ] build_report.return_value.write_report.assert_called_once() @mock.patch("codemodder.codemods.libcst_transformer.update_code") - @mock.patch("codemodder.codemods.semgrep.semgrep_run", side_effect=semgrep_run) - def test_dry_run(self, _, mock_update_code, tmpdir): - codetf = tmpdir / "result.codetf" + @mock.patch( + "codemodder.codemods.libcst_transformer.LibcstTransformerPipeline.apply", + new_callable=mock.PropertyMock, + ) + @mock.patch("codemodder.context.CodemodExecutionContext.compile_results") + def test_dry_run(self, _, transform_apply, mock_update_code, dir_structure): + code_dir, codetf = dir_structure args = [ - "tests/samples/", + str(code_dir), "--output", str(codetf), "--dry-run", @@ -103,16 +128,17 @@ def test_dry_run(self, _, mock_update_code, tmpdir): res = run(args) assert res == 0 assert codetf.exists() - + transform_apply.assert_called() mock_update_code.assert_not_called() @pytest.mark.parametrize("dry_run", [True, False]) @mock.patch("codemodder.codetf.CodeTF.build") - def test_reporting(self, mock_reporting, dry_run): + def test_reporting(self, mock_reporting, dry_run, dir_structure): + code_dir, codetf = dir_structure args = [ - "tests/samples/", + str(code_dir), "--output", - "here.txt", + str(codetf), # Make this test faster by restricting the number of codemods "--codemod-include=use-generator,use-defusedxml,use-walrus-if", ] @@ -132,21 +158,33 @@ def test_reporting(self, mock_reporting, dry_run): mock_reporting.return_value.write_report.assert_called_once() @pytest.mark.parametrize("codemod", ["secure-random", "pixee:python/secure-random"]) + @mock.patch( + "codemodder.codemods.libcst_transformer.LibcstTransformerPipeline.apply", + new_callable=mock.PropertyMock, + ) @mock.patch("codemodder.context.CodemodExecutionContext.compile_results") @mock.patch("codemodder.codetf.CodeTF.write_report") - def test_run_codemod_name_or_id(self, write_report, mock_compile_results, codemod): + def test_run_codemod_name_or_id( + self, + write_report, + mock_compile_results, + transform_apply, + codemod, + dir_structure, + ): del write_report + code_dir, codetf = dir_structure args = [ - "tests/samples/", + str(code_dir), "--output", - "here.txt", + str(codetf), f"--codemod-include={codemod}", ] exit_code = run(args) assert exit_code == 0 - # todo: if no codemods run do we still compile results? mock_compile_results.assert_called() + transform_apply.assert_called() class TestCodemodIncludeExclude: @@ -154,12 +192,15 @@ class TestCodemodIncludeExclude: @mock.patch("codemodder.registry.logger.warning") @mock.patch("codemodder.codemodder.logger.info") @mock.patch("codemodder.codetf.CodeTF.write_report") - def test_codemod_include_no_match(self, write_report, info_logger, warning_logger): + def test_codemod_include_no_match( + self, write_report, info_logger, warning_logger, dir_structure + ): bad_codemod = "doesntexist" + code_dir, codetf = dir_structure args = [ - "tests/samples/", + str(code_dir), "--output", - "here.txt", + str(codetf), f"--codemod-include={bad_codemod}", ] run(args) @@ -177,14 +218,15 @@ def test_codemod_include_no_match(self, write_report, info_logger, warning_logge @mock.patch("codemodder.codemodder.logger.info") @mock.patch("codemodder.codetf.CodeTF.write_report") def test_codemod_include_some_match( - self, write_report, info_logger, warning_logger + self, write_report, info_logger, warning_logger, dir_structure ): bad_codemod = "doesntexist" good_codemod = "secure-random" + code_dir, codetf = dir_structure args = [ - "tests/samples/", + str(code_dir), "--output", - "here.txt", + str(codetf), f"--codemod-include={bad_codemod},{good_codemod}", ] run(args) @@ -199,14 +241,15 @@ def test_codemod_include_some_match( @mock.patch("codemodder.codemodder.logger.info") @mock.patch("codemodder.codetf.CodeTF.write_report") def test_codemod_exclude_some_match( - self, write_report, info_logger, warning_logger + self, write_report, info_logger, warning_logger, dir_structure ): bad_codemod = "doesntexist" good_codemod = "secure-random" + code_dir, codetf = dir_structure args = [ - "tests/samples/", + str(code_dir), "--output", - "here.txt", + str(codetf), f"--codemod-exclude={bad_codemod},{good_codemod}", ] run(args) @@ -229,13 +272,14 @@ def test_codemod_exclude_some_match( @mock.patch("codemodder.codetf.CodeTF.write_report") @mock.patch("codemodder.codemods.base_codemod.BaseCodemod.apply") def test_codemod_exclude_no_match( - self, apply, write_report, info_logger, warning_logger + self, apply, write_report, info_logger, warning_logger, dir_structure ): bad_codemod = "doesntexist" + code_dir, codetf = dir_structure args = [ - "tests/samples/", + str(code_dir), "--output", - "here.txt", + str(codetf), f"--codemod-exclude={bad_codemod}", ] @@ -248,14 +292,14 @@ def test_codemod_exclude_no_match( ) @mock.patch("codemodder.codemods.semgrep.semgrep_run") - def test_exclude_all_registered_codemods(self, mock_semgrep_run, tmpdir): - codetf = tmpdir / "result.codetf" + def test_exclude_all_registered_codemods(self, mock_semgrep_run, dir_structure): + code_dir, codetf = dir_structure assert not codetf.exists() registry = load_registered_codemods() names = ",".join(registry.names) args = [ - "tests/samples/", + str(code_dir), "--output", str(codetf), f"--codemod-exclude={names}", @@ -269,10 +313,11 @@ def test_exclude_all_registered_codemods(self, mock_semgrep_run, tmpdir): class TestExitCode: @mock.patch("codemodder.codetf.CodeTF.write_report") - def test_success_0(self, mock_report): + def test_no_changes_success_0(self, mock_report, dir_structure): del mock_report + code_dir, codetf = dir_structure args = [ - "tests/samples/", + str(code_dir), "--output", "here.txt", "--codemod-include=url-sandbox", @@ -300,7 +345,7 @@ def test_bad_project_dir_1(self, mock_report): def test_conflicting_include_exclude(self, mock_report): del mock_report args = [ - "tests/samples/", + "anything", "--output", "here.txt", "--codemod-exclude",