Skip to content
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.
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 @@ -22,6 +22,7 @@
import static org.eclipse.core.tests.resources.ResourceTestUtil.updateProjectDescription;
import static org.eclipse.core.tests.resources.ResourceTestUtil.waitForBuild;
import static org.eclipse.core.tests.resources.TestUtil.waitForCondition;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.ArrayList;
Expand Down Expand Up @@ -103,10 +104,18 @@ public void setUp() throws Exception {

@AfterEach
public void tearDown() throws Exception {
// Cleanup workspace first to ensure that auto-build is not started on projects
waitForBuild();
getWorkspace().getRoot().delete(true, true, createTestMonitor());
TimerBuilder.abortCurrentBuilds();
boolean buildsStillRunning;
try {
// Cleanup workspace first to ensure that auto-build is not started on projects
waitForBuild();
getWorkspace().getRoot().delete(true, true, createTestMonitor());
} finally {
TimerBuilder.abortCurrentBuilds();
// Reset unconditionally, so that a build leaked by this test does not affect
// any subsequent test
buildsStillRunning = TimerBuilder.reset();
}
assertFalse(buildsStillRunning, "builds are still running at the end of the test");
}

private void setWorkspaceMaxNumberOfConcurrentBuilds(int maximumNumberOfConcurrentBuilds) throws CoreException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@
*******************************************************************************/
package org.eclipse.core.tests.internal.builders;

import static org.eclipse.core.tests.resources.TestUtil.waitForCondition;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.BooleanSupplier;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
Expand All @@ -33,28 +32,39 @@ public class TimerBuilder extends IncrementalProjectBuilder {
public static final String DURATION_ARG = "duration";
public static final String RULE_TYPE_ARG = "ruleType";

private static final int SHUTDOWN_TIMEOUT_IN_MILLIS = 60_000;
private static final int SHUTDOWN_TIMEOUT_IN_MILLIS = 10_000;

private static BuildExecutionState executionState = new BuildExecutionState(-1);
private static volatile BuildExecutionState executionState = new BuildExecutionState(-1);

/**
* Tracks the builds of a single test. All state is guarded by the instance
* monitor, which is also used to signal running builds to abort.
*/
private static class BuildExecutionState {
private final int expectedNumberOfBuilds;
private final List<BuildEvent> events = Collections.synchronizedList(new ArrayList<>());
private volatile boolean shallAbort = false;
private volatile int maxSimultaneousBuilds = 0;
private volatile int currentlyRunningBuilds = 0;
private final List<BuildEvent> events = new ArrayList<>();
private boolean shallAbort;
private int maxSimultaneousBuilds;
private int currentlyRunningBuilds;

private BuildExecutionState(int expectedNumberOfBuilds) {
this.expectedNumberOfBuilds = expectedNumberOfBuilds;
}

private synchronized boolean isExecuting() {
return getProjectBuilds(BuildEventType.FINISH).size() < executionState.expectedNumberOfBuilds;
private synchronized boolean hasRunningBuilds() {
return currentlyRunningBuilds > 0;
}

private synchronized int getMaxSimultaneousBuilds() {
return maxSimultaneousBuilds;
}

private synchronized List<BuildEvent> getEvents() {
return new ArrayList<>(events);
}

private synchronized List<IProject> getProjectBuilds(BuildEventType eventType) {
return events.stream().filter(event -> event.eventType == eventType)
.map(event -> event.project).toList();
return events.stream().filter(event -> event.eventType == eventType).map(event -> event.project).toList();
}

private synchronized void startedExecutingProject(IProject project) {
Expand All @@ -63,22 +73,40 @@ private synchronized void startedExecutingProject(IProject project) {
events.add(new BuildEvent(project, BuildEventType.START));
}

private synchronized void endedExcecutingProject(IProject project) {
private synchronized void endedExecutingProject(IProject project) {
currentlyRunningBuilds--;
events.add(new BuildEvent(project, BuildEventType.FINISH));
notifyAll();
}

/**
* Blocks until the builds shall abort or the given duration has elapsed.
*/
private synchronized void awaitBuildDuration(long durationInMillis) {
awaitWithTimeout(durationInMillis, () -> shallAbort);
}

/**
* Requests all running builds to abort and waits for their termination.
*/
private synchronized void abortAndWaitForAllBuilds() {
shallAbort = true;
long durationInMillis = 0;
long waitingStartTimeInMillis = System.currentTimeMillis();
while (isExecuting() && durationInMillis < SHUTDOWN_TIMEOUT_IN_MILLIS) {
notifyAll();
awaitWithTimeout(SHUTDOWN_TIMEOUT_IN_MILLIS, () -> currentlyRunningBuilds == 0);
}

private synchronized void awaitWithTimeout(long timeoutInMillis, BooleanSupplier condition) {
long deadlineInNanos = System.nanoTime() + timeoutInMillis * 1_000_000L;
long remainingMillis = timeoutInMillis;
while (!condition.getAsBoolean() && remainingMillis > 0) {
try {
wait(SHUTDOWN_TIMEOUT_IN_MILLIS);
wait(remainingMillis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
durationInMillis = System.currentTimeMillis() - waitingStartTimeInMillis;
// Round up, so that the requested duration is never undercut
remainingMillis = (deadlineInNanos - System.nanoTime() + 999_999) / 1_000_000L;
}
}

Expand Down Expand Up @@ -138,15 +166,18 @@ public boolean contains(ISchedulingRule rule) {

@Override
protected IProject[] build(int kind, Map<String, String> args, IProgressMonitor monitor) throws CoreException {
assertNotEquals(-1, executionState.expectedNumberOfBuilds, "no expected number of builds has been set");
executionState.startedExecutingProject(getProject());
// Bind to the state of the current test, so that a leaked build does not
// contribute events to a subsequent test
BuildExecutionState state = executionState;
assertNotEquals(-1, state.expectedNumberOfBuilds, "no expected number of builds has been set");
state.startedExecutingProject(getProject());
try {
int durationInMillis = Integer.parseInt(args.get(DURATION_ARG));
waitForCondition(() -> executionState.shallAbort, durationInMillis);
state.awaitBuildDuration(Integer.parseInt(args.get(DURATION_ARG)));
} catch (Exception ex) {
ex.printStackTrace();
} finally {
state.endedExecutingProject(getProject());
}
executionState.endedExcecutingProject(getProject());
return new IProject[] {getProject()};
}

Expand Down Expand Up @@ -177,20 +208,36 @@ public static List<IProject> getFinishedProjectBuilds() {
}

public static int getMaximumNumberOfSimultaneousBuilds() {
return executionState.maxSimultaneousBuilds;
return executionState.getMaxSimultaneousBuilds();
}

public static Iterable<BuildEvent> getBuildEvents() {
return new ArrayList<>(executionState.events);
return executionState.getEvents();
}

/**
* Resets the tracked execution states. Asserts that no execution is still
* running.
* Resets the tracked execution states and defines the number of builds expected
* to be executed. Asserts that no execution is still running.
*/
public static void setExpectedNumberOfBuilds(int expectedNumberOfBuilds) {
assertFalse(executionState.isExecuting(), "builds are still running while resetting TimerBuilder");
assertFalse(replaceExecutionState(expectedNumberOfBuilds),
"builds are still running while resetting TimerBuilder");
}

/**
* Resets the tracked execution states and returns whether builds were still
* running.
*/
public static boolean reset() {
return replaceExecutionState(-1);
}

private static boolean replaceExecutionState(int expectedNumberOfBuilds) {
BuildExecutionState previousState = executionState;
// Replace the state before checking it, so that a leaked build does not make
// all subsequent tests fail as well
executionState = new BuildExecutionState(expectedNumberOfBuilds);
return previousState.hasRunningBuilds();
}

public static void abortCurrentBuilds() {
Expand Down
Loading