From 0b8b4095379f972b5ad5abf40bf2913c0c524661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Mon, 11 Jul 2016 12:28:00 +0200 Subject: [PATCH 1/6] Port code from runtime to evaluator --- build.sbt | 25 ++++--- src/main/scala/Evaluator.scala | 105 +++++++++++++++++++++++++++++ src/test/scala/EvaluatorSpec.scala | 29 ++++++++ 3 files changed, 149 insertions(+), 10 deletions(-) create mode 100644 src/main/scala/Evaluator.scala create mode 100644 src/test/scala/EvaluatorSpec.scala diff --git a/build.sbt b/build.sbt index b66b7959..1436edaa 100644 --- a/build.sbt +++ b/build.sbt @@ -1,15 +1,20 @@ -scalaVersion := "2.11.8" - lazy val http4sVersion = "0.15.0-SNAPSHOT" -resolvers += Resolver.sonatypeRepo("snapshots") - -libraryDependencies ++= Seq( - "org.http4s" %% "http4s-dsl" % http4sVersion, - "org.http4s" %% "http4s-blaze-server" % http4sVersion, - "org.http4s" %% "http4s-blaze-client" % http4sVersion, - "org.log4s" %% "log4s" % "1.3.0", - "org.slf4j" % "slf4j-simple" % "1.7.21" +lazy val evaluator = (project in file(".")) + .settings( + name := "evaluator", + scalaVersion := "2.11.8", + resolvers += Resolver.sonatypeRepo("snapshots"), + libraryDependencies ++= Seq( + "com.twitter" %% "util-eval" % "6.34.0", + "io.monix" %% "monix" % "2.0-RC8", + "org.http4s" %% "http4s-dsl" % http4sVersion, + "org.http4s" %% "http4s-blaze-server" % http4sVersion, + "org.http4s" %% "http4s-blaze-client" % http4sVersion, + "org.log4s" %% "log4s" % "1.3.0", + "org.slf4j" % "slf4j-simple" % "1.7.21", + "org.scalatest" %% "scalatest" % "2.2.4" % "test" + ) ) enablePlugins(JavaAppPackaging) diff --git a/src/main/scala/Evaluator.scala b/src/main/scala/Evaluator.scala new file mode 100644 index 00000000..2b692333 --- /dev/null +++ b/src/main/scala/Evaluator.scala @@ -0,0 +1,105 @@ +/* + * scala-exercises-evaluator + * Copyright (C) 2015-2016 47 Degrees, LLC. + */ + +package org.scalaexercises.evaluator + +import scala.language.reflectiveCalls + +import java.io.File +import java.nio.file.Path +import java.util.jar.JarFile +import java.util.concurrent.TimeoutException + +import scala.tools.nsc.{ Global, Settings } +import scala.tools.nsc.reporters._ +import scala.tools.nsc.io.{ VirtualDirectory, AbstractFile } +import scala.reflect.internal.util.{ Position, NoPosition, BatchSourceFile, AbstractFileClassLoader } + +import scala.util.Try +import scala.util.control.NonFatal +import scala.concurrent._ +import scala.concurrent.duration._ + +import monix.eval.Task +import monix.execution._ + +import com.twitter.util.Eval + +sealed trait Severity +final case object Info extends Severity +final case object Warning extends Severity +final case object Error extends Severity + +case class RangePosition(start: Int, point: Int, end: Int) +case class CompilationInfo(message: String, pos: Option[RangePosition]) +case class RuntimeError(val error: Throwable, position: Option[Int]) + +sealed trait EvalResult[+T] + +object EvalResult { + type CI = Map[Severity, List[CompilationInfo]] + + case object Timeout extends EvalResult[Nothing] + case class Success[T](complilationInfos: CI, result: T, consoleOutput: String) extends EvalResult[T] + case class Timeout[T]() extends EvalResult[T] + case class EvalRuntimeError(complilationInfos: CI, runtimeError: Option[RuntimeError]) extends EvalResult[Nothing] + case class CompilationError(complilationInfos: CI) extends EvalResult[Nothing] + case class GeneralError(stack: Throwable) extends EvalResult[Nothing] +} + +class Evaluator(timeout: FiniteDuration = 20.seconds) { + implicit val scheduler: Scheduler = Scheduler.io("evaluation-scheduler") + + private[this] def convert(errors: (Position, String, String)): (Severity, List[CompilationInfo]) = { + val (pos, msg, severity) = errors + val sev = severity match { + case "ERROR" ⇒ Error + case "WARNING" ⇒ Warning + case _ ⇒ Info + } + (sev, CompilationInfo(msg, Some(RangePosition(pos.start, pos.point, pos.end))) :: Nil) + } + + private[this] def performEval[T](code: String): EvalResult[T] = { + val eval = new Eval { + @volatile var errors: Map[Severity, List[CompilationInfo]] = Map.empty + + 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 += convert((pos, msg, severity.toString)) + } + override def reset() = { + super.reset() + errors = Map.empty + } + }) + } + + val result = for { + _ ← Try(eval.check(code)) + result ← Try(eval.apply[T](code, resetState = true)) + } yield result + + val errors: Map[Severity, List[CompilationInfo]] = eval.errors.toMap + + result match { + case scala.util.Success(r) ⇒ EvalResult.Success[T](errors, r, "") + case scala.util.Failure(t) ⇒ t match { + case e: Eval.CompilerException ⇒ EvalResult.CompilationError(errors) + case NonFatal(e) ⇒ EvalResult.EvalRuntimeError(errors, Option(RuntimeError(e, None))) + case e ⇒ EvalResult.GeneralError(e) + } + } + } + + def eval[T](code: String): EvalResult[T] = { + val task = Task({ performEval(code) }) + Try( + Await.result(task.runAsync, timeout) + ).getOrElse(EvalResult.Timeout()) + } +} diff --git a/src/test/scala/EvaluatorSpec.scala b/src/test/scala/EvaluatorSpec.scala new file mode 100644 index 00000000..8476f9cf --- /dev/null +++ b/src/test/scala/EvaluatorSpec.scala @@ -0,0 +1,29 @@ +/* + * scala-exercises-evaluator + * Copyright (C) 2015-2016 47 Degrees, LLC. + */ + +package org.scalaexercises.evaluator + +import scala.concurrent.duration._ +import org.scalatest._ + +class EvaluatorSpec extends FunSpec with Matchers { + val evaluator = new Evaluator(1 second) + + describe("evaluation") { + it("can evaluate simple expressions") { + val result: EvalResult[Int] = evaluator.eval("{ 41 + 1 }") + result should matchPattern { + case EvalResult.Success(_, 42, _) ⇒ + } + } + + it("fails with a timeout when takes longer than the configured timeout") { + val result: EvalResult[Int] = evaluator.eval("{ while(true) {}; 123 }") + result should matchPattern { + case t: EvalResult.Timeout[Int] ⇒ + } + } + } +} From a2a8c5c14fd4e77d1e2ba936bcaafb4f9078be16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Tue, 12 Jul 2016 12:38:16 +0200 Subject: [PATCH 2/6] First draft of evaluation with dependency loading --- .gitignore | 3 + build.sbt | 5 +- src/main/scala/Evaluation.scala | 539 +++++++++++++++++++++++++++++ src/main/scala/Evaluator.scala | 69 +++- src/test/scala/EvaluatorSpec.scala | 60 +++- 5 files changed, 665 insertions(+), 11 deletions(-) create mode 100644 src/main/scala/Evaluation.scala diff --git a/.gitignore b/.gitignore index b735d504..2d7bcaf6 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ project/plugins/project/ # Scala-IDE specific .scala_dependencies .worksheet + +# Ensime +.ensime diff --git a/build.sbt b/build.sbt index 1436edaa..0988380a 100644 --- a/build.sbt +++ b/build.sbt @@ -6,13 +6,16 @@ lazy val evaluator = (project in file(".")) scalaVersion := "2.11.8", resolvers += Resolver.sonatypeRepo("snapshots"), libraryDependencies ++= Seq( - "com.twitter" %% "util-eval" % "6.34.0", + "org.scala-lang" % "scala-compiler" % scalaVersion.value, + "com.twitter" %% "util-core" % "6.34.0", "io.monix" %% "monix" % "2.0-RC8", "org.http4s" %% "http4s-dsl" % http4sVersion, "org.http4s" %% "http4s-blaze-server" % http4sVersion, "org.http4s" %% "http4s-blaze-client" % http4sVersion, "org.log4s" %% "log4s" % "1.3.0", "org.slf4j" % "slf4j-simple" % "1.7.21", + "io.get-coursier" %% "coursier" % "1.0.0-M12", + "io.get-coursier" %% "coursier-cache" % "1.0.0-M12", "org.scalatest" %% "scalatest" % "2.2.4" % "test" ) ) diff --git a/src/main/scala/Evaluation.scala b/src/main/scala/Evaluation.scala new file mode 100644 index 00000000..d2908325 --- /dev/null +++ b/src/main/scala/Evaluation.scala @@ -0,0 +1,539 @@ +/* + * 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 com.twitter.util + +import com.twitter.conversions.string._ +import com.twitter.io.StreamIO +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} +import scala.tools.nsc.interpreter.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 + + + +/** + * 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 `. + * + * 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] = None, jars: List[File] = Nil) { + val classCleaner: Regex = "\\W".r + + 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 ClassScopedResolver(getClass), + 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() + } + + /** + * Will generate a classname of the form Evaluater__, + * where unique is computed from the jvmID (a random number) + * and a digest of code + */ + def applyProcessed[T](code: String, resetState: Boolean, jars: Seq[File]): T = { + val id = uniqueId(code) + val className = "Evaluator__" + id + applyProcessed(className, code, resetState, jars) + } + + /** + * same as apply[T], but does not run preprocessors. + */ + def applyProcessed[T](className: String, code: String, resetState: Boolean, jars: Seq[File]): T = { + val jarUrls = jars.map(jar => new java.net.URL(s"file://${jar.getAbsolutePath}")).toArray + val urlClassLoader = new URLClassLoader(jarUrls , compiler.getClass.getClassLoader) + val classLoader = new AbstractFileClassLoader(compiler.target, urlClassLoader) + + val cls = compiler( + wrapCodeInClass(className, code), className, resetState, classLoader + ) + cls.getConstructor().newInstance().asInstanceOf[() => T].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)) + } + + /** + * 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) + 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) + } + + private[util] def uniqueId(code: String, idOpt: Option[Int] = Some(Eval.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 + "_" + id + case _ => sha + } + } + + private[util] 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) + } + baseName.regexSub(classCleaner) { m => + "$%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 ClassScopedResolver(clazz: Class[_]) extends Resolver { + private[this] def quotePath(path: String) = + "/" + path + + def resolvable(path: String): Boolean = + clazz.getResourceAsStream(quotePath(path)) != null + + def get(path: String): InputStream = + clazz.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 { + apply(StreamIO.buffer(r.get(path)).toString, 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) + } + + /** + * 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.inUltimateSource(pos.source).lineContent.stripLineEnd :: + (" " * (pos.column - 1) + "^") :: + Nil + } else { + Nil + }) + } + + def displayPrompt { + // no. + } + + override def reset { + super.reset + messages.clear() + } + } + + val global = new Global(settings, reporter) + + 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() + } + + 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, classLoader: ClassLoader): 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) + + // 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 Eval.CompilerException(msgs) + } + } + + /** + * Compile a new class, load it, and return it. Thread-safe. + */ + def apply(code: String, className: String, resetState: Boolean = true, classLoader: ClassLoader): Class[_] = { + synchronized { + if (resetState) reset() + + apply(code) + findClass(className, classLoader).get // fixme + } + } + } +} + + +object Eval { + private val jvmId = java.lang.Math.abs(new Random().nextInt()) + + class CompilerException(val messages: List[List[String]]) extends Exception( + "Compiler exception " + messages.map(_.mkString("\n")).mkString("\n")) +} diff --git a/src/main/scala/Evaluator.scala b/src/main/scala/Evaluator.scala index 2b692333..c9a78e5b 100644 --- a/src/main/scala/Evaluator.scala +++ b/src/main/scala/Evaluator.scala @@ -7,7 +7,7 @@ package org.scalaexercises.evaluator import scala.language.reflectiveCalls -import java.io.File +import java.io.{ File, InputStream } import java.nio.file.Path import java.util.jar.JarFile import java.util.concurrent.TimeoutException @@ -17,15 +17,19 @@ import scala.tools.nsc.reporters._ import scala.tools.nsc.io.{ VirtualDirectory, AbstractFile } import scala.reflect.internal.util.{ Position, NoPosition, BatchSourceFile, AbstractFileClassLoader } +import scalaz._; import Scalaz._ import scala.util.Try import scala.util.control.NonFatal import scala.concurrent._ import scala.concurrent.duration._ +import scalaz.concurrent.{ Task => ZTask } import monix.eval.Task import monix.execution._ -import com.twitter.util.Eval +import com.twitter.util.Eval; import Eval._ + +import coursier._ sealed trait Severity final case object Info extends Severity @@ -52,6 +56,9 @@ object EvalResult { class Evaluator(timeout: FiniteDuration = 20.seconds) { implicit val scheduler: Scheduler = Scheduler.io("evaluation-scheduler") + type Dependency = (String, String, String) + type Remote = String + private[this] def convert(errors: (Position, String, String)): (Severity, List[CompilationInfo]) = { val (pos, msg, severity) = errors val sev = severity match { @@ -62,10 +69,40 @@ class Evaluator(timeout: FiniteDuration = 20.seconds) { (sev, CompilationInfo(msg, Some(RangePosition(pos.start, pos.point, pos.end))) :: Nil) } - private[this] def performEval[T](code: String): EvalResult[T] = { - val eval = new Eval { + def resolveArtifacts(remotes: Seq[Remote], dependencies: Seq[Dependency]): Resolution = { + val resolution = Resolution( + dependencies.map(d => { + Dependency( + Module(d._1, d._2), d._3 + ) + }).toSet + ) + val repositories: Seq[Repository] = remotes.map(url => MavenRepository(url)) + + val fetch = Fetch.from(repositories, Cache.fetch()) + resolution.process.run(fetch).run + } + + def fetchArtifacts(resolution: Resolution): List[coursier.FileError \/ File] = { + if (resolution.isDone) + ZTask.gatherUnordered( + resolution.artifacts.map(Cache.file(_).run) + ).run + else + Nil + } + + def createEval(jars: Seq[File]) = { + new Eval(jars = jars.toList) { @volatile var errors: Map[Severity, List[CompilationInfo]] = Map.empty + override lazy val compilerSettings: Settings = new EvalSettings(None){ + if (!jars.isEmpty) { + val newJars = jars.mkString(File.pathSeparator) + classpath.value = newJars + File.pathSeparator + classpath.value + } + } + override lazy val compilerMessageHandler: Option[Reporter] = Some(new AbstractReporter { override val settings: Settings = compilerSettings override def displayPrompt(): Unit = () @@ -78,10 +115,14 @@ class Evaluator(timeout: FiniteDuration = 20.seconds) { } }) } + } + + private[this] def performEval[T](code: String, jars: Seq[File]): EvalResult[T] = { + val eval = createEval(jars) val result = for { _ ← Try(eval.check(code)) - result ← Try(eval.apply[T](code, resetState = true)) + result ← Try(eval.applyProcessed[T](code, resetState = true, jars = jars)) } yield result val errors: Map[Severity, List[CompilationInfo]] = eval.errors.toMap @@ -96,10 +137,22 @@ class Evaluator(timeout: FiniteDuration = 20.seconds) { } } - def eval[T](code: String): EvalResult[T] = { - val task = Task({ performEval(code) }) + def eval[T](code: String, remotes: Seq[Remote] = Nil, dependencies: Seq[Dependency] = Nil): EvalResult[T] = { + // todo: don't take into account dependency resolution time in timeout + val resolution = resolveArtifacts(remotes, dependencies) + val allJars = fetchArtifacts(resolution).sequenceU + + val jars: Seq[File] = allJars match { + case \/-(jars) => jars + case -\/(fileError) => Nil // todo: handle errors + } + + val evaluation = Task({ + performEval(code, jars) + }) // todo + Try( - Await.result(task.runAsync, timeout) + Await.result(evaluation.runAsync, timeout) ).getOrElse(EvalResult.Timeout()) } } diff --git a/src/test/scala/EvaluatorSpec.scala b/src/test/scala/EvaluatorSpec.scala index 8476f9cf..9bf85e38 100644 --- a/src/test/scala/EvaluatorSpec.scala +++ b/src/test/scala/EvaluatorSpec.scala @@ -9,11 +9,12 @@ import scala.concurrent.duration._ import org.scalatest._ class EvaluatorSpec extends FunSpec with Matchers { - val evaluator = new Evaluator(1 second) + val evaluator = new Evaluator(10 seconds) describe("evaluation") { it("can evaluate simple expressions") { val result: EvalResult[Int] = evaluator.eval("{ 41 + 1 }") + result should matchPattern { case EvalResult.Success(_, 42, _) ⇒ } @@ -21,8 +22,63 @@ class EvaluatorSpec extends FunSpec with Matchers { it("fails with a timeout when takes longer than the configured timeout") { val result: EvalResult[Int] = evaluator.eval("{ while(true) {}; 123 }") + result should matchPattern { - case t: EvalResult.Timeout[Int] ⇒ + case t: EvalResult.Timeout[_] ⇒ + } + } + + it("can load dependencies for an evaluation") { + val code = """ + import cats._ + + Eval.now(42).value + """ + val remotes = List("https://oss.sonatype.org/content/repositories/releases/") + val dependencies = List( + ("org.typelevel", "cats_2.11", "0.6.0") + ) + + val result: EvalResult[Int] = evaluator.eval( + code, + remotes = remotes, + dependencies = dependencies + ) + + result should matchPattern { + case EvalResult.Success(_, 42, _) => + } + } + + it("can load different versions of a dependency across evaluations") { + val code = """ +import cats._ +Eval.now(42).value + """ + val remotes = List("https://oss.sonatype.org/content/repositories/releases/") + val dependencies1 = List( + ("org.typelevel", "cats_2.11", "0.4.1") + ) + val dependencies2 = List( + ("org.typelevel", "cats_2.11", "0.6.0") + ) + + val result1: EvalResult[Int] = evaluator.eval( + code, + remotes = remotes, + dependencies = dependencies1 + ) + val result2: EvalResult[Int] = evaluator.eval( + code, + remotes = remotes, + dependencies = dependencies2 + ) + + result1 should matchPattern { + case EvalResult.Success(_, 42, _) => + } + result2 should matchPattern { + case EvalResult.Success(_, 42, _) => } } } From 2831b704ba4874085ea8a595916652a161273067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Tue, 12 Jul 2016 13:02:15 +0200 Subject: [PATCH 3/6] One more test --- src/test/scala/EvaluatorSpec.scala | 46 ++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/test/scala/EvaluatorSpec.scala b/src/test/scala/EvaluatorSpec.scala index 9bf85e38..d4ec99a2 100644 --- a/src/test/scala/EvaluatorSpec.scala +++ b/src/test/scala/EvaluatorSpec.scala @@ -30,9 +30,9 @@ class EvaluatorSpec extends FunSpec with Matchers { it("can load dependencies for an evaluation") { val code = """ - import cats._ +import cats._ - Eval.now(42).value +Eval.now(42).value """ val remotes = List("https://oss.sonatype.org/content/repositories/releases/") val dependencies = List( @@ -81,5 +81,47 @@ Eval.now(42).value case EvalResult.Success(_, 42, _) => } } + + it("can run code from the exercises content") { + val code = """ +import stdlib._ +Asserts.scalaTestAsserts(true) +""" + val remotes = List("https://oss.sonatype.org/content/repositories/releases/") + val dependencies = List( + ("org.scala-exercises", "exercises-stdlib_2.11", "0.2.0") + ) + + val result: EvalResult[Unit] = evaluator.eval( + code, + remotes = remotes, + dependencies = dependencies + ) + + result should matchPattern { + case EvalResult.Success(_, (), _) => + } + } + + it("captures exceptions when running the exercises content") { + val code = """ +import stdlib._ +Asserts.scalaTestAsserts(false) +""" + val remotes = List("https://oss.sonatype.org/content/repositories/releases/") + val dependencies = List( + ("org.scala-exercises", "exercises-stdlib_2.11", "0.2.0") + ) + + val result: EvalResult[Unit] = evaluator.eval( + code, + remotes = remotes, + dependencies = dependencies + ) + + result should matchPattern { + case EvalResult.EvalRuntimeError(_, Some(RuntimeError(err: TestFailedException, _))) => + } + } } } From d198f6d954329fbc5be0fb956677436c21904a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Tue, 12 Jul 2016 13:28:36 +0200 Subject: [PATCH 4/6] Clean up evaluation code --- src/main/scala/Evaluation.scala | 450 ++++++++++---------------------- 1 file changed, 141 insertions(+), 309 deletions(-) diff --git a/src/main/scala/Evaluation.scala b/src/main/scala/Evaluation.scala index d2908325..757b7ca5 100644 --- a/src/main/scala/Evaluation.scala +++ b/src/main/scala/Evaluation.scala @@ -33,6 +33,128 @@ import scala.tools.nsc.reporters.{Reporter, AbstractReporter} import scala.tools.nsc.{Global, Settings} import scala.util.matching.Regex +/** + * 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], + output: AbstractFile, + settings: Settings, + messageHandler: Option[Reporter] +) { + + val cache = new mutable.HashMap[String, Class[_]]() + + 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.inUltimateSource(pos.source).lineContent.stripLineEnd :: + (" " * (pos.column - 1) + "^") :: + Nil + } else { + Nil + }) + } + + def displayPrompt { + // no. + } + + override def reset { + super.reset + messages.clear() + } + } + + val global = new Global(settings, reporter) + + def reset() { + targetDir match { + case None => { + output.asInstanceOf[VirtualDirectory].clear() + } + case Some(t) => { + output.foreach { abstractFile => + if (abstractFile.file == null || abstractFile.file.getName.endsWith(".class")) { + abstractFile.delete() + } + } + } + } + cache.clear() + reporter.reset() + } + + def findClass(className: String, classLoader: ClassLoader): 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 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 Eval.CompilerException(msgs) + } + } + + /** + * Compile a new class, load it, and return it. Thread-safe. + */ + def apply(code: String, className: String, resetState: Boolean = true, classLoader: ClassLoader): Class[_] = { + synchronized { + if (resetState) reset() + + apply(code) + findClass(className, classLoader).get // fixme + } + } +} /** @@ -40,22 +162,16 @@ import scala.util.matching.Regex * * 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 `. + * that directory. You can optionally pass a list of JARs to include to the classpath during + * compilation and evaluation. * * 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 + * - wrap code in an `apply` method in a generated class + * - compile the class adding the jars to the classpath * - contruct an instance of that class * - return the result of `apply()` */ class Eval(target: Option[File] = None, jars: List[File] = Nil) { - val classCleaner: Regex = "\\W".r - private lazy val compilerPath = try { classPathOfClass("scala.tools.nsc.Interpreter") } catch { @@ -70,28 +186,6 @@ class Eval(target: Option[File] = None, jars: List[File] = Nil) { 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 ClassScopedResolver(getClass), - 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 @@ -102,28 +196,11 @@ class Eval(target: Option[File] = None, jars: List[File] = Nil) { private[this] lazy val compiler = new StringCompiler( codeWrapperLineOffset, target, + compilerOutputDir, 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() - } - /** * Will generate a classname of the form Evaluater__, * where unique is computed from the jvmID (a random number) @@ -136,12 +213,11 @@ class Eval(target: Option[File] = None, jars: List[File] = Nil) { } /** - * same as apply[T], but does not run preprocessors. */ def applyProcessed[T](className: String, code: String, resetState: Boolean, jars: Seq[File]): T = { val jarUrls = jars.map(jar => new java.net.URL(s"file://${jar.getAbsolutePath}")).toArray val urlClassLoader = new URLClassLoader(jarUrls , compiler.getClass.getClassLoader) - val classLoader = new AbstractFileClassLoader(compiler.target, urlClassLoader) + val classLoader = new AbstractFileClassLoader(compilerOutputDir, urlClassLoader) val cls = compiler( wrapCodeInClass(className, code), className, resetState, classLoader @@ -150,25 +226,17 @@ class Eval(target: Option[File] = None, jars: List[File] = Nil) { } /** - * converts the given file to evaluable source. - * delegates to toSource(code: String) + * Converts the given file to evaluable source. */ 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) + Source.fromFile(file).mkString } /** * Compile an entire source file into the virtual classloader. */ def compile(code: String) { - compiler(sourceForString(code)) + compiler(code) } /** @@ -176,7 +244,7 @@ class Eval(target: Option[File] = None, jars: List[File] = Nil) { * @throws CompilerException if not Eval-able. */ def check(code: String) { - val id = uniqueId(sourceForString(code)) + val id = uniqueId(code) val className = "Evaluator__" + id val wrappedCode = wrapCodeInClass(className, code) compile(wrappedCode) // may throw CompilerException @@ -187,7 +255,7 @@ class Eval(target: Option[File] = None, jars: List[File] = Nil) { * @throws CompilerException if not Eval-able. */ def check(files: File*) { - val code = files.map { scala.io.Source.fromFile(_).mkString }.mkString("\n") + val code = files.map { Source.fromFile(_).mkString }.mkString("\n") check(code) } @@ -196,7 +264,7 @@ class Eval(target: Option[File] = None, jars: List[File] = Nil) { * @throws CompilerException if not Eval-able. */ def check(stream: InputStream) { - check(scala.io.Source.fromInputStream(stream).mkString) + check(Source.fromInputStream(stream).mkString) } private[util] def uniqueId(code: String, idOpt: Option[Int] = Some(Eval.jvmId)): String = { @@ -208,35 +276,18 @@ class Eval(target: Option[File] = None, jars: List[File] = Nil) { } } - private[util] 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) - } - baseName.regexSub(classCleaner) { m => - "$%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" + s""" +class ${className} extends (() => Any) with java.io.Serializable { + def apply() = { + $code + } +} +""" } /* @@ -297,84 +348,6 @@ class Eval(target: Option[File] = None, jars: List[File] = 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 ClassScopedResolver(clazz: Class[_]) extends Resolver { - private[this] def quotePath(path: String) = - "/" + path - - def resolvable(path: String): Boolean = - clazz.getResourceAsStream(quotePath(path)) != null - - def get(path: String): InputStream = - clazz.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 { - apply(StreamIO.buffer(r.get(path)).toString, 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) @@ -387,147 +360,6 @@ class Eval(target: Option[File] = None, jars: List[File] = Nil) { bootclasspath.value = pathList.mkString(File.pathSeparator) classpath.value = (pathList ::: impliedClassPath).mkString(File.pathSeparator) } - - /** - * 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.inUltimateSource(pos.source).lineContent.stripLineEnd :: - (" " * (pos.column - 1) + "^") :: - Nil - } else { - Nil - }) - } - - def displayPrompt { - // no. - } - - override def reset { - super.reset - messages.clear() - } - } - - val global = new Global(settings, reporter) - - 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() - } - - 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, classLoader: ClassLoader): 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) - - // 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 Eval.CompilerException(msgs) - } - } - - /** - * Compile a new class, load it, and return it. Thread-safe. - */ - def apply(code: String, className: String, resetState: Boolean = true, classLoader: ClassLoader): Class[_] = { - synchronized { - if (resetState) reset() - - apply(code) - findClass(className, classLoader).get // fixme - } - } - } } From 8f27978019a47cd1e95377a60b5f126baf9aee89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Tue, 12 Jul 2016 15:36:11 +0200 Subject: [PATCH 5/6] Do IO in the Task context --- src/main/scala/Evaluation.scala | 8 ++- src/main/scala/Evaluator.scala | 82 +++++++++++++++--------------- src/test/scala/EvaluatorSpec.scala | 18 ++++--- 3 files changed, 53 insertions(+), 55 deletions(-) diff --git a/src/main/scala/Evaluation.scala b/src/main/scala/Evaluation.scala index 757b7ca5..f4a9ba99 100644 --- a/src/main/scala/Evaluation.scala +++ b/src/main/scala/Evaluation.scala @@ -206,15 +206,13 @@ class Eval(target: Option[File] = None, jars: List[File] = Nil) { * where unique is computed from the jvmID (a random number) * and a digest of code */ - def applyProcessed[T](code: String, resetState: Boolean, jars: Seq[File]): T = { + def execute[T](code: String, resetState: Boolean, jars: Seq[File]): T = { val id = uniqueId(code) val className = "Evaluator__" + id - applyProcessed(className, code, resetState, jars) + execute(className, code, resetState, jars) } - /** - */ - def applyProcessed[T](className: String, code: String, resetState: Boolean, jars: Seq[File]): T = { + def execute[T](className: String, code: String, resetState: Boolean, jars: Seq[File]): T = { val jarUrls = jars.map(jar => new java.net.URL(s"file://${jar.getAbsolutePath}")).toArray val urlClassLoader = new URLClassLoader(jarUrls , compiler.getClass.getClassLoader) val classLoader = new AbstractFileClassLoader(compilerOutputDir, urlClassLoader) diff --git a/src/main/scala/Evaluator.scala b/src/main/scala/Evaluator.scala index c9a78e5b..b420dac2 100644 --- a/src/main/scala/Evaluator.scala +++ b/src/main/scala/Evaluator.scala @@ -22,10 +22,9 @@ import scala.util.Try import scala.util.control.NonFatal import scala.concurrent._ import scala.concurrent.duration._ -import scalaz.concurrent.{ Task => ZTask } +import scalaz.concurrent.Task -import monix.eval.Task -import monix.execution._ +import monix.execution.Scheduler import com.twitter.util.Eval; import Eval._ @@ -48,14 +47,15 @@ object EvalResult { case object Timeout extends EvalResult[Nothing] case class Success[T](complilationInfos: CI, result: T, consoleOutput: String) extends EvalResult[T] case class Timeout[T]() extends EvalResult[T] + case class UnresolvedDependency(error: coursier.FileError) extends EvalResult[Nothing] case class EvalRuntimeError(complilationInfos: CI, runtimeError: Option[RuntimeError]) extends EvalResult[Nothing] case class CompilationError(complilationInfos: CI) extends EvalResult[Nothing] case class GeneralError(stack: Throwable) extends EvalResult[Nothing] } -class Evaluator(timeout: FiniteDuration = 20.seconds) { - implicit val scheduler: Scheduler = Scheduler.io("evaluation-scheduler") - +class Evaluator(timeout: FiniteDuration = 20.seconds)( + implicit S: Scheduler +) { type Dependency = (String, String, String) type Remote = String @@ -69,28 +69,27 @@ class Evaluator(timeout: FiniteDuration = 20.seconds) { (sev, CompilationInfo(msg, Some(RangePosition(pos.start, pos.point, pos.end))) :: Nil) } - def resolveArtifacts(remotes: Seq[Remote], dependencies: Seq[Dependency]): Resolution = { - val resolution = Resolution( - dependencies.map(d => { - Dependency( - Module(d._1, d._2), d._3 - ) - }).toSet + def remoteToRepository(remote: Remote): Repository = + MavenRepository(remote) + + def dependencyToModule(dependency: Dependency): coursier.Dependency = + coursier.Dependency( + Module(dependency._1, dependency._2), dependency._3 ) - val repositories: Seq[Repository] = remotes.map(url => MavenRepository(url)) + def resolveArtifacts(remotes: Seq[Remote], dependencies: Seq[Dependency]): Task[Resolution] = { + val resolution = Resolution(dependencies.map(dependencyToModule).toSet) + val repositories: Seq[Repository] = remotes.map(remoteToRepository) val fetch = Fetch.from(repositories, Cache.fetch()) - resolution.process.run(fetch).run + resolution.process.run(fetch) } - def fetchArtifacts(resolution: Resolution): List[coursier.FileError \/ File] = { - if (resolution.isDone) - ZTask.gatherUnordered( - resolution.artifacts.map(Cache.file(_).run) - ).run - else - Nil - } + def fetchArtifacts(remotes: Seq[Remote], dependencies: Seq[Dependency]): Task[coursier.FileError \/ List[File]] = for { + resolution <- resolveArtifacts(remotes, dependencies) + artifacts <- Task.gatherUnordered( + resolution.artifacts.map(Cache.file(_).run) + ) + } yield artifacts.sequenceU def createEval(jars: Seq[File]) = { new Eval(jars = jars.toList) { @@ -117,12 +116,12 @@ class Evaluator(timeout: FiniteDuration = 20.seconds) { } } - private[this] def performEval[T](code: String, jars: Seq[File]): EvalResult[T] = { + private[this] def evaluate[T](code: String, jars: Seq[File]): EvalResult[T] = { val eval = createEval(jars) val result = for { _ ← Try(eval.check(code)) - result ← Try(eval.applyProcessed[T](code, resetState = true, jars = jars)) + result ← Try(eval.execute[T](code, resetState = true, jars = jars)) } yield result val errors: Map[Severity, List[CompilationInfo]] = eval.errors.toMap @@ -137,22 +136,21 @@ class Evaluator(timeout: FiniteDuration = 20.seconds) { } } - def eval[T](code: String, remotes: Seq[Remote] = Nil, dependencies: Seq[Dependency] = Nil): EvalResult[T] = { - // todo: don't take into account dependency resolution time in timeout - val resolution = resolveArtifacts(remotes, dependencies) - val allJars = fetchArtifacts(resolution).sequenceU - - val jars: Seq[File] = allJars match { - case \/-(jars) => jars - case -\/(fileError) => Nil // todo: handle errors - } - - val evaluation = Task({ - performEval(code, jars) - }) // todo - - Try( - Await.result(evaluation.runAsync, timeout) - ).getOrElse(EvalResult.Timeout()) + def eval[T]( + code: String, + remotes: Seq[Remote] = Nil, + dependencies: Seq[Dependency] = Nil + ): Task[EvalResult[T]] = { + for { + allJars <- fetchArtifacts(remotes, dependencies) + result <- allJars match { + case \/-(jars) => Task({ + evaluate(code, jars) + }).timed(timeout).handle({ + case err: TimeoutException => EvalResult.Timeout[T]() + }) + case -\/(fileError) => Task.now(EvalResult.UnresolvedDependency(fileError)) + } + } yield result } } diff --git a/src/test/scala/EvaluatorSpec.scala b/src/test/scala/EvaluatorSpec.scala index d4ec99a2..4100b4e3 100644 --- a/src/test/scala/EvaluatorSpec.scala +++ b/src/test/scala/EvaluatorSpec.scala @@ -6,14 +6,16 @@ package org.scalaexercises.evaluator import scala.concurrent.duration._ +import monix.execution.Scheduler import org.scalatest._ class EvaluatorSpec extends FunSpec with Matchers { - val evaluator = new Evaluator(10 seconds) + implicit val scheduler: Scheduler = Scheduler.io("exercises-spec") + val evaluator = new Evaluator(20 seconds) describe("evaluation") { it("can evaluate simple expressions") { - val result: EvalResult[Int] = evaluator.eval("{ 41 + 1 }") + val result: EvalResult[Int] = evaluator.eval("{ 41 + 1 }").run result should matchPattern { case EvalResult.Success(_, 42, _) ⇒ @@ -21,7 +23,7 @@ class EvaluatorSpec extends FunSpec with Matchers { } it("fails with a timeout when takes longer than the configured timeout") { - val result: EvalResult[Int] = evaluator.eval("{ while(true) {}; 123 }") + val result: EvalResult[Int] = evaluator.eval("{ while(true) {}; 123 }").run result should matchPattern { case t: EvalResult.Timeout[_] ⇒ @@ -43,7 +45,7 @@ Eval.now(42).value code, remotes = remotes, dependencies = dependencies - ) + ).run result should matchPattern { case EvalResult.Success(_, 42, _) => @@ -67,12 +69,12 @@ Eval.now(42).value code, remotes = remotes, dependencies = dependencies1 - ) + ).run val result2: EvalResult[Int] = evaluator.eval( code, remotes = remotes, dependencies = dependencies2 - ) + ).run result1 should matchPattern { case EvalResult.Success(_, 42, _) => @@ -96,7 +98,7 @@ Asserts.scalaTestAsserts(true) code, remotes = remotes, dependencies = dependencies - ) + ).run result should matchPattern { case EvalResult.Success(_, (), _) => @@ -117,7 +119,7 @@ Asserts.scalaTestAsserts(false) code, remotes = remotes, dependencies = dependencies - ) + ).run result should matchPattern { case EvalResult.EvalRuntimeError(_, Some(RuntimeError(err: TestFailedException, _))) => From 45c05b3cf9c3fa6d7cc9d4d3431fb12f028268d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Tue, 12 Jul 2016 16:12:57 +0200 Subject: [PATCH 6/6] Cleanup for submitting a PR --- build.sbt | 1 - src/main/scala/Evaluator.scala | 156 -------------- .../{Evaluation.scala => evaluation.scala} | 201 ++++++++++++------ src/main/scala/types.scala | 26 +++ src/test/scala/EvaluatorSpec.scala | 14 +- 5 files changed, 170 insertions(+), 228 deletions(-) delete mode 100644 src/main/scala/Evaluator.scala rename src/main/scala/{Evaluation.scala => evaluation.scala} (67%) create mode 100644 src/main/scala/types.scala diff --git a/build.sbt b/build.sbt index 0988380a..b7b72a61 100644 --- a/build.sbt +++ b/build.sbt @@ -7,7 +7,6 @@ lazy val evaluator = (project in file(".")) resolvers += Resolver.sonatypeRepo("snapshots"), libraryDependencies ++= Seq( "org.scala-lang" % "scala-compiler" % scalaVersion.value, - "com.twitter" %% "util-core" % "6.34.0", "io.monix" %% "monix" % "2.0-RC8", "org.http4s" %% "http4s-dsl" % http4sVersion, "org.http4s" %% "http4s-blaze-server" % http4sVersion, diff --git a/src/main/scala/Evaluator.scala b/src/main/scala/Evaluator.scala deleted file mode 100644 index b420dac2..00000000 --- a/src/main/scala/Evaluator.scala +++ /dev/null @@ -1,156 +0,0 @@ -/* - * scala-exercises-evaluator - * Copyright (C) 2015-2016 47 Degrees, LLC. - */ - -package org.scalaexercises.evaluator - -import scala.language.reflectiveCalls - -import java.io.{ File, InputStream } -import java.nio.file.Path -import java.util.jar.JarFile -import java.util.concurrent.TimeoutException - -import scala.tools.nsc.{ Global, Settings } -import scala.tools.nsc.reporters._ -import scala.tools.nsc.io.{ VirtualDirectory, AbstractFile } -import scala.reflect.internal.util.{ Position, NoPosition, BatchSourceFile, AbstractFileClassLoader } - -import scalaz._; import Scalaz._ -import scala.util.Try -import scala.util.control.NonFatal -import scala.concurrent._ -import scala.concurrent.duration._ -import scalaz.concurrent.Task - -import monix.execution.Scheduler - -import com.twitter.util.Eval; import Eval._ - -import coursier._ - -sealed trait Severity -final case object Info extends Severity -final case object Warning extends Severity -final case object Error extends Severity - -case class RangePosition(start: Int, point: Int, end: Int) -case class CompilationInfo(message: String, pos: Option[RangePosition]) -case class RuntimeError(val error: Throwable, position: Option[Int]) - -sealed trait EvalResult[+T] - -object EvalResult { - type CI = Map[Severity, List[CompilationInfo]] - - case object Timeout extends EvalResult[Nothing] - case class Success[T](complilationInfos: CI, result: T, consoleOutput: String) extends EvalResult[T] - case class Timeout[T]() extends EvalResult[T] - case class UnresolvedDependency(error: coursier.FileError) extends EvalResult[Nothing] - case class EvalRuntimeError(complilationInfos: CI, runtimeError: Option[RuntimeError]) extends EvalResult[Nothing] - case class CompilationError(complilationInfos: CI) extends EvalResult[Nothing] - case class GeneralError(stack: Throwable) extends EvalResult[Nothing] -} - -class Evaluator(timeout: FiniteDuration = 20.seconds)( - implicit S: Scheduler -) { - type Dependency = (String, String, String) - type Remote = String - - private[this] def convert(errors: (Position, String, String)): (Severity, List[CompilationInfo]) = { - val (pos, msg, severity) = errors - val sev = severity match { - case "ERROR" ⇒ Error - case "WARNING" ⇒ Warning - case _ ⇒ Info - } - (sev, CompilationInfo(msg, Some(RangePosition(pos.start, pos.point, pos.end))) :: Nil) - } - - def remoteToRepository(remote: Remote): Repository = - MavenRepository(remote) - - def dependencyToModule(dependency: Dependency): coursier.Dependency = - coursier.Dependency( - Module(dependency._1, dependency._2), dependency._3 - ) - - def resolveArtifacts(remotes: Seq[Remote], dependencies: Seq[Dependency]): Task[Resolution] = { - val resolution = Resolution(dependencies.map(dependencyToModule).toSet) - val repositories: Seq[Repository] = remotes.map(remoteToRepository) - val fetch = Fetch.from(repositories, Cache.fetch()) - resolution.process.run(fetch) - } - - def fetchArtifacts(remotes: Seq[Remote], dependencies: Seq[Dependency]): Task[coursier.FileError \/ List[File]] = for { - resolution <- resolveArtifacts(remotes, dependencies) - artifacts <- Task.gatherUnordered( - resolution.artifacts.map(Cache.file(_).run) - ) - } yield artifacts.sequenceU - - def createEval(jars: Seq[File]) = { - new Eval(jars = jars.toList) { - @volatile var errors: Map[Severity, List[CompilationInfo]] = Map.empty - - override lazy val compilerSettings: Settings = new EvalSettings(None){ - if (!jars.isEmpty) { - val newJars = jars.mkString(File.pathSeparator) - classpath.value = newJars + File.pathSeparator + classpath.value - } - } - - 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 += convert((pos, msg, severity.toString)) - } - override def reset() = { - super.reset() - errors = Map.empty - } - }) - } - } - - private[this] def evaluate[T](code: String, jars: Seq[File]): EvalResult[T] = { - val eval = createEval(jars) - - val result = for { - _ ← Try(eval.check(code)) - result ← Try(eval.execute[T](code, resetState = true, jars = jars)) - } yield result - - val errors: Map[Severity, List[CompilationInfo]] = eval.errors.toMap - - result match { - case scala.util.Success(r) ⇒ EvalResult.Success[T](errors, r, "") - case scala.util.Failure(t) ⇒ t match { - case e: Eval.CompilerException ⇒ EvalResult.CompilationError(errors) - case NonFatal(e) ⇒ EvalResult.EvalRuntimeError(errors, Option(RuntimeError(e, None))) - case e ⇒ EvalResult.GeneralError(e) - } - } - } - - def eval[T]( - code: String, - remotes: Seq[Remote] = Nil, - dependencies: Seq[Dependency] = Nil - ): Task[EvalResult[T]] = { - for { - allJars <- fetchArtifacts(remotes, dependencies) - result <- allJars match { - case \/-(jars) => Task({ - evaluate(code, jars) - }).timed(timeout).handle({ - case err: TimeoutException => EvalResult.Timeout[T]() - }) - case -\/(fileError) => Task.now(EvalResult.UnresolvedDependency(fileError)) - } - } yield result - } -} diff --git a/src/main/scala/Evaluation.scala b/src/main/scala/evaluation.scala similarity index 67% rename from src/main/scala/Evaluation.scala rename to src/main/scala/evaluation.scala index f4a9ba99..f401cf17 100644 --- a/src/main/scala/Evaluation.scala +++ b/src/main/scala/evaluation.scala @@ -1,37 +1,141 @@ /* - * 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. + * scala-exercises-evaluator + * Copyright (C) 2015-2016 47 Degrees, LLC. */ -package com.twitter.util +package org.scalaexercises.evaluator -import com.twitter.conversions.string._ -import com.twitter.io.StreamIO -import java.io._ -import java.math.BigInteger + +import scala.language.reflectiveCalls + +import java.io.{ File, InputStream } import java.net.URLClassLoader -import java.security.MessageDigest -import java.util.Random +import java.nio.file.Path import java.util.jar.JarFile -import scala.collection.mutable -import scala.io.Source -import scala.reflect.internal.util.{BatchSourceFile, Position} -import scala.tools.nsc.interpreter.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 +import java.util.concurrent.TimeoutException +import java.security.MessageDigest +import java.math.BigInteger + +import scala.tools.nsc.{ Global, Settings } +import scala.tools.nsc.reporters._ +import scala.tools.nsc.io.{ VirtualDirectory, AbstractFile } +import scala.reflect.internal.util.{ Position, NoPosition, BatchSourceFile, AbstractFileClassLoader } + +import scalaz._; import Scalaz._ +import scala.util.Try +import scala.util.control.NonFatal +import scala.concurrent._ +import scala.concurrent.duration._ +import scalaz.concurrent.Task + +import monix.execution.Scheduler + +import coursier._ + +import org.scalaexercises.evaluator._ + + +class Evaluator(timeout: FiniteDuration = 20.seconds)( + implicit S: Scheduler +) { + type Dependency = (String, String, String) + type Remote = String + + private[this] def convert(errors: (Position, String, String)): (Severity, List[CompilationInfo]) = { + val (pos, msg, severity) = errors + val sev = severity match { + case "ERROR" ⇒ Error + case "WARNING" ⇒ Warning + case _ ⇒ Informational + } + (sev, CompilationInfo(msg, Some(RangePosition(pos.start, pos.point, pos.end))) :: Nil) + } + + def remoteToRepository(remote: Remote): Repository = + MavenRepository(remote) + + def dependencyToModule(dependency: Dependency): coursier.Dependency = + coursier.Dependency( + Module(dependency._1, dependency._2), dependency._3 + ) + + def resolveArtifacts(remotes: Seq[Remote], dependencies: Seq[Dependency]): Task[Resolution] = { + val resolution = Resolution(dependencies.map(dependencyToModule).toSet) + val repositories: Seq[Repository] = remotes.map(remoteToRepository) + val fetch = Fetch.from(repositories, Cache.fetch()) + resolution.process.run(fetch) + } + + def fetchArtifacts(remotes: Seq[Remote], dependencies: Seq[Dependency]): Task[coursier.FileError \/ List[File]] = for { + resolution <- resolveArtifacts(remotes, dependencies) + artifacts <- Task.gatherUnordered( + resolution.artifacts.map(Cache.file(_).run) + ) + } yield artifacts.sequenceU + + def createEval(jars: Seq[File]) = { + new Eval(jars = jars.toList) { + @volatile var errors: Map[Severity, List[CompilationInfo]] = Map.empty + + override lazy val compilerSettings: Settings = new EvalSettings(None){ + if (!jars.isEmpty) { + val newJars = jars.mkString(File.pathSeparator) + classpath.value = newJars + File.pathSeparator + classpath.value + } + } + + 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 += convert((pos, msg, severity.toString)) + } + override def reset() = { + super.reset() + errors = Map.empty + } + }) + } + } + + private[this] def evaluate[T](code: String, jars: Seq[File]): EvalResult[T] = { + val eval = createEval(jars) + + val result = for { + _ ← Try(eval.check(code)) + result ← Try(eval.execute[T](code, resetState = true, jars = jars)) + } yield result + + val errors = eval.errors.toMap.asInstanceOf[EvalResult.CI] + + result match { + case scala.util.Success(r) ⇒ EvalSuccess[T](errors, r, "") + case scala.util.Failure(t) ⇒ t match { + case e: Eval.CompilerException ⇒ CompilationError(errors) + case NonFatal(e) ⇒ EvalRuntimeError(errors, Option(RuntimeError(e, None))) + case e ⇒ GeneralError(e) + } + } + } + + def eval[T]( + code: String, + remotes: Seq[Remote] = Nil, + dependencies: Seq[Dependency] = Nil + ): Task[EvalResult[T]] = { + for { + allJars <- fetchArtifacts(remotes, dependencies) + result <- allJars match { + case \/-(jars) => Task({ + evaluate(code, jars) + }).timed(timeout).handle({ + case err: TimeoutException => Timeout[T](timeout) + }) + case -\/(fileError) => Task.now(UnresolvedDependency(fileError.describe)) + } + } yield result + } +} /** * Dynamic scala compiler. Lots of (slow) state is created, so it may be advantageous to keep @@ -45,7 +149,7 @@ private class StringCompiler( messageHandler: Option[Reporter] ) { - val cache = new mutable.HashMap[String, Class[_]]() + val cache = new scala.collection.mutable.HashMap[String, Class[_]]() trait MessageCollector { val messages: Seq[List[String]] @@ -53,7 +157,7 @@ private class StringCompiler( val reporter = messageHandler getOrElse new AbstractReporter with MessageCollector { val settings = StringCompiler.this.settings - val messages = new mutable.ListBuffer[List[String]] + val messages = new scala.collection.mutable.ListBuffer[List[String]] def display(pos: Position, message: String, severity: Severity) { severity.count += 1 @@ -223,20 +327,6 @@ class Eval(target: Option[File] = None, jars: List[File] = Nil) { cls.getConstructor().newInstance().asInstanceOf[() => T].apply().asInstanceOf[T] } - /** - * Converts the given file to evaluable source. - */ - def toSource(file: File): String = { - Source.fromFile(file).mkString - } - - /** - * Compile an entire source file into the virtual classloader. - */ - def compile(code: String) { - compiler(code) - } - /** * Check if code is Eval-able. * @throws CompilerException if not Eval-able. @@ -245,27 +335,10 @@ class Eval(target: Option[File] = None, jars: List[File] = Nil) { val id = uniqueId(code) val className = "Evaluator__" + id val wrappedCode = wrapCodeInClass(className, code) - 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 { Source.fromFile(_).mkString }.mkString("\n") - check(code) - } - - /** - * Check if stream is Eval-able. - * @throws CompilerException if not Eval-able. - */ - def check(stream: InputStream) { - check(Source.fromInputStream(stream).mkString) + compiler(wrappedCode) } - private[util] def uniqueId(code: String, idOpt: Option[Int] = Some(Eval.jvmId)): String = { + private[this] def uniqueId(code: String, idOpt: Option[Int] = Some(Eval.jvmId)): String = { val digest = MessageDigest.getInstance("SHA-1").digest(code.getBytes()) val sha = new BigInteger(1, digest).toString(16) idOpt match { @@ -362,7 +435,7 @@ class ${className} extends (() => Any) with java.io.Serializable { object Eval { - private val jvmId = java.lang.Math.abs(new Random().nextInt()) + private val jvmId = java.lang.Math.abs(new java.util.Random().nextInt()) class CompilerException(val messages: List[List[String]]) extends Exception( "Compiler exception " + messages.map(_.mkString("\n")).mkString("\n")) diff --git a/src/main/scala/types.scala b/src/main/scala/types.scala new file mode 100644 index 00000000..1035d256 --- /dev/null +++ b/src/main/scala/types.scala @@ -0,0 +1,26 @@ +package org.scalaexercises.evaluator + +import scala.concurrent.duration._ + +sealed trait Severity +final case object Informational extends Severity +final case object Warning extends Severity +final case object Error extends Severity + +case class RangePosition(start: Int, point: Int, end: Int) +case class CompilationInfo(message: String, pos: Option[RangePosition]) +case class RuntimeError(val error: Throwable, position: Option[Int]) + +sealed trait EvalResult[+T] + +object EvalResult { + type CI = Map[Severity, List[CompilationInfo]] +} +import EvalResult._ + +case class EvalSuccess[T](complilationInfos: CI, result: T, consoleOutput: String) extends EvalResult[T] +case class Timeout[T](duration: FiniteDuration) extends EvalResult[T] +case class UnresolvedDependency[T](explanation: String) extends EvalResult[T] +case class EvalRuntimeError[T](complilationInfos: CI, runtimeError: Option[RuntimeError]) extends EvalResult[T] +case class CompilationError[T](complilationInfos: CI) extends EvalResult[T] +case class GeneralError[T](stack: Throwable) extends EvalResult[T] diff --git a/src/test/scala/EvaluatorSpec.scala b/src/test/scala/EvaluatorSpec.scala index 4100b4e3..c05d7bbf 100644 --- a/src/test/scala/EvaluatorSpec.scala +++ b/src/test/scala/EvaluatorSpec.scala @@ -18,7 +18,7 @@ class EvaluatorSpec extends FunSpec with Matchers { val result: EvalResult[Int] = evaluator.eval("{ 41 + 1 }").run result should matchPattern { - case EvalResult.Success(_, 42, _) ⇒ + case EvalSuccess(_, 42, _) ⇒ } } @@ -26,7 +26,7 @@ class EvaluatorSpec extends FunSpec with Matchers { val result: EvalResult[Int] = evaluator.eval("{ while(true) {}; 123 }").run result should matchPattern { - case t: EvalResult.Timeout[_] ⇒ + case Timeout(_) ⇒ } } @@ -48,7 +48,7 @@ Eval.now(42).value ).run result should matchPattern { - case EvalResult.Success(_, 42, _) => + case EvalSuccess(_, 42, _) => } } @@ -77,10 +77,10 @@ Eval.now(42).value ).run result1 should matchPattern { - case EvalResult.Success(_, 42, _) => + case EvalSuccess(_, 42, _) => } result2 should matchPattern { - case EvalResult.Success(_, 42, _) => + case EvalSuccess(_, 42, _) => } } @@ -101,7 +101,7 @@ Asserts.scalaTestAsserts(true) ).run result should matchPattern { - case EvalResult.Success(_, (), _) => + case EvalSuccess(_, (), _) => } } @@ -122,7 +122,7 @@ Asserts.scalaTestAsserts(false) ).run result should matchPattern { - case EvalResult.EvalRuntimeError(_, Some(RuntimeError(err: TestFailedException, _))) => + case EvalRuntimeError(_, Some(RuntimeError(err: TestFailedException, _))) => } } }