diff --git a/previous-exams/2021-midterm-solutions/m1.md b/previous-exams/2021-midterm/m1.md similarity index 95% rename from previous-exams/2021-midterm-solutions/m1.md rename to previous-exams/2021-midterm/m1.md index 0846974688e1aee42c5b8c7b7a7460f4aeeeb176..49f825688c2c36e8dd2daee4c7b27311635377f4 100644 --- a/previous-exams/2021-midterm-solutions/m1.md +++ b/previous-exams/2021-midterm/m1.md @@ -1,9 +1,3 @@ -Use the following commands to make a fresh clone of your repository: - -``` -git clone -b m1 git@gitlab.epfl.ch:lamp/student-repositories-s21/cs206-GASPAR.git m1 -``` - ## Useful links * [A guide to the Scala parallel collections](https://docs.scala-lang.org/overviews/parallel-collections/overview.html) diff --git a/previous-exams/2021-midterm/m1/.gitignore b/previous-exams/2021-midterm/m1/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..40937dc9b192820d0ede18efd3c7e6442a083b17 --- /dev/null +++ b/previous-exams/2021-midterm/m1/.gitignore @@ -0,0 +1,22 @@ +# General +*.DS_Store +*.swp +*~ + +# Dotty +*.class +*.tasty +*.hasTasty + +# sbt +target/ + +# IDE +.bsp +.bloop +.metals +.vscode + +# datasets +stackoverflow-grading.csv +wikipedia-grading.dat diff --git a/previous-exams/2021-midterm/m1/assignment.sbt b/previous-exams/2021-midterm/m1/assignment.sbt new file mode 100644 index 0000000000000000000000000000000000000000..da7eb3c8347293a18da0025fcd6060d8f8f7cc11 --- /dev/null +++ b/previous-exams/2021-midterm/m1/assignment.sbt @@ -0,0 +1,2 @@ +// Student tasks (i.e. submit, packageSubmission) +enablePlugins(StudentTasks) diff --git a/previous-exams/2021-midterm/m1/build.sbt b/previous-exams/2021-midterm/m1/build.sbt new file mode 100644 index 0000000000000000000000000000000000000000..e4766880cca56983c63e60a86fd6e83af3750053 --- /dev/null +++ b/previous-exams/2021-midterm/m1/build.sbt @@ -0,0 +1,12 @@ +course := "midterm" +assignment := "m1" +scalaVersion := "3.0.0-RC1" +scalacOptions ++= Seq("-language:implicitConversions", "-deprecation") + +libraryDependencies += "org.scalameta" %% "munit" % "0.7.22" + +val MUnitFramework = new TestFramework("munit.Framework") +testFrameworks += MUnitFramework +// Decode Scala names +testOptions += Tests.Argument(MUnitFramework, "-s") +testSuite := "m1.M1Suite" diff --git a/previous-exams/2021-midterm/m1/grading-tests.jar b/previous-exams/2021-midterm/m1/grading-tests.jar new file mode 100644 index 0000000000000000000000000000000000000000..e01bbb513ff0587ef7115a1037909157c65ef0fb Binary files /dev/null and b/previous-exams/2021-midterm/m1/grading-tests.jar differ diff --git a/previous-exams/2021-midterm/m1/project/FilteringReporterPlugin.scala b/previous-exams/2021-midterm/m1/project/FilteringReporterPlugin.scala new file mode 100644 index 0000000000000000000000000000000000000000..2e4fd9a4d998698cd52643344b33a5e719dd7971 --- /dev/null +++ b/previous-exams/2021-midterm/m1/project/FilteringReporterPlugin.scala @@ -0,0 +1,31 @@ +package sbt // To access the private[sbt] compilerReporter key +package filteringReporterPlugin + +import Keys._ +import ch.epfl.lamp._ + +object FilteringReporterPlugin extends AutoPlugin { + override lazy val projectSettings = Seq( + // Turn off warning coming from scalameter that we cannot fix without changing scalameter + compilerReporter in (Compile, compile) ~= { reporter => new FilteringReporter(reporter) } + ) +} + +class FilteringReporter(reporter: xsbti.Reporter) extends xsbti.Reporter { + + def reset(): Unit = reporter.reset() + def hasErrors: Boolean = reporter.hasErrors + def hasWarnings: Boolean = reporter.hasWarnings + def printSummary(): Unit = reporter.printSummary() + def problems: Array[xsbti.Problem] = reporter.problems + + def log(problem: xsbti.Problem): Unit = { + if (!problem.message.contains("An existential type that came from a Scala-2 classfile cannot be")) + reporter.log(problem) + } + + def comment(pos: xsbti.Position, msg: String): Unit = + reporter.comment(pos, msg) + + override def toString = s"CollectingReporter($reporter)" +} diff --git a/previous-exams/2021-midterm/m1/project/MOOCSettings.scala b/previous-exams/2021-midterm/m1/project/MOOCSettings.scala new file mode 100644 index 0000000000000000000000000000000000000000..1c40443a53085d23fadb134f4e1a505c32231f1d --- /dev/null +++ b/previous-exams/2021-midterm/m1/project/MOOCSettings.scala @@ -0,0 +1,49 @@ +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 { + + override def requires = super.requires && filteringReporterPlugin.FilteringReporterPlugin + + 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") + .withRank(KeyRanks.Invisible) + // 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/previous-exams/2021-midterm/m1/project/StudentTasks.scala b/previous-exams/2021-midterm/m1/project/StudentTasks.scala new file mode 100644 index 0000000000000000000000000000000000000000..c4669afe82dd2b45651f94dcad9e736f29d21432 --- /dev/null +++ b/previous-exams/2021-midterm/m1/project/StudentTasks.scala @@ -0,0 +1,303 @@ +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") + + lazy val Grading = config("grading") extend(Runtime) + } + + + import autoImport._ + import MOOCSettings.autoImport._ + + override lazy val projectSettings = Seq( + packageSubmissionSetting, + fork := true, + connectInput in run := true, + outputStrategy := Some(StdoutOutput), + ) ++ + packageSubmissionZipSettings ++ + inConfig(Grading)(Defaults.testSettings ++ Seq( + unmanagedJars += file("grading-tests.jar"), + + definedTests := (definedTests in Test).value, + internalDependencyClasspath := (internalDependencyClasspath in Test).value + )) + + + /** ********************************************************** + * 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, None) + 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("<arg>").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 <email-address> <submit-token> + | + |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/previous-exams/2021-midterm/m1/project/build.properties b/previous-exams/2021-midterm/m1/project/build.properties new file mode 100644 index 0000000000000000000000000000000000000000..0b2e09c5ac99bd3de91b2b139b94301c2b6e26f3 --- /dev/null +++ b/previous-exams/2021-midterm/m1/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.4.7 diff --git a/previous-exams/2021-midterm/m1/project/buildSettings.sbt b/previous-exams/2021-midterm/m1/project/buildSettings.sbt new file mode 100644 index 0000000000000000000000000000000000000000..8fac702aaf3f3c4ede79691c7b4e4a52f26f3f47 --- /dev/null +++ b/previous-exams/2021-midterm/m1/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/previous-exams/2021-midterm/m1/project/plugins.sbt b/previous-exams/2021-midterm/m1/project/plugins.sbt new file mode 100644 index 0000000000000000000000000000000000000000..fb7dbe068109e7f35c13b2762b865c7eec1979f3 --- /dev/null +++ b/previous-exams/2021-midterm/m1/project/plugins.sbt @@ -0,0 +1,3 @@ +// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3") diff --git a/previous-exams/2021-midterm/m1/src/main/scala/m1/Lib.scala b/previous-exams/2021-midterm/m1/src/main/scala/m1/Lib.scala new file mode 100644 index 0000000000000000000000000000000000000000..37ce78015dfcc3dd679e238ab5bad98903b7e03c --- /dev/null +++ b/previous-exams/2021-midterm/m1/src/main/scala/m1/Lib.scala @@ -0,0 +1,60 @@ +package m1 + +//////////////////////////////////////// +// NO NEED TO MODIFY THIS SOURCE FILE // +//////////////////////////////////////// + +trait Lib { + + /** If an array has `n` elements and `n < THRESHOLD`, then it should be processed sequentially */ + final val THRESHOLD: Int = 33 + + /** Compute the two values in parallel + * + * Note: Most tests just compute those two sequentially to make any bug simpler to debug + */ + def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) + + /** A limited array. It only contains the required operations for this exercise. */ + trait Arr[T] { + /** Get the i-th element of the array (0-based) */ + def apply(i: Int): T + /** Update the i-th element of the array with the given value (0-based) */ + def update(i: Int, x: T): Unit + /** Number of elements in this array */ + def length: Int + /** Create a copy of this array without the first element */ + def tail: Arr[T] + /** Create a copy of this array by mapping all the elements with the given function */ + def map[U](f: T => U): Arr[U] + } + + object Arr { + /** Create an array with the given elements */ + def apply[T](xs: T*): Arr[T] = { + val arr: Arr[T] = Arr.ofLength(xs.length) + for i <- 0 until xs.length do arr(i) = xs(i) + arr + } + + /** Create an array with the given length. All elements are initialized to `null`. */ + def ofLength[T](n: Int): Arr[T] = + newArrOfLength(n) + + } + + /** Create an array with the given length. All elements are initialized to `null`. */ + def newArrOfLength[T](n: Int): Arr[T] + + /** A fractional number representing `numerator/denominator` */ + case class Frac(numerator: Int, denominator: Int) { + def toDouble: Double = numerator.toDouble / denominator + } + + /** Tree result of an upsweep operation. Specialized for `Frac` results. */ + trait TreeRes { val res: Frac } + /** Leaf result of an upsweep operation. Specialized for `Frac` results. */ + case class Leaf(from: Int, to: Int, res: Frac) extends TreeRes + /** Tree node result of an upsweep operation. Specialized for `Frac` results. */ + case class Node(left: TreeRes, res: Frac, right: TreeRes) extends TreeRes +} diff --git a/previous-exams/2021-midterm/m1/src/main/scala/m1/M1.scala b/previous-exams/2021-midterm/m1/src/main/scala/m1/M1.scala new file mode 100644 index 0000000000000000000000000000000000000000..8eaea55ff391cd121b054a8fe3fc9a22e3e4e089 --- /dev/null +++ b/previous-exams/2021-midterm/m1/src/main/scala/m1/M1.scala @@ -0,0 +1,90 @@ +package m1 + + +trait M1 extends Lib { + // Functions and classes of Lib can be used in here + + /** Compute the rolling average of array. + * + * For an array `arr = Arr(x1, x2, x3, ..., xn)` the result is + * `Arr(x1 / 1, (x1 + x2) / 2, (x1 + x2 + x3) / 3, ..., (x1 + x2 + x3 + ... + xn) / n)` + */ + def rollingAveragesParallel(arr: Arr[Int]): Arr[Double] = { + if (arr.length == 0) return Arr.ofLength(0) + // TASK 1: Add missing parallelization in `upsweep` and `downsweep`. + // You should use the `parallel` method. + // You should use the sequential version if the number of elements is lower than THRESHOLD. + // TASK 2a: Pass `arr` to `upsweep` and `downsweep` instead of `tmp`. + // You will need to change some signatures and update the code appropriately. + // Remove the definition of `tmp` + // TASK 2b: Change the type of the array `out` from `Frac` to `Double` + // You will need to change some signatures and update the code appropriately. + // Remove the call `.map(frac => frac.toDouble)`. + // TASK 3: Remove the call to `.tail`. + // Update the update the code appropriately. + + val tmp: Arr[Frac] = arr.map(x => Frac(x, 1)) + val out: Arr[Frac] = Arr.ofLength(arr.length + 1) + val tree = upsweep(tmp, 0, arr.length) + downsweep(tmp, Frac(0, 0), tree, out) + out(0) = Frac(0, 0) + out.map(frac => frac.toDouble).tail + + // IDEAL SOLUTION + // val out = Arr.ofLength(arr.length) + // val tree = upsweep(arr, 0, arr.length) + // downsweep(arr, Frac(0, 0), tree, out) + // out + } + + def scanOp(acc: Frac, x: Frac) = // No need to modify this method + Frac(acc.numerator + x.numerator, acc.denominator + x.denominator) + + def upsweep(input: Arr[Frac], from: Int, to: Int): TreeRes = { + if (to - from < 2) + Leaf(from, to, reduceSequential(input, from + 1, to, input(from))) + else { + val mid = from + (to - from) / 2 + val (tL, tR) = ( + upsweep(input, from, mid), + upsweep(input, mid, to) + ) + Node(tL, scanOp(tL.res, tR.res), tR) + } + } + + def downsweep(input: Arr[Frac], a0: Frac, tree: TreeRes, output: Arr[Frac]): Unit = { + tree match { + case Node(left, _, right) => + ( + downsweep(input, a0, left, output), + downsweep(input, scanOp(a0, left.res), right, output) + ) + case Leaf(from, to, _) => + downsweepSequential(input, from, to, a0, output) + } + } + + def downsweepSequential(input: Arr[Frac], from: Int, to: Int, a0: Frac, output: Arr[Frac]): Unit = { + if (from < to) { + var i = from + var a = a0 + while (i < to) { + a = scanOp(a, input(i)) + i = i + 1 + output(i) = a + } + } + } + + def reduceSequential(input: Arr[Frac], from: Int, to: Int, a0: Frac): Frac = { + var a = a0 + var i = from + while (i < to) { + a = scanOp(a, input(i)) + i = i + 1 + } + a + } + +} diff --git a/previous-exams/2021-midterm/m1/src/test/scala/m1/M1Suite.scala b/previous-exams/2021-midterm/m1/src/test/scala/m1/M1Suite.scala new file mode 100644 index 0000000000000000000000000000000000000000..ab1b8652a90c04eddcf5767bc413126be1152f6e --- /dev/null +++ b/previous-exams/2021-midterm/m1/src/test/scala/m1/M1Suite.scala @@ -0,0 +1,156 @@ +package m1 + +class M1Suite extends munit.FunSuite { + + test("Rolling average result test (5pts)") { + RollingAveragesBasicLogicTest.basicTests() + RollingAveragesBasicLogicTest.normalTests() + RollingAveragesBasicLogicTest.largeTests() + } + + test("[TASK 1] Rolling average parallelism test (30pts)") { + RollingAveragesCallsToParallel.parallelismTest() + RollingAveragesParallel.basicTests() + RollingAveragesParallel.normalTests() + RollingAveragesParallel.largeTests() + } + + test("[TASK 2] Rolling average no `map` test (35pts)") { + RollingAveragesNoMap.basicTests() + RollingAveragesNoMap.normalTests() + RollingAveragesNoMap.largeTests() + } + + test("[TASK 3] Rolling average no `tail` test (30pts)") { + RollingAveragesNoTail.basicTests() + RollingAveragesNoTail.normalTests() + RollingAveragesNoTail.largeTests() + } + + + object RollingAveragesBasicLogicTest extends M1 with LibImpl with RollingAveragesTest { + def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = (op1, op2) + def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl(arr) + } + + object RollingAveragesCallsToParallel extends M1 with LibImpl with RollingAveragesTest { + private var count = 0 + def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = + count += 1 + (op1, op2) + + def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl(arr) + + def parallelismTest() = { + assertParallelCount(Arr(), 0) + assertParallelCount(Arr(1), 0) + assertParallelCount(Arr(1, 2, 3, 4), 0) + assertParallelCount(Arr(Array.tabulate(16)(identity): _*), 0) + assertParallelCount(Arr(Array.tabulate(32)(identity): _*), 0) + + assertParallelCount(Arr(Array.tabulate(33)(identity): _*), 2) + assertParallelCount(Arr(Array.tabulate(64)(identity): _*), 2) + assertParallelCount(Arr(Array.tabulate(128)(identity): _*), 6) + assertParallelCount(Arr(Array.tabulate(256)(identity): _*), 14) + assertParallelCount(Arr(Array.tabulate(1000)(identity): _*), 62) + assertParallelCount(Arr(Array.tabulate(1024)(identity): _*), 62) + } + + def assertParallelCount(arr: Arr[Int], expected: Int): Unit = { + try { + count = 0 + rollingAveragesParallel(arr) + assert(count == expected, { + val extra = if (expected == 0) "" else s" ${expected/2} for the `upsweep` and ${expected/2} for the `downsweep`" + s"\n$arr\n\nERROR: Expected $expected instead of $count calls to `parallel(...)` for an array of ${arr.length} elements. Current parallel threshold is $THRESHOLD.$extra" + }) + } finally { + count = 0 + } + } + + } + + object RollingAveragesNoMap extends M1 with LibImpl with RollingAveragesTest { + def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = (op1, op2) + def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl[T](arr) { + override def map[U](f: T => U): Arr[U] = throw Exception("Should not call Arr.map") + } + } + + object RollingAveragesNoTail extends M1 with LibImpl with RollingAveragesTest { + def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = (op1, op2) + def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl[T](arr) { + override def tail: Arr[T] = throw Exception("Should not call Arr.tail") + } + } + + object RollingAveragesParallel extends M1 with LibImpl with RollingAveragesTest { + import scala.concurrent.duration._ + val TIMEOUT = Duration(10, SECONDS) + def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = { + import concurrent.ExecutionContext.Implicits.global + import scala.concurrent._ + Await.result(Future(op1).zip(Future(op2)), TIMEOUT) // FIXME not timing-out + } + def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl(arr) + } + + trait LibImpl extends Lib { + + def newArrFrom[T](arr: Array[AnyRef]): Arr[T] + + def newArrOfLength[T](n: Int): Arr[T] = + newArrFrom(new Array(n)) + + class ArrImpl[T](val arr: Array[AnyRef]) extends Arr[T]: + def apply(i: Int): T = + arr(i).asInstanceOf[T] + def update(i: Int, x: T): Unit = + arr(i) = x.asInstanceOf[AnyRef] + def length: Int = + arr.length + def map[U](f: T => U): Arr[U] = + newArrFrom(arr.map(f.asInstanceOf[AnyRef => AnyRef])) + def tail: Arr[T] = + newArrFrom(arr.tail) + override def toString: String = + arr.mkString("Arr(", ", ", ")") + override def equals(that: Any): Boolean = + that match + case that: ArrImpl[_] => Array.equals(arr, that.arr) + case _ => false + } + + trait RollingAveragesTest extends M1 { + + def tabulate[T](n: Int)(f: Int => T): Arr[T] = + val arr = Arr.ofLength[T](n) + for i <- 0 until n do + arr(i) = f(i) + arr + + def basicTests() = { + assertEquals(rollingAveragesParallel(Arr()), Arr[Double]()) + assertEquals(rollingAveragesParallel(Arr(1)), Arr[Double](1)) + assertEquals(rollingAveragesParallel(Arr(1, 2, 3, 4)), Arr(1, 1.5, 2, 2.5)) + assertEquals(rollingAveragesParallel(Arr(4, 4, 4, 4)), Arr[Double](4, 4, 4, 4)) + } + + def normalTests() = { + assertEquals(rollingAveragesParallel(Arr(Array.tabulate(64)(identity): _*)), Arr(Array.tabulate(64)(_.toDouble / 2): _*)) + assertEquals(rollingAveragesParallel(Arr(4, 4, 4, 4)), Arr[Double](4, 4, 4, 4)) + assertEquals(rollingAveragesParallel(Arr(4, 8, 6, 4)), Arr[Double](4, 6, 6, 5.5)) + assertEquals(rollingAveragesParallel(Arr(4, 3, 2, 1)), Arr(4, 3.5, 3, 2.5)) + assertEquals(rollingAveragesParallel(Arr(Array.tabulate(64)(identity).reverse: _*)), Arr(Array.tabulate(64)(i => 63 - i.toDouble / 2): _*)) + assertEquals(rollingAveragesParallel(Arr(Array.tabulate(128)(i => 128 - 2*i).reverse: _*)), Arr(Array.tabulate(128)(i => -126d + i): _*)) + } + + def largeTests() = { + assertEquals(rollingAveragesParallel(Arr(Array.tabulate(500)(identity): _*)), Arr(Array.tabulate(500)(_.toDouble / 2): _*)) + assertEquals(rollingAveragesParallel(Arr(Array.tabulate(512)(identity): _*)), Arr(Array.tabulate(512)(_.toDouble / 2): _*)) + assertEquals(rollingAveragesParallel(Arr(Array.tabulate(1_000)(identity): _*)), Arr(Array.tabulate(1_000)(_.toDouble / 2): _*)) + assertEquals(rollingAveragesParallel(Arr(Array.tabulate(10_000)(identity): _*)), Arr(Array.tabulate(10_000)(_.toDouble / 2): _*)) + } + } +} \ No newline at end of file diff --git a/previous-exams/2021-midterm-solutions/m14.md b/previous-exams/2021-midterm/m14.md similarity index 95% rename from previous-exams/2021-midterm-solutions/m14.md rename to previous-exams/2021-midterm/m14.md index 62f775d8a176774aef56064544cb4935ff71e5ee..7b0deeb4e9c7712f21d55b7a861a81927c7afa0e 100644 --- a/previous-exams/2021-midterm-solutions/m14.md +++ b/previous-exams/2021-midterm/m14.md @@ -1,9 +1,3 @@ -Use the following commands to make a fresh clone of your repository: - -``` -git clone -b m14 git@gitlab.epfl.ch:lamp/student-repositories-s21/cs206-GASPAR.git m14 -``` - ## Useful links * [A guide to the Scala parallel collections](https://docs.scala-lang.org/overviews/parallel-collections/overview.html) diff --git a/previous-exams/2021-midterm/m14/.gitignore b/previous-exams/2021-midterm/m14/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..40937dc9b192820d0ede18efd3c7e6442a083b17 --- /dev/null +++ b/previous-exams/2021-midterm/m14/.gitignore @@ -0,0 +1,22 @@ +# General +*.DS_Store +*.swp +*~ + +# Dotty +*.class +*.tasty +*.hasTasty + +# sbt +target/ + +# IDE +.bsp +.bloop +.metals +.vscode + +# datasets +stackoverflow-grading.csv +wikipedia-grading.dat diff --git a/previous-exams/2021-midterm/m14/assignment.sbt b/previous-exams/2021-midterm/m14/assignment.sbt new file mode 100644 index 0000000000000000000000000000000000000000..da7eb3c8347293a18da0025fcd6060d8f8f7cc11 --- /dev/null +++ b/previous-exams/2021-midterm/m14/assignment.sbt @@ -0,0 +1,2 @@ +// Student tasks (i.e. submit, packageSubmission) +enablePlugins(StudentTasks) diff --git a/previous-exams/2021-midterm/m14/build.sbt b/previous-exams/2021-midterm/m14/build.sbt new file mode 100644 index 0000000000000000000000000000000000000000..aeee575ac20b4770fe264f7327098c1da3387794 --- /dev/null +++ b/previous-exams/2021-midterm/m14/build.sbt @@ -0,0 +1,12 @@ +course := "midterm" +assignment := "m14" +scalaVersion := "3.0.0-RC1" +scalacOptions ++= Seq("-language:implicitConversions", "-deprecation") + +libraryDependencies += "org.scalameta" %% "munit" % "0.7.22" + +val MUnitFramework = new TestFramework("munit.Framework") +testFrameworks += MUnitFramework +// Decode Scala names +testOptions += Tests.Argument(MUnitFramework, "-s") +testSuite := "m14.M14Suite" diff --git a/previous-exams/2021-midterm/m14/grading-tests.jar b/previous-exams/2021-midterm/m14/grading-tests.jar new file mode 100644 index 0000000000000000000000000000000000000000..9c8d6fdaac52512ffcac6882f5650c7673caa5f4 Binary files /dev/null and b/previous-exams/2021-midterm/m14/grading-tests.jar differ diff --git a/previous-exams/2021-midterm/m14/project/FilteringReporterPlugin.scala b/previous-exams/2021-midterm/m14/project/FilteringReporterPlugin.scala new file mode 100644 index 0000000000000000000000000000000000000000..2e4fd9a4d998698cd52643344b33a5e719dd7971 --- /dev/null +++ b/previous-exams/2021-midterm/m14/project/FilteringReporterPlugin.scala @@ -0,0 +1,31 @@ +package sbt // To access the private[sbt] compilerReporter key +package filteringReporterPlugin + +import Keys._ +import ch.epfl.lamp._ + +object FilteringReporterPlugin extends AutoPlugin { + override lazy val projectSettings = Seq( + // Turn off warning coming from scalameter that we cannot fix without changing scalameter + compilerReporter in (Compile, compile) ~= { reporter => new FilteringReporter(reporter) } + ) +} + +class FilteringReporter(reporter: xsbti.Reporter) extends xsbti.Reporter { + + def reset(): Unit = reporter.reset() + def hasErrors: Boolean = reporter.hasErrors + def hasWarnings: Boolean = reporter.hasWarnings + def printSummary(): Unit = reporter.printSummary() + def problems: Array[xsbti.Problem] = reporter.problems + + def log(problem: xsbti.Problem): Unit = { + if (!problem.message.contains("An existential type that came from a Scala-2 classfile cannot be")) + reporter.log(problem) + } + + def comment(pos: xsbti.Position, msg: String): Unit = + reporter.comment(pos, msg) + + override def toString = s"CollectingReporter($reporter)" +} diff --git a/previous-exams/2021-midterm/m14/project/MOOCSettings.scala b/previous-exams/2021-midterm/m14/project/MOOCSettings.scala new file mode 100644 index 0000000000000000000000000000000000000000..1c40443a53085d23fadb134f4e1a505c32231f1d --- /dev/null +++ b/previous-exams/2021-midterm/m14/project/MOOCSettings.scala @@ -0,0 +1,49 @@ +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 { + + override def requires = super.requires && filteringReporterPlugin.FilteringReporterPlugin + + 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") + .withRank(KeyRanks.Invisible) + // 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/previous-exams/2021-midterm/m14/project/StudentTasks.scala b/previous-exams/2021-midterm/m14/project/StudentTasks.scala new file mode 100644 index 0000000000000000000000000000000000000000..c4669afe82dd2b45651f94dcad9e736f29d21432 --- /dev/null +++ b/previous-exams/2021-midterm/m14/project/StudentTasks.scala @@ -0,0 +1,303 @@ +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") + + lazy val Grading = config("grading") extend(Runtime) + } + + + import autoImport._ + import MOOCSettings.autoImport._ + + override lazy val projectSettings = Seq( + packageSubmissionSetting, + fork := true, + connectInput in run := true, + outputStrategy := Some(StdoutOutput), + ) ++ + packageSubmissionZipSettings ++ + inConfig(Grading)(Defaults.testSettings ++ Seq( + unmanagedJars += file("grading-tests.jar"), + + definedTests := (definedTests in Test).value, + internalDependencyClasspath := (internalDependencyClasspath in Test).value + )) + + + /** ********************************************************** + * 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, None) + 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("<arg>").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 <email-address> <submit-token> + | + |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/previous-exams/2021-midterm/m14/project/build.properties b/previous-exams/2021-midterm/m14/project/build.properties new file mode 100644 index 0000000000000000000000000000000000000000..0b2e09c5ac99bd3de91b2b139b94301c2b6e26f3 --- /dev/null +++ b/previous-exams/2021-midterm/m14/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.4.7 diff --git a/previous-exams/2021-midterm/m14/project/buildSettings.sbt b/previous-exams/2021-midterm/m14/project/buildSettings.sbt new file mode 100644 index 0000000000000000000000000000000000000000..8fac702aaf3f3c4ede79691c7b4e4a52f26f3f47 --- /dev/null +++ b/previous-exams/2021-midterm/m14/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/previous-exams/2021-midterm/m14/project/plugins.sbt b/previous-exams/2021-midterm/m14/project/plugins.sbt new file mode 100644 index 0000000000000000000000000000000000000000..fb7dbe068109e7f35c13b2762b865c7eec1979f3 --- /dev/null +++ b/previous-exams/2021-midterm/m14/project/plugins.sbt @@ -0,0 +1,3 @@ +// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3") diff --git a/previous-exams/2021-midterm/m14/src/main/scala/m14/AbstractBlockingQueue.scala b/previous-exams/2021-midterm/m14/src/main/scala/m14/AbstractBlockingQueue.scala new file mode 100644 index 0000000000000000000000000000000000000000..a91a39c7ce364151f5c2aa9968de43aecc1ef984 --- /dev/null +++ b/previous-exams/2021-midterm/m14/src/main/scala/m14/AbstractBlockingQueue.scala @@ -0,0 +1,14 @@ +package m14 + +abstract class AbstractBlockingQueue[T] extends Monitor { + private var underlying: List[T] = Nil + + def getUnderlying(): List[T] = + underlying + + def setUnderlying(newValue: List[T]): Unit = + underlying = newValue + + def put(elem: T): Unit + def take(): T +} diff --git a/previous-exams/2021-midterm/m14/src/main/scala/m14/AbstractThreadPoolExecutor.scala b/previous-exams/2021-midterm/m14/src/main/scala/m14/AbstractThreadPoolExecutor.scala new file mode 100644 index 0000000000000000000000000000000000000000..670294c04eaa9376c25984061168a2c0ad275669 --- /dev/null +++ b/previous-exams/2021-midterm/m14/src/main/scala/m14/AbstractThreadPoolExecutor.scala @@ -0,0 +1,7 @@ +package m14 + +abstract class AbstractThreadPoolExecutor { + def execute(task: Unit => Unit): Unit + def start(): Unit + def shutdown(): Unit +} diff --git a/previous-exams/2021-midterm/m14/src/main/scala/m14/M14.scala b/previous-exams/2021-midterm/m14/src/main/scala/m14/M14.scala new file mode 100644 index 0000000000000000000000000000000000000000..18229ebdeed559f400045c0b20595394f0ac88f3 --- /dev/null +++ b/previous-exams/2021-midterm/m14/src/main/scala/m14/M14.scala @@ -0,0 +1,62 @@ +package m14 + +object M14 { + /** A thread pool that executes submitted task using one of several threads */ + class ThreadPoolExecutor(taskQueue: BlockingQueue[Unit => Unit], poolSize: Int) + extends AbstractThreadPoolExecutor { + + private class Worker extends Thread { + override def run(): Unit = { + try { + while (true) { + ??? + } + } catch { + case e: InterruptedException => + // Nothing to do here, we are shutting down gracefully. + } + } + } + private val workers: List[Worker] = List.fill(poolSize)(new Worker()) + + /** Executes the given task, passed by name. */ + def execute(task: Unit => Unit): Unit = + ??? + + /** Starts the thread pool. */ + def start(): Unit = + workers.foreach(_.start()) + + /** Instantly shuts down all actively executing tasks using an interrupt. */ + def shutdown(): Unit = + workers.foreach(_.interrupt()) + } + + /** + * A queue whose take operations blocks until the queue become non-empty. + * Elements must be retrived from this queue in a last in, first out order. + * All methods of this class are thread safe, that is, they can safely + * be used from multiple thread without any particular synchronization. + */ + class BlockingQueue[T] extends AbstractBlockingQueue[T] { + + // The state of this queue is stored in an underlying List[T] defined in + // the AbstractBlockingQueue class. Your implementation should access and + // update this list using the following setter and getter methods: + // - def getUnderlying(): List[T] + // - def setUnderlying(newValue: List[T]): Unit + // Using these methods is required for testing purposes. + + /** Inserts the specified element into this queue (non-blocking) */ + def put(elem: T): Unit = + ??? + + /** + * Retrieves and removes the head of this queue, waiting if necessary + * until an element becomes available (blocking). + * This queue operates in a last in, first out order. + */ + def take(): T = + ??? + } +} diff --git a/previous-exams/2021-midterm/m14/src/main/scala/m14/Monitor.scala b/previous-exams/2021-midterm/m14/src/main/scala/m14/Monitor.scala new file mode 100644 index 0000000000000000000000000000000000000000..97dd73a6038ef6966a899da8c6b1c7dc9c9109de --- /dev/null +++ b/previous-exams/2021-midterm/m14/src/main/scala/m14/Monitor.scala @@ -0,0 +1,23 @@ +package m14 + +class Dummy + +trait Monitor { + implicit val dummy: Dummy = new Dummy + + def wait()(implicit i: Dummy) = waitDefault() + + def synchronized[T](e: => T)(implicit i: Dummy) = synchronizedDefault(e) + + def notify()(implicit i: Dummy) = notifyDefault() + + def notifyAll()(implicit i: Dummy) = notifyAllDefault() + + private val lock = new AnyRef + + // Can be overriden. + def waitDefault(): Unit = lock.wait() + def synchronizedDefault[T](toExecute: => T): T = lock.synchronized(toExecute) + def notifyDefault(): Unit = lock.notify() + def notifyAllDefault(): Unit = lock.notifyAll() +} diff --git a/previous-exams/2021-midterm/m14/src/test/scala/m14/M14Suite.scala b/previous-exams/2021-midterm/m14/src/test/scala/m14/M14Suite.scala new file mode 100644 index 0000000000000000000000000000000000000000..cd04f2a32971eedc0e12400ae35f88d7c8f45571 --- /dev/null +++ b/previous-exams/2021-midterm/m14/src/test/scala/m14/M14Suite.scala @@ -0,0 +1,159 @@ +package m14 + +import instrumentation.SchedulableBlockingQueue +import instrumentation.TestHelper._ +import instrumentation.TestUtils._ + +class M14Suite extends munit.FunSuite { + import M14._ + + test("ThreadPool should put jobs in the queue, Workers should execute jobs from the queue (10pts)") { + case class PutE(e: Unit => Unit) extends Exception + val nThreads = 3 + var taken = false + class TestBlockingQueue extends BlockingQueue[Unit => Unit] { + override def put(e: Unit => Unit): Unit = + throw new PutE(e) + + override def take(): Unit => Unit = + x => { + taken = true + Thread.sleep(10 * 1000) + } + } + + val tpe = new ThreadPoolExecutor(new TestBlockingQueue, nThreads) + val unit2unit: Unit => Unit = x => () + try { + tpe.execute(unit2unit) + assert(false, "ThreadPoolExecutor does not put jobs in the queue") + } catch { + case PutE(e) => + assert(e == unit2unit) + } + tpe.start() + Thread.sleep(1000) + assert(taken, s"ThreadPoolExecutor workers do no execute jobs from the queue") + tpe.shutdown() + } + + test("BlockingQueue should work in a sequential setting (1pts)") { + testSequential[(Int, Int, Int, Int)]{ sched => + val queue = new SchedulableBlockingQueue[Int](sched) + queue.put(1) + queue.put(2) + queue.put(3) + queue.put(4) + (queue.take(), + queue.take(), + queue.take(), + queue.take()) + }{ tuple => + (tuple == (4, 3, 2, 1), s"Expected (4, 3, 2, 1) got $tuple") + } + } + + test("BlockingQueue should work when Thread 1: 'put(1)', Thread 2: 'take' (3pts)") { + testManySchedules(2, sched => { + val queue = new SchedulableBlockingQueue[Int](sched) + (List(() => queue.put(1), () => queue.take()), + args => (args(1) == 1, s"Expected 1, got ${args(1)}")) + }) + } + + test("BlockingQueue should not be able to take from an empty queue (3pts)") { + testSequential[Boolean]{ sched => + val queue = new SchedulableBlockingQueue[Int](sched); + queue.put(1) + queue.put(2) + queue.take() + queue.take() + failsOrTimesOut(queue.take()) + }{ res => + (res, "Was able to retrieve an element from an empty queue") + } + } + + test("BlockingQueue should work when Thread 1: 'put(1)', Thread 2: 'put(2)', Thread 3: 'take' (5pts)") { + testManySchedules(3, sched => { + val queue = new SchedulableBlockingQueue[Int](sched) + (List(() => queue.put(1), () => queue.put(2), () => queue.take()) + , args => { + val takeRes = args(2).asInstanceOf[Int] + val nocreation = (takeRes == 1 || takeRes == 2) + if (!nocreation) + (false, s"'take' should return either 1 or 2") + else (true, "") + }) + }) + } + + test("BlockingQueue should work when Thread 1: 'put(1)', Thread 2: 'put(2)', Thread 3: 'take', Thread 4: 'take' (10pts)") { + testManySchedules(4, sched => { + val queue = new SchedulableBlockingQueue[Int](sched) + (List(() => queue.put(1), () => queue.put(2), () => queue.take(), () => queue.take()) + , args => { + def m(): (Boolean, String) = { + val takeRes1 = args(2).asInstanceOf[Int] + val takeRes2 = args(3).asInstanceOf[Int] + val nocreation = (x: Int) => List(1, 2).contains(x) + if (!nocreation(takeRes1)) + return (false, s"'Thread 3: take' returned $takeRes1 but should return a value in {1, 2, 3}") + if (!nocreation(takeRes2)) + return (false, s"'Thread 4: take' returned $takeRes2 but should return a value in {1, 2, 3}") + + val noduplication = takeRes1 != takeRes2 + if (!noduplication) + (false, s"'Thread 3 and 4' returned the same value: $takeRes1") + else (true, "") + } + m() + }) + }) + } + + test("BlockingQueue should work when Thread 1: 'put(1)', Thread 2: 'put(2)', Thread 3: 'put(3)', Thread 4: 'take', Thread 5: 'take' (10pts)") { + testManySchedules(5, sched => { + val queue = new SchedulableBlockingQueue[Int](sched) + (List(() => queue.put(1), () => queue.put(2), () => queue.put(3), + () => queue.take(), () => queue.take()) + , args => { + def m(): (Boolean, String) = { + val takeRes1 = args(3).asInstanceOf[Int] + val takeRes2 = args(4).asInstanceOf[Int] + val nocreation = (x: Int) => List(1, 2, 3).contains(x) + if (!nocreation(takeRes1)) + return (false, s"'Thread 4: take' returned $takeRes1 but should return a value in {1, 2, 3}") + if (!nocreation(takeRes2)) + return (false, s"'Thread 5: take' returned $takeRes2 but should return a value in {1, 2, 3}") + + val noduplication = takeRes1 != takeRes2 + if (!noduplication) + return (false, s"'Thread 4 and 5' returned the same value: $takeRes1") + else (true, "") + } + m() + }) + }) + } + + test("BlockingQueue should work when Thread 1: 'put(1); put(2); take', Thread 2: 'put(3)', Thread 3: 'put(4)' (10pts)") { + testManySchedules(3, sched => { + val queue = new SchedulableBlockingQueue[Int](sched) + (List( + () => { queue.put(1); queue.put(2); queue.take() }, + () => queue.put(3), + () => queue.put(4) + ), args => { + val takeRes = args(0).asInstanceOf[Int] + val nocreation = List(1, 2, 3, 4).contains + if (!nocreation(takeRes)) + (false, s"'Thread 1: take' returned $takeRes, but should return a value in {1, 2, 3, 4}") + else if (takeRes == 1) + (false, s"'Thread 1' returned 2 before returning 1 (got $takeRes)") + else + (true, "") + }) + }) + } +} diff --git a/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/MockedMonitor.scala b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/MockedMonitor.scala new file mode 100644 index 0000000000000000000000000000000000000000..64aa205a5e1487a2dc0d3f6dc9f4435454a5b648 --- /dev/null +++ b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/MockedMonitor.scala @@ -0,0 +1,73 @@ +package m14 +package instrumentation + +trait MockedMonitor extends Monitor { + def scheduler: Scheduler + + // Can be overriden. + override def waitDefault() = { + scheduler.log("wait") + scheduler updateThreadState Wait(this, scheduler.threadLocks.tail) + } + override def synchronizedDefault[T](toExecute: =>T): T = { + scheduler.log("synchronized check") + val prevLocks = scheduler.threadLocks + scheduler updateThreadState Sync(this, prevLocks) // If this belongs to prevLocks, should just continue. + scheduler.log("synchronized -> enter") + try { + toExecute + } finally { + scheduler updateThreadState Running(prevLocks) + scheduler.log("synchronized -> out") + } + } + override def notifyDefault() = { + scheduler mapOtherStates { + state => state match { + case Wait(lockToAquire, locks) if lockToAquire == this => SyncUnique(this, state.locks) + case e => e + } + } + scheduler.log("notify") + } + override def notifyAllDefault() = { + scheduler mapOtherStates { + state => state match { + case Wait(lockToAquire, locks) if lockToAquire == this => Sync(this, state.locks) + case SyncUnique(lockToAquire, locks) if lockToAquire == this => Sync(this, state.locks) + case e => e + } + } + scheduler.log("notifyAll") + } +} + +trait LockFreeMonitor extends Monitor { + override def waitDefault() = { + throw new Exception("Please use lock-free structures and do not use wait()") + } + override def synchronizedDefault[T](toExecute: =>T): T = { + throw new Exception("Please use lock-free structures and do not use synchronized()") + } + override def notifyDefault() = { + throw new Exception("Please use lock-free structures and do not use notify()") + } + override def notifyAllDefault() = { + throw new Exception("Please use lock-free structures and do not use notifyAll()") + } +} + + +abstract class ThreadState { + def locks: Seq[AnyRef] +} +trait CanContinueIfAcquiresLock extends ThreadState { + def lockToAquire: AnyRef +} +case object Start extends ThreadState { def locks: Seq[AnyRef] = Seq.empty } +case object End extends ThreadState { def locks: Seq[AnyRef] = Seq.empty } +case class Wait(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState +case class SyncUnique(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState with CanContinueIfAcquiresLock +case class Sync(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState with CanContinueIfAcquiresLock +case class Running(locks: Seq[AnyRef]) extends ThreadState +case class VariableReadWrite(locks: Seq[AnyRef]) extends ThreadState diff --git a/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/SchedulableBlockingQueue.scala b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/SchedulableBlockingQueue.scala new file mode 100644 index 0000000000000000000000000000000000000000..16e68fa913f770e7b85cee2af2bdec390cb7ee4e --- /dev/null +++ b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/SchedulableBlockingQueue.scala @@ -0,0 +1,17 @@ +package m14 +package instrumentation + +class SchedulableBlockingQueue[T](val scheduler: Scheduler) + extends m14.M14.BlockingQueue[T] with MockedMonitor { + private var underlying: List[T] = Nil + + override def getUnderlying(): List[T] = + scheduler.exec { + underlying + }(s"Get $underlying") + + override def setUnderlying(newValue: List[T]): Unit = + scheduler.exec { + underlying = newValue + }(s"Set $newValue") +} diff --git a/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/Scheduler.scala b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/Scheduler.scala new file mode 100644 index 0000000000000000000000000000000000000000..448a8091eed701a6258b53110aef2ae17416afc8 --- /dev/null +++ b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/Scheduler.scala @@ -0,0 +1,305 @@ +package m14 +package instrumentation + +import java.util.concurrent._; +import scala.concurrent.duration._ +import scala.collection.mutable._ +import Stats._ + +import java.util.concurrent.atomic.AtomicInteger + +sealed abstract class Result +case class RetVal(rets: List[Any]) extends Result +case class Except(msg: String, stackTrace: Array[StackTraceElement]) extends Result +case class Timeout(msg: String) extends Result + +/** + * A class that maintains schedule and a set of thread ids. + * The schedules are advanced after an operation of a SchedulableBuffer is performed. + * Note: the real schedule that is executed may deviate from the input schedule + * due to the adjustments that had to be made for locks + */ +class Scheduler(sched: List[Int]) { + val maxOps = 500 // a limit on the maximum number of operations the code is allowed to perform + + private var schedule = sched + private var numThreads = 0 + private val realToFakeThreadId = Map[Long, Int]() + private val opLog = ListBuffer[String]() // a mutable list (used for efficient concat) + private val threadStates = Map[Int, ThreadState]() + + /** + * Runs a set of operations in parallel as per the schedule. + * Each operation may consist of many primitive operations like reads or writes + * to shared data structure each of which should be executed using the function `exec`. + * @timeout in milliseconds + * @return true - all threads completed on time, false -some tests timed out. + */ + def runInParallel(timeout: Long, ops: List[() => Any]): Result = { + numThreads = ops.length + val threadRes = Array.fill(numThreads) { None: Any } + var exception: Option[Except] = None + val syncObject = new Object() + var completed = new AtomicInteger(0) + // create threads + val threads = ops.zipWithIndex.map { + case (op, i) => + new Thread(new Runnable() { + def run(): Unit = { + val fakeId = i + 1 + setThreadId(fakeId) + try { + updateThreadState(Start) + val res = op() + updateThreadState(End) + threadRes(i) = res + // notify the master thread if all threads have completed + if (completed.incrementAndGet() == ops.length) { + syncObject.synchronized { syncObject.notifyAll() } + } + } catch { + case e: Throwable if exception != None => // do nothing here and silently fail + case e: Throwable => + log(s"throw ${e.toString}") + exception = Some(Except(s"Thread $fakeId crashed on the following schedule: \n" + opLog.mkString("\n"), + e.getStackTrace)) + syncObject.synchronized { syncObject.notifyAll() } + //println(s"$fakeId: ${e.toString}") + //Runtime.getRuntime().halt(0) //exit the JVM and all running threads (no other way to kill other threads) + } + } + }) + } + // start all threads + threads.foreach(_.start()) + // wait for all threads to complete, or for an exception to be thrown, or for the time out to expire + var remTime = timeout + syncObject.synchronized { + timed { if(completed.get() != ops.length) syncObject.wait(timeout) } { time => remTime -= time } + } + if (exception.isDefined) { + exception.get + } else if (remTime <= 1) { // timeout ? using 1 instead of zero to allow for some errors + Timeout(opLog.mkString("\n")) + } else { + // every thing executed normally + RetVal(threadRes.toList) + } + } + + // Updates the state of the current thread + def updateThreadState(state: ThreadState): Unit = { + val tid = threadId + synchronized { + threadStates(tid) = state + } + state match { + case Sync(lockToAquire, locks) => + if (locks.indexOf(lockToAquire) < 0) waitForTurn else { + // Re-aqcuiring the same lock + updateThreadState(Running(lockToAquire +: locks)) + } + case Start => waitStart() + case End => removeFromSchedule(tid) + case Running(_) => + case _ => waitForTurn // Wait, SyncUnique, VariableReadWrite + } + } + + def waitStart(): Unit = { + //while (threadStates.size < numThreads) { + //Thread.sleep(1) + //} + synchronized { + if (threadStates.size < numThreads) { + wait() + } else { + notifyAll() + } + } + } + + def threadLocks = { + synchronized { + threadStates(threadId).locks + } + } + + def threadState = { + synchronized { + threadStates(threadId) + } + } + + def mapOtherStates(f: ThreadState => ThreadState) = { + val exception = threadId + synchronized { + for (k <- threadStates.keys if k != exception) { + threadStates(k) = f(threadStates(k)) + } + } + } + + def log(str: String) = { + if((realToFakeThreadId contains Thread.currentThread().getId())) { + val space = (" " * ((threadId - 1) * 2)) + val s = space + threadId + ":" + "\n".r.replaceAllIn(str, "\n" + space + " ") + opLog += s + } + } + + /** + * Executes a read or write operation to a global data structure as per the given schedule + * @param msg a message corresponding to the operation that will be logged + */ + def exec[T](primop: => T)(msg: => String, postMsg: => Option[T => String] = None): T = { + if(! (realToFakeThreadId contains Thread.currentThread().getId())) { + primop + } else { + updateThreadState(VariableReadWrite(threadLocks)) + val m = msg + if(m != "") log(m) + if (opLog.size > maxOps) + throw new Exception(s"Total number of reads/writes performed by threads exceed $maxOps. A possible deadlock!") + val res = primop + postMsg match { + case Some(m) => log(m(res)) + case None => + } + res + } + } + + private def setThreadId(fakeId: Int) = synchronized { + realToFakeThreadId(Thread.currentThread.getId) = fakeId + } + + def threadId = + try { + realToFakeThreadId(Thread.currentThread().getId()) + } catch { + case e: NoSuchElementException => + throw new Exception("You are accessing shared variables in the constructor. This is not allowed. The variables are already initialized!") + } + + private def isTurn(tid: Int) = synchronized { + (!schedule.isEmpty && schedule.head != tid) + } + + def canProceed(): Boolean = { + val tid = threadId + canContinue match { + case Some((i, state)) if i == tid => + //println(s"$tid: Runs ! Was in state $state") + canContinue = None + state match { + case Sync(lockToAquire, locks) => updateThreadState(Running(lockToAquire +: locks)) + case SyncUnique(lockToAquire, locks) => + mapOtherStates { + _ match { + case SyncUnique(lockToAquire2, locks2) if lockToAquire2 == lockToAquire => Wait(lockToAquire2, locks2) + case e => e + } + } + updateThreadState(Running(lockToAquire +: locks)) + case VariableReadWrite(locks) => updateThreadState(Running(locks)) + } + true + case Some((i, state)) => + //println(s"$tid: not my turn but $i !") + false + case None => + false + } + } + + var threadPreference = 0 // In the case the schedule is over, which thread should have the preference to execute. + + /** returns true if the thread can continue to execute, and false otherwise */ + def decide(): Option[(Int, ThreadState)] = { + if (!threadStates.isEmpty) { // The last thread who enters the decision loop takes the decision. + //println(s"$threadId: I'm taking a decision") + if (threadStates.values.forall { case e: Wait => true case _ => false }) { + val waiting = threadStates.keys.map(_.toString).mkString(", ") + val s = if (threadStates.size > 1) "s" else "" + val are = if (threadStates.size > 1) "are" else "is" + throw new Exception(s"Deadlock: Thread$s $waiting $are waiting but all others have ended and cannot notify them.") + } else { + // Threads can be in Wait, Sync, SyncUnique, and VariableReadWrite mode. + // Let's determine which ones can continue. + val notFree = threadStates.collect { case (id, state) => state.locks }.flatten.toSet + val threadsNotBlocked = threadStates.toSeq.filter { + case (id, v: VariableReadWrite) => true + case (id, v: CanContinueIfAcquiresLock) => !notFree(v.lockToAquire) || (v.locks contains v.lockToAquire) + case _ => false + } + if (threadsNotBlocked.isEmpty) { + val waiting = threadStates.keys.map(_.toString).mkString(", ") + val s = if (threadStates.size > 1) "s" else "" + val are = if (threadStates.size > 1) "are" else "is" + val whoHasLock = threadStates.toSeq.flatMap { case (id, state) => state.locks.map(lock => (lock, id)) }.toMap + val reason = threadStates.collect { + case (id, state: CanContinueIfAcquiresLock) if !notFree(state.lockToAquire) => + s"Thread $id is waiting on lock ${state.lockToAquire} held by thread ${whoHasLock(state.lockToAquire)}" + }.mkString("\n") + throw new Exception(s"Deadlock: Thread$s $waiting are interlocked. Indeed:\n$reason") + } else if (threadsNotBlocked.size == 1) { // Do not consume the schedule if only one thread can execute. + Some(threadsNotBlocked(0)) + } else { + val next = schedule.indexWhere(t => threadsNotBlocked.exists { case (id, state) => id == t }) + if (next != -1) { + //println(s"$threadId: schedule is $schedule, next chosen is ${schedule(next)}") + val chosenOne = schedule(next) // TODO: Make schedule a mutable list. + schedule = schedule.take(next) ++ schedule.drop(next + 1) + Some((chosenOne, threadStates(chosenOne))) + } else { + threadPreference = (threadPreference + 1) % threadsNotBlocked.size + val chosenOne = threadsNotBlocked(threadPreference) // Maybe another strategy + Some(chosenOne) + //threadsNotBlocked.indexOf(threadId) >= 0 + /* + val tnb = threadsNotBlocked.map(_._1).mkString(",") + val s = if (schedule.isEmpty) "empty" else schedule.mkString(",") + val only = if (schedule.isEmpty) "" else " only" + throw new Exception(s"The schedule is $s but$only threads ${tnb} can continue")*/ + } + } + } + } else canContinue + } + + /** + * This will be called before a schedulable operation begins. + * This should not use synchronized + */ + var numThreadsWaiting = new AtomicInteger(0) + //var waitingForDecision = Map[Int, Option[Int]]() // Mapping from thread ids to a number indicating who is going to make the choice. + var canContinue: Option[(Int, ThreadState)] = None // The result of the decision thread Id of the thread authorized to continue. + private def waitForTurn = { + synchronized { + if (numThreadsWaiting.incrementAndGet() == threadStates.size) { + canContinue = decide() + notifyAll() + } + //waitingForDecision(threadId) = Some(numThreadsWaiting) + //println(s"$threadId Entering waiting with ticket number $numThreadsWaiting/${waitingForDecision.size}") + while (!canProceed()) wait() + } + numThreadsWaiting.decrementAndGet() + } + + /** + * To be invoked when a thread is about to complete + */ + private def removeFromSchedule(fakeid: Int) = synchronized { + //println(s"$fakeid: I'm taking a decision because I finished") + schedule = schedule.filterNot(_ == fakeid) + threadStates -= fakeid + if (numThreadsWaiting.get() == threadStates.size) { + canContinue = decide() + notifyAll() + } + } + + def getOperationLog() = opLog +} diff --git a/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/Stats.scala b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/Stats.scala new file mode 100644 index 0000000000000000000000000000000000000000..bc1241c543227a71727d2ca2987e3bdc9fed3210 --- /dev/null +++ b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/Stats.scala @@ -0,0 +1,23 @@ +package m14 +package instrumentation + +import java.lang.management._ + +/** + * A collection of methods that can be used to collect run-time statistics about Leon programs. + * This is mostly used to test the resources properties of Leon programs + */ +object Stats { + def timed[T](code: => T)(cont: Long => Unit): T = { + var t1 = System.currentTimeMillis() + val r = code + cont((System.currentTimeMillis() - t1)) + r + } + + def withTime[T](code: => T): (T, Long) = { + var t1 = System.currentTimeMillis() + val r = code + (r, (System.currentTimeMillis() - t1)) + } +} diff --git a/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/TestHelper.scala b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/TestHelper.scala new file mode 100644 index 0000000000000000000000000000000000000000..faa3505b2d85e546e20f9f228ca0ad1f6ac9c438 --- /dev/null +++ b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/TestHelper.scala @@ -0,0 +1,125 @@ +package m14 +package instrumentation + +import scala.util.Random +import scala.collection.mutable.{Map => MutableMap} + +import Stats._ + +object TestHelper { + val noOfSchedules = 10000 // set this to 100k during deployment + val readWritesPerThread = 20 // maximum number of read/writes possible in one thread + val contextSwitchBound = 10 + val testTimeout = 240 // the total time out for a test in seconds + val schedTimeout = 15 // the total time out for execution of a schedule in secs + + // Helpers + /*def testManySchedules(op1: => Any): Unit = testManySchedules(List(() => op1)) + def testManySchedules(op1: => Any, op2: => Any): Unit = testManySchedules(List(() => op1, () => op2)) + def testManySchedules(op1: => Any, op2: => Any, op3: => Any): Unit = testManySchedules(List(() => op1, () => op2, () => op3)) + def testManySchedules(op1: => Any, op2: => Any, op3: => Any, op4: => Any): Unit = testManySchedules(List(() => op1, () => op2, () => op3, () => op4))*/ + + def testSequential[T](ops: Scheduler => Any)(assertions: T => (Boolean, String)) = + testManySchedules(1, + (sched: Scheduler) => { + (List(() => ops(sched)), + (res: List[Any]) => assertions(res.head.asInstanceOf[T])) + }) + + /** + * @numThreads number of threads + * @ops operations to be executed, one per thread + * @assertion as condition that will executed after all threads have completed (without exceptions) + * the arguments are the results of the threads + */ + def testManySchedules(numThreads: Int, + ops: Scheduler => + (List[() => Any], // Threads + List[Any] => (Boolean, String)) // Assertion + ) = { + var timeout = testTimeout * 1000L + val threadIds = (1 to numThreads) + //(1 to scheduleLength).flatMap(_ => threadIds).toList.permutations.take(noOfSchedules).foreach { + val schedules = (new ScheduleGenerator(numThreads)).schedules() + var schedsExplored = 0 + schedules.takeWhile(_ => schedsExplored <= noOfSchedules && timeout > 0).foreach { + //case _ if timeout <= 0 => // break + case schedule => + schedsExplored += 1 + val schedr = new Scheduler(schedule) + //println("Exploring Sched: "+schedule) + val (threadOps, assertion) = ops(schedr) + if (threadOps.size != numThreads) + throw new IllegalStateException(s"Number of threads: $numThreads, do not match operations of threads: $threadOps") + timed { schedr.runInParallel(schedTimeout * 1000, threadOps) } { t => timeout -= t } match { + case Timeout(msg) => + throw new java.lang.AssertionError("assertion failed\n"+"The schedule took too long to complete. A possible deadlock! \n"+msg) + case Except(msg, stkTrace) => + val traceStr = "Thread Stack trace: \n"+stkTrace.map(" at "+_.toString).mkString("\n") + throw new java.lang.AssertionError("assertion failed\n"+msg+"\n"+traceStr) + case RetVal(threadRes) => + // check the assertion + val (success, custom_msg) = assertion(threadRes) + if (!success) { + val msg = "The following schedule resulted in wrong results: \n" + custom_msg + "\n" + schedr.getOperationLog().mkString("\n") + throw new java.lang.AssertionError("Assertion failed: "+msg) + } + } + } + if (timeout <= 0) { + throw new java.lang.AssertionError("Test took too long to complete! Cannot check all schedules as your code is too slow!") + } + } + + /** + * A schedule generator that is based on the context bound + */ + class ScheduleGenerator(numThreads: Int) { + val scheduleLength = readWritesPerThread * numThreads + val rands = (1 to scheduleLength).map(i => new Random(0xcafe * i)) // random numbers for choosing a thread at each position + def schedules(): LazyList[List[Int]] = { + var contextSwitches = 0 + var contexts = List[Int]() // a stack of thread ids in the order of context-switches + val remainingOps = MutableMap[Int, Int]() + remainingOps ++= (1 to numThreads).map(i => (i, readWritesPerThread)) // num ops remaining in each thread + val liveThreads = (1 to numThreads).toSeq.toBuffer + + /** + * Updates remainingOps and liveThreads once a thread is chosen for a position in the schedule + */ + def updateState(tid: Int): Unit = { + val remOps = remainingOps(tid) + if (remOps == 0) { + liveThreads -= tid + } else { + remainingOps += (tid -> (remOps - 1)) + } + } + val schedule = rands.foldLeft(List[Int]()) { + case (acc, r) if contextSwitches < contextSwitchBound => + val tid = liveThreads(r.nextInt(liveThreads.size)) + contexts match { + case prev :: tail if prev != tid => // we have a new context switch here + contexts +:= tid + contextSwitches += 1 + case prev :: tail => + case _ => // init case + contexts +:= tid + } + updateState(tid) + acc :+ tid + case (acc, _) => // here context-bound has been reached so complete the schedule without any more context switches + if (!contexts.isEmpty) { + contexts = contexts.dropWhile(remainingOps(_) == 0) + } + val tid = contexts match { + case top :: tail => top + case _ => liveThreads(0) // here, there has to be threads that have not even started + } + updateState(tid) + acc :+ tid + } + schedule #:: schedules() + } + } +} diff --git a/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/TestUtils.scala b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/TestUtils.scala new file mode 100644 index 0000000000000000000000000000000000000000..f980f99e34d653acde5590cb5e1e508607d3b61b --- /dev/null +++ b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/TestUtils.scala @@ -0,0 +1,20 @@ +package m14 +package instrumentation + +import scala.concurrent._ +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global + +object TestUtils { + def failsOrTimesOut[T](action: => T): Boolean = { + val asyncAction = Future { + action + } + try { + Await.result(asyncAction, 2000.millisecond) + } catch { + case _: Throwable => return true + } + return false + } +} diff --git a/previous-exams/2021-midterm-solutions/m15.md b/previous-exams/2021-midterm/m15.md similarity index 95% rename from previous-exams/2021-midterm-solutions/m15.md rename to previous-exams/2021-midterm/m15.md index 781e05a4681b94b94c4f8376bec6c2fae7d592cb..dd450975e1d5b2ccebf15375e61c87c9a4519437 100644 --- a/previous-exams/2021-midterm-solutions/m15.md +++ b/previous-exams/2021-midterm/m15.md @@ -1,9 +1,3 @@ -Use the following commands to make a fresh clone of your repository: - -``` -git clone -b m15 git@gitlab.epfl.ch:lamp/student-repositories-s21/cs206-GASPAR.git m15 -``` - ## Useful links * [A guide to the Scala parallel collections](https://docs.scala-lang.org/overviews/parallel-collections/overview.html) diff --git a/previous-exams/2021-midterm/m15/.gitignore b/previous-exams/2021-midterm/m15/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..40937dc9b192820d0ede18efd3c7e6442a083b17 --- /dev/null +++ b/previous-exams/2021-midterm/m15/.gitignore @@ -0,0 +1,22 @@ +# General +*.DS_Store +*.swp +*~ + +# Dotty +*.class +*.tasty +*.hasTasty + +# sbt +target/ + +# IDE +.bsp +.bloop +.metals +.vscode + +# datasets +stackoverflow-grading.csv +wikipedia-grading.dat diff --git a/previous-exams/2021-midterm/m15/assignment.sbt b/previous-exams/2021-midterm/m15/assignment.sbt new file mode 100644 index 0000000000000000000000000000000000000000..da7eb3c8347293a18da0025fcd6060d8f8f7cc11 --- /dev/null +++ b/previous-exams/2021-midterm/m15/assignment.sbt @@ -0,0 +1,2 @@ +// Student tasks (i.e. submit, packageSubmission) +enablePlugins(StudentTasks) diff --git a/previous-exams/2021-midterm/m15/build.sbt b/previous-exams/2021-midterm/m15/build.sbt new file mode 100644 index 0000000000000000000000000000000000000000..3b7539dcef795f03ecd7ed4ffcb5eb6839054b21 --- /dev/null +++ b/previous-exams/2021-midterm/m15/build.sbt @@ -0,0 +1,12 @@ +course := "midterm" +assignment := "m15" +scalaVersion := "3.0.0-RC1" +scalacOptions ++= Seq("-language:implicitConversions", "-deprecation") + +libraryDependencies += "org.scalameta" %% "munit" % "0.7.22" + +val MUnitFramework = new TestFramework("munit.Framework") +testFrameworks += MUnitFramework +// Decode Scala names +testOptions += Tests.Argument(MUnitFramework, "-s") +testSuite := "m15.M15Suite" diff --git a/previous-exams/2021-midterm/m15/grading-tests.jar b/previous-exams/2021-midterm/m15/grading-tests.jar new file mode 100644 index 0000000000000000000000000000000000000000..be10881c427c928b9cb66f040d9bc864841c1499 Binary files /dev/null and b/previous-exams/2021-midterm/m15/grading-tests.jar differ diff --git a/previous-exams/2021-midterm/m15/project/FilteringReporterPlugin.scala b/previous-exams/2021-midterm/m15/project/FilteringReporterPlugin.scala new file mode 100644 index 0000000000000000000000000000000000000000..2e4fd9a4d998698cd52643344b33a5e719dd7971 --- /dev/null +++ b/previous-exams/2021-midterm/m15/project/FilteringReporterPlugin.scala @@ -0,0 +1,31 @@ +package sbt // To access the private[sbt] compilerReporter key +package filteringReporterPlugin + +import Keys._ +import ch.epfl.lamp._ + +object FilteringReporterPlugin extends AutoPlugin { + override lazy val projectSettings = Seq( + // Turn off warning coming from scalameter that we cannot fix without changing scalameter + compilerReporter in (Compile, compile) ~= { reporter => new FilteringReporter(reporter) } + ) +} + +class FilteringReporter(reporter: xsbti.Reporter) extends xsbti.Reporter { + + def reset(): Unit = reporter.reset() + def hasErrors: Boolean = reporter.hasErrors + def hasWarnings: Boolean = reporter.hasWarnings + def printSummary(): Unit = reporter.printSummary() + def problems: Array[xsbti.Problem] = reporter.problems + + def log(problem: xsbti.Problem): Unit = { + if (!problem.message.contains("An existential type that came from a Scala-2 classfile cannot be")) + reporter.log(problem) + } + + def comment(pos: xsbti.Position, msg: String): Unit = + reporter.comment(pos, msg) + + override def toString = s"CollectingReporter($reporter)" +} diff --git a/previous-exams/2021-midterm/m15/project/MOOCSettings.scala b/previous-exams/2021-midterm/m15/project/MOOCSettings.scala new file mode 100644 index 0000000000000000000000000000000000000000..1c40443a53085d23fadb134f4e1a505c32231f1d --- /dev/null +++ b/previous-exams/2021-midterm/m15/project/MOOCSettings.scala @@ -0,0 +1,49 @@ +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 { + + override def requires = super.requires && filteringReporterPlugin.FilteringReporterPlugin + + 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") + .withRank(KeyRanks.Invisible) + // 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/previous-exams/2021-midterm/m15/project/StudentTasks.scala b/previous-exams/2021-midterm/m15/project/StudentTasks.scala new file mode 100644 index 0000000000000000000000000000000000000000..c4669afe82dd2b45651f94dcad9e736f29d21432 --- /dev/null +++ b/previous-exams/2021-midterm/m15/project/StudentTasks.scala @@ -0,0 +1,303 @@ +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") + + lazy val Grading = config("grading") extend(Runtime) + } + + + import autoImport._ + import MOOCSettings.autoImport._ + + override lazy val projectSettings = Seq( + packageSubmissionSetting, + fork := true, + connectInput in run := true, + outputStrategy := Some(StdoutOutput), + ) ++ + packageSubmissionZipSettings ++ + inConfig(Grading)(Defaults.testSettings ++ Seq( + unmanagedJars += file("grading-tests.jar"), + + definedTests := (definedTests in Test).value, + internalDependencyClasspath := (internalDependencyClasspath in Test).value + )) + + + /** ********************************************************** + * 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, None) + 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("<arg>").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 <email-address> <submit-token> + | + |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/previous-exams/2021-midterm/m15/project/build.properties b/previous-exams/2021-midterm/m15/project/build.properties new file mode 100644 index 0000000000000000000000000000000000000000..0b2e09c5ac99bd3de91b2b139b94301c2b6e26f3 --- /dev/null +++ b/previous-exams/2021-midterm/m15/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.4.7 diff --git a/previous-exams/2021-midterm/m15/project/buildSettings.sbt b/previous-exams/2021-midterm/m15/project/buildSettings.sbt new file mode 100644 index 0000000000000000000000000000000000000000..8fac702aaf3f3c4ede79691c7b4e4a52f26f3f47 --- /dev/null +++ b/previous-exams/2021-midterm/m15/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/previous-exams/2021-midterm/m15/project/plugins.sbt b/previous-exams/2021-midterm/m15/project/plugins.sbt new file mode 100644 index 0000000000000000000000000000000000000000..fb7dbe068109e7f35c13b2762b865c7eec1979f3 --- /dev/null +++ b/previous-exams/2021-midterm/m15/project/plugins.sbt @@ -0,0 +1,3 @@ +// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3") diff --git a/previous-exams/2021-midterm/m15/src/main/scala/m15/AbstractBlockingQueue.scala b/previous-exams/2021-midterm/m15/src/main/scala/m15/AbstractBlockingQueue.scala new file mode 100644 index 0000000000000000000000000000000000000000..85a28b26c1d28327725d98d501e4f855dce0f25e --- /dev/null +++ b/previous-exams/2021-midterm/m15/src/main/scala/m15/AbstractBlockingQueue.scala @@ -0,0 +1,14 @@ +package m15 + +abstract class AbstractBlockingQueue[T] extends Monitor { + private var underlying: List[T] = Nil + + def getUnderlying(): List[T] = + underlying + + def setUnderlying(newValue: List[T]): Unit = + underlying = newValue + + def put(elem: T): Unit + def take(): T +} diff --git a/previous-exams/2021-midterm/m15/src/main/scala/m15/AbstractThreadPoolExecutor.scala b/previous-exams/2021-midterm/m15/src/main/scala/m15/AbstractThreadPoolExecutor.scala new file mode 100644 index 0000000000000000000000000000000000000000..5e663e8b70cd7482feca29b9791bcd766174ae49 --- /dev/null +++ b/previous-exams/2021-midterm/m15/src/main/scala/m15/AbstractThreadPoolExecutor.scala @@ -0,0 +1,7 @@ +package m15 + +abstract class AbstractThreadPoolExecutor { + def execute(task: Unit => Unit): Unit + def start(): Unit + def shutdown(): Unit +} diff --git a/previous-exams/2021-midterm/m15/src/main/scala/m15/M15.scala b/previous-exams/2021-midterm/m15/src/main/scala/m15/M15.scala new file mode 100644 index 0000000000000000000000000000000000000000..00a4aed86504e6578a3dd70fa37bfbff7cc4bbcb --- /dev/null +++ b/previous-exams/2021-midterm/m15/src/main/scala/m15/M15.scala @@ -0,0 +1,65 @@ +package m15 + +object M15 { + /** A thread pool that executes submitted task using one of several threads */ + class ThreadPoolExecutor(taskQueue: BlockingQueue[Unit => Unit], poolSize: Int) + extends AbstractThreadPoolExecutor { + + private class Worker extends Thread { + override def run(): Unit = { + try { + while (true) { + ??? + } + } catch { + case e: InterruptedException => + // Nothing to do here, we are shutting down gracefully. + } + } + } + private val workers: List[Worker] = List.fill(poolSize)(new Worker()) + + /** Executes the given task, passed by name. */ + def execute(task: Unit => Unit): Unit = + ??? + + /** Starts the thread pool. */ + def start(): Unit = + workers.foreach(_.start()) + + /** Instantly shuts down all actively executing tasks using an interrupt. */ + def shutdown(): Unit = + workers.foreach(_.interrupt()) + } + + /** + * A queue whose take operations blocks until the queue become non-empty. + * Elements must be retrived from this queue in a first in, first out order. + * All methods of this class are thread safe, that is, they can safely + * be used from multiple thread without any particular synchronization. + */ + class BlockingQueue[T] extends AbstractBlockingQueue[T] { + + // The state of this queue is stored in an underlying List[T] defined in + // the AbstractBlockingQueue class. Your implementation should access and + // update this list using the following setter and getter methods: + // - def getUnderlying(): List[T] + // - def setUnderlying(newValue: List[T]): Unit + // Using these methods is required for testing purposes. + + /** Inserts the specified element into this queue (non-blocking) */ + def put(elem: T): Unit = + ??? + + /** + * Retrieves and removes the head of this queue, waiting if necessary + * until an element becomes available (blocking). + * This queue operates in a first in, first out order. + */ + def take(): T = + // Hint: The .last/.init methods on List are dual of .head/.head, + // they can be used to retrive the last element and the initial part of + // the list without its last element. + ??? + } +} diff --git a/previous-exams/2021-midterm/m15/src/main/scala/m15/Monitor.scala b/previous-exams/2021-midterm/m15/src/main/scala/m15/Monitor.scala new file mode 100644 index 0000000000000000000000000000000000000000..b64e697d613b2d44c3f892e4ae0eebf028b5d5e8 --- /dev/null +++ b/previous-exams/2021-midterm/m15/src/main/scala/m15/Monitor.scala @@ -0,0 +1,23 @@ +package m15 + +class Dummy + +trait Monitor { + implicit val dummy: Dummy = new Dummy + + def wait()(implicit i: Dummy) = waitDefault() + + def synchronized[T](e: => T)(implicit i: Dummy) = synchronizedDefault(e) + + def notify()(implicit i: Dummy) = notifyDefault() + + def notifyAll()(implicit i: Dummy) = notifyAllDefault() + + private val lock = new AnyRef + + // Can be overriden. + def waitDefault(): Unit = lock.wait() + def synchronizedDefault[T](toExecute: => T): T = lock.synchronized(toExecute) + def notifyDefault(): Unit = lock.notify() + def notifyAllDefault(): Unit = lock.notifyAll() +} diff --git a/previous-exams/2021-midterm/m15/src/test/scala/m15/M15Suite.scala b/previous-exams/2021-midterm/m15/src/test/scala/m15/M15Suite.scala new file mode 100644 index 0000000000000000000000000000000000000000..e0f2243993c4f40b8e43b0b51e1e45b8c77d2c21 --- /dev/null +++ b/previous-exams/2021-midterm/m15/src/test/scala/m15/M15Suite.scala @@ -0,0 +1,281 @@ +package m15 + +import instrumentation.SchedulableBlockingQueue +import instrumentation.TestHelper._ +import instrumentation.TestUtils._ + +class M15Suite extends munit.FunSuite { + import M15._ + + test("ThreadPool should put jobs in the queue, Workers should execute jobs from the queue (10pts)") { + case class PutE(e: Unit => Unit) extends Exception + val nThreads = 3 + var taken = false + class TestBlockingQueue extends BlockingQueue[Unit => Unit] { + override def put(e: Unit => Unit): Unit = + throw new PutE(e) + + override def take(): Unit => Unit = + x => { + taken = true + Thread.sleep(10 * 1000) + } + } + + val tpe = new ThreadPoolExecutor(new TestBlockingQueue, nThreads) + val unit2unit: Unit => Unit = x => () + try { + tpe.execute(unit2unit) + assert(false, "ThreadPoolExecutor does not put jobs in the queue") + } catch { + case PutE(e) => + assert(e == unit2unit) + } + tpe.start() + Thread.sleep(1000) + assert(taken, s"ThreadPoolExecutor workers do no execute jobs from the queue") + tpe.shutdown() + } + + test("BlockingQueue should work in a sequential setting (1pts)") { + testSequential[(Int, Int, Int, Int)]{ sched => + val queue = new SchedulableBlockingQueue[Int](sched) + queue.put(1) + queue.put(2) + queue.put(3) + queue.put(4) + (queue.take(), + queue.take(), + queue.take(), + queue.take()) + }{ tuple => + (tuple == (1, 2, 3, 4), s"Expected (1, 2, 3, 4) got $tuple") + } + } + + test("BlockingQueue should work when Thread 1: 'put(1)', Thread 2: 'take' (3pts)") { + testManySchedules(2, sched => { + val queue = new SchedulableBlockingQueue[Int](sched) + (List(() => queue.put(1), () => queue.take()), + args => (args(1) == 1, s"Expected 1, got ${args(1)}")) + }) + } + + test("BlockingQueue should not be able to take from an empty queue (3pts)") { + testSequential[Boolean]{ sched => + val queue = new SchedulableBlockingQueue[Int](sched); + queue.put(1) + queue.put(2) + queue.take() + queue.take() + failsOrTimesOut(queue.take()) + }{ res => + (res, "Was able to retrieve an element from an empty queue") + } + } + + test("Should work when Thread 1: 'put(1)', Thread 2: 'put(2)', Thread 3: 'take', and a buffer of size 1") { + testManySchedules(3, sched => { + val prodCons = new SchedulableBlockingQueue[Int](sched) + (List(() => prodCons.put(1), () => prodCons.put(2), () => prodCons.take()) + , args => { + val takeRes = args(2).asInstanceOf[Int] + val nocreation = (takeRes == 1 || takeRes == 2) + if (!nocreation) + (false, s"'take' should return either 1 or 2") + else (true, "") + }) + }) + } + + // testing no duplication + test("Should work when Thread 1: 'put(1)', Thread 2: 'put(2)', Thread 3: 'take', Thread 4: 'take', and a buffer of size 3") { + testManySchedules(4, sched => { + val prodCons = new SchedulableBlockingQueue[Int](sched) + (List(() => prodCons.put(1), () => prodCons.put(2), () => prodCons.take(), () => prodCons.take()) + , args => { + def m(): (Boolean, String) = { + val takeRes1 = args(2).asInstanceOf[Int] + val takeRes2 = args(3).asInstanceOf[Int] + val nocreation = (x: Int) => List(1, 2).contains(x) + if (!nocreation(takeRes1)) + return (false, s"'Thread 3: take' returned $takeRes1 but should return a value in {1, 2, 3}") + if (!nocreation(takeRes2)) + return (false, s"'Thread 4: take' returned $takeRes2 but should return a value in {1, 2, 3}") + + val noduplication = takeRes1 != takeRes2 + if (!noduplication) + (false, s"'Thread 3 and 4' returned the same value: $takeRes1") + else (true, "") + } + m() + }) + }) + } + + // testing no duplication with 5 threads + test("Should work when Thread 1: 'put(1)', Thread 2: 'put(2)', Thread 3: 'put(3)', Thread 4: 'take', Thread 5: 'take', and a buffer of size 1") { + testManySchedules(5, sched => { + val prodCons = new SchedulableBlockingQueue[Int](sched) + (List(() => prodCons.put(1), () => prodCons.put(2), () => prodCons.put(3), + () => prodCons.take(), () => prodCons.take()) + , args => { + def m(): (Boolean, String) = { + val takeRes1 = args(3).asInstanceOf[Int] + val takeRes2 = args(4).asInstanceOf[Int] + val nocreation = (x: Int) => List(1, 2, 3).contains(x) + if (!nocreation(takeRes1)) + return (false, s"'Thread 4: take' returned $takeRes1 but should return a value in {1, 2, 3}") + if (!nocreation(takeRes2)) + return (false, s"'Thread 5: take' returned $takeRes2 but should return a value in {1, 2, 3}") + + val noduplication = takeRes1 != takeRes2 + if (!noduplication) + return (false, s"'Thread 4 and 5' returned the same value: $takeRes1") + else (true, "") + } + m() + }) + }) + } + + // testing fifo buffer size 1 + test("Should work when Thread 1: 'put(1); put(2)', Thread 2: 'take', Thread 3: 'put(3)', Thread 4: 'put(4)', and a buffer of size 3") { + testManySchedules(4, sched => { + val prodCons = new SchedulableBlockingQueue[Int](sched) + (List(() => { prodCons.put(1); prodCons.put(2) }, () => prodCons.take(), + () => prodCons.put(3), () => prodCons.put(4)) + , args => { + def m(): (Boolean, String) = { + val takeRes = args(1).asInstanceOf[Int] + // no creation + val nocreation = (x: Int) => List(1, 2, 3, 4).contains(x) + if (!nocreation(takeRes)) + return (false, s"'Thread 2: take' returned $takeRes, but should return a value in {1, 2, 3, 4}") + // fifo (cannot have 2 without 1) + if (takeRes == 2) + (false, s"'Thread 2' returned 2 before returning 1") + else + (true, "") + } + m() + }) + }) + } + + // testing fifo buffer size 5 + test("Should work when Thread 1: 'put(1); put(2)', Thread 2: 'take', Thread 3: 'put(11)', Thread 4: 'put(10)', and a buffer of size 5") { + testManySchedules(4, sched => { + val prodCons = new SchedulableBlockingQueue[Int](sched) + (List(() => { prodCons.put(1); prodCons.put(2) }, () => prodCons.take(), + () => prodCons.put(11), () => prodCons.put(10)) + , args => { + def m(): (Boolean, String) = { + val takeRes = args(1).asInstanceOf[Int] + // no creation + val nocreation = (x: Int) => List(1, 2, 10, 11).contains(x) + if (!nocreation(takeRes)) + return (false, s"'Thread 2: take' returned $takeRes, but should return a value in {1, 2, 10, 11}") + // fifo (cannot have 2 without 1) + if (takeRes == 2) + (false, s"'Thread 2' returned 2 before returning 1") + else + (true, "") + } + m() + }) + }) + } + + // testing fifo on more complicated case + test("Should work when Thread 1: 'put(1); put(3)', Thread 2: 'put(2)', Thread 3: 'put(4)', Thread 4: 'take', Thread 5: 'take', and a buffer of size 10") { + testManySchedules(5, sched => { + val prodCons = new SchedulableBlockingQueue[Int](sched) + (List(() => { prodCons.put(1); prodCons.put(3) }, () => prodCons.put(2), + () => prodCons.put(4), () => prodCons.take(), () => prodCons.take()) + , args => { + def m(): (Boolean, String) = { + val takeRes1 = args(3).asInstanceOf[Int] + val takeRes2 = args(4).asInstanceOf[Int] + // no creation + val nocreation = (x: Int) => List(1, 2, 3, 4).contains(x) + if (!nocreation(takeRes1)) + return (false, s"'Thread 4: take' returned $takeRes1 but should return a value in {1, 2, 3, 4}") + if (!nocreation(takeRes2)) + return (false, s"'Thread 5: take' returned $takeRes2 but should return a value in {1, 2, 3, 4}") + // no duplication + if (takeRes1 == takeRes2) + return (false, s"'Thread 4 and 5' returned the same value: $takeRes1") + // fifo (cannot have 3 without 1) + val takes = List(takeRes1, takeRes2) + if (takes.contains(3) && !takes.contains(1)) + (false, s"'Thread 4 or 5' returned 3 before returning 1") + else + (true, "") + } + m() + }) + }) + } + + // combining put and take in one thread + test("Should work when Thread 1: 'put(21); put(22)', Thread 2: 'take', Thread 3: 'put(23); take', Thread 4: 'put(24); take', and a buffer of size 2") { + testManySchedules(4, sched => { + val prodCons = new SchedulableBlockingQueue[Int](sched) + (List(() => { prodCons.put(21); prodCons.put(22) }, () => prodCons.take(), + () => { prodCons.put(23); prodCons.take() }, () => { prodCons.put(24); prodCons.take() }) + , args => { + def m(): (Boolean, String) = { + val takes = List(args(1).asInstanceOf[Int], args(2).asInstanceOf[Int], args(3).asInstanceOf[Int]) + // no creation + val vals = List(21, 22, 23, 24) + + var i = 0 + while (i < takes.length) { + val x = takes(i) + if (!vals.contains(x)) + return (false, s"'Thread $i: take' returned $x but should return a value in $vals") + i += 1 + } + + // no duplication + if (takes.distinct.size != takes.size) + return (false, s"Takes did not return unique values: $takes") + // fifo (cannot have 22 without 21) + if (takes.contains(22) && !takes.contains(21)) + (false, s"`Takes returned 22 before returning 21") + else + (true, "") + } + m() + }) + }) + } + + // completely hidden hard to crack test + test("[Black box test] Values should be taken in the order they are put") { + testManySchedules(4, sched => { + val prodCons = new SchedulableBlockingQueue[(Char, Int)](sched) + val n = 2 + (List( + () => for (i <- 1 to n) { prodCons.put(('a', i)) }, + () => for (i <- 1 to n) { prodCons.put(('b', i)) }, + () => for (i <- 1 to n) { prodCons.put(('c', i)) }, + () => { + import scala.collection.mutable + var counts = mutable.HashMap.empty[Char, Int] + counts('a') = 0 + counts('b') = 0 + counts('c') = 0 + for (i <- 1 to (3 * n)) { + val (c, n) = prodCons.take() + counts(c) += 1 + assert(counts(c) == n) + } + }) + , _ => + (true, "") + ) + }) + } +} diff --git a/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/MockedMonitor.scala b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/MockedMonitor.scala new file mode 100644 index 0000000000000000000000000000000000000000..c0591e3e03adc249e4a857600d20362bed219ba9 --- /dev/null +++ b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/MockedMonitor.scala @@ -0,0 +1,73 @@ +package m15 +package instrumentation + +trait MockedMonitor extends Monitor { + def scheduler: Scheduler + + // Can be overriden. + override def waitDefault() = { + scheduler.log("wait") + scheduler updateThreadState Wait(this, scheduler.threadLocks.tail) + } + override def synchronizedDefault[T](toExecute: =>T): T = { + scheduler.log("synchronized check") + val prevLocks = scheduler.threadLocks + scheduler updateThreadState Sync(this, prevLocks) // If this belongs to prevLocks, should just continue. + scheduler.log("synchronized -> enter") + try { + toExecute + } finally { + scheduler updateThreadState Running(prevLocks) + scheduler.log("synchronized -> out") + } + } + override def notifyDefault() = { + scheduler mapOtherStates { + state => state match { + case Wait(lockToAquire, locks) if lockToAquire == this => SyncUnique(this, state.locks) + case e => e + } + } + scheduler.log("notify") + } + override def notifyAllDefault() = { + scheduler mapOtherStates { + state => state match { + case Wait(lockToAquire, locks) if lockToAquire == this => Sync(this, state.locks) + case SyncUnique(lockToAquire, locks) if lockToAquire == this => Sync(this, state.locks) + case e => e + } + } + scheduler.log("notifyAll") + } +} + +trait LockFreeMonitor extends Monitor { + override def waitDefault() = { + throw new Exception("Please use lock-free structures and do not use wait()") + } + override def synchronizedDefault[T](toExecute: =>T): T = { + throw new Exception("Please use lock-free structures and do not use synchronized()") + } + override def notifyDefault() = { + throw new Exception("Please use lock-free structures and do not use notify()") + } + override def notifyAllDefault() = { + throw new Exception("Please use lock-free structures and do not use notifyAll()") + } +} + + +abstract class ThreadState { + def locks: Seq[AnyRef] +} +trait CanContinueIfAcquiresLock extends ThreadState { + def lockToAquire: AnyRef +} +case object Start extends ThreadState { def locks: Seq[AnyRef] = Seq.empty } +case object End extends ThreadState { def locks: Seq[AnyRef] = Seq.empty } +case class Wait(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState +case class SyncUnique(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState with CanContinueIfAcquiresLock +case class Sync(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState with CanContinueIfAcquiresLock +case class Running(locks: Seq[AnyRef]) extends ThreadState +case class VariableReadWrite(locks: Seq[AnyRef]) extends ThreadState diff --git a/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/SchedulableBlockingQueue.scala b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/SchedulableBlockingQueue.scala new file mode 100644 index 0000000000000000000000000000000000000000..31b09bef249bc17111043612f069402ef13bdf4f --- /dev/null +++ b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/SchedulableBlockingQueue.scala @@ -0,0 +1,17 @@ +package m15 +package instrumentation + +class SchedulableBlockingQueue[T](val scheduler: Scheduler) + extends m15.M15.BlockingQueue[T] with MockedMonitor { + private var underlying: List[T] = Nil + + override def getUnderlying(): List[T] = + scheduler.exec { + underlying + }(s"Get $underlying") + + override def setUnderlying(newValue: List[T]): Unit = + scheduler.exec { + underlying = newValue + }(s"Set $newValue") +} diff --git a/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/Scheduler.scala b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/Scheduler.scala new file mode 100644 index 0000000000000000000000000000000000000000..fd5f427bb86376709efeebebf0dbabc1bc96e70a --- /dev/null +++ b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/Scheduler.scala @@ -0,0 +1,305 @@ +package m15 +package instrumentation + +import java.util.concurrent._; +import scala.concurrent.duration._ +import scala.collection.mutable._ +import Stats._ + +import java.util.concurrent.atomic.AtomicInteger + +sealed abstract class Result +case class RetVal(rets: List[Any]) extends Result +case class Except(msg: String, stackTrace: Array[StackTraceElement]) extends Result +case class Timeout(msg: String) extends Result + +/** + * A class that maintains schedule and a set of thread ids. + * The schedules are advanced after an operation of a SchedulableBuffer is performed. + * Note: the real schedule that is executed may deviate from the input schedule + * due to the adjustments that had to be made for locks + */ +class Scheduler(sched: List[Int]) { + val maxOps = 500 // a limit on the maximum number of operations the code is allowed to perform + + private var schedule = sched + private var numThreads = 0 + private val realToFakeThreadId = Map[Long, Int]() + private val opLog = ListBuffer[String]() // a mutable list (used for efficient concat) + private val threadStates = Map[Int, ThreadState]() + + /** + * Runs a set of operations in parallel as per the schedule. + * Each operation may consist of many primitive operations like reads or writes + * to shared data structure each of which should be executed using the function `exec`. + * @timeout in milliseconds + * @return true - all threads completed on time, false -some tests timed out. + */ + def runInParallel(timeout: Long, ops: List[() => Any]): Result = { + numThreads = ops.length + val threadRes = Array.fill(numThreads) { None: Any } + var exception: Option[Except] = None + val syncObject = new Object() + var completed = new AtomicInteger(0) + // create threads + val threads = ops.zipWithIndex.map { + case (op, i) => + new Thread(new Runnable() { + def run(): Unit = { + val fakeId = i + 1 + setThreadId(fakeId) + try { + updateThreadState(Start) + val res = op() + updateThreadState(End) + threadRes(i) = res + // notify the master thread if all threads have completed + if (completed.incrementAndGet() == ops.length) { + syncObject.synchronized { syncObject.notifyAll() } + } + } catch { + case e: Throwable if exception != None => // do nothing here and silently fail + case e: Throwable => + log(s"throw ${e.toString}") + exception = Some(Except(s"Thread $fakeId crashed on the following schedule: \n" + opLog.mkString("\n"), + e.getStackTrace)) + syncObject.synchronized { syncObject.notifyAll() } + //println(s"$fakeId: ${e.toString}") + //Runtime.getRuntime().halt(0) //exit the JVM and all running threads (no other way to kill other threads) + } + } + }) + } + // start all threads + threads.foreach(_.start()) + // wait for all threads to complete, or for an exception to be thrown, or for the time out to expire + var remTime = timeout + syncObject.synchronized { + timed { if(completed.get() != ops.length) syncObject.wait(timeout) } { time => remTime -= time } + } + if (exception.isDefined) { + exception.get + } else if (remTime <= 1) { // timeout ? using 1 instead of zero to allow for some errors + Timeout(opLog.mkString("\n")) + } else { + // every thing executed normally + RetVal(threadRes.toList) + } + } + + // Updates the state of the current thread + def updateThreadState(state: ThreadState): Unit = { + val tid = threadId + synchronized { + threadStates(tid) = state + } + state match { + case Sync(lockToAquire, locks) => + if (locks.indexOf(lockToAquire) < 0) waitForTurn else { + // Re-aqcuiring the same lock + updateThreadState(Running(lockToAquire +: locks)) + } + case Start => waitStart() + case End => removeFromSchedule(tid) + case Running(_) => + case _ => waitForTurn // Wait, SyncUnique, VariableReadWrite + } + } + + def waitStart(): Unit = { + //while (threadStates.size < numThreads) { + //Thread.sleep(1) + //} + synchronized { + if (threadStates.size < numThreads) { + wait() + } else { + notifyAll() + } + } + } + + def threadLocks = { + synchronized { + threadStates(threadId).locks + } + } + + def threadState = { + synchronized { + threadStates(threadId) + } + } + + def mapOtherStates(f: ThreadState => ThreadState) = { + val exception = threadId + synchronized { + for (k <- threadStates.keys if k != exception) { + threadStates(k) = f(threadStates(k)) + } + } + } + + def log(str: String) = { + if((realToFakeThreadId contains Thread.currentThread().getId())) { + val space = (" " * ((threadId - 1) * 2)) + val s = space + threadId + ":" + "\n".r.replaceAllIn(str, "\n" + space + " ") + opLog += s + } + } + + /** + * Executes a read or write operation to a global data structure as per the given schedule + * @param msg a message corresponding to the operation that will be logged + */ + def exec[T](primop: => T)(msg: => String, postMsg: => Option[T => String] = None): T = { + if(! (realToFakeThreadId contains Thread.currentThread().getId())) { + primop + } else { + updateThreadState(VariableReadWrite(threadLocks)) + val m = msg + if(m != "") log(m) + if (opLog.size > maxOps) + throw new Exception(s"Total number of reads/writes performed by threads exceed $maxOps. A possible deadlock!") + val res = primop + postMsg match { + case Some(m) => log(m(res)) + case None => + } + res + } + } + + private def setThreadId(fakeId: Int) = synchronized { + realToFakeThreadId(Thread.currentThread.getId) = fakeId + } + + def threadId = + try { + realToFakeThreadId(Thread.currentThread().getId()) + } catch { + case e: NoSuchElementException => + throw new Exception("You are accessing shared variables in the constructor. This is not allowed. The variables are already initialized!") + } + + private def isTurn(tid: Int) = synchronized { + (!schedule.isEmpty && schedule.head != tid) + } + + def canProceed(): Boolean = { + val tid = threadId + canContinue match { + case Some((i, state)) if i == tid => + //println(s"$tid: Runs ! Was in state $state") + canContinue = None + state match { + case Sync(lockToAquire, locks) => updateThreadState(Running(lockToAquire +: locks)) + case SyncUnique(lockToAquire, locks) => + mapOtherStates { + _ match { + case SyncUnique(lockToAquire2, locks2) if lockToAquire2 == lockToAquire => Wait(lockToAquire2, locks2) + case e => e + } + } + updateThreadState(Running(lockToAquire +: locks)) + case VariableReadWrite(locks) => updateThreadState(Running(locks)) + } + true + case Some((i, state)) => + //println(s"$tid: not my turn but $i !") + false + case None => + false + } + } + + var threadPreference = 0 // In the case the schedule is over, which thread should have the preference to execute. + + /** returns true if the thread can continue to execute, and false otherwise */ + def decide(): Option[(Int, ThreadState)] = { + if (!threadStates.isEmpty) { // The last thread who enters the decision loop takes the decision. + //println(s"$threadId: I'm taking a decision") + if (threadStates.values.forall { case e: Wait => true case _ => false }) { + val waiting = threadStates.keys.map(_.toString).mkString(", ") + val s = if (threadStates.size > 1) "s" else "" + val are = if (threadStates.size > 1) "are" else "is" + throw new Exception(s"Deadlock: Thread$s $waiting $are waiting but all others have ended and cannot notify them.") + } else { + // Threads can be in Wait, Sync, SyncUnique, and VariableReadWrite mode. + // Let's determine which ones can continue. + val notFree = threadStates.collect { case (id, state) => state.locks }.flatten.toSet + val threadsNotBlocked = threadStates.toSeq.filter { + case (id, v: VariableReadWrite) => true + case (id, v: CanContinueIfAcquiresLock) => !notFree(v.lockToAquire) || (v.locks contains v.lockToAquire) + case _ => false + } + if (threadsNotBlocked.isEmpty) { + val waiting = threadStates.keys.map(_.toString).mkString(", ") + val s = if (threadStates.size > 1) "s" else "" + val are = if (threadStates.size > 1) "are" else "is" + val whoHasLock = threadStates.toSeq.flatMap { case (id, state) => state.locks.map(lock => (lock, id)) }.toMap + val reason = threadStates.collect { + case (id, state: CanContinueIfAcquiresLock) if !notFree(state.lockToAquire) => + s"Thread $id is waiting on lock ${state.lockToAquire} held by thread ${whoHasLock(state.lockToAquire)}" + }.mkString("\n") + throw new Exception(s"Deadlock: Thread$s $waiting are interlocked. Indeed:\n$reason") + } else if (threadsNotBlocked.size == 1) { // Do not consume the schedule if only one thread can execute. + Some(threadsNotBlocked(0)) + } else { + val next = schedule.indexWhere(t => threadsNotBlocked.exists { case (id, state) => id == t }) + if (next != -1) { + //println(s"$threadId: schedule is $schedule, next chosen is ${schedule(next)}") + val chosenOne = schedule(next) // TODO: Make schedule a mutable list. + schedule = schedule.take(next) ++ schedule.drop(next + 1) + Some((chosenOne, threadStates(chosenOne))) + } else { + threadPreference = (threadPreference + 1) % threadsNotBlocked.size + val chosenOne = threadsNotBlocked(threadPreference) // Maybe another strategy + Some(chosenOne) + //threadsNotBlocked.indexOf(threadId) >= 0 + /* + val tnb = threadsNotBlocked.map(_._1).mkString(",") + val s = if (schedule.isEmpty) "empty" else schedule.mkString(",") + val only = if (schedule.isEmpty) "" else " only" + throw new Exception(s"The schedule is $s but$only threads ${tnb} can continue")*/ + } + } + } + } else canContinue + } + + /** + * This will be called before a schedulable operation begins. + * This should not use synchronized + */ + var numThreadsWaiting = new AtomicInteger(0) + //var waitingForDecision = Map[Int, Option[Int]]() // Mapping from thread ids to a number indicating who is going to make the choice. + var canContinue: Option[(Int, ThreadState)] = None // The result of the decision thread Id of the thread authorized to continue. + private def waitForTurn = { + synchronized { + if (numThreadsWaiting.incrementAndGet() == threadStates.size) { + canContinue = decide() + notifyAll() + } + //waitingForDecision(threadId) = Some(numThreadsWaiting) + //println(s"$threadId Entering waiting with ticket number $numThreadsWaiting/${waitingForDecision.size}") + while (!canProceed()) wait() + } + numThreadsWaiting.decrementAndGet() + } + + /** + * To be invoked when a thread is about to complete + */ + private def removeFromSchedule(fakeid: Int) = synchronized { + //println(s"$fakeid: I'm taking a decision because I finished") + schedule = schedule.filterNot(_ == fakeid) + threadStates -= fakeid + if (numThreadsWaiting.get() == threadStates.size) { + canContinue = decide() + notifyAll() + } + } + + def getOperationLog() = opLog +} diff --git a/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/Stats.scala b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/Stats.scala new file mode 100644 index 0000000000000000000000000000000000000000..e82c09813f44c824cdfa55d54d8dd00acb584b48 --- /dev/null +++ b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/Stats.scala @@ -0,0 +1,23 @@ +package m15 +package instrumentation + +import java.lang.management._ + +/** + * A collection of methods that can be used to collect run-time statistics about Leon programs. + * This is mostly used to test the resources properties of Leon programs + */ +object Stats { + def timed[T](code: => T)(cont: Long => Unit): T = { + var t1 = System.currentTimeMillis() + val r = code + cont((System.currentTimeMillis() - t1)) + r + } + + def withTime[T](code: => T): (T, Long) = { + var t1 = System.currentTimeMillis() + val r = code + (r, (System.currentTimeMillis() - t1)) + } +} diff --git a/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/TestHelper.scala b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/TestHelper.scala new file mode 100644 index 0000000000000000000000000000000000000000..5f863382c87cb808f70a3ed4aacabec1496f15c4 --- /dev/null +++ b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/TestHelper.scala @@ -0,0 +1,125 @@ +package m15 +package instrumentation + +import scala.util.Random +import scala.collection.mutable.{Map => MutableMap} + +import Stats._ + +object TestHelper { + val noOfSchedules = 10000 // set this to 100k during deployment + val readWritesPerThread = 20 // maximum number of read/writes possible in one thread + val contextSwitchBound = 10 + val testTimeout = 240 // the total time out for a test in seconds + val schedTimeout = 15 // the total time out for execution of a schedule in secs + + // Helpers + /*def testManySchedules(op1: => Any): Unit = testManySchedules(List(() => op1)) + def testManySchedules(op1: => Any, op2: => Any): Unit = testManySchedules(List(() => op1, () => op2)) + def testManySchedules(op1: => Any, op2: => Any, op3: => Any): Unit = testManySchedules(List(() => op1, () => op2, () => op3)) + def testManySchedules(op1: => Any, op2: => Any, op3: => Any, op4: => Any): Unit = testManySchedules(List(() => op1, () => op2, () => op3, () => op4))*/ + + def testSequential[T](ops: Scheduler => Any)(assertions: T => (Boolean, String)) = + testManySchedules(1, + (sched: Scheduler) => { + (List(() => ops(sched)), + (res: List[Any]) => assertions(res.head.asInstanceOf[T])) + }) + + /** + * @numThreads number of threads + * @ops operations to be executed, one per thread + * @assertion as condition that will executed after all threads have completed (without exceptions) + * the arguments are the results of the threads + */ + def testManySchedules(numThreads: Int, + ops: Scheduler => + (List[() => Any], // Threads + List[Any] => (Boolean, String)) // Assertion + ) = { + var timeout = testTimeout * 1000L + val threadIds = (1 to numThreads) + //(1 to scheduleLength).flatMap(_ => threadIds).toList.permutations.take(noOfSchedules).foreach { + val schedules = (new ScheduleGenerator(numThreads)).schedules() + var schedsExplored = 0 + schedules.takeWhile(_ => schedsExplored <= noOfSchedules && timeout > 0).foreach { + //case _ if timeout <= 0 => // break + case schedule => + schedsExplored += 1 + val schedr = new Scheduler(schedule) + //println("Exploring Sched: "+schedule) + val (threadOps, assertion) = ops(schedr) + if (threadOps.size != numThreads) + throw new IllegalStateException(s"Number of threads: $numThreads, do not match operations of threads: $threadOps") + timed { schedr.runInParallel(schedTimeout * 1000, threadOps) } { t => timeout -= t } match { + case Timeout(msg) => + throw new java.lang.AssertionError("assertion failed\n"+"The schedule took too long to complete. A possible deadlock! \n"+msg) + case Except(msg, stkTrace) => + val traceStr = "Thread Stack trace: \n"+stkTrace.map(" at "+_.toString).mkString("\n") + throw new java.lang.AssertionError("assertion failed\n"+msg+"\n"+traceStr) + case RetVal(threadRes) => + // check the assertion + val (success, custom_msg) = assertion(threadRes) + if (!success) { + val msg = "The following schedule resulted in wrong results: \n" + custom_msg + "\n" + schedr.getOperationLog().mkString("\n") + throw new java.lang.AssertionError("Assertion failed: "+msg) + } + } + } + if (timeout <= 0) { + throw new java.lang.AssertionError("Test took too long to complete! Cannot check all schedules as your code is too slow!") + } + } + + /** + * A schedule generator that is based on the context bound + */ + class ScheduleGenerator(numThreads: Int) { + val scheduleLength = readWritesPerThread * numThreads + val rands = (1 to scheduleLength).map(i => new Random(0xcafe * i)) // random numbers for choosing a thread at each position + def schedules(): LazyList[List[Int]] = { + var contextSwitches = 0 + var contexts = List[Int]() // a stack of thread ids in the order of context-switches + val remainingOps = MutableMap[Int, Int]() + remainingOps ++= (1 to numThreads).map(i => (i, readWritesPerThread)) // num ops remaining in each thread + val liveThreads = (1 to numThreads).toSeq.toBuffer + + /** + * Updates remainingOps and liveThreads once a thread is chosen for a position in the schedule + */ + def updateState(tid: Int): Unit = { + val remOps = remainingOps(tid) + if (remOps == 0) { + liveThreads -= tid + } else { + remainingOps += (tid -> (remOps - 1)) + } + } + val schedule = rands.foldLeft(List[Int]()) { + case (acc, r) if contextSwitches < contextSwitchBound => + val tid = liveThreads(r.nextInt(liveThreads.size)) + contexts match { + case prev :: tail if prev != tid => // we have a new context switch here + contexts +:= tid + contextSwitches += 1 + case prev :: tail => + case _ => // init case + contexts +:= tid + } + updateState(tid) + acc :+ tid + case (acc, _) => // here context-bound has been reached so complete the schedule without any more context switches + if (!contexts.isEmpty) { + contexts = contexts.dropWhile(remainingOps(_) == 0) + } + val tid = contexts match { + case top :: tail => top + case _ => liveThreads(0) // here, there has to be threads that have not even started + } + updateState(tid) + acc :+ tid + } + schedule #:: schedules() + } + } +} diff --git a/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/TestUtils.scala b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/TestUtils.scala new file mode 100644 index 0000000000000000000000000000000000000000..3f4afe8845bff9e2608f3b2e3b92a8739ad238cc --- /dev/null +++ b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/TestUtils.scala @@ -0,0 +1,20 @@ +package m15 +package instrumentation + +import scala.concurrent._ +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global + +object TestUtils { + def failsOrTimesOut[T](action: => T): Boolean = { + val asyncAction = Future { + action + } + try { + Await.result(asyncAction, 2000.millisecond) + } catch { + case _: Throwable => return true + } + return false + } +} diff --git a/previous-exams/2021-midterm-solutions/m2.md b/previous-exams/2021-midterm/m2.md similarity index 95% rename from previous-exams/2021-midterm-solutions/m2.md rename to previous-exams/2021-midterm/m2.md index 47b097671c167b9e5f677e622ac8ce3572b09a09..fb3fdb6cdbf1b3aa21b4220d71f966caaec9b9bf 100644 --- a/previous-exams/2021-midterm-solutions/m2.md +++ b/previous-exams/2021-midterm/m2.md @@ -1,9 +1,3 @@ -Use the following commands to make a fresh clone of your repository: - -``` -git clone -b m2 git@gitlab.epfl.ch:lamp/student-repositories-s21/cs206-GASPAR.git m2 -``` - ## Useful links * [A guide to the Scala parallel collections](https://docs.scala-lang.org/overviews/parallel-collections/overview.html) diff --git a/previous-exams/2021-midterm/m2/.gitignore b/previous-exams/2021-midterm/m2/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..40937dc9b192820d0ede18efd3c7e6442a083b17 --- /dev/null +++ b/previous-exams/2021-midterm/m2/.gitignore @@ -0,0 +1,22 @@ +# General +*.DS_Store +*.swp +*~ + +# Dotty +*.class +*.tasty +*.hasTasty + +# sbt +target/ + +# IDE +.bsp +.bloop +.metals +.vscode + +# datasets +stackoverflow-grading.csv +wikipedia-grading.dat diff --git a/previous-exams/2021-midterm/m2/assignment.sbt b/previous-exams/2021-midterm/m2/assignment.sbt new file mode 100644 index 0000000000000000000000000000000000000000..da7eb3c8347293a18da0025fcd6060d8f8f7cc11 --- /dev/null +++ b/previous-exams/2021-midterm/m2/assignment.sbt @@ -0,0 +1,2 @@ +// Student tasks (i.e. submit, packageSubmission) +enablePlugins(StudentTasks) diff --git a/previous-exams/2021-midterm/m2/build.sbt b/previous-exams/2021-midterm/m2/build.sbt new file mode 100644 index 0000000000000000000000000000000000000000..4a68d9e22fc13fac03309e5751f98bf7dd08349e --- /dev/null +++ b/previous-exams/2021-midterm/m2/build.sbt @@ -0,0 +1,12 @@ +course := "midterm" +assignment := "m2" +scalaVersion := "3.0.0-RC1" +scalacOptions ++= Seq("-language:implicitConversions", "-deprecation") + +libraryDependencies += "org.scalameta" %% "munit" % "0.7.22" + +val MUnitFramework = new TestFramework("munit.Framework") +testFrameworks += MUnitFramework +// Decode Scala names +testOptions += Tests.Argument(MUnitFramework, "-s") +testSuite := "m2.M2Suite" diff --git a/previous-exams/2021-midterm/m2/grading-tests.jar b/previous-exams/2021-midterm/m2/grading-tests.jar new file mode 100644 index 0000000000000000000000000000000000000000..0378ba0b46a3d1a19bbb73a2dbdf4c9b77b8cb83 Binary files /dev/null and b/previous-exams/2021-midterm/m2/grading-tests.jar differ diff --git a/previous-exams/2021-midterm/m2/project/FilteringReporterPlugin.scala b/previous-exams/2021-midterm/m2/project/FilteringReporterPlugin.scala new file mode 100644 index 0000000000000000000000000000000000000000..2e4fd9a4d998698cd52643344b33a5e719dd7971 --- /dev/null +++ b/previous-exams/2021-midterm/m2/project/FilteringReporterPlugin.scala @@ -0,0 +1,31 @@ +package sbt // To access the private[sbt] compilerReporter key +package filteringReporterPlugin + +import Keys._ +import ch.epfl.lamp._ + +object FilteringReporterPlugin extends AutoPlugin { + override lazy val projectSettings = Seq( + // Turn off warning coming from scalameter that we cannot fix without changing scalameter + compilerReporter in (Compile, compile) ~= { reporter => new FilteringReporter(reporter) } + ) +} + +class FilteringReporter(reporter: xsbti.Reporter) extends xsbti.Reporter { + + def reset(): Unit = reporter.reset() + def hasErrors: Boolean = reporter.hasErrors + def hasWarnings: Boolean = reporter.hasWarnings + def printSummary(): Unit = reporter.printSummary() + def problems: Array[xsbti.Problem] = reporter.problems + + def log(problem: xsbti.Problem): Unit = { + if (!problem.message.contains("An existential type that came from a Scala-2 classfile cannot be")) + reporter.log(problem) + } + + def comment(pos: xsbti.Position, msg: String): Unit = + reporter.comment(pos, msg) + + override def toString = s"CollectingReporter($reporter)" +} diff --git a/previous-exams/2021-midterm/m2/project/MOOCSettings.scala b/previous-exams/2021-midterm/m2/project/MOOCSettings.scala new file mode 100644 index 0000000000000000000000000000000000000000..1c40443a53085d23fadb134f4e1a505c32231f1d --- /dev/null +++ b/previous-exams/2021-midterm/m2/project/MOOCSettings.scala @@ -0,0 +1,49 @@ +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 { + + override def requires = super.requires && filteringReporterPlugin.FilteringReporterPlugin + + 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") + .withRank(KeyRanks.Invisible) + // 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/previous-exams/2021-midterm/m2/project/StudentTasks.scala b/previous-exams/2021-midterm/m2/project/StudentTasks.scala new file mode 100644 index 0000000000000000000000000000000000000000..c4669afe82dd2b45651f94dcad9e736f29d21432 --- /dev/null +++ b/previous-exams/2021-midterm/m2/project/StudentTasks.scala @@ -0,0 +1,303 @@ +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") + + lazy val Grading = config("grading") extend(Runtime) + } + + + import autoImport._ + import MOOCSettings.autoImport._ + + override lazy val projectSettings = Seq( + packageSubmissionSetting, + fork := true, + connectInput in run := true, + outputStrategy := Some(StdoutOutput), + ) ++ + packageSubmissionZipSettings ++ + inConfig(Grading)(Defaults.testSettings ++ Seq( + unmanagedJars += file("grading-tests.jar"), + + definedTests := (definedTests in Test).value, + internalDependencyClasspath := (internalDependencyClasspath in Test).value + )) + + + /** ********************************************************** + * 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, None) + 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("<arg>").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 <email-address> <submit-token> + | + |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/previous-exams/2021-midterm/m2/project/build.properties b/previous-exams/2021-midterm/m2/project/build.properties new file mode 100644 index 0000000000000000000000000000000000000000..0b2e09c5ac99bd3de91b2b139b94301c2b6e26f3 --- /dev/null +++ b/previous-exams/2021-midterm/m2/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.4.7 diff --git a/previous-exams/2021-midterm/m2/project/buildSettings.sbt b/previous-exams/2021-midterm/m2/project/buildSettings.sbt new file mode 100644 index 0000000000000000000000000000000000000000..8fac702aaf3f3c4ede79691c7b4e4a52f26f3f47 --- /dev/null +++ b/previous-exams/2021-midterm/m2/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/previous-exams/2021-midterm/m2/project/plugins.sbt b/previous-exams/2021-midterm/m2/project/plugins.sbt new file mode 100644 index 0000000000000000000000000000000000000000..fb7dbe068109e7f35c13b2762b865c7eec1979f3 --- /dev/null +++ b/previous-exams/2021-midterm/m2/project/plugins.sbt @@ -0,0 +1,3 @@ +// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3") diff --git a/previous-exams/2021-midterm/m2/src/main/scala/m2/Lib.scala b/previous-exams/2021-midterm/m2/src/main/scala/m2/Lib.scala new file mode 100644 index 0000000000000000000000000000000000000000..9f1aff59d87f86ee2092ec49479f388b4054a643 --- /dev/null +++ b/previous-exams/2021-midterm/m2/src/main/scala/m2/Lib.scala @@ -0,0 +1,60 @@ +package m2 + +//////////////////////////////////////// +// NO NEED TO MODIFY THIS SOURCE FILE // +//////////////////////////////////////// + +trait Lib { + + /** If an array has `n` elements and `n < THRESHOLD`, then it should be processed sequentially */ + final val THRESHOLD: Int = 33 + + /** Compute the two values in parallel + * + * Note: Most tests just compute those two sequentially to make any bug simpler to debug + */ + def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) + + /** A limited array. It only contains the required operations for this exercise. */ + trait Arr[T] { + /** Get the i-th element of the array (0-based) */ + def apply(i: Int): T + /** Update the i-th element of the array with the given value (0-based) */ + def update(i: Int, x: T): Unit + /** Number of elements in this array */ + def length: Int + /** Create a copy of this array without the first element */ + def tail: Arr[T] + /** Create a copy of this array by mapping all the elements with the given function */ + def map[U](f: T => U): Arr[U] + } + + object Arr { + /** Create an array with the given elements */ + def apply[T](xs: T*): Arr[T] = { + val arr: Arr[T] = Arr.ofLength(xs.length) + for i <- 0 until xs.length do arr(i) = xs(i) + arr + } + + /** Create an array with the given length. All elements are initialized to `null`. */ + def ofLength[T](n: Int): Arr[T] = + newArrOfLength(n) + + } + + /** Create an array with the given length. All elements are initialized to `null`. */ + def newArrOfLength[T](n: Int): Arr[T] + + /** A number representing `radicand^(1.0/degree)` */ + case class Root(radicand: Int, degree: Int) { + def toDouble: Double = scala.math.pow(radicand, 1.0/degree) + } + + /** Tree result of an upsweep operation. Specialized for `Root` results. */ + trait TreeRes { val res: Root } + /** Leaf result of an upsweep operation. Specialized for `Root` results. */ + case class Leaf(from: Int, to: Int, res: Root) extends TreeRes + /** Tree node result of an upsweep operation. Specialized for `Root` results. */ + case class Node(left: TreeRes, res: Root, right: TreeRes) extends TreeRes +} diff --git a/previous-exams/2021-midterm/m2/src/main/scala/m2/M2.scala b/previous-exams/2021-midterm/m2/src/main/scala/m2/M2.scala new file mode 100644 index 0000000000000000000000000000000000000000..0fcaa856b7ce4c7615d1f3b3df58c330322a4c46 --- /dev/null +++ b/previous-exams/2021-midterm/m2/src/main/scala/m2/M2.scala @@ -0,0 +1,89 @@ +package m2 + + +trait M2 extends Lib { + // Functions and classes of Lib can be used in here + + /** Compute the rolling geometric mean of an array. + * + * For an array `arr = Arr(x1, x2, x3, ..., xn)` the result is + * `Arr(math.pow(x1, 1), math.pow((x1 + x2), 1.0/2), math.pow((x1 + x2 + x3), 1.0/3), ..., math.pow((x1 + x2 + x3 + ... + xn), 1.0/n))` + */ + def rollingGeoMeanParallel(arr: Arr[Int]): Arr[Double] = { + if (arr.length == 0) return Arr.ofLength(0) + // TASK 1: Add missing parallelization in `upsweep` and `downsweep`. + // You should use the `parallel` method. + // You should use the sequential version if the number of elements is lower than THRESHOLD. + // TASK 2a: Pass `arr` to `upsweep` and `downsweep` instead of `tmp`. + // You will need to change some signatures and update the code appropriately. + // Remove the definition of `tmp` + // TASK 2b: Change the type of the array `out` from `Root` to `Double` + // You will need to change some signatures and update the code appropriately. + // Remove the call `.map(root => root.toDouble)`. + // TASK 3: Remove the call to `.tail`. + // Update the update the code appropriately. + + val tmp: Arr[Root] = arr.map(x => Root(x, 1)) + val out: Arr[Root] = Arr.ofLength(arr.length + 1) + val tree = upsweep(tmp, 0, arr.length) + downsweep(tmp, Root(1, 0), tree, out) + out(0) = Root(1, 0) + out.map(root => root.toDouble).tail + + // IDEAL SOLUTION + // val out = Arr.ofLength(arr.length) + // val tree = upsweep(arr, 0, arr.length) + // downsweep(arr, Root(1, 0), tree, out) + // out + } + + def scanOp(acc: Root, x: Root) = // No need to modify this method + Root(acc.radicand * x.radicand, acc.degree + x.degree) + + def upsweep(input: Arr[Root], from: Int, to: Int): TreeRes = { + if (to - from < 2) + Leaf(from, to, reduceSequential(input, from + 1, to, input(from))) + else { + val mid = from + (to - from) / 2 + val (tL, tR) = ( + upsweep(input, from, mid), + upsweep(input, mid, to) + ) + Node(tL, scanOp(tL.res, tR.res), tR) + } + } + + def downsweep(input: Arr[Root], a0: Root, tree: TreeRes, output: Arr[Root]): Unit = { + tree match { + case Node(left, _, right) => + ( + downsweep(input, a0, left, output), + downsweep(input, scanOp(a0, left.res), right, output) + ) + case Leaf(from, to, _) => + downsweepSequential(input, from, to, a0, output) + } + } + + def downsweepSequential(input: Arr[Root], from: Int, to: Int, a0: Root, output: Arr[Root]): Unit = { + if (from < to) { + var i = from + var a = a0 + while (i < to) { + a = scanOp(a, input(i)) + i = i + 1 + output(i) = a + } + } + } + + def reduceSequential(input: Arr[Root], from: Int, to: Int, a0: Root): Root = { + var a = a0 + var i = from + while (i < to) { + a = scanOp(a, input(i)) + i = i + 1 + } + a + } +} diff --git a/previous-exams/2021-midterm/m2/src/test/scala/m2/M2Suite.scala b/previous-exams/2021-midterm/m2/src/test/scala/m2/M2Suite.scala new file mode 100644 index 0000000000000000000000000000000000000000..a5b3a4461ea7db8a51a48de5ee0392971a61a3b6 --- /dev/null +++ b/previous-exams/2021-midterm/m2/src/test/scala/m2/M2Suite.scala @@ -0,0 +1,175 @@ +package m2 + +class M2Suite extends munit.FunSuite { + + test("Rolling geometric mean result test (5pts)") { + RollingGeoMeanBasicLogicTest.basicTests() + RollingGeoMeanBasicLogicTest.normalTests() + RollingGeoMeanBasicLogicTest.largeTests() + } + + test("[TASK 1] Rolling geometric mean parallelism test (30pts)") { + RollingGeoMeanCallsToParallel.parallelismTest() + RollingGeoMeanParallel.basicTests() + RollingGeoMeanParallel.normalTests() + RollingGeoMeanParallel.largeTests() + } + + test("[TASK 2] Rolling geometric mean no `map` test (35pts)") { + RollingGeoMeanNoMap.basicTests() + RollingGeoMeanNoMap.normalTests() + RollingGeoMeanNoMap.largeTests() + } + + test("[TASK 3] Rolling geometric mean no `tail` test (30pts)") { + RollingGeoMeanNoTail.basicTests() + RollingGeoMeanNoTail.normalTests() + RollingGeoMeanNoTail.largeTests() + } + + + object RollingGeoMeanBasicLogicTest extends M2 with LibImpl with RollingGeoMeanTest { + def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = (op1, op2) + def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl(arr) + } + + object RollingGeoMeanCallsToParallel extends M2 with LibImpl with RollingGeoMeanTest { + private var count = 0 + def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = + count += 1 + (op1, op2) + + def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl(arr) + + def parallelismTest() = { + assertParallelCount(Arr(), 0) + assertParallelCount(Arr(1), 0) + assertParallelCount(Arr(1, 2, 3, 4), 0) + assertParallelCount(Arr(Array.tabulate(16)(identity): _*), 0) + assertParallelCount(Arr(Array.tabulate(32)(identity): _*), 0) + + assertParallelCount(Arr(Array.tabulate(33)(identity): _*), 2) + assertParallelCount(Arr(Array.tabulate(64)(identity): _*), 2) + assertParallelCount(Arr(Array.tabulate(128)(identity): _*), 6) + assertParallelCount(Arr(Array.tabulate(256)(identity): _*), 14) + assertParallelCount(Arr(Array.tabulate(1000)(identity): _*), 62) + assertParallelCount(Arr(Array.tabulate(1024)(identity): _*), 62) + } + + def assertParallelCount(arr: Arr[Int], expected: Int): Unit = { + try { + count = 0 + rollingGeoMeanParallel(arr) + assert(count == expected, { + val extra = if (expected == 0) "" else s" ${expected/2} for the `upsweep` and ${expected/2} for the `downsweep`" + s"\n$arr\n\nERROR: Expected $expected instead of $count calls to `parallel(...)` for an array of ${arr.length} elements. Current parallel threshold is $THRESHOLD.$extra" + }) + } finally { + count = 0 + } + } + + } + + object RollingGeoMeanNoMap extends M2 with LibImpl with RollingGeoMeanTest { + def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = (op1, op2) + def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl[T](arr) { + override def map[U](f: T => U): Arr[U] = throw Exception("Should not call Arr.map") + } + } + + object RollingGeoMeanNoTail extends M2 with LibImpl with RollingGeoMeanTest { + def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = (op1, op2) + def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl[T](arr) { + override def tail: Arr[T] = throw Exception("Should not call Arr.tail") + } + } + + object RollingGeoMeanParallel extends M2 with LibImpl with RollingGeoMeanTest { + import scala.concurrent.duration._ + val TIMEOUT = Duration(10, SECONDS) + def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = { + import concurrent.ExecutionContext.Implicits.global + import scala.concurrent._ + Await.result(Future(op1).zip(Future(op2)), TIMEOUT) // FIXME not timing-out + } + def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl(arr) + } + + trait LibImpl extends Lib { + + def newArrFrom[T](arr: Array[AnyRef]): Arr[T] + + def newArrOfLength[T](n: Int): Arr[T] = + newArrFrom(new Array(n)) + + class ArrImpl[T](val arr: Array[AnyRef]) extends Arr[T]: + def apply(i: Int): T = + arr(i).asInstanceOf[T] + def update(i: Int, x: T): Unit = + arr(i) = x.asInstanceOf[AnyRef] + def length: Int = + arr.length + def map[U](f: T => U): Arr[U] = + newArrFrom(arr.map(f.asInstanceOf[AnyRef => AnyRef])) + def tail: Arr[T] = + newArrFrom(arr.tail) + override def toString: String = + arr.mkString("Arr(", ", ", ")") + override def equals(that: Any): Boolean = + that match + case that: ArrImpl[_] => Array.equals(arr, that.arr) + case _ => false + } + + trait RollingGeoMeanTest extends M2 { + + def tabulate[T](n: Int)(f: Int => T): Arr[T] = + val arr = Arr.ofLength[T](n) + for i <- 0 until n do + arr(i) = f(i) + arr + + def asSeq(arr: Arr[Double]) = + val array = new Array[Double](arr.length) + for i <- 0 to (arr.length - 1) do + array(i) = arr(i) + array.toSeq + + def scanOp_(acc: Root, x: Root) = + Root(acc.radicand * x.radicand, acc.degree + x.degree) + + def result(ds: Seq[Int]): Arr[Double] = + Arr(ds.map(x => Root(x, 1)).scan(Root(1, 0))(scanOp_).tail.map(_.toDouble): _*) + + def check(input: Seq[Int]) = + assertEquals( + // .toString calls are a terrible kludge so that NaNs compare equal to eachother... + asSeq(rollingGeoMeanParallel(Arr(input: _*))).map(_.toString), + asSeq(result(input)).map(_.toString) + ) + + def basicTests() = { + check(Seq()) + check(Seq(1)) + check(Seq(1, 2, 3, 4)) + check(Seq(4, 4, 4, 4)) + } + + def normalTests() = { + check(Seq.tabulate(64)(identity)) + check(Seq(4, 4, 4, 4)) + check(Seq(4, 8, 6, 4)) + check(Seq(4, 3, 2, 1)) + check(Seq.tabulate(64)(identity).reverse) + check(Seq.tabulate(128)(i => 128 - 2*i).reverse) + } + + def largeTests() = { + check(Seq.tabulate(500)(identity)) + check(Seq.tabulate(512)(identity)) + check(Seq.tabulate(1_000)(identity)) + check(Seq.tabulate(10_000)(identity)) + } + } +} diff --git a/previous-exams/2021-midterm-solutions/m20.md b/previous-exams/2021-midterm/m20.md similarity index 95% rename from previous-exams/2021-midterm-solutions/m20.md rename to previous-exams/2021-midterm/m20.md index 59f6c09d903ebea6a9c020a96a176032387c186a..78f1d3f2b5e0fa4cb7dd4da8f002402974dd2c39 100644 --- a/previous-exams/2021-midterm-solutions/m20.md +++ b/previous-exams/2021-midterm/m20.md @@ -1,9 +1,3 @@ -Use the following commands to make a fresh clone of your repository: - -``` -git clone -b m20 git@gitlab.epfl.ch:lamp/student-repositories-s21/cs206-GASPAR.git m20 -``` - ## Useful links * [A guide to the Scala parallel collections](https://docs.scala-lang.org/overviews/parallel-collections/overview.html) diff --git a/previous-exams/2021-midterm/m20/.gitignore b/previous-exams/2021-midterm/m20/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..40937dc9b192820d0ede18efd3c7e6442a083b17 --- /dev/null +++ b/previous-exams/2021-midterm/m20/.gitignore @@ -0,0 +1,22 @@ +# General +*.DS_Store +*.swp +*~ + +# Dotty +*.class +*.tasty +*.hasTasty + +# sbt +target/ + +# IDE +.bsp +.bloop +.metals +.vscode + +# datasets +stackoverflow-grading.csv +wikipedia-grading.dat diff --git a/previous-exams/2021-midterm/m20/assignment.sbt b/previous-exams/2021-midterm/m20/assignment.sbt new file mode 100644 index 0000000000000000000000000000000000000000..da7eb3c8347293a18da0025fcd6060d8f8f7cc11 --- /dev/null +++ b/previous-exams/2021-midterm/m20/assignment.sbt @@ -0,0 +1,2 @@ +// Student tasks (i.e. submit, packageSubmission) +enablePlugins(StudentTasks) diff --git a/previous-exams/2021-midterm/m20/build.sbt b/previous-exams/2021-midterm/m20/build.sbt new file mode 100644 index 0000000000000000000000000000000000000000..8cd8c7a0320af71ee8405888adcaa8556c98574f --- /dev/null +++ b/previous-exams/2021-midterm/m20/build.sbt @@ -0,0 +1,12 @@ +course := "midterm" +assignment := "m20" +scalaVersion := "3.0.0-RC1" +scalacOptions ++= Seq("-language:implicitConversions", "-deprecation") + +libraryDependencies += "org.scalameta" %% "munit" % "0.7.22" + +val MUnitFramework = new TestFramework("munit.Framework") +testFrameworks += MUnitFramework +// Decode Scala names +testOptions += Tests.Argument(MUnitFramework, "-s") +testSuite := "m20.M20Suite" diff --git a/previous-exams/2021-midterm/m20/grading-tests.jar b/previous-exams/2021-midterm/m20/grading-tests.jar new file mode 100644 index 0000000000000000000000000000000000000000..89112fdcd241e77ac54effcbaeb3c631caf37fa0 Binary files /dev/null and b/previous-exams/2021-midterm/m20/grading-tests.jar differ diff --git a/previous-exams/2021-midterm/m20/project/FilteringReporterPlugin.scala b/previous-exams/2021-midterm/m20/project/FilteringReporterPlugin.scala new file mode 100644 index 0000000000000000000000000000000000000000..2e4fd9a4d998698cd52643344b33a5e719dd7971 --- /dev/null +++ b/previous-exams/2021-midterm/m20/project/FilteringReporterPlugin.scala @@ -0,0 +1,31 @@ +package sbt // To access the private[sbt] compilerReporter key +package filteringReporterPlugin + +import Keys._ +import ch.epfl.lamp._ + +object FilteringReporterPlugin extends AutoPlugin { + override lazy val projectSettings = Seq( + // Turn off warning coming from scalameter that we cannot fix without changing scalameter + compilerReporter in (Compile, compile) ~= { reporter => new FilteringReporter(reporter) } + ) +} + +class FilteringReporter(reporter: xsbti.Reporter) extends xsbti.Reporter { + + def reset(): Unit = reporter.reset() + def hasErrors: Boolean = reporter.hasErrors + def hasWarnings: Boolean = reporter.hasWarnings + def printSummary(): Unit = reporter.printSummary() + def problems: Array[xsbti.Problem] = reporter.problems + + def log(problem: xsbti.Problem): Unit = { + if (!problem.message.contains("An existential type that came from a Scala-2 classfile cannot be")) + reporter.log(problem) + } + + def comment(pos: xsbti.Position, msg: String): Unit = + reporter.comment(pos, msg) + + override def toString = s"CollectingReporter($reporter)" +} diff --git a/previous-exams/2021-midterm/m20/project/MOOCSettings.scala b/previous-exams/2021-midterm/m20/project/MOOCSettings.scala new file mode 100644 index 0000000000000000000000000000000000000000..1c40443a53085d23fadb134f4e1a505c32231f1d --- /dev/null +++ b/previous-exams/2021-midterm/m20/project/MOOCSettings.scala @@ -0,0 +1,49 @@ +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 { + + override def requires = super.requires && filteringReporterPlugin.FilteringReporterPlugin + + 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") + .withRank(KeyRanks.Invisible) + // 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/previous-exams/2021-midterm/m20/project/StudentTasks.scala b/previous-exams/2021-midterm/m20/project/StudentTasks.scala new file mode 100644 index 0000000000000000000000000000000000000000..c4669afe82dd2b45651f94dcad9e736f29d21432 --- /dev/null +++ b/previous-exams/2021-midterm/m20/project/StudentTasks.scala @@ -0,0 +1,303 @@ +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") + + lazy val Grading = config("grading") extend(Runtime) + } + + + import autoImport._ + import MOOCSettings.autoImport._ + + override lazy val projectSettings = Seq( + packageSubmissionSetting, + fork := true, + connectInput in run := true, + outputStrategy := Some(StdoutOutput), + ) ++ + packageSubmissionZipSettings ++ + inConfig(Grading)(Defaults.testSettings ++ Seq( + unmanagedJars += file("grading-tests.jar"), + + definedTests := (definedTests in Test).value, + internalDependencyClasspath := (internalDependencyClasspath in Test).value + )) + + + /** ********************************************************** + * 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, None) + 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("<arg>").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 <email-address> <submit-token> + | + |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/previous-exams/2021-midterm/m20/project/build.properties b/previous-exams/2021-midterm/m20/project/build.properties new file mode 100644 index 0000000000000000000000000000000000000000..0b2e09c5ac99bd3de91b2b139b94301c2b6e26f3 --- /dev/null +++ b/previous-exams/2021-midterm/m20/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.4.7 diff --git a/previous-exams/2021-midterm/m20/project/buildSettings.sbt b/previous-exams/2021-midterm/m20/project/buildSettings.sbt new file mode 100644 index 0000000000000000000000000000000000000000..8fac702aaf3f3c4ede79691c7b4e4a52f26f3f47 --- /dev/null +++ b/previous-exams/2021-midterm/m20/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/previous-exams/2021-midterm/m20/project/plugins.sbt b/previous-exams/2021-midterm/m20/project/plugins.sbt new file mode 100644 index 0000000000000000000000000000000000000000..fb7dbe068109e7f35c13b2762b865c7eec1979f3 --- /dev/null +++ b/previous-exams/2021-midterm/m20/project/plugins.sbt @@ -0,0 +1,3 @@ +// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3") diff --git a/previous-exams/2021-midterm/m20/src/main/scala/m20/MultiWriterSeqCount.scala b/previous-exams/2021-midterm/m20/src/main/scala/m20/MultiWriterSeqCount.scala new file mode 100644 index 0000000000000000000000000000000000000000..7bb3a657410f5e706a9bf890b3e8d0483b445f45 --- /dev/null +++ b/previous-exams/2021-midterm/m20/src/main/scala/m20/MultiWriterSeqCount.scala @@ -0,0 +1,46 @@ +package m20 + +import instrumentation._ + +import scala.annotation.tailrec + +/** Multi-writer, multi-reader data structure containing a pair of integers. */ +class MultiWriterSeqCount extends Monitor: + /** Do not directly use this variable, use `generation`, `setGeneration` and + * `compareAndSetGeneration` instead. + */ + protected val myGeneration: AbstractAtomicVariable[Int] = new AtomicVariable(0) + protected def generation: Int = myGeneration.get + protected def setGeneration(newGeneration: Int): Unit = + myGeneration.set(newGeneration) + protected def compareAndSetGeneration(expected: Int, newValue: Int): Boolean = + myGeneration.compareAndSet(expected, newValue) + + /** Do not directly use this variable, use `x` and `setX` instead. */ + protected var myX: Int = 0 + protected def x: Int = myX + protected def setX(newX: Int): Unit = + myX = newX + + /** Do not directly use this variable, use `y` and `setY` instead. */ + protected var myY: Int = 0 + protected def y: Int = myY + protected def setY(newY: Int): Unit = + myY = newY + + /** Write new values into this data structure. + * This method is always safe to call. + * The implementation of this method is not allowed to call `synchronized`. + */ + final def write(newX: Int, newY: Int): Unit = ??? + + /** Copy the values previously written into this data structure into a tuple. + * This method is always safe to call. + * The implementation of this method is not allowed to call `synchronized`. + */ + final def copy(): (Int, Int) = + // You should be able to just copy-paste the implementation of `copy` you + // wrote in `SeqCount` here. + ??? + +end MultiWriterSeqCount diff --git a/previous-exams/2021-midterm/m20/src/main/scala/m20/SeqCount.scala b/previous-exams/2021-midterm/m20/src/main/scala/m20/SeqCount.scala new file mode 100644 index 0000000000000000000000000000000000000000..3003be40dd526ae89aa5ec93a7c2a6b7286d2d7e --- /dev/null +++ b/previous-exams/2021-midterm/m20/src/main/scala/m20/SeqCount.scala @@ -0,0 +1,42 @@ +package m20 + +import instrumentation._ + +import scala.annotation.tailrec + +/** Single-writer, multi-reader data structure containing a pair of integers. */ +class SeqCount extends Monitor: + /** Do not directly use this variable, use `generation` and `setGeneration` instead. */ + @volatile protected var myGeneration: Int = 0 + protected def generation: Int = myGeneration + protected def setGeneration(newGeneration: Int): Unit = + myGeneration = newGeneration + + /** Do not directly use this variable, use `x` and `setX` instead. */ + protected var myX: Int = 0 + protected def x: Int = myX + protected def setX(newX: Int): Unit = + myX = newX + + /** Do not directly use this variable, use `y` and `setY` instead. */ + protected var myY: Int = 0 + protected def y: Int = myY + protected def setY(newY: Int): Unit = + myY = newY + + /** Write new values into this data structure. + * This method must only be called from one thread at a time. + */ + final def write(newX: Int, newY: Int): Unit = + setGeneration(generation + 1) + setX(newX) + setY(newY) + setGeneration(generation + 1) + + /** Copy the values previously written into this data structure into a tuple. + * This method is always safe to call. + * The implementation of this method is not allowed to call `synchronized`. + */ + final def copy(): (Int, Int) = ??? + +end SeqCount diff --git a/previous-exams/2021-midterm/m20/src/main/scala/m20/instrumentation/AtomicVariable.scala b/previous-exams/2021-midterm/m20/src/main/scala/m20/instrumentation/AtomicVariable.scala new file mode 100644 index 0000000000000000000000000000000000000000..b96628365180b1e0c75d77f7adcdd9e56f718d3c --- /dev/null +++ b/previous-exams/2021-midterm/m20/src/main/scala/m20/instrumentation/AtomicVariable.scala @@ -0,0 +1,28 @@ +package m20.instrumentation + +import java.util.concurrent.atomic._ + +abstract class AbstractAtomicVariable[T] { + def get: T + def set(value: T): Unit + def compareAndSet(expect: T, newval: T) : Boolean +} + +class AtomicVariable[T](initial: T) extends AbstractAtomicVariable[T] { + + private val atomic = new AtomicReference[T](initial) + + override def get: T = atomic.get() + + override def set(value: T): Unit = atomic.set(value) + + override def compareAndSet(expected: T, newValue: T): Boolean = { + val current = atomic.get + if (current == expected) { + atomic.compareAndSet(current, newValue) + } + else { + false + } + } +} diff --git a/previous-exams/2021-midterm/m20/src/main/scala/m20/instrumentation/Monitor.scala b/previous-exams/2021-midterm/m20/src/main/scala/m20/instrumentation/Monitor.scala new file mode 100644 index 0000000000000000000000000000000000000000..bf844ace3980d4a1da1d59c4f80870c38c563dc6 --- /dev/null +++ b/previous-exams/2021-midterm/m20/src/main/scala/m20/instrumentation/Monitor.scala @@ -0,0 +1,23 @@ +package m20.instrumentation + +class Dummy + +trait Monitor { + implicit val dummy: Dummy = new Dummy + + def wait()(implicit i: Dummy) = waitDefault() + + def synchronized[T](e: => T)(implicit i: Dummy) = synchronizedDefault(e) + + def notify()(implicit i: Dummy) = notifyDefault() + + def notifyAll()(implicit i: Dummy) = notifyAllDefault() + + private val lock = new AnyRef + + // Can be overriden. + def waitDefault(): Unit = lock.wait() + def synchronizedDefault[T](toExecute: =>T): T = lock.synchronized(toExecute) + def notifyDefault(): Unit = lock.notify() + def notifyAllDefault(): Unit = lock.notifyAll() +} diff --git a/previous-exams/2021-midterm/m20/src/test/scala/m20/TestSuite.scala b/previous-exams/2021-midterm/m20/src/test/scala/m20/TestSuite.scala new file mode 100644 index 0000000000000000000000000000000000000000..14e707c78c31a466e107cabf565f8c1f2b8ddb75 --- /dev/null +++ b/previous-exams/2021-midterm/m20/src/test/scala/m20/TestSuite.scala @@ -0,0 +1,122 @@ +package m20 + +import scala.concurrent._ +import scala.concurrent.duration._ +import scala.collection.mutable.HashMap +import scala.util.Random +import instrumentation._ +import instrumentation.TestHelper._ +import instrumentation.TestUtils._ + +enum ThreadResult: + case WriteError(error: String) + case WriteSuccess + case Read(result: (Int, Int)) +import ThreadResult._ + +class M20Suite extends munit.FunSuite: + /** If at least one thread resulted in an error, + * return `(false, errorMessage)` otherwise return `(true, "")`. + */ + def processResults(results: List[ThreadResult]): (Boolean, String) = + val success = (true, "") + results.foldLeft(success) { + case (acc @ (false, _), _) => + // Report the first error found + acc + case (_, WriteError(error)) => + (false, error) + case (_, Read((x, y))) if x + 1 != y => + (false, s"Read ($x, $y) but expected y to be ${x + 1}") + case (_, _: Read | WriteSuccess) => + success + } + + def randomList(length: Int): List[Int] = + List.fill(length)(Random.nextInt) + + test("SeqCount: single-threaded write and copy (1 pts)") { + val sc = new SeqCount + randomList(100).lazyZip(randomList(100)).foreach { (x, y) => + sc.write(x, y) + assertEquals(sc.copy(), (x, y)) + } + } + + test("SeqCount: one write thread, two copy threads (4 pts)") { + testManySchedules(3, sched => + val sc = new SchedulableSeqCount(sched) + // Invariant in this test: y == x + 1 + sc.write(0, 1) + + val randomValues = randomList(length = 5) + + def writeThread(): ThreadResult = + randomValues.foldLeft(WriteSuccess) { + case (res: WriteError, _) => + // Report the first error found + res + case (_, i) => + sc.write(i, i + 1) + val writtenValues = (i, i + 1) + val readBack = sc.copy() + if writtenValues != readBack then + WriteError(s"Wrote $writtenValues but read back $readBack") + else + WriteSuccess + } + + def copyThread(): ThreadResult = + Read(sc.copy()) + + val threads = List( + () => writeThread(), + () => copyThread(), + () => copyThread() + ) + + (threads, results => processResults(results.asInstanceOf[List[ThreadResult]])) + ) + } + + test("MultiWriterSeqCount: single-threaded write and copy (1 pts)") { + val sc = new MultiWriterSeqCount + randomList(100).lazyZip(randomList(100)).foreach { (x, y) => + sc.write(x, y) + assertEquals(sc.copy(), (x, y)) + } + } + + test("MultiWriterSeqCount: two write threads, two copy threads (4 pts)") { + testManySchedules(4, sched => + val msc = new SchedulableMultiWriterSeqCount(sched) + // Invariant in this test: y == x + 1 + msc.write(0, 1) + + val randomValues = randomList(length = 5) + + def writeThread(): ThreadResult = + randomValues.foreach(i => msc.write(i, i + 1)) + // Unlke in the SeqCount test, we do not verify that we can read back + // the values we wrote, because the other writer thread might have + // overwritten them already. + WriteSuccess + + def copyThread(): ThreadResult = + Read(msc.copy()) + + val threads = List( + () => writeThread(), + () => writeThread(), + () => copyThread(), + () => copyThread() + ) + + (threads, results => processResults(results.asInstanceOf[List[ThreadResult]])) + ) + } + + import scala.concurrent.duration._ + override val munitTimeout = 200.seconds +end M20Suite + diff --git a/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/MockedMonitor.scala b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/MockedMonitor.scala new file mode 100644 index 0000000000000000000000000000000000000000..0b7e94fd37926fbc2c84b4a5ca8c7113a9b9d8fc --- /dev/null +++ b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/MockedMonitor.scala @@ -0,0 +1,72 @@ +package m20.instrumentation + +trait MockedMonitor extends Monitor { + def scheduler: Scheduler + + // Can be overriden. + override def waitDefault() = { + scheduler.log("wait") + scheduler updateThreadState Wait(this, scheduler.threadLocks.tail) + } + override def synchronizedDefault[T](toExecute: =>T): T = { + scheduler.log("synchronized check") + val prevLocks = scheduler.threadLocks + scheduler updateThreadState Sync(this, prevLocks) // If this belongs to prevLocks, should just continue. + scheduler.log("synchronized -> enter") + try { + toExecute + } finally { + scheduler updateThreadState Running(prevLocks) + scheduler.log("synchronized -> out") + } + } + override def notifyDefault() = { + scheduler mapOtherStates { + state => state match { + case Wait(lockToAquire, locks) if lockToAquire == this => SyncUnique(this, state.locks) + case e => e + } + } + scheduler.log("notify") + } + override def notifyAllDefault() = { + scheduler mapOtherStates { + state => state match { + case Wait(lockToAquire, locks) if lockToAquire == this => Sync(this, state.locks) + case SyncUnique(lockToAquire, locks) if lockToAquire == this => Sync(this, state.locks) + case e => e + } + } + scheduler.log("notifyAll") + } +} + +trait LockFreeMonitor extends Monitor { + override def waitDefault() = { + throw new Exception("Please use lock-free structures and do not use wait()") + } + override def synchronizedDefault[T](toExecute: =>T): T = { + throw new Exception("Please use lock-free structures and do not use synchronized()") + } + override def notifyDefault() = { + throw new Exception("Please use lock-free structures and do not use notify()") + } + override def notifyAllDefault() = { + throw new Exception("Please use lock-free structures and do not use notifyAll()") + } +} + + +abstract class ThreadState { + def locks: Seq[AnyRef] +} +trait CanContinueIfAcquiresLock extends ThreadState { + def lockToAquire: AnyRef +} +case object Start extends ThreadState { def locks: Seq[AnyRef] = Seq.empty } +case object End extends ThreadState { def locks: Seq[AnyRef] = Seq.empty } +case class Wait(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState +case class SyncUnique(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState with CanContinueIfAcquiresLock +case class Sync(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState with CanContinueIfAcquiresLock +case class Running(locks: Seq[AnyRef]) extends ThreadState +case class VariableReadWrite(locks: Seq[AnyRef]) extends ThreadState diff --git a/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/Scheduler.scala b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/Scheduler.scala new file mode 100644 index 0000000000000000000000000000000000000000..bdc09b50f0d8ab05e89a06654034c855f98e9b3d --- /dev/null +++ b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/Scheduler.scala @@ -0,0 +1,304 @@ +package m20.instrumentation + +import java.util.concurrent._; +import scala.concurrent.duration._ +import scala.collection.mutable._ +import Stats._ + +import java.util.concurrent.atomic.AtomicInteger + +sealed abstract class Result +case class RetVal(rets: List[Any]) extends Result +case class Except(msg: String, stackTrace: Array[StackTraceElement]) extends Result +case class Timeout(msg: String) extends Result + +/** + * A class that maintains schedule and a set of thread ids. + * The schedules are advanced after an operation of a SchedulableBuffer is performed. + * Note: the real schedule that is executed may deviate from the input schedule + * due to the adjustments that had to be made for locks + */ +class Scheduler(sched: List[Int]) { + val maxOps = 500 // a limit on the maximum number of operations the code is allowed to perform + + private var schedule = sched + private var numThreads = 0 + private val realToFakeThreadId = Map[Long, Int]() + private val opLog = ListBuffer[String]() // a mutable list (used for efficient concat) + private val threadStates = Map[Int, ThreadState]() + + /** + * Runs a set of operations in parallel as per the schedule. + * Each operation may consist of many primitive operations like reads or writes + * to shared data structure each of which should be executed using the function `exec`. + * @timeout in milliseconds + * @return true - all threads completed on time, false -some tests timed out. + */ + def runInParallel(timeout: Long, ops: List[() => Any]): Result = { + numThreads = ops.length + val threadRes = Array.fill(numThreads) { None: Any } + var exception: Option[Except] = None + val syncObject = new Object() + var completed = new AtomicInteger(0) + // create threads + val threads = ops.zipWithIndex.map { + case (op, i) => + new Thread(new Runnable() { + def run(): Unit = { + val fakeId = i + 1 + setThreadId(fakeId) + try { + updateThreadState(Start) + val res = op() + updateThreadState(End) + threadRes(i) = res + // notify the master thread if all threads have completed + if (completed.incrementAndGet() == ops.length) { + syncObject.synchronized { syncObject.notifyAll() } + } + } catch { + case e: Throwable if exception != None => // do nothing here and silently fail + case e: Throwable => + log(s"throw ${e.toString}") + exception = Some(Except(s"Thread $fakeId crashed on the following schedule: \n" + opLog.mkString("\n"), + e.getStackTrace)) + syncObject.synchronized { syncObject.notifyAll() } + //println(s"$fakeId: ${e.toString}") + //Runtime.getRuntime().halt(0) //exit the JVM and all running threads (no other way to kill other threads) + } + } + }) + } + // start all threads + threads.foreach(_.start()) + // wait for all threads to complete, or for an exception to be thrown, or for the time out to expire + var remTime = timeout + syncObject.synchronized { + timed { if(completed.get() != ops.length) syncObject.wait(timeout) } { time => remTime -= time } + } + if (exception.isDefined) { + exception.get + } else if (remTime <= 1) { // timeout ? using 1 instead of zero to allow for some errors + Timeout(opLog.mkString("\n")) + } else { + // every thing executed normally + RetVal(threadRes.toList) + } + } + + // Updates the state of the current thread + def updateThreadState(state: ThreadState): Unit = { + val tid = threadId + synchronized { + threadStates(tid) = state + } + state match { + case Sync(lockToAquire, locks) => + if (locks.indexOf(lockToAquire) < 0) waitForTurn else { + // Re-aqcuiring the same lock + updateThreadState(Running(lockToAquire +: locks)) + } + case Start => waitStart() + case End => removeFromSchedule(tid) + case Running(_) => + case _ => waitForTurn // Wait, SyncUnique, VariableReadWrite + } + } + + def waitStart(): Unit = { + //while (threadStates.size < numThreads) { + //Thread.sleep(1) + //} + synchronized { + if (threadStates.size < numThreads) { + wait() + } else { + notifyAll() + } + } + } + + def threadLocks = { + synchronized { + threadStates(threadId).locks + } + } + + def threadState = { + synchronized { + threadStates(threadId) + } + } + + def mapOtherStates(f: ThreadState => ThreadState) = { + val exception = threadId + synchronized { + for (k <- threadStates.keys if k != exception) { + threadStates(k) = f(threadStates(k)) + } + } + } + + def log(str: String) = { + if((realToFakeThreadId contains Thread.currentThread().getId())) { + val space = (" " * ((threadId - 1) * 2)) + val s = space + threadId + ":" + "\n".r.replaceAllIn(str, "\n" + space + " ") + opLog += s + } + } + + /** + * Executes a read or write operation to a global data structure as per the given schedule + * @param msg a message corresponding to the operation that will be logged + */ + def exec[T](primop: => T)(msg: => String, postMsg: => Option[T => String] = None): T = { + if(! (realToFakeThreadId contains Thread.currentThread().getId())) { + primop + } else { + updateThreadState(VariableReadWrite(threadLocks)) + val m = msg + if(m != "") log(m) + if (opLog.size > maxOps) + throw new Exception(s"Total number of reads/writes performed by threads exceed $maxOps. A possible deadlock!") + val res = primop + postMsg match { + case Some(m) => log(m(res)) + case None => + } + res + } + } + + private def setThreadId(fakeId: Int) = synchronized { + realToFakeThreadId(Thread.currentThread.getId) = fakeId + } + + def threadId = + try { + realToFakeThreadId(Thread.currentThread().getId()) + } catch { + case e: NoSuchElementException => + throw new Exception("You are accessing shared variables in the constructor. This is not allowed. The variables are already initialized!") + } + + private def isTurn(tid: Int) = synchronized { + (!schedule.isEmpty && schedule.head != tid) + } + + def canProceed(): Boolean = { + val tid = threadId + canContinue match { + case Some((i, state)) if i == tid => + //println(s"$tid: Runs ! Was in state $state") + canContinue = None + state match { + case Sync(lockToAquire, locks) => updateThreadState(Running(lockToAquire +: locks)) + case SyncUnique(lockToAquire, locks) => + mapOtherStates { + _ match { + case SyncUnique(lockToAquire2, locks2) if lockToAquire2 == lockToAquire => Wait(lockToAquire2, locks2) + case e => e + } + } + updateThreadState(Running(lockToAquire +: locks)) + case VariableReadWrite(locks) => updateThreadState(Running(locks)) + } + true + case Some((i, state)) => + //println(s"$tid: not my turn but $i !") + false + case None => + false + } + } + + var threadPreference = 0 // In the case the schedule is over, which thread should have the preference to execute. + + /** returns true if the thread can continue to execute, and false otherwise */ + def decide(): Option[(Int, ThreadState)] = { + if (!threadStates.isEmpty) { // The last thread who enters the decision loop takes the decision. + //println(s"$threadId: I'm taking a decision") + if (threadStates.values.forall { case e: Wait => true case _ => false }) { + val waiting = threadStates.keys.map(_.toString).mkString(", ") + val s = if (threadStates.size > 1) "s" else "" + val are = if (threadStates.size > 1) "are" else "is" + throw new Exception(s"Deadlock: Thread$s $waiting $are waiting but all others have ended and cannot notify them.") + } else { + // Threads can be in Wait, Sync, SyncUnique, and VariableReadWrite mode. + // Let's determine which ones can continue. + val notFree = threadStates.collect { case (id, state) => state.locks }.flatten.toSet + val threadsNotBlocked = threadStates.toSeq.filter { + case (id, v: VariableReadWrite) => true + case (id, v: CanContinueIfAcquiresLock) => !notFree(v.lockToAquire) || (v.locks contains v.lockToAquire) + case _ => false + } + if (threadsNotBlocked.isEmpty) { + val waiting = threadStates.keys.map(_.toString).mkString(", ") + val s = if (threadStates.size > 1) "s" else "" + val are = if (threadStates.size > 1) "are" else "is" + val whoHasLock = threadStates.toSeq.flatMap { case (id, state) => state.locks.map(lock => (lock, id)) }.toMap + val reason = threadStates.collect { + case (id, state: CanContinueIfAcquiresLock) if !notFree(state.lockToAquire) => + s"Thread $id is waiting on lock ${state.lockToAquire} held by thread ${whoHasLock(state.lockToAquire)}" + }.mkString("\n") + throw new Exception(s"Deadlock: Thread$s $waiting are interlocked. Indeed:\n$reason") + } else if (threadsNotBlocked.size == 1) { // Do not consume the schedule if only one thread can execute. + Some(threadsNotBlocked(0)) + } else { + val next = schedule.indexWhere(t => threadsNotBlocked.exists { case (id, state) => id == t }) + if (next != -1) { + //println(s"$threadId: schedule is $schedule, next chosen is ${schedule(next)}") + val chosenOne = schedule(next) // TODO: Make schedule a mutable list. + schedule = schedule.take(next) ++ schedule.drop(next + 1) + Some((chosenOne, threadStates(chosenOne))) + } else { + threadPreference = (threadPreference + 1) % threadsNotBlocked.size + val chosenOne = threadsNotBlocked(threadPreference) // Maybe another strategy + Some(chosenOne) + //threadsNotBlocked.indexOf(threadId) >= 0 + /* + val tnb = threadsNotBlocked.map(_._1).mkString(",") + val s = if (schedule.isEmpty) "empty" else schedule.mkString(",") + val only = if (schedule.isEmpty) "" else " only" + throw new Exception(s"The schedule is $s but$only threads ${tnb} can continue")*/ + } + } + } + } else canContinue + } + + /** + * This will be called before a schedulable operation begins. + * This should not use synchronized + */ + var numThreadsWaiting = new AtomicInteger(0) + //var waitingForDecision = Map[Int, Option[Int]]() // Mapping from thread ids to a number indicating who is going to make the choice. + var canContinue: Option[(Int, ThreadState)] = None // The result of the decision thread Id of the thread authorized to continue. + private def waitForTurn = { + synchronized { + if (numThreadsWaiting.incrementAndGet() == threadStates.size) { + canContinue = decide() + notifyAll() + } + //waitingForDecision(threadId) = Some(numThreadsWaiting) + //println(s"$threadId Entering waiting with ticket number $numThreadsWaiting/${waitingForDecision.size}") + while (!canProceed()) wait() + } + numThreadsWaiting.decrementAndGet() + } + + /** + * To be invoked when a thread is about to complete + */ + private def removeFromSchedule(fakeid: Int) = synchronized { + //println(s"$fakeid: I'm taking a decision because I finished") + schedule = schedule.filterNot(_ == fakeid) + threadStates -= fakeid + if (numThreadsWaiting.get() == threadStates.size) { + canContinue = decide() + notifyAll() + } + } + + def getOperationLog() = opLog +} diff --git a/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/Stats.scala b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/Stats.scala new file mode 100644 index 0000000000000000000000000000000000000000..e38dd2ab10b997f3e8d89fd8b9806bbb8c597e75 --- /dev/null +++ b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/Stats.scala @@ -0,0 +1,23 @@ +/* Copyright 2009-2015 EPFL, Lausanne */ +package m20.instrumentation + +import java.lang.management._ + +/** + * A collection of methods that can be used to collect run-time statistics about Leon programs. + * This is mostly used to test the resources properties of Leon programs + */ +object Stats { + def timed[T](code: => T)(cont: Long => Unit): T = { + var t1 = System.currentTimeMillis() + val r = code + cont((System.currentTimeMillis() - t1)) + r + } + + def withTime[T](code: => T): (T, Long) = { + var t1 = System.currentTimeMillis() + val r = code + (r, (System.currentTimeMillis() - t1)) + } +} diff --git a/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/TestHelper.scala b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/TestHelper.scala new file mode 100644 index 0000000000000000000000000000000000000000..b361ded58992134d41995527014ca62133b7e4d3 --- /dev/null +++ b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/TestHelper.scala @@ -0,0 +1,124 @@ +package m20.instrumentation + +import scala.util.Random +import scala.collection.mutable.{Map => MutableMap} + +import Stats._ + +object TestHelper { + val noOfSchedules = 10000 // set this to 100k during deployment + val readWritesPerThread = 20 // maximum number of read/writes possible in one thread + val contextSwitchBound = 10 + val testTimeout = 150 // the total time out for a test in seconds + val schedTimeout = 15 // the total time out for execution of a schedule in secs + + // Helpers + /*def testManySchedules(op1: => Any): Unit = testManySchedules(List(() => op1)) + def testManySchedules(op1: => Any, op2: => Any): Unit = testManySchedules(List(() => op1, () => op2)) + def testManySchedules(op1: => Any, op2: => Any, op3: => Any): Unit = testManySchedules(List(() => op1, () => op2, () => op3)) + def testManySchedules(op1: => Any, op2: => Any, op3: => Any, op4: => Any): Unit = testManySchedules(List(() => op1, () => op2, () => op3, () => op4))*/ + + def testSequential[T](ops: Scheduler => Any)(assertions: T => (Boolean, String)) = + testManySchedules(1, + (sched: Scheduler) => { + (List(() => ops(sched)), + (res: List[Any]) => assertions(res.head.asInstanceOf[T])) + }) + + /** + * @numThreads number of threads + * @ops operations to be executed, one per thread + * @assertion as condition that will executed after all threads have completed (without exceptions) + * the arguments are the results of the threads + */ + def testManySchedules(numThreads: Int, + ops: Scheduler => + (List[() => Any], // Threads + List[Any] => (Boolean, String)) // Assertion + ) = { + var timeout = testTimeout * 1000L + val threadIds = (1 to numThreads) + //(1 to scheduleLength).flatMap(_ => threadIds).toList.permutations.take(noOfSchedules).foreach { + val schedules = (new ScheduleGenerator(numThreads)).schedules() + var schedsExplored = 0 + schedules.takeWhile(_ => schedsExplored <= noOfSchedules && timeout > 0).foreach { + //case _ if timeout <= 0 => // break + case schedule => + schedsExplored += 1 + val schedr = new Scheduler(schedule) + //println("Exploring Sched: "+schedule) + val (threadOps, assertion) = ops(schedr) + if (threadOps.size != numThreads) + throw new IllegalStateException(s"Number of threads: $numThreads, do not match operations of threads: $threadOps") + timed { schedr.runInParallel(schedTimeout * 1000, threadOps) } { t => timeout -= t } match { + case Timeout(msg) => + throw new java.lang.AssertionError("assertion failed\n"+"The schedule took too long to complete. A possible deadlock! \n"+msg) + case Except(msg, stkTrace) => + val traceStr = "Thread Stack trace: \n"+stkTrace.map(" at "+_.toString).mkString("\n") + throw new java.lang.AssertionError("assertion failed\n"+msg+"\n"+traceStr) + case RetVal(threadRes) => + // check the assertion + val (success, custom_msg) = assertion(threadRes) + if (!success) { + val msg = "The following schedule resulted in wrong results: \n" + custom_msg + "\n" + schedr.getOperationLog().mkString("\n") + throw new java.lang.AssertionError("Assertion failed: "+msg) + } + } + } + if (timeout <= 0) { + throw new java.lang.AssertionError("Test took too long to complete! Cannot check all schedules as your code is too slow!") + } + } + + /** + * A schedule generator that is based on the context bound + */ + class ScheduleGenerator(numThreads: Int) { + val scheduleLength = readWritesPerThread * numThreads + val rands = (1 to scheduleLength).map(i => new Random(0xcafe * i)) // random numbers for choosing a thread at each position + def schedules(): LazyList[List[Int]] = { + var contextSwitches = 0 + var contexts = List[Int]() // a stack of thread ids in the order of context-switches + val remainingOps = MutableMap[Int, Int]() + remainingOps ++= (1 to numThreads).map(i => (i, readWritesPerThread)) // num ops remaining in each thread + val liveThreads = (1 to numThreads).toSeq.toBuffer + + /** + * Updates remainingOps and liveThreads once a thread is chosen for a position in the schedule + */ + def updateState(tid: Int): Unit = { + val remOps = remainingOps(tid) + if (remOps == 0) { + liveThreads -= tid + } else { + remainingOps += (tid -> (remOps - 1)) + } + } + val schedule = rands.foldLeft(List[Int]()) { + case (acc, r) if contextSwitches < contextSwitchBound => + val tid = liveThreads(r.nextInt(liveThreads.size)) + contexts match { + case prev :: tail if prev != tid => // we have a new context switch here + contexts +:= tid + contextSwitches += 1 + case prev :: tail => + case _ => // init case + contexts +:= tid + } + updateState(tid) + acc :+ tid + case (acc, _) => // here context-bound has been reached so complete the schedule without any more context switches + if (!contexts.isEmpty) { + contexts = contexts.dropWhile(remainingOps(_) == 0) + } + val tid = contexts match { + case top :: tail => top + case _ => liveThreads(0) // here, there has to be threads that have not even started + } + updateState(tid) + acc :+ tid + } + schedule #:: schedules() + } + } +} diff --git a/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/TestUtils.scala b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/TestUtils.scala new file mode 100644 index 0000000000000000000000000000000000000000..a6a1cac480c1547251b8782042706083cbd3ce4e --- /dev/null +++ b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/TestUtils.scala @@ -0,0 +1,19 @@ +package m20.instrumentation + +import scala.concurrent._ +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global + +object TestUtils { + def failsOrTimesOut[T](action: => T): Boolean = { + val asyncAction = Future { + action + } + try { + Await.result(asyncAction, 2000.millisecond) + } catch { + case _: Throwable => return true + } + return false + } +} diff --git a/previous-exams/2021-midterm/m20/src/test/scala/m20/overrides.scala b/previous-exams/2021-midterm/m20/src/test/scala/m20/overrides.scala new file mode 100644 index 0000000000000000000000000000000000000000..cffe5c598c82933dd3a7f7bc00d810d32c582bc6 --- /dev/null +++ b/previous-exams/2021-midterm/m20/src/test/scala/m20/overrides.scala @@ -0,0 +1,68 @@ +package m20 + +import instrumentation._ + +import scala.annotation.tailrec +import java.util.concurrent.atomic._ + +class SchedulableAtomicVariable[T](initial: T, scheduler: Scheduler, name: String) extends AbstractAtomicVariable[T]: + private val proxied: AtomicVariable[T] = new AtomicVariable[T](initial) + + override def get: T = scheduler.exec { + proxied.get + } (s"", Some(res => s"$name: get $res")) + + override def set(value: T): Unit = scheduler.exec { + proxied.set(value) + } (s"$name: set $value", None) + + override def compareAndSet(expected: T, newValue: T): Boolean = { + scheduler.exec { + proxied.compareAndSet(expected, newValue) + } (s"$name: compareAndSet(expected = $expected, newValue = $newValue)", Some(res => s"$name: Did it set? $res") ) + } + +end SchedulableAtomicVariable + +class SchedulableSeqCount(val scheduler: Scheduler) extends SeqCount with LockFreeMonitor: + override def generation: Int = scheduler.exec { + super.generation + } ("", Some(res => s"generation is $res")) + override def setGeneration(newGeneration: Int): Unit = scheduler.exec { + super.setGeneration(newGeneration) + } ( s"setGeneration($newGeneration)", None ) + + override def x: Int = scheduler.exec { + super.x + } ("", Some(res => s"x is $res")) + override def setX(newX: Int): Unit = scheduler.exec { + super.setX(newX) + } (s"setX($newX)", None) + + override def y: Int = scheduler.exec { + super.y + } ("", Some(res => s"y is $res")) + override def setY(newY: Int): Unit = scheduler.exec { + super.setY(newY) + } (s"setY($newY)", None) + +end SchedulableSeqCount + +class SchedulableMultiWriterSeqCount(val scheduler: Scheduler) extends MultiWriterSeqCount with LockFreeMonitor: + override protected val myGeneration: AbstractAtomicVariable[Int] = new SchedulableAtomicVariable(0, scheduler, "myGeneration") + + override def x: Int = scheduler.exec { + super.x + } ("", Some(res => s"x is $res")) + override def setX(newX: Int): Unit = scheduler.exec { + super.setX(newX) + } (s"setX($newX)", None) + + override def y: Int = scheduler.exec { + super.y + } ("", Some(res => s"y is $res")) + override def setY(newY: Int): Unit = scheduler.exec { + super.setY(newY) + } (s"setY($newY)", None) + +end SchedulableMultiWriterSeqCount diff --git a/previous-exams/2021-midterm-solutions/m21.md b/previous-exams/2021-midterm/m21.md similarity index 95% rename from previous-exams/2021-midterm-solutions/m21.md rename to previous-exams/2021-midterm/m21.md index fb11db8278973e5a3f9f6a660a477e5f792f5bae..82736a23664c7abba6312b6df914fedc297faf04 100644 --- a/previous-exams/2021-midterm-solutions/m21.md +++ b/previous-exams/2021-midterm/m21.md @@ -1,9 +1,3 @@ -Use the following commands to make a fresh clone of your repository: - -``` -git clone -b m21 git@gitlab.epfl.ch:lamp/student-repositories-s21/cs206-GASPAR.git m21 -``` - ## Useful links * [A guide to the Scala parallel collections](https://docs.scala-lang.org/overviews/parallel-collections/overview.html) diff --git a/previous-exams/2021-midterm/m21/.gitignore b/previous-exams/2021-midterm/m21/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..40937dc9b192820d0ede18efd3c7e6442a083b17 --- /dev/null +++ b/previous-exams/2021-midterm/m21/.gitignore @@ -0,0 +1,22 @@ +# General +*.DS_Store +*.swp +*~ + +# Dotty +*.class +*.tasty +*.hasTasty + +# sbt +target/ + +# IDE +.bsp +.bloop +.metals +.vscode + +# datasets +stackoverflow-grading.csv +wikipedia-grading.dat diff --git a/previous-exams/2021-midterm/m21/assignment.sbt b/previous-exams/2021-midterm/m21/assignment.sbt new file mode 100644 index 0000000000000000000000000000000000000000..da7eb3c8347293a18da0025fcd6060d8f8f7cc11 --- /dev/null +++ b/previous-exams/2021-midterm/m21/assignment.sbt @@ -0,0 +1,2 @@ +// Student tasks (i.e. submit, packageSubmission) +enablePlugins(StudentTasks) diff --git a/previous-exams/2021-midterm/m21/build.sbt b/previous-exams/2021-midterm/m21/build.sbt new file mode 100644 index 0000000000000000000000000000000000000000..15c62589add717bed6163f0f77d5d607033187bf --- /dev/null +++ b/previous-exams/2021-midterm/m21/build.sbt @@ -0,0 +1,12 @@ +course := "midterm" +assignment := "m21" +scalaVersion := "3.0.0-RC1" +scalacOptions ++= Seq("-language:implicitConversions", "-deprecation") + +libraryDependencies += "org.scalameta" %% "munit" % "0.7.22" + +val MUnitFramework = new TestFramework("munit.Framework") +testFrameworks += MUnitFramework +// Decode Scala names +testOptions += Tests.Argument(MUnitFramework, "-s") +testSuite := "m21.M21Suite" diff --git a/previous-exams/2021-midterm/m21/grading-tests.jar b/previous-exams/2021-midterm/m21/grading-tests.jar new file mode 100644 index 0000000000000000000000000000000000000000..a8c37ec97d20d360071bd45af4e344e62720698f Binary files /dev/null and b/previous-exams/2021-midterm/m21/grading-tests.jar differ diff --git a/previous-exams/2021-midterm/m21/project/FilteringReporterPlugin.scala b/previous-exams/2021-midterm/m21/project/FilteringReporterPlugin.scala new file mode 100644 index 0000000000000000000000000000000000000000..2e4fd9a4d998698cd52643344b33a5e719dd7971 --- /dev/null +++ b/previous-exams/2021-midterm/m21/project/FilteringReporterPlugin.scala @@ -0,0 +1,31 @@ +package sbt // To access the private[sbt] compilerReporter key +package filteringReporterPlugin + +import Keys._ +import ch.epfl.lamp._ + +object FilteringReporterPlugin extends AutoPlugin { + override lazy val projectSettings = Seq( + // Turn off warning coming from scalameter that we cannot fix without changing scalameter + compilerReporter in (Compile, compile) ~= { reporter => new FilteringReporter(reporter) } + ) +} + +class FilteringReporter(reporter: xsbti.Reporter) extends xsbti.Reporter { + + def reset(): Unit = reporter.reset() + def hasErrors: Boolean = reporter.hasErrors + def hasWarnings: Boolean = reporter.hasWarnings + def printSummary(): Unit = reporter.printSummary() + def problems: Array[xsbti.Problem] = reporter.problems + + def log(problem: xsbti.Problem): Unit = { + if (!problem.message.contains("An existential type that came from a Scala-2 classfile cannot be")) + reporter.log(problem) + } + + def comment(pos: xsbti.Position, msg: String): Unit = + reporter.comment(pos, msg) + + override def toString = s"CollectingReporter($reporter)" +} diff --git a/previous-exams/2021-midterm/m21/project/MOOCSettings.scala b/previous-exams/2021-midterm/m21/project/MOOCSettings.scala new file mode 100644 index 0000000000000000000000000000000000000000..1c40443a53085d23fadb134f4e1a505c32231f1d --- /dev/null +++ b/previous-exams/2021-midterm/m21/project/MOOCSettings.scala @@ -0,0 +1,49 @@ +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 { + + override def requires = super.requires && filteringReporterPlugin.FilteringReporterPlugin + + 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") + .withRank(KeyRanks.Invisible) + // 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/previous-exams/2021-midterm/m21/project/StudentTasks.scala b/previous-exams/2021-midterm/m21/project/StudentTasks.scala new file mode 100644 index 0000000000000000000000000000000000000000..c4669afe82dd2b45651f94dcad9e736f29d21432 --- /dev/null +++ b/previous-exams/2021-midterm/m21/project/StudentTasks.scala @@ -0,0 +1,303 @@ +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") + + lazy val Grading = config("grading") extend(Runtime) + } + + + import autoImport._ + import MOOCSettings.autoImport._ + + override lazy val projectSettings = Seq( + packageSubmissionSetting, + fork := true, + connectInput in run := true, + outputStrategy := Some(StdoutOutput), + ) ++ + packageSubmissionZipSettings ++ + inConfig(Grading)(Defaults.testSettings ++ Seq( + unmanagedJars += file("grading-tests.jar"), + + definedTests := (definedTests in Test).value, + internalDependencyClasspath := (internalDependencyClasspath in Test).value + )) + + + /** ********************************************************** + * 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, None) + 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("<arg>").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 <email-address> <submit-token> + | + |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/previous-exams/2021-midterm/m21/project/build.properties b/previous-exams/2021-midterm/m21/project/build.properties new file mode 100644 index 0000000000000000000000000000000000000000..0b2e09c5ac99bd3de91b2b139b94301c2b6e26f3 --- /dev/null +++ b/previous-exams/2021-midterm/m21/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.4.7 diff --git a/previous-exams/2021-midterm/m21/project/buildSettings.sbt b/previous-exams/2021-midterm/m21/project/buildSettings.sbt new file mode 100644 index 0000000000000000000000000000000000000000..8fac702aaf3f3c4ede79691c7b4e4a52f26f3f47 --- /dev/null +++ b/previous-exams/2021-midterm/m21/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/previous-exams/2021-midterm/m21/project/plugins.sbt b/previous-exams/2021-midterm/m21/project/plugins.sbt new file mode 100644 index 0000000000000000000000000000000000000000..fb7dbe068109e7f35c13b2762b865c7eec1979f3 --- /dev/null +++ b/previous-exams/2021-midterm/m21/project/plugins.sbt @@ -0,0 +1,3 @@ +// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3") diff --git a/previous-exams/2021-midterm/m21/src/main/scala/m21/MultiWriterSeqCount.scala b/previous-exams/2021-midterm/m21/src/main/scala/m21/MultiWriterSeqCount.scala new file mode 100644 index 0000000000000000000000000000000000000000..bec39210798debbcfada52cddc1d291d2c41d891 --- /dev/null +++ b/previous-exams/2021-midterm/m21/src/main/scala/m21/MultiWriterSeqCount.scala @@ -0,0 +1,46 @@ +package m21 + +import instrumentation._ + +import scala.annotation.tailrec + +/** Multi-writer, multi-reader data structure containing a pair of integers. */ +class MultiWriterSeqCount extends Monitor: + /** Do not directly use this variable, use `generation`, `setGeneration` and + * `compareAndSetGeneration` instead. + */ + protected val myGeneration: AbstractAtomicVariable[Int] = new AtomicVariable(1) + protected def generation: Int = myGeneration.get + protected def setGeneration(newGeneration: Int): Unit = + myGeneration.set(newGeneration) + protected def compareAndSetGeneration(expected: Int, newValue: Int): Boolean = + myGeneration.compareAndSet(expected, newValue) + + /** Do not directly use this variable, use `x` and `setX` instead. */ + protected var myX: Int = 0 + protected def x: Int = myX + protected def setX(newX: Int): Unit = + myX = newX + + /** Do not directly use this variable, use `y` and `setY` instead. */ + protected var myY: Int = 0 + protected def y: Int = myY + protected def setY(newY: Int): Unit = + myY = newY + + /** Write new values into this data structure. + * This method is always safe to call. + * The implementation of this method is not allowed to call `synchronized`. + */ + final def write(newX: Int, newY: Int): Unit = ??? + + /** Copy the values previously written into this data structure into a tuple. + * This method is always safe to call. + * The implementation of this method is not allowed to call `synchronized`. + */ + final def copy(): (Int, Int) = + // You should be able to just copy-paste the implementation of `copy` you + // wrote in `SeqCount` here. + ??? + +end MultiWriterSeqCount diff --git a/previous-exams/2021-midterm/m21/src/main/scala/m21/SeqCount.scala b/previous-exams/2021-midterm/m21/src/main/scala/m21/SeqCount.scala new file mode 100644 index 0000000000000000000000000000000000000000..f7a50513af3aebe478f9b9bd54ae77f919244728 --- /dev/null +++ b/previous-exams/2021-midterm/m21/src/main/scala/m21/SeqCount.scala @@ -0,0 +1,42 @@ +package m21 + +import instrumentation._ + +import scala.annotation.tailrec + +/** Single-writer, multi-reader data structure containing a pair of integers. */ +class SeqCount extends Monitor: + /** Do not directly use this variable, use `generation` and `setGeneration` instead. */ + @volatile protected var myGeneration: Int = 1 + protected def generation: Int = myGeneration + protected def setGeneration(newGeneration: Int): Unit = + myGeneration = newGeneration + + /** Do not directly use this variable, use `x` and `setX` instead. */ + protected var myX: Int = 0 + protected def x: Int = myX + protected def setX(newX: Int): Unit = + myX = newX + + /** Do not directly use this variable, use `y` and `setY` instead. */ + protected var myY: Int = 0 + protected def y: Int = myY + protected def setY(newY: Int): Unit = + myY = newY + + /** Write new values into this data structure. + * This method must only be called from one thread at a time. + */ + final def write(newX: Int, newY: Int): Unit = + setGeneration(generation + 1) + setX(newX) + setY(newY) + setGeneration(generation + 1) + + /** Copy the values previously written into this data structure into a tuple. + * This method is always safe to call. + * The implementation of this method is not allowed to call `synchronized`. + */ + final def copy(): (Int, Int) = ??? + +end SeqCount diff --git a/previous-exams/2021-midterm/m21/src/main/scala/m21/instrumentation/AtomicVariable.scala b/previous-exams/2021-midterm/m21/src/main/scala/m21/instrumentation/AtomicVariable.scala new file mode 100644 index 0000000000000000000000000000000000000000..5a5d2f4a94ad73544c265ee86d80085fcb5d48cb --- /dev/null +++ b/previous-exams/2021-midterm/m21/src/main/scala/m21/instrumentation/AtomicVariable.scala @@ -0,0 +1,28 @@ +package m21.instrumentation + +import java.util.concurrent.atomic._ + +abstract class AbstractAtomicVariable[T] { + def get: T + def set(value: T): Unit + def compareAndSet(expect: T, newval: T) : Boolean +} + +class AtomicVariable[T](initial: T) extends AbstractAtomicVariable[T] { + + private val atomic = new AtomicReference[T](initial) + + override def get: T = atomic.get() + + override def set(value: T): Unit = atomic.set(value) + + override def compareAndSet(expected: T, newValue: T): Boolean = { + val current = atomic.get + if (current == expected) { + atomic.compareAndSet(current, newValue) + } + else { + false + } + } +} diff --git a/previous-exams/2021-midterm/m21/src/main/scala/m21/instrumentation/Monitor.scala b/previous-exams/2021-midterm/m21/src/main/scala/m21/instrumentation/Monitor.scala new file mode 100644 index 0000000000000000000000000000000000000000..06551000717e74916191ea0ed606405390439aca --- /dev/null +++ b/previous-exams/2021-midterm/m21/src/main/scala/m21/instrumentation/Monitor.scala @@ -0,0 +1,23 @@ +package m21.instrumentation + +class Dummy + +trait Monitor { + implicit val dummy: Dummy = new Dummy + + def wait()(implicit i: Dummy) = waitDefault() + + def synchronized[T](e: => T)(implicit i: Dummy) = synchronizedDefault(e) + + def notify()(implicit i: Dummy) = notifyDefault() + + def notifyAll()(implicit i: Dummy) = notifyAllDefault() + + private val lock = new AnyRef + + // Can be overriden. + def waitDefault(): Unit = lock.wait() + def synchronizedDefault[T](toExecute: =>T): T = lock.synchronized(toExecute) + def notifyDefault(): Unit = lock.notify() + def notifyAllDefault(): Unit = lock.notifyAll() +} diff --git a/previous-exams/2021-midterm/m21/src/test/scala/m21/TestSuite.scala b/previous-exams/2021-midterm/m21/src/test/scala/m21/TestSuite.scala new file mode 100644 index 0000000000000000000000000000000000000000..5038092c46f6020e81887598e6e30d0f6b5d18b2 --- /dev/null +++ b/previous-exams/2021-midterm/m21/src/test/scala/m21/TestSuite.scala @@ -0,0 +1,122 @@ +package m21 + +import scala.concurrent._ +import scala.concurrent.duration._ +import scala.collection.mutable.HashMap +import scala.util.Random +import instrumentation._ +import instrumentation.TestHelper._ +import instrumentation.TestUtils._ + +enum ThreadResult: + case WriteError(error: String) + case WriteSuccess + case Read(result: (Int, Int)) +import ThreadResult._ + +class M21Suite extends munit.FunSuite: + /** If at least one thread resulted in an error, + * return `(false, errorMessage)` otherwise return `(true, "")`. + */ + def processResults(results: List[ThreadResult]): (Boolean, String) = + val success = (true, "") + results.foldLeft(success) { + case (acc @ (false, _), _) => + // Report the first error found + acc + case (_, WriteError(error)) => + (false, error) + case (_, Read((x, y))) if x + 1 != y => + (false, s"Read ($x, $y) but expected y to be ${x + 1}") + case (_, _: Read | WriteSuccess) => + success + } + + def randomList(length: Int): List[Int] = + List.fill(length)(Random.nextInt) + + test("SeqCount: single-threaded write and copy (1 pts)") { + val sc = new SeqCount + randomList(100).lazyZip(randomList(100)).foreach { (x, y) => + sc.write(x, y) + assertEquals(sc.copy(), (x, y)) + } + } + + test("SeqCount: one write thread, two copy threads (4 pts)") { + testManySchedules(3, sched => + val sc = new SchedulableSeqCount(sched) + // Invariant in this test: y == x + 1 + sc.write(0, 1) + + val randomValues = randomList(length = 5) + + def writeThread(): ThreadResult = + randomValues.foldLeft(WriteSuccess) { + case (res: WriteError, _) => + // Report the first error found + res + case (_, i) => + sc.write(i, i + 1) + val writtenValues = (i, i + 1) + val readBack = sc.copy() + if writtenValues != readBack then + WriteError(s"Wrote $writtenValues but read back $readBack") + else + WriteSuccess + } + + def copyThread(): ThreadResult = + Read(sc.copy()) + + val threads = List( + () => writeThread(), + () => copyThread(), + () => copyThread() + ) + + (threads, results => processResults(results.asInstanceOf[List[ThreadResult]])) + ) + } + + test("MultiWriterSeqCount: single-threaded write and copy (1 pts)") { + val sc = new MultiWriterSeqCount + randomList(100).lazyZip(randomList(100)).foreach { (x, y) => + sc.write(x, y) + assertEquals(sc.copy(), (x, y)) + } + } + + test("MultiWriterSeqCount: two write threads, two copy threads (4 pts)") { + testManySchedules(4, sched => + val msc = new SchedulableMultiWriterSeqCount(sched) + // Invariant in this test: y == x + 1 + msc.write(0, 1) + + val randomValues = randomList(length = 5) + + def writeThread(): ThreadResult = + randomValues.foreach(i => msc.write(i, i + 1)) + // Unlke in the SeqCount test, we do not verify that we can read back + // the values we wrote, because the other writer thread might have + // overwritten them already. + WriteSuccess + + def copyThread(): ThreadResult = + Read(msc.copy()) + + val threads = List( + () => writeThread(), + () => writeThread(), + () => copyThread(), + () => copyThread() + ) + + (threads, results => processResults(results.asInstanceOf[List[ThreadResult]])) + ) + } + + import scala.concurrent.duration._ + override val munitTimeout = 200.seconds +end M21Suite + diff --git a/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/MockedMonitor.scala b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/MockedMonitor.scala new file mode 100644 index 0000000000000000000000000000000000000000..f7629e1b14693b723511bb67212fcdb16c64421c --- /dev/null +++ b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/MockedMonitor.scala @@ -0,0 +1,72 @@ +package m21.instrumentation + +trait MockedMonitor extends Monitor { + def scheduler: Scheduler + + // Can be overriden. + override def waitDefault() = { + scheduler.log("wait") + scheduler updateThreadState Wait(this, scheduler.threadLocks.tail) + } + override def synchronizedDefault[T](toExecute: =>T): T = { + scheduler.log("synchronized check") + val prevLocks = scheduler.threadLocks + scheduler updateThreadState Sync(this, prevLocks) // If this belongs to prevLocks, should just continue. + scheduler.log("synchronized -> enter") + try { + toExecute + } finally { + scheduler updateThreadState Running(prevLocks) + scheduler.log("synchronized -> out") + } + } + override def notifyDefault() = { + scheduler mapOtherStates { + state => state match { + case Wait(lockToAquire, locks) if lockToAquire == this => SyncUnique(this, state.locks) + case e => e + } + } + scheduler.log("notify") + } + override def notifyAllDefault() = { + scheduler mapOtherStates { + state => state match { + case Wait(lockToAquire, locks) if lockToAquire == this => Sync(this, state.locks) + case SyncUnique(lockToAquire, locks) if lockToAquire == this => Sync(this, state.locks) + case e => e + } + } + scheduler.log("notifyAll") + } +} + +trait LockFreeMonitor extends Monitor { + override def waitDefault() = { + throw new Exception("Please use lock-free structures and do not use wait()") + } + override def synchronizedDefault[T](toExecute: =>T): T = { + throw new Exception("Please use lock-free structures and do not use synchronized()") + } + override def notifyDefault() = { + throw new Exception("Please use lock-free structures and do not use notify()") + } + override def notifyAllDefault() = { + throw new Exception("Please use lock-free structures and do not use notifyAll()") + } +} + + +abstract class ThreadState { + def locks: Seq[AnyRef] +} +trait CanContinueIfAcquiresLock extends ThreadState { + def lockToAquire: AnyRef +} +case object Start extends ThreadState { def locks: Seq[AnyRef] = Seq.empty } +case object End extends ThreadState { def locks: Seq[AnyRef] = Seq.empty } +case class Wait(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState +case class SyncUnique(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState with CanContinueIfAcquiresLock +case class Sync(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState with CanContinueIfAcquiresLock +case class Running(locks: Seq[AnyRef]) extends ThreadState +case class VariableReadWrite(locks: Seq[AnyRef]) extends ThreadState diff --git a/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/Scheduler.scala b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/Scheduler.scala new file mode 100644 index 0000000000000000000000000000000000000000..cef9ac5155bd3c6747aea2f7c4d85e3616e84fbb --- /dev/null +++ b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/Scheduler.scala @@ -0,0 +1,304 @@ +package m21.instrumentation + +import java.util.concurrent._; +import scala.concurrent.duration._ +import scala.collection.mutable._ +import Stats._ + +import java.util.concurrent.atomic.AtomicInteger + +sealed abstract class Result +case class RetVal(rets: List[Any]) extends Result +case class Except(msg: String, stackTrace: Array[StackTraceElement]) extends Result +case class Timeout(msg: String) extends Result + +/** + * A class that maintains schedule and a set of thread ids. + * The schedules are advanced after an operation of a SchedulableBuffer is performed. + * Note: the real schedule that is executed may deviate from the input schedule + * due to the adjustments that had to be made for locks + */ +class Scheduler(sched: List[Int]) { + val maxOps = 500 // a limit on the maximum number of operations the code is allowed to perform + + private var schedule = sched + private var numThreads = 0 + private val realToFakeThreadId = Map[Long, Int]() + private val opLog = ListBuffer[String]() // a mutable list (used for efficient concat) + private val threadStates = Map[Int, ThreadState]() + + /** + * Runs a set of operations in parallel as per the schedule. + * Each operation may consist of many primitive operations like reads or writes + * to shared data structure each of which should be executed using the function `exec`. + * @timeout in milliseconds + * @return true - all threads completed on time, false -some tests timed out. + */ + def runInParallel(timeout: Long, ops: List[() => Any]): Result = { + numThreads = ops.length + val threadRes = Array.fill(numThreads) { None: Any } + var exception: Option[Except] = None + val syncObject = new Object() + var completed = new AtomicInteger(0) + // create threads + val threads = ops.zipWithIndex.map { + case (op, i) => + new Thread(new Runnable() { + def run(): Unit = { + val fakeId = i + 1 + setThreadId(fakeId) + try { + updateThreadState(Start) + val res = op() + updateThreadState(End) + threadRes(i) = res + // notify the master thread if all threads have completed + if (completed.incrementAndGet() == ops.length) { + syncObject.synchronized { syncObject.notifyAll() } + } + } catch { + case e: Throwable if exception != None => // do nothing here and silently fail + case e: Throwable => + log(s"throw ${e.toString}") + exception = Some(Except(s"Thread $fakeId crashed on the following schedule: \n" + opLog.mkString("\n"), + e.getStackTrace)) + syncObject.synchronized { syncObject.notifyAll() } + //println(s"$fakeId: ${e.toString}") + //Runtime.getRuntime().halt(0) //exit the JVM and all running threads (no other way to kill other threads) + } + } + }) + } + // start all threads + threads.foreach(_.start()) + // wait for all threads to complete, or for an exception to be thrown, or for the time out to expire + var remTime = timeout + syncObject.synchronized { + timed { if(completed.get() != ops.length) syncObject.wait(timeout) } { time => remTime -= time } + } + if (exception.isDefined) { + exception.get + } else if (remTime <= 1) { // timeout ? using 1 instead of zero to allow for some errors + Timeout(opLog.mkString("\n")) + } else { + // every thing executed normally + RetVal(threadRes.toList) + } + } + + // Updates the state of the current thread + def updateThreadState(state: ThreadState): Unit = { + val tid = threadId + synchronized { + threadStates(tid) = state + } + state match { + case Sync(lockToAquire, locks) => + if (locks.indexOf(lockToAquire) < 0) waitForTurn else { + // Re-aqcuiring the same lock + updateThreadState(Running(lockToAquire +: locks)) + } + case Start => waitStart() + case End => removeFromSchedule(tid) + case Running(_) => + case _ => waitForTurn // Wait, SyncUnique, VariableReadWrite + } + } + + def waitStart(): Unit = { + //while (threadStates.size < numThreads) { + //Thread.sleep(1) + //} + synchronized { + if (threadStates.size < numThreads) { + wait() + } else { + notifyAll() + } + } + } + + def threadLocks = { + synchronized { + threadStates(threadId).locks + } + } + + def threadState = { + synchronized { + threadStates(threadId) + } + } + + def mapOtherStates(f: ThreadState => ThreadState) = { + val exception = threadId + synchronized { + for (k <- threadStates.keys if k != exception) { + threadStates(k) = f(threadStates(k)) + } + } + } + + def log(str: String) = { + if((realToFakeThreadId contains Thread.currentThread().getId())) { + val space = (" " * ((threadId - 1) * 2)) + val s = space + threadId + ":" + "\n".r.replaceAllIn(str, "\n" + space + " ") + opLog += s + } + } + + /** + * Executes a read or write operation to a global data structure as per the given schedule + * @param msg a message corresponding to the operation that will be logged + */ + def exec[T](primop: => T)(msg: => String, postMsg: => Option[T => String] = None): T = { + if(! (realToFakeThreadId contains Thread.currentThread().getId())) { + primop + } else { + updateThreadState(VariableReadWrite(threadLocks)) + val m = msg + if(m != "") log(m) + if (opLog.size > maxOps) + throw new Exception(s"Total number of reads/writes performed by threads exceed $maxOps. A possible deadlock!") + val res = primop + postMsg match { + case Some(m) => log(m(res)) + case None => + } + res + } + } + + private def setThreadId(fakeId: Int) = synchronized { + realToFakeThreadId(Thread.currentThread.getId) = fakeId + } + + def threadId = + try { + realToFakeThreadId(Thread.currentThread().getId()) + } catch { + case e: NoSuchElementException => + throw new Exception("You are accessing shared variables in the constructor. This is not allowed. The variables are already initialized!") + } + + private def isTurn(tid: Int) = synchronized { + (!schedule.isEmpty && schedule.head != tid) + } + + def canProceed(): Boolean = { + val tid = threadId + canContinue match { + case Some((i, state)) if i == tid => + //println(s"$tid: Runs ! Was in state $state") + canContinue = None + state match { + case Sync(lockToAquire, locks) => updateThreadState(Running(lockToAquire +: locks)) + case SyncUnique(lockToAquire, locks) => + mapOtherStates { + _ match { + case SyncUnique(lockToAquire2, locks2) if lockToAquire2 == lockToAquire => Wait(lockToAquire2, locks2) + case e => e + } + } + updateThreadState(Running(lockToAquire +: locks)) + case VariableReadWrite(locks) => updateThreadState(Running(locks)) + } + true + case Some((i, state)) => + //println(s"$tid: not my turn but $i !") + false + case None => + false + } + } + + var threadPreference = 0 // In the case the schedule is over, which thread should have the preference to execute. + + /** returns true if the thread can continue to execute, and false otherwise */ + def decide(): Option[(Int, ThreadState)] = { + if (!threadStates.isEmpty) { // The last thread who enters the decision loop takes the decision. + //println(s"$threadId: I'm taking a decision") + if (threadStates.values.forall { case e: Wait => true case _ => false }) { + val waiting = threadStates.keys.map(_.toString).mkString(", ") + val s = if (threadStates.size > 1) "s" else "" + val are = if (threadStates.size > 1) "are" else "is" + throw new Exception(s"Deadlock: Thread$s $waiting $are waiting but all others have ended and cannot notify them.") + } else { + // Threads can be in Wait, Sync, SyncUnique, and VariableReadWrite mode. + // Let's determine which ones can continue. + val notFree = threadStates.collect { case (id, state) => state.locks }.flatten.toSet + val threadsNotBlocked = threadStates.toSeq.filter { + case (id, v: VariableReadWrite) => true + case (id, v: CanContinueIfAcquiresLock) => !notFree(v.lockToAquire) || (v.locks contains v.lockToAquire) + case _ => false + } + if (threadsNotBlocked.isEmpty) { + val waiting = threadStates.keys.map(_.toString).mkString(", ") + val s = if (threadStates.size > 1) "s" else "" + val are = if (threadStates.size > 1) "are" else "is" + val whoHasLock = threadStates.toSeq.flatMap { case (id, state) => state.locks.map(lock => (lock, id)) }.toMap + val reason = threadStates.collect { + case (id, state: CanContinueIfAcquiresLock) if !notFree(state.lockToAquire) => + s"Thread $id is waiting on lock ${state.lockToAquire} held by thread ${whoHasLock(state.lockToAquire)}" + }.mkString("\n") + throw new Exception(s"Deadlock: Thread$s $waiting are interlocked. Indeed:\n$reason") + } else if (threadsNotBlocked.size == 1) { // Do not consume the schedule if only one thread can execute. + Some(threadsNotBlocked(0)) + } else { + val next = schedule.indexWhere(t => threadsNotBlocked.exists { case (id, state) => id == t }) + if (next != -1) { + //println(s"$threadId: schedule is $schedule, next chosen is ${schedule(next)}") + val chosenOne = schedule(next) // TODO: Make schedule a mutable list. + schedule = schedule.take(next) ++ schedule.drop(next + 1) + Some((chosenOne, threadStates(chosenOne))) + } else { + threadPreference = (threadPreference + 1) % threadsNotBlocked.size + val chosenOne = threadsNotBlocked(threadPreference) // Maybe another strategy + Some(chosenOne) + //threadsNotBlocked.indexOf(threadId) >= 0 + /* + val tnb = threadsNotBlocked.map(_._1).mkString(",") + val s = if (schedule.isEmpty) "empty" else schedule.mkString(",") + val only = if (schedule.isEmpty) "" else " only" + throw new Exception(s"The schedule is $s but$only threads ${tnb} can continue")*/ + } + } + } + } else canContinue + } + + /** + * This will be called before a schedulable operation begins. + * This should not use synchronized + */ + var numThreadsWaiting = new AtomicInteger(0) + //var waitingForDecision = Map[Int, Option[Int]]() // Mapping from thread ids to a number indicating who is going to make the choice. + var canContinue: Option[(Int, ThreadState)] = None // The result of the decision thread Id of the thread authorized to continue. + private def waitForTurn = { + synchronized { + if (numThreadsWaiting.incrementAndGet() == threadStates.size) { + canContinue = decide() + notifyAll() + } + //waitingForDecision(threadId) = Some(numThreadsWaiting) + //println(s"$threadId Entering waiting with ticket number $numThreadsWaiting/${waitingForDecision.size}") + while (!canProceed()) wait() + } + numThreadsWaiting.decrementAndGet() + } + + /** + * To be invoked when a thread is about to complete + */ + private def removeFromSchedule(fakeid: Int) = synchronized { + //println(s"$fakeid: I'm taking a decision because I finished") + schedule = schedule.filterNot(_ == fakeid) + threadStates -= fakeid + if (numThreadsWaiting.get() == threadStates.size) { + canContinue = decide() + notifyAll() + } + } + + def getOperationLog() = opLog +} diff --git a/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/Stats.scala b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/Stats.scala new file mode 100644 index 0000000000000000000000000000000000000000..6eb7239c2343a77ed7f9e8193cbacc2d57a16d61 --- /dev/null +++ b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/Stats.scala @@ -0,0 +1,23 @@ +/* Copyright 2009-2015 EPFL, Lausanne */ +package m21.instrumentation + +import java.lang.management._ + +/** + * A collection of methods that can be used to collect run-time statistics about Leon programs. + * This is mostly used to test the resources properties of Leon programs + */ +object Stats { + def timed[T](code: => T)(cont: Long => Unit): T = { + var t1 = System.currentTimeMillis() + val r = code + cont((System.currentTimeMillis() - t1)) + r + } + + def withTime[T](code: => T): (T, Long) = { + var t1 = System.currentTimeMillis() + val r = code + (r, (System.currentTimeMillis() - t1)) + } +} diff --git a/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/TestHelper.scala b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/TestHelper.scala new file mode 100644 index 0000000000000000000000000000000000000000..5be75d6590a34d6e7410129e66a2f2a3b17e216c --- /dev/null +++ b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/TestHelper.scala @@ -0,0 +1,124 @@ +package m21.instrumentation + +import scala.util.Random +import scala.collection.mutable.{Map => MutableMap} + +import Stats._ + +object TestHelper { + val noOfSchedules = 10000 // set this to 100k during deployment + val readWritesPerThread = 20 // maximum number of read/writes possible in one thread + val contextSwitchBound = 10 + val testTimeout = 150 // the total time out for a test in seconds + val schedTimeout = 15 // the total time out for execution of a schedule in secs + + // Helpers + /*def testManySchedules(op1: => Any): Unit = testManySchedules(List(() => op1)) + def testManySchedules(op1: => Any, op2: => Any): Unit = testManySchedules(List(() => op1, () => op2)) + def testManySchedules(op1: => Any, op2: => Any, op3: => Any): Unit = testManySchedules(List(() => op1, () => op2, () => op3)) + def testManySchedules(op1: => Any, op2: => Any, op3: => Any, op4: => Any): Unit = testManySchedules(List(() => op1, () => op2, () => op3, () => op4))*/ + + def testSequential[T](ops: Scheduler => Any)(assertions: T => (Boolean, String)) = + testManySchedules(1, + (sched: Scheduler) => { + (List(() => ops(sched)), + (res: List[Any]) => assertions(res.head.asInstanceOf[T])) + }) + + /** + * @numThreads number of threads + * @ops operations to be executed, one per thread + * @assertion as condition that will executed after all threads have completed (without exceptions) + * the arguments are the results of the threads + */ + def testManySchedules(numThreads: Int, + ops: Scheduler => + (List[() => Any], // Threads + List[Any] => (Boolean, String)) // Assertion + ) = { + var timeout = testTimeout * 1000L + val threadIds = (1 to numThreads) + //(1 to scheduleLength).flatMap(_ => threadIds).toList.permutations.take(noOfSchedules).foreach { + val schedules = (new ScheduleGenerator(numThreads)).schedules() + var schedsExplored = 0 + schedules.takeWhile(_ => schedsExplored <= noOfSchedules && timeout > 0).foreach { + //case _ if timeout <= 0 => // break + case schedule => + schedsExplored += 1 + val schedr = new Scheduler(schedule) + //println("Exploring Sched: "+schedule) + val (threadOps, assertion) = ops(schedr) + if (threadOps.size != numThreads) + throw new IllegalStateException(s"Number of threads: $numThreads, do not match operations of threads: $threadOps") + timed { schedr.runInParallel(schedTimeout * 1000, threadOps) } { t => timeout -= t } match { + case Timeout(msg) => + throw new java.lang.AssertionError("assertion failed\n"+"The schedule took too long to complete. A possible deadlock! \n"+msg) + case Except(msg, stkTrace) => + val traceStr = "Thread Stack trace: \n"+stkTrace.map(" at "+_.toString).mkString("\n") + throw new java.lang.AssertionError("assertion failed\n"+msg+"\n"+traceStr) + case RetVal(threadRes) => + // check the assertion + val (success, custom_msg) = assertion(threadRes) + if (!success) { + val msg = "The following schedule resulted in wrong results: \n" + custom_msg + "\n" + schedr.getOperationLog().mkString("\n") + throw new java.lang.AssertionError("Assertion failed: "+msg) + } + } + } + if (timeout <= 0) { + throw new java.lang.AssertionError("Test took too long to complete! Cannot check all schedules as your code is too slow!") + } + } + + /** + * A schedule generator that is based on the context bound + */ + class ScheduleGenerator(numThreads: Int) { + val scheduleLength = readWritesPerThread * numThreads + val rands = (1 to scheduleLength).map(i => new Random(0xcafe * i)) // random numbers for choosing a thread at each position + def schedules(): LazyList[List[Int]] = { + var contextSwitches = 0 + var contexts = List[Int]() // a stack of thread ids in the order of context-switches + val remainingOps = MutableMap[Int, Int]() + remainingOps ++= (1 to numThreads).map(i => (i, readWritesPerThread)) // num ops remaining in each thread + val liveThreads = (1 to numThreads).toSeq.toBuffer + + /** + * Updates remainingOps and liveThreads once a thread is chosen for a position in the schedule + */ + def updateState(tid: Int): Unit = { + val remOps = remainingOps(tid) + if (remOps == 0) { + liveThreads -= tid + } else { + remainingOps += (tid -> (remOps - 1)) + } + } + val schedule = rands.foldLeft(List[Int]()) { + case (acc, r) if contextSwitches < contextSwitchBound => + val tid = liveThreads(r.nextInt(liveThreads.size)) + contexts match { + case prev :: tail if prev != tid => // we have a new context switch here + contexts +:= tid + contextSwitches += 1 + case prev :: tail => + case _ => // init case + contexts +:= tid + } + updateState(tid) + acc :+ tid + case (acc, _) => // here context-bound has been reached so complete the schedule without any more context switches + if (!contexts.isEmpty) { + contexts = contexts.dropWhile(remainingOps(_) == 0) + } + val tid = contexts match { + case top :: tail => top + case _ => liveThreads(0) // here, there has to be threads that have not even started + } + updateState(tid) + acc :+ tid + } + schedule #:: schedules() + } + } +} diff --git a/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/TestUtils.scala b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/TestUtils.scala new file mode 100644 index 0000000000000000000000000000000000000000..5da760477971e5faad31593cd61552ec2e7b4ba6 --- /dev/null +++ b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/TestUtils.scala @@ -0,0 +1,19 @@ +package m21.instrumentation + +import scala.concurrent._ +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global + +object TestUtils { + def failsOrTimesOut[T](action: => T): Boolean = { + val asyncAction = Future { + action + } + try { + Await.result(asyncAction, 2000.millisecond) + } catch { + case _: Throwable => return true + } + return false + } +} diff --git a/previous-exams/2021-midterm/m21/src/test/scala/m21/overrides.scala b/previous-exams/2021-midterm/m21/src/test/scala/m21/overrides.scala new file mode 100644 index 0000000000000000000000000000000000000000..92d0cec220a473d6e112d44ca7456037d08503b6 --- /dev/null +++ b/previous-exams/2021-midterm/m21/src/test/scala/m21/overrides.scala @@ -0,0 +1,68 @@ +package m21 + +import instrumentation._ + +import scala.annotation.tailrec +import java.util.concurrent.atomic._ + +class SchedulableAtomicVariable[T](initial: T, scheduler: Scheduler, name: String) extends AbstractAtomicVariable[T]: + private val proxied: AtomicVariable[T] = new AtomicVariable[T](initial) + + override def get: T = scheduler.exec { + proxied.get + } (s"", Some(res => s"$name: get $res")) + + override def set(value: T): Unit = scheduler.exec { + proxied.set(value) + } (s"$name: set $value", None) + + override def compareAndSet(expected: T, newValue: T): Boolean = { + scheduler.exec { + proxied.compareAndSet(expected, newValue) + } (s"$name: compareAndSet(expected = $expected, newValue = $newValue)", Some(res => s"$name: Did it set? $res") ) + } + +end SchedulableAtomicVariable + +class SchedulableSeqCount(val scheduler: Scheduler) extends SeqCount with LockFreeMonitor: + override def generation: Int = scheduler.exec { + super.generation + } ("", Some(res => s"generation is $res")) + override def setGeneration(newGeneration: Int): Unit = scheduler.exec { + super.setGeneration(newGeneration) + } ( s"setGeneration($newGeneration)", None ) + + override def x: Int = scheduler.exec { + super.x + } ("", Some(res => s"x is $res")) + override def setX(newX: Int): Unit = scheduler.exec { + super.setX(newX) + } (s"setX($newX)", None) + + override def y: Int = scheduler.exec { + super.y + } ("", Some(res => s"y is $res")) + override def setY(newY: Int): Unit = scheduler.exec { + super.setY(newY) + } (s"setY($newY)", None) + +end SchedulableSeqCount + +class SchedulableMultiWriterSeqCount(val scheduler: Scheduler) extends MultiWriterSeqCount with LockFreeMonitor: + override protected val myGeneration: AbstractAtomicVariable[Int] = new SchedulableAtomicVariable(1, scheduler, "myGeneration") + + override def x: Int = scheduler.exec { + super.x + } ("", Some(res => s"x is $res")) + override def setX(newX: Int): Unit = scheduler.exec { + super.setX(newX) + } (s"setX($newX)", None) + + override def y: Int = scheduler.exec { + super.y + } ("", Some(res => s"y is $res")) + override def setY(newY: Int): Unit = scheduler.exec { + super.setY(newY) + } (s"setY($newY)", None) + +end SchedulableMultiWriterSeqCount diff --git a/previous-exams/2021-midterm-solutions/m3.md b/previous-exams/2021-midterm/m3.md similarity index 94% rename from previous-exams/2021-midterm-solutions/m3.md rename to previous-exams/2021-midterm/m3.md index ee3969ef97a4ed980c3d1cbbcfd1b60ced495a34..88365dc2670fdade4186d1b52204daea41bd9609 100644 --- a/previous-exams/2021-midterm-solutions/m3.md +++ b/previous-exams/2021-midterm/m3.md @@ -1,9 +1,3 @@ -Use the following commands to make a fresh clone of your repository: - -``` -git clone -b m3 git@gitlab.epfl.ch:lamp/student-repositories-s21/cs206-GASPAR.git m3 -``` - ## Useful links * [A guide to the Scala parallel collections](https://docs.scala-lang.org/overviews/parallel-collections/overview.html) diff --git a/previous-exams/2021-midterm/m3/.gitignore b/previous-exams/2021-midterm/m3/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..40937dc9b192820d0ede18efd3c7e6442a083b17 --- /dev/null +++ b/previous-exams/2021-midterm/m3/.gitignore @@ -0,0 +1,22 @@ +# General +*.DS_Store +*.swp +*~ + +# Dotty +*.class +*.tasty +*.hasTasty + +# sbt +target/ + +# IDE +.bsp +.bloop +.metals +.vscode + +# datasets +stackoverflow-grading.csv +wikipedia-grading.dat diff --git a/previous-exams/2021-midterm/m3/assignment.sbt b/previous-exams/2021-midterm/m3/assignment.sbt new file mode 100644 index 0000000000000000000000000000000000000000..da7eb3c8347293a18da0025fcd6060d8f8f7cc11 --- /dev/null +++ b/previous-exams/2021-midterm/m3/assignment.sbt @@ -0,0 +1,2 @@ +// Student tasks (i.e. submit, packageSubmission) +enablePlugins(StudentTasks) diff --git a/previous-exams/2021-midterm/m3/build.sbt b/previous-exams/2021-midterm/m3/build.sbt new file mode 100644 index 0000000000000000000000000000000000000000..b15645acb40417c7326345ed22ac4115e6e88735 --- /dev/null +++ b/previous-exams/2021-midterm/m3/build.sbt @@ -0,0 +1,12 @@ +course := "midterm" +assignment := "m3" +scalaVersion := "3.0.0-RC1" +scalacOptions ++= Seq("-language:implicitConversions", "-deprecation") + +libraryDependencies += "org.scalameta" %% "munit" % "0.7.22" + +val MUnitFramework = new TestFramework("munit.Framework") +testFrameworks += MUnitFramework +// Decode Scala names +testOptions += Tests.Argument(MUnitFramework, "-s") +testSuite := "m3.M3Suite" diff --git a/previous-exams/2021-midterm/m3/grading-tests.jar b/previous-exams/2021-midterm/m3/grading-tests.jar new file mode 100644 index 0000000000000000000000000000000000000000..01481c432fa1fc85da0c4c660daebb5f6fffd40d Binary files /dev/null and b/previous-exams/2021-midterm/m3/grading-tests.jar differ diff --git a/previous-exams/2021-midterm/m3/project/FilteringReporterPlugin.scala b/previous-exams/2021-midterm/m3/project/FilteringReporterPlugin.scala new file mode 100644 index 0000000000000000000000000000000000000000..2e4fd9a4d998698cd52643344b33a5e719dd7971 --- /dev/null +++ b/previous-exams/2021-midterm/m3/project/FilteringReporterPlugin.scala @@ -0,0 +1,31 @@ +package sbt // To access the private[sbt] compilerReporter key +package filteringReporterPlugin + +import Keys._ +import ch.epfl.lamp._ + +object FilteringReporterPlugin extends AutoPlugin { + override lazy val projectSettings = Seq( + // Turn off warning coming from scalameter that we cannot fix without changing scalameter + compilerReporter in (Compile, compile) ~= { reporter => new FilteringReporter(reporter) } + ) +} + +class FilteringReporter(reporter: xsbti.Reporter) extends xsbti.Reporter { + + def reset(): Unit = reporter.reset() + def hasErrors: Boolean = reporter.hasErrors + def hasWarnings: Boolean = reporter.hasWarnings + def printSummary(): Unit = reporter.printSummary() + def problems: Array[xsbti.Problem] = reporter.problems + + def log(problem: xsbti.Problem): Unit = { + if (!problem.message.contains("An existential type that came from a Scala-2 classfile cannot be")) + reporter.log(problem) + } + + def comment(pos: xsbti.Position, msg: String): Unit = + reporter.comment(pos, msg) + + override def toString = s"CollectingReporter($reporter)" +} diff --git a/previous-exams/2021-midterm/m3/project/MOOCSettings.scala b/previous-exams/2021-midterm/m3/project/MOOCSettings.scala new file mode 100644 index 0000000000000000000000000000000000000000..1c40443a53085d23fadb134f4e1a505c32231f1d --- /dev/null +++ b/previous-exams/2021-midterm/m3/project/MOOCSettings.scala @@ -0,0 +1,49 @@ +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 { + + override def requires = super.requires && filteringReporterPlugin.FilteringReporterPlugin + + 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") + .withRank(KeyRanks.Invisible) + // 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/previous-exams/2021-midterm/m3/project/StudentTasks.scala b/previous-exams/2021-midterm/m3/project/StudentTasks.scala new file mode 100644 index 0000000000000000000000000000000000000000..c4669afe82dd2b45651f94dcad9e736f29d21432 --- /dev/null +++ b/previous-exams/2021-midterm/m3/project/StudentTasks.scala @@ -0,0 +1,303 @@ +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") + + lazy val Grading = config("grading") extend(Runtime) + } + + + import autoImport._ + import MOOCSettings.autoImport._ + + override lazy val projectSettings = Seq( + packageSubmissionSetting, + fork := true, + connectInput in run := true, + outputStrategy := Some(StdoutOutput), + ) ++ + packageSubmissionZipSettings ++ + inConfig(Grading)(Defaults.testSettings ++ Seq( + unmanagedJars += file("grading-tests.jar"), + + definedTests := (definedTests in Test).value, + internalDependencyClasspath := (internalDependencyClasspath in Test).value + )) + + + /** ********************************************************** + * 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, None) + 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("<arg>").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 <email-address> <submit-token> + | + |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/previous-exams/2021-midterm/m3/project/build.properties b/previous-exams/2021-midterm/m3/project/build.properties new file mode 100644 index 0000000000000000000000000000000000000000..0b2e09c5ac99bd3de91b2b139b94301c2b6e26f3 --- /dev/null +++ b/previous-exams/2021-midterm/m3/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.4.7 diff --git a/previous-exams/2021-midterm/m3/project/buildSettings.sbt b/previous-exams/2021-midterm/m3/project/buildSettings.sbt new file mode 100644 index 0000000000000000000000000000000000000000..8fac702aaf3f3c4ede79691c7b4e4a52f26f3f47 --- /dev/null +++ b/previous-exams/2021-midterm/m3/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/previous-exams/2021-midterm/m3/project/plugins.sbt b/previous-exams/2021-midterm/m3/project/plugins.sbt new file mode 100644 index 0000000000000000000000000000000000000000..fb7dbe068109e7f35c13b2762b865c7eec1979f3 --- /dev/null +++ b/previous-exams/2021-midterm/m3/project/plugins.sbt @@ -0,0 +1,3 @@ +// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3") diff --git a/previous-exams/2021-midterm/m3/src/main/scala/m3/Lib.scala b/previous-exams/2021-midterm/m3/src/main/scala/m3/Lib.scala new file mode 100644 index 0000000000000000000000000000000000000000..68e89db2496199c75ec5362e9d9725f3c27facf5 --- /dev/null +++ b/previous-exams/2021-midterm/m3/src/main/scala/m3/Lib.scala @@ -0,0 +1,68 @@ +package m3 + +//////////////////////////////////////// +// NO NEED TO MODIFY THIS SOURCE FILE // +//////////////////////////////////////// + +trait Lib { + + /** If an array has `n` elements and `n < THRESHOLD`, then it should be processed sequentially */ + final val THRESHOLD: Int = 33 + + /** Compute the two values in parallel + * + * Note: Most tests just compute those two sequentially to make any bug simpler to debug + */ + def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) + + /** A limited array. It only contains the required operations for this exercise. */ + trait Arr[T] { + /** Get the i-th element of the array (0-based) */ + def apply(i: Int): T + /** Update the i-th element of the array with the given value (0-based) */ + def update(i: Int, x: T): Unit + /** Number of elements in this array */ + def length: Int + /** Create a copy of this array without the first element */ + def tail: Arr[T] + /** Create a copy of this array by mapping all the elements with the given function */ + def map[U](f: T => U): Arr[U] + } + + object Arr { + /** Create an array with the given elements */ + def apply[T](xs: T*): Arr[T] = { + val arr: Arr[T] = Arr.ofLength(xs.length) + for i <- 0 until xs.length do arr(i) = xs(i) + arr + } + + /** Create an array with the given length. All elements are initialized to `null`. */ + def ofLength[T](n: Int): Arr[T] = + newArrOfLength(n) + + } + + /** Create an array with the given length. All elements are initialized to `null`. */ + def newArrOfLength[T](n: Int): Arr[T] + + /** A number representing the average of a list of integers (the "window") */ + case class AvgWin(list: List[Int]) { + def push(i: Int) = list match { + case i3 :: i2 :: i1 :: Nil => AvgWin(i :: i3 :: i2 :: Nil) + case list => AvgWin(i :: list) + } + + def pushAll(other: AvgWin) = + other.list.foldRight(this)((el, self) => self.push(el)) + + def toDouble: Double = if list.isEmpty then 0 else list.sum / list.length + } + + /** Tree result of an upsweep operation. Specialized for `AvgWin` results. */ + trait TreeRes { val res: AvgWin } + /** Leaf result of an upsweep operation. Specialized for `AvgWin` results. */ + case class Leaf(from: Int, to: Int, res: AvgWin) extends TreeRes + /** Tree node result of an upsweep operation. Specialized for `AvgWin` results. */ + case class Node(left: TreeRes, res: AvgWin, right: TreeRes) extends TreeRes +} diff --git a/previous-exams/2021-midterm/m3/src/main/scala/m3/M3.scala b/previous-exams/2021-midterm/m3/src/main/scala/m3/M3.scala new file mode 100644 index 0000000000000000000000000000000000000000..c9b722da8dcb2ec0d381d49793b6bbcb3d0daf32 --- /dev/null +++ b/previous-exams/2021-midterm/m3/src/main/scala/m3/M3.scala @@ -0,0 +1,89 @@ +package m3 + + +trait M3 extends Lib { + // Functions and classes of Lib can be used in here + + /** Compute the rolling windowed mean of an array. + * + * For an array `arr = Arr(x1, x2, x3, ..., x_n)` the result is + * `Arr(x1, (x1+x2)/2, (x1+x2+x3)/3, (x2+x3+x4)/3, ..., (x_{n-2}, x_{n-1}, x_n)/3)` + */ + def rollingWinMeanParallel(arr: Arr[Int]): Arr[Double] = { + if (arr.length == 0) return Arr.ofLength(0) + // TASK 1: Add missing parallelization in `upsweep` and `downsweep`. + // You should use the `parallel` method. + // You should use the sequential version if the number of elements is lower than THRESHOLD. + // TASK 2a: Pass `arr` to `upsweep` and `downsweep` instead of `tmp`. + // You will need to change some signatures and update the code appropriately. + // Remove the definition of `tmp` + // TASK 2b: Change the type of the array `out` from `AvgWin` to `Double` + // You will need to change some signatures and update the code appropriately. + // Remove the call `.map(root => root.toDouble)`. + // TASK 3: Remove the call to `.tail`. + // Update the update the code appropriately. + + val tmp: Arr[AvgWin] = arr.map(x => AvgWin(x :: Nil)) + val out: Arr[AvgWin] = Arr.ofLength(arr.length + 1) + val tree = upsweep(tmp, 0, arr.length) + downsweep(tmp, AvgWin(Nil), tree, out) + out(0) = AvgWin(Nil) + out.map(root => root.toDouble).tail + + // IDEAL SOLUTION + // val out = Arr.ofLength(arr.length) + // val tree = upsweep(arr, 0, arr.length) + // downsweep(arr, AvgWin(Nil), tree, out) + // out + } + + def scanOp(acc: AvgWin, x: AvgWin) = // No need to modify this method + acc.pushAll(x) + + def upsweep(input: Arr[AvgWin], from: Int, to: Int): TreeRes = { + if (to - from < 2) + Leaf(from, to, reduceSequential(input, from + 1, to, input(from))) + else { + val mid = from + (to - from) / 2 + val (tL, tR) = ( + upsweep(input, from, mid), + upsweep(input, mid, to) + ) + Node(tL, scanOp(tL.res, tR.res), tR) + } + } + + def downsweep(input: Arr[AvgWin], a0: AvgWin, tree: TreeRes, output: Arr[AvgWin]): Unit = { + tree match { + case Node(left, _, right) => + ( + downsweep(input, a0, left, output), + downsweep(input, scanOp(a0, left.res), right, output) + ) + case Leaf(from, to, _) => + downsweepSequential(input, from, to, a0, output) + } + } + + def downsweepSequential(input: Arr[AvgWin], from: Int, to: Int, a0: AvgWin, output: Arr[AvgWin]): Unit = { + if (from < to) { + var i = from + var a = a0 + while (i < to) { + a = scanOp(a, input(i)) + i = i + 1 + output(i) = a + } + } + } + + def reduceSequential(input: Arr[AvgWin], from: Int, to: Int, a0: AvgWin): AvgWin = { + var a = a0 + var i = from + while (i < to) { + a = scanOp(a, input(i)) + i = i + 1 + } + a + } +} diff --git a/previous-exams/2021-midterm/m3/src/test/scala/m3/M3Suite.scala b/previous-exams/2021-midterm/m3/src/test/scala/m3/M3Suite.scala new file mode 100644 index 0000000000000000000000000000000000000000..176df6a14e456a6bfa29e250d20aa94253c2360b --- /dev/null +++ b/previous-exams/2021-midterm/m3/src/test/scala/m3/M3Suite.scala @@ -0,0 +1,174 @@ +package m3 + +class M3Suite extends munit.FunSuite { + + test("Rolling windowed average result test (5pts)") { + RollingWinMeanBasicLogicTest.basicTests() + RollingWinMeanBasicLogicTest.normalTests() + RollingWinMeanBasicLogicTest.largeTests() + } + + test("[TASK 1] Rolling windowed average parallelism test (30pts)") { + RollingWinMeanCallsToParallel.parallelismTest() + RollingWinMeanParallel.basicTests() + RollingWinMeanParallel.normalTests() + RollingWinMeanParallel.largeTests() + } + + test("[TASK 2] Rolling windowed average no `map` test (35pts)") { + RollingWinMeanNoMap.basicTests() + RollingWinMeanNoMap.normalTests() + RollingWinMeanNoMap.largeTests() + } + + test("[TASK 3] Rolling windowed average no `tail` test (30pts)") { + RollingWinMeanNoTail.basicTests() + RollingWinMeanNoTail.normalTests() + RollingWinMeanNoTail.largeTests() + } + + + object RollingWinMeanBasicLogicTest extends M3 with LibImpl with RollingWinMeanTest { + def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = (op1, op2) + def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl(arr) + } + + object RollingWinMeanCallsToParallel extends M3 with LibImpl with RollingWinMeanTest { + private var count = 0 + def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = + count += 1 + (op1, op2) + + def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl(arr) + + def parallelismTest() = { + assertParallelCount(Arr(), 0) + assertParallelCount(Arr(1), 0) + assertParallelCount(Arr(1, 2, 3, 4), 0) + assertParallelCount(Arr(Array.tabulate(16)(identity): _*), 0) + assertParallelCount(Arr(Array.tabulate(32)(identity): _*), 0) + + assertParallelCount(Arr(Array.tabulate(33)(identity): _*), 2) + assertParallelCount(Arr(Array.tabulate(64)(identity): _*), 2) + assertParallelCount(Arr(Array.tabulate(128)(identity): _*), 6) + assertParallelCount(Arr(Array.tabulate(256)(identity): _*), 14) + assertParallelCount(Arr(Array.tabulate(1000)(identity): _*), 62) + assertParallelCount(Arr(Array.tabulate(1024)(identity): _*), 62) + } + + def assertParallelCount(arr: Arr[Int], expected: Int): Unit = { + try { + count = 0 + rollingWinMeanParallel(arr) + assert(count == expected, { + val extra = if (expected == 0) "" else s" ${expected/2} for the `upsweep` and ${expected/2} for the `downsweep`" + s"\n$arr\n\nERROR: Expected $expected instead of $count calls to `parallel(...)` for an array of ${arr.length} elements. Current parallel threshold is $THRESHOLD.$extra" + }) + } finally { + count = 0 + } + } + + } + + object RollingWinMeanNoMap extends M3 with LibImpl with RollingWinMeanTest { + def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = (op1, op2) + def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl[T](arr) { + override def map[U](f: T => U): Arr[U] = throw Exception("Should not call Arr.map") + } + } + + object RollingWinMeanNoTail extends M3 with LibImpl with RollingWinMeanTest { + def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = (op1, op2) + def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl[T](arr) { + override def tail: Arr[T] = throw Exception("Should not call Arr.tail") + } + } + + object RollingWinMeanParallel extends M3 with LibImpl with RollingWinMeanTest { + import scala.concurrent.duration._ + val TIMEOUT = Duration(10, SECONDS) + def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = { + import concurrent.ExecutionContext.Implicits.global + import scala.concurrent._ + Await.result(Future(op1).zip(Future(op2)), TIMEOUT) // FIXME not timing-out + } + def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl(arr) + } + + trait LibImpl extends Lib { + + def newArrFrom[T](arr: Array[AnyRef]): Arr[T] + + def newArrOfLength[T](n: Int): Arr[T] = + newArrFrom(new Array(n)) + + class ArrImpl[T](val arr: Array[AnyRef]) extends Arr[T]: + def apply(i: Int): T = + arr(i).asInstanceOf[T] + def update(i: Int, x: T): Unit = + arr(i) = x.asInstanceOf[AnyRef] + def length: Int = + arr.length + def map[U](f: T => U): Arr[U] = + newArrFrom(arr.map(f.asInstanceOf[AnyRef => AnyRef])) + def tail: Arr[T] = + newArrFrom(arr.tail) + override def toString: String = + arr.mkString("Arr(", ", ", ")") + override def equals(that: Any): Boolean = + that match + case that: ArrImpl[_] => Array.equals(arr, that.arr) + case _ => false + } + + trait RollingWinMeanTest extends M3 { + + def tabulate[T](n: Int)(f: Int => T): Arr[T] = + val arr = Arr.ofLength[T](n) + for i <- 0 until n do + arr(i) = f(i) + arr + + def asSeq(arr: Arr[Double]) = + val array = new Array[Double](arr.length) + for i <- 0 to (arr.length - 1) do + array(i) = arr(i) + array.toSeq + + def scanOp_(acc: AvgWin, x: AvgWin) = + acc.pushAll(x) + + def result(ds: Seq[Int]): Arr[Double] = + Arr(ds.map(x => AvgWin(x :: Nil)).scan(AvgWin(Nil))(scanOp_).tail.map(_.toDouble): _*) + + def check(input: Seq[Int]) = + assertEquals( + asSeq(rollingWinMeanParallel(Arr(input: _*))), + asSeq(result(input)) + ) + + def basicTests() = { + check(Seq()) + check(Seq(1)) + check(Seq(1, 2, 3, 4)) + check(Seq(4, 4, 4, 4)) + } + + def normalTests() = { + check(Seq.tabulate(64)(identity)) + check(Seq(4, 4, 4, 4)) + check(Seq(4, 8, 6, 4)) + check(Seq(4, 3, 2, 1)) + check(Seq.tabulate(64)(identity).reverse) + check(Seq.tabulate(128)(i => 128 - 2*i).reverse) + } + + def largeTests() = { + check(Seq.tabulate(500)(identity)) + check(Seq.tabulate(512)(identity)) + check(Seq.tabulate(1_000)(identity)) + check(Seq.tabulate(10_000)(identity)) + } + } +} diff --git a/previous-exams/2021-midterm-solutions/m6.md b/previous-exams/2021-midterm/m6.md similarity index 97% rename from previous-exams/2021-midterm-solutions/m6.md rename to previous-exams/2021-midterm/m6.md index f0eb44964732a65fc946f8e095ad1948048799ac..6280bbb4202df0f0595b58e76a31a4182b96a6c4 100644 --- a/previous-exams/2021-midterm-solutions/m6.md +++ b/previous-exams/2021-midterm/m6.md @@ -1,9 +1,3 @@ -Use the following commands to make a fresh clone of your repository: - -``` -git clone -b m6 git@gitlab.epfl.ch:lamp/student-repositories-s21/cs206-GASPAR.git m6 -``` - ## Useful links * [A guide to the Scala parallel collections](https://docs.scala-lang.org/overviews/parallel-collections/overview.html) diff --git a/previous-exams/2021-midterm/m6/.gitignore b/previous-exams/2021-midterm/m6/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..40937dc9b192820d0ede18efd3c7e6442a083b17 --- /dev/null +++ b/previous-exams/2021-midterm/m6/.gitignore @@ -0,0 +1,22 @@ +# General +*.DS_Store +*.swp +*~ + +# Dotty +*.class +*.tasty +*.hasTasty + +# sbt +target/ + +# IDE +.bsp +.bloop +.metals +.vscode + +# datasets +stackoverflow-grading.csv +wikipedia-grading.dat diff --git a/previous-exams/2021-midterm/m6/assignment.sbt b/previous-exams/2021-midterm/m6/assignment.sbt new file mode 100644 index 0000000000000000000000000000000000000000..da7eb3c8347293a18da0025fcd6060d8f8f7cc11 --- /dev/null +++ b/previous-exams/2021-midterm/m6/assignment.sbt @@ -0,0 +1,2 @@ +// Student tasks (i.e. submit, packageSubmission) +enablePlugins(StudentTasks) diff --git a/previous-exams/2021-midterm/m6/build.sbt b/previous-exams/2021-midterm/m6/build.sbt new file mode 100644 index 0000000000000000000000000000000000000000..96606f6e8cb06d2e80e604e6d720bfbd3a83819c --- /dev/null +++ b/previous-exams/2021-midterm/m6/build.sbt @@ -0,0 +1,12 @@ +course := "midterm" +assignment := "m6" +scalaVersion := "3.0.0-RC1" +scalacOptions ++= Seq("-language:implicitConversions", "-deprecation") + +libraryDependencies += "org.scalameta" %% "munit" % "0.7.22" + +val MUnitFramework = new TestFramework("munit.Framework") +testFrameworks += MUnitFramework +// Decode Scala names +testOptions += Tests.Argument(MUnitFramework, "-s") +testSuite := "m6.M6Suite" diff --git a/previous-exams/2021-midterm/m6/grading-tests.jar b/previous-exams/2021-midterm/m6/grading-tests.jar new file mode 100644 index 0000000000000000000000000000000000000000..cdd54531b11cad5a6a0cb6060a4495ca86da20c0 Binary files /dev/null and b/previous-exams/2021-midterm/m6/grading-tests.jar differ diff --git a/previous-exams/2021-midterm/m6/project/FilteringReporterPlugin.scala b/previous-exams/2021-midterm/m6/project/FilteringReporterPlugin.scala new file mode 100644 index 0000000000000000000000000000000000000000..2e4fd9a4d998698cd52643344b33a5e719dd7971 --- /dev/null +++ b/previous-exams/2021-midterm/m6/project/FilteringReporterPlugin.scala @@ -0,0 +1,31 @@ +package sbt // To access the private[sbt] compilerReporter key +package filteringReporterPlugin + +import Keys._ +import ch.epfl.lamp._ + +object FilteringReporterPlugin extends AutoPlugin { + override lazy val projectSettings = Seq( + // Turn off warning coming from scalameter that we cannot fix without changing scalameter + compilerReporter in (Compile, compile) ~= { reporter => new FilteringReporter(reporter) } + ) +} + +class FilteringReporter(reporter: xsbti.Reporter) extends xsbti.Reporter { + + def reset(): Unit = reporter.reset() + def hasErrors: Boolean = reporter.hasErrors + def hasWarnings: Boolean = reporter.hasWarnings + def printSummary(): Unit = reporter.printSummary() + def problems: Array[xsbti.Problem] = reporter.problems + + def log(problem: xsbti.Problem): Unit = { + if (!problem.message.contains("An existential type that came from a Scala-2 classfile cannot be")) + reporter.log(problem) + } + + def comment(pos: xsbti.Position, msg: String): Unit = + reporter.comment(pos, msg) + + override def toString = s"CollectingReporter($reporter)" +} diff --git a/previous-exams/2021-midterm/m6/project/MOOCSettings.scala b/previous-exams/2021-midterm/m6/project/MOOCSettings.scala new file mode 100644 index 0000000000000000000000000000000000000000..1c40443a53085d23fadb134f4e1a505c32231f1d --- /dev/null +++ b/previous-exams/2021-midterm/m6/project/MOOCSettings.scala @@ -0,0 +1,49 @@ +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 { + + override def requires = super.requires && filteringReporterPlugin.FilteringReporterPlugin + + 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") + .withRank(KeyRanks.Invisible) + // 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/previous-exams/2021-midterm/m6/project/StudentTasks.scala b/previous-exams/2021-midterm/m6/project/StudentTasks.scala new file mode 100644 index 0000000000000000000000000000000000000000..c4669afe82dd2b45651f94dcad9e736f29d21432 --- /dev/null +++ b/previous-exams/2021-midterm/m6/project/StudentTasks.scala @@ -0,0 +1,303 @@ +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") + + lazy val Grading = config("grading") extend(Runtime) + } + + + import autoImport._ + import MOOCSettings.autoImport._ + + override lazy val projectSettings = Seq( + packageSubmissionSetting, + fork := true, + connectInput in run := true, + outputStrategy := Some(StdoutOutput), + ) ++ + packageSubmissionZipSettings ++ + inConfig(Grading)(Defaults.testSettings ++ Seq( + unmanagedJars += file("grading-tests.jar"), + + definedTests := (definedTests in Test).value, + internalDependencyClasspath := (internalDependencyClasspath in Test).value + )) + + + /** ********************************************************** + * 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, None) + 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("<arg>").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 <email-address> <submit-token> + | + |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/previous-exams/2021-midterm/m6/project/build.properties b/previous-exams/2021-midterm/m6/project/build.properties new file mode 100644 index 0000000000000000000000000000000000000000..0b2e09c5ac99bd3de91b2b139b94301c2b6e26f3 --- /dev/null +++ b/previous-exams/2021-midterm/m6/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.4.7 diff --git a/previous-exams/2021-midterm/m6/project/buildSettings.sbt b/previous-exams/2021-midterm/m6/project/buildSettings.sbt new file mode 100644 index 0000000000000000000000000000000000000000..8fac702aaf3f3c4ede79691c7b4e4a52f26f3f47 --- /dev/null +++ b/previous-exams/2021-midterm/m6/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/previous-exams/2021-midterm/m6/project/plugins.sbt b/previous-exams/2021-midterm/m6/project/plugins.sbt new file mode 100644 index 0000000000000000000000000000000000000000..fb7dbe068109e7f35c13b2762b865c7eec1979f3 --- /dev/null +++ b/previous-exams/2021-midterm/m6/project/plugins.sbt @@ -0,0 +1,3 @@ +// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3") diff --git a/previous-exams/2021-midterm/m6/src/main/scala/m6/M6.scala b/previous-exams/2021-midterm/m6/src/main/scala/m6/M6.scala new file mode 100644 index 0000000000000000000000000000000000000000..4e54913310d29d409c3bd5bb3512f2634f8297be --- /dev/null +++ b/previous-exams/2021-midterm/m6/src/main/scala/m6/M6.scala @@ -0,0 +1,69 @@ +package m6 + +import java.util.concurrent._ +import scala.util.DynamicVariable + +trait M6 extends Lib { + + class DLLCombinerImplementation(chunk_size: Int = 3) extends DLLCombiner(chunk_size){ + + // Computes every other Integer element of data array, starting from the first (index 0), up to the middle + def task1(data: Array[Int]): ForkJoinTask[Unit] = task { + ??? + } + + // Computes every other Integer element of data array, starting from the second, up to the middle + def task2(data: Array[Int]): ForkJoinTask[Unit] = task { + ??? + } + + // Computes every other Integer element of data array, starting from the second to last, up to the middle + def task3(data: Array[Int]): ForkJoinTask[Unit] = task { + ??? + } + + // Computes every other Integer element of data array, starting from the last, up to the middle + // This is executed on the current thread. + def task4(data: Array[Int]): Unit = { + ??? + } + + def result(): Array[Int] = { + val data = new Array[Int](cnt) + + ??? + + data + } + + private def copyForward(data: Array[Int], curr: Node, from: Int, to: Int, limit: Int) = { + var current = curr + var i_from = from + var i_to = to + + while (i_to < limit) { + try { + data(i_to) = current.value(i_from) + i_to += 2 + i_from += 2 + if(i_from == current.cnt){ + current = current.next + i_from = 0 + } + else if(i_from > current.cnt) { + current = current.next + i_from = 1 + if(current.cnt == 1) { + current = current.next + i_from = 0 + } + } + } + catch{ + case e: Exception => + } + } + } + } + +} diff --git a/previous-exams/2021-midterm/m6/src/main/scala/m6/lib.scala b/previous-exams/2021-midterm/m6/src/main/scala/m6/lib.scala new file mode 100644 index 0000000000000000000000000000000000000000..bfb28387fa6826b708bc35f3aa47417be7064840 --- /dev/null +++ b/previous-exams/2021-midterm/m6/src/main/scala/m6/lib.scala @@ -0,0 +1,84 @@ +package m6 + +import java.util.concurrent._ +import scala.util.DynamicVariable + +trait Lib { + + class Node(val size: Int) { + var value: Array[Int] = new Array(size) + var next: Node = null + var previous: Node = null + var cnt = 0 + + def add(v: Int) = { + value(cnt) = v + cnt += 1 + } + } + + // Simplified Combiner interface + // Implements methods += and combine + // Abstract methods should be implemented in subclasses + abstract class DLLCombiner(val chunk_size: Int) { + var head: Node = null + var last: Node = null + var cnt: Int = 0 + var chunks: Int = 0 + + // Adds an Integer to the last node of this array combiner. If the last node is full, allocates a new node. + def +=(elem: Int): Unit = { + if(cnt % chunk_size == 0) { + chunks = chunks + 1 + val node = new Node(chunk_size) + if (cnt == 0) { + head = node + last = node + } + else { + last.next = node + node.previous = last + last = node + } + } + last.add(elem) + cnt += 1 + } + + // Combines this array combiner and another given combiner in constant O(1) complexity. + def combine(that: DLLCombiner): DLLCombiner = { + assert(this.chunk_size == that.chunk_size) + if (this.cnt == 0) { + this.head = that.head + this.last = that.last + this.cnt = that.cnt + this.chunks = that.chunks + + this + } + else if (that.cnt == 0) + this + else { + this.last.next = that.head + that.head.previous = this.last + + this.cnt = this.cnt + that.cnt + this.chunks = this.chunks + that.chunks + this.last = that.last + + this + } + } + + def task1(data: Array[Int]): ForkJoinTask[Unit] + def task2(data: Array[Int]): ForkJoinTask[Unit] + def task3(data: Array[Int]): ForkJoinTask[Unit] + def task4(data: Array[Int]): Unit + + def result(): Array[Int] + + } + + def task[T](body: => T): ForkJoinTask[T] + +} \ No newline at end of file diff --git a/previous-exams/2021-midterm/m6/src/test/scala/m6/M6Suite.scala b/previous-exams/2021-midterm/m6/src/test/scala/m6/M6Suite.scala new file mode 100644 index 0000000000000000000000000000000000000000..e04fcf064cb9f9c7262b90f94365cd4651fa49d0 --- /dev/null +++ b/previous-exams/2021-midterm/m6/src/test/scala/m6/M6Suite.scala @@ -0,0 +1,422 @@ +package m6 + +import java.util.concurrent._ +import scala.util.DynamicVariable + +class M6Suite extends AbstractM6Suite { + + // (70+) 30 / 500 points for correct implementation, don't check parallelism + test("[Correctness] fetch result - simple combiners (5pts)") { + assertCorrectnessSimple() + } + + test("[Correctness] fetch result - small combiners (5pts)") { + assertCorrectnessBasic() + } + + test("[Correctness] fetch result - small combiners after combining (10pts)") { + assertCorrectnessCombined() + } + + test("[Correctness] fetch result - large combiners (10pts)") { + assertCorrectnessLarge() + } + + def assertCorrectnessSimple() = { + simpleCombiners.foreach(elem => assert(compare(elem._1, elem._2))) + } + + def assertCorrectnessBasic() = { + basicCombiners.foreach(elem => assert(compare(elem._1, elem._2))) + } + + def assertCorrectnessCombined() = { + combinedCombiners.foreach(elem => assert(compare(elem._1, elem._2))) + } + + def assertCorrectnessLarge() = { + largeCombiners.foreach(elem => assert(compare(elem._1, elem._2))) + } + + // (70+30+) 50 / 500 points for correct parallel implementation, don't check if it's exactly 1/4 of the array per task + private var count = 0 + private val expected = 3 + + override def task[T](body: => T): ForkJoinTask[T] = { + count += 1 + scheduler.value.schedule(body) + } + + test("[TaskCount] number of newly created tasks should be 3 (5pts)") { + assertTaskCountSimple() + } + + test("[TaskCount] fetch result and check parallel - simple combiners (5pts)") { + assertTaskCountSimple() + assertCorrectnessSimple() + } + + test("[TaskCount] fetch result and check parallel - small combiners (10pts)") { + assertTaskCountSimple() + assertCorrectnessBasic() + } + + test("[TaskCount] fetch result and check parallel - small combiners after combining (15pts)") { + assertTaskCountSimple() + assertCorrectnessCombined() + } + + test("[TaskCount] fetch result and check parallel - large combiners (15pts)") { + assertTaskCountSimple() + assertCorrectnessLarge() + } + + def assertTaskCountSimple(): Unit = { + simpleCombiners.foreach(elem => assertTaskCount(elem._1, elem._2)) + } + + def assertTaskCount(combiner: DLLCombinerTest, array: Array[Int]): Unit = { + try { + count = 0 + build(combiner, array) + combiner.result() + assertEquals(count, expected, { + s"ERROR: Expected $expected instead of $count calls to `task(...)`" + }) + } finally { + count = 0 + } + } + + //(70+30+50+) 200 / 500 points for correct parallel implementation, exactly 1/4 of the array per task + test("[TaskFairness] each task should compute 1/4 of the result (50pts)") { + assertTaskFairness(simpleCombiners.unzip._1) + } + + test("[TaskFairness] each task should correctly compute 1/4 of the result - simple combiners (20pts)") { + assertTaskFairness(simpleCombiners.unzip._1) + assertCorrectnessSimple() + } + + test("[TaskFairness] each task should correctly compute 1/4 of the result - small combiners (30pts)") { + assertTaskFairness(basicCombiners.unzip._1) + assertCorrectnessBasic() + } + + test("[TaskFairness] each task should correctly compute 1/4 of the result - small combiners after combining (50pts)") { + assertTaskFairness(combinedCombiners.unzip._1) + assertCorrectnessCombined() + } + + test("[TaskFairness] each task should correctly compute 1/4 of the result - large combiners (50pts)") { + assertTaskFairness(largeCombiners.unzip._1) + assertCorrectnessLarge() + } + + def assertTaskFairness(combiners: List[DLLCombinerTest]): Unit = { + def assertNewTaskFairness(combiner: DLLCombinerTest, task: ForkJoinTask[Unit], data: Array[Int]) = { + var count = 0 + var expected = combiner.cnt / 4 + task.join + count = data.count(elem => elem != 0) + + assert((count - expected).abs <= 1) + } + + def assertMainTaskFairness(combiner: DLLCombinerTest, task: Unit, data: Array[Int]) = { + var count = 0 + var expected = combiner.cnt / 4 + count = data.count(elem => elem != 0) + + assert((count - expected).abs <= 1) + } + + combiners.foreach { elem => + var data = Array.fill(elem.cnt)(0) + assertNewTaskFairness(elem, elem.task1(data), data) + + data = Array.fill(elem.cnt)(0) + assertNewTaskFairness(elem, elem.task2(data), data) + + data = Array.fill(elem.cnt)(0) + assertNewTaskFairness(elem, elem.task3(data), data) + + data = Array.fill(elem.cnt)(0) + assertMainTaskFairness(elem, elem.task4(data), data) + } + } + + //(70+30+50+200+) 150 / 500 points for correct parallel implementation, exactly 1/4 of the array per task, exactly the specified quarter + + test("[TaskPrecision] each task should compute specified 1/4 of the result - simple combiners (20pts)") { + assertTaskPrecision(simpleCombiners) + } + + test("[TaskPrecision] each task should compute specified 1/4 of the result - small combiners (30pts)") { + assertTaskPrecision(basicCombiners) + } + + test("[TaskPrecision] each task should compute specified 1/4 of the result - small combiners after combining (50pts)") { + assertTaskPrecision(combinedCombiners) + } + + test("[TaskPrecision] each task should compute specified 1/4 of the result - large combiners (50pts)") { + assertTaskPrecision(largeCombiners) + } + + def assertTaskPrecision(combiners: List[(DLLCombinerTest, Array[Int])]): Unit = { + combiners.foreach { elem => + var data = Array.fill(elem._1.cnt)(0) + var ref = Array.fill(elem._1.cnt)(0) + val task1 = elem._1.task1(data) + task1.join + Range(0, elem._1.cnt).foreach(i => (if (i < elem._1.cnt / 2 - 1 && i % 2 == 0) ref(i) = elem._2(i))) + assert(Range(0, elem._1.cnt / 2 - 1).forall(i => data(i) == ref(i))) + + data = Array.fill(elem._1.cnt)(0) + ref = Array.fill(elem._1.cnt)(0) + val task2 = elem._1.task2(data) + task2.join + Range(0, elem._1.cnt).foreach(i => (if (i < elem._1.cnt / 2 - 1 && i % 2 == 1) ref(i) = elem._2(i))) + assert(Range(0, elem._1.cnt / 2 - 1).forall(i => data(i) == ref(i))) + + data = Array.fill(elem._1.cnt)(0) + ref = Array.fill(elem._1.cnt)(0) + val task3 = elem._1.task3(data) + task3.join + Range(0, elem._1.cnt).foreach(i => (if (i > elem._1.cnt / 2 + 1 && i % 2 == elem._1.cnt % 2) ref(i) = elem._2(i))) + assert(Range(elem._1.cnt / 2 + 2, elem._1.cnt).forall(i => data(i) == ref(i))) + + data = Array.fill(elem._1.cnt)(0) + ref = Array.fill(elem._1.cnt)(0) + val task4 = elem._1.task4(data) + Range(0, elem._1.cnt).foreach(i => (if (i > elem._1.cnt / 2 + 1 && i % 2 != elem._1.cnt % 2) ref(i) = elem._2(i))) + assert(Range(elem._1.cnt / 2 + 2, elem._1.cnt).forall(i => data(i) == ref(i))) + } + } + + test("[Public] fetch simple result without combining (5pts)") { + val combiner1 = DLLCombinerTest(2) + combiner1 += 7 + combiner1 += 2 + combiner1 += 3 + combiner1 += 8 + combiner1 += 1 + combiner1 += 2 + combiner1 += 3 + combiner1 += 8 + + val result = combiner1.result() + val array = Array(7, 2, 3, 8, 1, 2, 3, 8) + + assert(Range(0,array.size).forall(i => array(i) == result(i))) + } + + test("[Public] fetch result without combining (5pts)") { + val combiner1 = DLLCombinerTest(2) + combiner1 += 7 + combiner1 += 2 + combiner1 += 3 + combiner1 += 8 + combiner1 += 1 + + val result = combiner1.result() + val array = Array(7, 2, 3, 8, 1) + + assert(Range(0,array.size).forall(i => array(i) == result(i))) + } + + test("[Public] fetch result after simple combining (5pts)") { + val combiner1 = DLLCombinerTest(2) + combiner1 += 7 + combiner1 += 2 + + val combiner2 = DLLCombinerTest(2) + combiner2 += 3 + combiner2 += 8 + + val combiner3 = DLLCombinerTest(2) + combiner2 += 1 + combiner2 += 9 + + val combiner4 = DLLCombinerTest(2) + combiner2 += 3 + combiner2 += 2 + + val result = combiner1.combine(combiner2).combine(combiner3).combine(combiner4).result() + val array = Array(7, 2, 3, 8, 1, 9, 3, 2) + + assert(Range(0,array.size).forall(i => array(i) == result(i))) + } + + test("[Public] fetch result - empty combiner (20pts)") { + val combiner1 = DLLCombinerTest(2) + val result = combiner1.result() + assertEquals(result.size, 0) + } + + test("[Public] fetch result - full single element combiner (15pts)") { + val combiner1 = DLLCombinerTest(3) + combiner1 += 4 + combiner1 += 2 + combiner1 += 6 + + val result = combiner1.result() + val array = Array(4, 2, 6) + + assert(Range(0,array.size).forall(i => array(i) == result(i))) + } + +} + + +trait AbstractM6Suite extends munit.FunSuite with LibImpl { + + def simpleCombiners = buildSimpleCombiners() + def basicCombiners = buildBasicCombiners() + def combinedCombiners = buildCombinedCombiners() + def largeCombiners = buildLargeCombiners() + + def buildSimpleCombiners() = { + val simpleCombiners = List( + (DLLCombinerTest(4), Array(4, 2, 6, 1, 5, 4, 3, 5, 6, 3, 4, 5, 6, 3, 4, 5)), + (DLLCombinerTest(4), Array(7, 2, 2, 9, 3, 2, 1, 1, 1, 1, 1, 1, 1, 2, 3, 2)), + (DLLCombinerTest(4), Array.fill(16)(5)) + ) + simpleCombiners.foreach(elem => build(elem._1, elem._2)) + simpleCombiners + } + + def buildBasicCombiners() = { + val basicCombiners = List( + (DLLCombinerTest(2), Array(4, 2, 6)), + (DLLCombinerTest(5), Array(4, 2, 6)), + (DLLCombinerTest(3), Array(4, 2, 6, 1, 7, 2, 4)), + (DLLCombinerTest(4), Array(7, 2, 2, 9, 3, 2, 11, 12, 13, 14, 15, 16, 17, 22)), + (DLLCombinerTest(3), Array.fill(16)(7)), + (DLLCombinerTest(3), Array.fill(19)(7)), + (DLLCombinerTest(3), Array.fill(5)(7)), + (DLLCombinerTest(3), Array.fill(6)(7)) + ) + basicCombiners.foreach(elem => build(elem._1, elem._2)) + basicCombiners + } + + def buildCombinedCombiners() = { + var combinedCombiners = List[(DLLCombinerTest, Array[Int])]() + Range(1, 10).foreach { chunk_size => + val array = basicCombiners.filter(elem => elem._1.chunk_size == chunk_size).foldLeft(Array[Int]()) { (acc, i) => acc ++ i._2 } + val combiner = DLLCombinerTest(chunk_size) + basicCombiners.filter(elem => elem._1.chunk_size == chunk_size).foreach(elem => combiner.combine(elem._1)) + + combinedCombiners = combinedCombiners :+ (combiner, array) + } + combinedCombiners + } + + def buildLargeCombiners() = { + val largeCombiners = List( + (DLLCombinerTest(21), Array.fill(1321)(4) ++ Array.fill(1322)(7)), + (DLLCombinerTest(18), Array.fill(1341)(2) ++ Array.fill(1122)(5)), + (DLLCombinerTest(3), Array.fill(1321)(4) ++ Array.fill(1322)(7) ++ Array.fill(321)(4) ++ Array.fill(322)(7)), + (DLLCombinerTest(12), Array.fill(992321)(4) ++ Array.fill(99322)(7)), + (DLLCombinerTest(4), Array.fill(953211)(4) ++ Array.fill(999322)(1)) + ) + largeCombiners.foreach(elem => build(elem._1, elem._2)) + largeCombiners + } + + def build(combiner: DLLCombinerTest, array: Array[Int]): DLLCombinerTest = { + array.foreach(elem => combiner += elem) + combiner + } + + def compare(combiner: DLLCombinerTest, array: Array[Int]): Boolean = { + val result = combiner.result() + Range(0,array.size).forall(i => array(i) == result(i)) + } + + def buildAndCompare(combiner: DLLCombinerTest, array: Array[Int]): Boolean = { + array.foreach(elem => combiner += elem) + val result = combiner.result() + + Range(0,array.size).forall(i => array(i) == result(i)) + } + +} + +trait LibImpl extends M6 { + + val forkJoinPool = new ForkJoinPool + + abstract class TaskScheduler { + def schedule[T](body: => T): ForkJoinTask[T] + } + + class DefaultTaskScheduler extends TaskScheduler { + def schedule[T](body: => T): ForkJoinTask[T] = { + val t = new RecursiveTask[T] { + def compute = body + } + Thread.currentThread match { + case wt: ForkJoinWorkerThread => + t.fork() + case _ => + forkJoinPool.execute(t) + } + t + } + } + + val scheduler = + new DynamicVariable[TaskScheduler](new DefaultTaskScheduler) + + def task[T](body: => T): ForkJoinTask[T] = { + scheduler.value.schedule(body) + } + + class DLLCombinerTest(chunk_size: Int = 3) extends DLLCombinerImplementation(chunk_size) { + + override def +=(elem: Int): Unit = { + if(cnt % chunk_size == 0) { + chunks = chunks + 1 + val node = new Node(chunk_size) + if (cnt == 0) { + head = node + last = node + } + else { + last.next = node + node.previous = last + last = node + } + } + last.add(elem) + cnt += 1 + } + + override def combine(that: DLLCombiner): DLLCombiner = { + assert(this.chunk_size == that.chunk_size) + if (this.cnt == 0) { + this.head = that.head + this.last = that.last + this.cnt = that.cnt + this.chunks = that.chunks + + this + } + else if (that.cnt == 0) + this + else { + this.last.next = that.head + that.head.previous = this.last + + this.cnt = this.cnt + that.cnt + this.chunks = this.chunks + that.chunks + this.last = that.last + + this + } + } + } +} diff --git a/previous-exams/2021-midterm-solutions/m7.md b/previous-exams/2021-midterm/m7.md similarity index 97% rename from previous-exams/2021-midterm-solutions/m7.md rename to previous-exams/2021-midterm/m7.md index 899703e0c7cfc8212fc6c2f95688d204bbf1a014..45a74f060eb38b9d95195c1eeaedd1bfa9dd06ed 100644 --- a/previous-exams/2021-midterm-solutions/m7.md +++ b/previous-exams/2021-midterm/m7.md @@ -1,9 +1,3 @@ -Use the following commands to make a fresh clone of your repository: - -``` -git clone -b m7 git@gitlab.epfl.ch:lamp/student-repositories-s21/cs206-GASPAR.git m7 -``` - ## Useful links * [A guide to the Scala parallel collections](https://docs.scala-lang.org/overviews/parallel-collections/overview.html) diff --git a/previous-exams/2021-midterm/m7/.gitignore b/previous-exams/2021-midterm/m7/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..40937dc9b192820d0ede18efd3c7e6442a083b17 --- /dev/null +++ b/previous-exams/2021-midterm/m7/.gitignore @@ -0,0 +1,22 @@ +# General +*.DS_Store +*.swp +*~ + +# Dotty +*.class +*.tasty +*.hasTasty + +# sbt +target/ + +# IDE +.bsp +.bloop +.metals +.vscode + +# datasets +stackoverflow-grading.csv +wikipedia-grading.dat diff --git a/previous-exams/2021-midterm/m7/assignment.sbt b/previous-exams/2021-midterm/m7/assignment.sbt new file mode 100644 index 0000000000000000000000000000000000000000..da7eb3c8347293a18da0025fcd6060d8f8f7cc11 --- /dev/null +++ b/previous-exams/2021-midterm/m7/assignment.sbt @@ -0,0 +1,2 @@ +// Student tasks (i.e. submit, packageSubmission) +enablePlugins(StudentTasks) diff --git a/previous-exams/2021-midterm/m7/build.sbt b/previous-exams/2021-midterm/m7/build.sbt new file mode 100644 index 0000000000000000000000000000000000000000..25a0926a0020518f0905ffb468b8daccfb2489c4 --- /dev/null +++ b/previous-exams/2021-midterm/m7/build.sbt @@ -0,0 +1,12 @@ +course := "midterm" +assignment := "m7" +scalaVersion := "3.0.0-RC1" +scalacOptions ++= Seq("-language:implicitConversions", "-deprecation") + +libraryDependencies += "org.scalameta" %% "munit" % "0.7.22" + +val MUnitFramework = new TestFramework("munit.Framework") +testFrameworks += MUnitFramework +// Decode Scala names +testOptions += Tests.Argument(MUnitFramework, "-s") +testSuite := "m7.M7Suite" diff --git a/previous-exams/2021-midterm/m7/grading-tests.jar b/previous-exams/2021-midterm/m7/grading-tests.jar new file mode 100644 index 0000000000000000000000000000000000000000..4260440385397b26a3f9b531aac5592925d392b3 Binary files /dev/null and b/previous-exams/2021-midterm/m7/grading-tests.jar differ diff --git a/previous-exams/2021-midterm/m7/project/FilteringReporterPlugin.scala b/previous-exams/2021-midterm/m7/project/FilteringReporterPlugin.scala new file mode 100644 index 0000000000000000000000000000000000000000..2e4fd9a4d998698cd52643344b33a5e719dd7971 --- /dev/null +++ b/previous-exams/2021-midterm/m7/project/FilteringReporterPlugin.scala @@ -0,0 +1,31 @@ +package sbt // To access the private[sbt] compilerReporter key +package filteringReporterPlugin + +import Keys._ +import ch.epfl.lamp._ + +object FilteringReporterPlugin extends AutoPlugin { + override lazy val projectSettings = Seq( + // Turn off warning coming from scalameter that we cannot fix without changing scalameter + compilerReporter in (Compile, compile) ~= { reporter => new FilteringReporter(reporter) } + ) +} + +class FilteringReporter(reporter: xsbti.Reporter) extends xsbti.Reporter { + + def reset(): Unit = reporter.reset() + def hasErrors: Boolean = reporter.hasErrors + def hasWarnings: Boolean = reporter.hasWarnings + def printSummary(): Unit = reporter.printSummary() + def problems: Array[xsbti.Problem] = reporter.problems + + def log(problem: xsbti.Problem): Unit = { + if (!problem.message.contains("An existential type that came from a Scala-2 classfile cannot be")) + reporter.log(problem) + } + + def comment(pos: xsbti.Position, msg: String): Unit = + reporter.comment(pos, msg) + + override def toString = s"CollectingReporter($reporter)" +} diff --git a/previous-exams/2021-midterm/m7/project/MOOCSettings.scala b/previous-exams/2021-midterm/m7/project/MOOCSettings.scala new file mode 100644 index 0000000000000000000000000000000000000000..1c40443a53085d23fadb134f4e1a505c32231f1d --- /dev/null +++ b/previous-exams/2021-midterm/m7/project/MOOCSettings.scala @@ -0,0 +1,49 @@ +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 { + + override def requires = super.requires && filteringReporterPlugin.FilteringReporterPlugin + + 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") + .withRank(KeyRanks.Invisible) + // 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/previous-exams/2021-midterm/m7/project/StudentTasks.scala b/previous-exams/2021-midterm/m7/project/StudentTasks.scala new file mode 100644 index 0000000000000000000000000000000000000000..c4669afe82dd2b45651f94dcad9e736f29d21432 --- /dev/null +++ b/previous-exams/2021-midterm/m7/project/StudentTasks.scala @@ -0,0 +1,303 @@ +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") + + lazy val Grading = config("grading") extend(Runtime) + } + + + import autoImport._ + import MOOCSettings.autoImport._ + + override lazy val projectSettings = Seq( + packageSubmissionSetting, + fork := true, + connectInput in run := true, + outputStrategy := Some(StdoutOutput), + ) ++ + packageSubmissionZipSettings ++ + inConfig(Grading)(Defaults.testSettings ++ Seq( + unmanagedJars += file("grading-tests.jar"), + + definedTests := (definedTests in Test).value, + internalDependencyClasspath := (internalDependencyClasspath in Test).value + )) + + + /** ********************************************************** + * 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, None) + 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("<arg>").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 <email-address> <submit-token> + | + |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/previous-exams/2021-midterm/m7/project/build.properties b/previous-exams/2021-midterm/m7/project/build.properties new file mode 100644 index 0000000000000000000000000000000000000000..0b2e09c5ac99bd3de91b2b139b94301c2b6e26f3 --- /dev/null +++ b/previous-exams/2021-midterm/m7/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.4.7 diff --git a/previous-exams/2021-midterm/m7/project/buildSettings.sbt b/previous-exams/2021-midterm/m7/project/buildSettings.sbt new file mode 100644 index 0000000000000000000000000000000000000000..8fac702aaf3f3c4ede79691c7b4e4a52f26f3f47 --- /dev/null +++ b/previous-exams/2021-midterm/m7/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/previous-exams/2021-midterm/m7/project/plugins.sbt b/previous-exams/2021-midterm/m7/project/plugins.sbt new file mode 100644 index 0000000000000000000000000000000000000000..fb7dbe068109e7f35c13b2762b865c7eec1979f3 --- /dev/null +++ b/previous-exams/2021-midterm/m7/project/plugins.sbt @@ -0,0 +1,3 @@ +// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3") diff --git a/previous-exams/2021-midterm/m7/src/main/scala/m7/M7.scala b/previous-exams/2021-midterm/m7/src/main/scala/m7/M7.scala new file mode 100644 index 0000000000000000000000000000000000000000..20afd03b796e95251fa3c92993066c7bebfe6ea8 --- /dev/null +++ b/previous-exams/2021-midterm/m7/src/main/scala/m7/M7.scala @@ -0,0 +1,69 @@ +package m7 + +import java.util.concurrent._ +import scala.util.DynamicVariable + +trait M7 extends Lib { + + class DLLCombinerImplementation(chunk_size: Int = 3) extends DLLCombiner(chunk_size){ + + // Computes every other Integer element of data array, starting from the second, up to the middle + def task1(data: Array[Int]): ForkJoinTask[Unit] = task { + ??? + } + + // Computes every other Integer element of data array, starting from the first (index 0), up to the middle + def task2(data: Array[Int]): ForkJoinTask[Unit] = task { + ??? + } + + // Computes every other Integer element of data array, starting from the last, up to the middle + def task3(data: Array[Int]): ForkJoinTask[Unit] = task { + ??? + } + + // Computes every other Integer element of data array, starting from the second to last, up to the middle + // This is executed on the current thread. + def task4(data: Array[Int]): Unit = { + ??? + } + + def result(): Array[Int] = { + val data = new Array[Int](cnt) + + ??? + + data + } + + private def copyForward(data: Array[Int], curr: Node, from: Int, to: Int, limit: Int) = { + var current = curr + var i_from = from + var i_to = to + + while (i_to < limit) { + try { + data(i_to) = current.value(i_from) + i_to += 2 + i_from += 2 + if(i_from == current.cnt){ + current = current.next + i_from = 0 + } + else if(i_from > current.cnt) { + current = current.next + i_from = 1 + if(current.cnt == 1) { + current = current.next + i_from = 0 + } + } + } + catch{ + case e: Exception => + } + } + } + } + +} diff --git a/previous-exams/2021-midterm/m7/src/main/scala/m7/lib.scala b/previous-exams/2021-midterm/m7/src/main/scala/m7/lib.scala new file mode 100644 index 0000000000000000000000000000000000000000..20eda60c5fe1bae1271615650af917ff08929690 --- /dev/null +++ b/previous-exams/2021-midterm/m7/src/main/scala/m7/lib.scala @@ -0,0 +1,84 @@ +package m7 + +import java.util.concurrent._ +import scala.util.DynamicVariable + +trait Lib { + + class Node(val size: Int) { + var value: Array[Int] = new Array(size) + var next: Node = null + var previous: Node = null + var cnt = 0 + + def add(v: Int) = { + value(cnt) = v + cnt += 1 + } + } + + // Simplified Combiner interface + // Implements methods += and combine + // Abstract methods should be implemented in subclasses + abstract class DLLCombiner(val chunk_size: Int) { + var head: Node = null + var last: Node = null + var cnt: Int = 0 + var chunks: Int = 0 + + // Adds an Integer to the last node of this array combiner. If the last node is full, allocates a new node. + def +=(elem: Int): Unit = { + if(cnt % chunk_size == 0) { + chunks = chunks + 1 + val node = new Node(chunk_size) + if (cnt == 0) { + head = node + last = node + } + else { + last.next = node + node.previous = last + last = node + } + } + last.add(elem) + cnt += 1 + } + + // Combines this array combiner and another given combiner in constant O(1) complexity. + def combine(that: DLLCombiner): DLLCombiner = { + assert(this.chunk_size == that.chunk_size) + if (this.cnt == 0) { + this.head = that.head + this.last = that.last + this.cnt = that.cnt + this.chunks = that.chunks + + this + } + else if (that.cnt == 0) + this + else { + this.last.next = that.head + that.head.previous = this.last + + this.cnt = this.cnt + that.cnt + this.chunks = this.chunks + that.chunks + this.last = that.last + + this + } + } + + def task1(data: Array[Int]): ForkJoinTask[Unit] + def task2(data: Array[Int]): ForkJoinTask[Unit] + def task3(data: Array[Int]): ForkJoinTask[Unit] + def task4(data: Array[Int]): Unit + + def result(): Array[Int] + + } + + def task[T](body: => T): ForkJoinTask[T] + +} \ No newline at end of file diff --git a/previous-exams/2021-midterm/m7/src/test/scala/m7/M7Suite.scala b/previous-exams/2021-midterm/m7/src/test/scala/m7/M7Suite.scala new file mode 100644 index 0000000000000000000000000000000000000000..8e1c344eacca6b10c5646a7318fb1c0c19252bc5 --- /dev/null +++ b/previous-exams/2021-midterm/m7/src/test/scala/m7/M7Suite.scala @@ -0,0 +1,422 @@ +package m7 + +import java.util.concurrent._ +import scala.util.DynamicVariable + +class M7Suite extends AbstractM7Suite { + + // (70+) 30 / 500 points for correct implementation, don't check parallelism + test("[Correctness] fetch result - simple combiners (5pts)") { + assertCorrectnessSimple() + } + + test("[Correctness] fetch result - small combiners (5pts)") { + assertCorrectnessBasic() + } + + test("[Correctness] fetch result - small combiners after combining (10pts)") { + assertCorrectnessCombined() + } + + test("[Correctness] fetch result - large combiners (10pts)") { + assertCorrectnessLarge() + } + + def assertCorrectnessSimple() = { + simpleCombiners.foreach(elem => assert(compare(elem._1, elem._2))) + } + + def assertCorrectnessBasic() = { + basicCombiners.foreach(elem => assert(compare(elem._1, elem._2))) + } + + def assertCorrectnessCombined() = { + combinedCombiners.foreach(elem => assert(compare(elem._1, elem._2))) + } + + def assertCorrectnessLarge() = { + largeCombiners.foreach(elem => assert(compare(elem._1, elem._2))) + } + + // (70+30+) 50 / 500 points for correct parallel implementation, don't check if it's exactly 1/4 of the array per task + private var count = 0 + private val expected = 3 + + override def task[T](body: => T): ForkJoinTask[T] = { + count += 1 + scheduler.value.schedule(body) + } + + test("[TaskCount] number of newly created tasks should be 3 (5pts)") { + assertTaskCountSimple() + } + + test("[TaskCount] fetch result and check parallel - simple combiners (5pts)") { + assertTaskCountSimple() + assertCorrectnessSimple() + } + + test("[TaskCount] fetch result and check parallel - small combiners (10pts)") { + assertTaskCountSimple() + assertCorrectnessBasic() + } + + test("[TaskCount] fetch result and check parallel - small combiners after combining (15pts)") { + assertTaskCountSimple() + assertCorrectnessCombined() + } + + test("[TaskCount] fetch result and check parallel - large combiners (15pts)") { + assertTaskCountSimple() + assertCorrectnessLarge() + } + + def assertTaskCountSimple(): Unit = { + simpleCombiners.foreach(elem => assertTaskCount(elem._1, elem._2)) + } + + def assertTaskCount(combiner: DLLCombinerTest, array: Array[Int]): Unit = { + try { + count = 0 + build(combiner, array) + combiner.result() + assertEquals(count, expected, { + s"ERROR: Expected $expected instead of $count calls to `task(...)`" + }) + } finally { + count = 0 + } + } + + //(70+30+50+) 200 / 500 points for correct parallel implementation, exactly 1/4 of the array per task + test("[TaskFairness] each task should compute 1/4 of the result (50pts)") { + assertTaskFairness(simpleCombiners.unzip._1) + } + + test("[TaskFairness] each task should correctly compute 1/4 of the result - simple combiners (20pts)") { + assertTaskFairness(simpleCombiners.unzip._1) + assertCorrectnessSimple() + } + + test("[TaskFairness] each task should correctly compute 1/4 of the result - small combiners (30pts)") { + assertTaskFairness(basicCombiners.unzip._1) + assertCorrectnessBasic() + } + + test("[TaskFairness] each task should correctly compute 1/4 of the result - small combiners after combining (50pts)") { + assertTaskFairness(combinedCombiners.unzip._1) + assertCorrectnessCombined() + } + + test("[TaskFairness] each task should correctly compute 1/4 of the result - large combiners (50pts)") { + assertTaskFairness(largeCombiners.unzip._1) + assertCorrectnessLarge() + } + + def assertTaskFairness(combiners: List[DLLCombinerTest]): Unit = { + def assertNewTaskFairness(combiner: DLLCombinerTest, task: ForkJoinTask[Unit], data: Array[Int]) = { + var count = 0 + var expected = combiner.cnt / 4 + task.join + count = data.count(elem => elem != 0) + + assert((count - expected).abs <= 1) + } + + def assertMainTaskFairness(combiner: DLLCombinerTest, task: Unit, data: Array[Int]) = { + var count = 0 + var expected = combiner.cnt / 4 + count = data.count(elem => elem != 0) + + assert((count - expected).abs <= 1) + } + + combiners.foreach { elem => + var data = Array.fill(elem.cnt)(0) + assertNewTaskFairness(elem, elem.task1(data), data) + + data = Array.fill(elem.cnt)(0) + assertNewTaskFairness(elem, elem.task2(data), data) + + data = Array.fill(elem.cnt)(0) + assertNewTaskFairness(elem, elem.task3(data), data) + + data = Array.fill(elem.cnt)(0) + assertMainTaskFairness(elem, elem.task4(data), data) + } + } + + //(70+30+50+200+) 150 / 500 points for correct parallel implementation, exactly 1/4 of the array per task, exactly the specified quarter + + test("[TaskPrecision] each task should compute specified 1/4 of the result - simple combiners (20pts)") { + assertTaskPrecision(simpleCombiners) + } + + test("[TaskPrecision] each task should compute specified 1/4 of the result - small combiners (30pts)") { + assertTaskPrecision(basicCombiners) + } + + test("[TaskPrecision] each task should compute specified 1/4 of the result - small combiners after combining (50pts)") { + assertTaskPrecision(combinedCombiners) + } + + test("[TaskPrecision] each task should compute specified 1/4 of the result - large combiners (50pts)") { + assertTaskPrecision(largeCombiners) + } + + def assertTaskPrecision(combiners: List[(DLLCombinerTest, Array[Int])]): Unit = { + combiners.foreach { elem => + var data = Array.fill(elem._1.cnt)(0) + var ref = Array.fill(elem._1.cnt)(0) + val task1 = elem._1.task1(data) + task1.join + Range(0, elem._1.cnt).foreach(i => (if (i < elem._1.cnt / 2 - 1 && i % 2 == 1) ref(i) = elem._2(i))) + assert(Range(0, elem._1.cnt / 2 - 1).forall(i => data(i) == ref(i))) + + data = Array.fill(elem._1.cnt)(0) + ref = Array.fill(elem._1.cnt)(0) + val task2 = elem._1.task2(data) + task2.join + Range(0, elem._1.cnt).foreach(i => (if (i < elem._1.cnt / 2 - 1 && i % 2 == 0) ref(i) = elem._2(i))) + assert(Range(0, elem._1.cnt / 2 - 1).forall(i => data(i) == ref(i))) + + data = Array.fill(elem._1.cnt)(0) + ref = Array.fill(elem._1.cnt)(0) + val task3 = elem._1.task3(data) + task3.join + Range(0, elem._1.cnt).foreach(i => (if (i > elem._1.cnt / 2 + 1 && i % 2 != elem._1.cnt % 2) ref(i) = elem._2(i))) + assert(Range(elem._1.cnt / 2 + 2, elem._1.cnt).forall(i => data(i) == ref(i))) + + data = Array.fill(elem._1.cnt)(0) + ref = Array.fill(elem._1.cnt)(0) + val task4 = elem._1.task4(data) + Range(0, elem._1.cnt).foreach(i => (if (i > elem._1.cnt / 2 + 1 && i % 2 == elem._1.cnt % 2) ref(i) = elem._2(i))) + assert(Range(elem._1.cnt / 2 + 2, elem._1.cnt).forall(i => data(i) == ref(i))) + } + } + + test("[Public] fetch simple result without combining (5pts)") { + val combiner1 = DLLCombinerTest(2) + combiner1 += 7 + combiner1 += 2 + combiner1 += 3 + combiner1 += 8 + combiner1 += 1 + combiner1 += 2 + combiner1 += 3 + combiner1 += 8 + + val result = combiner1.result() + val array = Array(7, 2, 3, 8, 1, 2, 3, 8) + + assert(Range(0,array.size).forall(i => array(i) == result(i))) + } + + test("[Public] fetch result without combining (5pts)") { + val combiner1 = DLLCombinerTest(2) + combiner1 += 7 + combiner1 += 2 + combiner1 += 3 + combiner1 += 8 + combiner1 += 1 + + val result = combiner1.result() + val array = Array(7, 2, 3, 8, 1) + + assert(Range(0,array.size).forall(i => array(i) == result(i))) + } + + test("[Public] fetch result after simple combining (5pts)") { + val combiner1 = DLLCombinerTest(2) + combiner1 += 7 + combiner1 += 2 + + val combiner2 = DLLCombinerTest(2) + combiner2 += 3 + combiner2 += 8 + + val combiner3 = DLLCombinerTest(2) + combiner2 += 1 + combiner2 += 9 + + val combiner4 = DLLCombinerTest(2) + combiner2 += 3 + combiner2 += 2 + + val result = combiner1.combine(combiner2).combine(combiner3).combine(combiner4).result() + val array = Array(7, 2, 3, 8, 1, 9, 3, 2) + + assert(Range(0,array.size).forall(i => array(i) == result(i))) + } + + test("[Public] fetch result - empty combiner (20pts)") { + val combiner1 = DLLCombinerTest(2) + val result = combiner1.result() + assertEquals(result.size, 0) + } + + test("[Public] fetch result - full single element combiner (15pts)") { + val combiner1 = DLLCombinerTest(3) + combiner1 += 4 + combiner1 += 2 + combiner1 += 6 + + val result = combiner1.result() + val array = Array(4, 2, 6) + + assert(Range(0,array.size).forall(i => array(i) == result(i))) + } + +} + + +trait AbstractM7Suite extends munit.FunSuite with LibImpl { + + def simpleCombiners = buildSimpleCombiners() + def basicCombiners = buildBasicCombiners() + def combinedCombiners = buildCombinedCombiners() + def largeCombiners = buildLargeCombiners() + + def buildSimpleCombiners() = { + val simpleCombiners = List( + (DLLCombinerTest(4), Array(4, 2, 6, 1, 5, 4, 3, 5, 6, 3, 4, 5, 6, 3, 4, 5)), + (DLLCombinerTest(4), Array(7, 2, 2, 9, 3, 2, 1, 1, 1, 1, 1, 1, 1, 2, 3, 2)), + (DLLCombinerTest(4), Array.fill(16)(5)) + ) + simpleCombiners.foreach(elem => build(elem._1, elem._2)) + simpleCombiners + } + + def buildBasicCombiners() = { + val basicCombiners = List( + (DLLCombinerTest(2), Array(4, 2, 6)), + (DLLCombinerTest(5), Array(4, 2, 6)), + (DLLCombinerTest(3), Array(4, 2, 6, 1, 7, 2, 4)), + (DLLCombinerTest(4), Array(7, 2, 2, 9, 3, 2, 11, 12, 13, 14, 15, 16, 17, 22)), + (DLLCombinerTest(3), Array.fill(16)(7)), + (DLLCombinerTest(3), Array.fill(19)(7)), + (DLLCombinerTest(3), Array.fill(5)(7)), + (DLLCombinerTest(3), Array.fill(6)(7)) + ) + basicCombiners.foreach(elem => build(elem._1, elem._2)) + basicCombiners + } + + def buildCombinedCombiners() = { + var combinedCombiners = List[(DLLCombinerTest, Array[Int])]() + Range(1, 10).foreach { chunk_size => + val array = basicCombiners.filter(elem => elem._1.chunk_size == chunk_size).foldLeft(Array[Int]()) { (acc, i) => acc ++ i._2 } + val combiner = DLLCombinerTest(chunk_size) + basicCombiners.filter(elem => elem._1.chunk_size == chunk_size).foreach(elem => combiner.combine(elem._1)) + + combinedCombiners = combinedCombiners :+ (combiner, array) + } + combinedCombiners + } + + def buildLargeCombiners() = { + val largeCombiners = List( + (DLLCombinerTest(21), Array.fill(1321)(4) ++ Array.fill(1322)(7)), + (DLLCombinerTest(18), Array.fill(1341)(2) ++ Array.fill(1122)(5)), + (DLLCombinerTest(3), Array.fill(1321)(4) ++ Array.fill(1322)(7) ++ Array.fill(321)(4) ++ Array.fill(322)(7)), + (DLLCombinerTest(12), Array.fill(992321)(4) ++ Array.fill(99322)(7)), + (DLLCombinerTest(4), Array.fill(953211)(4) ++ Array.fill(999322)(1)) + ) + largeCombiners.foreach(elem => build(elem._1, elem._2)) + largeCombiners + } + + def build(combiner: DLLCombinerTest, array: Array[Int]): DLLCombinerTest = { + array.foreach(elem => combiner += elem) + combiner + } + + def compare(combiner: DLLCombinerTest, array: Array[Int]): Boolean = { + val result = combiner.result() + Range(0,array.size).forall(i => array(i) == result(i)) + } + + def buildAndCompare(combiner: DLLCombinerTest, array: Array[Int]): Boolean = { + array.foreach(elem => combiner += elem) + val result = combiner.result() + + Range(0,array.size).forall(i => array(i) == result(i)) + } + +} + +trait LibImpl extends M7 { + + val forkJoinPool = new ForkJoinPool + + abstract class TaskScheduler { + def schedule[T](body: => T): ForkJoinTask[T] + } + + class DefaultTaskScheduler extends TaskScheduler { + def schedule[T](body: => T): ForkJoinTask[T] = { + val t = new RecursiveTask[T] { + def compute = body + } + Thread.currentThread match { + case wt: ForkJoinWorkerThread => + t.fork() + case _ => + forkJoinPool.execute(t) + } + t + } + } + + val scheduler = + new DynamicVariable[TaskScheduler](new DefaultTaskScheduler) + + def task[T](body: => T): ForkJoinTask[T] = { + scheduler.value.schedule(body) + } + + class DLLCombinerTest(chunk_size: Int = 3) extends DLLCombinerImplementation(chunk_size) { + + override def +=(elem: Int): Unit = { + if(cnt % chunk_size == 0) { + chunks = chunks + 1 + val node = new Node(chunk_size) + if (cnt == 0) { + head = node + last = node + } + else { + last.next = node + node.previous = last + last = node + } + } + last.add(elem) + cnt += 1 + } + + override def combine(that: DLLCombiner): DLLCombiner = { + assert(this.chunk_size == that.chunk_size) + if (this.cnt == 0) { + this.head = that.head + this.last = that.last + this.cnt = that.cnt + this.chunks = that.chunks + + this + } + else if (that.cnt == 0) + this + else { + this.last.next = that.head + that.head.previous = this.last + + this.cnt = this.cnt + that.cnt + this.chunks = this.chunks + that.chunks + this.last = that.last + + this + } + } + } +}