Skip to content

Commit 319115d

Browse files
RC-356 Extract Jetty incremental analysis into plugin integration tests
1 parent d7866af commit 319115d

3 files changed

Lines changed: 190 additions & 174 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ Note for internal contributors: in order to also execute the tests that depend o
137137

138138
mvn clean install -Pit-plugin
139139

140+
`IncrementalAnalysisTest` verifies server-side cache reuse and analysis time for a main branch and two pull requests using the Eclipse Jetty projects under `its/sources`. It requires Enterprise Edition and runs with the Sonar way profile, independently of the issue-baseline comparisons in the ruling suite.
141+
140142
#### Ruling Test
141143

142144
The "Ruling Test" is an integration test suite that launches the analysis of a large code base, saves the issues created by the plugin in report files, and then compares those results to the set of expected issues (stored as JSON files).
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package com.sonar.it.java;
2+
3+
import com.sonar.it.java.suite.JavaTestSuite;
4+
import com.sonar.it.java.suite.TestUtils;
5+
import com.sonar.orchestrator.build.BuildResult;
6+
import com.sonar.orchestrator.build.MavenBuild;
7+
import com.sonar.orchestrator.container.Edition;
8+
import com.sonar.orchestrator.junit4.OrchestratorRule;
9+
import java.io.File;
10+
import java.io.IOException;
11+
import java.nio.file.Path;
12+
import java.util.List;
13+
import java.util.regex.Matcher;
14+
import java.util.regex.Pattern;
15+
import java.util.stream.Collectors;
16+
import org.junit.ClassRule;
17+
import org.junit.Test;
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
23+
public class IncrementalAnalysisTest {
24+
25+
private static final String PROJECT_KEY = "org.sonarsource.it.projects:incremental-eclipse-jetty";
26+
private static final String MAIN_BRANCH = "eclipse-jetty-main";
27+
private static final List<String> MODULES = List.of(
28+
"jetty-http", "jetty-io", "jetty-jmx", "jetty-server", "jetty-slf4j-impl", "jetty-util", "jetty-util-ajax", "jetty-xml", "tests/jetty-http-tools");
29+
private static final String CAN_SKIP_UNCHANGED_FILES_LOG =
30+
"The Java analyzer is running in a context where unchanged files can be skipped.";
31+
private static final Pattern FILES_LEVERAGED_FROM_CACHE = Pattern.compile(
32+
"The Java analyzer was able to leverage cached data from previous analyses for (\\d+) out of (\\d+) files\\.");
33+
private static final Logger LOG = LoggerFactory.getLogger(IncrementalAnalysisTest.class);
34+
35+
@ClassRule
36+
public static final OrchestratorRule ORCHESTRATOR = createOrchestrator();
37+
38+
private static OrchestratorRule createOrchestrator() {
39+
if (Boolean.getBoolean("communityEditionTestsOnly")) {
40+
return null;
41+
}
42+
return OrchestratorRule.builderEnv()
43+
.useDefaultAdminCredentialsForBuilds(true)
44+
.setSonarVersion(System.getProperty("sonar.runtimeVersion", "LATEST_RELEASE"))
45+
.setEdition(Edition.ENTERPRISE_LW)
46+
.activateLicense()
47+
.addPlugin(JavaTestSuite.JAVA_PLUGIN_LOCATION)
48+
.build();
49+
}
50+
51+
@Test
52+
public void eclipse_jetty_incremental() throws IOException {
53+
if (Boolean.getBoolean("communityEditionTestsOnly")) {
54+
return;
55+
}
56+
57+
TestUtils.provisionProject(ORCHESTRATOR, PROJECT_KEY, "incremental-eclipse-jetty", "java", "Sonar way");
58+
AnalysisResult mainBranchResult = analyze(jettyBuild("eclipse-jetty")
59+
.setProperty("sonar.branch.name", MAIN_BRANCH));
60+
AnalysisResult largePrResult = analyze(pullRequestBuild("eclipse-jetty-similar-to-main", "eclipse-jetty-same-issues-as-main")
61+
.setProperty("sonar.java.ignoreUnnamedModuleForSplitPackage", "true"));
62+
AnalysisResult smallPrResult = analyze(pullRequestBuild("eclipse-jetty-similar-to-main-small", "eclipse-jetty-same-issues-as-main-small"));
63+
64+
CacheUsage mainBranchUsage = cacheUsage(mainBranchResult.buildResult());
65+
logCacheUsage("Main branch", mainBranchUsage);
66+
assertThat(mainBranchUsage.total())
67+
.as("The main branch analysis should report cache usage")
68+
.isPositive();
69+
assertThat(mainBranchUsage.fromCache())
70+
.as("The first analysis of the main branch has no previous analysis to leverage")
71+
.isZero();
72+
assertCacheWasLeveraged(largePrResult.buildResult(), "Large PR", 0.5);
73+
assertCacheWasLeveraged(smallPrResult.buildResult(), "Small PR", 0.9);
74+
75+
assertThat(largePrResult.durationMillis())
76+
.as("Large PR incremental analysis should not be significantly slower than main branch analysis")
77+
.isLessThan((long) (mainBranchResult.durationMillis() * 1.25));
78+
assertThat(smallPrResult.durationMillis())
79+
.as("Small PR incremental analysis should be faster than main branch analysis")
80+
.isLessThan((long) (mainBranchResult.durationMillis() * 0.90))
81+
.as("Small PR incremental analysis should be faster than large PR incremental analysis")
82+
.isLessThan((long) (largePrResult.durationMillis() * 0.95));
83+
}
84+
85+
private static MavenBuild pullRequestBuild(String sourceProject, String branch) throws IOException {
86+
return jettyBuild(sourceProject)
87+
.setProperties(
88+
"sonar.pullrequest.key", branch,
89+
"sonar.pullrequest.branch", branch,
90+
"sonar.pullrequest.base", MAIN_BRANCH);
91+
}
92+
93+
private static MavenBuild jettyBuild(String sourceProject) throws IOException {
94+
Path projectDir = new File(TestUtils.homeDir().getParentFile(), "sources/" + sourceProject).getCanonicalFile().toPath();
95+
String binaries = MODULES.stream()
96+
.map(module -> projectDir.resolve(module).resolve("target/classes").toString())
97+
.collect(Collectors.joining(","));
98+
return TestUtils.createMavenBuild()
99+
.setPom(projectDir.resolve("pom.xml").toFile())
100+
.setCleanPackageSonarGoals()
101+
.addArgument("-DskipTests")
102+
.addArgument("-Dpmd.skip=true")
103+
.addArgument("-Dcheckstyle.skip=true")
104+
.setProperty("sonar.projectKey", PROJECT_KEY)
105+
.setProperty("sonar.java.binaries", binaries)
106+
.setProperty("sonar.exclusions", "jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java," +
107+
"jetty-osgi/jetty-osgi-boot/src/main/java/org/eclipse/jetty/osgi/boot/internal/serverfactory/ServerInstanceWrapper.java")
108+
.setProperty("sonar.scm.provider", "git")
109+
.setProperty("sonar.scm.disabled", "false")
110+
.setProperty("sonar.java.skipUnchanged", "true")
111+
.setProperty("sonar.analysisCache.enabled", "true")
112+
.setProperty("sonar.cpd.exclusions", "**/*")
113+
.setProperty("sonar.import_unknown_files", "true")
114+
.setProperty("sonar.skipPackageDesign", "true")
115+
.setProperty("sonar.internal.analysis.failFast", "true");
116+
}
117+
118+
private static AnalysisResult analyze(MavenBuild build) {
119+
long start = System.nanoTime();
120+
BuildResult buildResult = ORCHESTRATOR.executeBuild(build);
121+
return new AnalysisResult(buildResult, (System.nanoTime() - start) / 1_000_000);
122+
}
123+
124+
private static void assertCacheWasLeveraged(BuildResult result, String label, double filesCachedRatio) {
125+
CacheUsage usage = cacheUsage(result);
126+
logCacheUsage(label, usage);
127+
assertThat(result.getLogs())
128+
.as("%s should be analyzed in a context where unchanged files can be skipped", label)
129+
.contains(CAN_SKIP_UNCHANGED_FILES_LOG);
130+
assertThat(usage.total())
131+
.as("%s should report how many files the analyzer leveraged from the cache", label)
132+
.isPositive();
133+
assertThat(usage.ratio())
134+
.as("%s should leverage the cache for at least %d%% of files, but only %d out of %d did",
135+
label, Math.round(filesCachedRatio * 100), usage.fromCache(), usage.total())
136+
.isGreaterThanOrEqualTo(filesCachedRatio);
137+
}
138+
139+
private static void logCacheUsage(String label, CacheUsage usage) {
140+
LOG.info("[incremental analysis] {}: the Java analyzer leveraged cached data for {} out of {} files ({}%).",
141+
label, usage.fromCache(), usage.total(), Math.round(usage.ratio() * 100));
142+
}
143+
144+
/**
145+
* Sums the per-module cache counts so that assertions cover the whole multi-module project.
146+
*/
147+
private static CacheUsage cacheUsage(BuildResult result) {
148+
Matcher matcher = FILES_LEVERAGED_FROM_CACHE.matcher(result.getLogs());
149+
int fromCache = 0;
150+
int total = 0;
151+
while (matcher.find()) {
152+
fromCache += Integer.parseInt(matcher.group(1));
153+
total += Integer.parseInt(matcher.group(2));
154+
}
155+
return new CacheUsage(fromCache, total);
156+
}
157+
158+
private record AnalysisResult(BuildResult buildResult, long durationMillis) {
159+
}
160+
161+
private record CacheUsage(int fromCache, int total) {
162+
double ratio() {
163+
return total == 0 ? 0d : (double) fromCache / total;
164+
}
165+
}
166+
}

0 commit comments

Comments
 (0)