You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@batchee.apache.org by rm...@apache.org on 2015/11/30 11:26:18 UTC

incubator-batchee git commit: BATCHEE-75 allow custom user commands to be used in the cli + upgrade airline

Repository: incubator-batchee
Updated Branches:
  refs/heads/master 77a0a69f3 -> 0ab8285da


BATCHEE-75 allow custom user commands to be used in the cli + upgrade airline


Project: http://git-wip-us.apache.org/repos/asf/incubator-batchee/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-batchee/commit/0ab8285d
Tree: http://git-wip-us.apache.org/repos/asf/incubator-batchee/tree/0ab8285d
Diff: http://git-wip-us.apache.org/repos/asf/incubator-batchee/diff/0ab8285d

Branch: refs/heads/master
Commit: 0ab8285da00facb41808e91e0632b37f3052b0dc
Parents: 77a0a69
Author: Romain Manni-Bucau <rm...@gmail.com>
Authored: Mon Nov 30 11:26:43 2015 +0100
Committer: Romain Manni-Bucau <rm...@gmail.com>
Committed: Mon Nov 30 11:26:43 2015 +0100

----------------------------------------------------------------------
 tools/cli/pom.xml                               |  2 +-
 .../java/org/apache/batchee/cli/BatchEECLI.java | 48 ++++++++++++++--
 .../org/apache/batchee/cli/command/Abandon.java |  4 +-
 .../apache/batchee/cli/command/Executions.java  |  4 +-
 .../apache/batchee/cli/command/Instances.java   |  4 +-
 .../batchee/cli/command/JobOperatorCommand.java |  2 +-
 .../org/apache/batchee/cli/command/Names.java   |  2 +-
 .../org/apache/batchee/cli/command/Restart.java |  4 +-
 .../org/apache/batchee/cli/command/Running.java |  2 +-
 .../batchee/cli/command/SocketCommand.java      |  2 +-
 .../cli/command/SocketConfigurableCommand.java  |  2 +-
 .../org/apache/batchee/cli/command/Start.java   |  4 +-
 .../batchee/cli/command/StartableCommand.java   | 60 +++++++++++++++++---
 .../org/apache/batchee/cli/command/Status.java  |  2 +-
 .../org/apache/batchee/cli/command/Stop.java    |  4 +-
 .../apache/batchee/cli/command/UserCommand.java | 22 +++++++
 .../java/org/apache/batchee/cli/MainTest.java   | 10 +++-
 .../org/apache/batchee/cli/command/User1.java   | 27 +++++++++
 .../org/apache/batchee/cli/command/User2.java   | 27 +++++++++
 .../org.apache.batchee.cli.command.UserCommand  |  2 +
 20 files changed, 200 insertions(+), 34 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/pom.xml
----------------------------------------------------------------------
diff --git a/tools/cli/pom.xml b/tools/cli/pom.xml
index 369595f..7cd6b4d 100644
--- a/tools/cli/pom.xml
+++ b/tools/cli/pom.xml
@@ -51,7 +51,7 @@
     <dependency>
       <groupId>io.airlift</groupId>
       <artifactId>airline</artifactId>
-      <version>0.6</version>
+      <version>0.7</version>
       <exclusions>
         <exclusion>
           <groupId>javax.inject</groupId>

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/main/java/org/apache/batchee/cli/BatchEECLI.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/BatchEECLI.java b/tools/cli/src/main/java/org/apache/batchee/cli/BatchEECLI.java
index bf44e07..ef737b4 100644
--- a/tools/cli/src/main/java/org/apache/batchee/cli/BatchEECLI.java
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/BatchEECLI.java
@@ -16,9 +16,9 @@
  */
 package org.apache.batchee.cli;
 
-import io.airlift.command.Cli;
-import io.airlift.command.Help;
-import io.airlift.command.ParseException;
+import io.airlift.airline.Cli;
+import io.airlift.airline.Help;
+import io.airlift.airline.ParseException;
 import org.apache.batchee.cli.command.Abandon;
 import org.apache.batchee.cli.command.Executions;
 import org.apache.batchee.cli.command.Instances;
@@ -29,6 +29,14 @@ import org.apache.batchee.cli.command.Start;
 import org.apache.batchee.cli.command.Status;
 import org.apache.batchee.cli.command.Stop;
 
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.Enumeration;
+
+import static java.lang.ClassLoader.getSystemClassLoader;
+
 public class BatchEECLI {
     public static void main(final String[] args) {
         final Cli.CliBuilder<Runnable> builder = Cli.<Runnable>builder("batchee")
@@ -41,11 +49,41 @@ public class BatchEECLI {
                         Stop.class, Abandon.class,
                         Instances.class, Executions.class);
 
+        // user extension point
+        try { // read manually cause we dont want to instantiate them there, so no ServiceLoader
+            final ClassLoader tccl = Thread.currentThread().getContextClassLoader();
+            final ClassLoader loader = tccl != null ? tccl : getSystemClassLoader();
+            final Enumeration<URL> uc = loader.getResources("META-INF/services/org.apache.batchee.cli.command.UserCommand");
+            while (uc.hasMoreElements()) {
+                final URL url = uc.nextElement();
+                BufferedReader r = null;
+                try {
+                    r = new BufferedReader(new InputStreamReader(url.openStream()));
+                    String line;
+                    while ((line = r.readLine()) != null) {
+                        if (line.startsWith("#") || line.trim().isEmpty()) {
+                            continue;
+                        }
+                        builder.withCommand(Class.class.cast(loader.loadClass(line.trim())));
+                    }
+                } catch (final IOException ioe) {
+                    throw new IllegalStateException(ioe);
+                } catch (final ClassNotFoundException cnfe) {
+                    throw new IllegalArgumentException(cnfe);
+                } finally {
+                    if (r != null) {
+                        r.close();
+                    }
+                }
+            }
+        } catch (final IOException e) {
+            throw new IllegalStateException(e);
+        }
+
         final Cli<Runnable> parser = builder.build();
 
         try {
-            final Runnable cmd = parser.parse(args);
-            cmd.run();
+            parser.parse(args).run();
         } catch (final ParseException e) {
             parser.parse("help").run();
         }

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/main/java/org/apache/batchee/cli/command/Abandon.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/Abandon.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/Abandon.java
index 4f4259a..eed60af 100644
--- a/tools/cli/src/main/java/org/apache/batchee/cli/command/Abandon.java
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/Abandon.java
@@ -16,8 +16,8 @@
  */
 package org.apache.batchee.cli.command;
 
-import io.airlift.command.Command;
-import io.airlift.command.Option;
+import io.airlift.airline.Command;
+import io.airlift.airline.Option;
 
 @Command(name = "abandon", description = "abandon a batch from its id")
 public class Abandon extends SocketCommand {

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/main/java/org/apache/batchee/cli/command/Executions.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/Executions.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/Executions.java
index 321a49e..40d4253 100644
--- a/tools/cli/src/main/java/org/apache/batchee/cli/command/Executions.java
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/Executions.java
@@ -16,8 +16,8 @@
  */
 package org.apache.batchee.cli.command;
 
-import io.airlift.command.Command;
-import io.airlift.command.Option;
+import io.airlift.airline.Command;
+import io.airlift.airline.Option;
 import org.apache.batchee.container.impl.JobInstanceImpl;
 import org.apache.commons.lang3.StringUtils;
 

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/main/java/org/apache/batchee/cli/command/Instances.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/Instances.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/Instances.java
index 10fce0e..10a4350 100644
--- a/tools/cli/src/main/java/org/apache/batchee/cli/command/Instances.java
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/Instances.java
@@ -16,8 +16,8 @@
  */
 package org.apache.batchee.cli.command;
 
-import io.airlift.command.Command;
-import io.airlift.command.Option;
+import io.airlift.airline.Command;
+import io.airlift.airline.Option;
 
 import javax.batch.operations.JobOperator;
 import javax.batch.runtime.JobInstance;

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/main/java/org/apache/batchee/cli/command/JobOperatorCommand.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/JobOperatorCommand.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/JobOperatorCommand.java
index bb2b505..c8c3c23 100644
--- a/tools/cli/src/main/java/org/apache/batchee/cli/command/JobOperatorCommand.java
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/JobOperatorCommand.java
@@ -16,7 +16,7 @@
  */
 package org.apache.batchee.cli.command;
 
-import io.airlift.command.Option;
+import io.airlift.airline.Option;
 import org.apache.batchee.cli.classloader.ChildFirstURLClassLoader;
 import org.apache.batchee.cli.lifecycle.Lifecycle;
 import org.apache.batchee.cli.zip.Zips;

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/main/java/org/apache/batchee/cli/command/Names.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/Names.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/Names.java
index 46100ee..6ef2282 100644
--- a/tools/cli/src/main/java/org/apache/batchee/cli/command/Names.java
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/Names.java
@@ -16,7 +16,7 @@
  */
 package org.apache.batchee.cli.command;
 
-import io.airlift.command.Command;
+import io.airlift.airline.Command;
 
 import java.io.File;
 import java.io.FilenameFilter;

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/main/java/org/apache/batchee/cli/command/Restart.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/Restart.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/Restart.java
index d991b06..be345b6 100644
--- a/tools/cli/src/main/java/org/apache/batchee/cli/command/Restart.java
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/Restart.java
@@ -16,8 +16,8 @@
  */
 package org.apache.batchee.cli.command;
 
-import io.airlift.command.Command;
-import io.airlift.command.Option;
+import io.airlift.airline.Command;
+import io.airlift.airline.Option;
 
 import javax.batch.operations.JobOperator;
 

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/main/java/org/apache/batchee/cli/command/Running.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/Running.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/Running.java
index 2a6211c..7e0ff0a 100644
--- a/tools/cli/src/main/java/org/apache/batchee/cli/command/Running.java
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/Running.java
@@ -16,7 +16,7 @@
  */
 package org.apache.batchee.cli.command;
 
-import io.airlift.command.Command;
+import io.airlift.airline.Command;
 
 import javax.batch.operations.JobOperator;
 import javax.batch.operations.NoSuchJobException;

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/main/java/org/apache/batchee/cli/command/SocketCommand.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/SocketCommand.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/SocketCommand.java
index efc2070..3785d14 100644
--- a/tools/cli/src/main/java/org/apache/batchee/cli/command/SocketCommand.java
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/SocketCommand.java
@@ -16,7 +16,7 @@
  */
 package org.apache.batchee.cli.command;
 
-import io.airlift.command.Option;
+import io.airlift.airline.Option;
 import org.apache.batchee.container.exception.BatchContainerRuntimeException;
 import org.apache.commons.io.IOUtils;
 

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/main/java/org/apache/batchee/cli/command/SocketConfigurableCommand.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/SocketConfigurableCommand.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/SocketConfigurableCommand.java
index d1b2aeb..8241745 100644
--- a/tools/cli/src/main/java/org/apache/batchee/cli/command/SocketConfigurableCommand.java
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/SocketConfigurableCommand.java
@@ -16,7 +16,7 @@
  */
 package org.apache.batchee.cli.command;
 
-import io.airlift.command.Option;
+import io.airlift.airline.Option;
 
 public abstract class SocketConfigurableCommand extends JobOperatorCommand {
     @Option(name = "-wait", description = "should wait the end of the batch", arity = 1)

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/main/java/org/apache/batchee/cli/command/Start.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/Start.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/Start.java
index 84416cb..4b310c6 100644
--- a/tools/cli/src/main/java/org/apache/batchee/cli/command/Start.java
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/Start.java
@@ -16,8 +16,8 @@
  */
 package org.apache.batchee.cli.command;
 
-import io.airlift.command.Command;
-import io.airlift.command.Option;
+import io.airlift.airline.Command;
+import io.airlift.airline.Option;
 
 import javax.batch.operations.JobOperator;
 

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/main/java/org/apache/batchee/cli/command/StartableCommand.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/StartableCommand.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/StartableCommand.java
index d5f332c..6e16962 100644
--- a/tools/cli/src/main/java/org/apache/batchee/cli/command/StartableCommand.java
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/StartableCommand.java
@@ -16,7 +16,8 @@
  */
 package org.apache.batchee.cli.command;
 
-import io.airlift.command.Arguments;
+import io.airlift.airline.Arguments;
+import io.airlift.airline.Option;
 import org.apache.batchee.util.Batches;
 import org.apache.commons.io.IOUtils;
 
@@ -39,6 +40,13 @@ public abstract class StartableCommand extends SocketConfigurableCommand {
     @Arguments(description = "properties to pass to the batch")
     protected List<String> properties;
 
+    // some unix systems dont support negative systems.
+    @Option(name = "-error-exit-code", description = "exit code if any error occurs, should be > 0 or ignored")
+    protected int errorExitCode = -1;
+
+    @Option(name = "-failure-exit-code", description = "exit code if the batch result is not completed, should be > 0 and wait should be true or ignored")
+    protected int failureExitCode = -1;
+
     @Override
     public void doRun() {
         final JobOperator operator = operator();
@@ -60,23 +68,52 @@ public abstract class StartableCommand extends SocketConfigurableCommand {
             if (adminThread != null && adminThread.getServerSocket() != null) {
                 IOUtils.closeQuietly(adminThread.getServerSocket());
             }
-            e.printStackTrace();
-            //X TODO how about a system return code? otherwise shell scripts cannot see if we did fine or not...
+            if (errorExitCode >= 0) {
+                System.exit(errorExitCode);
+            }
+            e.printStackTrace(); // ensure it is traced
             return;
         }
 
-        if (wait) {
-            Batches.waitForEnd(operator, id);
-            report(operator, id);
+        try {
+            if (wait) {
+                finishBatch(operator, id);
+            }
+        } finally {
+            stopAdminThread(adminThread, id);
         }
+    }
+
+    private void finishBatch(final JobOperator operator, final long id) {
+        Batches.waitForEnd(operator, id);
+        if (report(operator, id).getBatchStatus() == BatchStatus.FAILED) {
+            if (failureExitCode >= 0) {
+                System.exit(failureExitCode);
+            }
+        }
+    }
+
+    private void stopAdminThread(final AdminThread adminThread, final long id) {
         if (adminThread != null) {
             adminThread.setId(id);
+            if (wait) {
+                try {
+                    try {
+                        adminThread.serverSocket.close();
+                    } catch (final IOException e) {
+                        // no-op
+                    }
+                    adminThread.join();
+                } catch (final InterruptedException e) {
+                    Thread.interrupted();
+                }
+            } // else let it live
         }
     }
 
     protected abstract long doStart(JobOperator operator);
 
-    private void report(final JobOperator operator, final long id) {
+    private JobExecution report(final JobOperator operator, final long id) {
         final JobExecution execution = operator.getJobExecution(id);
 
         info("");
@@ -102,6 +139,7 @@ public abstract class StartableCommand extends SocketConfigurableCommand {
         }
 
         info(LINE);
+        return execution;
     }
 
     private static String statusToString(final BatchStatus status) {
@@ -172,9 +210,13 @@ public abstract class StartableCommand extends SocketConfigurableCommand {
                     }
                 }
             } catch (final IOException e) {
-                e.printStackTrace();
+                if (!serverSocket.isClosed()) {
+                    e.printStackTrace();
+                }
             } finally {
-                IOUtils.closeQuietly(serverSocket);
+                if (!serverSocket.isClosed()) {
+                    IOUtils.closeQuietly(serverSocket);
+                }
             }
         }
 

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/main/java/org/apache/batchee/cli/command/Status.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/Status.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/Status.java
index 109cc73..b5eb0a6 100644
--- a/tools/cli/src/main/java/org/apache/batchee/cli/command/Status.java
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/Status.java
@@ -16,7 +16,7 @@
  */
 package org.apache.batchee.cli.command;
 
-import io.airlift.command.Command;
+import io.airlift.airline.Command;
 import org.apache.commons.lang3.StringUtils;
 
 import javax.batch.operations.JobOperator;

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/main/java/org/apache/batchee/cli/command/Stop.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/Stop.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/Stop.java
index 6ccc89b..d183027 100644
--- a/tools/cli/src/main/java/org/apache/batchee/cli/command/Stop.java
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/Stop.java
@@ -16,8 +16,8 @@
  */
 package org.apache.batchee.cli.command;
 
-import io.airlift.command.Command;
-import io.airlift.command.Option;
+import io.airlift.airline.Command;
+import io.airlift.airline.Option;
 
 @Command(name = "stop", description = "stop a batch from its id")
 public class Stop extends SocketCommand {

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/main/java/org/apache/batchee/cli/command/UserCommand.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/main/java/org/apache/batchee/cli/command/UserCommand.java b/tools/cli/src/main/java/org/apache/batchee/cli/command/UserCommand.java
new file mode 100644
index 0000000..3309e76
--- /dev/null
+++ b/tools/cli/src/main/java/org/apache/batchee/cli/command/UserCommand.java
@@ -0,0 +1,22 @@
+/*
+ * 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.batchee.cli.command;
+
+// use io.airlift.airline.Command and more generally io.airlift.airline.*
+// and register your command using META-INF/services/org.apache.batchee.cli.command.UserCommand mecanism
+public interface UserCommand extends Runnable {
+}

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/test/java/org/apache/batchee/cli/MainTest.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/test/java/org/apache/batchee/cli/MainTest.java b/tools/cli/src/test/java/org/apache/batchee/cli/MainTest.java
index b16f5ce..2774fcc 100644
--- a/tools/cli/src/test/java/org/apache/batchee/cli/MainTest.java
+++ b/tools/cli/src/test/java/org/apache/batchee/cli/MainTest.java
@@ -106,7 +106,7 @@ public class MainTest {
         do {
             idx = out.indexOf(str);
         } while (idx < 0);
-        final int end = out.indexOf(System.getProperty("line.separator"));
+        final int end = out.lastIndexOf(System.getProperty("line.separator"));
         final long id = Long.parseLong(out.substring(idx + str.length(), end));
 
         main(new String[]{"stop", "-id", Long.toString(id), "-socket", "1236"});
@@ -183,6 +183,14 @@ public class MainTest {
         assertEquals("start stop", MyLifecycle.result);
     }
 
+    @Test
+    public void user() {
+        for (int i = 1; i < 3; i++) {
+            main(new String[]{"user" + i});
+            assertThat(stdout.getLog(), containsString("User " + i + " called"));
+        }
+    }
+
     public static class MyLifecycle implements Lifecycle<String> {
         private static String result;
 

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/test/java/org/apache/batchee/cli/command/User1.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/test/java/org/apache/batchee/cli/command/User1.java b/tools/cli/src/test/java/org/apache/batchee/cli/command/User1.java
new file mode 100644
index 0000000..1688693
--- /dev/null
+++ b/tools/cli/src/test/java/org/apache/batchee/cli/command/User1.java
@@ -0,0 +1,27 @@
+/*
+ * 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.batchee.cli.command;
+
+import io.airlift.airline.Command;
+
+@Command(name = "user1")
+public class User1 implements UserCommand {
+    @Override
+    public void run() {
+        System.out.println("User 1 called");
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/test/java/org/apache/batchee/cli/command/User2.java
----------------------------------------------------------------------
diff --git a/tools/cli/src/test/java/org/apache/batchee/cli/command/User2.java b/tools/cli/src/test/java/org/apache/batchee/cli/command/User2.java
new file mode 100644
index 0000000..773c2ae
--- /dev/null
+++ b/tools/cli/src/test/java/org/apache/batchee/cli/command/User2.java
@@ -0,0 +1,27 @@
+/*
+ * 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.batchee.cli.command;
+
+import io.airlift.airline.Command;
+
+@Command(name = "user2")
+public class User2 implements UserCommand {
+    @Override
+    public void run() {
+        System.out.println("User 2 called");
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-batchee/blob/0ab8285d/tools/cli/src/test/resources/META-INF/services/org.apache.batchee.cli.command.UserCommand
----------------------------------------------------------------------
diff --git a/tools/cli/src/test/resources/META-INF/services/org.apache.batchee.cli.command.UserCommand b/tools/cli/src/test/resources/META-INF/services/org.apache.batchee.cli.command.UserCommand
new file mode 100644
index 0000000..bd05bf5
--- /dev/null
+++ b/tools/cli/src/test/resources/META-INF/services/org.apache.batchee.cli.command.UserCommand
@@ -0,0 +1,2 @@
+org.apache.batchee.cli.command.User1
+org.apache.batchee.cli.command.User2