# frozen_string_literal: true require 'pathname' require 'tempfile' describe "edit" do before do @old_editor = Pry.config.editor @file = @line = @contents = nil Pry.config.editor = lambda do |file, line| @file = file @line = line @contents = File.read(@file) nil end end after do Pry.config.editor = @old_editor end describe "with FILE" do before do # OS-specific tempdir name. For GNU/Linux it's "tmp", for Windows it's # something "Temp". @tf_dir = Pathname.new(Dir.tmpdir) @tf_path = File.expand_path(File.join(@tf_dir.to_s, 'bar.rb')) FileUtils.touch(@tf_path) end after do FileUtils.rm(@tf_path) if File.exist?(@tf_path) end it "should not allow patching any known kind of file" do ["file.rb", "file.c", "file.py", "file.yml", "file.gemspec", "/tmp/file", "\\\\Temp\\\\file"].each do |file| expect { pry_eval "edit -p #{file}" } .to raise_error(NotImplementedError, /Cannot yet patch false objects!/) end end it "should invoke Pry.config.editor with absolutified filenames" do pry_eval 'edit lib/pry.rb' expect(@file).to eq File.expand_path('lib/pry.rb') pry_eval "edit #{@tf_path}" expect(@file).to eq @tf_path end it "should guess the line number from a colon" do pry_eval 'edit lib/pry.rb:10' expect(@line).to eq 10 end it "should use the line number from -l" do pry_eval 'edit -l 10 lib/pry.rb' expect(@line).to eq 10 end it "should not delete the file!" do pry_eval 'edit Rakefile' expect(File.exist?(@file)).to eq true end it "works with files that contain blanks in their names" do tf_path = File.join(File.dirname(@tf_path), 'swoop and doop.rb') FileUtils.touch(tf_path) pry_eval "edit #{tf_path}" expect(@file).to eq tf_path FileUtils.rm(tf_path) end if respond_to?(:require_relative, true) it "should work with require relative" do Pry.config.editor = lambda { |file, _line| File.open(file, 'w') { |f| f << 'require_relative "baz.rb"' } File.open(file.gsub('bar.rb', 'baz.rb'), 'w') do |f| f << "Pad.required = true; FileUtils.rm(__FILE__)" end nil } pry_eval "edit #{@tf_path}" expect(Pad.required).to eq true end end describe do before do Pad.counter = 0 Pry.config.editor = lambda { |file, _line| File.open(file, 'w') { |f| f << "Pad.counter = Pad.counter + 1" } nil } end it "should reload the file if it is a ruby file" do temp_file do |tf| counter = Pad.counter path = tf.path pry_eval "edit #{path}" expect(Pad.counter).to eq counter + 1 end end it "should not reload the file if it is not a ruby file" do temp_file('.py') do |tf| counter = Pad.counter path = tf.path pry_eval "edit #{path}" expect(Pad.counter).to eq counter end end it "should not reload a ruby file if -n is given" do temp_file do |tf| counter = Pad.counter path = tf.path pry_eval "edit -n #{path}" expect(Pad.counter).to eq counter end end it "should reload a non-ruby file if -r is given" do temp_file('.pryrc') do |tf| counter = Pad.counter path = tf.path pry_eval "edit -r #{path}" expect(Pad.counter).to eq counter + 1 end end end describe do before do @reloading = nil Pry.config.editor = lambda do |file, line, reloading| @file = file @line = line @reloading = reloading nil end end it "should pass the editor a reloading arg" do pry_eval 'edit lib/pry.rb' expect(@reloading).to eq true pry_eval 'edit -n lib/pry.rb' expect(@reloading).to eq false end end end describe "with --ex" do before do @t = pry_tester do def last_exception=(exception) @pry.last_exception = exception end def last_exception @pry.last_exception end end end describe "with a real file" do before do @tf = Tempfile.new(["pry", ".rb"]) @path = @tf.path @tf << "_foo = 1\n_bar = 2\nraise RuntimeError" @tf.flush begin load @path rescue RuntimeError => e @t.last_exception = e end end after do @tf.close(true) end it "should reload the file" do @t.pry.config.editor = lambda { |file, _line| File.open(file, 'w') { |f| f << "FOO = 'BAR'" } nil } expect(defined?(FOO)).to equal nil @t.eval 'edit --ex' expect(FOO).to eq 'BAR' end # regression test (this used to edit the current method instead # of the exception) it 'edits the exception even when in a patched method context' do source_location = nil Pry.config.editor = lambda { |file, line| source_location = [file, line] nil } Pad.le = @t.last_exception redirect_pry_io(InputTester.new("def broken_method", "binding.pry", "end", "broken_method", "pry_instance.last_exception = Pad.le", "edit --ex -n", "exit-all", "exit-all")) do Object.new.pry end expect(source_location).to contain_exactly(%r{(/private)?#{@path}}, 3) Pad.clear end it "should not reload the file if -n is passed" do Pry.config.editor = lambda { |file, _line| File.open(file, 'w') { |f| f << "FOO2 = 'BAZ'" } nil } expect(defined?(FOO2)).to equal nil @t.eval 'edit -n --ex' expect(defined?(FOO2)).to equal nil end describe "with --patch" do # Original source code must be untouched. it "should apply changes only in memory (monkey patching)" do @t.pry.config.editor = lambda { |file, _line| File.open(file, 'w') { |f| f << "FOO3 = 'PIYO'" } @patched_def = File.open(file, 'r').read nil } expect(defined?(FOO3)).to equal nil @t.eval 'edit --ex --patch' expect(FOO3).to eq 'PIYO' @tf.rewind expect(@tf.read).to eq "_foo = 1\n_bar = 2\nraise RuntimeError" expect(@patched_def).to eq "FOO3 = 'PIYO'" end end end describe "with --ex NUM" do before do @t.pry.config.editor = proc do |file, line| @__ex_file__ = file @__ex_line__ = line nil end @t.last_exception = mock_exception('a:1', 'b:2', 'c:3') end it 'should start on first level of backtrace with just --ex' do @t.eval 'edit -n --ex' expect(@__ex_file__).to eq "a" expect(@__ex_line__).to eq 1 end it 'should start editor on first level of backtrace with --ex 0' do @t.eval 'edit -n --ex 0' expect(@__ex_file__).to eq "a" expect(@__ex_line__).to eq 1 end it 'should start editor on second level of backtrace with --ex 1' do @t.eval 'edit -n --ex 1' expect(@__ex_file__).to eq "b" expect(@__ex_line__).to eq 2 end it 'should start editor on third level of backtrace with --ex 2' do @t.eval 'edit -n --ex 2' expect(@__ex_file__).to eq "c" expect(@__ex_line__).to eq 3 end it 'should display error message when backtrace level is invalid' do expect { @t.eval 'edit -n --ex 4' }.to raise_error Pry::CommandError end end end describe "without FILE" do before do @t = pry_tester end it "should edit the current expression if it's incomplete" do @t.push 'def a' @t.process_command 'edit' expect(@contents).to eq "def a\n" end it "should edit the previous expression if the current is empty" do @t.eval 'undef a if self.singleton_class.method_defined? :a' @t.eval 'def a; 2; end', 'edit' expect(@contents).to eq "def a; 2; end\n" end it "should use a blank file if -t is specified" do @t.eval 'undef a if self.singleton_class.method_defined? :a' @t.eval 'def a; 5; end', 'edit -t' expect(@contents).to eq "\n" end it "should use a blank file if -t given, even during an expression" do @t.push 'def a;' @t.process_command 'edit -t' expect(@contents).to eq "\n" end it "should position the cursor at the end of the expression" do @t.eval 'undef a if self.singleton_class.method_defined? :a' @t.eval "def a; 2;\nend" @t.process_command 'edit' expect(@line).to eq 2 end it "should evaluate the expression" do @t.pry.config.editor = lambda { |file, _line| File.open(file, 'w') { |f| f << "'FOO'\n" } nil } @t.process_command 'edit' expect(@t.eval_string).to eq "'FOO'\n" end it "should ignore -n for tempfiles" do @t.pry.config.editor = lambda { |file, _line| File.open(file, 'w') { |f| f << "'FOO'\n" } nil } @t.process_command "edit -n" expect(@t.eval_string).to eq "'FOO'\n" end it "should not evaluate a file with -n" do @t.pry.config.editor = lambda { |file, _line| File.open(file, 'w') { |f| f << "'FOO'\n" } nil } begin @t.process_command 'edit -n spec/fixtures/foo.rb' expect(File.read("spec/fixtures/foo.rb")).to eq "'FOO'\n" expect(@t.eval_string).to eq '' ensure FileUtils.rm "spec/fixtures/foo.rb" end end it "should write the evaluated command to history" do quote = 'history repeats itself, first as tradegy...' @t.pry.config.editor = lambda { |file, _line| File.open(file, 'w') do |f| f << quote end nil } @t.process_command 'edit' expect(Pry.history.to_a.last).to eq quote end end describe "with --in" do it "should edit the nth line of _in_" do pry_eval '10', '11', 'edit --in -2' expect(@contents).to eq "10\n" end it "should edit the last line if no argument is given" do pry_eval '10', '11', 'edit --in' expect(@contents).to eq "11\n" end it "should edit a range of lines if a range is given" do pry_eval "10", "11", "edit -i 1,2" expect(@contents).to eq "10\n11\n" end it "should edit a multi-line expression as it occupies one line of _in_" do pry_eval "class #{1.class}\n def invert; -self; end\nend", "edit -i 1" expect(@contents).to eq "class #{1.class}\n def invert; -self; end\nend\n" end it "should not work with a filename" do expect { pry_eval 'edit ruby.rb -i' }.to raise_error( Pry::CommandError, /Only one of --ex, --temp, --in, --method and FILE/ ) end it "should not work with nonsense" do expect { pry_eval 'edit --in three' }.to raise_error( Pry::CommandError, /Not a valid range: three/ ) end end describe 'when editing a method by name' do def use_editor(tester, options) tester.pry.config.editor = lambda do |filename, _line| File.open(filename, 'w') { |f| f.write options.fetch(:replace_all) } nil end tester end # rubocop:disable Style/SingleLineMethods it 'uses patch editing on methods that were previously patched' do # initial definition tester = pry_tester binding filename = __FILE__ line = __LINE__ + 2 klass = Class.new do def m; 1; end end expect(klass.new.m).to eq 1 # now patch it use_editor(tester, replace_all: 'def m; 2; end').eval('edit --patch klass#m') expect(klass.new.m).to eq 2 # edit by name, no --patch use_editor(tester, replace_all: 'def m; 3; end').eval("edit klass#m") expect(klass.new.m).to eq 3 # original file is unchanged expect(File.readlines(filename)[line - 1].strip).to eq 'def m; 1; end' end # rubocop:enable Style/SingleLineMethods it 'can repeatedly edit methods that were defined in the console' do # initial definition tester = pry_tester binding tester.eval("klass = Class.new do\n"\ " def m; 1; end\n"\ "end") expect(tester.eval("klass.new.m")).to eq 1 # first edit use_editor(tester, replace_all: 'def m; 2; end').eval('edit klass#m') expect(tester.eval('klass.new.m')).to eq 2 # repeat edit use_editor(tester, replace_all: 'def m; 3; end').eval('edit klass#m') expect(tester.eval('klass.new.m')).to eq 3 end end describe "old edit-method tests now migrated to edit" do describe "on a method defined in a file" do before do Object.remove_const :X if defined? ::X Object.remove_const :A if defined? ::A @tempfile = Tempfile.new(['pry', '.rb']) @tempfile.puts(<<-CLASSES) module A def a :yup end def b :kinda end end class X include A def self.x :double_yup end def x :nope end def b super end alias c b def y? :because end class B G = :nawt def foo _foo = :possibly G end end end CLASSES @tempfile.flush load @tempfile.path @tempfile_path = @tempfile.path end after do @tempfile.close(true) end describe 'without -p' do before do @file = @line = @contents = nil Pry.config.editor = lambda do |file, line| @file = file @line = line nil end end # Workaround for https://github.com/jruby/jruby/issues/5436. let(:expected_file) { %r{(/private)?#{@tempfile_path}} } it "should correctly find a class method" do pry_eval 'edit X.x' expect(@file).to match(expected_file) expect(@line).to eq 14 end it "should correctly find an instance method" do pry_eval 'edit X#x' expect(@file).to match(expected_file) expect(@line).to eq 18 end it "should correctly find a method on an instance" do pry_eval 'x = X.new', 'edit x.x' expect(@file).to match(expected_file) expect(@line).to eq 18 end it "should correctly find a method from a module" do pry_eval 'edit X#a' expect(@file).to match(expected_file) expect(@line).to eq 2 end it "should correctly find an aliased method" do pry_eval 'edit X#c' expect(@file).to match(expected_file) expect(@line).to eq 22 end end describe 'with -p' do before do Pry.config.editor = lambda do |file, _line| lines = File.read(file).lines.to_a lines[1] = if lines[2] =~ /end/ ":maybe\n" else "_foo = :maybe\n" end File.open(file, 'w') do |f| f.write(lines.join) end @patched_def = String(lines[1]).chomp nil end end it "should successfully replace a class method" do pry_eval 'edit -p X.x' expect(X.method(:x).owner).to eq class << X; self end expect(X.method(:x).receiver).to eq X expect(X.x).to eq :maybe end it "should successfully replace an instance method" do pry_eval 'edit -p X#x' expect(X.instance_method(:x).owner).to eq X expect(X.new.x).to eq :maybe end it "should successfully replace a method on an instance" do pry_eval 'instance = X.new', 'edit -p instance.x' instance = X.new expect(instance.method(:x).owner).to eq X expect(instance.x).to eq :maybe end it "should successfully replace a method from a module" do pry_eval 'edit -p X#a' expect(X.instance_method(:a).owner).to eq A expect(X.new.a).to eq :maybe end it "should successfully replace a method with a question mark" do pry_eval 'edit -p X#y?' expect(X.instance_method(:y?).owner).to eq X expect(X.new.y?).to eq :maybe end it "should preserve module nesting" do pry_eval 'edit -p X::B#foo' expect(X::B.instance_method(:foo).owner).to eq X::B expect(X::B.new.foo).to eq :nawt end describe "monkey-patching" do before do @edit = 'edit --patch ' # A shortcut. end # @param [Integer] lineno # @return [String] the stripped line from the tempfile at +lineno+ def stripped_line_at(lineno) @tempfile.rewind @tempfile.each_line.to_a[lineno].strip end # Applies the monkey patch for +method+ with help of evaluation of # +eval_strs+. The idea is to capture the initial line number (before # the monkey patch), because it gets overwritten by the line number from # the monkey patch. And our goal is to check that the original # definition hasn't changed. # @param [UnboundMethod] method # @param [Array] eval_strs # @return [Array