Skip to content

Commit 926d422

Browse files
author
Benedikt Artelt
committed
Merge branch 'codeocean-tuil' into origin-swp
2 parents cff0a14 + 5d570d4 commit 926d422

File tree

10 files changed

+289
-82
lines changed

10 files changed

+289
-82
lines changed

Gemfile.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,8 @@ GEM
242242
activerecord
243243
kaminari-core (= 1.2.2)
244244
kaminari-core (1.2.2)
245-
kramdown (2.4.0)
246-
rexml
245+
kramdown (2.5.1)
246+
rexml (>= 3.3.9)
247247
kramdown-parser-gfm (1.1.0)
248248
kramdown (~> 2.0)
249249
language_server-protocol (3.17.0.3)
@@ -338,7 +338,7 @@ GEM
338338
psych (5.2.0)
339339
stringio
340340
public_suffix (6.0.1)
341-
puma (6.4.3)
341+
puma (6.5.0)
342342
nio4r (~> 2.0)
343343
pundit (2.4.0)
344344
activesupport (>= 3.0.0)
@@ -484,7 +484,7 @@ GEM
484484
sprockets-rails
485485
tilt
486486
securerandom (0.3.2)
487-
selenium-webdriver (4.26.0)
487+
selenium-webdriver (4.27.0)
488488
base64 (~> 0.2)
489489
logger (~> 1.4)
490490
rexml (~> 3.2, >= 3.2.5)

app/controllers/submissions_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def render_file
100100
url = render_protected_upload_url(id: @file.id, filename: @file.filepath)
101101
redirect_to AuthenticatedUrlHelper.sign(url, @file)
102102
else
103-
response.set_header('Content-Length', @file.size)
103+
#response.set_header('Content-Length', @file.size)
104104
send_data(@file.content, filename: @file.name_with_extension, disposition: 'inline')
105105
end
106106
end

app/policies/exercise_policy.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def study_group_dashboard?
1414
end
1515

1616
def detailed_statistics?
17-
admin?
17+
admin? || teacher_in_study_group?
1818
end
1919

2020
%i[clone? destroy? edit? update?].each do |action|

config/environments/production.rb

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@
4444
# config.asset_host = "http://assets.example.com"
4545

4646
# Specifies the header that your server uses for sending files.
47-
# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
48-
config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
47+
# config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
48+
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
4949

5050
# Store uploaded files on the local file system (see config/storage.yml for options).
5151
config.active_storage.service = :local
@@ -60,11 +60,10 @@
6060
config.assume_ssl = true
6161

6262
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
63-
config.force_ssl = true
64-
63+
# config.force_ssl = true
6564
# Skip http-to-https redirect for the default health check endpoint.
6665
# config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }
67-
config.ssl_options = {hsts: {preload: true}}
66+
# config.ssl_options = {hsts: {preload: true}}
6867

6968
if ENV['RAILS_LOG_TO_STDOUT'].present?
7069
# Log to STDOUT by default
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# How to add a new programming language to Codeocean
2+
## Overview
3+
Adding a new programming language to Codeocean is done in three steps:
4+
1. Create a Docker image that contains the necessary tools to compile, run, etc.
5+
2. Create a Ruby file under lib/ that specifies the name of the testing framework, parses the output of the testing tools, and returns the number of tests, number of failed tests, and associated error messages.
6+
3. Create a new execution environment for the programming language in Codeoceans graphical interface.
7+
8+
### 1. Create the Docker image
9+
The following Dockerfile is used as a starting point:
10+
```dockerfile
11+
FROM openhpi/docker_exec_phusion
12+
LABEL description=''
13+
LABEL run_command=''
14+
LABEL test_command=''
15+
16+
# insert installation routines, environment variable adjustment, etc. here
17+
18+
# switch user
19+
USER user
20+
```
21+
22+
Each execution environment must be derived from `openhpi/docker_exec_phusion`. This image is based on `phusion/baseimage:master`, a Docker-optimized version of Ubuntu that provides, among other things, the [install_clean](https://github.com/phusion/baseimage-docker#overview) command for installing Ubuntu packages.
23+
At the end, the user needs to be switched so that he does not get elevated privileges in his container.
24+
25+
Specify the run and test command to be used for the programming language. You can use the parameter `%{filename}` inside the command string. When a user clicks on *run* or *test* for a certain file in Codeoceans graphical interface, `%{filename}` will be replaced with the name of this file. Note that the run and test command are not automatically imported into Codeocean when you add a new Execution Environment.
26+
27+
Add the installation routines and customizations needed for the programming language.
28+
29+
Build with the following command:
30+
`docker build --no-cache -t openhpi/co_execenv_<language> .`
31+
where `<language>` is the corresponding programming language.
32+
33+
Select the image in Codeoceans graphical interface when creating a new Execution Environment.
34+
35+
### 2. Create the Adapter
36+
Create a file named my_adapter.rb and add the following content:
37+
```ruby
38+
# frozen_string_literal: true
39+
40+
class MyAdapter < TestingFrameworkAdapter
41+
def self.framework_name
42+
# insert here what you want to call your test framework
43+
end
44+
45+
def parse_output(output)
46+
# insert code here that parses the output generated by the testing tools
47+
end
48+
end
49+
```
50+
51+
Change the name of the file and class to match your programming language and testing tools.
52+
This class is a subclass of *`TestingFrameworkAdapter`*. Implement the methods *`self.framework_name`* and *`parse_output(output)`*:
53+
*`self.framework_name`* shall return the name of the testing framework as a string.
54+
*`parse_output`* gets a [hash object](https://ruby-doc.org/core-3.1.2/Hash.html) *`output`* with the following keys and data types of values:
55+
- `file_role`: String
56+
- `waiting_for_container_time`: Float
57+
- `stdout`: String
58+
- `stderr`: String
59+
- `messages`: Array of hash objects with the keys `:cmd, :stream, :log, :timestamp`.
60+
- `exit_code`: Integer
61+
- `container_execution_time`: Float
62+
- `status`: Symbol
63+
64+
*`parse_output`* shall return a [hash object](https://ruby-doc.org/core-3.1.2/Hash.html) with the following keys:
65+
- `count`: Integer ( the number of executed tests)
66+
- `failed`: Integer ( the number of failed tests)
67+
- `error_messages`: Array of strings (the error messages of the failed tests.)
68+
69+
### 3. Create a new Execution Environment
70+
1. In Codeoceans graphical interface select `Administration` > `Execution Environments` and click on `Add Execution Environment`.
71+
2. Give it a name and default file type.
72+
3. Select the Docker image you built above.
73+
4. Copy and paste the run and test command from the Dockerfile into the corresponding fields.
74+
5. Select the testing framework you defined above in *`self.framework_name`*.
75+
6. It might be necessary to set the `Prewarming Pool Size` to > 0.
76+
7. Make further adjustments if necessary and then click on `Create Execution Environment`.

docs/cargo_test_adapter.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# The Cargo Test Adapter
2+
This documentation focusses on the file `lib/cargo_test_adapter.rb`. It only explains the functions `parse_json_objects_per_line(string)` and `extract_test_result_parameters(parsed_json_objects)`. For more details on `self.framework_name` and `parse_output(output)` see [add_new_programming_language.md](add_new_programming_language.md).
3+
4+
The code assumes that the JSON output was generated by Cargo version 1.59.0 with the command `cargo test --no-fail-fast -q --color never --message-format json-diagnostic-short -- --color never -Z unstable-options --format json`. Note that the [json output format for tests is unstable](https://doc.rust-lang.org/rustc/tests/index.html#--format-format) and might change with newer Rust and Cargo versions. If that happens the code must be updated.
5+
6+
## Example project
7+
Let us look at a Rust project consisting of three files:
8+
9+
`Cargo.toml`:
10+
```
11+
[package]
12+
name = "add"
13+
version = "0.1.0"
14+
edition = "2021"
15+
16+
[dependencies]
17+
```
18+
19+
`src/main.rs`:
20+
```rust
21+
fn main() { println!("Hello, world!"); }
22+
23+
fn add(a: i32, b: i32) -> i32 { a }
24+
25+
fn mult(a: i32, b: i32) -> i32 { a }
26+
27+
#[cfg(test)]
28+
mod tests;
29+
```
30+
31+
`src/tests.rs`:
32+
```rust
33+
use super::*;
34+
35+
#[test]
36+
fn test_add() {
37+
assert_eq!(add(1,1), 2);
38+
}
39+
40+
#[test]
41+
fn test_mutl() {
42+
assert_eq!(mult(1,0), 0);
43+
assert_eq!(mult(1,1), 1);
44+
assert_eq!(mult(-1,1), -1);
45+
assert_eq!(mult(1,-1), -1);
46+
assert_eq!(mult(-1,-1), 1);
47+
}
48+
```
49+
50+
The output generated by our test command inside Codeocean will look like this (the compiler messages are left out because they are not needed):
51+
52+
```json
53+
[...]
54+
{"reason":"build-finished","success":true}
55+
{ "type": "suite", "event": "started", "test_count": 2 }
56+
{ "type": "test", "event": "started", "name": "tests::test_add" }
57+
{ "type": "test", "event": "started", "name": "tests::test_mutl" }
58+
{ "type": "test", "name": "tests::test_mutl", "event": "failed", "stdout": "thread 'tests::test_mutl' panicked at 'assertion failed: `(left == right)`\n left: `1`,\n right: `0`', src/tests.rs:10:5\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace\n" }
59+
{ "type": "test", "name": "tests::test_add", "event": "failed", "stdout": "thread 'tests::test_add' panicked at 'assertion failed: `(left == right)`\n left: `1`,\n right: `2`', src/tests.rs:5:5\n" }
60+
{ "type": "suite", "event": "failed", "passed": 0, "failed": 2, "allowed_fail": 0, "ignored": 0, "measured": 0, "filtered_out": 0, "exec_time": 0.000608784 }
61+
error: test failed, to rerun pass '--bin add'
62+
```
63+
64+
We see that each line contains either a valid JSON object or some text. In `parse_json_objects_per_line(string)` we therefore iterate over each line, parse it and if it is valid JSON push it into an array. If it is not a JSON object, we skip the line.
65+
66+
The array is then passed to `extract_test_result_parameters`. In here we first find out if the compilation was successful by finding the JSON object with the key-value pair `"reason" : "build-finished"` and accessing the value of the `"success"` key. If compilation was successful, we count the number of tests performed, the number of tests that failed and collect all error messages. The `count` and `failed` variables are updated with `+=` because there may be multiple JSON objects that match the `"type" : "suite"` key-value pairs and have either the `"test_count"` or `"failed"` keys. This happens, for example, when there are unit tests and integration tests in the same project.

lib/cargo_test_adapter.rb

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# frozen_string_literal: true
2+
3+
require 'json'
4+
5+
class CargoTestAdapter < TestingFrameworkAdapter
6+
def self.framework_name
7+
'CargoTest'
8+
end
9+
10+
def parse_output(output)
11+
extract_test_result_parameters(parse_json_objects_per_line(output[:stdout]))
12+
end
13+
14+
def pretty_format(test_result_obj)
15+
test_result_obj["stdout"]
16+
end
17+
18+
# Assumption: One JSON object per row. If a row is not a JSON object, it will not be included in parsed_json_objects.
19+
def parse_json_objects_per_line(string)
20+
parsed_json_objects = []
21+
string.each_line(chomp: true) { |line|
22+
begin
23+
obj = JSON.parse(line)
24+
parsed_json_objects.push(obj)
25+
rescue JSON::ParserError
26+
end
27+
}
28+
parsed_json_objects
29+
end
30+
31+
def extract_test_result_parameters(parsed_json_objects)
32+
compile_success = false
33+
count = 0
34+
failed = 0
35+
error_messages = []
36+
37+
parsed_json_objects.each { |obj|
38+
if obj["reason"] == "build-finished" && obj.has_key?("success")
39+
compile_success = obj["success"]
40+
end
41+
}
42+
43+
if compile_success
44+
parsed_json_objects.each { |obj|
45+
if obj["type"] == "suite"
46+
if obj.has_key?("test_count")
47+
count += obj["test_count"]
48+
elsif obj.has_key?("failed")
49+
failed += obj["failed"]
50+
end
51+
end
52+
if obj["type"] == "test" && obj["event"] == "failed"
53+
error_messages.push(pretty_format(obj))
54+
end
55+
}
56+
else
57+
error_messages.push("Could not compile. See below for more details.")
58+
end
59+
60+
{count: count, failed: failed, error_messages: error_messages}
61+
end
62+
end
63+

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
"@babel/preset-env": "^7.26.0",
88
"@babel/runtime": "^7.26.0",
99
"@egjs/hammerjs": "^2.0.17",
10-
"@fortawesome/fontawesome-free": "^6.6.0",
10+
"@fortawesome/fontawesome-free": "^6.7.1",
1111
"@popperjs/core": "^2.11.8",
12-
"@sentry/browser": "^8.38.0",
12+
"@sentry/browser": "^8.40.0",
1313
"@toast-ui/editor": "^3.2.2",
1414
"@webpack-cli/serve": "^2.0.5",
1515
"ace-builds": "^1.36.5",
@@ -42,7 +42,7 @@
4242
"sass-loader": "^16.0.3",
4343
"shakapacker": "8.0.2",
4444
"showdown": "^2.1.0",
45-
"sortablejs": "^1.15.3",
45+
"sortablejs": "^1.15.4",
4646
"sorttable": "^1.0.2",
4747
"style-loader": "^4.0.0",
4848
"terser-webpack-plugin": "^5.3.10",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/bash
2+
# A small script to start codeocean and dockercontainerpool on two tmux panes in the same window.
3+
# Must be called from within the vagrant VM.
4+
sessionname='codeocean-session'
5+
windowname='codeocean'
6+
tmux new-session -d -s $sessionname
7+
tmux new-window -n $windowname -t 0 -k 'cd /home/vagrant/dockercontainerpool && rails s -p 7100'
8+
tmux split-window -h -b -t 0 'cd /home/vagrant/codeocean && rails s -b 0.0.0.0 -p 7000'
9+
tmux set-option -t 0 remain-on-exit on
10+
tmux attach -t $sessionname
11+

0 commit comments

Comments
 (0)