
Since both projects are developed together and by the same team, we also want to have the same code structure and utility methods available in both projects. Therefore, this commit changes many files, but without a functional change.
275 lines
10 KiB
Ruby
275 lines
10 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
|
|
RSpec.describe Runner do
|
|
let(:runner_id) { attributes_for(:runner)[:runner_id] }
|
|
let(:strategy_class) { described_class.strategy_class }
|
|
let(:strategy) { instance_double(strategy_class) }
|
|
|
|
describe 'attribute validation' do
|
|
let(:runner) { create(:runner) }
|
|
|
|
it 'validates the presence of the runner id' do
|
|
described_class.skip_callback(:validation, :before, :request_id)
|
|
runner.update(runner_id: nil)
|
|
expect(runner.errors[:runner_id]).to be_present
|
|
described_class.set_callback(:validation, :before, :request_id)
|
|
end
|
|
|
|
it 'validates the presence of an execution environment' do
|
|
runner.update(execution_environment: nil)
|
|
expect(runner.errors[:execution_environment]).to be_present
|
|
end
|
|
|
|
it 'validates the presence of a contributor' do
|
|
runner.update(contributor: nil)
|
|
expect(runner.errors[:contributor]).to be_present
|
|
end
|
|
end
|
|
|
|
describe '::strategy_class' do
|
|
shared_examples 'uses the strategy defined in the constant' do |strategy, strategy_class|
|
|
let(:codeocean_config) { instance_double(CodeOcean::Config) }
|
|
let(:runner_management_config) { {runner_management: {enabled: true, strategy:}} }
|
|
|
|
before do
|
|
allow(CodeOcean::Config).to receive(:new).with(:code_ocean).and_return(codeocean_config)
|
|
allow(codeocean_config).to receive(:read).and_return(runner_management_config)
|
|
end
|
|
|
|
it "uses #{strategy_class} as strategy class for constant #{strategy}" do
|
|
expect(described_class.strategy_class).to eq(strategy_class)
|
|
end
|
|
end
|
|
|
|
available_strategies = {
|
|
poseidon: Runner::Strategy::Poseidon,
|
|
docker_container_pool: Runner::Strategy::DockerContainerPool,
|
|
}
|
|
available_strategies.each do |strategy, strategy_class|
|
|
it_behaves_like 'uses the strategy defined in the constant', strategy, strategy_class
|
|
end
|
|
end
|
|
|
|
describe '#destroy_at_management' do
|
|
let(:runner) { described_class.create }
|
|
|
|
before do
|
|
allow(strategy_class).to receive_messages(request_from_management: runner_id, new: strategy)
|
|
end
|
|
|
|
it 'delegates to its strategy' do
|
|
expect(strategy).to receive(:destroy_at_management)
|
|
runner.destroy_at_management
|
|
end
|
|
end
|
|
|
|
describe '#attach to execution' do
|
|
let(:runner) { described_class.create }
|
|
let(:command) { 'ls' }
|
|
let(:event_loop) { instance_double(Runner::EventLoop) }
|
|
let(:connection) { instance_double(Runner::Connection) }
|
|
|
|
before do
|
|
allow(strategy_class).to receive_messages(request_from_management: runner_id, new: strategy)
|
|
allow(event_loop).to receive(:wait)
|
|
allow(connection).to receive(:error).and_return(nil)
|
|
allow(Runner::EventLoop).to receive(:new).and_return(event_loop)
|
|
allow(strategy).to receive(:attach_to_execution).and_return(connection)
|
|
end
|
|
|
|
it 'delegates to its strategy' do
|
|
expect(strategy).to receive(:attach_to_execution)
|
|
runner.attach_to_execution(command)
|
|
end
|
|
|
|
it 'returns the execution time' do
|
|
starting_time = Time.zone.now
|
|
execution_time = runner.attach_to_execution(command)
|
|
test_time = Time.zone.now - starting_time
|
|
expect(execution_time).to be_between(0.0, test_time).exclusive
|
|
end
|
|
|
|
it 'blocks until the event loop is stopped' do
|
|
allow(event_loop).to receive(:wait) { sleep(1) }
|
|
execution_time = runner.attach_to_execution(command)
|
|
expect(execution_time).to be > 1
|
|
end
|
|
|
|
context 'when an error is returned' do
|
|
let(:error_message) { 'timeout' }
|
|
let(:error) { Runner::Error::ExecutionTimeout.new(error_message) }
|
|
|
|
before { allow(connection).to receive(:error).and_return(error) }
|
|
|
|
it 'raises the error' do
|
|
expect { runner.attach_to_execution(command) }.to raise_error do |raised_error|
|
|
expect(raised_error).to be_a(Runner::Error::ExecutionTimeout)
|
|
expect(raised_error.message).to eq(error_message)
|
|
end
|
|
end
|
|
|
|
it 'attaches the execution time to the error' do
|
|
test_starting_time = Time.zone.now
|
|
expect { runner.attach_to_execution(command) }.to raise_error do |raised_error|
|
|
test_time = Time.zone.now - test_starting_time
|
|
expect(raised_error.execution_duration).to be_between(0.0, test_time).exclusive
|
|
# The `starting_time` is shortly after the `test_starting_time``
|
|
expect(raised_error.starting_time).to be > test_starting_time
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#copy_files' do
|
|
let(:runner) { described_class.create }
|
|
|
|
before do
|
|
allow(strategy_class).to receive_messages(request_from_management: runner_id, new: strategy)
|
|
end
|
|
|
|
it 'delegates to its strategy' do
|
|
expect(strategy).to receive(:copy_files).once
|
|
runner.copy_files(nil)
|
|
end
|
|
|
|
context 'when a RunnerNotFound exception is raised' do
|
|
before do
|
|
was_called = false
|
|
allow(strategy).to receive(:copy_files) do
|
|
unless was_called
|
|
was_called = true
|
|
raise Runner::Error::RunnerNotFound.new
|
|
end
|
|
end
|
|
end
|
|
|
|
it 'requests a new id' do
|
|
expect(runner).to receive(:request_new_id)
|
|
runner.copy_files(nil)
|
|
end
|
|
|
|
it 'calls copy_file twice' do
|
|
# copy_files is called again after a new runner was requested.
|
|
expect(strategy).to receive(:copy_files).twice
|
|
runner.copy_files(nil)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'creation' do
|
|
let(:contributor) { create(:external_user) }
|
|
let(:execution_environment) { create(:ruby) }
|
|
let(:create_action) { -> { described_class.create(contributor:, execution_environment:) } }
|
|
|
|
it 'requests a runner id from the runner management' do
|
|
expect(strategy_class).to receive(:request_from_management)
|
|
create_action.call
|
|
end
|
|
|
|
it 'returns a valid runner' do
|
|
allow(strategy_class).to receive(:request_from_management).and_return(runner_id)
|
|
expect(create_action.call).to be_valid
|
|
end
|
|
|
|
it 'sets the strategy' do
|
|
allow(strategy_class).to receive(:request_from_management).and_return(runner_id)
|
|
strategy = strategy_class.new(runner_id, execution_environment)
|
|
allow(strategy_class).to receive(:new).with(runner_id, execution_environment).and_return(strategy)
|
|
runner = create_action.call
|
|
expect(runner.strategy).to eq(strategy)
|
|
end
|
|
|
|
it 'does not call the runner management again while a runner id is set' do
|
|
expect(strategy_class).to receive(:request_from_management).and_return(runner_id).once
|
|
runner = create_action.call
|
|
runner.update(contributor: create(:external_user))
|
|
end
|
|
end
|
|
|
|
describe '#request_new_id' do
|
|
let(:runner) { create(:runner) }
|
|
|
|
context 'when the environment is available in the runner management' do
|
|
it 'requests the runner management' do
|
|
expect(strategy_class).to receive(:request_from_management)
|
|
runner.send(:request_new_id)
|
|
end
|
|
|
|
it 'updates the runner id' do
|
|
allow(strategy_class).to receive(:request_from_management).and_return(runner_id)
|
|
runner.send(:request_new_id)
|
|
expect(runner.runner_id).to eq(runner_id)
|
|
end
|
|
|
|
it 'updates the strategy' do
|
|
allow(strategy_class).to receive(:request_from_management).and_return(runner_id)
|
|
strategy = strategy_class.new(runner_id, runner.execution_environment)
|
|
allow(strategy_class).to receive(:new).with(runner_id, runner.execution_environment).and_return(strategy)
|
|
runner.send(:request_new_id)
|
|
expect(runner.strategy).to eq(strategy)
|
|
end
|
|
end
|
|
|
|
context 'when the environment could not be found in the runner management' do
|
|
let(:environment_id) { runner.execution_environment.id }
|
|
|
|
before { allow(strategy_class).to receive(:request_from_management).and_raise(Runner::Error::EnvironmentNotFound) }
|
|
|
|
it 'syncs the execution environment' do
|
|
expect(strategy_class).to receive(:sync_environment).with(runner.execution_environment)
|
|
runner.send(:request_new_id)
|
|
rescue Runner::Error::EnvironmentNotFound
|
|
# Ignored because this error is expected (see tests below).
|
|
end
|
|
|
|
it 'raises an error when the environment could be synced' do
|
|
allow(strategy_class).to receive(:sync_environment).with(runner.execution_environment).and_return(true)
|
|
expect { runner.send(:request_new_id) }.to raise_error(Runner::Error::EnvironmentNotFound, /#{environment_id}.*successfully synced/)
|
|
end
|
|
|
|
it 'raises an error when the environment could not be synced' do
|
|
allow(strategy_class).to receive(:sync_environment).with(runner.execution_environment).and_raise(Runner::Error::EnvironmentNotFound)
|
|
expect { runner.send(:request_new_id) }.to raise_error(Runner::Error::EnvironmentNotFound, /#{environment_id}.*could not be synced/)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '::for' do
|
|
let(:contributor) { create(:external_user) }
|
|
let(:exercise) { create(:fibonacci) }
|
|
|
|
context 'when the runner could not be saved' do
|
|
before { allow(strategy_class).to receive(:request_from_management).and_return(nil) }
|
|
|
|
it 'raises an error' do
|
|
expect { described_class.for(contributor, exercise.execution_environment) }.to raise_error(Runner::Error::Unknown, /could not be saved/)
|
|
end
|
|
end
|
|
|
|
context 'when a runner already exists' do
|
|
let!(:existing_runner) { create(:runner, contributor:, execution_environment: exercise.execution_environment) }
|
|
|
|
it 'returns the existing runner' do
|
|
new_runner = described_class.for(contributor, exercise.execution_environment)
|
|
expect(new_runner).to eq(existing_runner)
|
|
end
|
|
|
|
it 'sets the strategy' do
|
|
runner = described_class.for(contributor, exercise.execution_environment)
|
|
expect(runner.strategy).to be_present
|
|
end
|
|
end
|
|
|
|
context 'when no runner exists' do
|
|
before { allow(strategy_class).to receive(:request_from_management).and_return(runner_id) }
|
|
|
|
it 'returns a new runner' do
|
|
runner = described_class.for(contributor, exercise.execution_environment)
|
|
expect(runner).to be_valid
|
|
end
|
|
end
|
|
end
|
|
end
|