|
- module KGrader
- class Submission
- attr_reader :course, :semester, :assignment, :student
- MAX_COLS = 79
-
- def initialize(filesystem, course, semester, assignment, student)
- @fs = filesystem
- @course = course
- @semester = semester
- @assignment = assignment
- @student = student
-
- @root = @fs.submission @course.name, @semester, @assignment.name, student
- @status = nil
- end
-
- def status
- @status ||= @fs.load(statusfile).to_sym
- end
-
- def status=(new_status)
- File.write statusfile, new_status
- @status = new_status
- end
-
- def exists?
- File.exists? statusfile
- end
-
- def create
- FileUtils.mkdir_p @root
- self.status = :init
- nil
- end
-
- def fetch(due)
- if status == :init
- @course.backend.clone repo, @semester, @assignment.id, @student
- rewind due
- self.status = :ungraded
- else
- oldrev = revision if status == :graded
- self.status = :fetching
- @course.backend.update repo
- newrev = rewind due
- self.status = newrev == oldrev ? :graded : :ungraded
- end
- nil
- end
-
- def grade
- grade_prep
- stage
- build
- test
- save
- grade_post
- @summary
- end
-
- def commit
- if status == :graded && File.exists?(pendingfile)
- message = @assignment.commit_message @student
- FileUtils.cp gradefile, File.join(repo, @assignment.report)
- @course.backend.commit repo, message, @assignment.report
- FileUtils.rm pendingfile
- end
- nil
- end
-
- private
- def repo
- File.join @root, 'repo'
- end
-
- def statusfile
- File.join @root, 'status.txt'
- end
-
- def gradefile
- File.join @root, 'grade.txt'
- end
-
- def pendingfile
- File.join @root, 'pending'
- end
-
- def buildlog
- File.join @root, 'build.log'
- end
-
- def testlog
- File.join @root, 'test.log'
- end
-
- def revision
- @course.backend.revision repo
- end
-
- def rewind(date)
- log = @course.backend.log repo
- target = log.find { |commit| commit[:date] <= date }
- if target.nil?
- raise SubmissionError, "no commits before due date: #{student}"
- end
-
- rev = target[:rev]
- @course.backend.update repo, rev
- rev
- end
-
- def grade_prep
- @failure = false
- @comments = []
- @summary = nil
- @tests = @assignment.tests.clone.each { |test| test[:score] = 0 }
-
- self.status = :ungraded
- FileUtils.rm_f [buildlog, testlog]
- @fs.jail.reset
- @fs.jail.init
- end
-
- def stage
- @assignment.manifest[:provided].each do |entry|
- @fs.jail.stage entry[:path], entry[:name]
- end
- @assignment.manifest[:graded].each do |entry|
- @fs.jail.stage File.join(repo, entry[:name]), entry[:name]
- end
- end
-
- def build
- @assignment.build_steps.each do |command|
- return build_failure unless @fs.jail.exec command, buildlog
- end
- end
-
- def test
- return if @failure
- @tests.each do |test|
- test[:score] = @fs.jail.run_test test[:script], testlog do |comment|
- @comments.push "#{test[:name]}: #{comment}"
- end
- end
- end
-
- def save
- File.write gradefile, generate_report
- FileUtils.touch pendingfile
- end
-
- def grade_post
- self.status = :graded
- @fs.jail.reset
- @summary = generate_summary unless @summary
- end
-
- def build_failure
- @failure = true
- @comments.push "failed to compile"
- @summary = "#{format_points 0, max_score}: failed to compile"
- end
-
- def generate_report
- header = "#{assignment.title} Grade Report for #{student}"
- header = header.center(MAX_COLS).rstrip
- hr1 = '-' * MAX_COLS
- hr2 = '=' * MAX_COLS
-
- metadata = [
- "commit revision: #{revision}",
- "commit date: #{format_time @course.backend.commit_date(repo)}",
- "grade date: #{format_time Time.now}"
- ]
- version = KGrader.version
- metadata.push "grader version: #{version}" if version
- metadata = metadata.join("\n")
-
- tests = "tests:\n" + @tests.map do |test|
- score = format_points(test[:score], test[:max], max_score)
- justify_both " - #{test[:name]}", score
- end.join("\n")
-
- total = justify_both "total:", format_points(score, max_score)
-
- all_comments = (@comments + @assignment.extra_comments)
- if all_comments
- comments = "comments:\n" + all_comments.map do |cmt|
- " - #{cmt}\n"
- end.join
- else
- comments = ""
- end
-
- [header, hr2, metadata, hr1, tests, hr1, total, hr1, comments].join "\n"
- end
-
- def generate_summary
- tests = @tests.map do |test|
- "#{test[:score].to_s.rjust get_span(test[:max])}/#{test[:max]}"
- end.join ', '
- "#{format_points score, max_score}: #{tests}"
- end
-
- def score
- @tests.reduce(0) { |sum, t| sum + t[:score] }
- end
-
- def max_score
- @tests.reduce(0) { |sum, t| sum + t[:max] }
- end
-
- def format_points(score, max, span_max = nil)
- percent = (score.to_f * 100 / max).round.to_s.rjust 3
- span = get_span(span_max || max)
- "#{percent}% (#{score.to_s.rjust span}/#{max.to_s.rjust span})"
- end
-
- def justify_both left, right
- "#{left}#{right.rjust MAX_COLS - left.length}"
- end
-
- def get_span(max)
- (Math.log10(max) + 1).to_i
- end
-
- def format_time(time)
- time.localtime.strftime "%H:%M, %b %d, %Y %Z"
- end
- end
- end
|