Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[JENKINS-46064] ChangeLogStrategy extension and improve testing
  • Loading branch information
rsandell committed Aug 16, 2017
1 parent 465eea3 commit c8e55bb
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 96 deletions.
5 changes: 3 additions & 2 deletions pipeline-model-definition/pom.xml
Expand Up @@ -243,8 +243,9 @@
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>github-branch-source</artifactId>
<version>2.0.5</version>
<artifactId>scm-api</artifactId>
<version>2.2.1-SNAPSHOT</version> <!-- TODO https://github.com/jenkinsci/scm-api-plugin/pull/46 -->
<classifier>tests</classifier>
<scope>test</scope>
</dependency>

Expand Down
@@ -0,0 +1,63 @@
/*
* The MIT License
*
* Copyright (c) 2017, CloudBees, 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 org.jenkinsci.plugins.pipeline.modeldefinition.when.impl;

import hudson.Extension;
import jenkins.scm.api.SCMHead;
import org.jenkinsci.plugins.pipeline.modeldefinition.when.ChangeLogStrategy;

import javax.annotation.Nonnull;

@Extension
public class DefaultChangeLogStrategy extends ChangeLogStrategy {

private Class<?> bitbucketPr;
private Class<?> githubPr;

public DefaultChangeLogStrategy() {
try {
githubPr = Class.forName("org.jenkinsci.plugins.github_branch_source.PullRequestSCMHead");
} catch (ClassNotFoundException _) {
githubPr = null;
}
try {
bitbucketPr = Class.forName("com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMHead");
} catch (ClassNotFoundException _) {
bitbucketPr = null;
}
}

@Override
protected boolean shouldExamineAllBuilds(@Nonnull SCMHead head) {
if (githubPr != null && head.getClass().isAssignableFrom(githubPr)) {
return true;
}
if (bitbucketPr != null && head.getClass().isAssignableFrom(bitbucketPr)) {
return true;
}
return false;
}
}
Expand Up @@ -26,6 +26,8 @@
package org.jenkinsci.plugins.pipeline.modeldefinition.when.impl

import hudson.scm.ChangeLogSet
import jenkins.scm.api.SCMHead
import org.jenkinsci.plugins.pipeline.modeldefinition.when.ChangeLogStrategy
import org.jenkinsci.plugins.pipeline.modeldefinition.when.DeclarativeStageConditional
import org.jenkinsci.plugins.pipeline.modeldefinition.when.DeclarativeStageConditionalScript
import org.jenkinsci.plugins.workflow.cps.CpsScript
Expand All @@ -44,9 +46,8 @@ abstract class AbstractChangelogConditionalScript<S extends DeclarativeStageCond
RunWrapper run = this.script.getProperty("currentBuild")
if (run != null) {
List<ChangeLogSet<? extends ChangeLogSet.Entry>> changeSets = []
def branchJobProperty = run.rawBuild.parent.getProperty(BranchJobProperty.class) //TODO is there an easier way of finding this?
if (branchJobProperty != null) {
def branch = branchJobProperty.getBranch()
def head = SCMHead.HeadByItem.findHead(run.rawBuild.parent)
if (head != null) {
/*
Some special handling for pull requests to take into consideration all the builds for a particular PR.
Since a PR is a series of changes that will be merged in some way as one unit so all the changes should be considered.
Expand All @@ -57,23 +58,9 @@ abstract class AbstractChangelogConditionalScript<S extends DeclarativeStageCond
There are some caveats here, like if build 3 contains a revert commit of what is in build 2
we will still "trigger" for change sets on the commit that was reverted.
*/
boolean includeAllBuilds = false
try {
Class githubPr = Class.forName("org.jenkinsci.plugins.github_branch_source.PullRequestSCMHead")
if (branch.head.class.isAssignableFrom(githubPr)) {
includeAllBuilds = true
}
} catch (ClassNotFoundException _) { /*Ignore*/}
if (!includeAllBuilds) { //If not a GitHub pr then maybe bitbucket?
try {
Class bitbucketPr = Class.forName("com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMHead")
if (branch.head.class.isAssignableFrom(bitbucketPr)) {
includeAllBuilds = true
}
} catch (ClassNotFoundException _) { /*Ignore*/
}
}
if (includeAllBuilds) {
//TODO JENKINS-33274

if (ChangeLogStrategy.isExamineAllBuilds(head)) {
script.echo "Examining changelog from all builds of this change request."
for (RunWrapper currB = run; currB != null; currB = currB.previousBuild) {
changeSets.addAll(currB.getChangeSets())
Expand Down
Expand Up @@ -52,10 +52,13 @@ class ChangelogConditionalScript extends AbstractChangelogConditionalScript<Chan
//But it's probably simpler to make a build step that recaptures that information

if (gitChangeSetClass != null && change?.getClass()?.isAssignableFrom(gitChangeSetClass)) {
return describable.pattern.matcher(change.title).matches() || describable.multiLinePattern.matcher(change.comment).matches()
String title = change.title == null ? "" : change.title
String comment = change.comment == null ? "" : change.comment
return describable.pattern.matcher(title).matches() || describable.multiLinePattern.matcher(comment).matches()
} else {
//Something generic
return describable.pattern.matcher(change.msg).matches()
String msg = change.msg == null ? "" : change.msg
return describable.pattern.matcher(msg).matches() || describable.multiLinePattern.matcher(msg).matches()
}
}
}
Expand Up @@ -28,29 +28,36 @@
import com.gargoylesoftware.htmlunit.HttpMethod;
import com.gargoylesoftware.htmlunit.WebRequest;
import com.gargoylesoftware.htmlunit.util.NameValuePair;
import hudson.model.Queue;
import hudson.model.Result;
import hudson.model.Slave;
import jenkins.branch.Branch;
import jenkins.branch.BranchProperty;
import jenkins.branch.BranchSource;
import jenkins.scm.api.SCMHead;
import jenkins.scm.impl.mock.MockSCMController;
import jenkins.scm.impl.mock.MockSCMDiscoverChangeRequests;
import jenkins.scm.impl.mock.MockSCMSource;
import net.sf.json.JSONObject;
import org.jenkinsci.plugins.github_branch_source.BranchSCMHead;
import org.jenkinsci.plugins.github_branch_source.PullRequestSCMHead;
import org.jenkinsci.plugins.pipeline.modeldefinition.endpoints.ModelConverterAction;
import org.jenkinsci.plugins.pipeline.modeldefinition.model.Stage;
import org.jenkinsci.plugins.pipeline.modeldefinition.when.ChangeLogStrategy;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.multibranch.BranchJobProperty;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject;
import org.junit.BeforeClass;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.TestExtension;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.Collections;
import java.util.concurrent.ExecutionException;

import static org.hamcrest.collection.IsEmptyCollection.empty;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.core.IsNot.not;
import static org.jenkinsci.plugins.pipeline.modeldefinition.util.IsJsonObjectContaining.hasEntry;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
Expand Down Expand Up @@ -152,32 +159,57 @@ public void whenChangeset() throws Exception {

@Test
public void whenChangesetPR() throws Exception {
//TODO JENKINS-46086 First time build always skips the changelog
final ExpectationsBuilder builder = expect("when/changelog", "changeset")
.logContains("Hello", "Stage 'Two' skipped due to when conditional", "Warning, empty changelog. Probably because this is the first build.")
.logNotContains("JS World");
builder.go();

builder.resetForNewRun(Result.SUCCESS);

sampleRepo.write("webapp/js/somecode.js", "//fake file");
sampleRepo.git("add", "webapp/js/somecode.js");
sampleRepo.git("commit", "--message=files");

builder.logContains("Hello", "JS World")
.logNotContains("Stage 'Two' skipped due to when conditional", "Warning, empty changelog.")
.go();

builder.resetForNewRun(Result.SUCCESS);
fakePRMarker(builder.getRun().getParent());

sampleRepo.write("dontcare.txt", "empty");
sampleRepo.git("add", "dontcare.txt");
sampleRepo.git("commit", "--message=file");

builder.logContains("Hello", "JS World", "Examining changelog from all builds of this change request") //Should go for the file added in build 2
.logNotContains("Stage 'Two' skipped due to when conditional", "Warning, empty changelog.")
.go();
//TODO JENKINS-46086 First time build "always" skips the changelog when git, not when mock

MockSCMController controller = MockSCMController.create();
controller.createRepository("repoX");
controller.createBranch("repoX", "master");
final int num = controller.openChangeRequest("repoX", "master");
final String crNum = "change-request/" + num;
controller.addFile("repoX", crNum, "Jenkinsfile", "Jenkinsfile", pipelineSourceFromResources("when/changelog/changeset").getBytes());

WorkflowMultiBranchProject project = j.createProject(WorkflowMultiBranchProject.class);
project.getSourcesList().add(new BranchSource(new MockSCMSource(controller, "repoX", new MockSCMDiscoverChangeRequests())));

waitFor(project.scheduleBuild2(0));
j.waitUntilNoActivity();
assertThat(project.getItems(), not(empty()));

final WorkflowJob job = project.getItems().iterator().next();
final WorkflowRun build = job.getLastBuild();
assertNotNull(build);
j.assertLogContains("Hello", build);
j.assertLogContains("Stage 'Two' skipped due to when conditional", build);
j.assertLogNotContains("JS World", build);

controller.addFile("repoX", crNum,
"files",
"webapp/js/somecode.js", "//fake file".getBytes());

waitFor(project.scheduleBuild2(0));
j.waitUntilNoActivity();
final WorkflowRun build2 = job.getLastBuild();
assertThat(build2, not(equalTo(build)));

j.assertLogContains("Hello", build2);
j.assertLogContains("JS World", build2);
j.assertLogNotContains("Stage 'Two' skipped due to when conditional", build2);
j.assertLogNotContains("Warning, empty changelog", build2);

controller.addFile("repoX", crNum,
"file",
"dontcare.txt", "empty".getBytes());

waitFor(project.scheduleBuild2(0));
j.waitUntilNoActivity();
final WorkflowRun build3 = job.getLastBuild();
assertThat(build3, not(equalTo(build2)));

j.assertLogContains("Hello", build3);
j.assertLogContains("JS World", build3);
j.assertLogContains("Examining changelog from all builds of this change request", build3);
j.assertLogNotContains("Stage 'Two' skipped due to when conditional", build3);
j.assertLogNotContains("Warning, empty changelog", build3);
}

@Test
Expand All @@ -201,46 +233,86 @@ public void whenChangelog() throws Exception {

@Test
public void whenChangelogPR() throws Exception {
//TODO JENKINS-46086 First time build always skips the changelog
final ExpectationsBuilder builder = expect("when/changelog", "changelog")
.logContains("Hello", "Stage 'Two' skipped due to when conditional", "Warning, empty changelog. Probably because this is the first build.")
.logNotContains("Dull World");
builder.go();

builder.resetForNewRun(Result.SUCCESS);

sampleRepo.write("something.txt", "//fake file");
sampleRepo.git("add", "something.txt");
sampleRepo.git("commit", "-m", "Some title that we don't care about\n\nSome explanation\n[DEPENDENCY] some-app#45");

builder.logContains("Hello", "Dull World")
.logNotContains("Stage 'Two' skipped due to when conditional", "Warning, empty changelog.")
.go();

builder.resetForNewRun(Result.SUCCESS);
fakePRMarker(builder.getRun().getParent());

sampleRepo.write("something2.txt", "//fake file");
sampleRepo.git("add", "something2.txt");
sampleRepo.git("commit", "-m", "Some title");

builder.logContains("Hello", "Dull World", "Examining changelog from all builds of this change request") //Should go for the log in build 2
.logNotContains("Stage 'Two' skipped due to when conditional", "Warning, empty changelog.")
.go();
//TODO JENKINS-46086 First time build "always" skips the changelog when git, not when mock

MockSCMController controller = MockSCMController.create();
controller.createRepository("repoX");
controller.createBranch("repoX", "master");
final int num = controller.openChangeRequest("repoX", "master");
final String crNum = "change-request/" + num;
controller.addFile("repoX", crNum, "Jenkinsfile", "Jenkinsfile", pipelineSourceFromResources("when/changelog/changelog").getBytes());

WorkflowMultiBranchProject project = j.createProject(WorkflowMultiBranchProject.class);
project.getSourcesList().add(new BranchSource(new MockSCMSource(controller, "repoX", new MockSCMDiscoverChangeRequests())));

waitFor(project.scheduleBuild2(0));
j.waitUntilNoActivity();
assertThat(project.getItems(), not(empty()));

final WorkflowJob job = project.getItems().iterator().next();
final WorkflowRun build = job.getLastBuild();
assertNotNull(build);
j.assertLogContains("Hello", build);
j.assertLogContains("Stage 'Two' skipped due to when conditional", build);
j.assertLogNotContains("Dull World", build);

controller.addFile("repoX", crNum,
"Some title that we don't care about\n\nSome explanation\n[DEPENDENCY] some-app#45",
"something.txt", "//fake file".getBytes());

waitFor(project.scheduleBuild2(0));
j.waitUntilNoActivity();
final WorkflowRun build2 = job.getLastBuild();
assertThat(build2, not(equalTo(build)));

j.assertLogContains("Hello", build2);
j.assertLogContains("Dull World", build2);
j.assertLogNotContains("Stage 'Two' skipped due to when conditional", build2);
j.assertLogNotContains("Warning, empty changelog", build2);

controller.addFile("repoX", crNum,
"Some title",
"something2.txt", "//fake file".getBytes());

waitFor(project.scheduleBuild2(0));
j.waitUntilNoActivity();
final WorkflowRun build3 = job.getLastBuild();
assertThat(build3, not(equalTo(build2)));

j.assertLogContains("Hello", build3);
j.assertLogContains("Dull World", build3);
j.assertLogContains("Examining changelog from all builds of this change request", build3);
j.assertLogNotContains("Stage 'Two' skipped due to when conditional", build3);
j.assertLogNotContains("Warning, empty changelog", build3);

}

private void waitFor(Queue.Item item) throws InterruptedException, ExecutionException {
while (item != null && item.getFuture() == null) {
Thread.sleep(200);
}
item.getFuture().waitForStart();
}

static void fakePRMarker(WorkflowJob job) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
//Black magic ahead!!
final Constructor<PullRequestSCMHead> pullRequestSCMHeadConstructor = PullRequestSCMHead.class.getDeclaredConstructor(String.class, boolean.class, int.class, BranchSCMHead.class, String.class, String.class, String.class);
pullRequestSCMHeadConstructor.setAccessible(true);
final PullRequestSCMHead scmHead = pullRequestSCMHeadConstructor.newInstance("fake", false, 0, null, "fake", "fake", "master");
Branch b = new Branch("fake", scmHead, null, Collections.<BranchProperty>emptyList());
final Constructor<BranchJobProperty> branchJobPropertyConstructor = BranchJobProperty.class.getDeclaredConstructor(Branch.class);
branchJobPropertyConstructor.setAccessible(true);
final BranchJobProperty branchJobProperty = branchJobPropertyConstructor.newInstance(b);
job.addProperty(branchJobProperty);

@TestExtension
public static class TestChangeLogStrategy extends ChangeLogStrategy {
//Implement in a similar way as DefaultChangeLogStrategy to be a bit more true to reality.
private Class<?> mockPr;

public TestChangeLogStrategy() {
try {
mockPr = Class.forName("jenkins.scm.impl.mock.MockChangeRequestSCMHead");
} catch (ClassNotFoundException _) {
mockPr = null;
}
}

@Override
protected boolean shouldExamineAllBuilds(@Nonnull SCMHead head) {
if (mockPr != null && head.getClass().isAssignableFrom(mockPr)) {
return true;
}
return false;
}
}
}

0 comments on commit c8e55bb

Please sign in to comment.