Skip to content

Commit a2f03c5

Browse files
authored
Merge pull request #143 from applandinc/optional-appmap-yml
feat: Provide default appmap.yml settings
2 parents 8a91636 + 7fa8159 commit a2f03c5

8 files changed

+229
-54
lines changed

lib/appmap.rb

+12-5
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class << self
2727
# Gets the configuration. If there is no configuration, the default
2828
# configuration is initialized.
2929
def configuration
30-
@configuration ||= initialize
30+
@configuration ||= initialize_configuration
3131
end
3232

3333
# Sets the configuration. This is only expected to happen once per
@@ -38,12 +38,19 @@ def configuration=(config)
3838
@configuration = config
3939
end
4040

41-
# Configures AppMap for recording. Default behavior is to configure from "appmap.yml".
41+
def default_config_file_path
42+
ENV['APPMAP_CONFIG_FILE'] || 'appmap.yml'
43+
end
44+
45+
# Configures AppMap for recording. Default behavior is to configure from
46+
# APPMAP_CONFIG_FILE, or 'appmap.yml'. If no config file is available, a
47+
# configuration will be automatically generated and used - and the user is prompted
48+
# to create the config file.
49+
#
4250
# This method also activates the code hooks which record function calls as trace events.
4351
# Call this function before the program code is loaded by the Ruby VM, otherwise
4452
# the load events won't be seen and the hooks won't activate.
45-
def initialize(config_file_path = 'appmap.yml')
46-
raise "AppMap configuration file #{config_file_path} does not exist" unless ::File.exists?(config_file_path)
53+
def initialize_configuration(config_file_path = default_config_file_path)
4754
warn "Configuring AppMap from path #{config_file_path}"
4855
Config.load_from_file(config_file_path).tap do |configuration|
4956
self.configuration = configuration
@@ -118,4 +125,4 @@ def detect_metadata
118125
require 'appmap/minitest'
119126
end
120127

121-
AppMap.initialize if ENV['APPMAP'] == 'true'
128+
AppMap.initialize_configuration if ENV['APPMAP'] == 'true'

lib/appmap/config.rb

+113-28
Original file line numberDiff line numberDiff line change
@@ -223,10 +223,14 @@ def method_hook(cls, method_names, labels)
223223
'JSON::Ext::Generator::State' => TargetMethods.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json.generate])),
224224
}.freeze
225225

226-
attr_reader :name, :packages, :exclude, :hooked_methods, :builtin_hooks
226+
attr_reader :name, :appmap_dir, :packages, :exclude, :hooked_methods, :builtin_hooks
227227

228-
def initialize(name, packages, exclude: [], functions: [])
228+
def initialize(name,
229+
packages: [],
230+
exclude: [],
231+
functions: [])
229232
@name = name
233+
@appmap_dir = AppMap::DEFAULT_APPMAP_DIR
230234
@packages = packages
231235
@hook_paths = Set.new(packages.map(&:path))
232236
@exclude = exclude
@@ -253,38 +257,119 @@ def initialize(name, packages, exclude: [], functions: [])
253257
class << self
254258
# Loads configuration data from a file, specified by the file name.
255259
def load_from_file(config_file_name)
256-
require 'yaml'
257-
load YAML.safe_load(::File.read(config_file_name))
260+
logo = lambda do
261+
Util.color(<<~LOGO, :magenta)
262+
___ __ ___
263+
/ _ | ___ ___ / |/ /__ ____
264+
/ __ |/ _ \\/ _ \\/ /|_/ / _ `/ _ \\
265+
/_/ |_/ .__/ .__/_/ /_/\\_,_/ .__/
266+
/_/ /_/ /_/
267+
LOGO
268+
end
269+
270+
config_present = true if File.exists?(config_file_name)
271+
272+
config_data = if config_present
273+
require 'yaml'
274+
YAML.safe_load(::File.read(config_file_name))
275+
else
276+
warn logo.()
277+
warn ''
278+
warn Util.color(%Q|NOTICE: The AppMap config file #{config_file_name} was not found!|, :magenta, bold: true)
279+
warn ''
280+
warn Util.color(<<~MISSING_FILE_MSG, :magenta)
281+
AppMap uses this file to customize its behavior. For example, you can use
282+
the 'packages' setting to indicate which local file paths and dependency
283+
gems you want to include in the AppMap. Since you haven't provided specific
284+
settings, the appmap gem will try and guess some reasonable defaults.
285+
To suppress this message, create the file:
286+
287+
#{Pathname.new(config_file_name).expand_path}.
288+
289+
Here are the default settings that will be used in the meantime. You can
290+
copy and paste this example to start your appmap.yml.
291+
MISSING_FILE_MSG
292+
{}
293+
end
294+
load(config_data).tap do |config|
295+
config_yaml = {
296+
'name' => config.name,
297+
'packages' => config.packages.select{|p| p.path}.map do |pkg|
298+
{ 'path' => pkg.path }
299+
end,
300+
'exclude' => []
301+
}.compact
302+
unless config_present
303+
warn Util.color(YAML.dump(config_yaml), :magenta)
304+
warn logo.()
305+
end
306+
end
258307
end
259308

260309
# Loads configuration from a Hash.
261310
def load(config_data)
262-
functions = (config_data['functions'] || []).map do |function_data|
263-
package = function_data['package']
264-
cls = function_data['class']
265-
functions = function_data['function'] || function_data['functions']
266-
raise 'AppMap class configuration should specify package, class and function(s)' unless package && cls && functions
267-
functions = Array(functions).map(&:to_sym)
268-
labels = function_data['label'] || function_data['labels']
269-
labels = Array(labels).map(&:to_s) if labels
270-
Function.new(package, cls, labels, functions)
311+
name = config_data['name'] || guess_name
312+
config_params = {
313+
exclude: config_data['exclude']
314+
}.compact
315+
316+
if config_data['functions']
317+
config_params[:functions] = config_data['functions'].map do |function_data|
318+
package = function_data['package']
319+
cls = function_data['class']
320+
functions = function_data['function'] || function_data['functions']
321+
raise %q(AppMap config 'function' element should specify 'package', 'class' and 'function' or 'functions') unless package && cls && functions
322+
323+
functions = Array(functions).map(&:to_sym)
324+
labels = function_data['label'] || function_data['labels']
325+
labels = Array(labels).map(&:to_s) if labels
326+
Function.new(package, cls, labels, functions)
327+
end
271328
end
272-
packages = (config_data['packages'] || []).map do |package|
273-
gem = package['gem']
274-
path = package['path']
275-
raise 'AppMap package configuration should specify gem or path, not both' if gem && path
276-
277-
if gem
278-
shallow = package['shallow']
279-
# shallow is true by default for gems
280-
shallow = true if shallow.nil?
281-
Package.build_from_gem(gem, exclude: package['exclude'] || [], shallow: shallow)
329+
330+
config_params[:packages] = \
331+
if config_data['packages']
332+
config_data['packages'].map do |package|
333+
gem = package['gem']
334+
path = package['path']
335+
raise %q(AppMap config 'package' element should specify 'gem' or 'path', not both) if gem && path
336+
337+
if gem
338+
shallow = package['shallow']
339+
# shallow is true by default for gems
340+
shallow = true if shallow.nil?
341+
Package.build_from_gem(gem, exclude: package['exclude'] || [], shallow: shallow)
342+
else
343+
Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow'])
344+
end
345+
end.compact
282346
else
283-
Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow'])
347+
Array(guess_paths).map do |path|
348+
Package.build_from_path(path)
349+
end
284350
end
285-
end.compact
286-
exclude = config_data['exclude'] || []
287-
Config.new config_data['name'], packages, exclude: exclude, functions: functions
351+
352+
Config.new name, config_params
353+
end
354+
355+
def guess_name
356+
reponame = lambda do
357+
next unless File.directory?('.git')
358+
359+
repo_name = `git config --get remote.origin.url`.strip
360+
repo_name.split('/').last.split('.').first unless repo_name == ''
361+
end
362+
dirname = -> { Dir.pwd.split('/').last }
363+
364+
reponame.() || dirname.()
365+
end
366+
367+
def guess_paths
368+
if defined?(::Rails)
369+
%w[app/controllers app/models]
370+
elsif File.directory?('lib')
371+
%w[lib]
372+
end
288373
end
289374
end
290375

@@ -294,7 +379,7 @@ def to_h
294379
packages: packages.map(&:to_h),
295380
functions: @functions.map(&:to_h),
296381
exclude: exclude
297-
}
382+
}.compact
298383
end
299384

300385
# Determines if methods defined in a file path should possibly be hooked.

lib/appmap/util.rb

+21
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@
44

55
module AppMap
66
module Util
7+
# https://wynnnetherland.com/journal/a-stylesheet-author-s-guide-to-terminal-colors/
8+
# Embed in a String to clear all previous ANSI sequences.
9+
CLEAR = "\e[0m"
10+
BOLD = "\e[1m"
11+
12+
# Colors
13+
BLACK = "\e[30m"
14+
RED = "\e[31m"
15+
GREEN = "\e[32m"
16+
YELLOW = "\e[33m"
17+
BLUE = "\e[34m"
18+
MAGENTA = "\e[35m"
19+
CYAN = "\e[36m"
20+
WHITE = "\e[37m"
21+
722
class << self
823
# scenario_filename builds a suitable file name from a scenario name.
924
# Special characters are removed, and the file name is truncated to fit within
@@ -128,6 +143,12 @@ def write_appmap(filename, appmap)
128143
FileUtils.mv tempfile.path, filename
129144
end
130145
end
146+
147+
def color(text, color, bold: false)
148+
color = Util.const_get(color.to_s.upcase) if color.is_a?(Symbol)
149+
bold = bold ? BOLD : ""
150+
"#{bold}#{color}#{text}#{CLEAR}"
151+
end
131152
end
132153
end
133154
end

lib/appmap/version.rb

+2
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ module AppMap
66
VERSION = '0.50.0'
77

88
APPMAP_FORMAT_VERSION = '1.5.1'
9+
10+
DEFAULT_APPMAP_DIR = 'tmp/appmap'.freeze
911
end

spec/abstract_controller_base_spec.rb

+57-18
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
11
require 'rails_spec_helper'
22

33
describe 'Rails' do
4+
shared_context 'rails integration test setup' do
5+
def tmpdir
6+
'tmp/spec/AbstractControllerBase'
7+
end
8+
9+
unless use_existing_data?
10+
before(:all) do
11+
FileUtils.rm_rf tmpdir
12+
FileUtils.mkdir_p tmpdir
13+
run_spec 'spec/controllers/users_controller_spec.rb'
14+
run_spec 'spec/controllers/users_controller_api_spec.rb'
15+
end
16+
end
17+
18+
let(:appmap) { JSON.parse File.read File.join tmpdir, 'appmap/rspec', appmap_json_file }
19+
let(:appmap_json_path) { File.join(tmpdir, 'appmap/rspec', appmap_json_file) }
20+
let(:appmap) { JSON.parse File.read(appmap_json_path) }
21+
let(:events) { appmap['events'] }
22+
end
23+
424
%w[5 6].each do |rails_major_version| # rubocop:disable Metrics/BlockLength
525
context "#{rails_major_version}" do
626
include_context 'Rails app pg database', "spec/fixtures/rails#{rails_major_version}_users_app" unless use_existing_data?
27+
include_context 'rails integration test setup'
728

829
def run_spec(spec_name)
930
cmd = <<~CMD.gsub "\n", ' '
@@ -13,24 +34,6 @@ def run_spec(spec_name)
1334
run_cmd cmd, chdir: fixture_dir
1435
end
1536

16-
def tmpdir
17-
'tmp/spec/AbstractControllerBase'
18-
end
19-
20-
unless use_existing_data?
21-
before(:all) do
22-
FileUtils.rm_rf tmpdir
23-
FileUtils.mkdir_p tmpdir
24-
run_spec 'spec/controllers/users_controller_spec.rb'
25-
run_spec 'spec/controllers/users_controller_api_spec.rb'
26-
end
27-
end
28-
29-
let(:appmap) { JSON.parse File.read File.join tmpdir, 'appmap/rspec', appmap_json_file }
30-
let(:appmap_json_path) { File.join(tmpdir, 'appmap/rspec', appmap_json_file) }
31-
let(:appmap) { JSON.parse File.read(appmap_json_path) }
32-
let(:events) { appmap['events'] }
33-
3437
describe 'an API route' do
3538
describe 'creating an object' do
3639
let(:appmap_json_file) do
@@ -253,4 +256,40 @@ def tmpdir
253256
end
254257
end
255258
end
259+
260+
describe 'with default appmap.yml' do
261+
include_context 'Rails app pg database', "spec/fixtures/rails5_users_app" unless use_existing_data?
262+
include_context 'rails integration test setup'
263+
264+
def run_spec(spec_name)
265+
cmd = <<~CMD.gsub "\n", ' '
266+
docker-compose run --rm -e RAILS_ENV=test -e APPMAP=true -e APPMAP_CONFIG_FILE=no/such/file
267+
-v #{File.absolute_path tmpdir}:/app/tmp app ./bin/rspec #{spec_name}
268+
CMD
269+
run_cmd cmd, chdir: fixture_dir
270+
end
271+
272+
let(:appmap_json_file) do
273+
'Api_UsersController_POST_api_users_with_required_parameters_creates_a_user.appmap.json'
274+
end
275+
276+
it 'http_server_request is recorded' do
277+
expect(events).to include(
278+
hash_including(
279+
'http_server_request' => hash_including(
280+
'request_method' => 'POST',
281+
'path_info' => '/api/users'
282+
)
283+
)
284+
)
285+
end
286+
287+
it 'controller method is recorded' do
288+
expect(events).to include hash_including(
289+
'defined_class' => 'Api::UsersController',
290+
'method_id' => 'build_user',
291+
'path' => 'app/controllers/api/users_controller.rb',
292+
)
293+
end
294+
end
256295
end

spec/config_spec.rb

+21
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,25 @@
5555

5656
expect(config.to_h.deep_stringify_keys!).to eq(config_expectation)
5757
end
58+
59+
context do
60+
let(:warnings) { @warnings ||= [] }
61+
let(:warning) { warnings.join }
62+
before do
63+
expect(AppMap::Config).to receive(:warn).at_least(1) { |msg| warnings << msg }
64+
end
65+
it 'prints a warning and uses a default config' do
66+
config = AppMap::Config.load_from_file 'no/such/file'
67+
expect(config.to_h).to eq(YAML.load(<<~CONFIG))
68+
:name: appmap-ruby
69+
:packages:
70+
- :path: lib
71+
:handler_class: AppMap::Handler::Function
72+
:shallow: false
73+
:functions: []
74+
:exclude: []
75+
CONFIG
76+
expect(warning).to include('NOTICE: The AppMap config file no/such/file was not found!')
77+
end
78+
end
5879
end

spec/hook_spec.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def visit_NilClass(o)
2121
def invoke_test_file(file, setup: nil, &block)
2222
AppMap.configuration = nil
2323
package = AppMap::Config::Package.build_from_path(file)
24-
config = AppMap::Config.new('hook_spec', [ package ])
24+
config = AppMap::Config.new('hook_spec', packages: [ package ])
2525
AppMap.configuration = config
2626
tracer = nil
2727
AppMap::Hook.new(config).enable do
@@ -57,7 +57,7 @@ def test_hook_behavior(file, events_yaml, setup: nil, &block)
5757
it 'excludes named classes and methods' do
5858
load 'spec/fixtures/hook/exclude.rb'
5959
package = AppMap::Config::Package.build_from_path('spec/fixtures/hook/exclude.rb')
60-
config = AppMap::Config.new('hook_spec', [ package ], exclude: %w[ExcludeTest])
60+
config = AppMap::Config.new('hook_spec', packages: [ package ], exclude: %w[ExcludeTest])
6161
AppMap.configuration = config
6262

6363
expect(config.never_hook?(ExcludeTest, ExcludeTest.new.method(:instance_method))).to be_truthy

0 commit comments

Comments
 (0)