Skip to content

Commit

Permalink
Implementation of expression evaluator based on JDI
Browse files Browse the repository at this point in the history
Allows user to evaluate any Scala expression in context of some breakpoint
during debug. Expression is transformed in multiple phases and evaluated
using calls to remote, debuged machine and result is fetched back to user.

Because of complexity of Scala language, implementation details of JVM
platform and necessity for manual evaluation not all Scala features are
supported yet.

Co-authored-by:
 - Pawel Batko <pawel.batko@gmail.com>
 - Piotr Kukielka <piotr.kukielka@gmail.com>
 - Jerzy Muller <sidus.gemini@gmail.com>
 - Michal Pociecha <michal.pociecha@gmail.com>
 - Krzysztof Romanowski <romanowski.kr@gmail.com>
  • Loading branch information
romanowski authored and kiritsuku committed Mar 11, 2015
1 parent 5333d48 commit bee91fb
Show file tree
Hide file tree
Showing 194 changed files with 9,894 additions and 5 deletions.
@@ -1,16 +1,20 @@
/*
* Copyright (c) 2014 Contributor. All rights reserved.
*/
package org.scalaide.debug.internal

import org.junit.runners.Suite
import org.junit.runner.RunWith
import org.scalaide.debug.internal.editor.StackFrameVariableOfTreeFinderTest
import org.scalaide.debug.internal.expression.ExpressionsTestSuite
import org.scalaide.debug.internal.launching.RemoteConnectorTest
import org.scalaide.debug.internal.model.ScalaThreadTest
import org.scalaide.debug.internal.model.ScalaStackFrameTest
import org.scalaide.debug.internal.model.ScalaValueTest
import org.scalaide.debug.internal.model.ScalaDebugTargetTest
import org.scalaide.debug.internal.model.DebugTargetTerminationTest
import org.scalaide.debug.internal.model.MethodClassifierUnitTest
import org.scalaide.debug.internal.model.ScalaDebugCacheTest
import org.scalaide.debug.internal.launching.RemoteConnectorTest
import org.scalaide.debug.internal.editor.StackFrameVariableOfTreeFinderTest

/**
* Junit test suite for the Scala debugger.
Expand All @@ -33,6 +37,7 @@ import org.scalaide.debug.internal.editor.StackFrameVariableOfTreeFinderTest
classOf[RemoteConnectorTest],
classOf[ScalaDebugBreakpointTest],
classOf[ScalaDebugCacheTest],
classOf[StackFrameVariableOfTreeFinderTest]))
class ScalaDebugTestSuite {
}
classOf[StackFrameVariableOfTreeFinderTest],
classOf[ExpressionsTestSuite]
))
class ScalaDebugTestSuite
@@ -0,0 +1,101 @@
/*
* Copyright (c) 2014 Contributor. All rights reserved.
*/
package org.scalaide.debug.internal.expression

import scala.tools.reflect.ToolBoxError
import scala.util.Failure
import scala.util.Success

import org.eclipse.jdt.debug.core.IJavaBreakpoint
import org.junit.Assert._
import org.junit.BeforeClass
import org.scalaide.debug.internal.expression.TestValues.ValuesTestCase
import org.scalaide.debug.internal.expression.proxies.JdiProxy
import org.scalaide.logging.HasLogger

class BaseIntegrationTest(protected val companion: BaseIntegrationTestCompanion) extends HasLogger {

private val resultRegex = s"(.+) \\(of type: (.+)\\)".r

/**
* Test code and returns tuple with (returnedValue, returnedType)
*/
protected final def runCode(code: String): (String, String) = {
val proxy = runInEclipse(code)
val resultString = proxy.proxyContext.show(proxy)

resultString match {
case resultRegex(resultString, resultType) => (resultString, resultType)
case resultString =>
fail(s"'$resultString' don't match 'res (of type: resType)' standard")
throw new RuntimeException("Fail should throw an exception!")
}
}

/** test code for given value and its type */
protected final def eval(code: String, expectedValue: String, expectedType: String): Unit = {
val (resultValue, resultType) = runCode(code)
assertEquals("Result value differs:", expectedValue, resultValue)
assertEquals("Result type differs:", expectedType, resultType)
}

/**
* Checks if given error type is thrown i.e. when given operation is not permitted for given type
*/
protected def evalWithToolboxError(code: String): Unit = {
try {
runInEclipse(code).toString
fail(s"ToolBoxError should be thrown")
} catch {
case expected: ToolBoxError => assertTrue(true)
}
}

protected def runInEclipse(code: String): JdiProxy =
companion.expressionEvaluator.apply(code) match {
case Success(result) => result
case Failure(exception) => throw exception
}
}

/**
* Companion for integration test.
*
* @param projectName name of project in `test-workspace` to work on
* @param fileName name of file to run during test
* @param lineNumber line number in which breakpoint should be added
*/
class BaseIntegrationTestCompanion(projectName: String, fileName: String, lineNumber: Int)
extends CommonIntegrationTestCompanion(projectName) {

var expressionEvaluator: JdiExpressionEvaluator = null

def this(testCaseSettings: IntegrationTestCaseSettings = ValuesTestCase) =
this(testCaseSettings.projectName, testCaseSettings.fileName, testCaseSettings.breakpointLine)

protected def suspensionPolicy: Int = IJavaBreakpoint.SUSPEND_THREAD

/**
* Type in which breakpoint is set. By default `debug.<fileName>$`.
*/
protected def typeName: String = "debug." + fileName + "$"

@BeforeClass
def prepareTestDebugSession(): Unit = {
refreshBinaryFiles()

session = initDebugSession(fileName)

session.runToLine(typeName, lineNumber, suspendPolicy = suspensionPolicy)

expressionEvaluator = initializeEvaluator(session)
}

}

trait IntegrationTestCaseSettings {
val projectName: String
val fileName: String
val breakpointLine: Int
}
@@ -0,0 +1,65 @@
/*
* Copyright (c) 2014 Contributor. All rights reserved.
*/
package org.scalaide.debug.internal.expression

import org.eclipse.core.internal.resources.ResourceException
import org.eclipse.core.resources.IncrementalProjectBuilder
import org.eclipse.core.runtime.NullProgressMonitor
import org.junit.AfterClass
import org.junit.BeforeClass
import org.scalaide.core.testsetup.SDTTestUtils
import org.scalaide.core.testsetup.TestProjectSetup
import org.scalaide.debug.internal.ScalaDebugRunningTest
import org.scalaide.debug.internal.ScalaDebugTestSession
import org.scalaide.logging.HasLogger

class CommonIntegrationTestCompanion(projectName: String)
extends TestProjectSetup(projectName, bundleName = "org.scala-ide.sdt.debug.tests")
with ScalaDebugRunningTest
with HasLogger {

var session: ScalaDebugTestSession = null

protected def initDebugSession(launchConfigurationName: String): ScalaDebugTestSession =
ScalaDebugTestSession(file(launchConfigurationName + ".launch"))

private val testName = getClass.getSimpleName.init

@BeforeClass
def setup(): Unit = {
logger.info(s"Test $testName started")
}

@AfterClass
def doCleanup(): Unit = {
logger.info(s"Test $testName finished")
cleanDebugSession()
deleteProject()
}

protected def refreshBinaryFiles(): Unit = {
project.underlying.build(IncrementalProjectBuilder.CLEAN_BUILD, new NullProgressMonitor)
project.underlying.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, new NullProgressMonitor)
}

protected def initializeEvaluator(session: ScalaDebugTestSession): JdiExpressionEvaluator = {
val target = session.debugTarget
new JdiExpressionEvaluator(target.classPath)
}

private def cleanDebugSession(): Unit = {
if (session ne null) {
session.terminate()
session = null
}
}

private def deleteProject(): Unit = {
try {
SDTTestUtils.deleteProjects(project)
} catch {
case e: ResourceException => // could not delete resource, but don't you worry ;)
}
}
}
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2014 Contributor. All rights reserved.
*/
package org.scalaide.debug.internal.expression

import org.eclipse.jdt.debug.core.IJavaBreakpoint
import org.eclipse.jface.viewers.StructuredSelection
import org.junit.Assert._
import org.junit.Test
import org.scalaide.debug.internal.ScalaDebugger
import org.scalaide.debug.internal.expression.TestValues.DifferentStackFramesTestCase
import org.scalaide.debug.internal.model.ScalaStackFrame
import org.scalaide.debug.internal.model.ScalaThread

class DifferentStackFramesTest extends BaseIntegrationTest(DifferentStackFramesTest) {

private def changeThread(name: String) {
val newThread = ScalaDebugger.currentThread.getDebugTarget.getThreads
.filter(_.getName == name).head.asInstanceOf[ScalaThread]
ScalaDebugger.updateCurrentThread(new StructuredSelection(newThread))
assertTrue(s"Thread $name is not suspended", ScalaDebugger.currentThread.isSuspended)
}

private def changeFrame(index: Int) {
val currentThread = ScalaDebugger.currentThread
val newFrame = ScalaStackFrame(currentThread, currentThread.threadRef.frame(index), index)
ScalaDebugger.updateCurrentThread(new StructuredSelection(newFrame))
}

@Test
def testFrameAccess() {
/* Frames:
0: recFunction(0)
1: recFunction(1)
2: recFunction(2)
3: recFunction(3)
4: compute()
...
*/
eval("input", "0", Names.Java.boxed.Integer)
changeFrame(1)
eval("input", "1", Names.Java.boxed.Integer)
changeFrame(2)
eval("input", "2", Names.Java.boxed.Integer)
changeFrame(4)
eval("input", "5", Names.Java.boxed.Integer)

changeFrame(0)
eval("input", "0", Names.Java.boxed.Integer)
}

@Test
def testThreadAccess() {
changeThread(DifferentStackFramesTestCase.demonThreadName)

ExpressionManager.compute("input") match {
case EvaluationFailure(errorMessage) => assertTrue(s"Error message differs, got: $errorMessage",
errorMessage.contains("is not suspended as a result of JDI event."))
case other => fail(s"Expected `not at breakpoint` message, got: $other")
}

changeThread(DifferentStackFramesTestCase.mainThread)
eval("input", "0", Names.Java.boxed.Integer)

companion.session.disconnect()
}
}

object DifferentStackFramesTest extends BaseIntegrationTestCompanion(DifferentStackFramesTestCase) {

override protected val suspensionPolicy: Int = IJavaBreakpoint.SUSPEND_VM
}
@@ -0,0 +1,100 @@
/*
* Copyright (c) 2014 Contributor. All rights reserved.
*/
package org.scalaide.debug.internal.expression

import scala.util.Failure
import scala.util.Success

import org.junit.Assert._
import org.junit.Test
import org.scalaide.debug.internal.model.ScalaThread
import Names.Scala

class ExpressionManagerTest extends BaseIntegrationTest(ExpressionManagerTest) {

/**
* Executes code using [[org.scalaide.debug.internal.expression.ExpressionManager]] and checks result.
*
* @param code to compile and run
* @param expectedResult `Some(<string that should be returned>)` or `None` if error should be returned
* @param expectedError `Some(<string that should exist in result>)` or `None` if correct result should be returned
*/
protected final def withExpressionManager(code: String, expectedResult: Option[String], expectedError: Option[String]) = {
var result: Option[String] = None

var error: String = null

ExpressionManager.compute(code) match {
case SuccessWithValue(scalaValue, outputText) =>
result = Some(outputText)
case SuccessWithoutValue(outputText) =>
result = Some(outputText)
case EvaluationFailure(errorMessage) =>
error = errorMessage
}

expectedError.foreach(expected => assertTrue(s"'$error' does not contain '$expected'", error.contains(expected)))
assertEquals(expectedResult, result)
}

@Test
def testDisplayNullResult(): Unit = withExpressionManager(
code = "null",
expectedError = None,
expectedResult = Some(s"${Scala.nullLiteral} (of type: ${Scala.nullType})"))

@Test
def testDisplayNullValue(): Unit = withExpressionManager(
code = "Libs.nullVal",
expectedError = None,
expectedResult = Some(s"${Scala.nullLiteral} (of type: ${Scala.nullType})"))

// If in this test we'd use function returning Unit, some other tests in this class would fail (they work when we run them separately).
// In this case there's error from compiler: <Cannot read source file> in scala.tools.nsc.transform.AddInterfaces$LazyImplClassType.implType$1(AddInterfaces.scala:190).
// And there's no such problem during real work with expression evaluator installed in Eclipse.
@Test
def testDisplayUnitResult(): Unit = withExpressionManager(
code = "print('a')",
expectedError = None,
expectedResult = Some(s"${Scala.unitLiteral} (of type: ${Scala.unitType})"))

@Test
def testDisplayIntResult(): Unit = withExpressionManager(
code = "int",
expectedError = None,
expectedResult = Some(s"${TestValues.ValuesTestCase.int} (of type: ${Names.Java.boxed.Integer})"))

@Test
def testDisplayEmptyExpressionError(): Unit = withExpressionManager(
code = "",
expectedError = Some("Expression is empty"),
expectedResult = None)

@Test
def testDisplayInvalidExpressionError(): Unit = withExpressionManager(
code = "1 === 2",
expectedError = Some(ExpressionException.reflectiveCompilationFailureMessage("")),
expectedResult = None)

@Test
def testDisplayInvalidExpressionErrorWithTypeIssue(): Unit = withExpressionManager(
code = "List.alaString",
expectedError = Some(ExpressionException.reflectiveCompilationFailureMessage("")),
expectedResult = None)

@Test
def testDisplayExceptionMessage(): Unit = withExpressionManager(
code = "Seq(1, 2, 3).apply(4)",
expectedError = Some("java.lang.IndexOutOfBoundsException: 4"),
expectedResult = None)

@Test
def testDisplayIllegalNothingTypeInferred(): Unit = withExpressionManager(
code = "None.get",
expectedError = Some(ExpressionException.nothingTypeInferredMessage),
expectedResult = None)

}

object ExpressionManagerTest extends BaseIntegrationTestCompanion
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2014 Contributor. All rights reserved.
*/
package org.scalaide.debug.internal.expression

import org.junit.runner.RunWith
import org.junit.runners.Suite
import org.scalaide.debug.internal.expression.features.FeaturesTestSuite
import org.scalaide.debug.internal.expression.proxies.phases.PhasesTestSuite

/**
* Junit test suite for the Scala debugger.
*/
@RunWith(classOf[Suite])
@Suite.SuiteClasses(
Array(
classOf[ExpressionManagerTest],
classOf[FeaturesTestSuite],
classOf[DifferentStackFramesTest],
classOf[PhasesTestSuite]))
class ExpressionsTestSuite

0 comments on commit bee91fb

Please sign in to comment.