appmap 0.32.0 → 0.34.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/.rbenv-gemsets +1 -0
- data/CHANGELOG.md +28 -0
- data/README.md +8 -2
- data/Rakefile +10 -3
- data/appmap.gemspec +6 -0
- data/ext/appmap/appmap.c +36 -0
- data/ext/appmap/extconf.rb +6 -0
- data/lib/appmap.rb +22 -10
- data/lib/appmap/class_map.rb +11 -6
- data/lib/appmap/config.rb +51 -25
- data/lib/appmap/cucumber.rb +19 -2
- data/lib/appmap/hook.rb +41 -16
- data/lib/appmap/hook/method.rb +32 -7
- data/lib/appmap/open.rb +57 -0
- data/lib/appmap/rails/sql_handler.rb +5 -10
- data/lib/appmap/rspec.rb +1 -1
- data/lib/appmap/util.rb +18 -1
- data/lib/appmap/version.rb +1 -1
- data/spec/fixtures/hook/instance_method.rb +4 -0
- data/spec/fixtures/hook/singleton_method.rb +21 -12
- data/spec/hook_spec.rb +141 -13
- data/spec/open_spec.rb +19 -0
- data/test/cli_test.rb +10 -0
- data/test/fixtures/openssl_recorder/Gemfile +3 -0
- data/test/fixtures/openssl_recorder/appmap.yml +3 -0
- data/test/fixtures/openssl_recorder/lib/openssl_cert_sign.rb +94 -0
- data/test/fixtures/openssl_recorder/lib/openssl_encrypt.rb +34 -0
- data/test/fixtures/openssl_recorder/lib/openssl_key_sign.rb +28 -0
- data/test/openssl_test.rb +203 -0
- metadata +71 -3
    
        data/lib/appmap/hook.rb
    CHANGED
    
    | @@ -7,22 +7,17 @@ module AppMap | |
| 7 7 | 
             
                LOG = (ENV['DEBUG'] == 'true')
         | 
| 8 8 |  | 
| 9 9 | 
             
                class << self
         | 
| 10 | 
            +
                  def lock_builtins
         | 
| 11 | 
            +
                    return if @builtins_hooked
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    @builtins_hooked = true
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 10 16 | 
             
                  # Return the class, separator ('.' or '#'), and method name for
         | 
| 11 17 | 
             
                  # the given method.
         | 
| 12 18 | 
             
                  def qualify_method_name(method)
         | 
| 13 19 | 
             
                    if method.owner.singleton_class?
         | 
| 14 | 
            -
                       | 
| 15 | 
            -
                      # #<Class:Foo> or
         | 
| 16 | 
            -
                      # #<Class:#<Bar:0x0123ABC>>. Retrieve the name of
         | 
| 17 | 
            -
                      # the class from the string.
         | 
| 18 | 
            -
                      #
         | 
| 19 | 
            -
                      # (There really isn't a better way to do this. The
         | 
| 20 | 
            -
                      # singleton's reference to the class it was created
         | 
| 21 | 
            -
                      # from is stored in an instance variable named
         | 
| 22 | 
            -
                      # '__attached__'. It doesn't have the '@' prefix, so
         | 
| 23 | 
            -
                      # it's internal only, and not accessible from user
         | 
| 24 | 
            -
                      # code.)
         | 
| 25 | 
            -
                      class_name = /#<Class:((#<(?<cls>.*?):)|((?<cls>.*?)>))/.match(method.owner.to_s)['cls']
         | 
| 20 | 
            +
                      class_name = singleton_method_owner_name(method)
         | 
| 26 21 | 
             
                      [ class_name, '.', method.name ]
         | 
| 27 22 | 
             
                    else
         | 
| 28 23 | 
             
                      [ method.owner.name, '#', method.name ]
         | 
| @@ -39,6 +34,8 @@ module AppMap | |
| 39 34 | 
             
                def enable &block
         | 
| 40 35 | 
             
                  require 'appmap/hook/method'
         | 
| 41 36 |  | 
| 37 | 
            +
                  hook_builtins
         | 
| 38 | 
            +
             | 
| 42 39 | 
             
                  tp = TracePoint.new(:end) do |trace_point|
         | 
| 43 40 | 
             
                    cls = trace_point.self
         | 
| 44 41 |  | 
| @@ -47,12 +44,10 @@ module AppMap | |
| 47 44 |  | 
| 48 45 | 
             
                    hook = lambda do |hook_cls|
         | 
| 49 46 | 
             
                      lambda do |method_id|
         | 
| 50 | 
            -
                        next if method_id.to_s =~ /_hooked_by_appmap$/
         | 
| 51 | 
            -
             | 
| 52 47 | 
             
                        method = hook_cls.public_instance_method(method_id)
         | 
| 53 48 | 
             
                        hook_method = Hook::Method.new(hook_cls, method)
         | 
| 54 49 |  | 
| 55 | 
            -
                        warn "AppMap: Examining #{ | 
| 50 | 
            +
                        warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
         | 
| 56 51 |  | 
| 57 52 | 
             
                        disasm = RubyVM::InstructionSequence.disasm(method)
         | 
| 58 53 | 
             
                        # Skip methods that have no instruction sequence, as they are obviously trivial.
         | 
| @@ -63,7 +58,7 @@ module AppMap | |
| 63 58 | 
             
                        next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
         | 
| 64 59 |  | 
| 65 60 | 
             
                        next unless \
         | 
| 66 | 
            -
                          config.always_hook?( | 
| 61 | 
            +
                          config.always_hook?(hook_cls, method.name) ||
         | 
| 67 62 | 
             
                          config.included_by_location?(method)
         | 
| 68 63 |  | 
| 69 64 | 
             
                        hook_method.activate
         | 
| @@ -76,5 +71,35 @@ module AppMap | |
| 76 71 |  | 
| 77 72 | 
             
                  tp.enable(&block)
         | 
| 78 73 | 
             
                end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                def hook_builtins
         | 
| 76 | 
            +
                  return unless self.class.lock_builtins
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  class_from_string = lambda do |fq_class|
         | 
| 79 | 
            +
                    fq_class.split('::').inject(Object) do |mod, class_name|
         | 
| 80 | 
            +
                      mod.const_get(class_name)
         | 
| 81 | 
            +
                    end
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  Config::BUILTIN_METHODS.each do |class_name, hook|
         | 
| 85 | 
            +
                    require hook.package.package_name if hook.package.package_name
         | 
| 86 | 
            +
                    Array(hook.method_names).each do |method_name|
         | 
| 87 | 
            +
                      method_name = method_name.to_sym
         | 
| 88 | 
            +
                      cls = class_from_string.(class_name)
         | 
| 89 | 
            +
                      method = \
         | 
| 90 | 
            +
                        begin
         | 
| 91 | 
            +
                          cls.instance_method(method_name)
         | 
| 92 | 
            +
                        rescue NameError
         | 
| 93 | 
            +
                          cls.method(method_name) rescue nil
         | 
| 94 | 
            +
                        end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                      if method
         | 
| 97 | 
            +
                        Hook::Method.new(cls, method).activate
         | 
| 98 | 
            +
                      else
         | 
| 99 | 
            +
                        warn "Method #{method_name} not found on #{cls.name}" 
         | 
| 100 | 
            +
                      end
         | 
| 101 | 
            +
                    end
         | 
| 102 | 
            +
                  end
         | 
| 103 | 
            +
                end
         | 
| 79 104 | 
             
              end
         | 
| 80 105 | 
             
            end
         | 
    
        data/lib/appmap/hook/method.rb
    CHANGED
    
    | @@ -1,21 +1,41 @@ | |
| 1 1 | 
             
            module AppMap
         | 
| 2 2 | 
             
              class Hook
         | 
| 3 3 | 
             
                class Method
         | 
| 4 | 
            -
                  attr_reader :hook_class, :hook_method | 
| 4 | 
            +
                  attr_reader :hook_class, :hook_method
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  # +method_display_name+ may be nil if name resolution gets
         | 
| 7 | 
            +
                  # deferred until runtime (e.g. for a singleton method on an
         | 
| 8 | 
            +
                  # embedded Struct).
         | 
| 9 | 
            +
                  attr_reader :method_display_name
         | 
| 5 10 |  | 
| 6 11 | 
             
                  HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
         | 
| 7 12 | 
             
                  private_constant :HOOK_DISABLE_KEY
         | 
| 8 13 |  | 
| 14 | 
            +
                  # Grab the definition of Time.now here, to avoid interfering
         | 
| 15 | 
            +
                  # with the method we're hooking.
         | 
| 16 | 
            +
                  TIME_NOW = Time.method(:now)
         | 
| 17 | 
            +
                  private_constant :TIME_NOW
         | 
| 18 | 
            +
                  
         | 
| 9 19 | 
             
                  def initialize(hook_class, hook_method)
         | 
| 10 20 | 
             
                    @hook_class = hook_class
         | 
| 11 21 | 
             
                    @hook_method = hook_method
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    # Get the class for the method, if it's known.
         | 
| 12 24 | 
             
                    @defined_class, method_symbol = Hook.qualify_method_name(@hook_method)
         | 
| 13 | 
            -
                    @method_display_name = [@defined_class, method_symbol, @hook_method.name].join
         | 
| 25 | 
            +
                    @method_display_name = [@defined_class, method_symbol, @hook_method.name].join if @defined_class
         | 
| 14 26 | 
             
                  end
         | 
| 15 27 |  | 
| 16 28 | 
             
                  def activate
         | 
| 17 | 
            -
                     | 
| 29 | 
            +
                    if Hook::LOG
         | 
| 30 | 
            +
                      msg = if method_display_name
         | 
| 31 | 
            +
                              "#{method_display_name}"
         | 
| 32 | 
            +
                            else
         | 
| 33 | 
            +
                              "#{hook_method.name} (class resolution deferrred)"
         | 
| 34 | 
            +
                            end
         | 
| 35 | 
            +
                      warn "AppMap: Hooking " + msg
         | 
| 36 | 
            +
                    end
         | 
| 18 37 |  | 
| 38 | 
            +
                    defined_class = @defined_class
         | 
| 19 39 | 
             
                    hook_method = self.hook_method
         | 
| 20 40 | 
             
                    before_hook = self.method(:before_hook)
         | 
| 21 41 | 
             
                    after_hook = self.method(:after_hook)
         | 
| @@ -24,12 +44,17 @@ module AppMap | |
| 24 44 | 
             
                    hook_class.define_method hook_method.name do |*args, &block|
         | 
| 25 45 | 
             
                      instance_method = hook_method.bind(self).to_proc
         | 
| 26 46 |  | 
| 47 | 
            +
                      # We may not have gotten the class for the method during
         | 
| 48 | 
            +
                      # initialization (e.g. for a singleton method on an embedded
         | 
| 49 | 
            +
                      # struct), so make sure we have it now.
         | 
| 50 | 
            +
                      defined_class,_ = Hook.qualify_method_name(hook_method) unless defined_class
         | 
| 51 | 
            +
             | 
| 27 52 | 
             
                      hook_disabled = Thread.current[HOOK_DISABLE_KEY]
         | 
| 28 53 | 
             
                      enabled = true if !hook_disabled && AppMap.tracing.enabled?
         | 
| 29 54 | 
             
                      return instance_method.call(*args, &block) unless enabled
         | 
| 30 55 |  | 
| 31 56 | 
             
                      call_event, start_time = with_disabled_hook.() do
         | 
| 32 | 
            -
                        before_hook.(self, args)
         | 
| 57 | 
            +
                        before_hook.(self, defined_class, args)
         | 
| 33 58 | 
             
                      end
         | 
| 34 59 | 
             
                      return_value = nil
         | 
| 35 60 | 
             
                      exception = nil
         | 
| @@ -48,16 +73,16 @@ module AppMap | |
| 48 73 |  | 
| 49 74 | 
             
                  protected
         | 
| 50 75 |  | 
| 51 | 
            -
                  def before_hook(receiver, args)
         | 
| 76 | 
            +
                  def before_hook(receiver, defined_class, args)
         | 
| 52 77 | 
             
                    require 'appmap/event'
         | 
| 53 78 | 
             
                    call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
         | 
| 54 79 | 
             
                    AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
         | 
| 55 | 
            -
                    [ call_event,  | 
| 80 | 
            +
                    [ call_event, TIME_NOW.call ]
         | 
| 56 81 | 
             
                  end
         | 
| 57 82 |  | 
| 58 83 | 
             
                  def after_hook(call_event, start_time, return_value, exception)
         | 
| 59 84 | 
             
                    require 'appmap/event'
         | 
| 60 | 
            -
                    elapsed =  | 
| 85 | 
            +
                    elapsed = TIME_NOW.call - start_time
         | 
| 61 86 | 
             
                    return_event = \
         | 
| 62 87 | 
             
                      AppMap::Event::MethodReturn.build_from_invocation call_event.id, elapsed, return_value, exception
         | 
| 63 88 | 
             
                    AppMap.tracing.record_event return_event
         | 
    
        data/lib/appmap/open.rb
    ADDED
    
    | @@ -0,0 +1,57 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module AppMap
         | 
| 4 | 
            +
              OpenStruct = Struct.new(:appmap)
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              class Open < OpenStruct
         | 
| 7 | 
            +
                attr_reader :port
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def perform
         | 
| 10 | 
            +
                  server = run_server
         | 
| 11 | 
            +
                  open_browser
         | 
| 12 | 
            +
                  server.kill
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def page
         | 
| 16 | 
            +
                  require 'rack/utils'
         | 
| 17 | 
            +
                  <<~PAGE
         | 
| 18 | 
            +
                  <!DOCTYPE html>
         | 
| 19 | 
            +
                  <html>
         | 
| 20 | 
            +
                  <head>
         | 
| 21 | 
            +
                    <title>…</title>
         | 
| 22 | 
            +
                    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
         | 
| 23 | 
            +
                    <script type="text/javascript">
         | 
| 24 | 
            +
                    function dosubmit() { document.forms[0].submit(); }
         | 
| 25 | 
            +
                    </script>
         | 
| 26 | 
            +
                  </head>
         | 
| 27 | 
            +
                  <body onload="dosubmit();">
         | 
| 28 | 
            +
                    <form action="https://app.land/scenario_uploads" method="POST" accept-charset="utf-8">
         | 
| 29 | 
            +
                    <input type="hidden" name="data" value='#{Rack::Utils.escape_html appmap.to_json}'>
         | 
| 30 | 
            +
                    </form>
         | 
| 31 | 
            +
                  </body>
         | 
| 32 | 
            +
                  </html>
         | 
| 33 | 
            +
                  PAGE
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def run_server
         | 
| 37 | 
            +
                  require 'rack'
         | 
| 38 | 
            +
                  Thread.new do
         | 
| 39 | 
            +
                    Rack::Handler::WEBrick.run(
         | 
| 40 | 
            +
                      lambda do |env|
         | 
| 41 | 
            +
                        return [200, { 'Content-Type' => 'text/html' }, [page]]
         | 
| 42 | 
            +
                      end,
         | 
| 43 | 
            +
                      :Port => 0
         | 
| 44 | 
            +
                    ) do |server|
         | 
| 45 | 
            +
                      @port = server.config[:Port]
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
                  end.tap do
         | 
| 48 | 
            +
                    sleep 1.0
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def open_browser
         | 
| 53 | 
            +
                  system 'open', "http://localhost:#{@port}"
         | 
| 54 | 
            +
                  sleep 5.0
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
              end
         | 
| 57 | 
            +
            end
         | 
| @@ -73,20 +73,15 @@ module AppMap | |
| 73 73 |  | 
| 74 74 | 
             
                    class ActiveRecordExaminer
         | 
| 75 75 | 
             
                      def server_version
         | 
| 76 | 
            -
                         | 
| 77 | 
            -
             | 
| 78 | 
            -
                          ActiveRecord::Base.connection.postgresql_version
         | 
| 79 | 
            -
                        when :sqlite
         | 
| 80 | 
            -
                          ActiveRecord::Base.connection.database_version.to_s
         | 
| 81 | 
            -
                        else
         | 
| 82 | 
            -
                          warn "Unable to determine database version for #{database_type.inspect}"
         | 
| 83 | 
            -
                        end
         | 
| 76 | 
            +
                        ActiveRecord::Base.connection.try(:database_version) ||\
         | 
| 77 | 
            +
                          warn("Unable to determine database version for #{database_type.inspect}")
         | 
| 84 78 | 
             
                      end
         | 
| 85 79 |  | 
| 86 80 | 
             
                      def database_type
         | 
| 87 | 
            -
                         | 
| 81 | 
            +
                        type = ActiveRecord::Base.connection.adapter_name.downcase.to_sym
         | 
| 82 | 
            +
                        type = :postgres if type == :postgresql
         | 
| 88 83 |  | 
| 89 | 
            -
                         | 
| 84 | 
            +
                        type
         | 
| 90 85 | 
             
                      end
         | 
| 91 86 |  | 
| 92 87 | 
             
                      def execute_query(sql)
         | 
    
        data/lib/appmap/rspec.rb
    CHANGED
    
    | @@ -154,7 +154,7 @@ module AppMap | |
| 154 154 | 
             
                    end
         | 
| 155 155 |  | 
| 156 156 | 
             
                    labels = labels.map(&:to_s).map(&:strip).reject(&:blank?).map(&:downcase).uniq
         | 
| 157 | 
            -
                    description.reject!(&:nil?).reject(&:blank?)
         | 
| 157 | 
            +
                    description.reject!(&:nil?).reject!(&:blank?)
         | 
| 158 158 | 
             
                    default_description = description.last
         | 
| 159 159 | 
             
                    description.reverse!
         | 
| 160 160 |  | 
    
        data/lib/appmap/util.rb
    CHANGED
    
    | @@ -36,6 +36,23 @@ module AppMap | |
| 36 36 | 
             
                    [ fname, extension ].join
         | 
| 37 37 | 
             
                  end
         | 
| 38 38 |  | 
| 39 | 
            +
                  # sanitize_paths removes ephemeral values from objects with
         | 
| 40 | 
            +
                  # embedded paths (e.g. an event or a classmap), making events
         | 
| 41 | 
            +
                  # easier to compare across runs.
         | 
| 42 | 
            +
                  def sanitize_paths(h)
         | 
| 43 | 
            +
                    require 'hashie'
         | 
| 44 | 
            +
                    h.extend(Hashie::Extensions::DeepLocate)
         | 
| 45 | 
            +
                    keys = %i(path location)
         | 
| 46 | 
            +
                    entries = h.deep_locate ->(k,v,o) {
         | 
| 47 | 
            +
                      next unless keys.include?(k)
         | 
| 48 | 
            +
                      
         | 
| 49 | 
            +
                      fix = ->(v) {v.gsub(%r{#{Gem.dir}/gems/.*(?=lib)}, '')}
         | 
| 50 | 
            +
                      keys.each {|k| o[k] = fix.(o[k]) if o[k] }
         | 
| 51 | 
            +
                    }
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    h
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
                  
         | 
| 39 56 | 
             
                  # sanitize_event removes ephemeral values from an event, making
         | 
| 40 57 | 
             
                  # events easier to compare across runs.
         | 
| 41 58 | 
             
                  def sanitize_event(event, &block)
         | 
| @@ -49,7 +66,7 @@ module AppMap | |
| 49 66 |  | 
| 50 67 | 
             
                    case event[:event]
         | 
| 51 68 | 
             
                    when :call
         | 
| 52 | 
            -
                      event | 
| 69 | 
            +
                      sanitize_paths(event)
         | 
| 53 70 | 
             
                    end
         | 
| 54 71 |  | 
| 55 72 | 
             
                    event
         | 
    
        data/lib/appmap/version.rb
    CHANGED
    
    
| @@ -15,6 +15,20 @@ class SingletonMethod | |
| 15 15 | 
             
                'defined with self class scope'
         | 
| 16 16 | 
             
              end
         | 
| 17 17 |  | 
| 18 | 
            +
              module AddMethod
         | 
| 19 | 
            +
                def self.included(base)
         | 
| 20 | 
            +
                  base.module_eval do
         | 
| 21 | 
            +
                    define_method "added_method" do
         | 
| 22 | 
            +
                      _added_method
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
                
         | 
| 27 | 
            +
                def _added_method
         | 
| 28 | 
            +
                  'defined by including a module'
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
              
         | 
| 18 32 | 
             
              # When called, do_include calls +include+ to bring in the module
         | 
| 19 33 | 
             
              # AddMethod. AddMethod defines a new instance method, which gets
         | 
| 20 34 | 
             
              # added to the singleton class of SingletonMethod.
         | 
| @@ -32,23 +46,18 @@ class SingletonMethod | |
| 32 46 | 
             
                  end
         | 
| 33 47 | 
             
                end
         | 
| 34 48 | 
             
              end
         | 
| 35 | 
            -
              
         | 
| 36 | 
            -
              def to_s
         | 
| 37 | 
            -
                'Singleton Method fixture'
         | 
| 38 | 
            -
              end
         | 
| 39 | 
            -
            end
         | 
| 40 49 |  | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
                    _added_method
         | 
| 50 | 
            +
              STRUCT_TEST = Struct.new(:attr) do
         | 
| 51 | 
            +
                class << self
         | 
| 52 | 
            +
                  def say_struct_singleton
         | 
| 53 | 
            +
                    'singleton for a struct'
         | 
| 46 54 | 
             
                  end
         | 
| 47 55 | 
             
                end
         | 
| 48 56 | 
             
              end
         | 
| 49 57 |  | 
| 50 | 
            -
              def  | 
| 51 | 
            -
                ' | 
| 58 | 
            +
              def to_s
         | 
| 59 | 
            +
                'Singleton Method fixture'
         | 
| 52 60 | 
             
              end
         | 
| 53 61 | 
             
            end
         | 
| 54 62 |  | 
| 63 | 
            +
             | 
    
        data/spec/hook_spec.rb
    CHANGED
    
    | @@ -22,12 +22,12 @@ describe 'AppMap class Hooking', docker: false do | |
| 22 22 | 
             
                  while tracer.event?
         | 
| 23 23 | 
             
                    events << tracer.next_event.to_h
         | 
| 24 24 | 
             
                  end
         | 
| 25 | 
            -
                end.map(&AppMap::Util.method(:sanitize_event)) | 
| 25 | 
            +
                end.map(&AppMap::Util.method(:sanitize_event))
         | 
| 26 26 | 
             
              end
         | 
| 27 27 |  | 
| 28 28 | 
             
              def invoke_test_file(file, setup: nil, &block)
         | 
| 29 29 | 
             
                AppMap.configuration = nil
         | 
| 30 | 
            -
                package = AppMap::Package.new(file | 
| 30 | 
            +
                package = AppMap::Config::Package.new(file)
         | 
| 31 31 | 
             
                config = AppMap::Config.new('hook_spec', [ package ])
         | 
| 32 32 | 
             
                AppMap.configuration = config
         | 
| 33 33 | 
             
                tracer = nil
         | 
| @@ -50,8 +50,9 @@ describe 'AppMap class Hooking', docker: false do | |
| 50 50 | 
             
              def test_hook_behavior(file, events_yaml, setup: nil, &block)
         | 
| 51 51 | 
             
                config, tracer = invoke_test_file(file, setup: setup, &block)
         | 
| 52 52 |  | 
| 53 | 
            -
                events = collect_events(tracer)
         | 
| 54 | 
            -
             | 
| 53 | 
            +
                events = collect_events(tracer).to_yaml
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                expect(Diffy::Diff.new(events_yaml, events).to_s).to eq('')
         | 
| 55 56 |  | 
| 56 57 | 
             
                [ config, tracer ]
         | 
| 57 58 | 
             
              end
         | 
| @@ -99,7 +100,7 @@ describe 'AppMap class Hooking', docker: false do | |
| 99 100 | 
             
                  InstanceMethod.new.say_default
         | 
| 100 101 | 
             
                end
         | 
| 101 102 | 
             
                class_map = AppMap.class_map(tracer.event_methods).to_yaml
         | 
| 102 | 
            -
                expect(Diffy::Diff.new( | 
| 103 | 
            +
                expect(Diffy::Diff.new(<<~YAML, class_map).to_s).to eq('')
         | 
| 103 104 | 
             
                ---
         | 
| 104 105 | 
             
                - :name: spec/fixtures/hook/instance_method.rb
         | 
| 105 106 | 
             
                  :type: package
         | 
| @@ -341,7 +342,7 @@ describe 'AppMap class Hooking', docker: false do | |
| 341 342 | 
             
                  :defined_class: SingletonMethod
         | 
| 342 343 | 
             
                  :method_id: added_method
         | 
| 343 344 | 
             
                  :path: spec/fixtures/hook/singleton_method.rb
         | 
| 344 | 
            -
                  :lineno:  | 
| 345 | 
            +
                  :lineno: 21
         | 
| 345 346 | 
             
                  :static: false
         | 
| 346 347 | 
             
                  :parameters: []
         | 
| 347 348 | 
             
                  :receiver:
         | 
| @@ -349,10 +350,10 @@ describe 'AppMap class Hooking', docker: false do | |
| 349 350 | 
             
                    :value: Singleton Method fixture
         | 
| 350 351 | 
             
                - :id: 2
         | 
| 351 352 | 
             
                  :event: :call
         | 
| 352 | 
            -
                  :defined_class: AddMethod
         | 
| 353 | 
            +
                  :defined_class: SingletonMethod::AddMethod
         | 
| 353 354 | 
             
                  :method_id: _added_method
         | 
| 354 355 | 
             
                  :path: spec/fixtures/hook/singleton_method.rb
         | 
| 355 | 
            -
                  :lineno:  | 
| 356 | 
            +
                  :lineno: 27
         | 
| 356 357 | 
             
                  :static: false
         | 
| 357 358 | 
             
                  :parameters: []
         | 
| 358 359 | 
             
                  :receiver:
         | 
| @@ -394,10 +395,44 @@ describe 'AppMap class Hooking', docker: false do | |
| 394 395 | 
             
                load 'spec/fixtures/hook/singleton_method.rb'
         | 
| 395 396 | 
             
                setup = -> { SingletonMethod.new_with_instance_method }
         | 
| 396 397 | 
             
                test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml, setup: setup do |s|
         | 
| 398 | 
            +
                  # Make sure we're testing the right thing
         | 
| 399 | 
            +
                  say_instance_defined = s.method(:say_instance_defined)
         | 
| 400 | 
            +
                  expect(say_instance_defined.owner.to_s).to start_with('#<Class:#<SingletonMethod:')
         | 
| 401 | 
            +
             | 
| 402 | 
            +
                  # Verify the native extension works as expected
         | 
| 403 | 
            +
                  expect(AppMap::Hook.singleton_method_owner_name(say_instance_defined)).to eq('SingletonMethod')
         | 
| 404 | 
            +
                  
         | 
| 397 405 | 
             
                  expect(s.say_instance_defined).to eq('defined for an instance')
         | 
| 398 406 | 
             
                end
         | 
| 399 407 | 
             
              end
         | 
| 400 408 |  | 
| 409 | 
            +
              it 'hooks a singleton method on an embedded struct' do
         | 
| 410 | 
            +
                events_yaml = <<~YAML
         | 
| 411 | 
            +
                ---
         | 
| 412 | 
            +
                - :id: 1
         | 
| 413 | 
            +
                  :event: :call
         | 
| 414 | 
            +
                  :defined_class: SingletonMethod::STRUCT_TEST
         | 
| 415 | 
            +
                  :method_id: say_struct_singleton
         | 
| 416 | 
            +
                  :path: spec/fixtures/hook/singleton_method.rb
         | 
| 417 | 
            +
                  :lineno: 52
         | 
| 418 | 
            +
                  :static: true
         | 
| 419 | 
            +
                  :parameters: []
         | 
| 420 | 
            +
                  :receiver:
         | 
| 421 | 
            +
                    :class: Class
         | 
| 422 | 
            +
                    :value: SingletonMethod::STRUCT_TEST
         | 
| 423 | 
            +
                - :id: 2
         | 
| 424 | 
            +
                  :event: :return
         | 
| 425 | 
            +
                  :parent_id: 1
         | 
| 426 | 
            +
                  :return_value:
         | 
| 427 | 
            +
                    :class: String
         | 
| 428 | 
            +
                    :value: singleton for a struct
         | 
| 429 | 
            +
                YAML
         | 
| 430 | 
            +
             | 
| 431 | 
            +
                test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml do
         | 
| 432 | 
            +
                  expect(SingletonMethod::STRUCT_TEST.say_struct_singleton).to eq('singleton for a struct')
         | 
| 433 | 
            +
                end
         | 
| 434 | 
            +
              end
         | 
| 435 | 
            +
              
         | 
| 401 436 | 
             
              it 'Reports exceptions' do
         | 
| 402 437 | 
             
                events_yaml = <<~YAML
         | 
| 403 438 | 
             
                ---
         | 
| @@ -465,7 +500,7 @@ describe 'AppMap class Hooking', docker: false do | |
| 465 500 | 
             
                    :event: :call
         | 
| 466 501 | 
             
                    :defined_class: ActiveSupport::SecurityUtils
         | 
| 467 502 | 
             
                    :method_id: secure_compare
         | 
| 468 | 
            -
                    :path:  | 
| 503 | 
            +
                    :path: lib/active_support/security_utils.rb
         | 
| 469 504 | 
             
                    :lineno: 26
         | 
| 470 505 | 
             
                    :static: true
         | 
| 471 506 | 
             
                    :parameters:
         | 
| @@ -481,12 +516,52 @@ describe 'AppMap class Hooking', docker: false do | |
| 481 516 | 
             
                      :class: Module
         | 
| 482 517 | 
             
                      :value: ActiveSupport::SecurityUtils
         | 
| 483 518 | 
             
                  - :id: 3
         | 
| 519 | 
            +
                    :event: :call
         | 
| 520 | 
            +
                    :defined_class: Digest::Instance
         | 
| 521 | 
            +
                    :method_id: digest
         | 
| 522 | 
            +
                    :path: Digest::Instance#digest
         | 
| 523 | 
            +
                    :static: false
         | 
| 524 | 
            +
                    :parameters:
         | 
| 525 | 
            +
                    - :name: arg
         | 
| 526 | 
            +
                      :class: String
         | 
| 527 | 
            +
                      :value: string
         | 
| 528 | 
            +
                      :kind: :rest
         | 
| 529 | 
            +
                    :receiver:
         | 
| 530 | 
            +
                      :class: Digest::SHA256
         | 
| 531 | 
            +
                      :value: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
         | 
| 532 | 
            +
                  - :id: 4
         | 
| 533 | 
            +
                    :event: :return
         | 
| 534 | 
            +
                    :parent_id: 3
         | 
| 535 | 
            +
                    :return_value:
         | 
| 536 | 
            +
                      :class: String
         | 
| 537 | 
            +
                      :value: "G2__)__qc____X____3_].\\x02y__.___/_"
         | 
| 538 | 
            +
                  - :id: 5
         | 
| 539 | 
            +
                    :event: :call
         | 
| 540 | 
            +
                    :defined_class: Digest::Instance
         | 
| 541 | 
            +
                    :method_id: digest
         | 
| 542 | 
            +
                    :path: Digest::Instance#digest
         | 
| 543 | 
            +
                    :static: false
         | 
| 544 | 
            +
                    :parameters:
         | 
| 545 | 
            +
                    - :name: arg
         | 
| 546 | 
            +
                      :class: String
         | 
| 547 | 
            +
                      :value: string
         | 
| 548 | 
            +
                      :kind: :rest
         | 
| 549 | 
            +
                    :receiver:
         | 
| 550 | 
            +
                      :class: Digest::SHA256
         | 
| 551 | 
            +
                      :value: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
         | 
| 552 | 
            +
                  - :id: 6
         | 
| 553 | 
            +
                    :event: :return
         | 
| 554 | 
            +
                    :parent_id: 5
         | 
| 555 | 
            +
                    :return_value:
         | 
| 556 | 
            +
                      :class: String
         | 
| 557 | 
            +
                      :value: "G2__)__qc____X____3_].\\x02y__.___/_"
         | 
| 558 | 
            +
                  - :id: 7
         | 
| 484 559 | 
             
                    :event: :return
         | 
| 485 560 | 
             
                    :parent_id: 2
         | 
| 486 561 | 
             
                    :return_value:
         | 
| 487 562 | 
             
                      :class: TrueClass
         | 
| 488 563 | 
             
                      :value: 'true'
         | 
| 489 | 
            -
                  - :id:  | 
| 564 | 
            +
                  - :id: 8
         | 
| 490 565 | 
             
                    :event: :return
         | 
| 491 566 | 
             
                    :parent_id: 1
         | 
| 492 567 | 
             
                    :return_value:
         | 
| @@ -523,22 +598,75 @@ describe 'AppMap class Hooking', docker: false do | |
| 523 598 | 
             
                        :children:
         | 
| 524 599 | 
             
                        - :name: secure_compare
         | 
| 525 600 | 
             
                          :type: function
         | 
| 526 | 
            -
                          :location:  | 
| 601 | 
            +
                          :location: lib/active_support/security_utils.rb:26
         | 
| 527 602 | 
             
                          :static: true
         | 
| 528 603 | 
             
                          :labels:
         | 
| 529 604 | 
             
                          - security
         | 
| 605 | 
            +
                          - crypto
         | 
| 606 | 
            +
                  - :name: openssl
         | 
| 607 | 
            +
                    :type: package
         | 
| 608 | 
            +
                    :children:
         | 
| 609 | 
            +
                    - :name: Digest
         | 
| 610 | 
            +
                      :type: class
         | 
| 611 | 
            +
                      :children:
         | 
| 612 | 
            +
                      - :name: Instance
         | 
| 613 | 
            +
                        :type: class
         | 
| 614 | 
            +
                        :children:
         | 
| 615 | 
            +
                        - :name: digest
         | 
| 616 | 
            +
                          :type: function
         | 
| 617 | 
            +
                          :location: Digest::Instance#digest
         | 
| 618 | 
            +
                          :static: false
         | 
| 619 | 
            +
                          :labels:
         | 
| 620 | 
            +
                          - security
         | 
| 621 | 
            +
                          - crypto
         | 
| 530 622 | 
             
                  YAML
         | 
| 531 623 |  | 
| 532 624 | 
             
                  config, tracer = invoke_test_file 'spec/fixtures/hook/compare.rb' do
         | 
| 533 625 | 
             
                    expect(Compare.compare('string', 'string')).to be_truthy
         | 
| 534 626 | 
             
                  end
         | 
| 535 | 
            -
                  cm = AppMap::ClassMap.build_from_methods(config, tracer.event_methods)
         | 
| 627 | 
            +
                  cm = AppMap::Util.sanitize_paths(AppMap::ClassMap.build_from_methods(config, tracer.event_methods))
         | 
| 536 628 | 
             
                  entry = cm[1][:children][0][:children][0][:children][0]
         | 
| 537 629 | 
             
                  # Sanity check, make sure we got the right one
         | 
| 538 630 | 
             
                  expect(entry[:name]).to eq('secure_compare')
         | 
| 539 631 | 
             
                  spec = Gem::Specification.find_by_name('activesupport')
         | 
| 540 632 | 
             
                  entry[:location].gsub!(spec.base_dir + '/', '')
         | 
| 541 | 
            -
                  expect(Diffy::Diff.new(cm.to_yaml | 
| 633 | 
            +
                  expect(Diffy::Diff.new(classmap_yaml, cm.to_yaml).to_s).to eq('')
         | 
| 634 | 
            +
                end
         | 
| 635 | 
            +
              end
         | 
| 636 | 
            +
             | 
| 637 | 
            +
              it "doesn't cause expectations on Time.now to fail" do
         | 
| 638 | 
            +
                events_yaml = <<~YAML
         | 
| 639 | 
            +
                ---
         | 
| 640 | 
            +
                - :id: 1
         | 
| 641 | 
            +
                  :event: :call
         | 
| 642 | 
            +
                  :defined_class: InstanceMethod
         | 
| 643 | 
            +
                  :method_id: say_the_time
         | 
| 644 | 
            +
                  :path: spec/fixtures/hook/instance_method.rb
         | 
| 645 | 
            +
                  :lineno: 24
         | 
| 646 | 
            +
                  :static: false
         | 
| 647 | 
            +
                  :parameters: []
         | 
| 648 | 
            +
                  :receiver:
         | 
| 649 | 
            +
                    :class: InstanceMethod
         | 
| 650 | 
            +
                    :value: Instance Method fixture
         | 
| 651 | 
            +
                - :id: 2
         | 
| 652 | 
            +
                  :event: :return
         | 
| 653 | 
            +
                  :parent_id: 1
         | 
| 654 | 
            +
                  :return_value:
         | 
| 655 | 
            +
                    :class: String
         | 
| 656 | 
            +
                    :value: '2020-01-01 00:00:00 +0000'
         | 
| 657 | 
            +
                YAML
         | 
| 658 | 
            +
                test_hook_behavior 'spec/fixtures/hook/instance_method.rb', events_yaml do
         | 
| 659 | 
            +
                  require 'timecop'
         | 
| 660 | 
            +
                  begin
         | 
| 661 | 
            +
                    tz = ENV['TZ']
         | 
| 662 | 
            +
                    ENV['TZ'] = 'UTC'
         | 
| 663 | 
            +
                    Timecop.freeze(Time.utc('2020-01-01')) do
         | 
| 664 | 
            +
                      expect(Time).to receive(:now).exactly(3).times.and_call_original
         | 
| 665 | 
            +
                      expect(InstanceMethod.new.say_the_time).to be
         | 
| 666 | 
            +
                    end
         | 
| 667 | 
            +
                  ensure
         | 
| 668 | 
            +
                    ENV['TZ'] = tz
         | 
| 669 | 
            +
                  end
         | 
| 542 670 | 
             
                end
         | 
| 543 671 | 
             
              end
         | 
| 544 672 | 
             
            end
         |