summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgjoranv <gv@verizonmedia.com>2020-04-17 18:46:12 +0200
committerGitHub <noreply@github.com>2020-04-17 18:46:12 +0200
commita3cb09f37cef3595776eff9884042ab534b08eed (patch)
tree786ba7c2d52f087aaac9e221ff1fd3ea6c4de32b
parent00840534118e457b3694cfe508d9c3d6dcef6d64 (diff)
parent83df0f345a9f2e9f1fb9415a0906e6dd2563115b (diff)
Merge pull request #12955 from vespa-engine/gjoranv/bundle-loader-refactor-and-unit-test
Gjoranv/bundle loader refactor and unit test
-rw-r--r--container-core/src/main/java/com/yahoo/container/core/config/BundleInstaller.java21
-rw-r--r--container-core/src/main/java/com/yahoo/container/core/config/BundleManager.java (renamed from container-core/src/main/java/com/yahoo/container/core/config/BundleLoader.java)204
-rw-r--r--container-core/src/main/java/com/yahoo/container/core/config/DiskBundleInstaller.java31
-rw-r--r--container-core/src/main/java/com/yahoo/container/core/config/FileAcquirerBundleInstaller.java30
-rw-r--r--container-core/src/main/java/com/yahoo/container/core/config/HandlersConfigurerDi.java7
-rw-r--r--container-core/src/test/java/com/yahoo/container/core/config/BundleManagerTest.java106
-rw-r--r--container-core/src/test/java/com/yahoo/container/core/config/TestBundle.java102
-rw-r--r--container-core/src/test/java/com/yahoo/container/core/config/TestBundleInstaller.java20
-rw-r--r--container-core/src/test/java/com/yahoo/container/core/config/TestOsgi.java57
9 files changed, 472 insertions, 106 deletions
diff --git a/container-core/src/main/java/com/yahoo/container/core/config/BundleInstaller.java b/container-core/src/main/java/com/yahoo/container/core/config/BundleInstaller.java
new file mode 100644
index 00000000000..fc919571b6c
--- /dev/null
+++ b/container-core/src/main/java/com/yahoo/container/core/config/BundleInstaller.java
@@ -0,0 +1,21 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.core.config;
+
+import com.yahoo.config.FileReference;
+import com.yahoo.osgi.Osgi;
+import org.osgi.framework.Bundle;
+
+import java.util.List;
+
+/**
+ * @author gjoranv
+ */
+public interface BundleInstaller {
+
+ /**
+ * Installs the bundle with the given file reference, plus all bundles in its X-JDisc-Preinstall-Bundle directive.
+ * Returns all bundles installed to the given OSGi framework as a result of this call.
+ */
+ List<Bundle> installBundles(FileReference reference, Osgi osgi) throws InterruptedException;
+
+}
diff --git a/container-core/src/main/java/com/yahoo/container/core/config/BundleLoader.java b/container-core/src/main/java/com/yahoo/container/core/config/BundleManager.java
index ca11ad387ee..5e9b42a5fda 100644
--- a/container-core/src/main/java/com/yahoo/container/core/config/BundleLoader.java
+++ b/container-core/src/main/java/com/yahoo/container/core/config/BundleManager.java
@@ -1,4 +1,4 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.container.core.config;
import com.yahoo.collections.PredicateSplit;
@@ -9,13 +9,12 @@ import com.yahoo.osgi.Osgi;
import org.osgi.framework.Bundle;
import org.osgi.framework.wiring.BundleRevision;
-import java.io.File;
+import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import java.util.stream.Collectors;
@@ -23,12 +22,12 @@ import static com.yahoo.collections.PredicateSplit.partition;
import static com.yahoo.container.core.BundleLoaderProperties.DISK_BUNDLE_PREFIX;
/**
- * Manages the set of installed 3rd-party component bundles.
+ * Manages the set of installed and active/inactive bundles.
*
- * @author Tony Vaagenes
* @author gjoranv
+ * @author Tony Vaagenes
*/
-public class BundleLoader {
+public class BundleManager {
/* Map of file refs of active bundles (not scheduled for uninstall) to a list of all bundles that were installed
* (pre-install directive) by the bundle pointed to by the file ref (including itself).
@@ -40,20 +39,96 @@ public class BundleLoader {
*/
private final Map<FileReference, List<Bundle>> reference2Bundles = new LinkedHashMap<>();
- private final Logger log = Logger.getLogger(BundleLoader.class.getName());
+ private final Logger log = Logger.getLogger(BundleManager.class.getName());
private final Osgi osgi;
- public BundleLoader(Osgi osgi) {
+ // A custom bundle installer for non-disk bundles, to be used for testing
+ private BundleInstaller customBundleInstaller = null;
+
+ public BundleManager(Osgi osgi) {
this.osgi = osgi;
}
- private void install(List<FileReference> references) {
+ /**
+ * Installs the given set of bundles and returns the set of bundles that is no longer used
+ * by the application, and should therefore be scheduled for uninstall.
+ */
+ public synchronized Set<Bundle> use(List<FileReference> newFileReferences) {
+ // Must be done before allowing duplicates because allowed duplicates affect osgi.getCurrentBundles
+ Set<Bundle> bundlesToUninstall = getObsoleteBundles(newFileReferences);
+
+ Set<FileReference> obsoleteReferences = getObsoleteFileReferences(newFileReferences);
+ allowDuplicateBundles(obsoleteReferences);
+ removeInactiveFileReferences(obsoleteReferences);
+
+ installBundles(newFileReferences);
+ startBundles();
+
+ bundlesToUninstall.removeAll(allActiveBundles());
+ log.info("Bundles to schedule for uninstall: " + bundlesToUninstall);
+
+ log.info(installedBundlesMessage());
+ return bundlesToUninstall;
+ }
+
+ /**
+ * Returns the bundles that are not assumed to be retained by the new application generation.
+ * Note that at this point we don't yet know the full set of new bundles, because of the potential
+ * pre-install directives in the new bundles. However, only "disk bundles" (file:) can be listed
+ * in the pre-install directive, so we know about all the obsolete application bundles.
+ */
+ private Set<Bundle> getObsoleteBundles(List<FileReference> newReferences) {
+ Set<Bundle> bundlesToRemove = new HashSet<>(osgi.getCurrentBundles());
+
+ for (FileReference fileReferenceToKeep : newReferences) {
+ if (reference2Bundles.containsKey(fileReferenceToKeep)) {
+ bundlesToRemove.removeAll(reference2Bundles.get(fileReferenceToKeep));
+ }
+ }
+ bundlesToRemove.removeAll(osgi.getInitialBundles());
+ return bundlesToRemove;
+ }
+
+
+ private Set<FileReference> getObsoleteFileReferences(List<FileReference> newReferences) {
+ Set<FileReference> obsoleteReferences = new HashSet<>(reference2Bundles.keySet());
+ obsoleteReferences.removeAll(newReferences);
+ return obsoleteReferences;
+ }
+
+ /**
+ * Allow duplicates (bsn+version) for each bundle that corresponds to obsolete file references,
+ * and avoid allowing duplicates for bundles that were installed via the
+ * X-JDisc-Preinstall-Bundle directive. These bundles are always "disk bundles" (library
+ * bundles installed on the node, and not transferred via file distribution).
+ * Such bundles will never have duplicates because they always have the same location id.
+ */
+ private void allowDuplicateBundles(Set<FileReference> obsoleteReferences) {
+ // The bundle at index 0 for each file reference always corresponds to the bundle at the file reference location
+ Set<Bundle> allowedDuplicates = obsoleteReferences.stream()
+ .filter(reference -> ! isDiskBundle(reference))
+ .map(reference -> reference2Bundles.get(reference).get(0))
+ .collect(Collectors.toSet());
+
+ log.info(() -> allowedDuplicates.isEmpty() ? "" : "Adding bundles to allowed duplicates: " + allowedDuplicates);
+ osgi.allowDuplicateBundles(allowedDuplicates);
+ }
+
+ /**
+ * Cleans up the map of active file references
+ */
+ private void removeInactiveFileReferences(Set<FileReference> fileReferencesToRemove) {
+ // Clean up the map of active bundles
+ fileReferencesToRemove.forEach(reference2Bundles::remove);
+ }
+
+ private void installBundles(List<FileReference> references) {
Set<FileReference> bundlesToInstall = new HashSet<>(references);
// This is just an optimization, as installing a bundle with the same location id returns the already installed bundle.
bundlesToInstall.removeAll(reference2Bundles.keySet());
- PredicateSplit<FileReference> bundlesToInstall_isDisk = partition(bundlesToInstall, BundleLoader::isDiskBundle);
+ PredicateSplit<FileReference> bundlesToInstall_isDisk = partition(bundlesToInstall, BundleManager::isDiskBundle);
installBundlesFromDisk(bundlesToInstall_isDisk.trueValues);
installBundlesFromFileDistribution(bundlesToInstall_isDisk.falseValues);
@@ -81,7 +156,9 @@ public class BundleLoader {
FileAcquirer fileAcquirer = Container.get().getFileAcquirer();
boolean hasFileDistribution = (fileAcquirer != null);
if (hasFileDistribution) {
- installWithFileDistribution(bundlesToInstall, fileAcquirer);
+ installWithFileDistribution(bundlesToInstall, new FileAcquirerBundleInstaller(fileAcquirer));
+ } else if (customBundleInstaller != null) {
+ installWithFileDistribution(bundlesToInstall, customBundleInstaller);
} else {
log.warning("Can't retrieve bundles since file distribution is disabled.");
}
@@ -89,25 +166,18 @@ public class BundleLoader {
}
private void installBundleFromDisk(FileReference reference) {
- assert(reference.value().startsWith(DISK_BUNDLE_PREFIX));
- String referenceFileName = reference.value().substring(DISK_BUNDLE_PREFIX.length());
log.info("Installing bundle from disk with reference '" + reference.value() + "'");
- File file = new File(referenceFileName);
- if ( ! file.exists()) {
- throw new IllegalArgumentException("Reference '" + reference.value() + "' not found on disk.");
- }
-
- List<Bundle> bundles = osgi.install(file.getAbsolutePath());
-
+ var bundleInstaller = new DiskBundleInstaller();
+ List<Bundle> bundles = bundleInstaller.installBundles(reference, osgi);
reference2Bundles.put(reference, bundles);
}
- private void installWithFileDistribution(List<FileReference> bundlesToInstall, FileAcquirer fileAcquirer) {
+ private void installWithFileDistribution(List<FileReference> bundlesToInstall, BundleInstaller bundleInstaller) {
for (FileReference reference : bundlesToInstall) {
try {
log.info("Installing bundle with reference '" + reference.value() + "'");
- List<Bundle> bundles = obtainBundles(reference, fileAcquirer);
+ List<Bundle> bundles = bundleInstaller.installBundles(reference, osgi);
reference2Bundles.put(reference, bundles);
}
catch(Exception e) {
@@ -116,11 +186,6 @@ public class BundleLoader {
}
}
- private List<Bundle> obtainBundles(FileReference reference, FileAcquirer fileAcquirer) throws InterruptedException {
- File file = fileAcquirer.waitFor(reference, 7, TimeUnit.DAYS);
- return osgi.install(file.getAbsolutePath());
- }
-
/**
* Resolves and starts (calls the Bundles BundleActivator) all bundles. Bundle resolution must take place
* after all bundles are installed to ensure that the framework can resolve dependencies between bundles.
@@ -146,87 +211,12 @@ public class BundleLoader {
return (bundleRevision.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0;
}
- /**
- * Returns the bundles that are not assumed to be retained by the new application generation.
- * Note that at this point we don't yet know the full set of new bundles, because of the potential
- * pre-install directives in the new bundles. However, only "disk bundles" (file:) can be listed
- * in the pre-install directive, so we know about all the obsolete application bundles.
- */
- private Set<Bundle> getObsoleteBundles(List<FileReference> newReferences) {
- Set<Bundle> bundlesToRemove = new HashSet<>(osgi.getCurrentBundles());
-
- for (FileReference fileReferenceToKeep : newReferences) {
- if (reference2Bundles.containsKey(fileReferenceToKeep)) {
- bundlesToRemove.removeAll(reference2Bundles.get(fileReferenceToKeep));
- }
- }
- bundlesToRemove.removeAll(osgi.getInitialBundles());
- return bundlesToRemove;
- }
-
- /**
- * Cleans up the map of active file references
- */
- private void removeInactiveFileReferences(List<FileReference> newReferences) {
- // Clean up the map of active bundles
- Set<FileReference> fileReferencesToRemove = getObsoleteFileReferences(newReferences);
- fileReferencesToRemove.forEach(reference2Bundles::remove);
- }
-
-
- /**
- * Allow duplicates (bsn+version) for each bundle that corresponds to obsolete file references,
- * and avoid allowing duplicates for bundles that were installed via the
- * X-JDisc-Preinstall-Bundle directive. These bundles are always "disk bundles" (library
- * bundles installed on the node, and not transferred via file distribution).
- * Such bundles will never have duplicates because they always have the same location id.
- */
- private void allowDuplicateBundles(List<FileReference> newReferences) {
- Set<FileReference> obsoleteReferences = getObsoleteFileReferences(newReferences);
-
- // The bundle at index 0 for each file reference always corresponds to the bundle at the file reference location
- Set<Bundle> allowedDuplicates = obsoleteReferences.stream()
- .filter(reference -> ! isDiskBundle(reference))
- .map(reference -> reference2Bundles.get(reference).get(0))
- .collect(Collectors.toSet());
-
- log.info(() -> allowedDuplicates.isEmpty() ? "" : "Adding bundles to allowed duplicates: " + allowedDuplicates);
- osgi.allowDuplicateBundles(allowedDuplicates);
- }
-
- private Set<FileReference> getObsoleteFileReferences(List<FileReference> newReferences) {
- Set<FileReference> obsoleteReferences = new HashSet<>(reference2Bundles.keySet());
- obsoleteReferences.removeAll(newReferences);
- return obsoleteReferences;
- }
-
private Set<Bundle> allActiveBundles() {
return reference2Bundles.keySet().stream()
.flatMap(reference -> reference2Bundles.get(reference).stream())
.collect(Collectors.toSet());
}
- /**
- * Installs the given set of bundles and returns the set of bundles that is no longer used
- * by the application, and should therefore be scheduled for uninstall.
- */
- public synchronized Set<Bundle> use(List<FileReference> newBundles) {
- // Must be done before allowing duplicates because allowed duplicates affect osgi.getCurrentBundles
- Set<Bundle> bundlesToUninstall = getObsoleteBundles(newBundles);
-
- allowDuplicateBundles(newBundles);
- removeInactiveFileReferences(newBundles);
-
- install(newBundles);
- startBundles();
-
- bundlesToUninstall.removeAll(allActiveBundles());
- log.info("Bundles to schedule for uninstall: " + bundlesToUninstall);
-
- log.info(installedBundlesMessage());
- return bundlesToUninstall;
- }
-
private String installedBundlesMessage() {
StringBuilder sb = new StringBuilder("Installed bundles: {" );
for (Bundle b : osgi.getBundles())
@@ -236,4 +226,14 @@ public class BundleLoader {
return sb.toString();
}
+ // Only for testing
+ void useCustomBundleInstaller(BundleInstaller bundleInstaller) {
+ customBundleInstaller = bundleInstaller;
+ }
+
+ // Only for testing
+ List<FileReference> getActiveFileReferences() {
+ return new ArrayList<>(reference2Bundles.keySet());
+ }
+
}
diff --git a/container-core/src/main/java/com/yahoo/container/core/config/DiskBundleInstaller.java b/container-core/src/main/java/com/yahoo/container/core/config/DiskBundleInstaller.java
new file mode 100644
index 00000000000..3edabe9f861
--- /dev/null
+++ b/container-core/src/main/java/com/yahoo/container/core/config/DiskBundleInstaller.java
@@ -0,0 +1,31 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.core.config;
+
+import com.yahoo.config.FileReference;
+import com.yahoo.osgi.Osgi;
+import org.osgi.framework.Bundle;
+
+import java.io.File;
+import java.util.List;
+
+import static com.yahoo.container.core.BundleLoaderProperties.DISK_BUNDLE_PREFIX;
+
+/**
+ * @author gjoranv
+ */
+public class DiskBundleInstaller implements BundleInstaller {
+
+ @Override
+ public List<Bundle> installBundles(FileReference reference, Osgi osgi) {
+ assert(reference.value().startsWith(DISK_BUNDLE_PREFIX));
+ String referenceFileName = reference.value().substring(DISK_BUNDLE_PREFIX.length());
+
+ File file = new File(referenceFileName);
+ if ( ! file.exists()) {
+ throw new IllegalArgumentException("Reference '" + reference.value() + "' not found on disk.");
+ }
+
+ return osgi.install(file.getAbsolutePath());
+ }
+
+}
diff --git a/container-core/src/main/java/com/yahoo/container/core/config/FileAcquirerBundleInstaller.java b/container-core/src/main/java/com/yahoo/container/core/config/FileAcquirerBundleInstaller.java
new file mode 100644
index 00000000000..6f6f86a6366
--- /dev/null
+++ b/container-core/src/main/java/com/yahoo/container/core/config/FileAcquirerBundleInstaller.java
@@ -0,0 +1,30 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.core.config;
+
+import com.yahoo.config.FileReference;
+import com.yahoo.filedistribution.fileacquirer.FileAcquirer;
+import com.yahoo.osgi.Osgi;
+import org.osgi.framework.Bundle;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author gjoranv
+ */
+public class FileAcquirerBundleInstaller implements BundleInstaller {
+
+ private final FileAcquirer fileAcquirer;
+
+ public FileAcquirerBundleInstaller(FileAcquirer fileAcquirer) {
+ this.fileAcquirer = fileAcquirer;
+ }
+
+ @Override
+ public List<Bundle> installBundles(FileReference reference, Osgi osgi) throws InterruptedException {
+ File file = fileAcquirer.waitFor(reference, 7, TimeUnit.DAYS);
+ return osgi.install(file.getAbsolutePath());
+ }
+
+}
diff --git a/container-core/src/main/java/com/yahoo/container/core/config/HandlersConfigurerDi.java b/container-core/src/main/java/com/yahoo/container/core/config/HandlersConfigurerDi.java
index ef132694e10..d87b38e8b18 100644
--- a/container-core/src/main/java/com/yahoo/container/core/config/HandlersConfigurerDi.java
+++ b/container-core/src/main/java/com/yahoo/container/core/config/HandlersConfigurerDi.java
@@ -9,7 +9,6 @@ import com.yahoo.component.ComponentSpecification;
import com.yahoo.component.provider.ComponentRegistry;
import com.yahoo.concurrent.ThreadFactoryFactory;
import com.yahoo.config.FileReference;
-import com.yahoo.container.core.config.testutil.MockOsgiWrapper;
import com.yahoo.container.di.ComponentDeconstructor;
import com.yahoo.container.di.Container;
import com.yahoo.container.di.componentgraph.core.ComponentGraph;
@@ -82,7 +81,7 @@ public class HandlersConfigurerDi {
OsgiFramework osgiFramework) {
this(subscriberFactory, vespaContainer, configId, deconstructor, discInjector,
- new ContainerAndDiOsgi(osgiFramework, new BundleLoader(new OsgiImpl(osgiFramework))));
+ new ContainerAndDiOsgi(osgiFramework, new BundleManager(new OsgiImpl(osgiFramework))));
}
// Only public for testing
@@ -102,9 +101,9 @@ public class HandlersConfigurerDi {
private static class ContainerAndDiOsgi extends OsgiImpl implements OsgiWrapper {
private final OsgiFramework osgiFramework;
- private final BundleLoader bundleLoader;
+ private final BundleManager bundleLoader;
- public ContainerAndDiOsgi(OsgiFramework osgiFramework, BundleLoader bundleLoader) {
+ public ContainerAndDiOsgi(OsgiFramework osgiFramework, BundleManager bundleLoader) {
super(osgiFramework);
this.osgiFramework = osgiFramework;
this.bundleLoader = bundleLoader;
diff --git a/container-core/src/test/java/com/yahoo/container/core/config/BundleManagerTest.java b/container-core/src/test/java/com/yahoo/container/core/config/BundleManagerTest.java
new file mode 100644
index 00000000000..414e6b05128
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/core/config/BundleManagerTest.java
@@ -0,0 +1,106 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.core.config;
+
+import com.yahoo.config.FileReference;
+import org.junit.Before;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author gjoranv
+ */
+public class BundleManagerTest {
+
+ private static final FileReference BUNDLE_1_REF = new FileReference("bundle-1");
+ private static final Bundle BUNDLE_1 = new TestBundle(BUNDLE_1_REF.value());
+ private static final FileReference BUNDLE_2_REF = new FileReference("bundle-2");
+ private static final Bundle BUNDLE_2 = new TestBundle(BUNDLE_2_REF.value());
+
+ private BundleManager bundleLoader;
+ private TestOsgi osgi;
+
+ @Before
+ public void setup() {
+ osgi = new TestOsgi(testBundles());
+ var bundleInstaller = new TestBundleInstaller();
+ bundleLoader = new BundleManager(osgi);
+ bundleLoader.useCustomBundleInstaller(bundleInstaller);
+ }
+
+ @Test
+ public void bundles_are_installed_and_started() {
+ bundleLoader.use(List.of(BUNDLE_1_REF));
+ assertEquals(1, osgi.getInstalledBundles().size());
+
+ // The bundle is installed and started
+ TestBundle installedBundle = (TestBundle)osgi.getInstalledBundles().get(0);
+ assertEquals(BUNDLE_1.getSymbolicName(), installedBundle.getSymbolicName());
+ assertTrue(installedBundle.started);
+
+ // The file reference is active
+ assertEquals(1, bundleLoader.getActiveFileReferences().size());
+ assertEquals(BUNDLE_1_REF, bundleLoader.getActiveFileReferences().get(0));
+ }
+
+ @Test
+ public void new_bundle_can_be_installed_in_reconfig() {
+ bundleLoader.use(List.of(BUNDLE_1_REF));
+ Set<Bundle> obsoleteBundles = bundleLoader.use(List.of(BUNDLE_1_REF, BUNDLE_2_REF));
+
+ // No bundles are obsolete
+ assertTrue(obsoleteBundles.isEmpty());
+
+ // Both bundles are installed
+ assertEquals(2, osgi.getInstalledBundles().size());
+ assertEquals(BUNDLE_1.getSymbolicName(), osgi.getInstalledBundles().get(0).getSymbolicName());
+ assertEquals(BUNDLE_2.getSymbolicName(), osgi.getInstalledBundles().get(1).getSymbolicName());
+
+ // Both bundles are current
+ assertEquals(2, osgi.getCurrentBundles().size());
+ assertEquals(BUNDLE_1.getSymbolicName(), osgi.getCurrentBundles().get(0).getSymbolicName());
+ assertEquals(BUNDLE_2.getSymbolicName(), osgi.getCurrentBundles().get(1).getSymbolicName());
+
+
+ // Both file references are active
+ assertEquals(2, bundleLoader.getActiveFileReferences().size());
+ assertEquals(BUNDLE_1_REF, bundleLoader.getActiveFileReferences().get(0));
+ assertEquals(BUNDLE_2_REF, bundleLoader.getActiveFileReferences().get(1));
+ }
+
+ @Test
+ public void unused_bundle_is_marked_obsolete_after_reconfig() {
+ bundleLoader.use(List.of(BUNDLE_1_REF));
+ Set<Bundle> obsoleteBundles = bundleLoader.use(List.of(BUNDLE_2_REF));
+
+ // The returned set of obsolete bundles contains bundle-1
+ assertEquals(1, obsoleteBundles.size());
+ assertEquals(BUNDLE_1.getSymbolicName(), obsoleteBundles.iterator().next().getSymbolicName());
+
+ // Both bundles are installed
+ assertEquals(2, osgi.getInstalledBundles().size());
+ assertEquals(BUNDLE_1.getSymbolicName(), osgi.getInstalledBundles().get(0).getSymbolicName());
+ assertEquals(BUNDLE_2.getSymbolicName(), osgi.getInstalledBundles().get(1).getSymbolicName());
+
+ // Only bundle-2 is current
+ assertEquals(1, osgi.getCurrentBundles().size());
+ assertEquals(BUNDLE_2.getSymbolicName(), osgi.getCurrentBundles().get(0).getSymbolicName());
+
+ // Only the bundle-2 file reference is active
+ assertEquals(1, bundleLoader.getActiveFileReferences().size());
+ assertEquals(BUNDLE_2_REF, bundleLoader.getActiveFileReferences().get(0));
+ }
+
+
+ private static Map<String, Bundle> testBundles() {
+ return Map.of(BUNDLE_1_REF.value(), BUNDLE_1,
+ BUNDLE_2_REF.value(), BUNDLE_2);
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/container/core/config/TestBundle.java b/container-core/src/test/java/com/yahoo/container/core/config/TestBundle.java
new file mode 100644
index 00000000000..421f4302c27
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/core/config/TestBundle.java
@@ -0,0 +1,102 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.core.config;
+
+import com.yahoo.container.bundle.MockBundle;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Version;
+import org.osgi.framework.wiring.BundleCapability;
+import org.osgi.framework.wiring.BundleRequirement;
+import org.osgi.framework.wiring.BundleRevision;
+import org.osgi.framework.wiring.BundleWiring;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+
+import java.util.List;
+
+/**
+ * @author gjoranv
+ */
+class TestBundle extends MockBundle {
+
+ private static final BundleRevision revision = new TestBundleRevision();
+
+ private final String symbolicName;
+
+ boolean started = false;
+
+ TestBundle(String symbolicName) {
+ this.symbolicName = symbolicName;
+ }
+
+ @Override
+ public void start() {
+ started = true;
+ }
+
+ @Override
+ public String getSymbolicName() {
+ return symbolicName;
+ }
+
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <T> T adapt(Class<T> type) {
+ if (type.equals(BundleRevision.class)) {
+ return (T) revision;
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+
+ static class TestBundleRevision implements BundleRevision {
+
+ // Ensure this is not seen as a fragment bundle.
+ @Override
+ public int getTypes() {
+ return 0;
+ }
+
+ @Override
+ public String getSymbolicName() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Version getVersion() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<BundleCapability> getDeclaredCapabilities(String namespace) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<BundleRequirement> getDeclaredRequirements(String namespace) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public BundleWiring getWiring() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<Capability> getCapabilities(String namespace) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<Requirement> getRequirements(String namespace) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Bundle getBundle() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/container/core/config/TestBundleInstaller.java b/container-core/src/test/java/com/yahoo/container/core/config/TestBundleInstaller.java
new file mode 100644
index 00000000000..43a5268eabf
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/core/config/TestBundleInstaller.java
@@ -0,0 +1,20 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.core.config;
+
+import com.yahoo.config.FileReference;
+import com.yahoo.osgi.Osgi;
+import org.osgi.framework.Bundle;
+
+import java.util.List;
+
+/**
+ * @author gjoranv
+ */
+class TestBundleInstaller implements BundleInstaller {
+
+ @Override
+ public List<Bundle> installBundles(FileReference reference, Osgi osgi) {
+ return osgi.install(reference.value());
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/container/core/config/TestOsgi.java b/container-core/src/test/java/com/yahoo/container/core/config/TestOsgi.java
new file mode 100644
index 00000000000..54a3159239c
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/core/config/TestOsgi.java
@@ -0,0 +1,57 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.core.config;
+
+import com.yahoo.osgi.MockOsgi;
+import org.osgi.framework.Bundle;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author gjoranv
+ */
+class TestOsgi extends MockOsgi {
+
+ private final Map<String, Bundle> availableBundles;
+
+ private final List<Bundle> installedBundles = new ArrayList<>();
+ private final List<Bundle> allowedDuplicates = new ArrayList<>();
+
+ TestOsgi(Map<String, Bundle> availableBundles) {
+ this.availableBundles = availableBundles;
+ }
+
+ @Override
+ public List<Bundle> install(String fileReferenceValue) {
+ if (! availableBundles.containsKey(fileReferenceValue))
+ throw new IllegalArgumentException("No such bundle: " + fileReferenceValue);
+
+ Bundle bundle = availableBundles.get(fileReferenceValue);
+ installedBundles.add(bundle);
+ return List.of(bundle);
+ }
+
+ @Override
+ public Bundle[] getBundles() {
+ return installedBundles.toArray(new Bundle[0]);
+ }
+
+ public List<Bundle> getInstalledBundles() {
+ return installedBundles;
+ }
+
+ @Override
+ public List<Bundle> getCurrentBundles() {
+ var currentBundles = new ArrayList<>(installedBundles);
+ currentBundles.removeAll(allowedDuplicates);
+ return currentBundles;
+ }
+
+ @Override
+ public void allowDuplicateBundles(Collection<Bundle> bundles) {
+ allowedDuplicates.addAll(bundles);
+ }
+
+}