From 6e04bc73268ff8cef4b2570c231ba23f66bc4308 Mon Sep 17 00:00:00 2001 From: Kevin Gilpin Date: Fri, 2 Sep 2022 14:57:08 -0400 Subject: [PATCH 1/2] test: Request recording --- spec/rails_spec_helper.rb | 36 +++++++++++++++++++++++++++++ spec/remote_recording_spec.rb | 36 +++++++---------------------- spec/request_recording_spec.rb | 41 ++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 28 deletions(-) create mode 100644 spec/request_recording_spec.rb diff --git a/spec/rails_spec_helper.rb b/spec/rails_spec_helper.rb index 6f3c53ac..d5f71681 100644 --- a/spec/rails_spec_helper.rb +++ b/spec/rails_spec_helper.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true require 'open3' +require 'random-port' +require 'socket' require 'spec_helper' require 'active_support' @@ -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 diff --git a/spec/remote_recording_spec.rb b/spec/remote_recording_spec.rb index 085d3086..558caed5 100644 --- a/spec/remote_recording_spec.rb +++ b/spec/remote_recording_spec.rb @@ -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) @@ -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 @@ -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)) diff --git a/spec/request_recording_spec.rb b/spec/request_recording_spec.rb new file mode 100644 index 00000000..12e60aec --- /dev/null +++ b/spec/request_recording_spec.rb @@ -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 From 3532a853c9a125a15ed3ef364fd2e9e28d85810b Mon Sep 17 00:00:00 2001 From: Kevin Gilpin Date: Tue, 6 Sep 2022 11:13:22 -0400 Subject: [PATCH 2/2] wip: Record DAO association load events --- lib/appmap/gem_hooks/activerecord.yml | 6 ++++ lib/appmap/handler/association_handler.rb | 41 +++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 lib/appmap/handler/association_handler.rb diff --git a/lib/appmap/gem_hooks/activerecord.yml b/lib/appmap/gem_hooks/activerecord.yml index 1512a830..ab467e9b 100644 --- a/lib/appmap/gem_hooks/activerecord.yml +++ b/lib/appmap/gem_hooks/activerecord.yml @@ -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 diff --git a/lib/appmap/handler/association_handler.rb b/lib/appmap/handler/association_handler.rb new file mode 100644 index 00000000..96dffc36 --- /dev/null +++ b/lib/appmap/handler/association_handler.rb @@ -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