diff --git a/tycho-core/pom.xml b/tycho-core/pom.xml index 512fad0dcb..ea6f9bae5a 100644 --- a/tycho-core/pom.xml +++ b/tycho-core/pom.xml @@ -312,7 +312,18 @@ org.eclipse.jdt.core 3.33.0 - + + org.eclipse.pde + org.eclipse.pde.core + 3.16.100 + test + + + * + * + + + \ No newline at end of file diff --git a/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/AbstractMavenTargetTest.java b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/AbstractMavenTargetTest.java new file mode 100644 index 0000000000..cb260a500f --- /dev/null +++ b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/AbstractMavenTargetTest.java @@ -0,0 +1,225 @@ +/******************************************************************************* + * Copyright (c) 2023, 2023 Hannes Wellmann and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Hannes Wellmann - initial API and implementation + *******************************************************************************/ +package org.eclipse.m2e.pde.target.tests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.ServiceLoader.Provider; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.stream.Collectors; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.URIUtil; +import org.eclipse.equinox.frameworkadmin.BundleInfo; +import org.eclipse.m2e.pde.target.tests.spi.TargetLocationLoader; +import org.eclipse.pde.core.target.ITargetLocation; +import org.eclipse.pde.core.target.NameVersionDescriptor; +import org.eclipse.pde.core.target.TargetBundle; +import org.eclipse.pde.core.target.TargetFeature; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; + +public abstract class AbstractMavenTargetTest { + static final String SOURCE_BUNDLE_SUFFIX = ".source"; + static final TargetBundle[] EMPTY = {}; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private static ServiceLoader LOCATION_LOADER = ServiceLoader.load(TargetLocationLoader.class, + AbstractMavenTargetTest.class.getClassLoader()); + private static TargetLocationLoader loader; + + ITargetLocation resolveMavenTarget(String targetXML) throws Exception { + return getLoader().resolveMavenTarget(targetXML, temporaryFolder.newFolder()); + } + + private static TargetLocationLoader getLoader() { + if (loader == null) { + Provider provider = LOCATION_LOADER.stream().sorted().findFirst().orElseThrow( + () -> new IllegalStateException("No TargetLocationLoader found on classpath of test!")); + loader = provider.get(); + } + return loader; + } + + protected static void assertStatusOk(IStatus status) { + if (!status.isOK()) { + throw new AssertionError(status.toString(), status.getException()); + } + } + + // --- common assertion utilities --- + + private static Map assertTargetContent(List expectedUnits, T[] allUnit, + BiPredicate matcher, Function getLocation, Predicate isSourceUnit, + Function getSourceTarget, Function toString) { + + Map units = new HashMap<>(); + List allElements = new ArrayList<>(Arrays.asList(allUnit)); + for (U expectedUnit : expectedUnits) { + List matchingUnits = allElements.stream().filter(u -> matcher.test(expectedUnit, u)).toList(); + + if (matchingUnits.isEmpty()) { + fail("Expected unit is missing: " + expectedUnit); + } else if (matchingUnits.size() == 1) { + T targetUnit = matchingUnits.get(0); + allElements.remove(targetUnit); + + assertEquals("Unexpected 'original' state of " + targetUnit, expectedUnit.isOriginal(), + isOriginalArtifact(expectedUnit, targetUnit, getLocation)); + assertEquals("Unexpected 'isSource' state of " + targetUnit, expectedUnit.isSourceBundle(), + isSourceUnit.test(targetUnit)); + if (expectedUnit.isSourceBundle()) { + String expectedSourceTarget = expectedUnit.id().substring(0, + expectedUnit.id().length() - SOURCE_BUNDLE_SUFFIX.length()); + assertEquals("Source target id", expectedSourceTarget, getSourceTarget.apply(targetUnit)); + } else { + assertNull(getSourceTarget.apply(targetUnit)); + } + units.put(expectedUnit, targetUnit); + } else { + fail("Expected bundle contaiend multiple times:" + expectedUnit); + } + } + if (!allElements.isEmpty()) { + String unepxectedBundlesList = allElements.stream().map(u -> " " + toString.apply(u)) + .collect(Collectors.joining("\n")); + fail("Encoutnered the following unexpected bundles:" + unepxectedBundlesList); + } + return units; + } + + private static boolean isOriginalArtifact(ExpectedUnit expectedUnit, T unit, Function getLocation) { + ArtifactKey key = expectedUnit.key(); + if (key == null) { + return false; + } + URI location = getLocation.apply(unit); + String expectedPathSuffix = "/" + String.join("/", ".m2", "repository", key.groupId().replace('.', '/'), + key.artifactId(), key.version(), key.artifactId() + "-" + key.version() + ".jar"); + return location.toASCIIString().endsWith(expectedPathSuffix); + } + + // --- assertion utilities for Bundles in target --- + + static ExpectedBundle originalOSGiBundle(String bsn, String version, String groupArtifact) { + return originalOSGiBundle(bsn, version, groupArtifact, version); + } + + static ExpectedBundle originalOSGiBundle(String bsn, String version, String groupArtifact, String mavenVersion) { + return new ExpectedBundle(bsn, version, false, true, + ArtifactKey.fromPortableString(groupArtifact + ":" + mavenVersion + "::")); + } + + static ExpectedBundle generatedBundle(String bsn, String version, String groupArtifact) { + return new ExpectedBundle(bsn, version, false, false, + ArtifactKey.fromPortableString(groupArtifact + ":" + version + "::")); + } + + static List withSourceBundles(List mainBundles) { + return mainBundles.stream().mapMulti((unit, downStream) -> { + downStream.accept(unit); + String sourceId = unit.bsn() + SOURCE_BUNDLE_SUFFIX; + ExpectedBundle sourceUnit = new ExpectedBundle(sourceId, unit.version(), true, false, unit.key()); + downStream.accept(sourceUnit); + }).toList(); + } + + static Attributes getManifestMainAttributes(TargetBundle targetBundle) throws IOException { + BundleInfo bundleInfo = targetBundle.getBundleInfo(); + File file = URIUtil.toFile(bundleInfo.getLocation()); + try (var jar = new JarFile(file)) { + return jar.getManifest().getMainAttributes(); + } + } + + static void assertTargetBundles(ITargetLocation target, List expectedUnits) { + assertTargetContent(expectedUnits, target.getBundles(), // + (expectedBundle, bundle) -> { + BundleInfo info = bundle.getBundleInfo(); + return expectedBundle.bsn().equals(info.getSymbolicName()) + && expectedBundle.version().equals(info.getVersion()); + }, // + tb -> tb.getBundleInfo().getLocation(), // + tb -> tb.isSourceBundle(), + tb -> tb.getSourceTarget() != null ? tb.getSourceTarget().getSymbolicName() : null, + tb -> tb.getBundleInfo().getSymbolicName() + ":" + tb.getBundleInfo().getVersion()); + } + + // --- assertion utilities for Features in a target --- + + static ExpectedFeature originalFeature(String id, String version, String groupArtifact, + List containedPlugins) { + ArtifactKey key = ArtifactKey.fromPortableString(groupArtifact + ":" + version + "::"); + return new ExpectedFeature(id, version, false, true, key, containedPlugins); + } + + static ExpectedFeature generatedFeature(String id, String version, List containedPlugins) { + return new ExpectedFeature(id, version, false, false, null, containedPlugins); + } + + static NameVersionDescriptor featurePlugin(String bsn, String version) { + return new NameVersionDescriptor(bsn, version); + } + + static List withSourceFeatures(List mainFeatures) { + return mainFeatures.stream().mapMulti((feature, downStream) -> { + downStream.accept(feature); + String sourceId = feature.id() + SOURCE_BUNDLE_SUFFIX; + List sourcePlugins = feature.containedPlugins().stream() + .map(d -> featurePlugin(d.getId() + SOURCE_BUNDLE_SUFFIX, d.getVersion())).toList(); + ExpectedFeature sourceUnit = new ExpectedFeature(sourceId, feature.version(), true, false, feature.key(), + sourcePlugins); + downStream.accept(sourceUnit); + }).toList(); + } + + static Map assertTargetFeatures(ITargetLocation target, + List expectedFeatures) { + var encounteredFeatures = assertTargetContent(expectedFeatures, target.getFeatures(), // + (expectedFeature, feature) -> expectedFeature.id().equals(feature.getId()) + && expectedFeature.version().equals(feature.getVersion()), // + f -> Path.of(f.getLocation()).toUri(), // + f -> isSourceFeature(f), // + f -> isSourceFeature(f) ? f.getId().substring(0, f.getId().length() - SOURCE_BUNDLE_SUFFIX.length()) + : null, // + f -> f.getId() + ":" + f.getVersion()); + encounteredFeatures.forEach((expectedFeature, feature) -> { + assertEquals(Set.copyOf(expectedFeature.containedPlugins()), Set.of(feature.getPlugins())); + }); + return encounteredFeatures; + } + + private static boolean isSourceFeature(TargetFeature f) { + return f.getId().endsWith(SOURCE_BUNDLE_SUFFIX) + && Arrays.stream(f.getPlugins()).allMatch(d -> d.getId().endsWith(SOURCE_BUNDLE_SUFFIX)); + } +} diff --git a/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/ArtifactKey.java b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/ArtifactKey.java new file mode 100644 index 0000000000..098af99152 --- /dev/null +++ b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/ArtifactKey.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2008-2022 Sonatype, Inc. + * All rights reserved. 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: + * Sonatype, Inc. - initial API and implementation + * Hannes Wellmann - Convert to record + *******************************************************************************/ + +package org.eclipse.m2e.pde.target.tests; + +import java.io.Serializable; + +import org.eclipse.osgi.util.NLS; + +public record ArtifactKey(String groupId, String artifactId, String version, String classifier) + implements Serializable { + public static ArtifactKey fromPortableString(String str) { + int p = 0; + int c = nextColonIndex(str, p); + String groupId = substring(str, p, c); + + p = c + 1; + c = nextColonIndex(str, p); + String artifactId = substring(str, p, c); + + p = c + 1; + c = nextColonIndex(str, p); + String version = substring(str, p, c); + + p = c + 1; + c = nextColonIndex(str, p); + String classifier = substring(str, p, c); + + return new ArtifactKey(groupId, artifactId, version, classifier); + } + + private static String substring(String str, int start, int end) { + String substring = str.substring(start, end); + return "".equals(substring) ? null : substring; //$NON-NLS-1$ + } + + private static int nextColonIndex(String str, int pos) { + int idx = str.indexOf(':', pos); + if (idx < 0) { + throw new IllegalArgumentException(NLS.bind("Invalid portable string: {0}", str)); + } + return idx; + } +} diff --git a/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/DependencyExclusionTest.java b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/DependencyExclusionTest.java new file mode 100644 index 0000000000..28613dddb0 --- /dev/null +++ b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/DependencyExclusionTest.java @@ -0,0 +1,171 @@ +/******************************************************************************* + * Copyright (c) 2023, 2023 Hannes Wellmann and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Hannes Wellmann - initial API and implementation + *******************************************************************************/ +package org.eclipse.m2e.pde.target.tests; + +import static org.junit.Assert.assertArrayEquals; + +import java.util.List; + +import org.eclipse.pde.core.target.ITargetLocation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class DependencyExclusionTest extends AbstractMavenTargetTest { + @Parameter(0) + public Boolean includeSource; + + @Parameters(name = "includeSource={0}") + public static List dependencyConfigurations() { + return List.of(false, true); + } + + @Test + public void testExclusionOfDirectRequirement() throws Exception { + ITargetLocation target = resolveMavenTarget(String.format( + """ + + + + org.junit.platform + junit-platform-commons + 1.9.3 + jar + + + org.apiguardian:apiguardian-api:1.1.2 + + """, + includeSource)); + assertStatusOk(target.getStatus()); + assertArrayEquals(EMPTY, target.getFeatures()); + List expectedBundles = List.of(junitPlatformCommons("1.9.3")); + assertTargetBundles(target, includeSource ? withSourceBundles(expectedBundles) : expectedBundles); + } + + @Test + public void testExclusionOfDirectAndTransitivRequirement() throws Exception { + ITargetLocation target = resolveMavenTarget(String + .format( + """ + + + + org.junit.jupiter + junit-jupiter-api + 5.9.3 + jar + + + org.apiguardian:apiguardian-api:1.1.2 + + """, + includeSource)); + assertStatusOk(target.getStatus()); + assertArrayEquals(EMPTY, target.getFeatures()); + List expectedBundles = List.of(// + junitJupiterAPI(), // + junitPlatformCommons("1.9.3"), // + opentest4j()); + assertTargetBundles(target, includeSource ? withSourceBundles(expectedBundles) : expectedBundles); + + } + + @Test + public void testExclusionOfMultipleVersions() throws Exception { + ITargetLocation target = resolveMavenTarget(String + .format( + """ + + + + org.junit.jupiter + junit-jupiter-api + 5.9.3 + jar + + + org.junit.platform + junit-platform-commons + 1.7.2 + jar + + + org.apiguardian:apiguardian-api:1.1.2 + org.apiguardian:apiguardian-api:1.1.0 + + """, + includeSource)); + assertStatusOk(target.getStatus()); + assertArrayEquals(EMPTY, target.getFeatures()); + List expectedBundles = List.of(// + junitJupiterAPI(), // + junitPlatformCommons("1.9.3"), // + junitPlatformCommons("1.7.2"), // + opentest4j()); + assertTargetBundles(target, includeSource ? withSourceBundles(expectedBundles) : expectedBundles); + } + + @Test + public void testExclusionOfDifferentVersions() throws Exception { + ITargetLocation target = resolveMavenTarget(String + .format( + """ + + + + org.junit.jupiter + junit-jupiter-api + 5.9.3 + jar + + + org.junit.platform + junit-platform-commons + 1.7.2 + jar + + + org.apiguardian:apiguardian-api:1.1.0 + + """, + includeSource)); + assertStatusOk(target.getStatus()); + assertArrayEquals(EMPTY, target.getFeatures()); + List expectedBundles = List.of(// + junitJupiterAPI(), // + junitPlatformCommons("1.9.3"), // + junitPlatformCommons("1.7.2"), // + apiGuardian("1.1.2"), opentest4j()); + assertTargetBundles(target, includeSource ? withSourceBundles(expectedBundles) : expectedBundles); + } + + private static ExpectedBundle junitPlatformCommons(String version) { + return originalOSGiBundle("junit-platform-commons", version, "org.junit.platform:junit-platform-commons"); + } + + private static ExpectedBundle junitJupiterAPI() { + return originalOSGiBundle("junit-jupiter-api", "5.9.3", "org.junit.jupiter:junit-jupiter-api"); + } + + private static ExpectedBundle apiGuardian(String version) { + return originalOSGiBundle("org.apiguardian.api", version, "org.apiguardian:apiguardian-api"); + } + + private static ExpectedBundle opentest4j() { + return originalOSGiBundle("org.opentest4j", "1.2.0", "org.opentest4j:opentest4j"); + } +} diff --git a/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/ExpectedBundle.java b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/ExpectedBundle.java new file mode 100644 index 0000000000..eea73c5a39 --- /dev/null +++ b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/ExpectedBundle.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2023, 2023 Hannes Wellmann and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Hannes Wellmann - initial API and implementation + *******************************************************************************/ +package org.eclipse.m2e.pde.target.tests; + +public record ExpectedBundle(String bsn, String version, boolean isSourceBundle, boolean isOriginal, + ArtifactKey key) implements ExpectedUnit { + + @Override + public String id() { + return bsn(); + } + + @Override + public String toString() { + return bsn + ":" + version; + } +} \ No newline at end of file diff --git a/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/ExpectedFeature.java b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/ExpectedFeature.java new file mode 100644 index 0000000000..d5bc4a18ac --- /dev/null +++ b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/ExpectedFeature.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2023, 2023 Hannes Wellmann and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Hannes Wellmann - initial API and implementation + *******************************************************************************/ +package org.eclipse.m2e.pde.target.tests; + +import java.util.List; + +import org.eclipse.pde.core.target.NameVersionDescriptor; + +public record ExpectedFeature(String id, String version, boolean isSourceBundle, boolean isOriginal, + ArtifactKey key, List containedPlugins) implements ExpectedUnit { + + @Override + public String toString() { + return id + ":" + version; + } +} \ No newline at end of file diff --git a/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/ExpectedUnit.java b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/ExpectedUnit.java new file mode 100644 index 0000000000..0f06e28c4e --- /dev/null +++ b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/ExpectedUnit.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * Copyright (c) 2023, 2023 Hannes Wellmann and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Hannes Wellmann - initial API and implementation + *******************************************************************************/ +package org.eclipse.m2e.pde.target.tests; +interface ExpectedUnit { + + String id(); + + boolean isSourceBundle(); + + boolean isOriginal(); + + ArtifactKey key(); +} \ No newline at end of file diff --git a/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/MavenFeatureTest.java b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/MavenFeatureTest.java new file mode 100644 index 0000000000..0e92df62b0 --- /dev/null +++ b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/MavenFeatureTest.java @@ -0,0 +1,187 @@ +/******************************************************************************* + * Copyright (c) 2023, 2023 Hannes Wellmann and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Hannes Wellmann - initial API and implementation + *******************************************************************************/ +package org.eclipse.m2e.pde.target.tests; + +import java.util.List; + +import org.eclipse.pde.core.target.ITargetLocation; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class MavenFeatureTest extends AbstractMavenTargetTest { + @Parameter(0) + public Boolean includeSource; + + @Parameters(name = "includeSource={0}") + public static List dependencyConfigurations() { + return List.of(false, true); + } + + @Test + public void testLocationContentFeatureGeneration() throws Exception { + ITargetLocation target = resolveMavenTarget(String + .format( + """ + + + + This is a great feature + + + My copyright description. + + + No license granted. + + + + + + org.junit.jupiter + junit-jupiter-api + 5.9.3 + jar + + + + """, + includeSource)); + assertStatusOk(target.getStatus()); + List expectedBundles = List.of( // + junitJupiterAPI(), // + junitPlatformCommons(), // + apiGuardian(), opentest4j()); + assertTargetBundles(target, includeSource ? withSourceBundles(expectedBundles) : expectedBundles); + List expectedFeature = List.of(generatedFeature("my.junit.feature", "1.2.3.qualifier", List.of(// + featurePlugin("junit-jupiter-api", "5.9.3"), // + featurePlugin("junit-platform-commons", "1.9.3"), // + featurePlugin("org.apiguardian.api", "1.1.2"), // + featurePlugin("org.opentest4j", "1.2.0"), // + featurePlugin("slf4j.api", null)))); + List expectedFeatures = includeSource ? withSourceFeatures(expectedFeature) : expectedFeature; + expectedFeatures = expectedFeatures.stream().map(f -> { + if (f.containedPlugins().stream().noneMatch(d -> d.getId().equals("slf4j.api.source"))) { + return f; + } + // Explicitly listed Plug-ins are just removed for a Source-Feature, if they are + // (probably) not a source Plug-in and are not mapped to a source Plug-in. + return new ExpectedFeature(f.id(), f.version(), f.isSourceBundle(), f.isOriginal(), f.key(), + f.containedPlugins().stream().filter(d -> !d.getId().equals("slf4j.api.source")).toList()); + }).toList(); + assertTargetFeatures(target, expectedFeatures); + } + + @Test + public void testPomArtifactFeatureGeneration() throws Exception { + ITargetLocation target = resolveMavenTarget(String + .format( + """ + + + + com.sun.xml.bind + jaxb-ri + 4.0.2 + pom + + + + """, + includeSource)); + assertStatusOk(target.getStatus()); + List expectedBundles = List.of( // + originalOSGiBundle("com.sun.xml.bind.jaxb-core", "4.0.2", "com.sun.xml.bind:jaxb-core"), + originalOSGiBundle("com.sun.xml.bind.jaxb-impl", "4.0.2", "com.sun.xml.bind:jaxb-impl"), + originalOSGiBundle("com.sun.xml.bind.jaxb-jxc", "4.0.2", "com.sun.xml.bind:jaxb-jxc"), + originalOSGiBundle("com.sun.xml.bind.jaxb-xjc", "4.0.2", "com.sun.xml.bind:jaxb-xjc"), + originalOSGiBundle("com.sun.xml.fastinfoset.FastInfoset", "2.1.0", + "com.sun.xml.fastinfoset:FastInfoset"), + originalOSGiBundle("jakarta.activation-api", "2.1.1", "jakarta.activation:jakarta.activation-api"), + originalOSGiBundle("jakarta.xml.bind-api", "4.0.0", "jakarta.xml.bind:jakarta.xml.bind-api"), + originalOSGiBundle("org.jvnet.staxex.stax-ex", "2.1.0", "org.jvnet.staxex:stax-ex")); + assertTargetBundles(target, includeSource ? withSourceBundles(expectedBundles) : expectedBundles); + List expectedFeature = List.of(generatedFeature("com.sun.xml.bind.jaxb-ri.pom", "0.0.1", + List.of(// + featurePlugin("com.sun.xml.bind.jaxb-core", "4.0.2"), + featurePlugin("com.sun.xml.bind.jaxb-impl", "4.0.2"), + featurePlugin("com.sun.xml.bind.jaxb-jxc", "4.0.2"), + featurePlugin("com.sun.xml.bind.jaxb-xjc", "4.0.2"), + featurePlugin("com.sun.xml.fastinfoset.FastInfoset", "2.1.0"), + featurePlugin("jakarta.activation-api", "2.1.1"), + featurePlugin("jakarta.xml.bind-api", "4.0.0"), + featurePlugin("org.jvnet.staxex.stax-ex", "2.1.0")))); + assertTargetFeatures(target, includeSource ? withSourceFeatures(expectedFeature) : expectedFeature); + + } + + @Test + public void testFeatureArtifact() throws Exception { + // TODO: For real feature artifacts, which don't have a source-artifact, a + // source feature is not generated (yet). + Assume.assumeFalse(includeSource); + ITargetLocation target = resolveMavenTarget(String + .format( + """ + + + + org.eclipse.vorto + org.eclipse.vorto.feature + 1.0.0 + jar + + + + """, + includeSource)); + assertStatusOk(target.getStatus()); + assertTargetBundles(target, List.of()); + List expectedFeature = List.of(generatedFeature("org.eclipse.vorto.feature", "1.0.0", List.of(// + featurePlugin("org.eclipse.vorto.core", "1.0.0"), // + featurePlugin("org.eclipse.vorto.editor", "1.0.0"), // + featurePlugin("org.eclipse.vorto.editor.datatype", "1.0.0"), + featurePlugin("org.eclipse.vorto.editor.datatype.ide", "1.0.0"), + featurePlugin("org.eclipse.vorto.editor.datatype.ui", "1.0.0"), + featurePlugin("org.eclipse.vorto.editor.functionblock", "1.0.0"), + featurePlugin("org.eclipse.vorto.editor.functionblock.ide", "1.0.0"), + featurePlugin("org.eclipse.vorto.editor.functionblock.ui", "1.0.0"), + featurePlugin("org.eclipse.vorto.editor.infomodel", "1.0.0"), + featurePlugin("org.eclipse.vorto.editor.infomodel.ide", "1.0.0"), + featurePlugin("org.eclipse.vorto.editor.infomodel.ui", "1.0.0"), + featurePlugin("org.eclipse.vorto.editor.mapping", "1.0.0"), + featurePlugin("org.eclipse.vorto.editor.mapping.ide", "1.0.0"), + featurePlugin("org.eclipse.vorto.editor.mapping.ui", "1.0.0")))); + assertTargetFeatures(target, includeSource ? withSourceFeatures(expectedFeature) : expectedFeature); + } + + private static ExpectedBundle junitPlatformCommons() { + return originalOSGiBundle("junit-platform-commons", "1.9.3", "org.junit.platform:junit-platform-commons"); + } + + private static ExpectedBundle junitJupiterAPI() { + return originalOSGiBundle("junit-jupiter-api", "5.9.3", "org.junit.jupiter:junit-jupiter-api"); + } + + private static ExpectedBundle apiGuardian() { + return originalOSGiBundle("org.apiguardian.api", "1.1.2", "org.apiguardian:apiguardian-api"); + } + + private static ExpectedBundle opentest4j() { + return originalOSGiBundle("org.opentest4j", "1.2.0", "org.opentest4j:opentest4j"); + } +} diff --git a/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/MavenTargetsTransitiveDependenciesTest.java b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/MavenTargetsTransitiveDependenciesTest.java new file mode 100644 index 0000000000..1efd5b8435 --- /dev/null +++ b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/MavenTargetsTransitiveDependenciesTest.java @@ -0,0 +1,140 @@ +/******************************************************************************* + * Copyright (c) 2023, 2023 Hannes Wellmann and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Hannes Wellmann - initial API and implementation + *******************************************************************************/ +package org.eclipse.m2e.pde.target.tests; + +import static org.junit.Assert.assertArrayEquals; + +import java.util.Collection; +import java.util.List; + +import org.eclipse.pde.core.target.ITargetLocation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class MavenTargetsTransitiveDependenciesTest extends AbstractMavenTargetTest { + + @Parameter(0) + public String dependencyDepth; + @Parameter(1) + public String dependencyScopes; + @Parameter(2) + public String sources; + @Parameter(3) + public List expectedBundles; + + @Parameters(name = "includeDependencyDepth={0} - includeDependencyScopes={1} - includeSource={2}") + public static Collection dependencyConfigurations() { + return List.of(// + new Object[] { "none", "", "false", List.of( // + junitJupiter("junit-jupiter")) }, + + new Object[] { "none", "", "true", withSourceBundles(List.of( // + junitJupiter("junit-jupiter"))) }, + + new Object[] { "direct", "compile", "false", List.of( // + junitJupiter("junit-jupiter"), // + junitJupiter("junit-jupiter-api"), // + junitJupiter("junit-jupiter-params")) }, + + new Object[] { "direct", "compile", "true", withSourceBundles(List.of( // + junitJupiter("junit-jupiter"), // + junitJupiter("junit-jupiter-api"), // + junitJupiter("junit-jupiter-params"))) }, + + new Object[] { "direct", "compile,runtime", "false", List.of( // + junitJupiter("junit-jupiter"), // + junitJupiter("junit-jupiter-api"), // + junitJupiter("junit-jupiter-params"), // + junitJupiter("junit-jupiter-engine")) }, + + new Object[] { "direct", "compile,runtime", "true", withSourceBundles(List.of( // + junitJupiter("junit-jupiter"), // + junitJupiter("junit-jupiter-api"), // + junitJupiter("junit-jupiter-params"), // + junitJupiter("junit-jupiter-engine"))) }, + + new Object[] { "infinite", "compile", "false", List.of( // + junitJupiter("junit-jupiter"), // + junitJupiter("junit-jupiter-api"), // + junitJupiter("junit-jupiter-params"), // + junitPlatform("junit-platform-commons"), // + apiGuardian(), opentest4j()) }, + + new Object[] { "infinite", "compile", "true", withSourceBundles(List.of(// + junitJupiter("junit-jupiter"), // + junitJupiter("junit-jupiter-api"), // + junitJupiter("junit-jupiter-params"), // + junitPlatform("junit-platform-commons"), // + apiGuardian(), opentest4j())) }, + + new Object[] { "infinite", "compile,runtime", "false", List.of( // + junitJupiter("junit-jupiter"), // + junitJupiter("junit-jupiter-api"), // + junitJupiter("junit-jupiter-params"), // + junitJupiter("junit-jupiter-engine"), // + junitPlatform("junit-platform-commons"), // + junitPlatform("junit-platform-engine"), // + apiGuardian(), opentest4j()) }, + + new Object[] { "infinite", "compile,runtime", "true", withSourceBundles(List.of( // + junitJupiter("junit-jupiter"), // + junitJupiter("junit-jupiter-api"), // + junitJupiter("junit-jupiter-params"), // + junitJupiter("junit-jupiter-engine"), // + junitPlatform("junit-platform-commons"), // + junitPlatform("junit-platform-engine"), // + apiGuardian(), opentest4j())) } + + ); + } + + private static ExpectedBundle junitJupiter(String artifactID) { + return originalOSGiBundle(artifactID, "5.9.3", "org.junit.jupiter:" + artifactID); + } + + private static ExpectedBundle junitPlatform(String artifactID) { + return originalOSGiBundle(artifactID, "1.9.3", "org.junit.platform:" + artifactID); + } + + private static ExpectedBundle apiGuardian() { + return originalOSGiBundle("org.apiguardian.api", "1.1.2", "org.apiguardian:apiguardian-api"); + } + + private static ExpectedBundle opentest4j() { + return originalOSGiBundle("org.opentest4j", "1.2.0", "org.opentest4j:opentest4j"); + } + + @Test + public void testSingleRootArtifact() throws Exception { + String targetDefinition = """ + + + + org.junit.jupiter + junit-jupiter + 5.9.3 + jar + + + + """ + .formatted(dependencyDepth, dependencyScopes, sources); + ITargetLocation target = resolveMavenTarget(targetDefinition); + assertTargetBundles(target, expectedBundles); + assertArrayEquals(EMPTY, target.getFeatures()); + } +} \ No newline at end of file diff --git a/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/MixedCasesTest.java b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/MixedCasesTest.java new file mode 100644 index 0000000000..a3f53687ee --- /dev/null +++ b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/MixedCasesTest.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * Copyright (c) 2023, 2023 Hannes Wellmann and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Hannes Wellmann - initial API and implementation + *******************************************************************************/ +package org.eclipse.m2e.pde.target.tests; + +import static org.junit.Assert.assertArrayEquals; + +import java.util.List; + +import org.eclipse.pde.core.target.ITargetLocation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class MixedCasesTest extends AbstractMavenTargetTest { + @Parameter(0) + public Boolean includeSource; + + @Parameters(name = "includeSource={0}") + public static List dependencyConfigurations() { + return List.of(false, true); + } + + @Test + public void testMultipleArtifactsWithWrappingAndExclusion() throws Exception { + ITargetLocation target = resolveMavenTarget(String + .format( + """ + + + + com.google.guava + failureaccess + 1.0.1 + jar + + + com.google.guava + guava + 30.1.1-jre + jar + + + com.google.inject + guice + 5.1.0 + jar + + + + com.google.code.findbugs:jsr305:3.0.1 + com.google.code.findbugs:jsr305:3.0.2 + com.google.j2objc:j2objc-annotations:1.3 + org.checkerframework:checker-qual:3.5.0 + + """, + includeSource)); + assertStatusOk(target.getStatus()); + assertArrayEquals(EMPTY, target.getFeatures()); + List expectedBundles = List.of(// + originalOSGiBundle("com.google.inject", "5.1.0", "com.google.inject:guice"), + originalOSGiBundle("com.google.guava", "30.1.0.jre", "com.google.guava:guava", "30.1-jre"), + originalOSGiBundle("com.google.guava", "30.1.1.jre", "com.google.guava:guava", "30.1.1-jre"), + originalOSGiBundle("com.google.guava.failureaccess", "1.0.1", "com.google.guava:failureaccess"), + originalOSGiBundle("org.objectweb.asm", "9.2.0", "org.ow2.asm:asm", "9.2"), + originalOSGiBundle("checker-qual", "3.8.0", "org.checkerframework:checker-qual"), + generatedBundle("m2e.wrapped.com.google.errorprone.error_prone_annotations", "2.3.4", + "com.google.errorprone:error_prone_annotations"), + generatedBundle("m2e.wrapped.com.google.errorprone.error_prone_annotations", "2.5.1", + "com.google.errorprone:error_prone_annotations"), + generatedBundle("m2e.wrapped.javax.inject.javax.inject", "1", "javax.inject:javax.inject"), + generatedBundle("m2e.wrapped.aopalliance.aopalliance", "1.0", "aopalliance:aopalliance"), + generatedBundle("m2e.wrapped.com.google.guava.listenablefuture", + "9999.0.0.empty-to-avoid-conflict-with-guava", "com.google.guava:listenablefuture")); + if (includeSource) { + expectedBundles = withSourceBundles(expectedBundles).stream() + .filter(e -> !"m2e.wrapped.com.google.guava.listenablefuture.source".equals(e.id())).toList(); + // com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava + // doesn't have sources + } + assertTargetBundles(target, expectedBundles); + } +} diff --git a/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/OSGiMetadataGenerationTest.java b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/OSGiMetadataGenerationTest.java new file mode 100644 index 0000000000..f3fc26cf6e --- /dev/null +++ b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/OSGiMetadataGenerationTest.java @@ -0,0 +1,284 @@ +/******************************************************************************* + * Copyright (c) 2023, 2023 Hannes Wellmann and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Hannes Wellmann - initial API and implementation + *******************************************************************************/ +package org.eclipse.m2e.pde.target.tests; + +import static org.eclipse.osgi.util.ManifestElement.parseHeader; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.jar.Attributes; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.osgi.util.ManifestElement; +import org.eclipse.pde.core.target.ITargetLocation; +import org.eclipse.pde.core.target.TargetBundle; +import org.junit.Test; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.Version; +import org.osgi.framework.VersionRange; + +public class OSGiMetadataGenerationTest extends AbstractMavenTargetTest { + + @Test + public void testNonOSGiArtifact_missingArtifactError() throws Exception { + ITargetLocation target = resolveMavenTarget(""" + + + + com.google.errorprone + error_prone_annotations + 2.18.0 + jar + + + + """); + IStatus targetStatus = target.getStatus(); + assertEquals(String.valueOf(targetStatus), IStatus.ERROR, targetStatus.getSeverity()); + + assertEquals(1, targetStatus.getChildren().length); + String notABundleErrorMessage = "com.google.errorprone:error_prone_annotations:jar:2.18.0 is not a bundle"; + assertEquals(notABundleErrorMessage, targetStatus.getChildren()[0].getMessage()); + + assertArrayEquals(EMPTY, target.getFeatures()); + TargetBundle[] allBundles = target.getBundles(); + assertEquals(1, allBundles.length); + IStatus status = allBundles[0].getStatus(); + assertEquals(IStatus.ERROR, status.getSeverity()); + assertEquals(notABundleErrorMessage, status.getMessage()); + } + + @Test + public void testNonOSGiArtifact_missingArtifactIgnore() throws Exception { + ITargetLocation target = resolveMavenTarget(""" + + + + com.google.errorprone + error_prone_annotations + 2.18.0 + jar + + + + """); + assertStatusOk(target.getStatus()); + assertArrayEquals(EMPTY, target.getFeatures()); + assertArrayEquals(EMPTY, target.getBundles()); + } + + @Test + public void testNonOSGiArtifact_missingArtifactGenerate_defaultInstructions() throws Exception { + ITargetLocation target = resolveMavenTarget(""" + + + + com.google.errorprone + error_prone_annotations + 2.18.0 + jar + + + + """); + assertStatusOk(target.getStatus()); + assertArrayEquals(EMPTY, target.getFeatures()); + ExpectedBundle expectedBundle = generatedBundle("wrapped.com.google.errorprone.error_prone_annotations", + "2.18.0", "com.google.errorprone:error_prone_annotations"); + assertTargetBundles(target, withSourceBundles(List.of(expectedBundle))); + + // Validate generated metadata + Attributes attributes = getManifestMainAttributes(getGeneratedBundle(target)); + assertEquals("wrapped.com.google.errorprone.error_prone_annotations", + attributes.getValue(Constants.BUNDLE_SYMBOLICNAME)); + assertEquals("Bundle derived from maven artifact com.google.errorprone:error_prone_annotations:2.18.0", + attributes.getValue(Constants.BUNDLE_NAME)); + assertEqualManifestHeaders(Constants.IMPORT_PACKAGE, attributes, + "javax.lang.model.element;resolution:=optional"); + assertEqualManifestHeaders(Constants.EXPORT_PACKAGE, attributes, + "com.google.errorprone.annotations;version=\"2.18.0\";uses:=\"javax.lang.model.element\"", + "com.google.errorprone.annotations.concurrent;version=\"2.18.0\""); + assertNull(attributes.getValue(Constants.REQUIRE_BUNDLE)); + assertEquals("*", attributes.getValue(Constants.DYNAMICIMPORT_PACKAGE)); + + Attributes sourceAttributes = getManifestMainAttributes(getGeneratedSourceBundle(target)); + assertEquals("wrapped.com.google.errorprone.error_prone_annotations.source", + sourceAttributes.getValue(Constants.BUNDLE_SYMBOLICNAME)); + assertEquals("Source Bundle for wrapped.com.google.errorprone.error_prone_annotations:2.18.0", + sourceAttributes.getValue(Constants.BUNDLE_NAME)); + assertEqualManifestHeaders("Eclipse-SourceBundle", sourceAttributes, + "wrapped.com.google.errorprone.error_prone_annotations;version=\"2.18.0\";roots:=\".\""); + assertNull(sourceAttributes.getValue(Constants.IMPORT_PACKAGE)); + assertNull(sourceAttributes.getValue(Constants.EXPORT_PACKAGE)); + assertNull(sourceAttributes.getValue(Constants.REQUIRE_BUNDLE)); + assertNull(sourceAttributes.getValue(Constants.DYNAMICIMPORT_PACKAGE)); + } + + @Test + public void testNonOSGiArtifact_missingArtifactGenerate_customInstructions() throws Exception { + ITargetLocation target = resolveMavenTarget( + """ + + + + com.google.errorprone + error_prone_annotations + 2.18.0 + jar + + + + + """); + assertStatusOk(target.getStatus()); + assertArrayEquals(EMPTY, target.getFeatures()); + ExpectedBundle expectedBundle = generatedBundle("m2e.custom.test.wrapped.error_prone_annotations", "2.18.0", + "com.google.errorprone:error_prone_annotations"); + assertTargetBundles(target, withSourceBundles(List.of(expectedBundle))); + + // Validate generated metadata + Attributes attributes = getManifestMainAttributes(getGeneratedBundle(target)); + assertEquals("m2e.custom.test.wrapped.error_prone_annotations", + attributes.getValue(Constants.BUNDLE_SYMBOLICNAME)); + assertEquals("Bundle in Test from artifact com.google.errorprone:error_prone_annotations:2.18.0:", + attributes.getValue(Constants.BUNDLE_NAME)); + assertEqualManifestHeaders(Constants.IMPORT_PACKAGE, attributes, "javax.lang.model.element"); + assertEqualManifestHeaders(Constants.EXPORT_PACKAGE, attributes, + "com.google.errorprone.annotations;version=\"2.18.0\";uses:=\"javax.lang.model.element\"", + "com.google.errorprone.annotations.concurrent;version=\"2.18.0\""); + assertNull(attributes.getValue(Constants.REQUIRE_BUNDLE)); + assertNull(attributes.getValue(Constants.DYNAMICIMPORT_PACKAGE)); + + Attributes sourceAttributes = getManifestMainAttributes(getGeneratedSourceBundle(target)); + assertEquals("m2e.custom.test.wrapped.error_prone_annotations.source", + sourceAttributes.getValue(Constants.BUNDLE_SYMBOLICNAME)); + assertEquals("Source Bundle for m2e.custom.test.wrapped.error_prone_annotations:2.18.0", + sourceAttributes.getValue(Constants.BUNDLE_NAME)); + assertEqualManifestHeaders("Eclipse-SourceBundle", sourceAttributes, + "m2e.custom.test.wrapped.error_prone_annotations;version=\"2.18.0\";roots:=\".\""); + assertNull(sourceAttributes.getValue(Constants.IMPORT_PACKAGE)); + assertNull(sourceAttributes.getValue(Constants.EXPORT_PACKAGE)); + assertNull(sourceAttributes.getValue(Constants.REQUIRE_BUNDLE)); + assertNull(sourceAttributes.getValue(Constants.DYNAMICIMPORT_PACKAGE)); + } + + @Test + public void testNonOSGiArtifact_missingArtifactGenerate_changedCustomInstructions() throws Exception { + String targetXML = """ + + + + com.google.errorprone + error_prone_annotations + 2.18.0 + jar + + + + + """; + ITargetLocation target = resolveMavenTarget(targetXML.formatted("m2e.wrapped.${mvnArtifactId}")); + assertStatusOk(target.getStatus()); + assertArrayEquals(EMPTY, target.getFeatures()); + assertEquals(2, target.getBundles().length); + assertEquals("m2e.wrapped.error_prone_annotations", + getGeneratedBundle(target).getBundleInfo().getSymbolicName()); + assertEquals("m2e.wrapped.error_prone_annotations.source", + getGeneratedSourceBundle(target).getBundleInfo().getSymbolicName()); + + target = resolveMavenTarget(targetXML.formatted("others.wrapped.${mvnArtifactId}")); + assertStatusOk(target.getStatus()); + assertArrayEquals(EMPTY, target.getFeatures()); + assertEquals(2, target.getBundles().length); + assertEquals("others.wrapped.error_prone_annotations", + getGeneratedBundle(target).getBundleInfo().getSymbolicName()); + assertEquals("others.wrapped.error_prone_annotations.source", + getGeneratedSourceBundle(target).getBundleInfo().getSymbolicName()); + } + + @Test + public void testNonOSGiArtifact_missingArtifactGenerate_hasVersions() throws Exception { + ITargetLocation target = resolveMavenTarget( + """ + + + + org.apache.lucene + lucene-analysis-common + 9.5.0 + jar + + + + """); + assertStatusOk(target.getStatus()); + Optional luceneAnalysisCommon = Arrays.stream(target.getBundles()).filter( + tb -> tb.getBundleInfo().getSymbolicName().equals("wrapped.org.apache.lucene.lucene-analysis-common")) + .findFirst(); + assertTrue("lucene-analysis-common bundle not found in target state", luceneAnalysisCommon.isPresent()); + Attributes manifest = getManifestMainAttributes(luceneAnalysisCommon.get()); + ManifestElement[] importHeader = parseHeader(Constants.IMPORT_PACKAGE, + manifest.getValue(Constants.IMPORT_PACKAGE)); + for (ManifestElement element : importHeader) { + String value = element.getValue(); + if (value.startsWith("org.apache.lucene.")) { + String attribute = element.getAttribute(Constants.VERSION_ATTRIBUTE); + assertNotNull("Package " + value + " has no version attribute: " + element, attribute); + VersionRange versionRange = VersionRange.valueOf(attribute); + assertEquals("Unexpected version range " + versionRange + " on package " + value + ": " + element, + 0, versionRange.getLeft().compareTo(Version.valueOf("9.5.0"))); + } + } + } + + private static TargetBundle getGeneratedBundle(ITargetLocation target) { + return Arrays.stream(target.getBundles()).filter(b -> !b.isSourceBundle()).findFirst().orElseThrow(); + } + + private static TargetBundle getGeneratedSourceBundle(ITargetLocation target) { + return Arrays.stream(target.getBundles()).filter(TargetBundle::isSourceBundle).findFirst().orElseThrow(); + } + + private static void assertEqualManifestHeaders(String header, Attributes mainManifestAttributes, + String... expectedHeaderValues) throws BundleException { + ManifestElement[] expected = parseHeader(header, String.join(",", expectedHeaderValues)); + ManifestElement[] actual = parseHeader(header, mainManifestAttributes.getValue(header)); + Function toString = a -> Arrays.stream(a).map(ManifestElement::toString) + .toArray(String[]::new); + assertEquals(Set.of(toString.apply(expected)), Set.of(toString.apply(actual))); // order is irrelevant + } + +} \ No newline at end of file diff --git a/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/spi/TargetLocationLoader.java b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/spi/TargetLocationLoader.java new file mode 100644 index 0000000000..63bc27be82 --- /dev/null +++ b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/spi/TargetLocationLoader.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 2023 Christoph Läubrich and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.m2e.pde.target.tests.spi; + +import java.io.File; + +import org.eclipse.pde.core.target.ITargetLocation; + +/** + * In cases where we want to reuse the test-cases (specifically Tycho), one can plug in an own + * loader that transform the xml into a suitable location for the tests. + * + */ +public interface TargetLocationLoader extends Comparable { + + static final String MAVEN_LOCATION_TYPE = "Maven"; + + /** + * @return the priority of this loader in case there are multiple ones, the one with the highest + * priority will be chosen for the test + */ + int getPriority(); + + /** + * Resolve the given target xml fragment into a resolved location, the expectation is that: + *
    + *
  • The location is resolved
  • + *
  • The status can be queried and return a result reflecting the outcome of< the + * operation
  • + *
  • getBundles() / getFeatures() return the appropriate items
  • + *
+ * + * @param targetXML + * @return the resolved location + * @throws Exception + */ + ITargetLocation resolveMavenTarget(String targetXML, File tempDir) throws Exception; + + @Override + default int compareTo(TargetLocationLoader o) { + return Integer.compare(o.getPriority(), getPriority()); + } + +} diff --git a/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/spi/TychoTargetLocationLoader.java b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/spi/TychoTargetLocationLoader.java new file mode 100644 index 0000000000..49a635a965 --- /dev/null +++ b/tycho-core/src/test/java/org/eclipse/m2e/pde/target/tests/spi/TychoTargetLocationLoader.java @@ -0,0 +1,294 @@ +/******************************************************************************* + * Copyright (c) 2023 Christoph Läubrich and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.m2e.pde.target.tests.spi; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.repository.ArtifactRepository; +import org.apache.maven.artifact.repository.LegacyLocalRepositoryManager; +import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout; +import org.apache.maven.execution.DefaultMavenExecutionRequest; +import org.apache.maven.execution.DefaultMavenExecutionResult; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.LegacySupport; +import org.apache.maven.plugin.testing.stubs.StubArtifactRepository; +import org.apache.maven.repository.internal.MavenRepositorySystemUtils; +import org.apache.maven.session.scope.internal.SessionScope; +import org.codehaus.plexus.PlexusContainer; +import org.codehaus.plexus.component.repository.exception.ComponentLookupException; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.equinox.p2.metadata.expression.IExpression; +import org.eclipse.equinox.p2.query.Collector; +import org.eclipse.equinox.p2.query.IQuery; +import org.eclipse.equinox.p2.query.IQueryResult; +import org.eclipse.equinox.p2.repository.artifact.IArtifactDescriptor; +import org.eclipse.pde.core.target.ITargetDefinition; +import org.eclipse.pde.core.target.ITargetLocation; +import org.eclipse.pde.core.target.TargetBundle; +import org.eclipse.pde.core.target.TargetFeature; +import org.eclipse.tycho.core.resolver.MavenTargetLocationFactory; +import org.eclipse.tycho.core.resolver.shared.IncludeSourceMode; +import org.eclipse.tycho.core.resolver.target.FileArtifactRepository; +import org.eclipse.tycho.core.resolver.target.TargetDefinitionContent; +import org.eclipse.tycho.targetplatform.TargetDefinition.Location; +import org.eclipse.tycho.targetplatform.TargetDefinition.MavenGAVLocation; +import org.eclipse.tycho.targetplatform.TargetDefinitionFile; +import org.eclipse.tycho.testing.TychoPlexusTestCase; + +public class TychoTargetLocationLoader implements TargetLocationLoader { + + @Override + public int getPriority() { + // use a higher priority to override m2e provided loader... + return 100; + } + + @Override + public ITargetLocation resolveMavenTarget(String targetXML, File tempDir) throws Exception { + File targetFile = new File(tempDir, "test.target"); + try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(targetFile), + StandardCharsets.UTF_8)) { + writer.write(""); + writer.write(targetXML); + writer.write(""); + } + PlexusContainer container = new ContainerFactory().createContainer(tempDir); + MavenTargetLocationFactory locationFactory = container.lookup(MavenTargetLocationFactory.class); + TargetDefinitionFile targetDefinition = TargetDefinitionFile.read(targetFile); + for (Location loc : targetDefinition.getLocations()) { + if (loc instanceof MavenGAVLocation gav) { + File locationTempDir = new File(tempDir, "tmp"); + locationTempDir.mkdirs(); + return getLocationOf(gav, locationFactory, locationTempDir.toPath()); + } + } + throw new IllegalArgumentException("Can't extract MavenGAVLocation from " + targetXML); + } + + private ITargetLocation getLocationOf(MavenGAVLocation gav, MavenTargetLocationFactory locationFactory, + Path tmpFir) { + try { + return getLocationOf(locationFactory.resolveTargetDefinitionContent(gav, IncludeSourceMode.honor), tmpFir); + } catch (Exception e) { + return new ITargetLocation() { + + @Override + public T getAdapter(Class adapter) { + return null; + } + + @Override + public String serialize() { + throw new UnsupportedOperationException(); + } + + @Override + public IStatus resolve(ITargetDefinition definition, IProgressMonitor monitor) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isResolved() { + return true; + } + + @Override + public String[] getVMArguments() { + throw new UnsupportedOperationException(); + } + + @Override + public String getType() { + return MAVEN_LOCATION_TYPE; + } + + @Override + public IStatus getStatus() { + return Status.error("Resolve target location failed: " + e, e); + } + + @Override + public String getLocation(boolean resolve) throws CoreException { + throw new UnsupportedOperationException(); + } + + @Override + public TargetFeature[] getFeatures() { + return new TargetFeature[0]; + } + + @Override + public TargetBundle[] getBundles() { + return new TargetBundle[0]; + } + }; + } + } + + private ITargetLocation getLocationOf(TargetDefinitionContent content, Path tmpFir) throws Exception { + List bundles = new ArrayList<>(); + List features = new ArrayList<>(); + FileArtifactRepository resolvedItems = (FileArtifactRepository) content.getArtifactRepository(); + IQuery query = new IQuery<>() { + + @Override + public IQueryResult perform(Iterator iterator) { + Collector result = new Collector<>(); + while (iterator.hasNext()) { + IArtifactDescriptor candidate = iterator.next(); + if (candidate != null) + if (!result.accept(candidate)) + break; + } + return result; + } + + @Override + public IExpression getExpression() { + return null; + } + }; + Set descriptors = resolvedItems.descriptorQueryable().query(query, null).toSet(); + for (IArtifactDescriptor descriptor : descriptors) { + File artifactFile = resolvedItems.getArtifactFile(descriptor); + if ("osgi.bundle".equals(descriptor.getArtifactKey().getClassifier())) { + bundles.add(new TargetBundle(artifactFile)); + } else if ("org.eclipse.update.feature".equals(descriptor.getArtifactKey().getClassifier())) { + Path feature; + try (JarFile file = new JarFile(artifactFile)) { + Path tempDirectory = Files.createTempDirectory(tmpFir, "extract"); + ZipEntry entry = file.getEntry("feature.xml"); + InputStream stream = file.getInputStream(entry); + feature = tempDirectory.resolve("feature.xml"); + Files.copy(stream, feature); + } + features.add(new TargetFeature(feature.toFile())); + } + } + return new ITargetLocation() { + + @Override + public T getAdapter(Class adapter) { + return null; + } + + @Override + public String serialize() { + throw new UnsupportedOperationException(); + } + + @Override + public IStatus resolve(ITargetDefinition definition, IProgressMonitor monitor) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isResolved() { + return true; + } + + @Override + public String[] getVMArguments() { + throw new UnsupportedOperationException(); + } + + @Override + public String getType() { + return MAVEN_LOCATION_TYPE; + } + + @Override + public IStatus getStatus() { + return Status.OK_STATUS; + } + + @Override + public String getLocation(boolean resolve) throws CoreException { + throw new UnsupportedOperationException(); + } + + @Override + public TargetFeature[] getFeatures() { + return features.toArray(TargetFeature[]::new); + } + + @Override + public TargetBundle[] getBundles() { + return bundles.toArray(TargetBundle[]::new); + } + }; + } + + private static final class ContainerFactory extends TychoPlexusTestCase { + + private boolean init; + + public PlexusContainer createContainer(File tempDir) throws ComponentLookupException { + PlexusContainer container = super.getContainer(); + if (!init) { + init = true; + LegacySupport legacySupport = lookup(LegacySupport.class); +// Settings settings = new Settings(); + ArtifactRepository localRepository = new StubArtifactRepository( + new File(tempDir, ".m2/repository").getAbsolutePath()) { + DefaultRepositoryLayout layout = new DefaultRepositoryLayout(); + + @Override + public String pathOf(Artifact artifact) { + return layout.pathOf(artifact); + } + }; + DefaultMavenExecutionRequest request = new DefaultMavenExecutionRequest(); + request.setUserProperties(System.getProperties()); + request.setLocalRepository(localRepository); + request.setGoals(List.of()); + request.setBaseDirectory(new File(tempDir, "build")); + request.setStartTime(new Date()); + + RepositorySystemSession repositorySystemSession = LegacyLocalRepositoryManager.overlay(localRepository, + MavenRepositorySystemUtils.newSession(), null); + MavenSession mavenSession = new MavenSession(container, repositorySystemSession, request, + new DefaultMavenExecutionResult()); +// MavenSession mavenSession = new MavenSession(container, settings, localRepository, null, null, +// List.of(), new File(tempDir, "build").getAbsolutePath(), System.getProperties(), +// System.getProperties(), new Date()); + SessionScope sessionScope = container.lookup(SessionScope.class); + mavenSession.setProjects(Collections.emptyList()); + sessionScope.enter(); + sessionScope.seed(MavenSession.class, mavenSession); + legacySupport.setSession(mavenSession); + } + return container; + } + } + +} diff --git a/tycho-core/src/test/resources/META-INF/services/org.eclipse.m2e.pde.target.tests.spi.TargetLocationLoader b/tycho-core/src/test/resources/META-INF/services/org.eclipse.m2e.pde.target.tests.spi.TargetLocationLoader new file mode 100644 index 0000000000..d9b7d33d4a --- /dev/null +++ b/tycho-core/src/test/resources/META-INF/services/org.eclipse.m2e.pde.target.tests.spi.TargetLocationLoader @@ -0,0 +1 @@ +org.eclipse.m2e.pde.target.tests.spi.TychoTargetLocationLoader \ No newline at end of file diff --git a/tycho-testing-harness/src/main/java/org/eclipse/tycho/testing/TychoPlexusTestCase.java b/tycho-testing-harness/src/main/java/org/eclipse/tycho/testing/TychoPlexusTestCase.java index 43f931564c..4c92fa5847 100644 --- a/tycho-testing-harness/src/main/java/org/eclipse/tycho/testing/TychoPlexusTestCase.java +++ b/tycho-testing-harness/src/main/java/org/eclipse/tycho/testing/TychoPlexusTestCase.java @@ -19,7 +19,9 @@ import java.util.Date; import java.util.List; +import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.repository.ArtifactRepository; +import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.LegacySupport; import org.apache.maven.plugin.testing.stubs.StubArtifactRepository; @@ -66,7 +68,14 @@ public void setUpServiceAndSession() throws ComponentLookupException, IOExceptio LegacySupport legacySupport = lookup(LegacySupport.class); PlexusContainer container = ext.getContainer(); Settings settings = new Settings(); - ArtifactRepository localRepository = new StubArtifactRepository(temporaryFolder.newFolder().getAbsolutePath()); + ArtifactRepository localRepository = new StubArtifactRepository(temporaryFolder.newFolder().getAbsolutePath()) { + DefaultRepositoryLayout layout = new DefaultRepositoryLayout(); + + @Override + public String pathOf(Artifact artifact) { + return layout.pathOf(artifact); + } + }; MavenSession mavenSession = new MavenSession(container, settings, localRepository, null, null, List.of(), temporaryFolder.newFolder().getAbsolutePath(), System.getProperties(), System.getProperties(), new Date());