diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 31a3ff9c..3018e4de 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -146,18 +146,26 @@ class SubmissionsController < ApplicationController end runner_socket.on :exit do |exit_code| + @exit_code = exit_code exit_statement = if @output.empty? && exit_code.zero? + @status = :ok t('exercises.implement.no_output_exit_successful', timestamp: l(Time.zone.now, format: :short), exit_code: exit_code) elsif @output.empty? + @status = :failed t('exercises.implement.no_output_exit_failure', timestamp: l(Time.zone.now, format: :short), exit_code: exit_code) elsif exit_code.zero? + @status = :ok "\n#{t('exercises.implement.exit_successful', timestamp: l(Time.zone.now, format: :short), exit_code: exit_code)}" else + @status = :failed "\n#{t('exercises.implement.exit_failure', timestamp: l(Time.zone.now, format: :short), exit_code: exit_code)}" end client_socket.send_data JSON.dump({cmd: :write, stream: :stdout, data: "#{exit_statement}\n"}) - client_socket.send_data JSON.dump({cmd: :out_of_memory}) if exit_code == 137 + if exit_code == 137 + client_socket.send_data JSON.dump({cmd: :out_of_memory}) + @status = :out_of_memory + end close_client_connection(client_socket) end @@ -169,30 +177,38 @@ class SubmissionsController < ApplicationController close_client_connection(client_socket) Rails.logger.debug { "Running a submission timed out: #{e.message}" } @output = "timeout: #{@output}" + @status = :timeout extract_durations(e) rescue Runner::Error => e client_socket.send_data JSON.dump({cmd: :status, status: :container_depleted}) close_client_connection(client_socket) Rails.logger.debug { "Runner error while running a submission: #{e.message}" } + @status = :container_depleted extract_durations(e) ensure - save_run_output + save_testrun_output 'run' end def score hijack do |tubesock| tubesock.onopen do |_event| - kill_client_socket(tubesock) if @embed_options[:disable_score] + switch_locale do + kill_client_socket(tubesock) if @embed_options[:disable_score] - tubesock.send_data(JSON.dump(@submission.calculate_score)) - # To enable hints when scoring a submission, uncomment the next line: - # send_hints(tubesock, StructuredError.where(submission: @submission)) - kill_client_socket(tubesock) + tubesock.send_data(JSON.dump(@submission.calculate_score)) + # To enable hints when scoring a submission, uncomment the next line: + # send_hints(tubesock, StructuredError.where(submission: @submission)) + kill_client_socket(tubesock) + rescue Runner::Error => e + tubesock.send_data JSON.dump({cmd: :status, status: :container_depleted}) + kill_client_socket(tubesock) + Rails.logger.debug { "Runner error while scoring submission #{@submission.id}: #{e.message}" } + @passed = false + @status = :container_depleted + extract_durations(e) + save_testrun_output 'assess' + end end - rescue Runner::Error => e - tubesock.send_data JSON.dump({cmd: :status, status: :container_depleted}) - kill_client_socket(tubesock) - Rails.logger.debug { "Runner error while scoring submission #{@submission.id}: #{e.message}" } end end @@ -203,15 +219,21 @@ class SubmissionsController < ApplicationController def test hijack do |tubesock| tubesock.onopen do |_event| - kill_client_socket(tubesock) if @embed_options[:disable_run] + switch_locale do + kill_client_socket(tubesock) if @embed_options[:disable_run] - tubesock.send_data(JSON.dump(@submission.test(@file))) - kill_client_socket(tubesock) + tubesock.send_data(JSON.dump(@submission.test(@file))) + kill_client_socket(tubesock) + rescue Runner::Error => e + tubesock.send_data JSON.dump({cmd: :status, status: :container_depleted}) + kill_client_socket(tubesock) + Rails.logger.debug { "Runner error while testing submission #{@submission.id}: #{e.message}" } + @passed = false + @status = :container_depleted + extract_durations(e) + save_testrun_output 'assess' + end end - rescue Runner::Error => e - tubesock.send_data JSON.dump({cmd: :status, status: :container_depleted}) - kill_client_socket(tubesock) - Rails.logger.debug { "Runner error while testing submission #{@submission.id}: #{e.message}" } end end @@ -293,12 +315,15 @@ class SubmissionsController < ApplicationController end # save the output of this "run" as a "testrun" (scoring runs are saved in submission.rb) - def save_run_output - testrun = Testrun.create( + def save_testrun_output(cause) + testrun = Testrun.create!( file: @file, - cause: 'run', + passed: @passed, + cause: cause, submission: @submission, - output: @output, + exit_code: @exit_code, # might be nil, e.g., when the run did not finish + status: @status, + output: @output.presence, # TODO: Remove duplicated saving of the output after creating TestrunMessages container_execution_time: @container_execution_time, waiting_for_container_time: @waiting_for_container_time ) diff --git a/app/models/runner.rb b/app/models/runner.rb index 458ae144..88e36ae5 100644 --- a/app/models/runner.rb +++ b/app/models/runner.rb @@ -116,7 +116,7 @@ class Runner < ApplicationRecord output.merge!(status: :failed, container_execution_time: e.execution_duration) rescue Runner::Error => e Rails.logger.debug { "Running command `#{command}` failed: #{e.message}" } - output.merge!(status: :failed, container_execution_time: e.execution_duration) + output.merge!(status: :container_depleted, container_execution_time: e.execution_duration) ensure # We forward the exception if requested raise e if raise_exception && defined?(e) && e.present? diff --git a/app/models/submission.rb b/app/models/submission.rb index 89649e97..3f3852f0 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -252,7 +252,9 @@ class Submission < ApplicationRecord cause: 'assess', # Required to differ run and assess for RfC show file: file, # Test file that was executed passed: passed, - output: testrun_output, + exit_code: output[:exit_code], + status: output[:status], + output: testrun_output.presence, container_execution_time: output[:container_execution_time], waiting_for_container_time: output[:waiting_for_container_time] ) diff --git a/app/models/testrun.rb b/app/models/testrun.rb index 126a1499..feea7a09 100644 --- a/app/models/testrun.rb +++ b/app/models/testrun.rb @@ -4,4 +4,16 @@ class Testrun < ApplicationRecord belongs_to :file, class_name: 'CodeOcean::File', optional: true belongs_to :submission belongs_to :testrun_execution_environment, optional: true, dependent: :destroy + has_many :testrun_messages, dependent: :destroy + + enum status: { + ok: 0, + failed: 1, + container_depleted: 2, + timeout: 3, + out_of_memory: 4, + }, _default: :ok, _prefix: true + + validates :exit_code, numericality: {only_integer: true, min: 0, max: 255}, allow_nil: true + validates :status, presence: true end diff --git a/app/models/testrun_message.rb b/app/models/testrun_message.rb new file mode 100644 index 00000000..db30717a --- /dev/null +++ b/app/models/testrun_message.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class TestrunMessage < ApplicationRecord + belongs_to :testrun + + enum cmd: { + input: 0, + write: 1, + clear: 2, + turtle: 3, + turtlebatch: 4, + render: 5, + exit: 6, + timeout: 7, + out_of_memory: 8, + status: 9, + hint: 10, + client_kill: 11, + exception: 12, + result: 13, + }, _default: :write, _prefix: true + + enum stream: { + stdin: 0, + stdout: 1, + stderr: 2, + }, _prefix: true + + validates :cmd, presence: true + validates :timestamp, presence: true + + validate :either_data_or_log + + def either_data_or_log + if [data, log].count(&:present?) > 1 + errors.add(log, "can't be present if data is also present") + end + end + private :either_data_or_log +end diff --git a/db/migrate/20220415215111_add_details_to_testruns.rb b/db/migrate/20220415215111_add_details_to_testruns.rb new file mode 100644 index 00000000..4988f601 --- /dev/null +++ b/db/migrate/20220415215111_add_details_to_testruns.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class AddDetailsToTestruns < ActiveRecord::Migration[6.1] + def change + change_table :testruns do |t| + t.integer :exit_code, limit: 2, null: true, comment: 'No exit code is available in case of a timeout' + t.check_constraint 'exit_code >= 0 AND exit_code <= 255', name: 'exit_code_constraint' + t.integer :status, limit: 1, null: false, default: 0, comment: 'Used as enum in Rails' + end + + enable_extension 'pgcrypto' unless extensions.include?('pgcrypto') + + create_table :testrun_messages, id: :uuid do |t| + t.belongs_to :testrun, foreign_key: true, null: false, index: true + t.interval :timestamp, null: false, default: '00:00:00' + t.integer :cmd, limit: 1, null: false, default: 1, comment: 'Used as enum in Rails' + t.integer :stream, limit: 1, null: true, comment: 'Used as enum in Rails' + t.text :log, null: true + t.jsonb :data, null: true + t.check_constraint 'log IS NULL OR data IS NULL', name: 'either_data_or_log' + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index ea8791a6..549b2029 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,10 +10,11 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_04_15_125948) do +ActiveRecord::Schema.define(version: 2022_04_15_215111) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" + enable_extension "pgcrypto" enable_extension "plpgsql" create_table "anomaly_notifications", id: :serial, force: :cascade do |t| @@ -470,6 +471,19 @@ ActiveRecord::Schema.define(version: 2022_04_15_125948) do t.index ["testrun_id"], name: "index_testrun_execution_environments_on_testrun_id" end + create_table "testrun_messages", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.bigint "testrun_id", null: false + t.interval "timestamp", default: "PT0S", null: false + t.integer "cmd", limit: 2, default: 0, null: false, comment: "Used as enum in Rails" + t.integer "stream", limit: 2, comment: "Used as enum in Rails" + t.text "log" + t.jsonb "data" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["testrun_id"], name: "index_testrun_messages_on_testrun_id" + t.check_constraint "(log IS NULL) OR (data IS NULL)", name: "either_data_or_log" + end + create_table "testruns", id: :serial, force: :cascade do |t| t.boolean "passed" t.text "output" @@ -480,7 +494,10 @@ ActiveRecord::Schema.define(version: 2022_04_15_125948) do t.string "cause" t.interval "container_execution_time" t.interval "waiting_for_container_time" + t.integer "exit_code", limit: 2, comment: "No exit code is available in case of a timeout" + t.integer "status", limit: 2, default: 0, null: false, comment: "Used as enum in Rails" t.index ["submission_id"], name: "index_testruns_on_submission_id" + t.check_constraint "(exit_code >= 0) AND (exit_code <= 255)", name: "exit_code_constraint" end create_table "tips", force: :cascade do |t| @@ -547,6 +564,7 @@ ActiveRecord::Schema.define(version: 2022_04_15_125948) do add_foreign_key "submissions", "study_groups" add_foreign_key "testrun_execution_environments", "execution_environments" add_foreign_key "testrun_execution_environments", "testruns" + add_foreign_key "testrun_messages", "testruns" add_foreign_key "tips", "file_types" add_foreign_key "user_exercise_feedbacks", "submissions" end