diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ac236cb..2be44372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,17 @@ #### Features +* Add Pry::Testable, an improved modular replacement for PryTestHelpers. + **breaking change**. [#1679](https://github.com/pry/pry/pull/1679). + * Add a new category module: "Pry::Platform". Loosely related to #1668 below. [#1670](https://github.com/pry/pry/pull/1670) * Add `mac_osx?` and `linux?` utility functions to Pry::Helpers::BaseHelpers. -[#1668](https://github.com/pry/pry/pull/1668) +[#1668](https://github.com/pry/pry/pull/1668). * Add utility functions for drawing colorised text on a colorised background. -[#1673](https://github.com/pry/pry/pull/1673) +[#1673](https://github.com/pry/pry/pull/1673). #### Bug fixes diff --git a/lib/pry/test/helper.rb b/lib/pry/test/helper.rb deleted file mode 100644 index e71536a0..00000000 --- a/lib/pry/test/helper.rb +++ /dev/null @@ -1,179 +0,0 @@ -require 'pry' - -# in case the tests call reset_defaults, ensure we reset them to -# amended (test friendly) values -class << Pry - alias_method :orig_reset_defaults, :reset_defaults - def reset_defaults - orig_reset_defaults - - Pry.config.color = false - Pry.config.pager = false - Pry.config.should_load_rc = false - Pry.config.should_load_local_rc= false - Pry.config.should_load_plugins = false - Pry.config.history.should_load = false - Pry.config.history.should_save = false - Pry.config.correct_indent = false - Pry.config.hooks = Pry::Hooks.new - Pry.config.collision_warning = false - end -end -Pry.reset_defaults - -# A global space for storing temporary state during tests. - -module PryTestHelpers - - module_function - - # inject a variable into a binding - def inject_var(name, value, b) - Pry.current[:pry_local] = value - b.eval("#{name} = ::Pry.current[:pry_local]") - ensure - Pry.current[:pry_local] = nil - end - - def constant_scope(*names) - names.each do |name| - Object.remove_const name if Object.const_defined?(name) - end - - yield - ensure - names.each do |name| - Object.remove_const name if Object.const_defined?(name) - end - end - - # Open a temp file and yield it to the block, closing it after - # @return [String] The path of the temp file - def temp_file(ext='.rb') - file = Tempfile.new(['pry', ext]) - yield file - ensure - file.close(true) if file - File.unlink("#{file.path}c") if File.exist?("#{file.path}c") # rbx - end - - def unindent(*args) - Pry::Helpers::CommandHelpers.unindent(*args) - end - - def mock_command(cmd, args=[], opts={}) - output = StringIO.new - pry = Pry.new(output: output) - ret = cmd.new(opts.merge(pry_instance: pry, :output => output)).call_safely(*args) - Struct.new(:output, :return).new(output.string, ret) - end - - def mock_exception(*mock_backtrace) - StandardError.new.tap do |e| - e.define_singleton_method(:backtrace) { mock_backtrace } - end - end - - def inner_scope - catch(:inner_scope) do - yield ->{ throw(:inner_scope, self) } - end - end -end - -def pry_tester(*args, &block) - if args.length == 0 || args[0].is_a?(Hash) - args.unshift(Pry.toplevel_binding) - end - - PryTester.new(*args).tap do |t| - (class << t; self; end).class_eval(&block) if block - end -end - -def pry_eval(*eval_strs) - if eval_strs.first.is_a? String - binding = Pry.toplevel_binding - else - binding = Pry.binding_for(eval_strs.shift) - end - - pry_tester(binding).eval(*eval_strs) -end - -class PryTester - extend Pry::Forwardable - - attr_reader :pry, :out - - def_delegators :@pry, :eval_string, :eval_string= - - def initialize(target = TOPLEVEL_BINDING, options = {}) - @pry = Pry.new(options.merge(:target => target)) - @history = options[:history] - - @pry.inject_sticky_locals! - reset_output - end - - def eval(*strs) - reset_output - result = nil - - strs.flatten.each do |str| - # Check for space prefix. See #1369. - if str !~ /^\s\S/ - str = "#{str.strip}\n" - end - @history.push str if @history - - if @pry.process_command(str) - result = last_command_result_or_output - else - result = @pry.evaluate_ruby(str) - end - end - - result - end - - def push(*lines) - Array(lines).flatten.each do |line| - @pry.eval(line) - end - end - - def push_binding(context) - @pry.push_binding context - end - - def last_output - @out.string if @out - end - - def process_command(command_str) - @pry.process_command(command_str) or raise "Not a valid command" - last_command_result_or_output - end - - def last_command_result - result = Pry.current[:pry_cmd_result] - result.retval if result - end - - protected - - def last_command_result_or_output - result = last_command_result - if result != Pry::Command::VOID_VALUE - result - else - last_output - end - end - - def reset_output - @out = StringIO.new - @pry.output = @out - end -end diff --git a/lib/pry/testable.rb b/lib/pry/testable.rb new file mode 100644 index 00000000..afc4dc25 --- /dev/null +++ b/lib/pry/testable.rb @@ -0,0 +1,70 @@ +# good idea ??? +# if you're testing pry plugin you should require pry by yourself, no? +require 'pry' if not defined?(Pry) + +module Pry::Testable + extend self + require_relative "testable/pry_tester" + require_relative "testable/evalable" + require_relative "testable/mockable" + require_relative "testable/variables" + require_relative "testable/utility" + + # + # When {Pry::Testable} is included into another module or class, + # the following modules are also included: {Pry::Testable::Mockable}, + # {Pry::Testable::Evalable}, {Pry::Testable::Variables}, and + # {Pry::Testable::Utility}. + # + # @note + # Each of the included modules mentioned above may also be used + # standalone or in a pick-and-mix fashion. + # + # @param [Module] mod + # A class or module. + # + # @return [void] + # + def self.included(mod) + mod.module_eval do + include Pry::Testable::Mockable + include Pry::Testable::Evalable + include Pry::Testable::Variables + include Pry::Testable::Utility + end + end + + TEST_DEFAULTS = { + color: false, + pager: false, + should_load_rc: false, + should_load_local_rc: false, + correct_indent: false, + collison_warning: false, + history: { + should_load: false, + should_save: false + } + } + private_constant :TEST_DEFAULTS + + # + # Sets various configuration options that make Pry optimal for a test + # environment, see source code for complete details. + # + # @return [void] + # + def self.set_testenv_variables + Pry.config = Pry::Config.from_hash(TEST_DEFAULTS, Pry::Config::Default.new) + Pry.config.hooks = Pry::Hooks.new + end + + # + # Reset the Pry configuration to their default values. + # + # @return [void] + # + def self.unset_testenv_variables + Pry.config = Pry::Config.from_hash({}, Pry::Config::Default.new) + end +end diff --git a/lib/pry/testable/evalable.rb b/lib/pry/testable/evalable.rb new file mode 100644 index 00000000..a1cb84a3 --- /dev/null +++ b/lib/pry/testable/evalable.rb @@ -0,0 +1,15 @@ +module Pry::Testable::Evalable + def pry_tester(*args, &block) + if args.length == 0 || args[0].is_a?(Hash) + args.unshift(Pry.toplevel_binding) + end + Pry::Testable::PryTester.new(*args).tap do |t| + t.singleton_class.class_eval(&block) if block + end + end + + def pry_eval(*eval_strs) + b = String === eval_strs.first ? Pry.toplevel_binding : Pry.binding_for(eval_strs.shift) + pry_tester(b).eval(*eval_strs) + end +end diff --git a/lib/pry/testable/mockable.rb b/lib/pry/testable/mockable.rb new file mode 100644 index 00000000..2b3d7581 --- /dev/null +++ b/lib/pry/testable/mockable.rb @@ -0,0 +1,14 @@ +module Pry::Testable::Mockable + def mock_command(cmd, args=[], opts={}) + output = StringIO.new + pry = Pry.new(output: output) + ret = cmd.new(opts.merge(pry_instance: pry, :output => output)).call_safely(*args) + Struct.new(:output, :return).new(output.string, ret) + end + + def mock_exception(*mock_backtrace) + StandardError.new.tap do |e| + e.define_singleton_method(:backtrace) { mock_backtrace } + end + end +end diff --git a/lib/pry/testable/pry_tester.rb b/lib/pry/testable/pry_tester.rb new file mode 100644 index 00000000..39d32dce --- /dev/null +++ b/lib/pry/testable/pry_tester.rb @@ -0,0 +1,73 @@ +class Pry::Testable::PryTester + extend Pry::Forwardable + attr_reader :pry, :out + def_delegators :@pry, :eval_string, :eval_string= + + def initialize(target = TOPLEVEL_BINDING, options = {}) + @pry = Pry.new(options.merge(:target => target)) + @history = options[:history] + @pry.inject_sticky_locals! + reset_output + end + + def eval(*strs) + reset_output + result = nil + + strs.flatten.each do |str| + # Check for space prefix. See #1369. + if str !~ /^\s\S/ + str = "#{str.strip}\n" + end + @history.push str if @history + + if @pry.process_command(str) + result = last_command_result_or_output + else + result = @pry.evaluate_ruby(str) + end + end + + result + end + + def push(*lines) + Array(lines).flatten.each do |line| + @pry.eval(line) + end + end + + def push_binding(context) + @pry.push_binding context + end + + def last_output + @out.string if @out + end + + def process_command(command_str) + @pry.process_command(command_str) or raise "Not a valid command" + last_command_result_or_output + end + + def last_command_result + result = Pry.current[:pry_cmd_result] + result.retval if result + end + + protected + + def last_command_result_or_output + result = last_command_result + if result != Pry::Command::VOID_VALUE + result + else + last_output + end + end + + def reset_output + @out = StringIO.new + @pry.output = @out + end +end diff --git a/lib/pry/testable/utility.rb b/lib/pry/testable/utility.rb new file mode 100644 index 00000000..c0df14f2 --- /dev/null +++ b/lib/pry/testable/utility.rb @@ -0,0 +1,26 @@ +module Pry::Testable::Utility + # + # Creates a Tempfile then unlinks it after the block has yielded. + # + # @yieldparam [String] file + # The path of the temp file + # + # @return [void] + # + def temp_file(ext='.rb') + file = Tempfile.open(['pry', ext]) + yield file + ensure + file.close(true) if file + end + + def unindent(*args) + Pry::Helpers::CommandHelpers.unindent(*args) + end + + def inner_scope + catch(:inner_scope) do + yield ->{ throw(:inner_scope, self) } + end + end +end diff --git a/lib/pry/testable/variables.rb b/lib/pry/testable/variables.rb new file mode 100644 index 00000000..13f1bcb3 --- /dev/null +++ b/lib/pry/testable/variables.rb @@ -0,0 +1,46 @@ +module Pry::Testable::Variables + # + # @example + # temporary_constants(:Foo, :Bar) do + # Foo = Class.new(RuntimeError) + # Bar = Class.new(RuntimeError) + # end + # Foo # => NameError + # Bar # => NameError + # + # @param [Array] *names + # An array of constant names that be defined by a block, + # and removed by this method afterwards. + # + # @return [void] + # + def temporary_constants(*names) + names.each do |name| + Object.remove_const name if Object.const_defined?(name) + end + yield + ensure + names.each do |name| + Object.remove_const name if Object.const_defined?(name) + end + end + + # + # @param [String] name + # The name of a variable. + # + # @param [String] value + # Its value. + # + # @param [Binding] b + # The binding object to insert a variable into. + # + # @return [void] + # + def insert_variable(name, value, b) + Pry.current[:pry_local] = value + b.eval("#{name} = ::Pry.current[:pry_local]") + ensure + Pry.current[:pry_local] = nil + end +end diff --git a/spec/command_spec.rb b/spec/command_spec.rb index f4c0237b..fd47cfef 100644 --- a/spec/command_spec.rb +++ b/spec/command_spec.rb @@ -461,7 +461,7 @@ describe "Pry::Command" do before do @context = Object.new @set.command "walking-spanish", "down the hall", :takes_block => true do - PryTestHelpers.inject_var(:@x, command_block.call, target) + insert_variable(:@x, command_block.call, target) end @set.import Pry::Commands @@ -482,9 +482,9 @@ describe "Pry::Command" do @set.block_command "walking-spanish", "litella's been screeching for a blind pig.", :takes_block => true do |x, y| - PryTestHelpers.inject_var(:@x, x, target) - PryTestHelpers.inject_var(:@y, y, target) - PryTestHelpers.inject_var(:@block_var, command_block.call, target) + insert_variable(:@x, x, target) + insert_variable(:@y, y, target) + insert_variable(:@block_var, command_block.call, target) end @t.eval 'walking-spanish john carl| { :jesus }' @@ -516,8 +516,8 @@ describe "Pry::Command" do describe "arg_string" do it 'should remove block-related content from arg_string (with one normal arg)' do @set.block_command "walking-spanish", "down the hall", :takes_block => true do |x, y| - PryTestHelpers.inject_var(:@arg_string, arg_string, target) - PryTestHelpers.inject_var(:@x, x, target) + insert_variable(:@arg_string, arg_string, target) + insert_variable(:@x, x, target) end @t.eval 'walking-spanish john| { :jesus }' @@ -527,7 +527,7 @@ describe "Pry::Command" do it 'should remove block-related content from arg_string (with no normal args)' do @set.block_command "walking-spanish", "down the hall", :takes_block => true do - PryTestHelpers.inject_var(:@arg_string, arg_string, target) + insert_variable(:@arg_string, arg_string, target) end @t.eval 'walking-spanish | { :jesus }' @@ -538,7 +538,7 @@ describe "Pry::Command" do it 'should NOT remove block-related content from arg_string when :takes_block => false' do block_string = "| { :jesus }" @set.block_command "walking-spanish", "homemade special", :takes_block => false do - PryTestHelpers.inject_var(:@arg_string, arg_string, target) + insert_variable(:@arg_string, arg_string, target) end @t.eval "walking-spanish #{block_string}" @@ -551,8 +551,8 @@ describe "Pry::Command" do describe "block_command" do it "should remove block-related content from arguments" do @set.block_command "walking-spanish", "glass is full of sand", :takes_block => true do |x, y| - PryTestHelpers.inject_var(:@x, x, target) - PryTestHelpers.inject_var(:@y, y, target) + insert_variable(:@x, x, target) + insert_variable(:@y, y, target) end @t.eval 'walking-spanish | { :jesus }' @@ -563,8 +563,8 @@ describe "Pry::Command" do it "should NOT remove block-related content from arguments if :takes_block => false" do @set.block_command "walking-spanish", "litella screeching for a blind pig", :takes_block => false do |x, y| - PryTestHelpers.inject_var(:@x, x, target) - PryTestHelpers.inject_var(:@y, y, target) + insert_variable(:@x, x, target) + insert_variable(:@y, y, target) end @t.eval 'walking-spanish | { :jesus }' @@ -578,8 +578,8 @@ describe "Pry::Command" do it "should remove block-related content from arguments" do @set.create_command "walking-spanish", "punk sanders carved one out of wood", :takes_block => true do def process(x, y) - PryTestHelpers.inject_var(:@x, x, target) - PryTestHelpers.inject_var(:@y, y, target) + insert_variable(:@x, x, target) + insert_variable(:@y, y, target) end end @@ -592,8 +592,8 @@ describe "Pry::Command" do it "should NOT remove block-related content from arguments if :takes_block => false" do @set.create_command "walking-spanish", "down the hall", :takes_block => false do def process(x, y) - PryTestHelpers.inject_var(:@x, x, target) - PryTestHelpers.inject_var(:@y, y, target) + insert_variable(:@x, x, target) + insert_variable(:@y, y, target) end end @@ -610,7 +610,7 @@ describe "Pry::Command" do describe "{} style blocks" do it 'should accept multiple parameters' do @set.block_command "walking-spanish", "down the hall", :takes_block => true do - PryTestHelpers.inject_var(:@x, command_block.call(1, 2), target) + insert_variable(:@x, command_block.call(1, 2), target) end @t.eval 'walking-spanish | { |x, y| [x, y] }' @@ -623,7 +623,7 @@ describe "Pry::Command" do it 'should accept multiple parameters' do @set.create_command "walking-spanish", "litella", :takes_block => true do def process - PryTestHelpers.inject_var(:@x, command_block.call(1, 2), target) + insert_variable(:@x, command_block.call(1, 2), target) end end @@ -649,7 +649,7 @@ describe "Pry::Command" do describe "block_command" do it "should expose block in command_block method" do @set.block_command "walking-spanish", "glass full of sand", :takes_block => true do - PryTestHelpers.inject_var(:@x, command_block.call, target) + insert_variable(:@x, command_block.call, target) end @t.eval 'walking-spanish | { :jesus }' @@ -673,7 +673,7 @@ describe "Pry::Command" do it "should expose block in command_block method" do @set.create_command "walking-spanish", "homemade special", :takes_block => true do def process - PryTestHelpers.inject_var(:@x, command_block.call, target) + insert_variable(:@x, command_block.call, target) end end diff --git a/spec/commands/show_doc_spec.rb b/spec/commands/show_doc_spec.rb index 7b5c6aec..0c9f0be2 100644 --- a/spec/commands/show_doc_spec.rb +++ b/spec/commands/show_doc_spec.rb @@ -263,7 +263,7 @@ describe "show-doc" do end it 'should lookup module name with respect to current context' do - constant_scope(:AlphaClass, :BetaClass) do + temporary_constants(:AlphaClass, :BetaClass) do # top-level beta class BetaClass def alpha @@ -283,7 +283,7 @@ describe "show-doc" do end it 'should look up nested modules' do - constant_scope(:AlphaClass) do + temporary_constants(:AlphaClass) do class AlphaClass # nested beta class BetaClass diff --git a/spec/commands/show_source_spec.rb b/spec/commands/show_source_spec.rb index 6e18ceb9..f0f19ec9 100644 --- a/spec/commands/show_source_spec.rb +++ b/spec/commands/show_source_spec.rb @@ -429,7 +429,7 @@ describe "show-source" do it 'should lookup module name with respect to current context' do - constant_scope(:AlphaClass, :BetaClass) do + temporary_constants(:AlphaClass, :BetaClass) do class BetaClass def alpha end @@ -447,7 +447,7 @@ describe "show-source" do end it 'should lookup nested modules' do - constant_scope(:AlphaClass) do + temporary_constants(:AlphaClass) do class AlphaClass class BetaClass def beta diff --git a/spec/helper.rb b/spec/helper.rb index 2e39250f..97f5dfd1 100644 --- a/spec/helper.rb +++ b/spec/helper.rb @@ -1,5 +1,5 @@ require 'bundler/setup' -require 'pry/test/helper' +require 'pry/testable' Bundler.require :default, :test require_relative 'spec_helpers/mock_pry' require_relative 'spec_helpers/repl_tester' @@ -26,12 +26,22 @@ if ENV["SET_TRACE_FUNC"] } end -puts "Ruby v#{RUBY_VERSION} (#{defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby"}), Pry v#{Pry::VERSION}, method_source v#{MethodSource::VERSION}, CodeRay v#{CodeRay::VERSION}, Pry::Slop v#{Pry::Slop::VERSION}" - RSpec.configure do |config| config.expect_with :rspec do |c| c.syntax = [:should, :expect] end - config.include PryTestHelpers + config.before(:each) do + Pry::Testable.set_testenv_variables + end + + config.after(:each) do + Pry::Testable.unset_testenv_variables + end + config.include Pry::Testable::Mockable + config.include Pry::Testable::Utility + include Pry::Testable::Evalable + include Pry::Testable::Variables end + +puts "Ruby v#{RUBY_VERSION} (#{defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby"}), Pry v#{Pry::VERSION}, method_source v#{MethodSource::VERSION}, CodeRay v#{CodeRay::VERSION}, Pry::Slop v#{Pry::Slop::VERSION}" diff --git a/spec/pry_output_spec.rb b/spec/pry_output_spec.rb index ad4f47df..9df5beaf 100644 --- a/spec/pry_output_spec.rb +++ b/spec/pry_output_spec.rb @@ -116,7 +116,7 @@ describe Pry do describe "custom non-IO object as $stdout" do it "does not crash pry" do old_stdout = $stdout - pry_eval = PryTester.new(binding) + pry_eval = pry_tester(binding) expect(pry_eval.eval("$stdout = Class.new { def write(*) end }.new", ":ok")).to eq(:ok) $stdout = old_stdout end