trinidad_rack 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ == Trinidad-Rack
2
+
3
+ Copyright (c) 2011 David Calavera
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ == Additional Bundled Software
25
+
26
+ lib/trinidad_rack/rack_servlet.rb is a work of Don Werve and it's licensed according to the terms of Apache License, Version 2.0 (current).
27
+ See http://www.apache.org/licenses/LICENSE-2.0 for details.
data/README ADDED
@@ -0,0 +1,30 @@
1
+ Trinidad-Rack
2
+ -------------
3
+
4
+ Library that replaces Trinidad's rack layer from JRuby-Rack to the Mizuno Rack Servlet.
5
+
6
+ It provides better integration with tools that relie on Rack like Capybara
7
+ or any other Rack-Test based tool.
8
+
9
+ RackServlet is a work of Don Werve and it's licensed under the Apache
10
+ License. This library modifies its code to remove the asynchronous layer
11
+ bounded to Jetty.
12
+
13
+ Installation
14
+ ============
15
+
16
+ $ jruby -S gem install trinidad_rack
17
+
18
+ Execution
19
+ =========
20
+
21
+ $ jruby -S trinidad_rack
22
+
23
+ or
24
+
25
+ $ rackup -s Trinidad
26
+
27
+ [IMPORTANT]
28
+
29
+ This is totally experimental and due to Mizuno and JRuby-Rack
30
+ work in a completely different way some features cannot work as expected.
@@ -0,0 +1,144 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'date'
4
+
5
+ #############################################################################
6
+ #
7
+ # Helper functions
8
+ #
9
+ #############################################################################
10
+
11
+ def name
12
+ @name ||= Dir['*.gemspec'].first.split('.').first
13
+ end
14
+
15
+ def version
16
+ line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
17
+ line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
18
+ end
19
+
20
+ def date
21
+ Date.today.to_s
22
+ end
23
+
24
+ def rubyforge_project
25
+ name
26
+ end
27
+
28
+ def gemspec_file
29
+ "#{name}.gemspec"
30
+ end
31
+
32
+ def gem_file
33
+ "#{name}-#{version}.gem"
34
+ end
35
+
36
+ def replace_header(head, header_name)
37
+ head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
38
+ end
39
+
40
+ #############################################################################
41
+ #
42
+ # Standard tasks
43
+ #
44
+ #############################################################################
45
+
46
+ task :default => :test
47
+
48
+ require 'rake/testtask'
49
+ Rake::TestTask.new(:test) do |test|
50
+ test.libs << 'lib' << 'test'
51
+ test.pattern = 'test/**/test_*.rb'
52
+ test.verbose = true
53
+ end
54
+
55
+ desc "Generate RCov test coverage and open in your browser"
56
+ task :coverage do
57
+ require 'rcov'
58
+ sh "rm -fr coverage"
59
+ sh "rcov test/test_*.rb"
60
+ sh "open coverage/index.html"
61
+ end
62
+
63
+ require 'rake/rdoctask'
64
+ Rake::RDocTask.new do |rdoc|
65
+ rdoc.rdoc_dir = 'rdoc'
66
+ rdoc.title = "#{name} #{version}"
67
+ rdoc.rdoc_files.include('README*')
68
+ rdoc.rdoc_files.include('lib/**/*.rb')
69
+ end
70
+
71
+ desc "Open an irb session preloaded with this library"
72
+ task :console do
73
+ sh "irb -rubygems -r ./lib/#{name}.rb"
74
+ end
75
+
76
+ #############################################################################
77
+ #
78
+ # Custom tasks (add your own tasks here)
79
+ #
80
+ #############################################################################
81
+
82
+ task :default => :spec
83
+
84
+ require 'rspec/core/rake_task'
85
+
86
+ RSpec::Core::RakeTask.new(:spec) do |spec|
87
+ spec.rspec_opts = ['--colour', "--format documentation"]
88
+ end
89
+
90
+ #############################################################################
91
+ #
92
+ # Packaging tasks
93
+ #
94
+ #############################################################################
95
+
96
+ task :release => :build do
97
+ unless `git branch` =~ /^\* master$/
98
+ puts "You must be on the master branch to release!"
99
+ exit!
100
+ end
101
+ sh "git commit --allow-empty -a -m 'Release #{version}'"
102
+ sh "git tag v#{version}"
103
+ sh "git push origin master"
104
+ sh "git push --tags"
105
+ sh "gem push pkg/#{name}-#{version}.gem"
106
+ end
107
+
108
+ task :build => :gemspec do
109
+ sh "mkdir -p pkg"
110
+ sh "gem build #{gemspec_file}"
111
+ sh "mv #{gem_file} pkg"
112
+ end
113
+
114
+ task :install => :build do
115
+ sh "gem install pkg/#{name}-#{version}.gem"
116
+ end
117
+
118
+ task :gemspec do
119
+ # read spec file and split out manifest section
120
+ spec = File.read(gemspec_file)
121
+ head, manifest, tail = spec.split(" # = MANIFEST =\n")
122
+
123
+ # replace name version and date
124
+ replace_header(head, :name)
125
+ replace_header(head, :version)
126
+ replace_header(head, :date)
127
+ #comment this out if your rubyforge_project has a different name
128
+ replace_header(head, :rubyforge_project)
129
+
130
+ # determine file list from git ls-files
131
+ files = `git ls-files`.
132
+ split("\n").
133
+ sort.
134
+ reject { |file| file =~ /^\./ }.
135
+ reject { |file| file =~ /^(rdoc|pkg|src|trinidad-libs|rakelib)/ }.
136
+ map { |file| " #{file}" }.
137
+ join("\n")
138
+
139
+ # piece file back together and write
140
+ manifest = " s.files = %w[\n#{files}\n ]\n"
141
+ spec = [head, manifest, tail].join(" # = MANIFEST =\n")
142
+ File.open(gemspec_file, 'w') { |io| io.write(spec) }
143
+ puts "Updated #{gemspec_file}"
144
+ end
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env jruby
2
+
3
+ require 'rack'
4
+
5
+ require 'trinidad_rack'
6
+
7
+ server = Rack::Server.new
8
+ server.options[:server] = 'trinidad'
9
+ server.start
@@ -0,0 +1,18 @@
1
+ require 'rack'
2
+ require 'trinidad'
3
+
4
+ module Rack
5
+ module Handler
6
+ class Trinidad
7
+ def self.run(app, options={})
8
+ opts = options.dup
9
+ opts[:app] = app
10
+ opts[:port] = 3000
11
+ opts[:address] = (options[:Host] || 'localhost')
12
+ ::Trinidad::Server.new(opts).start
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ Rack::Handler.register 'trinidad', 'Rack::Handler::Trinidad'
@@ -0,0 +1,10 @@
1
+ require 'rack/handler/trinidad'
2
+
3
+ require 'trinidad_rack/rack_servlet'
4
+ require 'trinidad_rack/trinidad_ext'
5
+
6
+ module Trinidad
7
+ module Rack
8
+ VERSION = '0.1.0'
9
+ end
10
+ end
@@ -0,0 +1,196 @@
1
+ #
2
+ # Wraps a Rack application in a Java servlet.
3
+ #
4
+ # Relevant documentation:
5
+ #
6
+ # http://rack.rubyforge.org/doc/SPEC.html
7
+ # http://java.sun.com/j2ee/sdk_1.3/techdocs/api/javax
8
+ # /servlet/http/HttpServlet.html
9
+ #
10
+ module Trinidad
11
+ module Rack
12
+ java_import 'javax.servlet.http.HttpServlet'
13
+
14
+ class RackServlet < HttpServlet
15
+ java_import 'java.io.FileInputStream'
16
+
17
+ #
18
+ # Sets the Rack application that handles requests sent to this
19
+ # servlet container.
20
+ #
21
+ def rackup(app)
22
+ @app = app
23
+ end
24
+
25
+ #
26
+ # Takes an incoming request (as a Java Servlet) and dispatches it to
27
+ # the rack application setup via [rackup]. All this really involves
28
+ # is translating the various bits of the Servlet API into the Rack
29
+ # API on the way in, and translating the response back on the way
30
+ # out.
31
+ #
32
+ # Also, we implement a common extension to the Rack api for
33
+ # asynchronous request processing. We supply an 'async.callback'
34
+ # parameter in env to the Rack application. If we catch an
35
+ # :async symbol thrown by the app, we initiate a Jetty continuation.
36
+ #
37
+ # When 'async.callback' gets a response with empty headers and an
38
+ # empty body, we declare the async response finished.
39
+ #
40
+ def service(request, response)
41
+ # Turn the ServletRequest into a Rack env hash
42
+ env = servlet_to_rack(request)
43
+
44
+ # Add our own special bits to the rack environment so that
45
+ # Rack middleware can have access to the Java internals.
46
+ env['rack.java.servlet'] = true
47
+ env['rack.java.servlet.request'] = request
48
+ env['rack.java.servlet.response'] = response
49
+
50
+ rack_response = @app.call(env)
51
+
52
+ # For apps that don't throw :async.
53
+ unless(rack_response[0] == -1)
54
+ # Nope, nothing asynchronous here.
55
+ rack_to_servlet(rack_response, response)
56
+ return
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ #
63
+ # Turns a Servlet request into a Rack request hash.
64
+ #
65
+ def servlet_to_rack(request)
66
+ # The Rack request that we will pass on.
67
+ env = Hash.new
68
+
69
+ # Map Servlet bits to Rack bits.
70
+ env['REQUEST_METHOD'] = request.getMethod
71
+ env['QUERY_STRING'] = request.getQueryString.to_s
72
+ env['SERVER_NAME'] = request.getServerName
73
+ env['SERVER_PORT'] = request.getServerPort.to_s
74
+ env['rack.version'] = ::Rack::VERSION
75
+ env['rack.url_scheme'] = request.getScheme
76
+ env['HTTP_VERSION'] = request.getProtocol
77
+ env["SERVER_PROTOCOL"] = request.getProtocol
78
+ env['REMOTE_ADDR'] = request.getRemoteAddr
79
+ env['REMOTE_HOST'] = request.getRemoteHost
80
+
81
+ # request.getPathInfo seems to be blank, so we're using the URI.
82
+ env['REQUEST_PATH'] = request.getRequestURI
83
+ env['PATH_INFO'] = request.getRequestURI
84
+ env['SCRIPT_NAME'] = ""
85
+
86
+ # Rack says URI, but it hands off a URL.
87
+ env['REQUEST_URI'] = request.getRequestURL.toString
88
+
89
+ # Java chops off the query string, but a Rack application will
90
+ # expect it, so we'll add it back if present
91
+ env['REQUEST_URI'] << "?#{env['QUERY_STRING']}" \
92
+ if env['QUERY_STRING']
93
+
94
+ # JRuby is like the matrix, only there's no spoon or fork().
95
+ env['rack.multiprocess'] = false
96
+ env['rack.multithread'] = true
97
+ env['rack.run_once'] = false
98
+
99
+ # Populate the HTTP headers.
100
+ request.getHeaderNames.each do |header_name|
101
+ header = header_name.upcase.tr('-', '_')
102
+ env["HTTP_#{header}"] = request.getHeader(header_name)
103
+ end
104
+
105
+ # Rack Weirdness: HTTP_CONTENT_TYPE and HTTP_CONTENT_LENGTH
106
+ # both need to have the HTTP_ part dropped.
107
+ env["CONTENT_TYPE"] = env.delete("HTTP_CONTENT_TYPE") \
108
+ if env["HTTP_CONTENT_TYPE"]
109
+ env["CONTENT_LENGTH"] = env.delete("HTTP_CONTENT_LENGTH") \
110
+ if env["HTTP_CONTENT_LENGTH"]
111
+
112
+ # The input stream is a wrapper around the Java InputStream.
113
+ env['rack.input'] = request.getInputStream.to_io
114
+
115
+ # The output stream defaults to stderr.
116
+ env['rack.errors'] ||= $stderr
117
+
118
+ # All done, hand back the Rack request.
119
+ return(env)
120
+ end
121
+
122
+ #
123
+ # Turns a Rack response into a Servlet response; can be called
124
+ # multiple times. Returns true if this is the full request (either
125
+ # a synchronous request or the last part of an async request),
126
+ # false otherwise.
127
+ #
128
+ # Note that keep-alive *only* happens if we get either a pathname
129
+ # (because we can find the length ourselves), or if we get a
130
+ # Content-Length header as part of the response. While we can
131
+ # readily buffer the response object to figure out how long it is,
132
+ # we have no guarantee that we aren't going to be buffering
133
+ # something *huge*.
134
+ #
135
+ # http://docstore.mik.ua/orelly/java-ent/servlet/ch05_03.htm
136
+ #
137
+ def rack_to_servlet(rack_response, response)
138
+ # Split apart the Rack response.
139
+ status, headers, body = rack_response
140
+
141
+ # We assume the request is finished if we got empty headers,
142
+ # an empty body, and we have a committed response.
143
+ finished = (headers.empty? and \
144
+ body.respond_to?(:empty?) and body.empty?)
145
+ return(true) if (finished and response.isCommitted)
146
+
147
+ # No need to send headers again if we've already shipped
148
+ # data out on an async request.
149
+ unless(response.isCommitted)
150
+ # Set the HTTP status code.
151
+ response.setStatus(status)
152
+
153
+ # Did we get a Content-Length header?
154
+ content_length = headers.delete('Content-Length')
155
+ response.setContentLength(content_length.to_i) \
156
+ if content_length
157
+
158
+ # Add all the result headers.
159
+ headers.each { |h, v| response.addHeader(h, v) }
160
+ end
161
+
162
+ # How else would we write output?
163
+ output = response.getOutputStream
164
+
165
+ # Turn the body into something nice and Java-y.
166
+ if(body.respond_to?(:to_path))
167
+ # We've been told to serve a file; use FileInputStream to
168
+ # stream the file directly to the servlet, because this
169
+ # is a lot faster than doing it with Ruby.
170
+ file = java.io.File.new(body.to_path)
171
+
172
+ # We set the content-length so we can use Keep-Alive,
173
+ # unless this is an async request.
174
+ response.setContentLength(file.length) \
175
+ unless content_length
176
+
177
+ # Stream the file directly.
178
+ buffer = Java::byte[4096].new
179
+ input_stream = FileInputStream.new(file)
180
+ while((count = input_stream.read(buffer)) != -1)
181
+ output.write(buffer, 0, count)
182
+ end
183
+ input_stream.close
184
+ else
185
+ body.each { |l| output.write(l.to_java_bytes) }
186
+ end
187
+
188
+ # Close the body if we're supposed to.
189
+ body.close if body.respond_to?(:close)
190
+
191
+ # All done.
192
+ output.flush
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,41 @@
1
+ module Trinidad
2
+ class Server
3
+ alias :old_load_config :load_config
4
+ def load_config(config)
5
+ old_load_config(config)
6
+ trap_signals
7
+ end
8
+
9
+ def trap_signals
10
+ # trap signals and stop tomcat properly to make sure resque is also stopped properly
11
+ trap('INT') { tomcat.stop }
12
+ trap('TERM') { tomcat.stop }
13
+ end
14
+ end
15
+
16
+ class WebApp
17
+ def app
18
+ @config[:app]
19
+ end
20
+ end
21
+
22
+ module Lifecycle
23
+ class Default < Base
24
+ def configure_rack_servlet(context)
25
+ wrapper = context.create_wrapper
26
+
27
+ rack = Trinidad::Rack::RackServlet.new
28
+ rack.rackup(@webapp.app)
29
+ wrapper.servlet = rack
30
+ wrapper.name = 'RackServlet'
31
+
32
+ context.add_child(wrapper)
33
+ context.add_servlet_mapping('/*', wrapper.name)
34
+ end
35
+ def configure_rack_listener(context)
36
+ # we need to disable this method to ensure trinidad doesn't load any
37
+ # application listener
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,73 @@
1
+ ## This is the rakegem gemspec template. Make sure you read and understand
2
+ ## all of the comments. Some sections require modification, and others can
3
+ ## be deleted if you don't need them. Once you understand the contents of
4
+ ## this file, feel free to delete any comments that begin with two hash marks.
5
+ ## You can find comprehensive Gem::Specification documentation, at
6
+ ## http://docs.rubygems.org/read/chapter/20
7
+ Gem::Specification.new do |s|
8
+ s.specification_version = 2 if s.respond_to? :specification_version=
9
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
+ s.rubygems_version = '1.3.5'
11
+
12
+ ## Leave these as is they will be modified for you by the rake gemspec task.
13
+ ## If your rubyforge_project name is different, then edit it and comment out
14
+ ## the sub! line in the Rakefile
15
+ s.name = 'trinidad_rack'
16
+ s.version = '0.1.0'
17
+ s.date = '2011-04-10'
18
+ s.rubyforge_project = 'trinidad_rack'
19
+
20
+ ## Make sure your summary is short. The description may be as long
21
+ ## as you like.
22
+ s.summary = "Simple Rack module and servlet for Trinidad"
23
+ s.description = "Simple Rack module and servlet for Trinidad based on Mizuno"
24
+
25
+ ## List the primary authors. If there are a bunch of authors, it's probably
26
+ ## better to set the email to an email list or something. If you don't have
27
+ ## a custom homepage, consider using your GitHub URL or the like.
28
+ s.authors = ["David Calavera"]
29
+ s.email = 'calavera@apache.org'
30
+ s.homepage = 'http://github.com/trinidad/trinidad_rack'
31
+
32
+ s.executables = %w[trinidad_rack]
33
+ s.default_executable = 'trinidad_rack'
34
+
35
+ ## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
36
+ ## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
37
+ s.require_paths = %w[lib]
38
+
39
+ ## Specify any RDoc options here. You'll want to add your README and
40
+ ## LICENSE files to the extra_rdoc_files list.
41
+ s.rdoc_options = ["--charset=UTF-8"]
42
+ s.extra_rdoc_files = %w[README LICENSE]
43
+
44
+ ## List your runtime dependencies here. Runtime dependencies are those
45
+ ## that are needed for an end user to actually USE your code.
46
+ s.add_dependency('trinidad', '>=1.0.1')
47
+
48
+ ## List your development dependencies here. Development dependencies are
49
+ ## those that are only needed during development
50
+ s.add_development_dependency('rspec', '>=2.2')
51
+ s.add_development_dependency('mocha')
52
+
53
+ ## Leave this section as-is. It will be automatically generated from the
54
+ ## contents of your Git repository via the gemspec task. DO NOT REMOVE
55
+ ## THE MANIFEST COMMENTS, they are used as delimiters by the task.
56
+ # = MANIFEST =
57
+ s.files = %w[
58
+ LICENSE
59
+ README
60
+ Rakefile
61
+ bin/trinidad_rack
62
+ lib/rack/handler/trinidad.rb
63
+ lib/trinidad_rack.rb
64
+ lib/trinidad_rack/rack_servlet.rb
65
+ lib/trinidad_rack/trinidad_ext.rb
66
+ trinidad_rack.gemspec
67
+ ]
68
+ # = MANIFEST =
69
+
70
+ ## Test files will be grabbed from the file list. Make sure the path glob
71
+ ## matches what you actually use.
72
+ s.test_files = s.files.select { |path| path =~ /^spec\/.*\_spec.rb/ }
73
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: trinidad_rack
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - David Calavera
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-04-10 00:00:00 -07:00
14
+ default_executable: trinidad_rack
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: trinidad
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: 1.0.1
25
+ type: :runtime
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: "2.2"
36
+ type: :development
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: mocha
40
+ prerelease: false
41
+ requirement: &id003 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ type: :development
48
+ version_requirements: *id003
49
+ description: Simple Rack module and servlet for Trinidad based on Mizuno
50
+ email: calavera@apache.org
51
+ executables:
52
+ - trinidad_rack
53
+ extensions: []
54
+
55
+ extra_rdoc_files:
56
+ - README
57
+ - LICENSE
58
+ files:
59
+ - LICENSE
60
+ - README
61
+ - Rakefile
62
+ - bin/trinidad_rack
63
+ - lib/rack/handler/trinidad.rb
64
+ - lib/trinidad_rack.rb
65
+ - lib/trinidad_rack/rack_servlet.rb
66
+ - lib/trinidad_rack/trinidad_ext.rb
67
+ - trinidad_rack.gemspec
68
+ has_rdoc: true
69
+ homepage: http://github.com/trinidad/trinidad_rack
70
+ licenses: []
71
+
72
+ post_install_message:
73
+ rdoc_options:
74
+ - --charset=UTF-8
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: "0"
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: "0"
89
+ requirements: []
90
+
91
+ rubyforge_project: trinidad_rack
92
+ rubygems_version: 1.5.1
93
+ signing_key:
94
+ specification_version: 2
95
+ summary: Simple Rack module and servlet for Trinidad
96
+ test_files: []
97
+