diff --git a/CLI_ARGS.md b/CLI_ARGS.md index 09dd65e7..a111fc20 100644 --- a/CLI_ARGS.md +++ b/CLI_ARGS.md @@ -64,6 +64,8 @@ | `--sonar-pullrequest-base`, `-Dsonar.pullrequest.base` | Base branch of the pull request being analyzed | | `--sonar-pullrequest-branch`, `-Dsonar.pullrequest.branch` | Branch of the pull request being analyzed | | `--sonar-pullrequest-key`, `-Dsonar.pullrequest.key` | Key of the pull request being analyzed | +| `--sonar-python-analysis-parallel`, `--no-sonar-python-analysis-parallel`, `--analysis-in-parallel`, `--no-analysis-in-parallel`, `-Dsonar.python.analysis.parallel` | When set to False the analysis will be single threaded | +| `--sonar-python-analysis-threads`, `--nr-analysis-threads`, `-Dsonar.python.analysis.threads` | Set the number of threads to use during analysis. This property is ignored if --sonar-python-analysis-parallel is set to False | | `--sonar-python-skip-unchanged`, `--no-sonar-python-skip-unchanged` | Override the SonarQube configuration of skipping or not the analysis of unchanged Python files | | `--sonar-qualitygate-timeout`, `-Dsonar.qualitygate.timeout` | The number of seconds that the scanner should wait for a report to be processed | | `--sonar-qualitygate-wait`, `--no-sonar-qualitygate-wait` | Forces the analysis step to poll the server instance and wait for the Quality Gate status | diff --git a/src/pysonar_scanner/configuration/cli.py b/src/pysonar_scanner/configuration/cli.py index 75f4bbe9..e28b3dde 100644 --- a/src/pysonar_scanner/configuration/cli.py +++ b/src/pysonar_scanner/configuration/cli.py @@ -170,6 +170,22 @@ def __create_parser(cls): "--sonar-modules", "-Dsonar.modules", type=str, help="Comma-delimited list of modules to analyze" ) + parser.add_argument( + "--sonar-python-analysis-parallel", + "--analysis-in-parallel", + "-Dsonar.python.analysis.parallel", + type=bool, + action=argparse.BooleanOptionalAction, + help="When set to False the analysis will be single threaded", + ) + parser.add_argument( + "--sonar-python-analysis-threads", + "--nr-analysis-threads", + "-Dsonar.python.analysis.threads", + type=int, + help="Set the number of threads to use during analysis. This property is ignored if --sonar-python-analysis-parallel is set to False", + ) + server_connection_group = parser.add_argument_group("SonarQube Connection") server_connection_group.add_argument( "--sonar-host-url", diff --git a/src/pysonar_scanner/configuration/properties.py b/src/pysonar_scanner/configuration/properties.py index c185e1f2..0b3b9480 100644 --- a/src/pysonar_scanner/configuration/properties.py +++ b/src/pysonar_scanner/configuration/properties.py @@ -94,6 +94,8 @@ SONAR_WORKING_DIRECTORY: Key = "sonar.working.directory" SONAR_SCM_FORCE_RELOAD_ALL: Key = "sonar.scm.forceReloadAll" SONAR_MODULES: Key = "sonar.modules" +SONAR_PYTHON_ANALYSIS_PARALLEL: Key = "sonar.python.analysis.parallel" +SONAR_PYTHON_ANALYSIS_THREADS: Key = "sonar.python.analysis.threads" SONAR_PYTHON_XUNIT_REPORT_PATH: Key = "sonar.python.xunit.reportPath" SONAR_PYTHON_XUNIT_SKIP_DETAILS: Key = "sonar.python.xunit.skipDetails" SONAR_PYTHON_MYPY_REPORT_PATHS: Key = "sonar.python.mypy.reportPaths" @@ -541,6 +543,16 @@ def env_variable_name(self) -> str: default_value=None, cli_getter=None, deprecation_message="SONAR_SCANNER_OPTS is deprecated, please use SONAR_SCANNER_JAVA_OPTS instead." - ) + ), + Property( + name=SONAR_PYTHON_ANALYSIS_PARALLEL, + default_value=None, + cli_getter=lambda args: args.sonar_python_analysis_parallel + ), + Property( + name=SONAR_PYTHON_ANALYSIS_THREADS, + default_value=None, + cli_getter=lambda args: args.sonar_python_analysis_threads + ), ] # fmt: on diff --git a/tests/unit/test_configuration_cli.py b/tests/unit/test_configuration_cli.py index b9e3ea20..e2e02466 100644 --- a/tests/unit/test_configuration_cli.py +++ b/tests/unit/test_configuration_cli.py @@ -26,6 +26,8 @@ from pysonar_scanner.configuration.properties import ( SONAR_HOST_URL, SONAR_ORGANIZATION, + SONAR_PYTHON_ANALYSIS_PARALLEL, + SONAR_PYTHON_ANALYSIS_THREADS, SONAR_PYTHON_BANDIT_REPORT_PATHS, SONAR_PYTHON_FLAKE8_REPORT_PATHS, SONAR_PYTHON_MYPY_REPORT_PATHS, @@ -159,6 +161,8 @@ SONAR_PYTHON_COVERAGE_REPORT_PATHS: "path/to/coverage1,path/to/coverage2", SONAR_COVERAGE_EXCLUSIONS: "*/.local/*,/usr/*,utils/tirefire.py", SONAR_PYTHON_SKIP_UNCHANGED: True, + SONAR_PYTHON_ANALYSIS_PARALLEL: True, + SONAR_PYTHON_ANALYSIS_THREADS: 2, SONAR_PYTHON_XUNIT_REPORT_PATH: "path/to/xunit/report", SONAR_PYTHON_XUNIT_SKIP_DETAILS: True, SONAR_PYTHON_MYPY_REPORT_PATHS: "path/to/mypy/reports", @@ -207,6 +211,36 @@ def test_alternative_cli_args(self): } self.assertDictEqual(configuration, expected_configuration) + def test_alternative_analysis_threads_cli_args(self): + base_args = ["myscript.py", "-t", "myToken", "--sonar-project-key", "myProjectKey"] + report_args = ["--nr-analysis-threads", "3"] + + expected_configuration = { + SONAR_TOKEN: "myToken", + SONAR_PROJECT_KEY: "myProjectKey", + SONAR_PYTHON_ANALYSIS_THREADS: 3, + } + + with patch("sys.argv", base_args + report_args), patch("sys.stderr", new=StringIO()): + configuration = CliConfigurationLoader.load() + self.assertDictEqual(configuration, expected_configuration) + + def test_alternative_analysis_single_threaded_cli_args(self): + base_args = ["myscript.py", "-t", "myToken", "--sonar-project-key", "myProjectKey"] + report_args = [ + "--no-analysis-in-parallel", + ] + + expected_configuration = { + SONAR_TOKEN: "myToken", + SONAR_PROJECT_KEY: "myProjectKey", + SONAR_PYTHON_ANALYSIS_PARALLEL: False, + } + + with patch("sys.argv", base_args + report_args), patch("sys.stderr", new=StringIO()): + configuration = CliConfigurationLoader.load() + self.assertDictEqual(configuration, expected_configuration) + def test_alternative_report_cli_args(self): base_args = ["myscript.py", "-t", "myToken", "--sonar-project-key", "myProjectKey"] report_args = [ @@ -404,6 +438,9 @@ def test_impossible_os_choice(self): "module1,module2", "--sonar-scanner-java-heap-size", "8000Mb", + "--analysis-in-parallel", + "--nr-analysis-threads", + "2", ], ) def test_all_cli_args(self): @@ -474,6 +511,8 @@ def test_all_cli_args(self): "-Dsonar.python.pylint.reportPath=path/to/pylint/report", "-Dsonar.python.coverage.reportPaths=path/to/coverage1,path/to/coverage2", "-Dsonar.coverage.exclusions=*/.local/*,/usr/*,utils/tirefire.py", + "-Dsonar.python.analysis.parallel", + "-Dsonar.python.analysis.threads=2", "-Dsonar.python.skipUnchanged=true", "-Dsonar.python.xunit.reportPath=path/to/xunit/report", "-Dsonar.python.xunit.skipDetails=true",