From 7c646dede8123e6ef4b345243cb0e5d9cc858503 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Tue, 3 Mar 2026 13:26:33 +0100 Subject: [PATCH 1/2] Fix performance issue and infinite loop in classpath computation Optimize access rule accumulation in RequiredPluginsClasspathContainer by using Set instead of List to avoid O(N) containment checks during insertion, which was causing O(N^2) complexity for plug-ins with many re-exported packages. Add a recursion guard in findExportedPackages to prevent infinite loops when processing cyclic re-exports in implicit or secondary dependencies. Add two tests: - RequiredPluginsClasspathContainerPerformanceTest: verifies that classpath computation finishes quickly even when secondary dependencies form a cyclic re-export graph. - ChainedReexportPerformanceTest: verifies performance in a chained re-export scenario to ensure no O(N^2) scaling. Co-Authored-By: Claude Sonnet 4.6 --- .../RequiredPluginsClasspathContainer.java | 45 +++-- .../META-INF/MANIFEST.MF | 1 + .../ChainedReexportPerformanceTest.java | 188 ++++++++++++++++++ ...ginsClasspathContainerPerformanceTest.java | 187 +++++++++++++++++ .../org/eclipse/pde/ui/tests/AllPDETests.java | 4 + 5 files changed, 403 insertions(+), 22 deletions(-) create mode 100644 ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/core/tests/internal/classpath/ChainedReexportPerformanceTest.java create mode 100644 ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/core/tests/internal/classpath/RequiredPluginsClasspathContainerPerformanceTest.java diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/RequiredPluginsClasspathContainer.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/RequiredPluginsClasspathContainer.java index 279f6c46e5..7c271e09eb 100644 --- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/RequiredPluginsClasspathContainer.java +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/RequiredPluginsClasspathContainer.java @@ -85,7 +85,7 @@ class RequiredPluginsClasspathContainer { private static final String JUNIT4_PLUGIN = "org.junit"; private static final VersionRange JUNIT_4_VERSION = new VersionRange("[4.0,5)"); //$NON-NLS-1$ @SuppressWarnings("nls") - private static final Set JUNIT5_RUNTIME_PLUGINS = Set.of("org.junit", // + private static final List JUNIT5_RUNTIME_PLUGINS = List.of("org.junit", // "junit-platform-launcher", "org.junit.platform.launcher", "junit-jupiter-engine", // BSN of the bundle from Maven-Central @@ -223,7 +223,7 @@ private List computePluginEntriesByModel() throws CoreException return List.of(); } - Map> map = retrieveVisiblePackagesFromState(desc); + Map> map = retrieveVisiblePackagesFromState(desc); // Add any library entries contributed via classpath contributor // extension (Bug 363733) @@ -338,8 +338,8 @@ private static synchronized Stream getClasspathContributo return Stream.concat(fClasspathContributors.stream(), PDECore.getDefault().getClasspathContributors()); } - private Map> retrieveVisiblePackagesFromState(BundleDescription desc) { - Map> visiblePackages = new HashMap<>(); + private Map> retrieveVisiblePackagesFromState(BundleDescription desc) { + Map> visiblePackages = new HashMap<>(); StateHelper helper = BundleHelper.getPlatformAdmin().getStateHelper(); addVisiblePackagesFromState(helper, desc, visiblePackages); if (desc.getHost() != null) { @@ -349,7 +349,7 @@ private Map> retrieveVisiblePackagesFromState(Bund } private void addVisiblePackagesFromState(StateHelper helper, BundleDescription desc, - Map> visiblePackages) { + Map> visiblePackages) { if (desc == null) { return; } @@ -359,11 +359,9 @@ private void addVisiblePackagesFromState(StateHelper helper, BundleDescription d if (exporter == null) { continue; } - List list = visiblePackages.computeIfAbsent(exporter, e -> new ArrayList<>()); + LinkedHashSet list = visiblePackages.computeIfAbsent(exporter, e -> new LinkedHashSet<>()); Rule rule = getRule(helper, desc, export); - if (!list.contains(rule)) { - list.add(rule); - } + list.add(rule); } } @@ -375,7 +373,7 @@ private Rule getRule(StateHelper helper, BundleDescription desc, ExportPackageDe } protected void addDependencyViaImportPackage(BundleDescription desc, Set added, - Map> map, List entries) throws CoreException { + Map> map, List entries) throws CoreException { if (desc == null || !added.add(desc)) { return; } @@ -393,12 +391,12 @@ protected void addDependencyViaImportPackage(BundleDescription desc, Set added, - Map> map, List entries) throws CoreException { + Map> map, List entries) throws CoreException { addDependency(desc, added, map, entries, true); } private void addDependency(BundleDescription desc, Set added, - Map> map, List entries, boolean useInclusion) + Map> map, List entries, boolean useInclusion) throws CoreException { if (desc == null || !added.add(desc)) { return; @@ -441,7 +439,7 @@ private void addDependency(BundleDescription desc, Set added, } } - private boolean addPlugin(BundleDescription desc, boolean useInclusions, Map> map, + private boolean addPlugin(BundleDescription desc, boolean useInclusions, Map> map, List entries) throws CoreException { IPluginModelBase model = PluginRegistry.findModel((Resource) desc); if (model == null || !model.isEnabled()) { @@ -469,7 +467,7 @@ private boolean addPlugin(BundleDescription desc, boolean useInclusions, Map getInclusions(Map> map, IPluginModelBase model) { + private List getInclusions(Map> map, IPluginModelBase model) { BundleDescription desc = model.getBundleDescription(); if (desc == null || "false".equals(System.getProperty("pde.restriction")) //$NON-NLS-1$ //$NON-NLS-2$ || !(fModel instanceof IBundlePluginModelBase) || TargetPlatformHelper.getTargetVersion() < 3.1) { @@ -479,12 +477,12 @@ private List getInclusions(Map> map, IPlugin if (desc.getHost() != null) { desc = (BundleDescription) desc.getHost().getSupplier(); } - List rules = map.getOrDefault(desc, List.of()); - return (rules.isEmpty() && !ClasspathUtilCore.hasBundleStructure(model)) ? null : rules; + LinkedHashSet rules = map.getOrDefault(desc, new LinkedHashSet<>()); + return (rules.isEmpty() && !ClasspathUtilCore.hasBundleStructure(model)) ? null : new ArrayList<>(rules); } private void addHostPlugin(HostSpecification hostSpec, Set added, - Map> map, List entries) throws CoreException { + Map> map, List entries) throws CoreException { BaseDescription desc = hostSpec.getSupplier(); if (desc instanceof BundleDescription host) { @@ -628,7 +626,7 @@ private void addJunit5RuntimeDependencies(Set added, List> rules = Map.of(desc, List.of()); + Map> rules = Map.of(desc, new LinkedHashSet<>()); addPlugin(desc, true, rules, entries); } } @@ -691,7 +689,7 @@ private void addTransitiveDependenciesWithForbiddenAccess(Set while (transitiveDeps.hasNext()) { BundleDescription desc = transitiveDeps.next(); if (added.add(desc)) { - Map> rules = Map.of(desc, List.of()); + Map> rules = Map.of(desc, new LinkedHashSet<>()); addPlugin(desc, true, rules, entries); } } @@ -705,21 +703,24 @@ private void addExtraModel(BundleDescription desc, Set added, if (added.contains(bundleDesc)) { return; } - Map> rules = new HashMap<>(); + Map> rules = new HashMap<>(); findExportedPackages(bundleDesc, desc, rules); addDependency(bundleDesc, added, rules, entries, true); } } protected final void findExportedPackages(BundleDescription desc, BundleDescription projectDesc, - Map> map) { + Map> map) { if (desc != null) { Queue queue = new ArrayDeque<>(); queue.add(desc); while (!queue.isEmpty()) { BundleDescription bdesc = queue.remove(); + if (map.containsKey(bdesc)) { + continue; + } ExportPackageDescription[] expkgs = bdesc.getExportPackages(); - List rules = new ArrayList<>(); + LinkedHashSet rules = new LinkedHashSet<>(); for (ExportPackageDescription expkg : expkgs) { boolean discouraged = restrictPackage(projectDesc, expkg); IPath path = IPath.fromOSString(expkg.getName().replace('.', '/') + "/*"); //$NON-NLS-1$ diff --git a/ui/org.eclipse.pde.ui.tests/META-INF/MANIFEST.MF b/ui/org.eclipse.pde.ui.tests/META-INF/MANIFEST.MF index 3a3f32be76..f5ed639eff 100644 --- a/ui/org.eclipse.pde.ui.tests/META-INF/MANIFEST.MF +++ b/ui/org.eclipse.pde.ui.tests/META-INF/MANIFEST.MF @@ -59,6 +59,7 @@ Import-Package: jakarta.annotation;version="[2.1.0,3.0.0)", org.eclipse.pde.internal.build, org.hamcrest, org.junit, + org.junit.jupiter.api;version="[5.8.1,6.0.0)", org.junit.jupiter.api.function;version="[5.8.1,6.0.0)", org.junit.jupiter.migrationsupport;version="[5.13.0,6.0.0)", org.junit.platform.suite.api;version="[1.13.0,2.0.0)", diff --git a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/core/tests/internal/classpath/ChainedReexportPerformanceTest.java b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/core/tests/internal/classpath/ChainedReexportPerformanceTest.java new file mode 100644 index 0000000000..77e6ff26a8 --- /dev/null +++ b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/core/tests/internal/classpath/ChainedReexportPerformanceTest.java @@ -0,0 +1,188 @@ +/******************************************************************************* + * Copyright (c) 2025 vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Lars Vogel - initial API and implementation + *******************************************************************************/ +package org.eclipse.pde.core.tests.internal.classpath; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspaceDescription; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.pde.core.plugin.IPluginModelBase; +import org.eclipse.pde.core.plugin.PluginRegistry; +import org.eclipse.pde.core.project.IBundleProjectDescription; +import org.eclipse.pde.core.project.IBundleProjectService; +import org.eclipse.pde.core.project.IRequiredBundleDescription; +import org.eclipse.pde.core.target.ITargetDefinition; +import org.eclipse.pde.core.target.ITargetLocation; +import org.eclipse.pde.core.target.ITargetPlatformService; +import org.eclipse.pde.internal.core.PDECore; +import org.eclipse.pde.internal.ui.wizards.tools.UpdateClasspathJob; +import org.eclipse.pde.ui.tests.runtime.TestUtils; +import org.eclipse.pde.ui.tests.util.ProjectUtils; +import org.eclipse.pde.ui.tests.util.TargetPlatformUtil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.osgi.framework.Version; + +public class ChainedReexportPerformanceTest { + + private static final String CHAIN_PREFIX = "Chain_"; + private static final int PACKAGE_COUNT = 1000; + private static final int BUNDLE_CHAIN_DEPTH = 5; + private File targetDir; + private ITargetDefinition savedTarget; + + @BeforeAll + public static void beforeAll() throws Exception { + ProjectUtils.deleteAllWorkspaceProjects(); + } + + @AfterAll + public static void afterAll() throws Exception { + ProjectUtils.deleteAllWorkspaceProjects(); + } + + @BeforeEach + public void setUp() throws Exception { + savedTarget = TargetPlatformUtil.TPS.getWorkspaceTargetDefinition(); + + IWorkspaceDescription desc = ResourcesPlugin.getWorkspace().getDescription(); + desc.setAutoBuilding(false); + ResourcesPlugin.getWorkspace().setDescription(desc); + + targetDir = Files.createTempDirectory("pde_chain_perf_target").toFile(); + createChainedTargetPlatform(); + } + + @AfterEach + public void tearDown() throws Exception { + IWorkspaceDescription desc = ResourcesPlugin.getWorkspace().getDescription(); + desc.setAutoBuilding(true); + ResourcesPlugin.getWorkspace().setDescription(desc); + + if (targetDir != null && targetDir.exists()) { + Files.walk(targetDir.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } + + if (savedTarget != null && savedTarget != TargetPlatformUtil.TPS.getWorkspaceTargetDefinition()) { + TargetPlatformUtil.loadAndSetTarget(savedTarget); + } + } + + private void createChainedTargetPlatform() throws Exception { + // Create a chain of bundles: B_0 -> B_1 -> ... -> B_N (all re-exporting) + for (int i = 0; i < BUNDLE_CHAIN_DEPTH; i++) { + String name = CHAIN_PREFIX + i; + String exports = createPackageExports(name); + String requires = (i > 0) ? (CHAIN_PREFIX + (i - 1) + ";visibility:=reexport") : null; + createBundle(targetDir, name, exports, requires); + } + + ITargetPlatformService tps = PDECore.getDefault().acquireService(ITargetPlatformService.class); + ITargetDefinition target = tps.newTarget(); + target.setTargetLocations(new ITargetLocation[] { tps.newDirectoryLocation(targetDir.getAbsolutePath()) }); + TargetPlatformUtil.loadAndSetTarget(target); + } + + private String createPackageExports(String bundleName) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < PACKAGE_COUNT; i++) { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(bundleName).append(".pkg.").append(i); + } + return sb.toString(); + } + + private void createBundle(File dir, String name, String exports, String requires) throws IOException { + File jarFile = new File(dir, name + ".jar"); + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFile))) { + Manifest manifest = new Manifest(); + Attributes main = manifest.getMainAttributes(); + main.put(Attributes.Name.MANIFEST_VERSION, "1.0"); + main.put(new Attributes.Name("Bundle-ManifestVersion"), "2"); + main.put(new Attributes.Name("Bundle-SymbolicName"), name); + main.put(new Attributes.Name("Bundle-Version"), "1.0.0"); + if (exports != null) { + main.put(new Attributes.Name("Export-Package"), exports); + } + if (requires != null) { + main.put(new Attributes.Name("Require-Bundle"), requires); + } + + ZipEntry entry = new ZipEntry("META-INF/MANIFEST.MF"); + jos.putNextEntry(entry); + manifest.write(jos); + jos.closeEntry(); + } + } + + @Test + public void testChainedReexportPerformance() throws Exception { + IBundleProjectService service = PDECore.getDefault().acquireService(IBundleProjectService.class); + + String consumerName = "ConsumerBundle"; + IProject consumerProj = ResourcesPlugin.getWorkspace().getRoot().getProject(consumerName); + consumerProj.create(null); + consumerProj.open(null); + + IBundleProjectDescription consumerDesc = service.getDescription(consumerProj); + consumerDesc.setSymbolicName(consumerName); + consumerDesc.setBundleVersion(new Version("1.0.0")); + + // Require the last bundle in the chain with re-export + IRequiredBundleDescription mainReq = service.newRequiredBundle(CHAIN_PREFIX + (BUNDLE_CHAIN_DEPTH - 1), null, + false, true); + consumerDesc.setRequiredBundles(new IRequiredBundleDescription[] { mainReq }); + + consumerDesc.setNatureIds(new String[] { JavaCore.NATURE_ID, IBundleProjectDescription.PLUGIN_NATURE }); + consumerDesc.apply(null); + + ResourcesPlugin.getWorkspace().build(IncrementalProjectBuilder.FULL_BUILD, new NullProgressMonitor()); + TestUtils.waitForJobs("Init", 500, 5000); + + IPluginModelBase consumerModel = PluginRegistry.findModel(consumerProj); + if (consumerModel == null) { + throw new IllegalStateException("Consumer model not found"); + } + + UpdateClasspathJob.scheduleFor(List.of(consumerModel), false); + boolean timedOut = TestUtils.waitForJobs("classpath", 100, 10000); + assertFalse(timedOut, "Performance regression: classpath computation timed out for chained re-exports"); + + IClasspathEntry[] resolvedClasspath = JavaCore.create(consumerProj).getRawClasspath(); + assertTrue(resolvedClasspath.length > 0, "Classpath should not be empty"); + } +} diff --git a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/core/tests/internal/classpath/RequiredPluginsClasspathContainerPerformanceTest.java b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/core/tests/internal/classpath/RequiredPluginsClasspathContainerPerformanceTest.java new file mode 100644 index 0000000000..a35d5ddfcb --- /dev/null +++ b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/core/tests/internal/classpath/RequiredPluginsClasspathContainerPerformanceTest.java @@ -0,0 +1,187 @@ +/******************************************************************************* + * Copyright (c) 2025 vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Lars Vogel - initial API and implementation + *******************************************************************************/ +package org.eclipse.pde.core.tests.internal.classpath; + +import static org.junit.jupiter.api.Assertions.assertTimeout; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Comparator; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspaceDescription; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.pde.core.build.IBuild; +import org.eclipse.pde.core.build.IBuildEntry; +import org.eclipse.pde.core.plugin.IPluginModelBase; +import org.eclipse.pde.core.plugin.PluginRegistry; +import org.eclipse.pde.core.project.IBundleProjectDescription; +import org.eclipse.pde.core.project.IBundleProjectService; +import org.eclipse.pde.core.target.ITargetDefinition; +import org.eclipse.pde.core.target.ITargetLocation; +import org.eclipse.pde.core.target.ITargetPlatformService; +import org.eclipse.pde.internal.core.PDECore; +import org.eclipse.pde.internal.core.build.WorkspaceBuildModel; +import org.eclipse.pde.internal.core.project.PDEProject; +import org.eclipse.pde.internal.ui.wizards.tools.UpdateClasspathJob; +import org.eclipse.pde.ui.tests.runtime.TestUtils; +import org.eclipse.pde.ui.tests.util.ProjectUtils; +import org.eclipse.pde.ui.tests.util.TargetPlatformUtil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.osgi.framework.Version; + +public class RequiredPluginsClasspathContainerPerformanceTest { + + private static final String CYCLE_BUNDLE_PREFIX = "Cycle_"; + private File targetDir; + private ITargetDefinition savedTarget; + + @BeforeAll + public static void beforeAll() throws Exception { + ProjectUtils.deleteAllWorkspaceProjects(); + } + + @AfterAll + public static void afterAll() throws Exception { + ProjectUtils.deleteAllWorkspaceProjects(); + } + + @BeforeEach + public void setUp() throws Exception { + savedTarget = TargetPlatformUtil.TPS.getWorkspaceTargetDefinition(); + + IWorkspaceDescription desc = ResourcesPlugin.getWorkspace().getDescription(); + desc.setAutoBuilding(false); + ResourcesPlugin.getWorkspace().setDescription(desc); + + targetDir = Files.createTempDirectory("pde_perf_target").toFile(); + createCyclicTargetPlatform(); + } + + @AfterEach + public void tearDown() throws Exception { + IWorkspaceDescription desc = ResourcesPlugin.getWorkspace().getDescription(); + desc.setAutoBuilding(true); + ResourcesPlugin.getWorkspace().setDescription(desc); + + if (targetDir != null && targetDir.exists()) { + Files.walk(targetDir.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } + + if (savedTarget != null && savedTarget != TargetPlatformUtil.TPS.getWorkspaceTargetDefinition()) { + TargetPlatformUtil.loadAndSetTarget(savedTarget); + } + } + + private void createCyclicTargetPlatform() throws Exception { + // Cycle_A -> reexports Cycle_B + // Cycle_B -> reexports Cycle_C + // Cycle_C -> reexports Cycle_A + createBundle(targetDir, CYCLE_BUNDLE_PREFIX + "A", null, CYCLE_BUNDLE_PREFIX + "B;visibility:=reexport"); + createBundle(targetDir, CYCLE_BUNDLE_PREFIX + "B", null, CYCLE_BUNDLE_PREFIX + "C;visibility:=reexport"); + createBundle(targetDir, CYCLE_BUNDLE_PREFIX + "C", null, CYCLE_BUNDLE_PREFIX + "A;visibility:=reexport"); + + ITargetPlatformService tps = PDECore.getDefault().acquireService(ITargetPlatformService.class); + ITargetDefinition target = tps.newTarget(); + target.setTargetLocations(new ITargetLocation[] { tps.newDirectoryLocation(targetDir.getAbsolutePath()) }); + TargetPlatformUtil.loadAndSetTarget(target); + } + + private void createBundle(File dir, String name, String exports, String requires) throws IOException { + File jarFile = new File(dir, name + ".jar"); + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFile))) { + Manifest manifest = new Manifest(); + Attributes main = manifest.getMainAttributes(); + main.put(Attributes.Name.MANIFEST_VERSION, "1.0"); + main.put(new Attributes.Name("Bundle-ManifestVersion"), "2"); + main.put(new Attributes.Name("Bundle-SymbolicName"), name); + main.put(new Attributes.Name("Bundle-Version"), "1.0.0"); + if (exports != null) { + main.put(new Attributes.Name("Export-Package"), exports); + } + if (requires != null) { + main.put(new Attributes.Name("Require-Bundle"), requires); + } + + ZipEntry entry = new ZipEntry("META-INF/MANIFEST.MF"); + jos.putNextEntry(entry); + manifest.write(jos); + jos.closeEntry(); + } + } + + @Test + public void testCyclicReexportInSecondaryDependencies() throws Exception { + IBundleProjectService service = PDECore.getDefault().acquireService(IBundleProjectService.class); + + String consumerName = "ConsumerBundle"; + IProject consumerProj = ResourcesPlugin.getWorkspace().getRoot().getProject(consumerName); + consumerProj.create(null); + consumerProj.open(null); + + IBundleProjectDescription consumerDesc = service.getDescription(consumerProj); + consumerDesc.setSymbolicName(consumerName); + consumerDesc.setBundleVersion(new Version("1.0.0")); + consumerDesc.setNatureIds(new String[] { JavaCore.NATURE_ID, IBundleProjectDescription.PLUGIN_NATURE }); + consumerDesc.apply(null); + + // Add secondary dependency to build.properties + IFile buildProps = PDEProject.getBuildProperties(consumerProj); + WorkspaceBuildModel buildModel = new WorkspaceBuildModel(buildProps); + buildModel.load(); + IBuild build = buildModel.getBuild(); + IBuildEntry entry = build.getEntry(IBuildEntry.SECONDARY_DEPENDENCIES); + if (entry == null) { + entry = buildModel.getFactory().createEntry(IBuildEntry.SECONDARY_DEPENDENCIES); + build.add(entry); + } + entry.addToken(CYCLE_BUNDLE_PREFIX + "A"); + buildModel.save(); + + ResourcesPlugin.getWorkspace().build(IncrementalProjectBuilder.FULL_BUILD, new NullProgressMonitor()); + TestUtils.waitForJobs("Init", 500, 5000); + + IPluginModelBase consumerModel = PluginRegistry.findModel(consumerProj); + if (consumerModel == null) { + throw new IllegalStateException("Consumer model not found"); + } + + Job job = UpdateClasspathJob.scheduleFor(List.of(consumerModel), false); + assertTimeout(Duration.ofSeconds(5), () -> job.join(), + "Performance regression or infinite loop: classpath computation timed out for cyclic re-exports"); + + IClasspathEntry[] resolvedClasspath = JavaCore.create(consumerProj).getRawClasspath(); + assertTrue(resolvedClasspath.length > 0, "Classpath should not be empty"); + } +} diff --git a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/AllPDETests.java b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/AllPDETests.java index 8316577644..034af08321 100644 --- a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/AllPDETests.java +++ b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/AllPDETests.java @@ -14,8 +14,10 @@ package org.eclipse.pde.ui.tests; import org.eclipse.pde.core.tests.internal.AllPDECoreTests; +import org.eclipse.pde.core.tests.internal.classpath.ChainedReexportPerformanceTest; import org.eclipse.pde.core.tests.internal.classpath.ClasspathResolutionTest; import org.eclipse.pde.core.tests.internal.classpath.ClasspathResolutionTest2; +import org.eclipse.pde.core.tests.internal.classpath.RequiredPluginsClasspathContainerPerformanceTest; import org.eclipse.pde.core.tests.internal.core.builders.BundleErrorReporterTest; import org.eclipse.pde.core.tests.internal.util.PDESchemaHelperTest; import org.eclipse.pde.ui.tests.build.properties.AllValidatorTests; @@ -59,6 +61,8 @@ BundleRootTests.class, // PluginRegistryTests.class, // ClasspathResolverTest.class, // + RequiredPluginsClasspathContainerPerformanceTest.class, // + ChainedReexportPerformanceTest.class, // ClasspathUpdaterTest.class, // PDESchemaHelperTest.class, // ClasspathContributorTest.class, // From 23f8aac1720cfc41496fa2450868a13fab93a241 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Wed, 18 Mar 2026 17:14:11 +0100 Subject: [PATCH 2/2] Fix deprecation warning in ChainedReexportPerformanceTest Cast null to VersionRange (org.osgi.framework) to resolve to the non-deprecated overload of IBundleProjectService.newRequiredBundle(). Co-Authored-By: Claude Opus 4.6 --- .../internal/classpath/ChainedReexportPerformanceTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/core/tests/internal/classpath/ChainedReexportPerformanceTest.java b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/core/tests/internal/classpath/ChainedReexportPerformanceTest.java index 77e6ff26a8..3b1f4c2e5c 100644 --- a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/core/tests/internal/classpath/ChainedReexportPerformanceTest.java +++ b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/core/tests/internal/classpath/ChainedReexportPerformanceTest.java @@ -54,6 +54,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.osgi.framework.Version; +import org.osgi.framework.VersionRange; public class ChainedReexportPerformanceTest { @@ -163,8 +164,8 @@ public void testChainedReexportPerformance() throws Exception { consumerDesc.setBundleVersion(new Version("1.0.0")); // Require the last bundle in the chain with re-export - IRequiredBundleDescription mainReq = service.newRequiredBundle(CHAIN_PREFIX + (BUNDLE_CHAIN_DEPTH - 1), null, - false, true); + IRequiredBundleDescription mainReq = service.newRequiredBundle(CHAIN_PREFIX + (BUNDLE_CHAIN_DEPTH - 1), + (VersionRange) null, false, true); consumerDesc.setRequiredBundles(new IRequiredBundleDescription[] { mainReq }); consumerDesc.setNatureIds(new String[] { JavaCore.NATURE_ID, IBundleProjectDescription.PLUGIN_NATURE });