Skip to content
Snippets Groups Projects
Commit 8ebf5598 authored by Dragana Milovancevic's avatar Dragana Milovancevic
Browse files

Add 2022-final

parent 5255bdeb
No related tags found
1 merge request!11Add 2022-final
Showing
with 1543 additions and 0 deletions
*.DS_Store
*.swp
*~
*.class
*.tasty
target/
logs/
.bloop
.bsp
.dotty-ide-artifact
.dotty-ide.json
.idea
.metals
.vscode
*.csv
*.dat
metals.sbt
// Student tasks (i.e. submit, packageSubmission)
enablePlugins(StudentTasks)
assignmentVersion.withRank(KeyRanks.Invisible) := "39e6c8f1"
course := "concpar"
assignment := "concpar22final01"
scalaVersion := "3.1.0"
scalacOptions ++= Seq("-language:implicitConversions", "-deprecation")
libraryDependencies += "org.scalameta" %% "munit" % "1.0.0-M3" % Test
val MUnitFramework = new TestFramework("munit.Framework")
testFrameworks += MUnitFramework
// Decode Scala names
testOptions += Tests.Argument(MUnitFramework, "-s")
package ch.epfl.lamp
import sbt._
import Keys._
import scala.util.{Failure, Success, Try}
import scalaj.http._
import play.api.libs.json.{Json, JsObject, JsPath}
/**
* 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(courseId: String, key: String, partId: String, itemId: String, premiumItemId: Option[String])
object CourseraStudent extends AutoPlugin {
override def requires = super.requires && MOOCSettings
object autoImport {
val options = SettingKey[Map[String, Map[String, String]]]("options")
val courseraId = settingKey[CourseraId]("Coursera-specific information identifying the assignment")
// Convenient alias
type CourseraId = ch.epfl.lamp.CourseraId
val CourseraId = ch.epfl.lamp.CourseraId
}
import StudentTasks.autoImport._
import MOOCSettings.autoImport._
import autoImport._
override lazy val projectSettings = Seq(
submitSetting,
)
/** 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.
StudentTasks.scalafixLinting.value
val args: Seq[String] = Def.spaceDelimited("<arg>").parsed
val s: TaskStreams = streams.value // for logging
val jar = (Compile / packageSubmissionZip).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 "progfun1" => "scala-functional-programming"
case "progfun2" => "scala-functional-program-design"
case "parprog1" => "scala-parallel-programming"
case "bigdata" => "scala-spark-big-data"
case "capstone" => "scala-capstone"
case "reactive" => "scala-akka-reactive"
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)
StudentTasks.failSubmit()
}
val base64Jar = StudentTasks.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)
}
}
}
package ch.epfl.lamp
import sbt._
import sbt.Keys._
/**
* Settings shared by all assignments, reused in various tasks.
*/
object MOOCSettings extends AutoPlugin {
object autoImport {
val course = SettingKey[String]("course")
val assignment = SettingKey[String]("assignment")
val datasetUrl = settingKey[String]("URL of the dataset used for testing")
val downloadDataset = taskKey[File]("Download the dataset required for the assignment")
val assignmentVersion = settingKey[String]("Hash string indicating the version of the assignment")
}
import autoImport._
lazy val downloadDatasetDef = downloadDataset := {
val logger = streams.value.log
datasetUrl.?.value match {
case Some(url) =>
import scalaj.http.Http
import sbt.io.IO
val dest = (Compile / resourceManaged).value / assignment.value / url.split("/").last
if (!dest.exists()) {
IO.touch(dest)
logger.info(s"Downloading $url")
val res = Http(url).method("GET")
val is = res.asBytes.body
IO.write(dest, is)
}
dest
case None =>
logger.info(s"No dataset defined in datasetUrl")
throw new sbt.MessageOnlyException("No dataset to download for this assignment")
}
}
override val projectSettings: Seq[Def.Setting[_]] = Seq(
downloadDatasetDef,
Test / parallelExecution := false,
// Report test result after each test instead of waiting for every test to finish
Test / logBuffered := false,
name := s"${course.value}-${assignment.value}"
)
}
package ch.epfl.lamp
import sbt._
import Keys._
import scalafix.sbt.ScalafixPlugin.autoImport._
import java.io.{File, FileInputStream, IOException}
import org.apache.commons.codec.binary.Base64
/**
* 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._
// Run scalafix linting after compilation to avoid seeing parser errors twice
// Keep in sync with the use of scalafix in Grader
// (--exclude doesn't work (https://github.com/lampepfl-courses/moocs/pull/28#issuecomment-427894795)
// so we customize unmanagedSources below instead)
val scalafixLinting = Def.taskDyn {
if (new File(".scalafix.conf").exists()) {
(Compile / scalafix).toTask(" --check").dependsOn(Compile / compile)
} else Def.task(())
}
val testsJar = file("grading-tests.jar")
override lazy val projectSettings = Seq(
// Run scalafix linting in parallel with the tests
(Test / test) := {
scalafixLinting.value
(Test / test).value
},
packageSubmissionSetting,
fork := true,
run / connectInput := true,
outputStrategy := Some(StdoutOutput),
scalafixConfig := {
val scalafixDotConf = (baseDirectory.value / ".scalafix.conf")
if (scalafixDotConf.exists) Some(scalafixDotConf) else None
}
) ++ packageSubmissionZipSettings ++ (
if(testsJar.exists) inConfig(Grading)(Defaults.testSettings ++ Seq(
unmanagedJars += testsJar,
definedTests := (Test / definedTests).value,
internalDependencyClasspath := (Test / internalDependencyClasspath).value,
managedClasspath := (Test / managedClasspath).value,
))
else Nil
)
val packageSubmissionZipSettings = Seq(
packageSubmissionZip := {
val submission = crossTarget.value / "submission.zip"
val sources = (Compile / packageSourcesOnly).value
val binaries = (Compile / packageBinWithoutResources).value
IO.zip(Seq(sources -> "sources.zip", binaries -> "binaries.jar"), submission, None)
submission
},
packageSourcesOnly / artifactClassifier := Some("sources"),
Compile / packageBinWithoutResources / artifact ~= (art => art.withName(art.name + "-without-resources"))
) ++
inConfig(Compile)(
Defaults.packageTaskSettings(packageSourcesOnly, Defaults.sourceMappings) ++
Defaults.packageTaskSettings(packageBinWithoutResources, Def.task {
val relativePaths =
(Compile / resources).value.flatMap(Path.relativeTo((Compile / resourceDirectories).value)(_))
(Compile / packageBin / mappings).value.filterNot { case (_, path) => relativePaths.contains(path) }
})
)
val maxSubmitFileSize = {
val mb = 1024 * 1024
10 * mb
}
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 := {
// Fail if scalafix linting does not pass.
scalafixLinting.value
val args: Seq[String] = Def.spaceDelimited("[path]").parsed
val s: TaskStreams = streams.value // for logging
val jar = (Compile / packageSubmissionZip).value
val base64Jar = prepareJar(jar, s)
val path = args.headOption.getOrElse((baseDirectory.value / "submission.jar").absolutePath)
scala.tools.nsc.io.File(path).writeAll(base64Jar)
}
def failSubmit(): Nothing = {
sys.error("Submission failed")
}
/**
* *****************
* DEALING WITH JARS
*/
def encodeBase64(bytes: Array[Byte]): String =
new String(Base64.encodeBase64(bytes))
}
sbt.version=1.6.1
// Used for Coursera submission (StudentPlugin)
libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
libraryDependencies += "com.typesafe.play" %% "play-json" % "2.9.2"
// Used for Base64 (StudentPlugin)
libraryDependencies += "commons-codec" % "commons-codec" % "1.15"
\ No newline at end of file
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.26")
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8")
package concpar22final01
trait Problem1 extends Lib:
class DLLCombinerImplementation extends DLLCombiner:
// Copies every other Integer element of data array, starting from the first (index 0), up to the middle
def task1(data: Array[Int]) = task {
var current = first
var i = 0
while current != null && i < size / 2 do
data(i) = current.value
i += 2
current = current.getNext2
}
// Copies every other Integer element of data array, starting from the second, up to the middle
def task2(data: Array[Int]) = task {
var current = second
var i = 1
while current != null && i < size / 2 do
data(i) = current.value
i += 2
current = current.getNext2
}
// Copies every other Integer element of data array, starting from the second to last, up to the middle
def task3(data: Array[Int]) = task {
var current = secondToLast
var i = size - 2
while current != null && i >= size / 2 do
data(i) = current.value
i -= 2
current = current.getPrevious2
}
// Copies 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]) =
var current = last
var i = size - 1
while current != null && i >= size / 2 do
data(i) = current.value
i -= 2
current = current.getPrevious2
def result(): Array[Int] =
val data = new Array[Int](size)
val t1 = task1(data)
val t2 = task2(data)
val t3 = task3(data)
task4(data)
t1.join()
t2.join()
t3.join()
data
package concpar22final01
import java.util.concurrent.*
import scala.util.DynamicVariable
trait Lib:
class Node(val value: Int):
protected var next: Node = null // null for last node.
protected var next2: Node = null // null for last node.
protected var previous: Node = null // null for first node.
protected var previous2: Node = null // null for first node.
def getNext: Node = next // do NOT use in the result method
def getNext2: Node = next2
def getPrevious: Node = previous // do NOT use in the result method
def getPrevious2: Node = previous2
def setNext(n: Node): Unit = next = n
def setNext2(n: Node): Unit = next2 = n
def setPrevious(n: Node): Unit = previous = n
def setPrevious2(n: Node): Unit = previous2 = n
// Simplified Combiner interface
// Implements methods += and combine
// Abstract methods should be implemented in subclasses
abstract class DLLCombiner:
var first: Node = null // null for empty lists.
var last: Node = null // null for empty lists.
var second: Node = null // null for empty lists.
var secondToLast: Node = null // null for empty lists.
var size: Int = 0
// Adds an Integer to this array combiner.
def +=(elem: Int): Unit =
val node = new Node(elem)
if size == 0 then
first = node
last = node
size = 1
else
last.setNext(node)
node.setPrevious(last)
node.setPrevious2(last.getPrevious)
if size > 1 then last.getPrevious.setNext2(node)
else second = node
secondToLast = last
last = node
size += 1
// Combines this array combiner and another given combiner in constant O(1) complexity.
def combine(that: DLLCombiner): DLLCombiner =
if this.size == 0 then that
else if that.size == 0 then this
else
this.last.setNext(that.first)
this.last.setNext2(that.first.getNext)
if this.last.getPrevious != null then
this.last.getPrevious.setNext2(that.first) // important
that.first.setPrevious(this.last)
that.first.setPrevious2(this.last.getPrevious)
if that.first.getNext != null then that.first.getNext.setPrevious2(this.last) // important
if this.size == 1 then second = that.first
this.size = this.size + that.size
this.last = that.last
this.secondToLast = that.secondToLast
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]
package concpar22final01
import java.util.concurrent.*
import scala.util.DynamicVariable
class Problem1Suite extends AbstractProblem1Suite:
test("[Public] fetch simple result without combining (2pts)") {
val combiner1 = new DLLCombinerTest
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 (2pts)") {
val combiner1 = new DLLCombinerTest
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 (2pts)") {
val combiner1 = new DLLCombinerTest
combiner1 += 7
combiner1 += 2
val combiner2 = new DLLCombinerTest
combiner2 += 3
combiner2 += 8
val combiner3 = new DLLCombinerTest
combiner3 += 1
combiner3 += 9
val combiner4 = new DLLCombinerTest
combiner4 += 3
combiner4 += 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 - small combiner (2pts)") {
val combiner1 = new DLLCombinerTest
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)))
}
// (25+) 15 / 250 points for correct implementation, don't check parallelism
test("[Correctness] fetch result - simple combiners (2pts)") {
assertCorrectnessSimple()
}
test("[Correctness] fetch result - small combiners (3pts)") {
assertCorrectnessBasic()
}
test("[Correctness] fetch result - small combiners after combining (5pts)") {
assertCorrectnessCombined()
}
test("[Correctness] fetch result - large combiners (5pts)") {
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)))
// (25+15+) 25 / 250 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 (5pts)") {
assertTaskCountSimple()
assertCorrectnessBasic()
}
test("[TaskCount] fetch result and check parallel - small combiners after combining (5pts)") {
assertTaskCountSimple()
assertCorrectnessCombined()
}
test("[TaskCount] fetch result and check parallel - large combiners (5pts)") {
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
// (25+15+25+) 50 / 250 points for correct implementation that uses only next2 and previous2, and not next and previous
test("[Skip2] fetch parallel result and check skip2 - simple combiners (10pts)") {
assertTaskCountSimple()
assertSkipSimple()
assertCorrectnessSimple()
}
test("[Skip2] fetch result and check skip2 - simple combiners (10pts)") {
assertSkipSimple()
assertCorrectnessSimple()
}
test("[Skip2] fetch result and check skip2 - small combiners (10pts)") {
assertSkipSimple()
assertCorrectnessBasic()
}
test("[Skip2] fetch result and check skip2 - small combiners after combining (10pts)") {
assertSkipSimple()
assertCorrectnessCombined()
}
test("[Skip2] fetch result and check skip2 - large combiners (10pts)") {
assertSkipSimple()
assertCorrectnessLarge()
}
def assertSkipSimple(): Unit = simpleCombiners.foreach(elem => assertSkip(elem._1, elem._2))
def assertSkip(combiner: DLLCombinerTest, array: Array[Int]): Unit =
build(combiner, array)
combiner.result()
assertEquals(
combiner.nonSkipped,
false, {
s"ERROR: Calls to 'next' and 'previous' are not allowed! You should only use 'next2` and 'previous2' in your solution."
}
)
// (25+15+25+50+) 75 / 250 points for correct parallel implementation, exactly 1/4 of the array per task
test("[TaskFairness] each task should compute 1/4 of the result (15pts)") {
assertTaskFairness(simpleCombiners.unzip._1)
}
test(
"[TaskFairness] each task should correctly compute 1/4 of the result - simple combiners (15pts)"
) {
assertTaskFairness(simpleCombiners.unzip._1)
assertCorrectnessSimple()
}
test(
"[TaskFairness] each task should correctly compute 1/4 of the result - small combiners (15pts)"
) {
assertTaskFairness(basicCombiners.unzip._1)
assertCorrectnessBasic()
}
test(
"[TaskFairness] each task should correctly compute 1/4 of the result - small combiners after combining (15pts)"
) {
assertTaskFairness(combinedCombiners.unzip._1)
assertCorrectnessCombined()
}
test(
"[TaskFairness] each task should correctly compute 1/4 of the result - large combiners (15pts)"
) {
assertTaskFairness(largeCombiners.unzip._1)
assertCorrectnessLarge()
}
def assertTaskFairness(combiners: List[DLLCombiner]): Unit =
def assertNewTaskFairness(combiner: DLLCombiner, task: ForkJoinTask[Unit], data: Array[Int]) =
var count = 0
var expected = combiner.size / 4
task.join
count = data.count(elem => elem != 0)
assert((count - expected).abs <= 1)
def assertMainTaskFairness(combiner: DLLCombiner, task: Unit, data: Array[Int]) =
var count = 0
var expected = combiner.size / 4
count = data.count(elem => elem != 0)
assert((count - expected).abs <= 1)
combiners.foreach { elem =>
var data = Array.fill(elem.size)(0)
assertNewTaskFairness(elem, elem.task1(data), data)
data = Array.fill(elem.size)(0)
assertNewTaskFairness(elem, elem.task2(data), data)
data = Array.fill(elem.size)(0)
assertNewTaskFairness(elem, elem.task3(data), data)
data = Array.fill(elem.size)(0)
assertMainTaskFairness(elem, elem.task4(data), data)
}
// (25+15+25+50+75+) 60 / 250 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 (10pts)"
) {
assertTaskPrecision(simpleCombiners)
}
test(
"[TaskPrecision] task1 should compute specified 1/4 of the result - simple combiners (5pts)"
) {
assertTaskPrecision1(simpleCombiners)
}
test(
"[TaskPrecision] task2 should compute specified 1/4 of the result - simple combiners (5pts)"
) {
assertTaskPrecision2(simpleCombiners)
}
test(
"[TaskPrecision] task3 should compute specified 1/4 of the result - simple combiners (5pts)"
) {
assertTaskPrecision3(simpleCombiners)
}
test(
"[TaskPrecision] task4 should compute specified 1/4 of the result - simple combiners (5pts)"
) {
assertTaskPrecision4(simpleCombiners)
}
test(
"[TaskPrecision] each task should compute specified 1/4 of the result - other combiners (30pts)"
) {
assertTaskPrecision(basicCombiners)
assertTaskPrecision(combinedCombiners)
assertTaskPrecision(largeCombiners)
}
def assertTaskPrecision(combiners: List[(DLLCombiner, Array[Int])]): Unit =
assertTaskPrecision1(combiners)
assertTaskPrecision2(combiners)
assertTaskPrecision3(combiners)
assertTaskPrecision4(combiners)
def assertTaskPrecision1(combiners: List[(DLLCombiner, Array[Int])]): Unit =
combiners.foreach { elem =>
var data = Array.fill(elem._1.size)(0)
var ref = Array.fill(elem._1.size)(0)
val task1 = elem._1.task1(data)
task1.join
Range(0, elem._1.size).foreach(i =>
(if i < elem._1.size / 2 - 1 && i % 2 == 0 then ref(i) = elem._2(i))
)
assert(Range(0, elem._1.size / 2 - 1).forall(i => data(i) == ref(i)))
}
def assertTaskPrecision2(combiners: List[(DLLCombiner, Array[Int])]): Unit =
combiners.foreach { elem =>
var data = Array.fill(elem._1.size)(0)
var ref = Array.fill(elem._1.size)(0)
val task2 = elem._1.task2(data)
task2.join
Range(0, elem._1.size).foreach(i =>
(if i < elem._1.size / 2 - 1 && i % 2 == 1 then ref(i) = elem._2(i))
)
assert(Range(0, elem._1.size / 2 - 1).forall(i => data(i) == ref(i)))
}
def assertTaskPrecision3(combiners: List[(DLLCombiner, Array[Int])]): Unit =
combiners.foreach { elem =>
var data = Array.fill(elem._1.size)(0)
var ref = Array.fill(elem._1.size)(0)
val task3 = elem._1.task3(data)
task3.join
Range(0, elem._1.size).foreach(i =>
(if i > elem._1.size / 2 + 1 && i % 2 == elem._1.size % 2 then ref(i) = elem._2(i))
)
assert(Range(elem._1.size / 2 + 2, elem._1.size).forall(i => data(i) == ref(i)))
}
def assertTaskPrecision4(combiners: List[(DLLCombiner, Array[Int])]): Unit =
combiners.foreach { elem =>
var data = Array.fill(elem._1.size)(0)
var ref = Array.fill(elem._1.size)(0)
val task4 = elem._1.task4(data)
Range(0, elem._1.size).foreach(i =>
(if i > elem._1.size / 2 + 1 && i % 2 != elem._1.size % 2 then ref(i) = elem._2(i))
)
assert(Range(elem._1.size / 2 + 2, elem._1.size).forall(i => data(i) == ref(i)))
}
trait AbstractProblem1Suite extends munit.FunSuite with LibImpl:
def simpleCombiners = buildSimpleCombiners()
def basicCombiners = buildBasicCombiners()
def combinedCombiners = buildCombinedCombiners()
def largeCombiners = buildLargeCombiners()
def buildSimpleCombiners() =
val simpleCombiners = List(
(new DLLCombinerTest, Array(4, 2, 6, 1, 5, 4, 3, 5, 6, 3, 4, 5, 6, 3, 4, 5)),
(new DLLCombinerTest, Array(7, 2, 2, 9, 3, 2, 1, 1, 1, 1, 1, 1, 1, 2, 3, 2)),
(new DLLCombinerTest, Array.fill(16)(5))
)
simpleCombiners.foreach(elem => build(elem._1, elem._2))
simpleCombiners
def buildBasicCombiners() =
val basicCombiners = List(
(new DLLCombinerTest, Array(4, 2, 6)),
(new DLLCombinerTest, Array(4, 1, 6)),
(new DLLCombinerTest, Array(7, 2, 2, 9, 3, 2, 11, 12, 5, 14, 15, 1, 17, 23)),
(new DLLCombinerTest, Array(7, 2, 9, 9, 3, 2, 11, 12, 13, 14, 15, 16, 17, 22)),
(new DLLCombinerTest, Array.fill(16)(7)),
(new DLLCombinerTest, Array.fill(16)(4)),
(new DLLCombinerTest, Array.fill(5)(3)),
(new DLLCombinerTest, Array.fill(5)(7)),
(new DLLCombinerTest, Array.fill(5)(4))
)
basicCombiners.foreach(elem => build(elem._1, elem._2))
basicCombiners
def buildCombinedCombiners() =
var combinedCombiners = List[(DLLCombiner, Array[Int])]()
Range(1, 10).foreach { n =>
val array = basicCombiners.filter(elem => elem._1.size == n).foldLeft(Array[Int]()) {
(acc, i) => acc ++ i._2
}
val empty: DLLCombiner = new DLLCombinerTest
val combiner = basicCombiners.filter(elem => elem._1.size == n).map(_._1).foldLeft(empty) {
(acc, c) => acc.combine(c)
}
combinedCombiners = combinedCombiners :+ (combiner, array)
}
combinedCombiners
def buildLargeCombiners() =
val largeCombiners = List(
(new DLLCombinerTest, Array.fill(1321)(4) ++ Array.fill(1322)(7)),
(new DLLCombinerTest, Array.fill(1341)(2) ++ Array.fill(1122)(5)),
(
new DLLCombinerTest,
Array.fill(1321)(4) ++ Array.fill(1322)(7) ++ Array.fill(321)(4) ++ Array.fill(322)(7)
),
(new DLLCombinerTest, Array.fill(992321)(4) ++ Array.fill(99322)(7)),
(new DLLCombinerTest, 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: DLLCombiner, 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 Problem1:
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 NodeTest(val v: Int, val myCombiner: DLLCombinerTest) extends Node(v):
override def getNext: Node =
myCombiner.nonSkipped = true
next
override def getNext2: Node = next2
override def getPrevious: Node =
myCombiner.nonSkipped = true
previous
override def getPrevious2: Node = previous2
override def setNext(n: Node): Unit = next = n
override def setNext2(n: Node): Unit = next2 = n
override def setPrevious(n: Node): Unit = previous = n
override def setPrevious2(n: Node): Unit = previous2 = n
class DLLCombinerTest extends DLLCombinerImplementation:
var nonSkipped = false
override def result(): Array[Int] =
nonSkipped = false
super.result()
override def +=(elem: Int): Unit =
val node = new NodeTest(elem, this)
if size == 0 then
first = node
last = node
size = 1
else
last.setNext(node)
node.setPrevious(last)
node.setPrevious2(last.getPrevious)
if size > 1 then last.getPrevious.setNext2(node)
else second = node
secondToLast = last
last = node
size += 1
override def combine(that: DLLCombiner): DLLCombiner =
if this.size == 0 then that
else if that.size == 0 then this
else
this.last.setNext(that.first)
this.last.setNext2(that.first.getNext)
if this.last.getPrevious != null then
this.last.getPrevious.setNext2(that.first) // important
that.first.setPrevious(this.last)
that.first.setPrevious2(this.last.getPrevious)
if that.first.getNext != null then that.first.getNext.setPrevious2(this.last) // important
if this.size == 1 then second = that.first
this.size = this.size + that.size
this.last = that.last
this.secondToLast = that.secondToLast
this
*.DS_Store
*.swp
*~
*.class
*.tasty
target/
logs/
.bloop
.bsp
.dotty-ide-artifact
.dotty-ide.json
.idea
.metals
.vscode
*.csv
*.dat
metals.sbt
// Student tasks (i.e. submit, packageSubmission)
enablePlugins(StudentTasks)
assignmentVersion.withRank(KeyRanks.Invisible) := "39e6c8f1"
course := "concpar"
assignment := "concpar22final02"
scalaVersion := "3.1.0"
scalacOptions ++= Seq("-language:implicitConversions", "-deprecation")
libraryDependencies += "org.scalameta" %% "munit" % "0.7.26" % Test
val MUnitFramework = new TestFramework("munit.Framework")
testFrameworks += MUnitFramework
// Decode Scala names
testOptions += Tests.Argument(MUnitFramework, "-s")
package ch.epfl.lamp
import sbt._
import Keys._
import scala.util.{Failure, Success, Try}
import scalaj.http._
import play.api.libs.json.{Json, JsObject, JsPath}
/**
* 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(courseId: String, key: String, partId: String, itemId: String, premiumItemId: Option[String])
object CourseraStudent extends AutoPlugin {
override def requires = super.requires && MOOCSettings
object autoImport {
val options = SettingKey[Map[String, Map[String, String]]]("options")
val courseraId = settingKey[CourseraId]("Coursera-specific information identifying the assignment")
// Convenient alias
type CourseraId = ch.epfl.lamp.CourseraId
val CourseraId = ch.epfl.lamp.CourseraId
}
import StudentTasks.autoImport._
import MOOCSettings.autoImport._
import autoImport._
override lazy val projectSettings = Seq(
submitSetting,
)
/** 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.
StudentTasks.scalafixLinting.value
val args: Seq[String] = Def.spaceDelimited("<arg>").parsed
val s: TaskStreams = streams.value // for logging
val jar = (Compile / packageSubmissionZip).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 "progfun1" => "scala-functional-programming"
case "progfun2" => "scala-functional-program-design"
case "parprog1" => "scala-parallel-programming"
case "bigdata" => "scala-spark-big-data"
case "capstone" => "scala-capstone"
case "reactive" => "scala-akka-reactive"
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)
StudentTasks.failSubmit()
}
val base64Jar = StudentTasks.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)
}
}
}
package ch.epfl.lamp
import sbt._
import sbt.Keys._
/**
* Settings shared by all assignments, reused in various tasks.
*/
object MOOCSettings extends AutoPlugin {
object autoImport {
val course = SettingKey[String]("course")
val assignment = SettingKey[String]("assignment")
val datasetUrl = settingKey[String]("URL of the dataset used for testing")
val downloadDataset = taskKey[File]("Download the dataset required for the assignment")
val assignmentVersion = settingKey[String]("Hash string indicating the version of the assignment")
}
import autoImport._
lazy val downloadDatasetDef = downloadDataset := {
val logger = streams.value.log
datasetUrl.?.value match {
case Some(url) =>
import scalaj.http.Http
import sbt.io.IO
val dest = (Compile / resourceManaged).value / assignment.value / url.split("/").last
if (!dest.exists()) {
IO.touch(dest)
logger.info(s"Downloading $url")
val res = Http(url).method("GET")
val is = res.asBytes.body
IO.write(dest, is)
}
dest
case None =>
logger.info(s"No dataset defined in datasetUrl")
throw new sbt.MessageOnlyException("No dataset to download for this assignment")
}
}
override val projectSettings: Seq[Def.Setting[_]] = Seq(
downloadDatasetDef,
Test / parallelExecution := false,
// Report test result after each test instead of waiting for every test to finish
Test / logBuffered := false,
name := s"${course.value}-${assignment.value}"
)
}
package ch.epfl.lamp
import sbt._
import Keys._
import scalafix.sbt.ScalafixPlugin.autoImport._
import java.io.{File, FileInputStream, IOException}
import org.apache.commons.codec.binary.Base64
/**
* 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._
// Run scalafix linting after compilation to avoid seeing parser errors twice
// Keep in sync with the use of scalafix in Grader
// (--exclude doesn't work (https://github.com/lampepfl-courses/moocs/pull/28#issuecomment-427894795)
// so we customize unmanagedSources below instead)
val scalafixLinting = Def.taskDyn {
if (new File(".scalafix.conf").exists()) {
(Compile / scalafix).toTask(" --check").dependsOn(Compile / compile)
} else Def.task(())
}
val testsJar = file("grading-tests.jar")
override lazy val projectSettings = Seq(
// Run scalafix linting in parallel with the tests
(Test / test) := {
scalafixLinting.value
(Test / test).value
},
packageSubmissionSetting,
fork := true,
run / connectInput := true,
outputStrategy := Some(StdoutOutput),
scalafixConfig := {
val scalafixDotConf = (baseDirectory.value / ".scalafix.conf")
if (scalafixDotConf.exists) Some(scalafixDotConf) else None
}
) ++ packageSubmissionZipSettings ++ (
if(testsJar.exists) inConfig(Grading)(Defaults.testSettings ++ Seq(
unmanagedJars += testsJar,
definedTests := (Test / definedTests).value,
internalDependencyClasspath := (Test / internalDependencyClasspath).value,
managedClasspath := (Test / managedClasspath).value,
))
else Nil
)
val packageSubmissionZipSettings = Seq(
packageSubmissionZip := {
val submission = crossTarget.value / "submission.zip"
val sources = (Compile / packageSourcesOnly).value
val binaries = (Compile / packageBinWithoutResources).value
IO.zip(Seq(sources -> "sources.zip", binaries -> "binaries.jar"), submission, None)
submission
},
packageSourcesOnly / artifactClassifier := Some("sources"),
Compile / packageBinWithoutResources / artifact ~= (art => art.withName(art.name + "-without-resources"))
) ++
inConfig(Compile)(
Defaults.packageTaskSettings(packageSourcesOnly, Defaults.sourceMappings) ++
Defaults.packageTaskSettings(packageBinWithoutResources, Def.task {
val relativePaths =
(Compile / resources).value.flatMap(Path.relativeTo((Compile / resourceDirectories).value)(_))
(Compile / packageBin / mappings).value.filterNot { case (_, path) => relativePaths.contains(path) }
})
)
val maxSubmitFileSize = {
val mb = 1024 * 1024
10 * mb
}
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 := {
// Fail if scalafix linting does not pass.
scalafixLinting.value
val args: Seq[String] = Def.spaceDelimited("[path]").parsed
val s: TaskStreams = streams.value // for logging
val jar = (Compile / packageSubmissionZip).value
val base64Jar = prepareJar(jar, s)
val path = args.headOption.getOrElse((baseDirectory.value / "submission.jar").absolutePath)
scala.tools.nsc.io.File(path).writeAll(base64Jar)
}
def failSubmit(): Nothing = {
sys.error("Submission failed")
}
/**
* *****************
* DEALING WITH JARS
*/
def encodeBase64(bytes: Array[Byte]): String =
new String(Base64.encodeBase64(bytes))
}
sbt.version=1.6.1
// Used for Coursera submission (StudentPlugin)
libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
libraryDependencies += "com.typesafe.play" %% "play-json" % "2.9.2"
// Used for Base64 (StudentPlugin)
libraryDependencies += "commons-codec" % "commons-codec" % "1.15"
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment