Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
9 changes: 9 additions & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version = "3.8.2"
align.preset = most
maxColumn = 160
trailingCommas = always
continuationIndent.callSite = 2
continuationIndent.defnSite = 2
runner.dialect = scala3
rewrite.scala3.convertToNewSyntax = yes
rewrite.scala3.removeOptionalBraces = yes
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM sbtscala/scala-sbt:openjdk-8u342_1.7.3_2.13.10
FROM sbtscala/scala-sbt:eclipse-temurin-jammy-22_36_1.10.1_3.4.2

WORKDIR /opt/test-runner

Expand Down
2 changes: 1 addition & 1 deletion bin/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ slug="$1"
input_dir="${2%/}"
output_dir="${3%/}"

test_runner_jar=/opt/test-runner/target/scala-2.13/TestRunner-assembly-0.1.0-SNAPSHOT.jar
test_runner_jar=/opt/test-runner/target/scala-3.4.2/TestRunner-assembly-0.1.0-SNAPSHOT.jar

workdir=/tmp/exercise
workdir_target="${workdir}/target"
Expand Down
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Dependencies._

ThisBuild / scalaVersion := "2.13.6"
ThisBuild / scalaVersion := "3.4.2"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / organization := "org.exercism"
ThisBuild / organizationName := "exercism"
Expand All @@ -11,7 +11,7 @@ lazy val root = (project in file("."))
// not in the Test scope so that it gets added to the fat jar generated by sbt assembly
libraryDependencies += scalaTest,
libraryDependencies += scalaCheck,
libraryDependencies += "org.json" % "json" % "20220320",
libraryDependencies += json,
// for the LensPerson exercise
libraryDependencies ++= monocle
)
12 changes: 7 additions & 5 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import sbt._

object Dependencies {
val monocleVersion = "2.0.0"
lazy val scalaTest = "org.scalatest" %% "scalatest" % "3.2.19"
lazy val scalaCheck = "org.scalatestplus" %% "scalacheck-1-18" % "3.2.19.0"

lazy val scalaTest = "org.scalatest" %% "scalatest" % "3.2.10"
lazy val scalaCheck = "org.scalatestplus" %% "scalacheck-1-15" % "3.2.9.0"
val monocleVersion = "3.2.0"

lazy val monocle = Seq(
"com.github.julien-truffaut" %% "monocle-core" % monocleVersion,
"com.github.julien-truffaut" %% "monocle-macro" % monocleVersion
"dev.optics" %% "monocle-core" % monocleVersion,
"dev.optics" %% "monocle-macro" % monocleVersion
)

lazy val json = "org.json" % "json" % "20220320"
}
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.5.7
sbt.version=1.10.1
2 changes: 1 addition & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.2.0")
103 changes: 58 additions & 45 deletions src/main/scala/Application.scala
Original file line number Diff line number Diff line change
@@ -1,80 +1,93 @@
import org.json.{JSONArray, JSONObject, XML}

import java.io.{File, FileFilter, FileWriter}
import scala.io.Source

object Application extends App {
require(args.length == 3, s"Invalid number of arguments. Expected: <build-log-file-path> <test-results-folder-path> <results-json-file-path>, got: ${args.mkString("", ", ", "")}")
val buildLogFilePath = args(0)
val testResultsFolderPath = args(1)
val resultsJsonFilePath = args(2)

val testResultsFolder = new File(testResultsFolderPath)
if (!testResultsFolder.isDirectory) {
throw new RuntimeException(s"Expected $testResultsFolderPath to be a folder")
}
val testResultFiles = testResultsFolder.listFiles(new FileFilter() {
override def accept(file: File): Boolean = file.getName.matches("TEST-.*\\.xml")
}).toList
writeResultsJSON(buildLogFilePath, testResultFiles, resultsJsonFilePath)
object Application:
@main
def run(
buildLogFilePath: String,
testResultsFolderPath: String,
resultsJsonFilePath: String,
): Unit =
val testResultsFolder = new File(testResultsFolderPath)
if !testResultsFolder.isDirectory then
throw new RuntimeException(
s"Expected $testResultsFolderPath to be a folder",
)
val testResultFiles = testResultsFolder
.listFiles(
new FileFilter():
override def accept(file: File): Boolean =
file.getName.matches("TEST-.*\\.xml"),
)
.toList
writeResultsJSON(buildLogFilePath, testResultFiles, resultsJsonFilePath)

def writeResultsJSON(buildLogFilePath: String, testResultsFiles: List[File], resultsJsonFilePath: String): Unit = {
val resultsJsonFile = new File(resultsJsonFilePath)
def writeResultsJSON(
buildLogFilePath: String,
testResultsFiles: List[File],
resultsJsonFilePath: String,
): Unit =
val resultsJsonFile = new File(resultsJsonFilePath)
val resultsJsonFileWriter = new FileWriter(resultsJsonFile)

val json = toExercismJSON(buildLogFilePath, testResultsFiles)
json.write(resultsJsonFileWriter)
resultsJsonFileWriter.close()
}

def getTestSuiteObject(testResultsFile: File): JSONObject = {
def getTestSuiteObject(testResultsFile: File): JSONObject =
val bufferedSource = Source.fromFile(testResultsFile)
val xml = bufferedSource.mkString
val xml = bufferedSource.mkString
bufferedSource.close
XML.toJSONObject(xml).getJSONObject("testsuite")
}

// log, not xml
def findErrorsInLog(buildLogFilePath: String): String = {
def findErrorsInLog(buildLogFilePath: String): String =
val fileSource = Source.fromFile(buildLogFilePath)
val rawContent = fileSource.mkString
fileSource.close
if (rawContent.contains("error: ")) rawContent else ""
}
if rawContent.contains("Error: ") then rawContent else ""

def toTestCaseJSON(testCase: JSONObject): JSONObject = {
def toTestCaseJSON(testCase: JSONObject): JSONObject =
val fail = testCase.optJSONObject("failure")
new JSONObject()
.put("name", testCase.get("name").toString)
.put("status", if (fail != null) "fail" else "pass")
.put("message", if (fail != null) fail.getString("message") else JSONObject.NULL)
.put("status", if fail != null then "fail" else "pass")
.put(
"message",
if fail != null then fail.getString("message") else JSONObject.NULL,
)
.put("output", JSONObject.NULL)
.put("test_code", JSONObject.NULL)
}

def toExercismJSON(buildLogFilePath: String, testResultsFiles: List[File]): JSONObject = {
val baseObject = new JSONObject().put("version", 2)
def toExercismJSON(
buildLogFilePath: String,
testResultsFiles: List[File],
): JSONObject =
val baseObject = new JSONObject().put("version", 2)
val errorMessage = findErrorsInLog(buildLogFilePath)
if (errorMessage.nonEmpty) {
println("errorMessage" -> errorMessage)
if errorMessage.nonEmpty then
baseObject
.put("status", "error")
.put("message", errorMessage)
} else {
val (failuresCount, testCases) = testResultsFiles.map(getTestSuiteObject)
else
val (failuresCount, testCases) = testResultsFiles
.map(getTestSuiteObject)
.filter(testSuite => testSuite.has("testcase"))
.map(testSuite => {
val failuresCount = testSuite.getInt("failures")
val testcase = testSuite.get("testcase")
val testCases: Array[JSONObject] = testcase match {
case arr: JSONArray => (0 until arr.length).map(idx => toTestCaseJSON(arr.getJSONObject(idx))).toArray
.map(testSuite =>
val failuresCount = testSuite.getInt("failures")
val testcase = testSuite.get("testcase")
val testCases: Array[JSONObject] = testcase match
case arr: JSONArray =>
(0 until arr.length)
.map(idx => toTestCaseJSON(arr.getJSONObject(idx)))
.toArray
case obj: JSONObject => Array(toTestCaseJSON(obj))
}
(failuresCount, testCases)
}).reduce((a, b) => (a._1 + b._1, Array.concat(a._2, b._2)))
(failuresCount, testCases),
)
.reduce((a, b) => (a._1 + b._1, Array.concat(a._2, b._2)))
baseObject
.put("status", if (failuresCount > 0) "fail" else "pass")
.put("status", if failuresCount > 0 then "fail" else "pass")
.put("message", JSONObject.NULL)
.put("tests", testCases)
}
}
}
52 changes: 21 additions & 31 deletions src/test/scala/ApplicationSpec.scala
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
import org.json.{JSONArray, JSONObject}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers

import java.io.File

class ApplicationSpec extends AnyFunSuite with Matchers {
class ApplicationSpec extends AnyFunSuite, Matchers:

def getTestCasesJSON(path: String): JSONArray = {
def getTestCasesJSON(path: String): JSONArray =
val file = new File(path)
Application.getTestSuiteObject(file).getJSONArray("testcase")
}

test("A successful xml should pass simply") {
test("A successful xml should pass simply"):
val xmlTestURL = getClass.getResource("/GradeSchool_successful.xml").getPath
val jsonArray = getTestCasesJSON(xmlTestURL)
val objects = (0 until jsonArray.length).map(jsonArray.getJSONObject(_).optJSONObject("failure"))
val jsonArray = getTestCasesJSON(xmlTestURL)
val objects = (0 until jsonArray.length).map(jsonArray.getJSONObject(_).optJSONObject("failure"))
objects should contain only null
}

test("A successful xml should be properly formatted as JSON") {
val xmlTestURL = getClass.getResource("/GradeSchool_successful.xml").getPath
val outputFileURL = getClass.getResource("/outputs/output.txt").getPath
test("A successful xml should be properly formatted as JSON"):
val xmlTestURL = getClass.getResource("/GradeSchool_successful.xml").getPath
val outputFileURL = getClass.getResource("/outputs/output.txt").getPath
val exercismOutput: JSONObject = Application.toExercismJSON(outputFileURL, List(new File(xmlTestURL)))

assert(exercismOutput.getInt("version") == 2)
Expand All @@ -36,11 +33,10 @@ class ApplicationSpec extends AnyFunSuite with Matchers {
assert(testCases(4).toString() == """{"output":null,"name":"get students in a grade","test_code":null,"message":null,"status":"pass"}""")
assert(testCases(5).toString() == """{"output":null,"name":"get students in a non-existent grade","test_code":null,"message":null,"status":"pass"}""")
assert(testCases(6).toString() == """{"output":null,"name":"sort school","test_code":null,"message":null,"status":"pass"}""")
}

test("A successful xml with a single test case should be properly formatted as JSON") {
val xmlTestURL = getClass.getResource("/HelloWorld_successful.xml").getPath
val outputFileURL = getClass.getResource("/outputs/output.txt").getPath
test("A successful xml with a single test case should be properly formatted as JSON"):
val xmlTestURL = getClass.getResource("/HelloWorld_successful.xml").getPath
val outputFileURL = getClass.getResource("/outputs/output.txt").getPath
val exercismOutput: JSONObject = Application.toExercismJSON(outputFileURL, List(new File(xmlTestURL)))

assert(exercismOutput.getInt("version") == 2)
Expand All @@ -50,18 +46,16 @@ class ApplicationSpec extends AnyFunSuite with Matchers {
val testCases = exercismOutput.get("tests").asInstanceOf[Array[JSONObject]]
assert(testCases.length == 1)
assert(testCases(0).toString() == """{"output":null,"name":"Say Hi!","test_code":null,"message":null,"status":"pass"}""")
}

test("A failing xml should contain a failure object") {
test("A failing xml should contain a failure object"):
val xmlTestURL = getClass.getResource("/GradeSchool_failure.xml").getFile
val jsonArray = getTestCasesJSON(xmlTestURL)
val objects = (0 until jsonArray.length).map(jsonArray.getJSONObject(_).optJSONObject("failure"))
val jsonArray = getTestCasesJSON(xmlTestURL)
val objects = (0 until jsonArray.length).map(jsonArray.getJSONObject(_).optJSONObject("failure"))
objects.exists(_ !== null)
}

test("A failing xml should be properly formatted as JSON") {
val xmlTestURL = getClass.getResource("/GradeSchool_failure.xml").getPath
val outputFileURL = getClass.getResource("/outputs/output_fail.txt").getPath
test("A failing xml should be properly formatted as JSON"):
val xmlTestURL = getClass.getResource("/GradeSchool_failure.xml").getPath
val outputFileURL = getClass.getResource("/outputs/output_fail.txt").getPath
val exercismOutput: JSONObject = Application.toExercismJSON(outputFileURL, List(new File(xmlTestURL)))
assert(exercismOutput.getInt("version") == 2)
assert(exercismOutput.getString("status") == "fail")
Expand All @@ -80,19 +74,15 @@ class ApplicationSpec extends AnyFunSuite with Matchers {
assert(testCases(4).toString() == """{"output":null,"name":"get students in a grade","test_code":null,"message":null,"status":"pass"}""")
assert(testCases(5).toString() == """{"output":null,"name":"get students in a non-existent grade","test_code":null,"message":null,"status":"pass"}""")
assert(testCases(6).toString() == """{"output":null,"name":"sort school","test_code":null,"message":null,"status":"pass"}""")
}

test("An xml with a syntax error should be properly reported as JSON") {
val outputFileURL = getClass.getResource("/outputs/output_error.txt").getPath
test("An xml with a syntax error should be properly reported as JSON"):
val outputFileURL = getClass.getResource("/outputs/output_error.txt").getPath
val exercismOutput: JSONObject = Application.toExercismJSON(outputFileURL, null)
assert(exercismOutput.getInt("version") == 2)
assert(exercismOutput.getString("status") == "error")
}

test("An xml with a syntax error due to an empty file should be properly reported as JSON") {
val outputFileURL = getClass.getResource("/outputs/output_empty.txt").getPath
test("An xml with a syntax error due to an empty file should be properly reported as JSON"):
val outputFileURL = getClass.getResource("/outputs/output_empty.txt").getPath
val exercismOutput: JSONObject = Application.toExercismJSON(outputFileURL, null)
assert(exercismOutput.getInt("version") == 2)
assert(exercismOutput.getString("status") == "error")
}
}
2 changes: 1 addition & 1 deletion tests/example-empty-file/expected_results.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"message":"/solution/src/test/scala/ExampleEmptyFileTest.scala:8: error: not found: value Leap\n Leap.leapYear(2015) should be (false)\n ^\n/solution/src/test/scala/ExampleEmptyFileTest.scala:13: error: not found: value Leap\n Leap.leapYear(1996) should be (true)\n ^\n/solution/src/test/scala/ExampleEmptyFileTest.scala:18: error: not found: value Leap\n Leap.leapYear(2100) should be (false)\n ^\n/solution/src/test/scala/ExampleEmptyFileTest.scala:23: error: not found: value Leap\n Leap.leapYear(2000) should be (true)\n ^\n4 errors\n","version":2,"status":"error"}
{"message":"\u001b[31m\u001b[31m-- [E006] Not Found Error: /solution/src/test/scala/ExampleEmptyFileTest.scala:8:4 \u001b[0m\u001b[0m\n\u001b[31m8 |\u001b[0m Leap.leapYear(\u001b[31m2015\u001b[0m) should be (\u001b[31mfalse\u001b[0m)\n\u001b[31m\u001b[31m |\u001b[0m ^^^^\u001b[0m\n\u001b[31m |\u001b[0m Not found: Leap - did you mean trap?\n\u001b[31m |\u001b[0m\n\u001b[31m |\u001b[0m longer explanation available when compiling with `-explain`\n\u001b[31m\u001b[31m-- [E006] Not Found Error: /solution/src/test/scala/ExampleEmptyFileTest.scala:13:4 \u001b[0m\u001b[0m\n\u001b[31m13 |\u001b[0m Leap.leapYear(\u001b[31m1996\u001b[0m) should be (\u001b[31mtrue\u001b[0m)\n\u001b[31m\u001b[31m |\u001b[0m ^^^^\u001b[0m\n\u001b[31m |\u001b[0m Not found: Leap - did you mean trap?\n\u001b[31m |\u001b[0m\n\u001b[31m |\u001b[0m longer explanation available when compiling with `-explain`\n\u001b[31m\u001b[31m-- [E006] Not Found Error: /solution/src/test/scala/ExampleEmptyFileTest.scala:18:4 \u001b[0m\u001b[0m\n\u001b[31m18 |\u001b[0m Leap.leapYear(\u001b[31m2100\u001b[0m) should be (\u001b[31mfalse\u001b[0m)\n\u001b[31m\u001b[31m |\u001b[0m ^^^^\u001b[0m\n\u001b[31m |\u001b[0m Not found: Leap - did you mean trap?\n\u001b[31m |\u001b[0m\n\u001b[31m |\u001b[0m longer explanation available when compiling with `-explain`\n\u001b[31m\u001b[31m-- [E006] Not Found Error: /solution/src/test/scala/ExampleEmptyFileTest.scala:23:4 \u001b[0m\u001b[0m\n\u001b[31m23 |\u001b[0m Leap.leapYear(\u001b[31m2000\u001b[0m) should be (\u001b[31mtrue\u001b[0m)\n\u001b[31m\u001b[31m |\u001b[0m ^^^^\u001b[0m\n\u001b[31m |\u001b[0m Not found: Leap - did you mean trap?\n\u001b[31m |\u001b[0m\n\u001b[31m |\u001b[0m longer explanation available when compiling with `-explain`\n\u001b[33m\u001b[33m-- Warning: /solution/src/test/scala/ExampleEmptyFileTest.scala:8:24 -------\u001b[0m\u001b[0m\n\u001b[33m8 |\u001b[0m Leap.leapYear(\u001b[31m2015\u001b[0m) should be (\u001b[31mfalse\u001b[0m)\n\u001b[33m\u001b[33m |\u001b[0m ^^^^^^\u001b[0m\n\u001b[33m |\u001b[0mAlphanumeric method should is not declared \u001b[33minfix\u001b[0m; it should not be used as infix operator.\n\u001b[33m |\u001b[0mInstead, use method syntax .should(...) or backticked identifier `should`.\n\u001b[33m |\u001b[0mThe latter can be rewritten automatically under -rewrite -source 3.4-migration.\n\u001b[33m\u001b[33m-- Warning: /solution/src/test/scala/ExampleEmptyFileTest.scala:13:24 ------\u001b[0m\u001b[0m\n\u001b[33m13 |\u001b[0m Leap.leapYear(\u001b[31m1996\u001b[0m) should be (\u001b[31mtrue\u001b[0m)\n\u001b[33m\u001b[33m |\u001b[0m ^^^^^^\u001b[0m\n\u001b[33m |\u001b[0mAlphanumeric method should is not declared \u001b[33minfix\u001b[0m; it should not be used as infix operator.\n\u001b[33m |\u001b[0mInstead, use method syntax .should(...) or backticked identifier `should`.\n\u001b[33m |\u001b[0mThe latter can be rewritten automatically under -rewrite -source 3.4-migration.\n\u001b[33m\u001b[33m-- Warning: /solution/src/test/scala/ExampleEmptyFileTest.scala:18:24 ------\u001b[0m\u001b[0m\n\u001b[33m18 |\u001b[0m Leap.leapYear(\u001b[31m2100\u001b[0m) should be (\u001b[31mfalse\u001b[0m)\n\u001b[33m\u001b[33m |\u001b[0m ^^^^^^\u001b[0m\n\u001b[33m |\u001b[0mAlphanumeric method should is not declared \u001b[33minfix\u001b[0m; it should not be used as infix operator.\n\u001b[33m |\u001b[0mInstead, use method syntax .should(...) or backticked identifier `should`.\n\u001b[33m |\u001b[0mThe latter can be rewritten automatically under -rewrite -source 3.4-migration.\n\u001b[33m\u001b[33m-- Warning: /solution/src/test/scala/ExampleEmptyFileTest.scala:23:24 ------\u001b[0m\u001b[0m\n\u001b[33m23 |\u001b[0m Leap.leapYear(\u001b[31m2000\u001b[0m) should be (\u001b[31mtrue\u001b[0m)\n\u001b[33m\u001b[33m |\u001b[0m ^^^^^^\u001b[0m\n\u001b[33m |\u001b[0mAlphanumeric method should is not declared \u001b[33minfix\u001b[0m; it should not be used as infix operator.\n\u001b[33m |\u001b[0mInstead, use method syntax .should(...) or backticked identifier `should`.\n\u001b[33m |\u001b[0mThe latter can be rewritten automatically under -rewrite -source 3.4-migration.\n4 warnings found\n4 errors found\n","version":2,"status":"error"}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import java.time.LocalDate

import monocle.Lens
import monocle.syntax.all._
import monocle.macros.syntax.lens._
import monocle.macros.GenLens

object ExampleLensPerson {
Expand Down
29 changes: 1 addition & 28 deletions tests/example-multiple-tests/expected_results.json
Original file line number Diff line number Diff line change
@@ -1,28 +1 @@
{
"tests": [
{
"output": null,
"name": "two-one",
"test_code": null,
"message": null,
"status": "pass"
},
{
"output": null,
"name": "one-one",
"test_code": null,
"message": null,
"status": "pass"
},
{
"output": null,
"name": "one-two",
"test_code": null,
"message": null,
"status": "pass"
}
],
"message": null,
"version": 2,
"status": "pass"
}
{"tests":[{"output":null,"name":"one-one","test_code":null,"message":null,"status":"pass"},{"output":null,"name":"two-one","test_code":null,"message":null,"status":"pass"},{"output":null,"name":"one-two","test_code":null,"message":null,"status":"pass"}],"message":null,"version":2,"status":"pass"}
Loading