changeset 516:dc7425e6574c

fix activator test and introduce MultipleServiceTracker reviewed-by: neugens review-thread: http://icedtea.classpath.org/pipermail/thermostat/2012-August/002570.html A bundle which requires multiple externally-provided services needs to do some osgi gymnastics. This introduces a single class to perform this behaviour, and as a side effect greatly simplifies the testing of activators that need this behaviour.
author Jon VanAlten <jon.vanalten@redhat.com>
date Thu, 02 Aug 2012 13:11:13 -0400
parents ac3d23577264
children 4565a2c69167
files common/core/src/main/java/com/redhat/thermostat/common/MultipleServiceTracker.java common/core/src/test/java/com/redhat/thermostat/common/MultipleServiceTrackerTest.java distribution/config/bundles.properties launcher/pom.xml launcher/src/main/java/com/redhat/thermostat/launcher/internal/Activator.java launcher/src/main/java/com/redhat/thermostat/launcher/internal/LauncherImpl.java launcher/src/test/java/com/redhat/thermostat/launcher/LauncherTest.java launcher/src/test/java/com/redhat/thermostat/launcher/internal/ActivatorTest.java main/src/main/resources/com/redhat/thermostat/main/impl/bootstrapbundles.properties
diffstat 9 files changed, 400 insertions(+), 127 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common/core/src/main/java/com/redhat/thermostat/common/MultipleServiceTracker.java	Thu Aug 02 13:11:13 2012 -0400
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.common;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
+
+/**
+ * 
+ * This class is intended to be used within BundleActivator implementations that require
+ * some action be taken only after certain services have appeared.  It is not actually
+ * an extension to the ServiceTracker class, but embeds a number of ServiceTracker objects
+ * (one per required service) nonetheless.
+ *
+ */
+public class MultipleServiceTracker {
+
+    public interface Action {
+        public void doIt();
+    }
+
+    class InternalServiceTrackerCustomizer implements ServiceTrackerCustomizer {
+
+        private static final String OBJECT_CLASS = "objectClass";
+        private ServiceTracker tracker;
+
+        void setTracker(ServiceTracker tracker) {
+            this.tracker = tracker;
+        }
+
+        @Override
+        public Object addingService(ServiceReference reference) {
+            ensureTracker();
+            services.put(getServiceClassName(reference), context.getService(reference));
+            if (allServicesReady()) {
+                action.doIt();
+            }
+            return tracker.addingService(reference);
+        }
+
+        @Override
+        public void modifiedService(ServiceReference reference, Object service) {
+            // We don't actually need to do anything here.
+            ensureTracker();
+            tracker.modifiedService(reference, service);
+        }
+
+        @Override
+        public void removedService(ServiceReference reference, Object service) {
+            ensureTracker();
+            services.put(getServiceClassName(reference), null);
+            tracker.removedService(reference, service);
+        }
+
+        private void ensureTracker() {
+            if (tracker == null) {
+                // This class is used only internally, and is initialized within the constructor.  The trackers that could
+                // be generating events cannot be opened except by calling open on the enclosing class, so this should
+                // never ever ever ever happen.
+                throw new IllegalStateException("Trackers should not be opened before this guy has been set.");
+            }
+        }
+
+        private Object getServiceClassName(ServiceReference reference) {
+            return ((String[]) reference.getProperty(OBJECT_CLASS))[0];
+        }
+    }
+
+    private Map<Object, Object> services;
+    private Collection<ServiceTracker> trackers;
+    private Action action;
+    private BundleContext context;
+
+    public MultipleServiceTracker(BundleContext context, Class[] classes, Action action) {
+        action.getClass();
+        context.getClass();
+        classes.getClass(); // Harmless call to cause NPE if passed null.
+        this.context = context;
+        services = new HashMap<>();
+        trackers = new ArrayList<>();
+        for (Class clazz: classes) {
+            InternalServiceTrackerCustomizer tc = new InternalServiceTrackerCustomizer();
+            ServiceTracker tracker = new ServiceTracker(context, clazz.getName(), tc);
+            tc.setTracker(tracker);
+            trackers.add(tracker);
+            services.put(clazz.getName(), null);
+        }
+        this.action = action;
+    }
+
+    public void open() {
+        for (ServiceTracker tracker : trackers) {
+            tracker.open();
+        }
+    }
+
+    public void close() {
+        for (ServiceTracker tracker: trackers) {
+            tracker.close();
+        }
+    }
+
+    private boolean allServicesReady() {
+        for (Entry<Object, Object> entry: services.entrySet()) {
+            if (entry.getValue() == null) {
+                return false;
+            }
+        }
+        return true;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common/core/src/test/java/com/redhat/thermostat/common/MultipleServiceTrackerTest.java	Thu Aug 02 13:11:13 2012 -0400
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.common;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import com.redhat.thermostat.common.MultipleServiceTracker.Action;
+import com.redhat.thermostat.common.MultipleServiceTracker.InternalServiceTrackerCustomizer;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isA;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verify;
+import static org.powermock.api.mockito.PowerMockito.verifyNew;
+import static org.powermock.api.mockito.PowerMockito.whenNew;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({MultipleServiceTracker.class, InternalServiceTrackerCustomizer.class})
+public class MultipleServiceTrackerTest {
+
+    private Action action;
+    private BundleContext context;
+    private ServiceTracker objectTracker, stringTracker;
+    private ServiceReference objectReference, stringReference;
+
+    @Before
+    public void setUp() throws Exception {
+        action = mock(Action.class);
+        context = mock(BundleContext.class);
+
+        objectReference = mock(ServiceReference.class);
+        String[] objObjectClassProperty = {Object.class.getName()};
+        when(objectReference.getProperty(eq("objectClass"))).thenReturn(objObjectClassProperty);
+        when(context.getService(objectReference)).thenReturn(new Object());
+        objectTracker = mock(ServiceTracker.class);
+        whenNew(ServiceTracker.class).
+                withParameterTypes(BundleContext.class, String.class, ServiceTrackerCustomizer.class).
+                withArguments(eq(context), eq(Object.class.getName()),
+                        isA(ServiceTrackerCustomizer.class)).thenReturn(objectTracker);
+
+        stringReference = mock(ServiceReference.class);
+        String[] stringObjectClassProperty = {String.class.getName()};
+        when(stringReference.getProperty(eq("objectClass"))).thenReturn(stringObjectClassProperty);
+        when(context.getService(stringReference)).thenReturn("foo");
+        stringTracker = mock(ServiceTracker.class);
+        whenNew(ServiceTracker.class).
+                withParameterTypes(BundleContext.class, String.class, ServiceTrackerCustomizer.class).
+                withArguments(eq(context), eq(String.class.getName()),
+                        isA(ServiceTrackerCustomizer.class)).thenReturn(stringTracker);
+    }
+
+    @Test
+    public void testSingleClass() throws Exception {
+
+        Class[] deps = { Object.class };
+        MultipleServiceTracker tracker = new MultipleServiceTracker(context, deps, action);
+
+        ArgumentCaptor<ServiceTrackerCustomizer> customizerCaptor = ArgumentCaptor.forClass(ServiceTrackerCustomizer.class);
+        verifyNew(ServiceTracker.class).withArguments(eq(context),
+                eq(Object.class.getName()),
+                customizerCaptor.capture());
+        ServiceTrackerCustomizer customizer = customizerCaptor.getValue();
+
+        tracker.open();
+        verify(objectTracker).open();
+
+        customizer.addingService(objectReference);
+        verify(action).doIt();
+    }
+
+    @Test
+    public void testMultipleClasses() throws Exception {
+        Class[] deps = { Object.class, String.class };
+        MultipleServiceTracker tracker = new MultipleServiceTracker(context, deps, action);
+
+        ArgumentCaptor<ServiceTrackerCustomizer> customizerCaptor = ArgumentCaptor.forClass(ServiceTrackerCustomizer.class);
+        verifyNew(ServiceTracker.class).withArguments(eq(context),
+                eq(Object.class.getName()),
+                customizerCaptor.capture());
+        verifyNew(ServiceTracker.class).withArguments(eq(context),
+                eq(String.class.getName()),
+                customizerCaptor.capture());
+        ServiceTrackerCustomizer customizer = customizerCaptor.getValue();
+
+        tracker.open();
+        verify(objectTracker).open();
+        verify(stringTracker).open();
+
+        customizer.addingService(objectReference);
+        customizer.addingService(stringReference);
+        verify(action).doIt();
+    }
+
+    
+}
--- a/distribution/config/bundles.properties	Thu Aug 02 13:11:13 2012 -0400
+++ b/distribution/config/bundles.properties	Thu Aug 02 13:11:13 2012 -0400
@@ -1,4 +1,5 @@
 launcher = thermostat-launcher-@project.version@.jar, \
+           thermostat-keyring-@project.version@.jar, \
            thermostat-common-core-@project.version@.jar, \
            thermostat-osgi-process-handler-@project.version@.jar, \
            thermostat-tools-@project.version@.jar, \
--- a/launcher/pom.xml	Thu Aug 02 13:11:13 2012 -0400
+++ b/launcher/pom.xml	Thu Aug 02 13:11:13 2012 -0400
@@ -119,5 +119,8 @@
       <scope>test</scope>
     </dependency>
   </dependencies>
-  
+
+  <properties>
+    <argLine>-XX:-UseSplitVerifier</argLine>
+  </properties> 
 </project>
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/Activator.java	Thu Aug 02 13:11:13 2012 -0400
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/Activator.java	Thu Aug 02 13:11:13 2012 -0400
@@ -39,24 +39,45 @@
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
-import org.osgi.util.tracker.ServiceTracker;
 
 import com.redhat.thermostat.bundles.OSGiRegistryService;
 import com.redhat.thermostat.common.CommandLoadingBundleActivator;
+import com.redhat.thermostat.common.MultipleServiceTracker;
+import com.redhat.thermostat.common.MultipleServiceTracker.Action;
 import com.redhat.thermostat.common.cli.CommandContextFactory;
 import com.redhat.thermostat.launcher.Launcher;
 import com.redhat.thermostat.utils.keyring.Keyring;
 
 public class Activator extends CommandLoadingBundleActivator {
 
+    class RegisterLauncherAction implements Action {
+
+        private BundleContext context;
+
+        RegisterLauncherAction(BundleContext context) {
+            this.context = context;
+        }
+        @Override
+        public void doIt() {
+            ServiceReference reference = context.getServiceReference(OSGiRegistryService.class);
+            OSGiRegistryService bundleService = (OSGiRegistryService) context.getService(reference);
+            LauncherImpl launcher = new LauncherImpl(context,
+                    new CommandContextFactory(context), bundleService);
+            launcherServiceRegistration = context.registerService(Launcher.class.getName(), launcher, null);
+        }
+        
+    }
+
+    @SuppressWarnings("rawtypes")
     private ServiceRegistration launcherServiceRegistration;
-    private ServiceTracker bundleRegistryServiceTracker, keyringServiceTracker;
+    @SuppressWarnings("rawtypes")
+    private MultipleServiceTracker tracker;
 
     @Override
     public void start(final BundleContext context) throws Exception {
         super.start(context);
-        keyringServiceTracker = new KeyringServiceTracker(context);
-        keyringServiceTracker.open();
+        tracker = new MultipleServiceTracker(context, new Class[] {OSGiRegistryService.class, Keyring.class}, new RegisterLauncherAction(context));
+        tracker.open();
     }
 
     @Override
@@ -65,46 +86,8 @@
         if (launcherServiceRegistration != null) {
             launcherServiceRegistration.unregister();
         }
-        if (bundleRegistryServiceTracker != null) {
-            bundleRegistryServiceTracker.close();
-        }
-        if (keyringServiceTracker != null) {
-            keyringServiceTracker.close();
-        }
-    }
-
-    private class KeyringServiceTracker extends ServiceTracker {
-        public KeyringServiceTracker(BundleContext context) {
-            super(context, Keyring.class.getName(), null);
-        }
-
-        @Override
-        public Object addingService(ServiceReference reference) {
-            bundleRegistryServiceTracker = new OSGiRegistryServiceTracker(context);
-            bundleRegistryServiceTracker.open();
-            
-            return super.addingService(reference);
+        if (tracker != null) {
+            tracker.close();
         }
     }
-
-    private class OSGiRegistryServiceTracker extends ServiceTracker {
-
-        private CommandContextFactory cmdContextFactory;
-
-        public OSGiRegistryServiceTracker(BundleContext context) {
-            super(context, OSGiRegistryService.class.getName(), null);
-            this.cmdContextFactory = new CommandContextFactory(context);
-        }
-            
-        @Override
-        public Object addingService(ServiceReference reference) {
-            OSGiRegistryService bundleService = (OSGiRegistryService) context.getService(reference);
-            LauncherImpl launcher = new LauncherImpl(cmdContextFactory, bundleService);
-            launcher.setBundleContext(context);
-            launcherServiceRegistration = context.registerService(Launcher.class.getName(), launcher, null);
-
-            return super.addingService(reference);
-        }
-    }
-
 }
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/LauncherImpl.java	Thu Aug 02 13:11:13 2012 -0400
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/LauncherImpl.java	Thu Aug 02 13:11:13 2012 -0400
@@ -56,7 +56,6 @@
 import com.redhat.thermostat.common.cli.CommandContextFactory;
 import com.redhat.thermostat.common.cli.CommandException;
 import com.redhat.thermostat.common.cli.CommandRegistry;
-import com.redhat.thermostat.common.cli.OSGiContext;
 import com.redhat.thermostat.common.config.ClientPreferences;
 import com.redhat.thermostat.common.config.InvalidConfigurationException;
 import com.redhat.thermostat.common.storage.ConnectionException;
@@ -66,7 +65,7 @@
 import com.redhat.thermostat.launcher.Launcher;
 import com.redhat.thermostat.utils.keyring.Keyring;
 
-public class LauncherImpl implements Launcher, OSGiContext {
+public class LauncherImpl implements Launcher {
 
     private static final String UNKNOWN_COMMAND_MESSAGE = "unknown command '%s'\n";
 
@@ -82,17 +81,14 @@
     private BundleContext context;
     private OSGiRegistryService registry;
     
-    public LauncherImpl(CommandContextFactory cmdCtxFactory, OSGiRegistryService registry) {
+    public LauncherImpl(BundleContext context, CommandContextFactory cmdCtxFactory,
+            OSGiRegistryService registry) {
+        this.context = context;
         this.cmdCtxFactory = cmdCtxFactory;
         this.registry = registry;
     }
 
     @Override
-    public void setBundleContext(BundleContext context) {
-        this.context = context;
-    }
-
-    @Override
     public synchronized void run() {
         usageCount++;
         try {
--- a/launcher/src/test/java/com/redhat/thermostat/launcher/LauncherTest.java	Thu Aug 02 13:11:13 2012 -0400
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/LauncherTest.java	Thu Aug 02 13:11:13 2012 -0400
@@ -256,11 +256,9 @@
         });
         ctxFactory.getCommandRegistry().registerCommands(Arrays.asList(errorCmd));
 
-        LauncherImpl launcher = new LauncherImpl(ctxFactory, registry);
+        LauncherImpl launcher = new LauncherImpl(bundleContext, ctxFactory, registry);
         Keyring keyring = mock(Keyring.class);
         launcher.setPreferences(new ClientPreferences(keyring));
-        
-        launcher.setBundleContext(bundleContext);
         launcher.setArgs(new String[] { "error" });
         launcher.run();
         assertEquals("test error\n", ctxFactory.getError());
@@ -268,12 +266,10 @@
     }
 
     private void runAndVerifyCommand(String[] args, String expected) {
-        LauncherImpl launcher = new LauncherImpl(ctxFactory, registry);
+        LauncherImpl launcher = new LauncherImpl(bundleContext, ctxFactory, registry);
         
         Keyring keyring = mock(Keyring.class);
         launcher.setPreferences(new ClientPreferences(keyring));
-        
-        launcher.setBundleContext(bundleContext);
         launcher.setArgs(args);
         launcher.run();
         assertEquals(expected, ctxFactory.getOutput());
@@ -282,8 +278,7 @@
 
     @Test
     public void verifyStorageCommandSetsUpDAOFactory() {
-        LauncherImpl launcher = new LauncherImpl(ctxFactory, registry);
-        launcher.setBundleContext(bundleContext);
+        LauncherImpl launcher = new LauncherImpl(bundleContext, ctxFactory, registry);
         Keyring keyring = mock(Keyring.class);
         launcher.setPreferences(new ClientPreferences(keyring));
         
@@ -294,8 +289,7 @@
 
     @Test
     public void verifyStorageCommandSetsUpDAOFactoryWithAuth() {
-        LauncherImpl launcher = new LauncherImpl(ctxFactory, registry);
-        launcher.setBundleContext(bundleContext);
+        LauncherImpl launcher = new LauncherImpl(bundleContext, ctxFactory, registry);
         Keyring keyring = mock(Keyring.class);
         launcher.setPreferences(new ClientPreferences(keyring));
         
@@ -307,8 +301,7 @@
     public void verifyPrefsAreUsed() {
         ClientPreferences prefs = mock(ClientPreferences.class);
         when(prefs.getConnectionUrl()).thenReturn("mongo://fluff:12345");
-        LauncherImpl l = new LauncherImpl(ctxFactory, registry);
-        l.setBundleContext(bundleContext);
+        LauncherImpl l = new LauncherImpl(bundleContext, ctxFactory, registry);
         l.setPreferences(prefs);
         l.setArgs(new String[] { "test3" });
         l.run();
--- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/ActivatorTest.java	Thu Aug 02 13:11:13 2012 -0400
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/ActivatorTest.java	Thu Aug 02 13:11:13 2012 -0400
@@ -37,93 +37,97 @@
 package com.redhat.thermostat.launcher.internal;
 
 import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Matchers.isA;
-import static org.mockito.Mockito.doNothing;
+import static org.mockito.Matchers.isNull;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.powermock.api.mockito.PowerMockito.verifyNew;
+import static org.powermock.api.mockito.PowerMockito.whenNew;
 
 import java.util.Dictionary;
-import java.util.HashMap;
 import java.util.Hashtable;
-import java.util.Map;
-import java.util.Map.Entry;
 
+import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
+import org.osgi.framework.BundleActivator;
 import org.osgi.framework.BundleContext;
-import org.osgi.framework.ServiceEvent;
-import org.osgi.framework.ServiceListener;
 import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
 
+import com.redhat.thermostat.bundles.OSGiRegistryService;
+import com.redhat.thermostat.common.CommandLoadingBundleActivator;
+import com.redhat.thermostat.common.MultipleServiceTracker;
+import com.redhat.thermostat.common.MultipleServiceTracker.Action;
 import com.redhat.thermostat.common.cli.Command;
 import com.redhat.thermostat.launcher.Launcher;
+import com.redhat.thermostat.launcher.internal.Activator.RegisterLauncherAction;
+import com.redhat.thermostat.utils.keyring.Keyring;
 
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({Activator.class, CommandLoadingBundleActivator.class, RegisterLauncherAction.class, BundleActivator.class})
 public class ActivatorTest {
 
+    private BundleContext context;
+    private MultipleServiceTracker tracker;
+    private ServiceReference registryServiceReference, helpCommandReference;
+    private ServiceRegistration launcherServiceRegistration, helpCommandRegistration;
+    private OSGiRegistryService registryService;
+    private Command helpCommand;
+
+    @Before
+    public void setUp() throws Exception {
+        context = mock(BundleContext.class);
+
+        registryServiceReference = mock(ServiceReference.class);
+        launcherServiceRegistration = mock(ServiceRegistration.class);
+        registryService = mock(OSGiRegistryService.class);
+        when(context.getServiceReference(eq(OSGiRegistryService.class))).thenReturn(registryServiceReference);
+        when(context.getService(eq(registryServiceReference))).thenReturn(registryService);
+        when(context.registerService(eq(Launcher.class.getName()), any(), (Dictionary) isNull())).
+                thenReturn(launcherServiceRegistration);
+
+        helpCommandRegistration = mock(ServiceRegistration.class);
+        helpCommandReference = mock(ServiceReference.class);
+        helpCommand = mock(Command.class);
+        when(helpCommandRegistration.getReference()).thenReturn(helpCommandReference);
+        when(context.registerService(eq(Command.class.getName()), any(), isA(Dictionary.class))).
+                thenReturn(helpCommandRegistration);
+        when(context.getService(helpCommandReference)).thenReturn(helpCommand);
+
+        tracker = mock(MultipleServiceTracker.class);
+        whenNew(MultipleServiceTracker.class).
+                withParameterTypes(BundleContext.class, Class[].class, Action.class).
+                withArguments(eq(context), eq(new Class[] {OSGiRegistryService.class, Keyring.class}),
+                        isA(Action.class)).thenReturn(tracker);
+    }
+
     @Test
-    public void testRegisterServices() throws Exception {
-        final Map<ServiceRegistration, Object> regs = new HashMap<>();
-        BundleContext bCtx = mock(BundleContext.class);
-        
-        when(bCtx.registerService(anyString(), any(), any(Dictionary.class))).then(new Answer<ServiceRegistration>() {
+    public void testActivatorLifecycle() throws Exception {
+        Activator activator = new Activator();
 
-            @Override
-            public ServiceRegistration answer(InvocationOnMock invocation) throws Throwable {
-                ServiceRegistration reg = mock(ServiceRegistration.class);
-                when(reg.getReference()).thenReturn(mock(ServiceReference.class));
-                regs.put(reg, invocation.getArguments()[1]);
-                return reg;
-            }
-        });
-        when(bCtx.getService(isA(ServiceReference.class))).then(new Answer<Object>() {
-            @Override
-            public Object answer(InvocationOnMock invocation) throws Throwable {
-                ServiceReference ref = (ServiceReference) invocation.getArguments()[0];
-                for (Entry<ServiceRegistration,Object> registration: regs.entrySet()) {
-                    if (registration.getKey().getReference().equals(ref)) {
-                        return registration.getValue();
-                    }
-                }
-                return null;
-            }
-        });
-        
-        ArgumentCaptor<ServiceListener> serviceCaptor = ArgumentCaptor.forClass(ServiceListener.class);
-        doNothing().when(bCtx).addServiceListener(serviceCaptor.capture());
+        activator.start(context);
 
-        Activator activator = new Activator();
-        activator.start(bCtx);
-
-        verify(bCtx).addServiceListener(serviceCaptor.capture(), anyString());
-        ServiceListener listener = serviceCaptor.getValue();
-        
-        ServiceReference reference = mock(ServiceReference.class);
-        ServiceEvent event = mock(ServiceEvent.class);
-        when(event.getServiceReference()).thenReturn(reference);
-        when(event.getType()).thenReturn(ServiceEvent.REGISTERED);
-        
-        listener.serviceChanged(event);
-        verify(event).getServiceReference();
-        
-        
-        
         Hashtable<String, Object> props = new Hashtable<>();
         props.put(Command.NAME, "help");
-        verify(bCtx).registerService(eq(Command.class.getName()), isA(HelpCommand.class), eq(props));
+        verify(context).registerService(eq(Command.class.getName()), isA(HelpCommand.class), eq(props));
 
-        verify(bCtx).registerService(eq(Launcher.class.getName()), isA(Launcher.class), any(Dictionary.class));
-
-        activator.stop(bCtx);
+        ArgumentCaptor<Action> actionCaptor = ArgumentCaptor.forClass(Action.class);
+        verifyNew(MultipleServiceTracker.class).withArguments(eq(context),
+                eq(new Class[] {OSGiRegistryService.class, Keyring.class}),
+                actionCaptor.capture());
+        Action action = actionCaptor.getValue();
 
-        for (ServiceRegistration reg : regs.keySet()) {
-            verify(reg).unregister();
-        }
+        action.doIt();
+        verify(context).registerService(eq(Launcher.class.getName()), isA(Launcher.class), (Dictionary) isNull());
+
+        activator.stop(context);
+        verify(launcherServiceRegistration).unregister();
+        verify(tracker).close();
     }
-
 }
--- a/main/src/main/resources/com/redhat/thermostat/main/impl/bootstrapbundles.properties	Thu Aug 02 13:11:13 2012 -0400
+++ b/main/src/main/resources/com/redhat/thermostat/main/impl/bootstrapbundles.properties	Thu Aug 02 13:11:13 2012 -0400
@@ -1,4 +1,5 @@
-bundles=thermostat-common-core-${project.version}.jar, \
+bundles=thermostat-keyring-${project.version}.jar, \
+        thermostat-common-core-${project.version}.jar, \
         thermostat-bundles-${project.version}.jar, \
         thermostat-tools-${project.version}.jar, \
         thermostat-launcher-${project.version}.jar, \