appmap 0.61.1 → 0.62.0

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