appmap 0.59.2 → 0.62.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/.travis.yml +6 -16
- data/ARCHITECTURE.md +68 -0
- data/CHANGELOG.md +37 -0
- data/exe/appmap-agent-validate +19 -0
- data/exe/appmap-index +7 -0
- data/lib/appmap.rb +2 -0
- data/lib/appmap/agent.rb +0 -11
- data/lib/appmap/command/agent_setup/status.rb +1 -1
- data/lib/appmap/command/agent_setup/validate.rb +24 -0
- data/lib/appmap/command/index.rb +25 -0
- data/lib/appmap/command/inspect.rb +0 -1
- data/lib/appmap/config.rb +8 -1
- data/lib/appmap/depends.rb +2 -0
- data/lib/appmap/depends/api.rb +84 -0
- data/lib/appmap/depends/configuration.rb +59 -0
- data/lib/appmap/depends/node_cli.rb +44 -0
- data/lib/appmap/depends/rake_tasks.rb +58 -0
- data/lib/appmap/depends/test_file_inspector.rb +89 -0
- data/lib/appmap/depends/test_runner.rb +106 -0
- data/lib/appmap/depends/util.rb +34 -0
- data/lib/appmap/service/config_analyzer.rb +7 -8
- data/lib/appmap/service/integration_test_path_finder.rb +29 -25
- data/lib/appmap/service/test_command_provider.rb +5 -7
- data/lib/appmap/service/validator/config_validator.rb +89 -0
- data/lib/appmap/service/validator/violation.rb +50 -0
- data/lib/appmap/version.rb +4 -1
- data/package.json +1 -1
- data/spec/depends/api_spec.rb +184 -0
- data/spec/depends/spec_helper.rb +27 -0
- data/spec/fixtures/config/invalid_config.yml +2 -2
- data/spec/fixtures/config/invalid_yaml_config.yml +3 -0
- data/spec/fixtures/depends/.gitignore +2 -0
- data/spec/fixtures/depends/app/controllers/api/api_keys_controller.rb +2 -0
- data/spec/fixtures/depends/app/controllers/organizations_controller.rb +2 -0
- data/spec/fixtures/depends/app/models/api_key.rb +2 -0
- data/spec/fixtures/depends/app/models/configuration.rb +2 -0
- data/spec/fixtures/depends/app/models/show.rb +2 -0
- data/spec/fixtures/depends/app/models/user.rb +2 -0
- data/spec/fixtures/depends/revoke_api_key.appmap.json +901 -0
- data/spec/fixtures/depends/spec/actual_rspec_test.rb +7 -0
- data/spec/fixtures/depends/spec/api_spec.rb +2 -0
- data/spec/fixtures/depends/spec/user_spec.rb +2 -0
- data/spec/fixtures/depends/test/actual_minitest_test.rb +5 -0
- data/spec/fixtures/depends/user_page_scenario.appmap.json +1776 -0
- data/spec/fixtures/rails5_users_app/create_app +3 -3
- data/spec/fixtures/rails6_users_app/create_app +3 -3
- data/spec/fixtures/rails6_users_app/lib/tasks/appmap.rake +11 -1
- data/spec/service/config_analyzer_spec.rb +4 -4
- data/spec/service/integration_test_path_finder_spec.rb +24 -0
- data/spec/service/validator/violation_spec.rb +68 -0
- data/test/agent_setup_status_test.rb +8 -5
- data/test/agent_setup_validate_test.rb +75 -0
- data/test/test_helper.rb +3 -0
- data/yarn.lock +23 -9
- metadata +38 -2
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'shellwords'
|
4
|
+
require 'appmap/node_cli'
|
5
|
+
|
6
|
+
module AppMap
|
7
|
+
module Depends
|
8
|
+
# +Command+ wraps the Node +depends+ command.
|
9
|
+
class NodeCLI < ::AppMap::NodeCLI
|
10
|
+
# Directory name to prefix to the list of modified files which is provided to +depends+.
|
11
|
+
attr_accessor :base_dir
|
12
|
+
# AppMap field to report.
|
13
|
+
attr_accessor :field
|
14
|
+
|
15
|
+
def initialize(verbose:, appmap_dir:)
|
16
|
+
super(verbose: verbose, appmap_dir: appmap_dir)
|
17
|
+
|
18
|
+
@base_dir = nil
|
19
|
+
@field = 'source_location'
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the source_location field of every AppMap that is "out of date" with respect to one of the
|
23
|
+
# +modified_files+.
|
24
|
+
def depends(modified_files = nil)
|
25
|
+
index_appmaps
|
26
|
+
|
27
|
+
cmd = %w[depends]
|
28
|
+
cmd += [ '--field', field ] if field
|
29
|
+
cmd += [ '--appmap-dir', appmap_dir ] if appmap_dir
|
30
|
+
cmd += [ '--base-dir', base_dir ] if base_dir
|
31
|
+
|
32
|
+
options = {}
|
33
|
+
if modified_files
|
34
|
+
cmd << '--stdin-files'
|
35
|
+
options[:stdin_data] = modified_files.map(&:shellescape).join("\n")
|
36
|
+
warn "Checking modified files: #{modified_files.join(' ')}" if verbose
|
37
|
+
end
|
38
|
+
|
39
|
+
stdout, = command cmd, options
|
40
|
+
stdout.split("\n")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
require 'appmap/node_cli'
|
5
|
+
require_relative 'api'
|
6
|
+
|
7
|
+
module AppMap
|
8
|
+
module Depends
|
9
|
+
module RakeTasks
|
10
|
+
extend self
|
11
|
+
extend Rake::DSL
|
12
|
+
|
13
|
+
def depends_api
|
14
|
+
AppMap::Depends::API.new(Rake.verbose == true)
|
15
|
+
end
|
16
|
+
|
17
|
+
def configuration
|
18
|
+
AppMap.configuration
|
19
|
+
end
|
20
|
+
|
21
|
+
def define_tasks
|
22
|
+
namespace :depends do
|
23
|
+
task :modified do
|
24
|
+
@appmap_modified_files = depends_api.modified(appmap_dir: configuration.appmap_dir, base_dir: configuration.depends_config.base_dir)
|
25
|
+
depends_api.report_list 'Out of date', @appmap_modified_files
|
26
|
+
end
|
27
|
+
|
28
|
+
task :test_file_report do
|
29
|
+
@appmap_test_file_report = depends_api.inspect_test_files(appmap_dir: configuration.appmap_dir, test_file_patterns: configuration.depends_config.test_file_patterns)
|
30
|
+
@appmap_test_file_report.report
|
31
|
+
end
|
32
|
+
|
33
|
+
task :run_tests do
|
34
|
+
if @appmap_test_file_report
|
35
|
+
@appmap_test_file_report.clean_appmaps
|
36
|
+
@appmap_modified_files += @appmap_test_file_report.modified_files
|
37
|
+
end
|
38
|
+
|
39
|
+
if @appmap_modified_files.empty?
|
40
|
+
warn 'AppMaps are up to date'
|
41
|
+
next
|
42
|
+
end
|
43
|
+
|
44
|
+
start_time = Time.current
|
45
|
+
depends_api.run_tests(@appmap_modified_files, appmap_dir: configuration.appmap_dir)
|
46
|
+
|
47
|
+
warn "Tests succeeded - removing out of date AppMaps."
|
48
|
+
removed = depends_api.remove_out_of_date_appmaps(start_time, appmap_dir: configuration.appmap_dir, base_dir: configuration.depends_config.base_dir)
|
49
|
+
warn "Removed out of date AppMaps: #{removed.join(' ')}" unless removed.empty?
|
50
|
+
end
|
51
|
+
|
52
|
+
desc configuration.depends_config.description
|
53
|
+
task :update => [ :modified, :test_file_report, :run_tests ] + configuration.depends_config.dependent_tasks
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module AppMap
|
6
|
+
module Depends
|
7
|
+
class TestFileInspector
|
8
|
+
TestReport = Struct.new(:metadata_files, :added, :removed, :changed, :failed) do
|
9
|
+
private_methods :metadata_files
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
report = []
|
13
|
+
report << "Added test files : #{added.to_a.join(' ')}" unless added.empty?
|
14
|
+
report << "Removed test files : #{removed.to_a.join(' ')}" unless removed.empty?
|
15
|
+
report << "Changed test files : #{changed.to_a.join(' ')}" unless changed.empty?
|
16
|
+
report << "Failed test files : #{failed.to_a.join(' ')}" unless failed.empty?
|
17
|
+
report.compact.join("\n")
|
18
|
+
end
|
19
|
+
|
20
|
+
def report
|
21
|
+
warn to_s unless empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
def empty?
|
25
|
+
[ added, removed, changed, failed ].all?(&:empty?)
|
26
|
+
end
|
27
|
+
|
28
|
+
def modified_files
|
29
|
+
added + changed + failed
|
30
|
+
end
|
31
|
+
|
32
|
+
# Delete AppMaps which depend on test cases that have been deleted.
|
33
|
+
def clean_appmaps
|
34
|
+
return if removed.empty?
|
35
|
+
|
36
|
+
count = metadata_files.each_with_object(0) do |metadata_file, count|
|
37
|
+
metadata = JSON.parse(File.read(metadata_file))
|
38
|
+
source_location = Util.normalize_path(metadata['source_location'])
|
39
|
+
appmap_path = File.join(metadata_file.split('/')[0...-1])
|
40
|
+
|
41
|
+
if source_location && removed.member?(source_location)
|
42
|
+
Util.delete_appmap(appmap_path)
|
43
|
+
count += 1
|
44
|
+
end
|
45
|
+
end
|
46
|
+
count
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
attr_reader :test_dir
|
51
|
+
attr_reader :test_file_patterns
|
52
|
+
|
53
|
+
def initialize(test_dir, test_file_patterns)
|
54
|
+
@test_dir = test_dir
|
55
|
+
@test_file_patterns = test_file_patterns
|
56
|
+
end
|
57
|
+
|
58
|
+
def report
|
59
|
+
metadata_files = Dir.glob(File.join(test_dir, '**', 'metadata.json'))
|
60
|
+
source_locations = Set.new
|
61
|
+
changed_test_files = Set.new
|
62
|
+
failed_test_files = Set.new
|
63
|
+
metadata_files.each do |metadata_file|
|
64
|
+
metadata = JSON.parse(File.read(metadata_file))
|
65
|
+
appmap_path = File.join(metadata_file.split('/')[0...-1])
|
66
|
+
|
67
|
+
appmap_mtime = File.read(File.join(appmap_path, 'mtime')).to_i
|
68
|
+
source_location = Util.normalize_path(metadata['source_location'])
|
69
|
+
test_status = metadata['test_status']
|
70
|
+
next unless source_location && test_status
|
71
|
+
|
72
|
+
source_location_mtime = (File.stat(source_location).mtime.to_f * 1000).to_i rescue nil
|
73
|
+
source_locations << source_location
|
74
|
+
if source_location_mtime
|
75
|
+
changed_test_files << source_location if source_location_mtime > appmap_mtime
|
76
|
+
failed_test_files << source_location unless test_status == 'succeeded'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
test_files = Set.new(test_file_patterns.map(&Dir.method(:glob)).flatten)
|
81
|
+
added_test_files = test_files - source_locations
|
82
|
+
changed_test_files -= added_test_files
|
83
|
+
removed_test_files = source_locations - test_files
|
84
|
+
|
85
|
+
TestReport.new(metadata_files, added_test_files, removed_test_files, changed_test_files, failed_test_files)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "shellwords"
|
4
|
+
|
5
|
+
module AppMap
|
6
|
+
module Depends
|
7
|
+
class << self
|
8
|
+
def select_rspec_tests(test_files)
|
9
|
+
select_tests_by_directory(test_files, 'spec')
|
10
|
+
end
|
11
|
+
|
12
|
+
def select_minitest_tests(test_files)
|
13
|
+
select_tests_by_directory(test_files, 'test')
|
14
|
+
end
|
15
|
+
|
16
|
+
def rspec_test_command(test_files)
|
17
|
+
"bundle exec rspec --format documentation -t '~empty' -t '~large' -t '~unstable' #{test_files}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def minitest_test_command(test_files)
|
21
|
+
"bundle exec rails test #{test_files}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def select_tests_by_directory(test_files, dir)
|
25
|
+
test_files
|
26
|
+
.map(&method(:simplify_path))
|
27
|
+
.uniq
|
28
|
+
.select { |path| path.split('/').first == dir }
|
29
|
+
end
|
30
|
+
|
31
|
+
def normalize_test_files(test_files)
|
32
|
+
test_files
|
33
|
+
.map(&method(:simplify_path))
|
34
|
+
.uniq
|
35
|
+
.map(&:shellescape).join(' ')
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_env
|
39
|
+
# DISABLE_SPRING because it's likely to not have APPMAP=true
|
40
|
+
{ 'RAILS_ENV' => 'test', 'APPMAP' => 'true', 'DISABLE_SPRING' => '1' }
|
41
|
+
end
|
42
|
+
|
43
|
+
def simplify_path(file)
|
44
|
+
file.index(Dir.pwd) == 0 ? file[Dir.pwd.length+1..-1] : file
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class TestRunner
|
49
|
+
def initialize(test_files)
|
50
|
+
@test_files = test_files
|
51
|
+
end
|
52
|
+
|
53
|
+
def run
|
54
|
+
%i[rspec minitest].each do |framework|
|
55
|
+
run_tests select_tests_fn(framework), build_environment_fn(framework), test_command_fn(framework)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def build_environment_fn(framework)
|
60
|
+
lookup_method("#{framework}_environment_method") do |method|
|
61
|
+
lambda do
|
62
|
+
method.call
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def select_tests_fn(framework)
|
68
|
+
lookup_method("#{framework}_select_tests_method") do |method|
|
69
|
+
lambda do |test_files|
|
70
|
+
method.call(test_files)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_command_fn(framework)
|
76
|
+
lookup_method("#{framework}_test_command_method") do |method|
|
77
|
+
lambda do |test_files|
|
78
|
+
method.call(test_files)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
|
85
|
+
def lookup_method(setting_name, &block)
|
86
|
+
method_name = AppMap.configuration.depends_config.send(setting_name)
|
87
|
+
method_tokens = method_name.split(/\:\:|\./)
|
88
|
+
cls = Object
|
89
|
+
while method_tokens.size > 1
|
90
|
+
cls = cls.const_get(method_tokens.shift)
|
91
|
+
end
|
92
|
+
cls.public_method(method_tokens.first)
|
93
|
+
end
|
94
|
+
|
95
|
+
def run_tests(select_tests_fn, env_fn, test_command_fn)
|
96
|
+
test_files = select_tests_fn.(@test_files)
|
97
|
+
return if test_files.empty?
|
98
|
+
|
99
|
+
test_files = Depends.normalize_test_files(test_files)
|
100
|
+
command = test_command_fn.(test_files)
|
101
|
+
succeeded = system(env_fn.(), command)
|
102
|
+
raise %Q|Command failed: #{command}| unless succeeded
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module AppMap
|
2
|
+
module Depends
|
3
|
+
module Util
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def normalize_path(path, pwd: Dir.pwd)
|
7
|
+
normalize_path_fn(pwd).(path)
|
8
|
+
end
|
9
|
+
|
10
|
+
def normalize_paths(paths, pwd: Dir.pwd)
|
11
|
+
paths.map(&normalize_path_fn(pwd))
|
12
|
+
end
|
13
|
+
|
14
|
+
def delete_appmap(appmap_path)
|
15
|
+
FileUtils.rm_rf(appmap_path)
|
16
|
+
appmap_file_path = [ appmap_path, 'appmap.json' ].join('.')
|
17
|
+
File.unlink(appmap_file_path) if File.exists?(appmap_file_path)
|
18
|
+
rescue
|
19
|
+
warn "Unable to delete AppMap: #{$!}"
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def normalize_path_fn(pwd)
|
25
|
+
lambda do |path|
|
26
|
+
next path if AppMap::Util.blank?(path)
|
27
|
+
|
28
|
+
path = path[pwd.length + 1..-1] if path.index(pwd) == 0
|
29
|
+
path.split(':')[0]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'appmap/service/validator/config_validator'
|
4
|
+
|
3
5
|
module AppMap
|
4
6
|
module Service
|
5
7
|
class ConfigAnalyzer
|
@@ -7,11 +9,10 @@ module AppMap
|
|
7
9
|
|
8
10
|
def initialize(config_file)
|
9
11
|
@config_file = config_file
|
10
|
-
@config = load_config
|
11
12
|
end
|
12
13
|
|
13
14
|
def app_name
|
14
|
-
|
15
|
+
config_validator.config.to_h['name'] if present?
|
15
16
|
end
|
16
17
|
|
17
18
|
def present?
|
@@ -19,16 +20,14 @@ module AppMap
|
|
19
20
|
end
|
20
21
|
|
21
22
|
def valid?
|
22
|
-
|
23
|
+
config_validator.valid?
|
23
24
|
end
|
24
25
|
|
25
26
|
private
|
26
27
|
|
27
|
-
def
|
28
|
-
AppMap::
|
29
|
-
rescue
|
30
|
-
nil
|
28
|
+
def config_validator
|
29
|
+
@validator ||= AppMap::Service::Validator::ConfigValidator.new(@config_file)
|
31
30
|
end
|
32
31
|
end
|
33
32
|
end
|
34
|
-
end
|
33
|
+
end
|
@@ -5,39 +5,43 @@ require 'appmap/service/test_framework_detector'
|
|
5
5
|
module AppMap
|
6
6
|
module Service
|
7
7
|
class IntegrationTestPathFinder
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
paths = { rspec: [], minitest: [], cucumber: [] }
|
12
|
-
paths[:rspec] = find_rspec_paths if TestFrameworkDetector.rspec_present?
|
13
|
-
paths[:minitest] = find_minitest_paths if TestFrameworkDetector.minitest_present?
|
14
|
-
paths[:cucumber] = find_cucumber_paths if TestFrameworkDetector.cucumber_present?
|
15
|
-
paths
|
16
|
-
end
|
17
|
-
end
|
8
|
+
def initialize(base_path = '')
|
9
|
+
@base_path = base_path
|
10
|
+
end
|
18
11
|
|
19
|
-
|
20
|
-
|
12
|
+
def find
|
13
|
+
@paths ||= begin
|
14
|
+
paths = { rspec: [], minitest: [], cucumber: [] }
|
15
|
+
paths[:rspec] = find_rspec_paths if TestFrameworkDetector.rspec_present?
|
16
|
+
paths[:minitest] = find_minitest_paths if TestFrameworkDetector.minitest_present?
|
17
|
+
paths[:cucumber] = find_cucumber_paths if TestFrameworkDetector.cucumber_present?
|
18
|
+
paths
|
21
19
|
end
|
20
|
+
end
|
22
21
|
|
23
|
-
|
22
|
+
def count_paths
|
23
|
+
find.flatten(2).length - 3
|
24
|
+
end
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
26
|
+
private
|
27
|
+
|
28
|
+
def find_rspec_paths
|
29
|
+
find_non_empty_paths(%w[spec/controllers spec/requests spec/integration spec/api spec/features spec/system])
|
30
|
+
end
|
28
31
|
|
29
32
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
+
def find_minitest_paths
|
34
|
+
top_level_paths = %w[test/controllers test/integration]
|
35
|
+
children_paths = Dir.glob('test/**/{controllers,integration}')
|
36
|
+
find_non_empty_paths((top_level_paths + children_paths).uniq).sort
|
37
|
+
end
|
33
38
|
|
34
|
-
|
35
|
-
|
36
|
-
|
39
|
+
def find_cucumber_paths
|
40
|
+
find_non_empty_paths(%w[features])
|
41
|
+
end
|
37
42
|
|
38
|
-
|
39
|
-
|
40
|
-
end
|
43
|
+
def find_non_empty_paths(paths)
|
44
|
+
paths.select { |path| Dir.exist?(@base_path + path) && !Dir.empty?(@base_path + path) }
|
41
45
|
end
|
42
46
|
end
|
43
47
|
end
|