aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan Christopher Vogt <oss.nsp@cvogt.org>2016-11-07 23:59:15 -0500
committerGitHub <noreply@github.com>2016-11-07 23:59:14 -0500
commit9f17b211154bdd9f0a0fe1ca0125287641ad602c (patch)
treec62a5de32e771b9e2f4c709bde74b1a0c8425089
parentfd849d293448d55c6bcb6f8440f44838b51fc860 (diff)
parentd9400855a2f8539d2ca88b6f7e4731dc37a1411d (diff)
downloadcbt-9f17b211154bdd9f0a0fe1ca0125287641ad602c.tar.gz
cbt-9f17b211154bdd9f0a0fe1ca0125287641ad602c.tar.bz2
cbt-9f17b211154bdd9f0a0fe1ca0125287641ad602c.zip
Merge pull request #299 from cvogt/integrate-eval
Integrate twitter-eval
-rw-r--r--libraries/eval/Eval.scala630
-rw-r--r--libraries/eval/Readme.md7
-rw-r--r--libraries/eval/build/build.scala14
-rw-r--r--libraries/eval/build/build/build.scala7
-rw-r--r--libraries/eval/test/EvalTest.scala268
-rw-r--r--libraries/eval/test/resources/Base.scala4
-rw-r--r--libraries/eval/test/resources/Deprecated.scala6
-rw-r--r--libraries/eval/test/resources/Derived.scala1
-rw-r--r--libraries/eval/test/resources/DerivedWithInclude.scala3
-rw-r--r--libraries/eval/test/resources/HelloJoe.scala3
-rw-r--r--libraries/eval/test/resources/IncludeInclude.scala7
-rw-r--r--libraries/eval/test/resources/OnePlusOne.scala1
-rw-r--r--libraries/eval/test/resources/RubyInclude.scala3
-rw-r--r--libraries/eval/test/resources/file-with-dash.scala1
-rw-r--r--stage1/MultiClassLoader.scala2
-rw-r--r--test/test.scala31
16 files changed, 977 insertions, 11 deletions
diff --git a/libraries/eval/Eval.scala b/libraries/eval/Eval.scala
new file mode 100644
index 0000000..6a58491
--- /dev/null
+++ b/libraries/eval/Eval.scala
@@ -0,0 +1,630 @@
+/*
+ * Copyright 2010 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cbt.eval
+
+import java.io._
+import java.math.BigInteger
+import java.net.URLClassLoader
+import java.security.MessageDigest
+import java.util.Random
+import java.util.jar.JarFile
+import scala.collection.mutable
+import scala.io.Source
+import scala.reflect.internal.util.{BatchSourceFile, Position, AbstractFileClassLoader}
+import scala.tools.nsc.io.{AbstractFile, VirtualDirectory}
+import scala.tools.nsc.reporters.{Reporter, AbstractReporter}
+import scala.tools.nsc.{Global, Settings}
+import scala.util.matching.Regex
+
+
+/**
+ * Evaluate a file or string and return the result.
+ */
+object Eval {
+ private val jvmId = java.lang.Math.abs(new Random().nextInt())
+ val classCleaner: Regex = "\\W".r
+}
+
+/**
+ * Evaluates files, strings, or input streams as Scala code, and returns the result.
+ *
+ * If `target` is `None`, the results are compiled to memory (and are therefore ephemeral). If
+ * `target` is `Some(path)`, the path must point to a directory, and classes will be saved into
+ * that directory.
+ *
+ * Eval also supports a limited set of preprocessors. Currently, "limited" means "exactly one":
+ * directives of the form `#include <file>`.
+ *
+ * The flow of evaluation is:
+ * - extract a string of code from the file, string, or input stream
+ * - run preprocessors on that string
+ * - wrap processed code in an `apply` method in a generated class
+ * - compile the class
+ * - contruct an instance of that class
+ * - return the result of `apply()`
+ */
+class Eval(target: Option[File]) {
+ /**
+ * empty constructor for backwards compatibility
+ */
+ def this() {
+ this(None)
+ }
+
+ import Eval.jvmId
+
+ private lazy val compilerPath = try {
+ classPathOfClass("scala.tools.nsc.Interpreter")
+ } catch {
+ case e: Throwable =>
+ throw new RuntimeException("Unable to load Scala interpreter from classpath (scala-compiler jar is missing?)", e)
+ }
+
+ private lazy val libPath = try {
+ classPathOfClass("scala.AnyVal")
+ } catch {
+ case e: Throwable =>
+ throw new RuntimeException("Unable to load scala base object from classpath (scala-library jar is missing?)", e)
+ }
+
+ /**
+ * Preprocessors to run the code through before it is passed to the Scala compiler.
+ * if you want to add new resolvers, you can do so with
+ * new Eval(...) {
+ * lazy val preprocessors = {...}
+ * }
+ */
+ protected lazy val preprocessors: Seq[Preprocessor] =
+ Seq(
+ new IncludePreprocessor(
+ Seq(
+ new ClassLoaderResolver(classLoader),
+ new FilesystemResolver(new File(".")),
+ new FilesystemResolver(new File("." + File.separator + "config"))
+ ) ++ (
+ Option(System.getProperty("com.twitter.util.Eval.includePath")) map { path =>
+ new FilesystemResolver(new File(path))
+ }
+ )
+ )
+ )
+
+ // For derived classes to provide an alternate compiler message handler.
+ protected lazy val compilerMessageHandler: Option[Reporter] = None
+
+ // For derived classes do customize or override the default compiler settings.
+ protected lazy val compilerSettings: Settings = new EvalSettings(target)
+
+ // Primary encapsulation around native Scala compiler
+ private[this] lazy val compiler = new StringCompiler(codeWrapperLineOffset, target, compilerSettings, compilerMessageHandler)
+
+ /**
+ * run preprocessors on our string, returning a String that is the processed source
+ */
+ def sourceForString(code: String): String = {
+ preprocessors.foldLeft(code) { (acc, p) =>
+ p(acc)
+ }
+ }
+
+ /**
+ * write the current checksum to a file
+ */
+ def writeChecksum(checksum: String, file: File) {
+ val writer = new FileWriter(file)
+ writer.write("%s".format(checksum))
+ writer.close()
+ }
+
+ /**
+ * val i: Int = new Eval()("1 + 1") // => 2
+ */
+ def apply[T](code: String, resetState: Boolean = true): T = {
+ val processed = sourceForString(code)
+ applyProcessed(processed, resetState)
+ }
+
+ /**
+ * val i: Int = new Eval()(new File("..."))
+ */
+ def apply[T](files: File*): T = {
+ if (target.isDefined) {
+ val targetDir = target.get
+ val unprocessedSource = files.map { scala.io.Source.fromFile(_).mkString }.mkString("\n")
+ val processed = sourceForString(unprocessedSource)
+ val sourceChecksum = uniqueId(processed, None)
+ val checksumFile = new File(targetDir, "checksum")
+ val lastChecksum = if (checksumFile.exists) {
+ Source.fromFile(checksumFile).getLines().take(1).toList.head
+ } else {
+ -1
+ }
+
+ if (lastChecksum != sourceChecksum) {
+ compiler.reset()
+ writeChecksum(sourceChecksum, checksumFile)
+ }
+
+ // why all this nonsense? Well.
+ // 1) We want to know which file the eval'd code came from
+ // 2) But sometimes files have characters that aren't valid in Java/Scala identifiers
+ // 3) And sometimes files with the same name live in different subdirectories
+ // so, clean it hash it and slap it on the end of Evaluator
+ val cleanBaseName = fileToClassName(files(0))
+ val className = "Evaluator__%s_%s".format(
+ cleanBaseName, sourceChecksum)
+ applyProcessed(className, processed, false)
+ } else {
+ apply(files.map { scala.io.Source.fromFile(_).mkString }.mkString("\n"), true)
+ }
+ }
+
+ /**
+ * val i: Int = new Eval()(getClass.getResourceAsStream("..."))
+ */
+ def apply[T](stream: InputStream): T = {
+ apply(sourceForString(Source.fromInputStream(stream).mkString))
+ }
+
+ /**
+ * same as apply[T], but does not run preprocessors.
+ * Will generate a classname of the form Evaluater__<unique>,
+ * where unique is computed from the jvmID (a random number)
+ * and a digest of code
+ */
+ def applyProcessed[T](code: String, resetState: Boolean): T = {
+ val id = uniqueId(code)
+ val className = "Evaluator__" + id
+ applyProcessed(className, code, resetState)
+ }
+
+ /**
+ * same as apply[T], but does not run preprocessors.
+ */
+ def applyProcessed[T](className: String, code: String, resetState: Boolean): T = {
+ val cls = compiler(wrapCodeInClass(className, code), className, resetState)
+ cls.getConstructor().newInstance().asInstanceOf[() => Any].apply().asInstanceOf[T]
+ }
+
+ /**
+ * converts the given file to evaluable source.
+ * delegates to toSource(code: String)
+ */
+ def toSource(file: File): String = {
+ toSource(scala.io.Source.fromFile(file).mkString)
+ }
+
+ /**
+ * converts the given file to evaluable source.
+ */
+ def toSource(code: String): String = {
+ sourceForString(code)
+ }
+
+ /**
+ * Compile an entire source file into the virtual classloader.
+ */
+ def compile(code: String) {
+ compiler(sourceForString(code))
+ }
+
+ /**
+ * Like `Eval()`, but doesn't reset the virtual classloader before evaluating. So if you've
+ * loaded classes with `compile`, they can be referenced/imported in code run by `inPlace`.
+ */
+ def inPlace[T](code: String) = {
+ apply[T](code, false)
+ }
+
+ /**
+ * Check if code is Eval-able.
+ * @throws CompilerException if not Eval-able.
+ */
+ def check(code: String) {
+ val id = uniqueId(sourceForString(code))
+ val className = "Evaluator__" + id
+ val wrappedCode = wrapCodeInClass(className, code)
+ resetReporter()
+ compile(wrappedCode) // may throw CompilerException
+ }
+
+ /**
+ * Check if files are Eval-able.
+ * @throws CompilerException if not Eval-able.
+ */
+ def check(files: File*) {
+ val code = files.map { scala.io.Source.fromFile(_).mkString }.mkString("\n")
+ check(code)
+ }
+
+ /**
+ * Check if stream is Eval-able.
+ * @throws CompilerException if not Eval-able.
+ */
+ def check(stream: InputStream) {
+ check(scala.io.Source.fromInputStream(stream).mkString)
+ }
+
+ def findClass(className: String): Class[_] = {
+ compiler.findClass(className).getOrElse { throw new ClassNotFoundException("no such class: " + className) }
+ }
+
+ private[eval] def resetReporter(): Unit = {
+ compiler.resetReporter()
+ }
+
+ private[eval] def uniqueId(code: String, idOpt: Option[Int] = Some(jvmId)): String = {
+ val digest = MessageDigest.getInstance("SHA-1").digest(code.getBytes())
+ val sha = new BigInteger(1, digest).toString(16)
+ idOpt match {
+ case Some(id) => sha + "_" + jvmId
+ case _ => sha
+ }
+ }
+
+ private[eval] def fileToClassName(f: File): String = {
+ // HOPE YOU'RE HAPPY GUYS!!!!
+ /* __
+ * __/|_/ /_ __ ______ ________/|_
+ * | / __ \/ / / / __ `/ ___/ /
+ * /_ __/ / / / /_/ / /_/ (__ )_ __|
+ * |/ /_/ /_/\__,_/\__, /____/ |/
+ * /____/
+ */
+ val fileName = f.getName
+ val baseName = fileName.lastIndexOf('.') match {
+ case -1 => fileName
+ case dot => fileName.substring(0, dot)
+ }
+ Eval.classCleaner.replaceAllIn( baseName, { m =>
+ Regex.quoteReplacement( "$%02x".format(m.group(0).charAt(0).toInt) )
+ })
+ }
+
+ /*
+ * Wraps source code in a new class with an apply method.
+ * NB: If this method is changed, make sure `codeWrapperLineOffset` is correct.
+ */
+ private[this] def wrapCodeInClass(className: String, code: String) = {
+ "class " + className + " extends (() => Any) with java.io.Serializable {\n" +
+ " def apply() = {\n" +
+ code + "\n" +
+ " }\n" +
+ "}\n"
+ }
+
+ /*
+ * Defines the number of code lines that proceed evaluated code.
+ * Used to ensure compile error messages report line numbers aligned with user's code.
+ * NB: If `wrapCodeInClass(String,String)` is changed, make sure this remains correct.
+ */
+ private[this] val codeWrapperLineOffset = 2
+
+ /*
+ * For a given FQ classname, trick the resource finder into telling us the containing jar.
+ */
+ private def classPathOfClass(className: String) = {
+ val resource = className.split('.').mkString("/", "/", ".class")
+ val path = getClass.getResource(resource).getPath
+ if (path.indexOf("file:") >= 0) {
+ val indexOfFile = path.indexOf("file:") + 5
+ val indexOfSeparator = path.lastIndexOf('!')
+ List(path.substring(indexOfFile, indexOfSeparator))
+ } else {
+ require(path.endsWith(resource))
+ List(path.substring(0, path.length - resource.length + 1))
+ }
+ }
+
+ /*
+ * Try to guess our app's classpath.
+ * This is probably fragile.
+ */
+ lazy val impliedClassPath: List[String] = {
+ def getClassPath(cl: ClassLoader, acc: List[List[String]] = List.empty): List[List[String]] = {
+ val cp = cl match {
+ case urlClassLoader: URLClassLoader => urlClassLoader.getURLs.filter(_.getProtocol == "file").
+ map(u => new File(u.toURI).getPath).toList
+ case _ => Nil
+ }
+ cl.getParent match {
+ case null => (cp :: acc).reverse
+ case parent => getClassPath(parent, cp :: acc)
+ }
+ }
+
+ val classPath = getClassPath(this.getClass.getClassLoader)
+ val currentClassPath = classPath.head
+
+ // if there's just one thing in the classpath, and it's a jar, assume an executable jar.
+ currentClassPath ::: (if (currentClassPath.size == 1 && currentClassPath(0).endsWith(".jar")) {
+ val jarFile = currentClassPath(0)
+ val relativeRoot = new File(jarFile).getParentFile()
+ val nestedClassPath = new JarFile(jarFile).getManifest.getMainAttributes.getValue("Class-Path")
+ if (nestedClassPath eq null) {
+ Nil
+ } else {
+ nestedClassPath.split(" ").map { f => new File(relativeRoot, f).getAbsolutePath }.toList
+ }
+ } else {
+ Nil
+ }) ::: classPath.tail.flatten
+ }
+
+ trait Preprocessor {
+ def apply(code: String): String
+ }
+
+ trait Resolver {
+ def resolvable(path: String): Boolean
+ def get(path: String): InputStream
+ }
+
+ class FilesystemResolver(root: File) extends Resolver {
+ private[this] def file(path: String): File =
+ new File(root.getAbsolutePath + File.separator + path)
+
+ def resolvable(path: String): Boolean =
+ file(path).exists
+
+ def get(path: String): InputStream =
+ new FileInputStream(file(path))
+ }
+
+ class ClassLoaderResolver(classLoader: ClassLoader) extends Resolver {
+ private[this] def quotePath(path: String) =
+ /*"/" +*/ path
+
+ def resolvable(path: String): Boolean = {
+ classLoader.getResourceAsStream(quotePath(path)) != null
+ }
+
+ def get(path: String): InputStream = {
+ classLoader.getResourceAsStream(quotePath(path))
+ }
+ }
+
+ class ResolutionFailedException(message: String) extends Exception
+
+ /*
+ * This is a preprocesor that can include files by requesting them from the given classloader
+ *
+ * Thusly, if you put FS directories on your classpath (e.g. config/ under your app root,) you
+ * mix in configuration from the filesystem.
+ *
+ * @example #include file-name.scala
+ *
+ * This is the only directive supported by this preprocessor.
+ *
+ * Note that it is *not* recursive. Included files cannot have includes
+ */
+ class IncludePreprocessor(resolvers: Seq[Resolver]) extends Preprocessor {
+ def maximumRecursionDepth = 100
+
+ def apply(code: String): String =
+ apply(code, maximumRecursionDepth)
+
+ def apply(code: String, maxDepth: Int): String = {
+ val lines = code.lines map { line: String =>
+ val tokens = line.trim.split(' ')
+ if (tokens.length == 2 && tokens(0).equals("#include")) {
+ val path = tokens(1)
+ resolvers find { resolver: Resolver =>
+ resolver.resolvable(path)
+ } match {
+ case Some(r: Resolver) => {
+ // recursively process includes
+ if (maxDepth == 0) {
+ throw new IllegalStateException("Exceeded maximum recusion depth")
+ } else {
+ val inputStream = r.get(path)
+ val string = new String(
+ Iterator.continually(
+ inputStream.read()
+ ).takeWhile(_ != -1).map(_.toByte).toArray
+ )
+ apply(string, maxDepth - 1)
+ }
+ }
+ case _ =>
+ throw new IllegalStateException("No resolver could find '%s'".format(path))
+ }
+ } else {
+ line
+ }
+ }
+ lines.mkString("\n")
+ }
+ }
+
+ lazy val compilerOutputDir = target match {
+ case Some(dir) => AbstractFile.getDirectory(dir)
+ case None => new VirtualDirectory("(memory)", None)
+ }
+
+ class EvalSettings(targetDir: Option[File]) extends Settings {
+ nowarnings.value = true // warnings are exceptions, so disable
+ outputDirs.setSingleOutput(compilerOutputDir)
+ private[this] val pathList = compilerPath ::: libPath
+ bootclasspath.value = pathList.mkString(File.pathSeparator)
+ classpath.value = (pathList ::: impliedClassPath).mkString(File.pathSeparator)
+ }
+
+ /** Class loader for finding classes compiled by StringCompiler.
+ * Override if the classloader of Eval does not have access to
+ * all classes used by the code it compiles.
+ */
+ def classLoader = this.getClass.getClassLoader
+
+ /**
+ * Dynamic scala compiler. Lots of (slow) state is created, so it may be advantageous to keep
+ * around one of these and reuse it.
+ */
+ private class StringCompiler(
+ lineOffset: Int, targetDir: Option[File], settings: Settings, messageHandler: Option[Reporter]) {
+
+ val cache = new mutable.HashMap[String, Class[_]]()
+ val target = compilerOutputDir
+
+ trait MessageCollector {
+ val messages: Seq[List[String]]
+ }
+
+ val reporter = messageHandler getOrElse new AbstractReporter with MessageCollector {
+ val settings = StringCompiler.this.settings
+ val messages = new mutable.ListBuffer[List[String]]
+
+ def display(pos: Position, message: String, severity: Severity) {
+ severity.count += 1
+ val severityName = severity match {
+ case ERROR => "error: "
+ case WARNING => "warning: "
+ case _ => ""
+ }
+ // the line number is not always available
+ val lineMessage =
+ try {
+ "line " + (pos.line - lineOffset)
+ } catch {
+ case _: Throwable => ""
+ }
+ messages += (severityName + lineMessage + ": " + message) ::
+ (if (pos.isDefined) {
+ pos.finalPosition.lineContent.stripLineEnd ::
+ (" " * (pos.column - 1) + "^") ::
+ Nil
+ } else {
+ Nil
+ })
+ }
+
+ def displayPrompt {
+ // no.
+ }
+
+ override def reset {
+ super.reset
+ messages.clear()
+ }
+ }
+
+ val global = new Global(settings, reporter)
+
+ /*
+ * Class loader for finding classes compiled by this StringCompiler.
+ * After each reset, this class loader will not be able to find old compiled classes.
+ */
+ var classLoader = new AbstractFileClassLoader(target, Eval.this.classLoader)
+
+ def reset() {
+ targetDir match {
+ case None => {
+ target.asInstanceOf[VirtualDirectory].clear()
+ }
+ case Some(t) => {
+ target.foreach { abstractFile =>
+ if (abstractFile.file == null || abstractFile.file.getName.endsWith(".class")) {
+ abstractFile.delete()
+ }
+ }
+ }
+ }
+ cache.clear()
+ reporter.reset()
+ classLoader = new AbstractFileClassLoader(target, Eval.this.classLoader)
+ }
+
+ def resetReporter(): Unit = {
+ synchronized {
+ reporter.reset()
+ }
+ }
+
+ object Debug {
+ val enabled =
+ System.getProperty("eval.debug") != null
+
+ def printWithLineNumbers(code: String) {
+ printf("Code follows (%d bytes)\n", code.length)
+
+ var numLines = 0
+ code.lines foreach { line: String =>
+ numLines += 1
+ println(numLines.toString.padTo(5, ' ') + "| " + line)
+ }
+ }
+ }
+
+ def findClass(className: String): Option[Class[_]] = {
+ synchronized {
+ cache.get(className).orElse {
+ try {
+ val cls = classLoader.loadClass(className)
+ cache(className) = cls
+ Some(cls)
+ } catch {
+ case e: ClassNotFoundException => None
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Compile scala code. It can be found using the above class loader.
+ */
+ def apply(code: String) {
+ if (Debug.enabled)
+ Debug.printWithLineNumbers(code)
+
+ //reset reporter, or will always throw exception after one error while resetState==false
+ resetReporter()
+
+ // if you're looking for the performance hit, it's 1/2 this line...
+ val compiler = new global.Run
+ val sourceFiles = List(new BatchSourceFile("(inline)", code))
+ // ...and 1/2 this line:
+ compiler.compileSources(sourceFiles)
+
+ if (reporter.hasErrors || reporter.WARNING.count > 0) {
+ val msgs: List[List[String]] = reporter match {
+ case collector: MessageCollector =>
+ collector.messages.toList
+ case _ =>
+ List(List(reporter.toString))
+ }
+ throw new CompilerException(msgs)
+ }
+ }
+
+ /**
+ * Compile a new class, load it, and return it. Thread-safe.
+ */
+ def apply(code: String, className: String, resetState: Boolean = true): Class[_] = {
+ synchronized {
+ if (resetState) reset()
+ findClass(className).getOrElse {
+ apply(code)
+ findClass(className).get
+ }
+ }
+ }
+ }
+
+ class CompilerException(val messages: List[List[String]]) extends Exception(
+ "Compiler exception " + messages.map(_.mkString("\n")).mkString("\n"))
+}
diff --git a/libraries/eval/Readme.md b/libraries/eval/Readme.md
new file mode 100644
index 0000000..1c7a54f
--- /dev/null
+++ b/libraries/eval/Readme.md
@@ -0,0 +1,7 @@
+Eval - Scala runtime code compilation and evaluation
+
+The code for Eval originally comes out of https://github.com/twitter/util . History was transferred to see origin and author of the changes in this file, but history was re-written to eliminate file renames.
+
+The code was moved rather than a dependency on twitter/util added for these reasons:
+- With minor changes the dependency on twitter `util-core` can be removed, which allows CBT to avoid adding it as a dependency
+- According to @ryanoneill, Eval is deprecated within twitter (https://github.com/twitter/util/pull/179)
diff --git a/libraries/eval/build/build.scala b/libraries/eval/build/build.scala
new file mode 100644
index 0000000..8dcaabd
--- /dev/null
+++ b/libraries/eval/build/build.scala
@@ -0,0 +1,14 @@
+import cbt._
+class Build(val context: Context) extends BaseBuild{
+ outer =>
+ override def dependencies = super.dependencies :+
+ new ScalaCompilerDependency( context.cbtHasChanged, context.paths.mavenCache, scalaVersion )
+
+ override def test: Option[ExitCode] = Some{
+ new BasicBuild(context.copy(projectDirectory = projectDirectory ++ "/test")) with ScalaTest{
+ override def dependencies = super.dependencies ++ Seq(
+ DirectoryDependency(projectDirectory++"/..")
+ )
+ }.run
+ }
+}
diff --git a/libraries/eval/build/build/build.scala b/libraries/eval/build/build/build.scala
new file mode 100644
index 0000000..22349c7
--- /dev/null
+++ b/libraries/eval/build/build/build.scala
@@ -0,0 +1,7 @@
+import cbt._
+
+class Build(val context: Context) extends BuildBuild{
+ override def dependencies = super.dependencies ++ Seq(
+ plugins.scalaTest
+ )
+}
diff --git a/libraries/eval/test/EvalTest.scala b/libraries/eval/test/EvalTest.scala
new file mode 100644
index 0000000..0489fba
--- /dev/null
+++ b/libraries/eval/test/EvalTest.scala
@@ -0,0 +1,268 @@
+package cbt.eval.test
+
+import java.io.{File, FileWriter}
+import org.scalatest.WordSpec
+import scala.io.Source
+import scala.language.reflectiveCalls
+import scala.reflect.internal.util.Position
+import scala.tools.nsc.Settings
+import scala.tools.nsc.reporters.{AbstractReporter, Reporter}
+import java.nio.file._
+
+class EvalTest extends WordSpec {
+ class Eval(f: Option[File] = None) extends cbt.eval.Eval(f){
+ override def classLoader = EvalTest.this.getClass.getClassLoader
+ }
+ object Eval extends Eval(None)
+ def fromResourcePath(path: String): File = {
+ assert(path.endsWith(".scala"))
+ val tmpFile = File.createTempFile(path.stripSuffix(".scala"),"scala")
+ Files.copy(
+ Paths.get( getClass.getResource(path).getFile),
+ tmpFile.toPath,
+ StandardCopyOption.REPLACE_EXISTING
+ )
+ tmpFile.deleteOnExit()
+ tmpFile
+ }
+ "Evaluator" should {
+
+ "apply('expression')" in {
+ assert((new Eval).apply[Int]("1 + 1") == 2)
+ }
+
+ "apply(new File(...))" in {
+ assert((new Eval).apply[Int](fromResourcePath("/OnePlusOne.scala")) == 2)
+ }
+
+ "apply(new File(...), new File(...))" in {
+ val derived = (new Eval).apply[() => String](
+ fromResourcePath("/Base.scala"),
+ fromResourcePath("/Derived.scala"))
+ assert(derived() == "hello")
+ }
+
+ "apply(new File(...) with a dash in the name with target" in {
+ val f = File.createTempFile("eval", "target")
+ f.delete()
+ f.mkdir()
+ val e = new Eval(Some(f))
+ val sourceFile = fromResourcePath("/file-with-dash.scala")
+ val res: String = e(sourceFile)
+ assert(res == "hello")
+ val className = e.fileToClassName(sourceFile)
+ val processedSource = e.sourceForString(Source.fromFile(sourceFile).getLines().mkString("\n"))
+ val fullClassName = "Evaluator__%s_%s.class".format(
+ className, e.uniqueId(processedSource, None))
+ val targetFileName = f.getAbsolutePath() + File.separator + fullClassName
+ val targetFile = new File(targetFileName)
+ assert(targetFile.exists)
+ }
+
+ "apply(new File(...) with target" in {
+ val f = File.createTempFile("eval", "target")
+ f.delete()
+ f.mkdir()
+ val e = new Eval(Some(f))
+ val sourceFile = fromResourcePath("/OnePlusOne.scala")
+ val res: Int = e(sourceFile)
+ assert(res == 2)
+
+ // make sure it created a class file with the expected name
+ val className = e.fileToClassName(sourceFile)
+ val processedSource = e.sourceForString(Source.fromFile(sourceFile).getLines().mkString("\n"))
+ val fullClassName = "Evaluator__%s_%s.class".format(
+ className, e.uniqueId(processedSource, None))
+ val targetFileName = f.getAbsolutePath() + File.separator + fullClassName
+ val targetFile = new File(targetFileName)
+ assert(targetFile.exists)
+ val targetMod = targetFile.lastModified
+
+ // eval again, make sure it works
+ val res2: Int = e(sourceFile)
+ // and make sure it didn't create a new file (1 + checksum)
+ assert(f.listFiles.length == 2)
+ // and make sure it didn't update the file
+ val targetFile2 = new File(targetFileName)
+ assert(targetFile2.lastModified == targetMod)
+
+ // touch source, ensure no-recompile (checksum hasn't changed)
+ sourceFile.setLastModified(System.currentTimeMillis())
+ val res3: Int = e(sourceFile)
+ assert(res3 == 2)
+ // and make sure it didn't create a different file
+ assert(f.listFiles.length == 2)
+ // and make sure it updated the file
+ val targetFile3 = new File(targetFileName)
+ assert(targetFile3.lastModified == targetMod)
+
+ // append a newline, altering checksum, verify recompile
+ val writer = new FileWriter(sourceFile)
+ writer.write("//a comment\n2\n")
+ writer.close()
+ val res4: Int = e(sourceFile)
+ assert(res4 == 2)
+ // and make sure it created a new file
+ val targetFile4 = new File(targetFileName)
+ assert(!targetFile4.exists)
+ }
+
+ "apply(InputStream)" in {
+ assert((new Eval).apply[Int](getClass.getResourceAsStream("/OnePlusOne.scala")) == 2)
+ }
+
+ "uses deprecated" in {
+ val deprecated = (new Eval).apply[() => String](
+ fromResourcePath("/Deprecated.scala"))
+ assert(deprecated() == "hello")
+ }
+
+ "inPlace('expression')" in {
+ // Old object API works
+ Eval.compile("object Doubler { def apply(n: Int) = n * 2 }")
+ assert(Eval.inPlace[Int]("Doubler(2)") == 4)
+ assert(Eval.inPlace[Int]("Doubler(14)") == 28)
+ // New class API fails
+ // val eval = new Eval
+ // eval.compile("object Doubler { def apply(n: Int) = n * 2 }")
+ // assert(eval.inPlace[Int]("Doubler(2)") == 4)
+ // assert(eval.inPlace[Int]("Doubler(14)") == 28)
+ }
+
+ "check" in {
+ (new Eval).check("23")
+ intercept[Eval.CompilerException] {
+ (new Eval).check("invalid")
+ }
+ }
+
+ "#include" in {
+ val derived = Eval[() => String](
+ fromResourcePath("/Base.scala"),
+ fromResourcePath("/DerivedWithInclude.scala"))
+ assert(derived() == "hello")
+ assert(derived.toString == "hello, joe")
+ }
+
+ "recursive #include" in {
+ val derived = Eval[() => String](
+ fromResourcePath("/Base.scala"),
+ fromResourcePath("/IncludeInclude.scala"))
+ assert(derived() == "hello")
+ assert(derived.toString == "hello, joe; hello, joe")
+ }
+
+ "toSource returns post-processed code" in {
+ val derived = Eval.toSource(fromResourcePath("/DerivedWithInclude.scala"))
+ assert(derived.contains("hello, joe"))
+ assert(derived.contains("new Base"))
+ }
+
+ "throws a compilation error when Ruby is #included" in {
+ intercept[Throwable] {
+ Eval[() => String](
+ fromResourcePath("RubyInclude.scala")
+ )
+ }
+ }
+
+ "clean class names" in {
+ val e = new Eval()
+ // regular old scala file
+ assert(e.fileToClassName(new File("foo.scala")) == "foo")
+ // without an extension
+ assert(e.fileToClassName(new File("foo")) == "foo")
+ // with lots o dots
+ assert(e.fileToClassName(new File("foo.bar.baz")) == "foo$2ebar")
+ // with dashes
+ assert(e.fileToClassName(new File("foo-bar-baz.scala")) == "foo$2dbar$2dbaz")
+ // with crazy things
+ assert(e.fileToClassName(new File("foo$! -@@@")) == "foo$24$21$20$2d$40$40$40")
+ }
+
+ "allow custom error reporting" when {
+ class Ctx {
+ val eval = new Eval {
+ @volatile var errors: Seq[(String, String)] = Nil
+
+ override lazy val compilerMessageHandler: Option[Reporter] = Some(new AbstractReporter {
+ override val settings: Settings = compilerSettings
+ override def displayPrompt(): Unit = ()
+ override def display(pos: Position, msg: String, severity: this.type#Severity): Unit = {
+ errors = errors :+ ((msg, severity.toString))
+ }
+ override def reset() = {
+ super.reset()
+ errors = Nil
+ }
+ })
+ }
+ }
+
+ "not report errors on success" in {
+ val ctx = new Ctx
+ import ctx._
+
+ assert(eval[Int]("val a = 3; val b = 2; a + b", true) == 5)
+ assert(eval.errors.isEmpty)
+ }
+
+ "report errors on bad code" in {
+ val ctx = new Ctx
+ import ctx._
+
+ intercept[Throwable] {
+ eval[Int]("val a = 3; val b = q; a + b", true)
+ }
+ assert(eval.errors.nonEmpty)
+ }
+
+ "reset state between invocations" in {
+ val ctx = new Ctx
+ import ctx._
+
+ intercept[Throwable] {
+ eval[Int]("val a = 3; val b = q; a + b", true)
+ }
+ assert(eval.errors.nonEmpty)
+ assert(eval[Int]("val d = 3; val e = 2; d + e", true) == 5)
+ assert(eval.errors.isEmpty)
+ }
+
+ "reset reporter between inPlace invocations" in {
+ val ctx = new Ctx
+ import ctx._
+
+ intercept[Throwable] {
+ eval.inPlace[Int]("val a = 3; val b = q; a + b")
+ }
+ assert(eval.errors.nonEmpty)
+ assert(eval.inPlace[Int]("val d = 3; val e = 2; d + e") == 5)
+ assert(eval.errors.isEmpty)
+ }
+
+ "reporter should be reset between checks, but loaded class should remain" in {
+ val ctx = new Ctx
+ import ctx._
+
+ // compile and load compiled class
+ eval.compile("class A()")
+
+ intercept[Throwable] {
+ eval.check("new B()")
+ }
+ assert(eval.errors.nonEmpty)
+
+ eval.check("new A()")
+ assert(eval.errors.isEmpty)
+ }
+
+ "eval with overriden classloader" in {
+ val eval = new Eval{
+ override def classLoader = this.getClass.getClassLoader
+ }
+ assert(eval.apply[Int]("1 + 1") == 2)
+ }
+ }
+ }
+}
diff --git a/libraries/eval/test/resources/Base.scala b/libraries/eval/test/resources/Base.scala
new file mode 100644
index 0000000..ec8ae22
--- /dev/null
+++ b/libraries/eval/test/resources/Base.scala
@@ -0,0 +1,4 @@
+trait Base extends (() => String) {
+ def apply() = "hello"
+}
+
diff --git a/libraries/eval/test/resources/Deprecated.scala b/libraries/eval/test/resources/Deprecated.scala
new file mode 100644
index 0000000..979affb
--- /dev/null
+++ b/libraries/eval/test/resources/Deprecated.scala
@@ -0,0 +1,6 @@
+new (() => String) {
+ @deprecated("don't use hello")
+ def hello() = "hello"
+
+ def apply() = hello()
+}
diff --git a/libraries/eval/test/resources/Derived.scala b/libraries/eval/test/resources/Derived.scala
new file mode 100644
index 0000000..1d8e0d8
--- /dev/null
+++ b/libraries/eval/test/resources/Derived.scala
@@ -0,0 +1 @@
+new Base { }
diff --git a/libraries/eval/test/resources/DerivedWithInclude.scala b/libraries/eval/test/resources/DerivedWithInclude.scala
new file mode 100644
index 0000000..476d60e
--- /dev/null
+++ b/libraries/eval/test/resources/DerivedWithInclude.scala
@@ -0,0 +1,3 @@
+new Base {
+#include HelloJoe.scala
+}
diff --git a/libraries/eval/test/resources/HelloJoe.scala b/libraries/eval/test/resources/HelloJoe.scala
new file mode 100644
index 0000000..c6ea42e
--- /dev/null
+++ b/libraries/eval/test/resources/HelloJoe.scala
@@ -0,0 +1,3 @@
+/* real-time declarative programming now */
+override def toString = "hello, joe"
+
diff --git a/libraries/eval/test/resources/IncludeInclude.scala b/libraries/eval/test/resources/IncludeInclude.scala
new file mode 100644
index 0000000..cc1f966
--- /dev/null
+++ b/libraries/eval/test/resources/IncludeInclude.scala
@@ -0,0 +1,7 @@
+val b1 =
+#include DerivedWithInclude.scala
+val b2 =
+#include DerivedWithInclude.scala
+new Base {
+ override def toString = b1 + "; " + b2
+}
diff --git a/libraries/eval/test/resources/OnePlusOne.scala b/libraries/eval/test/resources/OnePlusOne.scala
new file mode 100644
index 0000000..8d2f097
--- /dev/null
+++ b/libraries/eval/test/resources/OnePlusOne.scala
@@ -0,0 +1 @@
+1 + 1
diff --git a/libraries/eval/test/resources/RubyInclude.scala b/libraries/eval/test/resources/RubyInclude.scala
new file mode 100644
index 0000000..a763d52
--- /dev/null
+++ b/libraries/eval/test/resources/RubyInclude.scala
@@ -0,0 +1,3 @@
+object CodeThatIncludesSomeRuby {
+#include hello.rb
+}
diff --git a/libraries/eval/test/resources/file-with-dash.scala b/libraries/eval/test/resources/file-with-dash.scala
new file mode 100644
index 0000000..3580093
--- /dev/null
+++ b/libraries/eval/test/resources/file-with-dash.scala
@@ -0,0 +1 @@
+"hello"
diff --git a/stage1/MultiClassLoader.scala b/stage1/MultiClassLoader.scala
index 9546d47..34831ea 100644
--- a/stage1/MultiClassLoader.scala
+++ b/stage1/MultiClassLoader.scala
@@ -19,7 +19,7 @@ class MultiClassLoader(final val parents: Seq[ClassLoader])(implicit val logger:
// FIXME: is there more than findClass and findResource that needs to be dispatched?
override def findResource(name: String): URL = {
parents.foldLeft(null: URL)(
- (acc, parent) => if( acc == null ) parent.getResource(name) else null
+ (acc, parent) => if( acc == null ) parent.getResource(name) else acc
)
}
override def findResources(name: String): java.util.Enumeration[URL] = {
diff --git a/test/test.scala b/test/test.scala
index 5b4a4af..039ee6a 100644
--- a/test/test.scala
+++ b/test/test.scala
@@ -4,7 +4,8 @@ import java.util.concurrent.ConcurrentHashMap
import java.io.File
import java.nio.file._
import java.net.URL
-
+import scala.concurrent._
+import scala.concurrent.duration._
// micro framework
object Main{
def main(_args: Array[String]): Unit = {
@@ -43,13 +44,17 @@ object Main{
val pb = new ProcessBuilder( allArgs :_* )
pb.directory(cbtHome ++ ("/test/" ++ path))
val p = pb.start
- val berr = new BufferedReader(new InputStreamReader(p.getErrorStream));
- val bout = new BufferedReader(new InputStreamReader(p.getInputStream));
- import collection.JavaConversions._
- val err = Stream.continually(berr.readLine()).takeWhile(_ != null).mkString("\n")
- val out = Stream.continually(bout.readLine()).takeWhile(_ != null).mkString("\n")
+ val serr = new InputStreamReader(p.getErrorStream);
+ val sout = new InputStreamReader(p.getInputStream);
+ import scala.concurrent.ExecutionContext.Implicits.global
+ val err = Future(blocking(Iterator.continually(serr.read).takeWhile(_ != -1).map(_.toChar).mkString))
+ val out = Future(blocking(Iterator.continually(sout.read).takeWhile(_ != -1).map(_.toChar).mkString))
p.waitFor
- Result(p.exitValue == 0, out, err)
+ Result(
+ p.exitValue == 0,
+ Await.result( out, Duration.Inf ),
+ Await.result( err, Duration.Inf )
+ )
}
case class Result(exit0: Boolean, out: String, err: String)
def assertSuccess(res: Result, msg: => String)(implicit logger: Logger) = {
@@ -213,15 +218,15 @@ object Main{
{
val res = task("docJar","simple-fixed-cbt")
- assert( res.out endsWith "simple-fixed-cbt_2.11-0.1-javadoc.jar", res.out )
+ assert( res.out endsWith "simple-fixed-cbt_2.11-0.1-javadoc.jar\n", res.out )
assert( res.err contains "model contains", res.err )
- assert( res.err endsWith "documentable templates", res.err )
+ assert( res.err endsWith "documentable templates\n", res.err )
}
{
val res = runCbt("simple", Seq("printArgs","1","2","3"))
assert(res.exit0)
- assert(res.out == "1 2 3", res.out)
+ assert(res.out == "1 2 3\n", res.out)
}
{
@@ -255,6 +260,12 @@ object Main{
assert(res.err.contains("null is disabled"), res.out)
}
+ {
+ val res = runCbt("../libraries/eval", Seq("test"))
+ assert(res.exit0)
+ assert(res.out.contains("All tests passed"), res.out)
+ }
+
System.err.println(" DONE!")
System.err.println( successes.toString ++ " succeeded, "++ failures.toString ++ " failed" )
if(failures > 0) System.exit(1) else System.exit(0)