package net.sourceforge.cruisecontrol;
import java.util.Date;
import java.util.Map;
import java.io.File;
import junit.framework.TestCase;
import net.sourceforge.cruisecontrol.labelincrementers.EmptyLabelIncrementer;
import net.sourceforge.cruisecontrol.util.threadpool.ThreadQueue;
import net.sourceforge.cruisecontrol.testutil.TestUtil;
import org.jdom.Element;
public class ForcingBuildShouldNotLockProjectInQueuedStateTest extends TestCase {
private BuildQueue buildQueue;
private int runCount;
private static final String FORCING_BUILD_TEST_PROJECT_NAME = "forcing-build-test-project";
private final TestUtil.FilesToDelete filesToDelete = new TestUtil.FilesToDelete();
protected void setUp() throws Exception {
super.setUp();
runCount = 0;
// build create a FORCING_BUILD_TEST_PROJECT_NAME.ser file, so delete it
filesToDelete.add(new File(FORCING_BUILD_TEST_PROJECT_NAME + ".ser"));
}
protected void tearDown() throws Exception {
filesToDelete.delete();
}
public void testRepeatForcingBuildShouldNotLockProjectInQueuedState() throws Exception {
for (int i = 0; i < 10; i++) {
// may need to be run multiple times since the good thread may just be lucky and win...
runCount = i;
testForcingBuildShouldNotLockProjectInQueuedState();
}
}
/**
* This test tries to expose race condition.
* It's very time-dependent and requires running multiple times to be 100% sure.
* @throws Exception if test fails
*/
public void testForcingBuildShouldNotLockProjectInQueuedState() throws Exception {
final String msgRunCount = "(" + getName() + " runCount: " + runCount + ") ";
buildQueue = new BuildQueue();
buildQueue.start();
Thread.sleep(500);
try {
final Project forcedProject = simulateForcingBuild();
if (ProjectState.QUEUED == forcedProject.getState()) {
fail(msgRunCount + "Project must NOT be in queued state. "
+ "This means project is lost and will remain in this state until CC restart.");
}
final String failure = msgRunCount + "Totally unexpected project state. "
+ "It has to be either QUEUED (which means it should be detected by previous assertion) "
+ "or WAITING (which means that we are lucky and race condition is not exposed this time";
assertEquals(failure, ProjectState.WAITING.getName(), forcedProject.getState().getName());
} finally {
buildQueue.stop();
}
}
Project simulateForcingBuild() throws Exception {
final MockProject projectToForce = new MockProject();
projectToForce.setBuildQueue(buildQueue);
final MockScheduleThatAllowsControllingBuilder schedule = new MockScheduleThatAllowsControllingBuilder();
final ProjectConfig projectConfig = new MockProjectConfig(projectToForce, schedule);
projectConfig.setName(FORCING_BUILD_TEST_PROJECT_NAME);
projectConfig.configureProject();
schedule.setBuilderToBuildForever();
projectToForce.start();
// @todo Sleep time here may not be enough...maybe we need a state change listener?
Thread.sleep(250);
assertEquals(ProjectState.BUILDING.getName(), projectToForce.getState().getName());
// force during build
projectToForce.setBuildForced(true);
// finish current build so that forced build can act
schedule.setBuilderToFinishBuildingImmediately();
// give the build some time to finish
Thread.sleep(300);
// sanity checks
assertTrue(buildQueue.isWaiting());
int count = 0;
while ((ThreadQueue.getIdleTaskNames().size() != 0) && (count < 20)) {
count++;
Thread.sleep(100 * count);
}
assertTrue("ThreadQueue.getIdleTaskNames() should be empty", ThreadQueue.getIdleTaskNames().size() == 0);
count = 0;
while ((ThreadQueue.getBusyTaskNames().size() != 0) && (count < 20)) {
count++;
Thread.sleep(100 * count);
}
assertTrue("ThreadQueue.getBusyTaskNames() should be empty", ThreadQueue.getBusyTaskNames().size() == 0);
return projectToForce;
}
private final class MockProjectConfig extends ProjectConfig {
private final Project project;
private final Schedule schedule;
public MockProjectConfig(final Project project, final Schedule schedule) {
this.project = project;
this.schedule = schedule;
}
public void add(Listeners listeners) {
}
public LabelIncrementer getLabelIncrementer() {
return new EmptyLabelIncrementer();
}
public Schedule getSchedule() {
return schedule;
}
public Log getLog() {
return new Log() {
public void writeLogFile(Date now) throws CruiseControlException {
}
};
}
Project readProject(final String projectName) {
return project;
}
}
private final class MockScheduleThatAllowsControllingBuilder extends MockSchedule {
private boolean ihibitBuild = true;
private boolean scheduledBuildFired = false;
public long getTimeToNextBuild(final Date date, final long interval) {
// build only once
if (!scheduledBuildFired) {
scheduledBuildFired = true;
return 0;
}
return 999999999;
}
public synchronized Element build(final int buildNumber, final Date lastBuild, final Date now,
final Map<String, String> propMap, final String buildTarget,
final Progress progress)
throws CruiseControlException {
while (ihibitBuild) {
try {
this.wait();
} catch (InterruptedException e) {
fail("Exception: " + e.getMessage());
}
}
return super.build(buildNumber, lastBuild, now, propMap, buildTarget, progress);
}
public synchronized void setBuilderToBuildForever() {
this.ihibitBuild = true;
this.notify();
}
public synchronized void setBuilderToFinishBuildingImmediately() {
this.ihibitBuild = false;
this.notify();
}
}
private static class MockProject extends Project {
Element getModifications(boolean buildWasForced) {
return new Element("modifications");
}
}
}