Skip to content
This repository was archived by the owner on Sep 25, 2019. It is now read-only.
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 lib/coursemology/evaluator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module Coursemology::Evaluator
autoload :Models
autoload :Services
autoload :StringIO
autoload :Utils

eager_autoload do
autoload :Logging
Expand Down
20 changes: 20 additions & 0 deletions lib/coursemology/evaluator/models/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@ def initialize
verbose!
before_request :add_authentication

# Sets the key of the model. This is the key that all attributes are nested under, the same as
# the +require+ directive in the controller of the web application.
#
# @param [String] key The key to prefix all attributes with.
def self.model_key(key)
before_request do |name, param|
fix_put_parameters(key, name, param) if [:post, :patch, :put].include?(param.method[:method])
end
end
private_class_method :model_key

# Fixes the request parameters when executing a POST, PATCH or PUT.
#
# @param [String] key The key to prefix all attributes with.
# @param [Request] param The request parameter to prepend the key with.
def self.fix_put_parameters(key, _, param)
param.post_params = { key => param.post_params } unless param.post_params.empty?
end
private_class_method :fix_put_parameters

private

# Adds the authentication email and token to the request.
Expand Down
8 changes: 6 additions & 2 deletions lib/coursemology/evaluator/models/programming_evaluation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ class Coursemology::Evaluator::Models::ProgrammingEvaluation < Coursemology::Eva
extend ActiveSupport::Autoload
autoload :Package

get :find, 'courses/assessment/programming_evaluations/:id'
post :allocate, 'courses/assessment/programming_evaluations/allocate'
request_body_type :json
model_key :programming_evaluation

get :find, 'courses/assessment/programming_evaluations/:id'.freeze
post :allocate, 'courses/assessment/programming_evaluations/allocate'.freeze
put :save, 'courses/assessment/programming_evaluations/:id/result'.freeze

# Gets the language for the programming evaluation.
#
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ def execute
copy_package(container)
execute_package(container)

Result.new(container.logs(stdout: true), container.logs(stderr: true),
extract_test_report(container))
extract_result(container)
ensure
destroy_container(container) if container
end
Expand Down Expand Up @@ -115,6 +114,13 @@ def execute_package(container)
end
end

def extract_result(container)
logs = container.logs(stdout: true, stderr: true)

_, stdout, stderr = Coursemology::Evaluator::Utils.parse_docker_stream(logs)
Result.new(stdout, stderr, extract_test_report(container))
end

def extract_test_report(container)
stream = extract_test_report_archive(container)

Expand Down
41 changes: 41 additions & 0 deletions lib/coursemology/evaluator/utils.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module Coursemology::Evaluator::Utils
# Represents one block of the Docker Attach protocol.
DockerAttachBlock = Struct.new(:stream, :length, :bytes)

# Parses a Docker +attach+ protocol stream into its constituent protocols.
#
# See https://docs.docker.com/engine/reference/api/docker_remote_api_v1.19/#attach-to-a-container.
#
# This drops all blocks belonging to streams other than STDIN, STDOUT, or STDERR.
#
# @param [String] string The input stream to parse.
# @return [Array<(String, String, String)>] The stdin, stdout, and stderr output.
def self.parse_docker_stream(string)
result = ['', '', '']
stream = StringIO.new(string)

while (block = parse_docker_stream_read_block(stream))
next if block.stream >= result.length
result[block.stream] << block.bytes
end

stream.close
result
end

# Reads a block from the given stream, and parses it according to the Docker +attach+ protocol.
#
# @param [IO] stream The stream to read.
# @raise [IOError] If the stream is corrupt.
# @return [DockerAttachBlock] If there is data in the stream.
# @return [nil] If there is no data left in the stream.
def self.parse_docker_stream_read_block(stream)
header = stream.read(8)
return nil if header.blank?
fail IOError unless header.length == 8

console_stream, _, _, _, length = header.unpack('C4N')
DockerAttachBlock.new(console_stream, length, stream.read(length))
end
private_class_method :parse_docker_stream_read_block
end
4 changes: 2 additions & 2 deletions spec/coursemology/evaluator/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

describe '#allocate_evaluations' do
context 'when an evaluation is provided' do
let(:dummy_evaluation) { build(:programming_evaluation) }
let(:dummy_evaluation) { build_stubbed(:programming_evaluation) }
before do
expect(Coursemology::Evaluator::Models::ProgrammingEvaluation).to \
receive(:allocate).and_return([dummy_evaluation])
Expand Down Expand Up @@ -62,7 +62,7 @@

describe '#on_evaluation' do
let(:dummy_evaluation) do
build(:programming_evaluation).tap do |dummy_evaluation|
build_stubbed(:programming_evaluation).tap do |dummy_evaluation|
expect(dummy_evaluation).to receive(:evaluate)
end
end
Expand Down
41 changes: 41 additions & 0 deletions spec/coursemology/evaluator/models/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,47 @@
end
end

describe '.model_key' do
class self::DummyModel < Coursemology::Evaluator::Models::Base
model_key :dummy_model

put :save, '/'
end
subject { self.class::DummyModel.new }

let(:dummy_response) do
double.tap do |dummy_response|
allow(dummy_response).to receive(:on_complete)
allow(dummy_response).to receive(:finished?).and_return(true)
end
end

before do
expect(self.class::DummyModel).to receive(:fix_put_parameters).
and_wrap_original do |m, *args|
@request = args.last
expect(@request).to receive(:do_request).and_return(dummy_response)

m.call(*args)
end
end

it 'prepends all request data with the key' do
subject.lol = true
subject.save

expect(@request.post_params).to have_key(:dummy_model)
end

context 'when the data is empty' do
it 'does not prepend the key' do
subject.save

expect(@request.post_params).not_to have_key(:dummy_model)
end
end
end

describe '#adds_authentication' do
with_mock_client(api_user_email: 'email', api_token: 'token') do
let(:request) do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,22 @@ def evaluate_result
end
end

describe '#extract_result' do
let(:image) { 'python:2.7' }
let(:container) do
subject.send(:create_container, image).tap do |container|
subject.send(:execute_package, container)
end
end
after { subject.send(:destroy_container, container) }

it 'does not expose raw Docker Attach Protocol in the output' do
result = subject.send(:extract_result, container)
expect(result.stdout).not_to include("\u0000")
expect(result.stderr).not_to include("\u0000")
end
end

describe '#extract_test_report' do
let(:image) { 'python:2.7' }
let(:report_path) { File.join(__dir__, '../../../fixtures/sample_report.xml') }
Expand Down
34 changes: 34 additions & 0 deletions spec/coursemology/evaluator/utils_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
RSpec.describe Coursemology::Evaluator::Utils do
describe '.parse_docker_stream' do
def build_docker_header(stream, length)
[stream, 0, 0, 0, length].pack('CCCCN')
end

def build_docker_block(stream, data)
build_docker_header(stream, data.length) + data
end

let(:data) { 'abcdefgh' }
let(:stream) { STDOUT.fileno }
let(:docker_block) { build_docker_block(stream, data) }
subject { Coursemology::Evaluator::Utils.parse_docker_stream(docker_block) }

it 'parses a Docker Attach protocol fragment' do
expect(subject).to eq(['', data, ''])
end

context 'when the stream has a malformed header' do
let(:docker_block) { super()[0..5] }
it 'raises an IOError' do
expect { subject }.to raise_error(IOError)
end
end

context 'when the stream has a block that is nonstandard' do
let(:stream) { 7 }
it 'does not return the stream' do
expect(subject).to eq(['', '', ''])
end
end
end
end
17 changes: 14 additions & 3 deletions spec/factories/programming_evaluations.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
module ProgrammingEvaluationFactory
def self.define_package(evaluation)
file = File.new(File.join(__dir__, '../fixtures/programming_question_template.zip'), 'rb')
package = Coursemology::Evaluator::Models::ProgrammingEvaluation::Package.new(file)
evaluation.instance_variable_set(:@package, package)
end
end

FactoryGirl.define do
factory :programming_evaluation, class: Coursemology::Evaluator::Models::ProgrammingEvaluation do
id 1
language Coursemology::Polyglot::Language::Python::Python2Point7.name
memory_limit 32
time_limit 5

after(:stub) do |evaluation|
evaluation.define_singleton_method(:save) {}
ProgrammingEvaluationFactory.define_package(evaluation)
end

after(:build) do |evaluation|
file = File.new(File.join(__dir__, '../fixtures/programming_question_template.zip'), 'rb')
package = Coursemology::Evaluator::Models::ProgrammingEvaluation::Package.new(file)
evaluation.instance_variable_set(:@package, package)
ProgrammingEvaluationFactory.define_package(evaluation)
end
end
end
14 changes: 7 additions & 7 deletions spec/fixtures/vcr/cassettes/client/allocation_unauthorized.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 11 additions & 13 deletions spec/fixtures/vcr/cassettes/client/no_pending_evaluations.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading