transferred Code Ocean from original repository to GitHub

This commit is contained in:
Hauke Klement
2015-01-22 09:51:49 +01:00
commit 4cbf9970b1
683 changed files with 11979 additions and 0 deletions

26
lib/assessor.rb Normal file
View File

@ -0,0 +1,26 @@
class Assessor
MAXIMUM_SCORE = 1
def assess(output)
test_outcome = @testing_framework_adapter.test_outcome(output)
test_outcome.merge(score: calculate_score(test_outcome))
rescue Exception
{score: 0}
end
def calculate_score(test_outcome)
(test_outcome[:passed].to_f / test_outcome[:count].to_f)
end
private :calculate_score
def initialize(options = {})
if options[:execution_environment].testing_framework?
@testing_framework_adapter = Kernel.const_get(options[:execution_environment].testing_framework).new
else
raise Error.new('No testing framework adapter set!')
end
end
end
class Assessor::Error < RuntimeError
end

0
lib/assets/.keep Normal file
View File

View File

@ -0,0 +1,41 @@
(function() {
var DURATION = 10000;
var SEVERITIES = ['danger', 'info', 'success', 'warning'];
var buildFlash = function(options) {
if (options.text) {
var container = options.container;
var html = '';
if (options.icon) {
html += '<i class="' + options.icon.join(' ') + '">&nbsp;';
}
html += options.text;
container.html(html);
showFlashes();
}
};
var generateMethods = function() {
$.flash = {};
$.each(SEVERITIES, function(index, severity) {
$.flash[severity] = function(options) {
buildFlash($.extend(options, {
container: $('#flash-' + severity)
}));
};
});
};
var showFlashes = function() {
$('.flash').each(function() {
if ($(this).html() !== '') {
$(this).slideDown().delay(DURATION).slideUp(function() {
$(this).html('');
});
}
});
};
generateMethods();
$(showFlashes);
})();

View File

@ -0,0 +1,3 @@
.flash {
display: none;
}

14
lib/code_ocean/config.rb Normal file
View File

@ -0,0 +1,14 @@
module CodeOcean
class Config
def initialize(filename)
@filename = filename
end
def read
path = Rails.root.join('config', "#{@filename}.yml")
if File.exists?(path)
YAML.load_file(path)[Rails.env].symbolize_keys
end
end
end
end

175
lib/docker_client.rb Normal file
View File

@ -0,0 +1,175 @@
class DockerClient
CONFIG_PATH = Rails.root.join('config', 'docker.yml.erb')
CONTAINER_WORKSPACE_PATH = '/workspace'
LOCAL_WORKSPACE_ROOT = Rails.root.join('tmp', 'files', Rails.env)
attr_reader :assigned_ports
attr_reader :container_id
def bound_folders
@submission ? ["#{remote_workspace_path}:#{CONTAINER_WORKSPACE_PATH}"] : []
end
private :bound_folders
def self.check_availability!
initialize_environment
Timeout::timeout(config[:connection_timeout]) { Docker.version }
rescue Excon::Errors::SocketError, Timeout::Error
raise Error.new("The Docker host at #{Docker.url} is not reachable!")
end
def clean_workspace
FileUtils.rm_rf(local_workspace_path)
end
private :clean_workspace
def command_substitutions(filename)
{class_name: File.basename(filename, File.extname(filename)).camelize, filename: filename}
end
private :command_substitutions
def self.config
YAML.load(ERB.new(File.new(CONFIG_PATH, 'r').read).result)[Rails.env].with_indifferent_access
end
def copy_file_to_workspace(options = {})
FileUtils.cp(options[:file].native_file.path, File.join(local_workspace_path, options[:file].path || '', options[:file].name_with_extension))
end
def create_container(options = {})
Docker::Container.create('Cmd' => options[:command], 'Image' => @image.info['RepoTags'].first)
end
private :create_container
def create_workspace
@submission.collect_files.each do |file|
FileUtils.mkdir_p(File.join(local_workspace_path, file.path || ''))
if file.file_type.binary?
copy_file_to_workspace(file: file)
else
create_workspace_file(file: file)
end
end
end
private :create_workspace
def create_workspace_file(options = {})
file = File.new(File.join(local_workspace_path, options[:file].path || '', options[:file].name_with_extension), 'w')
file.write(options[:file].content)
file.close
end
private :create_workspace_file
def self.destroy_container(container)
container.stop.kill
if container.json['HostConfig']['PortBindings']
container.json['HostConfig']['PortBindings'].values.each do |configuration|
port = configuration.first['HostPort'].to_i
PortPool.release(port)
end
end
end
def execute_command(command, &block)
container = create_container(command: ['bash', '-c', command])
@container_id = container.id
start_container(container, &block)
end
def execute_in_workspace(submission, &block)
@submission = submission
create_workspace
block.call
ensure
clean_workspace if @submission
end
private :execute_in_workspace
def execute_run_command(submission, filename, &block)
execute_in_workspace(submission) do
execute_command(@execution_environment.run_command % command_substitutions(filename), &block)
end
end
def execute_test_command(submission, filename)
execute_in_workspace(submission) do
execute_command(@execution_environment.test_command % command_substitutions(filename))
end
end
def find_image_by_tag(tag)
Docker::Image.all.detect { |image| image.info['RepoTags'].flatten.include?(tag) }
end
private :find_image_by_tag
def self.image_tags
check_availability!
Docker::Image.all.map { |image| image.info['RepoTags'] }.flatten.reject { |tag| tag.include?('<none>') }
end
def initialize(options = {})
self.class.check_availability!
@execution_environment = options[:execution_environment]
@user = options[:user]
@image = find_image_by_tag(@execution_environment.docker_image)
raise Error.new("Cannot find image #{@execution_environment.docker_image}!") unless @image
end
def self.initialize_environment
unless config[:connection_timeout] && config[:workspace_root]
raise Error.new('Docker configuration missing!')
end
Docker.url = config[:host] if config[:host]
end
def local_workspace_path
File.join(LOCAL_WORKSPACE_ROOT, @submission.id.to_s)
end
private :local_workspace_path
def mapped_ports
@assigned_ports = []
(@execution_environment.exposed_ports || '').gsub(/\s/, '').split(',').map do |port|
@assigned_ports << PortPool.available_port
["#{port}/tcp", [{'HostPort' => @assigned_ports.last.to_s}]]
end.to_h
end
private :mapped_ports
def self.pull(docker_image)
`docker pull #{docker_image}` if docker_image
end
def remote_workspace_path
File.join(self.class.config[:workspace_root], @submission.id.to_s)
end
private :remote_workspace_path
def start_container(container, &block)
Timeout::timeout(@execution_environment.permitted_execution_time) do
container.start('Binds' => bound_folders, 'PortBindings' => mapped_ports)
container.wait(@execution_environment.permitted_execution_time)
stderr = []
stdout = []
container.streaming_logs(stderr: true, stdout: true) do |stream, chunk|
block.call(stream, chunk) if block_given?
if stream == :stderr
stderr.push(chunk)
else
stdout.push(chunk)
end
end
{status: :ok, stderr: stderr.join, stdout: stdout.join}
end
rescue Docker::Error::TimeoutError, Timeout::Error
{status: :timeout}
ensure
self.class.destroy_container(container)
end
private :start_container
end
class DockerClient::Error < RuntimeError
end
DockerClient.initialize_environment

75
lib/file_tree.rb Normal file
View File

@ -0,0 +1,75 @@
class FileTree < Tree::TreeNode
def file_icon(file)
if file.file_type.audio?
'fa fa-file-audio-o'
elsif file.file_type.image?
'fa fa-file-image-o'
elsif file.file_type.video?
'fa fa-file-video-o'
elsif file.read_only?
'fa fa-lock'
elsif file.file_type.executable?
'fa fa-file-code-o'
elsif file.file_type.renderable?
'fa fa-file-text-o'
else
'fa fa-file-o'
end
end
private :file_icon
def folder_icon
'fa fa-folder-o'
end
private :folder_icon
def initialize(files)
super(root_label)
files.each do |file|
parent = self
(file.path || '').split('/').each do |segment|
node = parent.children.detect { |child| child.name == segment } || parent.add(Tree::TreeNode.new(segment))
parent = node
end
parent.add(Tree::TreeNode.new(file.name_with_extension, file))
end
end
def map_to_js_tree(node)
{
children: node.children.map { |child| map_to_js_tree(child) },
icon: node_icon(node),
id: node.content.try(:ancestor_id),
state: {
disabled: !node.is_leaf?,
opened: !node.is_leaf?
},
text: node.name
}
end
private :map_to_js_tree
def node_icon(node)
if node.is_root?
folder_icon
elsif node.is_leaf?
file_icon(node.content)
else
folder_icon
end
end
private :node_icon
def root_label
I18n.t('exercises.editor_file_tree.file_root')
end
private :root_label
def to_js_tree
{
core: {
data: map_to_js_tree(self)
}
}.to_json
end
end

View File

@ -0,0 +1,30 @@
require 'rails/generators'
class TestingFrameworkAdapterGenerator < Rails::Generators::NamedBase
ADAPTER_PATH = ->(name) { Rails.root.join('lib', "#{name.underscore}_adapter.rb") }
SPEC_PATH = ->(name) { Rails.root.join('spec', 'lib', "#{name.underscore}_adapter_spec.rb") }
def create_testing_framework_adapter
create_file ADAPTER_PATH.call(file_name), <<-code
class #{file_name.camelize}Adapter < TestingFrameworkAdapter
def self.framework_name
'#{file_name.camelize}'
end
def parse_output(output)
end
end
code
end
def create_spec
create_file SPEC_PATH.call(file_name), <<-code
require 'rails_helper'
describe #{file_name.camelize}Adapter do
describe '#parse_output' do
end
end
code
end
end

19
lib/junit_adapter.rb Normal file
View File

@ -0,0 +1,19 @@
class JunitAdapter < TestingFrameworkAdapter
COUNT_REGEXP = /Tests run: (\d+)/
FAILURES_REGEXP = /Failures: (\d+)/
SUCCESS_REGEXP = /OK \((\d+) test[s]?\)/
def self.framework_name
'JUnit'
end
def parse_output(output)
if SUCCESS_REGEXP.match(output[:stdout])
{count: $1.to_i, passed: $1.to_i}
else
count = COUNT_REGEXP.match(output[:stdout]).try(:captures).try(:first).try(:to_i) || 0
failed = FAILURES_REGEXP.match(output[:stdout]).try(:captures).try(:first).try(:to_i) || 0
{count: count, failed: failed}
end
end
end

17
lib/nonce_store.rb Normal file
View File

@ -0,0 +1,17 @@
class NonceStore
def self.build_cache_key(nonce)
"lti_nonce_#{nonce}"
end
def self.add(nonce)
Rails.cache.write(build_cache_key(nonce), Time.now, expires_in: Lti::MAXIMUM_SESSION_AGE)
end
def self.delete(nonce)
Rails.cache.delete(build_cache_key(nonce))
end
def self.has?(nonce)
Rails.cache.exist?(build_cache_key(nonce))
end
end

16
lib/port_pool.rb Normal file
View File

@ -0,0 +1,16 @@
class PortPool
PORT_RANGE = DockerClient.config[:ports]
@available_ports = PORT_RANGE.to_a
@mutex = Mutex.new
def self.available_port
@mutex.synchronize do
@available_ports.delete(@available_ports.sample)
end
end
def self.release(port)
@available_ports << port if PORT_RANGE.include?(port) && !@available_ports.include?(port)
end
end

15
lib/py_unit_adapter.rb Normal file
View File

@ -0,0 +1,15 @@
class PyUnitAdapter < TestingFrameworkAdapter
COUNT_REGEXP = /Ran (\d+) tests/
FAILURES_REGEXP = /FAILED \(failures=(\d+)\)/
def self.framework_name
'PyUnit'
end
def parse_output(output)
count = COUNT_REGEXP.match(output[:stderr]).captures.first.to_i
matches = FAILURES_REGEXP.match(output[:stderr])
failed = matches ? matches.captures.try(:first).to_i : 0
{count: count, failed: failed}
end
end

14
lib/rspec_adapter.rb Normal file
View File

@ -0,0 +1,14 @@
class RspecAdapter < TestingFrameworkAdapter
REGEXP = /(\d+) examples?, (\d+) failures?/
def self.framework_name
'RSpec 3'
end
def parse_output(output)
captures = REGEXP.match(output[:stdout]).captures.map(&:to_i)
count = captures.first
failed = captures.second
{count: count, failed: failed}
end
end

12
lib/seeds_helper.rb Normal file
View File

@ -0,0 +1,12 @@
module SeedsHelper
def self.read_seed_file(filename)
file = File.new(seed_file_path(filename), 'r')
content = file.read
file.close
content
end
def self.seed_file_path(filename)
Rails.root.join('db', 'seeds', filename)
end
end

View File

@ -0,0 +1,16 @@
class SqlResultSetComparatorAdapter < TestingFrameworkAdapter
MISSING_TUPLES_REGEXP = /Missing tuples: \[\]/
UNEXPECTED_TUPLES_REGEXP = /Unexpected tuples: \[\]/
def self.framework_name
'SqlResultSetComparator'
end
def parse_output(output)
if MISSING_TUPLES_REGEXP.match(output[:stdout]) && UNEXPECTED_TUPLES_REGEXP.match(output[:stdout])
{count: 1, passed: 1}
else
{count: 1, failed: 1}
end
end
end

0
lib/tasks/.keep Normal file
View File

14
lib/tasks/docker.rake Normal file
View File

@ -0,0 +1,14 @@
namespace :docker do
desc 'List all installed Docker images'
task :images => :environment do
puts DockerClient.image_tags
end
desc 'Pull all Docker images referenced by execution environments'
task :pull => :environment do
ExecutionEnvironment.all.map(&:docker_image).each do |docker_image|
puts "Pulling #{docker_image}..."
DockerClient.pull(docker_image)
end
end
end

View File

@ -0,0 +1,25 @@
class TestingFrameworkAdapter
def augment_output(options = {})
if !options[:count]
options.merge(count: options[:failed] + options[:passed])
elsif !options[:failed]
options.merge(failed: options[:count] - options[:passed])
elsif !options[:passed]
options.merge(passed: options[:count] - options[:failed])
end
end
private :augment_output
def self.framework_name
self.name
end
def parse_output(output)
raise NotImplementedError.new("#{self.class} should implement #parse_output!")
end
private :parse_output
def test_outcome(output)
augment_output(parse_output(output))
end
end

20
lib/whistleblower.rb Normal file
View File

@ -0,0 +1,20 @@
class Whistleblower
PLACEHOLDER_REGEXP = /\$(\d)/
def find_hint(stderr)
Hint.where(execution_environment_id: @execution_environment.id).detect do |hint|
@matches = Regexp.new(hint.regular_expression).match(stderr)
end
end
private :find_hint
def generate_hint(stderr)
if hint = find_hint(stderr)
hint.message.gsub(PLACEHOLDER_REGEXP) { @matches[$1.to_i] }
end
end
def initialize(options = {})
@execution_environment = options[:execution_environment]
end
end