Skip to content

Commit 528fb18

Browse files
joke1196sonartech
authored andcommitted
SONARPY-3206 SONARPY-3333: Enable parallel rule execution by default (#486)
GitOrigin-RevId: 8df8795e038f8ff02683a5d8c9cd21023c81171c
1 parent 64abbd9 commit 528fb18

5 files changed

Lines changed: 106 additions & 21 deletions

File tree

python-commons/src/main/java/org/sonar/plugins/python/PythonScanner.java

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ public class PythonScanner extends Scanner {
6262

6363
private static final Logger LOG = LoggerFactory.getLogger(PythonScanner.class);
6464
private static final Pattern DATABRICKS_MAGIC_COMMAND_PATTERN = Pattern.compile("^\\h*#\\h*(MAGIC|COMMAND).*");
65-
public static final String THREADS_PROPERTY_NAME = "sonar.python.analysis.threads";
6665
private static final String ARCHITECTURE_CALLBACK_LOCK_KEY = "architectureCallbackLock";
6766

6867
private final Supplier<PythonParser> parserSupplier;
@@ -117,7 +116,7 @@ protected void logStart(int numThreads) {
117116
protected void scanFile(PythonInputFile inputFile) throws IOException {
118117
var pythonFile = SonarQubePythonFile.create(inputFile);
119118
InputFile.Type fileType = inputFile.wrappedFile().type();
120-
PythonVisitorContext visitorContext = createVisitorContext(inputFile, pythonFile, fileType);
119+
PythonVisitorContext visitorContext = createVisitorContext(inputFile, pythonFile);
121120

122121
executeChecks(visitorContext, checks.sonarPythonChecks(), fileType, inputFile);
123122
executeOtherChecks(inputFile, visitorContext, fileType);
@@ -144,7 +143,7 @@ protected void scanFile(PythonInputFile inputFile) throws IOException {
144143
searchForDataBricks(visitorContext);
145144
}
146145

147-
private PythonVisitorContext createVisitorContext(PythonInputFile inputFile, PythonFile pythonFile, InputFile.Type fileType) throws IOException {
146+
private PythonVisitorContext createVisitorContext(PythonInputFile inputFile, PythonFile pythonFile) throws IOException {
148147
PythonVisitorContext visitorContext;
149148
try {
150149
AstNode astNode = parserSupplier.get().parse(inputFile.contents());
@@ -356,12 +355,6 @@ public boolean getFoundDatabricks() {
356355
return foundDatabricks.get();
357356
}
358357

359-
@Override
360-
protected int getNumberOfThreads(SensorContext context) {
361-
return context.config().getInt(THREADS_PROPERTY_NAME)
362-
.orElse(1);
363-
}
364-
365358
private void runLockedByRepository(String repositoryKey, Runnable runnable) {
366359
var repositoryLock = repositoryLocks.computeIfAbsent(repositoryKey, k -> new ReentrantLock());
367360
try {

python-commons/src/main/java/org/sonar/plugins/python/Scanner.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
public abstract class Scanner {
3333
private static final Logger LOG = LoggerFactory.getLogger(Scanner.class);
3434
private static final String FAIL_FAST_PROPERTY_NAME = "sonar.internal.analysis.failFast";
35+
public static final String THREADS_PROPERTY_NAME = "sonar.python.analysis.threads";
3536
protected final SensorContext context;
3637

3738
protected Scanner(SensorContext context) {
@@ -141,6 +142,16 @@ private static boolean isParseErrorOnTestFile(PythonInputFile file, Exception e)
141142
return e instanceof RecognitionException && file.wrappedFile().type() == InputFile.Type.TEST;
142143
}
143144

144-
protected abstract int getNumberOfThreads(SensorContext context);
145+
protected int getNumberOfThreads(SensorContext context) {
146+
int minNumOfThreads = 1;
147+
int maxNumOfThreads = 6;
148+
int availableProcessors = (int) Math.round(Runtime.getRuntime().availableProcessors() * 0.9);
149+
150+
// Disabling parallelization if threads property is not setup properly
151+
return context.config()
152+
.getInt(THREADS_PROPERTY_NAME)
153+
.map(threads -> threads < 1 ? 1 : threads)
154+
.orElse(Math.max(minNumOfThreads, Math.min(availableProcessors, maxNumOfThreads)));
155+
}
145156

146157
}

python-commons/src/main/java/org/sonar/plugins/python/indexer/PythonIndexer.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,6 @@ public boolean canBeFullyScannedWithoutParsing(PythonInputFile inputFile) {
161161
public abstract CacheContext cacheContext();
162162

163163
class GlobalSymbolsScanner extends Scanner {
164-
private static final String THREADS_PROPERTY_NAME = "sonar.python.symbols.threads";
165164
protected GlobalSymbolsScanner(SensorContext context) {
166165
super(context);
167166
}
@@ -186,12 +185,6 @@ protected void scanFile(PythonInputFile inputFile) throws IOException {
186185
}
187186
}
188187

189-
@Override
190-
protected int getNumberOfThreads(SensorContext context) {
191-
return context.config().getInt(THREADS_PROPERTY_NAME)
192-
.orElse(Math.max(2, Math.min((int) Math.round(Runtime.getRuntime().availableProcessors() * 0.9), 6)));
193-
}
194-
195188
@Override
196189
protected void processException(Exception e, PythonInputFile file) {
197190
LOG.debug("Unable to construct project-level symbol table for file: {}", file, e);
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.plugins.python;
18+
19+
import java.io.IOException;
20+
import java.util.Optional;
21+
import org.junit.jupiter.api.BeforeEach;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.params.ParameterizedTest;
24+
import org.junit.jupiter.params.provider.ValueSource;
25+
import org.sonar.api.batch.sensor.SensorContext;
26+
import org.sonar.api.config.Configuration;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
import static org.mockito.Mockito.mock;
30+
import static org.mockito.Mockito.when;
31+
32+
class ScannerTest {
33+
34+
private SensorContext context;
35+
private Configuration configuration;
36+
private Scanner scanner;
37+
38+
private class TestScanner extends Scanner {
39+
40+
protected TestScanner(SensorContext context) {
41+
super(context);
42+
}
43+
44+
@Override
45+
protected void logStart(int numThreads) {
46+
throw new UnsupportedOperationException("Unimplemented method 'logStart'");
47+
}
48+
49+
@Override
50+
protected String name() {
51+
throw new UnsupportedOperationException("Unimplemented method 'name'");
52+
}
53+
54+
@Override
55+
protected void scanFile(PythonInputFile file) throws IOException {
56+
throw new UnsupportedOperationException("Unimplemented method 'scanFile'");
57+
}
58+
59+
@Override
60+
protected void processException(Exception e, PythonInputFile file) {
61+
throw new UnsupportedOperationException("Unimplemented method 'processException'");
62+
}
63+
64+
}
65+
66+
@BeforeEach
67+
void setUp() {
68+
context = mock(SensorContext.class);
69+
configuration = mock(Configuration.class);
70+
when(context.config()).thenReturn(configuration);
71+
scanner = new TestScanner(context);
72+
}
73+
74+
@Test
75+
void testGetNumberOfThreads_whenPropertySetToValidValue_shouldReturnConfiguredValue() {
76+
when(configuration.getInt("sonar.python.analysis.threads")).thenReturn(Optional.of(4));
77+
78+
int numberOfThreads = scanner.getNumberOfThreads(context);
79+
assertThat(numberOfThreads).isEqualTo(4);
80+
}
81+
82+
@ParameterizedTest
83+
@ValueSource(ints = {-1, 0, -12})
84+
void testGetNumberOfThreads_whenPropertySetIncorrectly_shouldReturnOne(int threads) {
85+
when(configuration.getInt("sonar.python.analysis.threads")).thenReturn(Optional.of(threads));
86+
87+
int numberOfThreads = scanner.getNumberOfThreads(context);
88+
assertThat(numberOfThreads).isEqualTo(1);
89+
}
90+
}

python-commons/src/test/java/org/sonar/plugins/python/indexer/SonarQubePythonIndexerTest.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ void init() throws IOException {
8383
Path workDir = Files.createTempDirectory("workDir");
8484
context.fileSystem().setWorkDir(workDir);
8585
context.settings().setProperty("sonar.python.skipUnchanged", true);
86-
context.settings().setProperty("sonar.python.symbols.threads", 2);
86+
context.settings().setProperty("sonar.python.analysis.threads", 2);
8787

8888
writeCache = new TestWriteCache();
8989
readCache = new TestReadCache();
@@ -480,16 +480,14 @@ void test_notebook_should_not_be_in_project_level_symbol_table() {
480480
@Test
481481
void test_sensor_single_thread() throws IOException {
482482
var contextSingleThread = SensorContextTester.create(baseDir);
483-
contextSingleThread.settings().setProperty("sonar.python.symbols.threads", 1);
483+
contextSingleThread.settings().setProperty("sonar.python.analysis.threads", 1);
484484
contextSingleThread.fileSystem().setWorkDir(Files.createTempDirectory("workDir"));
485485

486486
var inputFiles = List.of(createInputFile(baseDir, "main.py", InputFile.Status.SAME, InputFile.Type.MAIN));
487487
var indexer = new SonarQubePythonIndexer(inputFiles, cacheContext, contextSingleThread, new ProjectConfigurationBuilder());
488488
indexer.buildOnce(contextSingleThread);
489489

490490
assertThat(indexer.projectLevelSymbolTable().getSymbolsFromModule("main")).isNotEmpty();
491-
492-
493491
}
494492

495493
private byte[] importsAsByteArray(List<String> mod) {

0 commit comments

Comments
 (0)