coursemology-evaluator 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,50 +1,50 @@
1
- # frozen_string_literal: true
2
- class Coursemology::Evaluator::Models::Base < Flexirest::Base
3
- class << self
4
- attr_accessor :api_user_email
5
- attr_accessor :api_token
6
-
7
- def initialize
8
- Flexirest::Base.perform_caching = false
9
- default_config = Flexirest::Base.faraday_config
10
- Flexirest::Base.faraday_config do |faraday|
11
- # +follow_redirects+ must be added before declaring the adapter. See faraday_middleware#32,
12
- # last comment.
13
- faraday.response :follow_redirects
14
-
15
- default_config.call(faraday)
16
- end
17
- end
18
- end
19
-
20
- verbose!
21
- before_request :add_authentication
22
-
23
- # Sets the key of the model. This is the key that all attributes are nested under, the same as
24
- # the +require+ directive in the controller of the web application.
25
- #
26
- # @param [String] key The key to prefix all attributes with.
27
- def self.model_key(key)
28
- before_request do |name, param|
29
- fix_put_parameters(key, name, param) if [:post, :patch, :put].include?(param.method[:method])
30
- end
31
- end
32
- private_class_method :model_key
33
-
34
- # Fixes the request parameters when executing a POST, PATCH or PUT.
35
- #
36
- # @param [String] key The key to prefix all attributes with.
37
- # @param [Request] param The request parameter to prepend the key with.
38
- def self.fix_put_parameters(key, _, param)
39
- param.post_params = { key => param.post_params } unless param.post_params.empty?
40
- end
41
- private_class_method :fix_put_parameters
42
-
43
- private
44
-
45
- # Adds the authentication email and token to the request.
46
- def add_authentication(_, request)
47
- request.headers['X-User-Email'] = self.class.api_user_email
48
- request.headers['X-User-Token'] = self.class.api_token
49
- end
50
- end
1
+ # frozen_string_literal: true
2
+ class Coursemology::Evaluator::Models::Base < Flexirest::Base
3
+ class << self
4
+ attr_accessor :api_user_email
5
+ attr_accessor :api_token
6
+
7
+ def initialize
8
+ Flexirest::Base.perform_caching = false
9
+ default_config = Flexirest::Base.faraday_config
10
+ Flexirest::Base.faraday_config do |faraday|
11
+ # +follow_redirects+ must be added before declaring the adapter. See faraday_middleware#32,
12
+ # last comment.
13
+ faraday.response :follow_redirects
14
+
15
+ default_config.call(faraday)
16
+ end
17
+ end
18
+ end
19
+
20
+ verbose!
21
+ before_request :add_authentication
22
+
23
+ # Sets the key of the model. This is the key that all attributes are nested under, the same as
24
+ # the +require+ directive in the controller of the web application.
25
+ #
26
+ # @param [String] key The key to prefix all attributes with.
27
+ def self.model_key(key)
28
+ before_request do |name, param|
29
+ fix_put_parameters(key, name, param) if [:post, :patch, :put].include?(param.method[:method])
30
+ end
31
+ end
32
+ private_class_method :model_key
33
+
34
+ # Fixes the request parameters when executing a POST, PATCH or PUT.
35
+ #
36
+ # @param [String] key The key to prefix all attributes with.
37
+ # @param [Request] param The request parameter to prepend the key with.
38
+ def self.fix_put_parameters(key, _, param)
39
+ param.post_params = { key => param.post_params } unless param.post_params.empty?
40
+ end
41
+ private_class_method :fix_put_parameters
42
+
43
+ private
44
+
45
+ # Adds the authentication email and token to the request.
46
+ def add_authentication(_, request)
47
+ request.headers['X-User-Email'] = self.class.api_user_email
48
+ request.headers['X-User-Token'] = self.class.api_token
49
+ end
50
+ end
@@ -1,55 +1,55 @@
1
- # frozen_string_literal: true
2
- class Coursemology::Evaluator::Models::ProgrammingEvaluation < Coursemology::Evaluator::Models::Base
3
- extend ActiveSupport::Autoload
4
- autoload :Package
5
-
6
- request_body_type :json
7
- model_key :programming_evaluation
8
-
9
- get :find, 'courses/assessment/programming_evaluations/:id'.freeze
10
- post :allocate, 'courses/assessment/programming_evaluations/allocate'.freeze
11
- put :save, 'courses/assessment/programming_evaluations/:id/result'.freeze
12
-
13
- # Gets the language for the programming evaluation.
14
- #
15
- # @return [nil] If the language does not exist.
16
- # @return [Coursemology::Polyglot::Language] The language that the evaluation uses.
17
- def language
18
- Coursemology::Polyglot::Language.find_by(type: super)
19
- end
20
-
21
- # Sets the language for the programming evaluation.
22
- #
23
- # @param [String|nil|Coursemology::Polyglot::Language] language The language to set. If this is
24
- # a string, it is assumed to be the class name of the language.
25
- def language=(language)
26
- return super(language) if language.nil? || language.is_a?(String)
27
-
28
- fail ArgumentError unless language.is_a?(Coursemology::Polyglot::Language)
29
- super(language.class.name)
30
- end
31
-
32
- # Obtains the package.
33
- #
34
- # @return [Coursemology::Evaluator::Models::ProgrammingEvaluation::Package]
35
- def package
36
- @package ||= begin
37
- body = self.class._plain_request('courses/assessment/programming_evaluations/:id/package',
38
- :get, id: id)
39
- Package.new(Coursemology::Evaluator::StringIO.new(body))
40
- end
41
- end
42
-
43
- # Evaluates the package, and stores the result in this record.
44
- #
45
- # Call {Coursemology::Evaluator::Models::ProgrammingEvaluation#save} to save the record to the
46
- # server.
47
- def evaluate
48
- result = Coursemology::Evaluator::Services::EvaluateProgrammingPackageService.
49
- execute(self)
50
- self.stdout = result.stdout
51
- self.stderr = result.stderr
52
- self.test_report = result.test_report
53
- self.exit_code = result.exit_code
54
- end
55
- end
1
+ # frozen_string_literal: true
2
+ class Coursemology::Evaluator::Models::ProgrammingEvaluation < Coursemology::Evaluator::Models::Base
3
+ extend ActiveSupport::Autoload
4
+ autoload :Package
5
+
6
+ request_body_type :json
7
+ model_key :programming_evaluation
8
+
9
+ get :find, 'courses/assessment/programming_evaluations/:id'.freeze
10
+ post :allocate, 'courses/assessment/programming_evaluations/allocate'.freeze
11
+ put :save, 'courses/assessment/programming_evaluations/:id/result'.freeze
12
+
13
+ # Gets the language for the programming evaluation.
14
+ #
15
+ # @return [nil] If the language does not exist.
16
+ # @return [Coursemology::Polyglot::Language] The language that the evaluation uses.
17
+ def language
18
+ Coursemology::Polyglot::Language.find_by(type: super)
19
+ end
20
+
21
+ # Sets the language for the programming evaluation.
22
+ #
23
+ # @param [String|nil|Coursemology::Polyglot::Language] language The language to set. If this is
24
+ # a string, it is assumed to be the class name of the language.
25
+ def language=(language)
26
+ return super(language) if language.nil? || language.is_a?(String)
27
+
28
+ fail ArgumentError unless language.is_a?(Coursemology::Polyglot::Language)
29
+ super(language.class.name)
30
+ end
31
+
32
+ # Obtains the package.
33
+ #
34
+ # @return [Coursemology::Evaluator::Models::ProgrammingEvaluation::Package]
35
+ def package
36
+ @package ||= begin
37
+ body = self.class._plain_request('courses/assessment/programming_evaluations/:id/package',
38
+ :get, id: id)
39
+ Package.new(Coursemology::Evaluator::StringIO.new(body))
40
+ end
41
+ end
42
+
43
+ # Evaluates the package, and stores the result in this record.
44
+ #
45
+ # Call {Coursemology::Evaluator::Models::ProgrammingEvaluation#save} to save the record to the
46
+ # server.
47
+ def evaluate
48
+ result = Coursemology::Evaluator::Services::EvaluateProgrammingPackageService.
49
+ execute(self)
50
+ self.stdout = result.stdout
51
+ self.stderr = result.stderr
52
+ self.test_report = result.test_report
53
+ self.exit_code = result.exit_code
54
+ end
55
+ end
@@ -1,12 +1,12 @@
1
- # frozen_string_literal: true
2
- class Coursemology::Evaluator::Models::ProgrammingEvaluation::Package
3
- # The stream comprising the package.
4
- attr_reader :stream
5
-
6
- # Constructs a new Package.
7
- #
8
- # @param [IO] stream The stream comprising the package.
9
- def initialize(stream)
10
- @stream = stream
11
- end
12
- end
1
+ # frozen_string_literal: true
2
+ class Coursemology::Evaluator::Models::ProgrammingEvaluation::Package
3
+ # The stream comprising the package.
4
+ attr_reader :stream
5
+
6
+ # Constructs a new Package.
7
+ #
8
+ # @param [IO] stream The stream comprising the package.
9
+ def initialize(stream)
10
+ @stream = stream
11
+ end
12
+ end
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
- module Coursemology::Evaluator::Services
3
- extend ActiveSupport::Autoload
4
-
5
- autoload :EvaluateProgrammingPackageService
6
- end
1
+ # frozen_string_literal: true
2
+ module Coursemology::Evaluator::Services
3
+ extend ActiveSupport::Autoload
4
+
5
+ autoload :EvaluateProgrammingPackageService
6
+ end
@@ -1,151 +1,151 @@
1
- # frozen_string_literal: true
2
- class Coursemology::Evaluator::Services::EvaluateProgrammingPackageService
3
- Result = Struct.new(:stdout, :stderr, :test_report, :exit_code)
4
-
5
- # The path to the Coursemology user home directory.
6
- HOME_PATH = '/home/coursemology'.freeze
7
-
8
- # The path to where the package will be extracted.
9
- PACKAGE_PATH = File.join(HOME_PATH, 'package')
10
-
11
- # The path to where the test report will be at.
12
- REPORT_PATH = File.join(PACKAGE_PATH, 'report.xml')
13
-
14
- # The ratio to multiply the memory limits from our evaluation to the container by.
15
- MEMORY_LIMIT_RATIO = 1.megabyte / 1.kilobyte
16
-
17
- # Executes the given package in a container.
18
- #
19
- # @param [Coursemology::Evaluator::Models::ProgrammingEvaluation] evaluation The evaluation
20
- # from the server.
21
- # @return [Coursemology::Evaluator::Services::EvaluateProgrammingPackageService::Result] The
22
- # result of the evaluation.
23
- def self.execute(evaluation)
24
- new(evaluation).send(:execute)
25
- end
26
-
27
- # Creates a new service object.
28
- def initialize(evaluation)
29
- @evaluation = evaluation
30
- @package = evaluation.package
31
- end
32
-
33
- private
34
-
35
- # Evaluates the package.
36
- #
37
- # @return [Coursemology::Evaluator::Services::EvaluateProgrammingPackageService::Result]
38
- def execute
39
- container = create_container(@evaluation.language.class.docker_image)
40
- copy_package(container)
41
- execute_package(container)
42
-
43
- extract_result(container)
44
- ensure
45
- destroy_container(container) if container
46
- end
47
-
48
- def create_container(image)
49
- image_identifier = "coursemology/evaluator-image-#{image}"
50
- Coursemology::Evaluator::DockerContainer.create(image_identifier, argv: container_arguments)
51
- end
52
-
53
- def container_arguments
54
- result = []
55
- result.push("-c#{@evaluation.time_limit}") if @evaluation.time_limit
56
- result.push("-m#{@evaluation.memory_limit * MEMORY_LIMIT_RATIO}") if @evaluation.memory_limit
57
-
58
- result
59
- end
60
-
61
- # Copies the contents of the package to the container.
62
- #
63
- # @param [Docker::Container] container The container to copy the package into.
64
- def copy_package(container)
65
- tar = tar_package(@package)
66
- container.archive_in_stream(HOME_PATH) do
67
- tar.read(Excon.defaults[:chunk_size]).to_s
68
- end
69
- end
70
-
71
- # Converts the zip package into a tar package for the container.
72
- #
73
- # This also adds an additional +package+ directory to the start of the path, following tar
74
- # convention.
75
- #
76
- # @param [Coursemology::Evaluator::Models::ProgrammingEvaluation::Package] package The package
77
- # to convert to a tar.
78
- # @return [IO] A stream containing the tar.
79
- def tar_package(package)
80
- tar_file_stream = StringIO.new
81
- tar_file = Gem::Package::TarWriter.new(tar_file_stream)
82
- Zip::File.open_buffer(package.stream) do |zip_file|
83
- copy_archive(zip_file, tar_file, File.basename(PACKAGE_PATH))
84
- tar_file.close
85
- end
86
-
87
- tar_file_stream.seek(0)
88
- tar_file_stream
89
- end
90
-
91
- # Copies every entry from the zip archive to the tar archive, adding the optional prefix to the
92
- # start of each file name.
93
- #
94
- # @param [Zip::File] zip_file The zip file to read from.
95
- # @param [Gem::Package::TarWriter] tar_file The tar file to write to.
96
- # @param [String] prefix The prefix to add to every file name in the tar.
97
- def copy_archive(zip_file, tar_file, prefix = nil)
98
- zip_file.each do |entry|
99
- next unless entry.file?
100
-
101
- zip_entry_stream = entry.get_input_stream
102
- new_entry_name = prefix ? File.join(prefix, entry.name) : entry.name
103
- tar_file.add_file(new_entry_name, 0664) do |tar_entry_stream|
104
- IO.copy_stream(zip_entry_stream, tar_entry_stream)
105
- end
106
-
107
- zip_entry_stream.close
108
- end
109
- end
110
-
111
- def execute_package(container)
112
- container.start!
113
- container.wait
114
- end
115
-
116
- def extract_result(container)
117
- logs = container.logs(stdout: true, stderr: true)
118
-
119
- _, stdout, stderr = Coursemology::Evaluator::Utils.parse_docker_stream(logs)
120
- Result.new(stdout, stderr, extract_test_report(container), container.exit_code)
121
- end
122
-
123
- def extract_test_report(container)
124
- stream = extract_test_report_archive(container)
125
-
126
- tar_file = Gem::Package::TarReader.new(stream)
127
- tar_file.each do |file|
128
- return file.read
129
- end
130
- rescue Docker::Error::NotFoundError
131
- return nil
132
- end
133
-
134
- # Extracts the test report from the container.
135
- #
136
- # @return [StringIO] The stream containing the archive, the pointer is reset to the start of the
137
- # stream.
138
- def extract_test_report_archive(container)
139
- stream = StringIO.new
140
- container.archive_out(REPORT_PATH) do |bytes|
141
- stream.write(bytes)
142
- end
143
-
144
- stream.seek(0)
145
- stream
146
- end
147
-
148
- def destroy_container(container)
149
- container.delete
150
- end
151
- end
1
+ # frozen_string_literal: true
2
+ class Coursemology::Evaluator::Services::EvaluateProgrammingPackageService
3
+ Result = Struct.new(:stdout, :stderr, :test_report, :exit_code)
4
+
5
+ # The path to the Coursemology user home directory.
6
+ HOME_PATH = '/home/coursemology'.freeze
7
+
8
+ # The path to where the package will be extracted.
9
+ PACKAGE_PATH = File.join(HOME_PATH, 'package')
10
+
11
+ # The path to where the test report will be at.
12
+ REPORT_PATH = File.join(PACKAGE_PATH, 'report.xml')
13
+
14
+ # The ratio to multiply the memory limits from our evaluation to the container by.
15
+ MEMORY_LIMIT_RATIO = 1.megabyte / 1.kilobyte
16
+
17
+ # Executes the given package in a container.
18
+ #
19
+ # @param [Coursemology::Evaluator::Models::ProgrammingEvaluation] evaluation The evaluation
20
+ # from the server.
21
+ # @return [Coursemology::Evaluator::Services::EvaluateProgrammingPackageService::Result] The
22
+ # result of the evaluation.
23
+ def self.execute(evaluation)
24
+ new(evaluation).send(:execute)
25
+ end
26
+
27
+ # Creates a new service object.
28
+ def initialize(evaluation)
29
+ @evaluation = evaluation
30
+ @package = evaluation.package
31
+ end
32
+
33
+ private
34
+
35
+ # Evaluates the package.
36
+ #
37
+ # @return [Coursemology::Evaluator::Services::EvaluateProgrammingPackageService::Result]
38
+ def execute
39
+ container = create_container(@evaluation.language.class.docker_image)
40
+ copy_package(container)
41
+ execute_package(container)
42
+
43
+ extract_result(container)
44
+ ensure
45
+ destroy_container(container) if container
46
+ end
47
+
48
+ def create_container(image)
49
+ image_identifier = "coursemology/evaluator-image-#{image}"
50
+ Coursemology::Evaluator::DockerContainer.create(image_identifier, argv: container_arguments)
51
+ end
52
+
53
+ def container_arguments
54
+ result = []
55
+ result.push("-c#{@evaluation.time_limit}") if @evaluation.time_limit
56
+ result.push("-m#{@evaluation.memory_limit * MEMORY_LIMIT_RATIO}") if @evaluation.memory_limit
57
+
58
+ result
59
+ end
60
+
61
+ # Copies the contents of the package to the container.
62
+ #
63
+ # @param [Docker::Container] container The container to copy the package into.
64
+ def copy_package(container)
65
+ tar = tar_package(@package)
66
+ container.archive_in_stream(HOME_PATH) do
67
+ tar.read(Excon.defaults[:chunk_size]).to_s
68
+ end
69
+ end
70
+
71
+ # Converts the zip package into a tar package for the container.
72
+ #
73
+ # This also adds an additional +package+ directory to the start of the path, following tar
74
+ # convention.
75
+ #
76
+ # @param [Coursemology::Evaluator::Models::ProgrammingEvaluation::Package] package The package
77
+ # to convert to a tar.
78
+ # @return [IO] A stream containing the tar.
79
+ def tar_package(package)
80
+ tar_file_stream = StringIO.new
81
+ tar_file = Gem::Package::TarWriter.new(tar_file_stream)
82
+ Zip::File.open_buffer(package.stream) do |zip_file|
83
+ copy_archive(zip_file, tar_file, File.basename(PACKAGE_PATH))
84
+ tar_file.close
85
+ end
86
+
87
+ tar_file_stream.seek(0)
88
+ tar_file_stream
89
+ end
90
+
91
+ # Copies every entry from the zip archive to the tar archive, adding the optional prefix to the
92
+ # start of each file name.
93
+ #
94
+ # @param [Zip::File] zip_file The zip file to read from.
95
+ # @param [Gem::Package::TarWriter] tar_file The tar file to write to.
96
+ # @param [String] prefix The prefix to add to every file name in the tar.
97
+ def copy_archive(zip_file, tar_file, prefix = nil)
98
+ zip_file.each do |entry|
99
+ next unless entry.file?
100
+
101
+ zip_entry_stream = entry.get_input_stream
102
+ new_entry_name = prefix ? File.join(prefix, entry.name) : entry.name
103
+ tar_file.add_file(new_entry_name, 0664) do |tar_entry_stream|
104
+ IO.copy_stream(zip_entry_stream, tar_entry_stream)
105
+ end
106
+
107
+ zip_entry_stream.close
108
+ end
109
+ end
110
+
111
+ def execute_package(container)
112
+ container.start!
113
+ container.wait
114
+ end
115
+
116
+ def extract_result(container)
117
+ logs = container.logs(stdout: true, stderr: true)
118
+
119
+ _, stdout, stderr = Coursemology::Evaluator::Utils.parse_docker_stream(logs)
120
+ Result.new(stdout, stderr, extract_test_report(container), container.exit_code)
121
+ end
122
+
123
+ def extract_test_report(container)
124
+ stream = extract_test_report_archive(container)
125
+
126
+ tar_file = Gem::Package::TarReader.new(stream)
127
+ tar_file.each do |file|
128
+ return file.read.force_encoding(Encoding::UTF_8)
129
+ end
130
+ rescue Docker::Error::NotFoundError
131
+ return nil
132
+ end
133
+
134
+ # Extracts the test report from the container.
135
+ #
136
+ # @return [StringIO] The stream containing the archive, the pointer is reset to the start of the
137
+ # stream.
138
+ def extract_test_report_archive(container)
139
+ stream = StringIO.new
140
+ container.archive_out(REPORT_PATH) do |bytes|
141
+ stream.write(bytes)
142
+ end
143
+
144
+ stream.seek(0)
145
+ stream
146
+ end
147
+
148
+ def destroy_container(container)
149
+ container.delete
150
+ end
151
+ end