# frozen_string_literal: true require 'rails_spec_helper' require 'appmap/hook' require 'appmap/event' require 'diffy' # Show nulls as the literal +null+, rather than just leaving the field # empty. This make some of the expected YAML below easier to # understand. module ShowYamlNulls def visit_NilClass(o) @emitter.scalar('null', nil, 'tag:yaml.org,2002:null', true, false, Psych::Nodes::Scalar::ANY) end end Psych::Visitors::YAMLTree.prepend(ShowYamlNulls) describe 'AppMap class Hooking', docker: false do include_context 'collect events' def invoke_test_file(file, setup: nil, &block) AppMap.configuration = nil package = AppMap::Config::Package.build_from_path(file) config = AppMap::Config.new('hook_spec', packages: [ package ]) AppMap.configuration = config tracer = nil AppMap::Hook.new(config).enable do setup_result = setup.call if setup tracer = AppMap.tracing.trace AppMap::Event.reset_id_counter begin load file yield setup_result ensure AppMap.tracing.delete(tracer) end end [ config, tracer ] end def test_hook_behavior(file, events_yaml, setup: nil, &block) config, tracer = invoke_test_file(file, setup: setup, &block) events = collect_events(tracer).to_yaml expect(Diffy::Diff.new(events_yaml, events).to_s).to eq('') if events_yaml [ config, tracer, events ] end after do AppMap.configuration = nil end it 'excludes named classes and methods' do load 'spec/fixtures/hook/exclude.rb' package = AppMap::Config::Package.build_from_path('spec/fixtures/hook/exclude.rb') config = AppMap::Config.new('hook_spec', packages: [ package ], exclude: %w[ExcludeTest]) AppMap.configuration = config expect(config.never_hook?(ExcludeTest, ExcludeTest.new.method(:instance_method))).to be_truthy expect(config.never_hook?(ExcludeTest, ExcludeTest.method(:cls_method))).to be_truthy end it "an instance method named 'call' will be ignored" do events_yaml = <<~YAML --- [] YAML _, tracer = test_hook_behavior 'spec/fixtures/hook/method_named_call.rb', events_yaml do expect(MethodNamedCall.new.call(1, 2, 3, 4, 5)).to eq('1 2 3 4 5') end end it 'can custom hook and label a function' do events_yaml = <<~YAML --- - :id: 1 :event: :call :defined_class: CustomInstanceMethod :method_id: say_default :path: spec/fixtures/hook/custom_instance_method.rb :lineno: 8 :static: false :parameters: [] :receiver: :class: CustomInstanceMethod :value: CustomInstance Method fixture - :id: 2 :event: :return :parent_id: 1 :return_value: :class: String :value: default YAML config = AppMap::Config.load({ functions: [ { package: 'hook_spec', class: 'CustomInstanceMethod', functions: [ :say_default ], labels: ['cowsay'] } ] }.deep_stringify_keys) load 'spec/fixtures/hook/custom_instance_method.rb' hook_cls = CustomInstanceMethod method = hook_cls.instance_method(:say_default) require 'appmap/hook/method' package = config.lookup_package(hook_cls, method) expect(package).to be hook_method = AppMap::Hook::Method.new(package, hook_cls, method) hook_method.activate tracer = AppMap.tracing.trace AppMap::Event.reset_id_counter begin expect(CustomInstanceMethod.new.say_default).to eq('default') ensure AppMap.tracing.delete(tracer) end events = collect_events(tracer).to_yaml expect(Diffy::Diff.new(events_yaml, events).to_s).to eq('') class_map = AppMap.class_map(tracer.event_methods) expect(Diffy::Diff.new(<<~CLASSMAP, YAML.dump(class_map)).to_s).to eq('') --- - :name: hook_spec :type: package :children: - :name: CustomInstanceMethod :type: class :children: - :name: say_default :type: function :location: spec/fixtures/hook/custom_instance_method.rb:8 :static: false :labels: - cowsay CLASSMAP end it 'parses labels from comments' do _, tracer = invoke_test_file 'spec/fixtures/hook/labels.rb' do ClassWithLabel.new.fn_with_label end class_map = AppMap.class_map(tracer.event_methods).to_yaml expect(Diffy::Diff.new(<<~YAML, class_map).to_s).to eq('') --- - :name: spec/fixtures/hook/labels.rb :type: package :children: - :name: ClassWithLabel :type: class :children: - :name: fn_with_label :type: function :location: spec/fixtures/hook/labels.rb:4 :static: false :labels: - has-fn-label :comment: "# @label has-fn-label\\n" YAML end it 'hooks an instance method that takes no arguments' do events_yaml = <<~YAML --- - :id: 1 :event: :call :defined_class: InstanceMethod :method_id: say_default :path: spec/fixtures/hook/instance_method.rb :lineno: 8 :static: false :parameters: [] :receiver: :class: InstanceMethod :value: Instance Method fixture - :id: 2 :event: :return :parent_id: 1 :return_value: :class: String :value: default YAML test_hook_behavior 'spec/fixtures/hook/instance_method.rb', events_yaml do expect(InstanceMethod.new.say_default).to eq('default') end end it 'collects the methods that are invoked' do _, tracer = invoke_test_file 'spec/fixtures/hook/instance_method.rb' do InstanceMethod.new.say_default end expect(tracer.event_methods.to_a.map(&:class_name)).to eq([ 'InstanceMethod' ]) expect(tracer.event_methods.to_a.map(&:name)).to eq([ InstanceMethod.public_instance_method(:say_default).name ]) end it 'builds a class map of invoked methods' do _, tracer = invoke_test_file 'spec/fixtures/hook/instance_method.rb' do InstanceMethod.new.say_default end class_map = AppMap.class_map(tracer.event_methods).to_yaml expect(Diffy::Diff.new(<<~YAML, class_map).to_s).to eq('') --- - :name: spec/fixtures/hook/instance_method.rb :type: package :children: - :name: InstanceMethod :type: class :children: - :name: say_default :type: function :location: spec/fixtures/hook/instance_method.rb:8 :static: false YAML end it 'does not hook an attr_accessor' do events_yaml = <<~YAML --- [] YAML test_hook_behavior 'spec/fixtures/hook/attr_accessor.rb', events_yaml do obj = AttrAccessor.new obj.value = 'foo' expect(obj.value).to eq('foo') end end it 'does not hook a constructor' do events_yaml = <<~YAML --- [] YAML test_hook_behavior 'spec/fixtures/hook/constructor.rb', events_yaml do Constructor.new('foo') end end it 'hooks an instance method that takes an argument' do events_yaml = <<~YAML --- - :id: 1 :event: :call :defined_class: InstanceMethod :method_id: say_echo :path: spec/fixtures/hook/instance_method.rb :lineno: 12 :static: false :parameters: - :name: :arg :class: String :value: echo :kind: :req :receiver: :class: InstanceMethod :value: Instance Method fixture - :id: 2 :event: :return :parent_id: 1 :return_value: :class: String :value: echo YAML test_hook_behavior 'spec/fixtures/hook/instance_method.rb', events_yaml do expect(InstanceMethod.new.say_echo('echo')).to eq('echo') end end it 'hooks an instance method that takes a keyword argument' do events_yaml = <<~YAML --- - :id: 1 :event: :call :defined_class: InstanceMethod :method_id: say_kw :path: spec/fixtures/hook/instance_method.rb :lineno: 16 :static: false :parameters: - :name: :kw :class: String :value: kw :kind: :key :receiver: :class: InstanceMethod :value: Instance Method fixture - :id: 2 :event: :return :parent_id: 1 :return_value: :class: String :value: kw YAML test_hook_behavior 'spec/fixtures/hook/instance_method.rb', events_yaml do expect(InstanceMethod.new.say_kw(kw: 'kw')).to eq('kw') end end it 'hooks an instance method that takes a default keyword argument' do events_yaml = <<~YAML --- - :id: 1 :event: :call :defined_class: InstanceMethod :method_id: say_kw :path: spec/fixtures/hook/instance_method.rb :lineno: 16 :static: false :parameters: - :name: :kw :class: NilClass :value: null :kind: :key :receiver: :class: InstanceMethod :value: Instance Method fixture - :id: 2 :event: :return :parent_id: 1 :return_value: :class: String :value: kw YAML test_hook_behavior 'spec/fixtures/hook/instance_method.rb', events_yaml do expect(InstanceMethod.new.say_kw).to eq('kw') end end it 'hooks an instance method that takes keyword arguments' do events_yaml = <<~YAML --- - :id: 1 :event: :call :defined_class: InstanceMethod :method_id: say_kws :path: spec/fixtures/hook/instance_method.rb :lineno: 20 :static: false :parameters: - :name: :args :class: Array :value: "[4, 5]" :kind: :rest - :name: :kw1 :class: String :value: one :kind: :keyreq - :name: :kw2 :class: Integer :value: '2' :kind: :key - :name: :kws :class: Hash :value: "{:kw3=>:three}" :kind: :keyrest :receiver: :class: InstanceMethod :value: Instance Method fixture - :id: 2 :event: :return :parent_id: 1 :return_value: :class: String :value: one2{:kw3=>:three}45 YAML test_hook_behavior 'spec/fixtures/hook/instance_method.rb', events_yaml do expect(InstanceMethod.new.say_kws(4, 5, kw1: 'one', kw2: 2, kw3: :three)).to eq('one2{:kw3=>:three}45') end end it 'hooks an instance method that takes a block argument' do events_yaml = <<~YAML --- - :id: 1 :event: :call :defined_class: InstanceMethod :method_id: say_block :path: spec/fixtures/hook/instance_method.rb :lineno: 24 :static: false :parameters: - :name: :block :class: NilClass :value: null :kind: :block :receiver: :class: InstanceMethod :value: Instance Method fixture - :id: 2 :event: :return :parent_id: 1 :return_value: :class: String :value: albert YAML test_hook_behavior 'spec/fixtures/hook/instance_method.rb', events_yaml do expect(InstanceMethod.new.say_block { 'albert' }).to eq('albert') end end it 'hooks a singleton method' do events_yaml = <<~YAML --- - :id: 1 :event: :call :defined_class: SingletonMethod :method_id: say_default :path: spec/fixtures/hook/singleton_method.rb :lineno: 5 :static: true :parameters: [] :receiver: :class: Class :value: SingletonMethod - :id: 2 :event: :return :parent_id: 1 :return_value: :class: String :value: default YAML test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml do expect(SingletonMethod.say_default).to eq('default') end end it 'hooks a class method with explicit class name scope' do events_yaml = <<~YAML --- - :id: 1 :event: :call :defined_class: SingletonMethod :method_id: say_class_defined :path: spec/fixtures/hook/singleton_method.rb :lineno: 10 :static: true :parameters: [] :receiver: :class: Class :value: SingletonMethod - :id: 2 :event: :return :parent_id: 1 :return_value: :class: String :value: defined with explicit class scope YAML test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml do expect(SingletonMethod.say_class_defined).to eq('defined with explicit class scope') end end it "hooks a class method with 'self' as the class name scope" do events_yaml = <<~YAML --- - :id: 1 :event: :call :defined_class: SingletonMethod :method_id: say_self_defined :path: spec/fixtures/hook/singleton_method.rb :lineno: 14 :static: true :parameters: [] :receiver: :class: Class :value: SingletonMethod - :id: 2 :event: :return :parent_id: 1 :return_value: :class: String :value: defined with self class scope YAML test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml do expect(SingletonMethod.say_self_defined).to eq('defined with self class scope') end end it 'hooks an included method' do events_yaml = <<~YAML --- - :id: 1 :event: :call :defined_class: SingletonMethod :method_id: added_method :path: spec/fixtures/hook/singleton_method.rb :lineno: 21 :static: false :parameters: [] :receiver: :class: SingletonMethod :value: Singleton Method fixture - :id: 2 :event: :call :defined_class: SingletonMethod::AddMethod :method_id: _added_method :path: spec/fixtures/hook/singleton_method.rb :lineno: 27 :static: false :parameters: [] :receiver: :class: SingletonMethod :value: Singleton Method fixture - :id: 3 :event: :return :parent_id: 2 :return_value: :class: String :value: defined by including a module - :id: 4 :event: :return :parent_id: 1 :return_value: :class: String :value: defined by including a module YAML load 'spec/fixtures/hook/singleton_method.rb' setup = -> { SingletonMethod.new.do_include } test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml, setup: setup do |s| expect(s.added_method).to eq('defined by including a module') end end it "doesn't hook a singleton method defined for an instance" do # Ideally, Ruby would fire a TracePoint event when a singleton # class gets created by defining a method on an instance. It # currently doesn't, though, so there's no way for us to hook such # a method. # # This example will fail if Ruby's behavior changes at some point # in the future. events_yaml = <<~YAML --- [] YAML load 'spec/fixtures/hook/singleton_method.rb' setup = -> { SingletonMethod.new_with_instance_method } test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml, setup: setup do |s| # Make sure we're testing the right thing say_instance_defined = s.method(:say_instance_defined) expect(say_instance_defined.owner.to_s).to start_with('#