diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 2d3ad6d6..01a09a0c 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -2,6 +2,14 @@ ## Release 0.3.8 (NOT RELEASED YET) + + * [Fixed Issue 290][issue-290] + + Added support for retrieving build status with jobname and build number + directly avoiding intermediate server requests. Useful for other systems that + regularly monitor Jenkins builds. + + * [JENKINS-46472](https://issues.jenkins-ci.org/browse/JENKINS-46472) Added ability to modify offline cause for offline computers. diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/JenkinsServer.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/JenkinsServer.java index 0a6f79a4..25b4e9c4 100644 --- a/jenkins-client/src/main/java/com/offbytwo/jenkins/JenkinsServer.java +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/JenkinsServer.java @@ -29,6 +29,7 @@ import com.offbytwo.jenkins.client.util.UrlUtils; import com.offbytwo.jenkins.helper.JenkinsVersion; import com.offbytwo.jenkins.model.Build; +import com.offbytwo.jenkins.model.BuildWithDetails; import com.offbytwo.jenkins.model.Computer; import com.offbytwo.jenkins.model.ComputerSet; import com.offbytwo.jenkins.model.FolderJob; @@ -882,6 +883,52 @@ public void renameJob(FolderJob folder, String oldJobName, String newJobName, Bo + "/doRename?newName=" + EncodingUtils.encodeParam(newJobName), crumbFlag); } + + + + + /** + * Get build details for a specific job and build number directly. + * This method makes a single call to the server + * @param jobName the name of the job + * @param buildNum the build number + * @param treeProps any tree property query values + * @return details of build, null if not present + * @throws IOException In case of a failure. + * @see #getBuildDetails(com.offbytwo.jenkins.model.FolderJob, java.lang.String, int) + */ + public BuildWithDetails getBuildDetails(final String jobName, final int buildNum, + final String... treeProps) + throws IOException { + return getBuildDetails(null, UrlUtils.toFullJobPath(jobName), buildNum, treeProps); + } + + + + /** + * Get build details for a specific job and build number directly. + * This method makes a single call to the server + * @param folder the folder where the given job is located if any + * @param jobName the name of the job + * @param buildNum the build number + * @param treeProps any tree property query values + * @return details of build, null if not present + * @throws IOException In case of a failure. + */ + public BuildWithDetails getBuildDetails(final FolderJob folder, final String jobName, + final int buildNum, final String... treeProps) throws IOException { + try { + final String path = UrlUtils.toBuildBaseUrl(folder, jobName, buildNum, treeProps); + final BuildWithDetails details = client.get(path, BuildWithDetails.class); + details.setClient(client); + return details; + } catch (final HttpResponseException e) { + LOGGER.debug("getBuildDetails(jobName={}, buildNum={}, treeProps={}) status={}", + jobName, buildNum, treeProps, e.getStatusCode()); + if (e.getStatusCode() == HttpStatus.SC_NOT_FOUND) return null; + throw e; + } + } } diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/client/util/UrlUtils.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/client/util/UrlUtils.java index 6f8b17d9..00e8b41c 100644 --- a/jenkins-client/src/main/java/com/offbytwo/jenkins/client/util/UrlUtils.java +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/client/util/UrlUtils.java @@ -153,4 +153,29 @@ public static URI toNoApiUri(final URI uri, final String context, final String p + /** + * Helper to create the base url for a build, with or without a given folder + * @param folder the folder or {@code null} + * @param jobName the name of the job. + * @param buildNum the build number of interest + * @param treeProps any tree property query values + * @return converted base url. + */ + public static String toBuildBaseUrl(final FolderJob folder, final String jobName, + final int buildNum, final String... treeProps) { + final String path = UrlUtils.toJobBaseUrl(folder, jobName); + final StringBuilder sb = new StringBuilder(DEFAULT_BUFFER_SIZE); + sb.append(path); + if (!path.endsWith("/")) sb.append('/'); + sb.append(Integer.toString(buildNum)); + if (treeProps != null && treeProps.length > 0) { + sb.append("?tree="); + for(int i=0; i0) sb.append("%2C"); //comma + sb.append(EncodingUtils.encodeParam(treeProps[i])); + } + } + return sb.toString(); + } + } diff --git a/jenkins-client/src/test/java/com/offbytwo/jenkins/JenkinsServerTest.java b/jenkins-client/src/test/java/com/offbytwo/jenkins/JenkinsServerTest.java index 444d6d3b..53fe7420 100644 --- a/jenkins-client/src/test/java/com/offbytwo/jenkins/JenkinsServerTest.java +++ b/jenkins-client/src/test/java/com/offbytwo/jenkins/JenkinsServerTest.java @@ -27,11 +27,15 @@ import com.google.common.base.Optional; import com.offbytwo.jenkins.client.JenkinsHttpClient; +import com.offbytwo.jenkins.model.BuildWithDetails; import com.offbytwo.jenkins.model.FolderJob; import com.offbytwo.jenkins.model.Job; import com.offbytwo.jenkins.model.JobWithDetails; import com.offbytwo.jenkins.model.MainView; import com.offbytwo.jenkins.model.View; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpResponseException; +import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -328,6 +332,57 @@ public void getVersionShouldNotFailWithNPE() } + + @Test + public void testGetBuildDetails_NotFound() throws IOException { + final HttpResponseException ex = new HttpResponseException(HttpStatus.SC_NOT_FOUND,""); + given(client.get(anyString(), eq(BuildWithDetails.class))).willThrow(ex); + assertNull(server.getBuildDetails("someJob", 1)); + } + + @Test(expected=HttpResponseException.class) + public void testGetBuildDetails_ResponseException() throws IOException { + final HttpResponseException ex = new HttpResponseException(HttpStatus.SC_BAD_REQUEST,""); + given(client.get(anyString(), eq(BuildWithDetails.class))).willThrow(ex); + server.getBuildDetails("someJob", 1); + } + + + @Test + public void testGetBuildDetails_WithFolderJob() throws Exception { + final String expectedPath = "http://localhost/jobs/someFolder/job/someJob/1"; + final FolderJob folderJob = new FolderJob("someFolder", "http://localhost/jobs/someFolder/"); + final BuildWithDetails expected = new BuildWithDetails(); + given(client.get(eq(expectedPath), eq(BuildWithDetails.class))).willReturn(expected); + final BuildWithDetails details = server.getBuildDetails(folderJob, "someJob", 1); + assertTrue(details == expected); + } + + + + + @Test + public void testGetBuildDetails_WithOutFolderJob() throws Exception { + final String expectedPath = "/job/someJob/1"; + final BuildWithDetails expected = new BuildWithDetails(); + given(client.get(eq(expectedPath), eq(BuildWithDetails.class))).willReturn(expected); + final BuildWithDetails details = server.getBuildDetails("someJob", 1); + assertTrue(details == expected); + } + + @Test + public void testGetBuildDetails_WithTreeProperties() throws Exception { + final String expectedPath = "/job/someJob/1?tree=building%2Cculprits%5BfullName%5D"; + final BuildWithDetails expected = new BuildWithDetails(); + given(client.get(eq(expectedPath), eq(BuildWithDetails.class))).willReturn(expected); + final String[] treeProps = {"building", "culprits[fullName]"}; + final BuildWithDetails details = server.getBuildDetails("someJob", 1, treeProps); + assertTrue(details == expected); + } + + + + private void shouldGetFolderJobs(String... jobNames) throws IOException { // given String path = "http://localhost/jobs/someFolder/"; diff --git a/jenkins-client/src/test/java/com/offbytwo/jenkins/client/util/UrlUtilsTest.java b/jenkins-client/src/test/java/com/offbytwo/jenkins/client/util/UrlUtilsTest.java index 780600ba..9e916a24 100644 --- a/jenkins-client/src/test/java/com/offbytwo/jenkins/client/util/UrlUtilsTest.java +++ b/jenkins-client/src/test/java/com/offbytwo/jenkins/client/util/UrlUtilsTest.java @@ -321,4 +321,46 @@ public void testToNoQueryUri_NullContext() throws Exception { UrlUtils.toNoApiUri(new URI("http://localhost/jenkins"), null, "job/ajob"); } + + + @Test + public void testToBuildBaseUrl_WithFolder_WithTreeProperties() { + final String fpath = "http://localhost/jobs/someFolder/"; + final FolderJob folderJob = new FolderJob("someFolder", fpath); + final String expected = "http://localhost/jobs/someFolder/job/someJob/1234?tree=building%2Cculprits%5BfullName%5D"; + final String[] treeProps = {"building", "culprits[fullName]"}; + assertEquals(expected, UrlUtils.toBuildBaseUrl(folderJob, "someJob", 1234, treeProps)); + } + + + @Test + public void testToBuildBaseUrl_NoTrailingFolderSlash() { + final String fpath = "http://localhost/jobs/someFolder"; + final FolderJob folderJob = new FolderJob("someFolder", fpath); + final String expected = "http://localhost/jobs/someFolder/job/someJob/1234"; + assertEquals(expected, UrlUtils.toBuildBaseUrl(folderJob, "someJob", 1234)); + } + + + @Test + public void testToBuildBaseUrl_NullFolderJob() { + assertEquals("/job/someJob/1234", UrlUtils.toBuildBaseUrl(null, "someJob", 1234)); + } + + + @Test + public void testToBuildBaseUrl_EmptyJobName() { + final String fpath = "http://localhost/jobs/someFolder"; + final FolderJob folderJob = new FolderJob("someFolder", fpath); + final String expected = "http://localhost/jobs/someFolder/job/1234"; + assertEquals(expected, UrlUtils.toBuildBaseUrl(folderJob, "", 1234)); + } + + @Test(expected = NullPointerException.class) + public void testToBuildBaseUrl_NullJobName() { + final String fpath = "http://localhost/jobs/someFolder"; + final FolderJob folderJob = new FolderJob("someFolder", fpath); + UrlUtils.toBuildBaseUrl(folderJob, null, 1234); + } + } \ No newline at end of file diff --git a/jenkins-client/src/test/java/com/offbytwo/jenkins/integration/BuildWithDetailsIT.java b/jenkins-client/src/test/java/com/offbytwo/jenkins/integration/BuildWithDetailsIT.java index d7ba8dd4..e351bf33 100644 --- a/jenkins-client/src/test/java/com/offbytwo/jenkins/integration/BuildWithDetailsIT.java +++ b/jenkins-client/src/test/java/com/offbytwo/jenkins/integration/BuildWithDetailsIT.java @@ -18,9 +18,12 @@ import com.offbytwo.jenkins.model.JobWithDetails; import hudson.model.Cause; +import hudson.model.FreeStyleBuild; import hudson.model.FreeStyleProject; import hudson.model.ParametersAction; import hudson.model.StringParameterValue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; public class BuildWithDetailsIT { @@ -55,5 +58,34 @@ public void checkCauses() throws Exception { assertThat(build.getBuiltOn()).isEmpty(); } + + + + @Test + public void testGetBuildDetails_FullDetails() throws Exception { + final FreeStyleProject proj = jenkinsRule.createFreeStyleProject(); + final FreeStyleBuild build = proj.scheduleBuild2(0, new Cause.UserIdCause()).get(); + final BuildWithDetails details = server.getBuildDetails(proj.getName(), build.number); + assertEquals(BuildResult.SUCCESS, details.getResult()); + assertNotNull(details.getFullDisplayName()); + assertNotNull(details.getActions()); + assertNotNull(details.getUrl()); + } + + + @Test + public void testGetBuildDetails_SomeDetails() throws Exception { + final FreeStyleProject proj = jenkinsRule.createFreeStyleProject(); + final FreeStyleBuild build = proj.scheduleBuild2(0, new Cause.UserIdCause()).get(); + final String[] treeProps = {"building", "result", "actions[causes]"}; + BuildWithDetails details = server.getBuildDetails(proj.getName(), build.number, treeProps); + assertEquals(BuildResult.SUCCESS, details.getResult()); + assertFalse(details.isBuilding()); + assertNull(details.getFullDisplayName()); + assertNotNull(details.getActions()); + assertNull(details.getUrl()); + } + + }