diff --git a/core/src/main/java/hudson/cli/CLIAction.java b/core/src/main/java/hudson/cli/CLIAction.java new file mode 100644 index 0000000000000000000000000000000000000000..bb7a396da8600e6a712e8d38ec8f044929e938bd --- /dev/null +++ b/core/src/main/java/hudson/cli/CLIAction.java @@ -0,0 +1,123 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.cli; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; + +import jenkins.model.Jenkins; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import hudson.Extension; +import hudson.model.FullDuplexHttpChannel; +import hudson.model.RootAction; +import hudson.remoting.Channel; + +/** + * @author ogondza + */ +@Extension +@Restricted(NoExternalUse.class) +public class CLIAction implements RootAction { + + private transient final Map duplexChannels = new HashMap(); + + public String getIconFileName() { + return null; + } + + public String getDisplayName() { + + return "Jenkins CLI"; + } + + public String getUrlName() { + + return "/cli"; + } + + public void doCommand(StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException { + final Jenkins jenkins = Jenkins.getInstance(); + jenkins.checkPermission(Jenkins.READ); + + // Strip trailing slash + final String commandName = req.getRestOfPath().substring(1); + CLICommand command = CLICommand.clone(commandName); + if (command == null) { + rsp.sendError(HttpServletResponse.SC_NOT_FOUND, "No such command " + commandName); + return; + } + + req.setAttribute("command", command); + req.getView(this, "command.jelly").forward(req, rsp); + } + + /** + * Handles HTTP requests for duplex channels for CLI. + */ + public void doIndex(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, InterruptedException { + final Jenkins jenkins = Jenkins.getInstance(); + if (!"POST".equals(req.getMethod())) { + // for GET request, serve _cli.jelly, assuming this is a browser + jenkins.checkPermission(Jenkins.READ); + req.setAttribute("command", CLICommand.clone("help")); + req.getView(this,"index.jelly").forward(req,rsp); + return; + } + + // do not require any permission to establish a CLI connection + // the actual authentication for the connecting Channel is done by CLICommand + + UUID uuid = UUID.fromString(req.getHeader("Session")); + rsp.setHeader("Hudson-Duplex",""); // set the header so that the client would know + + FullDuplexHttpChannel server; + if(req.getHeader("Side").equals("download")) { + duplexChannels.put(uuid,server=new FullDuplexHttpChannel(uuid, !jenkins.hasPermission(Jenkins.ADMINISTER)) { + @Override + protected void main(Channel channel) throws IOException, InterruptedException { + // capture the identity given by the transport, since this can be useful for SecurityRealm.createCliAuthenticator() + channel.setProperty(CLICommand.TRANSPORT_AUTHENTICATION, Jenkins.getAuthentication()); + channel.setProperty(CliEntryPoint.class.getName(),new CliManagerImpl(channel)); + } + }); + try { + server.download(req,rsp); + } finally { + duplexChannels.remove(uuid); + } + } else { + duplexChannels.get(uuid).upload(req,rsp); + } + } +} diff --git a/core/src/main/java/hudson/cli/CLICommand.java b/core/src/main/java/hudson/cli/CLICommand.java index c5a672ff40bb73074d65b7451f873b9e82c31e16..86c8aa04dc75e2421b64a00241fb22492fa04d72 100644 --- a/core/src/main/java/hudson/cli/CLICommand.java +++ b/core/src/main/java/hudson/cli/CLICommand.java @@ -46,12 +46,15 @@ import org.apache.commons.discovery.resource.classes.DiscoverClasses; import org.apache.commons.discovery.resource.names.DiscoverServiceNames; import org.jvnet.hudson.annotation_indexer.Index; import org.jvnet.tiger_types.Types; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.args4j.ClassParser; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.spi.OptionHandler; import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; @@ -208,7 +211,7 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { this.stderr = stderr; this.locale = locale; registerOptionHandlers(); - CmdLineParser p = new CmdLineParser(this); + CmdLineParser p = getCmdLineParser(); // add options from the authenticator SecurityContext sc = SecurityContextHolder.getContext(); @@ -242,6 +245,16 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { sc.setAuthentication(old); // restore } } + + /** + * Get parser for this command. + * + * Exposed to be overridden by {@link CLIRegisterer}. + * @since TODO + */ + protected CmdLineParser getCmdLineParser() { + return new CmdLineParser(this); + } public Channel checkChannel() throws AbortException { if (channel==null) @@ -329,11 +342,46 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { protected abstract int run() throws Exception; protected void printUsage(PrintStream stderr, CmdLineParser p) { - stderr.println("java -jar jenkins-cli.jar "+getName()+" args..."); + stderr.print("java -jar jenkins-cli.jar " + getName()); + p.printSingleLineUsage(stderr); + stderr.println(); printUsageSummary(stderr); p.printUsage(stderr); } + /** + * Get single line summary as a string. + */ + @Restricted(NoExternalUse.class) + public final String getSingleLineSummary() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + getCmdLineParser().printSingleLineUsage(out); + return out.toString(); + } + + /** + * Get usage as a string. + */ + @Restricted(NoExternalUse.class) + public final String getUsage() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + getCmdLineParser().printUsage(out); + return out.toString(); + } + + /** + * Get long description as a string. + */ + @Restricted(NoExternalUse.class) + public final String getLongDescription() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(out); + + printUsageSummary(ps); + ps.close(); + return out.toString(); + } + /** * Called while producing usage. This is a good method to override * to render the general description of the command that goes beyond diff --git a/core/src/main/java/hudson/cli/GroovyCommand.java b/core/src/main/java/hudson/cli/GroovyCommand.java index 7035bfdaf9591a6f0ec8a0784145a2a09ad4e474..7866e2f7228d6fde0ea480631c52676da97702fc 100644 --- a/core/src/main/java/hudson/cli/GroovyCommand.java +++ b/core/src/main/java/hudson/cli/GroovyCommand.java @@ -66,7 +66,7 @@ public class GroovyCommand extends CLICommand { /** * Remaining arguments. */ - @Argument(index=1) + @Argument(metaVar="ARGUMENTS", index=1, usage="Command line arguments to pass into script.") public List remaining = new ArrayList(); protected int run() throws Exception { diff --git a/core/src/main/java/hudson/cli/HelpCommand.java b/core/src/main/java/hudson/cli/HelpCommand.java index d49dd2bccbef7920a9387ae27969ff93fee32fe3..bebb39dfcd9ce8aff1f1fb1a42c685e9ce941899 100644 --- a/core/src/main/java/hudson/cli/HelpCommand.java +++ b/core/src/main/java/hudson/cli/HelpCommand.java @@ -29,6 +29,8 @@ import jenkins.model.Jenkins; import java.util.Map; import java.util.TreeMap; +import org.kohsuke.args4j.Argument; + /** * Show the list of all commands. * @@ -36,18 +38,32 @@ import java.util.TreeMap; */ @Extension public class HelpCommand extends CLICommand { + + @Argument(metaVar="COMMAND", usage="Name of the command") + public String command; + @Override public String getShortDescription() { return Messages.HelpCommand_ShortDescription(); } + @Override protected int run() { if (!Jenkins.getInstance().hasPermission(Jenkins.READ)) { stderr.println("You must authenticate to access this Jenkins.\n" + "Use --username/--password/--password-file parameters or login command."); - return 0; + return -1; } + if (command != null) + return showCommandDetails(); + + showAllCommands(); + + return 0; + } + + private int showAllCommands() { Map commands = new TreeMap(); for (CLICommand c : CLICommand.all()) commands.put(c.getName(),c); @@ -56,6 +72,19 @@ public class HelpCommand extends CLICommand { stderr.println(" "+c.getName()); stderr.println(" "+c.getShortDescription()); } + + return 0; + } + + private int showCommandDetails() { + CLICommand command = CLICommand.clone(this.command); + if (command == null) { + stderr.format("No such command %s. Awailable commands are: ", this.command); + showAllCommands(); + return -1; + } + + command.printUsage(stderr, command.getCmdLineParser()); return 0; } diff --git a/core/src/main/java/hudson/cli/SetBuildParameterCommand.java b/core/src/main/java/hudson/cli/SetBuildParameterCommand.java index 2862e9e8d79f8e6c65da69f4f7cb917f26a98aae..d2c976e5e7aa14f3a6d031511b22178a108e579c 100644 --- a/core/src/main/java/hudson/cli/SetBuildParameterCommand.java +++ b/core/src/main/java/hudson/cli/SetBuildParameterCommand.java @@ -18,10 +18,10 @@ import java.util.Collections; */ @Extension public class SetBuildParameterCommand extends CommandDuringBuild { - @Argument(index=0, required=true, usage="Name of the build variable") + @Argument(index=0, metaVar="NAME", required=true, usage="Name of the build variable") public String name; - @Argument(index=1,required=true, usage="Value of the build variable") + @Argument(index=1, metaVar="VALUE", required=true, usage="Value of the build variable") public String value; @Override diff --git a/core/src/main/java/hudson/cli/declarative/CLIRegisterer.java b/core/src/main/java/hudson/cli/declarative/CLIRegisterer.java index 9cc5448616d51fb0ead5aa7e9edd41449099bacd..cb1d1db8f2386c23f3f10a8c7b7cd2ee2e97de57 100644 --- a/core/src/main/java/hudson/cli/declarative/CLIRegisterer.java +++ b/core/src/main/java/hudson/cli/declarative/CLIRegisterer.java @@ -110,48 +110,65 @@ public class CLIRegisterer extends ExtensionFinder { return name; } + @Override public String getShortDescription() { // format by using the right locale return res.format("CLI."+name+".shortDescription"); } + @Override + protected CmdLineParser getCmdLineParser() { + return bindMethod(new ArrayList()); + } + + private CmdLineParser bindMethod(List binders) { + + registerOptionHandlers(); + CmdLineParser parser = new CmdLineParser(null); + + // build up the call sequence + Stack chains = new Stack(); + Method method = m; + while (true) { + chains.push(method); + if (Modifier.isStatic(method.getModifiers())) + break; // the chain is complete. + + // the method in question is an instance method, so we need to resolve the instance by using another resolver + Class type = method.getDeclaringClass(); + try { + method = findResolver(type); + } catch (IOException ex) { + throw new RuntimeException("Unable to find the resolver method annotated with @CLIResolver for "+type, ex); + } + if (method==null) { + throw new RuntimeException("Unable to find the resolver method annotated with @CLIResolver for "+type); + } + } + + while (!chains.isEmpty()) + binders.add(new MethodBinder(chains.pop(),this,parser)); + + new ClassParser().parse(Jenkins.getInstance().getSecurityRealm().createCliAuthenticator(this), parser); + + return parser; + } + @Override public int main(List args, Locale locale, InputStream stdin, PrintStream stdout, PrintStream stderr) { this.stdout = stdout; this.stderr = stderr; this.locale = locale; - registerOptionHandlers(); - CmdLineParser parser = new CmdLineParser(null); + List binders = new ArrayList(); + + CmdLineParser parser = bindMethod(binders); try { SecurityContext sc = SecurityContextHolder.getContext(); Authentication old = sc.getAuthentication(); try { - // build up the call sequence - Stack chains = new Stack(); - Method method = m; - while (true) { - chains.push(method); - if (Modifier.isStatic(method.getModifiers())) - break; // the chain is complete. - - // the method in question is an instance method, so we need to resolve the instance by using another resolver - Class type = method.getDeclaringClass(); - method = findResolver(type); - if (method==null) { - stderr.println("Unable to find the resolver method annotated with @CLIResolver for "+type); - return 1; - } - } - - List binders = new ArrayList(); - - while (!chains.isEmpty()) - binders.add(new MethodBinder(chains.pop(),this,parser)); - // authentication CliAuthenticator authenticator = Jenkins.getInstance().getSecurityRealm().createCliAuthenticator(this); - new ClassParser().parse(authenticator,parser); // fill up all the binders parser.parseArgument(args); diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 5db624e3d191d3697cbc305f55c84a2b898227e1..e6898597284f59f67f1f9bf55e3d62bae34e1798 100755 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -3183,44 +3183,6 @@ public class Jenkins extends AbstractCIBase implements ModifiableTopLevelItemGro args.add(new byte[1024*1024]); } - private transient final Map duplexChannels = new HashMap(); - - /** - * Handles HTTP requests for duplex channels for CLI. - */ - public void doCli(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, InterruptedException { - if (!"POST".equals(req.getMethod())) { - // for GET request, serve _cli.jelly, assuming this is a browser - checkPermission(READ); - req.getView(this,"_cli.jelly").forward(req,rsp); - return; - } - - // do not require any permission to establish a CLI connection - // the actual authentication for the connecting Channel is done by CLICommand - - UUID uuid = UUID.fromString(req.getHeader("Session")); - rsp.setHeader("Hudson-Duplex",""); // set the header so that the client would know - - FullDuplexHttpChannel server; - if(req.getHeader("Side").equals("download")) { - duplexChannels.put(uuid,server=new FullDuplexHttpChannel(uuid, !hasPermission(ADMINISTER)) { - protected void main(Channel channel) throws IOException, InterruptedException { - // capture the identity given by the transport, since this can be useful for SecurityRealm.createCliAuthenticator() - channel.setProperty(CLICommand.TRANSPORT_AUTHENTICATION,getAuthentication()); - channel.setProperty(CliEntryPoint.class.getName(),new CliManagerImpl(channel)); - } - }); - try { - server.download(req,rsp); - } finally { - duplexChannels.remove(uuid); - } - } else { - duplexChannels.get(uuid).upload(req,rsp); - } - } - /** * Binds /userContent/... to $JENKINS_HOME/userContent. */ diff --git a/core/src/main/resources/hudson/cli/CLIAction/command.jelly b/core/src/main/resources/hudson/cli/CLIAction/command.jelly new file mode 100644 index 0000000000000000000000000000000000000000..c52cf2eb2298647eafa48dae92e846bbc1eb675c --- /dev/null +++ b/core/src/main/resources/hudson/cli/CLIAction/command.jelly @@ -0,0 +1,43 @@ + + + + + + + +

+ + Command ${command.name} +

+ + +

${command.longDescription}

+ + +
${command.usage}
+
+
+
+
diff --git a/core/src/main/resources/hudson/cli/CLIAction/example.jelly b/core/src/main/resources/hudson/cli/CLIAction/example.jelly new file mode 100644 index 0000000000000000000000000000000000000000..e5e9a49f9c1fe524940bbb01c4f1e30e289fd10d --- /dev/null +++ b/core/src/main/resources/hudson/cli/CLIAction/example.jelly @@ -0,0 +1,39 @@ + + + + + + + +
java -jar jenkins-cli.jar -s ${h.inferHudsonURL(request)} ${commandArgs}
+
+ diff --git a/core/src/main/resources/jenkins/model/Jenkins/_cli.jelly b/core/src/main/resources/hudson/cli/CLIAction/index.jelly similarity index 74% rename from core/src/main/resources/jenkins/model/Jenkins/_cli.jelly rename to core/src/main/resources/hudson/cli/CLIAction/index.jelly index b5dd24f4e2e61b11c0ea1a0eb05f2ebe061ea840..f9d40ee46c6b4d9326ea50655c60b83e0d79f69b 100644 --- a/core/src/main/resources/jenkins/model/Jenkins/_cli.jelly +++ b/core/src/main/resources/hudson/cli/CLIAction/index.jelly @@ -25,7 +25,7 @@ THE SOFTWARE. - +

@@ -34,15 +34,18 @@ THE SOFTWARE.

${%blurb(rootURL)}

-
java -jar jenkins-cli.jar -s ${h.inferHudsonURL(request)} help
-

${%Available Commands}

-
    - -
  • ${command.getName()}: ${command.getShortDescription()}
  • -
    -
+ + + +

${%Available Commands}

+ + + + + + + +
${command.name}${command.shortDescription}
diff --git a/core/src/main/resources/jenkins/model/Jenkins/_cli.properties b/core/src/main/resources/hudson/cli/CLIAction/index.properties similarity index 100% rename from core/src/main/resources/jenkins/model/Jenkins/_cli.properties rename to core/src/main/resources/hudson/cli/CLIAction/index.properties diff --git a/core/src/main/resources/jenkins/model/Jenkins/_cli_cs.properties b/core/src/main/resources/hudson/cli/CLIAction/index_cs.properties similarity index 100% rename from core/src/main/resources/jenkins/model/Jenkins/_cli_cs.properties rename to core/src/main/resources/hudson/cli/CLIAction/index_cs.properties diff --git a/core/src/main/resources/jenkins/model/Jenkins/_cli_da.properties b/core/src/main/resources/hudson/cli/CLIAction/index_da.properties similarity index 100% rename from core/src/main/resources/jenkins/model/Jenkins/_cli_da.properties rename to core/src/main/resources/hudson/cli/CLIAction/index_da.properties diff --git a/core/src/main/resources/jenkins/model/Jenkins/_cli_de.properties b/core/src/main/resources/hudson/cli/CLIAction/index_de.properties similarity index 100% rename from core/src/main/resources/jenkins/model/Jenkins/_cli_de.properties rename to core/src/main/resources/hudson/cli/CLIAction/index_de.properties diff --git a/core/src/main/resources/jenkins/model/Jenkins/_cli_es.properties b/core/src/main/resources/hudson/cli/CLIAction/index_es.properties similarity index 100% rename from core/src/main/resources/jenkins/model/Jenkins/_cli_es.properties rename to core/src/main/resources/hudson/cli/CLIAction/index_es.properties diff --git a/core/src/main/resources/jenkins/model/Jenkins/_cli_fr.properties b/core/src/main/resources/hudson/cli/CLIAction/index_fr.properties similarity index 100% rename from core/src/main/resources/jenkins/model/Jenkins/_cli_fr.properties rename to core/src/main/resources/hudson/cli/CLIAction/index_fr.properties diff --git a/core/src/main/resources/jenkins/model/Jenkins/_cli_it.properties b/core/src/main/resources/hudson/cli/CLIAction/index_it.properties similarity index 100% rename from core/src/main/resources/jenkins/model/Jenkins/_cli_it.properties rename to core/src/main/resources/hudson/cli/CLIAction/index_it.properties diff --git a/core/src/main/resources/jenkins/model/Jenkins/_cli_ja.properties b/core/src/main/resources/hudson/cli/CLIAction/index_ja.properties similarity index 100% rename from core/src/main/resources/jenkins/model/Jenkins/_cli_ja.properties rename to core/src/main/resources/hudson/cli/CLIAction/index_ja.properties diff --git a/core/src/main/resources/jenkins/model/Jenkins/_cli_lv.properties b/core/src/main/resources/hudson/cli/CLIAction/index_lv.properties similarity index 100% rename from core/src/main/resources/jenkins/model/Jenkins/_cli_lv.properties rename to core/src/main/resources/hudson/cli/CLIAction/index_lv.properties diff --git a/core/src/main/resources/jenkins/model/Jenkins/_cli_nl.properties b/core/src/main/resources/hudson/cli/CLIAction/index_nl.properties similarity index 100% rename from core/src/main/resources/jenkins/model/Jenkins/_cli_nl.properties rename to core/src/main/resources/hudson/cli/CLIAction/index_nl.properties diff --git a/core/src/main/resources/jenkins/model/Jenkins/_cli_pt_BR.properties b/core/src/main/resources/hudson/cli/CLIAction/index_pt_BR.properties similarity index 100% rename from core/src/main/resources/jenkins/model/Jenkins/_cli_pt_BR.properties rename to core/src/main/resources/hudson/cli/CLIAction/index_pt_BR.properties diff --git a/core/src/main/resources/jenkins/model/Jenkins/_cli_ru.properties b/core/src/main/resources/hudson/cli/CLIAction/index_ru.properties similarity index 100% rename from core/src/main/resources/jenkins/model/Jenkins/_cli_ru.properties rename to core/src/main/resources/hudson/cli/CLIAction/index_ru.properties diff --git a/core/src/main/resources/jenkins/model/Jenkins/_cli_zh_CN.properties b/core/src/main/resources/hudson/cli/CLIAction/index_zh_CN.properties similarity index 100% rename from core/src/main/resources/jenkins/model/Jenkins/_cli_zh_CN.properties rename to core/src/main/resources/hudson/cli/CLIAction/index_zh_CN.properties diff --git a/core/src/main/resources/jenkins/model/Jenkins/_cli_zh_TW.properties b/core/src/main/resources/hudson/cli/CLIAction/index_zh_TW.properties similarity index 100% rename from core/src/main/resources/jenkins/model/Jenkins/_cli_zh_TW.properties rename to core/src/main/resources/hudson/cli/CLIAction/index_zh_TW.properties diff --git a/core/src/main/resources/hudson/cli/Messages.properties b/core/src/main/resources/hudson/cli/Messages.properties index b9a2405a7916d6625e7cbdfabefca0ffd5c11bff..427399eaa00cd00a1a86d4b08a295781930f50ad 100644 --- a/core/src/main/resources/hudson/cli/Messages.properties +++ b/core/src/main/resources/hudson/cli/Messages.properties @@ -21,7 +21,7 @@ GroovyCommand.ShortDescription=\ GroovyshCommand.ShortDescription=\ Runs an interactive groovy shell. HelpCommand.ShortDescription=\ - Lists all the available commands. + Lists all the available commands or a detailed description of single command. InstallPluginCommand.ShortDescription=\ Installs a plugin either from a file, an URL, or from update center. InstallToolCommand.ShortDescription=\ diff --git a/test/src/test/java/hudson/cli/HelpCommandTest.java b/test/src/test/java/hudson/cli/HelpCommandTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9f6662f08dd1adcad36054895491e4846e48dc81 --- /dev/null +++ b/test/src/test/java/hudson/cli/HelpCommandTest.java @@ -0,0 +1,145 @@ +/* + * The MIT License + * + * Copyright 2013 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package hudson.cli; + +import static org.junit.Assert.*; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.text.IsEmptyString.isEmptyString; +import static org.hamcrest.text.StringContainsInOrder.stringContainsInOrder; + +import java.io.PrintStream; +import java.util.Arrays; + +import hudson.cli.CLICommandInvoker.Result; +import hudson.model.AbstractProject; + +import org.hamcrest.Matcher; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.JenkinsRule.WebClient; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; + +public class HelpCommandTest { + + @Rule public JenkinsRule j = new JenkinsRule(); + + @Test public void getHelpRunningCommand() { + + CLICommandInvoker command = new CLICommandInvoker(j, new HelpCommand()); + + String generalHelp = command.invoke().stderr(); + + assertContainsOverviewOfClassCommand(generalHelp); + assertContainsOverviewOfMethodCommand(generalHelp); + + Result result = command.invokeWithArgs(ClassCommand.NAME); + assertThat(result.returnCode(), equalTo(0)); + assertThat(result.stdout(), isEmptyString()); + + assertContainsUsageOfClassCommand(result.stderr()); + + result = command.invokeWithArgs("offline-node"); + + assertThat(result.returnCode(), equalTo(0)); + assertThat(result.stdout(), isEmptyString()); + + assertContainsUsageOfMethodCommand(result.stderr()); + } + + @Test public void getHelpUsingJenkinsUI() throws Exception { + + WebClient wc = j.createWebClient(); + String generalHelp = wc.goTo("cli").asText(); + + assertContainsOverviewOfClassCommand(generalHelp); + assertContainsOverviewOfMethodCommand(generalHelp); + + assertContainsUsageOfClassCommand(wc.goTo("cli/command/class-command").asText()); + assertContainsUsageOfMethodCommand(wc.goTo("cli/command/offline-node").asText()); + } + + private void assertContainsOverviewOfClassCommand(String text) { + assertThat(text, containsString(ClassCommand.NAME)); + assertThat(text, containsString(ClassCommand.SHORT_DESCRIPTION)); + } + + private void assertContainsUsageOfClassCommand(String text) { + assertThat(text, containsString("class-command [JOB] [-b BUILD]")); + assertThat(text, containsString(ClassCommand.LONG_DESCRIPTION)); + assertThat(text, containsStrings("JOB", "Job arg")); + assertThat(text, containsStrings("BUILD", "Build opt")); + } + + private void assertContainsOverviewOfMethodCommand(String text) { + assertThat(text, containsString("offline-node")); + } + + private void assertContainsUsageOfMethodCommand(String text) { + assertThat(text, containsString("offline-node NAME [-m VAL]")); + assertThat(text, containsStrings("NAME", "Slave name, or empty string for master")); + assertThat(text, containsStrings("-m VAL", "Record the note about why you are disconnecting this node")); + } + + private static Matcher containsStrings(String... strings) { + return stringContainsInOrder(Arrays.asList(strings)); + } + + @TestExtension + public static class ClassCommand extends CLICommand { + + private static final String SHORT_DESCRIPTION = "Short description of class-command"; + private static final String LONG_DESCRIPTION = "Long description of class-command"; + private static final String NAME = "class-command"; + + @Argument(usage="Job arg") + public AbstractProject job; + + @Option(name="-b", metaVar="BUILD", usage="Build opt") + public String build; + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortDescription() { + return SHORT_DESCRIPTION; + } + + @Override + protected void printUsageSummary(PrintStream stderr) { + stderr.println(LONG_DESCRIPTION); + } + + @Override + protected int run() throws Exception { + throw new UnsupportedOperationException(); + } + } +}