Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implementation of expression evaluator based on JDI
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
1 parent
5333d48
commit bee91fb
Showing
194 changed files
with
9,894 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
...-ide.sdt.debug.tests/src/org/scalaide/debug/internal/expression/BaseIntegrationTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
65 changes: 65 additions & 0 deletions
65
...bug.tests/src/org/scalaide/debug/internal/expression/CommonIntegrationTestCompanion.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ;) | ||
} | ||
} | ||
} |
72 changes: 72 additions & 0 deletions
72
...sdt.debug.tests/src/org/scalaide/debug/internal/expression/DifferentStackFramesTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
100 changes: 100 additions & 0 deletions
100
...de.sdt.debug.tests/src/org/scalaide/debug/internal/expression/ExpressionManagerTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
21 changes: 21 additions & 0 deletions
21
...ide.sdt.debug.tests/src/org/scalaide/debug/internal/expression/ExpressionsTestSuite.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.