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

[FIXED JENKINS-12735] Merge branch 'pull-542'

......@@ -55,7 +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=>
<li class=bug>
Handle version ranges in automatic maven module dependency wiring.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-12735">issue 12735</a>)
<li class=bug>
Detect bugs relating to short <code>Descriptor</code> names early.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-14995">issue 14995</a> continued)
......
......@@ -23,6 +23,8 @@
*/
package hudson.maven;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import hudson.CopyOnWrite;
import hudson.Functions;
import hudson.Util;
......@@ -45,7 +47,13 @@ import hudson.tasks.Maven.MavenInstallation;
import hudson.tasks.Publisher;
import hudson.util.AlternativeUiTextProvider;
import hudson.util.DescribableList;
import jenkins.model.Jenkins;
import org.apache.maven.project.MavenProject;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
......@@ -58,15 +66,6 @@ import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import jenkins.model.Jenkins;
import org.apache.maven.project.MavenProject;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
/**
* {@link Job} that builds projects based on Maven2.
*
......@@ -105,7 +104,7 @@ public class MavenModule extends AbstractMavenProject<MavenModule,MavenBuild> im
private transient ModuleName moduleName;
/**
* @see documentation in {@link PomInfo#relativePath}
* @see PomInfo#relativePath
*/
private String relativePath;
......@@ -423,7 +422,7 @@ public class MavenModule extends AbstractMavenProject<MavenModule,MavenBuild> im
// to emulate the old behavior that tries to identify the upstream by ignoring the version.
// Do this by putting groupId:artifactId:UNKNOWN to the modules list, but
// ONLY if we find a such an old MavenModule in this Jenkins instance.
boolean hasDependenciesWithUnknownVersion = hasDependenciesWithUnknownVersion();
final boolean hasDependenciesWithUnknownVersion = hasDependenciesWithUnknownVersion();
if (data == null) {
Map<ModuleDependency,MavenModule> modules = new HashMap<ModuleDependency,MavenModule>();
......@@ -431,7 +430,7 @@ public class MavenModule extends AbstractMavenProject<MavenModule,MavenBuild> im
if(!m.isBuildable()) continue;
ModuleDependency moduleDependency = m.asDependency();
MavenModule old = modules.get(moduleDependency);
MavenModule relevant = chooseMoreRelevantModule(old, m, moduleDependency);
MavenModule relevant = chooseMoreRelevantModule(old, m);
modules.put(moduleDependency, relevant);
if (hasDependenciesWithUnknownVersion) {
modules.put(moduleDependency.withUnknownVersion(),relevant);
......@@ -452,9 +451,6 @@ public class MavenModule extends AbstractMavenProject<MavenModule,MavenBuild> im
}
}
// In case two modules with the same name are defined, modules in the same MavenModuleSet
// take precedence.
// Can lead to OOME, if remembered in the computational data and there are lot big multi-module projects
// TODO: try to use soft references to clean the heap when needed
Map<ModuleDependency,MavenModule> myParentsModules; // = data.modulesPerParent.get(getParent());
......@@ -474,24 +470,40 @@ public class MavenModule extends AbstractMavenProject<MavenModule,MavenBuild> im
//data.modulesPerParent.put(getParent(), myParentsModules);
//}
// if the build style is the aggregator build, define dependencies against project,
// not module.
AbstractProject<?, ?> dest = getParent().isAggregatorStyleBuild() ? getParent() : this;
//Create a map of groupId:artifact id keys to modules for faster look ups in findMatchingDependentModule
Multimap<ModuleName,ModuleDependency> mapModules = data.byName();
for (ModuleDependency d : dependencies) {
MavenModule src = myParentsModules.get(d);
MavenModule src;
// In case two modules with the same name are defined, modules in the same MavenModuleSet
// take precedence.
src = myParentsModules.get(d);
// otherwise we can pick the module with the highest version number (within the constraint that
// it satisfies 'd')
if (src==null) {
src = data.allModules.get(d);
Collection<ModuleDependency> candidates = mapModules.get(d.getName());
ModuleDependency winner = d.findHighestFrom(candidates);
src = data.allModules.get(winner);
}
if(src!=null) {
DependencyGraph.Dependency dep = new MavenModuleDependency(
src.getParent().isAggregatorStyleBuild() ? src.getParent() : src,dest);
DependencyGraph.Dependency dep = new MavenModuleDependency(nodeOf(src),nodeOf(this));
if (!dep.pointsItself())
graph.addDependency(dep);
}
}
}
/**
* Determines the source/sink of the dependency from a module.
* This is because if the build is the aggregator build, we need to define dependencies against project,
* not module.
*/
private static AbstractMavenProject<?, ?> nodeOf(MavenModule m) {
return m.getParent().isAggregatorStyleBuild() ? m.getParent() : m;
}
/**
* Returns all Maven modules in this Jenkins instance.
......@@ -513,7 +525,7 @@ public class MavenModule extends AbstractMavenProject<MavenModule,MavenBuild> im
return false;
}
private MavenModule chooseMoreRelevantModule(MavenModule mm1, MavenModule mm2, ModuleDependency moduleDependency) {
private MavenModule chooseMoreRelevantModule(MavenModule mm1, MavenModule mm2) {
if (mm1 == null) {
return mm2;
......@@ -521,41 +533,37 @@ public class MavenModule extends AbstractMavenProject<MavenModule,MavenBuild> im
if (mm2 == null) {
return mm1;
}
final MavenModule moreRelevant;
final MavenModule lessRelevant;
int relevancy1 = getDependencyRelevancy(mm1);
int relevancy2 = getDependencyRelevancy(mm2);
if (relevancy1 > relevancy2) {
moreRelevant = mm1;
lessRelevant = mm2;
} else if (relevancy2 > relevancy1) {
moreRelevant = mm2;
lessRelevant = mm1;
} else {
// arbitrary, but reproduceable
if (mm1.getParent().getName().compareTo(mm2.getParent().getName()) < 0) {
moreRelevant = mm2;
lessRelevant = mm1;
} else { // should always mean > 0 as name is unique
moreRelevant = mm1;
lessRelevant = mm2;
}
int score = mm1.getDependencyRelevancy() - mm2.getDependencyRelevancy();
if (score==0) {
// tie breaker. this is arbitrary, but reproduceable
score = mm1.getParent().getFullName().compareTo(mm2.getParent().getFullName());
}
assert score!=0;
final MavenModule moreRelevant, lessRelevant;
if (score>0) { moreRelevant = mm1; lessRelevant = mm2; }
else { moreRelevant = mm2; lessRelevant = mm1; }
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("Choosing " + moreRelevant.getParent().getName() + " over " + lessRelevant.getParent().getName()
+ " for module " + moduleDependency.getName() + ". Relevancies: " + relevancy1 + ", " + relevancy2);
+ " for module " + mm1.asDependency().getName() + ". Relevancies: " + mm1.getDependencyRelevancy() + ", " + mm2.getDependencyRelevancy());
}
return moreRelevant;
}
private int getDependencyRelevancy(MavenModule mm) {
/**
* As a guide for automatic dependency computation,
* determine how much this particular build is "relevant" to other builds on this Jenkins.
*
* If the binary is being deployed, we assume the user intends the result of this build be used elsewhere,
* so we give a higher score.
*/
private int getDependencyRelevancy() {
int relevancy = 0;
for (String goal : Util.tokenize(mm.getGoals())) {
for (String goal : Util.tokenize(getGoals())) {
if ("deploy".equals(goal) || "deploy:deploy".equals(goal)) {
return 2;
}
......@@ -565,7 +573,7 @@ public class MavenModule extends AbstractMavenProject<MavenModule,MavenBuild> im
}
}
for (Publisher publisher : mm.getParent().getPublishers()) {
for (Publisher publisher : getParent().getPublishers()) {
if (publisher instanceof RedeployPublisher) {
return 2;
}
......@@ -576,14 +584,32 @@ public class MavenModule extends AbstractMavenProject<MavenModule,MavenBuild> im
private static class MavenDependencyComputationData {
boolean withUnknownVersions = false;
Map<ModuleDependency,MavenModule> allModules;
/**
* All {@link MavenModule}s in this Jenkins, keyed by their {@link MavenModule#asDependency()}.
*/
private final Map<ModuleDependency,MavenModule> allModules;
//Map<MavenModuleSet, Map<ModuleDependency,MavenModule>> modulesPerParent = new HashMap<MavenModuleSet, Map<ModuleDependency,MavenModule>>();
public MavenDependencyComputationData(
Map<ModuleDependency, MavenModule> modules) {
public MavenDependencyComputationData(Map<ModuleDependency, MavenModule> modules) {
this.allModules = modules;
}
/**
* Builds a map of all the modules, keyed against the groupId and artifactId. The values are a list of modules
* that match this criteria.
*
* @return {@link #allModules} keyed by their {@linkplain ModuleName names}.
*/
private Multimap<ModuleName,ModuleDependency> byName() {
Multimap<ModuleName,ModuleDependency> map = HashMultimap.create();
for (ModuleDependency dependency : allModules.keySet()) {
map.put(dependency.getName(),dependency);
}
return map;
}
}
@Override
......@@ -660,5 +686,5 @@ public class MavenModule extends AbstractMavenProject<MavenModule,MavenBuild> im
}
private static final Logger LOGGER = Logger.getLogger(MavenModule.class.getName());
}
......@@ -23,12 +23,21 @@
*/
package hudson.maven;
import org.apache.commons.collections.comparators.ReverseComparator;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.project.MavenProject;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.ReportPlugin;
import org.apache.maven.model.Extension;
import java.io.Serializable;
import java.util.Collection;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import hudson.Functions;
......@@ -41,6 +50,9 @@ import hudson.Functions;
public final class ModuleDependency implements Serializable {
public final String groupId;
public final String artifactId;
/**
* Version, possibly a version range.
*/
public final String version;
/**
......@@ -48,6 +60,20 @@ public final class ModuleDependency implements Serializable {
*/
public final boolean plugin;
/**
* Cached result of {@code VersionRange.createFromVersionSpec(version)}
*
* @see #getVersionAsRange()
*/
private VersionRange range;
/**
* Cache of the parsed form of {@link #version}
*
* @see #parseVersion()
*/
private ArtifactVersion parsedVersion;
public ModuleDependency(String groupId, String artifactId, String version) {
this(groupId, artifactId, version, false);
}
......@@ -135,6 +161,18 @@ public final class ModuleDependency implements Serializable {
return result;
}
public VersionRange getVersionAsRange() throws InvalidVersionSpecificationException {
if (range==null)
range = VersionRange.createFromVersionSpec(version);
return range;
}
public ArtifactVersion parseVersion() {
if (parsedVersion==null)
parsedVersion = new DefaultArtifactVersion(version);
return parsedVersion;
}
/**
* Upon reading from the disk, intern strings.
*/
......@@ -163,4 +201,57 @@ public final class ModuleDependency implements Serializable {
public static final String NONE = "-";
private static final long serialVersionUID = 1L;
/**
* Checks whether this ModuleDependency is satisfied by the dependency of the given ModuleDependency.
* This caters for versions where the version string defines a version range.
*
* @param other The dependency to check for.
* @return true if contained false otherwise.
*/
public boolean contains(ModuleDependency other) {
if (other == null || !getName().equals(other.getName()))
return false;
try {
return getVersionAsRange().containsVersion(other.parseVersion());
} catch (InvalidVersionSpecificationException ivse) {
return false;
}
}
/**
* Given a list of ModuleDependencies (of the same groupId and artifactId),
* picks the {@link ModuleDependency} that satisfies the constraint and has the highest version.
*
* @param candidates
* List that represents specific (non-range) versions.
* @return The highest satisfying ModuleDependency or null if none can be found.
*/
public ModuleDependency findHighestFrom(Collection<ModuleDependency> candidates) {
//Create a sorted map of the ModuleDependnecies sorted on version (descending order).
SortedMap<ArtifactVersion, ModuleDependency> sorted = new TreeMap<ArtifactVersion, ModuleDependency>(new ReverseComparator());
for (ModuleDependency candidate : candidates) {
sorted.put(candidate.parseVersion(), candidate);
}
//Now find the highest version that satisfies this dependency.
for (ModuleDependency e : sorted.values()) {
if (contains(e))
return e;
}
// non found
return null;
}
@Override
public String toString() {
return "ModuleDependency{" +
"groupId='" + groupId + '\'' +
", artifactId='" + artifactId + '\'' +
", version='" + version + '\'' +
", plugin=" + plugin +
'}';
}
}
......@@ -33,7 +33,7 @@ import org.apache.maven.model.ReportPlugin;
import java.io.Serializable;
/**
* Version independent name of a Maven project.
* Version independent name of a Maven project. GroupID+artifactId.
*
* @author Kohsuke Kawaguchi
* @see ModuleDependency
......
......@@ -10,12 +10,14 @@ import hudson.model.AbstractProject;
import hudson.model.DependencyGraph;
import hudson.model.MockHelper;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import junit.framework.Assert;
import org.apache.maven.model.Build;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Plugin;
import org.apache.maven.project.MavenProject;
import org.junit.Before;
......@@ -126,4 +128,201 @@ public class MavenModuleTest {
when(parent.getModules()).thenReturn(Collections.singleton(mock));
}
/**
* This test is a standard project that has a versioned dependency.
*/
@Test
public void testSimpleVersion() {
TestComponents testComponents = createTestComponents("1.0.1-SNAPSHOT");
DependencyGraph graph = testComponents.graph;
MavenModule appMavenModule = testComponents.applicationMavenModule;
MavenModule libMavenModule = testComponents.libraryMavenModule;
graph.build();
List<AbstractProject> appDownstream = graph.getDownstream(appMavenModule);
List<AbstractProject> appUpstream = graph.getUpstream(appMavenModule);
List<AbstractProject> libDownstream = graph.getDownstream(libMavenModule);
List<AbstractProject> libUpstream = graph.getUpstream(libMavenModule);
Assert.assertEquals(0, appDownstream.size());
Assert.assertEquals(1, appUpstream.size());
Assert.assertEquals(1, libDownstream.size());
Assert.assertEquals(0, libUpstream.size());
}
/**
* This tests that a version range declaration in the dependency of a top level project
* resolves the up and downstream correctly.
*/
@Test
public void testSimpleVersionRange() {
TestComponents testComponents = createTestComponents("[1.0.0, )");
DependencyGraph graph = testComponents.graph;
MavenModule appMavenModule = testComponents.applicationMavenModule;
MavenModule libMavenModule = testComponents.libraryMavenModule;
graph.build();
List<AbstractProject> appDownstream = graph.getDownstream(appMavenModule);
List<AbstractProject> appUpstream = graph.getUpstream(appMavenModule);
List<AbstractProject> libDownstream = graph.getDownstream(libMavenModule);
List<AbstractProject> libUpstream = graph.getUpstream(libMavenModule);
Assert.assertEquals(0, appDownstream.size());
Assert.assertEquals(1, appUpstream.size());
Assert.assertEquals(1, libDownstream.size());
Assert.assertEquals(0, libUpstream.size());
}
/**
* Test multiple projects with dependencies on differing library versions declared with
* multiple version definitions.
*/
@Test
public void testMultipleDependencies() {
MavenProject projectA = createMavenProject("ProjectA", "test", "projectA", "1.0-SNAPSHOT", "jar");
Dependency dependencyA = createDependency("test", "library", "[1.0, 2.0)");
projectA.getDependencies().add(dependencyA);
MavenProject projectB = createMavenProject("ProjectB", "test", "projectB", "2.0-SNAPSHOT", "jar");
Dependency dependencyB = createDependency("test", "library", "[1.1, 2.1]");
projectB.getDependencies().add(dependencyB);
MavenProject dependX = createMavenProject("DependX-1.1", "test", "library", "1.1.3-SNAPSHOT", "jar");
MavenProject dependY = createMavenProject("DependX-1.2", "test", "library", "1.2.1-SNAPSHOT", "jar");
MavenProject dependZ = createMavenProject("DependX-2.0", "test", "library", "2.0.1-SNAPSHOT", "jar");
MavenModuleSet parent = mock(MavenModuleSet.class);
when(parent.isAggregatorStyleBuild()).thenReturn(Boolean.FALSE);
//Now create maven modules for all the projects
MavenModule mavenModuleA = mockMavenModule(projectA);
MavenModule mavenModuleB = mockMavenModule(projectB);
MavenModule mavenModuleX = mockMavenModule(dependX);
MavenModule mavenModuleY = mockMavenModule(dependY);
MavenModule mavenModuleZ = mockMavenModule(dependZ);
Collection<AbstractProject<?,?>> allModules = Lists.<AbstractProject<?,?>>newArrayList(mavenModuleA,
mavenModuleB, mavenModuleX, mavenModuleY, mavenModuleZ);
for (AbstractProject<?, ?> module : allModules) {
MavenModule mm = (MavenModule) module;
enhanceMavenModuleMock(mm, parent, allModules);
}
DependencyGraph graph = MockHelper.mockDependencyGraph(allModules);
doCallRealMethod().when(graph).getDownstream(Matchers.any(AbstractProject.class));
doCallRealMethod().when(graph).getUpstream(Matchers.any(AbstractProject.class));
doCallRealMethod().when(graph).compare(Matchers.<AbstractProject>any(), Matchers.<AbstractProject>any());
graph.build();
List<AbstractProject> downstreamA = graph.getDownstream(mavenModuleA);
List<AbstractProject> upstreamA = graph.getUpstream(mavenModuleA);
Assert.assertEquals(0, downstreamA.size());
Assert.assertEquals(1, upstreamA.size());
Assert.assertSame(dependY.getVersion(), ((MavenModule) upstreamA.get(0)).getVersion());
List<AbstractProject> downstreamB = graph.getDownstream(mavenModuleB);
List<AbstractProject> upstreamB = graph.getUpstream(mavenModuleB);
Assert.assertEquals(0, downstreamB.size());
Assert.assertEquals(1, upstreamA.size());
Assert.assertSame(dependZ.getVersion(), ((MavenModule) upstreamB.get(0)).getVersion());
}
private TestComponents createTestComponents(String libraryVersion) {
MavenProject appProject = createMavenProject("testapp", "test", "application", "1.0-SNAPSHOT", "jar");
Dependency dependency = createDependency("test", "library", libraryVersion);
appProject.getDependencies().add(dependency);
MavenModule appMavenModule = mockMavenModule(appProject);
MavenProject libProject = createLibrary();
MavenModule libMavenModule = mockMavenModule(libProject);
MavenModuleSet parent = mock(MavenModuleSet.class);
when(parent.isAggregatorStyleBuild()).thenReturn(Boolean.FALSE);
when(appMavenModule.getParent()).thenReturn(parent);
when(libMavenModule.getParent()).thenReturn(parent);
Collection<MavenModule> projects = Lists.newArrayList(appMavenModule, libMavenModule);
when(parent.getModules()).thenReturn(projects);
when(appMavenModule.getAllMavenModules()).thenReturn(projects);
when(libMavenModule.getAllMavenModules()).thenReturn(projects);
DependencyGraph graph = MockHelper.mockDependencyGraph(Lists.<AbstractProject<?,?>>newArrayList(appMavenModule, libMavenModule));
doCallRealMethod().when(graph).getDownstream(Matchers.any(AbstractProject.class));
doCallRealMethod().when(graph).getUpstream(Matchers.any(AbstractProject.class));
TestComponents testComponents = new TestComponents();
testComponents.graph = graph;
testComponents.applicationMavenModule = appMavenModule;
testComponents.libraryMavenModule = libMavenModule;
return testComponents;
}
private static void enhanceMavenModuleMock(MavenModule module,
MavenModuleSet parent,
Collection allProjects) {
when(module.getParent()).thenReturn(parent);
when(module.getAllMavenModules()).thenReturn(allProjects);
}
private static MavenModule mockMavenModule(MavenProject project) {
MavenModule mavenModule = mock(MavenModule.class);
when(mavenModule.getName()).thenReturn(project.getName());
basicMocking(mavenModule);
mavenModule.doSetName(project.getGroupId() + '$' + project.getArtifactId());
PomInfo pomInfo = new PomInfo(project, null, "relPath");
mavenModule.reconfigure(pomInfo);
return mavenModule;
}
private static MavenProject createMavenProject(String name,
String groupId,
String artifactId,
String version,
String packaging) {
MavenProject proj = new MavenProject();
proj.setName(name);
proj.setGroupId(groupId);
proj.setArtifactId(artifactId);
proj.setVersion(version);
proj.setPackaging(packaging);
return proj;
}
private static Dependency createDependency(String groupId, String artifactId, String version) {
Dependency dependency = new Dependency();
dependency.setGroupId(groupId);
dependency.setArtifactId(artifactId);
dependency.setVersion(version);
return dependency;
}
private static MavenProject createLibrary() {
MavenProject proj = createMavenProject("testlib", "test", "library", "1.0.1-SNAPSHOT", "jar");
Dependency dependency = new Dependency();
dependency.setArtifactId("log4j");
dependency.setGroupId("log4j");
dependency.setVersion("1.6.15");
proj.getDependencies().add(dependency);
return proj;
}
private static class TestComponents {
public DependencyGraph graph;
public MavenModule applicationMavenModule;
public MavenModule libraryMavenModule;
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册