Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for custom evaluators using compiled langauges #260

Merged
merged 5 commits into from
May 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/assets/stylesheets/submission.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ table.results {
border-spacing: 0;
border-collapse: collapse;
width: 100%;
margin: 15px 0px;
}

.results th {
Expand Down
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
8 changes: 8 additions & 0 deletions app/models/submission/judge_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -285,10 +285,18 @@ def compiled?
data.has_key?('compile')
end

def evaluator_compiled?
data.has_key?('evaluator_compile')
end

def compilation
@compilation ||= Compilation.new(data['compile'])
end

def evaluator_compilation
@evaluator_compilation ||= Compilation.new(data['evaluator_compile'])
end

def prerequisite_sets
test_sets.slice(@presets)
end
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
25 changes: 18 additions & 7 deletions app/views/submissions/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@
</p>

<% @judge_data = @submission.judge_data %>
<table class="results">
<% if @judge_data.compiled? %>
<% if @judge_data.compiled? %>
<table class="results">
<tbody class="compilation status_<%= @judge_data.compilation.status %>">
<tr>
<th class="compile_command" colspan="3">Compilation: <span class="code"><%= @judge_data.compilation.command %></span></th>
Expand All @@ -59,9 +59,22 @@
<td colspan="5"><span class="code"><%= @judge_data.compilation.log %></span></td>
</tr>
</tbody>
<% end %>
</table>
&nbsp;
</table>
<% end %>
<% if @judge_data.evaluator_compiled? && @submission.problem.evaluator && policy(@submission.problem.evaluator).inspect? %>
<table class="results">
<tbody class="compilation status_<%= @judge_data.evaluator_compilation.status %>">
<tr>
<th class="compile_command" colspan="3">Evaluator Compilation: <span class="code"><%= @judge_data.evaluator_compilation.command %></span></th>
<th class="compile_status"><%= @judge_data.evaluator_compilation.judgement %></th>
<th>&nbsp;</th>
</tr>
<tr>
<td colspan="5"><span class="code"><%= @judge_data.evaluator_compilation.log %></span></td>
</tr>
</tbody>
</table>
<% end %>
<table class="results">
<% if !@judge_data.compiled? || @judge_data.compilation.status == :success %>
<tbody class="headings">
Expand Down Expand Up @@ -152,8 +165,6 @@
</tr>
</tbody>
</table>
<br>
<br>
<p>
<b>Source:</b>
<%= predisplay(@submission.source || "", language: @submission.language.lexer) %>
Expand Down
49 changes: 33 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,34 @@ 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
return result.merge!('evaluator_compile' => evaluator_compilation, 'status' => 2) 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 +105,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 +120,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 +168,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 +210,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 +230,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