From 9279dd8c61d4fa1d76f1c448cd2c60bbd7413a6a Mon Sep 17 00:00:00 2001 From: Kir Shatrov Date: Sat, 26 Dec 2020 13:23:54 +0300 Subject: [PATCH 01/66] Use frozen strings --- lib/webrick/accesslog.rb | 2 +- lib/webrick/cgi.rb | 2 +- lib/webrick/compat.rb | 2 +- lib/webrick/config.rb | 2 +- lib/webrick/cookie.rb | 4 ++-- lib/webrick/htmlutils.rb | 2 +- lib/webrick/httpauth.rb | 2 +- lib/webrick/httpauth/authenticator.rb | 2 +- lib/webrick/httpauth/basicauth.rb | 2 +- lib/webrick/httpauth/digestauth.rb | 2 +- lib/webrick/httpauth/htdigest.rb | 2 +- lib/webrick/httpauth/htgroup.rb | 2 +- lib/webrick/httpauth/htpasswd.rb | 2 +- lib/webrick/httpauth/userdb.rb | 2 +- lib/webrick/httpproxy.rb | 2 +- lib/webrick/httprequest.rb | 4 ++-- lib/webrick/httpresponse.rb | 12 ++++++------ lib/webrick/https.rb | 2 +- lib/webrick/httpserver.rb | 4 ++-- lib/webrick/httpservlet.rb | 2 +- lib/webrick/httpservlet/abstract.rb | 2 +- lib/webrick/httpservlet/cgi_runner.rb | 4 ++-- lib/webrick/httpservlet/cgihandler.rb | 2 +- lib/webrick/httpservlet/erbhandler.rb | 2 +- lib/webrick/httpservlet/filehandler.rb | 10 +++++----- lib/webrick/httpservlet/prochandler.rb | 2 +- lib/webrick/httpstatus.rb | 2 +- lib/webrick/httputils.rb | 6 +++--- lib/webrick/httpversion.rb | 2 +- lib/webrick/log.rb | 14 +++++++------- lib/webrick/server.rb | 2 +- lib/webrick/ssl.rb | 2 +- lib/webrick/utils.rb | 4 ++-- lib/webrick/version.rb | 2 +- 34 files changed, 56 insertions(+), 56 deletions(-) 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..fe95aad9 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 = {} diff --git a/lib/webrick/httpresponse.rb b/lib/webrick/httpresponse.rb index ba4494ab..c7238dc0 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 # @@ -332,7 +332,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 +419,7 @@ def check_header(header_value) # :stopdoc: def error_body(backtrace, ex, host, port) - @body = '' + @body = +'' @body << <<-_end_of_html_ @@ -453,11 +453,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 @@ -539,7 +539,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..dca5d2d0 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 # 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..456384e7 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 # @@ -231,7 +231,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 @@ -495,7 +495,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..478b1ac9 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 # diff --git a/lib/webrick/ssl.rb b/lib/webrick/ssl.rb index e448095a..0c81c9eb 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 # 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..a4cfd504 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 # From 61d2a06af26b2c4c9011f5c2775d4919ff17829a Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 16 Feb 2021 21:00:26 +0900 Subject: [PATCH 02/66] Removed test-suite of ruby repository --- test/lib/envutil.rb | 295 ------- test/lib/find_executable.rb | 22 - test/lib/leakchecker.rb | 203 ----- test/lib/minitest/autorun.rb | 14 - test/lib/minitest/mock.rb | 196 ----- test/lib/minitest/unit.rb | 1417 ------------------------------ test/lib/test/unit.rb | 1177 ------------------------- test/lib/test/unit/assertions.rb | 940 -------------------- test/lib/test/unit/parallel.rb | 208 ----- test/lib/test/unit/testcase.rb | 36 - 10 files changed, 4508 deletions(-) delete mode 100644 test/lib/envutil.rb delete mode 100644 test/lib/find_executable.rb delete mode 100644 test/lib/leakchecker.rb delete mode 100644 test/lib/minitest/autorun.rb delete mode 100644 test/lib/minitest/mock.rb delete mode 100644 test/lib/minitest/unit.rb delete mode 100644 test/lib/test/unit.rb delete mode 100644 test/lib/test/unit/assertions.rb delete mode 100644 test/lib/test/unit/parallel.rb delete mode 100644 test/lib/test/unit/testcase.rb 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/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 0 - end - $stdout.flush if flush - @status_line_size = 0 - end - - def add_status(line) - @status_line_size ||= 0 - if @options[:job_status] == :replace - line = line[0...(terminal_width-@status_line_size)] - end - print line - @status_line_size += line.size - end - - def jobs_status(worker) - return if !@options[:job_status] or @options[:verbose] - if @options[:job_status] == :replace - status_line = @workers.map(&:to_s).join(" ") - else - status_line = worker.to_s - end - update_status(status_line) or (puts; nil) - end - - def del_jobs_status - return unless @options[:job_status] == :replace && @status_line_size.nonzero? - del_status_line - end - - def output - (@output ||= nil) || super - end - - def _prepare_run(suites, type) - options[:job_status] ||= :replace if @tty && !@verbose - case options[:color] - when :always - color = true - when :auto, nil - color = (@tty || @options[:job_status] == :replace) && /dumb/ !~ ENV["TERM"] - else - color = false - end - if color - # dircolors-like style - colors = (colors = ENV['TEST_COLORS']) ? Hash[colors.scan(/(\w+)=([^:\n]*)/)] : {} - begin - File.read(File.join(__dir__, "../../colors")).scan(/(\w+)=([^:\n]*)/) do |n, c| - colors[n] ||= c - end - rescue - end - @passed_color = "\e[;#{colors["pass"] || "32"}m" - @failed_color = "\e[;#{colors["fail"] || "31"}m" - @skipped_color = "\e[;#{colors["skip"] || "33"}m" - @reset_color = "\e[m" - else - @passed_color = @failed_color = @skipped_color = @reset_color = "" - end - if color or @options[:job_status] == :replace - @verbose = !options[:parallel] - end - @output = Output.new(self) unless @options[:testing] - filter = options[:filter] - type = "#{type}_methods" - total = if filter - suites.inject(0) {|n, suite| n + suite.send(type).grep(filter).size} - else - suites.inject(0) {|n, suite| n + suite.send(type).size} - end - @test_count = 0 - @total_tests = total.to_s(10) - end - - def new_test(s) - @test_count += 1 - update_status(s) - end - - def update_status(s) - count = @test_count.to_s(10).rjust(@total_tests.size) - del_status_line(false) - print(@passed_color) - add_status("[#{count}/#{@total_tests}]") - print(@reset_color) - add_status(" #{s}") - $stdout.print "\r" if @options[:job_status] == :replace and !@verbose - $stdout.flush - end - - def _print(s); $stdout.print(s); end - def succeed; del_status_line; end - - def failed(s) - return if s and @options[:job_status] != :replace - sep = "\n" - @report_count ||= 0 - report.each do |msg| - if msg.start_with? "Skipped:" - if @options[:hide_skip] - del_status_line - next - end - color = @skipped_color - else - color = @failed_color - end - msg = msg.split(/$/, 2) - $stdout.printf("%s%s%3d) %s%s%s\n", - sep, color, @report_count += 1, - msg[0], @reset_color, msg[1]) - sep = nil - end - report.clear - end - - def initialize - super - @tty = $stdout.tty? - end - - def run(*args) - result = super - puts "\nruby -v: #{RUBY_DESCRIPTION}" - result - end - - private - def setup_options(opts, options) - super - - opts.separator "status line options:" - - options[:job_status] = nil - - opts.on '--jobs-status [TYPE]', [:normal, :replace, :none], - "Show status of jobs every file; Disabled when --jobs isn't specified." do |type| - options[:job_status] = (type || :normal if type != :none) - end - - opts.on '--color[=WHEN]', - [:always, :never, :auto], - "colorize the output. WHEN defaults to 'always'", "or can be 'never' or 'auto'." do |c| - options[:color] = c || :always - end - - opts.on '--tty[=WHEN]', - [:yes, :no], - "force to output tty control. WHEN defaults to 'yes'", "or can be 'no'." do |c| - @tty = c != :no - end - end - - class Output < Struct.new(:runner) # :nodoc: all - def puts(*a) $stdout.puts(*a) unless a.empty? end - def respond_to_missing?(*a) $stdout.respond_to?(*a) end - def method_missing(*a, &b) $stdout.__send__(*a, &b) end - - def print(s) - case s - when /\A(.*\#.*) = \z/ - runner.new_test($1) - when /\A(.* s) = \z/ - runner.add_status(" = #$1") - when /\A\.+\z/ - runner.succeed - when /\A[EFS]\z/ - runner.failed(s) - else - $stdout.print(s) - end - end - end - end - - module LoadPathOption # :nodoc: all - def non_options(files, options) - begin - require "rbconfig" - rescue LoadError - warn "#{caller(1)[0]}: warning: Parallel running disabled because can't get path to ruby; run specify with --ruby argument" - options[:parallel] = nil - else - options[:ruby] ||= [RbConfig.ruby] - end - - super - end - - def setup_options(parser, options) - super - parser.separator "load path options:" - parser.on '-Idirectory', 'Add library load path' do |dirs| - dirs.split(':').each { |d| $LOAD_PATH.unshift d } - end - end - end - - module GlobOption # :nodoc: all - @@testfile_prefix = "test" - @@testfile_suffix = "test" - - def setup_options(parser, options) - super - parser.separator "globbing options:" - parser.on '-b', '--basedir=DIR', 'Base directory of test suites.' do |dir| - options[:base_directory] = dir - end - parser.on '-x', '--exclude REGEXP', 'Exclude test files on pattern.' do |pattern| - (options[:reject] ||= []) << pattern - end - end - - def non_options(files, options) - paths = [options.delete(:base_directory), nil].uniq - if reject = options.delete(:reject) - reject_pat = Regexp.union(reject.map {|r| %r"#{r}"}) - end - files.map! {|f| - f = f.tr(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR - ((paths if /\A\.\.?(?:\z|\/)/ !~ f) || [nil]).any? do |prefix| - if prefix - path = f.empty? ? prefix : "#{prefix}/#{f}" - else - next if f.empty? - path = f - end - if !(match = (Dir["#{path}/**/#{@@testfile_prefix}_*.rb"] + Dir["#{path}/**/*_#{@@testfile_suffix}.rb"]).uniq).empty? - if reject - match.reject! {|n| - n[(prefix.length+1)..-1] if prefix - reject_pat =~ n - } - end - break match - elsif !reject or reject_pat !~ f and File.exist? path - break path - end - end or - raise ArgumentError, "file not found: #{f}" - } - files.flatten! - super(files, options) - end - end - - module GCStressOption # :nodoc: all - def setup_options(parser, options) - super - parser.separator "GC options:" - parser.on '--[no-]gc-stress', 'Set GC.stress as true' do |flag| - options[:gc_stress] = flag - end - end - - def non_options(files, options) - if options.delete(:gc_stress) - MiniTest::Unit::TestCase.class_eval do - oldrun = instance_method(:run) - define_method(:run) do |runner| - begin - gc_stress, GC.stress = GC.stress, true - oldrun.bind(self).call(runner) - ensure - GC.stress = gc_stress - end - end - end - end - super - end - end - - module RequireFiles # :nodoc: all - def non_options(files, options) - return false if !super - errors = {} - result = false - files.each {|f| - d = File.dirname(path = File.realpath(f)) - unless $:.include? d - $: << d - end - begin - require path unless options[:parallel] - result = true - rescue LoadError - next if errors[$!.message] - errors[$!.message] = true - puts "#{f}: #{$!}" - end - } - result - end - end - - module RepeatOption # :nodoc: all - def setup_options(parser, options) - super - options[:repeat_count] = nil - parser.separator "repeat options:" - parser.on '--repeat-count=NUM', "Number of times to repeat", Integer do |n| - options[:repeat_count] = n - end - end - - def _run_anything(type) - @repeat_count = @options[:repeat_count] - super - end - end - - module ExcludesOption # :nodoc: all - class ExcludedMethods < Struct.new(:excludes) - def exclude(name, reason) - excludes[name] = reason - end - - def exclude_from(klass) - excludes = self.excludes - pattern = excludes.keys.grep(Regexp).tap {|k| - break (Regexp.new(k.join('|')) unless k.empty?) - } - klass.class_eval do - public_instance_methods(false).each do |method| - if excludes[method] or (pattern and pattern =~ method) - remove_method(method) - end - end - public_instance_methods(true).each do |method| - if excludes[method] or (pattern and pattern =~ method) - undef_method(method) - end - end - end - end - - def self.load(dirs, name) - return unless dirs and name - instance = nil - dirs.each do |dir| - path = File.join(dir, name.gsub(/::/, '/') + ".rb") - begin - src = File.read(path) - rescue Errno::ENOENT - nil - else - instance ||= new({}) - instance.instance_eval(src, path) - end - end - instance - end - end - - def setup_options(parser, options) - super - if excludes = ENV["EXCLUDES"] - excludes = excludes.split(File::PATH_SEPARATOR) - end - options[:excludes] = excludes || [] - parser.separator "excludes options:" - parser.on '-X', '--excludes-dir DIRECTORY', "Directory name of exclude files" do |d| - options[:excludes].concat d.split(File::PATH_SEPARATOR) - end - end - - def _run_suite(suite, type) - if ex = ExcludedMethods.load(@options[:excludes], suite.name) - ex.exclude_from(suite) - end - super - end - end - - module SubprocessOption - def setup_options(parser, options) - super - parser.separator "subprocess options:" - parser.on '--subprocess-timeout-scale NUM', "Scale subprocess timeout", Float do |scale| - raise OptionParser::InvalidArgument, "timeout scale must be positive" unless scale > 0 - options[:timeout_scale] = scale - end - if scale = options[:timeout_scale] or - (scale = ENV["RUBY_TEST_SUBPROCESS_TIMEOUT_SCALE"] and (scale = scale.to_f) > 0) - EnvUtil.subprocess_timeout_scale = scale - end - end - end - - class Runner < MiniTest::Unit # :nodoc: all - include Test::Unit::Options - include Test::Unit::StatusLine - include Test::Unit::Parallel - include Test::Unit::Statistics - include Test::Unit::Skipping - include Test::Unit::GlobOption - include Test::Unit::RepeatOption - include Test::Unit::LoadPathOption - include Test::Unit::GCStressOption - include Test::Unit::ExcludesOption - include Test::Unit::SubprocessOption - include Test::Unit::RunCount - - class << self; undef autorun; end - - @@stop_auto_run = false - def self.autorun - at_exit { - Test::Unit::RunCount.run_once { - exit(Test::Unit::Runner.new.run(ARGV) || true) - } unless @@stop_auto_run - } unless @@installed_at_exit - @@installed_at_exit = true - end - - alias mini_run_suite _run_suite - - # Overriding of MiniTest::Unit#puke - def puke klass, meth, e - # TODO: - # this overriding is for minitest feature that skip messages are - # hidden when not verbose (-v), note this is temporally. - n = report.size - rep = super - if MiniTest::Skip === e and /no message given\z/ =~ e.message - report.slice!(n..-1) - rep = "." - end - rep - end - end - - class AutoRunner # :nodoc: all - class Runner < Test::Unit::Runner - include Test::Unit::RequireFiles - end - - attr_accessor :to_run, :options - - def initialize(force_standalone = false, default_dir = nil, argv = ARGV) - @force_standalone = force_standalone - @runner = Runner.new do |files, options| - options[:base_directory] ||= default_dir - files << default_dir if files.empty? and default_dir - @to_run = files - yield self if block_given? - files - end - Runner.runner = @runner - @options = @runner.option_parser - if @force_standalone - @options.banner.sub!(/\[options\]/, '\& tests...') - end - @argv = argv - end - - def process_args(*args) - @runner.process_args(*args) - !@to_run.empty? - end - - def run - if @force_standalone and not process_args(@argv) - abort @options.banner - end - @runner.run(@argv) || true - end - - def self.run(*args) - new(*args).run - end - end - - class ProxyError < StandardError # :nodoc: all - def initialize(ex) - @message = ex.message - @backtrace = ex.backtrace - end - - attr_accessor :message, :backtrace - end - end -end - -module MiniTest # :nodoc: all - class Unit - end -end - -class MiniTest::Unit::TestCase # :nodoc: all - test_order = self.test_order - class << self - attr_writer :test_order - undef test_order - end - def self.test_order - defined?(@test_order) ? @test_order : superclass.test_order - end - self.test_order = test_order - undef run_test - RUN_TEST_TRACE = "#{__FILE__}:#{__LINE__+3}:in `run_test'".freeze - def run_test(name) - progname, $0 = $0, "#{$0}: #{self.class}##{name}" - self.__send__(name) - ensure - $@.delete(RUN_TEST_TRACE) if $@ - $0 = progname - end -end - -Test::Unit::Runner.autorun diff --git a/test/lib/test/unit/assertions.rb b/test/lib/test/unit/assertions.rb deleted file mode 100644 index ee6a758f..00000000 --- a/test/lib/test/unit/assertions.rb +++ /dev/null @@ -1,940 +0,0 @@ -# frozen_string_literal: true -require 'minitest/unit' -require 'pp' - -module Test - module Unit - module Assertions - include MiniTest::Assertions - - def mu_pp(obj) #:nodoc: - obj.pretty_inspect.chomp - end - - MINI_DIR = File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), "minitest") #:nodoc: - - # :call-seq: - # assert(test, [failure_message]) - # - #Tests if +test+ is true. - # - #+msg+ may be a String or a Proc. If +msg+ is a String, it will be used - #as the failure message. Otherwise, the result of calling +msg+ will be - #used as the message if the assertion fails. - # - #If no +msg+ is given, a default message will be used. - # - # assert(false, "This was expected to be true") - def assert(test, *msgs) - case msg = msgs.first - when String, Proc - when nil - msgs.shift - else - bt = caller.reject { |s| s.start_with?(MINI_DIR) } - raise ArgumentError, "assertion message must be String or Proc, but #{msg.class} was given.", bt - end unless msgs.empty? - super - end - - # :call-seq: - # assert_block( failure_message = nil ) - # - #Tests the result of the given block. If the block does not return true, - #the assertion will fail. The optional +failure_message+ argument is the same as in - #Assertions#assert. - # - # assert_block do - # [1, 2, 3].any? { |num| num < 1 } - # end - def assert_block(*msgs) - assert yield, *msgs - end - - # :call-seq: - # assert_raise( *args, &block ) - # - #Tests if the given block raises an exception. Acceptable exception - #types may be given as optional arguments. If the last argument is a - #String, it will be used as the error message. - # - # assert_raise do #Fails, no Exceptions are raised - # end - # - # assert_raise NameError do - # puts x #Raises NameError, so assertion succeeds - # end - def assert_raise(*exp, &b) - case exp.last - when String, Proc - msg = exp.pop - end - - 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, message(msg) {"#{mu_pp(exp)} exception expected, not"}.call) - } - - return e - end - - exp = exp.first if exp.size == 1 - - flunk(message(msg) {"#{mu_pp(exp)} expected but nothing was raised"}) - end - - def assert_raises(*exp, &b) - raise NoMethodError, "use assert_raise", caller - end - - # :call-seq: - # assert_raise_with_message(exception, expected, msg = nil, &block) - # - #Tests if the given block raises an exception with the expected - #message. - # - # assert_raise_with_message(RuntimeError, "foo") do - # nil #Fails, no Exceptions are raised - # end - # - # assert_raise_with_message(RuntimeError, "foo") do - # raise ArgumentError, "foo" #Fails, different Exception is raised - # end - # - # assert_raise_with_message(RuntimeError, "foo") do - # raise "bar" #Fails, RuntimeError is raised but the message differs - # end - # - # assert_raise_with_message(RuntimeError, "foo") do - # raise "foo" #Raises RuntimeError with the message, so assertion succeeds - # end - def assert_raise_with_message(exception, expected, msg = nil, &block) - case expected - when String - assert = :assert_equal - when Regexp - assert = :assert_match - else - raise TypeError, "Expected #{expected.inspect} to be a kind of String or Regexp, not #{expected.class}" - end - - ex = m = nil - EnvUtil.with_default_internal(expected.encoding) do - ex = assert_raise(exception, msg || proc {"Exception(#{exception}) with message matches to #{expected.inspect}"}) do - yield - end - m = ex.message - end - msg = message(msg, "") {"Expected Exception(#{exception}) was raised, but the message doesn't match"} - - if assert == :assert_equal - assert_equal(expected, m, msg) - else - msg = message(msg) { "Expected #{mu_pp expected} to match #{mu_pp m}" } - assert expected =~ m, msg - block.binding.eval("proc{|_|$~=_}").call($~) - end - ex - end - - # :call-seq: - # assert_nothing_raised( *args, &block ) - # - #If any exceptions are given as arguments, the assertion will - #fail if one of those exceptions are raised. Otherwise, the test fails - #if any exceptions are raised. - # - #The final argument may be a failure message. - # - # assert_nothing_raised RuntimeError do - # raise Exception #Assertion passes, Exception is not a RuntimeError - # end - # - # assert_nothing_raised do - # raise Exception #Assertion fails - # end - def assert_nothing_raised(*args) - self._assertions += 1 - if Module === args.last - msg = nil - else - msg = args.pop - end - begin - line = __LINE__; yield - rescue MiniTest::Skip - raise - rescue Exception => e - bt = e.backtrace - as = e.instance_of?(MiniTest::Assertion) - if as - ans = /\A#{Regexp.quote(__FILE__)}:#{line}:in /o - bt.reject! {|ln| ans =~ ln} - end - if ((args.empty? && !as) || - args.any? {|a| a.instance_of?(Module) ? e.is_a?(a) : e.class == a }) - msg = message(msg) { "Exception raised:\n<#{mu_pp(e)}>" } - raise MiniTest::Assertion, msg.call, bt - else - raise - end - end - end - - # :call-seq: - # assert_nothing_thrown( failure_message = nil, &block ) - # - #Fails if the given block uses a call to Kernel#throw, and - #returns the result of the block otherwise. - # - #An optional failure message may be provided as the final argument. - # - # assert_nothing_thrown "Something was thrown!" do - # throw :problem? - # end - def assert_nothing_thrown(msg=nil) - begin - ret = yield - rescue ArgumentError => error - raise error if /\Auncaught throw (.+)\z/m !~ error.message - msg = message(msg) { "<#{$1}> was thrown when nothing was expected" } - flunk(msg) - end - assert(true, "Expected nothing to be thrown") - ret - end - - # :call-seq: - # assert_throw( tag, failure_message = nil, &block ) - # - #Fails unless the given block throws +tag+, returns the caught - #value otherwise. - # - #An optional failure message may be provided as the final argument. - # - # tag = Object.new - # assert_throw(tag, "#{tag} was not thrown!") do - # throw tag - # end - def assert_throw(tag, msg = nil) - ret = catch(tag) do - begin - yield(tag) - rescue UncaughtThrowError => e - thrown = e.tag - end - msg = message(msg) { - "Expected #{mu_pp(tag)} to have been thrown"\ - "#{%Q[, not #{thrown}] if thrown}" - } - assert(false, msg) - end - assert(true) - ret - end - - # :call-seq: - # assert_equal( expected, actual, failure_message = nil ) - # - #Tests if +expected+ is equal to +actual+. - # - #An optional failure message may be provided as the final argument. - def assert_equal(exp, act, msg = nil) - msg = message(msg) { - exp_str = mu_pp(exp) - act_str = mu_pp(act) - exp_comment = '' - act_comment = '' - if exp_str == act_str - if (exp.is_a?(String) && act.is_a?(String)) || - (exp.is_a?(Regexp) && act.is_a?(Regexp)) - exp_comment = " (#{exp.encoding})" - act_comment = " (#{act.encoding})" - elsif exp.is_a?(Float) && act.is_a?(Float) - exp_str = "%\#.#{Float::DIG+2}g" % exp - act_str = "%\#.#{Float::DIG+2}g" % act - elsif exp.is_a?(Time) && act.is_a?(Time) - if exp.subsec * 1000_000_000 == exp.nsec - exp_comment = " (#{exp.nsec}[ns])" - else - exp_comment = " (subsec=#{exp.subsec})" - end - if act.subsec * 1000_000_000 == act.nsec - act_comment = " (#{act.nsec}[ns])" - else - act_comment = " (subsec=#{act.subsec})" - end - elsif exp.class != act.class - # a subclass of Range, for example. - exp_comment = " (#{exp.class})" - act_comment = " (#{act.class})" - end - elsif !Encoding.compatible?(exp_str, act_str) - if exp.is_a?(String) && act.is_a?(String) - exp_str = exp.dump - act_str = act.dump - exp_comment = " (#{exp.encoding})" - act_comment = " (#{act.encoding})" - else - exp_str = exp_str.dump - act_str = act_str.dump - end - end - "<#{exp_str}>#{exp_comment} expected but was\n<#{act_str}>#{act_comment}" - } - assert(exp == act, msg) - end - - # :call-seq: - # assert_not_nil( expression, failure_message = nil ) - # - #Tests if +expression+ is not nil. - # - #An optional failure message may be provided as the final argument. - def assert_not_nil(exp, msg=nil) - msg = message(msg) { "<#{mu_pp(exp)}> expected to not be nil" } - assert(!exp.nil?, msg) - end - - # :call-seq: - # assert_not_equal( expected, actual, failure_message = nil ) - # - #Tests if +expected+ is not equal to +actual+. - # - #An optional failure message may be provided as the final argument. - def assert_not_equal(exp, act, msg=nil) - msg = message(msg) { "<#{mu_pp(exp)}> expected to be != to\n<#{mu_pp(act)}>" } - assert(exp != act, msg) - end - - # :call-seq: - # assert_no_match( regexp, string, failure_message = nil ) - # - #Tests if the given Regexp does not match a given String. - # - #An optional failure message may be provided as the final argument. - def assert_no_match(regexp, string, msg=nil) - assert_instance_of(Regexp, regexp, "The first argument to assert_no_match should be a Regexp.") - self._assertions -= 1 - msg = message(msg) { "<#{mu_pp(regexp)}> expected to not match\n<#{mu_pp(string)}>" } - assert(regexp !~ string, msg) - end - - # :call-seq: - # assert_not_same( expected, actual, failure_message = nil ) - # - #Tests if +expected+ is not the same object as +actual+. - #This test uses Object#equal? to test equality. - # - #An optional failure message may be provided as the final argument. - # - # assert_not_same("x", "x") #Succeeds - def assert_not_same(expected, actual, message="") - msg = message(msg) { build_message(message, < -with id expected to not be equal\\? to - -with id . -EOT - assert(!actual.equal?(expected), msg) - end - - # :call-seq: - # assert_respond_to( object, method, failure_message = nil ) - # - #Tests if the given Object responds to +method+. - # - #An optional failure message may be provided as the final argument. - # - # assert_respond_to("hello", :reverse) #Succeeds - # assert_respond_to("hello", :does_not_exist) #Fails - def assert_respond_to(obj, (meth, *priv), msg = nil) - unless priv.empty? - msg = message(msg) { - "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}#{" privately" if priv[0]}" - } - return assert obj.respond_to?(meth, *priv), msg - end - #get rid of overcounting - if caller_locations(1, 1)[0].path.start_with?(MINI_DIR) - return if obj.respond_to?(meth) - end - super(obj, meth, msg) - end - - # :call-seq: - # assert_not_respond_to( object, method, failure_message = nil ) - # - #Tests if the given Object does not respond to +method+. - # - #An optional failure message may be provided as the final argument. - # - # assert_not_respond_to("hello", :reverse) #Fails - # assert_not_respond_to("hello", :does_not_exist) #Succeeds - def assert_not_respond_to(obj, (meth, *priv), msg = nil) - unless priv.empty? - msg = message(msg) { - "Expected #{mu_pp(obj)} (#{obj.class}) to not respond to ##{meth}#{" privately" if priv[0]}" - } - return assert !obj.respond_to?(meth, *priv), msg - end - #get rid of overcounting - if caller_locations(1, 1)[0].path.start_with?(MINI_DIR) - return unless obj.respond_to?(meth) - end - refute_respond_to(obj, meth, msg) - end - - # :call-seq: - # assert_send( +send_array+, failure_message = nil ) - # - # Passes if the method send returns a true value. - # - # +send_array+ is composed of: - # * A receiver - # * A method - # * Arguments to the method - # - # Example: - # assert_send(["Hello world", :include?, "Hello"]) # -> pass - # assert_send(["Hello world", :include?, "Goodbye"]) # -> fail - def assert_send send_ary, m = nil - recv, msg, *args = send_ary - m = message(m) { - if args.empty? - argsstr = "" - else - (argsstr = mu_pp(args)).sub!(/\A\[(.*)\]\z/m, '(\1)') - end - "Expected #{mu_pp(recv)}.#{msg}#{argsstr} to return true" - } - assert recv.__send__(msg, *args), m - end - - # :call-seq: - # assert_not_send( +send_array+, failure_message = nil ) - # - # Passes if the method send doesn't return a true value. - # - # +send_array+ is composed of: - # * A receiver - # * A method - # * Arguments to the method - # - # Example: - # assert_not_send([[1, 2], :member?, 1]) # -> fail - # assert_not_send([[1, 2], :member?, 4]) # -> pass - def assert_not_send send_ary, m = nil - recv, msg, *args = send_ary - m = message(m) { - if args.empty? - argsstr = "" - else - (argsstr = mu_pp(args)).sub!(/\A\[(.*)\]\z/m, '(\1)') - end - "Expected #{mu_pp(recv)}.#{msg}#{argsstr} to return false" - } - assert !recv.__send__(msg, *args), m - end - - ms = instance_methods(true).map {|sym| sym.to_s } - ms.grep(/\Arefute_/) do |m| - mname = ('assert_not_'.dup << m.to_s[/.*?_(.*)/, 1]) - alias_method(mname, m) unless ms.include? mname - end - alias assert_include assert_includes - alias assert_not_include assert_not_includes - - def assert_all?(obj, m = nil, &blk) - failed = [] - obj.each do |*a, &b| - unless blk.call(*a, &b) - failed << (a.size > 1 ? a : a[0]) - end - end - assert(failed.empty?, message(m) {failed.pretty_inspect}) - end - - def assert_not_all?(obj, m = nil, &blk) - failed = [] - obj.each do |*a, &b| - if blk.call(*a, &b) - failed << a.size > 1 ? a : a[0] - end - end - assert(failed.empty?, message(m) {failed.pretty_inspect}) - end - - # compatibility with test-unit - alias pend skip - - if defined?(RubyVM::InstructionSequence) - def syntax_check(code, fname, line) - code = code.dup.force_encoding(Encoding::UTF_8) - RubyVM::InstructionSequence.compile(code, fname, fname, line) - :ok - end - else - def syntax_check(code, fname, line) - code = code.b - code.sub!(/\A(?:\xef\xbb\xbf)?(\s*\#.*$)*(\n)?/n) { - "#$&#{"\n" if $1 && !$2}BEGIN{throw tag, :ok}\n" - } - code = code.force_encoding(Encoding::UTF_8) - catch {|tag| eval(code, binding, fname, line - 1)} - end - end - - def prepare_syntax_check(code, fname = caller_locations(2, 1)[0], mesg = fname.to_s, verbose: nil) - verbose, $VERBOSE = $VERBOSE, verbose - case - when Array === fname - fname, line = *fname - when defined?(fname.path) && defined?(fname.lineno) - fname, line = fname.path, fname.lineno - else - line = 1 - end - yield(code, fname, line, mesg) - ensure - $VERBOSE = verbose - end - - def assert_valid_syntax(code, *args) - prepare_syntax_check(code, *args) do |src, fname, line, mesg| - yield if defined?(yield) - assert_nothing_raised(SyntaxError, mesg) do - assert_equal(:ok, syntax_check(src, fname, line), mesg) - end - end - end - - def assert_syntax_error(code, error, *args) - prepare_syntax_check(code, *args) do |src, fname, line, mesg| - yield if defined?(yield) - e = assert_raise(SyntaxError, mesg) do - syntax_check(src, fname, line) - end - assert_match(error, e.message, mesg) - e - end - end - - def assert_normal_exit(testsrc, message = '', child_env: nil, **opt) - assert_valid_syntax(testsrc, caller_locations(1, 1)[0]) - if child_env - child_env = [child_env] - else - child_env = [] - end - out, _, status = EnvUtil.invoke_ruby(child_env + %W'-W0', testsrc, true, :merge_to_stdout, **opt) - assert !status.signaled?, FailDesc[status, message, out] - end - - FailDesc = proc do |status, message = "", out = ""| - pid = status.pid - now = Time.now - faildesc = proc do - if signo = status.termsig - signame = Signal.signame(signo) - sigdesc = "signal #{signo}" - end - log = EnvUtil.diagnostic_reports(signame, pid, now) - if signame - sigdesc = "SIG#{signame} (#{sigdesc})" - end - if status.coredump? - sigdesc = "#{sigdesc} (core dumped)" - end - full_message = ''.dup - message = message.call if Proc === message - if message and !message.empty? - full_message << message << "\n" - end - full_message << "pid #{pid}" - full_message << " exit #{status.exitstatus}" if status.exited? - full_message << " killed by #{sigdesc}" if sigdesc - if out and !out.empty? - full_message << "\n" << out.b.gsub(/^/, '| ') - full_message.sub!(/(? marshal_error - ignore_stderr = nil - end - if res - if bt = res.backtrace - bt.each do |l| - l.sub!(/\A-:(\d+)/){"#{file}:#{line + $1.to_i}"} - end - bt.concat(caller) - else - res.set_backtrace(caller) - end - raise res unless SystemExit === res - end - - # really is it succeed? - unless ignore_stderr - # the body of assert_separately must not output anything to detect error - assert(stderr.empty?, FailDesc[status, "assert_separately failed with error message", stderr]) - end - assert(status.success?, FailDesc[status, "assert_separately failed", stderr]) - raise marshal_error if marshal_error - end - - def assert_warning(pat, msg = nil) - stderr = EnvUtil.verbose_warning { - EnvUtil.with_default_internal(pat.encoding) { - yield - } - } - msg = message(msg) {diff pat, stderr} - assert(pat === stderr, msg) - end - - def assert_warn(*args) - assert_warning(*args) {$VERBOSE = false; yield} - end - - def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: false, **opt) - require_relative '../../memory_status' - raise MiniTest::Skip, "unsupported platform" unless defined?(Memory::Status) - - token = "\e[7;1m#{$$.to_s}:#{Time.now.strftime('%s.%L')}:#{rand(0x10000).to_s(16)}:\e[m" - token_dump = token.dump - token_re = Regexp.quote(token) - envs = args.shift if Array === args and Hash === args.first - args = [ - "--disable=gems", - "-r", File.expand_path("../../../memory_status", __FILE__), - *args, - "-v", "-", - ] - if defined? Memory::NO_MEMORY_LEAK_ENVS then - envs ||= {} - newenvs = envs.merge(Memory::NO_MEMORY_LEAK_ENVS) { |_, _, _| break } - envs = newenvs if newenvs - end - args.unshift(envs) if envs - cmd = [ - 'END {STDERR.puts '"#{token_dump}"'"FINAL=#{Memory::Status.new}"}', - prepare, - 'STDERR.puts('"#{token_dump}"'"START=#{$initial_status = Memory::Status.new}")', - '$initial_size = $initial_status.size', - code, - 'GC.start', - ].join("\n") - _, err, status = EnvUtil.invoke_ruby(args, cmd, true, true, **opt) - before = err.sub!(/^#{token_re}START=(\{.*\})\n/, '') && Memory::Status.parse($1) - after = err.sub!(/^#{token_re}FINAL=(\{.*\})\n/, '') && Memory::Status.parse($1) - assert(status.success?, FailDesc[status, message, err]) - ([:size, (rss && :rss)] & after.members).each do |n| - b = before[n] - a = after[n] - next unless a > 0 and b > 0 - assert_operator(a.fdiv(b), :<, limit, message(message) {"#{n}: #{b} => #{a}"}) - end - rescue LoadError - skip - end - - def assert_cpu_usage_low(msg = nil, pct: 0.01) - require 'benchmark' - - tms = Benchmark.measure(msg || '') { yield } - max = pct * tms.real - if tms.real < 0.1 # TIME_QUANTUM_USEC in thread_pthread.c - warn "test #{msg || 'assert_cpu_usage_low'} too short to be accurate" - end - - # kernel resolution can limit the minimum time we can measure - # [ruby-core:81540] - min_hz = windows? ? 67 : 100 - min_measurable = 1.0 / min_hz - min_measurable *= 1.10 # add a little (10%) to account for misc. overheads - if max < min_measurable - max = min_measurable - end - - assert_operator tms.total, :<=, max, msg - end - - def assert_is_minus_zero(f) - assert(1.0/f == -Float::INFINITY, "#{f} is not -0.0") - end - - def assert_file - AssertFile - end - - # pattern_list is an array which contains regexp and :*. - # :* means any sequence. - # - # pattern_list is anchored. - # Use [:*, regexp, :*] for non-anchored match. - def assert_pattern_list(pattern_list, actual, message=nil) - rest = actual - anchored = true - pattern_list.each_with_index {|pattern, i| - if pattern == :* - anchored = false - else - if anchored - match = /\A#{pattern}/.match(rest) - else - match = pattern.match(rest) - end - unless match - msg = message(msg) { - expect_msg = "Expected #{mu_pp pattern}\n" - if /\n[^\n]/ =~ rest - actual_mesg = "to match\n" - rest.scan(/.*\n+/) { - actual_mesg << ' ' << $&.inspect << "+\n" - } - actual_mesg.sub!(/\+\n\z/, '') - else - actual_mesg = "to match #{mu_pp rest}" - end - actual_mesg << "\nafter #{i} patterns with #{actual.length - rest.length} characters" - expect_msg + actual_mesg - } - assert false, msg - end - rest = match.post_match - anchored = true - end - } - if anchored - assert_equal("", rest) - end - end - - # threads should respond to shift method. - # Array can be used. - def assert_join_threads(threads, message = nil) - errs = [] - values = [] - while th = threads.shift - begin - values << th.value - rescue Exception - errs << [th, $!] - end - end - if !errs.empty? - msg = "exceptions on #{errs.length} threads:\n" + - errs.map {|t, err| - "#{t.inspect}:\n" + - err.backtrace.map.with_index {|line, i| - if i == 0 - "#{line}: #{err.message} (#{err.class})" - else - "\tfrom #{line}" - end - }.join("\n") - }.join("\n---\n") - if message - msg = "#{message}\n#{msg}" - end - raise MiniTest::Assertion, msg - end - values - end - - class << (AssertFile = Struct.new(:failure_message).new) - include Assertions - def assert_file_predicate(predicate, *args) - if /\Anot_/ =~ predicate - predicate = $' - neg = " not" - end - result = File.__send__(predicate, *args) - result = !result if neg - mesg = "Expected file ".dup << args.shift.inspect - mesg << "#{neg} to be #{predicate}" - mesg << mu_pp(args).sub(/\A\[(.*)\]\z/m, '(\1)') unless args.empty? - mesg << " #{failure_message}" if failure_message - assert(result, mesg) - end - alias method_missing assert_file_predicate - - def for(message) - clone.tap {|a| a.failure_message = message} - end - end - - class AllFailures - attr_reader :failures - - def initialize - @count = 0 - @failures = {} - end - - def for(key) - @count += 1 - yield - rescue Exception => e - @failures[key] = [@count, e] - end - - def foreach(*keys) - keys.each do |key| - @count += 1 - begin - yield key - rescue Exception => e - @failures[key] = [@count, e] - end - end - end - - def message - i = 0 - total = @count.to_s - fmt = "%#{total.size}d" - @failures.map {|k, (n, v)| - "\n#{i+=1}. [#{fmt%n}/#{total}] Assertion for #{k.inspect}\n#{v.message.b.gsub(/^/, ' | ')}" - }.join("\n") - end - - def pass? - @failures.empty? - end - end - - def assert_all_assertions(msg = nil) - all = AllFailures.new - yield all - ensure - assert(all.pass?, message(msg) {all.message.chomp(".")}) - end - alias all_assertions assert_all_assertions - - def assert_all_assertions_foreach(msg = nil, *keys, &block) - all = AllFailures.new - all.foreach(*keys, &block) - ensure - assert(all.pass?, message(msg) {all.message.chomp(".")}) - end - alias all_assertions_foreach assert_all_assertions_foreach - - def build_message(head, template=nil, *arguments) #:nodoc: - template &&= template.chomp - template.gsub(/\G((?:[^\\]|\\.)*?)(\\)?\?/) { $1 + ($2 ? "?" : mu_pp(arguments.shift)) } - end - - def message(msg = nil, *args, &default) # :nodoc: - if Proc === msg - super(nil, *args) do - ary = [msg.call, (default.call if default)].compact.reject(&:empty?) - if 1 < ary.length - ary[0...-1] = ary[0...-1].map {|str| str.sub(/(? e - begin - trace = e.backtrace || ['unknown method'] - err = ["#{trace.shift}: #{e.message} (#{e.class})"] + trace.map{|t| t.prepend("\t") } - - _report "bye", Marshal.dump(err.join("\n")) - rescue Errno::EPIPE;end - exit - ensure - @stdin.close if @stdin - @stdout.close if @stdout - end - end - - def _report(res, *args) # :nodoc: - @stdout.write(args.empty? ? "#{res}\n" : "#{res} #{args.pack("m0")}\n") - end - - def puke(klass, meth, e) # :nodoc: - if e.is_a?(MiniTest::Skip) - new_e = MiniTest::Skip.new(e.message) - new_e.set_backtrace(e.backtrace) - e = new_e - end - @partial_report << [klass.name, meth, e.is_a?(MiniTest::Assertion) ? e : ProxyError.new(e)] - super - end - - def record(suite, method, assertions, time, error) # :nodoc: - case error - when nil - when MiniTest::Assertion, MiniTest::Skip - case error.cause - when nil, MiniTest::Assertion, MiniTest::Skip - else - bt = error.backtrace - error = error.class.new(error.message) - error.set_backtrace(bt) - end - else - error = ProxyError.new(error) - end - _report "record", Marshal.dump([suite.name, method, assertions, time, error]) - super - end - end - end -end - -if $0 == __FILE__ - module Test - module Unit - class TestCase < MiniTest::Unit::TestCase # :nodoc: all - undef on_parallel_worker? - def on_parallel_worker? - true - end - end - end - end - require 'rubygems' - module Gem # :nodoc: - end - class Gem::TestCase < MiniTest::Unit::TestCase # :nodoc: - @@project_dir = File.expand_path('../../../../..', __FILE__) - end - - Test::Unit::Worker.new.run(ARGV) -end diff --git a/test/lib/test/unit/testcase.rb b/test/lib/test/unit/testcase.rb deleted file mode 100644 index 58cfbcab..00000000 --- a/test/lib/test/unit/testcase.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true -require 'test/unit/assertions' - -module Test - module Unit - # remove silly TestCase class - remove_const(:TestCase) if defined?(self::TestCase) - - class TestCase < MiniTest::Unit::TestCase # :nodoc: all - include Assertions - - def on_parallel_worker? - false - end - - def run runner - @options = runner.options - super runner - end - - def self.test_order - :sorted - end - - def self.method_added(name) - super - return unless name.to_s.start_with?("test_") - @test_methods ||= {} - if @test_methods[name] - warn "test/unit warning: method #{ self }##{ name } is redefined" - end - @test_methods[name] = true - end - end - end -end From 9b40c7955f114c89b21dd762cd4ae5161b6dce99 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 16 Feb 2021 21:34:30 +0900 Subject: [PATCH 03/66] Use test-unit and CoreAssertions instead of deep copy from ruby core repository --- Rakefile | 13 +- test/lib/core_assertions.rb | 763 ++++++++++++++++++++++++++++++++++++ test/lib/envutil.rb | 365 +++++++++++++++++ test/lib/find_executable.rb | 22 ++ test/lib/helper.rb | 4 + 5 files changed, 1164 insertions(+), 3 deletions(-) create mode 100644 test/lib/core_assertions.rb create mode 100644 test/lib/envutil.rb create mode 100644 test/lib/find_executable.rb create mode 100644 test/lib/helper.rb diff --git a/Rakefile b/Rakefile index 43a5a3f5..d81dd1f4 100644 --- a/Rakefile +++ b/Rakefile @@ -2,9 +2,16 @@ 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 :sync_tool do + require 'fileutils' + FileUtils.cp "../ruby/tool/lib/test/unit/core_assertions.rb", "./test/lib" + FileUtils.cp "../ruby/tool/lib/envutil.rb", "./test/lib" + FileUtils.cp "../ruby/tool/lib/find_executable.rb", "./test/lib" end task :default => :test diff --git a/test/lib/core_assertions.rb b/test/lib/core_assertions.rb new file mode 100644 index 00000000..118c0d11 --- /dev/null +++ b/test/lib/core_assertions.rb @@ -0,0 +1,763 @@ +# frozen_string_literal: true + +module Test + module Unit + module Assertions + def _assertions= n # :nodoc: + @_assertions = n + end + + def _assertions # :nodoc: + @_assertions ||= 0 + end + + ## + # Returns a proc that will output +msg+ along with the default message. + + def message msg = nil, ending = nil, &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 + end + + module CoreAssertions + if defined?(MiniTest) + require_relative '../../envutil' + # for ruby core testing + include MiniTest::Assertions + + # Compatibility hack for assert_raise + Test::Unit::AssertionFailedError = MiniTest::Assertion + else + module MiniTest + class Assertion < Exception; end + class Skip < Assertion; end + end + + require 'pp' + require_relative 'envutil' + include Test::Unit::Assertions + end + + def mu_pp(obj) #:nodoc: + obj.pretty_inspect.chomp + end + + def assert_file + AssertFile + end + + FailDesc = proc do |status, message = "", out = ""| + now = Time.now + proc do + EnvUtil.failure_description(status, now, message, out) + end + end + + def assert_in_out_err(args, test_stdin = "", test_stdout = [], test_stderr = [], message = nil, + success: nil, **opt) + args = Array(args).dup + args.insert((Hash === args[0] ? 1 : 0), '--disable=gems') + stdout, stderr, status = EnvUtil.invoke_ruby(args, test_stdin, true, true, **opt) + desc = FailDesc[status, message, stderr] + if block_given? + raise "test_stdout ignored, use block only or without block" if test_stdout != [] + raise "test_stderr ignored, use block only or without block" if test_stderr != [] + yield(stdout.lines.map {|l| l.chomp }, stderr.lines.map {|l| l.chomp }, status) + else + all_assertions(desc) do |a| + [["stdout", test_stdout, stdout], ["stderr", test_stderr, stderr]].each do |key, exp, act| + a.for(key) do + if exp.is_a?(Regexp) + assert_match(exp, act) + elsif exp.all? {|e| String === e} + assert_equal(exp, act.lines.map {|l| l.chomp }) + else + assert_pattern_list(exp, act) + end + end + end + unless success.nil? + a.for("success?") do + if success + assert_predicate(status, :success?) + else + assert_not_predicate(status, :success?) + end + end + end + end + status + end + end + + if defined?(RubyVM::InstructionSequence) + def syntax_check(code, fname, line) + code = code.dup.force_encoding(Encoding::UTF_8) + RubyVM::InstructionSequence.compile(code, fname, fname, line) + :ok + ensure + raise if SyntaxError === $! + end + else + def syntax_check(code, fname, line) + code = code.b + code.sub!(/\A(?:\xef\xbb\xbf)?(\s*\#.*$)*(\n)?/n) { + "#$&#{"\n" if $1 && !$2}BEGIN{throw tag, :ok}\n" + } + code = code.force_encoding(Encoding::UTF_8) + catch {|tag| eval(code, binding, fname, line - 1)} + end + end + + def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: false, **opt) + # TODO: consider choosing some appropriate limit for MJIT and stop skipping this once it does not randomly fail + pend 'assert_no_memory_leak may consider MJIT memory usage as leak' if defined?(RubyVM::JIT) && RubyVM::JIT.enabled? + + require_relative '../../memory_status' + raise MiniTest::Skip, "unsupported platform" unless defined?(Memory::Status) + + token = "\e[7;1m#{$$.to_s}:#{Time.now.strftime('%s.%L')}:#{rand(0x10000).to_s(16)}:\e[m" + token_dump = token.dump + token_re = Regexp.quote(token) + envs = args.shift if Array === args and Hash === args.first + args = [ + "--disable=gems", + "-r", File.expand_path("../../../memory_status", __FILE__), + *args, + "-v", "-", + ] + if defined? Memory::NO_MEMORY_LEAK_ENVS then + envs ||= {} + newenvs = envs.merge(Memory::NO_MEMORY_LEAK_ENVS) { |_, _, _| break } + envs = newenvs if newenvs + end + args.unshift(envs) if envs + cmd = [ + 'END {STDERR.puts '"#{token_dump}"'"FINAL=#{Memory::Status.new}"}', + prepare, + 'STDERR.puts('"#{token_dump}"'"START=#{$initial_status = Memory::Status.new}")', + '$initial_size = $initial_status.size', + code, + 'GC.start', + ].join("\n") + _, err, status = EnvUtil.invoke_ruby(args, cmd, true, true, **opt) + before = err.sub!(/^#{token_re}START=(\{.*\})\n/, '') && Memory::Status.parse($1) + after = err.sub!(/^#{token_re}FINAL=(\{.*\})\n/, '') && Memory::Status.parse($1) + assert(status.success?, FailDesc[status, message, err]) + ([:size, (rss && :rss)] & after.members).each do |n| + b = before[n] + a = after[n] + next unless a > 0 and b > 0 + assert_operator(a.fdiv(b), :<, limit, message(message) {"#{n}: #{b} => #{a}"}) + end + rescue LoadError + pend + end + + # :call-seq: + # assert_nothing_raised( *args, &block ) + # + #If any exceptions are given as arguments, the assertion will + #fail if one of those exceptions are raised. Otherwise, the test fails + #if any exceptions are raised. + # + #The final argument may be a failure message. + # + # assert_nothing_raised RuntimeError do + # raise Exception #Assertion passes, Exception is not a RuntimeError + # end + # + # assert_nothing_raised do + # raise Exception #Assertion fails + # end + def assert_nothing_raised(*args) + self._assertions += 1 + if Module === args.last + msg = nil + else + msg = args.pop + end + begin + line = __LINE__; yield + rescue MiniTest::Skip + raise + rescue Exception => e + bt = e.backtrace + as = e.instance_of?(MiniTest::Assertion) + if as + ans = /\A#{Regexp.quote(__FILE__)}:#{line}:in /o + bt.reject! {|ln| ans =~ ln} + end + if ((args.empty? && !as) || + args.any? {|a| a.instance_of?(Module) ? e.is_a?(a) : e.class == a }) + msg = message(msg) { + "Exception raised:\n<#{mu_pp(e)}>\n" + + "Backtrace:\n" + + e.backtrace.map{|frame| " #{frame}"}.join("\n") + } + raise MiniTest::Assertion, msg.call, bt + else + raise + end + end + end + + def prepare_syntax_check(code, fname = nil, mesg = nil, verbose: nil) + fname ||= caller_locations(2, 1)[0] + mesg ||= fname.to_s + verbose, $VERBOSE = $VERBOSE, verbose + case + when Array === fname + fname, line = *fname + when defined?(fname.path) && defined?(fname.lineno) + fname, line = fname.path, fname.lineno + else + line = 1 + end + yield(code, fname, line, message(mesg) { + if code.end_with?("\n") + "```\n#{code}```\n" + else + "```\n#{code}\n```\n""no-newline" + end + }) + ensure + $VERBOSE = verbose + end + + def assert_valid_syntax(code, *args, **opt) + prepare_syntax_check(code, *args, **opt) do |src, fname, line, mesg| + yield if defined?(yield) + assert_nothing_raised(SyntaxError, mesg) do + assert_equal(:ok, syntax_check(src, fname, line), mesg) + end + end + end + + def assert_normal_exit(testsrc, message = '', child_env: nil, **opt) + assert_valid_syntax(testsrc, caller_locations(1, 1)[0]) + if child_env + child_env = [child_env] + else + child_env = [] + end + out, _, status = EnvUtil.invoke_ruby(child_env + %W'-W0', testsrc, true, :merge_to_stdout, **opt) + assert !status.signaled?, FailDesc[status, message, out] + end + + def assert_ruby_status(args, test_stdin="", message=nil, **opt) + out, _, status = EnvUtil.invoke_ruby(args, test_stdin, true, :merge_to_stdout, **opt) + desc = FailDesc[status, message, out] + assert(!status.signaled?, desc) + message ||= "ruby exit status is not success:" + assert(status.success?, desc) + end + + ABORT_SIGNALS = Signal.list.values_at(*%w"ILL ABRT BUS SEGV TERM") + + def separated_runner(out = nil) + out = out ? IO.new(out, 'w') : STDOUT + at_exit { + out.puts [Marshal.dump($!)].pack('m'), "assertions=\#{self._assertions}" + } + Test::Unit::Runner.class_variable_set(:@@stop_auto_run, true) if defined?(Test::Unit::Runner) + end + + def assert_separately(args, file = nil, line = nil, src, ignore_stderr: nil, **opt) + unless file and line + loc, = caller_locations(1,1) + file ||= loc.path + line ||= loc.lineno + end + capture_stdout = true + unless /mswin|mingw/ =~ RUBY_PLATFORM + capture_stdout = false + opt[:out] = MiniTest::Unit.output if defined?(MiniTest::Unit) + res_p, res_c = IO.pipe + opt[res_c.fileno] = res_c.fileno + end + src = < marshal_error + ignore_stderr = nil + res = nil + end + if res and !(SystemExit === res) + if bt = res.backtrace + bt.each do |l| + l.sub!(/\A-:(\d+)/){"#{file}:#{line + $1.to_i}"} + end + bt.concat(caller) + else + res.set_backtrace(caller) + end + raise res + end + + # really is it succeed? + unless ignore_stderr + # the body of assert_separately must not output anything to detect error + assert(stderr.empty?, FailDesc[status, "assert_separately failed with error message", stderr]) + end + assert(status.success?, FailDesc[status, "assert_separately failed", stderr]) + raise marshal_error if marshal_error + end + + # Run Ractor-related test without influencing the main test suite + def assert_ractor(src, args: [], require: nil, require_relative: nil, file: nil, line: nil, ignore_stderr: nil, **opt) + return unless defined?(Ractor) + + require = "require #{require.inspect}" if require + if require_relative + dir = File.dirname(caller_locations[0,1][0].absolute_path) + full_path = File.expand_path(require_relative, dir) + require = "#{require}; require #{full_path.inspect}" + end + + assert_separately(args, file, line, <<~RUBY, ignore_stderr: ignore_stderr, **opt) + #{require} + previous_verbose = $VERBOSE + $VERBOSE = nil + Ractor.new {} # trigger initial warning + $VERBOSE = previous_verbose + #{src} + RUBY + end + + # :call-seq: + # assert_throw( tag, failure_message = nil, &block ) + # + #Fails unless the given block throws +tag+, returns the caught + #value otherwise. + # + #An optional failure message may be provided as the final argument. + # + # tag = Object.new + # assert_throw(tag, "#{tag} was not thrown!") do + # throw tag + # end + def assert_throw(tag, msg = nil) + ret = catch(tag) do + begin + yield(tag) + rescue UncaughtThrowError => e + thrown = e.tag + end + msg = message(msg) { + "Expected #{mu_pp(tag)} to have been thrown"\ + "#{%Q[, not #{thrown}] if thrown}" + } + assert(false, msg) + end + assert(true) + ret + end + + # :call-seq: + # assert_raise( *args, &block ) + # + #Tests if the given block raises an exception. Acceptable exception + #types may be given as optional arguments. If the last argument is a + #String, it will be used as the error message. + # + # assert_raise do #Fails, no Exceptions are raised + # end + # + # assert_raise NameError do + # puts x #Raises NameError, so assertion succeeds + # end + def assert_raise(*exp, &b) + case exp.last + when String, Proc + msg = exp.pop + end + + 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 { + flunk(message(msg) {"#{mu_pp(exp)} exception expected, not #{mu_pp(e)}"}) + } + + return e + ensure + unless e + exp = exp.first if exp.size == 1 + + flunk(message(msg) {"#{mu_pp(exp)} expected but nothing was raised"}) + end + end + end + + # :call-seq: + # assert_raise_with_message(exception, expected, msg = nil, &block) + # + #Tests if the given block raises an exception with the expected + #message. + # + # assert_raise_with_message(RuntimeError, "foo") do + # nil #Fails, no Exceptions are raised + # end + # + # assert_raise_with_message(RuntimeError, "foo") do + # raise ArgumentError, "foo" #Fails, different Exception is raised + # end + # + # assert_raise_with_message(RuntimeError, "foo") do + # raise "bar" #Fails, RuntimeError is raised but the message differs + # end + # + # assert_raise_with_message(RuntimeError, "foo") do + # raise "foo" #Raises RuntimeError with the message, so assertion succeeds + # end + def assert_raise_with_message(exception, expected, msg = nil, &block) + case expected + when String + assert = :assert_equal + when Regexp + assert = :assert_match + else + raise TypeError, "Expected #{expected.inspect} to be a kind of String or Regexp, not #{expected.class}" + end + + ex = m = nil + EnvUtil.with_default_internal(expected.encoding) do + ex = assert_raise(exception, msg || proc {"Exception(#{exception}) with message matches to #{expected.inspect}"}) do + yield + end + m = ex.message + end + msg = message(msg, "") {"Expected Exception(#{exception}) was raised, but the message doesn't match"} + + if assert == :assert_equal + assert_equal(expected, m, msg) + else + msg = message(msg) { "Expected #{mu_pp expected} to match #{mu_pp m}" } + assert expected =~ m, msg + block.binding.eval("proc{|_|$~=_}").call($~) + end + ex + end + + MINI_DIR = File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), "minitest") #:nodoc: + + # :call-seq: + # assert(test, [failure_message]) + # + #Tests if +test+ is true. + # + #+msg+ may be a String or a Proc. If +msg+ is a String, it will be used + #as the failure message. Otherwise, the result of calling +msg+ will be + #used as the message if the assertion fails. + # + #If no +msg+ is given, a default message will be used. + # + # assert(false, "This was expected to be true") + def assert(test, *msgs) + case msg = msgs.first + when String, Proc + when nil + msgs.shift + else + bt = caller.reject { |s| s.start_with?(MINI_DIR) } + raise ArgumentError, "assertion message must be String or Proc, but #{msg.class} was given.", bt + end unless msgs.empty? + super + end + + # :call-seq: + # assert_respond_to( object, method, failure_message = nil ) + # + #Tests if the given Object responds to +method+. + # + #An optional failure message may be provided as the final argument. + # + # assert_respond_to("hello", :reverse) #Succeeds + # assert_respond_to("hello", :does_not_exist) #Fails + def assert_respond_to(obj, (meth, *priv), msg = nil) + unless priv.empty? + msg = message(msg) { + "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}#{" privately" if priv[0]}" + } + return assert obj.respond_to?(meth, *priv), msg + end + #get rid of overcounting + if caller_locations(1, 1)[0].path.start_with?(MINI_DIR) + return if obj.respond_to?(meth) + end + super(obj, meth, msg) + end + + # :call-seq: + # assert_not_respond_to( object, method, failure_message = nil ) + # + #Tests if the given Object does not respond to +method+. + # + #An optional failure message may be provided as the final argument. + # + # assert_not_respond_to("hello", :reverse) #Fails + # assert_not_respond_to("hello", :does_not_exist) #Succeeds + def assert_not_respond_to(obj, (meth, *priv), msg = nil) + unless priv.empty? + msg = message(msg) { + "Expected #{mu_pp(obj)} (#{obj.class}) to not respond to ##{meth}#{" privately" if priv[0]}" + } + return assert !obj.respond_to?(meth, *priv), msg + end + #get rid of overcounting + if caller_locations(1, 1)[0].path.start_with?(MINI_DIR) + return unless obj.respond_to?(meth) + end + refute_respond_to(obj, meth, msg) + end + + # pattern_list is an array which contains regexp and :*. + # :* means any sequence. + # + # pattern_list is anchored. + # Use [:*, regexp, :*] for non-anchored match. + def assert_pattern_list(pattern_list, actual, message=nil) + rest = actual + anchored = true + pattern_list.each_with_index {|pattern, i| + if pattern == :* + anchored = false + else + if anchored + match = /\A#{pattern}/.match(rest) + else + match = pattern.match(rest) + end + unless match + msg = message(msg) { + expect_msg = "Expected #{mu_pp pattern}\n" + if /\n[^\n]/ =~ rest + actual_mesg = +"to match\n" + rest.scan(/.*\n+/) { + actual_mesg << ' ' << $&.inspect << "+\n" + } + actual_mesg.sub!(/\+\n\z/, '') + else + actual_mesg = "to match " + mu_pp(rest) + end + actual_mesg << "\nafter #{i} patterns with #{actual.length - rest.length} characters" + expect_msg + actual_mesg + } + assert false, msg + end + rest = match.post_match + anchored = true + end + } + if anchored + assert_equal("", rest) + end + end + + def assert_warning(pat, msg = nil) + result = nil + stderr = EnvUtil.with_default_internal(pat.encoding) { + EnvUtil.verbose_warning { + result = yield + } + } + msg = message(msg) {diff pat, stderr} + assert(pat === stderr, msg) + result + end + + def assert_warn(*args) + assert_warning(*args) {$VERBOSE = false; yield} + end + + def assert_deprecated_warning(mesg = /deprecated/) + assert_warning(mesg) do + Warning[:deprecated] = true + yield + end + end + + def assert_deprecated_warn(mesg = /deprecated/) + assert_warn(mesg) do + Warning[:deprecated] = true + yield + end + end + + class << (AssertFile = Struct.new(:failure_message).new) + include CoreAssertions + def assert_file_predicate(predicate, *args) + if /\Anot_/ =~ predicate + predicate = $' + neg = " not" + end + result = File.__send__(predicate, *args) + result = !result if neg + mesg = "Expected file ".dup << args.shift.inspect + mesg << "#{neg} to be #{predicate}" + mesg << mu_pp(args).sub(/\A\[(.*)\]\z/m, '(\1)') unless args.empty? + mesg << " #{failure_message}" if failure_message + assert(result, mesg) + end + alias method_missing assert_file_predicate + + def for(message) + clone.tap {|a| a.failure_message = message} + end + end + + class AllFailures + attr_reader :failures + + def initialize + @count = 0 + @failures = {} + end + + def for(key) + @count += 1 + yield + rescue Exception => e + @failures[key] = [@count, e] + end + + def foreach(*keys) + keys.each do |key| + @count += 1 + begin + yield key + rescue Exception => e + @failures[key] = [@count, e] + end + end + end + + def message + i = 0 + total = @count.to_s + fmt = "%#{total.size}d" + @failures.map {|k, (n, v)| + v = v.message + "\n#{i+=1}. [#{fmt%n}/#{total}] Assertion for #{k.inspect}\n#{v.b.gsub(/^/, ' | ').force_encoding(v.encoding)}" + }.join("\n") + end + + def pass? + @failures.empty? + end + end + + # threads should respond to shift method. + # Array can be used. + def assert_join_threads(threads, message = nil) + errs = [] + values = [] + while th = threads.shift + begin + values << th.value + rescue Exception + errs << [th, $!] + th = nil + end + end + values + ensure + if th&.alive? + th.raise(Timeout::Error.new) + th.join rescue errs << [th, $!] + end + if !errs.empty? + msg = "exceptions on #{errs.length} threads:\n" + + errs.map {|t, err| + "#{t.inspect}:\n" + + RUBY_VERSION >= "2.5.0" ? err.full_message(highlight: false, order: :top) : err.message + }.join("\n---\n") + if message + msg = "#{message}\n#{msg}" + end + raise MiniTest::Assertion, msg + end + end + + def assert_all_assertions(msg = nil) + all = AllFailures.new + yield all + ensure + assert(all.pass?, message(msg) {all.message.chomp(".")}) + end + alias all_assertions assert_all_assertions + + def message(msg = nil, *args, &default) # :nodoc: + if Proc === msg + super(nil, *args) do + ary = [msg.call, (default.call if default)].compact.reject(&:empty?) + if 1 < ary.length + ary[0...-1] = ary[0...-1].map {|str| str.sub(/(? Date: Tue, 16 Feb 2021 21:34:39 +0900 Subject: [PATCH 04/66] Use Gemfile instead of Gem::Specification#add_development_dependency. --- Gemfile | 3 +++ webrick.gemspec | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index fa75df15..10284be2 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,6 @@ source 'https://rubygems.org' gemspec + +gem "rake" +gem "test-unit" diff --git a/webrick.gemspec b/webrick.gemspec index 2aab8560..f3f8370e 100644 --- a/webrick.gemspec +++ b/webrick.gemspec @@ -69,6 +69,4 @@ Gem::Specification.new do |s| "bug_tracker_uri" => "https://github.com/ruby/webrick/issues", } end - - s.add_development_dependency "rake" end From c8abb708c854641b2632d963acae555f39899c93 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 16 Feb 2021 21:38:14 +0900 Subject: [PATCH 05/66] Use Test::Unit::TestCase instead of Minitest::Unit::TestCase --- test/webrick/test_httpresponse.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/webrick/test_httpresponse.rb b/test/webrick/test_httpresponse.rb index 89a0f703..07648600 100644 --- a/test/webrick/test_httpresponse.rb +++ b/test/webrick/test_httpresponse.rb @@ -1,11 +1,10 @@ # frozen_string_literal: false require "webrick" -require "minitest/autorun" require "stringio" require "net/http" module WEBrick - class TestHTTPResponse < MiniTest::Unit::TestCase + class TestHTTPResponse < Test::Unit::TestCase class FakeLogger attr_reader :messages From 355221c561724eed5c21fe093b5c732884bb01a8 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 16 Feb 2021 21:54:01 +0900 Subject: [PATCH 06/66] Added module for helper methods --- test/webrick/utils.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/webrick/utils.rb b/test/webrick/utils.rb index 56d3a30e..d1a6ad52 100644 --- a/test/webrick/utils.rb +++ b/test/webrick/utils.rb @@ -39,6 +39,12 @@ class WEBrick::HTTPServlet::CGIHandler include Test::Unit::Assertions extend Test::Unit::Assertions + include Test::Unit::Util::Output + extend Test::Unit::Util::Output + + include Test::Unit::CoreAssertions + extend Test::Unit::CoreAssertions + module_function DefaultLogTester = lambda {|log, access_log| assert_equal([], log) } From f87aec09fce09c142d2aa5108987338114327e4c Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Tue, 2 Mar 2021 07:30:54 -0800 Subject: [PATCH 07/66] Revert "Allow empty POST and PUT requests without content length" This reverts commit 069e9b1908aad3a30a0dcf67b6d3bb13c3216d2c. Users reports this causes WEBrick to block forever if receiving a POST/PUT request with an empty body. --- lib/webrick/httprequest.rb | 2 +- test/webrick/test_httprequest.rb | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb index fe95aad9..ff2c8a8a 100644 --- a/lib/webrick/httprequest.rb +++ b/lib/webrick/httprequest.rb @@ -522,7 +522,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 diff --git a/test/webrick/test_httprequest.rb b/test/webrick/test_httprequest.rb index 759ccbda..a594f14f 100644 --- a/test/webrick/test_httprequest.rb +++ b/test/webrick/test_httprequest.rb @@ -425,18 +425,6 @@ def test_continue_not_sent assert_equal l, msg.size end - def test_empty_post - msg = <<-_end_of_message_ - POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1 - Host: test.ruby-lang.org:8080 - Content-Type: application/x-www-form-urlencoded - - _end_of_message_ - req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) - req.body - end - def test_bad_messages param = "foo=1;foo=2;foo=3;bar=x" msg = <<-_end_of_message_ From 1e4ff940bea29210562b3a48b0352446f104e9ad Mon Sep 17 00:00:00 2001 From: Mathieu Jobin Date: Sat, 20 Mar 2021 13:07:44 +0900 Subject: [PATCH 08/66] more rubies --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e1dee1d7..1c74a5f6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: name: build (${{ matrix.ruby }} / ${{ matrix.os }}) strategy: matrix: - ruby: [ 2.7, 2.6, 2.5, head ] + ruby: [ 3.0, 2.7, 2.6, 2.5, 2.4, 2.3, head ] os: [ ubuntu-latest, macos-latest ] runs-on: ${{ matrix.os }} steps: From 37e19c7aa9024f3c9fd07d0b7d5c7f954ddb5596 Mon Sep 17 00:00:00 2001 From: Mathieu Jobin Date: Sat, 20 Mar 2021 13:16:17 +0900 Subject: [PATCH 09/66] ruby 2.4 should be mininum now there is an argument error with 2.3 ........E =============================================================================== Error: test_slow_connect(TestWEBrickSSLServer): ArgumentError: wrong number of arguments (given 3, expected 1..2) /opt/hostedtoolcache/Ruby/2.3.8/x64/lib/ruby/2.3.0/timeout.rb:73:in `timeout' /home/runner/work/webrick/webrick/test/lib/envutil.rb:72:in `timeout' /home/runner/work/webrick/webrick/test/webrick/test_ssl_server.rb:58:in `test_slow_connect' 55: :SSLEnable => true, 56: :SSLCertName => "/C=JP/O=www.ruby-lang.org/CN=Ruby", 57: } => 58: EnvUtil.timeout(10) do 59: TestWEBrick.start_server(Echo, config) do |server, addr, port, log| 60: outer = TCPSocket.new(addr, port) 61: inner = TCPSocket.new(addr, port) =============================================================================== --- webrick.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webrick.gemspec b/webrick.gemspec index f3f8370e..e97a9635 100644 --- a/webrick.gemspec +++ b/webrick.gemspec @@ -57,7 +57,7 @@ Gem::Specification.new do |s| "lib/webrick/version.rb", "webrick.gemspec", ] - s.required_ruby_version = ">= 2.3.0" + s.required_ruby_version = ">= 2.4.0" s.authors = ["TAKAHASHI Masayoshi", "GOTOU YUUZOU", "Eric Wong"] s.email = [nil, nil, 'normal@ruby-lang.org'] From cf826ae39ece35a520d01b9373e3b4d656aecdff Mon Sep 17 00:00:00 2001 From: Mathieu Jobin Date: Sat, 20 Mar 2021 13:16:49 +0900 Subject: [PATCH 10/66] 3.0 needs quoting and removing 2.3 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1c74a5f6..2abb1a20 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: name: build (${{ matrix.ruby }} / ${{ matrix.os }}) strategy: matrix: - ruby: [ 3.0, 2.7, 2.6, 2.5, 2.4, 2.3, head ] + ruby: [ "3.0", 2.7, 2.6, 2.5, 2.4, head ] os: [ ubuntu-latest, macos-latest ] runs-on: ${{ matrix.os }} steps: From 6fcdac375bbf33a9781718903fe1f3cee54afb89 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 14 Apr 2021 12:29:59 +0900 Subject: [PATCH 11/66] Use stable version of actions/checkout --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2abb1a20..adf47df1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: os: [ ubuntu-latest, macos-latest ] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@master + - uses: actions/checkout@v2 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: From 3515081a51b91b730267ba2b224039ecfbf8bd7b Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 14 Apr 2021 12:30:17 +0900 Subject: [PATCH 12/66] Don't need to install bundler manually --- .github/workflows/test.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index adf47df1..1d93df90 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,8 +17,6 @@ jobs: with: ruby-version: ${{ matrix.ruby }} - name: Install dependencies - run: | - gem install bundler --no-document - bundle install + run: bundle install - name: Run test run: rake test From 88006dc379b221973fe0555ba14d837142fa3dd7 Mon Sep 17 00:00:00 2001 From: Kentaro Goto Date: Fri, 20 Aug 2021 20:22:31 +0900 Subject: [PATCH 13/66] Adds ico, fonts, videos, AVIF and WebP MIME type --- lib/webrick/httputils.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/webrick/httputils.rb b/lib/webrick/httputils.rb index 456384e7..4893ecb9 100644 --- a/lib/webrick/httputils.rb +++ b/lib/webrick/httputils.rb @@ -48,6 +48,7 @@ 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", @@ -65,6 +66,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 +76,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 +99,14 @@ 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", + "webp" => "image/webp", + "woff" => "font/woff", + "woff2" => "font/woff2", "xbm" => "image/x-xbitmap", "xhtml" => "text/html", "xls" => "application/vnd.ms-excel", From bd04d3abab43e0f8ae05a47ae6e66188c9f2d090 Mon Sep 17 00:00:00 2001 From: Olle Jonsson Date: Mon, 23 Aug 2021 09:32:40 +0200 Subject: [PATCH 14/66] add mime type for .webmanifest extension https://developer.mozilla.org/en-US/docs/Web/Manifest#deploying_a_manifest_with_the_link_tag https://w3c.github.io/manifest/#media-type-registration --- lib/webrick/httputils.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/webrick/httputils.rb b/lib/webrick/httputils.rb index 4893ecb9..05597f9e 100644 --- a/lib/webrick/httputils.rb +++ b/lib/webrick/httputils.rb @@ -104,6 +104,7 @@ def normalize_path(path) "txt" => "text/plain", "wasm" => "application/wasm", "webm" => "video/webm", + "webmanifest" => "application/manifest+json", "webp" => "image/webp", "woff" => "font/woff", "woff2" => "font/woff2", From 2cd72103e38497ae24126dae8b8eeaa031e8ba75 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Sat, 11 Sep 2021 10:14:37 +0900 Subject: [PATCH 15/66] Bump up the latest version of CoreAssertions --- Rakefile | 2 +- test/lib/core_assertions.rb | 67 ++++++++++++++++++++----------------- test/lib/envutil.rb | 6 ++-- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/Rakefile b/Rakefile index d81dd1f4..5a7afabd 100644 --- a/Rakefile +++ b/Rakefile @@ -9,7 +9,7 @@ end task :sync_tool do require 'fileutils' - FileUtils.cp "../ruby/tool/lib/test/unit/core_assertions.rb", "./test/lib" + FileUtils.cp "../ruby/tool/lib/core_assertions.rb", "./test/lib" FileUtils.cp "../ruby/tool/lib/envutil.rb", "./test/lib" FileUtils.cp "../ruby/tool/lib/find_executable.rb", "./test/lib" end diff --git a/test/lib/core_assertions.rb b/test/lib/core_assertions.rb index 118c0d11..44715255 100644 --- a/test/lib/core_assertions.rb +++ b/test/lib/core_assertions.rb @@ -24,23 +24,8 @@ def message msg = nil, ending = nil, &default end module CoreAssertions - if defined?(MiniTest) - require_relative '../../envutil' - # for ruby core testing - include MiniTest::Assertions - - # Compatibility hack for assert_raise - Test::Unit::AssertionFailedError = MiniTest::Assertion - else - module MiniTest - class Assertion < Exception; end - class Skip < Assertion; end - end - - require 'pp' - require_relative 'envutil' - include Test::Unit::Assertions - end + require_relative 'envutil' + require 'pp' def mu_pp(obj) #:nodoc: obj.pretty_inspect.chomp @@ -117,8 +102,8 @@ def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: fal # TODO: consider choosing some appropriate limit for MJIT and stop skipping this once it does not randomly fail pend 'assert_no_memory_leak may consider MJIT memory usage as leak' if defined?(RubyVM::JIT) && RubyVM::JIT.enabled? - require_relative '../../memory_status' - raise MiniTest::Skip, "unsupported platform" unless defined?(Memory::Status) + require_relative 'memory_status' + raise Test::Unit::PendedError, "unsupported platform" unless defined?(Memory::Status) token = "\e[7;1m#{$$.to_s}:#{Time.now.strftime('%s.%L')}:#{rand(0x10000).to_s(16)}:\e[m" token_dump = token.dump @@ -126,7 +111,7 @@ def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: fal envs = args.shift if Array === args and Hash === args.first args = [ "--disable=gems", - "-r", File.expand_path("../../../memory_status", __FILE__), + "-r", File.expand_path("../memory_status", __FILE__), *args, "-v", "-", ] @@ -183,11 +168,11 @@ def assert_nothing_raised(*args) end begin line = __LINE__; yield - rescue MiniTest::Skip + rescue Test::Unit::PendedError raise rescue Exception => e bt = e.backtrace - as = e.instance_of?(MiniTest::Assertion) + as = e.instance_of?(Test::Unit::AssertionFailedError) if as ans = /\A#{Regexp.quote(__FILE__)}:#{line}:in /o bt.reject! {|ln| ans =~ ln} @@ -199,7 +184,7 @@ def assert_nothing_raised(*args) "Backtrace:\n" + e.backtrace.map{|frame| " #{frame}"}.join("\n") } - raise MiniTest::Assertion, msg.call, bt + raise Test::Unit::AssertionFailedError, msg.call, bt else raise end @@ -260,9 +245,10 @@ def assert_ruby_status(args, test_stdin="", message=nil, **opt) ABORT_SIGNALS = Signal.list.values_at(*%w"ILL ABRT BUS SEGV TERM") def separated_runner(out = nil) + include(*Test::Unit::TestCase.ancestors.select {|c| !c.is_a?(Class) }) out = out ? IO.new(out, 'w') : STDOUT at_exit { - out.puts [Marshal.dump($!)].pack('m'), "assertions=\#{self._assertions}" + out.puts [Marshal.dump($!)].pack('m'), "assertions=#{self._assertions}" } Test::Unit::Runner.class_variable_set(:@@stop_auto_run, true) if defined?(Test::Unit::Runner) end @@ -276,14 +262,14 @@ def assert_separately(args, file = nil, line = nil, src, ignore_stderr: nil, **o capture_stdout = true unless /mswin|mingw/ =~ RUBY_PLATFORM capture_stdout = false - opt[:out] = MiniTest::Unit.output if defined?(MiniTest::Unit) + opt[:out] = Test::Unit::Runner.output if defined?(Test::Unit::Runner) res_p, res_c = IO.pipe - opt[res_c.fileno] = res_c.fileno + opt[:ios] = [res_c] end src = < e - return e if exp.include? MiniTest::Skip + rescue Test::Unit::PendedError => e + return e if exp.include? Test::Unit::PendedError raise e rescue Exception => e expected = exp.any? { |ex| @@ -477,7 +463,7 @@ def assert_raise_with_message(exception, expected, msg = nil, &block) ex end - MINI_DIR = File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), "minitest") #:nodoc: + MINI_DIR = File.join(File.dirname(File.expand_path(__FILE__)), "minitest") #:nodoc: # :call-seq: # assert(test, [failure_message]) @@ -623,6 +609,7 @@ def assert_deprecated_warn(mesg = /deprecated/) end class << (AssertFile = Struct.new(:failure_message).new) + include Assertions include CoreAssertions def assert_file_predicate(predicate, *args) if /\Anot_/ =~ predicate @@ -713,10 +700,20 @@ def assert_join_threads(threads, message = nil) if message msg = "#{message}\n#{msg}" end - raise MiniTest::Assertion, msg + raise Test::Unit::AssertionFailedError, msg end end + def assert_all?(obj, m = nil, &blk) + failed = [] + obj.each do |*a, &b| + unless blk.call(*a, &b) + failed << (a.size > 1 ? a : a[0]) + end + end + assert(failed.empty?, message(m) {failed.pretty_inspect}) + end + def assert_all_assertions(msg = nil) all = AllFailures.new yield all @@ -725,6 +722,14 @@ def assert_all_assertions(msg = nil) end alias all_assertions assert_all_assertions + def assert_all_assertions_foreach(msg = nil, *keys, &block) + all = AllFailures.new + all.foreach(*keys, &block) + ensure + assert(all.pass?, message(msg) {all.message.chomp(".")}) + end + alias all_assertions_foreach assert_all_assertions_foreach + def message(msg = nil, *args, &default) # :nodoc: if Proc === msg super(nil, *args) do diff --git a/test/lib/envutil.rb b/test/lib/envutil.rb index 937e1128..0391b90c 100644 --- a/test/lib/envutil.rb +++ b/test/lib/envutil.rb @@ -125,7 +125,7 @@ def terminate(pid, signal = :TERM, pgroup = nil, reprieve = 1) 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, + stdout_filter: nil, stderr_filter: nil, ios: nil, signal: :TERM, rubybin: EnvUtil.rubybin, precommand: nil, **opt) @@ -141,6 +141,8 @@ def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = out_p.set_encoding(encoding) if out_p err_p.set_encoding(encoding) if err_p end + ios.each {|i, o = i|opt[i] = o} if ios + c = "C" child_env = {} LANG_ENVS.each {|lc| child_env[lc] = c} @@ -152,7 +154,7 @@ def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = end child_env['ASAN_OPTIONS'] = ENV['ASAN_OPTIONS'] if ENV['ASAN_OPTIONS'] args = [args] if args.kind_of?(String) - pid = spawn(child_env, *precommand, rubybin, *args, **opt) + pid = spawn(child_env, *precommand, rubybin, *args, opt) in_c.close out_c&.close out_c = nil From 5bd917ad7d3352185596ce2be95fa03f9e9c884f Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Sat, 11 Sep 2021 10:19:29 +0900 Subject: [PATCH 16/66] Move specific depends of webrick repo to test helper --- test/lib/helper.rb | 5 +++++ test/webrick/utils.rb | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test/lib/helper.rb b/test/lib/helper.rb index 909f8f98..8c8c7526 100644 --- a/test/lib/helper.rb +++ b/test/lib/helper.rb @@ -2,3 +2,8 @@ require_relative "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/webrick/utils.rb b/test/webrick/utils.rb index d1a6ad52..a8568d0a 100644 --- a/test/webrick/utils.rb +++ b/test/webrick/utils.rb @@ -38,10 +38,6 @@ class WEBrick::HTTPServlet::CGIHandler require "test/unit" unless defined?(Test::Unit) include Test::Unit::Assertions extend Test::Unit::Assertions - - include Test::Unit::Util::Output - extend Test::Unit::Util::Output - include Test::Unit::CoreAssertions extend Test::Unit::CoreAssertions From 5666f2d56afb8408e8307f7fff70edfcc464c6e8 Mon Sep 17 00:00:00 2001 From: Finn Gao <10386624+printfinn@users.noreply.github.com> Date: Tue, 26 Oct 2021 16:36:26 +0800 Subject: [PATCH 17/66] Typo --- lib/webrick/server.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/webrick/server.rb b/lib/webrick/server.rb index 478b1ac9..f085d5d2 100644 --- a/lib/webrick/server.rb +++ b/lib/webrick/server.rb @@ -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] From 7fd238fbc2003255e45629ae2c09a19b128c8c1d Mon Sep 17 00:00:00 2001 From: Olle Jonsson Date: Tue, 26 Oct 2021 10:38:24 +0200 Subject: [PATCH 18/66] CI: use bundler-cache: true --- .github/workflows/test.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1d93df90..f8844ac4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,6 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - - name: Install dependencies - run: bundle install + bundler-cache: true # 'bundle install' and cache - name: Run test - run: rake test + run: bundle exec rake test From 6f9021347bb1bf82c4791a72b191ac7763914e8a Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 14 Dec 2021 20:29:32 -0800 Subject: [PATCH 19/66] s/RubyVM::JIT/RubyVM::MJIT/g --- test/lib/core_assertions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/core_assertions.rb b/test/lib/core_assertions.rb index 44715255..bac3856a 100644 --- a/test/lib/core_assertions.rb +++ b/test/lib/core_assertions.rb @@ -100,7 +100,7 @@ def syntax_check(code, fname, line) def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: false, **opt) # TODO: consider choosing some appropriate limit for MJIT and stop skipping this once it does not randomly fail - pend 'assert_no_memory_leak may consider MJIT memory usage as leak' if defined?(RubyVM::JIT) && RubyVM::JIT.enabled? + pend 'assert_no_memory_leak may consider MJIT memory usage as leak' if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? require_relative 'memory_status' raise Test::Unit::PendedError, "unsupported platform" unless defined?(Memory::Status) From b5c68768ed1be299349b740b7af0787e836781c0 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Mon, 3 Jan 2022 15:05:32 -0800 Subject: [PATCH 20/66] Fix invalid use of IP addresses in SNI Server Name Indication does not allow IP addresses (RFC 6066, section 3: `Literal IPv4 and IPv6 addresses are not permitted in "HostName".`). Recent versions of LibreSSL enforce this requirement, and the tests currently break when running on such versions. Use localhost instead of 127.0.0.1 to avoid the issue. This requires adding HTTPSNITest#connect, because s.hostname= is called before ssl_socket_connect. --- test/webrick/test_httpproxy.rb | 8 ++++---- test/webrick/test_https.rb | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/test/webrick/test_httpproxy.rb b/test/webrick/test_httpproxy.rb index 1c2f2fce..5a60abbf 100644 --- a/test/webrick/test_httpproxy.rb +++ b/test/webrick/test_httpproxy.rb @@ -282,7 +282,7 @@ def test_connect # 3. ------- GET or POST ----------> # key = TEST_KEY_RSA2048 - cert = make_certificate(key, "127.0.0.1") + cert = make_certificate(key, "localhost") s_config = { :SSLEnable =>true, :ServerName => "localhost", @@ -300,7 +300,7 @@ def test_connect res.body = "SSL #{req.request_method} #{req.path} #{req.body}" } TestWEBrick.start_httpproxy(config){|server, addr, port, log| - http = Net::HTTP.new("127.0.0.1", s_port, addr, port) + http = Net::HTTP.new("localhost", s_port, addr, port) http.use_ssl = true http.verify_callback = Proc.new do |preverify_ok, store_ctx| store_ctx.current_cert.to_der == cert.to_der @@ -398,7 +398,7 @@ def test_upstream_proxy # 3. ---------- GET or POST --------------> # key = TEST_KEY_RSA2048 - cert = make_certificate(key, "127.0.0.1") + cert = make_certificate(key, "localhost") s_config = { :SSLEnable =>true, :ServerName => "localhost", @@ -409,7 +409,7 @@ def test_upstream_proxy s_server.mount_proc("/"){|req2, res| res.body = "SSL #{req2.request_method} #{req2.path} #{req2.body}" } - http = Net::HTTP.new("127.0.0.1", s_port, addr, port, up_log.call + log.call + s_log.call) + http = Net::HTTP.new("localhost", s_port, addr, port, up_log.call + log.call + s_log.call) http.use_ssl = true http.verify_callback = Proc.new do |preverify_ok, store_ctx| store_ctx.current_cert.to_der == cert.to_der diff --git a/test/webrick/test_https.rb b/test/webrick/test_https.rb index ec0aac35..05f4058d 100644 --- a/test/webrick/test_https.rb +++ b/test/webrick/test_https.rb @@ -17,6 +17,11 @@ def empty_log.<<(str) class HTTPSNITest < ::Net::HTTP attr_accessor :sni_hostname + def connect + @address = 'localhost' if @address == '127.0.0.1' + super + end + def ssl_socket_connect(s, timeout) s.hostname = sni_hostname super From f81c2cc7c532a3d45d9a5f7032c508e18593424e Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Tue, 4 Jan 2022 09:20:25 -0800 Subject: [PATCH 21/66] Revert "Fix invalid use of IP addresses in SNI" This reverts commit b5c68768ed1be299349b740b7af0787e836781c0. The commit will not longer be needed after changes to net/http are merged to not set IP addresses as SNI server names. --- test/webrick/test_httpproxy.rb | 8 ++++---- test/webrick/test_https.rb | 5 ----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/test/webrick/test_httpproxy.rb b/test/webrick/test_httpproxy.rb index 5a60abbf..1c2f2fce 100644 --- a/test/webrick/test_httpproxy.rb +++ b/test/webrick/test_httpproxy.rb @@ -282,7 +282,7 @@ def test_connect # 3. ------- GET or POST ----------> # key = TEST_KEY_RSA2048 - cert = make_certificate(key, "localhost") + cert = make_certificate(key, "127.0.0.1") s_config = { :SSLEnable =>true, :ServerName => "localhost", @@ -300,7 +300,7 @@ def test_connect res.body = "SSL #{req.request_method} #{req.path} #{req.body}" } TestWEBrick.start_httpproxy(config){|server, addr, port, log| - http = Net::HTTP.new("localhost", s_port, addr, port) + http = Net::HTTP.new("127.0.0.1", s_port, addr, port) http.use_ssl = true http.verify_callback = Proc.new do |preverify_ok, store_ctx| store_ctx.current_cert.to_der == cert.to_der @@ -398,7 +398,7 @@ def test_upstream_proxy # 3. ---------- GET or POST --------------> # key = TEST_KEY_RSA2048 - cert = make_certificate(key, "localhost") + cert = make_certificate(key, "127.0.0.1") s_config = { :SSLEnable =>true, :ServerName => "localhost", @@ -409,7 +409,7 @@ def test_upstream_proxy s_server.mount_proc("/"){|req2, res| res.body = "SSL #{req2.request_method} #{req2.path} #{req2.body}" } - http = Net::HTTP.new("localhost", s_port, addr, port, up_log.call + log.call + s_log.call) + http = Net::HTTP.new("127.0.0.1", s_port, addr, port, up_log.call + log.call + s_log.call) http.use_ssl = true http.verify_callback = Proc.new do |preverify_ok, store_ctx| store_ctx.current_cert.to_der == cert.to_der diff --git a/test/webrick/test_https.rb b/test/webrick/test_https.rb index 05f4058d..ec0aac35 100644 --- a/test/webrick/test_https.rb +++ b/test/webrick/test_https.rb @@ -17,11 +17,6 @@ def empty_log.<<(str) class HTTPSNITest < ::Net::HTTP attr_accessor :sni_hostname - def connect - @address = 'localhost' if @address == '127.0.0.1' - super - end - def ssl_socket_connect(s, timeout) s.hostname = sni_hostname super From 77e9ea9aeed4525db34085f3cbc64147ab9cb532 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 25 Feb 2022 17:00:16 +0900 Subject: [PATCH 22/66] Revert "Update url of issue tracker." This reverts commit 082b6345c2fda320d9a3e6fa09fa7614dd3a5575. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 1bb4c9d4a8163ccb9b14241c42fe471953906398 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 25 Mar 2022 15:50:07 +0900 Subject: [PATCH 23/66] Added dependabot --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml 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' From 5793cd207814cb04779df56a091a00f7470532b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Mar 2022 06:50:27 +0000 Subject: [PATCH 24/66] Bump actions/checkout from 2 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f8844ac4..0021fa89 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: os: [ ubuntu-latest, macos-latest ] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: From f6301f2b208db70c53d1ccd59aba832b0d8f46e6 Mon Sep 17 00:00:00 2001 From: Benjamin Lee Date: Tue, 24 May 2022 18:57:15 -0700 Subject: [PATCH 25/66] remove unneeded bin/console and bin/setup files from gemspec --- webrick.gemspec | 2 -- 1 file changed, 2 deletions(-) diff --git a/webrick.gemspec b/webrick.gemspec index e97a9635..31423e9f 100644 --- a/webrick.gemspec +++ b/webrick.gemspec @@ -18,8 +18,6 @@ Gem::Specification.new do |s| "LICENSE.txt", "README.md", "Rakefile", - "bin/console", - "bin/setup", "lib/webrick.rb", "lib/webrick/accesslog.rb", "lib/webrick/cgi.rb", From 73c914e49ec1aba295c092cfdb4dcbc24f6caef3 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Fri, 25 Nov 2022 14:22:37 +0900 Subject: [PATCH 26/66] Use class methods of `File` over `Kernel.open` and `IO.read` --- lib/webrick/httputils.rb | 2 +- test/webrick/test_filehandler.rb | 4 ++-- test/webrick/test_httprequest.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/webrick/httputils.rb b/lib/webrick/httputils.rb index 05597f9e..48aa1371 100644 --- a/lib/webrick/httputils.rb +++ b/lib/webrick/httputils.rb @@ -123,7 +123,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 diff --git a/test/webrick/test_filehandler.rb b/test/webrick/test_filehandler.rb index 998e03f6..881fb54d 100644 --- a/test/webrick/test_filehandler.rb +++ b/test/webrick/test_filehandler.rb @@ -85,12 +85,12 @@ def test_make_partial_content "Content-Type: text/plain\r\n" \ "Content-Range: bytes 0-0/#{filesize}\r\n" \ "\r\n" \ - "#{IO.read(__FILE__, 1)}\r\n" \ + "#{File.read(__FILE__, 1)}\r\n" \ "--#{boundary}\r\n" \ "Content-Type: text/plain\r\n" \ "Content-Range: bytes #{off}-#{last}/#{filesize}\r\n" \ "\r\n" \ - "#{IO.read(__FILE__, 2, off)}\r\n" \ + "#{File.read(__FILE__, 2, off)}\r\n" \ "--#{boundary}--\r\n" assert_equal exp, body end diff --git a/test/webrick/test_httprequest.rb b/test/webrick/test_httprequest.rb index a594f14f..2ff08d63 100644 --- a/test/webrick/test_httprequest.rb +++ b/test/webrick/test_httprequest.rb @@ -245,7 +245,7 @@ def test_chunked _end_of_message_ msg.gsub!(/^ {6}/, "") - open(__FILE__){|io| + File.open(__FILE__){|io| while chunk = io.read(100) msg << chunk.size.to_s(16) << crlf msg << chunk << crlf From d42c291df4f2f5c31b9fdec1e503fac9e5f65b22 Mon Sep 17 00:00:00 2001 From: Ravi <33230211+bharjr01@users.noreply.github.com> Date: Thu, 26 Jan 2023 23:03:22 +0000 Subject: [PATCH 27/66] Accept put requests (#70) * Alias do_PUT method to handle PUT requests * Unit test to ensure PUT requests are handled --- lib/webrick/httpservlet/prochandler.rb | 1 + test/webrick/test_httpserver.rb | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/webrick/httpservlet/prochandler.rb b/lib/webrick/httpservlet/prochandler.rb index dca5d2d0..92e4f80d 100644 --- a/lib/webrick/httpservlet/prochandler.rb +++ b/lib/webrick/httpservlet/prochandler.rb @@ -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/test/webrick/test_httpserver.rb b/test/webrick/test_httpserver.rb index 4133be85..56ac6774 100644 --- a/test/webrick/test_httpserver.rb +++ b/test/webrick/test_httpserver.rb @@ -540,4 +540,23 @@ def test_big_chunks end } end + + def test_accept_put_requests + TestWEBrick.start_httpserver {|server, addr, port, log| + server.mount_proc("/", lambda {|req, res| + res.status = 200 + assert_equal("abcde", req.body) + }) + Thread.pass while server.status != :Running + + Net::HTTP.start(addr, port) do |http| + req = Net::HTTP::Put.new("/") + req.body = "abcde" + http.request(req){|res| + assert_equal("200", res.code) + } + server.shutdown + end + } + end end From e4570039c135e13fadd93df4766b61407098577f Mon Sep 17 00:00:00 2001 From: John W Higgins Date: Thu, 26 Jan 2023 15:07:49 -0800 Subject: [PATCH 28/66] Move the host request parsing to a separate method. (#85) Allows for someone to override the parsing to accommodate "alternatives". One could write the following to allow underscores in host names. require 'webrick/httprequest' module WEBrick class HTTPRequest private def parse_host_request_line(host, scheme) uri = URI.parse("#{scheme}://#{host}") [uri.host, uri.port] end end end Also adding the "o" option to the regex so the regex is only built once. --- lib/webrick/httprequest.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb index ff2c8a8a..680ac65a 100644 --- a/lib/webrick/httprequest.rb +++ b/lib/webrick/httprequest.rb @@ -491,8 +491,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,6 +503,11 @@ def parse_uri(str, scheme="http") return URI::parse(uri.to_s) end + def parse_host_request_line(host) + pattern = /\A(#{URI::REGEXP::PATTERN::HOST})(?::(\d+))?\z/no + host.scan(pattern)[0] + end + def read_body(socket, block) return unless socket if tc = self['transfer-encoding'] From 6cb9bf63a3900ded94aeeaf983df1ae89aef6939 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Thu, 26 Jan 2023 15:10:30 -0800 Subject: [PATCH 29/66] Only output prime information to $stderr if $VERBOSE (#88) This is currently done even in non-$VERBOSE mode. My guess as to the reason it is done is to give the user the impression that the server is doing something and not frozen, but I still think there should be a way to disable it. If the $VERBOSE flag isn't a good way to do that, let's offer some other way to do that. --- lib/webrick/ssl.rb | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/webrick/ssl.rb b/lib/webrick/ssl.rb index 0c81c9eb..6937f93b 100644 --- a/lib/webrick/ssl.rb +++ b/lib/webrick/ssl.rb @@ -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 From 1c9d2f4fc65b27d3408d6325266450768f4ab980 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 26 Jan 2023 18:10:51 -0800 Subject: [PATCH 30/66] Better support for connection upgrade and bi-directional streaming. (#101) --- lib/webrick/httpresponse.rb | 36 +++++++++++++++++++++++++------ test/webrick/test_httpresponse.rb | 33 ++++++++++++++++++++++++++++ test/webrick/test_httpserver.rb | 3 +-- 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/lib/webrick/httpresponse.rb b/lib/webrick/httpresponse.rb index c7238dc0..9b8837c5 100644 --- a/lib/webrick/httpresponse.rb +++ b/lib/webrick/httpresponse.rb @@ -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. @@ -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 @@ -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 diff --git a/test/webrick/test_httpresponse.rb b/test/webrick/test_httpresponse.rb index 07648600..1a2bc3e0 100644 --- a/test/webrick/test_httpresponse.rb +++ b/test/webrick/test_httpresponse.rb @@ -261,6 +261,39 @@ def test_send_body_proc_chunked assert_equal 0, logger.messages.length end + def test_send_body_proc_upgrade + @res.body = Proc.new { |out| out.write('hello'); out.close } + @res.upgrade!("text") + + IO.pipe do |r, w| + @res.send_response(w) + w.close + assert_match /Connection: upgrade\r\nUpgrade: text\r\n\r\nhello/, r.read + end + assert_empty logger.messages + end + + def test_send_body_proc_stream + @res.body = Proc.new do |socket| + chunk = socket.read + socket.write(chunk) + socket.close + end + + UNIXSocket.pair do |s1, s2| + thread = Thread.new do + @res.send_response(s1) + end + + s2.write("hello") + s2.close_write + chunk = s2.read + assert_match /Connection: close\r\n\r\nhello/, chunk + s2.close + end + assert_empty logger.messages + end + def test_set_error status = 400 message = 'missing attribute' diff --git a/test/webrick/test_httpserver.rb b/test/webrick/test_httpserver.rb index 56ac6774..93838649 100644 --- a/test/webrick/test_httpserver.rb +++ b/test/webrick/test_httpserver.rb @@ -377,8 +377,7 @@ def test_response_io_without_chunked_set :ServerName => "localhost" } log_tester = lambda {|log, access_log| - assert_equal(1, log.length) - assert_match(/WARN Could not determine content-length of response body./, log[0]) + assert_empty log } TestWEBrick.start_httpserver(config, log_tester){|server, addr, port, log| server.mount_proc("/", lambda { |req, res| From 841b7dadd5d2dc7cd5f5cceca2258ad53023f8dd Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 27 Jan 2023 11:21:18 +0900 Subject: [PATCH 31/66] Bump up 1.8.0 --- lib/webrick/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/webrick/version.rb b/lib/webrick/version.rb index a4cfd504..66c20a39 100644 --- a/lib/webrick/version.rb +++ b/lib/webrick/version.rb @@ -14,5 +14,5 @@ module WEBrick ## # The WEBrick version - VERSION = "1.7.0" + VERSION = "1.8.0" end From 950173565113d03d38f1d92996d061950a402241 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 27 Jan 2023 23:56:57 +1300 Subject: [PATCH 32/66] Fix warning about missing content type. --- test/webrick/test_httpserver.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/webrick/test_httpserver.rb b/test/webrick/test_httpserver.rb index 93838649..0c5a6147 100644 --- a/test/webrick/test_httpserver.rb +++ b/test/webrick/test_httpserver.rb @@ -541,21 +541,25 @@ def test_big_chunks end def test_accept_put_requests - TestWEBrick.start_httpserver {|server, addr, port, log| + TestWEBrick.start_httpserver do |server, addr, port, log| server.mount_proc("/", lambda {|req, res| res.status = 200 assert_equal("abcde", req.body) }) + Thread.pass while server.status != :Running Net::HTTP.start(addr, port) do |http| req = Net::HTTP::Put.new("/") req.body = "abcde" - http.request(req){|res| + req['content-type'] = "text/plain" + + http.request(req) do |res| assert_equal("200", res.code) - } + end + server.shutdown end - } + end end end From af5d2f57baa4ea6ee070343e0d28616f9d44e746 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 27 Jan 2023 02:57:26 -0800 Subject: [PATCH 33/66] Body should be non-frozen by default. (#103) --- lib/webrick/httpresponse.rb | 4 ++-- test/webrick/test_httpresponse.rb | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/webrick/httpresponse.rb b/lib/webrick/httpresponse.rb index 9b8837c5..dde0261d 100644 --- a/lib/webrick/httpresponse.rb +++ b/lib/webrick/httpresponse.rb @@ -122,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 @@ -441,7 +441,7 @@ def check_header(header_value) # :stopdoc: def error_body(backtrace, ex, host, port) - @body = +'' + @body = +"" @body << <<-_end_of_html_ diff --git a/test/webrick/test_httpresponse.rb b/test/webrick/test_httpresponse.rb index 1a2bc3e0..9909ded3 100644 --- a/test/webrick/test_httpresponse.rb +++ b/test/webrick/test_httpresponse.rb @@ -28,6 +28,10 @@ def setup @res.keep_alive = true end + def test_response_body_not_frozen + refute @res.body.frozen? + end + def test_prevent_response_splitting_headers_crlf res['X-header'] = "malicious\r\nCookie: cracked_indicator_for_test" io = StringIO.new From c566eb7d0b9145eb8c6588aa0de4da68cace5451 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 27 Jan 2023 02:57:44 -0800 Subject: [PATCH 34/66] Join test thread. (#104) --- test/webrick/test_httpresponse.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/webrick/test_httpresponse.rb b/test/webrick/test_httpresponse.rb index 9909ded3..48f8ace9 100644 --- a/test/webrick/test_httpresponse.rb +++ b/test/webrick/test_httpresponse.rb @@ -294,6 +294,8 @@ def test_send_body_proc_stream chunk = s2.read assert_match /Connection: close\r\n\r\nhello/, chunk s2.close + + thread.join end assert_empty logger.messages end From 8fd3ef1df9c44c6331692ba5381aecc9d5c813f4 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 27 Jan 2023 02:57:59 -0800 Subject: [PATCH 35/66] Fix several regexp warnings. (#105) --- test/webrick/test_httpresponse.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/webrick/test_httpresponse.rb b/test/webrick/test_httpresponse.rb index 48f8ace9..83734533 100644 --- a/test/webrick/test_httpresponse.rb +++ b/test/webrick/test_httpresponse.rb @@ -272,7 +272,7 @@ def test_send_body_proc_upgrade IO.pipe do |r, w| @res.send_response(w) w.close - assert_match /Connection: upgrade\r\nUpgrade: text\r\n\r\nhello/, r.read + assert_match(/Connection: upgrade\r\nUpgrade: text\r\n\r\nhello/, r.read) end assert_empty logger.messages end @@ -292,7 +292,7 @@ def test_send_body_proc_stream s2.write("hello") s2.close_write chunk = s2.read - assert_match /Connection: close\r\n\r\nhello/, chunk + assert_match(/Connection: close\r\n\r\nhello/, chunk) s2.close thread.join From 158a7ef85337e0e5cdce0c29cb5cb89c7beb9c53 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 27 Jan 2023 20:16:02 +0900 Subject: [PATCH 36/66] Bump up 1.8.1 --- lib/webrick/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/webrick/version.rb b/lib/webrick/version.rb index 66c20a39..ceeefc33 100644 --- a/lib/webrick/version.rb +++ b/lib/webrick/version.rb @@ -14,5 +14,5 @@ module WEBrick ## # The WEBrick version - VERSION = "1.8.0" + VERSION = "1.8.1" end From 79840f1abf2e7080b72a7749c7cf10387a8e4066 Mon Sep 17 00:00:00 2001 From: Olle Jonsson Date: Thu, 2 Feb 2023 20:26:01 +0100 Subject: [PATCH 37/66] Drop commented-out line (#108) --- lib/webrick/httputils.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/webrick/httputils.rb b/lib/webrick/httputils.rb index 48aa1371..d95147c5 100644 --- a/lib/webrick/httputils.rb +++ b/lib/webrick/httputils.rb @@ -55,7 +55,6 @@ def normalize_path(path) "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", From 8f0bda09d35cb5ffcb2f138039ea6b109014aedf Mon Sep 17 00:00:00 2001 From: Ryunosuke Sato Date: Fri, 10 Feb 2023 23:07:14 +0900 Subject: [PATCH 38/66] Add Ruby 3.1 & 3.1 to CI matrix --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0021fa89..2991c5a1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: name: build (${{ matrix.ruby }} / ${{ matrix.os }}) strategy: matrix: - ruby: [ "3.0", 2.7, 2.6, 2.5, 2.4, head ] + ruby: [ 3.2, 3.1, "3.0", 2.7, 2.6, 2.5, 2.4, head ] os: [ ubuntu-latest, macos-latest ] runs-on: ${{ matrix.os }} steps: From c5b7622af70ae6ca26a4067840c52128206c2b3f Mon Sep 17 00:00:00 2001 From: ooooooo_q Date: Sun, 16 Apr 2023 21:17:37 +0900 Subject: [PATCH 39/66] fix ReDoS parse_header --- lib/webrick/httputils.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/webrick/httputils.rb b/lib/webrick/httputils.rb index d95147c5..d82f95d5 100644 --- a/lib/webrick/httputils.rb +++ b/lib/webrick/httputils.rb @@ -157,13 +157,13 @@ def parse_header(raw) field = nil raw.each_line{|line| case line - when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):\s*(.*?)\s*\z/om - field, value = $1, $2 + when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):(.*?)\z/om + field, value = $1, $2.strip field.downcase! header[field] = [] unless header.has_key?(field) header[field] << value - when /^\s+(.*?)\s*\z/om - value = $1 + when /^\s+(.*?)/om + value = line.strip unless field raise HTTPStatus::BadRequest, "bad header '#{line}'." end From 9e3224864876af9b75b8038545a7962d79995b46 Mon Sep 17 00:00:00 2001 From: ooooooo_q Date: Sun, 16 Apr 2023 23:41:58 +0900 Subject: [PATCH 40/66] fix ReDoS split_header_value --- lib/webrick/httputils.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/webrick/httputils.rb b/lib/webrick/httputils.rb index d82f95d5..6b43146e 100644 --- a/lib/webrick/httputils.rb +++ b/lib/webrick/httputils.rb @@ -183,7 +183,7 @@ def parse_header(raw) # Splits a header value +str+ according to HTTP specification. def split_header_value(str) - str.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",]+)+) + str.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",]++)+) (?:,\s*|\Z)'xn).flatten end module_function :split_header_value From 96c29264519374ee41eaf27933d5049528264e98 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Tue, 15 Aug 2023 14:37:59 -0700 Subject: [PATCH 41/66] Raise HTTPStatus::BadRequest for requests with invalid/duplicate content-length headers Addresses CVE-2023-40225. Fixes #119 --- lib/webrick/httprequest.rb | 8 ++++++++ test/webrick/test_httprequest.rb | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb index 680ac65a..7a1686bc 100644 --- a/lib/webrick/httprequest.rb +++ b/lib/webrick/httprequest.rb @@ -479,6 +479,14 @@ def read_header(socket) end 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") diff --git a/test/webrick/test_httprequest.rb b/test/webrick/test_httprequest.rb index 2ff08d63..90332171 100644 --- a/test/webrick/test_httprequest.rb +++ b/test/webrick/test_httprequest.rb @@ -81,6 +81,31 @@ def test_request_uri_too_large } end + def test_invalid_content_length_header + ['', ' ', ' +1', ' -1', ' a'].each do |cl| + msg = <<-_end_of_message_ + GET / HTTP/1.1 + Content-Length:#{cl} + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg.gsub(/^ {8}/, ""))) + } + end + end + + def test_duplicate_content_length_header + msg = <<-_end_of_message_ + GET / HTTP/1.1 + Content-Length: 1 + Content-Length: 2 + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + } + end + def test_parse_headers msg = <<-_end_of_message_ GET /path HTTP/1.1 From 1da346b4a2271075ca970ac9490aa91fd033d660 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 23:03:07 +0000 Subject: [PATCH 42/66] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2991c5a1..27b3082c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: os: [ ubuntu-latest, macos-latest ] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: From 52ca8d5384fb83943854fa3b2c6c89548454e348 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 20 Oct 2023 17:28:58 +0900 Subject: [PATCH 43/66] Use re-using workflow --- .github/workflows/test.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 27b3082c..360c5824 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,11 +3,18 @@ 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: matrix: - ruby: [ 3.2, 3.1, "3.0", 2.7, 2.6, 2.5, 2.4, head ] + ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} os: [ ubuntu-latest, macos-latest ] runs-on: ${{ matrix.os }} steps: From 289581e566796e0cd5a15f2a4e9afb2f5a63b012 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 20 Oct 2023 17:43:02 +0900 Subject: [PATCH 44/66] Use test-unit-ruby-core gem --- Gemfile | 1 + Rakefile | 7 - test/lib/core_assertions.rb | 768 ------------------------------------ test/lib/envutil.rb | 367 ----------------- test/lib/find_executable.rb | 22 -- test/lib/helper.rb | 2 +- 6 files changed, 2 insertions(+), 1165 deletions(-) delete mode 100644 test/lib/core_assertions.rb delete mode 100644 test/lib/envutil.rb delete mode 100644 test/lib/find_executable.rb diff --git a/Gemfile b/Gemfile index 10284be2..f5b6c4b6 100644 --- a/Gemfile +++ b/Gemfile @@ -4,3 +4,4 @@ gemspec gem "rake" gem "test-unit" +gem "test-unit-ruby-core" diff --git a/Rakefile b/Rakefile index 5a7afabd..5d512c89 100644 --- a/Rakefile +++ b/Rakefile @@ -7,11 +7,4 @@ Rake::TestTask.new(:test) do |t| t.test_files = FileList["test/**/test_*.rb"] end -task :sync_tool do - require 'fileutils' - FileUtils.cp "../ruby/tool/lib/core_assertions.rb", "./test/lib" - FileUtils.cp "../ruby/tool/lib/envutil.rb", "./test/lib" - FileUtils.cp "../ruby/tool/lib/find_executable.rb", "./test/lib" -end - task :default => :test diff --git a/test/lib/core_assertions.rb b/test/lib/core_assertions.rb deleted file mode 100644 index bac3856a..00000000 --- a/test/lib/core_assertions.rb +++ /dev/null @@ -1,768 +0,0 @@ -# frozen_string_literal: true - -module Test - module Unit - module Assertions - def _assertions= n # :nodoc: - @_assertions = n - end - - def _assertions # :nodoc: - @_assertions ||= 0 - end - - ## - # Returns a proc that will output +msg+ along with the default message. - - def message msg = nil, ending = nil, &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 - end - - module CoreAssertions - require_relative 'envutil' - require 'pp' - - def mu_pp(obj) #:nodoc: - obj.pretty_inspect.chomp - end - - def assert_file - AssertFile - end - - FailDesc = proc do |status, message = "", out = ""| - now = Time.now - proc do - EnvUtil.failure_description(status, now, message, out) - end - end - - def assert_in_out_err(args, test_stdin = "", test_stdout = [], test_stderr = [], message = nil, - success: nil, **opt) - args = Array(args).dup - args.insert((Hash === args[0] ? 1 : 0), '--disable=gems') - stdout, stderr, status = EnvUtil.invoke_ruby(args, test_stdin, true, true, **opt) - desc = FailDesc[status, message, stderr] - if block_given? - raise "test_stdout ignored, use block only or without block" if test_stdout != [] - raise "test_stderr ignored, use block only or without block" if test_stderr != [] - yield(stdout.lines.map {|l| l.chomp }, stderr.lines.map {|l| l.chomp }, status) - else - all_assertions(desc) do |a| - [["stdout", test_stdout, stdout], ["stderr", test_stderr, stderr]].each do |key, exp, act| - a.for(key) do - if exp.is_a?(Regexp) - assert_match(exp, act) - elsif exp.all? {|e| String === e} - assert_equal(exp, act.lines.map {|l| l.chomp }) - else - assert_pattern_list(exp, act) - end - end - end - unless success.nil? - a.for("success?") do - if success - assert_predicate(status, :success?) - else - assert_not_predicate(status, :success?) - end - end - end - end - status - end - end - - if defined?(RubyVM::InstructionSequence) - def syntax_check(code, fname, line) - code = code.dup.force_encoding(Encoding::UTF_8) - RubyVM::InstructionSequence.compile(code, fname, fname, line) - :ok - ensure - raise if SyntaxError === $! - end - else - def syntax_check(code, fname, line) - code = code.b - code.sub!(/\A(?:\xef\xbb\xbf)?(\s*\#.*$)*(\n)?/n) { - "#$&#{"\n" if $1 && !$2}BEGIN{throw tag, :ok}\n" - } - code = code.force_encoding(Encoding::UTF_8) - catch {|tag| eval(code, binding, fname, line - 1)} - end - end - - def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: false, **opt) - # TODO: consider choosing some appropriate limit for MJIT and stop skipping this once it does not randomly fail - pend 'assert_no_memory_leak may consider MJIT memory usage as leak' if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? - - require_relative 'memory_status' - raise Test::Unit::PendedError, "unsupported platform" unless defined?(Memory::Status) - - token = "\e[7;1m#{$$.to_s}:#{Time.now.strftime('%s.%L')}:#{rand(0x10000).to_s(16)}:\e[m" - token_dump = token.dump - token_re = Regexp.quote(token) - envs = args.shift if Array === args and Hash === args.first - args = [ - "--disable=gems", - "-r", File.expand_path("../memory_status", __FILE__), - *args, - "-v", "-", - ] - if defined? Memory::NO_MEMORY_LEAK_ENVS then - envs ||= {} - newenvs = envs.merge(Memory::NO_MEMORY_LEAK_ENVS) { |_, _, _| break } - envs = newenvs if newenvs - end - args.unshift(envs) if envs - cmd = [ - 'END {STDERR.puts '"#{token_dump}"'"FINAL=#{Memory::Status.new}"}', - prepare, - 'STDERR.puts('"#{token_dump}"'"START=#{$initial_status = Memory::Status.new}")', - '$initial_size = $initial_status.size', - code, - 'GC.start', - ].join("\n") - _, err, status = EnvUtil.invoke_ruby(args, cmd, true, true, **opt) - before = err.sub!(/^#{token_re}START=(\{.*\})\n/, '') && Memory::Status.parse($1) - after = err.sub!(/^#{token_re}FINAL=(\{.*\})\n/, '') && Memory::Status.parse($1) - assert(status.success?, FailDesc[status, message, err]) - ([:size, (rss && :rss)] & after.members).each do |n| - b = before[n] - a = after[n] - next unless a > 0 and b > 0 - assert_operator(a.fdiv(b), :<, limit, message(message) {"#{n}: #{b} => #{a}"}) - end - rescue LoadError - pend - end - - # :call-seq: - # assert_nothing_raised( *args, &block ) - # - #If any exceptions are given as arguments, the assertion will - #fail if one of those exceptions are raised. Otherwise, the test fails - #if any exceptions are raised. - # - #The final argument may be a failure message. - # - # assert_nothing_raised RuntimeError do - # raise Exception #Assertion passes, Exception is not a RuntimeError - # end - # - # assert_nothing_raised do - # raise Exception #Assertion fails - # end - def assert_nothing_raised(*args) - self._assertions += 1 - if Module === args.last - msg = nil - else - msg = args.pop - end - begin - line = __LINE__; yield - rescue Test::Unit::PendedError - raise - rescue Exception => e - bt = e.backtrace - as = e.instance_of?(Test::Unit::AssertionFailedError) - if as - ans = /\A#{Regexp.quote(__FILE__)}:#{line}:in /o - bt.reject! {|ln| ans =~ ln} - end - if ((args.empty? && !as) || - args.any? {|a| a.instance_of?(Module) ? e.is_a?(a) : e.class == a }) - msg = message(msg) { - "Exception raised:\n<#{mu_pp(e)}>\n" + - "Backtrace:\n" + - e.backtrace.map{|frame| " #{frame}"}.join("\n") - } - raise Test::Unit::AssertionFailedError, msg.call, bt - else - raise - end - end - end - - def prepare_syntax_check(code, fname = nil, mesg = nil, verbose: nil) - fname ||= caller_locations(2, 1)[0] - mesg ||= fname.to_s - verbose, $VERBOSE = $VERBOSE, verbose - case - when Array === fname - fname, line = *fname - when defined?(fname.path) && defined?(fname.lineno) - fname, line = fname.path, fname.lineno - else - line = 1 - end - yield(code, fname, line, message(mesg) { - if code.end_with?("\n") - "```\n#{code}```\n" - else - "```\n#{code}\n```\n""no-newline" - end - }) - ensure - $VERBOSE = verbose - end - - def assert_valid_syntax(code, *args, **opt) - prepare_syntax_check(code, *args, **opt) do |src, fname, line, mesg| - yield if defined?(yield) - assert_nothing_raised(SyntaxError, mesg) do - assert_equal(:ok, syntax_check(src, fname, line), mesg) - end - end - end - - def assert_normal_exit(testsrc, message = '', child_env: nil, **opt) - assert_valid_syntax(testsrc, caller_locations(1, 1)[0]) - if child_env - child_env = [child_env] - else - child_env = [] - end - out, _, status = EnvUtil.invoke_ruby(child_env + %W'-W0', testsrc, true, :merge_to_stdout, **opt) - assert !status.signaled?, FailDesc[status, message, out] - end - - def assert_ruby_status(args, test_stdin="", message=nil, **opt) - out, _, status = EnvUtil.invoke_ruby(args, test_stdin, true, :merge_to_stdout, **opt) - desc = FailDesc[status, message, out] - assert(!status.signaled?, desc) - message ||= "ruby exit status is not success:" - assert(status.success?, desc) - end - - ABORT_SIGNALS = Signal.list.values_at(*%w"ILL ABRT BUS SEGV TERM") - - def separated_runner(out = nil) - include(*Test::Unit::TestCase.ancestors.select {|c| !c.is_a?(Class) }) - out = out ? IO.new(out, 'w') : STDOUT - at_exit { - out.puts [Marshal.dump($!)].pack('m'), "assertions=#{self._assertions}" - } - Test::Unit::Runner.class_variable_set(:@@stop_auto_run, true) if defined?(Test::Unit::Runner) - end - - def assert_separately(args, file = nil, line = nil, src, ignore_stderr: nil, **opt) - unless file and line - loc, = caller_locations(1,1) - file ||= loc.path - line ||= loc.lineno - end - capture_stdout = true - unless /mswin|mingw/ =~ RUBY_PLATFORM - capture_stdout = false - opt[:out] = Test::Unit::Runner.output if defined?(Test::Unit::Runner) - res_p, res_c = IO.pipe - opt[:ios] = [res_c] - end - src = < marshal_error - ignore_stderr = nil - res = nil - end - if res and !(SystemExit === res) - if bt = res.backtrace - bt.each do |l| - l.sub!(/\A-:(\d+)/){"#{file}:#{line + $1.to_i}"} - end - bt.concat(caller) - else - res.set_backtrace(caller) - end - raise res - end - - # really is it succeed? - unless ignore_stderr - # the body of assert_separately must not output anything to detect error - assert(stderr.empty?, FailDesc[status, "assert_separately failed with error message", stderr]) - end - assert(status.success?, FailDesc[status, "assert_separately failed", stderr]) - raise marshal_error if marshal_error - end - - # Run Ractor-related test without influencing the main test suite - def assert_ractor(src, args: [], require: nil, require_relative: nil, file: nil, line: nil, ignore_stderr: nil, **opt) - return unless defined?(Ractor) - - require = "require #{require.inspect}" if require - if require_relative - dir = File.dirname(caller_locations[0,1][0].absolute_path) - full_path = File.expand_path(require_relative, dir) - require = "#{require}; require #{full_path.inspect}" - end - - assert_separately(args, file, line, <<~RUBY, ignore_stderr: ignore_stderr, **opt) - #{require} - previous_verbose = $VERBOSE - $VERBOSE = nil - Ractor.new {} # trigger initial warning - $VERBOSE = previous_verbose - #{src} - RUBY - end - - # :call-seq: - # assert_throw( tag, failure_message = nil, &block ) - # - #Fails unless the given block throws +tag+, returns the caught - #value otherwise. - # - #An optional failure message may be provided as the final argument. - # - # tag = Object.new - # assert_throw(tag, "#{tag} was not thrown!") do - # throw tag - # end - def assert_throw(tag, msg = nil) - ret = catch(tag) do - begin - yield(tag) - rescue UncaughtThrowError => e - thrown = e.tag - end - msg = message(msg) { - "Expected #{mu_pp(tag)} to have been thrown"\ - "#{%Q[, not #{thrown}] if thrown}" - } - assert(false, msg) - end - assert(true) - ret - end - - # :call-seq: - # assert_raise( *args, &block ) - # - #Tests if the given block raises an exception. Acceptable exception - #types may be given as optional arguments. If the last argument is a - #String, it will be used as the error message. - # - # assert_raise do #Fails, no Exceptions are raised - # end - # - # assert_raise NameError do - # puts x #Raises NameError, so assertion succeeds - # end - def assert_raise(*exp, &b) - case exp.last - when String, Proc - msg = exp.pop - end - - begin - yield - rescue Test::Unit::PendedError => e - return e if exp.include? Test::Unit::PendedError - 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 { - flunk(message(msg) {"#{mu_pp(exp)} exception expected, not #{mu_pp(e)}"}) - } - - return e - ensure - unless e - exp = exp.first if exp.size == 1 - - flunk(message(msg) {"#{mu_pp(exp)} expected but nothing was raised"}) - end - end - end - - # :call-seq: - # assert_raise_with_message(exception, expected, msg = nil, &block) - # - #Tests if the given block raises an exception with the expected - #message. - # - # assert_raise_with_message(RuntimeError, "foo") do - # nil #Fails, no Exceptions are raised - # end - # - # assert_raise_with_message(RuntimeError, "foo") do - # raise ArgumentError, "foo" #Fails, different Exception is raised - # end - # - # assert_raise_with_message(RuntimeError, "foo") do - # raise "bar" #Fails, RuntimeError is raised but the message differs - # end - # - # assert_raise_with_message(RuntimeError, "foo") do - # raise "foo" #Raises RuntimeError with the message, so assertion succeeds - # end - def assert_raise_with_message(exception, expected, msg = nil, &block) - case expected - when String - assert = :assert_equal - when Regexp - assert = :assert_match - else - raise TypeError, "Expected #{expected.inspect} to be a kind of String or Regexp, not #{expected.class}" - end - - ex = m = nil - EnvUtil.with_default_internal(expected.encoding) do - ex = assert_raise(exception, msg || proc {"Exception(#{exception}) with message matches to #{expected.inspect}"}) do - yield - end - m = ex.message - end - msg = message(msg, "") {"Expected Exception(#{exception}) was raised, but the message doesn't match"} - - if assert == :assert_equal - assert_equal(expected, m, msg) - else - msg = message(msg) { "Expected #{mu_pp expected} to match #{mu_pp m}" } - assert expected =~ m, msg - block.binding.eval("proc{|_|$~=_}").call($~) - end - ex - end - - MINI_DIR = File.join(File.dirname(File.expand_path(__FILE__)), "minitest") #:nodoc: - - # :call-seq: - # assert(test, [failure_message]) - # - #Tests if +test+ is true. - # - #+msg+ may be a String or a Proc. If +msg+ is a String, it will be used - #as the failure message. Otherwise, the result of calling +msg+ will be - #used as the message if the assertion fails. - # - #If no +msg+ is given, a default message will be used. - # - # assert(false, "This was expected to be true") - def assert(test, *msgs) - case msg = msgs.first - when String, Proc - when nil - msgs.shift - else - bt = caller.reject { |s| s.start_with?(MINI_DIR) } - raise ArgumentError, "assertion message must be String or Proc, but #{msg.class} was given.", bt - end unless msgs.empty? - super - end - - # :call-seq: - # assert_respond_to( object, method, failure_message = nil ) - # - #Tests if the given Object responds to +method+. - # - #An optional failure message may be provided as the final argument. - # - # assert_respond_to("hello", :reverse) #Succeeds - # assert_respond_to("hello", :does_not_exist) #Fails - def assert_respond_to(obj, (meth, *priv), msg = nil) - unless priv.empty? - msg = message(msg) { - "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}#{" privately" if priv[0]}" - } - return assert obj.respond_to?(meth, *priv), msg - end - #get rid of overcounting - if caller_locations(1, 1)[0].path.start_with?(MINI_DIR) - return if obj.respond_to?(meth) - end - super(obj, meth, msg) - end - - # :call-seq: - # assert_not_respond_to( object, method, failure_message = nil ) - # - #Tests if the given Object does not respond to +method+. - # - #An optional failure message may be provided as the final argument. - # - # assert_not_respond_to("hello", :reverse) #Fails - # assert_not_respond_to("hello", :does_not_exist) #Succeeds - def assert_not_respond_to(obj, (meth, *priv), msg = nil) - unless priv.empty? - msg = message(msg) { - "Expected #{mu_pp(obj)} (#{obj.class}) to not respond to ##{meth}#{" privately" if priv[0]}" - } - return assert !obj.respond_to?(meth, *priv), msg - end - #get rid of overcounting - if caller_locations(1, 1)[0].path.start_with?(MINI_DIR) - return unless obj.respond_to?(meth) - end - refute_respond_to(obj, meth, msg) - end - - # pattern_list is an array which contains regexp and :*. - # :* means any sequence. - # - # pattern_list is anchored. - # Use [:*, regexp, :*] for non-anchored match. - def assert_pattern_list(pattern_list, actual, message=nil) - rest = actual - anchored = true - pattern_list.each_with_index {|pattern, i| - if pattern == :* - anchored = false - else - if anchored - match = /\A#{pattern}/.match(rest) - else - match = pattern.match(rest) - end - unless match - msg = message(msg) { - expect_msg = "Expected #{mu_pp pattern}\n" - if /\n[^\n]/ =~ rest - actual_mesg = +"to match\n" - rest.scan(/.*\n+/) { - actual_mesg << ' ' << $&.inspect << "+\n" - } - actual_mesg.sub!(/\+\n\z/, '') - else - actual_mesg = "to match " + mu_pp(rest) - end - actual_mesg << "\nafter #{i} patterns with #{actual.length - rest.length} characters" - expect_msg + actual_mesg - } - assert false, msg - end - rest = match.post_match - anchored = true - end - } - if anchored - assert_equal("", rest) - end - end - - def assert_warning(pat, msg = nil) - result = nil - stderr = EnvUtil.with_default_internal(pat.encoding) { - EnvUtil.verbose_warning { - result = yield - } - } - msg = message(msg) {diff pat, stderr} - assert(pat === stderr, msg) - result - end - - def assert_warn(*args) - assert_warning(*args) {$VERBOSE = false; yield} - end - - def assert_deprecated_warning(mesg = /deprecated/) - assert_warning(mesg) do - Warning[:deprecated] = true - yield - end - end - - def assert_deprecated_warn(mesg = /deprecated/) - assert_warn(mesg) do - Warning[:deprecated] = true - yield - end - end - - class << (AssertFile = Struct.new(:failure_message).new) - include Assertions - include CoreAssertions - def assert_file_predicate(predicate, *args) - if /\Anot_/ =~ predicate - predicate = $' - neg = " not" - end - result = File.__send__(predicate, *args) - result = !result if neg - mesg = "Expected file ".dup << args.shift.inspect - mesg << "#{neg} to be #{predicate}" - mesg << mu_pp(args).sub(/\A\[(.*)\]\z/m, '(\1)') unless args.empty? - mesg << " #{failure_message}" if failure_message - assert(result, mesg) - end - alias method_missing assert_file_predicate - - def for(message) - clone.tap {|a| a.failure_message = message} - end - end - - class AllFailures - attr_reader :failures - - def initialize - @count = 0 - @failures = {} - end - - def for(key) - @count += 1 - yield - rescue Exception => e - @failures[key] = [@count, e] - end - - def foreach(*keys) - keys.each do |key| - @count += 1 - begin - yield key - rescue Exception => e - @failures[key] = [@count, e] - end - end - end - - def message - i = 0 - total = @count.to_s - fmt = "%#{total.size}d" - @failures.map {|k, (n, v)| - v = v.message - "\n#{i+=1}. [#{fmt%n}/#{total}] Assertion for #{k.inspect}\n#{v.b.gsub(/^/, ' | ').force_encoding(v.encoding)}" - }.join("\n") - end - - def pass? - @failures.empty? - end - end - - # threads should respond to shift method. - # Array can be used. - def assert_join_threads(threads, message = nil) - errs = [] - values = [] - while th = threads.shift - begin - values << th.value - rescue Exception - errs << [th, $!] - th = nil - end - end - values - ensure - if th&.alive? - th.raise(Timeout::Error.new) - th.join rescue errs << [th, $!] - end - if !errs.empty? - msg = "exceptions on #{errs.length} threads:\n" + - errs.map {|t, err| - "#{t.inspect}:\n" + - RUBY_VERSION >= "2.5.0" ? err.full_message(highlight: false, order: :top) : err.message - }.join("\n---\n") - if message - msg = "#{message}\n#{msg}" - end - raise Test::Unit::AssertionFailedError, msg - end - end - - def assert_all?(obj, m = nil, &blk) - failed = [] - obj.each do |*a, &b| - unless blk.call(*a, &b) - failed << (a.size > 1 ? a : a[0]) - end - end - assert(failed.empty?, message(m) {failed.pretty_inspect}) - end - - def assert_all_assertions(msg = nil) - all = AllFailures.new - yield all - ensure - assert(all.pass?, message(msg) {all.message.chomp(".")}) - end - alias all_assertions assert_all_assertions - - def assert_all_assertions_foreach(msg = nil, *keys, &block) - all = AllFailures.new - all.foreach(*keys, &block) - ensure - assert(all.pass?, message(msg) {all.message.chomp(".")}) - end - alias all_assertions_foreach assert_all_assertions_foreach - - def message(msg = nil, *args, &default) # :nodoc: - if Proc === msg - super(nil, *args) do - ary = [msg.call, (default.call if default)].compact.reject(&:empty?) - if 1 < ary.length - ary[0...-1] = ary[0...-1].map {|str| str.sub(/(? Date: Fri, 20 Oct 2023 17:46:42 +0900 Subject: [PATCH 45/66] Use assert_raise instead of assert_raises --- test/webrick/test_httpresponse.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/webrick/test_httpresponse.rb b/test/webrick/test_httpresponse.rb index 83734533..6c4a1794 100644 --- a/test/webrick/test_httpresponse.rb +++ b/test/webrick/test_httpresponse.rb @@ -97,14 +97,14 @@ def test_prevent_response_splitting_cookie_headers_lf def test_set_redirect_response_splitting url = "malicious\r\nCookie: cracked_indicator_for_test" - assert_raises(URI::InvalidURIError) do + assert_raise(URI::InvalidURIError) do res.set_redirect(WEBrick::HTTPStatus::MultipleChoices, url) end end def test_set_redirect_html_injection url = 'http://example.com////?a' - assert_raises(WEBrick::HTTPStatus::MultipleChoices) do + assert_raise(WEBrick::HTTPStatus::MultipleChoices) do res.set_redirect(WEBrick::HTTPStatus::MultipleChoices, url) end res.status = 300 From 3704b62505b549baba9c0316d12ed9e116d7c6d6 Mon Sep 17 00:00:00 2001 From: KJ Tsanaktsidis Date: Wed, 27 Dec 2023 13:17:28 +1100 Subject: [PATCH 46/66] Fix WEBrick::TestFileHandler#test_short_filename test not working on mswin The test is currently skipped and can't possibly work on windows at the moment. It fails because $LOAD_PATH is not set up properly in the forked CGI process, so require 'uri' fails. This works properly in the test_cgi.rb tests, because it sets up a :RequestCallback to fix things up. Let's move the setup there into util.rb, so it can be shared with test_filehandler.rb as well. --- test/webrick/test_cgi.rb | 32 +++++--------------------------- test/webrick/test_filehandler.rb | 9 ++------- test/webrick/utils.rb | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+), 34 deletions(-) diff --git a/test/webrick/test_cgi.rb b/test/webrick/test_cgi.rb index 7a75cf56..a9be8f35 100644 --- a/test/webrick/test_cgi.rb +++ b/test/webrick/test_cgi.rb @@ -12,30 +12,8 @@ def teardown super end - def start_cgi_server(log_tester=TestWEBrick::DefaultLogTester, &block) - config = { - :CGIInterpreter => TestWEBrick::RubyBin, - :DocumentRoot => File.dirname(__FILE__), - :DirectoryIndex => ["webrick.cgi"], - :RequestCallback => Proc.new{|req, res| - def req.meta_vars - meta = super - meta["RUBYLIB"] = $:.join(File::PATH_SEPARATOR) - meta[RbConfig::CONFIG['LIBPATHENV']] = ENV[RbConfig::CONFIG['LIBPATHENV']] if RbConfig::CONFIG['LIBPATHENV'] - return meta - end - }, - } - if RUBY_PLATFORM =~ /mswin|mingw|cygwin|bccwin32/ - config[:CGIPathEnv] = ENV['PATH'] # runtime dll may not be in system dir. - end - TestWEBrick.start_httpserver(config, log_tester){|server, addr, port, log| - block.call(server, addr, port, log) - } - end - def test_cgi - start_cgi_server{|server, addr, port, log| + TestWEBrick.start_cgi_server{|server, addr, port, log| http = Net::HTTP.new(addr, port) req = Net::HTTP::Get.new("/webrick.cgi") http.request(req){|res| assert_equal("/webrick.cgi", res.body, log.call)} @@ -98,7 +76,7 @@ def test_bad_request log_tester = lambda {|log, access_log| assert_match(/BadRequest/, log.join) } - start_cgi_server(log_tester) {|server, addr, port, log| + TestWEBrick.start_cgi_server({}, log_tester) {|server, addr, port, log| sock = TCPSocket.new(addr, port) begin sock << "POST /webrick.cgi HTTP/1.0" << CRLF @@ -115,7 +93,7 @@ def test_bad_request end def test_cgi_env - start_cgi_server do |server, addr, port, log| + TestWEBrick.start_cgi_server do |server, addr, port, log| http = Net::HTTP.new(addr, port) req = Net::HTTP::Get.new("/webrick.cgi/dumpenv") req['proxy'] = 'http://example.com/' @@ -137,7 +115,7 @@ def test_bad_uri assert_equal(1, log.length) assert_match(/ERROR bad URI/, log[0]) } - start_cgi_server(log_tester) {|server, addr, port, log| + TestWEBrick.start_cgi_server({}, log_tester) {|server, addr, port, log| res = TCPSocket.open(addr, port) {|sock| sock << "GET /#{CtrlSeq}#{CRLF}#{CRLF}" sock.close_write @@ -155,7 +133,7 @@ def test_bad_header assert_equal(1, log.length) assert_match(/ERROR bad header/, log[0]) } - start_cgi_server(log_tester) {|server, addr, port, log| + TestWEBrick.start_cgi_server({}, log_tester) {|server, addr, port, log| res = TCPSocket.open(addr, port) {|sock| sock << "GET / HTTP/1.0#{CRLF}#{CtrlSeq}#{CRLF}#{CRLF}" sock.close_write diff --git a/test/webrick/test_filehandler.rb b/test/webrick/test_filehandler.rb index 881fb54d..452667d4 100644 --- a/test/webrick/test_filehandler.rb +++ b/test/webrick/test_filehandler.rb @@ -248,20 +248,15 @@ def test_unwise_in_path def test_short_filename return if File.executable?(__FILE__) # skip on strange file system - config = { - :CGIInterpreter => TestWEBrick::RubyBin, - :DocumentRoot => File.dirname(__FILE__), - :CGIPathEnv => ENV['PATH'], - } log_tester = lambda {|log, access_log| log = log.reject {|s| /ERROR `.*\' not found\./ =~ s } log = log.reject {|s| /WARN the request refers nondisclosure name/ =~ s } assert_equal([], log) } - TestWEBrick.start_httpserver(config, log_tester) do |server, addr, port, log| + TestWEBrick.start_cgi_server({}, log_tester) do |server, addr, port, log| http = Net::HTTP.new(addr, port) if windows? - root = config[:DocumentRoot].tr("/", "\\") + root = File.dirname(__FILE__).tr("/", "\\") fname = IO.popen(%W[dir /x #{root}\\webrick_long_filename.cgi], encoding: "binary", &:read) fname.sub!(/\A.*$^$.*$^$/m, '') if fname diff --git a/test/webrick/utils.rb b/test/webrick/utils.rb index a8568d0a..c8e84c37 100644 --- a/test/webrick/utils.rb +++ b/test/webrick/utils.rb @@ -81,4 +81,24 @@ def start_httpserver(config={}, log_tester=DefaultLogTester, &block) def start_httpproxy(config={}, log_tester=DefaultLogTester, &block) start_server(WEBrick::HTTPProxyServer, config, log_tester, &block) end + + def start_cgi_server(config={}, log_tester=TestWEBrick::DefaultLogTester, &block) + config = { + :CGIInterpreter => TestWEBrick::RubyBin, + :DocumentRoot => File.dirname(__FILE__), + :DirectoryIndex => ["webrick.cgi"], + :RequestCallback => Proc.new{|req, res| + def req.meta_vars + meta = super + meta["RUBYLIB"] = $:.join(File::PATH_SEPARATOR) + meta[RbConfig::CONFIG['LIBPATHENV']] = ENV[RbConfig::CONFIG['LIBPATHENV']] if RbConfig::CONFIG['LIBPATHENV'] + return meta + end + }, + }.merge(config) + if RUBY_PLATFORM =~ /mswin|mingw|cygwin|bccwin32/ + config[:CGIPathEnv] = ENV['PATH'] # runtime dll may not be in system dir. + end + start_server(WEBrick::HTTPServer, config, log_tester, &block) + end end From f9336e0bc3bf8d0e271c96285f92be59c6b04d3c Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Sat, 11 Nov 2023 16:46:08 -0800 Subject: [PATCH 47/66] Fix bug chunk extension detection This fixes a request smuggling vulnerability (Fixes #124). Co-authored-by: Ben Kallus --- lib/webrick/httprequest.rb | 2 +- test/webrick/test_httprequest.rb | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb index 7a1686bc..0cfc1c95 100644 --- a/lib/webrick/httprequest.rb +++ b/lib/webrick/httprequest.rb @@ -542,7 +542,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 ] diff --git a/test/webrick/test_httprequest.rb b/test/webrick/test_httprequest.rb index 90332171..73fa8a3d 100644 --- a/test/webrick/test_httprequest.rb +++ b/test/webrick/test_httprequest.rb @@ -289,6 +289,31 @@ def test_chunked assert_equal(expect, dst.string) end + def test_bad_chunked + crlf = "\x0d\x0a" + expect = File.binread(__FILE__).freeze + msg = <<-_end_of_message_ + POST /path HTTP/1.1\r + Transfer-Encoding: chunked\r + \r + 01x1\r + \r + 1 + _end_of_message_ + msg.gsub!(/^ {6}/, "") + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + assert_raise(WEBrick::HTTPStatus::BadRequest){ req.body } + + # chunked req.body_reader + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + dst = StringIO.new + assert_raise(WEBrick::HTTPStatus::BadRequest) do + IO.copy_stream(req.body_reader, dst) + end + end + def test_forwarded msg = <<-_end_of_message_ GET /foo HTTP/1.1 From a6342a59261781242cb1eabde4d3d2485c57e46a Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Mon, 13 Nov 2023 12:28:57 -0800 Subject: [PATCH 48/66] Make \r optional in chunk size detection As pointed out by Ken Ballus, WEBrick allows \n without preceding \r generally. It probably shouldn't, but since it does, do not require \r for chunk size detection. --- lib/webrick/httprequest.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb index 0cfc1c95..02c5dcb6 100644 --- a/lib/webrick/httprequest.rb +++ b/lib/webrick/httprequest.rb @@ -542,7 +542,7 @@ def read_body(socket, block) def read_chunk_size(socket) line = read_line(socket) - if /\A([0-9a-fA-F]+)(?:;(\S+(?:=\S+)?))?\r\n\z/ =~ line + if /\A([0-9a-fA-F]+)(?:;(\S+(?:=\S+)?))?\r?\n\z/ =~ line chunk_size = $1.hex chunk_ext = $2 [ chunk_size, chunk_ext ] From 75e1f10bf1fd00629128cb19515241c2486f45c8 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Mon, 13 Nov 2023 12:31:30 -0800 Subject: [PATCH 49/66] Remove unneeded lines in new test --- test/webrick/test_httprequest.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/webrick/test_httprequest.rb b/test/webrick/test_httprequest.rb index 73fa8a3d..471005c6 100644 --- a/test/webrick/test_httprequest.rb +++ b/test/webrick/test_httprequest.rb @@ -290,8 +290,6 @@ def test_chunked end def test_bad_chunked - crlf = "\x0d\x0a" - expect = File.binread(__FILE__).freeze msg = <<-_end_of_message_ POST /path HTTP/1.1\r Transfer-Encoding: chunked\r From abf2bc3710c17e3c0b560eea87effe937f9a0766 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Fri, 1 Dec 2023 11:29:47 -0800 Subject: [PATCH 50/66] Revert "Make \r optional in chunk size detection" This reverts commit 44cb9cb31cb9494fd820671738219ca4fc5e4f2a. --- lib/webrick/httprequest.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb index 02c5dcb6..0cfc1c95 100644 --- a/lib/webrick/httprequest.rb +++ b/lib/webrick/httprequest.rb @@ -542,7 +542,7 @@ def read_body(socket, block) def read_chunk_size(socket) line = read_line(socket) - if /\A([0-9a-fA-F]+)(?:;(\S+(?:=\S+)?))?\r?\n\z/ =~ line + if /\A([0-9a-fA-F]+)(?:;(\S+(?:=\S+)?))?\r\n\z/ =~ line chunk_size = $1.hex chunk_ext = $2 [ chunk_size, chunk_ext ] From cd0b1dd2286492287876773e719bb50a533d2698 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Fri, 1 Dec 2023 12:47:24 -0800 Subject: [PATCH 51/66] Reject null bytes in header lines Fixes #126 --- lib/webrick/httprequest.rb | 3 +++ test/webrick/test_httprequest.rb | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb index 0cfc1c95..43e4e8a3 100644 --- a/lib/webrick/httprequest.rb +++ b/lib/webrick/httprequest.rb @@ -475,6 +475,9 @@ def read_header(socket) 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 end diff --git a/test/webrick/test_httprequest.rb b/test/webrick/test_httprequest.rb index 471005c6..c0fb2e9d 100644 --- a/test/webrick/test_httprequest.rb +++ b/test/webrick/test_httprequest.rb @@ -312,6 +312,17 @@ def test_bad_chunked end end + def test_null_byte_in_header + msg = <<-_end_of_message_ + POST /path HTTP/1.1\r + Evil: evil\x00\r + \r + _end_of_message_ + msg.gsub!(/^ {6}/, "") + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ req.parse(StringIO.new(msg)) } + end + def test_forwarded msg = <<-_end_of_message_ GET /foo HTTP/1.1 From d1dbce884f2b40e766f19aa0f9d2131bb9e30624 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 28 Mar 2024 09:45:01 +0900 Subject: [PATCH 52/66] Use www.rfc-editor.org for RFC text. We use the following site for that now: * https://tools.ietf.org/ or http * https://datatracker.ietf.org or http Today, IETF said the official site of RFC is www.rfc-editor.org. FYI: https://authors.ietf.org/en/references-in-rfcxml I replaced them to www.rfc-editor.org. --- lib/webrick/httprequest.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb index 43e4e8a3..c422c737 100644 --- a/lib/webrick/httprequest.rb +++ b/lib/webrick/httprequest.rb @@ -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 From 76751152d49ed9851cd1d3dc4c04a04796e36319 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sat, 8 Jun 2024 10:13:43 +0900 Subject: [PATCH 53/66] Fix CI by explicit use of `macos-13` instead of `-latest`. (#131) macos-latest no longer supports Ruby < 2.7 due to architectural changes. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 360c5824..a4534c8a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} - os: [ ubuntu-latest, macos-latest ] + os: [ ubuntu-latest, macos-13 ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 From d2e2cac9929296a2f5f995de7e3875c5f5ddb11b Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sat, 8 Jun 2024 10:21:45 +0900 Subject: [PATCH 54/66] Merge multiple cookie headers, preserving semantic correctness. (#130) --- lib/webrick/httprequest.rb | 4 ++-- lib/webrick/httputils.rb | 18 +++++++++++++++++- test/webrick/test_httprequest.rb | 7 +++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb index c422c737..80b01e95 100644 --- a/lib/webrick/httprequest.rb +++ b/lib/webrick/httprequest.rb @@ -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 diff --git a/lib/webrick/httputils.rb b/lib/webrick/httputils.rb index 6b43146e..1653a072 100644 --- a/lib/webrick/httputils.rb +++ b/lib/webrick/httputils.rb @@ -152,6 +152,22 @@ 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 @@ -160,7 +176,7 @@ def parse_header(raw) when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):(.*?)\z/om field, value = $1, $2.strip 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+(.*?)/om value = line.strip diff --git a/test/webrick/test_httprequest.rb b/test/webrick/test_httprequest.rb index c0fb2e9d..87a27523 100644 --- a/test/webrick/test_httprequest.rb +++ b/test/webrick/test_httprequest.rb @@ -532,4 +532,11 @@ def test_eof_raised_when_line_is_nil req.parse(StringIO.new("")) } end + + def test_cookie_join + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new("GET / HTTP/1.1\r\ncookie: a=1\r\ncookie: b=2\r\n\r\n")) + assert_equal 2, req.cookies.length + assert_equal 'a=1; b=2', req['cookie'] + end end From 6f412123e368c5cc1059127993c793ef44518409 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sat, 8 Jun 2024 12:09:47 +0200 Subject: [PATCH 55/66] Test on macos-latest But exclude Ruby 2.4 and 2.5 because they aren't supported on macos-arm64. --- .github/workflows/test.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a4534c8a..d52899c4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,9 +13,15 @@ jobs: needs: ruby-versions name: build (${{ matrix.ruby }} / ${{ matrix.os }}) strategy: + fail-fast: false matrix: ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} - os: [ ubuntu-latest, macos-13 ] + os: [ ubuntu-latest, macos-latest ] + exclude: + - os: macos-latest + ruby: "2.4" + - os: macos-latest + ruby: "2.5" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 From a27d7ed45f630d9ee9a7e8cbd0542542e36a3219 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Sat, 8 Jun 2024 13:20:53 +0200 Subject: [PATCH 56/66] Test 2.4 & 2.5 on macos-amd64 --- .github/workflows/test.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d52899c4..edede5b1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,11 +17,13 @@ jobs: matrix: 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" + - { 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@v4 From ee60354bcb84ec33b9245e1d1aa6e1f7e8132101 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Tue, 25 Jun 2024 14:39:04 -0700 Subject: [PATCH 57/66] Require CRLF line endings in request line and headers Disallow bare CR, LF, NUL in header and request lines. Tighten parsing of request lines to only allow single spaces, as specified in the RFCs. Forcing this RFC-compliant behavior breaks a lot of tests, so fix the tests to correctly use CRLF instead of LF for requests (other than the specific checks for handling of bad requests). Fixes #137 --- lib/webrick/httprequest.rb | 4 +- lib/webrick/httputils.rb | 10 ++- test/webrick/test_filehandler.rb | 2 +- test/webrick/test_httprequest.rb | 149 +++++++++++++++++++++++++------ 4 files changed, 133 insertions(+), 32 deletions(-) diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb index 80b01e95..62ea54c8 100644 --- a/lib/webrick/httprequest.rb +++ b/lib/webrick/httprequest.rb @@ -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") @@ -471,7 +471,7 @@ def read_request_line(socket) def read_header(socket) if socket while line = read_line(socket) - break if /\A(#{CRLF}|#{LF})\z/om =~ line + break if /\A#{CRLF}\z/om =~ line if (@request_bytes += line.bytesize) > MAX_HEADER_LENGTH raise HTTPStatus::RequestEntityTooLarge, 'headers too large' end diff --git a/lib/webrick/httputils.rb b/lib/webrick/httputils.rb index 1653a072..ea67fdb0 100644 --- a/lib/webrick/httputils.rb +++ b/lib/webrick/httputils.rb @@ -173,16 +173,18 @@ def parse_header(raw) field = nil raw.each_line{|line| case line - when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):(.*?)\z/om - field, value = $1, $2.strip + when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):([^\r\n\0]*?)\r\n\z/om + field, value = $1, $2 field.downcase! header[field] = HEADER_CLASSES[field].new unless header.has_key?(field) header[field] << value - when /^\s+(.*?)/om - value = line.strip + when /^\s+([^\r\n\0]*?)\r\n/om unless field raise HTTPStatus::BadRequest, "bad header '#{line}'." end + value = line + value.lstrip! + value.slice!(-2..-1) header[field][-1] << " " << value else raise HTTPStatus::BadRequest, "bad header '#{line}'." diff --git a/test/webrick/test_filehandler.rb b/test/webrick/test_filehandler.rb index 452667d4..51654394 100644 --- a/test/webrick/test_filehandler.rb +++ b/test/webrick/test_filehandler.rb @@ -33,7 +33,7 @@ def make_range_request(range_spec) Range: #{range_spec} END_OF_REQUEST - return StringIO.new(msg.gsub(/^ {6}/, "")) + return StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")) end def make_range_response(file, range_spec) diff --git a/test/webrick/test_httprequest.rb b/test/webrick/test_httprequest.rb index 87a27523..fa181773 100644 --- a/test/webrick/test_httprequest.rb +++ b/test/webrick/test_httprequest.rb @@ -11,7 +11,7 @@ def teardown def test_simple_request msg = <<-_end_of_message_ -GET / +GET /\r _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) @@ -24,7 +24,7 @@ def test_parse_09 foobar # HTTP/0.9 request don't have header nor entity body. _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) assert_equal("GET", req.request_method) assert_equal("/", req.unparsed_uri) assert_equal(WEBrick::HTTPVersion.new("0.9"), req.http_version) @@ -41,7 +41,7 @@ def test_parse_10 _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) assert_equal("GET", req.request_method) assert_equal("/", req.unparsed_uri) assert_equal(WEBrick::HTTPVersion.new("1.0"), req.http_version) @@ -58,7 +58,7 @@ def test_parse_11 _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) assert_equal("GET", req.request_method) assert_equal("/path", req.unparsed_uri) assert_equal("", req.script_name) @@ -77,7 +77,7 @@ def test_request_uri_too_large _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::RequestURITooLarge){ - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) } end @@ -89,11 +89,101 @@ def test_invalid_content_length_header _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ - req.parse(StringIO.new(msg.gsub(/^ {8}/, ""))) + req.parse(StringIO.new(msg.gsub(/^ {8}/, "").gsub("\n", "\r\n"))) } end end + def test_bare_lf_request_line + msg = <<-_end_of_message_ + GET / HTTP/1.1 + Content-Length: 0\r + \r + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + } + end + + def test_bare_lf_header + msg = <<-_end_of_message_ + GET / HTTP/1.1\r + Content-Length: 0 + \r + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + } + end + + def test_bare_cr_request_line + msg = <<-_end_of_message_ + GET / HTTP/1.1\r\r + Content-Length: 0\r + \r + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + } + end + + def test_bare_cr_header + msg = <<-_end_of_message_ + GET / HTTP/1.1\r + Content-Type: foo\rbar\r + \r + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + } + end + + def test_invalid_request_lines + msg = <<-_end_of_message_ + GET / HTTP/1.1\r + Content-Length: 0\r + \r + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + } + + msg = <<-_end_of_message_ + GET / HTTP/1.1\r + Content-Length: 0\r + \r + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + } + + msg = <<-_end_of_message_ + GET /\r HTTP/1.1\r + Content-Length: 0\r + \r + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + } + + msg = <<-_end_of_message_ + GET / HTTP/1.1 \r + Content-Length: 0\r + \r + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + } + end + def test_duplicate_content_length_header msg = <<-_end_of_message_ GET / HTTP/1.1 @@ -102,7 +192,7 @@ def test_duplicate_content_length_header _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) } end @@ -118,13 +208,13 @@ def test_parse_headers Accept-Language: en;q=0.5, *; q=0 Accept-Language: ja Content-Type: text/plain - Content-Length: 7 + Content-Length: 8 X-Empty-Header: foobar _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) assert_equal( URI.parse("http://test.ruby-lang.org:8080/path"), req.request_uri) assert_equal("test.ruby-lang.org", req.host) @@ -135,9 +225,9 @@ def test_parse_headers req.accept) assert_equal(%w(gzip compress identity *), req.accept_encoding) assert_equal(%w(ja en *), req.accept_language) - assert_equal(7, req.content_length) + assert_equal(8, req.content_length) assert_equal("text/plain", req.content_type) - assert_equal("foobar\n", req.body) + assert_equal("foobar\r\n", req.body) assert_equal("", req["x-empty-header"]) assert_equal(nil, req["x-no-header"]) assert(req.query.empty?) @@ -146,7 +236,7 @@ def test_parse_headers def test_parse_header2() msg = <<-_end_of_message_ POST /foo/bar/../baz?q=a HTTP/1.0 - Content-Length: 9 + Content-Length: 10 User-Agent: FOO BAR BAZ @@ -154,14 +244,14 @@ def test_parse_header2() hogehoge _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) assert_equal("POST", req.request_method) assert_equal("/foo/baz", req.path) assert_equal("", req.script_name) assert_equal("/foo/baz", req.path_info) - assert_equal("9", req['content-length']) + assert_equal("10", req['content-length']) assert_equal("FOO BAR BAZ", req['user-agent']) - assert_equal("hogehoge\n", req.body) + assert_equal("hogehoge\r\n", req.body) end def test_parse_headers3 @@ -171,7 +261,7 @@ def test_parse_headers3 _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) assert_equal(URI.parse("http://test.ruby-lang.org/path"), req.request_uri) assert_equal("test.ruby-lang.org", req.host) assert_equal(80, req.port) @@ -182,7 +272,7 @@ def test_parse_headers3 _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) assert_equal(URI.parse("http://192.168.1.1/path"), req.request_uri) assert_equal("192.168.1.1", req.host) assert_equal(80, req.port) @@ -193,7 +283,7 @@ def test_parse_headers3 _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) assert_equal(URI.parse("http://[fe80::208:dff:feef:98c7]/path"), req.request_uri) assert_equal("[fe80::208:dff:feef:98c7]", req.host) @@ -205,7 +295,7 @@ def test_parse_headers3 _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) assert_equal(URI.parse("http://192.168.1.1:8080/path"), req.request_uri) assert_equal("192.168.1.1", req.host) assert_equal(8080, req.port) @@ -216,7 +306,7 @@ def test_parse_headers3 _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) assert_equal(URI.parse("http://[fe80::208:dff:feef:98c7]:8080/path"), req.request_uri) assert_equal("[fe80::208:dff:feef:98c7]", req.host) @@ -231,7 +321,7 @@ def test_parse_get_params _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) query = req.query assert_equal("1", query["foo"]) assert_equal(["1", "2", "3"], query["foo"].to_ary) @@ -251,7 +341,7 @@ def test_parse_post_params #{param} _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) query = req.query assert_equal("1", query["foo"]) assert_equal(["1", "2", "3"], query["foo"].to_ary) @@ -270,6 +360,7 @@ def test_chunked _end_of_message_ msg.gsub!(/^ {6}/, "") + msg.gsub!("\n", "\r\n") File.open(__FILE__){|io| while chunk = io.read(100) msg << chunk.size.to_s(16) << crlf @@ -335,6 +426,7 @@ def test_forwarded _end_of_message_ msg.gsub!(/^ {6}/, "") + msg.gsub!("\n", "\r\n") req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server.example.com", req.server_name) @@ -355,6 +447,7 @@ def test_forwarded _end_of_message_ msg.gsub!(/^ {6}/, "") + msg.gsub!("\n", "\r\n") req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server.example.com", req.server_name) @@ -377,6 +470,7 @@ def test_forwarded _end_of_message_ msg.gsub!(/^ {6}/, "") + msg.gsub!("\n", "\r\n") req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server.example.com", req.server_name) @@ -399,6 +493,7 @@ def test_forwarded _end_of_message_ msg.gsub!(/^ {6}/, "") + msg.gsub!("\n", "\r\n") req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server1.example.com", req.server_name) @@ -421,6 +516,7 @@ def test_forwarded _end_of_message_ msg.gsub!(/^ {6}/, "") + msg.gsub!("\n", "\r\n") req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server1.example.com", req.server_name) @@ -443,6 +539,7 @@ def test_forwarded _end_of_message_ msg.gsub!(/^ {6}/, "") + msg.gsub!("\n", "\r\n") req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server1.example.com", req.server_name) @@ -460,6 +557,7 @@ def test_continue_sent _end_of_message_ msg.gsub!(/^ {6}/, "") + msg.gsub!("\n", "\r\n") req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert req['expect'] @@ -476,6 +574,7 @@ def test_continue_not_sent _end_of_message_ msg.gsub!(/^ {6}/, "") + msg.gsub!("\n", "\r\n") req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert !req['expect'] @@ -495,7 +594,7 @@ def test_bad_messages _end_of_message_ assert_raise(WEBrick::HTTPStatus::LengthRequired){ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) req.body } @@ -508,7 +607,7 @@ def test_bad_messages _end_of_message_ assert_raise(WEBrick::HTTPStatus::BadRequest){ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) req.body } @@ -521,7 +620,7 @@ def test_bad_messages _end_of_message_ assert_raise(WEBrick::HTTPStatus::NotImplemented){ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) req.body } end From e72cb697836e2ff201a4a74c108fdca9d3d2d0ed Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 12 Jul 2024 02:45:43 +1200 Subject: [PATCH 58/66] Prefer squigly heredocs. (#143) --- test/webrick/test_filehandler.rb | 6 +- test/webrick/test_httprequest.rb | 207 ++++++++++++++----------------- 2 files changed, 97 insertions(+), 116 deletions(-) diff --git a/test/webrick/test_filehandler.rb b/test/webrick/test_filehandler.rb index 51654394..3b299d93 100644 --- a/test/webrick/test_filehandler.rb +++ b/test/webrick/test_filehandler.rb @@ -28,12 +28,12 @@ def get_res_body(res) end def make_range_request(range_spec) - msg = <<-END_OF_REQUEST + msg = <<~HTTP.gsub("\n", "\r\n") GET / HTTP/1.0 Range: #{range_spec} - END_OF_REQUEST - return StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")) + HTTP + return StringIO.new(msg) end def make_range_response(file, range_spec) diff --git a/test/webrick/test_httprequest.rb b/test/webrick/test_httprequest.rb index fa181773..6088f18f 100644 --- a/test/webrick/test_httprequest.rb +++ b/test/webrick/test_httprequest.rb @@ -10,21 +10,21 @@ def teardown end def test_simple_request - msg = <<-_end_of_message_ -GET /\r - _end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") + GET / + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert(req.meta_vars) # fails if @header was not initialized and iteration is attempted on the nil reference end def test_parse_09 - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET / foobar # HTTP/0.9 request don't have header nor entity body. - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) + req.parse(StringIO.new(msg)) assert_equal("GET", req.request_method) assert_equal("/", req.unparsed_uri) assert_equal(WEBrick::HTTPVersion.new("0.9"), req.http_version) @@ -36,12 +36,12 @@ def test_parse_09 end def test_parse_10 - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET / HTTP/1.0 - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) + req.parse(StringIO.new(msg)) assert_equal("GET", req.request_method) assert_equal("/", req.unparsed_uri) assert_equal(WEBrick::HTTPVersion.new("1.0"), req.http_version) @@ -53,12 +53,12 @@ def test_parse_10 end def test_parse_11 - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /path HTTP/1.1 - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) + req.parse(StringIO.new(msg)) assert_equal("GET", req.request_method) assert_equal("/path", req.unparsed_uri) assert_equal("", req.script_name) @@ -72,21 +72,21 @@ def test_parse_11 end def test_request_uri_too_large - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /#{"a"*2084} HTTP/1.1 - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::RequestURITooLarge){ - req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) + req.parse(StringIO.new(msg)) } end def test_invalid_content_length_header ['', ' ', ' +1', ' -1', ' a'].each do |cl| - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET / HTTP/1.1 Content-Length:#{cl} - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ req.parse(StringIO.new(msg.gsub(/^ {8}/, "").gsub("\n", "\r\n"))) @@ -95,11 +95,11 @@ def test_invalid_content_length_header end def test_bare_lf_request_line - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET / HTTP/1.1 Content-Length: 0\r \r - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) @@ -107,11 +107,11 @@ def test_bare_lf_request_line end def test_bare_lf_header - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET / HTTP/1.1\r Content-Length: 0 \r - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) @@ -119,11 +119,11 @@ def test_bare_lf_header end def test_bare_cr_request_line - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET / HTTP/1.1\r\r Content-Length: 0\r \r - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) @@ -131,11 +131,11 @@ def test_bare_cr_request_line end def test_bare_cr_header - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET / HTTP/1.1\r Content-Type: foo\rbar\r \r - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) @@ -143,41 +143,41 @@ def test_bare_cr_header end def test_invalid_request_lines - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET / HTTP/1.1\r Content-Length: 0\r \r - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) } - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET / HTTP/1.1\r Content-Length: 0\r \r - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) } - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /\r HTTP/1.1\r Content-Length: 0\r \r - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) } - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET / HTTP/1.1 \r Content-Length: 0\r \r - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) @@ -185,19 +185,19 @@ def test_invalid_request_lines end def test_duplicate_content_length_header - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET / HTTP/1.1 Content-Length: 1 Content-Length: 2 - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ - req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) + req.parse(StringIO.new(msg)) } end def test_parse_headers - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /path HTTP/1.1 Host: test.ruby-lang.org:8080 Connection: close @@ -212,9 +212,9 @@ def test_parse_headers X-Empty-Header: foobar - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) + req.parse(StringIO.new(msg)) assert_equal( URI.parse("http://test.ruby-lang.org:8080/path"), req.request_uri) assert_equal("test.ruby-lang.org", req.host) @@ -234,7 +234,7 @@ def test_parse_headers end def test_parse_header2() - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") POST /foo/bar/../baz?q=a HTTP/1.0 Content-Length: 10 User-Agent: @@ -242,9 +242,9 @@ def test_parse_header2() BAZ hogehoge - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) + req.parse(StringIO.new(msg)) assert_equal("POST", req.request_method) assert_equal("/foo/baz", req.path) assert_equal("", req.script_name) @@ -255,58 +255,58 @@ def test_parse_header2() end def test_parse_headers3 - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /path HTTP/1.1 Host: test.ruby-lang.org - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) + req.parse(StringIO.new(msg)) assert_equal(URI.parse("http://test.ruby-lang.org/path"), req.request_uri) assert_equal("test.ruby-lang.org", req.host) assert_equal(80, req.port) - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /path HTTP/1.1 Host: 192.168.1.1 - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) + req.parse(StringIO.new(msg)) assert_equal(URI.parse("http://192.168.1.1/path"), req.request_uri) assert_equal("192.168.1.1", req.host) assert_equal(80, req.port) - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /path HTTP/1.1 Host: [fe80::208:dff:feef:98c7] - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) + req.parse(StringIO.new(msg)) assert_equal(URI.parse("http://[fe80::208:dff:feef:98c7]/path"), req.request_uri) assert_equal("[fe80::208:dff:feef:98c7]", req.host) assert_equal(80, req.port) - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /path HTTP/1.1 Host: 192.168.1.1:8080 - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) + req.parse(StringIO.new(msg)) assert_equal(URI.parse("http://192.168.1.1:8080/path"), req.request_uri) assert_equal("192.168.1.1", req.host) assert_equal(8080, req.port) - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /path HTTP/1.1 Host: [fe80::208:dff:feef:98c7]:8080 - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) + req.parse(StringIO.new(msg)) assert_equal(URI.parse("http://[fe80::208:dff:feef:98c7]:8080/path"), req.request_uri) assert_equal("[fe80::208:dff:feef:98c7]", req.host) @@ -315,13 +315,13 @@ def test_parse_headers3 def test_parse_get_params param = "foo=1;foo=2;foo=3;bar=x" - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /path?#{param} HTTP/1.1 Host: test.ruby-lang.org:8080 - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) + req.parse(StringIO.new(msg)) query = req.query assert_equal("1", query["foo"]) assert_equal(["1", "2", "3"], query["foo"].to_ary) @@ -332,16 +332,16 @@ def test_parse_get_params def test_parse_post_params param = "foo=1;foo=2;foo=3;bar=x" - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1 Host: test.ruby-lang.org:8080 Content-Length: #{param.size} Content-Type: application/x-www-form-urlencoded #{param} - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) + req.parse(StringIO.new(msg)) query = req.query assert_equal("1", query["foo"]) assert_equal(["1", "2", "3"], query["foo"].to_ary) @@ -353,14 +353,12 @@ def test_parse_post_params def test_chunked crlf = "\x0d\x0a" expect = File.binread(__FILE__).freeze - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") POST /path HTTP/1.1 Host: test.ruby-lang.org:8080 Transfer-Encoding: chunked - _end_of_message_ - msg.gsub!(/^ {6}/, "") - msg.gsub!("\n", "\r\n") + HTTP File.open(__FILE__){|io| while chunk = io.read(100) msg << chunk.size.to_s(16) << crlf @@ -381,15 +379,14 @@ def test_chunked end def test_bad_chunked - msg = <<-_end_of_message_ + msg = <<~HTTP POST /path HTTP/1.1\r Transfer-Encoding: chunked\r \r 01x1\r \r 1 - _end_of_message_ - msg.gsub!(/^ {6}/, "") + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_raise(WEBrick::HTTPStatus::BadRequest){ req.body } @@ -404,18 +401,18 @@ def test_bad_chunked end def test_null_byte_in_header - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") POST /path HTTP/1.1\r Evil: evil\x00\r \r - _end_of_message_ + HTTP msg.gsub!(/^ {6}/, "") req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ req.parse(StringIO.new(msg)) } end def test_forwarded - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /foo HTTP/1.1 Host: localhost:10080 User-Agent: w3m/0.5.2 @@ -424,9 +421,7 @@ def test_forwarded X-Forwarded-Server: server.example.com Connection: Keep-Alive - _end_of_message_ - msg.gsub!(/^ {6}/, "") - msg.gsub!("\n", "\r\n") + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server.example.com", req.server_name) @@ -436,7 +431,7 @@ def test_forwarded assert_equal("123.123.123.123", req.remote_ip) assert(!req.ssl?) - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /foo HTTP/1.1 Host: localhost:10080 User-Agent: w3m/0.5.2 @@ -445,9 +440,7 @@ def test_forwarded X-Forwarded-Server: server.example.com Connection: Keep-Alive - _end_of_message_ - msg.gsub!(/^ {6}/, "") - msg.gsub!("\n", "\r\n") + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server.example.com", req.server_name) @@ -457,7 +450,7 @@ def test_forwarded assert_equal("123.123.123.123", req.remote_ip) assert(!req.ssl?) - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /foo HTTP/1.1 Host: localhost:10080 Client-IP: 234.234.234.234 @@ -468,9 +461,7 @@ def test_forwarded X-Requested-With: XMLHttpRequest Connection: Keep-Alive - _end_of_message_ - msg.gsub!(/^ {6}/, "") - msg.gsub!("\n", "\r\n") + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server.example.com", req.server_name) @@ -480,7 +471,7 @@ def test_forwarded assert_equal("234.234.234.234", req.remote_ip) assert(req.ssl?) - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /foo HTTP/1.1 Host: localhost:10080 Client-IP: 234.234.234.234 @@ -491,9 +482,7 @@ def test_forwarded X-Requested-With: XMLHttpRequest Connection: Keep-Alive - _end_of_message_ - msg.gsub!(/^ {6}/, "") - msg.gsub!("\n", "\r\n") + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server1.example.com", req.server_name) @@ -503,7 +492,7 @@ def test_forwarded assert_equal("234.234.234.234", req.remote_ip) assert(req.ssl?) - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /foo HTTP/1.1 Host: localhost:10080 Client-IP: 234.234.234.234 @@ -514,9 +503,7 @@ def test_forwarded X-Requested-With: XMLHttpRequest Connection: Keep-Alive - _end_of_message_ - msg.gsub!(/^ {6}/, "") - msg.gsub!("\n", "\r\n") + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server1.example.com", req.server_name) @@ -526,7 +513,7 @@ def test_forwarded assert_equal("234.234.234.234", req.remote_ip) assert(req.ssl?) - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /foo HTTP/1.1 Host: localhost:10080 Client-IP: 234.234.234.234 @@ -537,9 +524,7 @@ def test_forwarded X-Requested-With: XMLHttpRequest Connection: Keep-Alive - _end_of_message_ - msg.gsub!(/^ {6}/, "") - msg.gsub!("\n", "\r\n") + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server1.example.com", req.server_name) @@ -551,13 +536,11 @@ def test_forwarded end def test_continue_sent - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") POST /path HTTP/1.1 Expect: 100-continue - _end_of_message_ - msg.gsub!(/^ {6}/, "") - msg.gsub!("\n", "\r\n") + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert req['expect'] @@ -569,12 +552,10 @@ def test_continue_sent end def test_continue_not_sent - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") POST /path HTTP/1.1 - _end_of_message_ - msg.gsub!(/^ {6}/, "") - msg.gsub!("\n", "\r\n") + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert !req['expect'] @@ -585,42 +566,42 @@ def test_continue_not_sent def test_bad_messages param = "foo=1;foo=2;foo=3;bar=x" - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1 Host: test.ruby-lang.org:8080 Content-Type: application/x-www-form-urlencoded #{param} - _end_of_message_ + HTTP assert_raise(WEBrick::HTTPStatus::LengthRequired){ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) + req.parse(StringIO.new(msg)) req.body } - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1 Host: test.ruby-lang.org:8080 Content-Length: 100000 body is too short. - _end_of_message_ + HTTP assert_raise(WEBrick::HTTPStatus::BadRequest){ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) + req.parse(StringIO.new(msg)) req.body } - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1 Host: test.ruby-lang.org:8080 Transfer-Encoding: foobar body is too short. - _end_of_message_ + HTTP assert_raise(WEBrick::HTTPStatus::NotImplemented){ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))) + req.parse(StringIO.new(msg)) req.body } end From 426e214532bb0be5e4ab8b3c9cef328432012d0d Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Fri, 5 Jul 2024 10:36:09 -0700 Subject: [PATCH 59/66] Only strip space and horizontal tab in headers Previously, all whitespace was stripped, but that goes against the related RFCs. Fixes #139 --- lib/webrick/httputils.rb | 19 +++++++++++-------- test/webrick/test_httprequest.rb | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/lib/webrick/httputils.rb b/lib/webrick/httputils.rb index ea67fdb0..92f3044d 100644 --- a/lib/webrick/httputils.rb +++ b/lib/webrick/httputils.rb @@ -178,12 +178,12 @@ def parse_header(raw) field.downcase! header[field] = HEADER_CLASSES[field].new unless header.has_key?(field) header[field] << value - when /^\s+([^\r\n\0]*?)\r\n/om + when /^[ \t]+([^\r\n\0]*?)\r\n/om unless field raise HTTPStatus::BadRequest, "bad header '#{line}'." end value = line - value.lstrip! + value.gsub!(/\A[ \t]+/, '') value.slice!(-2..-1) header[field][-1] << " " << value else @@ -191,7 +191,10 @@ def parse_header(raw) end } header.each{|key, values| - values.each(&:strip!) + values.each{|value| + value.gsub!(/\A[ \t]+/, '') + value.gsub!(/[ \t]+\z/, '') + } } header end @@ -202,7 +205,7 @@ def parse_header(raw) def split_header_value(str) str.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",]++)+) - (?:,\s*|\Z)'xn).flatten + (?:,[ \t]*|\Z)'xn).flatten end module_function :split_header_value @@ -230,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]) @@ -331,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 diff --git a/test/webrick/test_httprequest.rb b/test/webrick/test_httprequest.rb index 6088f18f..ad00d60d 100644 --- a/test/webrick/test_httprequest.rb +++ b/test/webrick/test_httprequest.rb @@ -118,6 +118,27 @@ def test_bare_lf_header } end + def test_header_vt_ff_whitespace + msg = <<~HTTP + GET / HTTP/1.1\r + Foo: \x0b1\x0c\r + \r + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + assert_equal("\x0b1\x0c", req["Foo"]) + + msg = <<~HTTP + GET / HTTP/1.1\r + Foo: \x0b1\x0c\r + \x0b2\x0c\r + \r + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + assert_equal("\x0b1\x0c \x0b2\x0c", req["Foo"]) + end + def test_bare_cr_request_line msg = <<~HTTP.gsub("\n", "\r\n") GET / HTTP/1.1\r\r From e4efb4a2300540f14f93c09c06bf0357ac1597dc Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Thu, 11 Jul 2024 15:02:21 -0700 Subject: [PATCH 60/66] Remove unnecessary gsub calls in test_httprequest.rb --- test/webrick/test_httprequest.rb | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/test/webrick/test_httprequest.rb b/test/webrick/test_httprequest.rb index ad00d60d..84faefca 100644 --- a/test/webrick/test_httprequest.rb +++ b/test/webrick/test_httprequest.rb @@ -89,7 +89,7 @@ def test_invalid_content_length_header HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ - req.parse(StringIO.new(msg.gsub(/^ {8}/, "").gsub("\n", "\r\n"))) + req.parse(StringIO.new(msg)) } end end @@ -102,7 +102,7 @@ def test_bare_lf_request_line HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) } end @@ -114,7 +114,7 @@ def test_bare_lf_header HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) } end @@ -125,7 +125,7 @@ def test_header_vt_ff_whitespace \r HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) assert_equal("\x0b1\x0c", req["Foo"]) msg = <<~HTTP @@ -135,7 +135,7 @@ def test_header_vt_ff_whitespace \r HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) assert_equal("\x0b1\x0c \x0b2\x0c", req["Foo"]) end @@ -147,7 +147,7 @@ def test_bare_cr_request_line HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) } end @@ -159,7 +159,7 @@ def test_bare_cr_header HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) } end @@ -171,7 +171,7 @@ def test_invalid_request_lines HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) } msg = <<~HTTP.gsub("\n", "\r\n") @@ -181,7 +181,7 @@ def test_invalid_request_lines HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) } msg = <<~HTTP.gsub("\n", "\r\n") @@ -191,7 +191,7 @@ def test_invalid_request_lines HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) } msg = <<~HTTP.gsub("\n", "\r\n") @@ -201,7 +201,7 @@ def test_invalid_request_lines HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) } end @@ -427,7 +427,6 @@ def test_null_byte_in_header Evil: evil\x00\r \r HTTP - msg.gsub!(/^ {6}/, "") req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ req.parse(StringIO.new(msg)) } end From 2b38d5614e876d313fe981e87c4e35b91556d226 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Fri, 5 Jul 2024 11:03:21 -0700 Subject: [PATCH 61/66] Treat missing CRLF separator after headers as an EOFError Fix tests that did not have correctly formatted headers. This changes one test, with a request line that ends in bare LF instead of CRLF, from raising BadRequest to raising EOFError, but that seems reasonable. Fixes #140 --- lib/webrick/httprequest.rb | 10 +++++++++- test/webrick/test_httprequest.rb | 23 ++++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb index 62ea54c8..f6d0b671 100644 --- a/lib/webrick/httprequest.rb +++ b/lib/webrick/httprequest.rb @@ -470,8 +470,13 @@ def read_request_line(socket) def read_header(socket) if socket + end_of_headers = false + while line = read_line(socket) - break if /\A#{CRLF}\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 @@ -480,6 +485,9 @@ def read_header(socket) 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) diff --git a/test/webrick/test_httprequest.rb b/test/webrick/test_httprequest.rb index 84faefca..122a7c1b 100644 --- a/test/webrick/test_httprequest.rb +++ b/test/webrick/test_httprequest.rb @@ -86,6 +86,7 @@ def test_invalid_content_length_header msg = <<~HTTP.gsub("\n", "\r\n") GET / HTTP/1.1 Content-Length:#{cl} + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ @@ -101,7 +102,7 @@ def test_bare_lf_request_line \r HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - assert_raise(WEBrick::HTTPStatus::BadRequest){ + assert_raise(WEBrick::HTTPStatus::EOFError){ req.parse(StringIO.new(msg)) } end @@ -210,6 +211,7 @@ def test_duplicate_content_length_header GET / HTTP/1.1 Content-Length: 1 Content-Length: 2 + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::BadRequest){ @@ -633,6 +635,25 @@ def test_eof_raised_when_line_is_nil } end + def test_eof_raised_with_missing_line_between_headers_and_body + msg = <<~HTTP.gsub("\n", "\r\n") + GET / HTTP/1.0 + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::EOFError) { + req.parse(StringIO.new(msg)) + } + + msg = <<~HTTP.gsub("\n", "\r\n") + GET / HTTP/1.0 + Foo: 1 + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::EOFError) { + req.parse(StringIO.new(msg)) + } + end + def test_cookie_join req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new("GET / HTTP/1.1\r\ncookie: a=1\r\ncookie: b=2\r\n\r\n")) From 15a93914782789520837c334e0c302702aec34e2 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Fri, 21 Jun 2024 16:49:13 -0700 Subject: [PATCH 62/66] Return 400 response for chunked requests with unexpected data after chunk Fixes #133 --- lib/webrick/httprequest.rb | 6 +++++- test/webrick/test_httprequest.rb | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb index f6d0b671..4e1de8cf 100644 --- a/lib/webrick/httprequest.rb +++ b/lib/webrick/httprequest.rb @@ -574,7 +574,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/test/webrick/test_httprequest.rb b/test/webrick/test_httprequest.rb index 122a7c1b..ea7e5a9d 100644 --- a/test/webrick/test_httprequest.rb +++ b/test/webrick/test_httprequest.rb @@ -423,6 +423,29 @@ def test_bad_chunked end end + def test_bad_chunked_extra_data + msg = <<~HTTP + POST /path HTTP/1.1\r + Transfer-Encoding: chunked\r + \r + 3\r + ABCthis-all-gets-ignored\r + 0\r + \r + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + assert_raise(WEBrick::HTTPStatus::BadRequest){ req.body } + + # chunked req.body_reader + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + dst = StringIO.new + assert_raise(WEBrick::HTTPStatus::BadRequest) do + IO.copy_stream(req.body_reader, dst) + end + end + def test_null_byte_in_header msg = <<~HTTP.gsub("\n", "\r\n") POST /path HTTP/1.1\r From 0c600e169bd4ae267cb5eeb6197277c848323bbe Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 23 Jul 2024 14:22:49 +0200 Subject: [PATCH 63/66] Fix reference to URI::REGEXP::PATTERN::HOST Recent changes in the URI gem make it so that this constant may not be there. And I think even with older versions of the gem, it would depend on what the default parser was set to. --- lib/webrick/httprequest.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb index 4e1de8cf..527099fa 100644 --- a/lib/webrick/httprequest.rb +++ b/lib/webrick/httprequest.rb @@ -522,9 +522,10 @@ 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) - pattern = /\A(#{URI::REGEXP::PATTERN::HOST})(?::(\d+))?\z/no - host.scan(pattern)[0] + host.scan(HOST_PATTERN)[0] end def read_body(socket, block) From f5faca9222541591e1a7c3c97552ebb0c92733c7 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Wed, 18 Sep 2024 14:11:49 -0700 Subject: [PATCH 64/66] Prevent request smuggling If a request has both a content-length and transfer-encoding headers, return a 400 response. This is allowed by RFC 7230 section 3.3.3.3. Fixes #145 --- lib/webrick/httprequest.rb | 4 ++++ test/webrick/test_httprequest.rb | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb index 527099fa..0351a13e 100644 --- a/lib/webrick/httprequest.rb +++ b/lib/webrick/httprequest.rb @@ -531,6 +531,10 @@ def parse_host_request_line(host) 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}." diff --git a/test/webrick/test_httprequest.rb b/test/webrick/test_httprequest.rb index ea7e5a9d..d1283d4e 100644 --- a/test/webrick/test_httprequest.rb +++ b/test/webrick/test_httprequest.rb @@ -219,6 +219,24 @@ def test_duplicate_content_length_header } end + def test_content_length_and_transfer_encoding_headers_smuggling + msg = <<~HTTP.gsub("\n", "\r\n") + POST /user HTTP/1.1 + Content-Length: 28 + Transfer-Encoding: chunked + + 0 + + GET /admin HTTP/1.1 + + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.body + } + end + def test_parse_headers msg = <<~HTTP.gsub("\n", "\r\n") GET /path HTTP/1.1 From b9a4c81ea94dec02a750c6b34092c55234519bf1 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 24 Sep 2024 11:26:39 +0900 Subject: [PATCH 65/66] Removed trailing spaces --- lib/webrick/httprequest.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb index 0351a13e..1fa5dbb4 100644 --- a/lib/webrick/httprequest.rb +++ b/lib/webrick/httprequest.rb @@ -531,7 +531,7 @@ def parse_host_request_line(host) def read_body(socket, block) return unless socket if tc = self['transfer-encoding'] - if self['content-length'] + if self['content-length'] raise HTTPStatus::BadRequest, "request with both transfer-encoding and content-length, possible request smuggling" end From 0fb9de6788a3ba5fe903e63d778a0fb8c1dce786 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 24 Sep 2024 11:30:49 +0900 Subject: [PATCH 66/66] Bump up v1.8.2 --- lib/webrick/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/webrick/version.rb b/lib/webrick/version.rb index ceeefc33..fbea0dda 100644 --- a/lib/webrick/version.rb +++ b/lib/webrick/version.rb @@ -14,5 +14,5 @@ module WEBrick ## # The WEBrick version - VERSION = "1.8.1" + VERSION = "1.8.2" end