diff --git a/exercises/hello-world/HINTS.md b/exercises/hello-world/HINTS.md index b29110c06..c800c6dd7 100644 --- a/exercises/hello-world/HINTS.md +++ b/exercises/hello-world/HINTS.md @@ -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.* diff --git a/exercises/hello-world/example.scala b/exercises/hello-world/example.scala index 19dd5f2da..e67011ce4 100644 --- a/exercises/hello-world/example.scala +++ b/exercises/hello-world/example.scala @@ -1,5 +1,4 @@ object HelloWorld { def hello() = "Hello, World!" - - def hello(name: String) = s"Hello, $name!" } + diff --git a/exercises/hello-world/src/test/scala/HelloWorldTest.scala b/exercises/hello-world/src/test/scala/HelloWorldTest.scala index ad2092cb3..edff22b38 100644 --- a/exercises/hello-world/src/test/scala/HelloWorldTest.scala +++ b/exercises/hello-world/src/test/scala/HelloWorldTest.scala @@ -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!") } } + diff --git a/testgen/build.sbt b/testgen/build.sbt index 0c5bce45e..bfcbadee8 100644 --- a/testgen/build.sbt +++ b/testgen/build.sbt @@ -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" diff --git a/testgen/project/build.properties b/testgen/project/build.properties new file mode 100644 index 000000000..24be09b28 --- /dev/null +++ b/testgen/project/build.properties @@ -0,0 +1,2 @@ +sbt.version=0.13.13 + diff --git a/testgen/project/plugins.sbt b/testgen/project/plugins.sbt new file mode 100644 index 000000000..57c7b4f68 --- /dev/null +++ b/testgen/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.3.0") + diff --git a/testgen/src/main/scala/BowlingTestGenerator.scala b/testgen/src/main/scala/BowlingTestGenerator.scala index a83ad0069..e511ff495 100644 --- a/testgen/src/main/scala/BowlingTestGenerator.scala +++ b/testgen/src/main/scala/BowlingTestGenerator.scala @@ -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"-------------") } } diff --git a/testgen/src/main/scala/HelloWorldTestGenerator.scala b/testgen/src/main/scala/HelloWorldTestGenerator.scala new file mode 100644 index 000000000..4397589e5 --- /dev/null +++ b/testgen/src/main/scala/HelloWorldTestGenerator.scala @@ -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"-------------") + } +} \ No newline at end of file diff --git a/testgen/src/main/scala/NucleotideCountTestGenerator.scala b/testgen/src/main/scala/NucleotideCountTestGenerator.scala new file mode 100644 index 000000000..214fbc239 --- /dev/null +++ b/testgen/src/main/scala/NucleotideCountTestGenerator.scala @@ -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"-------------") + } +} \ No newline at end of file diff --git a/testgen/src/main/scala/SumOfMultiplesTestGenerator.scala b/testgen/src/main/scala/SumOfMultiplesTestGenerator.scala new file mode 100644 index 000000000..78fce5a73 --- /dev/null +++ b/testgen/src/main/scala/SumOfMultiplesTestGenerator.scala @@ -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"-------------") + } +} diff --git a/testgen/src/main/scala/testgen/CanonicalDataParser.scala b/testgen/src/main/scala/testgen/CanonicalDataParser.scala new file mode 100644 index 000000000..88340d208 --- /dev/null +++ b/testgen/src/main/scala/testgen/CanonicalDataParser.scala @@ -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 = ??? +} diff --git a/testgen/src/main/scala/testgen/TestSuiteBuilder.scala b/testgen/src/main/scala/testgen/TestSuiteBuilder.scala new file mode 100644 index 000000000..15305d2ff --- /dev/null +++ b/testgen/src/main/scala/testgen/TestSuiteBuilder.scala @@ -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]) + diff --git a/testgen/src/main/twirl/funSuiteTemplate.scala.txt b/testgen/src/main/twirl/funSuiteTemplate.scala.txt new file mode 100644 index 000000000..848bbd634 --- /dev/null +++ b/testgen/src/main/twirl/funSuiteTemplate.scala.txt @@ -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) + } +}} \ No newline at end of file