|
| 1 | +package test; |
| 2 | + |
| 3 | +import java.io.File; |
| 4 | +import java.util.Collection; |
| 5 | +import java.util.HashMap; |
| 6 | +import java.util.List; |
| 7 | +import java.util.Map; |
| 8 | +import java.util.SortedSet; |
| 9 | +import java.util.TreeSet; |
| 10 | + |
| 11 | +import org.assertj.core.api.SoftAssertions; |
| 12 | +import org.assertj.core.api.junit.jupiter.InjectSoftAssertions; |
| 13 | +import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension; |
| 14 | +import org.junit.jupiter.api.Test; |
| 15 | +import org.junit.jupiter.api.extension.ExtendWith; |
| 16 | +import org.osgi.framework.namespace.IdentityNamespace; |
| 17 | +import org.osgi.resource.Capability; |
| 18 | +import org.osgi.resource.Requirement; |
| 19 | + |
| 20 | +import aQute.bnd.build.Container; |
| 21 | +import aQute.bnd.build.Project; |
| 22 | +import aQute.bnd.build.Workspace; |
| 23 | +import aQute.bnd.osgi.Jar; |
| 24 | +import aQute.bnd.osgi.repository.BaseRepository; |
| 25 | +import aQute.bnd.osgi.repository.ResourcesRepository; |
| 26 | +import aQute.bnd.osgi.resource.CapReqBuilder; |
| 27 | +import aQute.bnd.osgi.resource.ResourceBuilder; |
| 28 | +import aQute.bnd.osgi.resource.ResourceUtils; |
| 29 | +import aQute.bnd.service.RepositoryPlugin; |
| 30 | +import aQute.bnd.service.Strategy; |
| 31 | +import aQute.bnd.test.jupiter.InjectTemporaryDirectory; |
| 32 | +import aQute.bnd.version.Version; |
| 33 | +import aQute.lib.io.IO; |
| 34 | + |
| 35 | +/** |
| 36 | + * Test the expansion of Eclipse features on -buildpath like paths. A feature |
| 37 | + * is a container of included bundles ({@code <plugin>}) and included features |
| 38 | + * ({@code <includes>}); on a path it expands to its member bundles. Required |
| 39 | + * features/plugins ({@code <requires>}) are dependencies and are not |
| 40 | + * expanded. |
| 41 | + */ |
| 42 | +@ExtendWith(SoftAssertionsExtension.class) |
| 43 | +public class FeatureBuildpathTest { |
| 44 | + private static final String FEATURE_TYPE = "org.eclipse.update.feature"; |
| 45 | + |
| 46 | + @InjectSoftAssertions |
| 47 | + SoftAssertions softly; |
| 48 | + |
| 49 | + @InjectTemporaryDirectory |
| 50 | + File tmp; |
| 51 | + |
| 52 | + /** |
| 53 | + * A repository mimicking the P2 repository semantics: bundles and |
| 54 | + * features can share bsn+version, get() dispatches on the requested |
| 55 | + * identity type, versions() only lists bundles, and the index is |
| 56 | + * queryable via the OSGi Repository API. |
| 57 | + */ |
| 58 | + static class FeatureRepo extends BaseRepository implements RepositoryPlugin { |
| 59 | + final ResourcesRepository index = new ResourcesRepository(); |
| 60 | + final Map<String, File> files = new HashMap<>(); |
| 61 | + final Map<String, TreeSet<Version>> bundleVersions = new HashMap<>(); |
| 62 | + |
| 63 | + void addBundle(String bsn, String version, File file) throws Exception { |
| 64 | + ResourceBuilder rb = new ResourceBuilder(); |
| 65 | + rb.addCapability(new CapReqBuilder(IdentityNamespace.IDENTITY_NAMESPACE) |
| 66 | + .addAttribute(IdentityNamespace.IDENTITY_NAMESPACE, bsn) |
| 67 | + .addAttribute(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE, IdentityNamespace.TYPE_BUNDLE) |
| 68 | + .addAttribute(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE, new Version(version))); |
| 69 | + index.add(rb.build()); |
| 70 | + files.put(bsn + ":" + version + ":" + IdentityNamespace.TYPE_BUNDLE, file); |
| 71 | + bundleVersions.computeIfAbsent(bsn, k -> new TreeSet<>()) |
| 72 | + .add(new Version(version)); |
| 73 | + } |
| 74 | + |
| 75 | + void addFeature(String id, String version, File file, Collection<Requirement> members) throws Exception { |
| 76 | + ResourceBuilder rb = new ResourceBuilder(); |
| 77 | + rb.addCapability(new CapReqBuilder(IdentityNamespace.IDENTITY_NAMESPACE) |
| 78 | + .addAttribute(IdentityNamespace.IDENTITY_NAMESPACE, id) |
| 79 | + .addAttribute(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE, FEATURE_TYPE) |
| 80 | + .addAttribute(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE, new Version(version))); |
| 81 | + for (Requirement member : members) { |
| 82 | + rb.addRequirement(CapReqBuilder.clone(member)); |
| 83 | + } |
| 84 | + index.add(rb.build()); |
| 85 | + files.put(id + ":" + version + ":" + FEATURE_TYPE, file); |
| 86 | + } |
| 87 | + |
| 88 | + @Override |
| 89 | + public File get(String bsn, Version version, Map<String, String> properties, DownloadListener... listeners) |
| 90 | + throws Exception { |
| 91 | + String requestedType = properties != null ? properties.get(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE) |
| 92 | + : null; |
| 93 | + String type = requestedType != null ? requestedType : IdentityNamespace.TYPE_BUNDLE; |
| 94 | + File file = files.get(bsn + ":" + version + ":" + type); |
| 95 | + if (file == null) |
| 96 | + return null; |
| 97 | + for (DownloadListener listener : listeners) { |
| 98 | + listener.success(file); |
| 99 | + } |
| 100 | + return file; |
| 101 | + } |
| 102 | + |
| 103 | + @Override |
| 104 | + public SortedSet<Version> versions(String bsn) throws Exception { |
| 105 | + TreeSet<Version> versions = bundleVersions.get(bsn); |
| 106 | + return versions != null ? versions : new TreeSet<>(); |
| 107 | + } |
| 108 | + |
| 109 | + @Override |
| 110 | + public List<String> list(String pattern) throws Exception { |
| 111 | + return List.copyOf(bundleVersions.keySet()); |
| 112 | + } |
| 113 | + |
| 114 | + @Override |
| 115 | + public Map<Requirement, Collection<Capability>> findProviders( |
| 116 | + Collection<? extends Requirement> requirements) { |
| 117 | + return index.findProviders(requirements); |
| 118 | + } |
| 119 | + |
| 120 | + @Override |
| 121 | + public PutResult put(java.io.InputStream stream, PutOptions options) throws Exception { |
| 122 | + throw new UnsupportedOperationException(); |
| 123 | + } |
| 124 | + |
| 125 | + @Override |
| 126 | + public boolean canWrite() { |
| 127 | + return false; |
| 128 | + } |
| 129 | + |
| 130 | + @Override |
| 131 | + public String getName() { |
| 132 | + return "FeatureRepo"; |
| 133 | + } |
| 134 | + |
| 135 | + @Override |
| 136 | + public String getLocation() { |
| 137 | + return "test"; |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + static Requirement pluginMember(String id, String version, Map<String, String> extra) throws Exception { |
| 142 | + CapReqBuilder builder = new CapReqBuilder(IdentityNamespace.IDENTITY_NAMESPACE) |
| 143 | + .addDirective("filter", "(&(osgi.identity=" + id + ")(version=" + version + "))") |
| 144 | + .addAttribute(ResourceUtils.FEATURE_RELATION_ATTRIBUTE, ResourceUtils.FEATURE_RELATION_PLUGIN) |
| 145 | + .addAttribute("id", id) |
| 146 | + .addAttribute("version", new Version(version)); |
| 147 | + if (extra != null) { |
| 148 | + extra.forEach(builder::addAttribute); |
| 149 | + } |
| 150 | + return builder.buildSyntheticRequirement(); |
| 151 | + } |
| 152 | + |
| 153 | + static Requirement includeMember(String id, String version, boolean optional) throws Exception { |
| 154 | + CapReqBuilder builder = new CapReqBuilder(IdentityNamespace.IDENTITY_NAMESPACE) |
| 155 | + .addDirective("filter", |
| 156 | + "(&(osgi.identity=" + id + ")(type=" + FEATURE_TYPE + ")(version=" + version + "))") |
| 157 | + .addAttribute(ResourceUtils.FEATURE_RELATION_ATTRIBUTE, ResourceUtils.FEATURE_RELATION_INCLUDE) |
| 158 | + .addAttribute("id", id) |
| 159 | + .addAttribute(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE, FEATURE_TYPE) |
| 160 | + .addAttribute("version", new Version(version)); |
| 161 | + if (optional) { |
| 162 | + builder.addDirective("resolution", "optional"); |
| 163 | + } |
| 164 | + return builder.buildSyntheticRequirement(); |
| 165 | + } |
| 166 | + |
| 167 | + static Requirement requireMember(String id) throws Exception { |
| 168 | + return new CapReqBuilder(IdentityNamespace.IDENTITY_NAMESPACE) |
| 169 | + .addDirective("filter", "(osgi.identity=" + id + ")") |
| 170 | + .addAttribute(ResourceUtils.FEATURE_RELATION_ATTRIBUTE, ResourceUtils.FEATURE_RELATION_REQUIRE) |
| 171 | + .addAttribute("id", id) |
| 172 | + .buildSyntheticRequirement(); |
| 173 | + } |
| 174 | + |
| 175 | + File dummyJar(String name) throws Exception { |
| 176 | + File file = new File(tmp, name + ".jar"); |
| 177 | + try (Jar jar = new Jar(name)) { |
| 178 | + jar.putResource("about.txt", new aQute.bnd.osgi.EmbeddedResource(name.getBytes(), 0L)); |
| 179 | + jar.write(file); |
| 180 | + } |
| 181 | + return file; |
| 182 | + } |
| 183 | + |
| 184 | + private Workspace getWorkspace() throws Exception { |
| 185 | + File wsDir = new File(tmp, "ws"); |
| 186 | + IO.copy(IO.getFile("testresources/ws"), wsDir); |
| 187 | + return new Workspace(wsDir); |
| 188 | + } |
| 189 | + |
| 190 | + @Test |
| 191 | + public void expandFeatureWithNestedIncludeOnBuildpath() throws Exception { |
| 192 | + try (Workspace ws = getWorkspace()) { |
| 193 | + FeatureRepo repo = new FeatureRepo(); |
| 194 | + repo.addBundle("bundle.a", "1.0.0", dummyJar("bundle.a-1.0.0")); |
| 195 | + repo.addBundle("bundle.b", "2.0.0", dummyJar("bundle.b-2.0.0")); |
| 196 | + repo.addBundle("bundle.c", "3.0.0", dummyJar("bundle.c-3.0.0")); |
| 197 | + repo.addFeature("sub.feature", "2.0.0", dummyJar("sub.feature-2.0.0"), |
| 198 | + List.of(pluginMember("bundle.c", "3.0.0", null))); |
| 199 | + repo.addFeature("test.feature", "1.0.0", dummyJar("test.feature-1.0.0"), List.of( |
| 200 | + pluginMember("bundle.a", "1.0.0", null), // |
| 201 | + pluginMember("bundle.b", "2.0.0", null), // |
| 202 | + includeMember("sub.feature", "2.0.0", false), // |
| 203 | + requireMember("required.bundle.not.in.repo"))); |
| 204 | + ws.addBasicPlugin(repo); |
| 205 | + |
| 206 | + try (Project project = ws.getProject("p1")) { |
| 207 | + List<Container> containers = project.getBundles(Strategy.LOWEST, |
| 208 | + "test.feature;version='1.0.0';type=org.eclipse.update.feature", "-buildpath"); |
| 209 | + |
| 210 | + softly.assertThat(project.check()) |
| 211 | + .as("no errors or warnings expected") |
| 212 | + .isTrue(); |
| 213 | + softly.assertThat(containers) |
| 214 | + .extracting(Container::getBundleSymbolicName) |
| 215 | + .containsExactly("bundle.a", "bundle.b", "bundle.c"); |
| 216 | + softly.assertThat(containers) |
| 217 | + .allMatch(c -> c.getType() == Container.TYPE.REPO) |
| 218 | + .allMatch(c -> c.getError() == null); |
| 219 | + } |
| 220 | + } |
| 221 | + } |
| 222 | + |
| 223 | + @Test |
| 224 | + public void expandFeatureSkipsForeignPlatformMembers() throws Exception { |
| 225 | + try (Workspace ws = getWorkspace()) { |
| 226 | + FeatureRepo repo = new FeatureRepo(); |
| 227 | + repo.addBundle("bundle.a", "1.0.0", dummyJar("bundle.a-1.0.0")); |
| 228 | + repo.addBundle("bundle.foreign", "1.0.0", dummyJar("bundle.foreign-1.0.0")); |
| 229 | + repo.addFeature("test.feature", "1.0.0", dummyJar("test.feature-1.0.0"), List.of( |
| 230 | + pluginMember("bundle.a", "1.0.0", null), // |
| 231 | + pluginMember("bundle.foreign", "1.0.0", Map.of("os", "qnx")))); |
| 232 | + ws.addBasicPlugin(repo); |
| 233 | + |
| 234 | + try (Project project = ws.getProject("p1")) { |
| 235 | + List<Container> containers = project.getBundles(Strategy.LOWEST, |
| 236 | + "test.feature;version='1.0.0';type=org.eclipse.update.feature", "-buildpath"); |
| 237 | + |
| 238 | + softly.assertThat(project.check()) |
| 239 | + .isTrue(); |
| 240 | + softly.assertThat(containers) |
| 241 | + .extracting(Container::getBundleSymbolicName) |
| 242 | + .containsExactly("bundle.a"); |
| 243 | + } |
| 244 | + } |
| 245 | + } |
| 246 | + |
| 247 | + @Test |
| 248 | + public void expandFeatureCycleTerminates() throws Exception { |
| 249 | + try (Workspace ws = getWorkspace()) { |
| 250 | + FeatureRepo repo = new FeatureRepo(); |
| 251 | + repo.addBundle("bundle.a", "1.0.0", dummyJar("bundle.a-1.0.0")); |
| 252 | + repo.addBundle("bundle.b", "1.0.0", dummyJar("bundle.b-1.0.0")); |
| 253 | + repo.addFeature("feature.a", "1.0.0", dummyJar("feature.a-1.0.0"), List.of( |
| 254 | + pluginMember("bundle.a", "1.0.0", null), // |
| 255 | + includeMember("feature.b", "1.0.0", false))); |
| 256 | + repo.addFeature("feature.b", "1.0.0", dummyJar("feature.b-1.0.0"), List.of( |
| 257 | + pluginMember("bundle.b", "1.0.0", null), // |
| 258 | + includeMember("feature.a", "1.0.0", false))); |
| 259 | + ws.addBasicPlugin(repo); |
| 260 | + |
| 261 | + try (Project project = ws.getProject("p1")) { |
| 262 | + List<Container> containers = project.getBundles(Strategy.LOWEST, |
| 263 | + "feature.a;version='1.0.0';type=org.eclipse.update.feature", "-buildpath"); |
| 264 | + |
| 265 | + softly.assertThat(project.check("Detected a cycle in the includes of feature")) |
| 266 | + .isTrue(); |
| 267 | + softly.assertThat(containers) |
| 268 | + .extracting(Container::getBundleSymbolicName) |
| 269 | + .containsExactly("bundle.a", "bundle.b"); |
| 270 | + } |
| 271 | + } |
| 272 | + } |
| 273 | + |
| 274 | + @Test |
| 275 | + public void expandFeatureFallsBackToHighestWhenExactVersionMissing() throws Exception { |
| 276 | + try (Workspace ws = getWorkspace()) { |
| 277 | + FeatureRepo repo = new FeatureRepo(); |
| 278 | + repo.addBundle("bundle.a", "1.0.0", dummyJar("bundle.a-1.0.0")); |
| 279 | + repo.addFeature("test.feature", "1.0.0", dummyJar("test.feature-1.0.0"), |
| 280 | + List.of(pluginMember("bundle.a", "9.9.9", null))); |
| 281 | + ws.addBasicPlugin(repo); |
| 282 | + |
| 283 | + try (Project project = ws.getProject("p1")) { |
| 284 | + List<Container> containers = project.getBundles(Strategy.LOWEST, |
| 285 | + "test.feature;version='1.0.0';type=org.eclipse.update.feature", "-buildpath"); |
| 286 | + |
| 287 | + softly.assertThat(project |
| 288 | + .check("Member bundle.a;version=9.9.9 of feature test.feature:1.0.0 not found with the exact")) |
| 289 | + .isTrue(); |
| 290 | + softly.assertThat(containers) |
| 291 | + .extracting(Container::getBundleSymbolicName) |
| 292 | + .containsExactly("bundle.a"); |
| 293 | + softly.assertThat(containers.get(0) |
| 294 | + .getVersion()) |
| 295 | + .isEqualTo("1.0.0"); |
| 296 | + } |
| 297 | + } |
| 298 | + } |
| 299 | + |
| 300 | + @Test |
| 301 | + public void expandFeatureSkipsMissingOptionalInclude() throws Exception { |
| 302 | + try (Workspace ws = getWorkspace()) { |
| 303 | + FeatureRepo repo = new FeatureRepo(); |
| 304 | + repo.addBundle("bundle.a", "1.0.0", dummyJar("bundle.a-1.0.0")); |
| 305 | + repo.addFeature("test.feature", "1.0.0", dummyJar("test.feature-1.0.0"), List.of( |
| 306 | + pluginMember("bundle.a", "1.0.0", null), // |
| 307 | + includeMember("no.such.feature", "1.0.0", true))); |
| 308 | + ws.addBasicPlugin(repo); |
| 309 | + |
| 310 | + try (Project project = ws.getProject("p1")) { |
| 311 | + List<Container> containers = project.getBundles(Strategy.LOWEST, |
| 312 | + "test.feature;version='1.0.0';type=org.eclipse.update.feature", "-buildpath"); |
| 313 | + |
| 314 | + softly.assertThat(project.check()) |
| 315 | + .as("missing optional include must not cause errors") |
| 316 | + .isTrue(); |
| 317 | + softly.assertThat(containers) |
| 318 | + .extracting(Container::getBundleSymbolicName) |
| 319 | + .containsExactly("bundle.a"); |
| 320 | + } |
| 321 | + } |
| 322 | + } |
| 323 | + |
| 324 | + @Test |
| 325 | + public void missingFeatureReportsError() throws Exception { |
| 326 | + try (Workspace ws = getWorkspace()) { |
| 327 | + ws.addBasicPlugin(new FeatureRepo()); |
| 328 | + |
| 329 | + try (Project project = ws.getProject("p1")) { |
| 330 | + List<Container> containers = project.getBundles(Strategy.LOWEST, |
| 331 | + "no.such.feature;version='1.0.0';type=org.eclipse.update.feature", "-buildpath"); |
| 332 | + |
| 333 | + softly.assertThat(project.check()) |
| 334 | + .as("the error is carried by the container and reported by doPath later") |
| 335 | + .isTrue(); |
| 336 | + softly.assertThat(containers) |
| 337 | + .hasSize(1); |
| 338 | + softly.assertThat(containers.get(0) |
| 339 | + .getType()) |
| 340 | + .isEqualTo(Container.TYPE.ERROR); |
| 341 | + softly.assertThat(containers.get(0) |
| 342 | + .getError()) |
| 343 | + .contains("Not found in"); |
| 344 | + } |
| 345 | + } |
| 346 | + } |
| 347 | +} |
0 commit comments