mumukit 0.3.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/bin/limit +10 -0
  3. data/bin/mulang +0 -0
  4. data/lib/locales/en.yml +4 -0
  5. data/lib/locales/es.yml +4 -0
  6. data/lib/mumukit/defaults/default_expectations_hook.rb +9 -0
  7. data/lib/mumukit/defaults/default_feedback_hook.rb +5 -0
  8. data/lib/mumukit/defaults/default_metadata_hook.rb +5 -0
  9. data/lib/mumukit/defaults/default_query_hook.rb +9 -0
  10. data/lib/mumukit/defaults/default_test_hook.rb +9 -0
  11. data/lib/mumukit/defaults/default_validation_hook.rb +4 -0
  12. data/lib/mumukit/defaults.rb +11 -0
  13. data/lib/mumukit/env.rb +11 -0
  14. data/lib/mumukit/hook.rb +30 -0
  15. data/lib/mumukit/isolated_environment.rb +51 -0
  16. data/lib/mumukit/metatest/checker.rb +34 -0
  17. data/lib/mumukit/metatest/errors.rb +10 -0
  18. data/lib/mumukit/metatest/framework.rb +20 -0
  19. data/lib/mumukit/metatest/identity_runner.rb +7 -0
  20. data/lib/mumukit/metatest.rb +7 -0
  21. data/lib/mumukit/request_validation_error.rb +4 -0
  22. data/lib/mumukit/runner.rb +33 -0
  23. data/lib/mumukit/runtime/info.rb +30 -0
  24. data/lib/mumukit/runtime/runtime.rb +35 -0
  25. data/lib/mumukit/runtime/shortcuts.rb +12 -0
  26. data/lib/mumukit/runtime/symbol.rb +17 -0
  27. data/lib/mumukit/runtime.rb +4 -0
  28. data/lib/mumukit/server/app.rb +71 -0
  29. data/lib/mumukit/server/response_builder.rb +62 -0
  30. data/lib/mumukit/server/test_server.rb +118 -0
  31. data/lib/mumukit/server.rb +7 -0
  32. data/lib/mumukit/templates/file_hook.rb +49 -0
  33. data/lib/mumukit/templates/mulang_expectations_hook.rb +57 -0
  34. data/lib/mumukit/templates/with_code_smells.rb +7 -0
  35. data/lib/mumukit/templates/with_embedded_environment.rb +12 -0
  36. data/lib/mumukit/templates/with_isolated_environment.rb +11 -0
  37. data/lib/mumukit/templates/with_mashup_file_content.rb +11 -0
  38. data/lib/mumukit/templates/with_structured_results.rb +15 -0
  39. data/lib/mumukit/templates.rb +12 -0
  40. data/lib/mumukit/version.rb +1 -1
  41. data/lib/mumukit/with_command_line.rb +33 -1
  42. data/lib/mumukit/with_content_type.rb +5 -0
  43. data/lib/mumukit/with_tempfile.rb +13 -2
  44. data/lib/mumukit.rb +62 -8
  45. metadata +180 -21
  46. data/lib/mumukit/file_test_compiler.rb +0 -14
  47. data/lib/mumukit/file_test_runner.rb +0 -20
  48. data/lib/mumukit/stub.rb +0 -5
  49. data/lib/mumukit/test_server.rb +0 -42
  50. data/lib/mumukit/test_server_app.rb +0 -28
  51. data/lib/stubs/expectations_runner.rb +0 -5
  52. data/lib/stubs/test_runner.rb +0 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 96ca9d2847fa9cf5412c483dd9e7900b0c093549
4
- data.tar.gz: 07439f192678d0ac52cf8009ed2249a6955bcc88
3
+ metadata.gz: 3f8afb7358f144c2e25c356c70f0061144ff2f04
4
+ data.tar.gz: d41e834b6c92fea4897c87d46f5d3303983a8209
5
5
  SHA512:
6
- metadata.gz: 7fb5c9cde7fb1e16ff83232a7b43dd11511c6ab504b07a1a72984bf36055d872fc48b51435ec027ff81a0626b5324595620cb626e776f4c2c9f17ac29bf6993d
7
- data.tar.gz: f5eb3c788559964d2ac32ed272631ee3305cf17b293435d25499be1259b64122cc67c5812e76f426f2f9712cb27b3b3aa1ad8f52856d2b874a3e2fb7d8645c51
6
+ metadata.gz: b4830f051f2f4ede2c1a76f05306974ea4c8d05947b2a0b8e4d2f7c3b3ad5dbb32207045accb30c10815bd90dd2e6ccc8a9e982a947e48c0fd17df2582b67e63
7
+ data.tar.gz: d1c908995f05aeab6cd7bc6a47c4614a20ec242cb3e49fee4497060c03ebc87a3636578652a3fb3716016543861405db1c7a4cd94335772303ed5cc30a06d30a
data/bin/limit ADDED
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+
3
+ SIZE=$1
4
+ shift
5
+ TIME=$1
6
+ shift
7
+ COMMAND=$@
8
+
9
+ ulimit -Sv $(expr 1024 \* $SIZE) -St $TIME
10
+ sh -c "$COMMAND"
data/bin/mulang ADDED
Binary file
@@ -0,0 +1,4 @@
1
+ en:
2
+ mumukit:
3
+ memory_exceeded: Memory limit exceeded. Is your program trying to allocate to much memory?
4
+ time_exceeded: Execution time limit of %{limit}s exceeded. Is your program performing an infinite loop or recursion?
@@ -0,0 +1,4 @@
1
+ es:
2
+ mumukit:
3
+ memory_exceeded: Limite de memoria excedido. ¿Tu programa está alocando demasiada memoria?
4
+ time_exceeded: Limite de tiempo de ejecución de %{limit}s excedido. ¿Tu programa tiene recursión o bucle infinito?
@@ -0,0 +1,9 @@
1
+ class Mumukit::Defaults::ExpectationsHook < Mumukit::Hook
2
+ def compile(request)
3
+ request
4
+ end
5
+
6
+ def run!(request)
7
+ []
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class Mumukit::Defaults::FeedbackHook < Mumukit::Hook
2
+ def run!(request, results)
3
+ ''
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Mumukit::Defaults::MetadataHook < Mumukit::Hook
2
+ def metadata
3
+ {}
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ class Mumukit::Defaults::QueryHook < Mumukit::Hook
2
+ def compile(request)
3
+ request
4
+ end
5
+
6
+ def run!(request)
7
+ ['unimplemented', :aborted]
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class Mumukit::Defaults::TestHook < Mumukit::Hook
2
+ def compile(request)
3
+ request
4
+ end
5
+
6
+ def run!(compilation)
7
+ ['unimplemented', :aborted]
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ class Mumukit::Defaults::ValidationHook < Mumukit::Hook
2
+ def validate!(request)
3
+ end
4
+ end
@@ -0,0 +1,11 @@
1
+ module Mumukit
2
+ module Defaults
3
+ end
4
+ end
5
+
6
+ require_relative './defaults/default_expectations_hook'
7
+ require_relative './defaults/default_query_hook'
8
+ require_relative './defaults/default_feedback_hook'
9
+ require_relative './defaults/default_validation_hook'
10
+ require_relative './defaults/default_test_hook'
11
+ require_relative './defaults/default_metadata_hook'
@@ -0,0 +1,11 @@
1
+ module Mumukit
2
+ module Env
3
+ def self.env
4
+ Thread.current[:mumukit_env]
5
+ end
6
+
7
+ def self.env=(env)
8
+ Thread.current[:mumukit_env] = env
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,30 @@
1
+ class Mumukit::Hook
2
+ attr_reader :config
3
+
4
+ include Mumukit::WithContentType
5
+
6
+ def initialize(config=nil)
7
+ @config = (config||{}).with_indifferent_access
8
+ end
9
+
10
+ def t(*args)
11
+ I18n.t(*args)
12
+ end
13
+
14
+ def method_missing(name, *args, &block)
15
+ super unless should_forward_to_config?(args, name, &block)
16
+ @config[name]
17
+ end
18
+
19
+ def env
20
+ Mumukit::Env.env
21
+ end
22
+
23
+ def logger
24
+ env['rack.logger']
25
+ end
26
+
27
+ def should_forward_to_config?(args, name)
28
+ args.length == 0 && !block_given? && @config[name]
29
+ end
30
+ end
@@ -0,0 +1,51 @@
1
+ require 'docker'
2
+ require 'pathname'
3
+
4
+ module Mumukit
5
+ class IsolatedEnvironment
6
+
7
+ attr_accessor :container
8
+
9
+ def configure!(*files)
10
+
11
+ filenames = files.map { |it| File.absolute_path(it.path) }
12
+ dirnames = filenames.map { |it| Pathname.new(it).dirname }
13
+
14
+ binds = dirnames.map { |it| "#{it}:#{it}" }
15
+ volumes = Hash[[dirnames.map { |it| [it, {}] }]]
16
+
17
+ command = yield(*filenames).split
18
+
19
+ self.container = Docker::Container.create(
20
+ 'Image' => Mumukit.config.docker_image,
21
+ 'Cmd' => command,
22
+ 'NetworkDisabled' => true,
23
+ 'HostConfig' => {
24
+ 'Binds' => binds},
25
+ 'Volumes' => volumes)
26
+ end
27
+
28
+ def run!
29
+ container.start
30
+ container.wait(Mumukit.config.command_time_limit)
31
+
32
+ exit = container.json['State']['ExitCode']
33
+ out = container.streaming_logs(stdout: true, stderr: true)
34
+
35
+ if exit == 0
36
+ [out, :passed]
37
+ else
38
+ [out, :failed]
39
+ end
40
+ rescue Docker::Error::TimeoutError => e
41
+ [I18n.t('mumukit.time_exceeded', limit: Mumukit.config.command_time_limit), :aborted]
42
+ end
43
+
44
+ def destroy!
45
+ if container
46
+ container.stop
47
+ container.delete
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,34 @@
1
+ module Mumukit::Metatest
2
+ class Checker
3
+ def check(value, example)
4
+ example[:postconditions].each { |key, arg| check_assertion key, value, arg, example }
5
+ [example[:name], :passed, render_success_output(value)]
6
+ rescue => e
7
+ [example[:name], :failed, render_error_output(value, e.message)]
8
+ end
9
+
10
+ def render_success_output(value)
11
+ nil
12
+ end
13
+
14
+ def render_error_output(value, error)
15
+ error
16
+ end
17
+
18
+ def check_assertion(key, value, arg, example)
19
+ send "check_#{key}", value, arg
20
+ end
21
+
22
+ def fail(message)
23
+ raise Mumukit::Metatest::Failed, message
24
+ end
25
+
26
+ def abort(message)
27
+ raise Mumukit::Metatest::Aborted, message
28
+ end
29
+
30
+ def error(message)
31
+ raise Mumukit::Metatest::Errored, message
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,10 @@
1
+ module Mumukit::Metatest
2
+ class Aborted < StandardError
3
+ end
4
+
5
+ class Errored < StandardError
6
+ end
7
+
8
+ class Failed < StandardError
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ module Mumukit::Metatest
2
+ class Framework
3
+ def initialize(options={})
4
+ @runner = options[:runner]
5
+ @checker = options[:checker]
6
+ end
7
+
8
+ def test(compilation, examples)
9
+ [examples.map { |it| example(compilation, it) }]
10
+ rescue Aborted => e
11
+ [e.message, :aborted]
12
+ rescue Errored => e
13
+ [e.message, :errored]
14
+ end
15
+
16
+ def example(compilation, example)
17
+ @checker.check(@runner.run(compilation, example), example)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ module Mumukit::Metatest
2
+ class IdentityRunner
3
+ def run(it, _example)
4
+ it
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Mumukit::Metatest
2
+ end
3
+
4
+ require_relative './metatest/errors'
5
+ require_relative './metatest/checker'
6
+ require_relative './metatest/framework'
7
+ require_relative './metatest/identity_runner'
@@ -0,0 +1,4 @@
1
+ module Mumukit
2
+ class RequestValidationError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,33 @@
1
+ class Mumukit::Runner
2
+ attr_reader :name, :runtime
3
+
4
+ def initialize(name)
5
+ @name = name
6
+ end
7
+
8
+ def configure
9
+ @config ||= self.class.default_config.clone
10
+ yield @config
11
+ end
12
+
13
+ def configure_runtime(config)
14
+ @runtime = Mumukit::Runtime.new(config)
15
+ end
16
+
17
+ def config
18
+ @config or raise 'This runner has not being configured yet'
19
+ end
20
+
21
+ def prefix
22
+ name.camelize
23
+ end
24
+
25
+ def self.default_config
26
+ @default_config
27
+ end
28
+
29
+ def self.configure_defaults
30
+ @default_config ||= OpenStruct.new
31
+ yield @default_config
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ module Mumukit::RuntimeInfo
2
+ def info
3
+ {
4
+ name: Mumukit.runner_name,
5
+ version: (File.read('version') rescue 'master'),
6
+ escualo_base_version: ENV['ESCUALO_BASE_VERSION'],
7
+ escualo_service_version: ENV['ESCUALO_SERVICE_VERSION'],
8
+ mumukit_version: Mumukit::VERSION,
9
+ output_content_type: Mumukit.config.content_type,
10
+ comment_type: Mumukit.config.comment_type,
11
+ features: {
12
+ query: query_hook?,
13
+ expectations: expectations_hook?,
14
+ feedback: feedback_hook?,
15
+ secure: validation_hook?,
16
+ stateful: Mumukit.config.stateful,
17
+ preprocessor: Mumukit.config.preprocessor_enabled,
18
+
19
+ sandboxed: any_hook_include?([:test, :query], Mumukit::Templates::WithIsolatedEnvironment),
20
+ structured: any_hook_include?([:test], Mumukit::Templates::WithStructuredResults) || Mumukit.config.structured
21
+ }
22
+ }
23
+ end
24
+
25
+ private
26
+
27
+ def any_hook_include?(hooks, mixin)
28
+ hooks.any? { |it| hook_includes?(it, mixin) }
29
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ class Mumukit::Runtime
2
+ include Mumukit::RuntimeShortcuts
3
+ include Mumukit::RuntimeInfo
4
+
5
+ def initialize(config)
6
+ @config = config
7
+ @hook_classes = {}
8
+ end
9
+
10
+ def hook_defined?(hook_name)
11
+ hook_name.to_default_mumukit_hook_class rescue raise "Wrong hook #{hook_name}"
12
+
13
+ Kernel.const_defined? hook_name.to_mumukit_hook_class_name
14
+ end
15
+
16
+ def hook_includes?(hook_name, mixin)
17
+ hook_class(hook_name).included_modules.include?(mixin)
18
+ end
19
+
20
+ def new_hook(hook_name)
21
+ hook_class(hook_name).new(@config)
22
+ end
23
+
24
+ private
25
+
26
+ def hook_class(hook_name)
27
+ @hook_classes[hook_name] ||=
28
+ if hook_defined? hook_name
29
+ hook_name.to_mumukit_hook_class
30
+ else
31
+ hook_name.to_default_mumukit_hook_class
32
+ end
33
+ end
34
+ end
35
+
@@ -0,0 +1,12 @@
1
+ module Mumukit::RuntimeShortcuts
2
+ def method_missing(name, *args, &block)
3
+ n = name.to_s
4
+ if n =~ /(.*)_hook\?/
5
+ hook_defined? $1.to_sym
6
+ elsif n =~ /(.*)_hook/
7
+ new_hook $1.to_sym
8
+ else
9
+ super
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ class Symbol
2
+ def to_mumukit_hook_class_name
3
+ "#{Mumukit.prefix}#{to_simple_mumukit_hook_class_name}"
4
+ end
5
+
6
+ def to_simple_mumukit_hook_class_name
7
+ "#{to_s.camelize.to_sym}Hook"
8
+ end
9
+
10
+ def to_mumukit_hook_class
11
+ Kernel.const_get to_mumukit_hook_class_name
12
+ end
13
+
14
+ def to_default_mumukit_hook_class
15
+ Kernel.const_get "Mumukit::Defaults::#{to_simple_mumukit_hook_class_name}"
16
+ end
17
+ end
@@ -0,0 +1,4 @@
1
+ require_relative './runtime/symbol'
2
+ require_relative './runtime/info'
3
+ require_relative './runtime/shortcuts'
4
+ require_relative './runtime/runtime'
@@ -0,0 +1,71 @@
1
+ require 'sinatra/base'
2
+ require 'yaml'
3
+ require 'json'
4
+
5
+
6
+ class Mumukit::Server::App < Sinatra::Base
7
+ configure do
8
+ set :mumuki_url, 'http://mumuki.io'
9
+ set :show_exceptions, :after_handler
10
+ enable :logging
11
+ end
12
+
13
+ configure :development do
14
+ set :config_filename, 'config/development.yml'
15
+ end
16
+
17
+ configure :production do
18
+ set :config_filename, 'config/production.yml'
19
+ end
20
+
21
+ runtime_config = YAML.load_file(settings.config_filename) rescue nil
22
+
23
+ Mumukit.configure_runtime(runtime_config)
24
+
25
+ set :server, Mumukit::Server::TestServer.new
26
+
27
+ before do
28
+ content_type 'application/json'
29
+ end
30
+
31
+ before do
32
+ Mumukit::Env.env = env
33
+ server.start_request!(parse_request)
34
+ end
35
+
36
+ helpers do
37
+ def server
38
+ settings.server
39
+ end
40
+
41
+ def parse_request
42
+ @parsed_request ||= server.parse_request(request)
43
+ end
44
+ end
45
+
46
+ get '/info' do
47
+ JSON.generate(server.info(request.url))
48
+ end
49
+
50
+ post '/test' do
51
+ JSON.generate(server.test!(parse_request))
52
+ end
53
+
54
+ post '/query' do
55
+ JSON.generate(server.query!(parse_request))
56
+ end
57
+
58
+ get '/*' do
59
+ redirect settings.mumuki_url
60
+ end
61
+
62
+ error StandardError do
63
+ content_type :json
64
+ status 200
65
+
66
+ message = Mumukit::ContentType::Plain.format_exception env['sinatra.error']
67
+ logger.error "Unhandled error #{message}"
68
+
69
+ {status: :errored, exit: message}.to_json
70
+ end
71
+ end
@@ -0,0 +1,62 @@
1
+ class Mumukit::Server::ResponseBuilder
2
+
3
+ def add_test_results(r)
4
+ @response = base_response(r)
5
+ end
6
+
7
+ def add_query_results(r)
8
+ @response = unstructured_base_response(r)
9
+ end
10
+
11
+ def add_expectation_results(r)
12
+ @response.merge!(expectationResults: r) if r.present?
13
+ end
14
+
15
+
16
+ def add_feedback(f)
17
+ @response.merge!(feedback: f) if f.present?
18
+ end
19
+
20
+ def build
21
+ @response
22
+ end
23
+
24
+ def self.build(&block)
25
+ builder = new
26
+ builder.instance_eval(&block)
27
+ builder.build
28
+ end
29
+
30
+ private
31
+
32
+ def base_response(test_results)
33
+ if structured_test_result?(test_results)
34
+ structured_base_response(test_results)
35
+ elsif unstructured_test_result?(test_results)
36
+ unstructured_base_response(test_results)
37
+ else
38
+ raise "Invalid test results format: #{test_results}. You must either return [results_array] or [results_string, status]"
39
+ end
40
+ end
41
+
42
+ def structured_test_result?(test_results)
43
+ test_results.size == 1 && test_results[0].is_a?(Array)
44
+ end
45
+
46
+ def unstructured_test_result?(test_results)
47
+ test_results.size == 2 && test_results[0].is_a?(String)
48
+ end
49
+
50
+ def structured_base_response(test_results)
51
+ {testResults: test_results[0].map { |title, status, result|
52
+ {title: title,
53
+ status: status,
54
+ result: result} }}
55
+ end
56
+
57
+ def unstructured_base_response(test_results)
58
+ {exit: test_results[1],
59
+ out: test_results[0]}
60
+ end
61
+
62
+ end
@@ -0,0 +1,118 @@
1
+ require 'yaml'
2
+ require 'ostruct'
3
+
4
+ class Mumukit::Server::TestServer
5
+
6
+ include Mumukit::WithContentType
7
+
8
+ def runtime
9
+ Mumukit.runtime
10
+ end
11
+
12
+ def info(url)
13
+ runtime.info.merge(runtime.metadata_hook.metadata).merge(url: url)
14
+ end
15
+
16
+ def start_request!(_request)
17
+ end
18
+
19
+ def parse_request(sinatra_request)
20
+ OpenStruct.new parse_request_headers(sinatra_request).merge(parse_request_body(sinatra_request))
21
+ end
22
+
23
+ def parse_request_headers(sinatra_request)
24
+ {}
25
+ end
26
+
27
+ def parse_request_body(sinatra_request)
28
+ JSON.parse(sinatra_request.body.read).tap do |it|
29
+ I18n.locale = it['locale'] || :en
30
+ end rescue {}
31
+ end
32
+
33
+ def test!(request)
34
+ respond_to(request) do |r|
35
+ test_results = run_tests! r
36
+ expectation_results = run_expectations! r
37
+
38
+ results = OpenStruct.new(test_results: test_results,
39
+ expectation_results: expectation_results)
40
+
41
+ feedback = run_feedback! r, results
42
+
43
+ Mumukit::Server::ResponseBuilder.build do
44
+ add_test_results(test_results)
45
+ add_expectation_results(expectation_results)
46
+ add_feedback(feedback)
47
+ end
48
+ end
49
+ end
50
+
51
+ def query!(request)
52
+ respond_to(request) do |r|
53
+ results = run_query!(r)
54
+ Mumukit::Server::ResponseBuilder.build do
55
+ add_query_results(results)
56
+ end
57
+ end
58
+ end
59
+
60
+ def run_query!(request)
61
+ compile_and_run runtime.query_hook, request
62
+ end
63
+
64
+ def run_tests!(request)
65
+ return ['', :passed] if request.test.blank?
66
+
67
+ compile_and_run runtime.test_hook, request
68
+ end
69
+
70
+ def run_expectations!(request)
71
+ if request.expectations
72
+ compile_and_run runtime.expectations_hook, request
73
+ else
74
+ []
75
+ end
76
+ end
77
+
78
+ def run_feedback!(request, results)
79
+ runtime.feedback_hook.run!(request, results)
80
+ end
81
+
82
+ private
83
+
84
+ def compile_and_run(hook, request)
85
+ compilation = hook.compile(preprocess request)
86
+ hook.run!(compilation)
87
+ end
88
+
89
+ def preprocess(request)
90
+ if Mumukit.config.preprocessor_enabled
91
+ directives_pipeline.transform(request)
92
+ else
93
+ request
94
+ end
95
+ end
96
+
97
+ def directives_pipeline
98
+ @pipeline ||= Mumukit::Directives::Pipeline.new(
99
+ [Mumukit::Directives::Sections.new,
100
+ Mumukit::Directives::Interpolations.new('test'),
101
+ Mumukit::Directives::Interpolations.new('extra'),
102
+ Mumukit::Directives::Flags.new],
103
+ Mumukit.config.comment_type)
104
+ end
105
+
106
+ def validate_request!(request)
107
+ runtime.validation_hook.validate! request
108
+ end
109
+
110
+ def respond_to(request)
111
+ yield request.tap { |r| validate_request! r }
112
+ rescue Mumukit::RequestValidationError => e
113
+ {exit: :aborted, out: e.message}
114
+ rescue Exception => e
115
+ {exit: :errored, out: content_type.format_exception(e)}
116
+ end
117
+
118
+ end