diff --git a/src/main/java/jenkins/test/RunMatchers.java b/src/main/java/jenkins/test/RunMatchers.java new file mode 100644 index 000000000..9b2793d33 --- /dev/null +++ b/src/main/java/jenkins/test/RunMatchers.java @@ -0,0 +1,138 @@ +/* + * The MIT License + * + * Copyright 2024 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 jenkins.test; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.model.Result; +import hudson.model.Run; +import java.io.IOException; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.jvnet.hudson.test.JenkinsRule; + +/** + * Matchers for {@link Run} objects. + */ +public final class RunMatchers { + private RunMatchers() {} + + /** + * Creates a matcher checking whether a build is successful. + */ + public static Matcher> isSuccessful() { + return new RunResultMatcher(Result.SUCCESS); + } + + /** + * Creates a matcher checking whether a build has a specific outcome. + */ + public static Matcher> hasStatus(Result result) { + return new RunResultMatcher(result); + } + + /** + * Creates a matcher checking whether build logs contain a specific message. + * @param message the expected message + */ + public static Matcher> logContains(String message) { + return new RunLogMatcher(message); + } + + /** + * Creates a matcher checking whether a build has completed. + */ + public static Matcher> completed() { + return new CompletedRunMatcher(); + } + + private static class RunResultMatcher extends TypeSafeMatcher> { + @NonNull + private final Result expectedResult; + + public RunResultMatcher(@NonNull Result expectedResult) { + this.expectedResult = expectedResult; + } + + @Override + public void describeTo(Description description) { + description.appendText("a build with result " + expectedResult); + } + + @Override + protected boolean matchesSafely(Run run) { + return run.getResult() == expectedResult; + } + + @Override + protected void describeMismatchSafely(Run item, Description mismatchDescription) { + mismatchDescription.appendText("was ").appendValue(item.getResult()); + } + } + + private static class RunLogMatcher extends TypeSafeMatcher> { + @NonNull + private final String message; + + private RunLogMatcher(@NonNull String message) { + this.message = message; + } + + @Override + protected boolean matchesSafely(Run run) { + try { + return JenkinsRule.getLog(run).contains(message); + } catch (IOException x) { + return false; + } + } + + @Override + protected void describeMismatchSafely(Run item, Description mismatchDescription) { + mismatchDescription.appendText("was \n"); + try { + mismatchDescription.appendText(JenkinsRule.getLog(item)); + } catch (IOException e) { + mismatchDescription.appendText(""); + } + } + + @Override + public void describeTo(Description description) { + description.appendText("log containing ").appendValue(message); + } + } + + private static class CompletedRunMatcher extends TypeSafeMatcher> { + @Override + protected boolean matchesSafely(Run run) { + return !run.isLogUpdated(); + } + + @Override + public void describeTo(Description description) { + description.appendText("a completed build"); + } + } +} diff --git a/src/main/java/org/jvnet/hudson/test/JenkinsRule.java b/src/main/java/org/jvnet/hudson/test/JenkinsRule.java index 996cee769..7a55f4f5b 100644 --- a/src/main/java/org/jvnet/hudson/test/JenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/JenkinsRule.java @@ -1509,6 +1509,8 @@ public C configRoundtrip(C cloud) throws Exception { /** * Asserts that the outcome of the build is a specific outcome. + *

+ * Consider {@link jenkins.test.RunMatchers#hasStatus(Result)} as an alternative. */ public R assertBuildStatus(Result status, R r) throws Exception { if (status == r.getResult()) { @@ -1583,6 +1585,8 @@ public FreeStyleBuild buildAndAssertSuccess(@NonNull FreeStyleProject job) throw /** * Asserts that the console output of the build contains the given substring. + *

+ * Consider {@link jenkins.test.RunMatchers#logContains(String)} as an alternative. */ public void assertLogContains(String substring, Run run) throws IOException { assertThat(getLog(run), containsString(substring)); @@ -1590,6 +1594,8 @@ public void assertLogContains(String substring, Run run) throws IOException { /** * Asserts that the console output of the build does not contain the given substring. + *

+ * Consider {@link org.hamcrest.Matchers#not} and {@link jenkins.test.RunMatchers#logContains(String)} as an alternative. */ public void assertLogNotContains(String substring, Run run) throws IOException { assertThat(getLog(run), not(containsString(substring))); @@ -1613,6 +1619,9 @@ public static String getLog(Run run) throws IOException { /** * Waits for a build to complete. * Useful in conjunction with {@link BuildWatcher}. + *

+ * As an alternative, if using Awaitibility, you can use {@code await().until(() -> r, RunMatchers.completed());} + * * @return the same build, once done * @since 1.607 */ @@ -1629,7 +1638,6 @@ public > R waitForCompletion(R r) throws InterruptedException } } } - // Could be using com.jayway.awaitility:awaitility but it seems like overkill here. while (r.isLogUpdated()) { Thread.sleep(100); } diff --git a/src/test/java/jenkins/test/RunMatchersTest.java b/src/test/java/jenkins/test/RunMatchersTest.java new file mode 100644 index 000000000..30c00ae32 --- /dev/null +++ b/src/test/java/jenkins/test/RunMatchersTest.java @@ -0,0 +1,72 @@ +/* + * The MIT License + * + * Copyright 2024 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 jenkins.test; + +import static jenkins.test.RunMatchers.completed; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.not; +import static jenkins.test.RunMatchers.logContains; +import static jenkins.test.RunMatchers.hasStatus; +import static jenkins.test.RunMatchers.isSuccessful; + +import hudson.Functions; +import hudson.model.Result; +import hudson.tasks.BatchFile; +import hudson.tasks.Shell; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.FailureBuilder; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.SleepBuilder; + +public class RunMatchersTest { + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + public void buildSuccessful() throws Exception { + var p = j.createFreeStyleProject(); + p.getBuildersList().add(new SleepBuilder(1000)); + var b = p.scheduleBuild2(0).waitForStart(); + assertThat(j.waitForCompletion(b), allOf(completed(), isSuccessful())); + } + + @Test + public void buildFailure() throws Exception { + var p = j.createFreeStyleProject(); + p.getBuildersList().add(new FailureBuilder()); + var b = p.scheduleBuild2(0).waitForStart(); + assertThat(j.waitForCompletion(b), hasStatus(Result.FAILURE)); + } + + @Test + public void assertThatLogContains() throws Exception { + var p = j.createFreeStyleProject(); + p.getBuildersList().add(Functions.isWindows() ? new BatchFile("echo hello") : new Shell("echo hello")); + var b = p.scheduleBuild2(0).get(); + System.out.println(b.getDisplayName() + " completed"); + assertThat(b, allOf(logContains("echo hello"), not(logContains("echo bye")))); + } +}