Files
codeocean/spec/lib/runner/strategy/docker_container_pool_spec.rb
Sebastian Serth 99bd46af1a Align project files with CodeHarbor
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.
2023-10-11 00:18:33 +02:00

270 lines
9.9 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
require 'pathname'
RSpec.describe Runner::Strategy::DockerContainerPool do
let(:runner_id) { attributes_for(:runner)[:runner_id] }
let(:execution_environment) { create(:ruby) }
let(:container_pool) { described_class.new(runner_id, execution_environment) }
let(:docker_container_pool_url) { 'https://localhost:1234' }
let(:config) { {url: docker_container_pool_url, unused_runner_expiration_time: 180} }
let(:container) { instance_double(Docker::Container) }
before do
allow(described_class).to receive(:config).and_return(config)
allow(container).to receive(:id).and_return(runner_id)
end
# All requests handle a Faraday error the same way.
shared_examples 'Faraday error handling' do |http_verb|
it 'raises a runner error' do
allow(Faraday).to receive(http_verb).and_raise(Faraday::TimeoutError)
expect { action.call }.to raise_error(Runner::Error::FaradayError)
end
end
describe '::request_from_management' do
let(:action) { -> { described_class.request_from_management(execution_environment) } }
let(:response_body) { nil }
let!(:request_runner_stub) do
WebMock
.stub_request(:post, "#{docker_container_pool_url}/docker_container_pool/get_container/#{execution_environment.id}")
.to_return(body: response_body, status: 200)
end
context 'when the DockerContainerPool returns an id' do
let(:response_body) { {id: runner_id}.to_json }
it 'successfully requests the DockerContainerPool' do
action.call
expect(request_runner_stub).to have_been_requested.once
end
it 'returns the received runner id' do
id = action.call
expect(id).to eq(runner_id)
end
end
context 'when the DockerContainerPool does not return an id' do
let(:response_body) { {}.to_json }
it 'raises an error' do
expect { action.call }.to raise_error(Runner::Error::NotAvailable)
end
end
context 'when the DockerContainerPool returns invalid JSON' do
let(:response_body) { '{hello}' }
it 'raises an error' do
expect { action.call }.to raise_error(Runner::Error::UnexpectedResponse)
end
end
include_examples 'Faraday error handling', :post
end
describe '#destroy_at_management' do
let(:action) { -> { container_pool.destroy_at_management } }
let!(:destroy_runner_stub) do
WebMock
.stub_request(:delete, "#{docker_container_pool_url}/docker_container_pool/destroy_container/#{runner_id}")
.to_return(body: nil, status: 200)
end
before { allow(container_pool).to receive(:container).and_return(container) }
it 'successfully requests the DockerContainerPool' do
action.call
expect(destroy_runner_stub).to have_been_requested.once
end
include_examples 'Faraday error handling', :delete
end
describe '#copy_files' do
let(:files) { [] }
let(:action) { -> { container_pool.copy_files(files) } }
let(:local_path) { Pathname.new('/tmp/container20') }
before do
allow(container_pool).to receive(:local_workspace_path).and_return(local_path)
allow(container_pool).to receive(:clean_workspace)
allow(FileUtils).to receive(:chmod_R)
end
it 'creates the workspace directory' do
expect(FileUtils).to receive(:mkdir_p).with(local_path)
container_pool.copy_files(files)
end
it 'cleans the workspace' do
expect(container_pool).to receive(:clean_workspace)
container_pool.copy_files(files)
end
it 'sets permission bits on the workspace' do
expect(FileUtils).to receive(:chmod_R).with('+rwtX', local_path)
container_pool.copy_files(files)
end
context 'when receiving a normal file' do
let(:file_content) { 'print("Hello World!")' }
let(:files) { [build(:file, content: file_content)] }
it 'writes the file to disk' do
expect(File).to receive(:write).with(local_path.join(files.first.filepath), file_content)
container_pool.copy_files(files)
end
it 'creates the file inside the workspace' do
expect(File).to receive(:write).with(local_path.join(files.first.filepath), files.first.content)
container_pool.copy_files(files)
end
it 'raises an error in case of an IOError' do
allow(File).to receive(:write).and_raise(IOError)
expect { container_pool.copy_files(files) }.to raise_error(Runner::Error::WorkspaceError, /#{files.first.filepath}/)
end
it 'does not create a directory for it' do
expect(FileUtils).not_to receive(:mkdir_p)
end
context 'when the file is inside a directory' do
let(:directory) { 'temp/dir' }
let(:files) { [build(:file, path: directory)] }
before do
allow(File).to receive(:write)
allow(FileUtils).to receive(:mkdir_p).with(local_path)
allow(FileUtils).to receive(:mkdir_p).with(local_path.join(directory))
end
it 'cleans the directory path' do
allow(container_pool).to receive(:local_path).and_call_original
expect(container_pool).to receive(:local_path).with(directory).and_call_original
container_pool.copy_files(files)
end
it 'creates the directory of the file' do
expect(FileUtils).to receive(:mkdir_p).with(local_path.join(directory))
container_pool.copy_files(files)
end
end
end
context 'when receiving a binary file' do
let(:files) { [build(:file, :image)] }
it 'copies the file inside the workspace' do
expect(File).to receive(:write).with(local_path.join(files.first.filepath), files.first.read)
container_pool.copy_files(files)
end
end
context 'when receiving multiple files' do
let(:files) { build_list(:file, 3) }
it 'creates all files' do
files.each do |file|
expect(File).to receive(:write).with(local_path.join(file.filepath), file.content)
end
container_pool.copy_files(files)
end
end
end
describe '#local_workspace_path' do
before { allow(container_pool).to receive(:container).and_return(container) }
it 'returns the local part of the mount binding' do
local_path = 'tmp/container20'
allow(container).to receive(:json).and_return({HostConfig: {Binds: ["#{local_path}:/workspace"]}}.as_json)
expect(container_pool.send(:local_workspace_path)).to eq(Pathname.new(local_path))
end
end
describe '#local_path' do
let(:local_workspace) { Pathname.new('/tmp/workspace') }
before { allow(container_pool).to receive(:local_workspace_path).and_return(local_workspace) }
it 'raises an error for relative paths outside of the workspace' do
expect { container_pool.send(:local_path, '../exercise.py') }.to raise_error(Runner::Error::WorkspaceError, %r{tmp/exercise.py})
end
it 'raises an error for absolute paths outside of the workspace' do
expect { container_pool.send(:local_path, '/test') }.to raise_error(Runner::Error::WorkspaceError, %r{/test})
end
it 'removes .. from the path' do
expect(container_pool.send(:local_path, 'test/../exercise.py')).to eq(Pathname.new('/tmp/workspace/exercise.py'))
end
it 'joins the given path with the local workspace path' do
expect(container_pool.send(:local_path, 'exercise.py')).to eq(Pathname.new('/tmp/workspace/exercise.py'))
end
end
describe '#clean_workspace' do
let(:local_workspace) { instance_double(Pathname) }
before { allow(container_pool).to receive(:local_workspace_path).and_return(local_workspace) }
it 'removes all children of the workspace recursively' do
children = %w[test.py exercise.rb subfolder].map {|child| Pathname.new(child) }
allow(local_workspace).to receive(:children).and_return(children)
expect(FileUtils).to receive(:rm_r).with(children, force: true)
container_pool.send(:clean_workspace)
end
it 'raises an error if the workspace does not exist' do
allow(local_workspace).to receive(:children).and_raise(Errno::ENOENT)
expect { container_pool.send(:clean_workspace) }.to raise_error(Runner::Error::WorkspaceError, /not exist/)
end
it 'raises an error if it lacks permission for deleting an entry' do
allow(local_workspace).to receive(:children).and_return(['test.py'])
allow(FileUtils).to receive(:remove_entry).and_raise(Errno::EPERM)
expect { container_pool.send(:clean_workspace) }.to raise_error(Runner::Error::WorkspaceError, /Not allowed/)
end
end
describe '#container' do
it 'raises an error if there is no container for the saved id' do
allow(Docker::Container).to receive(:get).and_raise(Docker::Error::NotFoundError)
expect { container_pool.send(:container) }.to raise_error(Runner::Error::RunnerNotFound)
end
it 'raises an error if the received container is not running' do
allow(Docker::Container).to receive(:get).and_return(container)
allow(container).to receive(:info).and_return({'State' => {'Running' => false}})
expect { container_pool.send(:container) }.to raise_error(Runner::Error::RunnerNotFound)
end
it 'returns the received container' do
allow(Docker::Container).to receive(:get).and_return(container)
allow(container).to receive(:info).and_return({'State' => {'Running' => true}})
expect(container_pool.send(:container)).to eq(container)
end
it 'does not request a container if one is saved' do
container_pool.instance_variable_set(:@container, container)
expect(Docker::Container).not_to receive(:get)
container_pool.send(:container)
end
end
describe '#attach_to_execution' do
# TODO: add tests here
let(:command) { 'ls' }
let(:event_loop) { Runner::EventLoop.new }
let(:action) { -> { container_pool.attach_to_execution(command, event_loop) } }
let(:websocket_url) { 'ws://ws.example.com/path/to/websocket' }
end
end