The document discusses various techniques for unit testing concurrent code, including using JUnit, TestNG, and a custom ThreadWeaver framework to test for threading issues in a FlawedList implementation. It also covers using jcstress for concurrency stress testing and notes that jcstress results may vary between runs and depend on processor architecture memory access semantics.
2. public class FlawedList<T> extends ArrayList<T> {
public boolean putIfAbsent(T object) {
boolean absent = !super.contains(object);
if (absent) {
super.add(object);
}
return absent;
}
}
Implementing a (flawed) unique list
3. @Test
public void testPutIfAbsent() {
FlawedList<String> list = new FlawedList<String>();
list.putIfAbsent("foo");
list.putIfAbsent("foo");
assertThat(list.size(), is(1));
}
JUnit
4. FlawedList<String> list = new FlawedList<String>();
@Test(threadPoolSize = 5, invocationCount = 20)
public void testList() {
list.putIfAbsent("foo");
assertThat(list.size(), is(1));
}
TestNG
5. public class FlawedList<T> extends ArrayList<T> {
public boolean putIfAbsent(T object) {
boolean absent = !super.contains(object);
if (absent) {
super.add(object);
}
return absent;
}
}
FlawedList<String> list = new FlawedList<String>();
@Test(threadPoolSize = 5, invocationCount = 20)
public void testList() {
list.putIfAbsent("foo");
assertThat(list.size(), is(1));
}
TestNG with breakpoint
6. public boolean putIfAbsent(T object) {
boolean absent = !super.contains(object);
if (absent) {
super.add(object);
}
return absent;
}
Testing with break points: first weaving
FlawedList["foo", "foo"]FlawedList["foo"]FlawedList[]
Testing with break points: second weaving
Thread 1
Thread 2
Legend: absent
true
falsetrue
ThreadWeaver
7. ThreadWeaver
public class WeavedFlawedListTest {
private FlawedList<String> list;
@ThreadedBefore public void before() {
list = new FlawedList<String>();
}
@ThreadedMain public void mainThread() {
list.putIfAbsent("foo");
}
@ThreadedSecondary public void secondThread() {
list.putIfAbsent("foo");
}
@ThreadedAfter public void after() {
assertEquals(1, list.size());
}
}
8. public class MyListTest {
@Test
public void testFlawedList() {
AnnotatedTestRunner runner = new AnnotatedTestRunner();
runner.runTests(getClass(), FlawedList.class);
}
// put method with @Threaded<...> annotations here
}
Seamless test-suite integration
9. @Test
public void testFlawedList() {
AnnotatedTestRunner runner = new AnnotatedTestRunner();
runner.runTests(getClass(), FlawedList.class);
}
Instrumented code:
public boolean putIfAbsent(T object) {
Framework.considerBreakpoint(Thread.currentThread(), 0);
boolean absent = !super.contains(object);
Framework.considerBreakpoint(Thread.currentThread(), 1);
if (absent) {
Framework.considerBreakpoint(Thread.currentThread(), 2);
super.add(object);
}
Framework.considerBreakpoint(Thread.currentThread(), 3);
return absent;
}
How does it work?
10. Beware of the Java memory model!
Framework.considerBreakpoint(Thread.currentThread(), 1);
Any breakpoint relies on synchronized code blocks which trigger a memory
synchronization and a “happens-before” relationship. The tested code is
therefore executed sequentially consistent.
@JCStressTest @State
@Outcome(id = "{0}", expect = Expect.ACCEPTABLE)
@Outcome(id = "{1}", expect = Expect.ACCEPTABLE)
@Outcome(id = "{2}", expect = Expect.FORBIDDEN)
class FlawedListStressTest {
final FlawedList list = new FlawedList();
@Actor void actor1() { list.putIfAbsent("foo"); }
@Actor void actor2() { list.putIfAbsent("foo"); }
@Arbiter void observe(IntResult1 result) {
result.r1 = list.size();
}
}
jcstress: concurrency stress testing concurrent code
11. Define two actors that execute potentially conflicting actions. Define legal,
suspicious and illegal outcomes of this interaction. jcstress executes these
actions repeatedly to maybe trigger concurrency bugs.
jcstress: concurrency stress testing concurrent code (2)
Not a unit test! Results might trigger, but it will likely vary from run to run.
Results might depend on the processor architecture. For example, ARM and
x86 have significantly different semantics on memory access reordering, for
Example ARM might reorder reads while x86 does never apply such reorderings.