scala.tools.partest.ReplTest.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of scala-partest Show documentation
Show all versions of scala-partest Show documentation
Scala Compiler Testing Tool
The newest version!
/*
* Scala (https://www.scala-lang.org)
*
* Copyright EPFL and Lightbend, Inc.
*
* Licensed under Apache License 2.0
* (http://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/
package scala.tools.partest
import java.io.File.pathSeparator
import scala.tools.nsc.Settings
import scala.tools.nsc.interpreter.shell.{ILoop, ShellConfig}
import scala.util.matching.Regex.{quoteReplacement, Match}
/** Test code or commands in a REPL.
*
* Just call `show` to print the result of `eval()` your `code`.
*
* Optionally, `transformSettings` or redefine `normalize`.
*
* The test filters the line of output that mentions a version number.
*/
abstract class ReplTest extends DirectTest {
// override to transform Settings object immediately before the finish
def transformSettings(s: Settings): Settings = s
// final because we need to enforce the existence of a couple settings.
final override def settings: Settings = {
val s = super.settings
s.Xnojline.value = true
if (getClass.getClassLoader.getParent != null) {
s.classpath.value = s.classpath.value match {
case "" => testOutput.toString
case cp => s"$cp$pathSeparator$testOutput"
}
s.usejavacp.value = true
}
transformSettings(s)
}
/** Transform a line of output, for comparison to expected output. */
protected def normalize(s: String): String = s
/** True for SessionTest to preserve session text. */
protected def inSession: Boolean = false
/** Config for test. */
protected def shellConfig(testSettings: Settings): ShellConfig = ILoop.TestConfig(testSettings)
/** The normalized output from evaluating the `code` script. */
protected def eval(): Iterator[String] = {
val testSettings = settings
log(s"eval(): settings = $testSettings")
val transcript = ILoop.runForTranscript(code, testSettings, shellConfig(testSettings), inSession = inSession)
log(s"transcript[[$transcript]]")
transcript.linesIterator.map(normalize)
}
/** Print the transcript produced by `eval`. */
override def show() = eval().foreach(println)
}
/** Strip Any.toString's id@abcdef16 hashCodes. These are generally at end of result lines. */
trait Hashless extends ReplTest {
import Hashless._
override def normalize(s: String) = stripIdentityHashCode(super.normalize(s))
}
object Hashless {
private val hashless = "@[a-fA-F0-9]+".r
private def stripIdentityHashCode(s: String): String = hashless.replaceAllIn(s, "@XXXXXXXX")
}
/** Strip dynamic parts of LambdaMetafactory synthetic class names. */
trait Lambdaless extends ReplTest {
import Lambdaless._
override def normalize(s: String) = stripLambdaClassName(super.normalize(s))
}
object Lambdaless {
private val lambdaless = """\$Lambda(?:\$\d+)?/(?:0x[a-f0-9]{16}|\d+)(?:@[a-fA-F0-9]+)?""".r
private def stripLambdaClassName(s: String): String = lambdaless.replaceAllIn(s, quoteReplacement(""))
}
/** Normalize a REPL stack trace by stripping line numbers and count of elided frames. */
trait StackCleaner extends ReplTest {
import StackCleaner._
override def normalize(s: String) = stripFrameCount(super.normalize(s))
}
object StackCleaner {
private val elidedAndMore = """(\s+\.{3} )\d+( elided and )\d+( more)""".r
private val elidedOrMore = """(\s+\.{3} )\d+( (?:elided|more))""".r
private val frame = """(\s+at [^(]+\(:)\d+(\))""".r
private def stripFrameCount(line: String) = line match {
case elidedAndMore(ellipsis, infix, suffix) => s"$ellipsis???$infix???$suffix" // must precede `elidedOrMore`
case elidedOrMore(ellipsis, suffix) => s"$ellipsis???$suffix"
case frame(prefix, suffix) => s"${prefix}XX${suffix}"
case _ => line
}
}
/** Run a REPL test from a session transcript.
*
* The `session` is read from the `.check` file,
* and the `code` is the session stripped of output text.
*
* By default, output is compared against the check file
* as usual. Optionally, redefine `show` to use `checkSession()`,
* which compares `eval()` result to `expected`.
*
* Redefine `session` if it must be constructed dynamically.
* Redefine `show` to use `checkSession` to postprocess
* the `expected` text and/or the result of `eval()`.
*
*/
abstract class SessionTest extends ReplTest {
/** Session transcript. */
def session: String = testPath.changeExtension("check").toFile.slurp()
/** Expected output, as an iterator, optionally marginally stripped. */
def expected = if (stripMargins) session.stripMargin.linesIterator else session.linesIterator
/** Override with true if session is a """string""" with margin indent. */
def stripMargins: Boolean = false
/** Analogous to stripMargins, don't mangle continuation lines on echo. */
override def inSession: Boolean = true
/** Code is the command list culled from the session (or the expected session output).
* Would be nicer if code were lazy lines so you could generate arbitrarily long text.
* Retain user input: prompt lines and continuations, without the prefix; or pasted text plus ctrl-D.
*/
import SessionTest._
lazy val pasted = input(prompt)
override final def code = pasted.findAllMatchIn(expected.mkString("", "\n", "\n")).map {
case pasted(null, null, prompted) =>
def continued(m: Match): Option[String] = m match {
case margin(text) => Some(quoteReplacement(text))
case _ => None
}
margin.replaceSomeIn(prompted, continued)
case pasted(cmd, pasted, null) =>
cmd + pasted + "\u0004"
case x => throw new MatchError(x)
}.mkString
// Just the last line of the interactive prompt
def prompt = "scala> "
/** When overriding show, facilitate the usual check, comparing session to eval result. */
def checkSession(): Unit = {
val evaled = eval().toList
val wanted = expected.toList
if (evaled.size != wanted.size) Console.println(s"Expected ${wanted.size} lines, got ${evaled.size}")
if (evaled != wanted) Console.print(nest.FileManager.compareContents(wanted, evaled, "expected", "actual"))
}
}
object SessionTest {
// \R for line break since Java 8
private def input(prompt: String) = raw"""(?m)^$prompt(:pa.*\R)// Entering paste mode.*\R\R((?:.*\R)*)// Exiting paste mode.*\R|^scala> (.*\R(?:\s*\| .*\R)*)""".r
private val margin = """(?m)^\s*\| (.*)$""".r
}