From 3366d4bcbc8614eb3b2681151f89da8b3ca31694 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Mon, 18 Apr 2016 15:14:02 -0700 Subject: [PATCH] Add Exception::Warning module for customized warning handling All warnings raised call Exception::Warning.warn, which by default does the same thing it does currently (rb_write_error_str). You can override Exception::Warning.warn to change the behavior. To provide a common API that can be used to modify warnings, add a lib/warning.rb file, which offers three methods: * Exception::Warning.ignore * Exception::Warning.process * Exception::Warning.clear Exception::Warning.ignore takes a regexp and optionally a path prefix, and ignores any warning that matches the regular expression if it starts with the path prefix. Exception::Warning.process takes an optional path prefix and a block, and if the string starts with the path prefix, it calls the block with the warning string instead of performing the default behavior. You can call Warning.process multiple times and it will be operate intelligently, choosing the longest path prefix that the string starts with. Exception::Warning.clear just clears the current ignored warnings and warning processors. By using path prefixes, it's fairly easy for a gem to set that warnings should be ignored inside the gem's directory. Note that path prefixes will not correctly handle warnings raised by Kernel#warn, unless the warning message given to Kernel#warn starts with the filename where the warning is used. --- error.c | 37 +++++++++++--- lib/warning.rb | 75 ++++++++++++++++++++++++++++ test/ruby/test_exception.rb | 116 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+), 8 deletions(-) create mode 100644 lib/warning.rb diff --git a/error.c b/error.c index b795dc1..965e915 100644 --- a/error.c +++ b/error.c @@ -42,6 +42,9 @@ VALUE rb_iseqw_new(const rb_iseq_t *); VALUE rb_eEAGAIN; VALUE rb_eEWOULDBLOCK; VALUE rb_eEINPROGRESS; +VALUE rb_mWarning; + +static ID id_warn; extern const char ruby_description[]; @@ -147,6 +150,19 @@ ruby_only_for_internal_use(const char *func) } static VALUE +rb_warning_s_warn(VALUE mod, VALUE str) +{ + rb_write_error_str(str); + return Qnil; +} + +static void +rb_write_warning_str(VALUE str) +{ + rb_funcall(rb_mWarning, id_warn, 1, str); +} + +static VALUE warn_vsprintf(rb_encoding *enc, const char *file, int line, const char *fmt, va_list args) { VALUE str = rb_enc_str_new(0, 0, enc); @@ -166,7 +182,7 @@ rb_compile_warn(const char *file, int line, const char *fmt, ...) va_start(args, fmt); str = warn_vsprintf(NULL, file, line, fmt, args); va_end(args); - rb_write_error_str(str); + rb_write_warning_str(str); } /* rb_compile_warning() reports only in verbose mode */ @@ -181,7 +197,7 @@ rb_compile_warning(const char *file, int line, const char *fmt, ...) va_start(args, fmt); str = warn_vsprintf(NULL, file, line, fmt, args); va_end(args); - rb_write_error_str(str); + rb_write_warning_str(str); } static VALUE @@ -206,7 +222,7 @@ rb_warn(const char *fmt, ...) va_start(args, fmt); mesg = warning_string(0, fmt, args); va_end(args); - rb_write_error_str(mesg); + rb_write_warning_str(mesg); } void @@ -220,7 +236,7 @@ rb_enc_warn(rb_encoding *enc, const char *fmt, ...) va_start(args, fmt); mesg = warning_string(enc, fmt, args); va_end(args); - rb_write_error_str(mesg); + rb_write_warning_str(mesg); } /* rb_warning() reports only in verbose mode */ @@ -235,7 +251,7 @@ rb_warning(const char *fmt, ...) va_start(args, fmt); mesg = warning_string(0, fmt, args); va_end(args); - rb_write_error_str(mesg); + rb_write_warning_str(mesg); } #if 0 @@ -250,7 +266,7 @@ rb_enc_warning(rb_encoding *enc, const char *fmt, ...) va_start(args, fmt); mesg = warning_string(enc, fmt, args); va_end(args); - rb_write_error_str(mesg); + rb_write_warning_str(mesg); } #endif @@ -2026,6 +2042,10 @@ Init_Exception(void) rb_mErrno = rb_define_module("Errno"); + rb_mWarning = rb_define_module_under(rb_eException, "Warning"); + rb_define_method(rb_mWarning, "warn", rb_warning_s_warn, 1); + rb_extend_object(rb_mWarning, rb_mWarning); + rb_define_global_function("warn", rb_warn_m, -1); id_new = rb_intern_const("new"); @@ -2040,6 +2060,7 @@ Init_Exception(void) id_Errno = rb_intern_const("Errno"); id_errno = rb_intern_const("errno"); id_i_path = rb_intern_const("@path"); + id_warn = rb_intern_const("warn"); id_iseq = rb_make_internal_id(); } @@ -2263,7 +2284,7 @@ rb_sys_warning(const char *fmt, ...) va_end(args); rb_str_set_len(mesg, RSTRING_LEN(mesg)-1); rb_str_catf(mesg, ": %s\n", strerror(errno_save)); - rb_write_error_str(mesg); + rb_write_warning_str(mesg); errno = errno_save; } @@ -2283,7 +2304,7 @@ rb_sys_enc_warning(rb_encoding *enc, const char *fmt, ...) va_end(args); rb_str_set_len(mesg, RSTRING_LEN(mesg)-1); rb_str_catf(mesg, ": %s\n", strerror(errno_save)); - rb_write_error_str(mesg); + rb_write_warning_str(mesg); errno = errno_save; } diff --git a/lib/warning.rb b/lib/warning.rb new file mode 100644 index 0000000..f50b53e --- /dev/null +++ b/lib/warning.rb @@ -0,0 +1,75 @@ +require 'monitor' + +module Exception::Warning + module Processor + # Clear all current ignored warnings and warning processors. + def clear + synchronize do + @ignore.clear + @process.clear + end + end + + # Ignore any warning messages matching the given regexp, if they + # start with the given path. Examples: + # + # # Ignore all uninitialized instance variable warnings + # Warning.ignore(/instance variable @\w+ not initialized/) + # + # # Ignore all uninitialized instance variable warnings in current file + # Warning.ignore(/instance variable @\w+ not initialized/, __FILE__) + def ignore(regexp, path='') + synchronize do + @ignore << [path, regexp] + end + nil + end + + # Handle all warnings starting with the given path, instead of + # the default behavior of printing them to $stderr. Examples: + # + # # Write warning to LOGGER at level warning + # Warning.process do |warning| + # LOGGER.warning(warning) + # end + # + # # Write warnings in the current file to LOGGER at level error level + # Warning.process(__FILE__) do |warning| + # LOGGER.error(warning) + # end + def process(path='', &block) + synchronize do + @process << [path, block] + @process.sort_by!(&:first) + @process.reverse! + end + nil + end + + # Handle ignored warnings and warning processors. If the warning is + # not ignored and there is no warning processor setup for the warning + # string, then use the default behavior of writing to $stderr. + def warn(str) + synchronize{@ignore.dup}.each do |path, regexp| + if str.start_with?(path) && str =~ regexp + return + end + end + + synchronize{@process.dup}.each do |path, block| + if str.start_with?(path) + block.call(str) + return + end + end + + super + end + end + + @ignore = [] + @process = [] + + extend MonitorMixin + extend Processor +end diff --git a/test/ruby/test_exception.rb b/test/ruby/test_exception.rb index 5023262..9b8192b 100644 --- a/test/ruby/test_exception.rb +++ b/test/ruby/test_exception.rb @@ -855,4 +855,120 @@ def test_message_of_name_error end end end + + def test_warning_ignore + obj = Object.new + + assert_warning /instance variable @ivar not initialized/ do + assert_nil(obj.instance_variable_get(:@ivar)) + end + + require 'warning' + + assert_warning /instance variable @ivar not initialized/ do + assert_nil(obj.instance_variable_get(:@ivar)) + end + + Exception::Warning.ignore(/instance variable @ivar not initialized/) + + assert_warning '' do + assert_nil(obj.instance_variable_get(:@ivar)) + end + + assert_warning /instance variable @ivar2 not initialized/ do + assert_nil(obj.instance_variable_get(:@ivar2)) + end + + Exception::Warning.ignore(/instance variable @ivar2 not initialized/, __FILE__) + + assert_warning '' do + assert_nil(obj.instance_variable_get(:@ivar2)) + end + + assert_warning /instance variable @ivar3 not initialized/ do + assert_nil(obj.instance_variable_get(:@ivar3)) + end + + Exception::Warning.ignore(/instance variable @ivar3 not initialized/, __FILE__+'a') + + assert_warning /instance variable @ivar3 not initialized/ do + assert_nil(obj.instance_variable_get(:@ivar3)) + end + + Exception::Warning.clear + + assert_warning /instance variable @ivar not initialized/ do + assert_nil(obj.instance_variable_get(:@ivar)) + end + ensure + Exception::Warning.clear + end + + def test_warning_process + obj = Object.new + warn = nil + + require 'warning' + + Exception::Warning.process(__FILE__+'a') do |warning| + warn = [0, warning] + end + + assert_warning /instance variable @ivar not initialized/ do + assert_nil(obj.instance_variable_get(:@ivar)) + end + assert_nil(warn) + + Exception::Warning.process(__FILE__) do |warning| + warn = [1, warning] + end + + assert_warning '' do + assert_nil(obj.instance_variable_get(:@ivar2)) + end + assert_equal(1, warn.first) + assert_match(/instance variable @ivar2 not initialized/, warn.last) + warn = nil + + Exception::Warning.process(File.dirname(__FILE__)) do |warning| + warn = [2, warning] + end + + assert_warning '' do + assert_nil(obj.instance_variable_get(:@ivar3)) + end + assert_equal(1, warn.first) + assert_match(/instance variable @ivar3 not initialized/, warn.last) + warn = nil + + Exception::Warning.process(__FILE__+':') do |warning| + warn = [3, warning] + end + + assert_warning '' do + assert_nil(obj.instance_variable_get(:@ivar4)) + end + assert_equal(3, warn.first) + assert_match(/instance variable @ivar4 not initialized/, warn.last) + warn = nil + + Exception::Warning.clear + + assert_warning /instance variable @ivar5 not initialized/ do + assert_nil(obj.instance_variable_get(:@ivar5)) + end + assert_nil(warn) + + Exception::Warning.process do |warning| + warn = [4, warning] + end + + assert_warning '' do + assert_nil(obj.instance_variable_get(:@ivar6)) + end + assert_equal(4, warn.first) + assert_match(/instance variable @ivar6 not initialized/, warn.last) + ensure + Exception::Warning.clear + end end -- 2.8.4