appmap 0.25.0 → 0.28.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -2
  3. data/CHANGELOG.md +33 -0
  4. data/README.md +123 -39
  5. data/exe/appmap +3 -57
  6. data/lib/appmap.rb +51 -32
  7. data/lib/appmap/command/record.rb +2 -61
  8. data/lib/appmap/cucumber.rb +89 -0
  9. data/lib/appmap/event.rb +1 -1
  10. data/lib/appmap/hook.rb +7 -1
  11. data/lib/appmap/metadata.rb +62 -0
  12. data/lib/appmap/middleware/remote_recording.rb +2 -7
  13. data/lib/appmap/rails/sql_handler.rb +0 -5
  14. data/lib/appmap/railtie.rb +2 -2
  15. data/lib/appmap/rspec.rb +20 -38
  16. data/lib/appmap/trace.rb +9 -9
  17. data/lib/appmap/util.rb +40 -0
  18. data/lib/appmap/version.rb +1 -1
  19. data/spec/fixtures/rails_users_app/Gemfile +1 -0
  20. data/spec/fixtures/rails_users_app/features/api_users.feature +13 -0
  21. data/spec/fixtures/rails_users_app/features/support/env.rb +4 -0
  22. data/spec/fixtures/rails_users_app/features/support/hooks.rb +11 -0
  23. data/spec/fixtures/rails_users_app/features/support/steps.rb +18 -0
  24. data/spec/hook_spec.rb +21 -3
  25. data/spec/rails_spec_helper.rb +2 -0
  26. data/spec/rspec_feature_metadata_spec.rb +2 -0
  27. data/spec/spec_helper.rb +8 -0
  28. data/spec/util_spec.rb +21 -0
  29. data/test/cli_test.rb +0 -13
  30. data/test/cucumber_test.rb +72 -0
  31. data/test/fixtures/cucumber4_recorder/Gemfile +5 -0
  32. data/test/fixtures/cucumber4_recorder/appmap.yml +3 -0
  33. data/test/fixtures/cucumber4_recorder/features/say_hello.feature +5 -0
  34. data/test/fixtures/cucumber4_recorder/features/support/env.rb +5 -0
  35. data/test/fixtures/cucumber4_recorder/features/support/hooks.rb +11 -0
  36. data/test/fixtures/cucumber4_recorder/features/support/steps.rb +9 -0
  37. data/test/fixtures/cucumber4_recorder/lib/hello.rb +7 -0
  38. data/test/fixtures/cucumber_recorder/Gemfile +5 -0
  39. data/test/fixtures/cucumber_recorder/appmap.yml +3 -0
  40. data/test/fixtures/cucumber_recorder/features/say_hello.feature +5 -0
  41. data/test/fixtures/cucumber_recorder/features/support/env.rb +5 -0
  42. data/test/fixtures/cucumber_recorder/features/support/hooks.rb +11 -0
  43. data/test/fixtures/cucumber_recorder/features/support/steps.rb +9 -0
  44. data/test/fixtures/cucumber_recorder/lib/hello.rb +7 -0
  45. data/test/fixtures/rspec_recorder/Gemfile +1 -1
  46. data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +12 -0
  47. data/test/rspec_test.rb +5 -0
  48. metadata +26 -3
  49. data/lib/appmap/command/upload.rb +0 -101
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppMap
4
+ module Util
5
+ class << self
6
+ # scenario_filename builds a suitable file name from a scenario name.
7
+ # Special characters are removed, and the file name is truncated to fit within
8
+ # shell limitations.
9
+ def scenario_filename(name, max_length: 255, separator: '_', extension: '.appmap.json')
10
+ # Cribbed from v5 version of ActiveSupport:Inflector#parameterize:
11
+ # https://github.com/rails/rails/blob/v5.2.4/activesupport/lib/active_support/inflector/transliterate.rb#L92
12
+ # Replace accented chars with their ASCII equivalents.
13
+
14
+ fname = name.encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
15
+
16
+ # Turn unwanted chars into the separator.
17
+ fname.gsub!(/[^a-z0-9\-_]+/i, separator)
18
+
19
+ re_sep = Regexp.escape(separator)
20
+ re_duplicate_separator = /#{re_sep}{2,}/
21
+ re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i
22
+
23
+ # No more than one of the separator in a row.
24
+ fname.gsub!(re_duplicate_separator, separator)
25
+
26
+ # Finally, Remove leading/trailing separator.
27
+ fname.gsub!(re_leading_trailing_separator, '')
28
+
29
+ if (fname.length + extension.length) > max_length
30
+ require 'base64'
31
+ require 'digest'
32
+ fname_digest = Base64.urlsafe_encode64 Digest::MD5.digest(fname), padding: false
33
+ fname[max_length - fname_digest.length - extension.length - 1..-1] = [ '-', fname_digest ].join
34
+ end
35
+
36
+ [ fname, extension ].join
37
+ end
38
+ end
39
+ end
40
+ end
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.25.0'
6
+ VERSION = '0.28.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.2'
9
9
  end
@@ -39,6 +39,7 @@ appmap_options = \
39
39
  gem 'appmap', appmap_options
40
40
 
41
41
  group :development, :test do
42
+ gem 'cucumber-rails', require: false
42
43
  gem 'rspec-rails'
43
44
  # Required for Sequel, since without ActiveRecord, the Rails transactional fixture support
44
45
  # isn't activated.
@@ -0,0 +1,13 @@
1
+ Feature: /api/users
2
+
3
+ @appmap-disable
4
+ Scenario: A user can be created
5
+ When I create a user
6
+ Then the response status should be 201
7
+
8
+ Scenario: When a user is created, it should be in the user list
9
+ Given I create a user
10
+ And the response status should be 201
11
+ When I list the users
12
+ Then the response status should be 200
13
+ And the response should include the user
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cucumber/rails'
4
+ require 'appmap/cucumber'
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ if AppMap::Cucumber.enabled?
4
+ Around('not @appmap-disable') do |scenario, block|
5
+ appmap = AppMap.record do
6
+ block.call
7
+ end
8
+
9
+ AppMap::Cucumber.write_scenario(scenario, appmap)
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ When 'I create a user' do
4
+ @response = post '/api/users', login: 'alice'
5
+ end
6
+
7
+ Then(/the response status should be (\d+)/) do |status|
8
+ expect(@response.status).to eq(status.to_i)
9
+ end
10
+
11
+ When 'I list the users' do
12
+ @response = get '/api/users'
13
+ @users = JSON.parse(@response.body)
14
+ end
15
+
16
+ Then 'the response should include the user' do
17
+ expect(@users.map { |u| u['login'] }).to include('alice')
18
+ end
@@ -31,8 +31,10 @@ describe 'AppMap class Hooking' do
31
31
  end
32
32
 
33
33
  def invoke_test_file(file, &block)
34
+ AppMap.configuration = nil
34
35
  package = AppMap::Hook::Package.new(file, [])
35
36
  config = AppMap::Hook::Config.new('hook_spec', [ package ])
37
+ AppMap.configuration = config
36
38
  AppMap::Hook.hook(config)
37
39
 
38
40
  tracer = AppMap.tracing.trace
@@ -55,6 +57,10 @@ describe 'AppMap class Hooking' do
55
57
  [ config, tracer ]
56
58
  end
57
59
 
60
+ after do
61
+ AppMap.configuration = nil
62
+ end
63
+
58
64
  it 'hooks an instance method that takes no arguments' do
59
65
  events_yaml = <<~YAML
60
66
  ---
@@ -90,10 +96,10 @@ describe 'AppMap class Hooking' do
90
96
  end
91
97
 
92
98
  it 'builds a class map of invoked methods' do
93
- config, tracer = invoke_test_file 'spec/fixtures/hook/instance_method.rb' do
99
+ _, tracer = invoke_test_file 'spec/fixtures/hook/instance_method.rb' do
94
100
  InstanceMethod.new.say_default
95
101
  end
96
- class_map = AppMap.class_map(config, tracer.event_methods).to_yaml
102
+ class_map = AppMap.class_map(tracer.event_methods).to_yaml
97
103
  expect(Diffy::Diff.new(class_map, <<~YAML).to_s).to eq('')
98
104
  ---
99
105
  - :name: spec/fixtures/hook/instance_method.rb
@@ -351,7 +357,19 @@ describe 'AppMap class Hooking' do
351
357
  :lineno: 9
352
358
  YAML
353
359
  test_hook_behavior 'spec/fixtures/hook/exception_method.rb', events_yaml do
354
- ExceptionMethod.new.raise_exception
360
+ begin
361
+ ExceptionMethod.new.raise_exception
362
+ rescue
363
+ # don't let the exception fail the test
364
+ end
365
+ end
366
+ end
367
+
368
+ it 're-raises exceptions' do
369
+ RSpec::Expectations.configuration.on_potential_false_positives = :nothing
370
+
371
+ invoke_test_file 'spec/fixtures/hook/exception_method.rb' do
372
+ expect { ExceptionMethod.new.raise_exception }.to raise_exception
355
373
  end
356
374
  end
357
375
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'open3'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rails_spec_helper'
2
4
 
3
5
  describe 'RSpec feature and feature group metadata' do
@@ -4,4 +4,12 @@ require 'json'
4
4
  require 'yaml'
5
5
  require 'English'
6
6
  require 'webdrivers/chromedriver'
7
+
8
+ # Disable default initialization of AppMap
9
+ ENV['APPMAP_INITIALIZE'] = 'false'
10
+
7
11
  require 'appmap'
12
+
13
+ RSpec.configure do |config|
14
+ config.example_status_persistence_file_path = "tmp/rspec_failed_examples.txt"
15
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'appmap/util'
5
+
6
+ describe AppMap::Util do
7
+ let(:subject) { AppMap::Util.method(:scenario_filename) }
8
+ describe 'scenario_filename' do
9
+ it 'leaves short names alone' do
10
+ expect(subject.call('foobar')).to eq('foobar.appmap.json')
11
+ end
12
+ it 'has a customizable suffix' do
13
+ expect(subject.call('foobar', extension: '.json')).to eq('foobar.json')
14
+ end
15
+ it 'limits the filename length' do
16
+ fname = (0...104).map { |i| ((i % 26) + 97).chr }.join
17
+
18
+ expect(subject.call(fname, max_length: 50)).to eq('abcdefghijklmno-RAd_SFbH1sUZ_OXfwPsfzw.appmap.json')
19
+ end
20
+ end
21
+ end
@@ -113,17 +113,4 @@ class CLITest < Minitest::Test
113
113
  assert_includes output, %("location":"lib/cli_record_test/main.rb:3")
114
114
  assert !File.file?(OUTPUT_FILENAME), "#{OUTPUT_FILENAME} should not exist"
115
115
  end
116
-
117
- def test_upload
118
- Dir.chdir 'test/fixtures/cli_record_test' do
119
- `#{File.expand_path '../exe/appmap', __dir__} record -o #{OUTPUT_FILENAME} ./lib/cli_record_test/main.rb`
120
- end
121
-
122
- upload_output = `./exe/appmap upload --no-open #{OUTPUT_FILENAME}`
123
- assert_equal 0, $CHILD_STATUS.exitstatus
124
- # Example: 93e1e07d-4b39-49ac-82bf-27d63e296cae
125
- assert_match(/Scenario Id/, upload_output)
126
- assert_match(/Batch Id/, upload_output)
127
- assert_match(/[0-9a-f]+\-[0-9a-f\-]+/, upload_output)
128
- end
129
116
  end
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'test_helper'
5
+ require 'English'
6
+
7
+ class CucumberTest < Minitest::Test
8
+ def perform_test(dir)
9
+ Bundler.with_clean_env do
10
+ Dir.chdir "test/fixtures/#{dir}" do
11
+ FileUtils.rm_rf 'tmp'
12
+ system 'bundle config --local local.appmap ../../..'
13
+ system 'bundle'
14
+ system({ 'APPMAP' => 'true' }, %(bundle exec cucumber))
15
+
16
+ yield
17
+ end
18
+ end
19
+ end
20
+
21
+ def test_cucumber
22
+ perform_test 'cucumber_recorder' do
23
+ appmap_file = 'tmp/appmap/cucumber/Say_hello.appmap.json'
24
+
25
+ assert File.file?(appmap_file),
26
+ %(appmap output file does not exist in #{Dir.new('tmp/appmap/cucumber').entries.join(', ')})
27
+ appmap = JSON.parse(File.read(appmap_file))
28
+ assert_equal AppMap::APPMAP_FORMAT_VERSION, appmap['version']
29
+ assert_includes appmap.keys, 'metadata'
30
+ metadata = appmap['metadata']
31
+
32
+ assert_equal 'say_hello', metadata['feature_group']
33
+ assert_equal 'I can say hello', metadata['feature']
34
+ assert_equal 'Say hello', metadata['name']
35
+ assert_includes metadata.keys, 'client'
36
+ assert_equal({ name: 'appmap', url: AppMap::URL, version: AppMap::VERSION }.stringify_keys, metadata['client'])
37
+ assert_includes metadata.keys, 'recorder'
38
+ assert_equal({ name: 'cucumber' }.stringify_keys, metadata['recorder'])
39
+
40
+ assert_includes metadata.keys, 'frameworks'
41
+ cucumber = metadata['frameworks'].select {|f| f['name'] == 'cucumber'}
42
+ assert_equal 1, cucumber.count
43
+ end
44
+ end
45
+
46
+ def test_cucumber4
47
+ perform_test 'cucumber4_recorder' do
48
+ appmap_file = 'tmp/appmap/cucumber/Say_hello.appmap.json'
49
+
50
+ assert File.file?(appmap_file),
51
+ %(appmap output file does not exist in #{Dir.new('tmp/appmap/cucumber').entries.join(', ')})
52
+ appmap = JSON.parse(File.read(appmap_file))
53
+ assert_equal AppMap::APPMAP_FORMAT_VERSION, appmap['version']
54
+ assert_includes appmap.keys, 'metadata'
55
+ metadata = appmap['metadata']
56
+
57
+ assert_equal 'say_hello', metadata['feature_group']
58
+ # In cucumber4, there's no access to the feature name from within the executing scenario
59
+ # (as far as I can tell).
60
+ assert_equal 'Say hello', metadata['feature']
61
+ assert_equal 'Say hello', metadata['name']
62
+ assert_includes metadata.keys, 'client'
63
+ assert_equal({ name: 'appmap', url: AppMap::URL, version: AppMap::VERSION }.stringify_keys, metadata['client'])
64
+ assert_includes metadata.keys, 'recorder'
65
+ assert_equal({ name: 'cucumber' }.stringify_keys, metadata['recorder'])
66
+
67
+ assert_includes metadata.keys, 'frameworks'
68
+ cucumber = metadata['frameworks'].select {|f| f['name'] == 'cucumber'}
69
+ assert_equal 1, cucumber.count
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'appmap', git: 'applandinc/appmap-ruby', branch: `git rev-parse --abbrev-ref HEAD`.strip
4
+ gem 'byebug'
5
+ gem 'cucumber', '>= 4'
@@ -0,0 +1,3 @@
1
+ name: cucumber_recorder
2
+ packages:
3
+ - path: lib
@@ -0,0 +1,5 @@
1
+ Feature: I can say hello
2
+
3
+ Scenario: Say hello
4
+ When I say hello
5
+ Then the message is hello
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cucumber'
4
+ require 'appmap/cucumber'
5
+ require File.join(__dir__, '../../lib/hello')
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appmap'
4
+
5
+ Around('not @appmap-disable') do |scenario, block|
6
+ appmap = AppMap.record do
7
+ block.call
8
+ end
9
+
10
+ AppMap::Cucumber.write_scenario(scenario, appmap)
11
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ When('I say hello') do
4
+ @message = Hello.new.say_hello
5
+ end
6
+
7
+ Then('the message is hello') do
8
+ raise 'Wrong message!' unless @message == 'Hello!'
9
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Hello
4
+ def say_hello
5
+ 'Hello!'
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'appmap', git: 'applandinc/appmap-ruby', branch: `git rev-parse --abbrev-ref HEAD`.strip
4
+ gem 'byebug'
5
+ gem 'cucumber', '< 4'
@@ -0,0 +1,3 @@
1
+ name: cucumber_recorder
2
+ packages:
3
+ - path: lib
@@ -0,0 +1,5 @@
1
+ Feature: I can say hello
2
+
3
+ Scenario: Say hello
4
+ When I say hello
5
+ Then the message is hello
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cucumber'
4
+ require 'appmap/cucumber'
5
+ require File.join(__dir__, '../../lib/hello')
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appmap'
4
+
5
+ Around('not @appmap-disable') do |scenario, block|
6
+ appmap = AppMap.record do
7
+ block.call
8
+ end
9
+
10
+ AppMap::Cucumber.write_scenario(scenario, appmap)
11
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ When('I say hello') do
4
+ @message = Hello.new.say_hello
5
+ end
6
+
7
+ Then('the message is hello') do
8
+ raise 'Wrong message!' unless @message == 'Hello!'
9
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Hello
4
+ def say_hello
5
+ 'Hello!'
6
+ end
7
+ end
@@ -1,5 +1,5 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'appmap', git: '../../..', branch: `git rev-parse --abbrev-ref HEAD`.strip
3
+ gem 'appmap', git: 'applandinc/appmap-ruby', branch: `git rev-parse --abbrev-ref HEAD`.strip
4
4
  gem 'byebug'
5
5
  gem 'rspec'
@@ -3,6 +3,18 @@ require 'appmap/rspec'
3
3
  require 'hello'
4
4
 
5
5
  describe Hello, feature_group: 'Saying hello' do
6
+ before do
7
+ # Trick appmap-ruby into thinking we're a Rails app.
8
+ stub_const('Rails', double('rails', version: 'fake.0'))
9
+ end
10
+
11
+ # The order of these examples is important. The tests check the
12
+ # appmap for 'says hello', and we want another example to get run
13
+ # before it.
14
+ it 'does not say goodbye', feature: 'Speak hello', appmap: true do
15
+ expect(Hello.new.say_hello).not_to eq('Goodbye!')
16
+ end
17
+
6
18
  it 'says hello', feature: 'Speak hello', appmap: true do
7
19
  expect(Hello.new.say_hello).to eq('Hello!')
8
20
  end