add support for scripting junit test scenarios using sigil junit FELIX-2537


git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@984419 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/sigil/gogo/junit/sigil.properties b/sigil/gogo/junit/sigil.properties
index 60c6a51..dfafcde 100644
--- a/sigil/gogo/junit/sigil.properties
+++ b/sigil/gogo/junit/sigil.properties
@@ -11,7 +11,6 @@
 
 -imports: \
 	junit.framework, \
-	org.apache.commons.cli, \
 	org.apache.felix.sigil.common.junit.server, \
 	org.apache.tools.ant, \
 	org.apache.tools.ant.taskdefs.optional.junit, \
diff --git a/sigil/gogo/junit/src/org/apache/felix/gogo/options/Option.java b/sigil/gogo/junit/src/org/apache/felix/gogo/options/Option.java
new file mode 100644
index 0000000..6b4a496
--- /dev/null
+++ b/sigil/gogo/junit/src/org/apache/felix/gogo/options/Option.java
@@ -0,0 +1,159 @@
+/*
+ * 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.gogo.options;
+
+import java.util.List;
+
+public interface Option {
+    /**
+     * stop parsing on the first unknown option. This allows one parser to get its own options and
+     * then pass the remaining options to another parser.
+     * 
+     * @param stopOnBadOption
+     */
+    Option setStopOnBadOption(boolean stopOnBadOption);
+
+    /**
+     * require options to precede args. Default is false, so options can appear between or after
+     * args.
+     * 
+     * @param optionsFirst
+     */
+    Option setOptionsFirst(boolean optionsFirst);
+
+    /**
+     * parse arguments. If skipArgv0 is true, then parsing begins at arg1. This allows for commands
+     * where argv0 is the command name rather than a real argument.
+     * 
+     * @param argv
+     * @param skipArg0
+     * @return
+     */
+    Option parse(List<? extends Object> argv, boolean skipArg0);
+
+    /**
+     * parse arguments.
+     * 
+     * @see {@link #parse(List, boolean)
+
+     */
+    Option parse(List<? extends Object> argv);
+
+    /**
+     * parse arguments.
+     * 
+     * @see {@link #parse(List, boolean)
+
+     */
+    Option parse(Object[] argv, boolean skipArg0);
+
+    /**
+     * parse arguments.
+     * 
+     * @see {@link #parse(List, boolean)
+
+     */
+    Option parse(Object[] argv);
+
+    /**
+     * test whether specified option has been explicitly set.
+     * 
+     * @param name
+     * @return
+     */
+    boolean isSet(String name);
+
+    /**
+     * get value of named option. If multiple options given, this method returns the last one. Use
+     * {@link #getList(String)} to get all values.
+     * 
+     * @param name
+     * @return
+     * @throws IllegalArgumentException
+     *             if value is not a String.
+     */
+    String get(String name);
+
+    /**
+     * get list of all values for named option.
+     * 
+     * @param name
+     * @return empty list if option not given and no default specified.
+     * @throws IllegalArgumentException
+     *             if all values are not Strings.
+     */
+    List<String> getList(String name);
+
+    /**
+     * get value of named option as an Object. If multiple options given, this method returns the
+     * last one. Use {@link #getObjectList(String)} to get all values.
+     * 
+     * @param name
+     * @return
+     */
+    Object getObject(String name);
+
+    /**
+     * get list of all Object values for named option.
+     * 
+     * @param name
+     * @return
+     */
+    List<Object> getObjectList(String name);
+
+    /**
+     * get value of named option as a Number.
+     * 
+     * @param name
+     * @return
+     * @throws IllegalArgumentException
+     *             if argument is not a Number.
+     */
+    int getNumber(String name);
+
+    /**
+     * get remaining non-options args as Strings.
+     * 
+     * @return
+     * @throws IllegalArgumentException
+     *             if args are not Strings.
+     */
+    List<String> args();
+
+    /**
+     * get remaining non-options args as Objects.
+     * 
+     * @return
+     */
+    List<Object> argObjects();
+
+    /**
+     * print usage message to System.err.
+     */
+    void usage();
+
+    /**
+     * print specified usage error to System.err. You should explicitly throw the returned
+     * exception.
+     * 
+     * @param error
+     * @return IllegalArgumentException
+     */
+    IllegalArgumentException usageError(String error);
+}
diff --git a/sigil/gogo/junit/src/org/apache/felix/gogo/options/Options.java b/sigil/gogo/junit/src/org/apache/felix/gogo/options/Options.java
new file mode 100644
index 0000000..e2fdba0
--- /dev/null
+++ b/sigil/gogo/junit/src/org/apache/felix/gogo/options/Options.java
@@ -0,0 +1,528 @@
+/*
+ * 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.gogo.options;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Yet another GNU long options parser. This one is configured by parsing its Usage string.
+ */
+public class Options implements Option {
+    public static void main(String[] args) {
+        final String[] usage = {
+            "test - test Options usage",
+            "  text before Usage: is displayed when usage() is called and no error has occurred.",
+            "  so can be used as a simple help message.",
+            "",
+            "Usage: testOptions [OPTION]... PATTERN [FILES]...",
+            "  Output control: arbitary non-option text can be included.",
+            "  -? --help                show help",
+            "  -c --count=COUNT           show COUNT lines",
+            "  -h --no-filename         suppress the prefixing filename on output",
+            "  -q --quiet, --silent     suppress all normal output",
+            "     --binary-files=TYPE   assume that binary files are TYPE",
+            "                           TYPE is 'binary', 'text', or 'without-match'",
+            "  -I                       equivalent to --binary-files=without-match",
+            "  -d --directories=ACTION  how to handle directories (default=skip)",
+            "                           ACTION is 'read', 'recurse', or 'skip'",
+            "  -D --devices=ACTION      how to handle devices, FIFOs and sockets",
+            "                           ACTION is 'read' or 'skip'",
+            "  -R, -r --recursive       equivalent to --directories=recurse" };
+
+        Option opt = Options.compile(usage).parse(args);
+
+        if (opt.isSet("help")) {
+            opt.usage(); // includes text before Usage:
+            return;
+        }
+
+        if (opt.args().size() == 0)
+            throw opt.usageError("PATTERN not specified");
+
+        System.out.println(opt);
+        if (opt.isSet("count"))
+            System.out.println("count = " + opt.getNumber("count"));
+        System.out.println("--directories specified: " + opt.isSet("directories"));
+        System.out.println("directories=" + opt.get("directories"));
+    }
+
+    public static final String NL = System.getProperty("line.separator", "\n");
+
+    // Note: need to double \ within ""
+    private static final String regex = "(?x)\\s*" + "(?:-([^-]))?" + // 1: short-opt-1
+            "(?:,?\\s*-(\\w))?" + // 2: short-opt-2
+            "(?:,?\\s*--(\\w[\\w-]*)(=\\w+)?)?" + // 3: long-opt-1 and 4:arg-1
+            "(?:,?\\s*--(\\w[\\w-]*))?" + // 5: long-opt-2
+            ".*?(?:\\(default=(.*)\\))?\\s*"; // 6: default
+
+    private static final int GROUP_SHORT_OPT_1 = 1;
+    private static final int GROUP_SHORT_OPT_2 = 2;
+    private static final int GROUP_LONG_OPT_1 = 3;
+    private static final int GROUP_ARG_1 = 4;
+    private static final int GROUP_LONG_OPT_2 = 5;
+    private static final int GROUP_DEFAULT = 6;
+
+    private final Pattern parser = Pattern.compile(regex);
+    private final Pattern uname = Pattern.compile("^Usage:\\s+(\\w+)");
+
+    private final Map<String, Boolean> unmodifiableOptSet;
+    private final Map<String, Object> unmodifiableOptArg;
+    private final Map<String, Boolean> optSet = new HashMap<String, Boolean>();
+    private final Map<String, Object> optArg = new HashMap<String, Object>();
+
+    private final Map<String, String> optName = new HashMap<String, String>();
+    private final Map<String, String> optAlias = new HashMap<String, String>();
+    private final List<Object> xargs = new ArrayList<Object>();
+    private List<String> args = null;
+
+    private static final String UNKNOWN = "unknown";
+    private String usageName = UNKNOWN;
+    private int usageIndex = 0;
+
+    private final String[] spec;
+    private final String[] gspec;
+    private final String defOpts;
+    private final String[] defArgs;
+    private PrintStream errStream = System.err;
+    private String error = null;
+
+    private boolean optionsFirst = false;
+    private boolean stopOnBadOption = false;
+
+    public static Option compile(String[] optSpec) {
+        return new Options(optSpec, null, null);
+    }
+
+    public static Option compile(String optSpec) {
+        return compile(optSpec.split("\\n"));
+    }
+
+    public static Option compile(String[] optSpec, Option gopt) {
+        return new Options(optSpec, null, gopt);
+    }
+
+    public static Option compile(String[] optSpec, String[] gspec) {
+        return new Options(optSpec, gspec, null);
+    }
+
+    public Option setStopOnBadOption(boolean stopOnBadOption) {
+        this.stopOnBadOption = stopOnBadOption;
+        return this;
+    }
+
+    public Option setOptionsFirst(boolean optionsFirst) {
+        this.optionsFirst = optionsFirst;
+        return this;
+    }
+
+    public boolean isSet(String name) {
+        if (!optSet.containsKey(name))
+            throw new IllegalArgumentException("option not defined in spec: " + name);
+
+        return optSet.get(name);
+    }
+
+    public Object getObject(String name) {
+        if (!optArg.containsKey(name))
+            throw new IllegalArgumentException("option not defined with argument: " + name);
+
+        List<Object> list = getObjectList(name);
+
+        return list.isEmpty() ? "" : list.get(list.size() - 1);
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<Object> getObjectList(String name) {
+        List<Object> list;
+        Object arg = optArg.get(name);
+
+        if ( arg == null ) {
+            throw new IllegalArgumentException("option not defined with argument: " + name);
+        }
+        
+        if (arg instanceof String) { // default value
+            list = new ArrayList<Object>();
+            if (!"".equals(arg))
+                list.add(arg);
+        }
+        else {
+            list = (List<Object>) arg;
+        }
+
+        return list;
+    }
+
+    public List<String> getList(String name) {
+        ArrayList<String> list = new ArrayList<String>();
+        for (Object o : getObjectList(name)) {
+            try {
+                list.add((String) o);
+            } catch (ClassCastException e) {
+                throw new IllegalArgumentException("option not String: " + name);
+            }
+        }
+        return list;
+    }
+
+    @SuppressWarnings("unchecked")
+    private void addArg(String name, Object value) {
+        List<Object> list;
+        Object arg = optArg.get(name);
+
+        if (arg instanceof String) { // default value
+            list = new ArrayList<Object>();
+            optArg.put(name, list);
+        }
+        else {
+            list = (List<Object>) arg;
+        }
+
+        list.add(value);
+    }
+
+    public String get(String name) {
+        try {
+            return (String) getObject(name);
+        } catch (ClassCastException e) {
+            throw new IllegalArgumentException("option not String: " + name);
+        }
+    }
+
+    public int getNumber(String name) {
+        String number = get(name);
+        try {
+            if (number != null)
+                return Integer.parseInt(number);
+            return 0;
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException("option '" + name + "' not Number: " + number);
+        }
+    }
+
+    public List<Object> argObjects() {
+        return xargs;
+    }
+
+    public List<String> args() {
+        if (args == null) {
+            args = new ArrayList<String>();
+            for (Object arg : xargs) {
+                args.add(arg == null ? "null" : arg.toString());
+            }
+        }
+        return args;
+    }
+
+    public void usage() {
+        StringBuilder buf = new StringBuilder();
+        int index = 0;
+
+        if (error != null) {
+            buf.append(error);
+            buf.append(NL);
+            index = usageIndex;
+        }
+
+        for (int i = index; i < spec.length; ++i) {
+            buf.append(spec[i]);
+            buf.append(NL);
+        }
+
+        String msg = buf.toString();
+
+        if (errStream != null) {
+            errStream.print(msg);
+        }
+    }
+
+    /**
+     * prints usage message and returns IllegalArgumentException, for you to throw.
+     */
+    public IllegalArgumentException usageError(String s) {
+        error = usageName + ": " + s;
+        usage();
+        return new IllegalArgumentException(error);
+    }
+
+    // internal constructor
+    private Options(String[] spec, String[] gspec, Option opt) {
+        this.gspec = gspec;
+        Options gopt = (Options) opt;
+
+        if (gspec == null && gopt == null) {
+            this.spec = spec;
+        }
+        else {
+            ArrayList<String> list = new ArrayList<String>();
+            list.addAll(Arrays.asList(spec));
+            list.addAll(Arrays.asList(gspec != null ? gspec : gopt.gspec));
+            this.spec = list.toArray(new String[0]);
+        }
+
+        Map<String, Boolean> myOptSet = new HashMap<String, Boolean>();
+        Map<String, Object> myOptArg = new HashMap<String, Object>();
+
+        parseSpec(myOptSet, myOptArg);
+
+        if (gopt != null) {
+            for (Entry<String, Boolean> e : gopt.optSet.entrySet()) {
+                if (e.getValue())
+                    myOptSet.put(e.getKey(), true);
+            }
+
+            for (Entry<String, Object> e : gopt.optArg.entrySet()) {
+                if (!e.getValue().equals(""))
+                    myOptArg.put(e.getKey(), e.getValue());
+            }
+
+            gopt.reset();
+        }
+
+        unmodifiableOptSet = Collections.unmodifiableMap(myOptSet);
+        unmodifiableOptArg = Collections.unmodifiableMap(myOptArg);
+
+        defOpts = System.getenv(usageName.toUpperCase() + "_OPTS");
+        defArgs = (defOpts != null) ? defOpts.split("\\s+") : new String[0];
+    }
+
+    /**
+     * parse option spec.
+     */
+    private void parseSpec(Map<String, Boolean> myOptSet, Map<String, Object> myOptArg) {
+        int index = 0;
+        for (String line : spec) {
+            Matcher m = parser.matcher(line);
+
+            if (m.matches()) {
+                final String opt = m.group(GROUP_LONG_OPT_1);
+                final String name = (opt != null) ? opt : m.group(GROUP_SHORT_OPT_1);
+
+                if (name != null) {
+                    if (myOptSet.containsKey(name))
+                        throw new IllegalArgumentException("duplicate option in spec: --" + name);
+                    myOptSet.put(name, false);
+                }
+
+                String dflt = (m.group(GROUP_DEFAULT) != null) ? m.group(GROUP_DEFAULT) : "";
+                if (m.group(GROUP_ARG_1) != null)
+                    myOptArg.put(opt, dflt);
+
+                String opt2 = m.group(GROUP_LONG_OPT_2);
+                if (opt2 != null) {
+                    optAlias.put(opt2, opt);
+                    myOptSet.put(opt2, false);
+                    if (m.group(GROUP_ARG_1) != null)
+                        myOptArg.put(opt2, "");
+                }
+
+                for (int i = 0; i < 2; ++i) {
+                    String sopt = m.group(i == 0 ? GROUP_SHORT_OPT_1 : GROUP_SHORT_OPT_2);
+                    if (sopt != null) {
+                        if (optName.containsKey(sopt))
+                            throw new IllegalArgumentException("duplicate option in spec: -" + sopt);
+                        optName.put(sopt, name);
+                    }
+                }
+            }
+
+            if (usageName == UNKNOWN) {
+                Matcher u = uname.matcher(line);
+                if (u.find()) {
+                    usageName = u.group(1);
+                    usageIndex = index;
+                }
+            }
+
+            index++;
+        }
+    }
+
+    private void reset() {
+        optSet.clear();
+        optSet.putAll(unmodifiableOptSet);
+        optArg.clear();
+        optArg.putAll(unmodifiableOptArg);
+        xargs.clear();
+        args = null;
+        error = null;
+    }
+
+    public Option parse(Object[] argv) {
+        return parse(argv, false);
+    }
+
+    public Option parse(List<? extends Object> argv) {
+        return parse(argv, false);
+    }
+
+    public Option parse(Object[] argv, boolean skipArg0) {
+        if (null == argv)
+            throw new IllegalArgumentException("argv is null");
+        
+        return parse(Arrays.asList(argv), skipArg0);
+    }
+
+    public Option parse(List<? extends Object> argv, boolean skipArg0) {
+        reset();
+        List<Object> args = new ArrayList<Object>();
+        args.addAll(Arrays.asList(defArgs));
+
+        for (Object arg : argv) {
+            if (skipArg0) {
+                skipArg0 = false;
+                usageName = arg.toString();
+            }
+            else {
+                args.add(arg);
+            }
+        }
+
+        String needArg = null;
+        String needOpt = null;
+        boolean endOpt = false;
+
+        for (Object oarg : args) {
+            String arg = oarg == null ? "null" : oarg.toString();
+
+            if (endOpt) {
+                xargs.add(oarg);
+            }
+            else if (needArg != null) {
+                addArg(needArg, oarg);
+                needArg = null;
+                needOpt = null;
+            }
+            else if (!arg.startsWith("-") || "-".equals(oarg)) {
+                if (optionsFirst)
+                    endOpt = true;
+                xargs.add(oarg);
+            }
+            else {
+                if (arg.equals("--"))
+                    endOpt = true;
+                else if (arg.startsWith("--")) {
+                    int eq = arg.indexOf("=");
+                    String value = (eq == -1) ? null : arg.substring(eq + 1);
+                    String name = arg.substring(2, ((eq == -1) ? arg.length() : eq));
+                    List<String> names = new ArrayList<String>();
+
+                    if (optSet.containsKey(name)) {
+                        names.add(name);
+                    }
+                    else {
+                        for (String k : optSet.keySet()) {
+                            if (k.startsWith(name))
+                                names.add(k);
+                        }
+                    }
+
+                    switch (names.size()) {
+                    case 1:
+                        name = names.get(0);
+                        optSet.put(name, true);
+                        if (optArg.containsKey(name)) {
+                            if (value != null)
+                                addArg(name, value);
+                            else
+                                needArg = name;
+                        }
+                        else if (value != null) {
+                            throw usageError("option '--" + name + "' doesn't allow an argument");
+                        }
+                        break;
+
+                    case 0:
+                        if (stopOnBadOption) {
+                            endOpt = true;
+                            xargs.add(oarg);
+                            break;
+                        }
+                        else
+                            throw usageError("invalid option '--" + name + "'");
+
+                    default:
+                        throw usageError("option '--" + name + "' is ambiguous: " + names);
+                    }
+                }
+                else {
+                    int i = 0;
+                    for (String c : arg.substring(1).split("")) {
+                        if (i++ == 0)
+                            continue;
+                        if (optName.containsKey(c)) {
+                            String name = optName.get(c);
+                            optSet.put(name, true);
+                            if (optArg.containsKey(name)) {
+                                if (i < arg.length()) {
+                                    addArg(name, arg.substring(i));
+                                }
+                                else {
+                                    needOpt = c;
+                                    needArg = name;
+                                }
+                                break;
+                            }
+                        }
+                        else {
+                            if (stopOnBadOption) {
+                                xargs.add("-" + c);
+                                endOpt = true;
+                            }
+                            else
+                                throw usageError("invalid option '" + c + "'");
+                        }
+                    }
+                }
+            }
+        }
+
+        if (needArg != null) {
+            String name = (needOpt != null) ? needOpt : "--" + needArg;
+            throw usageError("option '" + name + "' requires an argument");
+        }
+
+        // remove long option aliases
+        for (Entry<String, String> alias : optAlias.entrySet()) {
+            if (optSet.get(alias.getKey())) {
+                optSet.put(alias.getValue(), true);
+                if (optArg.containsKey(alias.getKey()))
+                    optArg.put(alias.getValue(), optArg.get(alias.getKey()));
+            }
+            optSet.remove(alias.getKey());
+            optArg.remove(alias.getKey());
+        }
+
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "isSet" + optSet + "\nArg" + optArg + "\nargs" + xargs;
+    }
+
+}
diff --git a/sigil/gogo/junit/src/org/apache/felix/sigil/gogo/junit/Activator.java b/sigil/gogo/junit/src/org/apache/felix/sigil/gogo/junit/Activator.java
index cee4020..404b859 100644
--- a/sigil/gogo/junit/src/org/apache/felix/sigil/gogo/junit/Activator.java
+++ b/sigil/gogo/junit/src/org/apache/felix/sigil/gogo/junit/Activator.java
@@ -18,55 +18,66 @@
  */
 package org.apache.felix.sigil.gogo.junit;
 
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
 import java.util.Hashtable;
-import java.util.Map;
+
+import junit.framework.Assert;
 
 import org.apache.felix.sigil.common.junit.server.JUnitService;
 import org.osgi.framework.BundleActivator;
 import org.osgi.framework.BundleContext;
-import org.osgi.framework.ServiceReference;
-import org.osgi.framework.ServiceRegistration;
 
 import org.osgi.service.command.CommandProcessor;
 import org.osgi.util.tracker.ServiceTracker;
 
+
 public class Activator implements BundleActivator
 {
 
-    public void start(final BundleContext ctx) throws Exception
+    public void start( final BundleContext ctx ) throws Exception
     {
-        final Hashtable props = new Hashtable();
-        props.put(CommandProcessor.COMMAND_SCOPE, "sigil");
-        props.put(CommandProcessor.COMMAND_FUNCTION, new String[] { "junit" });
+        Hashtable<String, Object> props = new Hashtable<String, Object>();
+        props.put( CommandProcessor.COMMAND_SCOPE, "sigil" );
+        props.put( CommandProcessor.COMMAND_FUNCTION, new String[]
+            { "runTests", "listTests" } );
 
-        ServiceTracker tracker = new ServiceTracker(ctx, JUnitService.class.getName(),
-            null)
-        {
-            private Map<ServiceReference, ServiceRegistration> regs;
-
-            @Override
-            public Object addingService(ServiceReference reference)
-            {
-                JUnitService svc = (JUnitService) super.addingService(reference);
-                ServiceRegistration reg = ctx.registerService(SigilJunit.class.getName(),
-                    new SigilJunit(svc), props);
-                regs.put(reference, reg);
-                return svc;
-            }
-
-            @Override
-            public void removedService(ServiceReference reference, Object service)
-            {
-                ServiceRegistration reg = regs.remove(reference);
-                reg.unregister();
-                super.removedService(reference, service);
-            }
-
-        };
+        ServiceTracker tracker = new ServiceTracker( ctx, JUnitService.class.getName(), null );
         tracker.open();
+
+        ctx.registerService( SigilJunitRunner.class.getName(), new SigilJunitRunner( tracker ), props );
+
+        props.put( CommandProcessor.COMMAND_FUNCTION, new String[]
+            { "newTest", "newTestSuite" } );
+        ctx.registerService( SigilTestAdapter.class.getName(), new SigilTestAdapter(), props );
+
+        props.put( CommandProcessor.COMMAND_SCOPE, "junit" );
+        props.put( CommandProcessor.COMMAND_FUNCTION, getAssertMethods() );
+        ctx.registerService( Assert.class.getName(), new Assert()
+        {
+        }, props );
     }
 
-    public void stop(BundleContext ctx) throws Exception
+
+    /**
+     * @return
+     */
+    private String[] getAssertMethods()
+    {
+        ArrayList<String> list = new ArrayList<String>();
+        for ( Method m : Assert.class.getDeclaredMethods() )
+        {
+            if ( Modifier.isPublic( m.getModifiers() ) ) {
+                list.add( m.getName() );
+            }
+        }
+        return list.toArray( new String[list.size()] );
+    }
+
+
+    public void stop( BundleContext ctx ) throws Exception
     {
     }
 
diff --git a/sigil/gogo/junit/src/org/apache/felix/sigil/gogo/junit/SigilJunit.java b/sigil/gogo/junit/src/org/apache/felix/sigil/gogo/junit/SigilJunit.java
deleted file mode 100644
index f13839e..0000000
--- a/sigil/gogo/junit/src/org/apache/felix/sigil/gogo/junit/SigilJunit.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * 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.sigil.gogo.junit;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.LinkedList;
-import java.util.regex.Pattern;
-
-import junit.framework.TestResult;
-import junit.framework.TestSuite;
-
-import org.apache.commons.cli.CommandLine;
-import org.apache.commons.cli.GnuParser;
-import org.apache.commons.cli.Options;
-import org.apache.commons.cli.ParseException;
-import org.apache.commons.cli.Parser;
-import org.apache.felix.sigil.common.junit.server.JUnitService;
-import org.apache.tools.ant.taskdefs.optional.junit.JUnitTest;
-import org.apache.tools.ant.taskdefs.optional.junit.XMLJUnitResultFormatter;
-
-public class SigilJunit
-{
-    private static final Options OPTIONS;
-
-    static
-    {
-        OPTIONS = new Options();
-        OPTIONS.addOption("d", "dir", true, "Directory to write ant test results to");
-        OPTIONS.addOption("q", "quiet", false,
-            "Run tests quietly, i.e. don't print steps to console");
-    }
-
-    private final JUnitService service;
-
-    public SigilJunit(JUnitService service)
-    {
-        this.service = service;
-    }
-
-    public boolean junit(String[] args) throws IOException, ParseException
-    {
-        Parser p = new GnuParser();
-        CommandLine cmd = p.parse(OPTIONS, args);
-        String[] cargs = cmd.getArgs();
-        if (cargs.length == 0)
-        {
-            for (String t : service.getTests())
-            {
-                System.out.println("\t" + t);
-                System.out.flush();
-            }
-            return true;
-        }
-        else
-        {
-            boolean quiet = cmd.hasOption('q');
-            String d = cmd.getOptionValue('d');
-            File dir = null;
-            if (d != null)
-            {
-                dir = new File(d);
-                dir.mkdirs();
-                System.out.println("Writing results to " + dir.getAbsolutePath());
-                System.out.flush();
-            }
-            return runTests(cargs, quiet, dir);
-        }
-    }
-
-    private boolean runTests(String[] args, boolean quiet, File dir) throws IOException
-    {
-        int count = 0;
-        int failures = 0;
-        int errors = 0;
-        for (String t : args)
-        {
-            TestSuite[] tests = findTests(t);
-            if (tests.length == 0)
-            {
-                System.err.println("No tests found for " + t);
-            }
-            else
-            {
-                for (TestSuite test : tests)
-                {
-                    TestResult result = new TestResult();
-                    if (!quiet)
-                    {
-                        result.addListener(new PrintListener());
-                    }
-
-                    JUnitTest antTest = null;
-                    FileOutputStream fout = null;
-                    XMLJUnitResultFormatter formatter = null;
-
-                    if (dir != null)
-                    {
-                        antTest = new JUnitTest(t, false, false, true);
-
-                        formatter = new XMLJUnitResultFormatter();
-                        formatter.startTestSuite(antTest);
-
-                        String name = "TEST-" + test.getName() + ".xml";
-
-                        File f = new File(dir, name);
-                        fout = new FileOutputStream(f);
-                        formatter.setOutput(fout);
-                        result.addListener(formatter);
-                    }
-
-                    test.run(result);
-
-                    if (dir != null)
-                    {
-                        antTest.setCounts(result.runCount(), result.failureCount(),
-                            result.errorCount());
-                        formatter.endTestSuite(antTest);
-                        fout.flush();
-                        fout.close();
-                    }
-                    count += result.runCount();
-                    failures += result.failureCount();
-                    errors += result.errorCount();
-                }
-            }
-        }
-
-        System.out.println("Ran " + count + " tests. " + failures + " failures " + errors
-            + " errors.");
-        System.out.flush();
-
-        return failures + errors == 0;
-    }
-
-    private TestSuite[] findTests(String t)
-    {
-        if (t.contains("*"))
-        {
-            Pattern p = compile(t);
-            LinkedList<TestSuite> tests = new LinkedList<TestSuite>();
-            for (String n : service.getTests())
-            {
-                if (p.matcher(n).matches())
-                {
-                    tests.add(service.createTest(n));
-                }
-            }
-            return tests.toArray(new TestSuite[tests.size()]);
-        }
-        else
-        {
-            TestSuite test = service.createTest(t);
-            return test == null ? new TestSuite[0] : new TestSuite[] { test };
-        }
-    }
-
-    public static final Pattern compile(String glob)
-    {
-        char[] chars = glob.toCharArray();
-        if (chars.length > 0)
-        {
-            StringBuilder builder = new StringBuilder(chars.length + 5);
-
-            builder.append('^');
-
-            for (char c : chars)
-            {
-                switch (c)
-                {
-                    case '*':
-                        builder.append(".*");
-                        break;
-                    case '.':
-                        builder.append("\\.");
-                        break;
-                    case '$':
-                        builder.append("\\$");
-                        break;
-                    default:
-                        builder.append(c);
-                }
-            }
-
-            return Pattern.compile(builder.toString());
-        }
-        else
-        {
-            return Pattern.compile(glob);
-        }
-    }
-}
diff --git a/sigil/gogo/junit/src/org/apache/felix/sigil/gogo/junit/SigilJunitRunner.java b/sigil/gogo/junit/src/org/apache/felix/sigil/gogo/junit/SigilJunitRunner.java
new file mode 100644
index 0000000..50f3d4f
--- /dev/null
+++ b/sigil/gogo/junit/src/org/apache/felix/sigil/gogo/junit/SigilJunitRunner.java
@@ -0,0 +1,299 @@
+/*
+ * 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.sigil.gogo.junit;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import junit.framework.TestCase;
+import junit.framework.TestResult;
+import junit.framework.TestSuite;
+
+import org.apache.felix.gogo.options.Option;
+import org.apache.felix.gogo.options.Options;
+import org.apache.felix.sigil.common.junit.server.JUnitService;
+import org.apache.tools.ant.taskdefs.optional.junit.JUnitTest;
+import org.apache.tools.ant.taskdefs.optional.junit.XMLJUnitResultFormatter;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class SigilJunitRunner
+{
+    private ServiceTracker tracker;
+
+    public SigilJunitRunner(ServiceTracker tracker)
+    {
+        this.tracker = tracker;
+    }
+
+    public boolean runTests(Object[] args) throws IOException
+    {
+        final String[] usage = {
+        "runTests - run unit tests",
+        "Usage: runTests [OPTION]... [TESTS]...",
+        "  -? --help                show help",
+        "  -d --directory=DIR       Write test results to specified directory",
+        "  -q --quiet               Do not output test results to console"};
+        
+        Option opts = Options.compile( usage ).parse( args );
+        
+        boolean quiet = opts.isSet( "quiet" );
+        String d = opts.isSet( "directory" ) ? opts.get( "directory" ) : null;
+        File dir = null;
+        if (d != null)
+        {
+            dir = new File(d);
+            dir.mkdirs();
+            if (!quiet) {
+                System.out.println("Writing results to " + dir.getAbsolutePath());
+                System.out.flush();
+            }
+        }
+        
+        List<Object> tests = opts.argObjects();
+        
+        return runTests(tests, quiet, dir);
+    }
+    
+    public void listTests() {
+        JUnitService service = ( JUnitService ) tracker.getService();
+        
+        if ( service == null ) {
+            throw new IllegalStateException(JUnitService.class.getName() + " not found");
+        }
+        
+        for (String t : service.getTests())
+        {
+            System.out.println("\t" + t);
+            System.out.flush();
+        }
+    }
+    
+    private boolean runTests(List<Object> tests, boolean quiet, File dir) throws IOException
+    {
+        int count = 0;
+        int failures = 0;
+        int errors = 0;
+        
+        TestSuite[] suites = buildTestSuites(tests);
+        
+        if (suites.length > 0) {
+            // redirect io to capture test output - broken due to gogo bug
+            PrintStream oldOut = System.out;
+            PrintStream oldErr = System.err;
+            ByteArrayOutputStream tempOut = new ByteArrayOutputStream();
+            ByteArrayOutputStream tempErr = new ByteArrayOutputStream();
+            
+            System.setOut( new PrintStream( tempOut, false ) );
+            System.setErr( new PrintStream( tempErr, false ) );
+            
+            try {
+                for (TestSuite test : suites)
+                {
+                    TestResult result = new TestResult();
+    
+                    runTests(test, result, quiet, dir, tempOut, tempErr);
+    
+                    tempOut.reset();
+                    tempErr.reset();
+                    count += result.runCount();
+                    failures += result.failureCount();
+                    errors += result.errorCount();
+                }
+            }
+            finally {
+                System.setOut( oldOut );
+                System.setErr( oldErr );                    
+            }
+        }
+
+        System.out.println("Ran " + count + " tests. " + failures + " failures " + errors
+            + " errors.");
+        System.out.flush();
+
+        return failures + errors == 0;
+    }
+
+    /**
+     * @param tests
+     * @return
+     */
+    private TestSuite[] buildTestSuites( List<Object> tests )
+    {
+        ArrayList<TestSuite> suites = new ArrayList<TestSuite>(tests.size());
+        
+        for (Object o : tests) {
+            TestSuite[] s = coerceTest(o);
+            if (s.length == 0)
+            {
+                System.err.println("No tests found for " + o);
+            }
+            else
+            {
+                for (TestSuite t : s) {
+                    suites.add(t);
+                }
+            }
+        }
+        
+        return suites.toArray(new TestSuite[suites.size()]);
+    }
+
+    /**
+     * @param tempOut
+     * @param tempErr
+     * @throws IOException 
+     */
+    private void runTests( TestSuite test, TestResult result, boolean quiet, File dir, ByteArrayOutputStream tempOut, ByteArrayOutputStream tempErr ) throws IOException
+    {
+        if (!quiet)
+        {
+            result.addListener(new PrintListener());
+        }
+
+        JUnitTest antTest = null;
+        FileOutputStream fout = null;
+        XMLJUnitResultFormatter formatter = null;
+
+        if (dir != null)
+        {
+            antTest = new JUnitTest(test.getName(), false, false, true);
+
+            formatter = new XMLJUnitResultFormatter();
+            formatter.startTestSuite(antTest);
+
+            String name = "TEST-" + test.getName() + ".xml";
+
+            File f = new File(dir, name);
+            fout = new FileOutputStream(f);
+            formatter.setOutput(fout);
+            result.addListener(formatter);
+        }
+        
+        test.run(result);
+
+        System.out.flush();
+        System.err.flush();
+
+        if ( dir != null ) {
+            formatter.setSystemOutput( tempOut.toString() );
+            formatter.setSystemError( tempErr.toString() );
+            
+            antTest.setCounts(result.runCount(), result.failureCount(),
+                result.errorCount());
+            
+            formatter.endTestSuite(antTest);
+            
+            fout.flush();
+            fout.close();
+        }                        
+    }
+
+    /**
+     * @param o
+     * @return
+     */
+    private TestSuite[] coerceTest( Object o )
+    {
+        if ( o instanceof TestCase ) {
+            TestCase t = ( TestCase ) o;
+            TestSuite suite = new TestSuite(t.getName());
+            suite.addTest(t);
+            return new TestSuite[] { suite };
+        }
+        else if (o instanceof TestSuite ) {
+            return new TestSuite[] { ( TestSuite ) o };
+        }
+        else if (o instanceof String) {
+            return findTests(( String ) o);
+        }
+        else {
+            throw new IllegalArgumentException("Unexpected test type " + o.getClass().getName() );
+        }
+    }
+
+    private TestSuite[] findTests(String t)
+    {
+        JUnitService service = ( JUnitService ) tracker.getService();
+        
+        if ( service == null ) {
+            throw new IllegalStateException(JUnitService.class.getName() + " not found");
+        }
+        
+        if (t.contains("*"))
+        {
+            Pattern p = compile(t);
+            LinkedList<TestSuite> tests = new LinkedList<TestSuite>();
+            for (String n : service.getTests())
+            {
+                if (p.matcher(n).matches())
+                {
+                    tests.add(service.createTest(n));
+                }
+            }
+            return tests.toArray(new TestSuite[tests.size()]);
+        }
+        else
+        {
+            TestSuite test = service.createTest(t);
+            return test == null ? new TestSuite[0] : new TestSuite[] { test };
+        }
+    }
+
+    public static final Pattern compile(String glob)
+    {
+        char[] chars = glob.toCharArray();
+        if (chars.length > 0)
+        {
+            StringBuilder builder = new StringBuilder(chars.length + 5);
+
+            builder.append('^');
+
+            for (char c : chars)
+            {
+                switch (c)
+                {
+                    case '*':
+                        builder.append(".*");
+                        break;
+                    case '.':
+                        builder.append("\\.");
+                        break;
+                    case '$':
+                        builder.append("\\$");
+                        break;
+                    default:
+                        builder.append(c);
+                }
+            }
+
+            return Pattern.compile(builder.toString());
+        }
+        else
+        {
+            return Pattern.compile(glob);
+        }
+    }
+}
diff --git a/sigil/gogo/junit/src/org/apache/felix/sigil/gogo/junit/SigilTestAdapter.java b/sigil/gogo/junit/src/org/apache/felix/sigil/gogo/junit/SigilTestAdapter.java
new file mode 100644
index 0000000..8137759
--- /dev/null
+++ b/sigil/gogo/junit/src/org/apache/felix/sigil/gogo/junit/SigilTestAdapter.java
@@ -0,0 +1,87 @@
+/*
+ * 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.sigil.gogo.junit;
+
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+
+import junit.framework.AssertionFailedError;
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestResult;
+import junit.framework.TestSuite;
+
+import org.osgi.service.command.CommandSession;
+import org.osgi.service.command.Function;
+
+
+public class SigilTestAdapter
+{
+    public TestCase newTest( final CommandSession session, final String name, final Function f, final Object... args )
+    {
+        return new TestCase( name )
+        {
+            public int countTestCases()
+            {
+                return 1;
+            }
+
+
+            public void run( TestResult result )
+            {
+                try
+                {
+                    f.execute( session, Arrays.asList( args ) );
+                }
+                catch ( InvocationTargetException e )
+                {
+                    Throwable c = e.getCause();
+                    if ( c instanceof AssertionFailedError )
+                    {
+                        result.addFailure( this, ( AssertionFailedError ) c );
+                    }
+                    else
+                    {
+                        result.addError( this, c );
+                    }
+                }
+                catch ( AssertionFailedError e )
+                {
+                    result.addFailure( this, e );
+                }
+                catch ( Throwable t )
+                {
+                    result.addError( this, t );
+                }
+            }
+        };
+    }
+
+
+    public TestSuite newTestSuite( String name, Test... tests )
+    {
+        TestSuite suite = new TestSuite( name );
+        for ( Test t : tests )
+        {
+            suite.addTest( t );
+        }
+        return suite;
+    }
+}