appmap 0.61.1 → 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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -16
  3. data/ARCHITECTURE.md +68 -0
  4. data/CHANGELOG.md +15 -0
  5. data/exe/appmap-index +7 -0
  6. data/lib/appmap.rb +2 -0
  7. data/lib/appmap/agent.rb +0 -11
  8. data/lib/appmap/command/index.rb +25 -0
  9. data/lib/appmap/command/inspect.rb +0 -1
  10. data/lib/appmap/config.rb +8 -1
  11. data/lib/appmap/depends.rb +2 -0
  12. data/lib/appmap/depends/api.rb +84 -0
  13. data/lib/appmap/depends/configuration.rb +59 -0
  14. data/lib/appmap/depends/node_cli.rb +44 -0
  15. data/lib/appmap/depends/rake_tasks.rb +58 -0
  16. data/lib/appmap/depends/test_file_inspector.rb +89 -0
  17. data/lib/appmap/depends/test_runner.rb +106 -0
  18. data/lib/appmap/depends/util.rb +34 -0
  19. data/lib/appmap/version.rb +1 -1
  20. data/package.json +1 -1
  21. data/spec/depends/api_spec.rb +184 -0
  22. data/spec/depends/spec_helper.rb +27 -0
  23. data/spec/fixtures/depends/.gitignore +2 -0
  24. data/spec/fixtures/depends/app/controllers/api/api_keys_controller.rb +2 -0
  25. data/spec/fixtures/depends/app/controllers/organizations_controller.rb +2 -0
  26. data/spec/fixtures/depends/app/models/api_key.rb +2 -0
  27. data/spec/fixtures/depends/app/models/configuration.rb +2 -0
  28. data/spec/fixtures/depends/app/models/show.rb +2 -0
  29. data/spec/fixtures/depends/app/models/user.rb +2 -0
  30. data/spec/fixtures/depends/revoke_api_key.appmap.json +901 -0
  31. data/spec/fixtures/depends/spec/actual_rspec_test.rb +7 -0
  32. data/spec/fixtures/depends/spec/api_spec.rb +2 -0
  33. data/spec/fixtures/depends/spec/user_spec.rb +2 -0
  34. data/spec/fixtures/depends/test/actual_minitest_test.rb +5 -0
  35. data/spec/fixtures/depends/user_page_scenario.appmap.json +1776 -0
  36. data/spec/fixtures/rails5_users_app/create_app +3 -3
  37. data/spec/fixtures/rails6_users_app/create_app +3 -3
  38. data/spec/fixtures/rails6_users_app/lib/tasks/appmap.rake +11 -1
  39. data/test/test_helper.rb +3 -0
  40. data/yarn.lock +23 -9
  41. metadata +29 -2
@@ -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
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.61.1'
6
+ VERSION = '0.62.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.5.1'
9
9
 
data/package.json CHANGED
@@ -17,6 +17,6 @@
17
17
  },
18
18
  "homepage": "https://github.com/applandinc/appmap-ruby#readme",
19
19
  "dependencies": {
20
- "@appland/cli": "^1.1.0"
20
+ "@appland/cli": "^1.3.2"
21
21
  }
22
22
  }
@@ -0,0 +1,184 @@
1
+ require_relative './spec_helper'
2
+ require 'appmap/depends/api'
3
+
4
+ module AppMap
5
+ module Depends
6
+ module APISpec
7
+ class << self
8
+ def minitest_environment_method
9
+ AppMap::Depends.test_env
10
+ end
11
+
12
+ def rspec_environment_method
13
+ AppMap::Depends.test_env
14
+ end
15
+
16
+ def minitest_test_command(test_files)
17
+ "time bundle exec ruby -rminitest -Itest #{test_files}"
18
+ end
19
+
20
+ alias minitest_test_command_method minitest_test_command
21
+
22
+ def rspec_test_command_method(test_files)
23
+ "time bundle exec rspec #{test_files}"
24
+ end
25
+
26
+ def rspec_select_tests_method(test_files)
27
+ AppMap::Depends.select_rspec_tests(test_files)
28
+ end
29
+
30
+ def minitest_select_tests_method(test_files)
31
+ AppMap::Depends.select_minitest_tests(test_files)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ describe 'Depends API' do
39
+ let(:api) { AppMap::Depends::API.new(ENV['DEBUG'] == 'true') }
40
+ let(:fixture_dir) { DEPENDS_TEST_DIR }
41
+
42
+ describe '.modified' do
43
+ it 'is empty by default' do
44
+ test_list = api.modified(appmap_dir: DEPENDS_TEST_DIR, base_dir: DEPENDS_BASE_DIR)
45
+ expect(test_list).to be_empty
46
+ end
47
+
48
+ it 'detects modification of a dependent file' do
49
+ FileUtils.touch 'spec/fixtures/depends/app/models/user.rb'
50
+ test_list = api.modified(appmap_dir: DEPENDS_TEST_DIR, base_dir: DEPENDS_BASE_DIR)
51
+ expect(test_list.to_a).to eq(%w[spec/fixtures/depends/spec/user_spec.rb])
52
+ end
53
+ end
54
+
55
+ describe '.inspect_test_files' do
56
+ it 'reports metadata, added, removed, changed, failed' do
57
+ test_report = api.inspect_test_files(appmap_dir: DEPENDS_TEST_DIR, test_file_patterns: %w[spec/fixtures/depends/spec/*_spec.rb])
58
+ expect(test_report.metadata_files).to eq(%w[spec/fixtures/depends/user_page_scenario/metadata.json spec/fixtures/depends/revoke_api_key/metadata.json])
59
+ expect(test_report.added).to be_empty
60
+ expect(test_report.removed).to be_empty
61
+ expect(test_report.changed).to be_empty
62
+ expect(test_report.failed.to_a).to eq(%w[spec/fixtures/depends/spec/user_spec.rb])
63
+ end
64
+ it 'detects an added test' do
65
+ FileUtils.touch 'spec/tmp/new_spec.rb'
66
+ test_report = api.inspect_test_files(appmap_dir: DEPENDS_TEST_DIR, test_file_patterns: %w[spec/fixtures/depends/spec/*_spec.rb spec/tmp/*_spec.rb])
67
+ expect(test_report.added.to_a).to eq(%w[spec/tmp/new_spec.rb])
68
+ end
69
+ it 'detects a removed test' do
70
+ FileUtils.mv 'spec/fixtures/depends/spec/user_spec.rb', 'spec/tmp/'
71
+ begin
72
+ test_report = api.inspect_test_files(appmap_dir: DEPENDS_TEST_DIR, test_file_patterns: %w[spec/fixtures/depends/spec/*_spec.rb spec/tmp/*_spec.rb])
73
+ expect(test_report.removed.to_a).to eq(%w[spec/fixtures/depends/spec/user_spec.rb])
74
+ ensure
75
+ FileUtils.mv 'spec/tmp/user_spec.rb', 'spec/fixtures/depends/spec/'
76
+ end
77
+ end
78
+ it 'detects a changed test' do
79
+ FileUtils.touch 'spec/fixtures/depends/spec/user_spec.rb'
80
+ test_report = api.inspect_test_files(appmap_dir: DEPENDS_TEST_DIR, test_file_patterns: %w[spec/fixtures/depends/spec/*_spec.rb])
81
+ expect(test_report.changed.to_a).to eq(%w[spec/fixtures/depends/spec/user_spec.rb])
82
+ end
83
+ it 'removes AppMaps whose source file has been removed' do
84
+ appmap = JSON.parse(File.read('spec/fixtures/depends/revoke_api_key.appmap.json'))
85
+ appmap['metadata']['source_location'] = 'spec/tmp/new_spec.rb'
86
+ new_spec_file = 'spec/fixtures/depends/revoke_api_key_2.appmap.json'
87
+ File.write new_spec_file, JSON.pretty_generate(appmap)
88
+
89
+ begin
90
+ update_appmap_index
91
+ test_report = api.inspect_test_files(appmap_dir: DEPENDS_TEST_DIR, test_file_patterns: %w[spec/fixtures/depends/spec/*_spec.rb])
92
+ expect(test_report.removed.to_a).to eq(%w[spec/tmp/new_spec.rb])
93
+
94
+ test_report.clean_appmaps
95
+
96
+ expect(File.exists?(new_spec_file)).to be_falsey
97
+ ensure
98
+ FileUtils.rm_f new_spec_file if File.exists?(new_spec_file)
99
+ FileUtils.rm_rf new_spec_file.split('.')[0]
100
+ end
101
+ end
102
+ end
103
+
104
+ describe '.run_tests' do
105
+ def run_tests
106
+ Dir.chdir 'spec/fixtures/depends' do
107
+ api.run_tests([ 'spec/actual_rspec_test.rb', 'test/actual_minitest_test.rb' ], appmap_dir: Pathname.new(DEPENDS_TEST_DIR).expand_path.to_s)
108
+ end
109
+ end
110
+
111
+ describe 'smoke test' do
112
+ around do |test|
113
+ @minitest_test_command_method = AppMap.configuration.depends_config.minitest_test_command_method
114
+ AppMap.configuration.depends_config.minitest_test_command_method = 'AppMap::Depends::APISpec.minitest_test_command'
115
+
116
+ test.call
117
+ ensure
118
+ AppMap.configuration.depends_config.minitest_test_command_method = @minitest_test_command
119
+ end
120
+
121
+ it 'passes a smoke test' do
122
+ run_tests
123
+ end
124
+ end
125
+
126
+ describe 'configuration settings' do
127
+ it 'can all be modified' do
128
+ defaults = {}
129
+
130
+ %i[rspec minitest].each do |framework|
131
+ %i[environment_method select_tests_method test_command_method].each do |setting|
132
+ full_setting = [ framework, setting ].join('_').to_sym
133
+ defaults[full_setting] = AppMap.configuration.depends_config.send(full_setting)
134
+ AppMap.configuration.depends_config.send("#{full_setting}=", "AppMap::Depends::APISpec.#{full_setting}")
135
+ end
136
+ end
137
+
138
+ run_tests
139
+ ensure
140
+ defaults.keys.each do |setting|
141
+ AppMap.configuration.depends_config.send("#{setting}=", defaults[setting])
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ describe '.remove_out_of_date_appmaps' do
148
+ it 'is a nop in normal circumstances' do
149
+ since = Time.now
150
+ removed = api.remove_out_of_date_appmaps(since, appmap_dir: DEPENDS_TEST_DIR, base_dir: DEPENDS_BASE_DIR)
151
+ expect(removed).to be_empty
152
+ end
153
+
154
+ it "removes an out-of-date AppMap that hasn't been brought up to date" do
155
+ # This AppMap will be modified before the 'since' time
156
+ appmap_path = "spec/fixtures/depends/user_page_scenario.appmap.json"
157
+ appmap = File.read(appmap_path)
158
+
159
+ sleep 0.01
160
+ since = Time.now
161
+ sleep 0.01
162
+
163
+ # Touch the rest of the AppMaps so that they are modified after +since+
164
+ Dir.glob('spec/fixtures/depends/*.appmap.json').each do |path|
165
+ next if path == appmap_path
166
+ FileUtils.touch path
167
+ end
168
+
169
+ sleep 0.01
170
+ # Make the AppMaps out of date
171
+ FileUtils.touch 'spec/fixtures/depends/app/models/user.rb'
172
+ sleep 0.01
173
+
174
+ begin
175
+ # At this point, we would run tests to bring the AppMaps up to date
176
+ # Then once the tests have finished, remove any AppMaps that weren't refreshed
177
+ removed = api.remove_out_of_date_appmaps(since, appmap_dir: DEPENDS_TEST_DIR, base_dir: DEPENDS_BASE_DIR)
178
+ expect(removed).to eq([ appmap_path.split('.')[0] ])
179
+ ensure
180
+ File.write(appmap_path, appmap)
181
+ end
182
+ end
183
+ end
184
+ end