HBASE-24806 Small Updates to Functionality of Shell IRB Workspace (#2232)

* HBASE-24806 Small Updates to Functionality of Shell IRB Workspace

- Move exception handler from Shell::Shell#eval_io to new method,
  Shell::Shell#exception_handler
- Add unit tests for Shell::Shell#exception_handler
- Change Shell::Shell#eval_io to no longer raise SystemExit when any error is
  seen and update unit test
- Update ruby test runner to catch SystemExit and fail to avoid tests that
  cause the test runner to incorrectly exit successfully
- Add Hbase::Loader module to find ruby scripts in the $LOAD_PATH and classpath
  using JRuby's loader.
- In hbase-shell, install IRB commands before exporting HBase commands. The
  HBase commands will override the IRB commands, and no warning will be
  printed.

* Remove unused variables from shell_test

Signed-off-by: Nick Dimiduk <ndimiduk@apache.org>
Signed-off-by: stack <stack@apache.org>
This commit is contained in:
Elliot 2020-08-18 16:14:34 -04:00 committed by GitHub
parent 6789aca9a0
commit 98e35842eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 115 additions and 20 deletions

View File

@ -174,16 +174,20 @@ def debug?
nil nil
end end
# For backwards compatibility, this will export all the HBase shell commands, constants, and # For backwards compatibility, this will export all the HBase shell commands, constants, and
# instance variables (@hbase and @shell) onto Ruby's top-level receiver object known as "main". # instance variables (@hbase and @shell) onto Ruby's top-level receiver object known as "main".
@shell.export_all(self) if top_level_definitions @shell.export_all(self) if top_level_definitions
# If script2run, try running it. If we're in interactive mode, will go on to run the shell unless # If script2run, try running it. If we're in interactive mode, will go on to run the shell unless
# script calls 'exit' or 'exit 0' or 'exit errcode'. # script calls 'exit' or 'exit 0' or 'exit errcode'.
@shell.eval_io(File.new(script2run)) if script2run require 'shell/hbase_loader'
if script2run
::Shell::Shell.exception_handler(!$fullBackTrace) { @shell.eval_io(Hbase::Loader.file_for_load(script2run), filename = script2run) }
end
# If we are not running interactively, evaluate standard input # If we are not running interactively, evaluate standard input
@shell.eval_io(STDIN) unless interactive ::Shell::Shell.exception_handler(!$fullBackTrace) { @shell.eval_io(STDIN) } unless interactive
if interactive if interactive
# Output a banner message that tells users where to go for help # Output a banner message that tells users where to go for help

View File

@ -297,11 +297,11 @@ For more on the HBase Shell, see http://hbase.apache.org/book.html
return @irb_workspace unless @irb_workspace.nil? return @irb_workspace unless @irb_workspace.nil?
hbase_receiver = HBaseReceiver.new hbase_receiver = HBaseReceiver.new
# Install all the hbase commands, constants, and instance variables @shell and @hbase. This
# must be BEFORE the irb commands are installed so that our help command is not overwritten.
export_all(hbase_receiver)
# install all the IRB commands onto our receiver # install all the IRB commands onto our receiver
IRB::ExtendCommandBundle.extend_object(hbase_receiver) IRB::ExtendCommandBundle.extend_object(hbase_receiver)
# Install all the hbase commands, constants, and instance variables @shell and @hbase. This
# will override names that conflict with IRB methods like "help".
export_all(hbase_receiver)
::IRB::WorkSpace.new(hbase_receiver.get_binding) ::IRB::WorkSpace.new(hbase_receiver.get_binding)
end end
@ -311,7 +311,10 @@ For more on the HBase Shell, see http://hbase.apache.org/book.html
# Unlike Ruby's require or load, this method allows us to execute code with a custom binding. In # Unlike Ruby's require or load, this method allows us to execute code with a custom binding. In
# this case, we are using the binding constructed with all the HBase shell constants and # this case, we are using the binding constructed with all the HBase shell constants and
# methods. # methods.
def eval_io(io) #
# @param [IO] io instance of Ruby's IO (or subclass like File) to read script from
# @param [String] filename to print in tracebacks
def eval_io(io, filename = 'stdin')
require 'irb/ruby-lex' require 'irb/ruby-lex'
# Mixing HBaseIOExtensions into IO allows us to pass IO objects to RubyLex. # Mixing HBaseIOExtensions into IO allows us to pass IO objects to RubyLex.
IO.include HBaseIOExtensions IO.include HBaseIOExtensions
@ -320,10 +323,20 @@ For more on the HBase Shell, see http://hbase.apache.org/book.html
scanner = RubyLex.new scanner = RubyLex.new
scanner.set_input(io) scanner.set_input(io)
scanner.each_top_level_statement do |statement, linenum|
puts(workspace.evaluate(nil, statement, filename, linenum))
end
nil
end
##
# Runs a block and logs exception from both Ruby and Java, optionally discarding the traceback
#
# @param [Boolean] hide_traceback if true, Exceptions will be converted to
# a SystemExit so that the traceback is not printed
def self.exception_handler(hide_traceback)
begin begin
scanner.each_top_level_statement do |statement, linenum| yield
puts(workspace.evaluate(nil, statement, 'stdin', linenum))
end
rescue Exception => e rescue Exception => e
message = e.to_s message = e.to_s
# exception unwrapping in shell means we'll have to handle Java exceptions # exception unwrapping in shell means we'll have to handle Java exceptions
@ -335,12 +348,9 @@ For more on the HBase Shell, see http://hbase.apache.org/book.html
# Include the 'ERROR' string to try to make transition easier for scripts that # Include the 'ERROR' string to try to make transition easier for scripts that
# may have already been relying on grepping output. # may have already been relying on grepping output.
puts "ERROR #{e.class}: #{message}" puts "ERROR #{e.class}: #{message}"
if $fullBacktrace raise e unless hide_traceback
# re-raising the will include a backtrace and exit.
raise e exit 1
else
exit 1
end
end end
nil nil
end end

View File

@ -0,0 +1,56 @@
#
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
module Hbase
##
# HBase::Loader serves a similar purpose to IRB::IrbLoader, but with a different separation of
# concerns. This loader allows us to directly get the path for a filename in ruby's load path,
# and then use that in combination with something like HBase::Shell#eval_io.
module Loader
##
# Determine the loadable path for a given filename by searching through $LOAD_PATH
#
# This serves a similar purpose to IRB::IrbLoader#search_file_from_ruby_path, but uses JRuby's
# loader, which allows us to find special paths like "uri:classloader" inside of a Jar.
#
# @param [String] filename
# @return [String] path
def self.path_for_load(filename)
return File.absolute_path(filename) if File.exist? filename
# Get JRuby's LoadService from the global (singleton) instance of the runtime
# (org.jruby.Ruby), which allows us to use JRuby's tools for searching the load path.
runtime = org.jruby.Ruby.getGlobalRuntime
loader = runtime.getLoadService
search_state = loader.findFileForLoad filename
raise LoadError, "no such file to load -- #{filename}" if search_state.library.nil?
search_state.loadName
end
##
# Return a file handle for the given file found in the load path
#
# @param [String] filename
# @return [File] file handle
def self.file_for_load(filename)
File.new(path_for_load(filename))
end
end
end

View File

@ -114,6 +114,7 @@ class ShellTest < Test::Unit::TestCase
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
define_test 'Shell::Shell#eval_io should evaluate IO' do define_test 'Shell::Shell#eval_io should evaluate IO' do
StringIO.include HBaseIOExtensions
# check that at least one of the commands is present while evaluating # check that at least one of the commands is present while evaluating
io = StringIO.new <<~EOF io = StringIO.new <<~EOF
puts (respond_to? :list) puts (respond_to? :list)
@ -123,7 +124,7 @@ class ShellTest < Test::Unit::TestCase
# check that at least one of the HBase constants is present while evaluating # check that at least one of the HBase constants is present while evaluating
io = StringIO.new <<~EOF io = StringIO.new <<~EOF
ROWPREFIXFILTER ROWPREFIXFILTER.dump
EOF EOF
output = capture_stdout { @shell.eval_io(io) } output = capture_stdout { @shell.eval_io(io) }
assert_match(/"ROWPREFIXFILTER"/, output) assert_match(/"ROWPREFIXFILTER"/, output)
@ -131,6 +132,25 @@ class ShellTest < Test::Unit::TestCase
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
define_test 'Shell::Shell#exception_handler should hide traceback' do
class TestException < RuntimeError; end
# When hide_traceback is true, exception_handler should replace exceptions
# with SystemExit so that the traceback is not printed.
assert_raises(SystemExit) do
::Shell::Shell.exception_handler(true) { raise TestException, 'Custom Exception' }
end
end
define_test 'Shell::Shell#exception_handler should show traceback' do
class TestException < RuntimeError; end
# When hide_traceback is false, exception_handler should re-raise Exceptions
assert_raises(TestException) do
::Shell::Shell.exception_handler(false) { raise TestException, 'Custom Exception' }
end
end
#-----------------------------------------------------------------------------
define_test 'Shell::Shell#print_banner should display Reference Guide link' do define_test 'Shell::Shell#print_banner should display Reference Guide link' do
@shell.interactive = true @shell.interactive = true
output = capture_stdout { @shell.print_banner } output = capture_stdout { @shell.print_banner }
@ -141,7 +161,7 @@ class ShellTest < Test::Unit::TestCase
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
define_test "Shell::Shell interactive mode should not throw" do define_test 'Shell::Shell interactive mode should not throw' do
# incorrect number of arguments # incorrect number of arguments
@shell.command('create', 'nothrow_table') @shell.command('create', 'nothrow_table')
@shell.command('create', 'nothrow_table', 'family_1') @shell.command('create', 'nothrow_table', 'family_1')

View File

@ -82,9 +82,14 @@ if java.lang.System.get_property('shell.test')
puts "Only running tests that match #{shell_test_pattern}" puts "Only running tests that match #{shell_test_pattern}"
runner_args << "--testcase=#{shell_test_pattern}" runner_args << "--testcase=#{shell_test_pattern}"
end end
# first couple of args are to match the defaults, so we can pass options to limit the tests run begin
if !(Test::Unit::AutoRunner.run(false, nil, runner_args)) # first couple of args are to match the defaults, so we can pass options to limit the tests run
raise "Shell unit tests failed. Check output file for details." unless Test::Unit::AutoRunner.run(false, nil, runner_args)
raise 'Shell unit tests failed. Check output file for details.'
end
rescue SystemExit => e
# Unit tests should not raise uncaught SystemExit exceptions. This could cause tests to be ignored.
raise 'Caught SystemExit during unit test execution! Check output file for details.'
end end
puts "Done with tests! Shutting down the cluster..." puts "Done with tests! Shutting down the cluster..."