2929import org .eclipse .jdt .core .IClasspathEntry ;
3030import org .eclipse .jdt .core .IJavaModelMarker ;
3131import org .eclipse .jdt .core .compiler .IProblem ;
32+ import org .eclipse .osgi .service .resolver .BundleDescription ;
33+ import org .eclipse .osgi .service .resolver .ResolverError ;
3234import org .eclipse .pde .core .plugin .IPluginModelBase ;
3335import org .eclipse .pde .internal .core .ClasspathComputer ;
3436import org .eclipse .pde .internal .core .PDECore ;
@@ -180,8 +182,12 @@ public class ClasspathResolutionTest2 {
180182 */
181183 static final List <String > TRANSITIVE_FORBIDDEN_BUNDLES = List .of ("C" , "D" , "E" , "F" , "H" );
182184
183- /** All test bundles other than A. */
184- static final List <String > ALL_TEST_BUNDLES = List .of ("B" , "C" , "D" , "E" , "F" , "G" , "H" );
185+ static final List <String > ALL_TEST_BUNDLES = List .of ("B" , "C" , "D" , "E" , "F" , "G" , "H" , "cyclic-capabilities/Af" ,
186+ "cyclic-capabilities/cyclic.capabilities.provider" );
187+
188+ private static IProject projectConsumer ;
189+
190+ private static IClasspathEntry [] consumerClasspathEntries ;
185191
186192 @ BeforeClass
187193 public static void setupBeforeClass () throws Exception {
@@ -195,6 +201,8 @@ public static void setupBeforeClass() throws Exception {
195201 projectA = ProjectUtils .importTestProject ("tests/projects/A" );
196202 projectAe = ProjectUtils .importTestProject ("tests/projects/Ae" );
197203 projectAd = ProjectUtils .importTestProject ("tests/projects/Ad" );
204+ projectConsumer = ProjectUtils
205+ .importTestProject ("tests/projects/cyclic-capabilities/cyclic.capabilities.consumer" );
198206
199207 // Trigger a full workspace build. The workspace builder respects
200208 // project build order, so dependency bundles (B-H, X) are built
@@ -214,6 +222,7 @@ public static void setupBeforeClass() throws Exception {
214222 projectA .build (IncrementalProjectBuilder .FULL_BUILD , new NullProgressMonitor ());
215223 projectAe .build (IncrementalProjectBuilder .FULL_BUILD , new NullProgressMonitor ());
216224 projectAd .build (IncrementalProjectBuilder .FULL_BUILD , new NullProgressMonitor ());
225+ projectConsumer .build (IncrementalProjectBuilder .FULL_BUILD , new NullProgressMonitor ());
217226 TestUtils .waitForJobs ("ClasspathResolutionTest2.rebuild" , 200 , 30_000 );
218227
219228 // Compute PDE classpath entries (only plugin dependencies, no
@@ -225,6 +234,10 @@ public static void setupBeforeClass() throws Exception {
225234 IPluginModelBase modelAd = PDECore .getDefault ().getModelManager ().findModel (projectAd );
226235 assertNotNull ("PDE model for project Ad must be available" , modelAd );
227236 classpathEntriesAd = ClasspathComputer .computeClasspathEntries (modelAd , projectAd );
237+
238+ IPluginModelBase consumerModel = PDECore .getDefault ().getModelManager ().findModel (projectConsumer );
239+ consumerClasspathEntries = ClasspathComputer .computeClasspathEntries (consumerModel ,
240+ projectConsumer );
228241 }
229242
230243 // =========================================================================
@@ -566,9 +579,9 @@ record ForbiddenRef(int line, String qualifiedType, String project) {
566579 // A has forbiddenReference=warning → WARNING
567580 // Ae has forbiddenReference=error → ERROR
568581 assertThat (m .getAttribute (IMarker .SEVERITY , -1 ))
569- .as ("Line %d in project %s: must be %s " + "severity (forbiddenReference=%s in "
570- + "project settings)" ,
571- ref .line , project .getName (), severityLabel , severityLabel .toLowerCase ())
582+ .as ("Line %d in project %s: must be %s " + "severity (forbiddenReference=%s in "
583+ + "project settings)" ,
584+ ref .line , project .getName (), severityLabel , severityLabel .toLowerCase ())
572585 .isEqualTo (expectedSeverity );
573586
574587 // Problem ID must be ForbiddenReference because all
@@ -675,8 +688,8 @@ public void testDiscouragedMarkersOnProjectAd() throws Exception {
675688 .as ("Discouraged marker must be on line 44 " + "(x.internal.MyObject)" ).isEqualTo (44 );
676689
677690 assertThat (m .getAttribute (IMarker .SEVERITY , -1 ))
678- .as ("Discouraged access must be WARNING " + "(discouragedReference=warning in "
679- + "project settings)" )
691+ .as ("Discouraged access must be WARNING " + "(discouragedReference=warning in "
692+ + "project settings)" )
680693 .isEqualTo (IMarker .SEVERITY_WARNING );
681694
682695 // DiscouragedReference vs ForbiddenReference: K_DISCOURAGED
@@ -714,7 +727,13 @@ public void testDiscouragedMarkersOnProjectAd() throws Exception {
714727 */
715728 @ Test
716729 public void testDependencyBundlesBuildClean () throws Exception {
717- for (String bundleName : ALL_TEST_BUNDLES ) {
730+ for (String projectPath : ALL_TEST_BUNDLES ) {
731+ String bundleName ;
732+ if (projectPath .contains ("/" )) {
733+ bundleName = projectPath .split ("/" )[1 ];
734+ } else {
735+ bundleName = projectPath ;
736+ }
718737 IProject project = getProject (bundleName );
719738 IMarker [] markers = project .findMarkers (IMarker .PROBLEM , true , IResource .DEPTH_INFINITE );
720739
@@ -746,6 +765,40 @@ public void testClasspathComputationIsDeterministic() throws Exception {
746765 .containsExactlyInAnyOrderElementsOf (originalNames );
747766 }
748767
768+ // =========================================================================
769+ // Section 7: Cyclic capability resolution for fragments
770+ // =========================================================================
771+
772+ /**
773+ * A fragment that uses {@code Require-Capability} to depend on a bundle
774+ * that itself {@code Require-Bundle}s the fragment's host must not end up
775+ * with the capability provider on its classpath — that would create a
776+ * cyclic dependency. Only the host bundle ({@code Af}) should appear.
777+ * <p>
778+ * <b>Test bundle dependency graph:</b>
779+ * <pre>
780+ * Af: simple host bundle
781+ * cyclic.capabilities.provider: Provide-Capability: some.test.capability
782+ * Require-Bundle: Af
783+ * cyclic.capabilities.consumer: Require-Capability: some.test.capability
784+ * Fragment-Host: Af
785+ * </pre>
786+ */
787+ @ Test
788+ public void testRequiredPluginsViaCapabilityForFragment () throws Exception {
789+ List <String > entryNames = Arrays .stream (consumerClasspathEntries )
790+ .map (e -> e .getPath ().lastSegment ()).toList ();
791+ IPluginModelBase consumerModel = PDECore .getDefault ().getModelManager ().findModel (projectConsumer );
792+ assertThat (consumerModel ).isNotNull ();
793+ BundleDescription bundleDescription = consumerModel .getBundleDescription ();
794+ ResolverError [] resolverErrors = bundleDescription .getContainingState ().getResolverErrors (bundleDescription );
795+ assertThat (resolverErrors ).isEmpty ();
796+ assertThat (entryNames ).as ("Fragment host Af must be on the consumer's classpath" ).contains ("Af" );
797+ assertThat (entryNames )
798+ .as ("cyclic.capabilities.provider must NOT be on the classpath (would create cycle)" )
799+ .noneMatch (name -> name .contains ("cyclic.capabilities.provider" ));
800+ }
801+
749802 // =========================================================================
750803 // Utility methods
751804 // =========================================================================
0 commit comments