depChanges = getDependencyChanges(build, build.getPreviousBuild());
for (AbstractBuild.DependencyChange depChange : depChanges.values()) {
builder.addAll(depChange.getBuilds());
}
return builder.build();
}
+
+ /**
+ * Gets the changes in the dependency between two given builds using {@link Fingerprinter.FingerprintAction}.
+ *
+ * This implements the functionality from {@link AbstractBuild#getDependencyChanges(AbstractBuild)} using
+ * {@link Run} instead of {@link AbstractBuild} as input build parameters, so that this aggregator can be used by
+ * the {@link AllChangesWorkflowAction} as well.
+ *
+ * @param build the current build to find dependencies to
+ * @param otherBuild another build to find dependencies from
+ * @return a map of projects to dependency changes or an empty map if there are no fingerprint actions or the other job is null
+ * @see AbstractBuild#getDependencyChanges(AbstractBuild)
+ */
+ private Map getDependencyChanges(Run build, Run otherBuild) {
+
+ if (otherBuild == null) {
+ return Collections.emptyMap();
+ }
+
+ Fingerprinter.FingerprintAction currentBuildFingerprints = build.getAction(Fingerprinter.FingerprintAction.class);
+ Fingerprinter.FingerprintAction previousBuildFingerprints = otherBuild.getAction(Fingerprinter.FingerprintAction.class);
+
+ if (currentBuildFingerprints == null || previousBuildFingerprints == null) {
+ return Collections.emptyMap();
+ }
+
+ Map currentBuildDependencies = currentBuildFingerprints.getDependencies(true);
+ Map previousBuildDependencies = previousBuildFingerprints.getDependencies(true);
+
+ Map changes = new HashMap<>();
+
+ for (Map.Entry entry : previousBuildDependencies.entrySet()) {
+ AbstractProject dependencyProject = entry.getKey();
+ Integer oldNumber = entry.getValue();
+ Integer newNumber = currentBuildDependencies.get(dependencyProject);
+ if (newNumber != null && oldNumber.compareTo(newNumber) < 0) {
+ changes.put(dependencyProject, new AbstractBuild.DependencyChange(dependencyProject, oldNumber, newNumber));
+ }
+ }
+
+ return changes;
+ }
}
diff --git a/src/main/java/org/jenkinsci/plugins/all_changes/SubProjectChangesAggregator.java b/src/main/java/org/jenkinsci/plugins/all_changes/SubProjectChangesAggregator.java
index 5ad2097..5aa93c8 100644
--- a/src/main/java/org/jenkinsci/plugins/all_changes/SubProjectChangesAggregator.java
+++ b/src/main/java/org/jenkinsci/plugins/all_changes/SubProjectChangesAggregator.java
@@ -47,7 +47,8 @@
* @author wolfs
*/
@Extension
-public class SubProjectChangesAggregator extends ChangesAggregator {
+public class SubProjectChangesAggregator extends ChangesAggregator {
+
@Override
public Collection aggregateBuildsWithChanges(AbstractBuild build) {
Plugin parameterizedTrigger = Util.getInstance().getPlugin("parameterized-trigger");
diff --git a/src/main/resources/org/jenkinsci/plugins/all_changes/AllChangesWorkflowAction/main.groovy b/src/main/resources/org/jenkinsci/plugins/all_changes/AllChangesWorkflowAction/main.groovy
index dba20bc..198fe06 100644
--- a/src/main/resources/org/jenkinsci/plugins/all_changes/AllChangesWorkflowAction/main.groovy
+++ b/src/main/resources/org/jenkinsci/plugins/all_changes/AllChangesWorkflowAction/main.groovy
@@ -24,6 +24,7 @@
import com.google.common.collect.Multimap
import hudson.Functions
+import hudson.model.Run
import hudson.scm.ChangeLogSet
import org.jenkinsci.plugins.workflow.job.WorkflowRun
import org.jvnet.localizer.LocaleProvider
@@ -61,7 +62,7 @@ private showChanges(Collection builds) {
def changedBuildCount = 1;
boolean hadChanges = false;
for (WorkflowRun build in builds) {
- Multimap changes = my.getAllChanges(build);
+ Multimap changes = my.getAllChanges(build);
if (changes.empty) {
continue
}
@@ -92,10 +93,10 @@ private showChanges(Collection builds) {
}
}
-private def showEntry(entry, WorkflowRun build, Collection builds) {
+private def showEntry(entry, WorkflowRun build, Collection builds) {
showChangeSet(entry)
boolean firstDrawn = false
- for (WorkflowRun b in builds) {
+ for (Run b in builds) {
if (b != build) {
if (!firstDrawn) {
text(" (")
diff --git a/src/test/java/org/jenkinsci/plugins/all_changes/AllChangesWorkflowActionTest.java b/src/test/java/org/jenkinsci/plugins/all_changes/AllChangesWorkflowActionTest.java
new file mode 100644
index 0000000..a56aa6d
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/all_changes/AllChangesWorkflowActionTest.java
@@ -0,0 +1,84 @@
+/*
+ * The MIT License
+ *
+ * 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 org.jenkinsci.plugins.all_changes;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import hudson.model.AbstractBuild;
+import hudson.model.Run;
+import org.jenkinsci.plugins.workflow.job.WorkflowRun;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.util.Set;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({
+ WorkflowRun.class
+})
+public class AllChangesWorkflowActionTest {
+
+ @Test
+ public void getContributingBuildsShouldWorkTransitively() throws Exception {
+
+ DependencyChangesAggregator aggregatorMock = mock(DependencyChangesAggregator.class);
+ WorkflowRun build = PowerMockito.mock(WorkflowRun.class);
+ AbstractBuild build2 = mock(AbstractBuild.class);
+ AbstractBuild build3 = mock(AbstractBuild.class);
+ when(aggregatorMock.aggregateBuildsWithChanges(build)).thenReturn(ImmutableList.of(build2));
+ when(aggregatorMock.aggregateBuildsWithChanges(build2)).thenReturn(ImmutableList.of(build3));
+
+ AllChangesWorkflowAction changesAction = new AllChangesWorkflowAction(null);
+ changesAction.setAggregator(aggregatorMock);
+
+ Set foundBuilds = changesAction.getContributingBuilds(build);
+
+ assertTrue(foundBuilds.equals(ImmutableSet.of(build, build2, build3)));
+ }
+
+ @Test
+ public void getContributingBuildsShouldWorkHandleCycles() throws Exception {
+
+ DependencyChangesAggregator aggregatorMock = mock(DependencyChangesAggregator.class);
+ WorkflowRun build = PowerMockito.mock(WorkflowRun.class);
+ AbstractBuild build2 = mock(AbstractBuild.class);
+ AbstractBuild build3 = mock(AbstractBuild.class);
+ when(aggregatorMock.aggregateBuildsWithChanges(build)).thenReturn(ImmutableList.of(build2));
+ when(aggregatorMock.aggregateBuildsWithChanges(build2)).thenReturn(ImmutableList.of(build3));
+ when(aggregatorMock.aggregateBuildsWithChanges(build3)).thenReturn(ImmutableList.of(build));
+
+ AllChangesWorkflowAction changesAction = new AllChangesWorkflowAction(null);
+ changesAction.setAggregator(aggregatorMock);
+
+ Set foundBuilds = changesAction.getContributingBuilds(build);
+
+ assertTrue(foundBuilds.equals(ImmutableSet.of(build, build2, build3)));
+ }
+}
diff --git a/src/test/java/org/jenkinsci/plugins/all_changes/DependencyChangesAggregatorTest.java b/src/test/java/org/jenkinsci/plugins/all_changes/DependencyChangesAggregatorTest.java
new file mode 100644
index 0000000..616d61e
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/all_changes/DependencyChangesAggregatorTest.java
@@ -0,0 +1,172 @@
+/*
+ * The MIT License
+ *
+ * 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 org.jenkinsci.plugins.all_changes;
+
+import hudson.FilePath;
+import hudson.model.*;
+import hudson.tasks.Fingerprinter;
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.jvnet.hudson.test.CreateFileBuilder;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.WithoutJenkins;
+import org.powermock.core.classloader.annotations.PowerMockIgnore;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.io.*;
+import java.util.*;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({
+ Fingerprinter.FingerprintAction.class
+})
+// needed to prevent PowerMock from changing the crypto things
+// (see "Troubleshooting" in https://wiki.jenkins.io/display/JENKINS/Mocking+in+Unit+Tests)
+@PowerMockIgnore({"javax.crypto.*" })
+public class DependencyChangesAggregatorTest {
+
+ @Rule
+ public JenkinsRule jenkins = new JenkinsRule();
+
+ private BuildListener testBuildListener;
+ private ByteArrayOutputStream out;
+
+ @Before
+ public void setup() {
+ out = new ByteArrayOutputStream();
+ testBuildListener = mock(BuildListener.class);
+ when(testBuildListener.getLogger()).thenReturn(new PrintStream(out));
+ }
+
+ @Test
+ public void returnsAllChangedDependencyBuilds() throws Exception {
+
+ FreeStyleProject proj1 = jenkins.createFreeStyleProject("proj1");
+ FreeStyleProject proj2 = jenkins.createFreeStyleProject("proj2");
+ FreeStyleProject proj3 = jenkins.createFreeStyleProject("proj3");
+ FreeStyleProject proj4 = jenkins.createFreeStyleProject("proj4");
+
+ jenkins.buildAndAssertSuccess(proj1);
+ createFileWithRandomString(proj1.getLastBuild(), "file1");
+ fingerprintFile(proj1.getLastBuild(), proj1.getLastBuild().getWorkspace(), "file1");
+
+ jenkins.buildAndAssertSuccess(proj2);
+ createFileWithRandomString(proj2.getLastBuild(), "file2");
+ fingerprintFile(proj2.getLastBuild(), proj2.getLastBuild().getWorkspace(), "file2");
+
+ jenkins.buildAndAssertSuccess(proj3);
+ createFileWithRandomString(proj3.getBuildByNumber(1), "file3");
+ fingerprintFile(proj3.getLastBuild(), proj3.getLastBuild().getWorkspace(), "file3");
+
+ jenkins.buildAndAssertSuccess(proj4);
+ fingerprintFile(proj4.getLastBuild(), proj1.getBuildByNumber(1).getWorkspace(), "file1");
+ fingerprintFile(proj4.getLastBuild(), proj2.getBuildByNumber(1).getWorkspace(), "file2");
+ fingerprintFile(proj4.getLastBuild(), proj3.getBuildByNumber(1).getWorkspace(), "file3");
+
+ jenkins.buildAndAssertSuccess(proj1);
+ createFileWithRandomString(proj1.getLastBuild(), "file1");
+ fingerprintFile(proj1.getLastBuild(), proj1.getLastBuild().getWorkspace(), "file1");
+
+ jenkins.buildAndAssertSuccess(proj1);
+ createFileWithRandomString(proj1.getLastBuild(), "file1");
+ fingerprintFile(proj1.getLastBuild(), proj1.getLastBuild().getWorkspace(), "file1");
+
+ jenkins.buildAndAssertSuccess(proj2);
+ createFileWithRandomString(proj2.getLastBuild(), "file2");
+ fingerprintFile(proj2.getLastBuild(), proj2.getLastBuild().getWorkspace(), "file2");
+
+ jenkins.buildAndAssertSuccess(proj4);
+ fingerprintFile(proj4.getLastBuild(), proj1.getBuildByNumber(2).getWorkspace(), "file1");
+ fingerprintFile(proj4.getLastBuild(), proj1.getBuildByNumber(3).getWorkspace(), "file1");
+ fingerprintFile(proj4.getLastBuild(), proj2.getBuildByNumber(2).getWorkspace(), "file2");
+
+ Collection res = (new DependencyChangesAggregator()).aggregateBuildsWithChanges(proj4.getBuildByNumber(2));
+
+ assertThat(res, Matchers.containsInAnyOrder(
+ proj1.getBuildByNumber(2),
+ proj1.getBuildByNumber(3),
+ proj2.getBuildByNumber(2)));
+ }
+
+ @Test
+ @WithoutJenkins
+ public void returnsEmptyCollectionIfBuildHasNoPreviousBuild() {
+
+ Run build = mock(Run.class);
+ when(build.getPreviousBuild()).thenReturn(null);
+
+ Collection res = new DependencyChangesAggregator().aggregateBuildsWithChanges(build);
+
+ assertThat(res, Matchers.empty());
+ }
+
+ @Test
+ @WithoutJenkins
+ public void returnsEmptyCollectionIfBuildHasNoFingerprintAction() {
+
+ Run build1 = mock(Run.class);
+ when(build1.getAction(Fingerprinter.FingerprintAction.class)).thenReturn(mock(Fingerprinter.FingerprintAction.class));
+
+ Run build2 = mock(Run.class);
+ when(build2.getPreviousBuild()).thenReturn(build1);
+ when(build2.getAction(Fingerprinter.FingerprintAction.class)).thenReturn(null);
+
+ Collection res = new DependencyChangesAggregator().aggregateBuildsWithChanges(build2);
+
+ assertThat(res, Matchers.empty());
+ }
+
+ @Test
+ @WithoutJenkins
+ public void returnsEmptyCollectionIfPreviousBuildHasNoFingerprintAction() {
+
+ Run build1 = mock(Run.class);
+ when(build1.getAction(Fingerprinter.FingerprintAction.class)).thenReturn(null);
+
+ Run build2 = mock(Run.class);
+ when(build2.getPreviousBuild()).thenReturn(build1);
+ when(build2.getAction(Fingerprinter.FingerprintAction.class)).thenReturn(mock(Fingerprinter.FingerprintAction.class));
+
+ Collection res = new DependencyChangesAggregator().aggregateBuildsWithChanges(build2);
+
+ assertThat(res, Matchers.empty());
+ }
+
+ private void createFileWithRandomString(AbstractBuild build, String filename) throws IOException, InterruptedException {
+ CreateFileBuilder fileBuilder = new CreateFileBuilder(filename, UUID.randomUUID().toString());
+ fileBuilder.perform(build, null, testBuildListener);
+ }
+
+ private void fingerprintFile(Run build, FilePath directory, String filename) throws InterruptedException {
+ Fingerprinter fingerprinter1 = new Fingerprinter(filename);
+ fingerprinter1.perform(build, directory, null, testBuildListener);
+ }
+}