提交 e76bd815 编写于 作者: K kohsuke

initial commit for the multi-config matrix project type, which is useful for...

initial commit for the multi-config matrix project type, which is useful for running tests and builds on a large number of semi-identical (but different) configurations.


git-svn-id: https://hudson.dev.java.net/svn/hudson/trunk/hudson/main@3525 71c3de6d-444a-0410-be80-ed276b4c234a
上级 3f7e266d
package hudson.matrix;
import hudson.Util;
import java.util.List;
import java.util.Collections;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.Enumeration;
import org.kohsuke.stapler.StaplerRequest;
/**
* Configuration axis.
*
* <p>
* This class represents a single dimension of the configuration matrix.
* For example, the JAX-WS RI test configuration might include
* one axis "container={glassfish,tomcat,jetty}" and another axis
* "stax={sjsxp,woodstox}", and so on.
*
* @author Kohsuke Kawaguchi
*/
public final class Axis implements Comparable<Axis>, Iterable<String> {
/**
* Name of this axis.
* Used as a variable name.
*/
public final String name;
/**
* Possible values for this axis.
*/
public final List<String> values;
public Axis(String name, List<String> values) {
this.name = name;
this.values = Collections.unmodifiableList(values);
}
public Iterator<String> iterator() {
return values.iterator();
}
public int size() {
return values.size();
}
public String value(int index) {
return values.get(index);
}
/**
* Axis is fully ordered so that we can convert between a list of axis
* and a string unambiguously.
*/
public int compareTo(Axis that) {
return this.name.compareTo(that.name);
}
public String toString() {
return new StringBuilder().append(name).append("={").append(Util.join(values,",")).append('}').toString();
}
/**
* Parses the submitted form (where possible values are
* presented as a list of checkboxes) and creates an axis
*/
public static Axis parsePrefixed(StaplerRequest req, String name) {
List<String> values = new ArrayList<String>();
String prefix = name+'.';
Enumeration e = req.getParameterNames();
while (e.hasMoreElements()) {
String paramName = (String) e.nextElement();
if(paramName.startsWith(prefix))
values.add(paramName.substring(prefix.length()));
}
return new Axis(name,values);
}
}
package hudson.matrix;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
/**
* List of {@link Axis}.
*
* @author Kohsuke Kawaguchi
*/
public class AxisList extends ArrayList<Axis> {
public AxisList() {
}
public AxisList(Collection<Axis> c) {
super(c);
}
public Axis find(String name) {
for (Axis a : this) {
if(a.name.equals(name))
return a;
}
return null;
}
/**
* List up all the possible combinations of this list.
*/
public Iterable<Combination> list() {
final int[] base = new int[size()];
int b = 1;
for( int i=size()-1; i>=0; i-- ) {
base[i] = b;
b *= get(i).size();
}
final int total = b; // number of total combinations
return new Iterable<Combination>() {
public Iterator<Combination> iterator() {
return new Iterator<Combination>() {
private int counter = 0;
public boolean hasNext() {
return counter<total;
}
public Combination next() {
String[] data = new String[size()];
int x = counter;
for( int i=0; i<data.length; i++) {
data[i] = get(i).value(x/base[i]);
x %= base[i];
}
assert x==0;
return new Combination(AxisList.this,data);
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
};
}
}
package hudson.matrix;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.StringTokenizer;
import java.util.HashMap;
/**
* A particular combination of {@link Axis} values.
*
* For example, when axes are "x={1,2},y={3,4}", then
* [1,3] is a combination (out of 4 possible combinations)
*
* @author Kohsuke Kawaguchi
*/
public final class Combination extends TreeMap<String,String> {
public Combination(AxisList axisList, List<String> values) {
for(int i=0; i<axisList.size(); i++)
put(axisList.get(i).name,values.get(i));
}
public Combination(AxisList axisList,String... values) {
this(axisList,Arrays.asList(values));
}
public Combination(Map<String,String> keyValuePairs) {
super(keyValuePairs);
}
/**
* Converts to the ID string representation:
* <tt>axisName=value,axisName=value,...</tt>
*/
public String toString() {
StringBuilder buf = new StringBuilder();
for (Map.Entry<String,String> e : entrySet()) {
if(buf.length()>0) buf.append(',');
buf.append(e.getKey()).append('=').append(e.getValue());
}
return buf.toString();
}
/**
* Reverse operation of {@link #toString()}.
*/
public static Combination fromString(String id) {
Map<String,String> m = new HashMap<String,String>();
StringTokenizer tokens = new StringTokenizer(id, ",");
while(tokens.hasMoreTokens()) {
String token = tokens.nextToken();
int idx = token.indexOf('=');
if(idx<0)
throw new IllegalArgumentException("Can't parse "+id);
m.put(token.substring(0,idx),token.substring(idx+1));
}
return new Combination(m);
}
// read-only
public void clear() {
throw new UnsupportedOperationException();
}
public void putAll(Map<? extends String, ? extends String> map) {
throw new UnsupportedOperationException();
}
public String put(String key, String value) {
throw new UnsupportedOperationException();
}
public String remove(Object key) {
throw new UnsupportedOperationException();
}
}
package hudson.matrix;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.Result;
import java.io.IOException;
import java.io.PrintStream;
/**
* @author Kohsuke Kawaguchi
*/
public class MatrixBuild extends AbstractBuild<MatrixProject,MatrixBuild> {
public MatrixBuild(MatrixProject job) throws IOException {
super(job);
}
@Override
public void run() {
run(new RunnerImpl());
}
private class RunnerImpl extends AbstractRunner {
protected Result doRun(BuildListener listener) throws Exception {
MatrixProject p = getProject();
PrintStream logger = listener.getLogger();
for(MatrixConfiguration c : p.getActiveConfigurations()) {
logger.println("Triggering "+c.getName());
c.scheduleBuild();
}
// TODO: wait for completion
return Result.SUCCESS;
}
public void post(BuildListener listener) {
// TODO: run aggregators
}
}
}
package hudson.matrix;
import hudson.FilePath;
import hudson.model.AbstractProject;
import hudson.model.DependencyGraph;
import hudson.model.Hudson;
import hudson.model.Node;
import hudson.model.SCMedItem;
import hudson.model.JDK;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Label;
import java.io.IOException;
/**
* One configuration of {@link MatrixProject}.
*
* @author Kohsuke Kawaguchi
*/
public class MatrixConfiguration extends AbstractProject<MatrixConfiguration,MatrixRun> implements SCMedItem {
/**
* The actual value combination.
*/
private transient /*final*/ Combination combination;
public MatrixConfiguration(MatrixProject parent, Combination c) {
super(parent,c.toString());
this.combination = c;
}
public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException {
super.onLoad(parent, name);
combination = Combination.fromString(name);
}
public MatrixProject getParent() {
return (MatrixProject)super.getParent();
}
@Override
public FilePath getWorkspace() {
Node node = getLastBuiltOn();
if(node==null) node = Hudson.getInstance();
return node.getWorkspaceFor(getParent()).child(getName());
}
@Override
protected Class<MatrixRun> getBuildClass() {
return MatrixRun.class;
}
@Override
public boolean isFingerprintConfigured() {
// TODO
return false;
}
@Override
protected void buildDependencyGraph(DependencyGraph graph) {
}
public MatrixConfiguration asProject() {
return this;
}
@Override
public Label getAssignedLabel() {
return Hudson.getInstance().getLabel(combination.get("label"));
}
@Override
public JDK getJDK() {
return Hudson.getInstance().getJDK(combination.get("jdk"));
}
/**
* JDK cannot be set on {@link MatrixConfiguration} because
* it's controlled by {@link MatrixProject}.
* @deprecated
* Not supported.
*/
public void setJDK(JDK jdk) throws IOException {
throw new UnsupportedOperationException();
}
}
package hudson.matrix;
import hudson.FilePath;
import hudson.model.AbstractProject;
import hudson.model.DependencyGraph;
import hudson.model.Descriptor.FormException;
import hudson.model.Hudson;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.ItemGroupMixIn;
import static hudson.model.ItemGroupMixIn.KEYED_BY_NAME;
import hudson.model.JDK;
import hudson.model.Label;
import hudson.model.Node;
import hudson.model.SCMedItem;
import hudson.model.TopLevelItem;
import hudson.model.TopLevelItemDescriptor;
import hudson.util.CopyOnWriteMap;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
/**
* @author Kohsuke Kawaguchi
*/
public class MatrixProject extends AbstractProject<MatrixProject,MatrixBuild> implements TopLevelItem, SCMedItem, ItemGroup<MatrixConfiguration> {
/**
* Other configuration axes.
*
* This also includes special axis "label" and "jdk" if they are configured.
*/
private volatile AxisList axes = new AxisList();
/**
* All {@link MatrixConfiguration}s, keyed by their {@link MatrixConfiguration#getName() names}.
*/
private transient /*final*/ Map<String,MatrixConfiguration> configurations = new CopyOnWriteMap.Tree<String,MatrixConfiguration>();
/**
* @see #getActiveConfigurations()
*/
private transient /*final*/ Set<MatrixConfiguration> activeConfigurations = new LinkedHashSet<MatrixConfiguration>();
public MatrixProject(String name) {
super(Hudson.getInstance(), name);
}
public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException {
super.onLoad(parent,name);
Collections.sort(axes); // perhaps the file was edited on disk and the sort order might have been broken
configurations = ItemGroupMixIn.<String,MatrixConfiguration>loadChildren(this,getConfigurationsDir(), KEYED_BY_NAME);
// find all active configurations
Set<MatrixConfiguration> active = new LinkedHashSet<MatrixConfiguration>();
for (Combination c : axes.list()) {
MatrixConfiguration config = configurations.get(c.toString());
if(config==null) {
config = new MatrixConfiguration(this,c);
configurations.put(config.getName(), config);
}
active.add(config);
}
this.activeConfigurations = active;
}
private File getConfigurationsDir() {
return new File(getRootDir(),"configurations");
}
/**
* Gets all active configurations.
* <p>
* In contract, inactive configurations are those that are left for archival purpose
* and no longer built when a new {@link MatrixBuild} is executed.
*/
public Collection<MatrixConfiguration> getActiveConfigurations() {
return activeConfigurations;
}
public Collection<MatrixConfiguration> getItems() {
return configurations.values();
}
public String getUrlChildPrefix() {
return ".";
}
public MatrixConfiguration getItem(String name) {
return configurations.get(name);
}
public File getRootDirFor(MatrixConfiguration child) {
return new File(getConfigurationsDir(),child.getName());
}
public Hudson getParent() {
return Hudson.getInstance();
}
/**
* @see #getJDKs()
*/
@Override @Deprecated
public JDK getJDK() {
return super.getJDK();
}
/**
* Gets the {@link JDK}s where the builds will be run.
* @return never null but can be empty
*/
public Set<JDK> getJDKs() {
Axis a = axes.find("jdk");
if(a==null) return Collections.emptySet();
Set<JDK> r = new HashSet<JDK>();
for (String j : a) {
JDK jdk = Hudson.getInstance().getJDK(j);
if(jdk!=null)
r.add(jdk);
}
return r;
}
/**
* Gets the {@link Label}s where the builds will be run.
* @return never null
*/
public Set<Label> getLabels() {
Axis a = axes.find("label");
if(a==null) return Collections.emptySet();
Set<Label> r = new HashSet<Label>();
for (String l : a)
r.add(Hudson.getInstance().getLabel(l));
return r;
}
@Override
public FilePath getWorkspace() {
Node node = getLastBuiltOn();
if(node==null) node = getParent();
return node.getWorkspaceFor(this);
}
protected Class<MatrixBuild> getBuildClass() {
return MatrixBuild.class;
}
public boolean isFingerprintConfigured() {
return false;
}
protected void buildDependencyGraph(DependencyGraph graph) {
// TODO: perhaps support downstream build triggering
}
public MatrixProject asProject() {
return this;
}
protected void submit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException {
super.submit(req, rsp);
AxisList newAxes = new AxisList();
newAxes.add(Axis.parsePrefixed(req,"jdk"));
newAxes.add(Axis.parsePrefixed(req,"label"));
this.axes = newAxes;
}
public DescriptorImpl getDescriptor() {
return DESCRIPTOR;
}
public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
public static final class DescriptorImpl extends TopLevelItemDescriptor {
private DescriptorImpl() {
super(MatrixProject.class);
}
public String getDisplayName() {
return "Build multi-configuration project";
}
public MatrixProject newInstance(String name) {
return new MatrixProject(name);
}
}
}
package hudson.matrix;
import hudson.model.AbstractBuild;
import java.io.IOException;
import java.io.File;
import java.util.Calendar;
/**
* Execution of {@link MatrixConfiguration}.
*
* @author Kohsuke Kawaguchi
*/
public class MatrixRun extends AbstractBuild<MatrixConfiguration,MatrixRun> {
public MatrixRun(MatrixConfiguration job) throws IOException {
super(job);
}
public MatrixRun(MatrixConfiguration job, Calendar timestamp) {
super(job, timestamp);
}
public MatrixRun(MatrixConfiguration project, File buildDir) throws IOException {
super(project, buildDir);
}
public void run() {
// TODO
throw new UnsupportedOperationException();
}
}
......@@ -11,24 +11,24 @@ import hudson.model.Executor;
import hudson.model.Hudson;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Items;
import static hudson.model.ItemGroupMixIn.loadChildren;
import hudson.model.Job;
import hudson.model.Node;
import hudson.model.Queue;
import hudson.model.Queue.Task;
import hudson.model.SCMedItem;
import hudson.model.TopLevelItem;
import hudson.model.TopLevelItemDescriptor;
import hudson.model.Queue.Task;
import hudson.tasks.Maven;
import hudson.tasks.Maven.MavenInstallation;
import hudson.util.CopyOnWriteMap;
import hudson.util.DescribableList;
import hudson.util.Function1;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
......@@ -169,7 +169,7 @@ public final class MavenModuleSet extends AbstractMavenProject<MavenModuleSet,Ma
}
public File getRootDirFor(MavenModule child) {
return new File(new File(getRootDir(),"modules"),child.getModuleName().toFileSystemName());
return new File(getModulesDir(),child.getModuleName().toFileSystemName());
}
public Collection<Job> getAllJobs() {
......@@ -213,23 +213,11 @@ public final class MavenModuleSet extends AbstractMavenProject<MavenModuleSet,Ma
public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException {
super.onLoad(parent, name);
File modulesDir = new File(getRootDir(),"modules");
modulesDir.mkdirs(); // make sure it exists
File[] subdirs = modulesDir.listFiles(new FileFilter() {
public boolean accept(File child) {
return child.isDirectory();
modules = loadChildren(this, getModulesDir(),new Function1<ModuleName,MavenModule>() {
public ModuleName call(MavenModule module) {
return module.getModuleName();
}
});
modules = new CopyOnWriteMap.Tree<ModuleName,MavenModule>();
for (File subdir : subdirs) {
try {
MavenModule item = (MavenModule) Items.load(this,subdir);
modules.put(item.getModuleName(), item);
} catch (IOException e) {
e.printStackTrace(); // TODO: logging
}
}
if(reporters==null)
reporters = new DescribableList<MavenReporter, Descriptor<MavenReporter>>(this);
......@@ -237,6 +225,10 @@ public final class MavenModuleSet extends AbstractMavenProject<MavenModuleSet,Ma
updateTransientActions();
}
private File getModulesDir() {
return new File(getRootDir(),"modules");
}
/**
* To make it easy to grasp relationship among modules
* and the module set, we'll align the build numbers of
......
......@@ -7,6 +7,7 @@ import java.io.File;
* Represents a grouping inherent to a kind of {@link Item}s.
*
* @author Kohsuke Kawaguchi
* @see ItemGroupMixIn
*/
public interface ItemGroup<T extends Item> extends PersistenceRoot, ModelObject {
/**
......
package hudson.model;
import hudson.util.CopyOnWriteMap;
import hudson.util.Function1;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.Map;
/**
* Defines a bunch of static methods to be used as a "mix-in" for {@link ItemGroup}
* implementations.
*
* @author Kohsuke Kawaguchi
*/
public class ItemGroupMixIn {
/**
* Loads all the child {@link Item}s.
*
* @param modulesDir
* Directory that contains sub-directories for each child item.
*/
public static <K,V extends Item> Map<K,V> loadChildren(ItemGroup parent, File modulesDir, Function1<? extends K,? super V> key) {
modulesDir.mkdirs(); // make sure it exists
File[] subdirs = modulesDir.listFiles(new FileFilter() {
public boolean accept(File child) {
return child.isDirectory();
}
});
CopyOnWriteMap.Tree<K,V> configurations = new CopyOnWriteMap.Tree<K,V>();
for (File subdir : subdirs) {
try {
V item = (V) Items.load(parent,subdir);
configurations.put(key.call(item), item);
} catch (IOException e) {
e.printStackTrace(); // TODO: logging
}
}
return configurations;
}
/**
* {@link Item} -> name function.
*/
public static final Function1<String,Item> KEYED_BY_NAME = new Function1<String, Item>() {
public String call(Item item) {
return item.getName();
}
};
}
......@@ -2,6 +2,7 @@ package hudson.model;
import com.thoughtworks.xstream.XStream;
import hudson.XmlFile;
import hudson.matrix.MatrixProject;
import hudson.maven.MavenModule;
import hudson.maven.MavenModuleSet;
import hudson.util.XStream2;
......@@ -25,7 +26,6 @@ public class Items {
public static final List<TopLevelItemDescriptor> LIST = Descriptor.toList(
Project.DESCRIPTOR,
MavenModuleSet.DESCRIPTOR,
//MatrixProject.DESCRIPTOR,
ExternalJob.DESCRIPTOR
);
......@@ -105,5 +105,10 @@ public class Items {
XSTREAM.alias("project",Project.class);
XSTREAM.alias("maven2", MavenModule.class);
XSTREAM.alias("maven2-module-set", MavenModule.class);
// this feature is not publicly exposed yet
if(System.getProperty("Matrix")!=null)
LIST.add(MatrixProject.DESCRIPTOR);
XSTREAM.alias("matrix-project",MatrixProject.class);
}
}
package hudson.util;
/**
* Unary function <tt>y=f(x)</tt>.
*
* @author Kohsuke Kawaguchi
*/
public interface Function1<R,P1> {
R call(P1 param1);
}
<!--
Config page.
-->
<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" xmlns:p="/lib/hudson/project">
<p:config-disableBuild/>
<f:section title="Advanced Project Options">
<f:advanced>
<p:config-quietPeriod />
</f:advanced>
</f:section>
<p:config-scm/>
<p:config-trigger/>
<f:section title="Configuration Matrix">
<!-- JDK config -->
<j:set var="jdks" value="${app.JDKs}" />
<j:if test="${!empty(jdks)}">
<f:entry title="JDK" help="/help/matrix/jdk.html">
<j:forEach var="jdk" items="${jdks}">
<f:checkbox name="jdk.${jdk.name}" id="jdk.${jdk.name}" checked="${it.JDKs.contains(jdk)}" />
<label for="jdk.${jdk.name}">${jdk.name}</label>
<st:nbsp/>
</j:forEach>
</f:entry>
</j:if>
<!-- master slave -->
<j:if test="${!empty(app.slaves)}">
<j:set var="labels" value="${it.labels}" />
<f:optionalBlock name="multipleNodes" title="Build on multiple nodes" checked="${!empty(labels)}"
help="/help/matrix/slave.html">
<f:entry title="Node">
<div id="nodeTree" class="yahooTree" style="border: 1px solid grey; height: 10em; overflow:auto;"></div>
<script src="${rootURL}/scripts/yui/treeview-${yuiSuffix}.js"></script>
<link rel="stylesheet" href="${rootURL}/scripts/yui/treeview-assets/tree.css" type="text/css" />
<script>
(function(){
var tree = new YAHOO.widget.TreeView("nodeTree");
var machines = new YAHOO.widget.TextNode("Individual nodes", tree.getRoot(), false);
var labels = new YAHOO.widget.TextNode("Labels", tree.getRoot(), false);
<j:forEach var="l" items="${app.labels}">
new YAHOO.widget.TextNode('&lt;input type="checkbox" name="label.${l.name}" id="label.${l.name}" <j:if test="${labels.contains(l)}">checked</j:if>&gt;<label for="label.${l.name}">${l.name} (${l.description})</label>', ${h.ifThenElse(l.isSelfLabel(),'machines','labels')}, false);
</j:forEach>
tree.draw();
<!--
force the rendering of HTML, so that input fields are there
even when the form is submitted without this tree expanded.
-->
tree.expandAll();
tree.collapseAll();
})();
</script>
</f:entry>
</f:optionalBlock>
</j:if>
</f:section>
</j:jelly>
<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" xmlns:p="/lib/hudson/project">
<l:layout title="${it.name}">
<st:include page="sidepanel.jelly" />
<l:main-panel>
<h1>Project ${it.name}</h1>
<t:editableDescription adminOnly="true"/>
<p:projectActionFloatingBox/>
</l:main-panel>
</l:layout>
</j:jelly>
\ No newline at end of file
<div>
Suitable for projects that need large number of different configurations,
such as testing on multiple environments, platform-specific builds, etc.
</div>
\ No newline at end of file
<div>
Specify the JDK(s) with which builds are performed. If none is selected,
the default JDK is used (no explicit <tt>JAVA_HOME</tt>, and <tt>java</tt> command
is assumed to be in <tt>PATH</tt>.) If multiple JDKs are selected,
the build matrix will includes all the configurations.
<br>
Selecting multiple values is typically useful when this job is running tests,
and you need to run tests on multiple different versions of JDKs.
</div>
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册