提交 eb49ae16 编写于 作者: J Jesse Glick

Merge pull request #757 from Vlatombe/views_include_jobs_folder

ListView can match now items belonging to nested ItemGroups
......@@ -55,6 +55,8 @@ Upcoming changes</a>
<!-- Record your changes in the trunk here. -->
<div id="trunk" style="display:none"><!--=TRUNK-BEGIN=-->
<ul class=image>
<li class=rfe>
Views can now include jobs located within folders
<li class=rfe>
Added confirmation dialog before reloading configuration from disk.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-15340">issue 15340</a>)
......
......@@ -25,13 +25,33 @@
*/
package hudson;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import hudson.cli.CLICommand;
import hudson.console.ConsoleAnnotationDescriptor;
import hudson.console.ConsoleAnnotatorFactory;
import hudson.model.*;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.model.DescriptorVisibilityFilter;
import hudson.model.Hudson;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Items;
import hudson.model.JDK;
import hudson.model.Job;
import hudson.model.JobPropertyDescriptor;
import hudson.model.ModelObject;
import hudson.model.Node;
import hudson.model.PageDecorator;
import hudson.model.ParameterDefinition;
import hudson.model.ParameterDefinition.ParameterDescriptor;
import hudson.model.Project;
import hudson.model.Run;
import hudson.model.TopLevelItem;
import hudson.model.User;
import hudson.model.View;
import hudson.scm.SCM;
import hudson.scm.SCMDescriptor;
import hudson.search.SearchableModelObject;
import hudson.security.AccessControlled;
import hudson.security.AuthorizationStrategy;
......@@ -53,36 +73,11 @@ import hudson.tasks.Publisher;
import hudson.tasks.UserAvatarResolver;
import hudson.util.Area;
import hudson.util.Iterators;
import hudson.scm.SCM;
import hudson.scm.SCMDescriptor;
import hudson.util.Secret;
import hudson.views.MyViewsTabBar;
import hudson.views.ViewsTabBar;
import hudson.widgets.RenderOnDemandClosure;
import jenkins.model.GlobalConfiguration;
import jenkins.model.GlobalConfigurationCategory;
import jenkins.model.GlobalConfigurationCategory.Unclassified;
import jenkins.model.Jenkins;
import jenkins.model.ModelObjectWithContextMenu;
import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
import org.apache.commons.jelly.JellyContext;
import org.apache.commons.jelly.JellyTagException;
import org.apache.commons.jelly.Script;
import org.apache.commons.jelly.XMLOutput;
import org.apache.commons.jexl.parser.ASTSizeFunction;
import org.apache.commons.jexl.util.Introspector;
import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
import org.jvnet.tiger_types.Types;
import org.kohsuke.stapler.Ancestor;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.jelly.InternationalizedStringExpression.RawHtmlArgument;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
......@@ -93,8 +88,8 @@ import java.lang.management.ManagementFactory;
import java.lang.management.MonitorInfo;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.lang.reflect.Type;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
......@@ -108,6 +103,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
......@@ -115,14 +111,42 @@ import java.util.Locale;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import jenkins.model.GlobalConfiguration;
import jenkins.model.GlobalConfigurationCategory;
import jenkins.model.GlobalConfigurationCategory.Unclassified;
import jenkins.model.Jenkins;
import jenkins.model.ModelObjectWithContextMenu;
import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
import org.apache.commons.jelly.JellyContext;
import org.apache.commons.jelly.JellyTagException;
import org.apache.commons.jelly.Script;
import org.apache.commons.jelly.XMLOutput;
import org.apache.commons.jexl.parser.ASTSizeFunction;
import org.apache.commons.jexl.util.Introspector;
import org.apache.commons.lang.StringUtils;
import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
import org.jvnet.tiger_types.Types;
import org.kohsuke.stapler.Ancestor;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.jelly.InternationalizedStringExpression.RawHtmlArgument;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
/**
* Utility functions used in views.
......@@ -895,32 +919,72 @@ public class Functions {
}
String path = ancestors.get(p);
if(path!=null) return path;
if(path!=null) {
return normalizeURI(path + '/');
}
Item i=p;
String url = "";
Collection<TopLevelItem> viewItems;
if (view != null) {
viewItems = view.getItems();
} else {
viewItems = Collections.emptyList();
}
while(true) {
ItemGroup ig = i.getParent();
url = i.getShortUrl()+url;
if(ig== Jenkins.getInstance()) {
if(ig== Jenkins.getInstance() || (view != null && ig == view.getOwnerItemGroup())) {
assert i instanceof TopLevelItem;
if(view!=null && view.contains((TopLevelItem)i)) {
if(viewItems.contains((TopLevelItem)i)) {
// if p and the current page belongs to the same view, then return a relative path
return ancestors.get(view)+'/'+url;
return normalizeURI(ancestors.get(view)+'/'+url);
} else {
// otherwise return a path from the root Hudson
return request.getContextPath()+'/'+p.getUrl();
return normalizeURI(request.getContextPath()+'/'+p.getUrl());
}
}
path = ancestors.get(ig);
if(path!=null) return path+'/'+url;
if(path!=null) {
return normalizeURI(path+'/'+url);
}
assert ig instanceof Item; // if not, ig must have been the Hudson instance
i = (Item) ig;
}
}
private static String normalizeURI(String uri) {
return URI.create(uri).normalize().toString();
}
/**
* Gets all the {@link TopLevelItem}s recursively in the {@link ItemGroup} tree.
*
* @since XXX
*/
public static List<TopLevelItem> getAllTopLevelItems(ItemGroup root) {
return Items.getAllItems(root, TopLevelItem.class);
}
/**
* Gets the relative display name to the given item from the specified group.
*
* @since XXX
* @param p the Item we want the relative display name
* @param g the ItemGroup used as point of reference for the item
* @return
* String like "foo » bar"
*/
public static String getRelativeDisplayNameFrom(Item p, ItemGroup g) {
if (p == null) return null;
String relativeName = p.getRelativeNameFrom(g);
if (relativeName == null) return null;
return relativeName.replace("/", " » ");
}
public static Map<Thread,StackTraceElement[]> dumpAllThreads() {
Map<Thread,StackTraceElement[]> sorted = new TreeMap<Thread,StackTraceElement[]>(new ThreadSorter());
......
......@@ -326,8 +326,25 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
if(n.length()==0) return getDisplayName();
else return n+" \u00BB "+getDisplayName();
}
/**
* This method only exists to disambiguate {@link #getRelativeNameFrom(ItemGroup)} and {@link #getRelativeNameFrom(Item)}
* @since XXX
* @see #getRelativeNameFrom(ItemGroup)
*/
public String getRelativeNameFromGroup(ItemGroup p) {
return getRelativeNameFrom(p);
}
/**
* @param p
* The ItemGroup instance used as context to evaluate the relative name of this AbstractItem
* @return
* The name of the current item, relative to p.
* Nested ItemGroups are separated by / character.
*/
public String getRelativeNameFrom(ItemGroup p) {
if (p == null) return getFullName();
// first list up all the parents
Map<ItemGroup,Integer> parents = new HashMap<ItemGroup,Integer>();
int depth=0;
......
......@@ -235,6 +235,49 @@ public class Items {
public static XmlFile getConfigFile(Item item) {
return getConfigFile(item.getRootDir());
}
/**
* Gets all the {@link Item}s recursively in the {@link ItemGroup} tree
* and filter them by the given type.
*
* @since XXX
*/
public static <T extends Item> List<T> getAllItems(final ItemGroup root, Class<T> type) {
List<T> r = new ArrayList<T>();
Stack<ItemGroup> q = new Stack<ItemGroup>();
q.push(root);
while(!q.isEmpty()) {
ItemGroup<?> parent = q.pop();
for (Item i : parent.getItems()) {
if(type.isInstance(i)) {
if (i.hasPermission(Item.READ))
r.add(type.cast(i));
}
if(i instanceof ItemGroup)
q.push((ItemGroup)i);
}
}
// sort by relative name, ignoring case
Collections.sort(r, new Comparator<T>() {
@Override
public int compare(T o1, T o2) {
if (o1 == null) {
if (o2 == null) {
return 0;
}
return 1;
}
if (o2 == null) {
return -1;
}
return o1.getRelativeNameFrom(root).compareToIgnoreCase(o2.getRelativeNameFrom(root));
}
});
return r;
}
/**
* Used to load/save job configuration.
......
......@@ -33,15 +33,7 @@ import hudson.util.FormValidation;
import hudson.util.HttpResponses;
import hudson.views.ListViewColumn;
import hudson.views.ViewJobFilter;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
import javax.annotation.concurrent.GuardedBy;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
......@@ -51,6 +43,18 @@ import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.annotation.concurrent.GuardedBy;
import javax.servlet.ServletException;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
/**
* Displays {@link Job}s in a flat list view.
*
......@@ -73,6 +77,11 @@ public class ListView extends View implements Saveable {
*/
private String includeRegex;
/**
* Whether to recurse in ItemGroups
*/
private boolean recurse;
/**
* Compiled include pattern from the includeRegex string.
*/
......@@ -138,24 +147,18 @@ public class ListView extends View implements Saveable {
*/
public List<TopLevelItem> getItems() {
SortedSet<String> names;
List<TopLevelItem> items = new ArrayList<TopLevelItem>();
synchronized (this) {
names = new TreeSet<String>(jobNames);
}
if (includePattern != null) {
for (Item item : getOwnerItemGroup().getItems()) {
String itemName = item.getName();
if (includePattern.matcher(itemName).matches()) {
names.add(itemName);
}
}
}
ItemGroup<? extends TopLevelItem> parent = getOwnerItemGroup();
includeItems(parent, names);
Boolean statusFilter = this.statusFilter; // capture the value to isolate us from concurrent update
List<TopLevelItem> items = new ArrayList<TopLevelItem>(names.size());
for (TopLevelItem item : getOwnerItemGroup().getItems()) {
if (!names.contains(item.getName())) continue;
for (TopLevelItem item : Items.getAllItems(getOwnerItemGroup(), TopLevelItem.class)) {
if (!names.contains(item.getRelativeNameFrom(getOwnerItemGroup()))) continue;
// Add if no status filter or filter matches enabled/disabled status:
if(statusFilter == null || !(item instanceof AbstractProject)
|| ((AbstractProject)item).isDisabled() ^ statusFilter)
......@@ -164,7 +167,7 @@ public class ListView extends View implements Saveable {
// check the filters
Iterable<ViewJobFilter> jobFilters = getJobFilters();
List<TopLevelItem> allItems = new ArrayList<TopLevelItem>(getOwnerItemGroup().getItems());
List<TopLevelItem> allItems = new ArrayList<TopLevelItem>(parent.getItems());
for (ViewJobFilter jobFilter: jobFilters) {
items = jobFilter.filter(items, allItems, this);
}
......@@ -173,11 +176,37 @@ public class ListView extends View implements Saveable {
return items;
}
public synchronized boolean contains(TopLevelItem item) {
return jobNames.contains(item.getName());
@Override
public boolean contains(TopLevelItem item) {
return getItems().contains(item);
}
private void includeItems(ItemGroup<? extends TopLevelItem> parent, SortedSet<String> names) {
includeItems(parent, parent, names);
}
private void includeItems(ItemGroup<? extends TopLevelItem> root, ItemGroup<? extends TopLevelItem> parent, SortedSet<String> names) {
if (includePattern != null) {
for (Item item : parent.getItems()) {
if (recurse && item instanceof ItemGroup) {
includeItems(root, (ItemGroup<? extends TopLevelItem>)item, names);
}
String itemName = item.getRelativeNameFrom(root);
if (includePattern.matcher(itemName).matches()) {
names.add(itemName);
}
}
}
}
public synchronized boolean jobNamesContains(TopLevelItem item) {
if (item == null) return false;
return jobNames.contains(item.getRelativeNameFrom(getOwnerItemGroup()));
}
/**
* Adds the given item to this view.
*
......@@ -185,7 +214,7 @@ public class ListView extends View implements Saveable {
*/
public void add(TopLevelItem item) throws IOException {
synchronized (this) {
jobNames.add(item.getName());
jobNames.add(item.getRelativeNameFrom(getOwnerItemGroup()));
}
save();
}
......@@ -193,6 +222,10 @@ public class ListView extends View implements Saveable {
public String getIncludeRegex() {
return includeRegex;
}
public boolean isRecurse() {
return recurse;
}
/**
* Filter by enabled/disabled status of jobs.
......@@ -208,7 +241,7 @@ public class ListView extends View implements Saveable {
TopLevelItem item = ((ModifiableItemGroup<? extends TopLevelItem>)ig).doCreateItem(req, rsp);
if(item!=null) {
synchronized (this) {
jobNames.add(item.getName());
jobNames.add(item.getRelativeNameFrom(getOwnerItemGroup()));
}
owner.save();
}
......@@ -257,11 +290,21 @@ public class ListView extends View implements Saveable {
*/
@Override
protected void submit(StaplerRequest req) throws ServletException, FormException, IOException {
JSONObject json = req.getSubmittedForm();
synchronized (this) {
recurse = json.optBoolean("recurse", true);
jobNames.clear();
for (TopLevelItem item : getOwnerItemGroup().getItems()) {
if(req.getParameter(item.getName())!=null)
jobNames.add(item.getName());
Iterable<? extends TopLevelItem> items;
if (recurse) {
items = Items.getAllItems(getOwnerItemGroup(), TopLevelItem.class);
} else {
items = getOwnerItemGroup().getItems();
}
for (TopLevelItem item : items) {
String relativeNameFrom = item.getRelativeNameFrom(getOwnerItemGroup());
if(req.getParameter(relativeNameFrom)!=null) {
jobNames.add(relativeNameFrom);
}
}
}
......@@ -279,12 +322,12 @@ public class ListView extends View implements Saveable {
if (columns == null) {
columns = new DescribableList<ListViewColumn,Descriptor<ListViewColumn>>(this);
}
columns.rebuildHetero(req, req.getSubmittedForm(), ListViewColumn.all(), "columns");
columns.rebuildHetero(req, json, ListViewColumn.all(), "columns");
if (jobFilters == null) {
jobFilters = new DescribableList<ViewJobFilter,Descriptor<ViewJobFilter>>(this);
}
jobFilters.rebuildHetero(req, req.getSubmittedForm(), ViewJobFilter.all(), "jobFilters");
jobFilters.rebuildHetero(req, json, ViewJobFilter.all(), "jobFilters");
String filter = Util.fixEmpty(req.getParameter("statusFilter"));
statusFilter = filter != null ? "1".equals(filter) : null;
......
......@@ -1385,24 +1385,7 @@ public class Jenkins extends AbstractCIBase implements ModifiableTopLevelItemGro
* and filter them by the given type.
*/
public <T extends Item> List<T> getAllItems(Class<T> type) {
List<T> r = new ArrayList<T>();
Stack<ItemGroup> q = new Stack<ItemGroup>();
q.push(this);
while(!q.isEmpty()) {
ItemGroup<?> parent = q.pop();
for (Item i : parent.getItems()) {
if(type.isInstance(i)) {
if (i.hasPermission(Item.READ))
r.add(type.cast(i));
}
if(i instanceof ItemGroup)
q.push((ItemGroup)i);
}
}
return r;
return Items.getAllItems(this, type);
}
/**
......
......@@ -111,7 +111,7 @@ THE SOFTWARE.
</p>
</j:when>
<j:otherwise>
<t:projectView jobs="${jobs}" jobBaseUrl="${rootURL}/" useFullName="true"/>
<t:projectView jobs="${jobs}"/>
</j:otherwise>
</j:choose>
......
......@@ -53,7 +53,7 @@ THE SOFTWARE.
</p>
</j:when>
<j:otherwise>
<t:projectView jobs="${jobs}" jobBaseUrl="${rootURL}/" useFullName="true"/>
<t:projectView jobs="${jobs}"/>
</j:otherwise>
</j:choose>
......
......@@ -36,12 +36,24 @@ THE SOFTWARE.
<f:option value="2" selected="${it.statusFilter==false}">${%Disabled jobs only}</f:option>
</select>
</f:entry>
<f:entry title="${%Recurse in subfolders}" field="recurse">
<f:checkbox id="recurse"/>
</f:entry>
<f:entry title="${%Jobs}">
<div class="listview-jobs">
<j:forEach var="job" items="${it.ownerItemGroup.items}">
<f:checkbox name="${job.name}" checked="${it.contains(job)}" title="${job.name}" />
<br/>
<j:forEach var="job" items="${h.getAllTopLevelItems(it.ownerItemGroup)}">
<j:set var="spanClass" value=""/>
<j:set var="spanStyle" value=""/>
<j:if test="${job.parent!=it.ownerItemGroup}">
<j:set var="spanClass" value="nested"/>
<j:set var="spanStyle" value="${it.recurse?'':'display:none'}"/>
</j:if>
<span class="${spanClass}" style="${spanStyle}">
<f:checkbox name="${job.getRelativeNameFromGroup(it.ownerItemGroup)}" checked="${it.jobNamesContains(job)}" title="${h.getRelativeDisplayNameFrom(job,it.ownerItemGroup)}" />
<br/>
</span>
</j:forEach>
</div>
</f:entry>
......@@ -66,5 +78,18 @@ THE SOFTWARE.
<f:repeatableHeteroProperty field="columns" hasHeader="true" addCaption="${%Add column}"/>
</f:block>
</f:section>
<script>
(function() {
Behaviour.specify("#recurse", 'ListView', 0, function(e) {
var nestedElements = $$('SPAN.nested')
e.onclick = function() {
nestedElements.each(function(el) {
e.checked ? el.show() : el.hide();
});
}
});
}());
</script>
</j:jelly>
......@@ -11,7 +11,7 @@ if (items.isEmpty()) {
}
include(my,"noJob.jelly");
} else {
t.projectView(jobs: items, jobBaseUrl: "", showViewTabs: true, columnExtensions: my.columns, indenter: my.indenter) {
t.projectView(jobs: items, showViewTabs: true, columnExtensions: my.columns, indenter: my.indenter) {
set("views",my.owner.views);
set("currentView",my);
if (my.owner.class == hudson.model.MyViewsProperty.class) {
......
......@@ -25,6 +25,6 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<td style="${indenter.getCss(job)}">
<a href="${jobBaseUrl}${job.shortUrl}" class='model-link'> ${useFullName ? job.fullDisplayName : job.displayName}</a>
<a href="${jobBaseUrl}${job.shortUrl}" class='model-link'> ${h.getRelativeDisplayNameFrom(job,currentView.owner.itemGroup)}</a>
</td>
</j:jelly>
\ No newline at end of file
......@@ -33,10 +33,9 @@ THE SOFTWARE.
</st:attribute>
<st:attribute name="useFullName" use="optional" type="boolean">
May be interpreted by columns to display the full name of a job.
Will also append a parent URL to jobBaseUrl if needed.
</st:attribute>
<st:attribute name="jobBaseUrl" type="String">
The base URL of all job links. Normally ${rootURL}/
Deprecated. Using this attribute has no effect.
</st:attribute>
<st:attribute name="showViewTabs" use="optional" type="boolean">
If the caller rendered a view tabes, set this attribute so that CSS is adjusted accordingly.
......@@ -79,7 +78,7 @@ THE SOFTWARE.
</j:forEach>
<j:forEach var="job" items="${jobs}">
<t:projectViewRow jobBaseUrl="${useFullName ? jobBaseUrl + job.parent.url : jobBaseUrl}"/>
<t:projectViewRow jobBaseUrl="${h.getRelativeLinkTo(job).substring(0, h.getRelativeLinkTo(job).length() - job.shortUrl.length())}"/>
</j:forEach>
</table>
<t:iconSize><t:rssBar/></t:iconSize>
......
......@@ -23,18 +23,30 @@
*/
package hudson;
import static org.junit.Assert.assertEquals;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.when;
import hudson.model.Action;
import hudson.model.ItemGroup;
import hudson.model.TopLevelItem;
import hudson.model.View;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import hudson.model.Action;
import static org.junit.Assert.*;
import jenkins.model.Jenkins;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.jvnet.hudson.test.Bug;
import org.kohsuke.stapler.Ancestor;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.core.classloader.annotations.PrepareForTest;
import static org.powermock.api.mockito.PowerMockito.*;
import org.powermock.modules.junit4.PowerMockRunner;
@RunWith(PowerMockRunner.class)
public class FunctionsTest {
......@@ -101,6 +113,94 @@ public class FunctionsTest {
assertEquals(contextPath + "/" + itUrl + path, result);
}
}
@Test
@PrepareForTest({Stapler.class, Jenkins.class})
public void testGetRelativeLinkTo_JobContainedInView() throws Exception{
Jenkins j = createMockJenkins();
ItemGroup parent = j;
String contextPath = "/jenkins";
StaplerRequest req = createMockRequest(contextPath);
mockStatic(Stapler.class);
when(Stapler.getCurrentRequest()).thenReturn(req);
View view = mock(View.class);
when(view.getOwnerItemGroup()).thenReturn(parent);
createMockAncestors(req, createAncestor(view, "."), createAncestor(j, "../.."));
TopLevelItem i = createMockItem(parent, "job/i/");
when(view.getItems()).thenReturn(Arrays.asList(i));
String result = Functions.getRelativeLinkTo(i);
assertEquals("job/i/", result);
}
@Test
@PrepareForTest({Stapler.class, Jenkins.class})
public void testGetRelativeLinkTo_JobNotContainedInView() throws Exception{
Jenkins j = createMockJenkins();
ItemGroup parent = j;
String contextPath = "/jenkins";
StaplerRequest req = createMockRequest(contextPath);
mockStatic(Stapler.class);
when(Stapler.getCurrentRequest()).thenReturn(req);
View view = mock(View.class);
when(view.getOwnerItemGroup()).thenReturn(parent);
createMockAncestors(req, createAncestor(j, "../.."), createAncestor(view, "."));
TopLevelItem i = createMockItem(parent, "job/i/");
when(view.getItems()).thenReturn(Collections.<TopLevelItem>emptyList());
String result = Functions.getRelativeLinkTo(i);
assertEquals("/jenkins/job/i/", result);
}
private interface TopLevelItemAndItemGroup <T extends TopLevelItem> extends TopLevelItem, ItemGroup<T> {}
@Test
@PrepareForTest({Stapler.class,Jenkins.class})
public void testGetRelativeLinkTo_JobContainedInViewWithinItemGroup() throws Exception{
Jenkins j = createMockJenkins();
TopLevelItemAndItemGroup parent = mock(TopLevelItemAndItemGroup.class);
when(parent.getShortUrl()).thenReturn("parent/");
String contextPath = "/jenkins";
StaplerRequest req = createMockRequest(contextPath);
mockStatic(Stapler.class);
when(Stapler.getCurrentRequest()).thenReturn(req);
View view = mock(View.class);
when(view.getOwnerItemGroup()).thenReturn(parent);
createMockAncestors(req, createAncestor(j, "../../.."), createAncestor(parent, "../.."), createAncestor(view, "."));
TopLevelItem i = createMockItem(parent, "job/i/", "parent/job/i/");
when(view.getItems()).thenReturn(Arrays.asList(i));
String result = Functions.getRelativeLinkTo(i);
assertEquals("job/i/", result);
}
private void createMockAncestors(StaplerRequest req, Ancestor... ancestors) {
List<Ancestor> ancestorsList = Arrays.asList(ancestors);
when(req.getAncestors()).thenReturn(ancestorsList);
}
private TopLevelItem createMockItem(ItemGroup p, String shortUrl) {
return createMockItem(p, shortUrl, shortUrl);
}
private TopLevelItem createMockItem(ItemGroup p, String shortUrl, String url) {
TopLevelItem i = mock(TopLevelItem.class);
when(i.getShortUrl()).thenReturn(shortUrl);
when(i.getUrl()).thenReturn(url);
when(i.getParent()).thenReturn(p);
return i;
}
private Jenkins createMockJenkins() {
mockStatic(Jenkins.class);
Jenkins j = mock(Jenkins.class);
when(Jenkins.getInstance()).thenReturn(j);
return j;
}
private static Ancestor createAncestor(Object o, String relativePath) {
Ancestor a = mock(Ancestor.class);
when(a.getObject()).thenReturn(o);
when(a.getRelativePath()).thenReturn(relativePath);
return a;
}
@Test
@PrepareForTest(Stapler.class)
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册