提交 b96d227f 编写于 作者: K Kohsuke Kawaguchi

[FIXED JENKINS-19996] Support view manipulation via Jenkins CLI

Merged pull request #971.
......@@ -55,6 +55,9 @@ Upcoming changes</a>
<!-- Record your changes in the trunk here. -->
<div id="trunk" style="display:none"><!--=TRUNK-BEGIN=-->
<ul class=image>
<li class=rfe>
Added CLI commands that manipulate views
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-19996">issue 19996</a>)
<li class=rfe>
Improved the /cli help screen.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-20023">issue 20023</a>)
......
/*
* 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 org.kohsuke.args4j.Argument;
import jenkins.model.Jenkins;
import hudson.Extension;
import hudson.model.Failure;
import hudson.model.View;
/**
* @author ogondza
* @since 1.538
*/
@Extension
public class CreateViewCommand extends CLICommand {
@Argument(usage="Name of the view to use instead of the one in XML")
public String viewName = null;
@Override
public String getShortDescription() {
return Messages.CreateViewCommand_ShortDescription();
}
@Override
protected int run() throws Exception {
final Jenkins jenkins = Jenkins.getInstance();
jenkins.checkPermission(View.CREATE);
View newView;
try {
newView = View.createViewFromXML(viewName, stdin);
} catch (Failure ex) {
stderr.format("Invalid view name: %s\n", ex.getMessage());
return -1;
}
final String newName = newView.getViewName();
if (jenkins.getView(newName) != null) {
stderr.format("View '%s' already exists\n", newName);
return -1;
}
jenkins.addView(newView);
return 0;
}
}
/*
* 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 hudson.Extension;
import hudson.model.ViewGroup;
import hudson.model.View;
import org.kohsuke.args4j.Argument;
/**
* @author ogondza
* @since 1.538
*/
@Extension
public class DeleteViewCommand extends CLICommand {
@Argument(usage="Name of the view to delete", required=true)
private View view;
@Override
public String getShortDescription() {
return Messages.DeleteViewCommand_ShortDescription();
}
@Override
protected int run() throws Exception {
view.checkPermission(View.DELETE);
final ViewGroup group = view.getOwner();
if (!group.canDelete(view)) {
stderr.format("%s does not allow to delete '%s' view\n",
group.getDisplayName(),
view.getViewName()
);
return -1;
}
group.deleteView(view);;
return 0;
}
}
/*
* 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 hudson.Extension;
import hudson.model.View;
import org.kohsuke.args4j.Argument;
/**
* @author ogondza
* @since 1.538
*/
@Extension
public class GetViewCommand extends CLICommand {
@Argument(usage="Name of the view to obtain", required=true)
private View view;
@Override
public String getShortDescription() {
return Messages.GetViewCommand_ShortDescription();
}
@Override
protected int run() throws Exception {
view.checkPermission(View.READ);
view.writeXml(stdout);
return 0;
}
}
/*
* 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 javax.xml.transform.stream.StreamSource;
import hudson.Extension;
import hudson.model.View;
import org.kohsuke.args4j.Argument;
/**
* @author ogondza
* @since 1.538
*/
@Extension
public class UpdateViewCommand extends CLICommand {
@Argument(usage="Name of the view to update", required=true)
private View view;
@Override
public String getShortDescription() {
return Messages.UpdateViewCommand_ShortDescription();
}
@Override
protected int run() throws Exception {
view.updateByXml(new StreamSource(stdin));
return 0;
}
}
/*
* 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.handlers;
import hudson.model.ViewGroup;
import hudson.model.View;
import java.util.StringTokenizer;
import jenkins.model.Jenkins;
import org.kohsuke.MetaInfServices;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.OptionDef;
import org.kohsuke.args4j.spi.OptionHandler;
import org.kohsuke.args4j.spi.Parameters;
import org.kohsuke.args4j.spi.Setter;
/**
* Refers to {@link View} by its name.
*
* <p>
* For example:
* <dl>
* <dt>my_view_name</dt><dd>refers to a top level view with given name.</dd>
* <dt>nested/inner</dt><dd>refers to a view named <tt>inner</tt> inside of a top level view group named <tt>nested</tt>.</dd>
* </dl>
*
* <p>
* View name is a non-empty sequence of {@link View} names delimited by '/'.
* Handler traverse the view names from left to right. First one is expected to
* be a top level view and all but the last one are expected to be instances of
* {@link ViewGroup}. Handler fails to resolve view provided a view with given
* name does not exist or a user was not granted {@link View.READ} permission.
*
* @author ogondza
* @since 1.538
*/
@MetaInfServices
public class ViewOptionHandler extends OptionHandler<View> {
public ViewOptionHandler(CmdLineParser parser, OptionDef option, Setter<View> setter) {
super(parser, option, setter);
}
@Override
public int parseArguments(Parameters params) throws CmdLineException {
setter.addValue(getView(params.getParameter(0)));
return 1;
}
private View getView(String name) throws CmdLineException {
View view = null;
ViewGroup group = Jenkins.getInstance();
final StringTokenizer tok = new StringTokenizer(name, "/");
while(tok.hasMoreTokens()) {
String viewName = tok.nextToken();
view = group.getView(viewName);
if (view == null) throw new CmdLineException(owner, String.format(
"No view named %s inside view %s",
viewName, group.getDisplayName()
));
view.checkPermission(View.READ);
if (view instanceof ViewGroup) {
group = (ViewGroup) view;
} else if (tok.hasMoreTokens()) {
throw new CmdLineException(
owner, view.getViewName() + " view can not contain views"
);
}
}
return view;
}
@Override
public String getDefaultMetaVariable() {
return "VIEW";
}
}
......@@ -83,6 +83,7 @@ import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
......@@ -1044,16 +1045,13 @@ public abstract class View extends AbstractModelObject implements AccessControll
return new HttpResponse() {
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {
rsp.setContentType("application/xml");
// pity we don't have a handy way to clone Jenkins.XSTREAM to temp add the omit Field
XStream2 xStream2 = new XStream2();
xStream2.omitField(View.class, "owner");
xStream2.toXMLUTF8(View.this, rsp.getOutputStream());
View.this.writeXml(rsp.getOutputStream());
}
};
}
if (req.getMethod().equals("POST")) {
// submission
updateByXml((Source)new StreamSource(req.getReader()));
updateByXml(new StreamSource(req.getReader()));
return HttpResponses.ok();
}
......@@ -1061,6 +1059,16 @@ public abstract class View extends AbstractModelObject implements AccessControll
return HttpResponses.error(SC_BAD_REQUEST, "Unexpected request method " + req.getMethod());
}
/**
* @since 1.538
*/
public void writeXml(OutputStream out) throws IOException {
// pity we don't have a handy way to clone Jenkins.XSTREAM to temp add the omit Field
XStream2 xStream2 = new XStream2();
xStream2.omitField(View.class, "owner");
xStream2.toXMLUTF8(View.this, out);
}
/**
* Updates Job by its XML definition.
*/
......@@ -1083,7 +1091,11 @@ public abstract class View extends AbstractModelObject implements AccessControll
// try to reflect the changes by reloading
InputStream in = new BufferedInputStream(new ByteArrayInputStream(out.toString().getBytes("UTF-8")));
try {
// Do not allow overwriting view name as it might collide with another
// view in same ViewGroup and might not satisfy Jenkins.checkGoodName.
String oldname = name;
Jenkins.XSTREAM.unmarshal(new XppDriver().createReader(in), this);
name = oldname;
} catch (StreamException e) {
throw new IOException2("Unable to read",e);
} catch(ConversionException e) {
......@@ -1185,11 +1197,17 @@ public abstract class View extends AbstractModelObject implements AccessControll
return v;
}
/**
* Instantiate View subtype from XML stream.
*
* @param name Alternative name to use or <tt>null</tt> to keep the one in xml.
*/
public static View createViewFromXML(String name, InputStream xml) throws IOException {
InputStream in = new BufferedInputStream(xml);
try {
View v = (View) Jenkins.XSTREAM.fromXML(in);
v.name = name;
if (name != null) v.name = name;
checkGoodName(v.name);
return v;
} catch(StreamException e) {
throw new IOException2("Unable to read",e);
......
......@@ -14,8 +14,12 @@ CreateJobCommand.ShortDescription=\
Creates a new job by reading stdin as a configuration XML file.
CreateNodeCommand.ShortDescription=\
Creates a new node by reading stdin as a XML configuration.
CreateViewCommand.ShortDescription=\
Creates a new view by reading stdin as a XML configuration.
DeleteBuildsCommand.ShortDescription=\
Deletes build record(s).
DeleteViewCommand.ShortDescription=\
Deletes view.
GroovyCommand.ShortDescription=\
Executes the specified Groovy script.
GroovyshCommand.ShortDescription=\
......@@ -49,6 +53,8 @@ GetJobCommand.ShortDescription=\
Dumps the job definition XML to stdout.
GetNodeCommand.ShortDescription=\
Dumps the node definition XML to stdout.
GetViewCommand.ShortDescription=\
Dumps the view definition XML to stdout.
SetBuildDisplayNameCommand.ShortDescription=\
Sets the displayName of a build.
WhoAmICommand.ShortDescription=\
......@@ -58,6 +64,8 @@ UpdateJobCommand.ShortDescription=\
UpdateNodeCommand.ShortDescription=\
Updates the node definition XML from stdin. The opposite of the get-node command.
SessionIdCommand.ShortDescription=Outputs the session ID, which changes every time Jenkins restarts.
UpdateViewCommand.ShortDescription=\
Updates the view definition XML from stdin. The opposite of the get-view command.
BuildCommand.CLICause.ShortDescription=Started by command line by {0}
BuildCommand.CLICause.CannotBuildDisabled=\
......
/*
* 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.handlers;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import hudson.model.ViewGroup;
import hudson.model.View;
import jenkins.model.Jenkins;
import org.acegisecurity.AccessDeniedException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.spi.Parameters;
import org.kohsuke.args4j.spi.Setter;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
@PrepareForTest(Jenkins.class)
@RunWith(PowerMockRunner.class)
public class ViewOptionHandlerTest {
@Mock private Setter<View> setter;
private ViewOptionHandler handler;
// Hierarchy of views used as a shared fixture:
// $JENKINS_URL/view/outer/view/nested/view/inner/
@Mock private View inner;
@Mock private CompositeView nested;
@Mock private CompositeView outer;
@Mock private Jenkins jenkins;
@Before public void setUp() {
MockitoAnnotations.initMocks(this);
handler = new ViewOptionHandler(null, null, setter);
when(inner.getViewName()).thenReturn("inner");
when(inner.getDisplayName()).thenCallRealMethod();
when(nested.getViewName()).thenReturn("nested");
when(nested.getDisplayName()).thenCallRealMethod();
when(nested.getView("inner")).thenReturn(inner);
when(outer.getViewName()).thenReturn("outer");
when(outer.getDisplayName()).thenCallRealMethod();
when(outer.getView("nested")).thenReturn(nested);
PowerMockito.mockStatic(Jenkins.class);
PowerMockito.when(Jenkins.getInstance()).thenReturn(jenkins);
when(jenkins.getView("outer")).thenReturn(outer);
when(jenkins.getDisplayName()).thenReturn("Jenkins");
}
@Test public void resolveTopLevelView() throws Exception {
parse("outer");
verify(setter).addValue(outer);
}
@Test public void resolveNestedView() throws Exception {
parse("outer/nested");
verify(setter).addValue(nested);
}
@Test public void resolveOuterView() throws Exception {
parse("outer/nested/inner");
verify(setter).addValue(inner);
}
@Test public void ignoreLeadingAndTrailingSlashes() throws Exception {
parse("/outer/nested/inner/");
verify(setter).addValue(inner);
}
@Test public void reportNonexistentTopLevelView() throws Exception {
assertEquals(
"No view named missing_view inside view Jenkins",
parseFailedWith(CmdLineException.class, "missing_view")
);
verifyZeroInteractions(setter);
}
@Test public void reportNonexistentNestedView() throws Exception {
assertEquals(
"No view named missing_view inside view outer",
parseFailedWith(CmdLineException.class, "outer/missing_view")
);
verifyZeroInteractions(setter);
}
@Test public void reportNonexistentInnerView() throws Exception {
assertEquals(
"No view named missing_view inside view nested",
parseFailedWith(CmdLineException.class, "outer/nested/missing_view")
);
verifyZeroInteractions(setter);
}
@Test public void reportTraversingViewThatIsNotAViewGroup() throws Exception {
assertEquals(
"inner view can not contain views",
parseFailedWith(CmdLineException.class, "outer/nested/inner/missing")
);
verifyZeroInteractions(setter);
}
@Test public void refuseToReadOuterView() throws Exception {
denyAccessOn(outer);
parseFailedWith(AccessDeniedException.class, "outer/nested/inner");
verify(outer).checkPermission(View.READ);
verifyNoMoreInteractions(outer);
verifyZeroInteractions(nested);
verifyZeroInteractions(inner);
verifyZeroInteractions(setter);
}
@Test public void refuseToReadNestedView() throws Exception {
denyAccessOn(nested);
parseFailedWith(AccessDeniedException.class, "outer/nested/inner");
verify(nested).checkPermission(View.READ);
verifyNoMoreInteractions(nested);
verifyZeroInteractions(inner);
verifyZeroInteractions(setter);
}
@Test public void refuseToReadInnerView() throws Exception {
denyAccessOn(inner);
parseFailedWith(AccessDeniedException.class, "outer/nested/inner");
verify(inner).checkPermission(View.READ);
verifyNoMoreInteractions(inner);
verifyZeroInteractions(setter);
}
private void denyAccessOn(View view) {
doThrow(new AccessDeniedException(null)).when(view).checkPermission(View.READ);
}
private String parseFailedWith(Class<? extends Exception> type, final String... params) throws Exception {
try {
parse(params);
} catch (Exception ex) {
if (!type.isAssignableFrom(ex.getClass())) throw ex;
return ex.getMessage();
}
fail("No exception thrown. Expected " + type.getClass());
return null;
}
private void parse(final String... params) throws CmdLineException {
handler.parseArguments(new Parameters() {
public String getParameter(int idx) throws CmdLineException {
return params[idx];
}
public int size() {
return params.length;
}
});
}
private static abstract class CompositeView extends View implements ViewGroup {
protected CompositeView(String name) {
super(name);
}
}
}
......@@ -24,6 +24,7 @@
package hudson.cli;
import hudson.Extension;
import hudson.model.User;
import hudson.security.Permission;
import hudson.security.GlobalMatrixAuthorizationStrategy;
......@@ -31,11 +32,16 @@ import hudson.security.GlobalMatrixAuthorizationStrategy;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import jenkins.model.Jenkins;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.jvnet.hudson.test.JenkinsRule;
/**
......@@ -56,6 +62,14 @@ public class CLICommandInvoker {
public CLICommandInvoker(final JenkinsRule rule, final CLICommand command) {
if (command.getClass().getAnnotation(Extension.class) == null) {
throw new AssertionError(String.format(
"Command %s is missing @Extension annotation.",
command.getClass()
));
}
this.rule = rule;
this.command = command;
}
......@@ -68,6 +82,8 @@ public class CLICommandInvoker {
public CLICommandInvoker withStdin(final InputStream stdin) {
if (stdin == null) throw new NullPointerException("No stdin provided");
this.stdin = stdin;
return this;
}
......@@ -151,5 +167,71 @@ public class CLICommandInvoker {
return err.toString();
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("CLI command exited with ").append(result);
String stdout = stdout();
if (!"".equals(stdout)) {
builder.append("\nSTDOUT:\n").append(stdout);
}
String stderr = stderr();
if (!"".equals(stderr)) {
builder.append("\nSTDERR:\n").append(stderr);
}
return builder.toString();
}
}
public abstract static class Matcher extends TypeSafeMatcher<Result> {
private final String description;
private Matcher(String description) {
this.description = description;
}
@Override
protected void describeMismatchSafely(Result result, Description description) {
description.appendText(result.toString());
}
public void describeTo(Description description) {
description.appendText(this.description);
}
public static Matcher hasNoStandardOutput() {
return new Matcher("No standard output") {
@Override protected boolean matchesSafely(Result result) {
return "".equals(result.stdout());
}
};
}
public static Matcher hasNoErrorOutput() {
return new Matcher("No error output") {
@Override protected boolean matchesSafely(Result result) {
return "".equals(result.stderr());
}
};
}
public static Matcher succeeded() {
return new Matcher("Exited with 0 return code") {
@Override protected boolean matchesSafely(Result result) {
return result.result == 0;
}
};
}
public static Matcher failedWith(final long expectedCode) {
return new Matcher("Exited with " + expectedCode + " return code") {
@Override protected boolean matchesSafely(Result result) {
return result.result == expectedCode;
}
};
}
}
}
/*
* 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 hudson.cli.CLICommandInvoker.Matcher.failedWith;
import static hudson.cli.CLICommandInvoker.Matcher.hasNoStandardOutput;
import static hudson.cli.CLICommandInvoker.Matcher.hasNoErrorOutput;
import static hudson.cli.CLICommandInvoker.Matcher.succeeded;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
import java.io.IOException;
import hudson.model.ListView;
import hudson.model.View;
import jenkins.model.Jenkins;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
public class CreateViewCommandTest {
private CLICommandInvoker command;
@Rule public final JenkinsRule j = new JenkinsRule();
@Before public void setUp() {
command = new CLICommandInvoker(j, new CreateViewCommand());
}
@Test public void createViewShouldFailWithoutViewCreatePermission() {
final CLICommandInvoker.Result result = command
.authorizedTo(Jenkins.READ)
.withStdin(this.getClass().getResourceAsStream("/hudson/cli/view.xml"))
.invoke()
;
assertThat(result, failedWith(-1));
assertThat(result, hasNoStandardOutput());
assertThat(result.stderr(), containsString("user is missing the View/Create permission"));
}
@Test public void createViewShouldSucceed() {
final CLICommandInvoker.Result result = command
.authorizedTo(View.CREATE, Jenkins.READ)
.withStdin(this.getClass().getResourceAsStream("/hudson/cli/view.xml"))
.invoke()
;
assertThat(result, succeeded());
assertThat(result, hasNoErrorOutput());
assertThat(result, hasNoStandardOutput());
final View updatedView = j.jenkins.getView("ViewFromXML");
assertThat(updatedView.getViewName(), equalTo("ViewFromXML"));
assertThat(updatedView.isFilterExecutors(), equalTo(true));
assertThat(updatedView.isFilterQueue(), equalTo(false));
}
@Test public void createViewSpecifyingNameExplicitlyShouldSucceed() {
final CLICommandInvoker.Result result = command
.authorizedTo(View.CREATE, Jenkins.READ)
.withStdin(this.getClass().getResourceAsStream("/hudson/cli/view.xml"))
.invokeWithArgs("CustomViewName")
;
assertThat(result, succeeded());
assertThat(result, hasNoErrorOutput());
assertThat(result, hasNoStandardOutput());
assertThat("A view with original name should not exist", j.jenkins.getView("ViewFromXML"), nullValue());
final View updatedView = j.jenkins.getView("CustomViewName");
assertThat(updatedView.getViewName(), equalTo("CustomViewName"));
assertThat(updatedView.isFilterExecutors(), equalTo(true));
assertThat(updatedView.isFilterQueue(), equalTo(false));
}
@Test public void createViewShouldFailIfViewAlreadyExists() throws IOException {
j.jenkins.addView(new ListView("ViewFromXML"));
final CLICommandInvoker.Result result = command
.authorizedTo(View.CREATE, Jenkins.READ)
.withStdin(this.getClass().getResourceAsStream("/hudson/cli/view.xml"))
.invoke()
;
assertThat(result, failedWith(-1));
assertThat(result, hasNoStandardOutput());
assertThat(result.stderr(), containsString("View 'ViewFromXML' already exists"));
}
@Test public void createViewShouldFailUsingInvalidName() {
final CLICommandInvoker.Result result = command
.authorizedTo(View.CREATE, Jenkins.READ)
.withStdin(this.getClass().getResourceAsStream("/hudson/cli/view.xml"))
.invokeWithArgs("..")
;
assertThat(result, failedWith(-1));
assertThat(result, hasNoStandardOutput());
assertThat(result.stderr(), containsString("Invalid view name"));
}
}
/*
* 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.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.nullValue;
import static hudson.cli.CLICommandInvoker.Matcher.hasNoStandardOutput;
import static hudson.cli.CLICommandInvoker.Matcher.hasNoErrorOutput;
import static hudson.cli.CLICommandInvoker.Matcher.succeeded;
import static hudson.cli.CLICommandInvoker.Matcher.failedWith;
import java.io.IOException;
import hudson.model.ListView;
import hudson.model.View;
import jenkins.model.Jenkins;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
public class DeleteViewCommandTest {
private CLICommandInvoker command;
@Rule public final JenkinsRule j = new JenkinsRule();
@Before public void setUp() {
command = new CLICommandInvoker(j, new DeleteViewCommand());
}
@Test public void deleteViewShouldFailWithoutViewDeletePermission() throws IOException {
j.jenkins.addView(new ListView("aView"));
final CLICommandInvoker.Result result = command
.authorizedTo(View.READ, Jenkins.READ)
.invokeWithArgs("aView")
;
assertThat(result, failedWith(-1));
assertThat(result, hasNoStandardOutput());
assertThat(result.stderr(), containsString("user is missing the View/Delete permission"));
}
@Test public void deleteViewShouldSucceed() throws Exception {
j.jenkins.addView(new ListView("aView"));
final CLICommandInvoker.Result result = command
.authorizedTo(View.READ, View.DELETE, Jenkins.READ)
.invokeWithArgs("aView")
;
assertThat(result, succeeded());
assertThat(result, hasNoStandardOutput());
assertThat(result, hasNoErrorOutput());
assertThat(j.jenkins.getView("aView"), nullValue());
}
@Test public void deleteViewShouldFailIfViewDoesNotExist() {
final CLICommandInvoker.Result result = command
.authorizedTo(View.READ, View.DELETE, Jenkins.READ)
.invokeWithArgs("never_created")
;
assertThat(result, failedWith(-1));
assertThat(result, hasNoStandardOutput());
assertThat(result.stderr(), containsString("No view named never_created inside view Jenkins"));
}
// ViewGroup.canDelete()
@Test public void deleteViewShouldFailIfViewGroupDoesNotAllowDeletion() {
final CLICommandInvoker.Result result = command
.authorizedTo(View.READ, View.DELETE, Jenkins.READ)
.invokeWithArgs("All")
;
assertThat(result, failedWith(-1));
assertThat(result, hasNoStandardOutput());
assertThat(result.stderr(), containsString("Jenkins does not allow to delete 'All' view"));
}
}
/*
* 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.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.startsWith;
import static hudson.cli.CLICommandInvoker.Matcher.failedWith;
import static hudson.cli.CLICommandInvoker.Matcher.hasNoStandardOutput;
import static hudson.cli.CLICommandInvoker.Matcher.hasNoErrorOutput;
import static hudson.cli.CLICommandInvoker.Matcher.succeeded;
import java.io.IOException;
import hudson.model.ListView;
import hudson.model.View;
import jenkins.model.Jenkins;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
public class GetViewCommandTest {
private CLICommandInvoker command;
@Rule public final JenkinsRule j = new JenkinsRule();
@Before public void setUp() {
command = new CLICommandInvoker(j, new GetViewCommand());
}
@Test public void getViewShouldFailWithoutViewReadPermission() throws IOException {
j.jenkins.addView(new ListView("aView"));
final CLICommandInvoker.Result result = command
.authorizedTo(Jenkins.READ)
.invokeWithArgs("aView")
;
assertThat(result, failedWith(-1));
assertThat(result, hasNoStandardOutput());
assertThat(result.stderr(), containsString("user is missing the View/Read permission"));
}
@Test public void getViewShouldYieldConfigXml() throws Exception {
j.jenkins.addView(new ListView("aView"));
final CLICommandInvoker.Result result = command
.authorizedTo(View.READ, Jenkins.READ)
.invokeWithArgs("aView")
;
assertThat(result, succeeded());
assertThat(result, hasNoErrorOutput());
assertThat(result.stdout(), startsWith("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"));
assertThat(result.stdout(), containsString("<name>aView</name>"));
}
@Test public void getViewShouldFailIfViewDoesNotExist() {
final CLICommandInvoker.Result result = command
.authorizedTo(View.READ, Jenkins.READ)
.invokeWithArgs("never_created")
;
assertThat(result, failedWith(-1));
assertThat(result, hasNoStandardOutput());
assertThat(result.stderr(), containsString("No view named never_created inside view Jenkins"));
}
}
/*
* 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 hudson.cli.CLICommandInvoker.Matcher.failedWith;
import static hudson.cli.CLICommandInvoker.Matcher.hasNoStandardOutput;
import static hudson.cli.CLICommandInvoker.Matcher.hasNoErrorOutput;
import static hudson.cli.CLICommandInvoker.Matcher.succeeded;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
import hudson.model.ListView;
import hudson.model.View;
import jenkins.model.Jenkins;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
public class UpdateViewCommandTest {
private CLICommandInvoker command;
@Rule public final JenkinsRule j = new JenkinsRule();
@Before public void setUp() {
command = new CLICommandInvoker(j, new UpdateViewCommand());
}
@Test public void updateViewShouldFailWithoutViewConfigurePermission() throws Exception {
j.jenkins.addView(new ListView("aView"));
final CLICommandInvoker.Result result = command
.authorizedTo(View.READ, Jenkins.READ)
.withStdin(this.getClass().getResourceAsStream("/hudson/cli/view.xml"))
.invokeWithArgs("aView")
;
assertThat(result, failedWith(-1));
assertThat(result, hasNoStandardOutput());
assertThat(result.stderr(), containsString("user is missing the View/Configure permission"));
}
@Test public void updateViewShouldModifyViewConfiguration() throws Exception {
j.jenkins.addView(new ListView("aView"));
final CLICommandInvoker.Result result = command
.authorizedTo(View.READ, View.CONFIGURE, Jenkins.READ)
.withStdin(this.getClass().getResourceAsStream("/hudson/cli/view.xml"))
.invokeWithArgs("aView")
;
assertThat(result, succeeded());
assertThat(result, hasNoErrorOutput());
assertThat("Update should not modify view name", j.jenkins.getView("ViewFromXML"), nullValue());
final View updatedView = j.jenkins.getView("aView");
assertThat(updatedView.getViewName(), equalTo("aView"));
assertThat(updatedView.isFilterExecutors(), equalTo(true));
assertThat(updatedView.isFilterQueue(), equalTo(false));
}
@Test public void updateViewShouldFailIfViewDoesNotExist() {
final CLICommandInvoker.Result result = command
.authorizedTo(View.READ, View.CONFIGURE, Jenkins.READ)
.withStdin(this.getClass().getResourceAsStream("/hudson/cli/view.xml"))
.invokeWithArgs("not_created")
;
assertThat(result, failedWith(-1));
assertThat(result, hasNoStandardOutput());
assertThat(result.stderr(), containsString("No view named not_created inside view Jenkins"));
}
}
<?xml version="1.0" encoding="UTF-8"?>
<hudson.model.ListView>
<name>ViewFromXML</name>
<filterExecutors>true</filterExecutors>
<filterQueue>false</filterQueue>
<properties class="hudson.model.View$PropertyList"/>
<jobNames>
<comparator class="hudson.util.CaseInsensitiveComparator"/>
<string>core_selenium-test</string>
<string>jenkins_lts_branch</string>
<string>jenkins_main_maven-3.1.0</string>
<string>jenkins_main_trunk</string>
<string>jenkins_pom</string>
<string>jenkins_rc_branch</string>
<string>jenkins_ui-changes_branch</string>
<string>remoting</string>
<string>selenium-tests</string>
</jobNames>
<jobFilters/>
<columns>
<hudson.views.StatusColumn/>
<hudson.views.WeatherColumn/>
<hudson.views.JobColumn/>
<hudson.views.LastSuccessColumn/>
<hudson.views.LastFailureColumn/>
<hudson.views.LastDurationColumn/>
<org.jenkins.ci.plugins.column.console.LastBuildColumn plugin="console-column-plugin@1.5"/>
<hudson.views.BuildButtonColumn/>
</columns>
<recurse>false</recurse>
</hudson.model.ListView>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册