Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: SITZ/Coursera
base: d36465f513
...
head fork: SITZ/Coursera
compare: 57c5a9d49b
Checking mergeability… Don't worry, you can still create the pull request.
  • 2 commits
  • 24 files changed
  • 0 commit comments
  • 1 contributor
Showing with 1,742 additions and 0 deletions.
  1. +10 −0 progfun/recfun/.classpath
  2. +12 −0 progfun/recfun/.project
  3. +4 −0 progfun/recfun/.settings/org.scala-ide.sdt.core.prefs
  4. +76 −0 progfun/recfun/build.sbt
  5. BIN  progfun/recfun/lib_managed/jars/junit/junit/junit-4.10.jar
  6. BIN  progfun/recfun/lib_managed/jars/org.hamcrest/hamcrest-core/hamcrest-core-1.1.jar
  7. BIN  progfun/recfun/lib_managed/jars/org.scalatest/scalatest_2.9.2/scalatest_2.9.2-1.8.jar
  8. +223 −0 progfun/recfun/project/CourseraHttp.scala
  9. +221 −0 progfun/recfun/project/GradingFeedback.scala
  10. +586 −0 progfun/recfun/project/ProgFunBuild.scala
  11. +35 −0 progfun/recfun/project/RecordingLogger.scala
  12. +28 −0 progfun/recfun/project/RichJsValue.scala
  13. +121 −0 progfun/recfun/project/ScalaTestRunner.scala
  14. +34 −0 progfun/recfun/project/Settings.scala
  15. +70 −0 progfun/recfun/project/StyleChecker.scala
  16. +1 −0  progfun/recfun/project/build.properties
  17. +72 −0 progfun/recfun/project/buildSettings.sbt
  18. +2 −0  progfun/recfun/project/project/buildPlugins.sbt
  19. +135 −0 progfun/recfun/project/scalastyle_config.xml
  20. +9 −0 progfun/recfun/src/main/scala/common/package.scala
  21. +28 −0 progfun/recfun/src/main/scala/recfun/Main.scala
  22. +27 −0 progfun/recfun/src/test/scala/recfun/BalanceSuite.scala
  23. +26 −0 progfun/recfun/src/test/scala/recfun/CountChangeSuite.scala
  24. +22 −0 progfun/recfun/src/test/scala/recfun/PascalSuite.scala
View
10 progfun/recfun/.classpath
@@ -0,0 +1,10 @@
+<classpath>
+ <classpathentry output="target/scala-2.9.2/classes" path="src/main/scala" kind="src"></classpathentry>
+ <classpathentry output="target/scala-2.9.2/test-classes" path="src/test/scala" kind="src"></classpathentry>
+ <classpathentry kind="con" path="org.scala-ide.sdt.launching.SCALA_CONTAINER"></classpathentry>
+ <classpathentry path="./lib_managed/jars/org.scalatest/scalatest_2.9.2/scalatest_2.9.2-1.8.jar" kind="lib"></classpathentry>
+ <classpathentry path="./lib_managed/jars/junit/junit/junit-4.10.jar" kind="lib"></classpathentry>
+ <classpathentry path="./lib_managed/jars/org.hamcrest/hamcrest-core/hamcrest-core-1.1.jar" kind="lib"></classpathentry>
+ <classpathentry path="org.eclipse.jdt.launching.JRE_CONTAINER" kind="con"></classpathentry>
+ <classpathentry path="bin" kind="output"></classpathentry>
+</classpath>
View
12 progfun/recfun/.project
@@ -0,0 +1,12 @@
+<projectDescription>
+ <name>progfun-recfun</name>
+ <buildSpec>
+ <buildCommand>
+ <name>org.scala-ide.sdt.core.scalabuilder</name>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.scala-ide.sdt.core.scalanature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
View
4 progfun/recfun/.settings/org.scala-ide.sdt.core.prefs
@@ -0,0 +1,4 @@
+#Generated by sbteclipse
+#Wed Sep 19 02:36:10 CEST 2012
+deprecation=true
+scala.compiler.useProjectSettings=true
View
76 progfun/recfun/build.sbt
@@ -0,0 +1,76 @@
+name <<= submitProjectName(pname => "progfun-"+ pname)
+
+version := "1.0.0"
+
+scalaVersion := "2.9.2"
+
+scalacOptions ++= Seq("-deprecation")
+
+libraryDependencies += "org.scalatest" %% "scalatest" % "1.8" % "test"
+
+libraryDependencies += "junit" % "junit" % "4.10" % "test"
+
+// This setting defines the project to which a solution is submitted. When creating a
+// handout, the 'createHandout' task will make sure that its value is correct.
+submitProjectName := "recfun"
+
+// See documentation in ProgFunBuild.scala
+projectDetailsMap := Map(
+ "example" -> ProjectDetails(
+ packageName = "example",
+ assignmentPartId = "fTzFogNl",
+ assignmentPartIdNumber = 5,
+ maxScore = 10d,
+ styleScoreRatio = 0.2),
+ "recfun" -> ProjectDetails(
+ packageName = "recfun",
+ assignmentPartId = "3Rarn9Ki",
+ assignmentPartIdNumber = 7,
+ maxScore = 10d,
+ styleScoreRatio = 0.2),
+ "funsets" -> ProjectDetails(
+ packageName = "funsets",
+ assignmentPartId = "fBXOL6Qd", // TODO
+ assignmentPartIdNumber = 6, // TODO
+ maxScore = 10d,
+ styleScoreRatio = 0.2),
+ "objsets" -> ProjectDetails(
+ packageName = "objsets",
+ assignmentPartId = "fBXOL6Qd", // TODO
+ assignmentPartIdNumber = 6, // TODO
+ maxScore = 10d,
+ styleScoreRatio = 0.2)
+)
+
+// Files that we hand out to the students
+handoutFiles <<= (baseDirectory, projectDetailsMap, commonSourcePackages) map { (basedir, detailsMap, commonSrcs) =>
+ (projectName: String) => {
+ val details = detailsMap.getOrElse(projectName, sys.error("Unknown project name: "+ projectName))
+ val commonFiles = (PathFinder.empty /: commonSrcs)((files, pkg) =>
+ files +++ (basedir / "src" / "main" / "scala" / pkg ** "*.scala")
+ )
+ (basedir / "src" / "main" / "scala" / details.packageName ** "*.scala") +++
+ commonFiles +++
+ (basedir / "src" / "test" / "scala" / details.packageName ** "*.scala") +++
+ (basedir / "build.sbt") +++
+ (basedir / "project" / "build.properties") +++
+ (basedir / "project" ** ("*.scala" || "*.sbt")) +++
+ (basedir / "project" / "scalastyle_config.xml") +++
+ (basedir / "lib_managed" ** "*.jar") +++
+ (basedir * (".classpath" || ".project")) +++
+ (basedir / ".settings" / "org.scala-ide.sdt.core.prefs")
+ }
+}
+
+// This setting allows to restrict the source files that are compiled and tested
+// to one specific project. It should be either the empty string, in which case all
+// projects are included, or one of the project names from the projectDetailsMap.
+currentProject := ""
+
+// Packages in src/main/scala that are used in every project. Included in every
+// handout, submission.
+commonSourcePackages += "common"
+
+// Packages in src/test/scala that are used for grading projects. Always included
+// compiling tests, grading a project.
+gradingTestPackages += "grading"
View
BIN  progfun/recfun/lib_managed/jars/junit/junit/junit-4.10.jar
Binary file not shown
View
BIN  progfun/recfun/lib_managed/jars/org.hamcrest/hamcrest-core/hamcrest-core-1.1.jar
Binary file not shown
View
BIN  progfun/recfun/lib_managed/jars/org.scalatest/scalatest_2.9.2/scalatest_2.9.2-1.8.jar
Binary file not shown
View
223 progfun/recfun/project/CourseraHttp.scala
@@ -0,0 +1,223 @@
+import dispatch.{Request, Http, NoLogging, StatusCode, url}
+import cc.spray.json.{JsNull, JsonParser, DefaultJsonProtocol, JsValue}
+import RichJsValue._
+import org.apache.commons.codec.digest.DigestUtils
+import org.apache.commons.codec.binary.{Hex, Base64}
+import java.io.{IOException, File, FileInputStream}
+import scalaz.Scalaz.{mkIdentity, ValidationNEL}
+
+import Settings._
+
+case class JsonSubmission(api_state: String, user_info: JsValue, submission_metadata: JsValue, solutions: JsValue, submission_encoding: String, submission: String)
+//case class JsonQueueResult(submission: JsonSubmission)
+object SubmitJsonProtocol extends DefaultJsonProtocol {
+ implicit val jsonSubmissionFormat = jsonFormat6(JsonSubmission)
+// implicit val jsonQueueResultFormat = jsonFormat1(JsonQueueResult)
+}
+
+object CourseraHttp {
+ private lazy val http = new Http with NoLogging
+
+ private def executeRequest[T](req: Request)(parse: String => ValidationNEL[String, T]): ValidationNEL[String, T] = {
+ try {
+ http(req >- { res =>
+ parse(res)
+ })
+ } catch {
+ case ex: IOException =>
+ ("Connection failed\n"+ ex.toString()).failNel
+ case StatusCode(code, message) =>
+ ("HTTP failed with status "+ code +"\n"+ message).failNel
+ }
+ }
+
+
+ /******************************
+ * SUBMITTING
+ */
+
+ def getChallenge(email: String, assignmentPartId: String): ValidationNEL[String, Challenge] = {
+ val baseReq = url(challengeUrl)
+ val withArgs = baseReq << Map("email_address" -> email,
+ "assignment_part_sid" -> assignmentPartId,
+ "response_encoding" -> "delim")
+
+ executeRequest(withArgs) { res =>
+ // example result. there might be an `aux_data` value at the end.
+ // |email_address|a@b.com|challenge_key|XXYYXXYYXXYY|state|XXYYXXYYXXYY|challenge_aux_data|
+ val parts = res.split('|').filterNot(_.isEmpty)
+ if (parts.length < 7)
+ ("Unexpected challenge format: \n"+ res).failNel
+ else
+ Challenge(parts(1), parts(3), parts(5)).successNel
+ }
+ }
+
+ def submitSolution(sourcesJar: File, assignmentPartId: String, challenge: Challenge, chResponse: String): ValidationNEL[String, String] = {
+ val fileLength = sourcesJar.length()
+ if (!sourcesJar.exists()) {
+ ("Sources jar archive does not exist\n"+ sourcesJar.getAbsolutePath).failNel
+ } else if (fileLength == 0l) {
+ ("Sources jar archive is empty\n"+ sourcesJar.getAbsolutePath).failNel
+ } else if (fileLength > maxSubmitFileSize) {
+ ("Sources jar archive is too big. Allowed size: "+
+ maxSubmitFileSize +" bytes, found "+ fileLength +" bytes.\n"+
+ sourcesJar.getAbsolutePath).failNel
+ } else {
+ val bytes = new Array[Byte](fileLength.toInt)
+ val sizeRead = try {
+ val is = new FileInputStream(sourcesJar)
+ val read = is.read(bytes)
+ is.close()
+ read
+ } catch {
+ case ex: IOException =>
+ ("Failed to read sources jar archive\n"+ ex.toString()).failNel
+ }
+ if (sizeRead != bytes.length) {
+ ("Failed to read the sources jar archive, size read: "+ sizeRead).failNel
+ } else {
+ val fileData = encodeBase64(bytes)
+ val baseReq = url(submitUrl)
+ val withArgs = baseReq << Map("assignment_part_sid" -> assignmentPartId,
+ "email_address" -> challenge.email,
+ "submission" -> fileData,
+ "submission_aux" -> "",
+ "challenge_response" -> chResponse,
+ "state" -> challenge.state)
+ executeRequest(withArgs) { res =>
+ // the API returns HTTP 200 even if there are failures, how impolite...
+ if (res.contains("Your submission has been accepted"))
+ res.successNel
+ else
+ res.failNel
+ }
+ }
+ }
+ }
+
+ def challengeResponse(challenge: Challenge, otPassword: String): String =
+ shaHexDigest(challenge.challengeKey + otPassword)
+
+
+ /********************************
+ * DOWNLOADING SUBMISSIONS
+ */
+
+ // def downloadFromQueue(queue: String, targetJar: File, apiKey: String): ValidationNEL[String, QueueResult] = {
+ // val baseReq = url(Settings.submitQueueUrl)
+ // val withArgsAndHeader = baseReq << Map("queue" -> queue) <:< Map("X-api-key" -> apiKey)
+
+ // executeRequest(withArgsAndHeader) { res =>
+ // extractJson(res, targetJar)
+ // }
+ // }
+
+ def readJsonFile(jsonFile: File, targetJar: File): ValidationNEL[String, QueueResult] = {
+ extractJson(sbt.IO.read(jsonFile), targetJar)
+ }
+
+ def extractJson(jsonData: String, targetJar: File): ValidationNEL[String, QueueResult] = {
+ import SubmitJsonProtocol._
+ for {
+ jsonSubmission <- {
+ try {
+ val parsed = JsonParser(jsonData)
+ val submission = parsed \ "submission"
+ if (submission == JsNull) {
+ ("Nothing to grade, queue is empty.").failNel
+ } else {
+ submission.convertTo[JsonSubmission].successNel
+ }
+ } catch {
+ case e: Exception =>
+ ("Could not parse submission\n"+ jsonData +"\n"+ fullExceptionString(e)).failNel
+ }
+ }
+ queueResult <- {
+ val encodedFile = jsonSubmission.submission
+ val jarContent = decodeBase64(encodedFile)
+ try {
+ sbt.IO.write(targetJar, jarContent)
+ QueueResult(jsonSubmission.api_state).successNel
+ } catch {
+ case e: IOException =>
+ ("Failed to write jar file to "+ targetJar.getAbsolutePath +"\n"+ e.toString).failNel
+ }
+ }
+ } yield queueResult
+ }
+
+ def unpackJar(file: File, targetDirectory: File): ValidationNEL[String, Unit] = {
+ try {
+ val files = sbt.IO.unzip(file, targetDirectory)
+ if (files.isEmpty)
+ ("No files found when unpacking jar file "+ file.getAbsolutePath).failNel
+ else
+ ().successNel
+ } catch {
+ case e: IOException =>
+ val msg = "Error while unpacking the jar file "+ file.getAbsolutePath +" to "+ targetDirectory.getAbsolutePath +"\n"+ e.toString
+ if (Settings.offlineMode) {
+ println("[offline mode] "+ msg)
+ ().successNel
+ } else {
+ msg.failNel
+ }
+ }
+ }
+
+
+ /********************************
+ * SUBMITTING GRADES
+ */
+
+ def submitGrade(feedback: String, score: String, apiState: String, apiKey: String): ValidationNEL[String, Unit] = {
+ import DefaultJsonProtocol._
+ val baseReq = url(Settings.uploadFeedbackUrl)
+ val withArgs = baseReq << Map("api_state" -> apiState, "score" -> score, "feedback" -> feedback) <:< Map("X-api-key" -> apiKey)
+ executeRequest(withArgs) { res =>
+ try {
+ val js = JsonParser(res)
+ val status = (js \ "status").convertTo[String]
+ if (status == "202")
+ ().successNel
+ else
+ ("Unexpected result from submit request: "+ status).failNel
+ } catch {
+ case e: Exception =>
+ ("Failed to parse response while submitting grade\n"+ res +"\n"+ fullExceptionString(e)).failNel
+ }
+ }
+ }
+
+
+ /*********************************
+ * TOOLS AND STUFF
+ */
+
+ def shaHexDigest(s: String): String = {
+ val chars = Hex.encodeHex(DigestUtils.sha(s))
+ new String(chars)
+ }
+
+
+ def fullExceptionString(e: Throwable) =
+ e.toString +"\n"+ e.getStackTrace.map(_.toString).mkString("\n")
+
+
+ /* Base 64 tools */
+
+ def encodeBase64(bytes: Array[Byte]): String =
+ new String(Base64.encodeBase64(bytes))
+
+ def decodeBase64(str: String): Array[Byte] = {
+ // codecs 1.4 has a version accepting a string, but not 1.2; jar hell.
+ Base64.decodeBase64(str.getBytes)
+ }
+}
+
+case class Challenge(email: String, challengeKey: String, state: String)
+
+case class QueueResult(apiState: String)
+
View
221 progfun/recfun/project/GradingFeedback.scala
@@ -0,0 +1,221 @@
+import collection.mutable.ListBuffer
+import org.apache.commons.lang3.StringEscapeUtils
+
+object GradingFeedback {
+
+ private val feedbackSummary = new ListBuffer[String]()
+ private val feedbackDetails = new ListBuffer[String]()
+
+ private def addSummary(msg: String) { feedbackSummary += msg; feedbackSummary += "\n\n" }
+ private def addDetails(msg: String) { feedbackDetails += msg; feedbackDetails += "\n\n" }
+
+ /**
+ * Converts the string to HTML - coursera displays the feedback in an html page.
+ */
+ def feedbackString(html: Boolean = true) = {
+ val total = totalGradeMessage(totalScore) + "\n\n"
+ // trim removes the newlines at the end
+ val s = (total + feedbackSummary.mkString + feedbackDetails.mkString).trim
+ if (html)
+ "<pre>"+ StringEscapeUtils.escapeHtml4(s) +"</pre>"
+ else
+ s
+ }
+
+ private var vTestScore: Double = 0d
+ private var vStyleScore: Double = 0d
+ def totalScore = vTestScore + vStyleScore
+
+ private var vMaxTestScore: Double = 0d
+ private var vMaxStyleScore: Double = 0d
+ def maxTestScore = vMaxTestScore
+ def maxStyleScore = vMaxStyleScore
+
+ // a string obtained from coursera when downloading an assignment. it has to be
+ // used again when uploading the grade.
+ var apiState: String = ""
+
+ /**
+ * `failed` means that there was an unexpected error during grading. This includes
+ * - student's code does not compile
+ * - our tests don't compile (against the student's code)
+ * - crash while executing ScalaTest (not test failures, but problems trying to run the tests!)
+ * - crash while executing the style checker (again, not finding style problems!)
+ *
+ * When failed is `true`, later grading stages will not be executed: this is handled automatically
+ * by SBT, tasks depending on a failed one are not run.
+ *
+ * However, these dependent tasks still fail (i.e. mapR on them is invoked). The variable below
+ * allows us to know if something failed before. In this case, we don't add any more things to
+ * the log. (see `ProgFunBuild.handleFailure`)
+ */
+ private var failed = false
+ def isFailed = failed
+
+ def initialize() {
+ feedbackSummary.clear()
+ feedbackDetails.clear()
+ vTestScore = 0d
+ vStyleScore = 0d
+ apiState = ""
+ failed = false
+ }
+
+ def setMaxScore(maxScore: Double, styleScoreRatio: Double) {
+ vMaxTestScore = maxScore * (1-styleScoreRatio)
+ vMaxStyleScore = maxScore * styleScoreRatio
+ }
+
+
+ /* Methods to build up the feedback log */
+
+ def downloadUnpackFailed(log: String) {
+ failed = true
+ addSummary(downloadUnpackFailedMessage)
+ addDetails("======== FAILURES WHILE DOWNLOADING OR EXTRACTING THE SUBMISSION ========")
+ addDetails(log)
+ }
+
+
+ def compileFailed(log: String) {
+ failed = true
+ addSummary(compileFailedMessage)
+ addDetails("======== COMPILATION FAILURES ========")
+ addDetails(log)
+ }
+
+ def testCompileFailed(log: String) {
+ failed = true
+ addSummary(testCompileFailedMessage)
+ addDetails("======== TEST COMPILATION FAILURES ========")
+ addDetails(log)
+ }
+
+
+
+ def allTestsPassed() {
+ addSummary(allTestsPassedMessage)
+ vTestScore = maxTestScore
+ }
+
+ def testsFailed(log: String, score: Double) {
+ addSummary(testsFailedMessage(score))
+ vTestScore = score
+ addDetails("======== LOG OF FAILED TESTS ========")
+ addDetails(log)
+ }
+
+ def testExecutionFailed(log: String) {
+ failed = true
+ addSummary(testExecutionFailedMessage)
+ addDetails("======== ERROR LOG OF TESTING TOOL ========")
+ addDetails(log)
+ }
+
+ def testExecutionDebugLog(log: String) {
+ addDetails("======== DEBUG OUTPUT OF TESTING TOOL ========")
+ addDetails(log)
+ }
+
+
+
+ def perfectStyle() {
+ addSummary(perfectStyleMessage)
+ vStyleScore = maxStyleScore
+ }
+
+ def styleProblems(log: String, score: Double) {
+ addSummary(styleProblemsMessage(score))
+ vStyleScore = score
+ addDetails("======== CODING STYLE ISSUES ========")
+ addDetails(log)
+ }
+
+
+
+ /* Feedback Messages */
+
+ private val downloadUnpackFailedMessage =
+ """We were not able to download your submission from the coursera servers, or extracting the
+ |archive containing your source code failed.
+ |
+ |If you see this error message as your grade feedback, please contact one of the teaching
+ |assistants. See below for a detailed error log.""".stripMargin
+
+ private val compileFailedMessage =
+ """We were not able to compile the source code you submitted. This is not expected to happen,
+ |because the `submit` command in SBT can only be executed if your source code compiles.
+ |
+ |Please verify the following points:
+ | - You should use the `submit` command in SBT to upload your solution
+ | - You should not perform any changes to the SBT project definition files, i.e. the *.sbt
+ | files, and the files in the `project/` directory
+ |
+ |Take a careful look at the compiler output below - maybe you can find out what the problem is.
+ |
+ |If you cannot find a solution, ask for help on the discussion forums on the course website:
+ | %s""".stripMargin.format(Settings.forumUrl)
+
+
+ private val testCompileFailedMessage =
+ """We were not able to compile our tests, and therefore we could not correct your submission.
+ |
+ |The most likely reason for this problem is that your submitted code uses different names
+ |for methods, classes, objects or different types than expected.
+ |
+ |In principle, this can only arise if you changed some names or types in the code that we
+ |provide, for instance a method name or a parameter type.
+ |
+ |To diagnose your problem, perform the following steps:
+ | - Run the tests that we provide with our hand-out. These tests verify that all names and
+ | types are correct. In case these tests pass, but you still see this message, please post
+ | a report on the forums [1].
+ | - Take a careful look at the error messages from the Scala compiler below. They should give
+ | you a hint where your code has an unexpected shape.
+ |
+ |If you cannot find a solution, ask for help on the discussion forums [1] on the course website.
+ |
+ |[1] Course forum: %s""".stripMargin.format(Settings.forumUrl)
+
+
+ private def testsFailedMessage(score: Double) =
+ """The code you submitted did not pass all of our tests: your submission achieved a score of
+ |%.2f out of %.2f in our tests.
+ |
+ |In order to find bugs in your code, we advise to perform the following steps:
+ | - Take a close look at the test output that you can find below: it should point you to
+ | the part of your code that has bugs.
+ | - Run the tests that we provide with the handout on your code.
+ | - The tests we provide do not test your code in depth: they are very incomplete. In order
+ | to test more aspects of your code, write your own unit tests.
+ | - Take another very careful look at the assignment description. Try to find out if you
+ | misunderstood parts of it. While reading through the assignment, write more tests.
+ |
+ |Below you can find a short feedback for every individual test that failed.""".stripMargin.format(score, vMaxTestScore)
+
+ // def so that we read the right value of vMaxTestScore (initialize modifies it)
+ private def allTestsPassedMessage =
+ """Your solution passed all of our tests, congratulations! You obtained the maximal test
+ |score of %.2f.""".stripMargin.format(vMaxTestScore)
+
+ private val testExecutionFailedMessage =
+ """An error occured while running our tests on your submission. This is not expected to
+ |happen, it means there is a bug in our testing environment.
+ |
+ |In order for us to help you, please contact one of the teaching assistants and send
+ |them the entire feedback message that you recieved.""".stripMargin
+
+ // def so that we read the right value of vMaxStyleScore (initialize modifies it)
+ private def perfectStyleMessage =
+ """Our automated style checker tool could not find any issues with your code. You obtained the maximal
+ |style score of %.2f.""".stripMargin.format(vMaxStyleScore)
+
+
+ private def styleProblemsMessage(score: Double) =
+ """Our automated style checker tool found issues in your code with respect to coding style: it
+ |computed a style score of %.2f out of %.2f for your submission. See below for detailed feedback.""".stripMargin.format(score, vMaxStyleScore)
+
+
+ private def totalGradeMessage(score: Double) =
+ """Your overall score for this assignment is %.2f out of %.2f""".format(score, vMaxTestScore + vMaxStyleScore)
+}
View
586 progfun/recfun/project/ProgFunBuild.scala
@@ -0,0 +1,586 @@
+import sbt._
+import Keys._
+
+import scalaz.Scalaz.mkIdentity
+import scalaz.{Success, Failure}
+import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
+
+/**
+ * See README.md for high-level overview
+ *
+ * Libraries Doc Links
+ *
+ * Coursera API
+ * - http://support.coursera.org/customer/portal/articles/573466-programming-assignments
+ * - the python script 'submit.py' that can be downloaded from the above site
+ *
+ * SBT
+ * - https://github.com/harrah/xsbt/wiki/Getting-Started-Full-Def
+ * - https://github.com/harrah/xsbt/wiki/Getting-Started-Custom-Settings
+ * - https://github.com/harrah/xsbt/wiki/Getting-Started-More-About-Settings
+ * - https://github.com/harrah/xsbt/wiki/Input-Tasks
+ * - https://github.com/harrah/xsbt/wiki/Tasks
+ * - http://harrah.github.com/xsbt/latest/api/index.html
+ * - https://groups.google.com/forum/?fromgroups#!forum/simple-build-tool
+ *
+ * Dispatch
+ * - http://dispatch-classic.databinder.net/Response+Bodies.html
+ * - http://www.flotsam.nl/dispatch-periodic-table.html
+ * - http://databinder.net/dispatch-doc/
+ *
+ * Scalaz
+ * - http://www.lunatech-research.com/archives/2012/03/02/validation-scala
+ * - http://scalaz.github.com/scalaz/scalaz-2.9.1-6.0.4/doc/index.html#scalaz.Validation
+ *
+ * Apache Commons Codec 1.4
+ * - http://www.jarvana.com/jarvana/view/commons-codec/commons-codec/1.4/commons-codec-1.4-javadoc.jar!/index.html
+ *
+ * Scalatest
+ * - http://doc.scalatest.org/1.8/index.html#org.scalatest.package
+ */
+object ProgFunBuild extends Build {
+
+ /***********************************************************
+ * MAIN PROJECT DEFINITION
+ */
+
+ lazy val assignmentProject = Project(id = "assignment", base = file(".")) settings(
+ // 'submit' depends on 'packageSrc', so needs to be a project-level setting: on build-level, 'packageSrc' is not defined
+ submitSetting,
+ createHandoutSetting,
+ // put all libs in the lib_managed directory, that way we can distribute eclipse project files
+ retrieveManaged := true,
+ EclipseKeys.relativizeLibs := true,
+ // Avoid generating eclipse source entries for the java directories
+ (unmanagedSourceDirectories in Compile) <<= (scalaSource in Compile)(Seq(_)),
+ (unmanagedSourceDirectories in Test) <<= (scalaSource in Test)(Seq(_)),
+ commonSourcePackages := Seq(), // see build.sbt
+ gradingTestPackages := Seq(), // see build.sbt
+ selectMainSources,
+ selectTestSources,
+ scalaTestSetting,
+ styleCheckSetting
+ )
+
+
+ /***********************************************************
+ * SETTINGS AND TASKS
+ */
+
+ /** The 'submit' task uses this project name (defined in the build.sbt file) to know where to submit the solution */
+ val submitProjectName = SettingKey[String]("submitProjectName")
+
+ /** Project-specific settings, see main build.sbt */
+ val projectDetailsMap = SettingKey[Map[String, ProjectDetails]]("projectDetailsMap")
+
+ /**
+ * The files that are handed out to students. Accepts a string denoting the project name for
+ * which a handout will be generated.
+ */
+ val handoutFiles = TaskKey[String => PathFinder]("handoutFiles")
+
+ /**
+ * This setting allows to restrict the source files that are compiled and tested
+ * to one specific project. It should be either the empty string, in which case all
+ * projects are included, or one of the project names from the projectDetailsMap.
+ */
+ val currentProject = SettingKey[String]("currentProject")
+
+ /** Package names of source packages common for all projects, see comment in build.sbt */
+ val commonSourcePackages = SettingKey[Seq[String]]("commonSourcePackages")
+
+ /** Package names of test sources for grading, see comment in build.sbt */
+ val gradingTestPackages = SettingKey[Seq[String]]("gradingTestPackages")
+
+ /************************************************************
+ * SUBMITTING A SOLUTION TO COURSERA
+ */
+
+
+ /** Task to submit a solution to coursera */
+ val submit = InputKey[Unit]("submit")
+
+ lazy val submitSetting = submit <<= inputTask { argTask =>
+ (argTask, compile in Compile, currentProject, packageSrc in Compile, submitProjectName, projectDetailsMap, streams) map { (args, _, currentProject, sourcesJar, projectName, detailsMap, s) =>
+ if (currentProject != "") {
+ val msg =
+ """The 'currentProject' setting is not empty: '%s'
+ |
+ |This error only appears if there are mistakes in the build scripts. Please re-download the assignment
+ |from the coursera webiste. Make sure that you did not perform any changes to the build files in the
+ |`project/` directory. If this error persits, ask for help on the course forums.""".format(currentProject).stripMargin +"\n "
+ s.log.error(msg)
+ failSubmit()
+ } else args match {
+ case email :: otPassword :: Nil =>
+ lazy val wrongNameMsg =
+ """Unknown project name: %s
+ |
+ |This error only appears if there are mistakes in the build scripts. Please re-download the assignment
+ |from the coursera webiste. Make sure that you did not perform any changes to the build files in the
+ |`project/` directory. If this error persits, ask for help on the course forums.""".format(projectName).stripMargin +"\n "
+ // log strips empty lines at the ond of `msg`. to have one we add "\n "
+ val details = detailsMap.getOrElse(projectName, {s.log.error(wrongNameMsg); failSubmit()})
+ submitSources(sourcesJar, details.assignmentPartId, email, otPassword, s.log)
+ case _ =>
+ val msg =
+ """No e-mail address and / or submission password provided. The required syntax for `submit` is
+ | submit <e-mail> <submissionPassword>
+ |
+ |The submission password, which is NOT YOUR LOGIN PASSWORD, can be obtained from the assignment page
+ | https://class.coursera.org/%s/assignment/index""".format(Settings.courseId).stripMargin +"\n "
+ s.log.error(msg)
+ failSubmit()
+ }
+ }
+ }
+
+
+ def submitSources(sourcesJar: File, partId: String, email: String, otPassword: String, logger: Logger) {
+ import CourseraHttp._
+ logger.info("Connecting to coursera. Obtaining challenge...")
+ val res = for {
+ challenge <- getChallenge(email, partId)
+ chResponse <- {
+ logger.info("Computing challenge response...")
+ challengeResponse(challenge, otPassword).successNel[String]
+ }
+ response <- {
+ logger.info("Submitting solution...")
+ submitSolution(sourcesJar, partId, challenge, chResponse)
+ }
+ } yield response
+
+ res match {
+ case Failure(msgs) =>
+ for (msg <- msgs.list) logger.error(msg)
+ failSubmit()
+ case Success(response) =>
+ logger.success("Your code was successfully submitted: "+ response)
+ }
+ }
+
+
+ def failSubmit(): Nothing = {
+ sys.error("Submission failed")
+ }
+
+
+
+ /***********************************************************
+ * CREATE THE HANDOUT ZIP FILE
+ */
+
+ val createHandout = InputKey[File]("createHandout")
+
+ // depends on "compile in Test" to make sure everything compiles. also makes sure that
+ // all dependencies are downloaded, because we pack the .jar files into the handout.
+ lazy val createHandoutSetting = createHandout <<= inputTask { argTask =>
+ (argTask, currentProject, baseDirectory, handoutFiles, submitProjectName, target, projectDetailsMap, compile in Test) map { (args, currentProject, basedir, filesFinder, submitProject, targetDir, detailsMap, _) =>
+ if (currentProject != "")
+ sys.error("\nthe 'currentProject' setting in build.sbt needs to be \"\" in order to create a handout")
+ else args match {
+ case handoutProjectName :: eclipseDone :: Nil if eclipseDone == "eclipseWasCalled" =>
+ if (handoutProjectName != submitProject)
+ sys.error("\nThe `submitProjectName` setting in `build.sbt` must match the project name for which a handout is generated\n ")
+ val files = filesFinder(handoutProjectName).get
+ val filesWithRelativeNames = files.x_!(relativeTo(basedir)) map {
+ case (file, name) => (file, handoutProjectName+"/"+name)
+ }
+ val targetZip = targetDir / (handoutProjectName +".zip")
+ IO.zip(filesWithRelativeNames, targetZip)
+ targetZip
+ case _ =>
+ val msg ="""
+ |
+ |Failed to create handout. Syntax: `createHandout <projectName> <eclipseWasCalled>`
+ |
+ |Valid project names are: %s
+ |
+ |The argument <eclipseWasCalled> needs to be the string "eclipseWasCalled". This is to remind
+ |you that you **need** to manually run the `eclipse` command before running `createHandout`.
+ | """.stripMargin.format(detailsMap.keys.mkString(", "))
+ sys.error(msg)
+ }
+ }
+ }
+
+
+ /************************************************************
+ * LIMITING SOURCES TO CURRENT PROJECT
+ */
+
+ def filter(basedir: File, packages: Seq[String]) = new FileFilter {
+ def accept(file: File) = {
+ IO.relativize(basedir, file) match {
+ case Some(str) =>
+ packages exists { pkg =>
+ str.startsWith(pkg)
+ }
+ case _ =>
+ sys.error("unexpected test file: "+ file)
+ }
+ }
+ }
+
+ def projectFiles(allFiles: Seq[File], basedir: File, projectName: String, globalPackages: Seq[String], detailsMap: Map[String, ProjectDetails]) = {
+ if (projectName == "") allFiles
+ else detailsMap.get(projectName) match {
+ case Some(project) =>
+ val finder = allFiles ** filter(basedir, globalPackages :+ project.packageName)
+ finder.get
+ case None =>
+ sys.error("currentProject is set to an invalid name: "+ projectName)
+ }
+ }
+
+ /**
+ * Only include source files of 'currentProject', helpful when preparign a specific assignment.
+ * Also keeps the source packages in 'commonSourcePackages'.
+ */
+ val selectMainSources = {
+ (unmanagedSources in Compile) <<= (unmanagedSources in Compile, scalaSource in Compile, projectDetailsMap, currentProject, commonSourcePackages) map { (sources, srcMainScalaDir, detailsMap, projectName, commonSrcs) =>
+ projectFiles(sources, srcMainScalaDir, projectName, commonSrcs, detailsMap)
+ }
+ }
+
+ /**
+ * Only include the test files which are defined in the package of the current project.
+ * Also keeps test sources in packages listed in 'gradingTestPackages'.
+ */
+ val selectTestSources = {
+ (unmanagedSources in Test) <<= (unmanagedSources in Test, scalaSource in Test, projectDetailsMap, currentProject, gradingTestPackages) map { (sources, srcTestScalaDir, detailsMap, projectName, gradingSrcs) =>
+ projectFiles(sources, srcTestScalaDir, projectName, gradingSrcs, detailsMap)
+ }
+ }
+
+
+ /************************************************************
+ * RUNNING WEIGHTED SCALATEST & STYLE CHECKER ON DEVELOPMENT SOURCES
+ */
+
+
+ val scalaTest = TaskKey[Unit]("scalaTest")
+ val scalaTestSetting = scalaTest <<=
+ (compile in Compile,
+ compile in Test,
+ fullClasspath in Test,
+ classDirectory in Test,
+ baseDirectory,
+ streams) map { (_, _, classpath, testClasses, basedir, s) =>
+ // we use `map`, so this is only executed if all dependencies succeed. no need to check `GradingFeedback.isFailed`
+ val logger = s.log
+ val outfile = basedir / Settings.testResultsFileName
+ val policyFile = basedir / Settings.policyFileName
+ val (score, maxScore, feedback, runLog) = ScalaTestRunner.runScalaTest(classpath, testClasses, outfile, policyFile, logger.error(_))
+ logger.info(feedback)
+ logger.info("Test Score: "+ score +" out of "+ maxScore)
+ if (!runLog.isEmpty) {
+ logger.info("Console output of ScalaTest process")
+ logger.info(runLog)
+ }
+ }
+
+ val styleCheck = TaskKey[Unit]("styleCheck")
+
+ /**
+ * depend on compile to make sure the sources pass the compiler
+ */
+ val styleCheckSetting = styleCheck <<= (compile in Compile, sources in Compile, streams) map { (_, sourceFiles, s) =>
+ val logger = s.log
+ val (feedback, score) = StyleChecker.assess(sourceFiles)
+ logger.info(feedback)
+ logger.info("Style Score: "+ score +" out of "+ StyleChecker.maxResult)
+ }
+
+
+ /************************************************************
+ * PROJECT DEFINITION FOR GRADING
+ */
+
+ lazy val submissionProject = Project(id = "submission", base = file(Settings.submissionDirName)) settings(
+ /** settings we take over from the assignment project */
+ version <<= (version in assignmentProject),
+ name <<= (name in assignmentProject),
+ scalaVersion <<= (scalaVersion in assignmentProject),
+ scalacOptions <<= (scalacOptions in assignmentProject),
+ libraryDependencies <<= (libraryDependencies in assignmentProject),
+
+ /** settings specific to the grading project */
+ initGradingSetting,
+ // default value, don't change. see comment on `val assignmentPartIdNumber`
+ assignmentPartIdNumber := -1,
+ gradeProjectDetailsSetting,
+ setMaxScoreSetting,
+ setMaxScoreHook,
+ // default value, don't change. see comment on `val apiKey`
+ apiKey := "",
+ getSubmissionSetting,
+ getSubmissionHook,
+ submissionLoggerSetting,
+ readCompileLog,
+ readTestCompileLog,
+ testSourcesFromAssignment,
+ selectTestsForProject,
+ scalaTestSubmissionSetting,
+ styleCheckSubmissionSetting,
+ gradeSetting,
+ EclipseKeys.skipProject := true
+ )
+
+ /**
+ * The assignment part id number of the project to be graded. Don't hard code this setting in .sbt or .scala!!!
+ * It should be set manually using 'set', explained in the README.md. In principle, we'd like to use an InputTask,
+ * but that's not possible, https://groups.google.com/forum/?fromgroups#!topic/simple-build-tool/3ymStbIdhZ0
+ */
+ val assignmentPartIdNumber = SettingKey[Int]("assignmentPartIdNumber")
+
+ /**
+ * The api key to access non-public api parts on coursera. This key is secret! It's defined in
+ * 'submission/settings.sbt', which is not part of the handout.
+ *
+ * Default value 'apiKey' to make the handout sbt project work
+ * - In the handout, apiKey needs to be defined, otherwise the build doesn't compile
+ * - When correcting, we define 'apiKey' in the 'submission/sectrets.sbt' file
+ * - The value in the .sbt file will take precedence when correcting (settings in .sbt take
+ * precedence over those in .scala)
+ */
+ val apiKey = SettingKey[String]("apiKey")
+
+
+ /************************************************************
+ * GRADING INITIALIZATION
+ */
+
+ val initGrading = TaskKey[Unit]("initGrading")
+ lazy val initGradingSetting = initGrading <<= (clean, sourceDirectory, baseDirectory) map { (_, submissionSrcDir, basedir) =>
+ deleteFiles(submissionSrcDir, basedir)
+ GradingFeedback.initialize()
+ RecordingLogger.clear()
+ }
+
+ def deleteFiles(submissionSrcDir: File, basedir: File) {
+ // don't delete anything in offline mode, useful for us when hacking testing / stylechecking
+ if (!Settings.offlineMode){
+ IO.delete(submissionSrcDir)
+ IO.delete(basedir / Settings.submissionJarFileName)
+ IO.delete(basedir / Settings.testResultsFileName)
+ }
+ }
+
+ /** ProjectDetails of the project that we are grading */
+ val gradeProjectDetails = TaskKey[ProjectDetails]("gradeProjectDetails")
+
+ // here we depend on `initialize` because we already use the GradingFeedback
+ lazy val gradeProjectDetailsSetting = gradeProjectDetails <<= (initGrading, assignmentPartIdNumber, projectDetailsMap in assignmentProject) map { (_, partIdNum, detailsMap) =>
+ detailsMap.find(_._2.assignmentPartIdNumber == partIdNum) match {
+ case Some((_, details)) =>
+ details
+ case None =>
+ val idNums = detailsMap.map(_._2.assignmentPartIdNumber)
+ val msgRaw =
+ """Unknown assignment part id number: %s
+ |Valid part id numbers are: %s
+ |
+ |In order to grade a project, the `assignmentPartIdNumber` setting has to be defined. If you are running
+ |interactively in the sbt console, type `set (assignmentPartIdNumber in submissionProject) := 1`. When
+ |running the grading task from the command line, add the above `set` command, e.g. execute
+ |
+ | sbt 'set (assignmentPartIdNumber in submissionProject) := 1' submission/grade"""
+ val msg = msgRaw.stripMargin.format(partIdNum, idNums.mkString(", ")) + "\n "
+ GradingFeedback.downloadUnpackFailed(msg)
+ sys.error(msg)
+ }
+ }
+
+ val setMaxScore = TaskKey[Unit]("setMaxScore")
+ val setMaxScoreSetting = setMaxScore <<= (gradeProjectDetails) map { project =>
+ GradingFeedback.setMaxScore(project.maxScore, project.styleScoreRatio)
+ }
+
+ // set the maximal score before running compile / test / ...
+ val setMaxScoreHook = (compile in Compile) <<= (compile in Compile).dependsOn(setMaxScore)
+
+
+ /************************************************************
+ * DOWNLOADING AND EXTRACTING SUBMISSION
+ */
+
+ val getSubmission = TaskKey[Unit]("getSubmission")
+ val getSubmissionSetting = getSubmission <<= (baseDirectory, gradeProjectDetails, scalaSource in Compile) map { (baseDir, project, scalaSrcDir) =>
+ readAndUnpackSubmission(baseDir, scalaSrcDir)
+ }
+
+ def readAndUnpackSubmission(baseDir: File, targetSourceDir: File) {
+ try {
+ val jsonFile = baseDir / Settings.submissionJsonFileName
+ val targetJar = baseDir / Settings.submissionJarFileName
+ val res = for {
+ queueResult <- {
+ if (Settings.offlineMode) {
+ println("[not unpacking from json file]")
+ QueueResult("").successNel
+ } else {
+ CourseraHttp.readJsonFile(jsonFile, targetJar)
+ }
+ }
+ _ <- {
+ GradingFeedback.apiState = queueResult.apiState
+ CourseraHttp.unpackJar(targetJar, targetSourceDir)
+ }
+ } yield ()
+
+ res match {
+ case Failure(msgs) =>
+ GradingFeedback.downloadUnpackFailed(msgs.list.mkString("\n"))
+ case _ =>
+ ()
+ }
+ } catch {
+ case e: Throwable =>
+ // generate some useful feedback in case something fails
+ GradingFeedback.downloadUnpackFailed(CourseraHttp.fullExceptionString(e))
+ throw e
+ }
+ if (GradingFeedback.isFailed) failDownloadUnpack()
+ }
+
+ // dependsOn makes sure that `getSubmission` is executed *before* `unmanagedSources`
+ val getSubmissionHook = (unmanagedSources in Compile) <<= (unmanagedSources in Compile).dependsOn(getSubmission)
+
+ def failDownloadUnpack(): Nothing = {
+ sys.error("Download or Unpack failed")
+ }
+
+ /************************************************************
+ * READING COMPILATION AND TEST COMPILATION LOGS
+ */
+
+
+ // extraLoggers need to be defined globally. (extraLoggers in Compile) does not work - sbt only
+ // looks at the global extraLoggers when creating the LogManager.
+ val submissionLoggerSetting = extraLoggers ~= { currentFunction =>
+ (key: ScopedKey[_]) => {
+ new FullLogger(RecordingLogger) +: currentFunction(key)
+ }
+ }
+
+ val readCompileLog = (compile in Compile) <<= (compile in Compile) mapR handleFailure(compileFailed)
+ val readTestCompileLog = (compile in Test) <<= (compile in Test) mapR handleFailure(compileTestFailed)
+
+ def handleFailure[R](handler: (Incomplete, String) => Unit) = (res: Result[R]) => res match {
+ case Inc(inc) =>
+ // Only call the handler of the task that actually failed. See comment in GradingFeedback.failed
+ if (!GradingFeedback.isFailed)
+ handler(inc, RecordingLogger.readAndClear())
+ throw inc
+ case Value(v) => v
+ }
+
+ def compileFailed(inc: Incomplete, log: String) {
+ GradingFeedback.compileFailed(log)
+ }
+
+ def compileTestFailed(inc: Incomplete, log: String) {
+ GradingFeedback.testCompileFailed(log)
+ }
+
+
+ /************************************************************
+ * RUNNING SCALATEST
+ */
+
+ /** The submission project takes test files from the main (assignment) project */
+ val testSourcesFromAssignment = {
+ (sourceDirectory in Test) <<= (sourceDirectory in (assignmentProject, Test))
+ }
+
+ /**
+ * Only include the test files which are defined in the package of the current project.
+ * Also keeps test sources in packages listed in 'gradingTestPackages'
+ */
+ val selectTestsForProject = {
+ (unmanagedSources in Test) <<= (unmanagedSources in Test, scalaSource in (assignmentProject, Test), gradingTestPackages in assignmentProject, gradeProjectDetails) map { (sources, testSrcScalaDir, gradingSrcs, project) =>
+ val finder = sources ** filter(testSrcScalaDir, gradingSrcs :+ project.packageName)
+ finder.get
+ }
+ }
+
+ val scalaTestSubmission = TaskKey[Unit]("scalaTestSubmission")
+ val scalaTestSubmissionSetting = scalaTestSubmission <<=
+ (compile in Compile,
+ compile in Test,
+ fullClasspath in Test,
+ classDirectory in Test,
+ baseDirectory) map { (_, _, classpath, testClasses, basedir) =>
+ // we use `map`, so this is only executed if all dependencies succeed. no need to check `GradingFeedback.isFailed`
+ val outfile = basedir / Settings.testResultsFileName
+ val policyFile = basedir / ".." / Settings.policyFileName
+ ScalaTestRunner.scalaTestGrade(classpath, testClasses, outfile, policyFile)
+ }
+
+
+
+ /************************************************************
+ * STYLE CHECKING
+ */
+
+ val styleCheckSubmission = TaskKey[Unit]("styleCheckSubmission")
+
+ /**
+ * - depend on scalaTestSubmission so that test get executed before style checking. the transitive
+ * dependencies also ensures that the "sources in Compile" don't have compilation errors
+ * - using `map` makes this task execute only if all its dependencies succeeded.
+ */
+ val styleCheckSubmissionSetting = styleCheckSubmission <<= (sources in Compile, scalaTestSubmission) map { (sourceFiles, _) =>
+ val (feedback, score) = StyleChecker.assess(sourceFiles)
+ if (score == StyleChecker.maxResult) {
+ GradingFeedback.perfectStyle()
+ } else {
+ val gradeScore = GradingFeedback.maxStyleScore * score / StyleChecker.maxResult
+ GradingFeedback.styleProblems(feedback, gradeScore)
+ }
+ }
+
+
+
+ /************************************************************
+ * SUBMITTING GRADES TO COURSERA
+ */
+
+ val grade = TaskKey[Unit]("grade")
+
+ // mapR: submit the grade / feedback in any case, also on failure
+ val gradeSetting = grade <<= (scalaTestSubmission, styleCheckSubmission, apiKey, streams) mapR { (_, _, apiKeyR, s) =>
+ val logOpt = s match {
+ case Value(v) => Some(v.log)
+ case _ => None
+ }
+ logOpt.foreach(_.info(GradingFeedback.feedbackString(html = false)))
+ apiKeyR match {
+ case Value(apiKey) if (!apiKey.isEmpty) =>
+ // if build failed early, we did not even get the api key from the submission queue
+ if (!GradingFeedback.apiState.isEmpty && !Settings.offlineMode) {
+ val scoreString = "%.2f".format(GradingFeedback.totalScore)
+ CourseraHttp.submitGrade(GradingFeedback.feedbackString(), scoreString, GradingFeedback.apiState, apiKey) match {
+ case Failure(msgs) =>
+ sys.error(msgs.list.mkString("\n"))
+ case _ =>
+ ()
+ }
+ } else if(Settings.offlineMode) {
+ logOpt.foreach(_.info(" \nSettings.offlineMode enabled, not uploading the feedback"))
+ } else {
+ sys.error("Could not submit feedback - apiState not initialized")
+ }
+ case _ =>
+ sys.error("Could not submit feedback - apiKey not defined: "+ apiKeyR)
+ }
+ }
+}
+
+case class ProjectDetails(packageName: String,
+ assignmentPartId: String,
+ assignmentPartIdNumber: Int,
+ maxScore: Double,
+ styleScoreRatio: Double)
View
35 progfun/recfun/project/RecordingLogger.scala
@@ -0,0 +1,35 @@
+import sbt._
+import collection.mutable.ListBuffer
+
+/**
+ * Logger to capture compiler output, test output
+ */
+
+object RecordingLogger extends Logger {
+ private val buffer = ListBuffer[String]()
+
+ def hasErrors = buffer.nonEmpty
+
+ def readAndClear() = {
+ val res = buffer.mkString("\n")
+ buffer.clear()
+ res
+ }
+
+ def clear() {
+ buffer.clear()
+ }
+
+ def log(level: Level.Value, message: => String) =
+ if (level == Level.Error) {
+ buffer += message
+ }
+
+ // we don't log success here
+ def success(message: => String) = ()
+
+ // invoked when a task throws an exception. invoked late, when the exception is logged, i.e.
+ // just before returning to the prompt. therefore we do nothing: storing the exception in the
+ // buffer would happen *after* the `handleFailure` reads the buffer.
+ def trace(t: => Throwable) = ()
+}
View
28 progfun/recfun/project/RichJsValue.scala
@@ -0,0 +1,28 @@
+import cc.spray.json._
+
+class RichJsValue(js: JsValue) {
+ def \ (name: String): JsValue = js match {
+ case JsObject(fields) =>
+ fields(name)
+ case _ =>
+ throw new IllegalArgumentException("Cannot select field "+ name +" from non-JsObject "+ js)
+ }
+
+ def hasFieldNamed(name: String) = js match {
+ case JsObject(fields) =>
+ fields.contains(name)
+ case _ =>
+ false
+ }
+
+ def arrayValues: List[JsValue] = js match {
+ case JsArray(values) =>
+ values
+ case _ =>
+ throw new IllegalArgumentException("Trying to select values from non-JsArray"+ js)
+ }
+}
+
+object RichJsValue {
+ implicit def enrichJsValue(js: JsValue) = new RichJsValue(js)
+}
View
121 progfun/recfun/project/ScalaTestRunner.scala
@@ -0,0 +1,121 @@
+import sbt._
+import Keys._
+import sys.process.{Process => SysProc, ProcessLogger}
+import java.util.concurrent._
+import collection.mutable.ListBuffer
+
+object ScalaTestRunner {
+
+ private def forkProcess(proc: SysProc, timeout: Int) {
+ val executor = Executors.newSingleThreadExecutor()
+ val future: Future[Unit] = executor.submit(new Callable[Unit] {
+ def call { proc.exitValue() }
+ })
+ try {
+ future.get(timeout, TimeUnit.SECONDS)
+ } catch {
+ case to: TimeoutException =>
+ future.cancel(true)
+ throw to
+ } finally {
+ executor.shutdown()
+ }
+ }
+
+ private def runPathString(file: File) = file.getAbsolutePath().replace(" ", "\\ ")
+
+ private def extractWeights(s: String, logError: String => Unit) = {
+ try {
+ val (nums, rest) = s.span(c => c != '\n')
+ val List(grade, max) = nums.split(';').toList
+ (grade.toInt, max.toInt, rest.drop(1))
+ } catch {
+ case e: Throwable =>
+ val msg = "Could not extract weight from grading feedback\n"+ s
+ logError(msg)
+ throw e
+ }
+ }
+
+
+ def runScalaTest(classpath: Classpath, testClasses: File, outfile: File, policyFile: File, logError: String => Unit) = {
+ val classpathString = classpath map {
+ case Attributed(file) => file.getAbsolutePath()
+ } mkString(":")
+
+ val testRunpath = runPathString(testClasses)
+
+ val outfileStr = outfile.getAbsolutePath
+ val policyFileStr = policyFile.getAbsolutePath
+ // Deleting the file is helpful: it makes reading the file below crash in case ScalaTest doesn't
+ // run as expected. Problem is, it's hard to detect if ScalaTest ran successfully or not: it
+ // exits with non-zero if there are failed tests, and also if it crashes...
+ new java.io.File(outfileStr).delete()
+
+ // we don't specify "-w packageToTest" - the build file only compiles the tests
+ // for the current project. so we don't need to do it again here.
+ val cmd = "java" ::
+ "-Djava.security.manager" :: "-Djava.security.policy="+ policyFileStr ::
+ "-DscalaTestReportFile="+ outfileStr ::
+ "-cp" :: classpathString ::
+ "org.scalatest.tools.Runner" ::
+ "-R" :: testRunpath ::
+ "-C" :: "grading.CourseraReporter" ::
+ Nil
+
+ // process deadlocks in Runner.PassFailReporter.allTestsPassed on runDoneSemaphore.acquire() when
+ // something is wrong, e.g. when there's an error.. So we have to run it with a timeout.
+
+ val out = new ListBuffer[String]()
+ var p: SysProc = null
+ try {
+ p = SysProc(cmd).run(ProcessLogger(out.append(_), out.append(_)))
+ forkProcess(p, Settings.scalaTestTimeout)
+ } catch {
+ case e: TimeoutException =>
+ val msg = "Timeout when running ScalaTest"+ out.mkString("\n","\n", "")
+ logError(msg)
+ p.destroy()
+ sys.error(msg)
+
+ case e: Throwable =>
+ val msg = "Error occured while running the ScalaTest command\n"+ e.toString + out.mkString("\n","\n", "")
+ logError(msg)
+ p.destroy()
+ throw e
+ }
+
+
+ val feedbackFileContent = try {
+ io.Source.fromFile(outfileStr).mkString
+ } catch {
+ case e: Throwable =>
+ val msg = "Error occured while reading the output file of ScalaTest\n"+ e.toString + out.mkString("\n","\n", "")
+ logError(msg)
+ throw e
+ }
+
+ val (score, maxScore, feedback) = extractWeights(feedbackFileContent, logError)
+ val runLog = out.mkString("\n").trim
+ (score, maxScore, feedback, runLog)
+ }
+
+ def scalaTestGrade(classpath: Classpath, testClasses: File, outfile: File, policyFile: File) {
+ val (score, maxScore, feedback, runLog) = runScalaTest(classpath, testClasses, outfile, policyFile, GradingFeedback.testExecutionFailed)
+ if (score == maxScore) {
+ GradingFeedback.allTestsPassed()
+ } else {
+ val scaledScore = GradingFeedback.maxTestScore * score / maxScore
+ GradingFeedback.testsFailed(feedback, scaledScore)
+ }
+
+ // The output `out` should in principle be empty: the reporter we use writes its results to a file.
+ // however, `out` contains valuable logs in case scalatest fails. We need to put them into the student
+ // feedback in order to have a chance of debugging problems.
+
+ if (!runLog.isEmpty) {
+ GradingFeedback.testExecutionDebugLog(runLog)
+ }
+ }
+}
+
View
34 progfun/recfun/project/Settings.scala
@@ -0,0 +1,34 @@
+object Settings {
+ // when changing this, also look at 'scripts/gradingImpl' and the files in s3/settings
+ val courseId = "progfun-2012-001"
+
+ val challengeUrl = "https://class.coursera.org/"+ courseId +"/assignment/challenge"
+
+ val submitUrl = "https://class.coursera.org/"+ courseId +"/assignment/submit"
+
+ val forumUrl = "https://class.coursera.org/"+ courseId +"/forum/index"
+
+ val submitQueueUrl = "https://class.coursera.org/"+ courseId +"/assignment/api/pending_submission"
+
+ val uploadFeedbackUrl = "https://class.coursera.org/"+ courseId +"/assignment/api/score"
+
+ val maxSubmitFileSize = {
+ val mb = 1024 * 1024
+ 10 * mb
+ }
+
+ val submissionDirName = "submission"
+
+ val testResultsFileName = "scalaTestLog.txt"
+ val policyFileName = "allowAllPolicy"
+ val submissionJsonFileName = "submission.json"
+ val submissionJarFileName = "submittedSrc.jar"
+
+ // time in seconds that we give scalatest for running
+ val scalaTestTimeout = 120
+
+ // debugging / developping options
+
+ // don't decode json and unpack the submission sources, don't upload feedback
+ val offlineMode = false
+}
View
70 progfun/recfun/project/StyleChecker.scala
@@ -0,0 +1,70 @@
+import sbt.File
+import java.io.ByteArrayOutputStream
+import java.io.PrintStream
+import org.scalastyle._
+
+object StyleChecker {
+ val maxResult = 100
+
+ class CustomTextOutput[T <: FileSpec]() extends Output[T] {
+ private val messageHelper = new MessageHelper(this.getClass().getClassLoader())
+
+ var fileCount: Int = _
+ override def message(m: Message[T]): Unit = m match {
+ case StartWork() =>
+ case EndWork() =>
+ case StartFile(file) =>
+ print("Checking file " + file + "...")
+ fileCount = 0
+ case EndFile(file) =>
+ if (fileCount == 0) println(" OK!")
+ case StyleError(file, clazz, key, level, args, line, column, customMessage) =>
+ report(line, column, messageHelper.text(level.name),
+ findMessage(messageHelper, clazz, key, args, customMessage))
+ case StyleException(file, clazz, message, stacktrace, line, column) =>
+ report(line, column, "error", message)
+ }
+
+ private def report(line: Option[Int], column: Option[Int], level: String, message: String) {
+ if (fileCount == 0) println("")
+ fileCount += 1
+ println(" " + fileCount + ". " + level + pos(line, column) + ":")
+ println(" " + message)
+ }
+
+ private def pos(line: Option[Int], column: Option[Int]): String = line match {
+ case Some(line) => " at line " + line + (column match {
+ case Some(column) => " character " + column
+ case None => ""
+ })
+ case None => ""
+ }
+ }
+
+ def score(outputResult: OutputResult) = {
+ val penalties = outputResult.errors + outputResult.warnings
+ scala.math.max(maxResult - penalties, 0)
+ }
+
+ def assess(sources: Seq[File]): (String, Int) = {
+ val configFile = new File("project/scalastyle_config.xml").getAbsolutePath
+
+ val messages = new ScalastyleChecker().checkFiles(
+ ScalastyleConfiguration.readFromXml(configFile),
+ Directory.getFiles(sources : _*))
+
+ val output = new ByteArrayOutputStream()
+ val outputResult = Console.withOut(new PrintStream(output)) {
+ new CustomTextOutput().output(messages)
+ }
+
+ val msg =
+ output.toString +
+ "Processed " + outputResult.files + " file(s)\n" +
+ "Found " + outputResult.errors + " errors\n" +
+ "Found " + outputResult.warnings + " warnings\n" +
+ (if (outputResult.errors+outputResult.warnings > 0) "Consult the style guide at http://goo.gl/5CIum" else "")
+
+ (msg, score(outputResult))
+ }
+}
View
1  progfun/recfun/project/build.properties
@@ -0,0 +1 @@
+sbt.version=0.11.3-2
View
72 progfun/recfun/project/buildSettings.sbt
@@ -0,0 +1,72 @@
+// needed for custom scalastyle package
+resolvers += "namin.github.com/maven-repository" at "http://namin.github.com/maven-repository/"
+
+resolvers += "Spray Repository" at "http://repo.spray.cc/"
+
+libraryDependencies += "net.databinder" %% "dispatch-http" % "0.8.8"
+
+libraryDependencies += "org.scalastyle" %% "scalastyle" % "0.1.1-SNAPSHOT"
+
+libraryDependencies += "cc.spray" %% "spray-json" % "1.1.1"
+
+// need scalatest also as a build dependency: the build implements a custom reporter
+libraryDependencies += "org.scalatest" %% "scalatest" % "1.8"
+
+// dispatch uses commons-codec, in version 1.4, so we can't go for 1.6.
+// libraryDependencies += "commons-codec" % "commons-codec" % "1.4"
+
+libraryDependencies += "org.apache.commons" % "commons-lang3" % "3.1"
+
+// sbteclipse-plugin uses scalaz-core 6.0.3, so we can't go 6.0.4
+// libraryDependencies += "org.scalaz" %% "scalaz-core" % "6.0.3"
+
+scalacOptions ++= Seq("-deprecation")
+
+addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.1.0")
+
+// for dependency-graph plugin
+// net.virtualvoid.sbt.graph.Plugin.graphSettings
+
+
+// [info] default:default-3fdafc_2.9.1:0.1-SNAPSHOT
+// [info] +-cc.spray:spray-json_2.9.1:1.1.1
+// [info] | +-org.parboiled:parboiled-scala:1.0.2
+// [info] | | +-org.parboiled:parboiled-core:1.0.2
+// [info] | | +-org.scala-lang:scala-library:2.9.1
+// [info] | |
+// [info] | +-org.scala-lang:scala-library:2.9.1
+// [info] |
+// [info] +-com.typesafe.sbteclipse:sbteclipse-plugin:2.1.0
+// [info] | +-com.typesafe.sbteclipse:sbteclipse-core:2.1.0
+// [info] | +-org.scalaz:scalaz-core_2.9.1:6.0.3
+// [info] | +-org.scala-lang:scala-library:2.9.1
+// [info] |
+// [info] +-net.databinder:dispatch-http_2.9.1:0.8.8
+// [info] | +-net.databinder:dispatch-core_2.9.1:0.8.8
+// [info] | | +-org.apache.httpcomponents:httpclient:4.1.3
+// [info] | | | +-commons-codec:commons-codec:1.4
+// [info] | | | +-commons-logging:commons-logging:1.1.1
+// [info] | | | +-org.apache.httpcomponents:httpcore:4.1.4
+// [info] | | |
+// [info] | | +-org.scala-lang:scala-library:2.9.1
+// [info] | |
+// [info] | +-net.databinder:dispatch-futures_2.9.1:0.8.8
+// [info] | | +-org.scala-lang:scala-library:2.9.1
+// [info] | |
+// [info] | +-org.apache.httpcomponents:httpclient:4.1.3
+// [info] | | +-commons-codec:commons-codec:1.4
+// [info] | | +-commons-logging:commons-logging:1.1.1
+// [info] | | +-org.apache.httpcomponents:httpcore:4.1.4
+// [info] | |
+// [info] | +-org.scala-lang:scala-library:2.9.1
+// [info] |
+// [info] +-org.scala-lang:scala-library:2.9.1
+// [info] +-org.scalastyle:scalastyle_2.9.1:0.1.0-SNAPSHOT
+// [info] | +-com.github.scopt:scopt_2.9.1:2.0.0
+// [info] | | +-org.scala-lang:scala-library:2.9.1
+// [info] | |
+// [info] | +-org.scalariform:scalariform_2.9.1:0.1.1
+// [info] | +-org.scala-lang:scala-library:2.9.1
+// [info] |
+// [info] +-org.scalatest:scalatest_2.9.1:1.8
+// [info] +-org.scala-lang:scala-library:2.9.1
View
2  progfun/recfun/project/project/buildPlugins.sbt
@@ -0,0 +1,2 @@
+// the dependency-graph plugin
+// addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.6.0")
View
135 progfun/recfun/project/scalastyle_config.xml
@@ -0,0 +1,135 @@
+<scalastyle commentFilter="disabled">
+ <name>scalastyle Coursera Configuration</name>
+ <check level="warning" class="org.scalastyle.file.FileTabChecker" enabled="false"></check>
+ <check level="warning" class="org.scalastyle.file.FileLengthChecker" enabled="true">
+ <parameters>
+ <parameter name="maxFileLength"><![CDATA[800]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.file.HeaderMatchesChecker" enabled="false">
+ <parameters>
+ <parameter name="header"><![CDATA[// Copyright (C) 2011-2012 the original author or authors.
+// See the LICENCE.txt file distributed with this work for additional
+// information regarding copyright ownership.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.SpacesAfterPlusChecker" enabled="false"></check>
+ <check level="warning" class="org.scalastyle.file.WhitespaceEndOfLineChecker" enabled="false"></check>
+ <check level="warning" class="org.scalastyle.scalariform.SpacesBeforePlusChecker" enabled="false"></check>
+ <check level="warning" class="org.scalastyle.file.FileLineLengthChecker" enabled="true">
+ <parameters>
+ <parameter name="maxLineLength"><![CDATA[160]]></parameter>
+ <parameter name="tabSize"><![CDATA[4]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.ClassNamesChecker" enabled="true">
+ <parameters>
+ <parameter name="regex"><![CDATA[[A-Z][A-Za-z]*]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.ObjectNamesChecker" enabled="true">
+ <parameters>
+ <parameter name="regex"><![CDATA[[A-Z][A-Za-z]*]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.PackageObjectNamesChecker" enabled="true">
+ <parameters>
+ <parameter name="regex"><![CDATA[^[a-z][A-Za-z]*$]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.EqualsHashCodeChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.IllegalImportsChecker" enabled="true">
+ <parameters>
+ <parameter name="illegalImports"><![CDATA[sun._,java.awt._]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.ParameterNumberChecker" enabled="true">
+ <parameters>
+ <parameter name="maxParameters"><![CDATA[8]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.MagicNumberChecker" enabled="false">
+ <parameters>
+ <parameter name="ignore"><![CDATA[-1,0,1,2,3]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.NoWhitespaceBeforeLeftBracketChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.NoWhitespaceAfterLeftBracketChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.ReturnChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.NullChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.NoCloneChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.NoFinalizeChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.CovariantEqualsChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.StructuralTypeChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.file.RegexChecker" enabled="false">
+ <parameters>
+ <parameter name="regex"><![CDATA[println]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.file.RegexChecker" enabled="true">
+ <parameters>
+ <parameter name="regex"><![CDATA[[ai]sInstanceOf]]></parameter>
+ <customMessage>Avoid casting</customMessage>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.NumberOfTypesChecker" enabled="true">
+ <parameters>
+ <parameter name="maxTypes"><![CDATA[30]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.CyclomaticComplexityChecker" enabled="true">
+ <parameters>
+ <parameter name="maximum"><![CDATA[10]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.UppercaseLChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.SimplifyBooleanExpressionChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.IfBraceChecker" enabled="false">
+ <parameters>
+ <parameter name="singleLineAllowed"><![CDATA[true]]></parameter>
+ <parameter name="doubleLineAllowed"><![CDATA[false]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.MethodLengthChecker" enabled="true">
+ <parameters>
+ <parameter name="maxLength"><![CDATA[50]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.MethodNamesChecker" enabled="false">
+ <parameters>
+ <parameter name="regex"><![CDATA[^[a-z][A-Za-z0-9]*$]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.MethodNamesChecker" enabled="true">
+ <parameters>
+ <parameter name="regex"><![CDATA[^[^A-Z].*$]]></parameter>
+ <customMessage>Method name should not start with an upper case letter.</customMessage>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.NumberOfMethodsInTypeChecker" enabled="true">
+ <parameters>
+ <parameter name="maxMethods"><![CDATA[30]]></parameter>
+ </parameters>
+ </check>
+ <check level="warning" class="org.scalastyle.scalariform.VarFieldChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.VarLocalChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.scalariform.WhileChecker" enabled="true"></check>
+ <check level="warning" class="org.scalastyle.file.RegexChecker" enabled="true">
+ <parameters>
+ <parameter name="regex"><![CDATA[ArrayList|ArrayBuffer|collection\.mutable]]></parameter>
+ <customMessage>Avoid using mutable collections.</customMessage>
+ </parameters>
+ </check>
+</scalastyle>
View
9 progfun/recfun/src/main/scala/common/package.scala
@@ -0,0 +1,9 @@
+package object common {
+ /** `???` can be used for marking methods that remain to be implemented.
+ * @throws An `Error`
+ */
+ def ??? : Nothing = throw new Error("an implementation is missing")
+
+ type ??? = Nothing
+ type *** = Any
+}
View
28 progfun/recfun/src/main/scala/recfun/Main.scala
@@ -0,0 +1,28 @@
+package recfun
+import common._
+
+object Main {
+ def main(args: Array[String]) {
+ println("Pascal's Triangle")
+ for (row <- 0 to 10) {
+ for (col <- 0 to row)
+ print(pascal(col, row) + " ")
+ println()
+ }
+ }
+
+ /**
+ * Exercise 1
+ */
+ def pascal(c: Int, r: Int): Int = ???
+
+ /**
+ * Exercise 2
+ */
+ def balance(chars: List[Char]): Boolean = ???
+
+ /**
+ * Exercise 3
+ */
+ def countChange(money: Int, coins: List[Int]): Int = ???
+}
View
27 progfun/recfun/src/test/scala/recfun/BalanceSuite.scala
@@ -0,0 +1,27 @@
+package recfun
+
+import org.scalatest.FunSuite
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+@RunWith(classOf[JUnitRunner])
+class BalanceSuite extends FunSuite {
+ import Main.balance
+
+ test("'(if (zero? x) max (/ 1 x))' is balanced.") {
+ assert(balance("(if (zero? x) max (/ 1 x))".toList))
+ }
+
+ test("'I told him ...' is balanced.") {
+ assert(balance("I told him (that it's not (yet) done).\n(But he wasn't listening)".toList))
+ }
+
+ test("':-)' is unbalanced.") {
+ assert(!balance(":-)".toList))
+ }
+
+ test("Counting is not enough.") {
+ assert(!balance("())(".toList))
+ }
+}
View
26 progfun/recfun/src/test/scala/recfun/CountChangeSuite.scala
@@ -0,0 +1,26 @@
+package recfun
+
+import org.scalatest.FunSuite
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+@RunWith(classOf[JUnitRunner])
+class CountChangeSuite extends FunSuite {
+ import Main.countChange
+ test("example from instructions") {
+ assert(countChange(4,List(1,2)) === 3)
+ }
+
+ test("sorted CHF") {
+ assert(countChange(300,List(5,10,20,50,100,200,500)) === 1022)
+ }
+
+ test("no pennies") {
+ assert(countChange(301,List(5,10,20,50,100,200,500)) === 0)
+ }
+
+ test("unsorted CHF") {
+ assert(countChange(300,List(500,5,50,100,20,200,10)) === 1022)
+ }
+}
View
22 progfun/recfun/src/test/scala/recfun/PascalSuite.scala
@@ -0,0 +1,22 @@
+package recfun
+
+import org.scalatest.FunSuite
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+@RunWith(classOf[JUnitRunner])
+class PascalSuite extends FunSuite {
+ import Main.pascal
+ test("col=0,row=2") {
+ assert(pascal(0,2) === 1)
+ }
+
+ test("col=1,row=2") {
+ assert(pascal(1,2) === 2)
+ }
+
+ test("col=1,row=3") {
+ assert(pascal(1,3) === 3)
+ }
+}

No commit comments for this range

Something went wrong with that request. Please try again.