Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ public int getOriginalNumber() {
}

public @CheckForNull Run<?, ?> getOriginal() {
// "run" may be null after construction before onAddedTo is called
// "run" may be null after deserialization before onLoad is called
if (run == null) {
return null;
}
return run.getParent().getBuildByNumber(originalNumber);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.jenkinsci.plugins.workflow.cps.replay;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;

import hudson.model.CauseAction;
import hudson.model.TaskListener;
import hudson.util.StreamTaskListener;
import hudson.util.XStream2;
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;

public class ReplayCauseTest {

@Rule
public JenkinsRule j = new JenkinsRule();

@Test
public void replayCausePrintIsSafeBeforeOnAddedTo() throws Exception {
WorkflowJob p = j.createProject(WorkflowJob.class, "p");
p.setDefinition(new CpsFlowDefinition("echo 'hello'", false));
WorkflowRun original = j.assertBuildStatusSuccess(p.scheduleBuild2(0));

// construct cause, don't call onAddedTo yet.
ReplayCause cause = new ReplayCause(original);
assertNull(cause.getRun());

// verify print() does not throw NPE.
TaskListener listener = StreamTaskListener.fromStdout();
cause.print(listener);
Comment on lines +28 to +34
Copy link
Member

Choose a reason for hiding this comment

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

This is meaningless; it does not correspond to a real scenario. A cause should not be used until it is attached to something.

Copy link
Author

Choose a reason for hiding this comment

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

I would not say its meaningless, because this is exactly what happens on our Jenkins instances about every two weeks when someone replays a job. So it IS a real scenario: there is a ReplayCause with a run that is null, and that's exactly what these tests check. I don't know (or can only speculate) why this is.
But since the constructor of ReplayCause does not set run, but only onLoad() and onAddedTo() do, there definitely is a window of opportunity in which run is null.
The same goes for the de-serialization case.
I didn't want to make this more complicated than necessary: There is a member that may be null when dereferenced, so a null-check seems to be in order. I can drop the tests, if you like.


// verify getOriginal() returns run after onAddedTo.
original.addAction(new CauseAction(cause));
assertNotNull(cause.getOriginal());
}

@Test
public void replayCausePrintIsSafeAfterDeserializeBeforeOnLoad() throws Exception {
WorkflowJob p = j.createProject(WorkflowJob.class, "p2");
p.setDefinition(new CpsFlowDefinition("echo 'hello'", false));
WorkflowRun original = j.assertBuildStatusSuccess(p.scheduleBuild2(0));

// Create a new run and attach the cause so run is initially set.
WorkflowRun newRun = j.assertBuildStatusSuccess(p.scheduleBuild2(0));
ReplayCause cause = new ReplayCause(original);
newRun.addAction(new CauseAction(cause));
assertNotNull(cause.getRun());

// serialize and deserialize the cause, which should clear the transient run field.
XStream2 xs = new XStream2();
String xml = xs.toXML(cause);

Object obj = xs.fromXML(xml);
if (obj instanceof ReplayCause) {
ReplayCause deserialized = (ReplayCause) obj;
assertNull(deserialized.getRun());
Comment on lines +53 to +60
Copy link
Member

Choose a reason for hiding this comment

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

Same for this.


// getOriginal() is safe to call and returns null until onLoad is called
TaskListener listener = StreamTaskListener.fromStdout();
deserialized.print(listener);

// simulate Jenkins calling onLoad to reattach the run (Run.onload() is protected)
deserialized.onLoad(newRun);
assertNotNull(deserialized.getOriginal());
}
}
}
Loading