1
0
Fork 0
mirror of https://github.com/pry/pry.git synced 2022-11-09 12:35:05 -05:00
pry--pry/spec/commands/edit_spec.rb
Kyrylo Silin edaa1383f2 Require files from pry.rb; deps from each file that uses them
This change brings some order to how we require files. Previously, we required
app files from everywhere, including pry.rb. Now we require app files only from
pry.rb.

External and stdlib dependencies are required at places where they're used, not
globally.
2019-03-10 13:20:03 +02:00

843 lines
23 KiB
Ruby

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 =
if Pry::Helpers::Platform.mri_19?
Pathname.new(Dir::Tmpname.tmpdir)
else
Pathname.new(Dir.tmpdir)
end
@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
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_.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
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
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
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
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
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...'
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 <<-EOS
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
EOS
@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'
class << X
X.method(:x).owner.should == 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<String>] eval_strs
# @return [Array<String] the lines with definitions of the same line
# before monkey patching and after (normally, they should be equal)
def apply_monkey_patch(method, *eval_strs)
_, lineno = method.source_location
definition_before = stripped_line_at(lineno)
pry_eval(*eval_strs)
definition_after = stripped_line_at(lineno)
[definition_before, definition_after]
end
it "should work for a class method" do
def_before, def_after =
apply_monkey_patch(X.method(:x), "#{@edit} X.x")
expect(def_before).to eq ':double_yup'
expect(def_after).to eq ':double_yup'
expect(@patched_def).to eq ':maybe'
end
it "should work for an instance method" do
def_before, def_after =
apply_monkey_patch(X.instance_method(:x), "#{@edit} X#x")
expect(def_before).to eq ':nope'
expect(def_after).to eq ':nope'
expect(@patched_def).to eq ':maybe'
end
it "should work for a method on an instance" do
def_before, def_after = apply_monkey_patch(
X.instance_method(:x), 'instance = X.new', "#{@edit} instance.x"
)
expect(def_before).to eq ':nope'
expect(def_after).to eq ':nope'
expect(@patched_def).to eq ':maybe'
end
it "should work for a method from a module" do
def_before, def_after =
apply_monkey_patch(X.instance_method(:a), "#{@edit} X#a")
expect(def_before).to eq ':yup'
expect(def_after).to eq ':yup'
expect(@patched_def).to eq ':maybe'
end
it "should work for a method with a question mark" do
def_before, def_after =
apply_monkey_patch(X.instance_method(:y?), "#{@edit} X#y?")
expect(def_before).to eq ':because'
expect(def_after).to eq ':because'
expect(@patched_def).to eq ':maybe'
end
it "should work with nesting" do
def_before, def_after =
apply_monkey_patch(X::B.instance_method(:foo), "#{@edit} X::B#foo")
expect(def_before).to eq '_foo = :possibly'
expect(def_after).to eq '_foo = :possibly'
expect(@patched_def).to eq '_foo = :maybe'
end
end
end
describe 'on an aliased method' do
before do
Pry.config.editor = lambda do |file, _line|
lines = File.read(file).lines.to_a
# rubocop:disable Lint/InterpolationCheck
lines[1] = '"#{super}aa".to_sym' + "\n"
# rubocop:enable Lint/InterpolationCheck
File.open(file, 'w') do |f|
f.write(lines.join)
end
nil
end
end
it "should change the alias, but not the original, without breaking super" do
$x = :bebe
pry_eval 'edit -p X#c'
expect(Pry::Method.from_str("X#c").alias?).to eq true
expect(X.new.b).to eq :kinda
expect(X.new.c).to eq :kindaaa
$x = nil
end
end
describe 'with three-arg editor' do
before do
@file = @line = @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 X.x'
expect(@reloading).to eq true
pry_eval 'edit -n X.x'
expect(@reloading).to eq false
end
end
end
end
describe "--method flag" do
before do
@t = pry_tester
class BinkyWink
eval %(
def m1
binding
end
)
def m2
_foo = :jeremy_jones
binding
end
end
end
after do
Object.remove_const(:BinkyWink)
end
it 'should edit method context' do
Pry.config.editor = lambda do |file, line|
expect([file, line]).to eq BinkyWink.instance_method(:m2).source_location
nil
end
t = pry_tester(BinkyWink.new.m2)
t.process_command "edit -m -n"
end
it 'errors when cannot find method context' do
Pry.config.editor = lambda do |file, line|
expect([file, line]).to eq BinkyWink.instance_method(:m1).source_location
nil
end
t = pry_tester(BinkyWink.new.m1)
expect { t.process_command "edit -m -n" }
.to raise_error(Pry::CommandError, /Cannot find a file for/)
end
it 'errors when a filename arg is passed with --method' do
expect { @t.process_command "edit -m Pry#repl" }
.to raise_error(Pry::CommandError, /Only one of/)
end
end
describe "pretty error messages" do
before do
@t = pry_tester
class TrinkyDink
eval %(
def m
end
)
end
end
after do
Object.remove_const(:TrinkyDink)
end
it 'should display a nice error message when cannot open a file' do
expect { @t.process_command "edit TrinkyDink#m" }
.to raise_error(Pry::CommandError, /Cannot find a file for/)
end
end
end