-
-
Notifications
You must be signed in to change notification settings - Fork 132
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add CanonicalDataParser (issue #331)
- Loading branch information
Showing
13 changed files
with
293 additions
and
61 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!" | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!") | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
sbt.version=0.13.13 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.3.0") | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"-------------") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"-------------") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"-------------") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"-------------") | ||
} | ||
} |
108 changes: 108 additions & 0 deletions
108
testgen/src/main/scala/testgen/CanonicalDataParser.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
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) | ||
} | ||
} | ||
|
||
// only exercise in this format seems to be beer-song, so implemented it for now | ||
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) | ||
} | ||
|
||
// def toLabeledTest(result: ParseResult): LabeledTest = ??? | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package testgen | ||
|
||
import java.io.FileWriter | ||
import java.io.File | ||
import play.twirl.api.Txt | ||
import play.twirl.api.Template1 | ||
import TestSuiteBuilder._ | ||
|
||
object TestSuiteBuilder { | ||
|
||
type TestSuiteTemplate = Template1[TestSuiteData, Txt] | ||
type ToTestCaseData = String => LabeledTestItem => TestCaseData | ||
|
||
private val DefaultTemplate: TestSuiteTemplate = | ||
txt.funSuiteTemplate.asInstanceOf[Template1[TestSuiteData, Txt]] | ||
|
||
def build(file: File, toTestCaseData: ToTestCaseData, imports: Seq[String] = Seq())( | ||
implicit template: TestSuiteTemplate = DefaultTemplate): String = | ||
{ | ||
val exercise @ Exercise(name, version, cases, comments) = | ||
CanonicalDataParser.parse(file) | ||
val tsName = testSuiteName(name) | ||
val testCasesAllPending = cases map toTestCaseData(sutName(name)) | ||
val testCases = | ||
testCasesAllPending updated(0, testCasesAllPending.head.copy(pending = false)) | ||
val testSuiteData = | ||
TestSuiteData(tsName, version, imports, testCases) | ||
|
||
template.render(testSuiteData).toString | ||
} | ||
|
||
def withLabeledTest(f: String => LabeledTest => TestCaseData): ToTestCaseData = | ||
sut => item => f(sut)(item.asInstanceOf[LabeledTest]) | ||
|
||
def fromLabeledTest(argNames: String*): ToTestCaseData = | ||
withLabeledTest { sut => labeledTest => | ||
val sutFunction = labeledTest.property | ||
val args = sutArgs(labeledTest.result, argNames: _*) | ||
val sutCall = s"$sut.$sutFunction(${args})" | ||
val expected = toString(labeledTest.expected) | ||
|
||
TestCaseData(labeledTest.description, sutCall, expected) | ||
} | ||
|
||
def main(args: Array[String]): Unit = { | ||
val path = "src/main/resources" | ||
val input: Map[String, ToTestCaseData] = | ||
Map("hello-world" -> fromLabeledTest(), | ||
"sum-of-multiples" -> fromLabeledTest("factors", "limit"), | ||
"bowling" -> fromLabeledTest("previous_rolls")) | ||
|
||
input foreach { case ((name, toTestCaseData)) => | ||
val file = new File(s"$path/$name.json") | ||
|
||
val code = build(file, toTestCaseData) | ||
println(s"------ $name -------") | ||
println(code) | ||
println(s"------ \\$name -------") | ||
} | ||
} | ||
|
||
def sutName(exerciseName: String) = | ||
(exerciseName split ("-") map (_.capitalize) mkString) | ||
def testSuiteName(exerciseName: String): String = | ||
sutName(exerciseName) + "Test" | ||
|
||
def sutArgs(parseResult: CanonicalDataParser.ParseResult, argNames: String*): String = | ||
argNames map (name => toString(parseResult(name))) mkString(", ") | ||
|
||
private def toString(expected: CanonicalDataParser.Expected): String = | ||
expected.fold(error => s"Left(${toString(error)})", toString) | ||
|
||
private def toString(any: Any): String = any match { | ||
case str: String => s""""$str"""" | ||
case _ => any.toString | ||
} | ||
|
||
def toFile(text: String, dest: File): Unit = { | ||
val fileWriter = new FileWriter(dest) | ||
try { fileWriter.write(text) } finally fileWriter.close | ||
} | ||
} | ||
|
||
case class TestCaseData(description: String, sutCall: String, expected: String, | ||
pending: Boolean = true) | ||
|
||
case class TestSuiteData(name: String, version: String, imports: Seq[String], | ||
testCases: Seq[TestCaseData]) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
@(data: testgen.TestSuiteData)@for(imp <- data.imports) {import @imp} | ||
import org.scalatest.{Matchers, FunSuite} | ||
@import testgen.TestSuiteBuilder._ | ||
|
||
/** @@version @data.version */ | ||
class @data.name extends FunSuite with Matchers { | ||
@for(testCase <- data.testCases) { | ||
test("@testCase.description") { @if(testCase.pending) { | ||
pending} | ||
@testCase.sutCall should be (@testCase.expected) | ||
} | ||
}} |