Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -678,8 +678,7 @@ private void addImplicitDependencies(BundleDescription desc, Set<BundleDescripti
*/
private void addTransitiveDependenciesWithForbiddenAccess(Set<BundleDescription> added,
List<IClasspathEntry> entries) throws CoreException {
Set<BundleDescription> closure = DependencyManager.findRequirementsClosure(added,
INCLUDE_OPTIONAL_DEPENDENCIES);
Set<BundleDescription> closure = DependencyManager.findRequirementsClosure(added);
String systemBundleBSN = TargetPlatformHelper.getPDEState().getSystemBundle();
Iterator<BundleDescription> transitiveDeps = closure.stream()
.filter(desc -> !desc.getSymbolicName().equals(systemBundleBSN))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
/*******************************************************************************
* Copyright (c) 2026 Andrey Loskutov <loskutov@gmx.de> 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:
* Andrey Loskutov <loskutov@gmx.de> - initial API and implementation
*******************************************************************************/
package org.eclipse.pde.core.tests.internal.classpath;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Stream;

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceDescription;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.internal.core.ClasspathComputer;
import org.eclipse.pde.internal.core.PDECore;
import org.eclipse.pde.ui.tests.runtime.TestUtils;
import org.eclipse.pde.ui.tests.util.ProjectUtils;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;

/**
* Regression test for classpath resolution of plugin projects. Tests that only
* the expected (no transitive optional) bundles are on the classpath, and that
* errors are reported for missing packages from not accessible bundles, but not
* for missing packages.
*
* See https://github.com/eclipse-pde/eclipse.pde/issues/2244.
*/
public class ClasspathResolutionTest2 {

@ClassRule
public static final TestRule CLEAR_WORKSPACE = ProjectUtils.DELETE_ALL_WORKSPACE_PROJECTS_BEFORE_AND_AFTER;

@Rule
public final TestRule deleteCreatedTestProjectsAfter = ProjectUtils.DELETE_CREATED_WORKSPACE_PROJECTS_AFTER;

private static IProject projectA;

private static IClasspathEntry[] classpathEntriesA;

static final List<String> expectedAccessibleBundles = List.of("B", "G");
static final List<String> expectedApiPackages = apiPackagesFor(expectedAccessibleBundles);
static final List<String> expectedInternalPackages = internalPackagesFor(expectedAccessibleBundles);

static final List<String> notAccessibleBundles = List.of("C", "D", "E", "F", "H");
static final List<String> notAccessibleApiPackages = apiPackagesFor(notAccessibleBundles);
static final List<String> notAccessibleInternalPackages = internalPackagesFor(notAccessibleBundles);

static final List<String> expectedIndirectlyRequiredBundles = List.of("C", "D");

static final List<String> allOtherProjects = Stream
.concat(expectedAccessibleBundles.stream(), notAccessibleBundles.stream()).sorted().toList();

static final List<String> expectedProjectsOnClasspath = Stream
.concat(expectedAccessibleBundles.stream(), expectedIndirectlyRequiredBundles.stream()).sorted().toList();

// See OSGiAnnotationsClasspathContributor. These annotations are added to
// classpath if runtime doesn't include them after
// https://github.com/eclipse-pde/eclipse.pde/pull/1116
static final Collection<String> OSGI_ANNOTATIONS = List.of("org.osgi.annotation.versioning", //$NON-NLS-1$
"org.osgi.annotation.bundle", "org.osgi.service.component.annotations", //$NON-NLS-1$ //$NON-NLS-2$
"org.osgi.service.metatype.annotations"); //$NON-NLS-1$

private static boolean wasAutoBuildEnabled;

@BeforeClass
public static void setupBeforeClass() throws Exception {
wasAutoBuildEnabled = isAutoBuildEnabled();
enableAutobuild(false);

List<IProject> importedProjects = new ArrayList<>();

for (String name : allOtherProjects) {
IProject project = ProjectUtils.importTestProject("tests/projects/" + name);
importedProjects.add(project);
}

// Build all projects in reversed order to ensure that dependencies are
// built before dependents
for (IProject project : importedProjects.reversed()) {
project.open(new NullProgressMonitor());
waitForJobs();
}
ResourcesPlugin.getWorkspace().build(IncrementalProjectBuilder.FULL_BUILD, new NullProgressMonitor());
waitForJobs();

// Now import and build project A, which depends on all other projects.
projectA = ProjectUtils.importTestProject("tests/projects/A");
projectA.open(new NullProgressMonitor());
TestUtils.processUIEvents(100);

projectA.build(IncrementalProjectBuilder.FULL_BUILD, new NullProgressMonitor());
waitForJobs();

IPluginModelBase modelA = PDECore.getDefault().getModelManager().findModel(projectA);
classpathEntriesA = ClasspathComputer.computeClasspathEntries(modelA, projectA);
}

@AfterClass
public static void tearDownAfterClass() throws Exception {
ProjectUtils.deleteAllWorkspaceProjects();
enableAutobuild(wasAutoBuildEnabled);
}

private static void waitForJobs() throws Exception {
TestUtils.waitForJobs("testRequiredPluginsClasspathContainerContract()", 100, 10000);
Job.getJobManager().join(ResourcesPlugin.FAMILY_AUTO_BUILD, null);
Job.getJobManager().join(ResourcesPlugin.FAMILY_MANUAL_BUILD, null);
TestUtils.processUIEvents(100);
}

/**
* Sets autobuild to the specified boolean value
*/
private static void enableAutobuild(boolean enable) throws CoreException {
IWorkspace workspace = ResourcesPlugin.getWorkspace();
IWorkspaceDescription desc = workspace.getDescription();
desc.setAutoBuilding(enable);
workspace.setDescription(desc);
waitForAutoBuild();
}

private static boolean isAutoBuildEnabled() {
IWorkspace workspace = ResourcesPlugin.getWorkspace();
IWorkspaceDescription desc = workspace.getDescription();
return desc.isAutoBuilding();
}

private static void waitForAutoBuild() {
Job.getJobManager().wakeUp(ResourcesPlugin.FAMILY_AUTO_BUILD);
try {
Job.getJobManager().join(ResourcesPlugin.FAMILY_AUTO_BUILD, new NullProgressMonitor());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

/**
* Check that the classpath of plugin A contains exactly the expected
* bundles. Checks that "missing type" compilation errors are reported for
* all references form not accessible bundles. Checks that no dependencies
* to transitive optional bundles are added to the classpath. This bundle
* classpath is computed by RequiredPluginsClasspathContainer.
*/
@Test
public void testRequiredPluginsClasspathContainerContract() throws Exception {
// Check every project except A - they should build without errors
List<IProject> otherProjects = allOtherProjects.stream().map(ClasspathResolutionTest2::getProject).toList();
for (IProject project : otherProjects) {
IMarker[] markers = project.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE);
for (IMarker marker : markers) {
if (marker.getAttribute(IMarker.SEVERITY, -1) == IMarker.SEVERITY_ERROR) {
fail("Unexpected error in project " + project.getName() + ": "
+ marker.getAttribute(IMarker.MESSAGE, ""));
}
}
}

// Check that project A has errors, and that all errors are related to
// missing packages from not accessible bundles, and that no error is
// related to missing packages from expected accessible bundles
IMarker[] markers = projectA.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE);

List<String> errorMessages = Arrays.asList(markers).stream()
.filter(marker -> marker.getAttribute(IMarker.SEVERITY, -1) == IMarker.SEVERITY_ERROR)
.map(marker -> marker.getAttribute(IMarker.MESSAGE, "")).toList();

for (String message : errorMessages) {
// Check that no error is related to missing packages from expected
// accessible bundles
boolean isRelatedToExpectedAccessibleBundle = false;
for (String bundle : expectedAccessibleBundles) {
String pack = bundle.toLowerCase();
if (message.contains(pack + " cannot be resolved to a type")
|| message.contains("project '" + bundle + "'")) {
isRelatedToExpectedAccessibleBundle = true;
break;
}
}
assertFalse("Unexpected error in project A: " + message, isRelatedToExpectedAccessibleBundle);

// and that all errors are related to missing packages from not
// accessible bundles
boolean isRelatedToNotAccessibleBundle = false;
for (String bundle : notAccessibleBundles) {
String pack = bundle.toLowerCase();
if (message.contains(pack + " cannot be resolved to a type")
|| message.contains("project '" + bundle + "'")) {
isRelatedToNotAccessibleBundle = true;
break;
}
}
assertTrue("Unexpected error in project A: " + message, isRelatedToNotAccessibleBundle);
}

// There must be at least one error, otherwise the test would not be
// meaningful
assertFalse("Expected errors in project A, but found none!", errorMessages.isEmpty());

// Check that all expected accessible bundles are on the classpath, and
// only those
List<String> projectNames = new ArrayList<>(
Arrays.asList(classpathEntriesA).stream().map(entry -> entry.getPath().lastSegment()).toList());

removeOsgiLibraries(projectNames);
assertThat(projectNames).containsExactlyInAnyOrderElementsOf(expectedProjectsOnClasspath);

// Same check using the API of PDECore and ClasspathComputer instead
List<String> classpathEntries = new ArrayList<>(getRequiredPluginContainerEntries(projectA));
removeOsgiLibraries(classpathEntries);
assertThat(classpathEntries).containsExactlyInAnyOrderElementsOf(expectedProjectsOnClasspath);
}

private static void removeOsgiLibraries(List<String> names) {
for (String annotationLibrary : OSGI_ANNOTATIONS) {
for (Iterator<String> iterator = names.iterator(); iterator.hasNext();) {
String name = iterator.next();
if (name.startsWith(annotationLibrary)) {
iterator.remove();
}
}
}
}

private static IProject getProject(String name) {
return ResourcesPlugin.getWorkspace().getRoot().getProject(name);
}

private static List<String> apiPackagesFor(List<String> expectedAccessibleBundles) {
return expectedAccessibleBundles.stream().flatMap(bundle -> List.of(bundle + ".api").stream()).toList();
}

private static List<String> internalPackagesFor(List<String> expectedAccessibleBundles) {
return expectedAccessibleBundles.stream().flatMap(bundle -> List.of(bundle + ".internal").stream()).toList();
}

private List<String> getRequiredPluginContainerEntries(IProject project) throws CoreException {
IPluginModelBase model = PDECore.getDefault().getModelManager().findModel(project);
IClasspathEntry[] computeClasspathEntries = ClasspathComputer.computeClasspathEntries(model, project);
return Arrays.stream(computeClasspathEntries).map(IClasspathEntry::getPath).map(IPath::lastSegment).toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import org.eclipse.pde.core.tests.internal.AllPDECoreTests;
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.core.builders.BundleErrorReporterTest;
import org.eclipse.pde.core.tests.internal.util.PDESchemaHelperTest;
import org.eclipse.pde.ui.tests.build.properties.AllValidatorTests;
Expand Down Expand Up @@ -63,6 +64,7 @@
ClasspathContributorTest.class, //
DynamicPluginProjectReferencesTest.class, //
ClasspathResolutionTest.class, //
ClasspathResolutionTest2.class, //
BundleErrorReporterTest.class, //
AllPDECoreTests.class, //
ProjectSmartImportTest.class, //
Expand Down
7 changes: 7 additions & 0 deletions ui/org.eclipse.pde.ui.tests/tests/projects/A/.classpath
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21"/>
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
<classpathentry kind="src" path="src"/>
<classpathentry kind="output" path="bin"/>
</classpath>
28 changes: 28 additions & 0 deletions ui/org.eclipse.pde.ui.tests/tests/projects/A/.project
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>A</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.pde.ManifestBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.pde.SchemaBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.pde.PluginNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=21
org.eclipse.jdt.core.compiler.compliance=21
org.eclipse.jdt.core.compiler.release=enabled
org.eclipse.jdt.core.compiler.source=21
org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
10 changes: 10 additions & 0 deletions ui/org.eclipse.pde.ui.tests/tests/projects/A/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: A
Bundle-SymbolicName: A
Bundle-Version: 1.0.0.qualifier
Export-Package: a.api
Require-Bundle: B;bundle-version="1.0.0",
G;bundle-version="1.0.0"
Automatic-Module-Name: A
Bundle-RequiredExecutionEnvironment: JavaSE-21
4 changes: 4 additions & 0 deletions ui/org.eclipse.pde.ui.tests/tests/projects/A/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
source.. = src/
output.. = bin/
bin.includes = META-INF/,\
.
Loading
Loading