Skip to content

Commit

Permalink
SI-8941 Deterministic tests for pres. compiler idempotency
Browse files Browse the repository at this point in the history
A retrospective test case which covers typechecking idempptency which
was introduced in 0b78a01 / 148736c. It also tests the
implicit class handling, which was fixed in the previous commit.

It is difficult to test this using existing presentation compiler
testing infrastructure, as one can't control at which point during
the first typechecking the subesquent work item will be noticed.

Instead, I've created a test with a custom subclass of
`interactive.Global` that allows precise, deterministic control
of when this happens. It overrides `signalDone`, which is called
after each tree is typechecked, and watches for a defintion with
a well known name. At that point, it triggers a targetted typecheck
of the tree marked with a special comment.

It is likely that this approach can be generalized to a reusable
base class down the track. In particular, I expect that some of
the nasty interactive ScalaDoc bugs could use this single-threaded
approach to testing the presentation compiler.
  • Loading branch information
retronym committed Oct 28, 2014
1 parent 7664e25 commit e5485fc
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 2 deletions.
6 changes: 4 additions & 2 deletions src/interactive/scala/tools/nsc/interactive/Global.scala
Expand Up @@ -64,6 +64,8 @@ trait InteractiveAnalyzer extends Analyzer {
// that case the definitions that were already attributed as
// well as any default parameters of such methods need to be
// re-entered in the current scope.
//
// Tested in test/files/presentation/t8941b
override def enterExistingSym(sym: Symbol, tree: Tree): Context = {
if (sym != null && sym.owner.isTerm) {
enterIfNotThere(sym)
Expand Down Expand Up @@ -737,7 +739,7 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "")
}
}

private def reloadSource(source: SourceFile) {
private[interactive] def reloadSource(source: SourceFile) {
val unit = new RichCompilationUnit(source)
unitOfFile(source.file) = unit
toBeRemoved -= source.file
Expand Down Expand Up @@ -786,7 +788,7 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "")
}

/** A fully attributed tree located at position `pos` */
private def typedTreeAt(pos: Position): Tree = getUnit(pos.source) match {
private[interactive] def typedTreeAt(pos: Position): Tree = getUnit(pos.source) match {
case None =>
reloadSources(List(pos.source))
try typedTreeAt(pos)
Expand Down
73 changes: 73 additions & 0 deletions test/files/presentation/t8941b/IdempotencyTest.scala
@@ -0,0 +1,73 @@
package scala.tools.nsc
package interactive
package tests.core

import reporters.{Reporter => CompilerReporter}
import scala.tools.nsc.interactive.InteractiveReporter
import scala.reflect.internal.util.SourceFile

/** Determistically interrupts typechecking of `code` when a defintion named
* `MagicInterruptionMarker` is typechecked, and then performs a targetted
* typecheck of the tree at the specal comment marker marker
*/
abstract class IdempotencyTest { self =>
private val settings = new Settings
settings.usejavacp.value = true

private object Break extends scala.util.control.ControlThrowable

private val compilerReporter: CompilerReporter = new InteractiveReporter {
override def compiler = self.compiler
}

object compiler extends Global(settings, compilerReporter) {
override def checkForMoreWork(pos: Position) {
}
override def signalDone(context: Context, old: Tree, result: Tree) {
// println("signalDone: " + old.toString.take(50).replaceAll("\n", "\\n"))
if (!interrupted && analyzer.lockedCount == 0 && interruptsEnabled && shouldInterrupt(result)) {
interrupted = true
val typed = typedTreeAt(markerPosition)
checkTypedTree(typed)
throw Break
}
super.signalDone(context, old, result)
}

// we're driving manually using our own thread, disable the check here.
override def assertCorrectThread() {}
}

import compiler._

private var interrupted = false

// Extension points
protected def code: String
protected def shouldInterrupt(tree: Tree): Boolean = {
tree.symbol != null && tree.symbol.name.toString == "MagicInterruptionMarker"
}
protected def checkTypedTree(tree: Tree): Unit = {}


private val source: SourceFile = newSourceFile(code)
private def markerPosition: Position = source.position(code.indexOf("/*?*/"))

def assertNoProblems() {
val problems = getUnit(source).get.problems
assert(problems.isEmpty, problems.mkString("\n"))
}

def show() {
reloadSource(source)
try {
typedTree(source, true)
assert(false, "Expected to break out of typechecking.")
} catch {
case Break => // expected
}
assertNoProblems()
}

def main(args: Array[String]) { show() }
}
53 changes: 53 additions & 0 deletions test/files/presentation/t8941b/Test.scala
@@ -0,0 +1,53 @@
import scala.tools.nsc.interactive.tests.core.IdempotencyTest

// At the time of writing this test, removing any part of `enterExistingSym`
// leads to a failure.
object Test {
def main(args: Array[String]) {
test("""
object Foo {
def term {
def foo(c: String = "") = c
class MagicInterruptionMarker
foo()/*?*/
}
}
""")

test("""
object Foo {
def term {
def foo = 42
class MagicInterruptionMarker
foo/*?*/
}
}
""")

test("""
object Foo {
def term {
lazy val foo = 42
class MagicInterruptionMarker
foo/*?*/
}
}
""")

test("""
object Foo {
implicit class C(val a: String) extends AnyVal
class MagicInterruptionMarker
""/*?*/
}
""")
}

def test(code0: String) {
val t = new IdempotencyTest {
def code = code0
}
t.show()
}
}

0 comments on commit e5485fc

Please sign in to comment.