|
- module KGrader
- class Submission
- attr_reader :course, :semester, :assignment, :student
-
- 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)
- target = File.join(repo, @assignment.report)
- message = @assignment.commit_message @student
- FileUtils.cp gradefile, target
- @course.backend.commit repo, message, target
- FileUtils.rm pendingfile
- end
- 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 = []
-
- self.status = :ungraded
- FileUtils.rm_f [buildlog, testlog]
- @fs.jail.reset
- @fs.jail.init
-
- @assignment.tests.each do |test|
- @tests.push({ :name => test[:name], :max => test[:max], :score => 0 })
- end
- 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
- @assignment.tests.each do |test|
- # TODO: execute script in jail and update @test/@comments; out testlog
- end
- end
-
- def save
- File.write gradefile, generate_report
- FileUtils.touch pendingfile
- end
-
- def grade_post
- # self.status = :graded # TODO: uncomment
- # @fs.jail.reset # TODO: uncomment
- @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
- # TODO
- @tests
- @assignment.extra_comments
- end
-
- def generate_summary
- tests = @tests.each 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)
- percent = (score.to_f * 100 / max).round.to_s.rjust 3
- span = get_span max
- "#{percent}% (#{score.to_s.rjust span}/#{max.to_s.rjust span})"
- end
-
- def get_span(max)
- (Math.log10(max) + 1).to_i
- end
- end
- end
|