diff options
-rw-r--r-- | libraries/eval/Eval.scala | 630 | ||||
-rw-r--r-- | libraries/eval/Readme.md | 7 | ||||
-rw-r--r-- | libraries/eval/build/build.scala | 14 | ||||
-rw-r--r-- | libraries/eval/build/build/build.scala | 7 | ||||
-rw-r--r-- | libraries/eval/test/EvalTest.scala | 268 | ||||
-rw-r--r-- | libraries/eval/test/resources/Base.scala | 4 | ||||
-rw-r--r-- | libraries/eval/test/resources/Deprecated.scala | 6 | ||||
-rw-r--r-- | libraries/eval/test/resources/Derived.scala | 1 | ||||
-rw-r--r-- | libraries/eval/test/resources/DerivedWithInclude.scala | 3 | ||||
-rw-r--r-- | libraries/eval/test/resources/HelloJoe.scala | 3 | ||||
-rw-r--r-- | libraries/eval/test/resources/IncludeInclude.scala | 7 | ||||
-rw-r--r-- | libraries/eval/test/resources/OnePlusOne.scala | 1 | ||||
-rw-r--r-- | libraries/eval/test/resources/RubyInclude.scala | 3 | ||||
-rw-r--r-- | libraries/eval/test/resources/file-with-dash.scala | 1 | ||||
-rw-r--r-- | stage1/MultiClassLoader.scala | 2 | ||||
-rw-r--r-- | test/test.scala | 31 |
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) |