From a57aa454e4fcfff4a13e8c2707f7bc6fcf083974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Wed, 18 Sep 2019 16:21:01 +0200 Subject: [PATCH] Import recfun handout --- .vscode/settings.json | 8 + build.sbt | 12 + grading-tests.jar | Bin 0 -> 6477 bytes project/MOOCSettings.scala | 23 ++ project/StudentTasks.scala | 323 ++++++++++++++++++++ project/build.properties | 1 + project/buildSettings.sbt | 8 + project/plugins.sbt | 2 + src/main/scala/recfun/RecFun.scala | 26 ++ src/main/scala/recfun/RecFunInterface.scala | 11 + src/test/scala/recfun/RecFunSuite.scala | 49 +++ student.sbt | 9 + 12 files changed, 472 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 build.sbt create mode 100644 grading-tests.jar create mode 100644 project/MOOCSettings.scala create mode 100644 project/StudentTasks.scala create mode 100644 project/build.properties create mode 100644 project/buildSettings.sbt create mode 100644 project/plugins.sbt create mode 100644 src/main/scala/recfun/RecFun.scala create mode 100644 src/main/scala/recfun/RecFunInterface.scala create mode 100644 src/test/scala/recfun/RecFunSuite.scala create mode 100644 student.sbt diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a35362b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "dotty": { + "trace": { + "remoteTracingUrl": "wss://lamppc36.epfl.ch/dotty-remote-tracer/upload/lsp.log", + "server": { "format": "JSON", "verbosity": "verbose" } + } + } +} diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..41b9ea8 --- /dev/null +++ b/build.sbt @@ -0,0 +1,12 @@ +course := "progfun1" +assignment := "recfun" +name := course.value + "-" + assignment.value +testSuite := "recfun.RecFunSuite" + +scalaVersion := "0.19.0-bin-20190917-d821081-NIGHTLY" + +scalacOptions ++= Seq("-language:implicitConversions", "-deprecation") + +libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % Test + +testOptions in Test += Tests.Argument(TestFrameworks.JUnit, "-a", "-v", "-s") diff --git a/grading-tests.jar b/grading-tests.jar new file mode 100644 index 0000000000000000000000000000000000000000..a8a7e5c2bc201a0af91508f07266dfa5d0c300a5 GIT binary patch literal 6477 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 I{_*vH043wc+W-In literal 0 HcmV?d00001 diff --git a/project/MOOCSettings.scala b/project/MOOCSettings.scala new file mode 100644 index 0000000..80e34b8 --- /dev/null +++ b/project/MOOCSettings.scala @@ -0,0 +1,23 @@ +package ch.epfl.lamp + +import sbt._ +import sbt.Keys._ + +/** + * Settings shared by all assignments, reused in various tasks. + */ +object MOOCSettings extends AutoPlugin { + + object autoImport { + val course = SettingKey[String]("course") + val assignment = SettingKey[String]("assignment") + val testSuite = SettingKey[String]("testSuite") + val options = SettingKey[Map[String, Map[String, String]]]("options") + } + + override def trigger = allRequirements + + override val projectSettings: Seq[Def.Setting[_]] = Seq( + parallelExecution in Test := false + ) +} diff --git a/project/StudentTasks.scala b/project/StudentTasks.scala new file mode 100644 index 0000000..bec5cec --- /dev/null +++ b/project/StudentTasks.scala @@ -0,0 +1,323 @@ +package ch.epfl.lamp + +import sbt._ +import Keys._ + +// import scalaj.http._ +import java.io.{File, FileInputStream, IOException} +import java.nio.file.FileSystems +import org.apache.commons.codec.binary.Base64 +// import play.api.libs.json.{Json, JsObject, JsPath} +import scala.util.{Failure, Success, Try} + +import MOOCSettings.autoImport._ + +case class AssignmentInfo( + key: String, + itemId: String, + premiumItemId: Option[String], + partId: String +) + +/** + * Provides tasks for submitting the assignment + */ +object StudentTasks extends AutoPlugin { + + object autoImport { + val assignmentInfo = SettingKey[AssignmentInfo]("assignmentInfo") + + val packageSourcesOnly = TaskKey[File]("packageSourcesOnly", "Package the sources of the project") + val packageBinWithoutResources = TaskKey[File]("packageBinWithoutResources", "Like packageBin, but without the resources") + val packageSubmissionZip = TaskKey[File]("packageSubmissionZip") + val packageSubmission = inputKey[Unit]("package solution as an archive file") + val runGradingTests = taskKey[Unit]("run black-box tests used for final grading") + } + + + import autoImport._ + + override lazy val projectSettings = Seq( + packageSubmissionSetting, + // submitSetting, // FIXME: restore assignmentInfo setting on assignments + runGradingTestsSettings, + + fork := true, + connectInput in run := true, + outputStrategy := Some(StdoutOutput) + ) ++ packageSubmissionZipSettings + + lazy val runGradingTestsSettings = runGradingTests := { + val testSuiteJar = "grading-tests.jar" + if (!new File(testSuiteJar).exists) { + throw new MessageOnlyException(s"Could not find tests JarFile: $testSuiteJar") + } + + val classPath = s"${(Test / dependencyClasspath).value.map(_.data).mkString(":")}:$testSuiteJar" + val junitProcess = + Fork.java.fork( + ForkOptions(), + "-cp" :: classPath :: + "org.junit.runner.JUnitCore" :: + (Test / testSuite).value :: + Nil + ) + + // Wait for tests to complete. + junitProcess.exitValue() + } + + + /** ********************************************************** + * SUBMITTING A SOLUTION TO COURSERA + */ + + val packageSubmissionZipSettings = Seq( + packageSubmissionZip := { + val submission = crossTarget.value / "submission.zip" + val sources = (packageSourcesOnly in Compile).value + val binaries = (packageBinWithoutResources in Compile).value + IO.zip(Seq(sources -> "sources.zip", binaries -> "binaries.jar"), submission) + submission + }, + artifactClassifier in packageSourcesOnly := Some("sources"), + artifact in (Compile, packageBinWithoutResources) ~= (art => art.withName(art.name + "-without-resources")) + ) ++ + inConfig(Compile)( + Defaults.packageTaskSettings(packageSourcesOnly, Defaults.sourceMappings) ++ + Defaults.packageTaskSettings(packageBinWithoutResources, Def.task { + val relativePaths = + (unmanagedResources in Compile).value.flatMap(Path.relativeTo((unmanagedResourceDirectories in Compile).value)(_)) + (mappings in (Compile, packageBin)).value.filterNot { case (_, path) => relativePaths.contains(path) } + }) + ) + + val maxSubmitFileSize = { + val mb = 1024 * 1024 + 10 * mb + } + + /** Check that the jar exists, isn't empty, isn't crazy big, and can be read + * If so, encode jar as base64 so we can send it to Coursera + */ + def prepareJar(jar: File, s: TaskStreams): String = { + val errPrefix = "Error submitting assignment jar: " + val fileLength = jar.length() + if (!jar.exists()) { + s.log.error(errPrefix + "jar archive does not exist\n" + jar.getAbsolutePath) + failSubmit() + } else if (fileLength == 0L) { + s.log.error(errPrefix + "jar archive is empty\n" + jar.getAbsolutePath) + failSubmit() + } else if (fileLength > maxSubmitFileSize) { + s.log.error(errPrefix + "jar archive is too big. Allowed size: " + + maxSubmitFileSize + " bytes, found " + fileLength + " bytes.\n" + + jar.getAbsolutePath) + failSubmit() + } else { + val bytes = new Array[Byte](fileLength.toInt) + val sizeRead = try { + val is = new FileInputStream(jar) + val read = is.read(bytes) + is.close() + read + } catch { + case ex: IOException => + s.log.error(errPrefix + "failed to read sources jar archive\n" + ex.toString) + failSubmit() + } + if (sizeRead != bytes.length) { + s.log.error(errPrefix + "failed to read the sources jar archive, size read: " + sizeRead) + failSubmit() + } else encodeBase64(bytes) + } + } + + /** Task to package solution to a given file path */ + lazy val packageSubmissionSetting = packageSubmission := { + val args: Seq[String] = Def.spaceDelimited("[path]").parsed + val s: TaskStreams = streams.value // for logging + val jar = (packageSubmissionZip in Compile).value + + val base64Jar = prepareJar(jar, s) + + val path = args.headOption.getOrElse((baseDirectory.value / "submission.jar").absolutePath) + scala.tools.nsc.io.File(path).writeAll(base64Jar) + } + +/* + /** Task to submit a solution to coursera */ + val submit = inputKey[Unit]("submit solution to Coursera") + lazy val submitSetting = submit := { + val args: Seq[String] = Def.spaceDelimited("").parsed + val s: TaskStreams = streams.value // for logging + val jar = (packageSubmissionZip in Compile).value + + val assignmentDetails = assignmentInfo.value + val assignmentKey = assignmentDetails.key + val courseName = + course.value match { + case "capstone" => "scala-capstone" + case "bigdata" => "scala-spark-big-data" + case other => other + } + + val partId = assignmentDetails.partId + val itemId = assignmentDetails.itemId + val premiumItemId = assignmentDetails.premiumItemId + + val (email, secret) = args match { + case email :: secret :: Nil => + (email, secret) + case _ => + val inputErr = + s"""|Invalid input to `submit`. The required syntax for `submit` is: + |submit + | + |The submit token is NOT YOUR LOGIN PASSWORD. + |It can be obtained from the assignment page: + |https://www.coursera.org/learn/$courseName/programming/$itemId + |${ + premiumItemId.fold("") { id => + s"""or (for premium learners): + |https://www.coursera.org/learn/$courseName/programming/$id + """.stripMargin + } + } + """.stripMargin + s.log.error(inputErr) + failSubmit() + } + + val base64Jar = prepareJar(jar, s) + val json = + s"""|{ + | "assignmentKey":"$assignmentKey", + | "submitterEmail":"$email", + | "secret":"$secret", + | "parts":{ + | "$partId":{ + | "output":"$base64Jar" + | } + | } + |}""".stripMargin + + def postSubmission[T](data: String): Try[HttpResponse[String]] = { + val http = Http("https://www.coursera.org/api/onDemandProgrammingScriptSubmissions.v1") + val hs = List( + ("Cache-Control", "no-cache"), + ("Content-Type", "application/json") + ) + s.log.info("Connecting to Coursera...") + val response = Try(http.postData(data) + .headers(hs) + .option(HttpOptions.connTimeout(10000)) // scalaj default timeout is only 100ms, changing that to 10s + .asString) // kick off HTTP POST + response + } + + val connectMsg = + s"""|Attempting to submit "${assignment.value}" assignment in "$courseName" course + |Using: + |- email: $email + |- submit token: $secret""".stripMargin + s.log.info(connectMsg) + + def reportCourseraResponse(response: HttpResponse[String]): Unit = { + val code = response.code + val respBody = response.body + + /* Sample JSON response from Coursera + { + "message": "Invalid email or token.", + "details": { + "learnerMessage": "Invalid email or token." + } + } + */ + + // Success, Coursera responds with 2xx HTTP status code + if (response.is2xx) { + val successfulSubmitMsg = + s"""|Successfully connected to Coursera. (Status $code) + | + |Assignment submitted successfully! + | + |You can see how you scored by going to: + |https://www.coursera.org/learn/$courseName/programming/$itemId/ + |${ + premiumItemId.fold("") { id => + s"""or (for premium learners): + |https://www.coursera.org/learn/$courseName/programming/$id + """.stripMargin + } + } + |and clicking on "My Submission".""".stripMargin + s.log.info(successfulSubmitMsg) + } + + // Failure, Coursera responds with 4xx HTTP status code (client-side failure) + else if (response.is4xx) { + val result = Try(Json.parse(respBody)).toOption + val learnerMsg = result match { + case Some(resp: JsObject) => + (JsPath \ "details" \ "learnerMessage").read[String].reads(resp).get + case Some(x) => // shouldn't happen + "Could not parse Coursera's response:\n" + x + case None => + "Could not parse Coursera's response:\n" + respBody + } + val failedSubmitMsg = + s"""|Submission failed. + |There was something wrong while attempting to submit. + |Coursera says: + |$learnerMsg (Status $code)""".stripMargin + s.log.error(failedSubmitMsg) + } + + // Failure, Coursera responds with 5xx HTTP status code (server-side failure) + else if (response.is5xx) { + val failedSubmitMsg = + s"""|Submission failed. + |Coursera seems to be unavailable at the moment (Status $code) + |Check https://status.coursera.org/ and try again in a few minutes. + """.stripMargin + s.log.error(failedSubmitMsg) + } + + // Failure, Coursera repsonds with an unexpected status code + else { + val failedSubmitMsg = + s"""|Submission failed. + |Coursera replied with an unexpected code (Status $code) + """.stripMargin + s.log.error(failedSubmitMsg) + } + } + + // kick it all off, actually make request + postSubmission(json) match { + case Success(resp) => reportCourseraResponse(resp) + case Failure(e) => + val failedConnectMsg = + s"""|Connection to Coursera failed. + |There was something wrong while attempting to connect to Coursera. + |Check your internet connection. + |${e.toString}""".stripMargin + s.log.error(failedConnectMsg) + } + + } +*/ + + def failSubmit(): Nothing = { + sys.error("Submission failed") + } + + /** + * ***************** + * DEALING WITH JARS + */ + def encodeBase64(bytes: Array[Byte]): String = + new String(Base64.encodeBase64(bytes)) +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..c0bab04 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.2.8 diff --git a/project/buildSettings.sbt b/project/buildSettings.sbt new file mode 100644 index 0000000..a309025 --- /dev/null +++ b/project/buildSettings.sbt @@ -0,0 +1,8 @@ +libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % Test +// Used for base64 encoding +libraryDependencies += "commons-codec" % "commons-codec" % "1.10" + +// Used for Coursera submussion +// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.3.0" +// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.6.9" + diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..64a2492 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("io.get-coursier" % "sbt-coursier" % "2.0.0-RC3-5") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.3.4") diff --git a/src/main/scala/recfun/RecFun.scala b/src/main/scala/recfun/RecFun.scala new file mode 100644 index 0000000..4a2b054 --- /dev/null +++ b/src/main/scala/recfun/RecFun.scala @@ -0,0 +1,26 @@ +package recfun + +object RecFun extends RecFunInterface { + + def main(args: Array[String]): Unit = + println("Pascal's Triangle") + for row <- 0 to 10 do + for col <- 0 to row do + print(s"${pascal(col, row)} ") + println() + + /** + * Exercise 1 + */ + def pascal(c: Int, r: Int): Int = ??? + + /** + * Exercise 2 + */ + def balance(chars: List[Char]): Boolean = ??? + + /** + * Exercise 3 + */ + def countChange(money: Int, coins: List[Int]): Int = ??? +} diff --git a/src/main/scala/recfun/RecFunInterface.scala b/src/main/scala/recfun/RecFunInterface.scala new file mode 100644 index 0000000..2de3ca3 --- /dev/null +++ b/src/main/scala/recfun/RecFunInterface.scala @@ -0,0 +1,11 @@ +package recfun + +/** + * The interface used by the grading infrastructure. Do not change signatures + * or your submission will fail with a NoSuchMethodError. + */ +trait RecFunInterface { + def pascal(c: Int, r: Int): Int + def balance(chars: List[Char]): Boolean + def countChange(money: Int, coins: List[Int]): Int +} diff --git a/src/test/scala/recfun/RecFunSuite.scala b/src/test/scala/recfun/RecFunSuite.scala new file mode 100644 index 0000000..4eefe37 --- /dev/null +++ b/src/test/scala/recfun/RecFunSuite.scala @@ -0,0 +1,49 @@ +package recfun + +import org.junit._ +import org.junit.Assert.assertEquals + +class RecFunSuite { + import RecFun._ + + // ------ balance tests ----------------------------------------------------- + + @Test def `balance: '(if (zero? x) max (/ 1 x))' is balanced`: Unit = + assert(balance("(if (zero? x) max (/ 1 x))".toList)) + + @Test def `balance: 'I told him ...' is balanced`: Unit = + assert(balance("I told him (that it's not (yet) done).\n(But he wasn't listening)".toList)) + + @Test def `balance: ':-)' is unbalanced`: Unit = + assert(!balance(":-)".toList)) + + @Test def `balance: counting is not enough`: Unit = + assert(!balance("())(".toList)) + + // ------ countChange tests ------------------------------------------------- + + @Test def `countChange: example given in instructions`: Unit = + assertEquals(3, countChange(4,List(1,2))) + + @Test def `countChange: sorted CHF`: Unit = + assertEquals(1022, countChange(300,List(5,10,20,50,100,200,500))) + + @Test def `countChange: no pennies`: Unit = + assertEquals(0, countChange(301,List(5,10,20,50,100,200,500))) + + @Test def `countChange: unsorted CHF`: Unit = + assertEquals(1022, countChange(300,List(500,5,50,100,20,200,10))) + + // ------ pascal tests ------------------------------------------------------ + + @Test def `pascal: col=0,row=2`: Unit = + assertEquals(1, pascal(0, 2)) + + @Test def `pascal: col=1,row=2`: Unit = + assertEquals(2, pascal(1, 2)) + + @Test def `pascal: col=1,row=3`: Unit = + assertEquals(3, pascal(1, 3)) + + @Rule def individualTestTimeout = new org.junit.rules.Timeout(10 * 1000) +} diff --git a/student.sbt b/student.sbt new file mode 100644 index 0000000..855fa0c --- /dev/null +++ b/student.sbt @@ -0,0 +1,9 @@ +// Used for base64 encoding +libraryDependencies += "commons-codec" % "commons-codec" % "1.10" + +// Used for Coursera submussion +// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2" +// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4" + +// Student tasks (i.e. packageSubmission) +enablePlugins(StudentTasks)