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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -16
  3. data/ARCHITECTURE.md +68 -0
  4. data/CHANGELOG.md +37 -0
  5. data/exe/appmap-agent-validate +19 -0
  6. data/exe/appmap-index +7 -0
  7. data/lib/appmap.rb +2 -0
  8. data/lib/appmap/agent.rb +0 -11
  9. data/lib/appmap/command/agent_setup/status.rb +1 -1
  10. data/lib/appmap/command/agent_setup/validate.rb +24 -0
  11. data/lib/appmap/command/index.rb +25 -0
  12. data/lib/appmap/command/inspect.rb +0 -1
  13. data/lib/appmap/config.rb +8 -1
  14. data/lib/appmap/depends.rb +2 -0
  15. data/lib/appmap/depends/api.rb +84 -0
  16. data/lib/appmap/depends/configuration.rb +59 -0
  17. data/lib/appmap/depends/node_cli.rb +44 -0
  18. data/lib/appmap/depends/rake_tasks.rb +58 -0
  19. data/lib/appmap/depends/test_file_inspector.rb +89 -0
  20. data/lib/appmap/depends/test_runner.rb +106 -0
  21. data/lib/appmap/depends/util.rb +34 -0
  22. data/lib/appmap/service/config_analyzer.rb +7 -8
  23. data/lib/appmap/service/integration_test_path_finder.rb +29 -25
  24. data/lib/appmap/service/test_command_provider.rb +5 -7
  25. data/lib/appmap/service/validator/config_validator.rb +89 -0
  26. data/lib/appmap/service/validator/violation.rb +50 -0
  27. data/lib/appmap/version.rb +4 -1
  28. data/package.json +1 -1
  29. data/spec/depends/api_spec.rb +184 -0
  30. data/spec/depends/spec_helper.rb +27 -0
  31. data/spec/fixtures/config/invalid_config.yml +2 -2
  32. data/spec/fixtures/config/invalid_yaml_config.yml +3 -0
  33. data/spec/fixtures/depends/.gitignore +2 -0
  34. data/spec/fixtures/depends/app/controllers/api/api_keys_controller.rb +2 -0
  35. data/spec/fixtures/depends/app/controllers/organizations_controller.rb +2 -0
  36. data/spec/fixtures/depends/app/models/api_key.rb +2 -0
  37. data/spec/fixtures/depends/app/models/configuration.rb +2 -0
  38. data/spec/fixtures/depends/app/models/show.rb +2 -0
  39. data/spec/fixtures/depends/app/models/user.rb +2 -0
  40. data/spec/fixtures/depends/revoke_api_key.appmap.json +901 -0
  41. data/spec/fixtures/depends/spec/actual_rspec_test.rb +7 -0
  42. data/spec/fixtures/depends/spec/api_spec.rb +2 -0
  43. data/spec/fixtures/depends/spec/user_spec.rb +2 -0
  44. data/spec/fixtures/depends/test/actual_minitest_test.rb +5 -0
  45. data/spec/fixtures/depends/user_page_scenario.appmap.json +1776 -0
  46. data/spec/fixtures/rails5_users_app/create_app +3 -3
  47. data/spec/fixtures/rails6_users_app/create_app +3 -3
  48. data/spec/fixtures/rails6_users_app/lib/tasks/appmap.rake +11 -1
  49. data/spec/service/config_analyzer_spec.rb +4 -4
  50. data/spec/service/integration_test_path_finder_spec.rb +24 -0
  51. data/spec/service/validator/violation_spec.rb +68 -0
  52. data/test/agent_setup_status_test.rb +8 -5
  53. data/test/agent_setup_validate_test.rb +75 -0
  54. data/test/test_helper.rb +3 -0
  55. data/yarn.lock +23 -9
  56. 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
- @config.to_h[:name] if present?
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
- present? && @config.to_h.key?(:name) && @config.to_h.key?(:packages)
23
+ config_validator.valid?
23
24
  end
24
25
 
25
26
  private
26
27
 
27
- def load_config
28
- AppMap::Config.load_from_file @config_file if present?
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
- class << self
9
- def find
10
- @paths ||= begin
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
- def count_paths
20
- find.flatten(2).length - 3
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
- private
22
+ def count_paths
23
+ find.flatten(2).length - 3
24
+ end
24
25
 
25
- def find_rspec_paths
26
- find_non_empty_paths(%w[spec/controllers spec/requests spec/integration spec/api spec/features spec/system])
27
- end
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
- def find_minitest_paths
31
- find_non_empty_paths(%w[test/controllers test/integration])
32
- end
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
- def find_cucumber_paths
35
- find_non_empty_paths(%w[features])
36
- end
39
+ def find_cucumber_paths
40
+ find_non_empty_paths(%w[features])
41
+ end
37
42
 
38
- def find_non_empty_paths(paths)
39
- paths.select { |path| Dir.exist?(path) && !Dir.empty?(path) }
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