FELIX-2955 Added a skeleton for a test to try and reproduce this bug. There is no test that actually tries the exact scenario yet.

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1124260 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/dependencymanager/test/src/test/java/org/apache/felix/dm/test/Ensure.java b/dependencymanager/test/src/test/java/org/apache/felix/dm/test/Ensure.java
index 07977db..1e3b29d 100644
--- a/dependencymanager/test/src/test/java/org/apache/felix/dm/test/Ensure.java
+++ b/dependencymanager/test/src/test/java/org/apache/felix/dm/test/Ensure.java
@@ -33,6 +33,7 @@
     private static final int RESOLUTION = 100;
     private static PrintStream STREAM = System.out;
     int step = 0;
+    private Throwable m_throwable;
     
     public Ensure() {
         if (DEBUG) {
@@ -145,4 +146,22 @@
             ensure.step(m_steps[m_stepIndex++]);
         }
     }
+
+    /**
+     * Saves a thrown exception that occurred in a different thread. You can only save one exception
+     * at a time this way.
+     */
+    public synchronized void throwable(Throwable throwable) {
+        m_throwable = throwable;
+    }
+
+    /**
+     * Throws a <code>Throwable</code> if one occurred in a different thread and that thread saved it
+     * using the <code>throwable()</code> method.
+     */
+    public synchronized void ensure() throws Throwable {
+        if (m_throwable != null) {
+            throw m_throwable;
+        }
+    }
 }
diff --git a/dependencymanager/test/src/test/java/org/apache/felix/dm/test/FELIX2955_ShellCommandTest.java b/dependencymanager/test/src/test/java/org/apache/felix/dm/test/FELIX2955_ShellCommandTest.java
new file mode 100644
index 0000000..b3ce9f8
--- /dev/null
+++ b/dependencymanager/test/src/test/java/org/apache/felix/dm/test/FELIX2955_ShellCommandTest.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.dm.test;
+
+import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
+import static org.ops4j.pax.exam.CoreOptions.options;
+import static org.ops4j.pax.exam.CoreOptions.provision;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import junit.framework.Assert;
+
+import org.apache.felix.dm.Component;
+import org.apache.felix.dm.DependencyManager;
+import org.apache.felix.shell.ShellService;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.Configuration;
+import org.ops4j.pax.exam.junit.JUnit4TestRunner;
+import org.osgi.framework.BundleContext;
+
+@RunWith(JUnit4TestRunner.class)
+public class FELIX2955_ShellCommandTest extends Base {
+    @Configuration
+    public static Option[] configuration() {
+        return options(
+            provision(
+                mavenBundle().groupId("org.osgi").artifactId("org.osgi.compendium").version(Base.OSGI_SPEC_VERSION),
+                mavenBundle().groupId("org.apache.felix").artifactId("org.apache.felix.configadmin").version("1.2.4"),
+                mavenBundle().groupId("org.apache.felix").artifactId("org.apache.felix.shell").version("1.4.2"),
+                mavenBundle().groupId("org.apache.felix").artifactId("org.apache.felix.dependencymanager").versionAsInProject(),
+                mavenBundle().groupId("org.apache.felix").artifactId("org.apache.felix.dependencymanager.shell").versionAsInProject()
+            )
+        );
+    }    
+    
+    @Test
+    public void testShellCommands(BundleContext context) throws Throwable {
+        DependencyManager m = new DependencyManager(context);
+        // helper class that ensures certain steps get executed in sequence
+        Ensure e = new Ensure();
+        
+        Component shellClient = m.createComponent()
+            .setImplementation(new ShellClient(e))
+            .add(m.createServiceDependency()
+                .setService(ShellService.class)
+                .setRequired(true)
+            );
+        m.add(shellClient);
+        e.waitForStep(3, 5000);
+        // now create a component with a missing dependency
+        Component missing = m.createComponent()
+            .setImplementation(new Object() { public String toString() { return "Object"; }})
+            .add(m.createServiceDependency()
+                .setService(Object.class)
+                .setRequired(true)
+            );
+        m.add(missing);
+        e.step(4);
+        e.waitForStep(5, 5000);
+        e.ensure();
+        m.remove(missing);
+        m.remove(shellClient);
+        
+    }
+    
+    public static class ShellClient {
+        volatile ShellService m_shell;
+        private final Ensure m_ensure;
+        
+        public ShellClient(Ensure e) {
+            m_ensure = e;
+        }
+
+        public void start() {
+            Thread t = new Thread("Shell Client") {
+                public void run() {
+                    m_ensure.step(1);
+                    // this first part may be brittle, since I probably cannot guarantee the name and bundle ID of the
+                    // generated probe
+                    execute("dm",
+                        "[11] pax-exam-probe\n" +
+                        "  ShellClient registered\n" +
+                        "    org.apache.felix.shell.ShellService service required available\n", 
+                        "");
+                    m_ensure.step(2);
+                    // see if there's anything that's not available
+                    execute("dm notavail",
+                        "", 
+                        "");
+                    m_ensure.step(3);
+                    // check again, now there should be something missing
+                    m_ensure.waitForStep(4, 5000);
+                    execute("dm notavail",
+                        "[11] pax-exam-probe\n" + 
+                        "  Object unregistered\n" + 
+                        "    java.lang.Object service required unavailable\n", 
+                        "");
+                    m_ensure.step(5);
+                };
+            };
+            t.start();
+        }
+        
+        @Override
+        public String toString() {
+            return "ShellClient";
+        }
+        
+        public void execute(String command, String expectedOutput, String expectedError) {
+            try {
+                ByteArrayOutputStream output = new ByteArrayOutputStream();
+                PrintStream out = new PrintStream(output);
+                ByteArrayOutputStream error = new ByteArrayOutputStream();
+                PrintStream err = new PrintStream(error);
+                m_shell.executeCommand(command, out, err);
+                Assert.assertEquals(expectedOutput, output.toString());
+                Assert.assertEquals(expectedError, error.toString());
+            }
+            catch (Throwable throwable) {
+                m_ensure.throwable(throwable);
+            }
+        }
+    }
+}