changeset 1021:8ea56b57334c

Make thermostat capable to return exit codes. Reviewed-by: vanaltj, omajid Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-February/005932.html
author Severin Gehwolf <sgehwolf@redhat.com>
date Thu, 21 Feb 2013 16:00:50 +0100
parents 2cf329c9cff9
children aa0c8707c880
files agent/cli/pom.xml agent/cli/src/main/java/com/redhat/thermostat/agent/cli/impl/AgentApplication.java agent/cli/src/main/java/com/redhat/thermostat/agent/cli/impl/db/StorageCommand.java agent/cli/src/test/java/com/redhat/thermostat/agent/cli/impl/ActivatorTest.java agent/cli/src/test/java/com/redhat/thermostat/agent/cli/impl/db/StorageCommandTest.java common/core/src/main/java/com/redhat/thermostat/common/Constants.java common/core/src/main/java/com/redhat/thermostat/common/ExitStatus.java launcher/src/main/java/com/redhat/thermostat/launcher/internal/Activator.java launcher/src/main/java/com/redhat/thermostat/launcher/internal/ExitStatusImpl.java launcher/src/main/java/com/redhat/thermostat/launcher/internal/LauncherImpl.java launcher/src/test/java/com/redhat/thermostat/launcher/internal/ActivatorTest.java launcher/src/test/java/com/redhat/thermostat/launcher/internal/DisallowSystemExitSecurityManager.java launcher/src/test/java/com/redhat/thermostat/launcher/internal/LauncherImplTest.java
diffstat 13 files changed, 386 insertions(+), 75 deletions(-) [+]
line wrap: on
line diff
--- a/agent/cli/pom.xml	Fri Mar 01 14:05:40 2013 +0100
+++ b/agent/cli/pom.xml	Thu Feb 21 16:00:50 2013 +0100
@@ -62,6 +62,16 @@
       <scope>test</scope>
     </dependency>
     <dependency>
+      <groupId>org.powermock</groupId>
+      <artifactId>powermock-api-mockito</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.powermock</groupId>
+      <artifactId>powermock-module-junit4</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
       <groupId>com.redhat.thermostat</groupId>
       <artifactId>thermostat-agent-core</artifactId>
       <version>${project.version}</version>
--- a/agent/cli/src/main/java/com/redhat/thermostat/agent/cli/impl/AgentApplication.java	Fri Mar 01 14:05:40 2013 +0100
+++ b/agent/cli/src/main/java/com/redhat/thermostat/agent/cli/impl/AgentApplication.java	Thu Feb 21 16:00:50 2013 +0100
@@ -245,7 +245,7 @@
             
         } catch (Exception e) {
             logger.log(Level.SEVERE, "Could not get BackendRegistry instance.", e);
-            System.exit(Constants.EXIT_BACKEND_LOAD_ERROR);
+            System.exit(Constants.EXIT_ERROR);
         }
 
         final Agent agent = new Agent(backendRegistry, configuration, storage, agentInfoDAO, backendInfoDAO);
@@ -259,7 +259,7 @@
             logger.log(Level.SEVERE,
                     "Agent could not start, probably because a configured backend could not be activated.",
                     le);
-            System.exit(Constants.EXIT_BACKEND_START_ERROR);
+            System.exit(Constants.EXIT_ERROR);
         }
         logger.fine("Agent started.");
 
--- a/agent/cli/src/main/java/com/redhat/thermostat/agent/cli/impl/db/StorageCommand.java	Fri Mar 01 14:05:40 2013 +0100
+++ b/agent/cli/src/main/java/com/redhat/thermostat/agent/cli/impl/db/StorageCommand.java	Thu Feb 21 16:00:50 2013 +0100
@@ -39,6 +39,12 @@
 import java.io.File;
 import java.io.IOException;
 
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+
+import com.redhat.thermostat.common.Constants;
+import com.redhat.thermostat.common.ExitStatus;
 import com.redhat.thermostat.common.cli.AbstractStateNotifyingCommand;
 import com.redhat.thermostat.common.cli.Arguments;
 import com.redhat.thermostat.common.cli.CommandContext;
@@ -54,9 +60,19 @@
 
     private DBStartupConfiguration configuration;
     private DBOptionParser parser;
+    private BundleContext context;
     
     private MongoProcessRunner runner;
     
+    public StorageCommand() {
+        this(FrameworkUtil.getBundle(StorageCommand.class).getBundleContext());
+    }
+    
+    // For testing
+    StorageCommand(BundleContext context) {
+        this.context = context;
+    }
+    
     private void parseArguments(Arguments args) throws InvalidConfigurationException {
     
         Configuration thermostatConfiguration = new Configuration();
@@ -113,14 +129,29 @@
     }
     
     private void startService() throws IOException, InterruptedException, InvalidConfigurationException, ApplicationException {
-        runner.startService();
+        try {
+            runner.startService();
+        } catch (ApplicationException | InvalidConfigurationException | IOException e) {
+            // something went wrong set status appropriately. This makes sure
+            // that the JVM exits with this status.
+            setExitStatus(Constants.EXIT_ERROR);
+            // rethrow
+            throw e;
+        }
         getNotifier().fireAction(ApplicationState.START);
     }
     
     
     private void stopService() throws IOException, InterruptedException, InvalidConfigurationException, ApplicationException {
-        check();
-        runner.stopService();
+        try {
+            check();
+            runner.stopService();
+        } catch (ApplicationException | InvalidConfigurationException | InterruptedException | IOException e) {
+            // something went wrong set status appropriately. This makes sure
+            // that the JVM exits with this status.
+            setExitStatus(Constants.EXIT_ERROR);
+            throw e;
+        }
         getNotifier().fireAction(ApplicationState.STOP);
     }
     
@@ -136,6 +167,14 @@
             throw new InvalidConfigurationException("database directories do not exist...");
         }
     }
+    
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    private void setExitStatus(final int newStatus) {
+        ServiceReference exitStatusRef = context.getServiceReference(ExitStatus.class);
+        ExitStatus status = (ExitStatus) context.getService(exitStatusRef);
+        status.setExitStatus(newStatus);
+        context.ungetService(exitStatusRef);
+    }
 
     public DBStartupConfiguration getConfiguration() {
         return configuration;
--- a/agent/cli/src/test/java/com/redhat/thermostat/agent/cli/impl/ActivatorTest.java	Fri Mar 01 14:05:40 2013 +0100
+++ b/agent/cli/src/test/java/com/redhat/thermostat/agent/cli/impl/ActivatorTest.java	Thu Feb 21 16:00:50 2013 +0100
@@ -38,18 +38,32 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.mock;
 
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.FrameworkUtil;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
 
 import com.redhat.thermostat.agent.cli.impl.db.StorageCommand;
 import com.redhat.thermostat.common.cli.Command;
 import com.redhat.thermostat.testutils.StubBundleContext;
 
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({FrameworkUtil.class})
 public class ActivatorTest {
 
     @Test
     public void verifyActivatorRegistersCommands() throws Exception {
 
+        PowerMockito.mockStatic(FrameworkUtil.class);
+        Bundle mockBundle = mock(Bundle.class);
+        when(FrameworkUtil.getBundle(StorageCommand.class)).thenReturn(mockBundle);
+        
         StubBundleContext bundleContext = new StubBundleContext();
         
         Activator activator = new Activator();
--- a/agent/cli/src/test/java/com/redhat/thermostat/agent/cli/impl/db/StorageCommandTest.java	Fri Mar 01 14:05:40 2013 +0100
+++ b/agent/cli/src/test/java/com/redhat/thermostat/agent/cli/impl/db/StorageCommandTest.java	Thu Feb 21 16:00:50 2013 +0100
@@ -37,7 +37,6 @@
 package com.redhat.thermostat.agent.cli.impl.db;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.doThrow;
@@ -47,29 +46,29 @@
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.util.Collection;
 import java.util.Properties;
 import java.util.Random;
 import java.util.concurrent.CountDownLatch;
 
 import junit.framework.Assert;
 
-import org.apache.commons.cli.Option;
-import org.apache.commons.cli.OptionGroup;
-import org.apache.commons.cli.Options;
+import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
 
-import com.redhat.thermostat.agent.cli.impl.db.MongoProcessRunner;
 import com.redhat.thermostat.common.ActionEvent;
 import com.redhat.thermostat.common.ActionListener;
+import com.redhat.thermostat.common.Constants;
+import com.redhat.thermostat.common.ExitStatus;
 import com.redhat.thermostat.common.cli.CommandContext;
 import com.redhat.thermostat.common.cli.CommandException;
 import com.redhat.thermostat.common.cli.SimpleArguments;
 import com.redhat.thermostat.common.config.InvalidConfigurationException;
 import com.redhat.thermostat.common.tools.ApplicationException;
 import com.redhat.thermostat.common.tools.ApplicationState;
+import com.redhat.thermostat.testutils.StubBundleContext;
 
 public class StorageCommandTest {
     
@@ -78,9 +77,11 @@
     private static final String DB = "storage/db";
 
     private String tmpDir;
+    private BundleContext context;
     
     @Before
     public void setup() {
+        context = mock(BundleContext.class);
         // need to create a dummy config file for the test
         try {
             Random random = new Random();
@@ -110,6 +111,11 @@
         }
     }
     
+    @After
+    public void tearDown() {
+        context = null;
+    }
+    
     @Test
     public void testConfig() throws CommandException {
         SimpleArguments args = new SimpleArguments();
@@ -119,7 +125,7 @@
         CommandContext ctx = mock(CommandContext.class);
         when(ctx.getArguments()).thenReturn(args);
 
-        StorageCommand service = new StorageCommand() {
+        StorageCommand service = new StorageCommand(context) {
             @Override
             MongoProcessRunner createRunner() {
                 throw new AssertionError("dry run should never create an actual runner");
@@ -146,7 +152,7 @@
         // TODO: stop not tested yet, but be sure it's not called from the code
         doThrow(new ApplicationException("mock exception")).when(runner).stopService();
         
-        StorageCommand service = new StorageCommand() {
+        StorageCommand service = new StorageCommand(context) {
             @Override
             MongoProcessRunner createRunner() {
                 return runner;
@@ -224,6 +230,104 @@
         
         Assert.assertTrue(result[0]);
     }
+    
+    @Test
+    public void exceptionSetsExitStatusOnFailure() throws Exception {
+        context = new StubBundleContext();
+        ExitStatus exitStatusImpl = new ExitStatus() {
+            
+            private int exitStatus = -1;
+            
+            @Override
+            public void setExitStatus(int newExitStatus) {
+                exitStatus = newExitStatus;
+            }
+            
+            @Override
+            public int getExitStatus() {
+                return exitStatus;
+            }
+        };
+        context.registerService(ExitStatus.class, exitStatusImpl, null);
+        assertEquals(-1, getExitStatusService(context).getExitStatus());
+        StorageCommand command = prepareService(false);
+        final CountDownLatch latch = new CountDownLatch(1);
+        final boolean[] result = new boolean[1];
+        command.getNotifier().addActionListener(new ActionListener<ApplicationState>() {
+            @SuppressWarnings("incomplete-switch")
+            @Override
+            public void actionPerformed(ActionEvent<ApplicationState> actionEvent) {
+                switch (actionEvent.getActionId()) {
+                case FAIL:
+                    result[0] = true;
+                    break;
+                    
+                case SUCCESS:
+                    result[0] = false;
+                    break;
+                }
+                latch.countDown();
+            }
+        });
+        command.run(prepareContext());
+        latch.await();
+        // should have failed
+        assertTrue(result[0]);
+        assertEquals(Constants.EXIT_ERROR, getExitStatusService(context).getExitStatus());
+    }
+    
+    @Test
+    public void exitStatusRemainsUntouchedOnSuccess() throws Exception {
+        context = new StubBundleContext();
+        ExitStatus exitStatusImpl = new ExitStatus() {
+            
+            private int exitStatus = -1;
+            
+            @Override
+            public void setExitStatus(int newExitStatus) {
+                exitStatus = newExitStatus;
+            }
+            
+            @Override
+            public int getExitStatus() {
+                return exitStatus;
+            }
+        };
+        context.registerService(ExitStatus.class, exitStatusImpl, null);
+        assertEquals(-1, getExitStatusService(context).getExitStatus());
+        StorageCommand command = prepareService(true);
+        final CountDownLatch latch = new CountDownLatch(1);
+        final boolean[] result = new boolean[1];
+        command.getNotifier().addActionListener(new ActionListener<ApplicationState>() {
+            @SuppressWarnings("incomplete-switch")
+            @Override
+            public void actionPerformed(ActionEvent<ApplicationState> actionEvent) {
+                switch (actionEvent.getActionId()) {
+                case FAIL:
+                    result[0] = false;
+                    break;
+                    
+                case SUCCESS:
+                    result[0] = true;
+                    break;
+                }
+                latch.countDown();
+            }
+        });
+        command.run(prepareContext());
+        latch.await();
+        // should have worked
+        assertTrue(result[0]);
+        // this impl of ExitStatus has a default value of -1
+        assertEquals(-1, getExitStatusService(context).getExitStatus());
+    }
+    
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    private ExitStatus getExitStatusService(BundleContext context) {
+        ServiceReference ref = context.getServiceReference(ExitStatus.class);
+        ExitStatus exitStatus = (ExitStatus)context.getService(ref);
+        return exitStatus;
+    }
 
     private CommandContext prepareContext() {
         SimpleArguments args = new SimpleArguments();
@@ -236,59 +340,16 @@
 
     @Test
     public void testName() {
-        StorageCommand dbService = new StorageCommand();
+        StorageCommand dbService = new StorageCommand(context);
         String name = dbService.getName();
         assertEquals("storage", name);
     }
 
     @Test
     public void testDescAndUsage() {
-        StorageCommand dbService = new StorageCommand();
+        StorageCommand dbService = new StorageCommand(context);
         assertNotNull(dbService.getDescription());
         assertNotNull(dbService.getUsage());
     }
-
-    @Ignore
-    @Test
-    public void testOptions() {
-        StorageCommand dbService = new StorageCommand();
-        Options options = dbService.getOptions();
-        assertNotNull(options);
-        assertEquals(4, options.getOptions().size());
-
-        assertTrue(options.hasOption("dryRun"));
-        Option dry = options.getOption("dryRun");
-        assertEquals("d", dry.getOpt());
-        assertEquals("run the service in dry run mode", dry.getDescription());
-        assertFalse(dry.isRequired());
-        assertFalse(dry.hasArg());
-
-        assertTrue(options.hasOption("start"));
-        Option start = options.getOption("start");
-        assertEquals("start the database", start.getDescription());
-        assertFalse(start.isRequired());
-        assertFalse(start.hasArg());
-
-        assertTrue(options.hasOption("stop"));
-        Option stop = options.getOption("stop");
-        assertEquals("stop the database", stop.getDescription());
-        assertFalse(stop.isRequired());
-        assertFalse(stop.hasArg());
-
-        assertTrue(options.hasOption("quiet"));
-        Option quiet = options.getOption("quiet");
-        assertEquals("q", quiet.getOpt());
-        assertEquals("don't produce any output", quiet.getDescription());
-        assertFalse(quiet.isRequired());
-        assertFalse(quiet.hasArg());
-
-        OptionGroup startStop = options.getOptionGroup(start);
-        assertTrue(startStop.isRequired());
-        @SuppressWarnings("unchecked")
-        Collection<Option> groupOpts = startStop.getOptions();
-        assertEquals(2, groupOpts.size());
-        assertTrue(groupOpts.contains(start));
-        assertTrue(groupOpts.contains(stop));
-    }
 }
 
--- a/common/core/src/main/java/com/redhat/thermostat/common/Constants.java	Fri Mar 01 14:05:40 2013 +0100
+++ b/common/core/src/main/java/com/redhat/thermostat/common/Constants.java	Thu Feb 21 16:00:50 2013 +0100
@@ -42,12 +42,8 @@
  */
 public class Constants {
 
-    public static final int EXIT_UNKNOWN_ERROR = 1;
-    public static final int EXIT_UNABLE_TO_CONNECT_TO_DATABASE = 2;
-    public static final int EXIT_UNABLE_TO_READ_PROPERTIES = 3;
-    public static final int EXIT_CONFIGURATION_ERROR = 4;
-    public static final int EXIT_BACKEND_LOAD_ERROR = 5;
-    public static final int EXIT_BACKEND_START_ERROR = 6;
+    public static final int EXIT_ERROR = 1;
+    public static final int EXIT_SUCCESS = 0;
 
     public static final String GENERIC_SERVICE_CLASSNAME = "GenericClassName";
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common/core/src/main/java/com/redhat/thermostat/common/ExitStatus.java	Thu Feb 21 16:00:50 2013 +0100
@@ -0,0 +1,27 @@
+package com.redhat.thermostat.common;
+
+import com.redhat.thermostat.annotations.Service;
+
+/**
+ * Service which can be used for certain commands to set the exit status on
+ * JVM termination. If the status is set multiple times, the last thread which
+ * calls {@link #setExitStatus(int)} wins.
+ */
+@Service
+public interface ExitStatus {
+
+    /**
+     * Sets the exit status, which will be used as status when the
+     * JVM terminates.
+     * 
+     * @param newExitStatus
+     */
+    void setExitStatus(int newExitStatus);
+    
+    /**
+     * Get the currently set exit status.
+     * 
+     * @return The exit status which should be used on JVM shutdown.
+     */
+    int getExitStatus();
+}
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/Activator.java	Fri Mar 01 14:05:40 2013 +0100
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/Activator.java	Thu Feb 21 16:00:50 2013 +0100
@@ -45,6 +45,8 @@
 import org.osgi.util.tracker.ServiceTracker;
 import org.osgi.util.tracker.ServiceTrackerCustomizer;
 
+import com.redhat.thermostat.common.Constants;
+import com.redhat.thermostat.common.ExitStatus;
 import com.redhat.thermostat.common.Launcher;
 import com.redhat.thermostat.common.cli.CommandContextFactory;
 import com.redhat.thermostat.common.cli.CommandInfoSource;
@@ -62,6 +64,7 @@
         private ServiceRegistration launcherReg;
         private ServiceRegistration bundleManReg;
         private ServiceRegistration cmdInfoReg;
+        private ServiceRegistration exitStatusReg;
         private BundleContext context;
         private BundleManager bundleService;
 
@@ -91,6 +94,8 @@
                     new CommandContextFactory(context), bundleService);
             launcherReg = context.registerService(Launcher.class.getName(), launcher, null);
             bundleManReg = context.registerService(BundleManager.class, bundleService, null);
+            ExitStatus exitStatus = new ExitStatusImpl(Constants.EXIT_SUCCESS);
+            exitStatusReg = context.registerService(ExitStatus.class, exitStatus, null);
             return keyring;
         }
 
@@ -105,6 +110,7 @@
             launcherReg.unregister();
             bundleManReg.unregister();
             cmdInfoReg.unregister();
+            exitStatusReg.unregister();
         }
 
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/ExitStatusImpl.java	Thu Feb 21 16:00:50 2013 +0100
@@ -0,0 +1,24 @@
+package com.redhat.thermostat.launcher.internal;
+
+import com.redhat.thermostat.common.ExitStatus;
+
+/**
+ * @see {@link ExitStatus}
+ *
+ */
+public class ExitStatusImpl implements ExitStatus {
+
+    private int exitStatus;
+    
+    ExitStatusImpl(int initialExitStatus) {
+        this.exitStatus = initialExitStatus;
+    }
+    
+    public synchronized void setExitStatus(int newExitStatus) {
+        this.exitStatus = newExitStatus;
+    }
+    
+    public synchronized int getExitStatus() {
+        return this.exitStatus;
+    }
+}
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/LauncherImpl.java	Fri Mar 01 14:05:40 2013 +0100
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/LauncherImpl.java	Thu Feb 21 16:00:50 2013 +0100
@@ -51,6 +51,8 @@
 import com.redhat.thermostat.common.ActionListener;
 import com.redhat.thermostat.common.ActionNotifier;
 import com.redhat.thermostat.common.ApplicationService;
+import com.redhat.thermostat.common.Constants;
+import com.redhat.thermostat.common.ExitStatus;
 import com.redhat.thermostat.common.Launcher;
 import com.redhat.thermostat.common.Version;
 import com.redhat.thermostat.common.cli.AbstractStateNotifyingCommand;
@@ -154,6 +156,7 @@
         return usageCount == 0;
     }
 
+    @SuppressWarnings({ "unchecked", "rawtypes" })
     private void shutdown() throws InternalError {
         try {
             ServiceReference appServiceRef = context.getServiceReference(ApplicationService.class);
@@ -165,6 +168,8 @@
                 context.ungetService(appServiceRef);
             }
 
+            // default to success for exit status
+            int exitStatus = Constants.EXIT_SUCCESS;
             if (context != null) {
                 ServiceReference storageRef = context.getServiceReference(Storage.class);
                 if (storageRef != null) {
@@ -173,9 +178,14 @@
                         storage.shutdown();
                     }
                 }
+                ServiceReference exitStatusRef = context.getServiceReference(ExitStatus.class);
+                if (exitStatusRef != null) {
+                    ExitStatus exitStatusService = (ExitStatus) context.getService(exitStatusRef);
+                    exitStatus = exitStatusService.getExitStatus();
+                }
             }
-
             context.getBundle(0).stop();
+            System.exit(exitStatus);
         } catch (BundleException e) {
             throw (InternalError) new InternalError().initCause(e);
         }
--- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/ActivatorTest.java	Fri Mar 01 14:05:40 2013 +0100
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/ActivatorTest.java	Thu Feb 21 16:00:50 2013 +0100
@@ -74,6 +74,7 @@
 import org.powermock.core.classloader.annotations.PrepareForTest;
 import org.powermock.modules.junit4.PowerMockRunner;
 
+import com.redhat.thermostat.common.ExitStatus;
 import com.redhat.thermostat.common.Launcher;
 import com.redhat.thermostat.common.MultipleServiceTracker;
 import com.redhat.thermostat.common.MultipleServiceTracker.Action;
@@ -199,6 +200,7 @@
         assertTrue(context.isServiceRegistered(CommandInfoSource.class.getName(), mock(CompoundCommandInfoSource.class).getClass()));
         assertTrue(context.isServiceRegistered(BundleManager.class.getName(), BundleManagerImpl.class));
         assertTrue(context.isServiceRegistered(Launcher.class.getName(), LauncherImpl.class));
+        assertTrue(context.isServiceRegistered(ExitStatus.class.getName(), ExitStatusImpl.class));
 
         customizer.removedService(null, null);
         
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/DisallowSystemExitSecurityManager.java	Thu Feb 21 16:00:50 2013 +0100
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2012, 2013 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.launcher.internal;
+
+import java.security.Permission;
+
+/*
+ * This is used in order to work around the problem of System.exit() being
+ * called in LauncherImpl.
+ * 
+ * This needs to be in its own file. Right now it's ownly used in
+ * LauncherImplTest. If you are getting StackOverflowErrors, make sure this
+ * SecurityManager impl isn't in the same class which is run with
+ * PowerMockRunner.
+ *
+ */
+public class DisallowSystemExitSecurityManager extends SecurityManager {
+
+    @Override
+    public void checkPermission(Permission perm) {
+        // allow anything.
+    }
+
+    @Override
+    public void checkPermission(Permission perm, Object context) {
+        // allow anything.
+    }
+
+    @Override
+    public void checkExit(int status) {
+        throw new ExitException(status);
+    }
+
+    @SuppressWarnings("serial")
+    public static class ExitException extends SecurityException {
+        private int exitStatus;
+
+        public ExitException(int exitStatus) {
+            this.exitStatus = exitStatus;
+        }
+
+        public int getExitStatus() {
+            return this.exitStatus;
+        }
+
+        @Override
+        public String getMessage() {
+            return "System.exit(" + this.exitStatus + ")";
+        }
+    }
+}
--- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/LauncherImplTest.java	Fri Mar 01 14:05:40 2013 +0100
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/LauncherImplTest.java	Thu Feb 21 16:00:50 2013 +0100
@@ -38,6 +38,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doThrow;
@@ -46,6 +47,7 @@
 import static org.mockito.Mockito.when;
 
 import java.io.IOException;
+import java.security.Permission;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -78,6 +80,7 @@
 import com.redhat.thermostat.common.ActionNotifier;
 import com.redhat.thermostat.common.ApplicationInfo;
 import com.redhat.thermostat.common.ApplicationService;
+import com.redhat.thermostat.common.Constants;
 import com.redhat.thermostat.common.Version;
 import com.redhat.thermostat.common.cli.AbstractStateNotifyingCommand;
 import com.redhat.thermostat.common.cli.Arguments;
@@ -93,6 +96,7 @@
 import com.redhat.thermostat.common.tools.ApplicationState;
 import com.redhat.thermostat.launcher.BundleManager;
 import com.redhat.thermostat.launcher.TestCommand;
+import com.redhat.thermostat.launcher.internal.DisallowSystemExitSecurityManager.ExitException;
 import com.redhat.thermostat.launcher.internal.HelpCommand;
 import com.redhat.thermostat.launcher.internal.LauncherImpl;
 import com.redhat.thermostat.launcher.internal.LauncherImpl.LoggingInitializer;
@@ -113,10 +117,17 @@
     private static final String name1 = "test1";
     private static final String name2 = "test2";
     private static final String name3 = "test3";
+    private static SecurityManager secMan;
       
     @BeforeClass
     public static void beforeClassSetUp() {
         defaultKeyringProvider = System.getProperty(KeyringProvider.KEYRING_FACTORY_PROPERTY);
+        // Launcher calls System.exit(). This causes issues for unit testing.
+        // We work around this by installing a security manager which disallows
+        // System.exit() and throws an ExitException instead. This exception in
+        // turn is caught by the wrapped launcher call.
+        secMan = System.getSecurityManager();
+        System.setSecurityManager(new DisallowSystemExitSecurityManager());
     }
     
     @AfterClass
@@ -124,6 +135,7 @@
         if (defaultKeyringProvider != null) {
             System.setProperty(KeyringProvider.KEYRING_FACTORY_PROPERTY, defaultKeyringProvider);
         }
+        System.setSecurityManager(secMan);
     }
     
     private static class TestCmd1 implements TestCommand.Handle {
@@ -440,17 +452,29 @@
         ctxFactory.getCommandRegistry().registerCommands(Arrays.asList(errorCmd));
 
         launcher.setArgs(new String[] { "error" });
-        launcher.run(false);
+        wrappedRun(launcher, false);
         assertEquals("test error\n", ctxFactory.getError());
 
     }
 
     private void runAndVerifyCommand(String[] args, String expected, boolean inShell) {
         launcher.setArgs(args);
-        launcher.run(inShell);
+        wrappedRun(launcher, inShell);
         assertEquals(expected, ctxFactory.getOutput());
         assertTrue(timerFactory.isShutdown());
     }
+    
+    private void wrappedRun(LauncherImpl launcher, boolean inShell) {
+        wrappedRun(launcher, inShell, null);
+    }
+    
+    private void wrappedRun(LauncherImpl launcher, boolean inShell, Collection<ActionListener<ApplicationState>> listeners) {
+        try {
+            launcher.run(listeners, inShell);
+        } catch (ExitException e) {
+            System.out.println(e.getMessage());
+        }
+    }
 
     @Test
     public void verifyPrefsAreUsed() {
@@ -463,7 +487,7 @@
         when(dbServiceFactory.createDbService(anyString(), anyString(), dbUrlCaptor.capture())).thenReturn(dbService);
         launcher.setPreferences(prefs);
         launcher.setArgs(new String[] { "test3" });
-        launcher.run(false);
+        wrappedRun(launcher, false);
         verify(dbService).connect();
         verify(prefs).getConnectionUrl();
         assertEquals(dbUrl, dbUrlCaptor.getValue());
@@ -485,7 +509,7 @@
         when(dbServiceFactory.createDbService(anyString(), anyString(), anyString())).thenReturn(dbService);
 
         launcher.setArgs(new String[] { "dummy" });
-        launcher.run(false);
+        wrappedRun(launcher, false);
         verify(dbService).connect();
     }
 
@@ -515,7 +539,7 @@
         PowerMockito.mockStatic(FrameworkUtil.class);
         when(FrameworkUtil.getBundle(Version.class)).thenReturn(sysBundle);
         launcher.setArgs(new String[] {Version.VERSION_OPTION});
-        launcher.run(false);
+        wrappedRun(launcher, false);
 
         assertEquals(expectedVersionInfo, ctxFactory.getOutput());
         assertTrue(timerFactory.isShutdown());
@@ -529,14 +553,14 @@
         String[] args = new String[] {"basic"};
 
         launcher.setArgs(args);
-        launcher.run(listeners, false);
+        wrappedRun(launcher, false, listeners);
         verify(notifier).addActionListener(listener);
     }
 
     @Test
     public void verifyLoggingIsInitialized() {
         launcher.setArgs(new String[] { "test1" });
-        launcher.run(false);
+        wrappedRun(launcher, false);
 
         verify(loggingInitializer).initialize();
     }
@@ -544,10 +568,23 @@
     @Test
     public void verifyShutdown() throws BundleException {
         launcher.setArgs(new String[] { "test1" });
-        launcher.run(false);
+        wrappedRun(launcher, false);
 
         verify(sysBundle).stop();
     }
+    
+    @Test
+    public void verifySetExitStatus() {
+        launcher.setArgs(new String[] { "test1" });
+        try {
+            launcher.run(false);
+            fail("Should have called System.exit()");
+        } catch (ExitException e) {
+            // pass, by default launcher exits with an exit status
+            // of 0.
+            assertEquals(Constants.EXIT_SUCCESS, e.getExitStatus());
+        }
+    }
 
     @Test
     public void verifyCommandSupportedInShellBehavesNormally() {
@@ -581,6 +618,6 @@
 
         ctxFactory.getCommandRegistry().registerCommand(mockCmd);
         runAndVerifyCommand(new String[] { cmdName }, expected, isInShell);
-    }
+    }   
 }