Skip to content

Commit 821f902

Browse files
committed
Add CanonicalDataParser (issue #331)
1 parent ae14d8e commit 821f902

15 files changed

+347
-99
lines changed

exercises/hello-world/HINTS.md

-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
## Hints
2-
For this exercise two Scala features come in handy:
3-
- [Default Parameter Values](http://docs.scala-lang.org/tutorials/tour/default-parameter-values.html)
4-
- [String Interpolation](http://docs.scala-lang.org/overviews/core/string-interpolation.html)
52

63
#### Common pitfalls that you should avoid
7-
- `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.
84
- 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.*

exercises/hello-world/example.scala

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
object HelloWorld {
22
def hello() = "Hello, World!"
3-
4-
def hello(name: String) = s"Hello, $name!"
53
}
4+
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
11
import org.scalatest.{Matchers, FunSuite}
22

3+
/** @version 1.0.0 */
34
class HelloWorldTest extends FunSuite with Matchers {
4-
test("Without name") {
5-
HelloWorld.hello() should be ("Hello, World!")
6-
}
7-
8-
test("with name") {
9-
pending
10-
HelloWorld.hello("Jane") should be ("Hello, Jane!")
11-
}
125

13-
test("with umlaut name") {
14-
pending
15-
HelloWorld.hello("Jürgen") should be ("Hello, Jürgen!")
6+
test("Say Hi!") {
7+
HelloWorld.hello() should be ("Hello, World!")
168
}
179
}
10+

testgen/build.sbt

+9
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,13 @@ name := "ExcercismScalaTestGenerator"
22

33
scalaVersion := "2.11.8"
44

5+
lazy val root = (project in file("."))
6+
.enablePlugins(SbtTwirl)
7+
.settings(
8+
sourceDirectories in (Compile, TwirlKeys.compileTemplates) += (baseDirectory.value.getParentFile / "src" / "main" / "twirl"))
9+
510
libraryDependencies += "com.typesafe.play" % "play-json_2.11" % "2.5.3"
11+
12+
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.4"
13+
14+
libraryDependencies += "com.typesafe.play" %% "twirl-api" % "1.3.0"

testgen/project/build.properties

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
sbt.version=0.13.13
2+

testgen/project/plugins.sbt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.3.0")
2+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import testgen._
2+
import TestSuiteBuilder._
3+
import java.io.File
4+
5+
object BeerSongTestGenerator {
6+
def main(args: Array[String]): Unit = {
7+
val file = new File("src/main/resources/beer-song.json")
8+
9+
val code = TestSuiteBuilder.build(file,
10+
fromLabeledTestAlt("verse" -> Seq("number"), "verses" -> Seq("beginning", "end")))
11+
println(s"-------------")
12+
println(code)
13+
println(s"-------------")
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,28 @@
1-
import play.api.libs.json.Json
2-
3-
import scala.io.Source
4-
5-
class BowlingTestGenerator {
6-
implicit val testCaseReader = Json.reads[BowlingTestCase]
7-
8-
private val filename = "bowling.json"
9-
private val fileContents = Source.fromFile(filename).getLines.mkString
10-
private val json = Json.parse(fileContents)
11-
12-
def write {
13-
val testCases = (json \ "score" \ "cases").get.as[List[BowlingTestCase]]
14-
val description = (json \ "score" \ "description").get.as[List[String]].mkString(" ")
15-
16-
implicit def testCaseToGen(tc: BowlingTestCase): TestCaseGen = {
17-
val callSUT =
18-
s"${tc.rolls}.foldLeft(Bowling())((acc, roll) => acc.roll(roll)).score()"
19-
val expected = ""
20-
val result = s"val score = $callSUT"
21-
val (matchRight, matchLeft) =
22-
if (tc.expected == -1)
23-
("""fail("Unexpected score returned. Failure expected")""", "")
24-
else
25-
(s"assert(n == ${tc.expected})", s"""fail("${tc.description}")""")
26-
val checkResult =
27-
s"""score match {
28-
case Right(n) => $matchRight
29-
case Left(_) => $matchLeft
30-
}"""
31-
32-
TestCaseGen(tc.description, callSUT, expected, result, checkResult)
33-
}
34-
35-
val testBuilder = new TestBuilder("BowlingTest")
36-
testBuilder.addTestCases(testCases, Some(description))
37-
testBuilder.toFile
38-
}
39-
}
40-
41-
case class BowlingTestCase(description: String,
42-
rolls: List[Int],
43-
expected: Int)
1+
import testgen._
2+
import TestSuiteBuilder._
3+
import java.io.File
444

455
object BowlingTestGenerator {
466
def main(args: Array[String]): Unit = {
47-
new BowlingTestGenerator().write
7+
val file = new File("src/main/resources/bowling.json")
8+
9+
def fromLabeledTest(argNames: String*): ToTestCaseData =
10+
withLabeledTest { sut => labeledTest =>
11+
val args = sutArgs(labeledTest.result, argNames: _*)
12+
val isDefined =
13+
labeledTest.expected.fold(Function.const(".isDefined"), Function.const(""))
14+
val sutCall =
15+
s"""val score = ${args}.foldLeft(Bowling())((acc, roll) => acc.roll(roll)).score()
16+
score$isDefined"""
17+
val expected =
18+
labeledTest.expected.fold(Function.const("true"), x => s"Some($x)")
19+
20+
TestCaseData(labeledTest.description, sutCall, expected)
21+
}
22+
23+
val code = TestSuiteBuilder.build(file, fromLabeledTest("previous_rolls"))
24+
println(s"-------------")
25+
println(code)
26+
println(s"-------------")
4827
}
4928
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import testgen._
2+
import TestSuiteBuilder._
3+
import java.io.File
4+
5+
object HelloWorldTestGenerator {
6+
def main(args: Array[String]): Unit = {
7+
val file = new File("src/main/resources/hello-world.json")
8+
9+
val code = TestSuiteBuilder.build(file, fromLabeledTest())
10+
println(s"-------------")
11+
println(code)
12+
println(s"-------------")
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import testgen._
2+
import TestSuiteBuilder._
3+
import java.io.File
4+
5+
object NucleotideCountTestGenerator {
6+
7+
def main(args: Array[String]): Unit = {
8+
val file = new File("src/main/resources/nucleotide-count.json")
9+
10+
val code = TestSuiteBuilder.build(file, fromLabeledTest("strand"))
11+
println(s"-------------")
12+
println(code)
13+
println(s"-------------")
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,25 @@
1-
import play.api.libs.json.Json
2-
3-
import scala.io.Source
4-
5-
// Generates test suite from json definition for the Panframs exercise.
6-
class PangramsTestGenerator {
7-
implicit val pangramTestCaseReader = Json.reads[PangramTestCase]
8-
9-
private val filename = "pangram.json"
10-
private val fileContents = Source.fromFile(filename).getLines.mkString
11-
private val json = Json.parse(fileContents)
12-
13-
def write {
14-
print("import org.scalatest.{FunSuite, Matchers}" + System.lineSeparator())
15-
print(System.lineSeparator())
16-
print("class PangramsTest extends FunSuite with Matchers {" + System.lineSeparator())
17-
18-
writeTestCases()
19-
20-
print("}" + System.lineSeparator())
21-
}
22-
23-
private def writeTestCases(): Unit = {
24-
val testCases = (json \ "cases").get.as[List[PangramTestCase]]
25-
26-
testCases.foreach(tc => {
27-
print("\ttest(\"" + tc.description + "\") {" + System.lineSeparator())
28-
29-
println("Pangrams.isPangram(\"" + tc.input + "\") should be (" + tc.expected + ")")
30-
31-
print("\t}" + System.lineSeparator())
32-
print(System.lineSeparator())
33-
})
34-
}
35-
}
36-
37-
case class PangramTestCase(description: String, input: String, expected: Boolean)
1+
import testgen._
2+
import TestSuiteBuilder._
3+
import java.io.File
384

395
object PangramsTestGenerator {
406
def main(args: Array[String]): Unit = {
41-
new PangramsTestGenerator().write
7+
val file = new File("src/main/resources/pangram.json")
8+
def fromLabeledTest(argNames: String*): ToTestCaseData =
9+
withLabeledTest { sut =>
10+
labeledTest =>
11+
val args = sutArgs(labeledTest.result, argNames: _*)
12+
val isPangram = labeledTest.property.mkString
13+
val sutCall =
14+
s"""Pangrams.$isPangram($args)"""
15+
val expected =
16+
labeledTest.expected.fold(Function.const("true"), x => s"$x")
17+
TestCaseData(labeledTest.description, sutCall, expected)
18+
}
19+
20+
val code = TestSuiteBuilder.build(file, fromLabeledTest("input"))
21+
println(s"‐‐‐‐‐‐‐‐‐‐‐‐‐")
22+
println(code)
23+
println(s"‐‐‐‐‐‐‐‐‐‐‐‐‐")
4224
}
4325
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import testgen._
2+
import TestSuiteBuilder._
3+
import java.io.File
4+
5+
object SumOfMultiplesTestGenerator {
6+
def main(args: Array[String]): Unit = {
7+
val file = new File("src/main/resources/sum-of-multiples.json")
8+
9+
val code = TestSuiteBuilder.build(file, fromLabeledTest("factors", "limit"))
10+
println(s"-------------")
11+
println(code)
12+
println(s"-------------")
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package testgen
2+
3+
import scala.io.Source
4+
import scala.util.parsing.json.JSON
5+
import CanonicalDataParser._
6+
import scala.util.Try
7+
import scala.Left
8+
import scala.Right
9+
import java.io.File
10+
11+
object CanonicalDataParser {
12+
type ParseResult = Map[String,Any]
13+
14+
type Description = String
15+
type Comments = Seq[String]
16+
type Cases = Seq[LabeledTestItem]
17+
type Property = String
18+
type Result = Any
19+
type Error = String
20+
type Expected = Either[Error, Result]
21+
type Properties = Option[Map[String,Any]]
22+
23+
def getOptional[T](result: ParseResult, key: String): Option[T] =
24+
result.get(key).asInstanceOf[Option[T]]
25+
def getRequired[T](result: ParseResult, key: String): T =
26+
getOptional(result, key) getOrElse (throw new Exception(s"missing: $key"))
27+
28+
def parse(file: File): Exercise = {
29+
val fileContents = Source.fromFile(file).getLines.mkString
30+
val rawParseResult =
31+
JSON.parseFull(fileContents).get.asInstanceOf[ParseResult]
32+
val parseResult = rawParseResult mapValues restoreInts
33+
println(parseResult)
34+
parseResult
35+
}
36+
37+
private def restoreInts(any: Any): Any =
38+
any match {
39+
case double: Double if (double.toInt.toDouble == double) => double.toInt
40+
case map: Map[_,_] => map mapValues restoreInts
41+
case seq: Seq[_] => seq map restoreInts
42+
case any => any
43+
}
44+
45+
def main(args: Array[String]): Unit = {
46+
val path = "src/main/resources"
47+
// val name = "hello-world.json"
48+
// val name = "sum-of-multiples.json"
49+
val name = "bowling.json"
50+
val result = parse(new File(s"$path/$name"))
51+
println(result)
52+
}
53+
}
54+
55+
case class Exercise(name: String, version: String, cases: Cases,
56+
comments: Option[Comments])
57+
object Exercise {
58+
implicit def fromParseResult(result: ParseResult): Exercise = {
59+
val cases: Cases =
60+
getRequired[Seq[ParseResult]](result, "cases") map LabeledTestItem.fromParseResult
61+
Exercise(getRequired(result, "exercise"), getRequired(result, "version"),
62+
flattenCases(cases), getOptional(result, "comments"))
63+
}
64+
65+
// so far there are to few LabeledTestGroups to handle them separately
66+
private def flattenCases(cases: Cases): Cases =
67+
cases match {
68+
case Seq() => Seq()
69+
case (ltg: LabeledTestGroup) +: xs => ltg.cases ++ flattenCases(xs)
70+
case (lt: LabeledTest) +: xs => lt +: flattenCases(xs)
71+
}
72+
}
73+
74+
sealed trait LabeledTestItem
75+
object LabeledTestItem {
76+
implicit def fromParseResult(result: ParseResult): LabeledTestItem =
77+
if (result.contains("cases")) result: LabeledTestGroup
78+
else result: LabeledTest
79+
}
80+
81+
case class LabeledTest(description: Description, property: Property,
82+
expected: Expected, result: ParseResult) extends LabeledTestItem
83+
object LabeledTest {
84+
implicit def fromParseResult(result: ParseResult): LabeledTest = {
85+
val expected: Expected = {
86+
val any = getRequired[Any](result, "expected")
87+
val error = Try {
88+
Left(any.asInstanceOf[Map[String,String]]("error"))
89+
}
90+
error.getOrElse(Right(any))
91+
}
92+
LabeledTest(getRequired(result, "description"), getRequired(result, "property"),
93+
expected, result)
94+
}
95+
}
96+
97+
case class LabeledTestGroup(description: Description, cases: Cases) extends LabeledTestItem
98+
object LabeledTestGroup {
99+
implicit def fromParseResult(result: ParseResult): LabeledTestGroup = {
100+
val description = getRequired[String](result, "description")
101+
val cases =
102+
getRequired[Seq[ParseResult]](result, "cases") map LabeledTestItem.fromParseResult
103+
LabeledTestGroup(description, cases)
104+
}
105+
}

0 commit comments

Comments
 (0)