From 983dc434302953866e17f2fc457481cff5392c26 Mon Sep 17 00:00:00 2001 From: raulraja Date: Thu, 14 Jul 2016 01:08:34 +0200 Subject: [PATCH 1/4] Progress checkpoint with most of it ready --- build.sbt | 14 +++++++++-- src/main/scala/codecs.scala | 33 ++++++++++++++++++++++++++ src/main/scala/evaluation.scala | 14 ++++------- src/main/scala/services.scala | 42 ++++++++++++++++++++++++++++----- src/main/scala/types.scala | 35 ++++++++++++++------------- 5 files changed, 104 insertions(+), 34 deletions(-) create mode 100644 src/main/scala/codecs.scala diff --git a/build.sbt b/build.sbt index b7b72a61..7d064fe6 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,6 @@ -lazy val http4sVersion = "0.15.0-SNAPSHOT" +val http4sVersion = "0.14.1" + +val circeVersion = "0.4.1" lazy val evaluator = (project in file(".")) .settings( @@ -11,12 +13,20 @@ lazy val evaluator = (project in file(".")) "org.http4s" %% "http4s-dsl" % http4sVersion, "org.http4s" %% "http4s-blaze-server" % http4sVersion, "org.http4s" %% "http4s-blaze-client" % http4sVersion, + "org.http4s" %% "http4s-circe" % http4sVersion, + "io.circe" %% "circe-core" % circeVersion, + "io.circe" %% "circe-generic" % circeVersion, + "io.circe" %% "circe-parser" % circeVersion, "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" ) -) + ) enablePlugins(JavaAppPackaging) + +addCompilerPlugin( + "org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full +) diff --git a/src/main/scala/codecs.scala b/src/main/scala/codecs.scala new file mode 100644 index 00000000..825e3ec5 --- /dev/null +++ b/src/main/scala/codecs.scala @@ -0,0 +1,33 @@ +package org.scalaexercises.evaluator + +import org.http4s._, org.http4s.dsl._ +import io.circe.{Encoder, Decoder, Json, Printer} +import org.http4s.headers.`Content-Type` +import io.circe.jawn.CirceSupportParser.facade +import io.circe.generic.semiauto._ + +trait Http4sCodecInstances { + + implicit val jsonDecoder: EntityDecoder[Json] = jawn.jawnDecoder(facade) + + implicit def jsonDecoderOf[A](implicit decoder: Decoder[A]): EntityDecoder[A] = + jsonDecoder.flatMapR { json => + decoder.decodeJson(json).fold( + failure => + DecodeResult.failure(InvalidMessageBodyFailure(s"Could not decode JSON: $json", Some(failure))), + DecodeResult.success(_) + ) + } + + implicit val jsonEntityEncoder: EntityEncoder[Json] = + EntityEncoder[String].contramap[Json] { json => + Printer.noSpaces.pretty(json) + }.withContentType(`Content-Type`(MediaType.`application/json`)) + + implicit def jsonEncoderOf[A](implicit encoder: Encoder[A]): EntityEncoder[A] = + jsonEntityEncoder.contramap[A](encoder.apply) + +} + +object codecs extends Http4sCodecInstances + diff --git a/src/main/scala/evaluation.scala b/src/main/scala/evaluation.scala index f401cf17..6d7940d2 100644 --- a/src/main/scala/evaluation.scala +++ b/src/main/scala/evaluation.scala @@ -38,17 +38,11 @@ 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]) = { + private[this] def convert(errors: (Position, String, String)): (String, 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) + (severity, CompilationInfo(msg, Some(RangePosition(pos.start, pos.point, pos.end))) :: Nil) } def remoteToRepository(remote: Remote): Repository = @@ -56,7 +50,7 @@ class Evaluator(timeout: FiniteDuration = 20.seconds)( def dependencyToModule(dependency: Dependency): coursier.Dependency = coursier.Dependency( - Module(dependency._1, dependency._2), dependency._3 + Module(dependency.groupId, dependency.artifactId), dependency.version ) def resolveArtifacts(remotes: Seq[Remote], dependencies: Seq[Dependency]): Task[Resolution] = { @@ -75,7 +69,7 @@ class Evaluator(timeout: FiniteDuration = 20.seconds)( def createEval(jars: Seq[File]) = { new Eval(jars = jars.toList) { - @volatile var errors: Map[Severity, List[CompilationInfo]] = Map.empty + @volatile var errors: Map[String, List[CompilationInfo]] = Map.empty override lazy val compilerSettings: Settings = new EvalSettings(None){ if (!jars.isEmpty) { diff --git a/src/main/scala/services.scala b/src/main/scala/services.scala index 6b699d75..229851c1 100644 --- a/src/main/scala/services.scala +++ b/src/main/scala/services.scala @@ -4,15 +4,45 @@ import org.http4s._, org.http4s.dsl._, org.http4s.server._ import org.http4s.server.blaze._ import org.log4s.getLogger +import monix.execution.Scheduler + +import scala.concurrent.duration._ + import scalaz.concurrent.Task +import scalaz._ object services { + import codecs._ + import io.circe.generic.auto._ + private val logger = getLogger - val service = HttpService { - case GET -> Root / "eval" => - Ok(s"Hello, evaluator!.") + implicit val scheduler: Scheduler = Scheduler.io("scala-evaluator") + + val evaluator = new Evaluator(20 seconds) + + def service = HttpService { + case req @ POST -> Root / "eval" => + import io.circe.syntax._ + req.decode[EvalRequest] { evalRequest => + evaluator.eval[Any]( + code = evalRequest.code, + remotes = evalRequest.resolvers, + dependencies = evalRequest.dependencies + ) flatMap { result => + val response = result match { + case EvalSuccess(cis, result, out) => + EvalResponse("ok", Option(result.toString), Option(result.asInstanceOf[AnyRef].getClass.getName), cis) + case Timeout(_) => EvalResponse("Timeout", None, None, Map.empty) + case UnresolvedDependency(msg) => EvalResponse(s"Unresolved Dependency : $msg", None, None, Map.empty) + case EvalRuntimeError(cis, _) => EvalResponse("Runtime error", None, None, cis) + case CompilationError(cis) => EvalResponse("Compilation Error", None, None, cis) + case GeneralError(err) => EvalResponse("Unforeseen Exception", None, None, Map.empty) + } + Ok(response.asJson) + } + } } } @@ -26,9 +56,9 @@ object EvaluatorServer extends App { val ip = Option(System.getenv("EVALUATOR_SERVER_IP")).getOrElse("0.0.0.0") val port = (Option(System.getenv("EVALUATOR_SERVER_PORT")) orElse - Option(System.getProperty("http.port"))) - .map(_.toInt) - .getOrElse(8080) + Option(System.getProperty("http.port"))) + .map(_.toInt) + .getOrElse(8080) logger.info(s"Initializing Evaluator at $ip:$port") diff --git a/src/main/scala/types.scala b/src/main/scala/types.scala index 1035d256..4ac6494e 100644 --- a/src/main/scala/types.scala +++ b/src/main/scala/types.scala @@ -1,26 +1,29 @@ package org.scalaexercises.evaluator import scala.concurrent.duration._ +import io.circe._, io.circe.generic.auto._ -sealed trait Severity -final case object Informational extends Severity -final case object Warning extends Severity -final case object Error extends Severity +final case class RangePosition(start: Int, point: Int, end: Int) +final case class CompilationInfo(message: String, pos: Option[RangePosition]) +final case class RuntimeError(val error: Throwable, position: Option[Int]) -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] +sealed trait EvalResult[+A] object EvalResult { - type CI = Map[Severity, List[CompilationInfo]] + type CI = Map[String, 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] +final case class EvalSuccess[A](complilationInfos: CI, result: A, consoleOutput: String) extends EvalResult[A] +final case class Timeout[A](duration: FiniteDuration) extends EvalResult[A] +final case class UnresolvedDependency[A](explanation: String) extends EvalResult[A] +final case class EvalRuntimeError[A](complilationInfos: CI, runtimeError: Option[RuntimeError]) extends EvalResult[A] +final case class CompilationError[A](complilationInfos: CI) extends EvalResult[A] +final case class GeneralError[A](stack: Throwable) extends EvalResult[A] + +final case class Dependency(groupId: String, artifactId: String, version: String) + +final case class EvalRequest(resolvers: List[String], dependencies: List[Dependency], code: String) +final case class EvalResponse(msg: String, value: Option[String], valueType: Option[String], compilationInfos: CI) + From d1790d4d42bd5030768913ebda03823c8f20434d Mon Sep 17 00:00:00 2001 From: raulraja Date: Thu, 14 Jul 2016 13:16:59 +0200 Subject: [PATCH 2/4] Minor formatting cleanup --- src/main/scala/codecs.scala | 3 +-- src/main/scala/types.scala | 48 ++++++++++++++++++++++++++++++------- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/main/scala/codecs.scala b/src/main/scala/codecs.scala index 825e3ec5..982ffb2d 100644 --- a/src/main/scala/codecs.scala +++ b/src/main/scala/codecs.scala @@ -4,8 +4,8 @@ import org.http4s._, org.http4s.dsl._ import io.circe.{Encoder, Decoder, Json, Printer} import org.http4s.headers.`Content-Type` import io.circe.jawn.CirceSupportParser.facade -import io.circe.generic.semiauto._ +/** Provides Json serialization codecs for the http4s services */ trait Http4sCodecInstances { implicit val jsonDecoder: EntityDecoder[Json] = jawn.jawnDecoder(facade) @@ -30,4 +30,3 @@ trait Http4sCodecInstances { } object codecs extends Http4sCodecInstances - diff --git a/src/main/scala/types.scala b/src/main/scala/types.scala index 4ac6494e..4112a082 100644 --- a/src/main/scala/types.scala +++ b/src/main/scala/types.scala @@ -3,9 +3,18 @@ package org.scalaexercises.evaluator import scala.concurrent.duration._ import io.circe._, io.circe.generic.auto._ -final case class RangePosition(start: Int, point: Int, end: Int) -final case class CompilationInfo(message: String, pos: Option[RangePosition]) -final case class RuntimeError(val error: Throwable, position: Option[Int]) +final case class RangePosition( + start: Int, + point: Int, + end: Int) + +final case class CompilationInfo( + message: String, + pos: Option[RangePosition]) + +final case class RuntimeError( + val error: Throwable, + position: Option[Int]) sealed trait EvalResult[+A] @@ -15,15 +24,36 @@ object EvalResult { import EvalResult._ -final case class EvalSuccess[A](complilationInfos: CI, result: A, consoleOutput: String) extends EvalResult[A] +final case class EvalSuccess[A]( + complilationInfos: CI, + result: A, + consoleOutput: String) extends EvalResult[A] + final case class Timeout[A](duration: FiniteDuration) extends EvalResult[A] + final case class UnresolvedDependency[A](explanation: String) extends EvalResult[A] -final case class EvalRuntimeError[A](complilationInfos: CI, runtimeError: Option[RuntimeError]) extends EvalResult[A] + +final case class EvalRuntimeError[A]( + complilationInfos: CI, + runtimeError: Option[RuntimeError]) extends EvalResult[A] + final case class CompilationError[A](complilationInfos: CI) extends EvalResult[A] -final case class GeneralError[A](stack: Throwable) extends EvalResult[A] -final case class Dependency(groupId: String, artifactId: String, version: String) +final case class GeneralError[A](stack: Throwable) extends EvalResult[A] -final case class EvalRequest(resolvers: List[String], dependencies: List[Dependency], code: String) -final case class EvalResponse(msg: String, value: Option[String], valueType: Option[String], compilationInfos: CI) +final case class Dependency( + groupId: String, + artifactId: String, + version: String) + +final case class EvalRequest( + resolvers: List[String], + dependencies: List[Dependency], + code: String) + +final case class EvalResponse( + msg: String, + value: Option[String], + valueType: Option[String], + compilationInfos: CI) From 47a60240ce2cd0e9988800f7c7440ca49c8c4c32 Mon Sep 17 00:00:00 2001 From: raulraja Date: Thu, 14 Jul 2016 13:25:05 +0200 Subject: [PATCH 3/4] Fixed tests --- src/test/scala/EvaluatorSpec.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/scala/EvaluatorSpec.scala b/src/test/scala/EvaluatorSpec.scala index c05d7bbf..15008e93 100644 --- a/src/test/scala/EvaluatorSpec.scala +++ b/src/test/scala/EvaluatorSpec.scala @@ -38,7 +38,7 @@ 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") + Dependency("org.typelevel", "cats_2.11", "0.6.0") ) val result: EvalResult[Int] = evaluator.eval( @@ -59,10 +59,10 @@ 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") + Dependency("org.typelevel", "cats_2.11", "0.4.1") ) val dependencies2 = List( - ("org.typelevel", "cats_2.11", "0.6.0") + Dependency("org.typelevel", "cats_2.11", "0.6.0") ) val result1: EvalResult[Int] = evaluator.eval( @@ -91,7 +91,7 @@ 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") + Dependency("org.scala-exercises", "exercises-stdlib_2.11", "0.2.0") ) val result: EvalResult[Unit] = evaluator.eval( @@ -112,7 +112,7 @@ 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") + Dependency("org.scala-exercises", "exercises-stdlib_2.11", "0.2.0") ) val result: EvalResult[Unit] = evaluator.eval( From 192e73d5844e178e24ad38a4097aa996455efd60 Mon Sep 17 00:00:00 2001 From: raulraja Date: Thu, 14 Jul 2016 17:43:58 +0200 Subject: [PATCH 4/4] Endpoints integration tests --- src/main/scala/services.scala | 14 +-- src/main/scala/types.scala | 25 ++++- src/test/scala/EvalEndpointSpec.scala | 129 ++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 11 deletions(-) create mode 100644 src/test/scala/EvalEndpointSpec.scala diff --git a/src/main/scala/services.scala b/src/main/scala/services.scala index 229851c1..65bcfb33 100644 --- a/src/main/scala/services.scala +++ b/src/main/scala/services.scala @@ -11,10 +11,12 @@ import scala.concurrent.duration._ import scalaz.concurrent.Task import scalaz._ + object services { import codecs._ import io.circe.generic.auto._ + import EvalResponse.messages._ private val logger = getLogger @@ -33,12 +35,12 @@ object services { ) flatMap { result => val response = result match { case EvalSuccess(cis, result, out) => - EvalResponse("ok", Option(result.toString), Option(result.asInstanceOf[AnyRef].getClass.getName), cis) - case Timeout(_) => EvalResponse("Timeout", None, None, Map.empty) - case UnresolvedDependency(msg) => EvalResponse(s"Unresolved Dependency : $msg", None, None, Map.empty) - case EvalRuntimeError(cis, _) => EvalResponse("Runtime error", None, None, cis) - case CompilationError(cis) => EvalResponse("Compilation Error", None, None, cis) - case GeneralError(err) => EvalResponse("Unforeseen Exception", None, None, Map.empty) + EvalResponse(`ok`, Option(result.toString), Option(result.asInstanceOf[AnyRef].getClass.getName), cis) + case Timeout(_) => EvalResponse(`Timeout Exceded`, None, None, Map.empty) + case UnresolvedDependency(msg) => EvalResponse(`Unresolved Dependency` + " : " + msg, None, None, Map.empty) + case EvalRuntimeError(cis, _) => EvalResponse(`Runtime Error`, None, None, cis) + case CompilationError(cis) => EvalResponse(`Compilation Error`, None, None, cis) + case GeneralError(err) => EvalResponse(`Unforeseen Exception`, None, None, Map.empty) } Ok(response.asJson) } diff --git a/src/main/scala/types.scala b/src/main/scala/types.scala index 4112a082..390f3afb 100644 --- a/src/main/scala/types.scala +++ b/src/main/scala/types.scala @@ -47,13 +47,28 @@ final case class Dependency( version: String) final case class EvalRequest( - resolvers: List[String], - dependencies: List[Dependency], + resolvers: List[String] = Nil, + dependencies: List[Dependency] = Nil, code: String) final case class EvalResponse( msg: String, - value: Option[String], - valueType: Option[String], - compilationInfos: CI) + value: Option[String] = None, + valueType: Option[String] = None, + compilationInfos: CI = Map.empty) + +object EvalResponse { + + object messages { + + val `ok` = "Ok" + val `Timeout Exceded` = "Timeout" + val `Unresolved Dependency` = "Unresolved Dependency" + val `Runtime Error` = "Runtime Error" + val `Compilation Error` = "Compilation Error" + val `Unforeseen Exception` = "Unforeseen Exception" + + } + +} diff --git a/src/test/scala/EvalEndpointSpec.scala b/src/test/scala/EvalEndpointSpec.scala new file mode 100644 index 00000000..deee9ec2 --- /dev/null +++ b/src/test/scala/EvalEndpointSpec.scala @@ -0,0 +1,129 @@ +/* + * scala-exercises-evaluator + * Copyright (C) 2015-2016 47 Degrees, LLC. + */ +package org.scalaexercises.evaluator + +import org.scalatest._ +import org.http4s._, org.http4s.dsl._, org.http4s.server._ + +import io.circe.syntax._ +import io.circe.generic.auto._ +import scalaz.stream.Process.emit +import java.nio.charset.StandardCharsets +import scodec.bits.ByteVector + +import org.http4s.{Status => HttpStatus} + +class EvalEndpointSpec extends FunSpec with Matchers { + + import services._ + import codecs._ + import EvalResponse.messages._ + + val sonatypeReleases = "https://oss.sonatype.org/content/repositories/releases/" :: Nil + + def serve(evalRequest: EvalRequest) = + service.run(Request( + POST, + Uri(path = "/eval"), + body = emit( + ByteVector.view( + evalRequest.asJson.noSpaces.getBytes(StandardCharsets.UTF_8) + ) + ) + )).run + + def verifyEvalResponse( + response: Response, + expectedStatus: HttpStatus, + expectedValue: Option[String] = None, + expectedMessage: String + ) = { + + response.status should be(expectedStatus) + val evalResponse = response.as[EvalResponse].run + evalResponse.value should be(expectedValue) + evalResponse.msg should be(expectedMessage) + } + + describe("evaluation") { + it("can evaluate simple expressions") { + verifyEvalResponse( + response = serve(EvalRequest(code = "{ 41 + 1 }")), + expectedStatus = HttpStatus.Ok, + expectedValue = Some("42"), + expectedMessage = `ok` + ) + } + + it("fails with a timeout when takes longer than the configured timeout") { + verifyEvalResponse( + response = serve(EvalRequest(code = "{ while(true) {}; 123 }")), + expectedStatus = HttpStatus.Ok, + expectedValue = None, + expectedMessage = `Timeout Exceded` + ) + } + + it("can load dependencies for an evaluation") { + verifyEvalResponse( + response = serve(EvalRequest( + code = "{import cats._; Eval.now(42).value}", + resolvers = sonatypeReleases, + dependencies = Dependency("org.typelevel", "cats_2.11", "0.6.0") :: Nil + )), + expectedStatus = HttpStatus.Ok, + expectedValue = Some("42"), + expectedMessage = `ok` + ) + } + + it("can load different versions of a dependency across evaluations") { + val code = "{import cats._; Eval.now(42).value}" + val resolvers = sonatypeReleases + + List("0.6.0", "0.4.1") foreach { version => + verifyEvalResponse( + response = serve(EvalRequest( + code = code, + resolvers = resolvers, + dependencies = Dependency("org.typelevel", "cats_2.11", version) :: Nil + )), + expectedStatus = HttpStatus.Ok, + expectedValue = Some("42"), + expectedMessage = `ok` + ) + } + + } + + it("can run code from the exercises content") { + verifyEvalResponse( + response = serve(EvalRequest( + code = "{import stdlib._; Asserts.scalaTestAsserts(true)}", + resolvers = sonatypeReleases, + dependencies = Dependency("org.scala-exercises", "exercises-stdlib_2.11", "0.2.0") :: Nil + )), + expectedStatus = HttpStatus.Ok, + expectedValue = Some("()"), + expectedMessage = `ok` + ) + } + + it("captures exceptions when running the exercises content") { + verifyEvalResponse( + response = serve(EvalRequest( + code = "{import stdlib._; Asserts.scalaTestAsserts(false)}", + resolvers = sonatypeReleases, + dependencies = Dependency("org.scala-exercises", "exercises-stdlib_2.11", "0.2.0") :: Nil + )), + expectedStatus = HttpStatus.Ok, + expectedValue = None, + expectedMessage = `Runtime Error` + ) + } + + } +} +