From 3813542c4eacec252f7088ab75da60a23a7cd75b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Tue, 19 Feb 2019 20:44:23 +0100 Subject: [PATCH] Add example assignment --- .gitignore | 16 ++ .gitlab-ci.yml | 37 +++ .vscode/settings.json | 8 + assignment.sbt | 9 + build.sbt | 8 + grading-tests.jar | Bin 0 -> 5053 bytes project/MOOCSettings.scala | 46 ++++ project/StudentTasks.scala | 318 ++++++++++++++++++++++++ project/build.properties | 1 + project/buildSettings.sbt | 5 + project/plugins.sbt | 2 + src/main/scala/example/Lists.scala | 41 +++ src/test/scala/example/ListsSuite.scala | 99 ++++++++ 13 files changed, 590 insertions(+) create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .vscode/settings.json create mode 100644 assignment.sbt 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/example/Lists.scala create mode 100644 src/test/scala/example/ListsSuite.scala diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..349d2e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# General +*.DS_Store +*.swp +*~ + +# Dotty +*.class +*.tasty +*.hasTasty + +# sbt +target/ + +# Dotty IDE +/.dotty-ide-artifact +/.dotty-ide.json diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..21abce1 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,37 @@ +# This configuration is not used for the final grading, you can change it if +# you know what you're doing. + +stages: + - build + - grade + +compile: + stage: build + image: lampepfl/moocs:dotty-2020-02-12 + except: + - tags + tags: + - cs206 + script: + - sbt packageSubmission + artifacts: + expire_in: 1 day + paths: + - submission.jar + +grade: + stage: grade + except: + - tags + tags: + - cs206 + image: + name: smarter3/moocs:progfun1-example-2020-02-14 + entrypoint: [""] + allow_failure: true + before_script: + - mkdir -p /shared/submission/ + - cp submission.jar /shared/submission/submission.jar + script: + - cd /grader + - /grader/grade | /grader/feedback-printer 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/assignment.sbt b/assignment.sbt new file mode 100644 index 0000000..76b01fe --- /dev/null +++ b/assignment.sbt @@ -0,0 +1,9 @@ +// Student tasks (i.e. submit, packageSubmission) +enablePlugins(StudentTasks) + +courseraId := ch.epfl.lamp.CourseraId( + key = "g4unnjZBEeWj7SIAC5PFxA", + itemId = "xIz9O", + premiumItemId = None, + partId = "d5jxI" +) diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..16f69eb --- /dev/null +++ b/build.sbt @@ -0,0 +1,8 @@ +course := "progfun1" +assignment := "example" +scalaVersion := "0.23.0-bin-20200211-5b006fb-NIGHTLY" +scalacOptions ++= Seq("-language:implicitConversions", "-deprecation") +libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % Test + +testOptions in Test += Tests.Argument(TestFrameworks.JUnit, "-a", "-v", "-s") +testSuite := "example.ListsSuite" diff --git a/grading-tests.jar b/grading-tests.jar new file mode 100644 index 0000000000000000000000000000000000000000..d1075f1b411ce0195037004aa2054ca4ca239994 GIT binary patch literal 5053 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 CS{D8Q literal 0 HcmV?d00001 diff --git a/project/MOOCSettings.scala b/project/MOOCSettings.scala new file mode 100644 index 0000000..171244f --- /dev/null +++ b/project/MOOCSettings.scala @@ -0,0 +1,46 @@ +package ch.epfl.lamp + +import sbt._ +import sbt.Keys._ + +/** + * Coursera uses two versions of each assignment. They both have the same assignment key and part id but have + * different item ids. + * + * @param key Assignment key + * @param partId Assignment partId + * @param itemId Item id of the non premium version + * @param premiumItemId Item id of the premium version (`None` if the assignment is optional) + */ +case class CourseraId(key: String, partId: String, itemId: String, premiumItemId: Option[String]) + +/** + * 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 options = SettingKey[Map[String, Map[String, String]]]("options") + val courseraId = settingKey[CourseraId]("Coursera-specific information identifying the assignment") + val testSuite = settingKey[String]("Fully qualified name of the test suite of this assignment") + // Convenient alias + type CourseraId = ch.epfl.lamp.CourseraId + val CourseraId = ch.epfl.lamp.CourseraId + } + + import autoImport._ + + override val globalSettings: Seq[Def.Setting[_]] = Seq( + // supershell is verbose, buggy and useless. + useSuperShell := false + ) + + override val projectSettings: Seq[Def.Setting[_]] = Seq( + parallelExecution in Test := false, + // Report test result after each test instead of waiting for every test to finish + logBuffered in Test := false, + name := s"${course.value}-${assignment.value}" + ) +} diff --git a/project/StudentTasks.scala b/project/StudentTasks.scala new file mode 100644 index 0000000..7604830 --- /dev/null +++ b/project/StudentTasks.scala @@ -0,0 +1,318 @@ +package ch.epfl.lamp + +import sbt._ +import Keys._ + +// import scalaj.http._ +import java.io.{File, FileInputStream, IOException} +import org.apache.commons.codec.binary.Base64 +// import play.api.libs.json.{Json, JsObject, JsPath} +import scala.util.{Failure, Success, Try} + +/** + * Provides tasks for submitting the assignment + */ +object StudentTasks extends AutoPlugin { + + override def requires = super.requires && MOOCSettings + + object autoImport { + 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._ + import MOOCSettings.autoImport._ + + override lazy val projectSettings = Seq( + packageSubmissionSetting, + // submitSetting, + 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(File.pathSeparator)}${File.pathSeparator}$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 := { + // Fail if scalafix linting does not pass. + scalafixLinting.value + + val args: Seq[String] = Def.spaceDelimited("").parsed + val s: TaskStreams = streams.value // for logging + val jar = (packageSubmissionZip in Compile).value + + val assignmentDetails = + courseraId.?.value.getOrElse(throw new MessageOnlyException("This assignment can not be submitted to Coursera because the `courseraId` setting is undefined")) + 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..a919a9b --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.3.8 diff --git a/project/buildSettings.sbt b/project/buildSettings.sbt new file mode 100644 index 0000000..8fac702 --- /dev/null +++ b/project/buildSettings.sbt @@ -0,0 +1,5 @@ +// Used for Coursera submission (StudentPlugin) +// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2" +// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4" +// Used for Base64 (StudentPlugin) +libraryDependencies += "commons-codec" % "commons-codec" % "1.10" diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..017735d --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.4.0") diff --git a/src/main/scala/example/Lists.scala b/src/main/scala/example/Lists.scala new file mode 100644 index 0000000..861f40e --- /dev/null +++ b/src/main/scala/example/Lists.scala @@ -0,0 +1,41 @@ +package example + +object Lists { + + /** + * This method computes the sum of all elements in the list xs. There are + * multiple techniques that can be used for implementing this method, and + * you will learn during the class. + * + * For this example assignment you can use the following methods in class + * `List`: + * + * - `xs.isEmpty: Boolean` returns `true` if the list `xs` is empty + * - `xs.head: Int` returns the head element of the list `xs`. If the list + * is empty an exception is thrown + * - `xs.tail: List[Int]` returns the tail of the list `xs`, i.e. the the + * list `xs` without its `head` element + * + * ''Hint:'' instead of writing a `for` or `while` loop, think of a recursive + * solution. + * + * @param xs A list of natural numbers + * @return The sum of all elements in `xs` + */ + def sum(xs: List[Int]): Int = ??? + + /** + * This method returns the largest element in a list of integers. If the + * list `xs` is empty it throws a `java.util.NoSuchElementException`. + * + * You can use the same methods of the class `List` as mentioned above. + * + * ''Hint:'' Again, think of a recursive solution instead of using looping + * constructs. You might need to define an auxiliary method. + * + * @param xs A list of natural numbers + * @return The largest element in `xs` + * @throws java.util.NoSuchElementException if `xs` is an empty list + */ + def max(xs: List[Int]): Int = ??? +} diff --git a/src/test/scala/example/ListsSuite.scala b/src/test/scala/example/ListsSuite.scala new file mode 100644 index 0000000..ed42759 --- /dev/null +++ b/src/test/scala/example/ListsSuite.scala @@ -0,0 +1,99 @@ +package example + +import org.junit._ +import org.junit.Assert.assertEquals + +/** + * This class implements a JUnit test suite for the methods in object + * `Lists` that need to be implemented as part of this assignment. A test + * suite is simply a collection of individual tests for some specific + * component of a program. + * + * To run this test suite, start "sbt" then run the "test" command. + */ +class ListsSuite { + + /** + * Tests are written using the @Test annotation + * + * The most common way to implement a test body is using the method `assert` + * which tests that its argument evaluates to `true`. So one of the simplest + * successful tests is the following: + */ + @Test def `one plus one is two (0pts)`: Unit = + assert(1 + 1 == 2) + + @Test def `one plus one is three (0pts)?`: Unit = + assert(1 + 1 == 3) // This assertion fails! Go ahead and fix it. + + /** + * One problem with the previous (failing) test is that JUnit will + * only tell you that a test failed, but it will not tell you what was + * the reason for the failure. The output looks like this: + * + * {{{ + * [info] - one plus one is three? *** FAILED *** + * }}} + * + * This situation can be improved by using a Assert.assertEquals + * (this is only possible in JUnit). So if you + * run the next test, JUnit will show the following output: + * + * {{{ + * [info] - details why one plus one is not three *** FAILED *** + * [info] 2 did not equal 3 (ListsSuite.scala:67) + * }}} + * + * We recommend to always use the Assert.assertEquals equality operator + * when writing tests. + */ + @Test def `details why one plus one is not three (0pts)`: Unit = + Assert.assertEquals(3, 1 + 1) // Fix me, please! + + /** + * Exceptional behavior of a methods can be tested using a try/catch + * and a failed assertion. + * + * In the following example, we test the fact that the method `intNotZero` + * throws an `IllegalArgumentException` if its argument is `0`. + */ + @Test def `intNotZero throws an exception if its argument is 0`: Unit = + try + intNotZero(0) + Assert.fail("No exception has been thrown") + catch + case e: IllegalArgumentException => () + + def intNotZero(x: Int): Int = + if x == 0 then throw new IllegalArgumentException("zero is not allowed") + else x + + /** + * Now we finally write some tests for the list functions that have to be + * implemented for this assignment. We fist import all members of the + * `List` object. + */ + import Lists._ + + + /** + * We only provide two very basic tests for you. Write more tests to make + * sure your `sum` and `max` methods work as expected. + * + * In particular, write tests for corner cases: negative numbers, zeros, + * empty lists, lists with repeated elements, etc. + * + * It is allowed to have multiple `assert` statements inside one test, + * however it is recommended to write an individual `test` statement for + * every tested aspect of a method. + */ + @Test def `sum of a few numbers (10pts)`: Unit = + assert(sum(List(1,2,0)) == 3) + + @Test def `max of a few numbers (10pts)`: Unit = + assert(max(List(3, 7, 2)) == 7) + + + + @Rule def individualTestTimeout = new org.junit.rules.Timeout(1000) +}