From 2af14442ee7e9406446056f0ad2e8cf3cb752c89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Tue, 24 Sep 2019 11:56:48 +0200 Subject: [PATCH] Import funsets handout --- .vscode/settings.json | 8 + build.sbt | 12 + grading-tests.jar | Bin 0 -> 40005 bytes project/MOOCSettings.scala | 23 ++ project/StudentTasks.scala | 323 ++++++++++++++++++ project/build.properties | 1 + project/buildSettings.sbt | 8 + project/plugins.sbt | 2 + src/main/scala/funsets/FunSets.scala | 91 +++++ src/main/scala/funsets/FunSetsInterface.scala | 20 ++ src/main/scala/funsets/Main.scala | 6 + src/test/scala/funsets/FunSetSuite.scala | 73 ++++ student.sbt | 9 + 13 files changed, 576 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/funsets/FunSets.scala create mode 100644 src/main/scala/funsets/FunSetsInterface.scala create mode 100644 src/main/scala/funsets/Main.scala create mode 100644 src/test/scala/funsets/FunSetSuite.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..324e60b --- /dev/null +++ b/build.sbt @@ -0,0 +1,12 @@ +course := "progfun1" +assignment := "funsets" +name := course.value + "-" + assignment.value +testSuite := "funsets.FunSetSuite" + +scalaVersion := "0.19.0-bin-20190918-dd68eb8-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..26d869ec492c16724fa4fdeb100f23b76d68017f GIT binary patch literal 40005 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 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..587ba85 --- /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(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 := { + 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/funsets/FunSets.scala b/src/main/scala/funsets/FunSets.scala new file mode 100644 index 0000000..3ea7c4f --- /dev/null +++ b/src/main/scala/funsets/FunSets.scala @@ -0,0 +1,91 @@ +package funsets + +/** + * 2. Purely Functional Sets. + */ +trait FunSets extends FunSetsInterface { + /** + * We represent a set by its characteristic function, i.e. + * its `contains` predicate. + */ + override type FunSet = Int => Boolean + + /** + * Indicates whether a set contains a given element. + */ + def contains(s: FunSet, elem: Int): Boolean = s(elem) + + /** + * Returns the set of the one given element. + */ + def singletonSet(elem: Int): FunSet = ??? + + + /** + * Returns the union of the two given sets, + * the sets of all elements that are in either `s` or `t`. + */ + def union(s: FunSet, t: FunSet): FunSet = ??? + + /** + * Returns the intersection of the two given sets, + * the set of all elements that are both in `s` and `t`. + */ + def intersect(s: FunSet, t: FunSet): FunSet = ??? + + /** + * Returns the difference of the two given sets, + * the set of all elements of `s` that are not in `t`. + */ + def diff(s: FunSet, t: FunSet): FunSet = ??? + + /** + * Returns the subset of `s` for which `p` holds. + */ + def filter(s: FunSet, p: Int => Boolean): FunSet = ??? + + + /** + * The bounds for `forall` and `exists` are +/- 1000. + */ + val bound = 1000 + + /** + * Returns whether all bounded integers within `s` satisfy `p`. + */ + def forall(s: FunSet, p: Int => Boolean): Boolean = + def iter(a: Int): Boolean = + if ??? then + ??? + else if ??? then + ??? + else + iter(???) + iter(???) + + /** + * Returns whether there exists a bounded integer within `s` + * that satisfies `p`. + */ + def exists(s: FunSet, p: Int => Boolean): Boolean = ??? + + /** + * Returns a set transformed by applying `f` to each element of `s`. + */ + def map(s: FunSet, f: Int => Int): FunSet = ??? + + /** + * Displays the contents of a set + */ + def toString(s: FunSet): String = + val xs = for i <- (-bound to bound) if contains(s, i) yield i + xs.mkString("{", ",", "}") + + /** + * Prints the contents of a set on the console. + */ + def printSet(s: FunSet): Unit = + println(toString(s)) +} + +object FunSets extends FunSets diff --git a/src/main/scala/funsets/FunSetsInterface.scala b/src/main/scala/funsets/FunSetsInterface.scala new file mode 100644 index 0000000..5e5ca94 --- /dev/null +++ b/src/main/scala/funsets/FunSetsInterface.scala @@ -0,0 +1,20 @@ +package funsets + +/** + * The interface used by the grading infrastructure. You should not edit any + * code here, or your submission may fail with a NoSuchMethodError. + */ +trait FunSetsInterface { + type FunSet = Int => Boolean + + def contains(s: FunSet, elem: Int): Boolean + def singletonSet(elem: Int): FunSet + def union(s: FunSet, t: FunSet): FunSet + def intersect(s: FunSet, t: Int => Boolean): FunSet + def diff(s: FunSet, t: FunSet): FunSet + def filter(s: FunSet, p: Int => Boolean): FunSet + def forall(s: FunSet, p: Int => Boolean): Boolean + def exists(s: FunSet, p: Int => Boolean): Boolean + def map(s: FunSet, f: Int => Int): FunSet + def toString(s: FunSet): String +} diff --git a/src/main/scala/funsets/Main.scala b/src/main/scala/funsets/Main.scala new file mode 100644 index 0000000..6126909 --- /dev/null +++ b/src/main/scala/funsets/Main.scala @@ -0,0 +1,6 @@ +package funsets + +object Main extends App { + import FunSets._ + println(contains(singletonSet(1), 1)) +} diff --git a/src/test/scala/funsets/FunSetSuite.scala b/src/test/scala/funsets/FunSetSuite.scala new file mode 100644 index 0000000..2837ad4 --- /dev/null +++ b/src/test/scala/funsets/FunSetSuite.scala @@ -0,0 +1,73 @@ +package funsets + +import org.junit._ + +/** + * This class is a test suite for the methods in object FunSets. + * + * To run this test suite, start "sbt" then run the "test" command. + */ +class FunSetSuite { + + import FunSets._ + + @Test def `contains is implemented`: Unit = + assert(contains(x => true, 100)) + + /** + * When writing tests, one would often like to re-use certain values for multiple + * tests. For instance, we would like to create an Int-set and have multiple test + * about it. + * + * Instead of copy-pasting the code for creating the set into every test, we can + * store it in the test class using a val: + * + * val s1 = singletonSet(1) + * + * However, what happens if the method "singletonSet" has a bug and crashes? Then + * the test methods are not even executed, because creating an instance of the + * test class fails! + * + * Therefore, we put the shared values into a separate trait (traits are like + * abstract classes), and create an instance inside each test method. + * + */ + + trait TestSets { + val s1 = singletonSet(1) + val s2 = singletonSet(2) + val s3 = singletonSet(3) + } + + /** + * This test is currently disabled (by using @Ignore) because the method + * "singletonSet" is not yet implemented and the test would fail. + * + * Once you finish your implementation of "singletonSet", remvoe the + * @Ignore annotation. + */ + @Ignore("not ready yet") @Test def `singleton set one contains one`: Unit = + /** + * We create a new instance of the "TestSets" trait, this gives us access + * to the values "s1" to "s3". + */ + new TestSets { + /** + * The string argument of "assert" is a message that is printed in case + * the test fails. This helps identifying which assertion failed. + */ + assert(contains(s1, 1), "Singleton") + } + + @Test def `union contains all elements of each set`: Unit = + new TestSets { + val s = union(s1, s2) + assert(contains(s, 1), "Union 1") + assert(contains(s, 2), "Union 2") + assert(!contains(s, 3), "Union 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)