Skip to content

Commit d1e508a

Browse files
committed
Add performance test for RequiredPluginsClasspathContainer
This test verifies that classpath computation finishes quickly even when secondary dependencies form a cyclic re-export graph. Previously, this scenario caused an infinite loop in 'findExportedPackages', leading to a build hang. The new test 'testCyclicReexportInSecondaryDependencies' constructs such a cycle and asserts that computation completes within a reasonable time.
1 parent 5779ee1 commit d1e508a

3 files changed

Lines changed: 185 additions & 0 deletions

File tree

ui/org.eclipse.pde.ui.tests/META-INF/MANIFEST.MF

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ Import-Package: jakarta.annotation;version="[2.1.0,3.0.0)",
5959
org.eclipse.pde.internal.build,
6060
org.hamcrest,
6161
org.junit,
62+
org.junit.jupiter.api;version="[5.8.1,6.0.0)",
6263
org.junit.jupiter.api.function;version="[5.8.1,6.0.0)",
6364
org.junit.jupiter.migrationsupport;version="[5.13.0,6.0.0)",
6465
org.junit.platform.suite.api;version="[1.13.0,2.0.0)",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package org.eclipse.pde.core.tests.internal.classpath;
2+
3+
import java.io.File;
4+
import java.io.FileOutputStream;
5+
import java.io.IOException;
6+
import java.nio.file.Files;
7+
import java.util.jar.Attributes;
8+
import java.util.jar.JarOutputStream;
9+
import java.util.jar.Manifest;
10+
import java.util.zip.ZipEntry;
11+
12+
import org.eclipse.core.resources.IFile;
13+
import org.eclipse.core.resources.IProject;
14+
import org.eclipse.core.resources.IWorkspaceDescription;
15+
import org.eclipse.core.resources.IncrementalProjectBuilder;
16+
import org.eclipse.core.resources.ResourcesPlugin;
17+
import org.eclipse.core.runtime.NullProgressMonitor;
18+
import org.eclipse.jdt.core.JavaCore;
19+
import org.eclipse.pde.core.build.IBuild;
20+
import org.eclipse.pde.core.build.IBuildEntry;
21+
import org.eclipse.pde.core.plugin.IPluginModelBase;
22+
import org.eclipse.pde.core.plugin.PluginRegistry;
23+
import org.eclipse.pde.core.project.IBundleProjectDescription;
24+
import org.eclipse.pde.core.project.IBundleProjectService;
25+
import org.eclipse.pde.core.target.ITargetDefinition;
26+
import org.eclipse.pde.core.target.ITargetLocation;
27+
import org.eclipse.pde.core.target.ITargetPlatformService;
28+
import org.eclipse.pde.internal.core.ClasspathComputer;
29+
import org.eclipse.pde.internal.core.PDECore;
30+
import org.eclipse.pde.internal.core.build.WorkspaceBuildModel;
31+
import org.eclipse.pde.internal.core.project.PDEProject;
32+
import org.eclipse.pde.ui.tests.util.ProjectUtils;
33+
import org.eclipse.pde.ui.tests.util.TargetPlatformUtil;
34+
import org.junit.jupiter.api.AfterAll;
35+
import org.junit.jupiter.api.AfterEach;
36+
import org.junit.jupiter.api.BeforeAll;
37+
import org.junit.jupiter.api.BeforeEach;
38+
import org.junit.jupiter.api.Test;
39+
import org.osgi.framework.Version;
40+
41+
public class RequiredPluginsClasspathContainerPerformanceTest {
42+
43+
@BeforeAll
44+
public static void beforeAll() throws Exception {
45+
ProjectUtils.deleteAllWorkspaceProjects();
46+
}
47+
48+
@AfterAll
49+
public static void afterAll() throws Exception {
50+
ProjectUtils.deleteAllWorkspaceProjects();
51+
}
52+
53+
private static final String CYCLE_BUNDLE_PREFIX = "Cycle_";
54+
private File targetDir;
55+
56+
@BeforeEach
57+
public void setUp() throws Exception {
58+
// Disable auto-building
59+
IWorkspaceDescription desc = ResourcesPlugin.getWorkspace().getDescription();
60+
desc.setAutoBuilding(false);
61+
ResourcesPlugin.getWorkspace().setDescription(desc);
62+
63+
targetDir = Files.createTempDirectory("pde_perf_target").toFile();
64+
createCyclicTargetPlatform();
65+
}
66+
67+
@AfterEach
68+
public void tearDown() throws Exception {
69+
// Restore auto-building
70+
IWorkspaceDescription desc = ResourcesPlugin.getWorkspace().getDescription();
71+
desc.setAutoBuilding(true);
72+
ResourcesPlugin.getWorkspace().setDescription(desc);
73+
74+
if (targetDir != null && targetDir.exists()) {
75+
deleteDir(targetDir);
76+
}
77+
78+
// Reset target platform
79+
ITargetPlatformService tps = PDECore.getDefault().acquireService(ITargetPlatformService.class);
80+
ITargetDefinition defaultTarget = tps.newDefaultTarget();
81+
TargetPlatformUtil.loadAndSetTarget(defaultTarget);
82+
}
83+
84+
private void deleteDir(File file) {
85+
File[] contents = file.listFiles();
86+
if (contents != null) {
87+
for (File f : contents) {
88+
deleteDir(f);
89+
}
90+
}
91+
file.delete();
92+
}
93+
94+
private void createCyclicTargetPlatform() throws Exception {
95+
// Cycle_A -> reexports Cycle_B
96+
// Cycle_B -> reexports Cycle_C
97+
// Cycle_C -> reexports Cycle_A
98+
createBundle(targetDir, CYCLE_BUNDLE_PREFIX + "A", null, CYCLE_BUNDLE_PREFIX + "B;visibility:=reexport");
99+
createBundle(targetDir, CYCLE_BUNDLE_PREFIX + "B", null, CYCLE_BUNDLE_PREFIX + "C;visibility:=reexport");
100+
createBundle(targetDir, CYCLE_BUNDLE_PREFIX + "C", null, CYCLE_BUNDLE_PREFIX + "A;visibility:=reexport");
101+
102+
// Set Target Platform
103+
ITargetPlatformService tps = PDECore.getDefault().acquireService(ITargetPlatformService.class);
104+
ITargetDefinition target = tps.newTarget();
105+
target.setTargetLocations(new ITargetLocation[] { tps.newDirectoryLocation(targetDir.getAbsolutePath()) });
106+
TargetPlatformUtil.loadAndSetTarget(target);
107+
}
108+
109+
private void createBundle(File dir, String name, String exports, String requires) throws IOException {
110+
File jarFile = new File(dir, name + ".jar");
111+
try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFile))) {
112+
Manifest manifest = new Manifest();
113+
Attributes main = manifest.getMainAttributes();
114+
main.put(Attributes.Name.MANIFEST_VERSION, "1.0");
115+
main.put(new Attributes.Name("Bundle-ManifestVersion"), "2");
116+
main.put(new Attributes.Name("Bundle-SymbolicName"), name);
117+
main.put(new Attributes.Name("Bundle-Version"), "1.0.0");
118+
if (exports != null) {
119+
main.put(new Attributes.Name("Export-Package"), exports);
120+
}
121+
if (requires != null) {
122+
main.put(new Attributes.Name("Require-Bundle"), requires);
123+
}
124+
125+
ZipEntry entry = new ZipEntry("META-INF/MANIFEST.MF");
126+
jos.putNextEntry(entry);
127+
manifest.write(jos);
128+
jos.closeEntry();
129+
}
130+
}
131+
132+
@Test
133+
public void testCyclicReexportInSecondaryDependencies() throws Exception {
134+
IBundleProjectService service = PDECore.getDefault().acquireService(IBundleProjectService.class);
135+
136+
// Create Consumer project
137+
String consumerName = "ConsumerBundle";
138+
IProject consumerProj = ResourcesPlugin.getWorkspace().getRoot().getProject(consumerName);
139+
consumerProj.create(null);
140+
consumerProj.open(null);
141+
142+
IBundleProjectDescription consumerDesc = service.getDescription(consumerProj);
143+
consumerDesc.setSymbolicName(consumerName);
144+
consumerDesc.setBundleVersion(new Version("1.0.0"));
145+
// No direct requirements, we use secondaryDependencies
146+
consumerDesc.setNatureIds(new String[] { JavaCore.NATURE_ID, IBundleProjectDescription.PLUGIN_NATURE });
147+
consumerDesc.apply(null);
148+
149+
// Add Secondary Dependency to build.properties
150+
IFile buildProps = PDEProject.getBuildProperties(consumerProj);
151+
WorkspaceBuildModel buildModel = new WorkspaceBuildModel(buildProps);
152+
buildModel.load();
153+
IBuild build = buildModel.getBuild();
154+
IBuildEntry entry = build.getEntry(IBuildEntry.SECONDARY_DEPENDENCIES);
155+
if (entry == null) {
156+
entry = buildModel.getFactory().createEntry(IBuildEntry.SECONDARY_DEPENDENCIES);
157+
build.add(entry);
158+
}
159+
entry.addToken(CYCLE_BUNDLE_PREFIX + "A");
160+
buildModel.save();
161+
162+
// Build to ensure models are ready
163+
ResourcesPlugin.getWorkspace().build(IncrementalProjectBuilder.FULL_BUILD, new NullProgressMonitor());
164+
165+
IPluginModelBase consumerModel = PluginRegistry.findModel(consumerProj);
166+
if (consumerModel == null) {
167+
throw new IllegalStateException("Consumer model not found");
168+
}
169+
170+
long start = System.currentTimeMillis();
171+
172+
// This triggers the computation (and potentially the infinite loop)
173+
ClasspathComputer.computeClasspathEntries(consumerModel, consumerProj);
174+
175+
long elapsed = System.currentTimeMillis() - start;
176+
System.out.println("Classpath computation took: " + elapsed + "ms for cyclic re-exported bundles.");
177+
178+
if (elapsed > 5000) {
179+
throw new AssertionError("Performance regression or Infinite Loop: Classpath computation took too long (" + elapsed + "ms)");
180+
}
181+
}
182+
}

ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/AllPDETests.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import org.eclipse.pde.core.tests.internal.AllPDECoreTests;
1717
import org.eclipse.pde.core.tests.internal.classpath.ClasspathResolutionTest;
18+
import org.eclipse.pde.core.tests.internal.classpath.RequiredPluginsClasspathContainerPerformanceTest;
1819
import org.eclipse.pde.core.tests.internal.core.builders.BundleErrorReporterTest;
1920
import org.eclipse.pde.core.tests.internal.util.PDESchemaHelperTest;
2021
import org.eclipse.pde.ui.tests.build.properties.AllValidatorTests;
@@ -58,6 +59,7 @@
5859
BundleRootTests.class, //
5960
PluginRegistryTests.class, //
6061
ClasspathResolverTest.class, //
62+
RequiredPluginsClasspathContainerPerformanceTest.class, //
6163
ClasspathUpdaterTest.class, //
6264
PDESchemaHelperTest.class, //
6365
ClasspathContributorTest.class, //

0 commit comments

Comments
 (0)