A code autograder for student homework submissions
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

198 lines
4.3 KiB

  1. module KGrader
  2. class Submission
  3. attr_reader :course, :semester, :assignment, :student
  4. def initialize(filesystem, course, semester, assignment, student)
  5. @fs = filesystem
  6. @course = course
  7. @semester = semester
  8. @assignment = assignment
  9. @student = student
  10. @root = @fs.submission @course.name, @semester, @assignment.name, student
  11. @status = nil
  12. end
  13. def status
  14. @status ||= @fs.load(statusfile).to_sym
  15. end
  16. def status=(new_status)
  17. File.write statusfile, new_status
  18. @status = new_status
  19. end
  20. def exists?
  21. File.exists? statusfile
  22. end
  23. def create
  24. FileUtils.mkdir_p @root
  25. self.status = :init
  26. nil
  27. end
  28. def fetch(due)
  29. if status == :init
  30. @course.backend.clone repo, @semester, @assignment.id, @student
  31. rewind due
  32. self.status = :ungraded
  33. else
  34. oldrev = revision if status == :graded
  35. self.status = :fetching
  36. @course.backend.update repo
  37. newrev = rewind due
  38. self.status = newrev == oldrev ? :graded : :ungraded
  39. end
  40. nil
  41. end
  42. def grade
  43. grade_prep
  44. stage
  45. build
  46. test
  47. save
  48. grade_post
  49. @summary
  50. end
  51. def commit
  52. if status == :graded && File.exists?(pendingfile)
  53. target = File.join(repo, @assignment.report)
  54. message = @assignment.commit_message @student
  55. FileUtils.cp gradefile, target
  56. @course.backend.commit repo, message, target
  57. FileUtils.rm pendingfile
  58. end
  59. end
  60. private
  61. def repo
  62. File.join @root, 'repo'
  63. end
  64. def statusfile
  65. File.join @root, 'status.txt'
  66. end
  67. def gradefile
  68. File.join @root, 'grade.txt'
  69. end
  70. def pendingfile
  71. File.join @root, 'pending'
  72. end
  73. def buildlog
  74. File.join @root, 'build.log'
  75. end
  76. def testlog
  77. File.join @root, 'test.log'
  78. end
  79. def revision
  80. @course.backend.revision repo
  81. end
  82. def rewind(date)
  83. log = @course.backend.log repo
  84. target = log.find { |commit| commit[:date] <= date }
  85. if target.nil?
  86. raise SubmissionError, "no commits before due date: #{student}"
  87. end
  88. rev = target[:rev]
  89. @course.backend.update repo, rev
  90. rev
  91. end
  92. def grade_prep
  93. @failure = false
  94. @comments = []
  95. @summary = nil
  96. @tests = []
  97. self.status = :ungraded
  98. FileUtils.rm_f [buildlog, testlog]
  99. @fs.jail.reset
  100. @fs.jail.init
  101. @assignment.tests.each do |test|
  102. @tests.push({ :name => test[:name], :max => test[:max], :score => 0 })
  103. end
  104. end
  105. def stage
  106. @assignment.manifest[:provided].each do |entry|
  107. @fs.jail.stage entry[:path], entry[:name]
  108. end
  109. @assignment.manifest[:graded].each do |entry|
  110. @fs.jail.stage File.join(repo, entry[:name]), entry[:name]
  111. end
  112. end
  113. def build
  114. @assignment.build_steps.each do |command|
  115. return build_failure unless @fs.jail.exec command, buildlog
  116. end
  117. end
  118. def test
  119. return if @failure
  120. @assignment.tests.each do |test|
  121. # TODO: execute script in jail and update @test/@comments; out testlog
  122. end
  123. end
  124. def save
  125. File.write gradefile, generate_report
  126. FileUtils.touch pendingfile
  127. end
  128. def grade_post
  129. # self.status = :graded # TODO: uncomment
  130. # @fs.jail.reset # TODO: uncomment
  131. @summary = generate_summary unless @summary
  132. end
  133. def build_failure
  134. @failure = true
  135. @comments.push "failed to compile"
  136. @summary = "#{format_points 0, max_score}: failed to compile"
  137. end
  138. def generate_report
  139. # TODO
  140. @tests
  141. @assignment.extra_comments
  142. end
  143. def generate_summary
  144. tests = @tests.each do |test|
  145. "#{test[:score].to_s.rjust get_span(test[:max])}/#{test[:max]}"
  146. end.join ', '
  147. "#{format_points score, max_score}: #{tests}"
  148. end
  149. def score
  150. @tests.reduce(0) { |sum, t| sum + t[:score] }
  151. end
  152. def max_score
  153. @tests.reduce(0) { |sum, t| sum + t[:max] }
  154. end
  155. def format_points(score, max)
  156. percent = (score.to_f * 100 / max).round.to_s.rjust 3
  157. span = get_span max
  158. "#{percent}% (#{score.to_s.rjust span}/#{max.to_s.rjust span})"
  159. end
  160. def get_span(max)
  161. (Math.log10(max) + 1).to_i
  162. end
  163. end
  164. end