# 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, status: 7, hint: 8, client_kill: 9, exception: 10, result: 11, canvasevent: 12, files: 13, }, _default: :write, _prefix: true enum stream: { stdin: 0, stdout: 1, stderr: 2, }, _prefix: true validates :cmd, presence: true validates :timestamp, presence: true validates :stream, length: {minimum: 0, allow_nil: false}, if: -> { cmd_write? } validates :log, length: {minimum: 0, allow_nil: false}, if: -> { cmd_write? } validate :either_data_or_log default_scope { order(timestamp: :asc) } scope :output, -> { where(cmd: 1, stream: %i[stdout stderr]) } def self.create_for(testrun, messages) # We don't want to store anything if the testrun passed return if testrun.passed? messages.map! do |message| # We create a new hash and move all known keys result = {} result[:testrun] = testrun result[:log] = (message.delete(:log) || message.delete(:data)) if message[:cmd] == :write || message.key?(:log) result[:timestamp] = message.delete :timestamp result[:stream] = message.delete :stream if message.key?(:stream) result[:cmd] = message.delete :cmd # The remaining keys will be stored in the `data` column result[:data] = message.presence if message.present? result end # Before storing all messages, we truncate some to save storage filtered_messages = filter_messages_by_size testrun, messages # An array with hashes is passed, all are stored validate_and_store!(filtered_messages) end def self.filter_messages_by_size(testrun, messages) limits = if testrun.submission.cause == 'requestComments' {data: {limit: 25, size: 0}, log: {limit: 5000, size: 0}} else {data: {limit: 10, size: 0}, log: {limit: 500, size: 0}} end filtered_messages = messages.map do |message| if message.key?(:log) && limits[:log][:size] < limits[:log][:limit] message[:log] = message[:log][0, limits[:log][:limit] - limits[:log][:size]] limits[:log][:size] += message[:log].size elsif message[:data] && limits[:data][:size] < limits[:data][:limit] limits[:data][:size] += 1 elsif !message.key?(:log) && limits[:data][:size] < limits[:data][:limit] # Accept short TestrunMessages (e.g. just transporting a status information) # without increasing the `limits[:data][:limit]` before the limit is reached else # Clear all remaining messages message = nil end message end filtered_messages.select(&:present?) end private_class_method :filter_messages_by_size def self.validate_and_store!(messages) validated_messages = messages.map do |message| testrun_message = TestrunMessage.new(message) testrun_message.validate! # We serialize the message without the ID, created_at and updated_at, as they are generated by the database. testrun_message.serializable_hash(except: %w[id created_at updated_at]) end # Now, we store all messages and skip validations (they are already done) TestrunMessage.insert_all!(validated_messages) if validated_messages.present? # rubocop:disable Rails/SkipsModelValidations end private_class_method :validate_and_store! 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