Skip to content

Commit

Permalink
Add CanonicalDataParser (issue #331)
Browse files Browse the repository at this point in the history
  • Loading branch information
abo64 committed Mar 29, 2017
1 parent ae14d8e commit e7ba3aa
Show file tree
Hide file tree
Showing 16 changed files with 378 additions and 99 deletions.
4 changes: 0 additions & 4 deletions exercises/hello-world/HINTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
## Hints
For this exercise two Scala features come in handy:
- [Default Parameter Values](http://docs.scala-lang.org/tutorials/tour/default-parameter-values.html)
- [String Interpolation](http://docs.scala-lang.org/overviews/core/string-interpolation.html)

#### Common pitfalls that you should avoid
- `null` is usually not considered a valid value in Scala, and there are no `null` checks needed (if you don't have to interface with Java code, say). Instead there is the [Option](http://danielwestheide.com/blog/2012/12/19/the-neophytes-guide-to-scala-part-5-the-option-type.html) type if you want to express the possible absence of a value. But for this exercise just assume a normal non-`null` String parameter.
- Usually there is no need in Scala to use `return`. For a discussion see [here](http://stackoverflow.com/questions/24856106/return-in-a-scala-function-literal). Or as a quote from that discussion: *Don't use return, it makes Scala cry.*
3 changes: 1 addition & 2 deletions exercises/hello-world/example.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
object HelloWorld {
def hello() = "Hello, World!"

def hello(name: String) = s"Hello, $name!"
}

15 changes: 4 additions & 11 deletions exercises/hello-world/src/test/scala/HelloWorldTest.scala
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import org.scalatest.{Matchers, FunSuite}

/** @version 1.0.0 */
class HelloWorldTest extends FunSuite with Matchers {
test("Without name") {
HelloWorld.hello() should be ("Hello, World!")
}

test("with name") {
pending
HelloWorld.hello("Jane") should be ("Hello, Jane!")
}

test("with umlaut name") {
pending
HelloWorld.hello("Jürgen") should be ("Hello, Jürgen!")
test("Say Hi!") {
HelloWorld.hello() should be ("Hello, World!")
}
}

9 changes: 9 additions & 0 deletions testgen/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,13 @@ name := "ExcercismScalaTestGenerator"

scalaVersion := "2.11.8"

lazy val root = (project in file("."))
.enablePlugins(SbtTwirl)
.settings(
sourceDirectories in (Compile, TwirlKeys.compileTemplates) += (baseDirectory.value.getParentFile / "src" / "main" / "twirl"))

libraryDependencies += "com.typesafe.play" % "play-json_2.11" % "2.5.3"

libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.4"

libraryDependencies += "com.typesafe.play" %% "twirl-api" % "1.3.0"
2 changes: 2 additions & 0 deletions testgen/project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
sbt.version=0.13.13

2 changes: 2 additions & 0 deletions testgen/project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.3.0")

15 changes: 15 additions & 0 deletions testgen/src/main/scala/BeerSongTestGenerator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import testgen._
import TestSuiteBuilder._
import java.io.File

object BeerSongTestGenerator {
def main(args: Array[String]): Unit = {
val file = new File("src/main/resources/beer-song.json")

val code = TestSuiteBuilder.build(file,
fromLabeledTestAlt("verse" -> Seq("number"), "verses" -> Seq("beginning", "end")))
println(s"-------------")
println(code)
println(s"-------------")
}
}
67 changes: 23 additions & 44 deletions testgen/src/main/scala/BowlingTestGenerator.scala
Original file line number Diff line number Diff line change
@@ -1,49 +1,28 @@
import play.api.libs.json.Json

import scala.io.Source

class BowlingTestGenerator {
implicit val testCaseReader = Json.reads[BowlingTestCase]

private val filename = "bowling.json"
private val fileContents = Source.fromFile(filename).getLines.mkString
private val json = Json.parse(fileContents)

def write {
val testCases = (json \ "score" \ "cases").get.as[List[BowlingTestCase]]
val description = (json \ "score" \ "description").get.as[List[String]].mkString(" ")

implicit def testCaseToGen(tc: BowlingTestCase): TestCaseGen = {
val callSUT =
s"${tc.rolls}.foldLeft(Bowling())((acc, roll) => acc.roll(roll)).score()"
val expected = ""
val result = s"val score = $callSUT"
val (matchRight, matchLeft) =
if (tc.expected == -1)
("""fail("Unexpected score returned. Failure expected")""", "")
else
(s"assert(n == ${tc.expected})", s"""fail("${tc.description}")""")
val checkResult =
s"""score match {
case Right(n) => $matchRight
case Left(_) => $matchLeft
}"""

TestCaseGen(tc.description, callSUT, expected, result, checkResult)
}

val testBuilder = new TestBuilder("BowlingTest")
testBuilder.addTestCases(testCases, Some(description))
testBuilder.toFile
}
}

case class BowlingTestCase(description: String,
rolls: List[Int],
expected: Int)
import testgen._
import TestSuiteBuilder._
import java.io.File

object BowlingTestGenerator {
def main(args: Array[String]): Unit = {
new BowlingTestGenerator().write
val file = new File("src/main/resources/bowling.json")

def fromLabeledTest(argNames: String*): ToTestCaseData =
withLabeledTest { sut => labeledTest =>
val args = sutArgs(labeledTest.result, argNames: _*)
val isDefined =
labeledTest.expected.fold(Function.const(".isDefined"), Function.const(""))
val sutCall =
s"""val score = ${args}.foldLeft(Bowling())((acc, roll) => acc.roll(roll)).score()
score$isDefined"""
val expected =
labeledTest.expected.fold(Function.const("true"), x => s"Some($x)")

TestCaseData(labeledTest.description, sutCall, expected)
}

val code = TestSuiteBuilder.build(file, fromLabeledTest("previous_rolls"))
println(s"-------------")
println(code)
println(s"-------------")
}
}
31 changes: 31 additions & 0 deletions testgen/src/main/scala/FoodChainTestGenerator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import testgen._
import TestSuiteBuilder._
import java.io.File

object FoodChainTestGenerator {
def main(args: Array[String]): Unit = {
val file = new File("src/main/resources/food-chain.json")

val RawQuote = "\"\"\""
def asRawString(str: String): String = s"$RawQuote$str$RawQuote"

def fromLabeledTest(argNames: String*)(
implicit sutFunction: LabeledTest => String = _.property): ToTestCaseData =
withLabeledTest { sut =>
labeledTest =>
val sutFunction = labeledTest.property
val args = sutArgs(labeledTest.result, argNames: _*)
val sutCall = s"$sut.$sutFunction(${args})"
val expectedLines = labeledTest.expected.right.get.asInstanceOf[List[String]]
val expected = expectedLines mkString ("", "\n", "\n\n")

TestCaseData(labeledTest.description, sutCall, asRawString(expected))
}

val code = TestSuiteBuilder.build(file,
fromLabeledTest("start verse"))
println(s"-------------")
println(code)
println(s"-------------")
}
}
14 changes: 14 additions & 0 deletions testgen/src/main/scala/HelloWorldTestGenerator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import testgen._
import TestSuiteBuilder._
import java.io.File

object HelloWorldTestGenerator {
def main(args: Array[String]): Unit = {
val file = new File("src/main/resources/hello-world.json")

val code = TestSuiteBuilder.build(file, fromLabeledTest())
println(s"-------------")
println(code)
println(s"-------------")
}
}
15 changes: 15 additions & 0 deletions testgen/src/main/scala/NucleotideCountTestGenerator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import testgen._
import TestSuiteBuilder._
import java.io.File

object NucleotideCountTestGenerator {

def main(args: Array[String]): Unit = {
val file = new File("src/main/resources/nucleotide-count.json")

val code = TestSuiteBuilder.build(file, fromLabeledTest("strand"))
println(s"-------------")
println(code)
println(s"-------------")
}
}
58 changes: 20 additions & 38 deletions testgen/src/main/scala/PangramsTestGenerator.scala
Original file line number Diff line number Diff line change
@@ -1,43 +1,25 @@
import play.api.libs.json.Json

import scala.io.Source

// Generates test suite from json definition for the Panframs exercise.
class PangramsTestGenerator {
implicit val pangramTestCaseReader = Json.reads[PangramTestCase]

private val filename = "pangram.json"
private val fileContents = Source.fromFile(filename).getLines.mkString
private val json = Json.parse(fileContents)

def write {
print("import org.scalatest.{FunSuite, Matchers}" + System.lineSeparator())
print(System.lineSeparator())
print("class PangramsTest extends FunSuite with Matchers {" + System.lineSeparator())

writeTestCases()

print("}" + System.lineSeparator())
}

private def writeTestCases(): Unit = {
val testCases = (json \ "cases").get.as[List[PangramTestCase]]

testCases.foreach(tc => {
print("\ttest(\"" + tc.description + "\") {" + System.lineSeparator())

println("Pangrams.isPangram(\"" + tc.input + "\") should be (" + tc.expected + ")")

print("\t}" + System.lineSeparator())
print(System.lineSeparator())
})
}
}

case class PangramTestCase(description: String, input: String, expected: Boolean)
import testgen._
import TestSuiteBuilder._
import java.io.File

object PangramsTestGenerator {
def main(args: Array[String]): Unit = {
new PangramsTestGenerator().write
val file = new File("src/main/resources/pangram.json")
def fromLabeledTest(argNames: String*): ToTestCaseData =
withLabeledTest { sut =>
labeledTest =>
val args = sutArgs(labeledTest.result, argNames: _*)
val isPangram = labeledTest.property.mkString
val sutCall =
s"""Pangrams.$isPangram($args)"""
val expected =
labeledTest.expected.fold(Function.const("true"), x => s"$x")
TestCaseData(labeledTest.description, sutCall, expected)
}

val code = TestSuiteBuilder.build(file, fromLabeledTest("input"))
println(s"‐‐‐‐‐‐‐‐‐‐‐‐‐")
println(code)
println(s"‐‐‐‐‐‐‐‐‐‐‐‐‐")
}
}
14 changes: 14 additions & 0 deletions testgen/src/main/scala/SumOfMultiplesTestGenerator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import testgen._
import TestSuiteBuilder._
import java.io.File

object SumOfMultiplesTestGenerator {
def main(args: Array[String]): Unit = {
val file = new File("src/main/resources/sum-of-multiples.json")

val code = TestSuiteBuilder.build(file, fromLabeledTest("factors", "limit"))
println(s"-------------")
println(code)
println(s"-------------")
}
}
105 changes: 105 additions & 0 deletions testgen/src/main/scala/testgen/CanonicalDataParser.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package testgen

import scala.io.Source
import scala.util.parsing.json.JSON
import CanonicalDataParser._
import scala.util.Try
import scala.Left
import scala.Right
import java.io.File

object CanonicalDataParser {
type ParseResult = Map[String,Any]

type Description = String
type Comments = Seq[String]
type Cases = Seq[LabeledTestItem]
type Property = String
type Result = Any
type Error = String
type Expected = Either[Error, Result]
type Properties = Option[Map[String,Any]]

def getOptional[T](result: ParseResult, key: String): Option[T] =
result.get(key).asInstanceOf[Option[T]]
def getRequired[T](result: ParseResult, key: String): T =
getOptional(result, key) getOrElse (throw new Exception(s"missing: $key"))

def parse(file: File): Exercise = {
val fileContents = Source.fromFile(file).getLines.mkString
val rawParseResult =
JSON.parseFull(fileContents).get.asInstanceOf[ParseResult]
val parseResult = rawParseResult mapValues restoreInts
println(parseResult)
parseResult
}

private def restoreInts(any: Any): Any =
any match {
case double: Double if (double.toInt.toDouble == double) => double.toInt
case map: Map[_,_] => map mapValues restoreInts
case seq: Seq[_] => seq map restoreInts
case any => any
}

def main(args: Array[String]): Unit = {
val path = "src/main/resources"
// val name = "hello-world.json"
// val name = "sum-of-multiples.json"
val name = "bowling.json"
val result = parse(new File(s"$path/$name"))
println(result)
}
}

case class Exercise(name: String, version: String, cases: Cases,
comments: Option[Comments])
object Exercise {
implicit def fromParseResult(result: ParseResult): Exercise = {
val cases: Cases =
getRequired[Seq[ParseResult]](result, "cases") map LabeledTestItem.fromParseResult
Exercise(getRequired(result, "exercise"), getRequired(result, "version"),
flattenCases(cases), getOptional(result, "comments"))
}

// so far there are to few LabeledTestGroups to handle them separately
private def flattenCases(cases: Cases): Cases =
cases match {
case Seq() => Seq()
case (ltg: LabeledTestGroup) +: xs => ltg.cases ++ flattenCases(xs)
case (lt: LabeledTest) +: xs => lt +: flattenCases(xs)
}
}

sealed trait LabeledTestItem
object LabeledTestItem {
implicit def fromParseResult(result: ParseResult): LabeledTestItem =
if (result.contains("cases")) result: LabeledTestGroup
else result: LabeledTest
}

case class LabeledTest(description: Description, property: Property,
expected: Expected, result: ParseResult) extends LabeledTestItem
object LabeledTest {
implicit def fromParseResult(result: ParseResult): LabeledTest = {
val expected: Expected = {
val any = getRequired[Any](result, "expected")
val error = Try {
Left(any.asInstanceOf[Map[String,String]]("error"))
}
error.getOrElse(Right(any))
}
LabeledTest(getRequired(result, "description"), getRequired(result, "property"),
expected, result)
}
}

case class LabeledTestGroup(description: Description, cases: Cases) extends LabeledTestItem
object LabeledTestGroup {
implicit def fromParseResult(result: ParseResult): LabeledTestGroup = {
val description = getRequired[String](result, "description")
val cases =
getRequired[Seq[ParseResult]](result, "cases") map LabeledTestItem.fromParseResult
LabeledTestGroup(description, cases)
}
}
Loading

0 comments on commit e7ba3aa

Please sign in to comment.