diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..b18fd293
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: 'github-actions'
+ directory: '/'
+ schedule:
+ interval: 'weekly'
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e1dee1d7..edede5b1 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -3,22 +3,34 @@ name: test
on: [push, pull_request]
jobs:
- build:
+ ruby-versions:
+ uses: ruby/actions/.github/workflows/ruby_versions.yml@master
+ with:
+ engine: cruby
+ min_version: 2.4
+
+ test:
+ needs: ruby-versions
name: build (${{ matrix.ruby }} / ${{ matrix.os }})
strategy:
+ fail-fast: false
matrix:
- ruby: [ 2.7, 2.6, 2.5, head ]
+ ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }}
os: [ ubuntu-latest, macos-latest ]
+ # CRuby < 2.6 does not support macos-arm64, so test those on amd64 instead
+ exclude:
+ - { os: macos-latest, ruby: '2.4' }
+ - { os: macos-latest, ruby: '2.5' }
+ include:
+ - { os: macos-13, ruby: '2.4' }
+ - { os: macos-13, ruby: '2.5' }
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@master
+ - uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
- - name: Install dependencies
- run: |
- gem install bundler --no-document
- bundle install
+ bundler-cache: true # 'bundle install' and cache
- name: Run test
- run: rake test
+ run: bundle exec rake test
diff --git a/Gemfile b/Gemfile
index fa75df15..f5b6c4b6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,3 +1,7 @@
source 'https://rubygems.org'
gemspec
+
+gem "rake"
+gem "test-unit"
+gem "test-unit-ruby-core"
diff --git a/README.md b/README.md
index c30fb1c6..0027072b 100644
--- a/README.md
+++ b/README.md
@@ -54,7 +54,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
## Contributing
-Bug reports and Patch are welcome on https://bugs.ruby-lang.org/.
+Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/webrick.
## License
diff --git a/Rakefile b/Rakefile
index 43a5a3f5..5d512c89 100644
--- a/Rakefile
+++ b/Rakefile
@@ -2,9 +2,9 @@ require "bundler/gem_tasks"
require "rake/testtask"
Rake::TestTask.new(:test) do |t|
- t.libs << "test" << "test/lib"
- t.libs << "lib"
- t.test_files = FileList['test/**/test_*.rb']
+ t.libs << "test/lib"
+ t.ruby_opts << "-rhelper"
+ t.test_files = FileList["test/**/test_*.rb"]
end
task :default => :test
diff --git a/lib/webrick/accesslog.rb b/lib/webrick/accesslog.rb
index e4849637..fccfd653 100644
--- a/lib/webrick/accesslog.rb
+++ b/lib/webrick/accesslog.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#--
# accesslog.rb -- Access log handling utilities
#
diff --git a/lib/webrick/cgi.rb b/lib/webrick/cgi.rb
index bb0ae2fc..f22480b8 100644
--- a/lib/webrick/cgi.rb
+++ b/lib/webrick/cgi.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# cgi.rb -- Yet another CGI library
#
diff --git a/lib/webrick/compat.rb b/lib/webrick/compat.rb
index c497a193..842da1eb 100644
--- a/lib/webrick/compat.rb
+++ b/lib/webrick/compat.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# compat.rb -- cross platform compatibility
#
diff --git a/lib/webrick/config.rb b/lib/webrick/config.rb
index 9f2ab44f..d67375c8 100644
--- a/lib/webrick/config.rb
+++ b/lib/webrick/config.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# config.rb -- Default configurations.
#
diff --git a/lib/webrick/cookie.rb b/lib/webrick/cookie.rb
index 5fd3bfb2..7b187822 100644
--- a/lib/webrick/cookie.rb
+++ b/lib/webrick/cookie.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# cookie.rb -- Cookie class
#
@@ -92,7 +92,7 @@ def expires
# The cookie string suitable for use in an HTTP header
def to_s
- ret = ""
+ ret = +""
ret << @name << "=" << @value
ret << "; " << "Version=" << @version.to_s if @version > 0
ret << "; " << "Domain=" << @domain if @domain
diff --git a/lib/webrick/htmlutils.rb b/lib/webrick/htmlutils.rb
index ed9f4ac0..7ff0bde2 100644
--- a/lib/webrick/htmlutils.rb
+++ b/lib/webrick/htmlutils.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#--
# htmlutils.rb -- HTMLUtils Module
#
diff --git a/lib/webrick/httpauth.rb b/lib/webrick/httpauth.rb
index f8bf09a6..4f5321c8 100644
--- a/lib/webrick/httpauth.rb
+++ b/lib/webrick/httpauth.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# httpauth.rb -- HTTP access authentication
#
diff --git a/lib/webrick/httpauth/authenticator.rb b/lib/webrick/httpauth/authenticator.rb
index 8f0eaa3a..a6ee28de 100644
--- a/lib/webrick/httpauth/authenticator.rb
+++ b/lib/webrick/httpauth/authenticator.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#--
# httpauth/authenticator.rb -- Authenticator mix-in module.
#
diff --git a/lib/webrick/httpauth/basicauth.rb b/lib/webrick/httpauth/basicauth.rb
index 7d0a9cfc..cc8137af 100644
--- a/lib/webrick/httpauth/basicauth.rb
+++ b/lib/webrick/httpauth/basicauth.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# httpauth/basicauth.rb -- HTTP basic access authentication
#
diff --git a/lib/webrick/httpauth/digestauth.rb b/lib/webrick/httpauth/digestauth.rb
index 3cf12899..25ce8ba0 100644
--- a/lib/webrick/httpauth/digestauth.rb
+++ b/lib/webrick/httpauth/digestauth.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# httpauth/digestauth.rb -- HTTP digest access authentication
#
diff --git a/lib/webrick/httpauth/htdigest.rb b/lib/webrick/httpauth/htdigest.rb
index 93b18e2c..c7e853b7 100644
--- a/lib/webrick/httpauth/htdigest.rb
+++ b/lib/webrick/httpauth/htdigest.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# httpauth/htdigest.rb -- Apache compatible htdigest file
#
diff --git a/lib/webrick/httpauth/htgroup.rb b/lib/webrick/httpauth/htgroup.rb
index e06c441b..108c9d0b 100644
--- a/lib/webrick/httpauth/htgroup.rb
+++ b/lib/webrick/httpauth/htgroup.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# httpauth/htgroup.rb -- Apache compatible htgroup file
#
diff --git a/lib/webrick/httpauth/htpasswd.rb b/lib/webrick/httpauth/htpasswd.rb
index abca3053..9a48e57d 100644
--- a/lib/webrick/httpauth/htpasswd.rb
+++ b/lib/webrick/httpauth/htpasswd.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# httpauth/htpasswd -- Apache compatible htpasswd file
#
diff --git a/lib/webrick/httpauth/userdb.rb b/lib/webrick/httpauth/userdb.rb
index 7a17715c..0d0f72b1 100644
--- a/lib/webrick/httpauth/userdb.rb
+++ b/lib/webrick/httpauth/userdb.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#--
# httpauth/userdb.rb -- UserDB mix-in module.
#
diff --git a/lib/webrick/httpproxy.rb b/lib/webrick/httpproxy.rb
index 7607c3df..196682ec 100644
--- a/lib/webrick/httpproxy.rb
+++ b/lib/webrick/httpproxy.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# httpproxy.rb -- HTTPProxy Class
#
diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb
index d34eac7e..1fa5dbb4 100644
--- a/lib/webrick/httprequest.rb
+++ b/lib/webrick/httprequest.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# httprequest.rb -- HTTPRequest Class
#
@@ -171,7 +171,7 @@ def initialize(config)
@accept_charset = []
@accept_encoding = []
@accept_language = []
- @body = ""
+ @body = +""
@addr = @peeraddr = nil
@attributes = {}
@@ -318,7 +318,7 @@ def content_type
def [](header_name)
if @header
value = @header[header_name.downcase]
- value.empty? ? nil : value.join(", ")
+ value.empty? ? nil : value.join
end
end
@@ -329,7 +329,7 @@ def each
if @header
@header.each{|k, v|
value = @header[k]
- yield(k, value.empty? ? nil : value.join(", "))
+ yield(k, value.empty? ? nil : value.join)
}
end
end
@@ -402,7 +402,7 @@ def fixup() # :nodoc:
# This method provides the metavariables defined by the revision 3
# of "The WWW Common Gateway Interface Version 1.1"
# To browse the current document of CGI Version 1.1, see below:
- # http://tools.ietf.org/html/rfc3875
+ # https://www.rfc-editor.org/rfc/rfc3875
def meta_vars
meta = Hash.new
@@ -458,7 +458,7 @@ def read_request_line(socket)
end
@request_time = Time.now
- if /^(\S+)\s+(\S++)(?:\s+HTTP\/(\d+\.\d+))?\r?\n/mo =~ @request_line
+ if /^(\S+) (\S++)(?: HTTP\/(\d+\.\d+))?\r\n/mo =~ @request_line
@request_method = $1
@unparsed_uri = $2
@http_version = HTTPVersion.new($3 ? $3 : "0.9")
@@ -470,15 +470,34 @@ def read_request_line(socket)
def read_header(socket)
if socket
+ end_of_headers = false
+
while line = read_line(socket)
- break if /\A(#{CRLF}|#{LF})\z/om =~ line
+ if line == CRLF
+ end_of_headers = true
+ break
+ end
if (@request_bytes += line.bytesize) > MAX_HEADER_LENGTH
raise HTTPStatus::RequestEntityTooLarge, 'headers too large'
end
+ if line.include?("\x00")
+ raise HTTPStatus::BadRequest, 'null byte in header'
+ end
@raw_header << line
end
+
+ # Allow if @header already set to support chunked trailers
+ raise HTTPStatus::EOFError unless end_of_headers || @header
end
@header = HTTPUtils::parse_header(@raw_header.join)
+
+ if (content_length = @header['content-length']) && content_length.length != 0
+ if content_length.length > 1
+ raise HTTPStatus::BadRequest, "multiple content-length request headers"
+ elsif !/\A\d+\z/.match?(content_length[0])
+ raise HTTPStatus::BadRequest, "invalid content-length request header"
+ end
+ end
end
def parse_uri(str, scheme="http")
@@ -491,8 +510,7 @@ def parse_uri(str, scheme="http")
if @forwarded_host
host, port = @forwarded_host, @forwarded_port
elsif self["host"]
- pattern = /\A(#{URI::REGEXP::PATTERN::HOST})(?::(\d+))?\z/n
- host, port = *self['host'].scan(pattern)[0]
+ host, port = parse_host_request_line(self["host"])
elsif @addr.size > 0
host, port = @addr[2], @addr[1]
else
@@ -504,9 +522,19 @@ def parse_uri(str, scheme="http")
return URI::parse(uri.to_s)
end
+ host_pattern = URI::RFC2396_Parser.new.pattern.fetch(:HOST)
+ HOST_PATTERN = /\A(#{host_pattern})(?::(\d+))?\z/n
+ def parse_host_request_line(host)
+ host.scan(HOST_PATTERN)[0]
+ end
+
def read_body(socket, block)
return unless socket
if tc = self['transfer-encoding']
+ if self['content-length']
+ raise HTTPStatus::BadRequest, "request with both transfer-encoding and content-length, possible request smuggling"
+ end
+
case tc
when /\Achunked\z/io then read_chunked(socket, block)
else raise HTTPStatus::NotImplemented, "Transfer-Encoding: #{tc}."
@@ -522,7 +550,7 @@ def read_body(socket, block)
if @remaining_size > 0 && @socket.eof?
raise HTTPStatus::BadRequest, "invalid body size."
end
- elsif BODY_CONTAINABLE_METHODS.member?(@request_method) && !@socket.eof
+ elsif BODY_CONTAINABLE_METHODS.member?(@request_method)
raise HTTPStatus::LengthRequired
end
return @body
@@ -530,7 +558,7 @@ def read_body(socket, block)
def read_chunk_size(socket)
line = read_line(socket)
- if /^([0-9a-fA-F]+)(?:;(\S+))?/ =~ line
+ if /\A([0-9a-fA-F]+)(?:;(\S+(?:=\S+)?))?\r\n\z/ =~ line
chunk_size = $1.hex
chunk_ext = $2
[ chunk_size, chunk_ext ]
@@ -551,7 +579,11 @@ def read_chunked(socket, block)
block.call(data)
end while (chunk_size -= sz) > 0
- read_line(socket) # skip CRLF
+ line = read_line(socket) # skip CRLF
+ unless line == "\r\n"
+ raise HTTPStatus::BadRequest, "extra data after chunk `#{line}'."
+ end
+
chunk_size, = read_chunk_size(socket)
end
read_header(socket) # trailer + CRLF
diff --git a/lib/webrick/httpresponse.rb b/lib/webrick/httpresponse.rb
index ba4494ab..dde0261d 100644
--- a/lib/webrick/httpresponse.rb
+++ b/lib/webrick/httpresponse.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# httpresponse.rb -- HTTPResponse Class
#
@@ -105,6 +105,11 @@ class InvalidHeader < StandardError
attr_reader :sent_size
+ ##
+ # Set the response body proc as an streaming/upgrade response.
+
+ attr_accessor :upgrade
+
##
# Creates a new HTTP response object. WEBrick::Config::HTTP is the
# default configuration.
@@ -117,7 +122,7 @@ def initialize(config)
@status = HTTPStatus::RC_OK
@reason_phrase = nil
@http_version = HTTPVersion::convert(@config[:HTTPVersion])
- @body = ''
+ @body = +""
@keep_alive = true
@cookies = []
@request_method = nil
@@ -217,6 +222,16 @@ def keep_alive?
@keep_alive
end
+ ##
+ # Sets the response to be a streaming/upgrade response.
+ # This will disable keep-alive and chunked transfer encoding.
+
+ def upgrade!(protocol)
+ @upgrade = protocol
+ @keep_alive = false
+ @chunked = false
+ end
+
##
# Sends the response on +socket+
@@ -242,6 +257,14 @@ def setup_header() # :nodoc:
@header['server'] ||= @config[:ServerSoftware]
@header['date'] ||= Time.now.httpdate
+ if @upgrade
+ @header['connection'] = 'upgrade'
+ @header['upgrade'] = @upgrade
+ @keep_alive = false
+
+ return
+ end
+
# HTTP/0.9 features
if @request_http_version < "1.0"
@http_version = HTTPVersion.new("0.9")
@@ -268,11 +291,10 @@ def setup_header() # :nodoc:
elsif %r{^multipart/byteranges} =~ @header['content-type']
@header.delete('content-length')
elsif @header['content-length'].nil?
- if @body.respond_to? :readpartial
- elsif @body.respond_to? :call
- make_body_tempfile
+ if @body.respond_to?(:bytesize)
+ @header['content-length'] = @body.bytesize.to_s
else
- @header['content-length'] = (@body ? @body.bytesize : 0).to_s
+ @header['connection'] = 'close'
end
end
@@ -332,7 +354,7 @@ def remove_body_tempfile # :nodoc:
def send_header(socket) # :nodoc:
if @http_version.major > 0
- data = status_line()
+ data = status_line().dup
@header.each{|key, value|
tmp = key.gsub(/\bwww|^te$|\b\w/){ $&.upcase }
data << "#{tmp}: #{check_header(value)}" << CRLF
@@ -419,7 +441,7 @@ def check_header(header_value)
# :stopdoc:
def error_body(backtrace, ex, host, port)
- @body = ''
+ @body = +""
@body << <<-_end_of_html_
@@ -453,11 +475,11 @@ def send_body_io(socket)
if @request_method == "HEAD"
# do nothing
elsif chunked?
- buf = ''
+ buf = +''
begin
@body.readpartial(@buffer_size, buf)
size = buf.bytesize
- data = "#{size.to_s(16)}#{CRLF}#{buf}#{CRLF}"
+ data = +"#{size.to_s(16)}#{CRLF}#{buf}#{CRLF}"
socket.write(data)
data.clear
@sent_size += size
@@ -517,14 +539,16 @@ def send_body_proc(socket)
@body.call(ChunkedWrapper.new(socket, self))
socket.write("0#{CRLF}#{CRLF}")
else
- size = @header['content-length'].to_i
if @bodytempfile
@bodytempfile.rewind
IO.copy_stream(@bodytempfile, socket)
else
@body.call(socket)
end
- @sent_size = size
+
+ if content_length = @header['content-length']
+ @sent_size = content_length.to_i
+ end
end
end
@@ -539,7 +563,7 @@ def write(buf)
socket = @socket
@resp.instance_eval {
size = buf.bytesize
- data = "#{size.to_s(16)}#{CRLF}#{buf}#{CRLF}"
+ data = +"#{size.to_s(16)}#{CRLF}#{buf}#{CRLF}"
socket.write(data)
data.clear
@sent_size += size
diff --git a/lib/webrick/https.rb b/lib/webrick/https.rb
index b0a49bc4..7f00b306 100644
--- a/lib/webrick/https.rb
+++ b/lib/webrick/https.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# https.rb -- SSL/TLS enhancement for HTTPServer
#
diff --git a/lib/webrick/httpserver.rb b/lib/webrick/httpserver.rb
index e85d0593..0d261bf0 100644
--- a/lib/webrick/httpserver.rb
+++ b/lib/webrick/httpserver.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# httpserver.rb -- HTTPServer Class
#
@@ -285,7 +285,7 @@ def compile
end
def normalize(dir)
- ret = dir ? dir.dup : ""
+ ret = dir ? dir.dup : +""
ret.sub!(%r|/+\z|, "")
ret
end
diff --git a/lib/webrick/httpservlet.rb b/lib/webrick/httpservlet.rb
index da49a140..4cb1822c 100644
--- a/lib/webrick/httpservlet.rb
+++ b/lib/webrick/httpservlet.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# httpservlet.rb -- HTTPServlet Utility File
#
diff --git a/lib/webrick/httpservlet/abstract.rb b/lib/webrick/httpservlet/abstract.rb
index bccb0918..6fae4de9 100644
--- a/lib/webrick/httpservlet/abstract.rb
+++ b/lib/webrick/httpservlet/abstract.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# httpservlet.rb -- HTTPServlet Module
#
diff --git a/lib/webrick/httpservlet/cgi_runner.rb b/lib/webrick/httpservlet/cgi_runner.rb
index 0398c167..b0948ae3 100644
--- a/lib/webrick/httpservlet/cgi_runner.rb
+++ b/lib/webrick/httpservlet/cgi_runner.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# cgi_runner.rb -- CGI launcher.
#
@@ -10,7 +10,7 @@
# $IPR: cgi_runner.rb,v 1.9 2002/09/25 11:33:15 gotoyuzo Exp $
def sysread(io, size)
- buf = ""
+ buf = +""
while size > 0
tmp = io.sysread(size)
buf << tmp
diff --git a/lib/webrick/httpservlet/cgihandler.rb b/lib/webrick/httpservlet/cgihandler.rb
index 4457770b..450aa380 100644
--- a/lib/webrick/httpservlet/cgihandler.rb
+++ b/lib/webrick/httpservlet/cgihandler.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# cgihandler.rb -- CGIHandler Class
#
diff --git a/lib/webrick/httpservlet/erbhandler.rb b/lib/webrick/httpservlet/erbhandler.rb
index cd09e5f2..3b232f7f 100644
--- a/lib/webrick/httpservlet/erbhandler.rb
+++ b/lib/webrick/httpservlet/erbhandler.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# erbhandler.rb -- ERBHandler Class
#
diff --git a/lib/webrick/httpservlet/filehandler.rb b/lib/webrick/httpservlet/filehandler.rb
index 010df0e9..7ab88bca 100644
--- a/lib/webrick/httpservlet/filehandler.rb
+++ b/lib/webrick/httpservlet/filehandler.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# filehandler.rb -- FileHandler Module
#
@@ -481,9 +481,9 @@ def set_dir_list(req, res)
elsif !namewidth or (namewidth = namewidth.to_i) < 2
namewidth = 25
end
- query = query.inject('') {|s, (k, v)| s << '&' << HTMLUtils::escape("#{k}=#{v}")}
+ query = query.inject('') {|s, (k, v)| s << '&' << HTMLUtils::escape("#{k}=#{v}")}.dup
- type = "text/html"
+ type = +"text/html"
case enc = Encoding.find('filesystem')
when Encoding::US_ASCII, Encoding::ASCII_8BIT
else
@@ -492,7 +492,7 @@ def set_dir_list(req, res)
res['content-type'] = type
title = "Index of #{HTMLUtils::escape(req.path)}"
- res.body = <<-_end_of_html_
+ res.body = +<<-_end_of_html_
@@ -528,7 +528,7 @@ def set_dir_list(req, res)
else
dname = name
end
- s = "#{HTMLUtils::escape(dname)} | "
+ s = +"
#{HTMLUtils::escape(dname)} | "
s << "" << (time ? time.strftime("%Y/%m/%d %H:%M") : "") << " | "
s << "" << (size >= 0 ? size.to_s : "-") << " |
\n"
res.body << s
diff --git a/lib/webrick/httpservlet/prochandler.rb b/lib/webrick/httpservlet/prochandler.rb
index 599ffc43..92e4f80d 100644
--- a/lib/webrick/httpservlet/prochandler.rb
+++ b/lib/webrick/httpservlet/prochandler.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# prochandler.rb -- ProcHandler Class
#
@@ -40,6 +40,7 @@ def do_GET(request, response)
end
alias do_POST do_GET
+ alias do_PUT do_GET
# :startdoc:
end
diff --git a/lib/webrick/httpstatus.rb b/lib/webrick/httpstatus.rb
index c811f219..c21c1d46 100644
--- a/lib/webrick/httpstatus.rb
+++ b/lib/webrick/httpstatus.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#--
# httpstatus.rb -- HTTPStatus Class
#
diff --git a/lib/webrick/httputils.rb b/lib/webrick/httputils.rb
index f1b9ddf9..92f3044d 100644
--- a/lib/webrick/httputils.rb
+++ b/lib/webrick/httputils.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# httputils.rb -- HTTPUtils Module
#
@@ -48,13 +48,13 @@ def normalize_path(path)
"ai" => "application/postscript",
"asc" => "text/plain",
"avi" => "video/x-msvideo",
+ "avif" => "image/avif",
"bin" => "application/octet-stream",
"bmp" => "image/bmp",
"class" => "application/octet-stream",
"cer" => "application/pkix-cert",
"crl" => "application/pkix-crl",
"crt" => "application/x-x509-ca-cert",
- #"crl" => "application/x-pkcs7-crl",
"css" => "text/css",
"dms" => "application/octet-stream",
"doc" => "application/msword",
@@ -65,6 +65,7 @@ def normalize_path(path)
"gif" => "image/gif",
"htm" => "text/html",
"html" => "text/html",
+ "ico" => "image/x-icon",
"jpe" => "image/jpeg",
"jpeg" => "image/jpeg",
"jpg" => "image/jpeg",
@@ -74,9 +75,11 @@ def normalize_path(path)
"lzh" => "application/octet-stream",
"mjs" => "application/javascript",
"mov" => "video/quicktime",
+ "mp4" => "video/mp4",
"mpe" => "video/mpeg",
"mpeg" => "video/mpeg",
"mpg" => "video/mpeg",
+ "otf" => "font/otf",
"pbm" => "image/x-portable-bitmap",
"pdf" => "application/pdf",
"pgm" => "image/x-portable-graymap",
@@ -95,8 +98,15 @@ def normalize_path(path)
"svg" => "image/svg+xml",
"tif" => "image/tiff",
"tiff" => "image/tiff",
+ "ttc" => "font/collection",
+ "ttf" => "font/ttf",
"txt" => "text/plain",
"wasm" => "application/wasm",
+ "webm" => "video/webm",
+ "webmanifest" => "application/manifest+json",
+ "webp" => "image/webp",
+ "woff" => "font/woff",
+ "woff2" => "font/woff2",
"xbm" => "image/x-xbitmap",
"xhtml" => "text/html",
"xls" => "application/vnd.ms-excel",
@@ -112,7 +122,7 @@ def normalize_path(path)
def load_mime_types(file)
# note: +file+ may be a "| command" for now; some people may
# rely on this, but currently we do not use this method by default.
- open(file){ |io|
+ File.open(file){ |io|
hash = Hash.new
io.each{ |line|
next if /^#/ =~ line
@@ -142,28 +152,49 @@ def mime_type(filename, mime_tab)
# Parses an HTTP header +raw+ into a hash of header fields with an Array
# of values.
+ class SplitHeader < Array
+ def join(separator = ", ")
+ super
+ end
+ end
+
+ class CookieHeader < Array
+ def join(separator = "; ")
+ super
+ end
+ end
+
+ HEADER_CLASSES = Hash.new(SplitHeader).update({
+ "cookie" => CookieHeader,
+ })
+
def parse_header(raw)
header = Hash.new([].freeze)
field = nil
raw.each_line{|line|
case line
- when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):\s*(.*?)\s*\z/om
+ when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):([^\r\n\0]*?)\r\n\z/om
field, value = $1, $2
field.downcase!
- header[field] = [] unless header.has_key?(field)
+ header[field] = HEADER_CLASSES[field].new unless header.has_key?(field)
header[field] << value
- when /^\s+(.*?)\s*\z/om
- value = $1
+ when /^[ \t]+([^\r\n\0]*?)\r\n/om
unless field
raise HTTPStatus::BadRequest, "bad header '#{line}'."
end
+ value = line
+ value.gsub!(/\A[ \t]+/, '')
+ value.slice!(-2..-1)
header[field][-1] << " " << value
else
raise HTTPStatus::BadRequest, "bad header '#{line}'."
end
}
header.each{|key, values|
- values.each(&:strip!)
+ values.each{|value|
+ value.gsub!(/\A[ \t]+/, '')
+ value.gsub!(/[ \t]+\z/, '')
+ }
}
header
end
@@ -173,8 +204,8 @@ def parse_header(raw)
# Splits a header value +str+ according to HTTP specification.
def split_header_value(str)
- str.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",]+)+)
- (?:,\s*|\Z)'xn).flatten
+ str.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",]++)+)
+ (?:,[ \t]*|\Z)'xn).flatten
end
module_function :split_header_value
@@ -202,9 +233,9 @@ def parse_range_header(ranges_specifier)
def parse_qvalues(value)
tmp = []
if value
- parts = value.split(/,\s*/)
+ parts = value.split(/,[ \t]*/)
parts.each {|part|
- if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
+ if m = %r{^([^ \t,]+?)(?:;[ \t]*q=(\d+(?:\.\d+)?))?$}.match(part)
val = m[1]
q = (m[2] or 1).to_f
tmp.push([val, q])
@@ -231,7 +262,7 @@ def dequote(str)
# Quotes and escapes quotes in +str+
def quote(str)
- '"' << str.gsub(/[\\\"]/o, "\\\1") << '"'
+ +'"' << str.gsub(/[\\\"]/o, "\\\1") << '"'
end
module_function :quote
@@ -303,8 +334,8 @@ def <<(str)
elsif str == CRLF
@header = HTTPUtils::parse_header(@raw_header.join)
if cd = self['content-disposition']
- if /\s+name="(.*?)"/ =~ cd then @name = $1 end
- if /\s+filename="(.*?)"/ =~ cd then @filename = $1 end
+ if /[ \t]+name="(.*?)"/ =~ cd then @name = $1 end
+ if /[ \t]+filename="(.*?)"/ =~ cd then @filename = $1 end
end
else
@raw_header << str
@@ -495,7 +526,7 @@ def unescape_form(str)
# Escapes path +str+
def escape_path(str)
- result = ""
+ result = +""
str.scan(%r{/([^/]*)}).each{|i|
result << "/" << _escape(i[0], UNESCAPED_PCHAR)
}
diff --git a/lib/webrick/httpversion.rb b/lib/webrick/httpversion.rb
index 8a251944..63495903 100644
--- a/lib/webrick/httpversion.rb
+++ b/lib/webrick/httpversion.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#--
# HTTPVersion.rb -- presentation of HTTP version
#
diff --git a/lib/webrick/log.rb b/lib/webrick/log.rb
index 2c1fdfe6..4de4bee1 100644
--- a/lib/webrick/log.rb
+++ b/lib/webrick/log.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#--
# log.rb -- Log Class
#
@@ -86,15 +86,15 @@ def <<(obj)
end
# Shortcut for logging a FATAL message
- def fatal(msg) log(FATAL, "FATAL " << format(msg)); end
+ def fatal(msg) log(FATAL, "FATAL " + format(msg)); end
# Shortcut for logging an ERROR message
- def error(msg) log(ERROR, "ERROR " << format(msg)); end
+ def error(msg) log(ERROR, "ERROR " + format(msg)); end
# Shortcut for logging a WARN message
- def warn(msg) log(WARN, "WARN " << format(msg)); end
+ def warn(msg) log(WARN, "WARN " + format(msg)); end
# Shortcut for logging an INFO message
- def info(msg) log(INFO, "INFO " << format(msg)); end
+ def info(msg) log(INFO, "INFO " + format(msg)); end
# Shortcut for logging a DEBUG message
- def debug(msg) log(DEBUG, "DEBUG " << format(msg)); end
+ def debug(msg) log(DEBUG, "DEBUG " + format(msg)); end
# Will the logger output FATAL messages?
def fatal?; @level >= FATAL; end
@@ -118,7 +118,7 @@ def debug?; @level >= DEBUG; end
# * Otherwise it will return +arg+.inspect.
def format(arg)
if arg.is_a?(Exception)
- "#{arg.class}: #{AccessLog.escape(arg.message)}\n\t" <<
+ +"#{arg.class}: #{AccessLog.escape(arg.message)}\n\t" <<
arg.backtrace.join("\n\t") << "\n"
elsif arg.respond_to?(:to_str)
AccessLog.escape(arg.to_str)
diff --git a/lib/webrick/server.rb b/lib/webrick/server.rb
index fd6b7a61..f085d5d2 100644
--- a/lib/webrick/server.rb
+++ b/lib/webrick/server.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# server.rb -- GenericServer Class
#
@@ -102,7 +102,7 @@ def initialize(config={}, default=Config::General)
@listeners = []
@shutdown_pipe = nil
unless @config[:DoNotListen]
- raise ArgumentError, "Port must an integer" unless @config[:Port].to_s == @config[:Port].to_i.to_s
+ raise ArgumentError, "Port must be an integer" unless @config[:Port].to_s == @config[:Port].to_i.to_s
@config[:Port] = @config[:Port].to_i
if @config[:Listen]
diff --git a/lib/webrick/ssl.rb b/lib/webrick/ssl.rb
index e448095a..6937f93b 100644
--- a/lib/webrick/ssl.rb
+++ b/lib/webrick/ssl.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# ssl.rb -- SSL/TLS enhancement for GenericServer
#
@@ -95,18 +95,22 @@ module Utils
# the issuer +cn+ and a +comment+ to be stored in the certificate.
def create_self_signed_cert(bits, cn, comment)
- rsa = OpenSSL::PKey::RSA.new(bits){|p, n|
- case p
- when 0; $stderr.putc "." # BN_generate_prime
- when 1; $stderr.putc "+" # BN_generate_prime
- when 2; $stderr.putc "*" # searching good prime,
- # n = #of try,
- # but also data from BN_generate_prime
- when 3; $stderr.putc "\n" # found good prime, n==0 - p, n==1 - q,
- # but also data from BN_generate_prime
- else; $stderr.putc "*" # BN_generate_prime
- end
- }
+ rsa = if $VERBOSE
+ OpenSSL::PKey::RSA.new(bits){|p, n|
+ case p
+ when 0; $stderr.putc "." # BN_generate_prime
+ when 1; $stderr.putc "+" # BN_generate_prime
+ when 2; $stderr.putc "*" # searching good prime,
+ # n = #of try,
+ # but also data from BN_generate_prime
+ when 3; $stderr.putc "\n" # found good prime, n==0 - p, n==1 - q,
+ # but also data from BN_generate_prime
+ else; $stderr.putc "*" # BN_generate_prime
+ end
+ }
+ else
+ OpenSSL::PKey::RSA.new(bits)
+ end
cert = OpenSSL::X509::Certificate.new
cert.version = 2
cert.serial = 1
diff --git a/lib/webrick/utils.rb b/lib/webrick/utils.rb
index a96d6f03..e4902d0b 100644
--- a/lib/webrick/utils.rb
+++ b/lib/webrick/utils.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# utils.rb -- Miscellaneous utilities
#
@@ -78,7 +78,7 @@ def create_listeners(address, port)
# Generates a random string of length +len+
def random_string(len)
rand_max = RAND_CHARS.bytesize
- ret = ""
+ ret = +""
len.times{ ret << RAND_CHARS[rand(rand_max)] }
ret
end
diff --git a/lib/webrick/version.rb b/lib/webrick/version.rb
index b62988bd..fbea0dda 100644
--- a/lib/webrick/version.rb
+++ b/lib/webrick/version.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#--
# version.rb -- version and release date
#
@@ -14,5 +14,5 @@ module WEBrick
##
# The WEBrick version
- VERSION = "1.7.0"
+ VERSION = "1.8.2"
end
diff --git a/test/lib/envutil.rb b/test/lib/envutil.rb
deleted file mode 100644
index d76ed660..00000000
--- a/test/lib/envutil.rb
+++ /dev/null
@@ -1,295 +0,0 @@
-# -*- coding: us-ascii -*-
-# frozen_string_literal: true
-require "open3"
-require "timeout"
-require_relative "find_executable"
-begin
- require 'rbconfig'
-rescue LoadError
-end
-begin
- require "rbconfig/sizeof"
-rescue LoadError
-end
-
-module EnvUtil
- def rubybin
- if ruby = ENV["RUBY"]
- return ruby
- end
- ruby = "ruby"
- exeext = RbConfig::CONFIG["EXEEXT"]
- rubyexe = (ruby + exeext if exeext and !exeext.empty?)
- 3.times do
- if File.exist? ruby and File.executable? ruby and !File.directory? ruby
- return File.expand_path(ruby)
- end
- if rubyexe and File.exist? rubyexe and File.executable? rubyexe
- return File.expand_path(rubyexe)
- end
- ruby = File.join("..", ruby)
- end
- if defined?(RbConfig.ruby)
- RbConfig.ruby
- else
- "ruby"
- end
- end
- module_function :rubybin
-
- LANG_ENVS = %w"LANG LC_ALL LC_CTYPE"
-
- DEFAULT_SIGNALS = Signal.list
- DEFAULT_SIGNALS.delete("TERM") if /mswin|mingw/ =~ RUBY_PLATFORM
-
- class << self
- attr_accessor :subprocess_timeout_scale
- end
-
- def apply_timeout_scale(t)
- if scale = EnvUtil.subprocess_timeout_scale
- t * scale
- else
- t
- end
- end
- module_function :apply_timeout_scale
-
- def timeout(sec, klass = nil, message = nil, &blk)
- return yield(sec) if sec == nil or sec.zero?
- sec = apply_timeout_scale(sec)
- Timeout.timeout(sec, klass, message, &blk)
- end
- module_function :timeout
-
- def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = false,
- encoding: nil, timeout: 10, reprieve: 1, timeout_error: Timeout::Error,
- stdout_filter: nil, stderr_filter: nil,
- signal: :TERM,
- rubybin: EnvUtil.rubybin,
- **opt)
- timeout = apply_timeout_scale(timeout)
- reprieve = apply_timeout_scale(reprieve) if reprieve
-
- in_c, in_p = IO.pipe
- out_p, out_c = IO.pipe if capture_stdout
- err_p, err_c = IO.pipe if capture_stderr && capture_stderr != :merge_to_stdout
- opt[:in] = in_c
- opt[:out] = out_c if capture_stdout
- opt[:err] = capture_stderr == :merge_to_stdout ? out_c : err_c if capture_stderr
- if encoding
- out_p.set_encoding(encoding) if out_p
- err_p.set_encoding(encoding) if err_p
- end
- c = "C"
- child_env = {}
- LANG_ENVS.each {|lc| child_env[lc] = c}
- if Array === args and Hash === args.first
- child_env.update(args.shift)
- end
- args = [args] if args.kind_of?(String)
- pid = spawn(child_env, rubybin, *args, **opt)
- in_c.close
- out_c.close if capture_stdout
- err_c.close if capture_stderr && capture_stderr != :merge_to_stdout
- if block_given?
- return yield in_p, out_p, err_p, pid
- else
- th_stdout = Thread.new { out_p.read } if capture_stdout
- th_stderr = Thread.new { err_p.read } if capture_stderr && capture_stderr != :merge_to_stdout
- in_p.write stdin_data.to_str unless stdin_data.empty?
- in_p.close
- if (!th_stdout || th_stdout.join(timeout)) && (!th_stderr || th_stderr.join(timeout))
- timeout_error = nil
- else
- signals = Array(signal).select do |sig|
- DEFAULT_SIGNALS[sig.to_s] or
- DEFAULT_SIGNALS[Signal.signame(sig)] rescue false
- end
- signals |= [:ABRT, :KILL]
- case pgroup = opt[:pgroup]
- when 0, true
- pgroup = -pid
- when nil, false
- pgroup = pid
- end
- while signal = signals.shift
- begin
- Process.kill signal, pgroup
- rescue Errno::EINVAL
- next
- rescue Errno::ESRCH
- break
- end
- if signals.empty? or !reprieve
- Process.wait(pid)
- else
- begin
- Timeout.timeout(reprieve) {Process.wait(pid)}
- rescue Timeout::Error
- end
- end
- end
- status = $?
- end
- stdout = th_stdout.value if capture_stdout
- stderr = th_stderr.value if capture_stderr && capture_stderr != :merge_to_stdout
- out_p.close if capture_stdout
- err_p.close if capture_stderr && capture_stderr != :merge_to_stdout
- status ||= Process.wait2(pid)[1]
- stdout = stdout_filter.call(stdout) if stdout_filter
- stderr = stderr_filter.call(stderr) if stderr_filter
- if timeout_error
- bt = caller_locations
- msg = "execution of #{bt.shift.label} expired"
- msg = Test::Unit::Assertions::FailDesc[status, msg, [stdout, stderr].join("\n")].()
- raise timeout_error, msg, bt.map(&:to_s)
- end
- return stdout, stderr, status
- end
- ensure
- [th_stdout, th_stderr].each do |th|
- th.kill if th
- end
- [in_c, in_p, out_c, out_p, err_c, err_p].each do |io|
- io&.close
- end
- [th_stdout, th_stderr].each do |th|
- th.join if th
- end
- end
- module_function :invoke_ruby
-
- alias rubyexec invoke_ruby
- class << self
- alias rubyexec invoke_ruby
- end
-
- def verbose_warning
- class << (stderr = "".dup)
- alias write <<
- end
- stderr, $stderr, verbose, $VERBOSE = $stderr, stderr, $VERBOSE, true
- yield stderr
- return $stderr
- ensure
- stderr, $stderr, $VERBOSE = $stderr, stderr, verbose
- end
- module_function :verbose_warning
-
- def default_warning
- verbose, $VERBOSE = $VERBOSE, false
- yield
- ensure
- $VERBOSE = verbose
- end
- module_function :default_warning
-
- def suppress_warning
- verbose, $VERBOSE = $VERBOSE, nil
- yield
- ensure
- $VERBOSE = verbose
- end
- module_function :suppress_warning
-
- def under_gc_stress(stress = true)
- stress, GC.stress = GC.stress, stress
- yield
- ensure
- GC.stress = stress
- end
- module_function :under_gc_stress
-
- def with_default_external(enc)
- verbose, $VERBOSE = $VERBOSE, nil
- origenc, Encoding.default_external = Encoding.default_external, enc
- $VERBOSE = verbose
- yield
- ensure
- verbose, $VERBOSE = $VERBOSE, nil
- Encoding.default_external = origenc
- $VERBOSE = verbose
- end
- module_function :with_default_external
-
- def with_default_internal(enc)
- verbose, $VERBOSE = $VERBOSE, nil
- origenc, Encoding.default_internal = Encoding.default_internal, enc
- $VERBOSE = verbose
- yield
- ensure
- verbose, $VERBOSE = $VERBOSE, nil
- Encoding.default_internal = origenc
- $VERBOSE = verbose
- end
- module_function :with_default_internal
-
- def labeled_module(name, &block)
- Module.new do
- singleton_class.class_eval {define_method(:to_s) {name}; alias inspect to_s}
- class_eval(&block) if block
- end
- end
- module_function :labeled_module
-
- def labeled_class(name, superclass = Object, &block)
- Class.new(superclass) do
- singleton_class.class_eval {define_method(:to_s) {name}; alias inspect to_s}
- class_eval(&block) if block
- end
- end
- module_function :labeled_class
-
- if /darwin/ =~ RUBY_PLATFORM
- DIAGNOSTIC_REPORTS_PATH = File.expand_path("~/Library/Logs/DiagnosticReports")
- DIAGNOSTIC_REPORTS_TIMEFORMAT = '%Y-%m-%d-%H%M%S'
- @ruby_install_name = RbConfig::CONFIG['RUBY_INSTALL_NAME']
-
- def self.diagnostic_reports(signame, pid, now)
- return unless %w[ABRT QUIT SEGV ILL TRAP].include?(signame)
- cmd = File.basename(rubybin)
- cmd = @ruby_install_name if "ruby-runner#{RbConfig::CONFIG["EXEEXT"]}" == cmd
- path = DIAGNOSTIC_REPORTS_PATH
- timeformat = DIAGNOSTIC_REPORTS_TIMEFORMAT
- pat = "#{path}/#{cmd}_#{now.strftime(timeformat)}[-_]*.crash"
- first = true
- 30.times do
- first ? (first = false) : sleep(0.1)
- Dir.glob(pat) do |name|
- log = File.read(name) rescue next
- if /\AProcess:\s+#{cmd} \[#{pid}\]$/ =~ log
- File.unlink(name)
- File.unlink("#{path}/.#{File.basename(name)}.plist") rescue nil
- return log
- end
- end
- end
- nil
- end
- else
- def self.diagnostic_reports(signame, pid, now)
- end
- end
-
- def self.gc_stress_to_class?
- unless defined?(@gc_stress_to_class)
- _, _, status = invoke_ruby(["-e""exit GC.respond_to?(:add_stress_to_class)"])
- @gc_stress_to_class = status.success?
- end
- @gc_stress_to_class
- end
-end
-
-if defined?(RbConfig)
- module RbConfig
- @ruby = EnvUtil.rubybin
- class << self
- undef ruby if method_defined?(:ruby)
- attr_reader :ruby
- end
- dir = File.dirname(ruby)
- CONFIG['bindir'] = dir
- Gem::ConfigMap[:bindir] = dir if defined?(Gem::ConfigMap)
- end
-end
diff --git a/test/lib/find_executable.rb b/test/lib/find_executable.rb
deleted file mode 100644
index 89c6fb8f..00000000
--- a/test/lib/find_executable.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-require "rbconfig"
-
-module EnvUtil
- def find_executable(cmd, *args)
- exts = RbConfig::CONFIG["EXECUTABLE_EXTS"].split | [RbConfig::CONFIG["EXEEXT"]]
- ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
- next if path.empty?
- path = File.join(path, cmd)
- exts.each do |ext|
- cmdline = [path + ext, *args]
- begin
- return cmdline if yield(IO.popen(cmdline, "r", err: [:child, :out], &:read))
- rescue
- next
- end
- end
- end
- nil
- end
- module_function :find_executable
-end
diff --git a/test/lib/helper.rb b/test/lib/helper.rb
new file mode 100644
index 00000000..53fe62b0
--- /dev/null
+++ b/test/lib/helper.rb
@@ -0,0 +1,9 @@
+require "test/unit"
+require "core_assertions"
+
+Test::Unit::TestCase.include Test::Unit::CoreAssertions
+
+module TestWEBrick
+ include Test::Unit::Util::Output
+ extend Test::Unit::Util::Output
+end
diff --git a/test/lib/leakchecker.rb b/test/lib/leakchecker.rb
deleted file mode 100644
index be4b8368..00000000
--- a/test/lib/leakchecker.rb
+++ /dev/null
@@ -1,203 +0,0 @@
-# frozen_string_literal: true
-class LeakChecker
- def initialize
- @fd_info = find_fds
- @tempfile_info = find_tempfiles
- @thread_info = find_threads
- @env_info = find_env
- end
-
- def check(test_name)
- leaks = [
- check_fd_leak(test_name),
- check_thread_leak(test_name),
- check_tempfile_leak(test_name),
- check_env(test_name)
- ]
- GC.start if leaks.any?
- end
-
- def find_fds
- if IO.respond_to?(:console) and (m = IO.method(:console)).arity.nonzero?
- m[:close]
- end
- fd_dir = "/proc/self/fd"
- if File.directory?(fd_dir)
- fds = Dir.open(fd_dir) {|d|
- a = d.grep(/\A\d+\z/, &:to_i)
- if d.respond_to? :fileno
- a -= [d.fileno]
- end
- a
- }
- fds.sort
- else
- []
- end
- end
-
- def check_fd_leak(test_name)
- leaked = false
- live1 = @fd_info
- live2 = find_fds
- fd_closed = live1 - live2
- if !fd_closed.empty?
- fd_closed.each {|fd|
- puts "Closed file descriptor: #{test_name}: #{fd}"
- }
- end
- fd_leaked = live2 - live1
- if !fd_leaked.empty?
- leaked = true
- h = {}
- ObjectSpace.each_object(IO) {|io|
- inspect = io.inspect
- begin
- autoclose = io.autoclose?
- fd = io.fileno
- rescue IOError # closed IO object
- next
- end
- (h[fd] ||= []) << [io, autoclose, inspect]
- }
- fd_leaked.each {|fd|
- str = ''.dup
- if h[fd]
- str << ' :'
- h[fd].map {|io, autoclose, inspect|
- s = ' ' + inspect
- s << "(not-autoclose)" if !autoclose
- s
- }.sort.each {|s|
- str << s
- }
- end
- puts "Leaked file descriptor: #{test_name}: #{fd}#{str}"
- }
- #system("lsof -p #$$") if !fd_leaked.empty?
- h.each {|fd, list|
- next if list.length <= 1
- if 1 < list.count {|io, autoclose, inspect| autoclose }
- str = list.map {|io, autoclose, inspect| " #{inspect}" + (autoclose ? "(autoclose)" : "") }.sort.join
- puts "Multiple autoclose IO object for a file descriptor:#{str}"
- end
- }
- end
- @fd_info = live2
- return leaked
- end
-
- def extend_tempfile_counter
- return if defined? LeakChecker::TempfileCounter
- m = Module.new {
- @count = 0
- class << self
- attr_accessor :count
- end
-
- def new(data)
- LeakChecker::TempfileCounter.count += 1
- super(data)
- end
- }
- LeakChecker.const_set(:TempfileCounter, m)
-
- class << Tempfile::Remover
- prepend LeakChecker::TempfileCounter
- end
- end
-
- def find_tempfiles(prev_count=-1)
- return [prev_count, []] unless defined? Tempfile
- extend_tempfile_counter
- count = TempfileCounter.count
- if prev_count == count
- [prev_count, []]
- else
- tempfiles = ObjectSpace.each_object(Tempfile).find_all {|t|
- t.instance_variable_defined?(:@tmpfile) and t.path
- }
- [count, tempfiles]
- end
- end
-
- def check_tempfile_leak(test_name)
- return false unless defined? Tempfile
- count1, initial_tempfiles = @tempfile_info
- count2, current_tempfiles = find_tempfiles(count1)
- leaked = false
- tempfiles_leaked = current_tempfiles - initial_tempfiles
- if !tempfiles_leaked.empty?
- leaked = true
- list = tempfiles_leaked.map {|t| t.inspect }.sort
- list.each {|str|
- puts "Leaked tempfile: #{test_name}: #{str}"
- }
- tempfiles_leaked.each {|t| t.close! }
- end
- @tempfile_info = [count2, initial_tempfiles]
- return leaked
- end
-
- def find_threads
- Thread.list.find_all {|t|
- t != Thread.current && t.alive?
- }
- end
-
- def check_thread_leak(test_name)
- live1 = @thread_info
- live2 = find_threads
- thread_finished = live1 - live2
- leaked = false
- if !thread_finished.empty?
- list = thread_finished.map {|t| t.inspect }.sort
- list.each {|str|
- puts "Finished thread: #{test_name}: #{str}"
- }
- end
- thread_leaked = live2 - live1
- if !thread_leaked.empty?
- leaked = true
- list = thread_leaked.map {|t| t.inspect }.sort
- list.each {|str|
- puts "Leaked thread: #{test_name}: #{str}"
- }
- end
- @thread_info = live2
- return leaked
- end
-
- def find_env
- ENV.to_h
- end
-
- def check_env(test_name)
- old_env = @env_info
- new_env = ENV.to_h
- return false if old_env == new_env
- (old_env.keys | new_env.keys).sort.each {|k|
- if old_env.has_key?(k)
- if new_env.has_key?(k)
- if old_env[k] != new_env[k]
- puts "Environment variable changed: #{test_name} : #{k.inspect} changed : #{old_env[k].inspect} -> #{new_env[k].inspect}"
- end
- else
- puts "Environment variable changed: #{test_name} : #{k.inspect} deleted"
- end
- else
- if new_env.has_key?(k)
- puts "Environment variable changed: #{test_name} : #{k.inspect} added"
- else
- flunk "unreachable"
- end
- end
- }
- @env_info = new_env
- return true
- end
-
- def puts(*a)
- MiniTest::Unit.output.puts(*a)
- end
-end
diff --git a/test/lib/minitest/autorun.rb b/test/lib/minitest/autorun.rb
deleted file mode 100644
index 84409662..00000000
--- a/test/lib/minitest/autorun.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# encoding: utf-8
-# frozen_string_literal: true
-
-begin
- require 'rubygems'
- gem 'minitest'
-rescue Gem::LoadError
- # do nothing
-end
-
-require 'minitest/unit'
-require 'minitest/mock'
-
-MiniTest::Unit.autorun
diff --git a/test/lib/minitest/mock.rb b/test/lib/minitest/mock.rb
deleted file mode 100644
index 224b06cb..00000000
--- a/test/lib/minitest/mock.rb
+++ /dev/null
@@ -1,196 +0,0 @@
-# encoding: utf-8
-# frozen_string_literal: true
-
-class MockExpectationError < StandardError; end # :nodoc:
-
-##
-# A simple and clean mock object framework.
-
-module MiniTest # :nodoc:
-
- ##
- # All mock objects are an instance of Mock
-
- class Mock
- alias :__respond_to? :respond_to?
-
- skip_methods = %w(object_id respond_to_missing? inspect === to_s)
-
- instance_methods.each do |m|
- undef_method m unless skip_methods.include?(m.to_s) || m =~ /^__/
- end
-
- def initialize # :nodoc:
- @expected_calls = Hash.new { |calls, name| calls[name] = [] }
- @actual_calls = Hash.new { |calls, name| calls[name] = [] }
- end
-
- ##
- # Expect that method +name+ is called, optionally with +args+ or a
- # +blk+, and returns +retval+.
- #
- # @mock.expect(:meaning_of_life, 42)
- # @mock.meaning_of_life # => 42
- #
- # @mock.expect(:do_something_with, true, [some_obj, true])
- # @mock.do_something_with(some_obj, true) # => true
- #
- # @mock.expect(:do_something_else, true) do |a1, a2|
- # a1 == "buggs" && a2 == :bunny
- # end
- #
- # +args+ is compared to the expected args using case equality (ie, the
- # '===' operator), allowing for less specific expectations.
- #
- # @mock.expect(:uses_any_string, true, [String])
- # @mock.uses_any_string("foo") # => true
- # @mock.verify # => true
- #
- # @mock.expect(:uses_one_string, true, ["foo"]
- # @mock.uses_one_string("bar") # => true
- # @mock.verify # => raises MockExpectationError
-
- def expect(name, retval, args=[], &blk)
- if block_given?
- raise ArgumentError, "args ignored when block given" unless args.empty?
- @expected_calls[name] << { :retval => retval, :block => blk }
- else
- raise ArgumentError, "args must be an array" unless Array === args
- @expected_calls[name] << { :retval => retval, :args => args }
- end
- self
- end
-
- def __call name, data # :nodoc:
- case data
- when Hash then
- "#{name}(#{data[:args].inspect[1..-2]}) => #{data[:retval].inspect}"
- else
- data.map { |d| __call name, d }.join ", "
- end
- end
-
- ##
- # Verify that all methods were called as expected. Raises
- # +MockExpectationError+ if the mock object was not called as
- # expected.
-
- def verify
- @expected_calls.each do |name, calls|
- calls.each do |expected|
- msg1 = "expected #{__call name, expected}"
- msg2 = "#{msg1}, got [#{__call name, @actual_calls[name]}]"
-
- raise MockExpectationError, msg2 if
- @actual_calls.has_key?(name) and
- not @actual_calls[name].include?(expected)
-
- raise MockExpectationError, msg1 unless
- @actual_calls.has_key?(name) and
- @actual_calls[name].include?(expected)
- end
- end
- true
- end
-
- def method_missing(sym, *args) # :nodoc:
- unless @expected_calls.has_key?(sym) then
- raise NoMethodError, "unmocked method %p, expected one of %p" %
- [sym, @expected_calls.keys.sort_by(&:to_s)]
- end
-
- index = @actual_calls[sym].length
- expected_call = @expected_calls[sym][index]
-
- unless expected_call then
- raise MockExpectationError, "No more expects available for %p: %p" %
- [sym, args]
- end
-
- expected_args, retval, val_block =
- expected_call.values_at(:args, :retval, :block)
-
- if val_block then
- raise MockExpectationError, "mocked method %p failed block w/ %p" %
- [sym, args] unless val_block.call(args)
-
- # keep "verify" happy
- @actual_calls[sym] << expected_call
- return retval
- end
-
- if expected_args.size != args.size then
- raise ArgumentError, "mocked method %p expects %d arguments, got %d" %
- [sym, expected_args.size, args.size]
- end
-
- fully_matched = expected_args.zip(args).all? { |mod, a|
- mod === a or mod == a
- }
-
- unless fully_matched then
- raise MockExpectationError, "mocked method %p called with unexpected arguments %p" %
- [sym, args]
- end
-
- @actual_calls[sym] << {
- :retval => retval,
- :args => expected_args.zip(args).map { |mod, a| mod === a ? mod : a }
- }
-
- retval
- end
-
- def respond_to?(sym, include_private = false) # :nodoc:
- return true if @expected_calls.has_key?(sym.to_sym)
- return __respond_to?(sym, include_private)
- end
- end
-end
-
-class Object # :nodoc:
-
- ##
- # Add a temporary stubbed method replacing +name+ for the duration
- # of the +block+. If +val_or_callable+ responds to #call, then it
- # returns the result of calling it, otherwise returns the value
- # as-is. Cleans up the stub at the end of the +block+. The method
- # +name+ must exist before stubbing.
- #
- # def test_stale_eh
- # obj_under_test = Something.new
- # refute obj_under_test.stale?
- #
- # Time.stub :now, Time.at(0) do
- # assert obj_under_test.stale?
- # end
- # end
-
- def stub name, val_or_callable, &block
- new_name = "__minitest_stub__#{name}"
-
- metaclass = class << self; self; end
-
- if respond_to? name and not methods.map(&:to_s).include? name.to_s then
- metaclass.send :define_method, name do |*args|
- super(*args)
- end
- end
-
- metaclass.send :alias_method, new_name, name
-
- metaclass.send :define_method, name do |*args|
- if val_or_callable.respond_to? :call then
- val_or_callable.call(*args)
- else
- val_or_callable
- end
- end
-
- yield self
- ensure
- metaclass.send :undef_method, name
- metaclass.send :alias_method, name, new_name
- metaclass.send :undef_method, new_name
- end
-end
diff --git a/test/lib/minitest/unit.rb b/test/lib/minitest/unit.rb
deleted file mode 100644
index 672fd152..00000000
--- a/test/lib/minitest/unit.rb
+++ /dev/null
@@ -1,1417 +0,0 @@
-# encoding: utf-8
-# frozen_string_literal: true
-
-require "optparse"
-require "rbconfig"
-require "leakchecker"
-
-##
-# Minimal (mostly drop-in) replacement for test-unit.
-#
-# :include: README.txt
-
-module MiniTest
-
- def self.const_missing name # :nodoc:
- case name
- when :MINI_DIR then
- msg = "MiniTest::MINI_DIR was removed. Don't violate other's internals."
- warn "WAR\NING: #{msg}"
- warn "WAR\NING: Used by #{caller.first}."
- const_set :MINI_DIR, "bad value"
- else
- super
- end
- end
-
- ##
- # Assertion base class
-
- class Assertion < Exception; end
-
- ##
- # Assertion raised when skipping a test
-
- class Skip < Assertion; end
-
- class << self
- ##
- # Filter object for backtraces.
-
- attr_accessor :backtrace_filter
- end
-
- class BacktraceFilter # :nodoc:
- def filter bt
- return ["No backtrace"] unless bt
-
- new_bt = []
-
- unless $DEBUG then
- bt.each do |line|
- break if line =~ /lib\/minitest/
- new_bt << line
- end
-
- new_bt = bt.reject { |line| line =~ /lib\/minitest/ } if new_bt.empty?
- new_bt = bt.dup if new_bt.empty?
- else
- new_bt = bt.dup
- end
-
- new_bt
- end
- end
-
- self.backtrace_filter = BacktraceFilter.new
-
- def self.filter_backtrace bt # :nodoc:
- backtrace_filter.filter bt
- end
-
- ##
- # MiniTest Assertions. All assertion methods accept a +msg+ which is
- # printed if the assertion fails.
-
- module Assertions
- ##
- # Returns the diff command to use in #diff. Tries to intelligently
- # figure out what diff to use.
-
- def self.diff
- @diff = if (RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ &&
- system("diff.exe", __FILE__, __FILE__)) then
- "diff.exe -u"
- elsif Minitest::Unit::Guard.maglev? then # HACK
- "diff -u"
- elsif system("gdiff", __FILE__, __FILE__)
- "gdiff -u" # solaris and kin suck
- elsif system("diff", __FILE__, __FILE__)
- "diff -u"
- else
- nil
- end unless defined? @diff
-
- @diff
- end
-
- ##
- # Set the diff command to use in #diff.
-
- def self.diff= o
- @diff = o
- end
-
- ##
- # Returns a diff between +exp+ and +act+. If there is no known
- # diff command or if it doesn't make sense to diff the output
- # (single line, short output), then it simply returns a basic
- # comparison between the two.
-
- def diff exp, act
- require "tempfile"
-
- expect = mu_pp_for_diff exp
- butwas = mu_pp_for_diff act
- result = nil
-
- need_to_diff =
- MiniTest::Assertions.diff &&
- (expect.include?("\n") ||
- butwas.include?("\n") ||
- expect.size > 30 ||
- butwas.size > 30 ||
- expect == butwas)
-
- return "Expected: #{mu_pp exp}\n Actual: #{mu_pp act}" unless
- need_to_diff
-
- tempfile_a = nil
- tempfile_b = nil
-
- Tempfile.open("expect") do |a|
- tempfile_a = a
- a.puts expect
- a.flush
-
- Tempfile.open("butwas") do |b|
- tempfile_b = b
- b.puts butwas
- b.flush
-
- result = `#{MiniTest::Assertions.diff} #{a.path} #{b.path}`
- result.sub!(/^\-\-\- .+/, "--- expected")
- result.sub!(/^\+\+\+ .+/, "+++ actual")
-
- if result.empty? then
- klass = exp.class
- result = [
- "No visible difference in the #{klass}#inspect output.\n",
- "You should look at the implementation of #== on ",
- "#{klass} or its members.\n",
- expect,
- ].join
- end
- end
- end
-
- result
- ensure
- tempfile_a.close! if tempfile_a
- tempfile_b.close! if tempfile_b
- end
-
- ##
- # This returns a human-readable version of +obj+. By default
- # #inspect is called. You can override this to use #pretty_print
- # if you want.
-
- def mu_pp obj
- s = obj.inspect
- s = s.encode Encoding.default_external if defined? Encoding
- s
- end
-
- ##
- # This returns a diff-able human-readable version of +obj+. This
- # differs from the regular mu_pp because it expands escaped
- # newlines and makes hex-values generic (like object_ids). This
- # uses mu_pp to do the first pass and then cleans it up.
-
- def mu_pp_for_diff obj
- mu_pp(obj).gsub(/\\n/, "\n").gsub(/:0x[a-fA-F0-9]{4,}/m, ':0xXXXXXX')
- end
-
- def _assertions= n # :nodoc:
- @_assertions = n
- end
-
- def _assertions # :nodoc:
- @_assertions ||= 0
- end
-
- ##
- # Fails unless +test+ is a true value.
-
- def assert test, msg = nil
- msg ||= "Failed assertion, no message given."
- self._assertions += 1
- unless test then
- msg = msg.call if Proc === msg
- raise MiniTest::Assertion, msg
- end
- true
- end
-
- ##
- # Fails unless +obj+ is empty.
-
- def assert_empty obj, msg = nil
- msg = message(msg) { "Expected #{mu_pp(obj)} to be empty" }
- assert_respond_to obj, :empty?
- assert obj.empty?, msg
- end
-
- ##
- # Fails unless exp == act printing the difference between
- # the two, if possible.
- #
- # If there is no visible difference but the assertion fails, you
- # should suspect that your #== is buggy, or your inspect output is
- # missing crucial details.
- #
- # For floats use assert_in_delta.
- #
- # See also: MiniTest::Assertions.diff
-
- def assert_equal exp, act, msg = nil
- msg = message(msg, "") { diff exp, act }
- assert exp == act, msg
- end
-
- ##
- # For comparing Floats. Fails unless +exp+ and +act+ are within +delta+
- # of each other.
- #
- # assert_in_delta Math::PI, (22.0 / 7.0), 0.01
-
- def assert_in_delta exp, act, delta = 0.001, msg = nil
- n = (exp - act).abs
- msg = message(msg) {
- "Expected |#{exp} - #{act}| (#{n}) to be <= #{delta}"
- }
- assert delta >= n, msg
- end
-
- ##
- # For comparing Floats. Fails unless +exp+ and +act+ have a relative
- # error less than +epsilon+.
-
- def assert_in_epsilon a, b, epsilon = 0.001, msg = nil
- assert_in_delta a, b, [a.abs, b.abs].min * epsilon, msg
- end
-
- ##
- # Fails unless +collection+ includes +obj+.
-
- def assert_includes collection, obj, msg = nil
- msg = message(msg) {
- "Expected #{mu_pp(collection)} to include #{mu_pp(obj)}"
- }
- assert_respond_to collection, :include?
- assert collection.include?(obj), msg
- end
-
- ##
- # Fails unless +obj+ is an instance of +cls+.
-
- def assert_instance_of cls, obj, msg = nil
- msg = message(msg) {
- "Expected #{mu_pp(obj)} to be an instance of #{cls}, not #{obj.class}"
- }
-
- assert obj.instance_of?(cls), msg
- end
-
- ##
- # Fails unless +obj+ is a kind of +cls+.
-
- def assert_kind_of cls, obj, msg = nil # TODO: merge with instance_of
- msg = message(msg) {
- "Expected #{mu_pp(obj)} to be a kind of #{cls}, not #{obj.class}" }
-
- assert obj.kind_of?(cls), msg
- end
-
- ##
- # Fails unless +matcher+ =~ +obj+.
-
- def assert_match matcher, obj, msg = nil
- msg = message(msg) { "Expected #{mu_pp matcher} to match #{mu_pp obj}" }
- assert_respond_to matcher, :"=~"
- matcher = Regexp.new Regexp.escape matcher if String === matcher
- assert matcher =~ obj, msg
- end
-
- ##
- # Fails unless +obj+ is nil
-
- def assert_nil obj, msg = nil
- msg = message(msg) { "Expected #{mu_pp(obj)} to be nil" }
- assert obj.nil?, msg
- end
-
- ##
- # For testing with binary operators.
- #
- # assert_operator 5, :<=, 4
-
- def assert_operator o1, op, o2 = (predicate = true; nil), msg = nil
- return assert_predicate o1, op, msg if predicate
- msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op} #{mu_pp(o2)}" }
- assert o1.__send__(op, o2), msg
- end
-
- ##
- # Fails if stdout or stderr do not output the expected results.
- # Pass in nil if you don't care about that streams output. Pass in
- # "" if you require it to be silent. Pass in a regexp if you want
- # to pattern match.
- #
- # NOTE: this uses #capture_io, not #capture_subprocess_io.
- #
- # See also: #assert_silent
-
- def assert_output stdout = nil, stderr = nil
- out, err = capture_io do
- yield
- end
-
- err_msg = Regexp === stderr ? :assert_match : :assert_equal if stderr
- out_msg = Regexp === stdout ? :assert_match : :assert_equal if stdout
-
- y = send err_msg, stderr, err, "In stderr" if err_msg
- x = send out_msg, stdout, out, "In stdout" if out_msg
-
- (!stdout || x) && (!stderr || y)
- end
-
- ##
- # For testing with predicates.
- #
- # assert_predicate str, :empty?
- #
- # This is really meant for specs and is front-ended by assert_operator:
- #
- # str.must_be :empty?
-
- def assert_predicate o1, op, msg = nil
- msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op}" }
- assert o1.__send__(op), msg
- end
-
- ##
- # Fails unless the block raises one of +exp+. Returns the
- # exception matched so you can check the message, attributes, etc.
-
- def assert_raises *exp
- msg = "#{exp.pop}.\n" if String === exp.last
-
- begin
- yield
- rescue MiniTest::Skip => e
- return e if exp.include? MiniTest::Skip
- raise e
- rescue Exception => e
- expected = exp.any? { |ex|
- if ex.instance_of? Module then
- e.kind_of? ex
- else
- e.instance_of? ex
- end
- }
-
- assert expected, proc {
- exception_details(e, "#{msg}#{mu_pp(exp)} exception expected, not")
- }
-
- return e
- end
-
- exp = exp.first if exp.size == 1
-
- flunk "#{msg}#{mu_pp(exp)} expected but nothing was raised."
- end
-
- ##
- # Fails unless +obj+ responds to +meth+.
-
- def assert_respond_to obj, meth, msg = nil
- msg = message(msg) {
- "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}"
- }
- assert obj.respond_to?(meth), msg
- end
-
- ##
- # Fails unless +exp+ and +act+ are #equal?
-
- def assert_same exp, act, msg = nil
- msg = message(msg) {
- data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
- "Expected %s (oid=%d) to be the same as %s (oid=%d)" % data
- }
- assert exp.equal?(act), msg
- end
-
- ##
- # +send_ary+ is a receiver, message and arguments.
- #
- # Fails unless the call returns a true value
- # TODO: I should prolly remove this from specs
-
- def assert_send send_ary, m = nil
- recv, msg, *args = send_ary
- m = message(m) {
- "Expected #{mu_pp(recv)}.#{msg}(*#{mu_pp(args)}) to return true" }
- assert recv.__send__(msg, *args), m
- end
-
- ##
- # Fails if the block outputs anything to stderr or stdout.
- #
- # See also: #assert_output
-
- def assert_silent
- assert_output "", "" do
- yield
- end
- end
-
- ##
- # Fails unless the block throws +sym+
-
- def assert_throws sym, msg = nil
- default = "Expected #{mu_pp(sym)} to have been thrown"
- caught = true
- catch(sym) do
- begin
- yield
- rescue ThreadError => e # wtf?!? 1.8 + threads == suck
- default += ", not \:#{e.message[/uncaught throw \`(\w+?)\'/, 1]}"
- rescue ArgumentError => e # 1.9 exception
- default += ", not #{e.message.split(/ /).last}"
- rescue NameError => e # 1.8 exception
- default += ", not #{e.name.inspect}"
- end
- caught = false
- end
-
- assert caught, message(msg) { default }
- end
-
- ##
- # Captures $stdout and $stderr into strings:
- #
- # out, err = capture_io do
- # puts "Some info"
- # warn "You did a bad thing"
- # end
- #
- # assert_match %r%info%, out
- # assert_match %r%bad%, err
- #
- # NOTE: For efficiency, this method uses StringIO and does not
- # capture IO for subprocesses. Use #capture_subprocess_io for
- # that.
-
- def capture_io
- require 'stringio'
-
- captured_stdout, captured_stderr = StringIO.new, StringIO.new
-
- synchronize do
- orig_stdout, orig_stderr = $stdout, $stderr
- $stdout, $stderr = captured_stdout, captured_stderr
-
- begin
- yield
- ensure
- $stdout = orig_stdout
- $stderr = orig_stderr
- end
- end
-
- return captured_stdout.string, captured_stderr.string
- end
- alias capture_output capture_io
-
- ##
- # Captures $stdout and $stderr into strings, using Tempfile to
- # ensure that subprocess IO is captured as well.
- #
- # out, err = capture_subprocess_io do
- # system "echo Some info"
- # system "echo You did a bad thing 1>&2"
- # end
- #
- # assert_match %r%info%, out
- # assert_match %r%bad%, err
- #
- # NOTE: This method is approximately 10x slower than #capture_io so
- # only use it when you need to test the output of a subprocess.
-
- def capture_subprocess_io
- require 'tempfile'
-
- captured_stdout, captured_stderr = Tempfile.new("out"), Tempfile.new("err")
-
- synchronize do
- orig_stdout, orig_stderr = $stdout.dup, $stderr.dup
- $stdout.reopen captured_stdout
- $stderr.reopen captured_stderr
-
- begin
- yield
-
- $stdout.rewind
- $stderr.rewind
-
- [captured_stdout.read, captured_stderr.read]
- ensure
- $stdout.reopen orig_stdout
- $stderr.reopen orig_stderr
- orig_stdout.close
- orig_stderr.close
- captured_stdout.close!
- captured_stderr.close!
- end
- end
- end
-
- ##
- # Returns details for exception +e+
-
- def exception_details e, msg
- [
- "#{msg}",
- "Class: <#{e.class}>",
- "Message: <#{e.message.inspect}>",
- "---Backtrace---",
- "#{MiniTest::filter_backtrace(e.backtrace).join("\n")}",
- "---------------",
- ].join "\n"
- end
-
- ##
- # Fails with +msg+
-
- def flunk msg = nil
- msg ||= "Epic Fail!"
- assert false, msg
- end
-
- ##
- # Returns a proc that will output +msg+ along with the default message.
-
- def message msg = nil, ending = ".", &default
- proc {
- msg = msg.call.chomp(".") if Proc === msg
- custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty?
- "#{custom_message}#{default.call}#{ending}"
- }
- end
-
- ##
- # used for counting assertions
-
- def pass msg = nil
- assert true
- end
-
- ##
- # Fails if +test+ is a true value
-
- def refute test, msg = nil
- msg ||= "Failed refutation, no message given"
- not assert(! test, msg)
- end
-
- ##
- # Fails if +obj+ is empty.
-
- def refute_empty obj, msg = nil
- msg = message(msg) { "Expected #{mu_pp(obj)} to not be empty" }
- assert_respond_to obj, :empty?
- refute obj.empty?, msg
- end
-
- ##
- # Fails if exp == act.
- #
- # For floats use refute_in_delta.
-
- def refute_equal exp, act, msg = nil
- msg = message(msg) {
- "Expected #{mu_pp(act)} to not be equal to #{mu_pp(exp)}"
- }
- refute exp == act, msg
- end
-
- ##
- # For comparing Floats. Fails if +exp+ is within +delta+ of +act+.
- #
- # refute_in_delta Math::PI, (22.0 / 7.0)
-
- def refute_in_delta exp, act, delta = 0.001, msg = nil
- n = (exp - act).abs
- msg = message(msg) {
- "Expected |#{exp} - #{act}| (#{n}) to not be <= #{delta}"
- }
- refute delta >= n, msg
- end
-
- ##
- # For comparing Floats. Fails if +exp+ and +act+ have a relative error
- # less than +epsilon+.
-
- def refute_in_epsilon a, b, epsilon = 0.001, msg = nil
- refute_in_delta a, b, a * epsilon, msg
- end
-
- ##
- # Fails if +collection+ includes +obj+.
-
- def refute_includes collection, obj, msg = nil
- msg = message(msg) {
- "Expected #{mu_pp(collection)} to not include #{mu_pp(obj)}"
- }
- assert_respond_to collection, :include?
- refute collection.include?(obj), msg
- end
-
- ##
- # Fails if +obj+ is an instance of +cls+.
-
- def refute_instance_of cls, obj, msg = nil
- msg = message(msg) {
- "Expected #{mu_pp(obj)} to not be an instance of #{cls}"
- }
- refute obj.instance_of?(cls), msg
- end
-
- ##
- # Fails if +obj+ is a kind of +cls+.
-
- def refute_kind_of cls, obj, msg = nil # TODO: merge with instance_of
- msg = message(msg) { "Expected #{mu_pp(obj)} to not be a kind of #{cls}" }
- refute obj.kind_of?(cls), msg
- end
-
- ##
- # Fails if +matcher+ =~ +obj+.
-
- def refute_match matcher, obj, msg = nil
- msg = message(msg) {"Expected #{mu_pp matcher} to not match #{mu_pp obj}"}
- assert_respond_to matcher, :"=~"
- matcher = Regexp.new Regexp.escape matcher if String === matcher
- refute matcher =~ obj, msg
- end
-
- ##
- # Fails if +obj+ is nil.
-
- def refute_nil obj, msg = nil
- msg = message(msg) { "Expected #{mu_pp(obj)} to not be nil" }
- refute obj.nil?, msg
- end
-
- ##
- # Fails if +o1+ is not +op+ +o2+. Eg:
- #
- # refute_operator 1, :>, 2 #=> pass
- # refute_operator 1, :<, 2 #=> fail
-
- def refute_operator o1, op, o2 = (predicate = true; nil), msg = nil
- return refute_predicate o1, op, msg if predicate
- msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op} #{mu_pp(o2)}"}
- refute o1.__send__(op, o2), msg
- end
-
- ##
- # For testing with predicates.
- #
- # refute_predicate str, :empty?
- #
- # This is really meant for specs and is front-ended by refute_operator:
- #
- # str.wont_be :empty?
-
- def refute_predicate o1, op, msg = nil
- msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op}" }
- refute o1.__send__(op), msg
- end
-
- ##
- # Fails if +obj+ responds to the message +meth+.
-
- def refute_respond_to obj, meth, msg = nil
- msg = message(msg) { "Expected #{mu_pp(obj)} to not respond to #{meth}" }
-
- refute obj.respond_to?(meth), msg
- end
-
- ##
- # Fails if +exp+ is the same (by object identity) as +act+.
-
- def refute_same exp, act, msg = nil
- msg = message(msg) {
- data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
- "Expected %s (oid=%d) to not be the same as %s (oid=%d)" % data
- }
- refute exp.equal?(act), msg
- end
-
- ##
- # Skips the current test. Gets listed at the end of the run but
- # doesn't cause a failure exit code.
-
- def skip msg = nil, bt = caller
- msg ||= "Skipped, no message given"
- @skip = true
- raise MiniTest::Skip, msg, bt
- end
-
- ##
- # Was this testcase skipped? Meant for #teardown.
-
- def skipped?
- defined?(@skip) and @skip
- end
-
- ##
- # Takes a block and wraps it with the runner's shared mutex.
-
- def synchronize
- Minitest::Unit.runner.synchronize do
- yield
- end
- end
- end
-
- class Unit # :nodoc:
- VERSION = "4.7.5" # :nodoc:
-
- attr_accessor :report, :failures, :errors, :skips # :nodoc:
- attr_accessor :assertion_count # :nodoc:
- attr_writer :test_count # :nodoc:
- attr_accessor :start_time # :nodoc:
- attr_accessor :help # :nodoc:
- attr_accessor :verbose # :nodoc:
- attr_writer :options # :nodoc:
-
- ##
- # :attr:
- #
- # if true, installs an "INFO" signal handler (only available to BSD and
- # OS X users) which prints diagnostic information about the test run.
- #
- # This is auto-detected by default but may be overridden by custom
- # runners.
-
- attr_accessor :info_signal
-
- ##
- # Lazy accessor for options.
-
- def options
- @options ||= {}
- end
-
- @@installed_at_exit ||= false
- @@out = $stdout
- @@after_tests = []
-
- ##
- # A simple hook allowing you to run a block of code after _all_ of
- # the tests are done. Eg:
- #
- # MiniTest::Unit.after_tests { p $debugging_info }
-
- def self.after_tests &block
- @@after_tests << block
- end
-
- ##
- # Registers MiniTest::Unit to run tests at process exit
-
- def self.autorun
- at_exit {
- # don't run if there was a non-exit exception
- next if $! and not $!.kind_of? SystemExit
-
- # the order here is important. The at_exit handler must be
- # installed before anyone else gets a chance to install their
- # own, that way we can be assured that our exit will be last
- # to run (at_exit stacks).
- exit_code = nil
-
- at_exit {
- @@after_tests.reverse_each(&:call)
- exit false if exit_code && exit_code != 0
- }
-
- exit_code = MiniTest::Unit.new.run ARGV
- } unless @@installed_at_exit
- @@installed_at_exit = true
- end
-
- ##
- # Returns the stream to use for output.
-
- def self.output
- @@out
- end
-
- ##
- # Sets MiniTest::Unit to write output to +stream+. $stdout is the default
- # output
-
- def self.output= stream
- @@out = stream
- end
-
- ##
- # Tells MiniTest::Unit to delegate to +runner+, an instance of a
- # MiniTest::Unit subclass, when MiniTest::Unit#run is called.
-
- def self.runner= runner
- @@runner = runner
- end
-
- ##
- # Returns the MiniTest::Unit subclass instance that will be used
- # to run the tests. A MiniTest::Unit instance is the default
- # runner.
-
- def self.runner
- @@runner ||= self.new
- end
-
- ##
- # Return all plugins' run methods (methods that start with "run_").
-
- def self.plugins
- @@plugins ||= (["run_tests"] +
- public_instance_methods(false).
- grep(/^run_/).map { |s| s.to_s }).uniq
- end
-
- ##
- # Return the IO for output.
-
- def output
- self.class.output
- end
-
- def puts *a # :nodoc:
- output.puts(*a)
- end
-
- def print *a # :nodoc:
- output.print(*a)
- end
-
- def test_count # :nodoc:
- @test_count ||= 0
- end
-
- ##
- # Runner for a given +type+ (eg, test vs bench).
-
- def _run_anything type
- suites = TestCase.send "#{type}_suites"
- return if suites.empty?
-
- puts
- puts "# Running #{type}s:"
- puts
-
- @test_count, @assertion_count = 0, 0
- test_count = assertion_count = 0
- sync = output.respond_to? :"sync=" # stupid emacs
- old_sync, output.sync = output.sync, true if sync
-
- count = 0
- begin
- start = Time.now
-
- results = _run_suites suites, type
-
- @test_count = results.inject(0) { |sum, (tc, _)| sum + tc }
- @assertion_count = results.inject(0) { |sum, (_, ac)| sum + ac }
- test_count += @test_count
- assertion_count += @assertion_count
- t = Time.now - start
- count += 1
- unless @repeat_count
- puts
- puts
- end
- puts "Finished%s %ss in %.6fs, %.4f tests/s, %.4f assertions/s.\n" %
- [(@repeat_count ? "(#{count}/#{@repeat_count}) " : ""), type,
- t, @test_count.fdiv(t), @assertion_count.fdiv(t)]
- end while @repeat_count && count < @repeat_count &&
- report.empty? && failures.zero? && errors.zero?
-
- output.sync = old_sync if sync
-
- report.each_with_index do |msg, i|
- puts "\n%3d) %s" % [i + 1, msg]
- end
-
- puts
- @test_count = test_count
- @assertion_count = assertion_count
-
- status
- end
-
- ##
- # Runs all the +suites+ for a given +type+.
- #
-
- def _run_suites suites, type
- suites.map { |suite| _run_suite suite, type }
- end
-
- ##
- # Run a single +suite+ for a given +type+.
-
- def _run_suite suite, type
- header = "#{type}_suite_header"
- puts send(header, suite) if respond_to? header
-
- filter = options[:filter] || '/./'
- filter = Regexp.new $1 if filter =~ /\/(.*)\//
-
- all_test_methods = suite.send "#{type}_methods"
-
- filtered_test_methods = all_test_methods.find_all { |m|
- filter === m || filter === "#{suite}##{m}"
- }
-
- leakchecker = LeakChecker.new
-
- assertions = filtered_test_methods.map { |method|
- inst = suite.new method
- inst._assertions = 0
-
- print "#{suite}##{method} = " if @verbose
-
- start_time = Time.now if @verbose
- result = inst.run self
-
- print "%.2f s = " % (Time.now - start_time) if @verbose
- print result
- puts if @verbose
- $stdout.flush
-
- unless defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # compiler process is wrongly considered as leaked
- leakchecker.check("#{inst.class}\##{inst.__name__}")
- end
-
- inst._assertions
- }
-
- return assertions.size, assertions.inject(0) { |sum, n| sum + n }
- end
-
- ##
- # Record the result of a single test. Makes it very easy to gather
- # information. Eg:
- #
- # class StatisticsRecorder < MiniTest::Unit
- # def record suite, method, assertions, time, error
- # # ... record the results somewhere ...
- # end
- # end
- #
- # MiniTest::Unit.runner = StatisticsRecorder.new
- #
- # NOTE: record might be sent more than once per test. It will be
- # sent once with the results from the test itself. If there is a
- # failure or error in teardown, it will be sent again with the
- # error or failure.
-
- def record suite, method, assertions, time, error
- end
-
- def location e # :nodoc:
- last_before_assertion = ""
- e.backtrace.reverse_each do |s|
- break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
- last_before_assertion = s
- end
- last_before_assertion.sub(/:in .*$/, '')
- end
-
- ##
- # Writes status for failed test +meth+ in +klass+ which finished with
- # exception +e+
-
- def puke klass, meth, e
- e = case e
- when MiniTest::Skip then
- @skips += 1
- return "S" unless @verbose
- "Skipped:\n#{klass}##{meth} [#{location e}]:\n#{e.message}\n"
- when MiniTest::Assertion then
- @failures += 1
- "Failure:\n#{klass}##{meth} [#{location e}]:\n#{e.message}\n"
- else
- @errors += 1
- bt = MiniTest::filter_backtrace(e.backtrace).join "\n "
- "Error:\n#{klass}##{meth}:\n#{e.class}: #{e.message.b}\n #{bt}\n"
- end
- @report << e
- e[0, 1]
- end
-
- def initialize # :nodoc:
- @report = []
- @errors = @failures = @skips = 0
- @verbose = false
- @mutex = Thread::Mutex.new
- @info_signal = Signal.list['INFO']
- @repeat_count = nil
- end
-
- def synchronize # :nodoc:
- if @mutex then
- @mutex.synchronize { yield }
- else
- yield
- end
- end
-
- def process_args args = [] # :nodoc:
- options = {}
- orig_args = args.dup
-
- OptionParser.new do |opts|
- opts.banner = 'minitest options:'
- opts.version = MiniTest::Unit::VERSION
-
- opts.on '-h', '--help', 'Display this help.' do
- puts opts
- exit
- end
-
- opts.on '-s', '--seed SEED', Integer, "Sets random seed" do |m|
- options[:seed] = m.to_i
- end
-
- opts.on '-v', '--verbose', "Verbose. Show progress processing files." do
- options[:verbose] = true
- end
-
- opts.on '-n', '--name PATTERN', "Filter test names on pattern (e.g. /foo/)" do |a|
- options[:filter] = a
- end
-
- opts.parse! args
- orig_args -= args
- end
-
- unless options[:seed] then
- srand
- options[:seed] = srand % 0xFFFF
- orig_args << "--seed" << options[:seed].to_s
- end
-
- srand options[:seed]
-
- self.verbose = options[:verbose]
- @help = orig_args.map { |s| s =~ /[\s|&<>$()]/ ? s.inspect : s }.join " "
-
- options
- end
-
- ##
- # Begins the full test run. Delegates to +runner+'s #_run method.
-
- def run args = []
- self.class.runner._run(args)
- end
-
- ##
- # Top level driver, controls all output and filtering.
-
- def _run args = []
- args = process_args args # ARGH!! blame test/unit process_args
- self.options.merge! args
-
- puts "Run options: #{help}"
-
- self.class.plugins.each do |plugin|
- send plugin
- break unless report.empty?
- end
-
- return failures + errors if self.test_count > 0 # or return nil...
- rescue Interrupt
- abort 'Interrupted'
- end
-
- ##
- # Runs test suites matching +filter+.
-
- def run_tests
- _run_anything :test
- end
-
- ##
- # Writes status to +io+
-
- def status io = self.output
- format = "%d tests, %d assertions, %d failures, %d errors, %d skips"
- io.puts format % [test_count, assertion_count, failures, errors, skips]
- end
-
- ##
- # Provides a simple set of guards that you can use in your tests
- # to skip execution if it is not applicable. These methods are
- # mixed into TestCase as both instance and class methods so you
- # can use them inside or outside of the test methods.
- #
- # def test_something_for_mri
- # skip "bug 1234" if jruby?
- # # ...
- # end
- #
- # if windows? then
- # # ... lots of test methods ...
- # end
-
- module Guard
-
- ##
- # Is this running on jruby?
-
- def jruby? platform = RUBY_PLATFORM
- "java" == platform
- end
-
- ##
- # Is this running on mri?
-
- def maglev? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE
- "maglev" == platform
- end
-
- module_function :maglev?
-
- ##
- # Is this running on mri?
-
- def mri? platform = RUBY_DESCRIPTION
- /^ruby/ =~ platform
- end
-
- ##
- # Is this running on rubinius?
-
- def rubinius? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE
- "rbx" == platform
- end
-
- ##
- # Is this running on windows?
-
- def windows? platform = RUBY_PLATFORM
- /mswin|mingw/ =~ platform
- end
- end
-
- ##
- # Provides before/after hooks for setup and teardown. These are
- # meant for library writers, NOT for regular test authors. See
- # #before_setup for an example.
-
- module LifecycleHooks
- ##
- # Runs before every test, after setup. This hook is meant for
- # libraries to extend minitest. It is not meant to be used by
- # test developers.
- #
- # See #before_setup for an example.
-
- def after_setup; end
-
- ##
- # Runs before every test, before setup. This hook is meant for
- # libraries to extend minitest. It is not meant to be used by
- # test developers.
- #
- # As a simplistic example:
- #
- # module MyMinitestPlugin
- # def before_setup
- # super
- # # ... stuff to do before setup is run
- # end
- #
- # def after_setup
- # # ... stuff to do after setup is run
- # super
- # end
- #
- # def before_teardown
- # super
- # # ... stuff to do before teardown is run
- # end
- #
- # def after_teardown
- # # ... stuff to do after teardown is run
- # super
- # end
- # end
- #
- # class MiniTest::Unit::TestCase
- # include MyMinitestPlugin
- # end
-
- def before_setup; end
-
- ##
- # Runs after every test, before teardown. This hook is meant for
- # libraries to extend minitest. It is not meant to be used by
- # test developers.
- #
- # See #before_setup for an example.
-
- def before_teardown; end
-
- ##
- # Runs after every test, after teardown. This hook is meant for
- # libraries to extend minitest. It is not meant to be used by
- # test developers.
- #
- # See #before_setup for an example.
-
- def after_teardown; end
- end
-
- ##
- # Subclass TestCase to create your own tests. Typically you'll want a
- # TestCase subclass per implementation class.
- #
- # See MiniTest::Assertions
-
- class TestCase
- include LifecycleHooks
- include Guard
- extend Guard
-
- attr_reader :__name__ # :nodoc:
-
- PASSTHROUGH_EXCEPTIONS = [NoMemoryError, SignalException,
- Interrupt, SystemExit] # :nodoc:
-
- ##
- # Runs the tests reporting the status to +runner+
-
- def run runner
- trap "INFO" do
- runner.report.each_with_index do |msg, i|
- warn "\n%3d) %s" % [i + 1, msg]
- end
- warn ''
- time = runner.start_time ? Time.now - runner.start_time : 0
- warn "Current Test: %s#%s %.2fs" % [self.class, self.__name__, time]
- runner.status $stderr
- end if runner.info_signal
-
- start_time = Time.now
-
- result = ""
- begin
- @passed = nil
- self.before_setup
- self.setup
- self.after_setup
- self.run_test self.__name__
- result = "." unless io?
- time = Time.now - start_time
- runner.record self.class, self.__name__, self._assertions, time, nil
- @passed = true
- rescue *PASSTHROUGH_EXCEPTIONS
- raise
- rescue Exception => e
- @passed = Skip === e
- time = Time.now - start_time
- runner.record self.class, self.__name__, self._assertions, time, e
- result = runner.puke self.class, self.__name__, e
- ensure
- %w{ before_teardown teardown after_teardown }.each do |hook|
- begin
- self.send hook
- rescue *PASSTHROUGH_EXCEPTIONS
- raise
- rescue Exception => e
- @passed = false
- runner.record self.class, self.__name__, self._assertions, time, e
- result = runner.puke self.class, self.__name__, e
- end
- end
- trap 'INFO', 'DEFAULT' if runner.info_signal
- end
- result
- end
-
- alias :run_test :__send__
-
- def initialize name # :nodoc:
- @__name__ = name
- @__io__ = nil
- @passed = nil
- @@current = self # FIX: make thread local
- end
-
- def self.current # :nodoc:
- @@current # FIX: make thread local
- end
-
- ##
- # Return the output IO object
-
- def io
- @__io__ = true
- MiniTest::Unit.output
- end
-
- ##
- # Have we hooked up the IO yet?
-
- def io?
- @__io__
- end
-
- def self.reset # :nodoc:
- @@test_suites = {}
- end
-
- reset
-
- ##
- # Make diffs for this TestCase use #pretty_inspect so that diff
- # in assert_equal can be more details. NOTE: this is much slower
- # than the regular inspect but much more usable for complex
- # objects.
-
- def self.make_my_diffs_pretty!
- require 'pp'
-
- define_method :mu_pp do |o|
- o.pretty_inspect
- end
- end
-
- def self.inherited klass # :nodoc:
- @@test_suites[klass] = true
- super
- end
-
- def self.test_order # :nodoc:
- :random
- end
-
- def self.test_suites # :nodoc:
- @@test_suites.keys.sort_by { |ts| ts.name.to_s }
- end
-
- def self.test_methods # :nodoc:
- methods = public_instance_methods(true).grep(/^test/).map { |m| m.to_s }
-
- case self.test_order
- when :parallel
- max = methods.size
- ParallelEach.new methods.sort.sort_by { rand max }
- when :random then
- max = methods.size
- methods.sort.sort_by { rand max }
- when :alpha, :sorted then
- methods.sort
- else
- raise "Unknown test_order: #{self.test_order.inspect}"
- end
- end
-
- ##
- # Returns true if the test passed.
-
- def passed?
- @passed
- end
-
- ##
- # Runs before every test. Use this to set up before each test
- # run.
-
- def setup; end
-
- ##
- # Runs after every test. Use this to clean up after each test
- # run.
-
- def teardown; end
-
- include MiniTest::Assertions
- end # class TestCase
- end # class Unit
-
- Test = Unit::TestCase
-end # module MiniTest
-
-Minitest = MiniTest # :nodoc: because ugh... I typo this all the time
diff --git a/test/lib/test/unit.rb b/test/lib/test/unit.rb
deleted file mode 100644
index c280c685..00000000
--- a/test/lib/test/unit.rb
+++ /dev/null
@@ -1,1177 +0,0 @@
-# frozen_string_literal: true
-begin
- gem 'minitest', '< 5.0.0' if defined? Gem
-rescue Gem::LoadError
-end
-require 'minitest/unit'
-require 'test/unit/assertions'
-require_relative '../envutil'
-require 'test/unit/testcase'
-require 'optparse'
-
-# See Test::Unit
-module Test
- ##
- # Test::Unit is an implementation of the xUnit testing framework for Ruby.
- #
- # If you are writing new test code, please use MiniTest instead of Test::Unit.
- #
- # Test::Unit has been left in the standard library to support legacy test
- # suites.
- module Unit
- TEST_UNIT_IMPLEMENTATION = 'test/unit compatibility layer using minitest' # :nodoc:
-
- module RunCount # :nodoc: all
- @@run_count = 0
-
- def self.have_run?
- @@run_count.nonzero?
- end
-
- def run(*)
- @@run_count += 1
- super
- end
-
- def run_once
- return if have_run?
- return if $! # don't run if there was an exception
- yield
- end
- module_function :run_once
- end
-
- module Options # :nodoc: all
- def initialize(*, &block)
- @init_hook = block
- @options = nil
- super(&nil)
- end
-
- def option_parser
- @option_parser ||= OptionParser.new
- end
-
- def process_args(args = [])
- return @options if @options
- orig_args = args.dup
- options = {}
- opts = option_parser
- setup_options(opts, options)
- opts.parse!(args)
- orig_args -= args
- args = @init_hook.call(args, options) if @init_hook
- non_options(args, options)
- @run_options = orig_args
- @help = orig_args.map { |s| s =~ /[\s|&<>$()]/ ? s.inspect : s }.join " "
- @options = options
- end
-
- private
- def setup_options(opts, options)
- opts.separator 'minitest options:'
- opts.version = MiniTest::Unit::VERSION
-
- opts.on '-h', '--help', 'Display this help.' do
- puts opts
- exit
- end
-
- opts.on '-s', '--seed SEED', Integer, "Sets random seed" do |m|
- options[:seed] = m
- end
-
- opts.on '-v', '--verbose', "Verbose. Show progress processing files." do
- options[:verbose] = true
- self.verbose = options[:verbose]
- end
-
- opts.on '-n', '--name PATTERN', "Filter test method names on pattern: /REGEXP/, !/REGEXP/ or STRING" do |a|
- (options[:filter] ||= []) << a
- end
-
- opts.on '--test-order=random|alpha|sorted', [:random, :alpha, :sorted] do |a|
- MiniTest::Unit::TestCase.test_order = a
- end
- end
-
- def non_options(files, options)
- filter = options[:filter]
- if filter
- pos_pat = /\A\/(.*)\/\z/
- neg_pat = /\A!\/(.*)\/\z/
- negative, positive = filter.partition {|s| neg_pat =~ s}
- if positive.empty?
- filter = nil
- elsif negative.empty? and positive.size == 1 and pos_pat !~ positive[0]
- filter = positive[0]
- else
- filter = Regexp.union(*positive.map! {|s| Regexp.new(s[pos_pat, 1] || "\\A#{Regexp.quote(s)}\\z")})
- end
- unless negative.empty?
- negative = Regexp.union(*negative.map! {|s| Regexp.new(s[neg_pat, 1])})
- filter = /\A(?=.*#{filter})(?!.*#{negative})/
- end
- if Regexp === filter
- # bypass conversion in minitest
- def filter.=~(other) # :nodoc:
- super unless Regexp === other
- end
- end
- options[:filter] = filter
- end
- true
- end
- end
-
- module Parallel # :nodoc: all
- def process_args(args = [])
- return @options if @options
- options = super
- if @options[:parallel]
- @files = args
- end
- options
- end
-
- def non_options(files, options)
- @jobserver = nil
- if !options[:parallel] and
- /(?:\A|\s)--jobserver-(?:auth|fds)=(\d+),(\d+)/ =~ ENV["MAKEFLAGS"]
- begin
- r = IO.for_fd($1.to_i(10), "rb", autoclose: false)
- w = IO.for_fd($2.to_i(10), "wb", autoclose: false)
- rescue
- r.close if r
- nil
- else
- @jobserver = [r, w]
- options[:parallel] ||= 1
- end
- end
- super
- end
-
- def status(*args)
- result = super
- raise @interrupt if @interrupt
- result
- end
-
- private
- def setup_options(opts, options)
- super
-
- opts.separator "parallel test options:"
-
- options[:retry] = true
-
- opts.on '-j N', '--jobs N', /\A(t)?(\d+)\z/, "Allow run tests with N jobs at once" do |_, t, a|
- options[:testing] = true & t # For testing
- options[:parallel] = a.to_i
- end
-
- opts.on '--separate', "Restart job process after one testcase has done" do
- options[:parallel] ||= 1
- options[:separate] = true
- end
-
- opts.on '--retry', "Retry running testcase when --jobs specified" do
- options[:retry] = true
- end
-
- opts.on '--no-retry', "Disable --retry" do
- options[:retry] = false
- end
-
- opts.on '--ruby VAL', "Path to ruby which is used at -j option" do |a|
- options[:ruby] = a.split(/ /).reject(&:empty?)
- end
- end
-
- class Worker
- def self.launch(ruby,args=[])
- io = IO.popen([*ruby, "-W1",
- "#{File.dirname(__FILE__)}/unit/parallel.rb",
- *args], "rb+")
- new(io, io.pid, :waiting)
- end
-
- attr_reader :quit_called
-
- def initialize(io, pid, status)
- @io = io
- @pid = pid
- @status = status
- @file = nil
- @real_file = nil
- @loadpath = []
- @hooks = {}
- @quit_called = false
- end
-
- def puts(*args)
- @io.puts(*args)
- end
-
- def run(task,type)
- @file = File.basename(task, ".rb")
- @real_file = task
- begin
- puts "loadpath #{[Marshal.dump($:-@loadpath)].pack("m0")}"
- @loadpath = $:.dup
- puts "run #{task} #{type}"
- @status = :prepare
- rescue Errno::EPIPE
- died
- rescue IOError
- raise unless /stream closed|closed stream/ =~ $!.message
- died
- end
- end
-
- def hook(id,&block)
- @hooks[id] ||= []
- @hooks[id] << block
- self
- end
-
- def read
- res = (@status == :quit) ? @io.read : @io.gets
- res && res.chomp
- end
-
- def close
- @io.close unless @io.closed?
- self
- rescue IOError
- end
-
- def quit
- return if @io.closed?
- @quit_called = true
- @io.puts "quit"
- end
-
- def kill
- Process.kill(:KILL, @pid)
- rescue Errno::ESRCH
- end
-
- def died(*additional)
- @status = :quit
- @io.close
- status = $?
- if status and status.signaled?
- additional[0] ||= SignalException.new(status.termsig)
- end
-
- call_hook(:dead,*additional)
- end
-
- def to_s
- if @file and @status != :ready
- "#{@pid}=#{@file}"
- else
- "#{@pid}:#{@status.to_s.ljust(7)}"
- end
- end
-
- attr_reader :io, :pid
- attr_accessor :status, :file, :real_file, :loadpath
-
- private
-
- def call_hook(id,*additional)
- @hooks[id] ||= []
- @hooks[id].each{|hook| hook[self,additional] }
- self
- end
-
- end
-
- def flush_job_tokens
- if @jobserver
- r, w = @jobserver.shift(2)
- @jobserver = nil
- w << @job_tokens.slice!(0..-1)
- r.close
- w.close
- end
- end
-
- def after_worker_down(worker, e=nil, c=false)
- return unless @options[:parallel]
- return if @interrupt
- flush_job_tokens
- warn e if e
- real_file = worker.real_file and warn "running file: #{real_file}"
- @need_quit = true
- warn ""
- warn "Some worker was crashed. It seems ruby interpreter's bug"
- warn "or, a bug of test/unit/parallel.rb. try again without -j"
- warn "option."
- warn ""
- STDERR.flush
- exit c
- end
-
- def after_worker_quit(worker)
- return unless @options[:parallel]
- return if @interrupt
- worker.close
- if @jobserver and (token = @job_tokens.slice!(0))
- @jobserver[1] << token
- end
- @workers.delete(worker)
- @dead_workers << worker
- @ios = @workers.map(&:io)
- end
-
- def launch_worker
- begin
- worker = Worker.launch(@options[:ruby], @run_options)
- rescue => e
- abort "ERROR: Failed to launch job process - #{e.class}: #{e.message}"
- end
- worker.hook(:dead) do |w,info|
- after_worker_quit w
- after_worker_down w, *info if !info.empty? && !worker.quit_called
- end
- @workers << worker
- @ios << worker.io
- @workers_hash[worker.io] = worker
- worker
- end
-
- def delete_worker(worker)
- @workers_hash.delete worker.io
- @workers.delete worker
- @ios.delete worker.io
- end
-
- def quit_workers
- return if @workers.empty?
- @workers.reject! do |worker|
- begin
- Timeout.timeout(1) do
- worker.quit
- end
- rescue Errno::EPIPE
- rescue Timeout::Error
- end
- worker.close
- end
-
- return if @workers.empty?
- begin
- Timeout.timeout(0.2 * @workers.size) do
- Process.waitall
- end
- rescue Timeout::Error
- @workers.each do |worker|
- worker.kill
- end
- @worker.clear
- end
- end
-
- FakeClass = Struct.new(:name)
- def fake_class(name)
- (@fake_classes ||= {})[name] ||= FakeClass.new(name)
- end
-
- def deal(io, type, result, rep, shutting_down = false)
- worker = @workers_hash[io]
- cmd = worker.read
- cmd.sub!(/\A\.+/, '') if cmd # read may return nil
- case cmd
- when ''
- # just only dots, ignore
- when /^okay$/
- worker.status = :running
- when /^ready(!)?$/
- bang = $1
- worker.status = :ready
-
- unless task = @tasks.shift
- worker.quit
- return nil
- end
- if @options[:separate] and not bang
- worker.quit
- worker = add_worker
- end
- worker.run(task, type)
- @test_count += 1
-
- jobs_status(worker)
- when /^done (.+?)$/
- begin
- r = Marshal.load($1.unpack("m")[0])
- rescue
- print "unknown object: #{$1.unpack("m")[0].dump}"
- return true
- end
- result << r[0..1] unless r[0..1] == [nil,nil]
- rep << {file: worker.real_file, report: r[2], result: r[3], testcase: r[5]}
- $:.push(*r[4]).uniq!
- jobs_status(worker) if @options[:job_status] == :replace
- return true
- when /^record (.+?)$/
- begin
- r = Marshal.load($1.unpack("m")[0])
- rescue => e
- print "unknown record: #{e.message} #{$1.unpack("m")[0].dump}"
- return true
- end
- record(fake_class(r[0]), *r[1..-1])
- when /^p (.+?)$/
- del_jobs_status
- print $1.unpack("m")[0]
- jobs_status(worker) if @options[:job_status] == :replace
- when /^after (.+?)$/
- @warnings << Marshal.load($1.unpack("m")[0])
- when /^bye (.+?)$/
- after_worker_down worker, Marshal.load($1.unpack("m")[0])
- when /^bye$/, nil
- if shutting_down || worker.quit_called
- after_worker_quit worker
- else
- after_worker_down worker
- end
- else
- print "unknown command: #{cmd.dump}\n"
- end
- return false
- end
-
- def _run_parallel suites, type, result
- if @options[:parallel] < 1
- warn "Error: parameter of -j option should be greater than 0."
- return
- end
-
- # Require needed things for parallel running
- require 'thread'
- require 'timeout'
- @tasks = @files.dup # Array of filenames.
- @need_quit = false
- @dead_workers = [] # Array of dead workers.
- @warnings = []
- @total_tests = @tasks.size.to_s(10)
- rep = [] # FIXME: more good naming
-
- @workers = [] # Array of workers.
- @workers_hash = {} # out-IO => worker
- @ios = [] # Array of worker IOs
- @job_tokens = String.new(encoding: Encoding::ASCII_8BIT) if @jobserver
- begin
- [@tasks.size, @options[:parallel]].min.times {launch_worker}
-
- while _io = IO.select(@ios)[0]
- break if _io.any? do |io|
- @need_quit or
- (deal(io, type, result, rep).nil? and
- !@workers.any? {|x| [:running, :prepare].include? x.status})
- end
- if @jobserver and @job_tokens and !@tasks.empty? and !@workers.any? {|x| x.status == :ready}
- t = @jobserver[0].read_nonblock([@tasks.size, @options[:parallel]].min, exception: false)
- if String === t
- @job_tokens << t
- t.size.times {launch_worker}
- end
- end
- end
- rescue Interrupt => ex
- @interrupt = ex
- return result
- ensure
- if @interrupt
- @ios.select!{|x| @workers_hash[x].status == :running }
- while !@ios.empty? && (__io = IO.select(@ios,[],[],10))
- __io[0].reject! {|io| deal(io, type, result, rep, true)}
- end
- end
-
- quit_workers
- flush_job_tokens
-
- unless @interrupt || !@options[:retry] || @need_quit
- parallel = @options[:parallel]
- @options[:parallel] = false
- suites, rep = rep.partition {|r| r[:testcase] && r[:file] && r[:report].any? {|e| !e[2].is_a?(MiniTest::Skip)}}
- suites.map {|r| r[:file]}.uniq.each {|file| require file}
- suites.map! {|r| eval("::"+r[:testcase])}
- del_status_line or puts
- unless suites.empty?
- puts "\n""Retrying..."
- _run_suites(suites, type)
- end
- @options[:parallel] = parallel
- end
- unless @options[:retry]
- del_status_line or puts
- end
- unless rep.empty?
- rep.each do |r|
- r[:report].each do |f|
- puke(*f) if f
- end
- end
- if @options[:retry]
- @errors += rep.map{|x| x[:result][0] }.inject(:+)
- @failures += rep.map{|x| x[:result][1] }.inject(:+)
- @skips += rep.map{|x| x[:result][2] }.inject(:+)
- end
- end
- unless @warnings.empty?
- warn ""
- @warnings.uniq! {|w| w[1].message}
- @warnings.each do |w|
- warn "#{w[0]}: #{w[1].message} (#{w[1].class})"
- end
- warn ""
- end
- end
- end
-
- def _run_suites suites, type
- _prepare_run(suites, type)
- @interrupt = nil
- result = []
- GC.start
- if @options[:parallel]
- _run_parallel suites, type, result
- else
- suites.each {|suite|
- begin
- result << _run_suite(suite, type)
- rescue Interrupt => e
- @interrupt = e
- break
- end
- }
- end
- del_status_line
- result
- end
- end
-
- module Skipping # :nodoc: all
- def failed(s)
- super if !s or @options[:hide_skip]
- end
-
- private
- def setup_options(opts, options)
- super
-
- opts.separator "skipping options:"
-
- options[:hide_skip] = true
-
- opts.on '-q', '--hide-skip', 'Hide skipped tests' do
- options[:hide_skip] = true
- end
-
- opts.on '--show-skip', 'Show skipped tests' do
- options[:hide_skip] = false
- end
- end
-
- private
- def _run_suites(suites, type)
- result = super
- report.reject!{|r| r.start_with? "Skipped:" } if @options[:hide_skip]
- report.sort_by!{|r| r.start_with?("Skipped:") ? 0 : \
- (r.start_with?("Failure:") ? 1 : 2) }
- failed(nil)
- result
- end
- end
-
- module Statistics
- def update_list(list, rec, max)
- if i = list.empty? ? 0 : list.bsearch_index {|*a| yield(*a)}
- list[i, 0] = [rec]
- list[max..-1] = [] if list.size >= max
- end
- end
-
- def record(suite, method, assertions, time, error)
- if @options.values_at(:longest, :most_asserted).any?
- @tops ||= {}
- rec = [suite.name, method, assertions, time, error]
- if max = @options[:longest]
- update_list(@tops[:longest] ||= [], rec, max) {|_,_,_,t,_|t