eval_in 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4ccfaf7503c99eaaece5c964e2dcbe82271ed1eb
4
- data.tar.gz: 87787a87e7f4540c0c021219a6959efb6f2cc247
3
+ metadata.gz: 9a9e273c4e6a93761fc1f35b47e35242397e8559
4
+ data.tar.gz: d78b600e459c3adece521d7c2540209e6c138955
5
5
  SHA512:
6
- metadata.gz: 3fa266438bdf528ec9d025bb63a858beb5aeea37e215d5ba0ace45badd654dd7eb88049c74af616104f58857c6dcb85c910d2386ce817816d041f19d7ebe93d5
7
- data.tar.gz: 350cc67567a1f13b103641cdb7517d2cc802b3e797167a34a622bd8447a4b87f2306d1f92093d3e3664731b57df75a692c963c12e7187d4f5d7fa8ea3ae0be27
6
+ metadata.gz: 3d6b167a6a021c1208dfac00a9de3078f90ee026bdf51198323afd14f2c2429ae00d49b63556ae1f092261a815852ac36c73bae57828d78bb8ab652fac62693e
7
+ data.tar.gz: 558dd782b6838e5c74945636bc9721cc684fd9ab3db11613de40f394eee39a6b888269a0c6bb40eb715f4547ebce905243c5dc25c2734fc9ab98369d6cd99ffb
data/Readme.md CHANGED
@@ -153,6 +153,138 @@ What languages can this run?
153
153
  </tr>
154
154
  </table>
155
155
 
156
+
157
+ Mocking for non-prod environments
158
+ ---------------------------------
159
+
160
+ ### Wiring the mock in
161
+
162
+ The mock that is provided will need to be set into place.
163
+ If the code is directly doing `EvalIn.call(...)`, then it is
164
+ a hard dependency, and there is no ablity to set the mock into place.
165
+
166
+ You will need to structure your code such that it receives the `eval_in`
167
+ service as an argument, or looks it up in some configuration or something
168
+ (I strongly prefer the former).
169
+ This will allow your test environment to give it a mock that tests that it
170
+ works in all the edge cases, or is just generally benign (doesn't make real http request in tests),
171
+ your dev environment to give it an `EvalIn` that looks so close to the real one you wouldn't even know,
172
+ and your prod environment to give it the actual `EvalIn` that actually uses the service.
173
+
174
+ If this is still unclear, here is an example:
175
+
176
+ ```ruby
177
+ # == before: hard dependency prevents you from giving a mock ==
178
+ def get_output(code)
179
+ EvalIn.call(code, language: 'ruby/mri-2.1').output
180
+ end
181
+
182
+ # == after: receive the thing you're talking to ==
183
+ def get_output(eval_in, code)
184
+ eval_in.call(code, language: 'ruby/mri-2.1').output
185
+ end
186
+
187
+ # in test, you call like this
188
+ output = 'the output'
189
+ assert_equal output, get_output(EvalIn::Mock.new(result: EvalIn::Result.new(output: output)), 'some code')
190
+
191
+ # in prod you call like this:
192
+ get_output(EvalIn, params[:code_sample])
193
+
194
+ # in dev you call like this:
195
+ get_output(EvalIn::Mock.new(languages: {'ruby/mri-2.1' => {program: RbConfig.ruby, args: []}}),
196
+ params[:code_sample])
197
+
198
+ # Now those last_two are probably in the same controller, right?
199
+ # How do you get it to do the right thing in the right env, then?
200
+ # well, you pass it to your controller, the same way you passed the `get_output`
201
+ # for example, you might do this in a rack middleware,
202
+ # then have it get the service from the `env` hash
203
+ # or set it in an environment initializer
204
+ ```
205
+
206
+ ### The provided mock
207
+
208
+ For a test or dev env where you don't care about correctness,
209
+ just that it does something that looks real,
210
+ you can make a mock that has a `Result`,
211
+ instantiated with any values you care about.
212
+
213
+ ```ruby
214
+ require 'eval_in/mock'
215
+ eval_in = EvalIn::Mock.new(result: EvalIn::Result.new(code: 'injected code', output: 'the output'))
216
+
217
+ eval_in.call('overridden code', language: 'irrelevant')
218
+ # => #<EvalIn::Result:0x007fb503a7a5e8
219
+ # @code="injected code",
220
+ # @exitstatus=-1,
221
+ # @language="",
222
+ # @language_friendly="",
223
+ # @output="the output",
224
+ # @status="",
225
+ # @url="">
226
+ ```
227
+
228
+ If you want your environment to behave approximately like the real `eval_in`,
229
+ you can instantiate a mock that knows how to evaluate code locally.
230
+ This is necessary, because it doesn't know how to execute these languages
231
+ (eval.in does that, it just knows how to talk to eval.in).
232
+ So you must provide it with a list of languages and how to execute them
233
+
234
+ This is probably idea for a dev environment, the results will be the most realistic.
235
+
236
+ **NOTE THAT THIS DOES NOT WORK ON JRUBY**
237
+
238
+ ```ruby
239
+ require 'eval_in/mock'
240
+
241
+ # a mock that can execute Ruby code and C code
242
+ eval_in = EvalIn::Mock.new(languages: {
243
+ 'ruby/mri-2.1' => {program: RbConfig.ruby,
244
+ args: []
245
+ },
246
+ 'c/gcc-4.9.1' => {program: RbConfig.ruby,
247
+ args: ['-e',
248
+ 'system "gcc -x c -o /tmp/eval_in_c_example #{ARGV.first}"
249
+ exec "/tmp/eval_in_c_example"']
250
+ },
251
+ })
252
+
253
+ eval_in.call 'puts "hello from ruby!"; exit 123', language: 'ruby/mri-2.1'
254
+ # => #<EvalIn::Result:0x007fb503a7d518
255
+ # @code="puts \"hello from ruby!\"; exit 123",
256
+ # @exitstatus=123,
257
+ # @language="ruby/mri-2.1",
258
+ # @language_friendly="ruby/mri-2.1",
259
+ # @output="hello from ruby!\n",
260
+ # @status="OK (0.072 sec real, 0.085 sec wall, 8 MB, 19 syscalls)",
261
+ # @url="https://eval.in/207744.json">
262
+
263
+ eval_in.call '#include <stdio.h>
264
+ int main() {
265
+ puts("hello from c!");
266
+ }', language: 'c/gcc-4.9.1'
267
+ # => #<EvalIn::Result:0x007fb503a850b0
268
+ # @code="#include <stdio.h>\nint main() {\n puts(\"hello from c!\");\n}",
269
+ # @exitstatus=0,
270
+ # @language="c/gcc-4.9.1",
271
+ # @language_friendly="c/gcc-4.9.1",
272
+ # @output="hello from c!\n",
273
+ # @status="OK (0.072 sec real, 0.085 sec wall, 8 MB, 19 syscalls)",
274
+ # @url="https://eval.in/207744.json">
275
+ ```
276
+
277
+ You can also provide a callback that will be invoked to handle the request.
278
+ This is probably ideal for testing more nuanced edge cases that the mock
279
+ doesn't inherently provide the ability to do.
280
+
281
+ ```ruby
282
+ require 'eval_in/mock'
283
+ eval_in = EvalIn::Mock.new on_call: -> code, options { raise EvalIn::RequestError, 'does my code do the right thing in the event of an exception?' }
284
+
285
+ eval_in.call('code', language: 'any') rescue $! # => #<EvalIn::RequestError: does my code do the right thing in the event of an exception?>
286
+ ```
287
+
156
288
  Attribution
157
289
  -----------
158
290
 
@@ -1,6 +1,6 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  $:.push File.expand_path("../lib", __FILE__)
3
- require "eval_in/version"
3
+ require "eval_in/constants"
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "eval_in"
@@ -1,80 +1,7 @@
1
- # encoding: utf-8
2
-
3
- require 'uri'
4
- require 'json'
5
- require 'net/http'
6
- require 'pathname'
7
- require 'eval_in/version'
1
+ require 'eval_in/http'
2
+ require 'eval_in/client'
8
3
 
9
4
  module EvalIn
10
- EvalInError = Class.new StandardError
11
- RequestError = Class.new EvalInError
12
- ResultNotFound = Class.new EvalInError
13
-
14
- # @example Generated with
15
- # nokogiri https://eval.in -e 'puts $_.xpath("//option/@value")'
16
- KNOWN_LANGUAGES = %w[
17
- c/gcc-4.4.3
18
- c/gcc-4.9.1
19
- c++/c++11-gcc-4.9.1
20
- c++/gcc-4.4.3
21
- c++/gcc-4.9.1
22
- coffeescript/node-0.10.29-coffee-1.7.1
23
- fortran/f95-4.4.3
24
- haskell/hugs98-sep-2006
25
- io/io-20131204
26
- javascript/node-0.10.29
27
- lua/lua-5.1.5
28
- lua/lua-5.2.3
29
- ocaml/ocaml-4.01.0
30
- php/php-5.5.14
31
- pascal/fpc-2.6.4
32
- perl/perl-5.20.0
33
- python/cpython-2.7.8
34
- python/cpython-3.4.1
35
- ruby/mri-1.0
36
- ruby/mri-1.8.7
37
- ruby/mri-1.9.3
38
- ruby/mri-2.0.0
39
- ruby/mri-2.1
40
- slash/slash-head
41
- assembly/nasm-2.07
42
- ]
43
-
44
- # The data structure containing the final result
45
- # its attributes default to null-objects for their given type
46
- class Result
47
- @attribute_names = [:exitstatus, :language, :language_friendly, :code, :output, :status, :url].freeze
48
- attr_accessor *@attribute_names
49
- class << self
50
- attr_reader :attribute_names
51
- end
52
-
53
- def initialize(attributes={})
54
- attributes = attributes.dup
55
- self.exitstatus = attributes.delete(:exitstatus) || -1
56
- self.language = attributes.delete(:language) || ""
57
- self.language_friendly = attributes.delete(:language_friendly) || ""
58
- self.code = attributes.delete(:code) || ""
59
- self.output = attributes.delete(:output) || ""
60
- self.status = attributes.delete(:status) || ""
61
- self.url = attributes.delete(:url) || ""
62
- stderr = attributes.delete(:stderr) || $stderr
63
- stderr.puts "Unexpected attributes! #{attributes.keys.inspect}" if attributes.any?
64
- end
65
-
66
- # Returns representation of the result built out of JSON primitives (hash, string, int)
67
- def as_json
68
- self.class.attribute_names.each_with_object Hash.new do |name, attributes|
69
- attributes[name.to_s] = public_send name
70
- end
71
- end
72
-
73
- def to_json
74
- JSON.dump(as_json)
75
- end
76
- end
77
-
78
5
  # @param code [String] the code to evaluate.
79
6
  # @option options [String] :language Mandatory, a language recognized by eval.in, such as any value in {KNOWN_LANGUAGES}.
80
7
  # @option options [String] :url Override the url to post the code to
@@ -86,7 +13,7 @@ module EvalIn
86
13
  # result = EvalIn.call 'puts "hello, #{gets}"', stdin: 'world', language: "ruby/mri-2.1"
87
14
  # result.output # => "hello, world\n"
88
15
  def self.call(code, options={})
89
- url = post_code(code, options)
16
+ url = Client.post_code(code, options)
90
17
  fetch_result url, options
91
18
  end
92
19
 
@@ -97,97 +24,7 @@ module EvalIn
97
24
  # result = EvalIn.fetch_result "https://eval.in/147.json"
98
25
  # result.output # => "Hello Charlie! "
99
26
  def self.fetch_result(raw_url, options={})
100
- raw_json_url = jsonify_url(raw_url)
101
- build_result fetch_result_json(raw_json_url, options)
102
- end
103
-
104
- # @api private
105
- def self.post_code(code, options)
106
- url = options.fetch(:url, "https://eval.in/")
107
- input = options.fetch(:stdin, "")
108
- language = options.fetch(:language) { raise ArgumentError, ":language is mandatory, but options only has #{options.keys.inspect}" }
109
- form_data = {"utf8" => "√", "code" => code, "execute" => "on", "lang" => language, "input" => input}
110
-
111
- result = post_request url, form_data, user_agent_for(options[:context])
112
-
113
- if result.code == '302'
114
- jsonify_url result['location']
115
- elsif KNOWN_LANGUAGES.include? language
116
- raise RequestError, "There was an unexpected error, we got back a response code of #{result.code}"
117
- else
118
- raise RequestError, "Perhaps language is wrong, you provided: #{language.inspect}\n"\
119
- "Known languages are: #{KNOWN_LANGUAGES.inspect}"
120
- end
121
- end
122
-
123
- # @api private
124
- def self.fetch_result_json(raw_url, options={})
125
- result = get_request raw_url, user_agent_for(options[:context])
126
- return JSON.parse(result.body).merge('url' => raw_url) if result.body
127
- raise ResultNotFound, "No json at #{raw_url.inspect}"
128
- end
129
-
130
- # @api private
131
- def self.build_result(response_json)
132
- exitstatus = case response_json['status']
133
- when nil then nil # let it choose default
134
- when /status (\d+)$/ then $1.to_i
135
- when /^Forbidden/ then 1
136
- else 0
137
- end
138
-
139
- Result.new exitstatus: exitstatus,
140
- language: response_json['lang'],
141
- language_friendly: response_json['lang_friendly'],
142
- code: response_json['code'],
143
- output: response_json['output'],
144
- status: response_json['status'],
145
- url: response_json['url']
146
- end
147
-
148
- # @api private
149
- def self.jsonify_url(url)
150
- uri = URI(url)
151
- uri.path = Pathname.new(uri.path).sub_ext('.json').to_s
152
- uri.to_s
153
- end
154
-
155
- # @private
156
- def self.user_agent_for(context)
157
- 'http://rubygems.org/gems/eval_in'.tap do |agent|
158
- context && agent.concat(" (#{context})")
159
- end
160
- end
161
-
162
- # @api private
163
- # Can't just use Net::HTTP.get, b/c it doesn't use ssl on 1.9.3
164
- # https://github.com/ruby/ruby/blob/v2_1_2/lib/net/http.rb#L478-479
165
- # https://github.com/ruby/ruby/blob/v1_9_3_547/lib/net/http.rb#L454
166
- def self.get_request(raw_url, user_agent)
167
- generic_request_for raw_url: raw_url,
168
- request_type: Net::HTTP::Get,
169
- user_agent: user_agent
170
- end
171
-
172
- # @private
173
- def self.post_request(raw_url, form_data, user_agent)
174
- generic_request_for raw_url: raw_url,
175
- request_type: Net::HTTP::Post,
176
- user_agent: user_agent,
177
- form_data: form_data
178
- end
179
-
180
- # @private
181
- # stole this out of implementation for post_form https://github.com/ruby/ruby/blob/2afed6eceff2951b949db7ded8167a75b431bad6/lib/net/http.rb#L503
182
- # can use this to view the request: http.set_debug_output $stdout
183
- def self.generic_request_for(params)
184
- uri = URI params.fetch(:raw_url)
185
- path = uri.path
186
- path = '/' if path.empty?
187
- request = params.fetch(:request_type).new(path)
188
- request['User-Agent'] = params[:user_agent] if params.key? :user_agent
189
- request.form_data = params[:form_data] if params.key? :form_data
190
- request.basic_auth uri.user, uri.password if uri.user
191
- Net::HTTP.start(uri.hostname, uri.port, use_ssl: (uri.scheme == 'https')) { |http| http.request request }
27
+ raw_json_url = HTTP.jsonify_url(raw_url)
28
+ Client.build_result Client.fetch_result_json(raw_json_url, options)
192
29
  end
193
30
  end
@@ -0,0 +1,77 @@
1
+ # encoding: utf-8
2
+
3
+ require 'json'
4
+ require 'eval_in/http'
5
+ require 'eval_in/result'
6
+ require 'eval_in/constants'
7
+
8
+ module EvalIn
9
+
10
+ # Code that does the integration with the actual eval.in service
11
+ # It is primarily in place to help the toplevel methods wire things together
12
+ # in the ways that they need to.
13
+ #
14
+ # **This should be assumed volatile, and you should avoid depending on it.**
15
+ module Client
16
+ extend self
17
+
18
+ # @api private
19
+ def post_code(code, options)
20
+ url = options.fetch(:url, "https://eval.in/")
21
+ input = options.fetch(:stdin, "")
22
+ language = language_or_error_from options
23
+ form_data = {"utf8" => "√", "code" => code, "execute" => "on", "lang" => language, "input" => input}
24
+
25
+ result = HTTP.post_request url, form_data, user_agent_for(options[:context])
26
+
27
+ if result.code == '302'
28
+ HTTP.jsonify_url result['location']
29
+ elsif KNOWN_LANGUAGES.include? language
30
+ raise RequestError, "There was an unexpected error, we got back a response code of #{result.code}"
31
+ else
32
+ raise RequestError, "Perhaps language is wrong, you provided: #{language.inspect}\n"\
33
+ "Known languages are: #{KNOWN_LANGUAGES.inspect}"
34
+ end
35
+ end
36
+
37
+ # @api private
38
+ def fetch_result_json(raw_url, options={})
39
+ result = HTTP.get_request raw_url, user_agent_for(options[:context])
40
+ return JSON.parse(result.body).merge('url' => raw_url) if result.body
41
+ raise ResultNotFound, "No json at #{raw_url.inspect}"
42
+ end
43
+
44
+ # @api private
45
+ def build_result(response_json)
46
+ exitstatus = case response_json['status']
47
+ when nil then nil # let Result choose default
48
+ when /status (\d+)$/ then $1.to_i
49
+ when /^Forbidden/ then 1
50
+ else 0
51
+ end
52
+
53
+ Result.new exitstatus: exitstatus,
54
+ language: response_json['lang'],
55
+ language_friendly: response_json['lang_friendly'],
56
+ code: response_json['code'],
57
+ output: response_json['output'],
58
+ status: response_json['status'],
59
+ url: response_json['url']
60
+ end
61
+
62
+ # @api private
63
+ def user_agent_for(context)
64
+ 'http://rubygems.org/gems/eval_in'.tap do |agent|
65
+ agent << " (#{context})" if context
66
+ end
67
+ end
68
+
69
+ # @api private
70
+ def language_or_error_from(options)
71
+ options.fetch :language do
72
+ raise ArgumentError, ":language is mandatory, but options only has #{options.keys.inspect}"
73
+ end
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,37 @@
1
+ module EvalIn
2
+ VERSION = '0.2.0'
3
+
4
+ EvalInError = Class.new StandardError
5
+ RequestError = Class.new EvalInError
6
+ ResultNotFound = Class.new EvalInError
7
+
8
+ # @example Generated with
9
+ # nokogiri https://eval.in -e 'puts $_.xpath("//option/@value")'
10
+ KNOWN_LANGUAGES = %w[
11
+ c/gcc-4.4.3
12
+ c/gcc-4.9.1
13
+ c++/c++11-gcc-4.9.1
14
+ c++/gcc-4.4.3
15
+ c++/gcc-4.9.1
16
+ coffeescript/node-0.10.29-coffee-1.7.1
17
+ fortran/f95-4.4.3
18
+ haskell/hugs98-sep-2006
19
+ io/io-20131204
20
+ javascript/node-0.10.29
21
+ lua/lua-5.1.5
22
+ lua/lua-5.2.3
23
+ ocaml/ocaml-4.01.0
24
+ php/php-5.5.14
25
+ pascal/fpc-2.6.4
26
+ perl/perl-5.20.0
27
+ python/cpython-2.7.8
28
+ python/cpython-3.4.1
29
+ ruby/mri-1.0
30
+ ruby/mri-1.8.7
31
+ ruby/mri-1.9.3
32
+ ruby/mri-2.0.0
33
+ ruby/mri-2.1
34
+ slash/slash-head
35
+ assembly/nasm-2.07
36
+ ]
37
+ end