Skip to content

Commit

Permalink
Changing the implementation of UiThreadPosterTestDouble such that it …
Browse files Browse the repository at this point in the history
…will not allow any posted Runnables to run before call to join(). In addition, the new implementation allows for tests with "nested" posting of Runnables to UiThreadPosterTestDouble (i.e. when one posted Runnable posts additional Runnables).

This change is required in order to prevent some code to be executed while the tests that use UiThreadPosterTestDouble are still being set up, thus leading to inconsistent results. Now, it's guaranteed that no side effects of Runnables posted UiThreadPosterTestDouble will be visible until a call to join().
  • Loading branch information
techyourchance committed Jun 12, 2019
1 parent 5dfc9e4 commit a8a79ae
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,18 @@

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
* Test double of {@link UiThreadPoster} that can be used in tests in order to establish
* a happens-before relationship between any {@link Runnable} sent to execution and subsequent
* test assertions.
* Instead of using Android's UI (aka main) thread, this implementation sends each {@link Runnable}
* to a new background thread. Only one background thread is allowed to run at a time, thus
* simulating a serial execution of {@link Runnable}s.
* Instead of using Android's UI (aka main) thread, this implementation runs all {@link Runnable}s
* on a single background thread in order, thus simulating serial execution on UI thread.
*/
/* pp */ class UiThreadPosterTestDouble extends UiThreadPoster {

private final Object MONITOR = new Object();

private final Queue<Thread> mThreads = new LinkedList<>();
private final Queue<Runnable> mRunnables = new ConcurrentLinkedQueue<>();

@Override
protected Handler getMainHandler() {
Expand All @@ -30,47 +28,31 @@ protected Handler getMainHandler() {

@Override
public void post(final Runnable runnable) {
synchronized (MONITOR) {
Thread worker = new Thread(new Runnable() {
@Override
public void run() {
// make sure all previous threads finished
UiThreadPosterTestDouble.this.join();
runnable.run();
}
});
mThreads.add(worker);
worker.start();
}
mRunnables.add(runnable);
}

/**
* Call to this method will block until all {@link Runnable}s posted to this "test double"
* BEFORE THE MOMENT OF A CALL will be completed.<br>
* Execute all {@link Runnable}s posted to this "test double". The caller will block until the operation completes<br>
* Call to this method allows to establish a happens-before relationship between the previously
* posted {@link Runnable}s and subsequent code.
*/
public void join() {
Queue<Thread> threadsCopy;
synchronized (MONITOR) {
threadsCopy = new LinkedList<>(mThreads);
}

Thread thread;
while ((thread = threadsCopy.poll()) != null) {
try {

// due to the way post(Runnable) is being implemented, this method will be called
// by threads that were added to the queue; in this case, we need to join only on
// threads that precede the calling thread in the queue
if (thread.getId() == Thread.currentThread().getId()) {
break;
} else {
thread.join();
final Thread fakeUiThread = new Thread() {
@Override
public void run() {
Runnable runnable;
while ((runnable = mRunnables.poll()) != null) {
runnable.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};

fakeUiThread.start();

try {
fakeUiThread.join();
} catch (InterruptedException e) {
throw new RuntimeException("interrupted");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,7 @@

public class UiThreadPosterTestDoubleTest {

private static final int TEST_TIMEOUT_MS = 1000;
private static final int TEST_DELAY_MS = TEST_TIMEOUT_MS / 10;

/**
* This class will be used in order to check side effects in tests
*/
private class Appender {

private String mString = "";

private void append(String string) {
mString += string;
}

private String getString() {
return mString;
}
}
private static final int TEST_DELAY_MS = 10;

private UiThreadPosterTestDouble SUT;

Expand All @@ -43,11 +26,6 @@ public void executeThenJoin_singleRunnable_sideEffectNotVisibleBeforeJoin() thro
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2 * TEST_DELAY_MS);
} catch (InterruptedException e) {
e.printStackTrace();
}
appender.append("a");
}
};
Expand All @@ -66,18 +44,12 @@ public void executeThenJoin_singleRunnable_sideEffectsVisibleAfterJoin() throws
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2 * TEST_DELAY_MS);
} catch (InterruptedException e) {
e.printStackTrace();
}
appender.append("a");
}
};
// Act
SUT.post(runnable);
// Assert
Thread.sleep(TEST_DELAY_MS);
SUT.join();
assertThat(appender.getString(), is("a"));
}
Expand All @@ -90,33 +62,18 @@ public void executeThenJoin_multipleRunnables_sideEffectsNotVisibleBeforeJoin()
Runnable runnable1 = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2 * TEST_DELAY_MS);
} catch (InterruptedException e) {
e.printStackTrace();
}
appender.append("a");
}
};
Runnable runnable2 = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2 * TEST_DELAY_MS);
} catch (InterruptedException e) {
e.printStackTrace();
}
appender.append("b");
}
};
Runnable runnable3 = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2 * TEST_DELAY_MS);
} catch (InterruptedException e) {
e.printStackTrace();
}
appender.append("c");
}
};
Expand All @@ -129,41 +86,25 @@ public void run() {
assertThat(appender.getString(), is(""));
}


@Test
public void executeThenJoin_multipleRunnables_sideEffectsVisibleAfterJoinInOrder() throws Exception {
// Arrange
final Appender appender = new Appender();
Runnable runnable1 = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2 * TEST_DELAY_MS);
} catch (InterruptedException e) {
e.printStackTrace();
}
appender.append("a");
}
};
Runnable runnable2 = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2 * TEST_DELAY_MS);
} catch (InterruptedException e) {
e.printStackTrace();
}
appender.append("b");
}
};
Runnable runnable3 = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2 * TEST_DELAY_MS);
} catch (InterruptedException e) {
e.printStackTrace();
}
appender.append("c");
}
};
Expand All @@ -172,8 +113,55 @@ public void run() {
SUT.post(runnable2);
SUT.post(runnable3);
// Assert
Thread.sleep(TEST_DELAY_MS);
SUT.join();
assertThat(appender.getString(), is("abc"));
}


@Test
public void executeThenJoin_multipleNestedRunnables_sideEffectsVisibleAfterJoinInReversedOrder() throws Exception {
// Arrange
final Appender appender = new Appender();
final Runnable runnable1 = new Runnable() {
@Override
public void run() {
appender.append("a");
}
};
final Runnable runnable2 = new Runnable() {
@Override
public void run() {
SUT.post(runnable1);
appender.append("b");
}
};
final Runnable runnable3 = new Runnable() {
@Override
public void run() {
SUT.post(runnable2);
appender.append("c");
}
};
// Act
SUT.post(runnable3);
// Assert
SUT.join();
assertThat(appender.getString(), is("cba"));
}

/**
* This class will be used in order to check side effects in tests
*/
private class Appender {

private String mString = "";

private synchronized void append(String string) {
mString += string;
}

private synchronized String getString() {
return mString;
}
}
}

0 comments on commit a8a79ae

Please sign in to comment.