From 03319aef3dc0e1d10ebc07d91c960186a5180eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Wed, 30 Oct 2019 15:34:26 +0100 Subject: [PATCH] Import quickcheck handout --- .vscode/settings.json | 8 + build.sbt | 11 + grading-tests.jar | Bin 0 -> 35303 bytes project/MOOCSettings.scala | 25 ++ project/StudentTasks.scala | 323 ++++++++++++++++++ project/build.properties | 1 + project/buildSettings.sbt | 7 + project/plugins.sbt | 2 + src/main/scala/quickcheck/Heap.scala | 119 +++++++ src/main/scala/quickcheck/QuickCheck.scala | 18 + .../scala/quickcheck/QuickCheckSuite.scala | 55 +++ src/test/scala/quickcheck/test/Heap.scala | 96 ++++++ student.sbt | 9 + 13 files changed, 674 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/quickcheck/Heap.scala create mode 100644 src/main/scala/quickcheck/QuickCheck.scala create mode 100644 src/test/scala/quickcheck/QuickCheckSuite.scala create mode 100644 src/test/scala/quickcheck/test/Heap.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..beabef7 --- /dev/null +++ b/build.sbt @@ -0,0 +1,11 @@ +course := "progfun2" +assignment := "quickcheck" +name := course.value + "-" + assignment.value +testSuite := "quickcheck.QuickCheckSuite" + +scalaVersion := "0.19.0-RC1" +scalacOptions ++= Seq("-language:implicitConversions", "-deprecation") +libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % Test +libraryDependencies += ("org.scalacheck" %% "scalacheck" % "1.14.2").withDottyCompat(scalaVersion.value) + +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..9820c2a3559fa382bd260dfc091352ac799fdde4 GIT binary patch literal 35303 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 "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..f7847f8 --- /dev/null +++ b/project/buildSettings.sbt @@ -0,0 +1,7 @@ +// 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/quickcheck/Heap.scala b/src/main/scala/quickcheck/Heap.scala new file mode 100644 index 0000000..4475a8c --- /dev/null +++ b/src/main/scala/quickcheck/Heap.scala @@ -0,0 +1,119 @@ +package quickcheck + +trait IntHeap extends Heap { + override type A = Int + override def ord = scala.math.Ordering.Int +} + +// http://www.brics.dk/RS/96/37/BRICS-RS-96-37.pdf + +// Figure 1, page 3 +trait Heap { + type H // type of a heap + type A // type of an element + def ord: Ordering[A] // ordering on elements + + def empty: H // the empty heap + def isEmpty(h: H): Boolean // whether the given heap h is empty + + def insert(x: A, h: H): H // the heap resulting from inserting x into h + def meld(h1: H, h2: H): H // the heap resulting from merging h1 and h2 + + def findMin(h: H): A // a minimum of the heap h + def deleteMin(h: H): H // a heap resulting from deleting a minimum of h +} + +// Figure 3, page 7 +trait BinomialHeap extends Heap { + + type Rank = Int + case class Node(x: A, r: Rank, c: List[Node]) + override type H = List[Node] + + protected def root(t: Node) = t.x + protected def rank(t: Node) = t.r + protected def link(t1: Node, t2: Node): Node = // t1.r == t2.r + if ord.lteq(t1.x, t2.x) then + Node(t1.x, t1.r + 1, t2 :: t1.c) + else + Node(t2.x, t2.r + 1, t1 :: t2.c) + protected def ins(t: Node, ts: H): H = ts match + case Nil => List(t) + case tp :: ts => // t.r <= tp.r + if t.r < tp.r then + t :: tp :: ts + else + ins(link(t, tp), ts) + + override def empty = Nil + override def isEmpty(ts: H) = ts.isEmpty + + override def insert(x: A, ts: H) = ins(Node(x, 0, Nil), ts) + override def meld(ts1: H, ts2: H) = (ts1, ts2) match + case (Nil, ts) => ts + case (ts, Nil) => ts + case (t1 :: ts1, t2 :: ts2) => + if t1.r < t2.r then + t1 :: meld(ts1, t2 :: ts2) + else if t2.r < t1.r then + t2 :: meld(t1 :: ts1, ts2) + else + ins(link(t1, t2), meld(ts1, ts2)) + + override def findMin(ts: H) = ts match + case Nil => throw new NoSuchElementException("min of empty heap") + case t :: Nil => root(t) + case t :: ts => + val x = findMin(ts) + if ord.lteq(root(t), x) then + root(t) + else + x + override def deleteMin(ts: H) = ts match + case Nil => throw new NoSuchElementException("delete min of empty heap") + case t :: ts => + def getMin(t: Node, ts: H): (Node, H) = ts match + case Nil => (t, Nil) + case tp :: tsp => + val (tq, tsq) = getMin(tp, tsp) + if ord.lteq(root(t), root(tq)) then + (t, ts) + else + (tq, t :: tsq) + val (Node(_, _, c), tsq) = getMin(t, ts) + meld(c.reverse, tsq) +} + +trait Bogus1BinomialHeap extends BinomialHeap { + override def findMin(ts: H) = ts match + case Nil => throw new NoSuchElementException("min of empty heap") + case t :: ts => root(t) +} + +trait Bogus2BinomialHeap extends BinomialHeap { + override protected def link(t1: Node, t2: Node): Node = // t1.r == t2.r + if !ord.lteq(t1.x, t2.x) then + Node(t1.x, t1.r + 1, t2 :: t1.c) + else + Node(t2.x, t2.r + 1, t1 :: t2.c) +} + +trait Bogus3BinomialHeap extends BinomialHeap { + override protected def link(t1: Node, t2: Node): Node = // t1.r == t2.r + if ord.lteq(t1.x, t2.x) then + Node(t1.x, t1.r + 1, t1 :: t1.c) + else + Node(t2.x, t2.r + 1, t2 :: t2.c) +} + +trait Bogus4BinomialHeap extends BinomialHeap { + override def deleteMin(ts: H) = ts match + case Nil => throw new NoSuchElementException("delete min of empty heap") + case t :: ts => meld(t.c.reverse, ts) +} + +trait Bogus5BinomialHeap extends BinomialHeap { + override def meld(ts1: H, ts2: H) = ts1 match + case Nil => ts2 + case t1 :: ts1 => List(Node(t1.x, t1.r, ts1 ++ ts2)) +} diff --git a/src/main/scala/quickcheck/QuickCheck.scala b/src/main/scala/quickcheck/QuickCheck.scala new file mode 100644 index 0000000..59ca28b --- /dev/null +++ b/src/main/scala/quickcheck/QuickCheck.scala @@ -0,0 +1,18 @@ +package quickcheck + +import org.scalacheck._ +import Arbitrary._ +import Gen._ +import Prop.{BooleanOperators => _, _} + +abstract class QuickCheckHeap extends Properties("Heap") with IntHeap { + + lazy val genHeap: Gen[H] = ??? + implicit lazy val arbHeap: Arbitrary[H] = Arbitrary(genHeap) + + property("gen1") = forAll { (h: H) => + val m = if isEmpty(h) then 0 else findMin(h) + findMin(insert(m, h)) == m + } + +} diff --git a/src/test/scala/quickcheck/QuickCheckSuite.scala b/src/test/scala/quickcheck/QuickCheckSuite.scala new file mode 100644 index 0000000..de89e84 --- /dev/null +++ b/src/test/scala/quickcheck/QuickCheckSuite.scala @@ -0,0 +1,55 @@ +package quickcheck + +import org.scalacheck.Properties +import org.junit._ + +import org.scalacheck.Arbitrary._ +import org.scalacheck.Prop +import org.scalacheck.Prop.{BooleanOperators => _, _} +import org.scalacheck.Test.{check, Result, Failed, PropException} + +object QuickCheckBinomialHeap extends QuickCheckHeap with BinomialHeap + +class QuickCheckSuite { + def checkBogus(p: Properties): Unit = + def fail = throw new AssertionError( + s"A bogus heap should NOT satisfy all properties. Try to find the bug!") + + check(asProp(p))(identity) match + case r: Result => r.status match + case _: Failed => + () // OK: scalacheck found a counter example! + case p: PropException => + p.e match + case e: NoSuchElementException => + () // OK: the implementation throws NSEE + case _ => + fail + case _ => + fail + + /** Turns a `Properties` instance into a single `Prop` by combining all the properties */ + def asProp(properties: Properties): Prop = Prop.all(properties.properties.map(_._2).toSeq:_*) + + @Test def `Binomial heap satisfies properties. (5pts)`: Unit = + Assert.assertTrue( + check(asProp(new QuickCheckHeap with quickcheck.test.BinomialHeap))(identity).passed + ) + + @Test def `Bogus (1) binomial heap does not satisfy properties. (10pts)`: Unit = + checkBogus(new QuickCheckHeap with quickcheck.test.Bogus1BinomialHeap) + + @Test def `Bogus (2) binomial heap does not satisfy properties. (10pts)`: Unit = + checkBogus(new QuickCheckHeap with quickcheck.test.Bogus2BinomialHeap) + + @Test def `Bogus (3) binomial heap does not satisfy properties. (10pts)`: Unit = + checkBogus(new QuickCheckHeap with quickcheck.test.Bogus3BinomialHeap) + + @Test def `Bogus (4) binomial heap does not satisfy properties. (10pts)`: Unit = + checkBogus(new QuickCheckHeap with quickcheck.test.Bogus4BinomialHeap) + + @Test def `Bogus (5) binomial heap does not satisfy properties. (10pts)`: Unit = + checkBogus(new QuickCheckHeap with quickcheck.test.Bogus5BinomialHeap) + + @Rule def individualTestTimeout = new org.junit.rules.Timeout(10 * 1000) +} diff --git a/src/test/scala/quickcheck/test/Heap.scala b/src/test/scala/quickcheck/test/Heap.scala new file mode 100644 index 0000000..2f82fb3 --- /dev/null +++ b/src/test/scala/quickcheck/test/Heap.scala @@ -0,0 +1,96 @@ +package quickcheck.test + +// Figure 3, page 7 +trait BinomialHeap extends quickcheck.Heap { + + type Rank = Int + case class Node(x: A, r: Rank, c: List[Node]) + override type H = List[Node] + + protected def root(t: Node) = t.x + protected def rank(t: Node) = t.r + protected def link(t1: Node, t2: Node): Node = // t1.r == t2.r + if ord.lteq(t1.x, t2.x) then + Node(t1.x, t1.r + 1, t2 :: t1.c) + else + Node(t2.x, t2.r + 1, t1 :: t2.c) + protected def ins(t: Node, ts: H): H = ts match + case Nil => List(t) + case tp :: ts => // t.r<=tp.r + if t.r < tp.r then + t :: tp :: ts + else + ins(link(t, tp), ts) + + override def empty = Nil + override def isEmpty(ts: H) = ts.isEmpty + + override def insert(x: A, ts: H) = ins(Node(x, 0, Nil), ts) + override def meld(ts1: H, ts2: H) = (ts1, ts2) match + case (Nil, ts) => ts + case (ts, Nil) => ts + case (t1 :: ts1, t2 :: ts2) => + if t1.r < t2.r then + t1 :: meld(ts1, t2 :: ts2) + else if t2.r < t1.r then + t2 :: meld(t1 :: ts1, ts2) + else + ins(link(t1, t2), meld(ts1, ts2)) + + override def findMin(ts: H) = ts match + case Nil => throw new NoSuchElementException("min of empty heap") + case t :: Nil => root(t) + case t :: ts => + val x = findMin(ts) + if ord.lteq(root(t), x) then + root(t) + else + x + override def deleteMin(ts: H) = ts match + case Nil => throw new NoSuchElementException("delete min of empty heap") + case t :: ts => + def getMin(t: Node, ts: H): (Node, H) = ts match + case Nil => (t, Nil) + case tp :: tsp => + val (tq, tsq) = getMin(tp, tsp) + if ord.lteq(root(t), root(tq)) then + (t, ts) + else + (tq, t :: tsq) + val (Node(_, _, c), tsq) = getMin(t, ts) + meld(c.reverse, tsq) +} + +trait Bogus1BinomialHeap extends BinomialHeap { + override def findMin(ts: H) = ts match + case Nil => throw new NoSuchElementException("min of empty heap") + case t :: ts => root(t) +} + +trait Bogus2BinomialHeap extends BinomialHeap { + override protected def link(t1: Node, t2: Node): Node = // t1.r == t2.r + if !ord.lteq(t1.x, t2.x) then + Node(t1.x, t1.r + 1, t2 :: t1.c) + else + Node(t2.x, t2.r + 1, t1 :: t2.c) +} + +trait Bogus3BinomialHeap extends BinomialHeap { + override protected def link(t1: Node, t2: Node): Node = // t1.r == t2.r + if ord.lteq(t1.x, t2.x) then + Node(t1.x, t1.r + 1, t1 :: t1.c) + else + Node(t2.x, t2.r + 1, t2 :: t2.c) +} + +trait Bogus4BinomialHeap extends BinomialHeap { + override def deleteMin(ts: H) = ts match + case Nil => throw new NoSuchElementException("delete min of empty heap") + case t :: ts => meld(t.c.reverse, ts) +} + +trait Bogus5BinomialHeap extends BinomialHeap { + override def meld(ts1: H, ts2: H) = ts1 match + case Nil => ts2 + case t1 :: ts1 => List(Node(t1.x, t1.r, ts1++ts2)) +} 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)