Skip to content

Commit d010cef

Browse files
committed
Context escape detection.
During creation of each of DottyTests context is stolen from test and a WeakReference for it is created. After running all tests references are examined to detect if any of them has leaked.
1 parent 6bc463d commit d010cef

File tree

4 files changed

+160
-5
lines changed

4 files changed

+160
-5
lines changed

project/Build.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ object DottyBuild extends Build {
1515
resourceDirectory in Compile := baseDirectory.value / "resources",
1616
unmanagedSourceDirectories in Compile := Seq((scalaSource in Compile).value),
1717
unmanagedSourceDirectories in Test := Seq((scalaSource in Test).value),
18-
18+
1919
// include sources in eclipse (downloads source code for all dependencies)
2020
//http://stackoverflow.com/questions/10472840/how-to-attach-sources-to-sbt-managed-dependencies-in-scala-ide#answer-11683728
2121
com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys.withSource := true,
@@ -28,17 +28,19 @@ object DottyBuild extends Build {
2828
"org.scala-lang.modules" %% "scala-xml" % "1.0.1"),
2929

3030
// get junit onboard
31-
libraryDependencies += "com.novocode" % "junit-interface" % "0.9" % "test",
31+
libraryDependencies += "com.novocode" % "junit-interface" % "0.11-RC1" % "test",
3232

3333
// scalac options
3434
scalacOptions in Global ++= Seq("-feature", "-deprecation", "-language:_"),
3535

3636
// enable verbose exception messages for JUnit
37-
testOptions += Tests.Argument(TestFrameworks.JUnit, "-a", "-v"),
37+
testOptions += Tests.Argument(TestFrameworks.JUnit, "-a", "-v", "--run-listener=test.ContextEscapeDetector"),
3838
// Adjust classpath for running dotty
3939
mainClass in (Compile, run) := Some("dotty.tools.dotc.Main"),
4040
fork in run := true,
4141
fork in Test := true,
42+
parallelExecution in Test := false,
43+
4244
// http://grokbase.com/t/gg/simple-build-tool/135ke5y90p/sbt-setting-jvm-boot-paramaters-for-scala
4345
javaOptions <++= (managedClasspath in Runtime, packageBin in Compile) map { (attList, bin) =>
4446
// put the Scala {library, reflect, compiler} in the classpath

test/test/ContextEscapeDetection.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package test;
2+
3+
import dotty.tools.dotc.core.Contexts;
4+
import org.junit.*;
5+
6+
import java.lang.ref.WeakReference;
7+
import java.util.LinkedList;
8+
import java.util.List;
9+
10+
11+
public abstract class ContextEscapeDetection {
12+
public static class TestContext{
13+
public TestContext(WeakReference<Contexts.Context> context, String testName) {
14+
this.context = context;
15+
this.testName = testName;
16+
}
17+
18+
public final WeakReference<Contexts.Context> context;
19+
public final String testName;
20+
21+
}
22+
public static final List<TestContext> contexts = new LinkedList<>();
23+
24+
public abstract Contexts.Context getCtx();
25+
26+
public abstract void clearCtx();
27+
28+
@Before
29+
public synchronized void stealContext() {
30+
contexts.add(new TestContext(new WeakReference<>(this.getCtx()), this.getClass().getName()));
31+
}
32+
33+
@After
34+
public synchronized void clearContext() {
35+
this.clearCtx();
36+
}
37+
38+
39+
}
40+

test/test/ContextEscapeDetector.java

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package test;
2+
3+
import org.junit.runner.Description;
4+
import org.junit.runner.Result;
5+
import org.junit.runner.notification.RunListener;
6+
import org.junit.Assert;
7+
8+
import java.lang.ref.WeakReference;
9+
10+
public class ContextEscapeDetector extends RunListener {
11+
12+
//context can be captured by objects, eg NoDenotation
13+
public static final int CONTEXTS_ALLOWED = 1;
14+
15+
@Override
16+
public void testRunFinished(Result result) throws Exception {
17+
if (contextsAlive() > CONTEXTS_ALLOWED) {
18+
forceGCHeuristic0();
19+
if (contextsAlive() > CONTEXTS_ALLOWED) {
20+
forceGCHeuristic1();
21+
if (contextsAlive() > CONTEXTS_ALLOWED) {
22+
forceGCHeuristic2();
23+
forceGCHeuristic1();
24+
int contextAlive = contextsAlive();
25+
if (contextAlive > CONTEXTS_ALLOWED) {
26+
StringBuilder names = new StringBuilder();
27+
for (ContextEscapeDetection.TestContext ref : ContextEscapeDetection.contexts) {
28+
if (ref.context.get() != null) names.append(ref.testName).append(' ');
29+
}
30+
Assert.fail("Multiple contexts survived test suite: " + names.toString());
31+
}
32+
}
33+
}
34+
}
35+
super.testRunFinished(result);
36+
}
37+
38+
private static synchronized int contextsAlive() {
39+
int count = 0;
40+
for (ContextEscapeDetection.TestContext ref : ContextEscapeDetection.contexts) {
41+
if (ref.context.get() != null) count++;
42+
}
43+
return count;
44+
}
45+
46+
private static volatile Object o = null;
47+
48+
private static synchronized void forceGCHeuristic0() {
49+
System.gc();
50+
Runtime.getRuntime().gc();
51+
System.gc();
52+
Runtime.getRuntime().gc();
53+
System.gc();
54+
Runtime.getRuntime().gc();
55+
System.gc();
56+
Runtime.getRuntime().gc();
57+
System.gc();
58+
}
59+
60+
private static synchronized void forceGCHeuristic1() {
61+
Object obj = new Object();
62+
WeakReference ref = new WeakReference<Object>(obj);
63+
obj = null;
64+
while (ref.get() != null) {
65+
System.gc();
66+
}
67+
}
68+
69+
private static synchronized void forceGCHeuristic2() {
70+
try {
71+
Object[] arr = new Object[1024]; // upto 8 GB
72+
WeakReference ref = new WeakReference<Object>(arr);
73+
o = arr; // make sure array isn't optimized away
74+
75+
Runtime runtime = Runtime.getRuntime();
76+
// allocate memory until no more that 64MB is left
77+
for (int i = 0; i < 1024 &&
78+
runtime.totalMemory() != runtime.maxMemory() ||
79+
runtime.freeMemory() < 1024 * 1024 * 64; i++) {
80+
int[] data = new int[1024 * 1024]; // 8MB
81+
for (int j = 0; j < 1024 * 1024; j++) {
82+
data[j] = j; // force actual pages allocation
83+
}
84+
arr[i] = data;
85+
}
86+
o = null;
87+
arr = new Object[128];
88+
o = arr;
89+
// allocate 1 more GB
90+
for (int i = 0; i < 128; i++) {
91+
int[] data = new int[1024 * 1024]; // 8MB
92+
for (int j = 0; j < 1024 * 1024; j++) {
93+
data[j] = j; // force actual pages allocation
94+
}
95+
arr[i] = data;
96+
}
97+
o = null;
98+
arr = null;
99+
100+
forceGCHeuristic0();
101+
while (ref.get() != null) {
102+
System.gc();
103+
}
104+
} catch (OutOfMemoryError e) {
105+
o = null;
106+
// just swallow
107+
}
108+
}
109+
}

test/test/DottyTest.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ import dotty.tools.dotc.Compiler
1414
import dotty.tools.dotc
1515
import dotty.tools.dotc.core.Phases.Phase
1616

17-
class DottyTest {
17+
class DottyTest extends ContextEscapeDetection{
1818

1919
dotty.tools.dotc.parsing.Scanners // initialize keywords
2020

21-
implicit val ctx: Context = {
21+
implicit var ctx: Contexts.Context = {
2222
val base = new ContextBase
2323
import base.settings._
2424
val ctx = base.initialCtx.fresh
@@ -37,6 +37,10 @@ class DottyTest {
3737
ctx
3838
}
3939

40+
override def getCtx: Context = ctx
41+
override def clearCtx() = {
42+
ctx = null
43+
}
4044
private def compilerWithChecker(phase: String)(assertion:(tpd.Tree, Context) => Unit) = new Compiler {
4145
override def phases = {
4246
val allPhases = super.phases

0 commit comments

Comments
 (0)