Add reductions assignment

This commit is contained in:
Guillaume Martres 2019-02-19 20:44:23 +01:00
commit d92e0cb72a
17 changed files with 894 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
# General
*.DS_Store
*.swp
*~
# Dotty
*.class
*.tasty
*.hasTasty
# sbt
target/
# Dotty IDE
/.dotty-ide-artifact
/.dotty-ide.json

36
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,36 @@
# DO NOT EDIT THIS FILE
stages:
- build
- grade
compile:
stage: build
image: lampepfl/moocs:dotty-2020-02-12
except:
- tags
tags:
- cs206
script:
- sbt packageSubmission
artifacts:
expire_in: 1 day
paths:
- submission.jar
grade:
stage: grade
except:
- tags
tags:
- cs206
image:
name: smarter3/moocs:parprog1-reductions-2020-02-24-2
entrypoint: [""]
allow_failure: true
before_script:
- mkdir -p /shared/submission/
- cp submission.jar /shared/submission/submission.jar
script:
- cd /grader
- /grader/grade | /grader/feedback-printer

8
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"dotty": {
"trace": {
"remoteTracingUrl": "wss://lamppc36.epfl.ch/dotty-remote-tracer/upload/lsp.log",
"server": { "format": "JSON", "verbosity": "verbose" }
}
}
}

9
assignment.sbt Normal file
View File

@ -0,0 +1,9 @@
// Student tasks (i.e. submit, packageSubmission)
enablePlugins(StudentTasks)
courseraId := ch.epfl.lamp.CourseraId(
key = "lUUWddoGEeWPHw6r45-nxw",
itemId = "U1eU3",
premiumItemId = Some("4rXwX"),
partId = "gmSnR"
)

13
build.sbt Normal file
View File

@ -0,0 +1,13 @@
course := "parprog1"
assignment := "reductions"
scalaVersion := "0.23.0-bin-20200211-5b006fb-NIGHTLY"
scalacOptions ++= Seq("-language:implicitConversions", "-deprecation")
libraryDependencies ++= Seq(
"com.storm-enroute" %% "scalameter-core" % "0.19",
"org.scala-lang.modules" %% "scala-parallel-collections" % "0.2.0",
"com.novocode" % "junit-interface" % "0.11" % Test
).map(_.withDottyCompat(scalaVersion.value))
testOptions in Test += Tests.Argument(TestFrameworks.JUnit, "-a", "-v", "-s")
testSuite := "reductions.ReductionsSuite"

BIN
grading-tests.jar Normal file

Binary file not shown.

View File

@ -0,0 +1,46 @@
package ch.epfl.lamp
import sbt._
import sbt.Keys._
/**
* Coursera uses two versions of each assignment. They both have the same assignment key and part id but have
* different item ids.
*
* @param key Assignment key
* @param partId Assignment partId
* @param itemId Item id of the non premium version
* @param premiumItemId Item id of the premium version (`None` if the assignment is optional)
*/
case class CourseraId(key: String, partId: String, itemId: String, premiumItemId: Option[String])
/**
* Settings shared by all assignments, reused in various tasks.
*/
object MOOCSettings extends AutoPlugin {
object autoImport {
val course = SettingKey[String]("course")
val assignment = SettingKey[String]("assignment")
val options = SettingKey[Map[String, Map[String, String]]]("options")
val courseraId = settingKey[CourseraId]("Coursera-specific information identifying the assignment")
val testSuite = settingKey[String]("Fully qualified name of the test suite of this assignment")
// Convenient alias
type CourseraId = ch.epfl.lamp.CourseraId
val CourseraId = ch.epfl.lamp.CourseraId
}
import autoImport._
override val globalSettings: Seq[Def.Setting[_]] = Seq(
// supershell is verbose, buggy and useless.
useSuperShell := false
)
override val projectSettings: Seq[Def.Setting[_]] = Seq(
parallelExecution in Test := false,
// Report test result after each test instead of waiting for every test to finish
logBuffered in Test := false,
name := s"${course.value}-${assignment.value}"
)
}

318
project/StudentTasks.scala Normal file
View File

@ -0,0 +1,318 @@
package ch.epfl.lamp
import sbt._
import Keys._
// import scalaj.http._
import java.io.{File, FileInputStream, IOException}
import org.apache.commons.codec.binary.Base64
// import play.api.libs.json.{Json, JsObject, JsPath}
import scala.util.{Failure, Success, Try}
/**
* Provides tasks for submitting the assignment
*/
object StudentTasks extends AutoPlugin {
override def requires = super.requires && MOOCSettings
object autoImport {
val packageSourcesOnly = TaskKey[File]("packageSourcesOnly", "Package the sources of the project")
val packageBinWithoutResources = TaskKey[File]("packageBinWithoutResources", "Like packageBin, but without the resources")
val packageSubmissionZip = TaskKey[File]("packageSubmissionZip")
val packageSubmission = inputKey[Unit]("package solution as an archive file")
val runGradingTests = taskKey[Unit]("run black-box tests used for final grading")
}
import autoImport._
import MOOCSettings.autoImport._
override lazy val projectSettings = Seq(
packageSubmissionSetting,
// submitSetting,
runGradingTestsSettings,
fork := true,
connectInput in run := true,
outputStrategy := Some(StdoutOutput),
) ++ packageSubmissionZipSettings
lazy val runGradingTestsSettings = runGradingTests := {
val testSuiteJar = "grading-tests.jar"
if (!new File(testSuiteJar).exists) {
throw new MessageOnlyException(s"Could not find tests JarFile: $testSuiteJar")
}
val classPath = s"${(Test / dependencyClasspath).value.map(_.data).mkString(File.pathSeparator)}${File.pathSeparator}$testSuiteJar"
val junitProcess =
Fork.java.fork(
ForkOptions(),
"-cp" :: classPath ::
"org.junit.runner.JUnitCore" ::
(Test / testSuite).value ::
Nil
)
// Wait for tests to complete.
junitProcess.exitValue()
}
/** **********************************************************
* SUBMITTING A SOLUTION TO COURSERA
*/
val packageSubmissionZipSettings = Seq(
packageSubmissionZip := {
val submission = crossTarget.value / "submission.zip"
val sources = (packageSourcesOnly in Compile).value
val binaries = (packageBinWithoutResources in Compile).value
IO.zip(Seq(sources -> "sources.zip", binaries -> "binaries.jar"), submission)
submission
},
artifactClassifier in packageSourcesOnly := Some("sources"),
artifact in (Compile, packageBinWithoutResources) ~= (art => art.withName(art.name + "-without-resources"))
) ++
inConfig(Compile)(
Defaults.packageTaskSettings(packageSourcesOnly, Defaults.sourceMappings) ++
Defaults.packageTaskSettings(packageBinWithoutResources, Def.task {
val relativePaths =
(unmanagedResources in Compile).value.flatMap(Path.relativeTo((unmanagedResourceDirectories in Compile).value)(_))
(mappings in (Compile, packageBin)).value.filterNot { case (_, path) => relativePaths.contains(path) }
})
)
val maxSubmitFileSize = {
val mb = 1024 * 1024
10 * mb
}
/** Check that the jar exists, isn't empty, isn't crazy big, and can be read
* If so, encode jar as base64 so we can send it to Coursera
*/
def prepareJar(jar: File, s: TaskStreams): String = {
val errPrefix = "Error submitting assignment jar: "
val fileLength = jar.length()
if (!jar.exists()) {
s.log.error(errPrefix + "jar archive does not exist\n" + jar.getAbsolutePath)
failSubmit()
} else if (fileLength == 0L) {
s.log.error(errPrefix + "jar archive is empty\n" + jar.getAbsolutePath)
failSubmit()
} else if (fileLength > maxSubmitFileSize) {
s.log.error(errPrefix + "jar archive is too big. Allowed size: " +
maxSubmitFileSize + " bytes, found " + fileLength + " bytes.\n" +
jar.getAbsolutePath)
failSubmit()
} else {
val bytes = new Array[Byte](fileLength.toInt)
val sizeRead = try {
val is = new FileInputStream(jar)
val read = is.read(bytes)
is.close()
read
} catch {
case ex: IOException =>
s.log.error(errPrefix + "failed to read sources jar archive\n" + ex.toString)
failSubmit()
}
if (sizeRead != bytes.length) {
s.log.error(errPrefix + "failed to read the sources jar archive, size read: " + sizeRead)
failSubmit()
} else encodeBase64(bytes)
}
}
/** Task to package solution to a given file path */
lazy val packageSubmissionSetting = packageSubmission := {
val args: Seq[String] = Def.spaceDelimited("[path]").parsed
val s: TaskStreams = streams.value // for logging
val jar = (packageSubmissionZip in Compile).value
val base64Jar = prepareJar(jar, s)
val path = args.headOption.getOrElse((baseDirectory.value / "submission.jar").absolutePath)
scala.tools.nsc.io.File(path).writeAll(base64Jar)
}
/*
/** Task to submit a solution to coursera */
val submit = inputKey[Unit]("submit solution to Coursera")
lazy val submitSetting = submit := {
// Fail if scalafix linting does not pass.
scalafixLinting.value
val args: Seq[String] = Def.spaceDelimited("<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))
}

1
project/build.properties Normal file
View File

@ -0,0 +1 @@
sbt.version=1.3.8

View File

@ -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"

2
project/plugins.sbt Normal file
View File

@ -0,0 +1,2 @@
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.4.0")

View File

@ -0,0 +1,26 @@
package reductions
// Interfaces used by the grading infrastructure. Do not change signatures
// or your submission will fail with a NoSuchMethodError.
trait LineOfSightInterface {
def lineOfSight(input: Array[Float], output: Array[Float]): Unit
def upsweepSequential(input: Array[Float], from: Int, until: Int): Float
def upsweep(input: Array[Float], from: Int, end: Int, threshold: Int): Tree
def downsweepSequential(input: Array[Float], output: Array[Float], startingAngle: Float, from: Int, until: Int): Unit
def downsweep(input: Array[Float], output: Array[Float], startingAngle: Float, tree: Tree): Unit
def parLineOfSight(input: Array[Float], output: Array[Float], threshold: Int): Unit
}
trait ParallelCountChangeInterface {
def countChange(money: Int, coins: List[Int]): Int
def parCountChange(money: Int, coins: List[Int], threshold: (Int, List[Int]) => Boolean): Int
def moneyThreshold(startingMoney: Int): (Int, List[Int]) => Boolean
def totalCoinsThreshold(totalCoins: Int): (Int, List[Int]) => Boolean
def combinedThreshold(startingMoney: Int, allCoins: List[Int]): (Int, List[Int]) => Boolean
}
trait ParallelParenthesesBalancingInterface {
def balance(chars: Array[Char]): Boolean
def parBalance(chars: Array[Char], threshold: Int): Boolean
}

View File

@ -0,0 +1,89 @@
package reductions
import org.scalameter._
object LineOfSightRunner {
val standardConfig = config(
Key.exec.minWarmupRuns -> 40,
Key.exec.maxWarmupRuns -> 80,
Key.exec.benchRuns -> 100,
Key.verbose -> false
) withWarmer(new Warmer.Default)
def main(args: Array[String]): Unit = {
val length = 10000000
val input = (0 until length).map(_ % 100 * 1.0f).toArray
val output = new Array[Float](length + 1)
val seqtime = standardConfig measure {
LineOfSight.lineOfSight(input, output)
}
println(s"sequential time: $seqtime")
val partime = standardConfig measure {
LineOfSight.parLineOfSight(input, output, 10000)
}
println(s"parallel time: $partime")
println(s"speedup: ${seqtime.value / partime.value}")
}
}
sealed abstract class Tree {
def maxPrevious: Float
}
case class Node(left: Tree, right: Tree) extends Tree {
val maxPrevious = left.maxPrevious.max(right.maxPrevious)
}
case class Leaf(from: Int, until: Int, maxPrevious: Float) extends Tree
object LineOfSight extends LineOfSightInterface {
def lineOfSight(input: Array[Float], output: Array[Float]): Unit = {
???
}
/** Traverses the specified part of the array and returns the maximum angle.
*/
def upsweepSequential(input: Array[Float], from: Int, until: Int): Float = {
???
}
/** Traverses the part of the array starting at `from` and until `end`, and
* returns the reduction tree for that part of the array.
*
* The reduction tree is a `Leaf` if the length of the specified part of the
* array is smaller or equal to `threshold`, and a `Node` otherwise.
* If the specified part of the array is longer than `threshold`, then the
* work is divided and done recursively in parallel.
*/
def upsweep(input: Array[Float], from: Int, end: Int,
threshold: Int): Tree = {
???
}
/** Traverses the part of the `input` array starting at `from` and until
* `until`, and computes the maximum angle for each entry of the output array,
* given the `startingAngle`.
*/
def downsweepSequential(input: Array[Float], output: Array[Float],
startingAngle: Float, from: Int, until: Int): Unit = {
???
}
/** Pushes the maximum angle in the prefix of the array to each leaf of the
* reduction `tree` in parallel, and then calls `downsweepSequential` to write
* the `output` angles.
*/
def downsweep(input: Array[Float], output: Array[Float], startingAngle: Float,
tree: Tree): Unit = {
???
}
/** Compute the line-of-sight in parallel. */
def parLineOfSight(input: Array[Float], output: Array[Float],
threshold: Int): Unit = {
???
}
}

View File

@ -0,0 +1,79 @@
package reductions
import org.scalameter._
object ParallelCountChangeRunner {
@volatile var seqResult = 0
@volatile var parResult = 0
val standardConfig = config(
Key.exec.minWarmupRuns -> 20,
Key.exec.maxWarmupRuns -> 40,
Key.exec.benchRuns -> 80,
Key.verbose -> false
) withWarmer(new Warmer.Default)
def main(args: Array[String]): Unit = {
val amount = 250
val coins = List(1, 2, 5, 10, 20, 50)
val seqtime = standardConfig measure {
seqResult = ParallelCountChange.countChange(amount, coins)
}
println(s"sequential result = $seqResult")
println(s"sequential count time: $seqtime")
def measureParallelCountChange(threshold: => ParallelCountChange.Threshold): Unit = try {
val fjtime = standardConfig measure {
parResult = ParallelCountChange.parCountChange(amount, coins, threshold)
}
println(s"parallel result = $parResult")
println(s"parallel count time: $fjtime")
println(s"speedup: ${seqtime.value / fjtime.value}")
} catch {
case e: NotImplementedError =>
println("Not implemented.")
}
println("\n# Using moneyThreshold\n")
measureParallelCountChange(ParallelCountChange.moneyThreshold(amount))
println("\n# Using totalCoinsThreshold\n")
measureParallelCountChange(ParallelCountChange.totalCoinsThreshold(coins.length))
println("\n# Using combinedThreshold\n")
measureParallelCountChange(ParallelCountChange.combinedThreshold(amount, coins))
}
}
object ParallelCountChange extends ParallelCountChangeInterface {
/** Returns the number of ways change can be made from the specified list of
* coins for the specified amount of money.
*/
def countChange(money: Int, coins: List[Int]): Int = {
???
}
type Threshold = (Int, List[Int]) => Boolean
/** In parallel, counts the number of ways change can be made from the
* specified list of coins for the specified amount of money.
*/
def parCountChange(money: Int, coins: List[Int], threshold: Threshold): Int = {
???
}
/** Threshold heuristic based on the starting money. */
def moneyThreshold(startingMoney: Int): Threshold =
???
/** Threshold heuristic based on the total number of initial coins. */
def totalCoinsThreshold(totalCoins: Int): Threshold =
???
/** Threshold heuristic based on the starting money and the initial list of coins. */
def combinedThreshold(startingMoney: Int, allCoins: List[Int]): Threshold = {
???
}
}

View File

@ -0,0 +1,64 @@
package reductions
import scala.annotation._
import org.scalameter._
object ParallelParenthesesBalancingRunner {
@volatile var seqResult = false
@volatile var parResult = false
val standardConfig = config(
Key.exec.minWarmupRuns -> 40,
Key.exec.maxWarmupRuns -> 80,
Key.exec.benchRuns -> 120,
Key.verbose -> false
) withWarmer(new Warmer.Default)
def main(args: Array[String]): Unit = {
val length = 100000000
val chars = new Array[Char](length)
val threshold = 10000
val seqtime = standardConfig measure {
seqResult = ParallelParenthesesBalancing.balance(chars)
}
println(s"sequential result = $seqResult")
println(s"sequential balancing time: $seqtime")
val fjtime = standardConfig measure {
parResult = ParallelParenthesesBalancing.parBalance(chars, threshold)
}
println(s"parallel result = $parResult")
println(s"parallel balancing time: $fjtime")
println(s"speedup: ${seqtime.value / fjtime.value}")
}
}
object ParallelParenthesesBalancing extends ParallelParenthesesBalancingInterface {
/** Returns `true` iff the parentheses in the input `chars` are balanced.
*/
def balance(chars: Array[Char]): Boolean = {
???
}
/** Returns `true` iff the parentheses in the input `chars` are balanced.
*/
def parBalance(chars: Array[Char], threshold: Int): Boolean = {
def traverse(idx: Int, until: Int, arg1: Int, arg2: Int) /*: ???*/ = {
???
}
def reduce(from: Int, until: Int) /*: ???*/ = {
???
}
reduce(0, chars.length) == ???
}
// For those who want more:
// Prove that your reduction operator is associative!
}

View File

@ -0,0 +1,58 @@
import java.util.concurrent._
import scala.util.DynamicVariable
import org.scalameter._
package object reductions {
val forkJoinPool = new ForkJoinPool
abstract class TaskScheduler {
def schedule[T](body: => T): ForkJoinTask[T]
def parallel[A, B](taskA: => A, taskB: => B): (A, B) = {
val right = task {
taskB
}
val left = taskA
(left, right.join())
}
}
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)
}
def parallel[A, B](taskA: => A, taskB: => B): (A, B) = {
scheduler.value.parallel(taskA, taskB)
}
def parallel[A, B, C, D](taskA: => A, taskB: => B, taskC: => C, taskD: => D): (A, B, C, D) = {
val ta = task { taskA }
val tb = task { taskB }
val tc = task { taskC }
val td = taskD
(ta.join(), tb.join(), tc.join(), td)
}
// Workaround Dotty's handling of the existential type KeyValue
implicit def keyValueCoerce[T](kv: (Key[T], T)): KeyValue = {
kv.asInstanceOf[KeyValue]
}
}

View File

@ -0,0 +1,124 @@
package reductions
import java.util.concurrent._
import scala.collection._
import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory
import org.junit._
import org.junit.Assert.assertEquals
class ReductionsSuite {
/*****************
* LINE OF SIGHT *
*****************/
import LineOfSight._
@Test def `lineOfSight should correctly handle an array of size 4`: Unit = {
val output = new Array[Float](4)
lineOfSight(Array[Float](0f, 1f, 8f, 9f), output)
assertEquals(List(0f, 1f, 4f, 4f), output.toList)
}
/*******************************
* PARALLEL COUNT CHANGE SUITE *
*******************************/
import ParallelCountChange._
@Test def `countChange should return 0 for money < 0`: Unit = {
def check(money: Int, coins: List[Int]) =
assert(countChange(money, coins) == 0,
s"countChang($money, _) should be 0")
check(-1, List())
check(-1, List(1, 2, 3))
check(-Int.MinValue, List())
check(-Int.MinValue, List(1, 2, 3))
}
@Test def `countChange should return 1 when money == 0`: Unit = {
def check(coins: List[Int]) =
assert(countChange(0, coins) == 1,
s"countChang(0, _) should be 1")
check(List())
check(List(1, 2, 3))
check(List.range(1, 100))
}
@Test def `countChange should return 0 for money > 0 and coins = List()`: Unit = {
def check(money: Int) =
assert(countChange(money, List()) == 0,
s"countChang($money, List()) should be 0")
check(1)
check(Int.MaxValue)
}
@Test def `countChange should work when there is only one coin`: Unit = {
def check(money: Int, coins: List[Int], expected: Int) =
assert(countChange(money, coins) == expected,
s"countChange($money, $coins) should be $expected")
check(1, List(1), 1)
check(2, List(1), 1)
check(1, List(2), 0)
check(Int.MaxValue, List(Int.MaxValue), 1)
check(Int.MaxValue - 1, List(Int.MaxValue), 0)
}
@Test def `countChange should work for multi-coins`: Unit = {
def check(money: Int, coins: List[Int], expected: Int) =
assert(countChange(money, coins) == expected,
s"countChange($money, $coins) should be $expected")
check(50, List(1, 2, 5, 10), 341)
check(250, List(1, 2, 5, 10, 20, 50), 177863)
}
/**********************************
* PARALLEL PARENTHESES BALANCING *
**********************************/
import ParallelParenthesesBalancing._
@Test def `balance should work for empty string`: Unit = {
def check(input: String, expected: Boolean) =
assert(balance(input.toArray) == expected,
s"balance($input) should be $expected")
check("", true)
}
@Test def `balance should work for string of length 1`: Unit = {
def check(input: String, expected: Boolean) =
assert(balance(input.toArray) == expected,
s"balance($input) should be $expected")
check("(", false)
check(")", false)
check(".", true)
}
@Test def `balance should work for string of length 2`: Unit = {
def check(input: String, expected: Boolean) =
assert(balance(input.toArray) == expected,
s"balance($input) should be $expected")
check("()", true)
check(")(", false)
check("((", false)
check("))", false)
check(".)", false)
check(".(", false)
check("(.", false)
check(").", false)
}
@Rule def individualTestTimeout = new org.junit.rules.Timeout(10 * 1000)
}