Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/appmap/gem_hooks/activerecord.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@
label: dao.materialize
- method: ActiveRecord::FixtureSet::File#raw_rows
# label: deserialize.safe
- methods:
- ActiveRecord::Associations::Association#load_target
- ActiveRecord::Associations::CollectionProxy#load_target
- ActiveRecord::Associations::CollectionAssociation#load_target
label: dao.association.load
handler_class: AppMap::Handler::AssociationHandler
41 changes: 41 additions & 0 deletions lib/appmap/handler/association_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
require 'appmap/handler/function_handler'

module AppMap
module Handler
class AssociationHandler < FunctionHandler
ASSOCIATION_PROPERTIES = %i[name table_name class_name].freeze
@@warn_on_receiver_type = Set.new

def handle_call(receiver, args)
super.tap do |event|
reflection = \
if receiver.respond_to?(:reflection)
receiver.reflection
elsif receiver.instance_variables.member?(:@association)
receiver.instance_variable_get("@association").reflection
end

unless reflection
unless @@warn_on_receiver_type.member?(receiver.class)
warn "AppMap: Association details are not available for #{receiver.class}"
@@warn_on_receiver_type << receiver.class
end
return
end

properties = \
ASSOCIATION_PROPERTIES
.each_with_object([]) do |m, memo|
value = reflection.send(m) rescue nil
memo << {
name: m.to_s,
class: String,
value: value.to_s
} if [ String, Symbol ].find {|t| value.is_a?(t)}
end
event.receiver[:properties] = properties
end
end
end
end
end
36 changes: 36 additions & 0 deletions spec/rails_spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

require 'open3'
require 'random-port'
require 'socket'

require 'spec_helper'
require 'active_support'
Expand Down Expand Up @@ -115,6 +117,40 @@ def run_process(method, cmd, env, options = {})
shared_context 'Rails app pg database' do |dir|
before(:all) { @app = TestRailsApp.for_fixture dir }
let(:app) { @app }
let(:users_path) { '/users' }
end

shared_context 'Rails app service running' do
def start_server(rails_app_environment: { 'ORM_MODULE' => 'sequel', 'APPMAP' => 'true' })
service_port = RandomPort::Pool::SINGLETON.acquire
@app.prepare_db
server = @app.spawn_cmd \
"./bin/rails server -p #{service_port}", rails_app_environment

uri = URI("http://localhost:#{service_port}/health")

100.times do
begin
Net::HTTP.get(uri)
break
rescue Errno::ECONNREFUSED
sleep 0.1
end
end

[ service_port, server ]
end

def json_body(res)
JSON.parse(res.body).deep_symbolize_keys
end

def stop_server(server)
if server
Process.kill 'INT', server
Process.wait server
end
end
end

shared_context 'rails integration test setup' do
Expand Down
36 changes: 8 additions & 28 deletions spec/remote_recording_spec.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
require 'rails_spec_helper'

require 'random-port'

require 'net/http'
require 'socket'

describe 'remote recording', :order => :defined do
def json_body(res)
Expand All @@ -13,34 +10,16 @@ def json_body(res)
rails_versions.each do |rails_version|
context "with rails #{rails_version}" do
include_context 'rails app', rails_version
include_context 'Rails app service running'

before(:context) do
@service_port = RandomPort::Pool::SINGLETON.acquire
@app.prepare_db
@server = @app.spawn_cmd \
"./bin/rails server -p #{@service_port}",
'ORM_MODULE' => 'sequel',
'APPMAP' => 'true'

uri = URI("http://localhost:#{@service_port}/health")

100.times do
Net::HTTP.get(uri)
break
rescue Errno::ECONNREFUSED
sleep 0.1
end
before(:all) do
@service_port, @server = start_server
end

after(:context) do
if @server
Process.kill 'INT', @server
Process.wait @server
end
after(:all) do
stop_server(@server)
end

let(:service_address) { URI("http://localhost:#{@service_port}") }
let(:users_path) { '/users' }
let(:record_path) { '/_appmap/record' }

it 'returns the recording status' do
Expand Down Expand Up @@ -80,10 +59,11 @@ def json_body(res)
end

it 'stops recording' do
# Generate some events
Net::HTTP.start(service_address.hostname, service_address.port) { |http|
users_res = Net::HTTP.start(service_address.hostname, service_address.port) { |http|
http.request(Net::HTTP::Get.new(users_path) )
}
# Request recording is not enabled by environment variable
expect(users_res).to_not include('appmap-file-name')

res = Net::HTTP.start(service_address.hostname, service_address.port) { |http|
http.request(Net::HTTP::Delete.new(record_path))
Expand Down
41 changes: 41 additions & 0 deletions spec/request_recording_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
require 'rails_spec_helper'

require 'net/http'

describe 'request recording', :order => :defined do
include_context 'Rails app pg database', 'spec/fixtures/rails6_users_app'
include_context 'Rails app service running'

before(:all) do
@service_port, @server = start_server(rails_app_environment: { 'ORM_MODULE' => 'sequel', 'APPMAP' => 'true', 'APPMAP_RECORD_REQUESTS' => 'true' })
end
after(:all) do
stop_server(@server)
end

let(:service_address) { URI("http://localhost:#{@service_port}") }

it 'creates an AppMap for a request' do
# Generate some events
Net::HTTP.start(service_address.hostname, service_address.port) { |http|
http.request(Net::HTTP::Get.new(users_path) )
}
Net::HTTP.start(service_address.hostname, service_address.port) { |http|
http.request(Net::HTTP::Get.new(users_path) )
}
res = Net::HTTP.start(service_address.hostname, service_address.port) { |http|
http.request(Net::HTTP::Get.new(users_path) )
}

expect(res).to be_a(Net::HTTPOK)
expect(res).to include('appmap-file-name')
appmap_file_name = res['AppMap-File-Name']
expect(File.exists?(appmap_file_name)).to be(true)
appmap = JSON.parse(File.read(appmap_file_name))
# Every event should come from the same thread
expect(appmap['events'].map {|evt| evt['thread_id']}.uniq.length).to eq(1)
# AppMap should contain only one request and response
expect(appmap['events'].select {|evt| evt['http_server_request']}.length).to eq(1)
expect(appmap['events'].select {|evt| evt['http_server_response']}.length).to eq(1)
end
end