Skip to content

Evaluation Endpoint #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 14, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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
)
32 changes: 32 additions & 0 deletions src/main/scala/codecs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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

/** Provides Json serialization codecs for the http4s services */
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
14 changes: 4 additions & 10 deletions src/main/scala/evaluation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,25 +38,19 @@ 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 =
MavenRepository(remote)

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] = {
Expand All @@ -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) {
Expand Down
44 changes: 38 additions & 6 deletions src/main/scala/services.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,47 @@ 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._
import EvalResponse.messages._

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 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)
}
}
}

}
Expand All @@ -26,9 +58,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")

Expand Down
78 changes: 63 additions & 15 deletions src/main/scala/types.scala
Original file line number Diff line number Diff line change
@@ -1,26 +1,74 @@
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)

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])
final case class CompilationInfo(
message: String,
pos: Option[RangePosition])

sealed trait EvalResult[+T]
final case class RuntimeError(
val error: Throwable,
position: Option[Int])

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] = Nil,
dependencies: List[Dependency] = Nil,
code: String)

final case class EvalResponse(
msg: String,
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"

}

}

129 changes: 129 additions & 0 deletions src/test/scala/EvalEndpointSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* scala-exercises-evaluator
* Copyright (C) 2015-2016 47 Degrees, LLC. <http://www.47deg.com>
*/
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`
)
}

}
}

Loading