Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding TailLog as an alternative to BuildWatcher #532

Merged
merged 3 commits into from
Dec 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/java/org/jvnet/hudson/test/BuildWatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
* Should work in combination with {@link JenkinsRule} or {@link RestartableJenkinsRule}.
* @see JenkinsRule#waitForCompletion
* @see JenkinsRule#waitForMessage
* @see TailLog
* @since 1.607
*/
public final class BuildWatcher extends ExternalResource {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@
* <li>{@link Assume} is not available.
* <li>{@link TestExtension} is not available.
* <li>{@link LoggerRule} is not available, however additional loggers can be configured via {@link #withLogger(Class, Level)}}.
* <li>{@link BuildWatcher} is not available.
* <li>{@link BuildWatcher} is not available, but you can use {@link TailLog} instead.
* <li>There is not currently enough flexibility in how the controller is launched.
* </ul>
* <p>Systems not yet tested:
Expand Down
124 changes: 124 additions & 0 deletions src/main/java/org/jvnet/hudson/test/TailLog.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* The MIT License
*
* Copyright 2022 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.jvnet.hudson.test;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.console.PlainTextConsoleOutputStream;
import hudson.model.Job;
import hudson.model.Run;
import java.io.File;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Semaphore;
import org.apache.commons.io.input.Tailer;
import org.apache.commons.io.input.TailerListenerAdapter;
import org.jvnet.hudson.test.recipes.LocalData;

/**
* Utility to display the log of a build in real time.
* Unlike {@link BuildWatcher}, this works well with both {@link RealJenkinsRule} and {@link LocalData}.
* Use in a {@code try}-with-resources block, typically calling {@link #waitForCompletion} at the end.
*/
@SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "irrelevant")
public final class TailLog implements AutoCloseable {

private final Semaphore finished = new Semaphore(0);
private final Tailer tailer;
private final PrefixedOutputStream.Builder prefixedOutputStreamBuilder = PrefixedOutputStream.builder();

/**
* Watch a build already loaded in the current JVM.
*/
public TailLog(Run<?, ?> b) {
this(b.getRootDir(), b.getParent().getFullName(), b.getNumber());
}

/**
* Watch a build expected to be loaded in the current JVM.
* <em>Note</em>: this constructor will not work for a branch project (child of {@code MultiBranchProject}).
* @param job a {@link Job#getFullName}
*/
public TailLog(JenkinsRule jr, String job, int number) {
this(runRootDir(jr.jenkins.getRootDir(), job, number), job, number);
}

/**
* Watch a build expected to be loaded in a controller JVM.
* <em>Note</em>: this constructor will not work for a branch project (child of {@code MultiBranchProject}).
* @param job a {@link Job#getFullName}
*/
public TailLog(RealJenkinsRule rjr, String job, int number) {
this(runRootDir(rjr.getHome(), job, number), job, number);
}

private static File runRootDir(File home, String job, int number) {
// For MultiBranchProject the last segment would be "branches" not "jobs":
return new File(home, "jobs/" + job.replace("/", "/jobs/") + "/builds/" + number);
}

/**
* Applies ANSI coloration to log lines produced by this instance.
* Ignored when on CI.
* Does not work when the build has already started by the time this method is called.
*/
public TailLog withColor(PrefixedOutputStream.Color color) {
prefixedOutputStreamBuilder.withColor(color);
return this;
}

/**
* Watch a build expected to run at a specific file location.
* @param buildDirectory expected {@link Run#getRootDir}
* @param job a {@link Job#getFullName}
*/
public TailLog(File buildDirectory, String job, int number) {
tailer = Tailer.create(new File(buildDirectory, "log"), new TailerListenerAdapter() {
PrintStream ps;
@Override
public void handle(String line) {
if (ps == null) {
ps = new PrintStream(new PlainTextConsoleOutputStream(prefixedOutputStreamBuilder.withName(job + '#' + number).build(System.out)), true, StandardCharsets.UTF_8);
}
ps.append(DeltaSupportLogFormatter.elapsedTime());
ps.print(' ');
ps.print(line);
ps.println();
if (line.startsWith("Finished: ")) {
finished.release();
}
}
});
}

public void waitForCompletion() throws InterruptedException {
finished.acquire();
}

@Override
public void close() {
tailer.stop();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jenkinsci/mock-slave-plugin@43ec9c7 from jenkinsci/mock-slave-plugin#165 might also need to wait for the tailer thread to exit, which I guess would mean manually starting the thread (not using create) and calling join.

}

}
3 changes: 2 additions & 1 deletion src/main/java/org/jvnet/hudson/test/recipes/LocalData.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.HudsonHomeLoader.Local;
import org.jvnet.hudson.test.JenkinsRecipe;

import org.jvnet.hudson.test.TailLog;

import java.lang.annotation.Documented;
import java.lang.annotation.Target;
Expand Down Expand Up @@ -77,6 +77,7 @@
* The choice of zip and directory depends on the nature of the test data, as well as the size of it.
*
* @author Kohsuke Kawaguchi
* @see TailLog
*/
@Documented
@Recipe(LocalData.RunnerImpl.class)
Expand Down
7 changes: 5 additions & 2 deletions src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,13 @@ private static void _error(JenkinsRule r) throws Throwable {
}

@Test public void agentBuild() throws Throwable {
rr.then(RealJenkinsRuleTest::_agentBuild);
try (TailLog tailLog = new TailLog(rr, "p", 1).withColor(PrefixedOutputStream.Color.MAGENTA)) {
rr.then(RealJenkinsRuleTest::_agentBuild);
tailLog.waitForCompletion();
}
}
private static void _agentBuild(JenkinsRule r) throws Throwable {
FreeStyleProject p = r.createFreeStyleProject();
FreeStyleProject p = r.createFreeStyleProject("p");
AtomicReference<Boolean> ran = new AtomicReference<>(false);
p.getBuildersList().add(new TestBuilder() {
@Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
Expand Down