Skip to content

Commit

Permalink
Add support for custom evaluators using compiled langauges
Browse files Browse the repository at this point in the history
  • Loading branch information
Holmes98 committed Jan 27, 2024
1 parent 4fc9595 commit a967d34
Show file tree
Hide file tree
Showing 9 changed files with 53 additions and 20 deletions.
2 changes: 1 addition & 1 deletion app/controllers/evaluators_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ class EvaluatorsController < ApplicationController

def permitted_params
@_permitted_params ||= begin
permitted_attributes = [:name, :description, :source]
permitted_attributes = [:name, :description, :source, :language_id]
permitted_attributes << :owner_id if policy(@evaluator || Evaluator).transfer?
params.require(:evaluator).permit(*permitted_attributes)
end
Expand Down
1 change: 1 addition & 0 deletions app/models/evaluator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ class Evaluator < ActiveRecord::Base

has_many :problems
belongs_to :owner, :class_name => :User
belongs_to :language

validates :name, :presence => true

Expand Down
4 changes: 4 additions & 0 deletions app/views/evaluators/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
<%= f.label :source %><br />
<%= f.text_area :source %>
</div>
<div class="field">
<%= f.label :language_id %><br>
<%= f.select :language_id, grouped_options_for_select(Language.grouped_submission_options, @evaluator.language_id), :include_blank => true %>
</div>
<div class="field">
<%= f.label :owner_id %><br />
<% if policy(@evaluator).transfer? %>
Expand Down
2 changes: 2 additions & 0 deletions app/views/evaluators/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<th>Name</th>
<th>Description</th>
<th>User</th>
<th>Langauge</th>
<th></th>
<% if policy(Evaluator).update? %>
<th></th>
Expand All @@ -22,6 +23,7 @@
<td><%= evaluator.name %></td>
<td><%= evaluator.description %></td>
<td><%= evaluator.owner_id %></td>
<td><%= evaluator.language&.name %></td>
<td><%= link_to 'Show', evaluator %></td>
<% if policy(Evaluator).update? %>
<td><%= link_to 'Edit', edit_evaluator_path(evaluator) if policy(evaluator).update? %></td>
Expand Down
4 changes: 3 additions & 1 deletion app/views/evaluators/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
</p>
<% if policy(@evaluator).inspect? %>
<p>
<b>Language:</b>
<%= @evaluator.language&.name %><br>
<b>Source:</b>
<pre><%= @evaluator.source %></pre>
<%= predisplay @evaluator.source, language: @evaluator.language&.lexer %>
</p>
<% end %>

Expand Down
2 changes: 1 addition & 1 deletion app/views/problems/_admin.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
<% if @problem.evaluator %>
<%= link_to @problem.evaluator.name, @problem.evaluator %>
<% if policy(@problem.evaluator).inspect? # privilege required to see evaluator source %>
<%= predisplay @problem.evaluator.source, language: :sh %>
<%= predisplay @problem.evaluator.source, language: @problem.evaluator.language&.lexer %>
<% end %>
<% else %>
Default evaluator
Expand Down
50 changes: 34 additions & 16 deletions app/workers/judge_submission_worker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def perform(submission_id)
raise
end

EvalFileName = "eval.sh"
EvalFileName = "eval"
OutputBaseLimit = 1024 * 1024 * 2

attr_accessor :submission, :exe_filename
Expand All @@ -68,14 +68,35 @@ def judge
result = {}
setup_judging do
if submission.language.compiled
result['compile'] = compile!(exe_filename) # possible caching
return result.merge!(grade_compile_error(result['compile'])) if result['compile']['stat'] != 0 #error
result['compile'] = compile!(submission.source, submission.language, exe_filename) # possible caching
return result.merge!(grade_compile_error(result['compile'])) if result['compile']['stat'] != 0 # error
else
File.open(File.expand_path(exe_filename, tmpdir),"w") { |f| f.write(submission.source) }
end

run_command = submission.language.run_command(exe_filename)

if problem.evaluator.nil?
eval_command = nil
else
if problem.evaluator.language.nil?
eval_command = "./#{EvalFileName}"
else
eval_command = problem.evaluator.language.run_command(EvalFileName)
end

if problem.evaluator.language&.compiled
evaluator_compilation = compile!(problem.evaluator.source, problem.evaluator.language, EvalFileName) # possible caching
raise evaluator_compilation['log'] if evaluator_compilation['stat'] != 0 # error
else
File.open(File.expand_path(EvalFileName, tmpdir),"w") do |file|
file.chmod(0700)
file.write(problem.evaluator.source.gsub(/\r\n?/, "\n"))
end
end
end


result['test_cases'] = {}
result['test_sets'] = {}

Expand All @@ -85,7 +106,7 @@ def judge
prereqs = problem.test_cases.where(:id => problem.prerequisite_sets.joins(:test_case_relations).select(:test_case_relations => :test_case_id))

prereqs.each do |test_case|
result['test_cases'][test_case.id] = judge_test_case(test_case, run_command, resource_limits) unless result['test_cases'].has_key?(test_case.id)
result['test_cases'][test_case.id] = judge_test_case(test_case, run_command, eval_command, resource_limits) unless result['test_cases'].has_key?(test_case.id)
end

problem.prerequisite_sets.each do |test_set|
Expand All @@ -100,7 +121,7 @@ def judge

# test cases
(problem.test_cases - prereqs).each do |test_case|
result['test_cases'][test_case.id] = judge_test_case(test_case, run_command, resource_limits) unless result['test_cases'].has_key?(test_case.id)
result['test_cases'][test_case.id] = judge_test_case(test_case, run_command, eval_command, resource_limits) unless result['test_cases'].has_key?(test_case.id)
end

# test sets
Expand Down Expand Up @@ -148,18 +169,18 @@ def setup_judging
end
end

def compile! output
result = submission.language.compile(box, submission.source, output, :mem => 393216, :wall_time => 60)
def compile!(source, language, output)
result = language.compile(box, source, output, :mem => 393216, :wall_time => 60)
FileUtils.copy(box.expand_path(output), File.expand_path(output, tmpdir)) if result['stat'] == 0
return result
ensure
box.clean!
end

def judge_test_case(test_case, run_command, resource_limits)
def judge_test_case(test_case, run_command, eval_command, resource_limits)
FileUtils.copy(File.expand_path(exe_filename, tmpdir), box.expand_path(exe_filename))
result = run_test_case(test_case, run_command, resource_limits)
result['evaluator'] = evaluate_output(test_case, result['output'], result['output_size'], problem.evaluator)
result['evaluator'] = evaluate_output(test_case, result['output'], result['output_size'], eval_command)
result['log'] = truncate_output(result['log']) # log only a small portion
result['output'] = truncate_output(result['output'].slice(0,100)) # store only a small portion
result
Expand Down Expand Up @@ -190,21 +211,18 @@ def run_test_case(test_case, run_command, resource_limits = {})
box.clean!
end

def evaluate_output(test_case, output, output_size, evaluator)
def evaluate_output(test_case, output, output_size, eval_command)
stream_limit = OutputBaseLimit + test_case.output.bytesize*2
if output_size > stream_limit
return {'evaluation' => 0, 'log' => "Output exceeded the streamsize limit of #{stream_limit}.", 'meta' => {'status' => 'OK'}}
end
expected = conditioned_output(test_case.output)
actual = conditioned_output(output)
if evaluator.nil?
if eval_command.nil?
{'evaluation' => (actual == expected ? 1 : 0), 'meta' => {'status' => 'OK'}}
else
r = {}
box.fopen(EvalFileName,"w") do |file|
file.chmod(0700)
file.write(problem.evaluator.source.gsub(/\r\n?/, "\n"))
end
FileUtils.copy(File.expand_path(EvalFileName, tmpdir), box.expand_path(EvalFileName))
resource_limits = { :mem => 524288, :time => time_limit*3+15, :wall_time => time_limit*3+30 }
box.fopen("actual","w") { |f| f.write(actual) } # DEPRECATED
box.fopen("input","w") { |f| f.write(test_case.input) } # DEPRECATED
Expand All @@ -213,7 +231,7 @@ def evaluate_output(test_case, output, output_size, evaluator)
eval_output = nil
str_to_pipe(test_case.input, expected) do |input_stream, output_stream|
run_opts = resource_limits.reverse_merge(:processes => true, 3 => input_stream, 4 => output_stream, :stdin_data => actual, :output_limit => OutputBaseLimit + test_case.output.bytesize*4, :clean_utf8 => true, :inherit_fds => true)
(stdout,), (r['log'],r['log_size']), (r['box'],), r['meta'], status = box.capture5("./#{EvalFileName} #{deprecated_args}", run_opts )
(stdout,), (r['log'],r['log_size']), (r['box'],), r['meta'], status = box.capture5("#{eval_command} #{deprecated_args}", run_opts )
r['log'] = truncate_output(r['log'])
return r.merge('stat' => 2, 'box' => 'Output was not a valid UTF-8 encoding\n'+r['box']) if !output.force_encoding("UTF-8").valid_encoding?
eval_output = stdout.strip.split(nil,2)
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20230225054132_add_language_id_to_evaluators.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddLanguageIdToEvaluators < ActiveRecord::Migration
def change
add_column :evaluators, :language_id, :integer
end
end
3 changes: 2 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 20200418113601) do
ActiveRecord::Schema.define(version: 20230225054132) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand Down Expand Up @@ -93,6 +93,7 @@
t.integer "owner_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.integer "language_id"
end

create_table "file_attachments", force: :cascade do |t|
Expand Down

0 comments on commit a967d34

Please sign in to comment.