Skip to content

Commit 5cf3eec

Browse files
committed
Added custom AblyTestSuite to run tests faster on local and CI
1 parent 264bd9e commit 5cf3eec

File tree

5 files changed

+185
-2
lines changed

5 files changed

+185
-2
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.ably.lib.test.common.toolkit;
2+
3+
import org.junit.runners.Suite;
4+
import org.junit.runners.model.InitializationError;
5+
import org.junit.runners.model.RunnerBuilder;
6+
7+
public class AblyTestSuite extends Suite {
8+
public AblyTestSuite(Class<?> klass, RunnerBuilder builder) throws InitializationError {
9+
super(klass, builder);
10+
this.setScheduler(new ParallelScheduler());
11+
}
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.ably.lib.test.common.toolkit;
2+
3+
public class CatThrower<T extends Throwable> {
4+
5+
/**
6+
* Will throw the given {@link Throwable}.
7+
*/
8+
public static void sneakyThrow(Throwable t) {
9+
new CatThrower<Error>().sneakyThrow2(t);
10+
}
11+
12+
private void sneakyThrow2(Throwable t) throws T {
13+
throw (T) t;
14+
}
15+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package io.ably.lib.test.common.toolkit;
2+
3+
4+
import java.io.PrintWriter;
5+
import java.io.StringWriter;
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
9+
public class MultiException extends RuntimeException {
10+
private static final long serialVersionUID = 1L;
11+
private static final String EXCEPTION_SEPARATOR = "\n\t______________________________________________________________________\n";
12+
private final List<Throwable> nested = new ArrayList<>();
13+
14+
public MultiException() {
15+
super("Multiple exceptions");
16+
}
17+
18+
public void add(Throwable throwable) {
19+
if (throwable != null) {
20+
synchronized (nested) {
21+
if (throwable instanceof MultiException) {
22+
MultiException other = (MultiException) throwable;
23+
synchronized (other.nested) {
24+
nested.addAll(other.nested);
25+
}
26+
} else {
27+
nested.add(throwable);
28+
}
29+
}
30+
}
31+
}
32+
33+
/**
34+
* If this <code>MultiException</code> is empty then no action is taken,
35+
* if it contains a single <code>Throwable</code> that is thrown,
36+
* otherwise this <code>MultiException</code> is thrown.
37+
*/
38+
public void throwIfNotEmpty() {
39+
synchronized (nested) {
40+
if (nested.isEmpty()) {
41+
// Do nothing
42+
} else if (nested.size() == 1) {
43+
Throwable t = nested.get(0);
44+
CatThrower.sneakyThrow(t);
45+
} else {
46+
throw this;
47+
}
48+
}
49+
}
50+
51+
@Override
52+
public String getMessage() {
53+
synchronized (nested) {
54+
if (nested.isEmpty()) {
55+
return "<no nested exceptions>";
56+
} else {
57+
StringBuilder sb = new StringBuilder();
58+
int n = nested.size();
59+
sb.append(n).append(n == 1 ? " nested exception:" : " nested exceptions:");
60+
for (Throwable t : nested) {
61+
sb.append(EXCEPTION_SEPARATOR).append("\n\t");
62+
StringWriter sw = new StringWriter();
63+
t.printStackTrace(new PrintWriter(sw));
64+
sb.append(sw.toString().replace("\n", "\n\t").trim());
65+
}
66+
sb.append(EXCEPTION_SEPARATOR);
67+
return sb.toString();
68+
}
69+
}
70+
}
71+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package io.ably.lib.test.common.toolkit;
2+
3+
import org.junit.runners.model.RunnerScheduler;
4+
5+
import java.util.Deque;
6+
import java.util.LinkedList;
7+
import java.util.concurrent.ForkJoinPool;
8+
import java.util.concurrent.ForkJoinTask;
9+
import java.util.concurrent.ForkJoinWorkerThread;
10+
11+
import static java.util.concurrent.ForkJoinTask.inForkJoinPool;
12+
13+
/**
14+
* ParallelScheduler runs each test class with threadpool of size equal to CPU cores.
15+
* Default value can be changed by setting System.setProperty("maxParallelTestThreads", "4")
16+
*/
17+
class ParallelScheduler implements RunnerScheduler {
18+
19+
static ForkJoinPool forkJoinPool = setUpForkJoinPool();
20+
21+
static ForkJoinPool setUpForkJoinPool() {
22+
int numThreads;
23+
try {
24+
String configuredNumThreads = System.getProperty("maxParallelTestThreads");
25+
numThreads = Math.max(2, Integer.parseInt(configuredNumThreads));
26+
} catch (Exception ignored) {
27+
Runtime runtime = Runtime.getRuntime();
28+
numThreads = Math.max(2, runtime.availableProcessors());
29+
}
30+
ForkJoinPool.ForkJoinWorkerThreadFactory threadFactory = pool -> {
31+
if (pool.getPoolSize() >= pool.getParallelism()) {
32+
return null;
33+
} else {
34+
ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);
35+
thread.setName("JUnit-" + thread.getName());
36+
return thread;
37+
}
38+
};
39+
return new ForkJoinPool(numThreads, threadFactory, null, false);
40+
}
41+
42+
private final Deque<ForkJoinTask<?>> _asyncTasks = new LinkedList<>();
43+
private Runnable _lastScheduledChild;
44+
45+
@Override
46+
public void schedule(Runnable childStatement) {
47+
if (_lastScheduledChild != null) {
48+
// Execute previously scheduled child asynchronously ...
49+
if (inForkJoinPool()) {
50+
_asyncTasks.addFirst(ForkJoinTask.adapt(_lastScheduledChild).fork());
51+
} else {
52+
_asyncTasks.addFirst(forkJoinPool.submit(_lastScheduledChild));
53+
}
54+
}
55+
// Note: We don't schedule the childStatement immediately here,
56+
// but remember it, so that we can synchronously execute the
57+
// last scheduled child in the finished method() -- this way,
58+
// the current thread does not immediately call join() in the
59+
// finished() method, which might block it ...
60+
_lastScheduledChild = childStatement;
61+
}
62+
63+
@Override
64+
public void finished() {
65+
MultiException me = new MultiException();
66+
if (_lastScheduledChild != null) {
67+
if (inForkJoinPool()) {
68+
// Execute the last scheduled child in the current thread ...
69+
try { _lastScheduledChild.run(); } catch (Throwable t) { me.add(t); }
70+
} else {
71+
// Submit the last scheduled child to the ForkJoinPool too,
72+
// because all tests should run in the worker threads ...
73+
_asyncTasks.addFirst(forkJoinPool.submit(_lastScheduledChild));
74+
}
75+
// Make sure all asynchronously executed children are done, before we return ...
76+
for (ForkJoinTask<?> task : _asyncTasks) {
77+
// Note: Because we have added all tasks via addFirst into _asyncTasks,
78+
// task.join() is able to steal tasks from other worker threads, if there
79+
// are tasks, which have not been started yet.
80+
try { task.join(); } catch (Throwable t) { me.add(t); }
81+
}
82+
me.throwIfNotEmpty();
83+
}
84+
}
85+
}

lib/src/test/java/io/ably/lib/test/realtime/RealtimeSuite.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package io.ably.lib.test.realtime;
22

3-
import com.googlecode.junittoolbox.ParallelSuite;
3+
import io.ably.lib.test.common.toolkit.AblyTestSuite;
44
import org.junit.AfterClass;
55
import org.junit.BeforeClass;
66
import org.junit.runner.JUnitCore;
@@ -11,7 +11,7 @@
1111

1212
import io.ably.lib.test.common.Setup;
1313

14-
@RunWith(ParallelSuite.class)
14+
@RunWith(AblyTestSuite.class)
1515
@SuiteClasses({
1616
ConnectionManagerTest.class,
1717
RealtimeHttpHeaderTest.class,

0 commit comments

Comments
 (0)