http_spew 0.4.1 → 0.7.1

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.
data/GIT-VERSION-FILE CHANGED
@@ -1 +1 @@
1
- GIT_VERSION = 0.4.1
1
+ GIT_VERSION = 0.7.1
data/GIT-VERSION-GEN CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/bin/sh
2
2
 
3
3
  GVF=GIT-VERSION-FILE
4
- DEF_VER=v0.4.1.GIT
4
+ DEF_VER=v0.7.1
5
5
 
6
6
  LF='
7
7
  '
data/GNUmakefile CHANGED
@@ -1,6 +1,5 @@
1
1
  all::
2
- RSYNC_DEST := bogomips.org:/srv/bogomips/http_spew
3
- rfproject := rainbows
2
+ RSYNC_DEST := yhbt.net:/srv/yhbt/http_spew
4
3
  rfpackage := http_spew
5
4
 
6
5
  RUBY_VERSION_FILE = lib/http_spew/version.rb
@@ -10,7 +9,7 @@ include pkg.mk
10
9
  $(RUBY_VERSION_FILE): GIT-VERSION-FILE
11
10
  @$(RM) -f $@+
12
11
  @echo >> $@+ '# -*- encoding: binary -*-'
13
- @echo >> $@+ 'HTTP_Spew.const_set :VERSION, "$(GIT_VERSION)"'
12
+ @echo >> $@+ 'HTTP_Spew.const_set :VERSION, "$(GIT_VERSION)".freeze'
14
13
  @mv $@+ $@
15
14
 
16
15
  build: $(RUBY_VERSION_FILE)
data/LATEST CHANGED
@@ -1,4 +1,6 @@
1
- === http_spew 0.4.1 / 2012-09-23 00:01 UTC
1
+ === http_spew 0.7.1 / 2022-01-16 08:45 UTC
2
2
 
3
- Fix formatting of user-supplied headers.
3
+ 2 changes since v0.7.0:
4
+ doc: s/bogomips.org/yhbt.net/
5
+ fix mismatched indentation warnings
4
6
 
data/LICENSE CHANGED
@@ -1,17 +1,14 @@
1
- HTTP Spew is copyrighted Free Software by all contributors, see logs in
1
+ http_spew is copyrighted Free Software by all contributors, see logs in
2
2
  revision control for names and email addresses of all of them.
3
3
 
4
4
  You can redistribute it and/or modify it under the terms of the GNU
5
- General Public License, version 2 *or* 3 ({GPLv3}[link:COPYING]) as
6
- published by the Free Software Foundation. The project leader
7
- (Eric Wong) reserves the right to relicense HTTP Spew under future versions
8
- of the GPL.
5
+ General Public License as published by the Free Software Foundation;
6
+ either version 2 of the License, or (at your option) any later version.
9
7
 
10
- HTTP Spew is distributed in the hope that it will be useful, but WITHOUT
8
+ http_spew is distributed in the hope that it will be useful, but WITHOUT
11
9
  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
- FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
13
- License for more details.
10
+ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ for more details.
14
12
 
15
- You should have received a copy of the GNU General Public License
16
- along with HTTP Spew; if not, write to the Free Software Foundation, Inc.,
17
- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
13
+ You should have received a copy of the GNU General Public License along
14
+ with this program; if not, see https://www.gnu.org/licenses/gpl-2.0.txt
data/NEWS CHANGED
@@ -1,3 +1,58 @@
1
+ === http_spew 0.7.1 / 2022-01-16 08:45 UTC
2
+
3
+ 2 changes since v0.7.0:
4
+ doc: s/bogomips.org/yhbt.net/
5
+ fix mismatched indentation warnings
6
+
7
+ === http_spew 0.7.0 - modernizing for newer Rubies / 2017-06-12 22:00 UTC
8
+
9
+ Ruby 2.3+ is now required as kgio dependencies are gone.
10
+ This is unlikely to be a problem in practice, as hardly
11
+ anybody uses this at the moment.
12
+
13
+ 5 changes since 0.6.0:
14
+
15
+ remove most uses of kgio
16
+ make kgio entirely optional
17
+ update to only use public email address
18
+ http_spew: remove kgio require at top level
19
+ gemspec: remove olddoc as a build dependency
20
+
21
+ === http_spew 0.6.0 - even less / 2016-11-05 02:15 UTC
22
+
23
+ Rack "to_path" support is dropped since few servers can
24
+ take advantage of this in a non-buggy way.
25
+
26
+ As a consolation, #each should be less GC-intensive by
27
+ with explicit calls to String#clear to reduce memory
28
+ pressure.
29
+
30
+ 3 changes since 0.5.0
31
+
32
+ pkg.mk: avoid network for "gem install"
33
+ request: drop to_path support
34
+ explicitly clear large buf when it is obviously safe
35
+
36
+ === http_spew 0.5.0 / 2016-10-31 20:43 UTC
37
+
38
+ This release requires Ruby 2.1 or later.
39
+
40
+ 13 changes since 0.4.1:
41
+
42
+ gemspec: require kcar >= 0.3.1
43
+ test/helper: explicit redirect for Ruby 2.0.0
44
+ update packaging + docs (website)
45
+ allow all future GPL versions
46
+ add benchmark scripts
47
+ relax dependency on unicorn
48
+ declare empty classes with constant assignment
49
+ test_upload: use object_id to check matches
50
+ use frozen string literals for Ruby 2.1+
51
+ merge into kcar project and mailing list
52
+ dedicated mailing list
53
+ rely on opt_str_freeze in more places
54
+ use monotonic clock for timing
55
+
1
56
  === http_spew 0.4.1 / 2012-09-23 00:01 UTC
2
57
 
3
58
  Fix formatting of user-supplied headers.
data/README CHANGED
@@ -11,7 +11,6 @@ Use HTTP Spew if you wish you could kinda multicast with HTTP...
11
11
  * No support for DNS resolution (WONTFIX, ever)
12
12
  * No support for HTTPS
13
13
  * No support for keepalive (yet?)
14
- * No support for Ruby 1.8, this is Ruby 1.9-only
15
14
  * Not remotely RFC-compliant
16
15
  * Messes up analytics/reporting on servers
17
16
  * Resets server connections
@@ -33,13 +32,13 @@ It's also completely untested and unused anywhere!
33
32
 
34
33
  You can get the latest source via git from the following locations:
35
34
 
36
- git://bogomips.org/http_spew.git
35
+ git://yhbt.net/http_spew.git
37
36
  git://repo.or.cz/http_spew.git (mirror)
38
37
 
39
38
  You may browse the code from the web and download the latest snapshot
40
39
  tarballs here:
41
40
 
42
- * http://bogomips.org/http_spew.git (cgit)
41
+ * https://yhbt.net/http_spew.git
43
42
  * http://repo.or.cz/w/http_spew.git (gitweb)
44
43
 
45
44
  Inline patches (from "git format-patch") to the mailing list are
@@ -54,8 +53,8 @@ don't email the git mailing list or maintainer with http_spew patches.
54
53
  == Contact
55
54
 
56
55
  All feedback (bug reports, user/development discussion, patches, pull
57
- requests) go to the mailing list: mailto:http.spew@librelist.org
56
+ requests) go to the mailing list: mailto:http_spew-public@yhbt.net
58
57
 
59
- Mailing list archives in mbox format may be downloaded here:
58
+ Mailing list archives may be viewed and downloaded here:
60
59
 
61
- http://bogomips.org/http_spew/archives/
60
+ https://yhbt.net/http_spew-public/
@@ -0,0 +1,52 @@
1
+ # -*- encoding: binary -*-
2
+ require "./test/helper"
3
+ require "benchmark"
4
+
5
+ class TestBMContentMD5 < Test::Unit::TestCase
6
+ def setup
7
+ @addr, @port, @srv = start_server("./test/content-md5.ru", 1)
8
+ @sockaddr = Socket.pack_sockaddr_in(@port, @addr)
9
+ @env = {
10
+ "REQUEST_METHOD" => "PUT",
11
+ "REQUEST_URI" => "/",
12
+ "HTTP_HOST" => "example.com",
13
+ }
14
+ @tmpfiles = []
15
+ @bs = ENV['bs'] ? ENV['bs'].to_i : 1024 * 1024
16
+ @count = ENV['count'] ? ENV['count'].to_i : 1000
17
+ @cmd = %w(dd if=/dev/zero)
18
+ @cmd << "bs=#@bs"
19
+ @cmd << "count=#@count"
20
+ end
21
+
22
+ def teardown
23
+ Process.kill(:QUIT, @srv)
24
+ Process.waitpid2(@srv)
25
+ @tmpfiles.each { |tmp| tmp.closed? or tmp.close! }
26
+ end
27
+
28
+ def test_upload_with_md5
29
+ rd, wr = IO.pipe
30
+ pid = fork do
31
+ $stdout.reopen(wr)
32
+ rd.close
33
+ wr.close
34
+ exec(*@cmd)
35
+ end
36
+ wr.close
37
+ @env["CONTENT_LENGTH"] = (@bs * @count).to_s
38
+ @env["rack.input"] = rd
39
+ input = HTTP_Spew::ContentMD5.new(@env)
40
+ assert_nil @env["CONTENT_LENGTH"]
41
+ assert_equal "chunked", @env["HTTP_TRANSFER_ENCODING"]
42
+ req = HTTP_Spew::Request.new(@env, input, @sockaddr)
43
+ rv = nil
44
+ res = Benchmark.measure do
45
+ rv = req.run(100000)
46
+ end
47
+ assert_equal 200, rv[0].to_i
48
+ pid, status = Process.waitpid2(pid)
49
+ assert status.success?
50
+ p res
51
+ end
52
+ end
@@ -0,0 +1,59 @@
1
+ require "./test/helper"
2
+ require "benchmark"
3
+
4
+ class TestBMContentMD5InputSpray < Test::Unit::TestCase
5
+ def setup
6
+ @nr = 4
7
+ @addr, @port, @srv = start_server("./test/content-md5.ru", @nr)
8
+ @sockaddr = Socket.pack_sockaddr_in(@port, @addr)
9
+ @env = {
10
+ "REQUEST_METHOD" => "PUT",
11
+ "REQUEST_URI" => "/",
12
+ "HTTP_HOST" => "example.com",
13
+ }
14
+ @tmpfiles = []
15
+ @bs = ENV['bs'] ? ENV['bs'].to_i : 1024 * 1024
16
+ @count = ENV['count'] ? ENV['count'].to_i : 1000
17
+ @cmd = %w(dd if=/dev/zero)
18
+ @cmd << "bs=#@bs"
19
+ @cmd << "count=#@count"
20
+ end
21
+
22
+ def teardown
23
+ Process.kill(:QUIT, @srv)
24
+ Process.waitpid2(@srv)
25
+ @tmpfiles.each { |tmp| tmp.closed? or tmp.close! }
26
+ end
27
+
28
+ def test_spray_with_md5
29
+ rd, wr = IO.pipe
30
+ pid = fork do
31
+ $stdout.reopen(wr)
32
+ rd.close
33
+ wr.close
34
+ exec(*@cmd)
35
+ end
36
+ wr.close
37
+ @env["CONTENT_LENGTH"] = (@bs * @count).to_s
38
+ @env["rack.input"] = rd
39
+ input = HTTP_Spew::ContentMD5.new(@env)
40
+ sprayer = HTTP_Spew::InputSpray.new(@env, @nr, input)
41
+ assert_nil @env["CONTENT_LENGTH"]
42
+ assert_equal "chunked", @env["HTTP_TRANSFER_ENCODING"]
43
+ reqs = sprayer.readers.map do |md5_input|
44
+ HTTP_Spew::Request.new(@env, md5_input, @sockaddr)
45
+ end
46
+ assert_equal @nr, reqs.size
47
+ rv = nil
48
+ res = Benchmark.measure do
49
+ rv = HTTP_Spew.wait_mt reqs.size, reqs, 3600
50
+ end
51
+ assert_equal @nr, rv.size
52
+ rv.each do |resp|
53
+ assert_equal 200, resp.response[0].to_i
54
+ end
55
+ pid, status = Process.waitpid2(pid)
56
+ assert status.success?
57
+ p res
58
+ end
59
+ end
data/http_spew.gemspec CHANGED
@@ -1,24 +1,20 @@
1
- ENV["VERSION"] or abort "VERSION= must be specified"
2
- manifest = File.readlines('.manifest').map! { |x| x.chomp! }
3
- require 'wrongdoc'
4
- extend Wrongdoc::Gemspec
5
- name, summary, title = readme_metadata
1
+ manifest = File.exist?('.manifest') ?
2
+ IO.readlines('.manifest').map!(&:chomp!) : `git ls-files`.split("\n")
6
3
 
7
4
  Gem::Specification.new do |s|
8
5
  s.name = %q{http_spew}
9
- s.version = ENV["VERSION"].dup
10
- s.authors = ["HTTP Spew hackers"]
11
- s.date = Time.now.utc.strftime('%Y-%m-%d')
12
- s.description = readme_description
13
- s.email = %q{http.spew@librelist.org}
14
- s.extra_rdoc_files = extra_rdoc_files(manifest)
6
+ s.version = (ENV['VERSION'] || '0.7.1').dup
7
+ s.authors = ["http_spew hackers"]
8
+ s.description = File.read('README').split("\n\n")[1]
9
+ s.email = %q{http_spew-public@yhbt.net}
10
+ s.extra_rdoc_files = IO.readlines('.document').map!(&:chomp!).keep_if do |f|
11
+ File.exist?(f)
12
+ end
15
13
  s.files = manifest
16
- s.homepage = Wrongdoc.config[:rdoc_url]
17
- s.summary = summary
18
- s.rdoc_options = rdoc_options
19
- s.rubyforge_project = %q{rainbows}
14
+ s.homepage = 'https://yhbt.net/http_spew/'
15
+ s.summary = 'HTTP Spew - LAN-only HTTP spam^H^H^H^Hclient for Ruby'
20
16
  s.test_files = Dir["test/test_*.rb"]
21
- s.add_dependency(%q<kcar>, "~> 0.3")
22
- s.add_dependency(%q<kgio>, "~> 2.6")
23
- s.add_development_dependency(%q<wrongdoc>, "~> 1.5")
17
+ s.add_dependency(%q<kcar>, [ "~> 0.3", ">= 0.3.1"])
18
+ s.required_ruby_version = '>= 2.3'
19
+ s.licenses = %w(GPL-2.0+)
24
20
  end
@@ -3,17 +3,28 @@
3
3
  # This is a OS-level pipe that overrides IO#read to provide
4
4
  # IO#readpartial-like semantics while remaining Rack::Lint-compatible
5
5
  # for EOF, meaning we return nil on EOF instead of raising EOFError.
6
- class HTTP_Spew::ChunkyPipe < Kgio::Pipe
6
+ class HTTP_Spew::ChunkyPipe < IO
7
7
 
8
8
  # other threads may force an error to be raised in the +read+
9
9
  # method
10
10
  attr_accessor :error
11
11
 
12
+ class << self
13
+ alias new pipe
14
+ end
15
+
12
16
  # Override IO#read to behave like IO#readpartial, but still return +nil+
13
17
  # on EOF instead of raising EOFError.
14
- def read(*args)
18
+ def read(len = 16384, buf = '')
15
19
  check_err!
16
- kgio_read(*args) || check_err! || close
20
+ case read_nonblock(len, buf, exception: false)
21
+ when nil
22
+ return check_err! || close
23
+ when :wait_readable
24
+ wait_readable # retry
25
+ else
26
+ return buf
27
+ end while true
17
28
  end
18
29
 
19
30
  def check_err!
@@ -41,11 +41,11 @@ module HTTP_Spew::ClassMethods
41
41
  end
42
42
 
43
43
  def with_timeout(t)
44
- t0 = Time.now
44
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
45
45
  yield
46
- ensure
47
- t[0] -= Time.now - t0
48
- t[0] = 0.0 if t[0] < 0
46
+ ensure
47
+ t[0] -= Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
48
+ t[0] = 0.0 if t[0] < 0
49
49
  end
50
50
 
51
51
  # Returns an array of requests that are complete, including those
@@ -53,14 +53,14 @@ module HTTP_Spew::ClassMethods
53
53
  # If +need+ is fullfilled, it closes all incomplete requests.
54
54
  def wait_mt(need, requests, timeout)
55
55
  ready, failed = [], []
56
- r, w = Kgio::Pipe.new
56
+ r, w = IO.pipe
57
57
  active = []
58
58
  t = [ timeout ]
59
59
  requests.each_with_index do |req, i|
60
60
  active << Thread.new do
61
61
  begin
62
62
  rv = req.run(timeout)
63
- w.write([ i ].pack("v"))
63
+ w.write([ i ].pack("v".freeze))
64
64
  rv
65
65
  rescue => err
66
66
  err
@@ -68,8 +68,8 @@ module HTTP_Spew::ClassMethods
68
68
  end
69
69
  end
70
70
  begin
71
- with_timeout(t) { r.kgio_wait_readable(t[0]) }
72
- req_idx = r.read(2).unpack("v")[0]
71
+ with_timeout(t) { r.wait_readable(t[0]) }
72
+ req_idx = r.read(2).unpack("v".freeze)[0]
73
73
  thr = active[req_idx]
74
74
  with_timeout(t) { thr.join(t[0]) }
75
75
  rv = thr.value
@@ -81,9 +81,9 @@ module HTTP_Spew::ClassMethods
81
81
  pending = requests - ready
82
82
  error = HTTP_Spew::TimeoutError.new("request timed out")
83
83
  ready.concat(error_all(pending, error))
84
- ensure
85
- w.close
86
- r.close
84
+ ensure
85
+ w.close
86
+ r.close
87
87
  end
88
88
 
89
89
  def wait(need, requests, timeout)
@@ -110,7 +110,7 @@ module HTTP_Spew::ClassMethods
110
110
  break if pollset.empty?
111
111
 
112
112
  busy = pollset.keys
113
- rv = with_timeout(t) { Kgio.poll(pollset, (t[0] * 1000).to_i) } or break
113
+ rv = with_timeout(t) { do_poll(pollset, t[0]) } or break
114
114
  end while t[0] > 0.0 && requests = rv.keys.concat(busy).uniq!
115
115
 
116
116
  ready.concat(failed)
@@ -121,4 +121,32 @@ module HTTP_Spew::ClassMethods
121
121
  end
122
122
  ready
123
123
  end
124
+
125
+ begin
126
+ require 'kgio'
127
+ def do_poll(pollset, sec) # :nodoc:
128
+ Kgio.poll(pollset, (sec * 1000).to_i)
129
+ end
130
+ rescue LoadError
131
+ # emulate Kgio.poll with IO.select
132
+ def do_poll(pollset, sec) # :nodoc:
133
+ rd = []
134
+ wr = []
135
+ pollset.each do |io, events|
136
+ case events
137
+ when :wait_readable
138
+ rd << io
139
+ when :wait_writable
140
+ wr << io
141
+ else
142
+ raise "BUG: unsupported event #{event.inspect} for #{io.inspect}"
143
+ end
144
+ end
145
+ ready = IO.select(rd, wr, nil, sec) or return
146
+ pollset.clear
147
+ ready[0].each { |io| pollset[io] = 1 } # POLLIN
148
+ ready[1].each { |io| pollset[io] = 4 } # POLLOUT
149
+ pollset
150
+ end
151
+ end
124
152
  end
@@ -7,17 +7,16 @@ class HTTP_Spew::ContentMD5
7
7
  attr_reader :content_md5
8
8
  attr_reader :bytes_digested
9
9
 
10
- CRLF = "\r\n" # :nodoc:
11
-
12
10
  def initialize(env, input = env["rack.input"])
13
11
  if trailer = env["HTTP_TRAILER"]
14
12
  unless trailer.split(/\s*,\s*/).grep(/\AContent-MD5\z/i)[0]
15
- trailer << (trailer.empty? ? "Content-MD5" : ",Content-MD5")
13
+ trailer << (trailer.empty? ? "Content-MD5".freeze
14
+ : ",Content-MD5".freeze)
16
15
  end
17
16
  else
18
- env["HTTP_TRAILER"] = "Content-MD5"
17
+ env["HTTP_TRAILER"] = "Content-MD5".freeze
19
18
  end
20
- env["HTTP_TRANSFER_ENCODING"] = "chunked"
19
+ env["HTTP_TRANSFER_ENCODING"] = "chunked".freeze
21
20
  @to_io, wr = HTTP_Spew::ChunkyPipe.new
22
21
  expect_md5 = env.delete("HTTP_CONTENT_MD5")
23
22
  expect_len = env.delete("CONTENT_LENGTH")
@@ -41,8 +40,9 @@ class HTTP_Spew::ContentMD5
41
40
  @bytes_digested += n
42
41
  wr.write("#{n.to_s(16)}\r\n")
43
42
  digest.update(buf)
44
- wr.write(buf << CRLF)
43
+ wr.write(buf << "\r\n".freeze)
45
44
  end while input.read(0x4000, buf)
45
+ buf.clear
46
46
  end
47
47
  if expect_len && expect_len.to_i != @bytes_digested
48
48
  raise HTTP_Spew::LengthError,
@@ -1,18 +1,10 @@
1
1
  # -*- encoding: binary -*-
2
2
  module HTTP_Spew::Headers
3
- # :stopdoc:
4
- REQUEST_METHOD = "REQUEST_METHOD"
5
- REQUEST_URI = "REQUEST_URI"
6
- CRLF = "\r\n"
7
- QUERY_STRING = "QUERY_STRING"
8
- PATH_INFO = "PATH_INFO"
9
- CONTENT_TYPE = "CONTENT_TYPE" # specified by Rack to be !/^HTTP_/
10
- # :startdoc:
11
3
 
12
4
  # regenerates the request_uri from a Rack +env+
13
5
  def request_uri(env)
14
- qs = env[QUERY_STRING]
15
- qs.size == 0 ? env[PATH_INFO] : "#{env[PATH_INFO]}?#{qs}"
6
+ qs = env['QUERY_STRING']
7
+ qs.size == 0 ? env['PATH_INFO'] : "#{env['PATH_INFO']}?#{qs}"
16
8
  end
17
9
  module_function :request_uri
18
10
 
@@ -27,26 +19,25 @@ module HTTP_Spew::Headers
27
19
  #
28
20
  # buf, input = env_to_headers(env, input)
29
21
  def env_to_headers(env, input)
30
- req = "#{env[REQUEST_METHOD]} " \
31
- "#{env[REQUEST_URI] || request_uri(env)} HTTP/1.1\r\n" \
22
+ req = "#{env['REQUEST_METHOD']} " \
23
+ "#{env['REQUEST_URI'] || request_uri(env)} HTTP/1.1\r\n" \
32
24
  "Connection: close\r\n"
33
- uscore, dash = "_", "-"
34
25
  env.each do |key,value|
35
26
  %r{\AHTTP_(\w+)\z} =~ key or next
36
27
  key = $1
37
28
  %r{\A(?:VERSION|EXPECT|TRANSFER_ENCODING|CONNECTION|KEEP_ALIVE)\z}x =~
38
29
  key and next
39
30
 
40
- key.tr!(uscore, dash)
31
+ key.tr!('_'.freeze, '-'.freeze)
41
32
  req << "#{key}: #{value}\r\n"
42
33
  end
43
34
  if input
44
35
  req << (input.respond_to?(:size) ?
45
36
  "Content-Length: #{input.size}\r\n" :
46
- "Transfer-Encoding: chunked\r\n")
47
- ct = env[CONTENT_TYPE] and req << "Content-Type: #{ct}\r\n"
37
+ "Transfer-Encoding: chunked\r\n".freeze)
38
+ ct = env['CONTENT_TYPE'] and req << "Content-Type: #{ct}\r\n"
48
39
  end
49
- req << CRLF
40
+ req << "\r\n".freeze
50
41
  String === input ? (req << input) : [ req, input ]
51
42
  end
52
43
  module_function :env_to_headers
@@ -35,6 +35,7 @@ class HTTP_Spew::InputSpray
35
35
  @pipes.delete_if { |rd, wr| write_fail?(rd, wr, buf) }.empty? and
36
36
  raise HTTP_Spew::NoWritersError, "all writers have died", []
37
37
  end
38
+ buf.clear
38
39
  rescue => e
39
40
  @pipes.each { |rd, _| rd.error = e }
40
41
  ensure