Skip to content

Commit 7fa8159

Browse files
committed
feat: Provide default appmap.yml settings
If appmap.yml is not found, warn the user. Detect some possible source paths, and the project name from the .git file, and use this info to create a temporary appmap.yml. Print it to the console so the user can use it as a starting point.
1 parent 8a91636 commit 7fa8159

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)