Skip to content

Commit

Permalink
Migrate ConfigurationSessionTestSuite to JUnit 5 eclipse-platform#903
Browse files Browse the repository at this point in the history
This introduces a JUnit 5 replacement for the JUnit 3
ConfigurationSessionTestSuite that allows to define session tests using
a custom workbench configuration with an explicit config.ini defining a
custom-defined bundle set.

It is realized by the CustomSessionConfiguration class, which
encapsulates all information and logic to set up a custom workbench
configuration, and an additional parameterization of the JUnit 5
SessionTestExecution extension, which allows this extension to replace
the default workbench configuration with the custom one.

The new capability of defining a custom workbench configuration for a
JUnit 5 session test is applied to the PlatformURLSessionTest, which was
based on the JUnit 3 ConfigurationSessionTestSuite so far.

Contributes to
eclipse-platform#903
  • Loading branch information
HeikoKlare committed May 13, 2024
1 parent 416af86 commit a055e52
Show file tree
Hide file tree
Showing 6 changed files with 434 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*******************************************************************************
* Copyright (c) 2024 Vector Informatik GmbH and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.core.tests.harness.session;

import java.nio.file.Path;
import org.eclipse.core.tests.harness.session.customization.SessionCustomization;
import org.osgi.framework.Bundle;

/**
* A session customization to use a custom session configuration, i.e., a custom
* "config.ini" file with a defined set of bundles to be used.
*/
public interface CustomSessionConfiguration extends SessionCustomization {

/**
* Sets the given configuration directory. If not called, a temporary folder is
* used as the configuration directory.
*
* @param configurationDirectory the path of the directory to place the
* configuration in, must not be {@code null}
*
* @return this
*/
public CustomSessionConfiguration setConfigurationDirectory(Path configurationDirectory);

/**
* Adds the bundle containing the given class to the session configuration.
*
* @param classFromBundle a class from the bundle to add, must not be
* {@code null}
*
* @return this
*/
public CustomSessionConfiguration addBundle(Class<?> classFromBundle);

/**
* Adds the bundle to the session configuration.
*
* @param bundle the bundle to add, must not be {@code null}
*
* @return this
*/
public CustomSessionConfiguration addBundle(Bundle bundle);

/**
* Activates the "osgi.configuration.cascaded" option for this configuration.
*
* @return this
*/
public CustomSessionConfiguration setCascaded();

/**
* Marks the configuration area as read only. This will be effective from the
* second session on, since the the first session requires a writable
* configuration area for initial setup.
*
* @return this
*/
public CustomSessionConfiguration setReadOnly();

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.tests.harness.session.customization.CustomSessionConfigurationImpl;
import org.eclipse.core.tests.harness.session.customization.CustomSessionWorkspaceImpl;
import org.eclipse.core.tests.harness.session.customization.SessionCustomization;
import org.eclipse.core.tests.harness.session.samples.SampleSessionTests;
Expand Down Expand Up @@ -149,6 +151,44 @@ public SessionTestExtensionBuilder withCustomization(CustomSessionWorkspace sess
return this;
}

/**
* Sets the given session configuration that uses a custom "config.ini" file
* with a defined set of bundles to be used. The customization can be
* instantiated via
* {@link SessionTestExtension#createCustomSessionConfiguration()}. By default,
* a temporary folder is used as a configuration directory.
* <p>
* <b>Example usage with default configuration directory:</b>
*
* <pre>
* &#64;RegisterExtension
* SessionTestExtension sessionTestExtension = SessionTestExtension.forPlugin("")
* .withConfiguration(SessionTestExtension.createCustomConfiguration()).create();
* </pre>
*
* <b>Example usage with custom configuration directory:</b>
*
* <pre>
* &#64;TempDir
* Path configurationDirectory;
*
* &#64;RegisterExtension
* SessionTestExtension extension = SessionTestExtension.forPlugin("")
* .withConfiguration(
* SessionTestExtension.createCustomConfiguration().setConfigurationDirectory(configurationDirectory))
* .create();
* </pre>
*
* @param sessionConfiguration the custom configuration to use for the session
* tests
*/
public SessionTestExtensionBuilder withCustomization(CustomSessionConfiguration sessionConfiguration) {
Objects.requireNonNull(sessionConfiguration);
sessionConfiguration.addBundle(Platform.getBundle(storedPluginId));
this.storedSessionCustomizations.add(sessionConfiguration);
return this;
}

/**
* {@return a <code>SessionTestExtension</code> created with the information in
* this builder}
Expand All @@ -172,6 +212,14 @@ public static CustomSessionWorkspace createCustomWorkspace() {
return new CustomSessionWorkspaceImpl();
}

/**
* {@return a custom Eclipse instance configuration that, by default, uses a
* temporary folder to store the configuration files}
*/
public static CustomSessionConfiguration createCustomConfiguration() {
return new CustomSessionConfigurationImpl();
}

public void setEclipseArgument(String key, String value) {
setup.setEclipseArgument(key, value);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
/*******************************************************************************
* Copyright (c) 2024 Vector Informatik GmbH and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.core.tests.harness.session.customization;

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

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.stream.Collectors;
import org.eclipse.core.internal.runtime.InternalPlatform;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.tests.harness.session.CustomSessionConfiguration;
import org.eclipse.core.tests.session.ConfigurationSessionTestSuite;
import org.eclipse.core.tests.session.Setup;
import org.junit.Assert;
import org.osgi.framework.Bundle;
import org.osgi.framework.Constants;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.Version;

@SuppressWarnings("restriction")
public class CustomSessionConfigurationImpl implements CustomSessionConfiguration {
private static final String PROP_BUNDLES = "osgi.bundles";
private static final String PROP_FRAMEWORK = "osgi.framework";
private static final String PROP_BUNDLES_DEFAULT_START_LEVEL = "osgi.bundles.defaultStartLevel";
private static final String PROP_INSTALL_AREA = "osgi.install.area";
private static final String PROP_CONFIG_AREA_READ_ONLY = InternalPlatform.PROP_CONFIG_AREA + ".readOnly";
private static final String PROP_CONFIG_CASCADED = "osgi.configuration.cascaded";

private final Collection<BundleReference> bundleReferences = new ArrayList<>();
private Path configurationDirectory;
private boolean readOnly = false;
private boolean cascaded = false;
private boolean firstExecutedSession = true;

public CustomSessionConfigurationImpl() {
addMinimalBundleSet();
}

@SuppressWarnings("deprecation")
private void addMinimalBundleSet() {
// Just use any class from the bundles we want to add as minimal bundle set

addBundle(org.eclipse.core.runtime.FileLocator.class, "@2:start"); // org.eclipse.equinox.common
addBundle(org.eclipse.core.runtime.Platform.class, "@:start"); // org.eclipse.core.runtime
addBundle(org.eclipse.core.runtime.jobs.Job.class); // org.eclipse.core.jobs
addBundle(org.eclipse.core.runtime.IExtension.class); // org.eclipse.equinox.registry
addBundle(org.eclipse.core.runtime.preferences.IEclipsePreferences.class); // org.eclipse.equinox.preferences
addBundle(org.osgi.service.prefs.Preferences.class); // org.osgi.service.prefs
addBundle(org.eclipse.core.runtime.content.IContentType.class); // org.eclipse.core.contenttype
addBundle(org.eclipse.equinox.app.IApplication.class); // org.eclipse.equinox.app

addBundle(org.eclipse.core.tests.harness.TestHarnessPlugin.class); // org.eclipse.core.tests.harness
addBundle(org.eclipse.test.performance.Performance.class); // org.eclipse.test.performance

addBundle(org.eclipse.jdt.internal.junit.runner.ITestLoader.class); // org.eclipse.jdt.junit.runtime
addBundle(org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader.class); // org.eclipse.jdt.junit4.runtime
addBundle(org.eclipse.jdt.internal.junit5.runner.JUnit5TestLoader.class); // org.eclipse.jdt.junit5.runtime
addBundle(org.eclipse.pde.internal.junit.runtime.CoreTestApplication.class); // org.eclipse.pde.junit.runtime

addBundle(net.bytebuddy.ByteBuddy.class); // net.bytebuddy for org.assertj.core.api
addBundle(org.assertj.core.api.Assertions.class); // org.assertj.core.api
addBundle(org.hamcrest.CoreMatchers.class); // org.hamcrest.core

// The org.junit bundle requires an org.hamcrest.core bundle, but as of version
// 2.x, the org.hamcrest bundle above provides the actual classes. So we need to
// ensure that the actual org.hamcrest.core bundle required by org.junit is
// added too.
if ("org.hamcrest".equals(FrameworkUtil.getBundle(org.hamcrest.CoreMatchers.class).getSymbolicName())) {
Bundle maxHamcrestCoreBundle = null;
Version maxHamcrestCoreVersion = null;
for (Bundle bundle : FrameworkUtil.getBundle(ConfigurationSessionTestSuite.class).getBundleContext()
.getBundles()) {
if ("org.hamcrest.core".equals(bundle.getSymbolicName())) {
Version version = bundle.getVersion();
if (maxHamcrestCoreVersion == null || maxHamcrestCoreVersion.compareTo(version) < 0) {
maxHamcrestCoreVersion = version;
maxHamcrestCoreBundle = bundle;
}
}
}
if (maxHamcrestCoreBundle != null) {
addBundle(maxHamcrestCoreBundle, null);
}
}

addBundle(org.junit.Test.class); // org.junit
addBundle(org.junit.jupiter.api.Test.class); // junit-jupiter-api
addBundle(org.junit.jupiter.engine.JupiterTestEngine.class); // junit-jupiter-engine
addBundle(org.junit.jupiter.migrationsupport.EnableJUnit4MigrationSupport.class); // junit-jupiter-migrationsupport
addBundle(org.junit.jupiter.params.ParameterizedTest.class); // junit-jupiter-params
addBundle(org.junit.vintage.engine.VintageTestEngine.class); // junit-vintage-engine
addBundle(org.junit.platform.commons.JUnitException.class); // junit-platform-commons
addBundle(org.junit.platform.engine.TestEngine.class); // junit-platform-engine
addBundle(org.junit.platform.launcher.Launcher.class); // junit-platform-launcher
addBundle(org.junit.platform.runner.JUnitPlatform.class); // junit-platform-runner
addBundle(org.junit.platform.suite.api.Suite.class); // junit-platform-suite-api
addBundle(org.junit.platform.suite.commons.SuiteLauncherDiscoveryRequestBuilder.class); // junit-platform-suite-commons
addBundle(org.junit.platform.suite.engine.SuiteTestEngine.class); // junit-platform-suite-engine
addBundle(org.apiguardian.api.API.class); // org.apiguardian.api
addBundle(org.opentest4j.AssertionFailedError.class); // org.opentest4j
}

@Override
public CustomSessionConfiguration setCascaded() {
this.cascaded = true;
return this;
}

@Override
public CustomSessionConfiguration setReadOnly() {
this.readOnly = true;
return this;
}

@Override
public CustomSessionConfiguration setConfigurationDirectory(Path configurationDirectory) {
Objects.requireNonNull(configurationDirectory);
this.configurationDirectory = configurationDirectory;
return this;
}

private Path getConfigurationDirectory() throws IOException {
if (configurationDirectory == null) {
this.configurationDirectory = Files.createTempDirectory(null);
this.configurationDirectory.toFile().deleteOnExit();
}
return configurationDirectory;
}

@Override
public void prepareSession(Setup setup) throws IOException {
if (firstExecutedSession) {
// configuration area needs to be written on first start
overwriteConfigurationAreaWritability(setup);
}
setCustomConfigurationArea(setup);
if (firstExecutedSession) {
createOrRefreshConfigIni();
}
}

@Override
public void cleanupSession(Setup setup) {
if (firstExecutedSession) {
// after first session, use configuration area's readability configuration
removeConfigurationAreaWritabilityOverwrite(setup);
}
firstExecutedSession = false;
}

private void overwriteConfigurationAreaWritability(Setup setup) {
setup.setSystemProperty(PROP_CONFIG_AREA_READ_ONLY, Boolean.FALSE.toString());
}

private void removeConfigurationAreaWritabilityOverwrite(Setup setup) {
setup.setSystemProperty(PROP_CONFIG_AREA_READ_ONLY, null);
}

private void setCustomConfigurationArea(Setup setup) throws IOException {
// the base implementation will have set this to the host configuration
setup.setEclipseArgument(Setup.CONFIGURATION, null);
setup.setSystemProperty(InternalPlatform.PROP_CONFIG_AREA, getConfigurationDirectory().toString());
}

private void createOrRefreshConfigIni() throws IOException {
Properties contents = new Properties();
contents.put(PROP_BUNDLES, String.join(",", getBundleUrls()));
contents.put(PROP_FRAMEWORK, getOsgiFrameworkBundleUrl());
contents.put(PROP_BUNDLES_DEFAULT_START_LEVEL, "4");
contents.put(PROP_INSTALL_AREA, Platform.getInstallLocation().getURL().toExternalForm());
contents.put(PROP_CONFIG_CASCADED, Boolean.valueOf(cascaded).toString());
contents.put(PROP_CONFIG_AREA_READ_ONLY, Boolean.valueOf(readOnly).toString());
// save the properties
Path configINI = getConfigurationDirectory().resolve("config.ini");
try (OutputStream out = Files.newOutputStream(configINI)) {
contents.store(out, null);
}
}

@Override
public CustomSessionConfiguration addBundle(Class<?> classFromBundle) {
Objects.requireNonNull(classFromBundle);
addBundle(classFromBundle, null);
return this;
}

private void addBundle(Class<?> classFromBundle, String suffix) {
Bundle bundle = FrameworkUtil.getBundle(classFromBundle);
Assert.assertNotNull("Class is not from a bundle: " + classFromBundle, bundle);
addBundle(bundle, suffix);
}

@Override
public CustomSessionConfiguration addBundle(Bundle bundle) {
Objects.requireNonNull(bundle);
addBundle(bundle, "");
return this;
}

private void addBundle(Bundle bundle, String suffix) {
bundleReferences.add(new BundleReference(bundle, suffix));
}

private Collection<String> getBundleUrls() {
assertThat(bundleReferences).as("check bundles are not empty").isNotEmpty();
return bundleReferences.stream().map(BundleReference::toURL).collect(Collectors.toList());
}

private static String getOsgiFrameworkBundleUrl() {
Bundle osgiFrameworkBundle = FrameworkUtil.getBundle(CustomSessionConfigurationImpl.class).getBundleContext()
.getBundle(Constants.SYSTEM_BUNDLE_LOCATION);
BundleReference osgiFrameworkBundleReference = new BundleReference(osgiFrameworkBundle);
return osgiFrameworkBundleReference.toURL();
}

private record BundleReference(Bundle bundle, String suffix) {
BundleReference(Bundle bundle, String suffix) {
this.bundle = bundle;
this.suffix = suffix != null ? suffix : "";
}

BundleReference(Bundle bundle) {
this(bundle, null);
}

String toURL() {
Optional<File> location = FileLocator.getBundleFileLocation(bundle);
assertTrue("Unable to locate bundle with id: " + bundle.getSymbolicName(), location.isPresent());
String externalForm;
try {
externalForm = location.get().toURI().toURL().toExternalForm();
} catch (Exception e) {
throw new IllegalArgumentException("Failed to convert file to URL string:" + location.get(), e);
}
// workaround for bug 88070
return "reference:" + externalForm + suffix;
}
}
}
Loading

0 comments on commit a055e52

Please sign in to comment.