appmap 0.26.0 → 0.31.0
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 -2
- data/CHANGELOG.md +38 -0
- data/README.md +144 -31
- data/Rakefile +1 -1
- data/exe/appmap +3 -1
- data/lib/appmap.rb +55 -35
- data/lib/appmap/algorithm/stats.rb +2 -1
- data/lib/appmap/class_map.rb +16 -24
- data/lib/appmap/command/record.rb +2 -61
- data/lib/appmap/config.rb +91 -0
- data/lib/appmap/cucumber.rb +89 -0
- data/lib/appmap/event.rb +6 -6
- data/lib/appmap/hook.rb +94 -116
- data/lib/appmap/metadata.rb +62 -0
- data/lib/appmap/middleware/remote_recording.rb +2 -6
- data/lib/appmap/minitest.rb +141 -0
- data/lib/appmap/rails/action_handler.rb +2 -2
- data/lib/appmap/rails/sql_handler.rb +2 -2
- data/lib/appmap/railtie.rb +2 -2
- data/lib/appmap/record.rb +27 -0
- data/lib/appmap/rspec.rb +20 -38
- data/lib/appmap/trace.rb +19 -11
- data/lib/appmap/util.rb +40 -0
- data/lib/appmap/version.rb +1 -1
- data/package-lock.json +3 -3
- data/spec/abstract_controller4_base_spec.rb +1 -1
- data/spec/abstract_controller_base_spec.rb +1 -1
- data/spec/config_spec.rb +3 -3
- data/spec/fixtures/hook/compare.rb +7 -0
- data/spec/fixtures/hook/openssl_sign.rb +87 -0
- data/spec/fixtures/hook/singleton_method.rb +54 -0
- data/spec/fixtures/rails_users_app/Gemfile +1 -0
- data/spec/fixtures/rails_users_app/features/api_users.feature +13 -0
- data/spec/fixtures/rails_users_app/features/support/env.rb +4 -0
- data/spec/fixtures/rails_users_app/features/support/hooks.rb +11 -0
- data/spec/fixtures/rails_users_app/features/support/steps.rb +18 -0
- data/spec/hook_spec.rb +243 -36
- data/spec/rails_spec_helper.rb +2 -0
- data/spec/rspec_feature_metadata_spec.rb +2 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/util_spec.rb +21 -0
- data/test/cli_test.rb +2 -2
- data/test/cucumber_test.rb +72 -0
- data/test/fixtures/cucumber4_recorder/Gemfile +5 -0
- data/test/fixtures/cucumber4_recorder/appmap.yml +3 -0
- data/test/fixtures/cucumber4_recorder/features/say_hello.feature +5 -0
- data/test/fixtures/cucumber4_recorder/features/support/env.rb +5 -0
- data/test/fixtures/cucumber4_recorder/features/support/hooks.rb +11 -0
- data/test/fixtures/cucumber4_recorder/features/support/steps.rb +9 -0
- data/test/fixtures/cucumber4_recorder/lib/hello.rb +7 -0
- data/test/fixtures/cucumber_recorder/Gemfile +5 -0
- data/test/fixtures/cucumber_recorder/appmap.yml +3 -0
- data/test/fixtures/cucumber_recorder/features/say_hello.feature +5 -0
- data/test/fixtures/cucumber_recorder/features/support/env.rb +5 -0
- data/test/fixtures/cucumber_recorder/features/support/hooks.rb +11 -0
- data/test/fixtures/cucumber_recorder/features/support/steps.rb +9 -0
- data/test/fixtures/cucumber_recorder/lib/hello.rb +7 -0
- data/test/fixtures/minitest_recorder/Gemfile +5 -0
- data/test/fixtures/minitest_recorder/appmap.yml +3 -0
- data/test/fixtures/minitest_recorder/lib/hello.rb +5 -0
- data/test/fixtures/minitest_recorder/test/hello_test.rb +12 -0
- data/test/fixtures/process_recorder/appmap.yml +3 -0
- data/test/fixtures/process_recorder/hello.rb +9 -0
- data/test/fixtures/rspec_recorder/Gemfile +1 -1
- data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +12 -0
- data/test/minitest_test.rb +38 -0
- data/test/record_process_test.rb +35 -0
- data/test/rspec_test.rb +5 -0
- metadata +39 -3
- data/spec/fixtures/hook/class_method.rb +0 -17
| @@ -74,10 +74,11 @@ module AppMap | |
| 74 74 |  | 
| 75 75 | 
             
                    class_name_func = ->(event) { event['defined_class'] }
         | 
| 76 76 | 
             
                    full_name_func = lambda do |event|
         | 
| 77 | 
            +
                      call = event['event'] == 'call'
         | 
| 77 78 | 
             
                      class_name = event['defined_class']
         | 
| 78 79 | 
             
                      static = event['static']
         | 
| 79 80 | 
             
                      function_name = event['method_id']
         | 
| 80 | 
            -
                      [ class_name, static ? '.' : '#', function_name ].join if class_name && !static.nil? && function_name
         | 
| 81 | 
            +
                      [ class_name, static ? '.' : '#', function_name ].join if call && class_name && !static.nil? && function_name
         | 
| 81 82 | 
             
                    end
         | 
| 82 83 |  | 
| 83 84 | 
             
                    class_frequency = frequency_calc.call(class_name_func)
         | 
    
        data/lib/appmap/class_map.rb
    CHANGED
    
    | @@ -1,7 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require 'active_support/core_ext'
         | 
| 4 | 
            -
             | 
| 5 3 | 
             
            module AppMap
         | 
| 6 4 | 
             
              class ClassMap
         | 
| 7 5 | 
             
                module HasChildren
         | 
| @@ -50,7 +48,7 @@ module AppMap | |
| 50 48 | 
             
                    end
         | 
| 51 49 | 
             
                  end
         | 
| 52 50 | 
             
                  Function = Struct.new(:name) do
         | 
| 53 | 
            -
                    attr_accessor :static, :location
         | 
| 51 | 
            +
                    attr_accessor :static, :location, :labels
         | 
| 54 52 |  | 
| 55 53 | 
             
                    def type
         | 
| 56 54 | 
             
                      'function'
         | 
| @@ -61,8 +59,9 @@ module AppMap | |
| 61 59 | 
             
                        name: name,
         | 
| 62 60 | 
             
                        type: type,
         | 
| 63 61 | 
             
                        location: location,
         | 
| 64 | 
            -
                        static: static
         | 
| 65 | 
            -
             | 
| 62 | 
            +
                        static: static,
         | 
| 63 | 
            +
                        labels: labels
         | 
| 64 | 
            +
                      }.delete_if {|k,v| v.nil?}
         | 
| 66 65 | 
             
                    end
         | 
| 67 66 | 
             
                  end
         | 
| 68 67 | 
             
                end
         | 
| @@ -71,35 +70,25 @@ module AppMap | |
| 71 70 | 
             
                  def build_from_methods(config, methods)
         | 
| 72 71 | 
             
                    root = Types::Root.new
         | 
| 73 72 | 
             
                    methods.each do |method|
         | 
| 74 | 
            -
                      package = package_for_method( | 
| 75 | 
            -
             | 
| 73 | 
            +
                      package = config.package_for_method(method) \
         | 
| 74 | 
            +
                        or raise "No package found for method #{method}"
         | 
| 75 | 
            +
                      add_function root, package, method
         | 
| 76 76 | 
             
                    end
         | 
| 77 77 | 
             
                    root.children.map(&:to_h)
         | 
| 78 78 | 
             
                  end
         | 
| 79 79 |  | 
| 80 80 | 
             
                  protected
         | 
| 81 81 |  | 
| 82 | 
            -
                  def  | 
| 83 | 
            -
                    location = method. | 
| 84 | 
            -
                    location_file, = location
         | 
| 85 | 
            -
                    location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
         | 
| 86 | 
            -
             | 
| 87 | 
            -
                    packages.find do |pkg|
         | 
| 88 | 
            -
                      (location_file.index(pkg.path) == 0) &&
         | 
| 89 | 
            -
                        !pkg.exclude.find { |p| location_file.index(p) }
         | 
| 90 | 
            -
                    end or raise "No package found for method #{method}"
         | 
| 91 | 
            -
                  end
         | 
| 92 | 
            -
             | 
| 93 | 
            -
                  def add_function(root, package_name, method)
         | 
| 94 | 
            -
                    location = method.method.source_location
         | 
| 82 | 
            +
                  def add_function(root, package, method)
         | 
| 83 | 
            +
                    location = method.source_location
         | 
| 95 84 | 
             
                    location_file, lineno = location
         | 
| 96 85 | 
             
                    location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
         | 
| 97 86 |  | 
| 98 | 
            -
                    static = method. | 
| 87 | 
            +
                    static = method.static
         | 
| 99 88 |  | 
| 100 89 | 
             
                    object_infos = [
         | 
| 101 90 | 
             
                      {
         | 
| 102 | 
            -
                        name:  | 
| 91 | 
            +
                        name: package.path,
         | 
| 103 92 | 
             
                        type: 'package'
         | 
| 104 93 | 
             
                      }
         | 
| 105 94 | 
             
                    ]
         | 
| @@ -109,12 +98,15 @@ module AppMap | |
| 109 98 | 
             
                        type: 'class'
         | 
| 110 99 | 
             
                      }
         | 
| 111 100 | 
             
                    end
         | 
| 112 | 
            -
                     | 
| 113 | 
            -
                      name: method. | 
| 101 | 
            +
                    function_info = {
         | 
| 102 | 
            +
                      name: method.name,
         | 
| 114 103 | 
             
                      type: 'function',
         | 
| 115 104 | 
             
                      location: [ location_file, lineno ].join(':'),
         | 
| 116 105 | 
             
                      static: static
         | 
| 117 106 | 
             
                    }
         | 
| 107 | 
            +
                    function_info[:labels] = package.labels if package.labels
         | 
| 108 | 
            +
                    object_infos << function_info
         | 
| 109 | 
            +
                    
         | 
| 118 110 | 
             
                    parent = root
         | 
| 119 111 | 
             
                    object_infos.each do |info|
         | 
| 120 112 | 
             
                      parent = find_or_create parent.children, info do
         | 
| @@ -5,66 +5,7 @@ module AppMap | |
| 5 5 | 
             
                RecordStruct = Struct.new(:config, :program)
         | 
| 6 6 |  | 
| 7 7 | 
             
                class Record < RecordStruct
         | 
| 8 | 
            -
                  class << self
         | 
| 9 | 
            -
                    # Builds a Hash of metadata which can be detected by inspecting the system.
         | 
| 10 | 
            -
                    def detect_metadata
         | 
| 11 | 
            -
                      {
         | 
| 12 | 
            -
                        language: {
         | 
| 13 | 
            -
                          name: 'ruby',
         | 
| 14 | 
            -
                          engine: RUBY_ENGINE,
         | 
| 15 | 
            -
                          version: RUBY_VERSION
         | 
| 16 | 
            -
                        },
         | 
| 17 | 
            -
                        client: {
         | 
| 18 | 
            -
                          name: 'appmap',
         | 
| 19 | 
            -
                          url: AppMap::URL,
         | 
| 20 | 
            -
                          version: AppMap::VERSION
         | 
| 21 | 
            -
                        }
         | 
| 22 | 
            -
                      }.tap do |m|
         | 
| 23 | 
            -
                        if defined?(::Rails)
         | 
| 24 | 
            -
                          m[:frameworks] ||= []
         | 
| 25 | 
            -
                          m[:frameworks] << {
         | 
| 26 | 
            -
                            name: 'rails',
         | 
| 27 | 
            -
                            version: ::Rails.version
         | 
| 28 | 
            -
                          }
         | 
| 29 | 
            -
                        end
         | 
| 30 | 
            -
                        m[:git] = git_metadata if git_available
         | 
| 31 | 
            -
                      end
         | 
| 32 | 
            -
                    end
         | 
| 33 | 
            -
             | 
| 34 | 
            -
                    protected
         | 
| 35 | 
            -
             | 
| 36 | 
            -
                    def git_available
         | 
| 37 | 
            -
                      @git_available = system('git status 2>&1 > /dev/null') if @git_available.nil?
         | 
| 38 | 
            -
                    end
         | 
| 39 | 
            -
             | 
| 40 | 
            -
                    def git_metadata
         | 
| 41 | 
            -
                      git_repo = `git config --get remote.origin.url`.strip
         | 
| 42 | 
            -
                      git_branch = `git rev-parse --abbrev-ref HEAD`.strip
         | 
| 43 | 
            -
                      git_sha = `git rev-parse HEAD`.strip
         | 
| 44 | 
            -
                      git_status = `git status -s`.split("\n").map(&:strip)
         | 
| 45 | 
            -
                      git_last_annotated_tag = `git describe --abbrev=0 2>/dev/null`.strip
         | 
| 46 | 
            -
                      git_last_annotated_tag = nil if git_last_annotated_tag.blank?
         | 
| 47 | 
            -
                      git_last_tag = `git describe --abbrev=0 --tags 2>/dev/null`.strip
         | 
| 48 | 
            -
                      git_last_tag = nil if git_last_tag.blank?
         | 
| 49 | 
            -
                      git_commits_since_last_annotated_tag = `git describe`.strip =~ /-(\d+)-(\w+)$/[1] rescue 0 if git_last_annotated_tag
         | 
| 50 | 
            -
                      git_commits_since_last_tag = `git describe --tags`.strip =~ /-(\d+)-(\w+)$/[1] rescue 0 if git_last_tag
         | 
| 51 | 
            -
             | 
| 52 | 
            -
                      {
         | 
| 53 | 
            -
                        repository: git_repo,
         | 
| 54 | 
            -
                        branch: git_branch,
         | 
| 55 | 
            -
                        commit: git_sha,
         | 
| 56 | 
            -
                        status: git_status,
         | 
| 57 | 
            -
                        git_last_annotated_tag: git_last_annotated_tag,
         | 
| 58 | 
            -
                        git_last_tag: git_last_tag,
         | 
| 59 | 
            -
                        git_commits_since_last_annotated_tag: git_commits_since_last_annotated_tag,
         | 
| 60 | 
            -
                        git_commits_since_last_tag: git_commits_since_last_tag
         | 
| 61 | 
            -
                      }
         | 
| 62 | 
            -
                    end
         | 
| 63 | 
            -
                  end
         | 
| 64 | 
            -
             | 
| 65 8 | 
             
                  def perform(&block)
         | 
| 66 | 
            -
                    AppMap::Hook.hook(config)
         | 
| 67 | 
            -
             | 
| 68 9 | 
             
                    tracer = AppMap.tracing.trace
         | 
| 69 10 |  | 
| 70 11 | 
             
                    events = []
         | 
| @@ -85,8 +26,8 @@ module AppMap | |
| 85 26 | 
             
                      quit = true
         | 
| 86 27 | 
             
                      event_thread.join
         | 
| 87 28 | 
             
                      yield AppMap::APPMAP_FORMAT_VERSION,
         | 
| 88 | 
            -
                             | 
| 89 | 
            -
                            AppMap.class_map( | 
| 29 | 
            +
                            AppMap.detect_metadata,
         | 
| 30 | 
            +
                            AppMap.class_map(tracer.event_methods),
         | 
| 90 31 | 
             
                            events
         | 
| 91 32 | 
             
                    end
         | 
| 92 33 |  | 
| @@ -0,0 +1,91 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module AppMap
         | 
| 4 | 
            +
              Package = Struct.new(:path, :exclude, :labels) do
         | 
| 5 | 
            +
                def initialize(path, exclude, labels = nil)
         | 
| 6 | 
            +
                  super
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
                
         | 
| 9 | 
            +
                def to_h
         | 
| 10 | 
            +
                  {
         | 
| 11 | 
            +
                    path: path,
         | 
| 12 | 
            +
                    exclude: exclude.blank? ? nil : exclude,
         | 
| 13 | 
            +
                    labels: labels.blank? ? nil : labels
         | 
| 14 | 
            +
                  }.compact
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              class Config
         | 
| 19 | 
            +
                # Methods that should always be hooked, with their containing
         | 
| 20 | 
            +
                # package and labels that should be applied to them.
         | 
| 21 | 
            +
                HOOKED_METHODS = {
         | 
| 22 | 
            +
                  'ActiveSupport::SecurityUtils' => {
         | 
| 23 | 
            +
                    secure_compare: Package.new('active_support', nil, ['security'])
         | 
| 24 | 
            +
                  },
         | 
| 25 | 
            +
                  'OpenSSL::X509::Certificate' => {
         | 
| 26 | 
            +
                    sign: Package.new('openssl', nil, ['security'])
         | 
| 27 | 
            +
                  }
         | 
| 28 | 
            +
                }
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                attr_reader :name, :packages
         | 
| 31 | 
            +
                def initialize(name, packages = [])
         | 
| 32 | 
            +
                  @name = name
         | 
| 33 | 
            +
                  @packages = packages
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                class << self
         | 
| 37 | 
            +
                  # Loads configuration data from a file, specified by the file name.
         | 
| 38 | 
            +
                  def load_from_file(config_file_name)
         | 
| 39 | 
            +
                    require 'yaml'
         | 
| 40 | 
            +
                    load YAML.safe_load(::File.read(config_file_name))
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  # Loads configuration from a Hash.
         | 
| 44 | 
            +
                  def load(config_data)
         | 
| 45 | 
            +
                    packages = (config_data['packages'] || []).map do |package|
         | 
| 46 | 
            +
                      Package.new(package['path'], package['exclude'] || [])
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
                    Config.new config_data['name'], packages
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def to_h
         | 
| 53 | 
            +
                  {
         | 
| 54 | 
            +
                    name: name,
         | 
| 55 | 
            +
                    packages: packages.map(&:to_h)
         | 
| 56 | 
            +
                  }
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                def package_for_method(method)
         | 
| 60 | 
            +
                  location = method.source_location
         | 
| 61 | 
            +
                  location_file, = location
         | 
| 62 | 
            +
                  return unless location_file
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                  defined_class,_,method_name = Hook.qualify_method_name(method)
         | 
| 65 | 
            +
                  hooked_method = find_hooked_method(defined_class, method_name)
         | 
| 66 | 
            +
                  return hooked_method if hooked_method
         | 
| 67 | 
            +
                  
         | 
| 68 | 
            +
                  location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
         | 
| 69 | 
            +
                  packages.find do |pkg|
         | 
| 70 | 
            +
                    (location_file.index(pkg.path) == 0) &&
         | 
| 71 | 
            +
                      !pkg.exclude.find { |p| location_file.index(p) }
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                def included_by_location?(method)
         | 
| 76 | 
            +
                  !!package_for_method(method)
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                def always_hook?(defined_class, method_name)
         | 
| 80 | 
            +
                  !!find_hooked_method(defined_class, method_name)
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                def find_hooked_method(defined_class, method_name)
         | 
| 84 | 
            +
                  find_hooked_class(defined_class)[method_name]
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
                
         | 
| 87 | 
            +
                def find_hooked_class(defined_class)
         | 
| 88 | 
            +
                  HOOKED_METHODS[defined_class] || {}
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
              end
         | 
| 91 | 
            +
            end
         | 
| @@ -0,0 +1,89 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'appmap/util'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module AppMap
         | 
| 6 | 
            +
              module Cucumber
         | 
| 7 | 
            +
                ScenarioAttributes = Struct.new(:name, :feature, :feature_group)
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                ProviderStruct = Struct.new(:scenario) do
         | 
| 10 | 
            +
                  def feature_group
         | 
| 11 | 
            +
                    # e.g. <Cucumber::Core::Ast::Location::Precise: cucumber/api/features/authenticate.feature:1>
         | 
| 12 | 
            +
                    feature_path.split('/').last.split('.')[0]
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                # ProviderBefore4 provides scenario name, feature name, and feature group name for Cucumber
         | 
| 17 | 
            +
                # versions before 4.0.
         | 
| 18 | 
            +
                class ProviderBefore4 < ProviderStruct
         | 
| 19 | 
            +
                  def attributes
         | 
| 20 | 
            +
                    ScenarioAttributes.new(scenario.name, scenario.feature.name, feature_group)
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  def feature_path
         | 
| 24 | 
            +
                    scenario.feature.location.to_s
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                # Provider4 provides scenario name, feature name, and feature group name for Cucumber
         | 
| 29 | 
            +
                # versions 4.0 and later.
         | 
| 30 | 
            +
                class Provider4 < ProviderStruct
         | 
| 31 | 
            +
                  def attributes
         | 
| 32 | 
            +
                    ScenarioAttributes.new(scenario.name, scenario.name.split(' ')[0..1].join(' '), feature_group)
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  def feature_path
         | 
| 36 | 
            +
                    scenario.location.file
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                class << self
         | 
| 41 | 
            +
                  def write_scenario(scenario, appmap)
         | 
| 42 | 
            +
                    appmap['metadata'] = update_metadata(scenario, appmap['metadata'])
         | 
| 43 | 
            +
                    scenario_filename = AppMap::Util.scenario_filename(appmap['metadata']['name'])
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                    FileUtils.mkdir_p 'tmp/appmap/cucumber'
         | 
| 46 | 
            +
                    File.write(File.join('tmp/appmap/cucumber', scenario_filename), JSON.generate(appmap))
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def enabled?
         | 
| 50 | 
            +
                    ENV['APPMAP'] == 'true'
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  protected
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  def cucumber_version
         | 
| 56 | 
            +
                    Gem.loaded_specs['cucumber']&.version&.to_s
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  def provider(scenario)
         | 
| 60 | 
            +
                    major, = cucumber_version.split('.').map(&:to_i)
         | 
| 61 | 
            +
                    if major < 4
         | 
| 62 | 
            +
                      ProviderBefore4
         | 
| 63 | 
            +
                    else
         | 
| 64 | 
            +
                      Provider4
         | 
| 65 | 
            +
                    end.new(scenario)
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  def update_metadata(scenario, base_metadata)
         | 
| 69 | 
            +
                    attributes = provider(scenario).attributes
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                    base_metadata.tap do |m|
         | 
| 72 | 
            +
                      m['name'] = attributes.name
         | 
| 73 | 
            +
                      m['feature'] = attributes.feature
         | 
| 74 | 
            +
                      m['feature_group'] = attributes.feature_group
         | 
| 75 | 
            +
                      m['labels'] ||= []
         | 
| 76 | 
            +
                      m['labels'] += (scenario.tags&.map(&:name) || [])
         | 
| 77 | 
            +
                      m['frameworks'] ||= []
         | 
| 78 | 
            +
                      m['frameworks'] << {
         | 
| 79 | 
            +
                        'name' => 'cucumber',
         | 
| 80 | 
            +
                        'version' => Gem.loaded_specs['cucumber']&.version&.to_s
         | 
| 81 | 
            +
                      }
         | 
| 82 | 
            +
                      m['recorder'] = {
         | 
| 83 | 
            +
                        'name' => 'cucumber'
         | 
| 84 | 
            +
                      }
         | 
| 85 | 
            +
                    end
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
              end
         | 
| 89 | 
            +
            end
         | 
    
        data/lib/appmap/event.rb
    CHANGED
    
    | @@ -15,15 +15,13 @@ module AppMap | |
| 15 15 | 
             
                  end
         | 
| 16 16 | 
             
                end
         | 
| 17 17 |  | 
| 18 | 
            -
                MethodEventStruct = Struct.new(:id, :event, :defined_class, :method_id, :path, :lineno, : | 
| 18 | 
            +
                MethodEventStruct = Struct.new(:id, :event, :defined_class, :method_id, :path, :lineno, :thread_id)
         | 
| 19 19 |  | 
| 20 20 | 
             
                class MethodEvent < MethodEventStruct
         | 
| 21 21 | 
             
                  LIMIT = 100
         | 
| 22 22 |  | 
| 23 23 | 
             
                  class << self
         | 
| 24 24 | 
             
                    def build_from_invocation(me, event_type, defined_class, method)
         | 
| 25 | 
            -
                      singleton = method.owner.singleton_class?
         | 
| 26 | 
            -
             | 
| 27 25 | 
             
                      me.id = AppMap::Event.next_id_counter
         | 
| 28 26 | 
             
                      me.event = event_type
         | 
| 29 27 | 
             
                      me.defined_class = defined_class
         | 
| @@ -32,7 +30,6 @@ module AppMap | |
| 32 30 | 
             
                      path = path[Dir.pwd.length + 1..-1] if path.index(Dir.pwd) == 0
         | 
| 33 31 | 
             
                      me.path = path
         | 
| 34 32 | 
             
                      me.lineno = method.source_location[1]
         | 
| 35 | 
            -
                      me.static = singleton
         | 
| 36 33 | 
             
                      me.thread_id = Thread.current.object_id
         | 
| 37 34 | 
             
                    end
         | 
| 38 35 |  | 
| @@ -62,11 +59,10 @@ module AppMap | |
| 62 59 | 
             
                    end
         | 
| 63 60 | 
             
                  end
         | 
| 64 61 |  | 
| 65 | 
            -
                  alias static? static
         | 
| 66 62 | 
             
                end
         | 
| 67 63 |  | 
| 68 64 | 
             
                class MethodCall < MethodEvent
         | 
| 69 | 
            -
                  attr_accessor :parameters, :receiver
         | 
| 65 | 
            +
                  attr_accessor :parameters, :receiver, :static
         | 
| 70 66 |  | 
| 71 67 | 
             
                  class << self
         | 
| 72 68 | 
             
                    def build_from_invocation(mc = MethodCall.new, defined_class, method, receiver, arguments)
         | 
| @@ -88,6 +84,7 @@ module AppMap | |
| 88 84 | 
             
                          object_id: receiver.__id__,
         | 
| 89 85 | 
             
                          value: display_string(receiver)
         | 
| 90 86 | 
             
                        }
         | 
| 87 | 
            +
                        mc.static = receiver.is_a?(Module)
         | 
| 91 88 | 
             
                        MethodEvent.build_from_invocation(mc, :call, defined_class, method)
         | 
| 92 89 | 
             
                      end
         | 
| 93 90 | 
             
                    end
         | 
| @@ -95,10 +92,13 @@ module AppMap | |
| 95 92 |  | 
| 96 93 | 
             
                  def to_h
         | 
| 97 94 | 
             
                    super.tap do |h|
         | 
| 95 | 
            +
                      h[:static] = static
         | 
| 98 96 | 
             
                      h[:parameters] = parameters
         | 
| 99 97 | 
             
                      h[:receiver] = receiver
         | 
| 100 98 | 
             
                    end
         | 
| 101 99 | 
             
                  end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  alias static? static
         | 
| 102 102 | 
             
                end
         | 
| 103 103 |  | 
| 104 104 | 
             
                class MethodReturnIgnoreValue < MethodEvent
         | 
    
        data/lib/appmap/hook.rb
    CHANGED
    
    | @@ -6,147 +6,125 @@ module AppMap | |
| 6 6 | 
             
              class Hook
         | 
| 7 7 | 
             
                LOG = false
         | 
| 8 8 |  | 
| 9 | 
            -
                 | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 9 | 
            +
                HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                class << self
         | 
| 12 | 
            +
                  # Return the class, separator ('.' or '#'), and method name for
         | 
| 13 | 
            +
                  # the given method.
         | 
| 14 | 
            +
                  def qualify_method_name(method)
         | 
| 15 | 
            +
                    if method.owner.singleton_class?
         | 
| 16 | 
            +
                      # Singleton class names can take two forms:
         | 
| 17 | 
            +
                      # #<Class:Foo> or
         | 
| 18 | 
            +
                      # #<Class:#<Bar:0x0123ABC>>. Retrieve the name of
         | 
| 19 | 
            +
                      # the class from the string.
         | 
| 20 | 
            +
                      # 
         | 
| 21 | 
            +
                      # (There really isn't a better way to do this. The
         | 
| 22 | 
            +
                      # singleton's reference to the class it was created
         | 
| 23 | 
            +
                      # from is stored in an instance variable named
         | 
| 24 | 
            +
                      # '__attached__'. It doesn't have the '@' prefix, so
         | 
| 25 | 
            +
                      # it's internal only, and not accessible from user
         | 
| 26 | 
            +
                      # code.)
         | 
| 27 | 
            +
                      class_name = /#<Class:((#<(?<cls>.*?):)|((?<cls>.*?)>))/.match(method.owner.to_s)['cls']
         | 
| 28 | 
            +
                      [ class_name, '.', method.name ]
         | 
| 29 | 
            +
                    else
         | 
| 30 | 
            +
                      [ method.owner.name, '#', method.name ]
         | 
| 31 | 
            +
                    end
         | 
| 15 32 | 
             
                  end
         | 
| 16 33 | 
             
                end
         | 
| 17 34 |  | 
| 18 | 
            -
                 | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
                      require 'yaml'
         | 
| 23 | 
            -
                      load YAML.safe_load(::File.read(config_file_name))
         | 
| 24 | 
            -
                    end
         | 
| 35 | 
            +
                attr_reader :config
         | 
| 36 | 
            +
                def initialize(config)
         | 
| 37 | 
            +
                  @config = config
         | 
| 38 | 
            +
                end
         | 
| 25 39 |  | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
                     | 
| 40 | 
            +
                # Observe class loading and hook all methods which match the config.
         | 
| 41 | 
            +
                def enable &block
         | 
| 42 | 
            +
                  before_hook = lambda do |defined_class, method, receiver, args|
         | 
| 43 | 
            +
                    require 'appmap/event'
         | 
| 44 | 
            +
                    call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, method, receiver, args)
         | 
| 45 | 
            +
                    AppMap.tracing.record_event call_event, defined_class: defined_class, method: method
         | 
| 46 | 
            +
                    [ call_event, Time.now ]
         | 
| 33 47 | 
             
                  end
         | 
| 34 48 |  | 
| 35 | 
            -
                   | 
| 36 | 
            -
                     | 
| 49 | 
            +
                  after_hook = lambda do |call_event, defined_class, method, start_time, return_value, exception|
         | 
| 50 | 
            +
                    require 'appmap/event'
         | 
| 51 | 
            +
                    elapsed = Time.now - start_time
         | 
| 52 | 
            +
                    return_event = AppMap::Event::MethodReturn.build_from_invocation \
         | 
| 53 | 
            +
                                                                 defined_class, method, call_event.id, elapsed, return_value, exception
         | 
| 54 | 
            +
                    AppMap.tracing.record_event return_event
         | 
| 37 55 | 
             
                  end
         | 
| 38 56 |  | 
| 39 | 
            -
                   | 
| 40 | 
            -
                     | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
                     | 
| 57 | 
            +
                  with_disabled_hook = lambda do |&fn|
         | 
| 58 | 
            +
                    # Don't record functions, such as to_s and inspect, that might be called
         | 
| 59 | 
            +
                    # by the fn. Otherwise there can be a stack overflow.
         | 
| 60 | 
            +
                    Thread.current[HOOK_DISABLE_KEY] = true
         | 
| 61 | 
            +
                    begin
         | 
| 62 | 
            +
                      fn.call
         | 
| 63 | 
            +
                    ensure
         | 
| 64 | 
            +
                      Thread.current[HOOK_DISABLE_KEY] = false
         | 
| 65 | 
            +
                    end
         | 
| 44 66 | 
             
                  end
         | 
| 45 | 
            -
                end
         | 
| 46 67 |  | 
| 47 | 
            -
             | 
| 68 | 
            +
                  tp = TracePoint.new(:end) do |tp|
         | 
| 69 | 
            +
                    hook = self
         | 
| 70 | 
            +
                    cls = tp.self
         | 
| 48 71 |  | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
                  def hook(config = AppMap.configure)
         | 
| 52 | 
            -
                    package_include_paths = config.packages.map(&:path)
         | 
| 53 | 
            -
                    package_exclude_paths = config.packages.map do |pkg|
         | 
| 54 | 
            -
                      pkg.exclude.map do |exclude|
         | 
| 55 | 
            -
                        File.join(pkg.path, exclude)
         | 
| 56 | 
            -
                      end
         | 
| 57 | 
            -
                    end.flatten
         | 
| 72 | 
            +
                    instance_methods = cls.public_instance_methods(false)
         | 
| 73 | 
            +
                    class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods
         | 
| 58 74 |  | 
| 59 | 
            -
                     | 
| 60 | 
            -
                       | 
| 61 | 
            -
             | 
| 62 | 
            -
                      AppMap.tracing.record_event call_event, defined_class: defined_class, method: method
         | 
| 63 | 
            -
                      [ call_event, Time.now ]
         | 
| 64 | 
            -
                    end
         | 
| 75 | 
            +
                    hook_method = lambda do |cls|
         | 
| 76 | 
            +
                      lambda do |method_id|
         | 
| 77 | 
            +
                        next if method_id.to_s =~ /_hooked_by_appmap$/
         | 
| 65 78 |  | 
| 66 | 
            -
             | 
| 67 | 
            -
             | 
| 68 | 
            -
             | 
| 69 | 
            -
             | 
| 70 | 
            -
                        defined_class, method, call_event.id, elapsed, return_value, exception
         | 
| 71 | 
            -
                      AppMap.tracing.record_event return_event
         | 
| 72 | 
            -
                    end
         | 
| 79 | 
            +
                        method = cls.public_instance_method(method_id)
         | 
| 80 | 
            +
                        disasm = RubyVM::InstructionSequence.disasm(method)
         | 
| 81 | 
            +
                        # Skip methods that have no instruction sequence, as they are obviously trivial.
         | 
| 82 | 
            +
                        next unless disasm
         | 
| 73 83 |  | 
| 74 | 
            -
             | 
| 75 | 
            -
             | 
| 76 | 
            -
                      # by the fn. Otherwise there can be a stack oveflow.
         | 
| 77 | 
            -
                      Thread.current[HOOK_DISABLE_KEY] = true
         | 
| 78 | 
            -
                      begin
         | 
| 79 | 
            -
                        fn.call
         | 
| 80 | 
            -
                      ensure
         | 
| 81 | 
            -
                        Thread.current[HOOK_DISABLE_KEY] = false
         | 
| 82 | 
            -
                      end
         | 
| 83 | 
            -
                    end
         | 
| 84 | 
            +
                        defined_class, method_symbol, method_name = Hook.qualify_method_name(method)
         | 
| 85 | 
            +
                        method_display_name = [defined_class, method_symbol, method_name].join
         | 
| 84 86 |  | 
| 85 | 
            -
             | 
| 86 | 
            -
             | 
| 87 | 
            -
             | 
| 88 | 
            -
                      instance_methods = cls.public_instance_methods(false)
         | 
| 89 | 
            -
                      class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods
         | 
| 90 | 
            -
             | 
| 91 | 
            -
                      hook_method = lambda do |cls|
         | 
| 92 | 
            -
                        lambda do |method_id|
         | 
| 93 | 
            -
                          next if method_id.to_s =~ /_hooked_by_appmap$/
         | 
| 94 | 
            -
             | 
| 95 | 
            -
                          method = cls.public_instance_method(method_id)
         | 
| 96 | 
            -
                          location = method.source_location
         | 
| 97 | 
            -
                          location_file, = location
         | 
| 98 | 
            -
                          next unless location_file
         | 
| 99 | 
            -
             | 
| 100 | 
            -
                          location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
         | 
| 101 | 
            -
                          match = package_include_paths.find { |p| location_file.index(p) == 0 }
         | 
| 102 | 
            -
                          match &&= !package_exclude_paths.find { |p| location_file.index(p) }
         | 
| 103 | 
            -
                          next unless match
         | 
| 104 | 
            -
             | 
| 105 | 
            -
                          disasm = RubyVM::InstructionSequence.disasm(method)
         | 
| 106 | 
            -
                          # Skip methods that have no instruction sequence, as they are obviously trivial.
         | 
| 107 | 
            -
                          next unless disasm
         | 
| 108 | 
            -
             | 
| 109 | 
            -
                          defined_class, method_symbol = \
         | 
| 110 | 
            -
                            if method.owner.singleton_class?
         | 
| 111 | 
            -
                              # Singleton class name is like: #<Class:<(.*)>>
         | 
| 112 | 
            -
                              class_name = method.owner.to_s['#<Class:<'.length-1..-2]
         | 
| 113 | 
            -
                              [ class_name, '.' ]
         | 
| 114 | 
            -
                            else
         | 
| 115 | 
            -
                              [ method.owner.name, '#' ]
         | 
| 116 | 
            -
                            end
         | 
| 87 | 
            +
                        # Don't try and trace the AppMap methods or there will be
         | 
| 88 | 
            +
                        # a stack overflow in the defined hook method.
         | 
| 89 | 
            +
                        next if /\AAppMap[:\.]/.match?(method_display_name) 
         | 
| 117 90 |  | 
| 118 | 
            -
             | 
| 91 | 
            +
                        next unless \
         | 
| 92 | 
            +
                          config.always_hook?(defined_class, method_name) ||
         | 
| 93 | 
            +
                          config.included_by_location?(method)
         | 
| 119 94 |  | 
| 120 | 
            -
             | 
| 121 | 
            -
                            base_method = method.bind(self).to_proc
         | 
| 95 | 
            +
                        warn "AppMap: Hooking #{method_display_name}" if LOG
         | 
| 122 96 |  | 
| 123 | 
            -
             | 
| 124 | 
            -
             | 
| 125 | 
            -
                            return base_method.call(*args, &block) unless enabled
         | 
| 97 | 
            +
                        cls.define_method method_id do |*args, &block|
         | 
| 98 | 
            +
                          base_method = method.bind(self).to_proc
         | 
| 126 99 |  | 
| 127 | 
            -
             | 
| 128 | 
            -
             | 
| 129 | 
            -
             | 
| 130 | 
            -
             | 
| 131 | 
            -
             | 
| 132 | 
            -
                             | 
| 133 | 
            -
             | 
| 134 | 
            -
             | 
| 135 | 
            -
             | 
| 136 | 
            -
             | 
| 137 | 
            -
                             | 
| 138 | 
            -
             | 
| 139 | 
            -
             | 
| 140 | 
            -
             | 
| 100 | 
            +
                          hook_disabled = Thread.current[HOOK_DISABLE_KEY]
         | 
| 101 | 
            +
                          enabled = true if !hook_disabled && AppMap.tracing.enabled?
         | 
| 102 | 
            +
                          return base_method.call(*args, &block) unless enabled
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                          call_event, start_time = with_disabled_hook.call do
         | 
| 105 | 
            +
                            before_hook.call(defined_class, method, self, args)
         | 
| 106 | 
            +
                          end
         | 
| 107 | 
            +
                          return_value = nil
         | 
| 108 | 
            +
                          exception = nil
         | 
| 109 | 
            +
                          begin
         | 
| 110 | 
            +
                            return_value = base_method.call(*args, &block)
         | 
| 111 | 
            +
                          rescue
         | 
| 112 | 
            +
                            exception = $ERROR_INFO
         | 
| 113 | 
            +
                            raise
         | 
| 114 | 
            +
                          ensure
         | 
| 115 | 
            +
                            with_disabled_hook.call do
         | 
| 116 | 
            +
                              after_hook.call(call_event, defined_class, method, start_time, return_value, exception)
         | 
| 141 117 | 
             
                            end
         | 
| 142 118 | 
             
                          end
         | 
| 143 119 | 
             
                        end
         | 
| 144 120 | 
             
                      end
         | 
| 145 | 
            -
             | 
| 146 | 
            -
                      instance_methods.each(&hook_method.call(cls))
         | 
| 147 | 
            -
                      class_methods.each(&hook_method.call(cls.singleton_class))
         | 
| 148 121 | 
             
                    end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                    instance_methods.each(&hook_method.call(cls))
         | 
| 124 | 
            +
                    class_methods.each(&hook_method.call(cls.singleton_class))
         | 
| 149 125 | 
             
                  end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                  tp.enable(&block)
         | 
| 150 128 | 
             
                end
         | 
| 151 129 | 
             
              end
         | 
| 152 130 | 
             
            end
         |