appmap 0.59.2 → 0.62.0

Sign up to get free protection for your applications and to get access to all the features.
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