coursemology-evaluator 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,11 @@
1
+ class Coursemology::Evaluator::Models::ProgrammingEvaluation::Package
2
+ # The stream comprising the package.
3
+ attr_reader :stream
4
+
5
+ # Constructs a new Package.
6
+ #
7
+ # @param [IO] stream The stream comprising the package.
8
+ def initialize(stream)
9
+ @stream = stream
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ module Coursemology::Evaluator::Services
2
+ extend ActiveSupport::Autoload
3
+
4
+ autoload :EvaluateProgrammingPackageService
5
+ end
@@ -0,0 +1,150 @@
1
+ class Coursemology::Evaluator::Services::EvaluateProgrammingPackageService
2
+ Result = Struct.new(:stdout, :stderr, :test_report, :exit_code)
3
+
4
+ # The path to the Coursemology user home directory.
5
+ HOME_PATH = '/home/coursemology'.freeze
6
+
7
+ # The path to where the package will be extracted.
8
+ PACKAGE_PATH = File.join(HOME_PATH, 'package')
9
+
10
+ # The path to where the test report will be at.
11
+ REPORT_PATH = File.join(PACKAGE_PATH, 'report.xml')
12
+
13
+ # The ratio to multiply the memory limits from our evaluation to the container by.
14
+ MEMORY_LIMIT_RATIO = 1.megabyte / 1.kilobyte
15
+
16
+ # Executes the given package in a container.
17
+ #
18
+ # @param [Coursemology::Evaluator::Models::ProgrammingEvaluation] evaluation The evaluation
19
+ # from the server.
20
+ # @return [Coursemology::Evaluator::Services::EvaluateProgrammingPackageService::Result] The
21
+ # result of the evaluation.
22
+ def self.execute(evaluation)
23
+ new(evaluation).send(:execute)
24
+ end
25
+
26
+ # Creates a new service object.
27
+ def initialize(evaluation)
28
+ @evaluation = evaluation
29
+ @package = evaluation.package
30
+ end
31
+
32
+ private
33
+
34
+ # Evaluates the package.
35
+ #
36
+ # @return [Coursemology::Evaluator::Services::EvaluateProgrammingPackageService::Result]
37
+ def execute
38
+ container = create_container(@evaluation.language.class.docker_image)
39
+ copy_package(container)
40
+ execute_package(container)
41
+
42
+ extract_result(container)
43
+ ensure
44
+ destroy_container(container) if container
45
+ end
46
+
47
+ def create_container(image)
48
+ image_identifier = "coursemology/evaluator-image-#{image}"
49
+ Coursemology::Evaluator::DockerContainer.create(image_identifier, argv: container_arguments)
50
+ end
51
+
52
+ def container_arguments
53
+ result = []
54
+ result.push("-c#{@evaluation.time_limit}") if @evaluation.time_limit
55
+ result.push("-m#{@evaluation.memory_limit * MEMORY_LIMIT_RATIO}") if @evaluation.memory_limit
56
+
57
+ result
58
+ end
59
+
60
+ # Copies the contents of the package to the container.
61
+ #
62
+ # @param [Docker::Container] container The container to copy the package into.
63
+ def copy_package(container)
64
+ tar = tar_package(@package)
65
+ container.archive_in_stream(HOME_PATH) do
66
+ tar.read(Excon.defaults[:chunk_size]).to_s
67
+ end
68
+ end
69
+
70
+ # Converts the zip package into a tar package for the container.
71
+ #
72
+ # This also adds an additional +package+ directory to the start of the path, following tar
73
+ # convention.
74
+ #
75
+ # @param [Coursemology::Evaluator::Models::ProgrammingEvaluation::Package] package The package
76
+ # to convert to a tar.
77
+ # @return [IO] A stream containing the tar.
78
+ def tar_package(package)
79
+ tar_file_stream = StringIO.new
80
+ tar_file = Gem::Package::TarWriter.new(tar_file_stream)
81
+ Zip::File.open_buffer(package.stream) do |zip_file|
82
+ copy_archive(zip_file, tar_file, File.basename(PACKAGE_PATH))
83
+ tar_file.close
84
+ end
85
+
86
+ tar_file_stream.seek(0)
87
+ tar_file_stream
88
+ end
89
+
90
+ # Copies every entry from the zip archive to the tar archive, adding the optional prefix to the
91
+ # start of each file name.
92
+ #
93
+ # @param [Zip::File] zip_file The zip file to read from.
94
+ # @param [Gem::Package::TarWriter] tar_file The tar file to write to.
95
+ # @param [String] prefix The prefix to add to every file name in the tar.
96
+ def copy_archive(zip_file, tar_file, prefix = nil)
97
+ zip_file.each do |entry|
98
+ next unless entry.file?
99
+
100
+ zip_entry_stream = entry.get_input_stream
101
+ new_entry_name = prefix ? File.join(prefix, entry.name) : entry.name
102
+ tar_file.add_file(new_entry_name, 0664) do |tar_entry_stream|
103
+ IO.copy_stream(zip_entry_stream, tar_entry_stream)
104
+ end
105
+
106
+ zip_entry_stream.close
107
+ end
108
+ end
109
+
110
+ def execute_package(container)
111
+ container.start!
112
+ container.wait
113
+ end
114
+
115
+ def extract_result(container)
116
+ logs = container.logs(stdout: true, stderr: true)
117
+
118
+ _, stdout, stderr = Coursemology::Evaluator::Utils.parse_docker_stream(logs)
119
+ Result.new(stdout, stderr, extract_test_report(container), container.exit_code)
120
+ end
121
+
122
+ def extract_test_report(container)
123
+ stream = extract_test_report_archive(container)
124
+
125
+ tar_file = Gem::Package::TarReader.new(stream)
126
+ tar_file.each do |file|
127
+ return file.read
128
+ end
129
+ rescue Docker::Error::NotFoundError
130
+ return nil
131
+ end
132
+
133
+ # Extracts the test report from the container.
134
+ #
135
+ # @return [StringIO] The stream containing the archive, the pointer is reset to the start of the
136
+ # stream.
137
+ def extract_test_report_archive(container)
138
+ stream = StringIO.new
139
+ container.archive_out(REPORT_PATH) do |bytes|
140
+ stream.write(bytes)
141
+ end
142
+
143
+ stream.seek(0)
144
+ stream
145
+ end
146
+
147
+ def destroy_container(container)
148
+ container.delete
149
+ end
150
+ end
@@ -0,0 +1,13 @@
1
+ # Adapter for StringIO for compatibility with RubyZip.
2
+ #
3
+ # StringIO does not inherit from IO, so RubyZip does not accept StringIO in place of IO.
4
+ class Coursemology::Evaluator::StringIO < ::StringIO
5
+ def is_a?(klass)
6
+ klass == IO || super
7
+ end
8
+
9
+ # RubyZip assumes all IO objects respond to path.
10
+ def path
11
+ self
12
+ end
13
+ end
@@ -0,0 +1,41 @@
1
+ module Coursemology::Evaluator::Utils
2
+ # Represents one block of the Docker Attach protocol.
3
+ DockerAttachBlock = Struct.new(:stream, :length, :bytes)
4
+
5
+ # Parses a Docker +attach+ protocol stream into its constituent protocols.
6
+ #
7
+ # See https://docs.docker.com/engine/reference/api/docker_remote_api_v1.19/#attach-to-a-container.
8
+ #
9
+ # This drops all blocks belonging to streams other than STDIN, STDOUT, or STDERR.
10
+ #
11
+ # @param [String] string The input stream to parse.
12
+ # @return [Array<(String, String, String)>] The stdin, stdout, and stderr output.
13
+ def self.parse_docker_stream(string)
14
+ result = ['', '', '']
15
+ stream = StringIO.new(string)
16
+
17
+ while (block = parse_docker_stream_read_block(stream))
18
+ next if block.stream >= result.length
19
+ result[block.stream] << block.bytes
20
+ end
21
+
22
+ stream.close
23
+ result
24
+ end
25
+
26
+ # Reads a block from the given stream, and parses it according to the Docker +attach+ protocol.
27
+ #
28
+ # @param [IO] stream The stream to read.
29
+ # @raise [IOError] If the stream is corrupt.
30
+ # @return [DockerAttachBlock] If there is data in the stream.
31
+ # @return [nil] If there is no data left in the stream.
32
+ def self.parse_docker_stream_read_block(stream)
33
+ header = stream.read(8)
34
+ return nil if header.blank?
35
+ fail IOError unless header.length == 8
36
+
37
+ console_stream, _, _, _, length = header.unpack('C4N')
38
+ DockerAttachBlock.new(console_stream, length, stream.read(length))
39
+ end
40
+ private_class_method :parse_docker_stream_read_block
41
+ end
@@ -1,4 +1,4 @@
1
1
  module Coursemology; end
2
2
  module Coursemology::Evaluator
3
- VERSION = '0.0.0'
3
+ VERSION = '0.1.0'
4
4
  end
@@ -0,0 +1,2 @@
1
+ # This augments the base polyglot library with methods needed for the evaluator.
2
+ Dir[File.join(__dir__, '**/*.rb')].each { |f| require f }
@@ -0,0 +1,23 @@
1
+ class Coursemology::Polyglot::Language
2
+ # Finds the language class with the specified name.
3
+ #
4
+ # @param [String] type The name of the class.
5
+ # @return [nil] If the type is not defined.
6
+ # @return [Class] If the type was found.
7
+ def self.find_by(type:)
8
+ class_ = concrete_languages.find { |language| language.name == type }
9
+ class_.new if class_
10
+ end
11
+
12
+ # Finds the language class with the specified name.
13
+ #
14
+ # @param [String] type The name of the class.
15
+ # @return [Class] If the type was found.
16
+ # @raise [ArgumentError] When the type was not found.
17
+ def self.find_by!(type:)
18
+ language = find_by(type: type)
19
+ fail ArgumentError, "Cannot find the language #{type}" unless language
20
+
21
+ language
22
+ end
23
+ end
metadata CHANGED
@@ -1,43 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coursemology-evaluator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Low
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-12-12 00:00:00.000000000 Z
11
+ date: 2016-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.10'
19
+ version: '0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.10'
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,23 +52,201 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: factory_girl
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: coveralls
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: codeclimate-test-reporter
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: vcr
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: activesupport
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 4.2.0
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 4.2.0
139
+ - !ruby/object:Gem::Dependency
140
+ name: flexirest
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '1.2'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '1.2'
153
+ - !ruby/object:Gem::Dependency
154
+ name: faraday_middleware
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: coursemology-polyglot
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: 0.0.3
174
+ type: :runtime
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: 0.0.3
181
+ - !ruby/object:Gem::Dependency
182
+ name: docker-api
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: 1.2.5
188
+ type: :runtime
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: 1.2.5
195
+ - !ruby/object:Gem::Dependency
196
+ name: rubyzip
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :runtime
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
55
209
  description: Sets up a consistent environment for evaluating programming packages.
56
210
  email:
57
211
  - joel@joelsplace.sg
58
- executables: []
212
+ executables:
213
+ - evaluator
59
214
  extensions: []
60
215
  extra_rdoc_files: []
61
216
  files:
217
+ - ".env"
62
218
  - ".gitignore"
219
+ - ".hound.yml"
63
220
  - ".idea/Coursemology Evaluator.iml"
64
221
  - ".rspec"
222
+ - ".rubocop.unhound.yml"
223
+ - ".rubocop.yml"
65
224
  - ".travis.yml"
66
225
  - Gemfile
226
+ - Gemfile.lock
227
+ - Procfile
67
228
  - README.md
68
229
  - Rakefile
230
+ - bin/evaluator
69
231
  - coursemology-evaluator.gemspec
70
232
  - lib/coursemology/evaluator.rb
233
+ - lib/coursemology/evaluator/cli.rb
234
+ - lib/coursemology/evaluator/client.rb
235
+ - lib/coursemology/evaluator/docker_container.rb
236
+ - lib/coursemology/evaluator/logging.rb
237
+ - lib/coursemology/evaluator/logging/client_log_subscriber.rb
238
+ - lib/coursemology/evaluator/logging/docker_log_subscriber.rb
239
+ - lib/coursemology/evaluator/models.rb
240
+ - lib/coursemology/evaluator/models/base.rb
241
+ - lib/coursemology/evaluator/models/programming_evaluation.rb
242
+ - lib/coursemology/evaluator/models/programming_evaluation/package.rb
243
+ - lib/coursemology/evaluator/services.rb
244
+ - lib/coursemology/evaluator/services/evaluate_programming_package_service.rb
245
+ - lib/coursemology/evaluator/string_io.rb
246
+ - lib/coursemology/evaluator/utils.rb
71
247
  - lib/coursemology/evaluator/version.rb
248
+ - lib/coursemology/polyglot/extensions.rb
249
+ - lib/coursemology/polyglot/extensions/language.rb
72
250
  homepage: http://coursemology.org
73
251
  licenses:
74
252
  - MIT
@@ -89,7 +267,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
89
267
  version: '0'
90
268
  requirements: []
91
269
  rubyforge_project:
92
- rubygems_version: 2.4.8
270
+ rubygems_version: 2.4.5.1
93
271
  signing_key:
94
272
  specification_version: 4
95
273
  summary: Coursemology programming package evaluator