flutter 0.1.0.pre.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -5
  3. data/Gemfile +1 -0
  4. data/README.md +62 -2
  5. data/Rakefile +35 -1
  6. data/codecov.yml +13 -0
  7. data/lib/flutter/minitest.rb +6 -8
  8. data/lib/flutter/parser.rb +18 -16
  9. data/lib/flutter/persistence.rb +51 -5
  10. data/lib/flutter/rspec.rb +7 -7
  11. data/lib/flutter/tracker.rb +48 -5
  12. data/lib/flutter/version.rb +1 -1
  13. metadata +21 -95
  14. data/integration_tests/minitest/grape_app/.gitignore +0 -67
  15. data/integration_tests/minitest/grape_app/.ruby-version +0 -1
  16. data/integration_tests/minitest/grape_app/Gemfile +0 -17
  17. data/integration_tests/minitest/grape_app/Gemfile.lock +0 -89
  18. data/integration_tests/minitest/grape_app/README.md +0 -2
  19. data/integration_tests/minitest/grape_app/Rakefile +0 -9
  20. data/integration_tests/minitest/grape_app/api/api.rb +0 -34
  21. data/integration_tests/minitest/grape_app/api/routes/api_helpers.rb +0 -12
  22. data/integration_tests/minitest/grape_app/api/routes/change_request/api.rb +0 -69
  23. data/integration_tests/minitest/grape_app/api/routes/change_request/response_entity.rb +0 -18
  24. data/integration_tests/minitest/grape_app/api/routes/event/api.rb +0 -121
  25. data/integration_tests/minitest/grape_app/api/routes/event/response_entity.rb +0 -41
  26. data/integration_tests/minitest/grape_app/api/routes/project/api.rb +0 -59
  27. data/integration_tests/minitest/grape_app/api/routes/project/response_entity.rb +0 -13
  28. data/integration_tests/minitest/grape_app/api/routes/property/api.rb +0 -78
  29. data/integration_tests/minitest/grape_app/api/routes/property/response_entity.rb +0 -31
  30. data/integration_tests/minitest/grape_app/api/routes/trackable_object/api.rb +0 -64
  31. data/integration_tests/minitest/grape_app/api/routes/trackable_object/response_entity.rb +0 -24
  32. data/integration_tests/minitest/grape_app/api/routes/tracking_spec/api.rb +0 -88
  33. data/integration_tests/minitest/grape_app/api/routes/tracking_spec/response_entity.rb +0 -17
  34. data/integration_tests/minitest/grape_app/api/routes/tracking_spec/spec_response.rb +0 -19
  35. data/integration_tests/minitest/grape_app/api/routes/user/api.rb +0 -22
  36. data/integration_tests/minitest/grape_app/api/routes/user/response_entity.rb +0 -12
  37. data/integration_tests/minitest/grape_app/app/app.rb +0 -30
  38. data/integration_tests/minitest/grape_app/app/change_request/endpoint.rb +0 -24
  39. data/integration_tests/minitest/grape_app/app/change_request/generate_system_changes.rb +0 -25
  40. data/integration_tests/minitest/grape_app/app/change_request/service.rb +0 -79
  41. data/integration_tests/minitest/grape_app/app/event/endpoint.rb +0 -78
  42. data/integration_tests/minitest/grape_app/app/event/service.rb +0 -68
  43. data/integration_tests/minitest/grape_app/app/event/validator.rb +0 -108
  44. data/integration_tests/minitest/grape_app/app/project/endpoint.rb +0 -24
  45. data/integration_tests/minitest/grape_app/app/project/service.rb +0 -42
  46. data/integration_tests/minitest/grape_app/app/property/endpoint.rb +0 -39
  47. data/integration_tests/minitest/grape_app/app/property/service.rb +0 -56
  48. data/integration_tests/minitest/grape_app/app/trackable_object/endpoint.rb +0 -38
  49. data/integration_tests/minitest/grape_app/app/trackable_object/service.rb +0 -49
  50. data/integration_tests/minitest/grape_app/app/trackable_object/validator.rb +0 -40
  51. data/integration_tests/minitest/grape_app/app/tracking_spec/endpoint.rb +0 -58
  52. data/integration_tests/minitest/grape_app/app/tracking_spec/expand_tracking_spec_events.rb +0 -91
  53. data/integration_tests/minitest/grape_app/app/tracking_spec/service.rb +0 -51
  54. data/integration_tests/minitest/grape_app/app/tracking_spec/validator.rb +0 -61
  55. data/integration_tests/minitest/grape_app/app/utils/errors/api_exceptions.rb +0 -10
  56. data/integration_tests/minitest/grape_app/app/utils/errors/service_exceptions.rb +0 -12
  57. data/integration_tests/minitest/grape_app/app/versioned_entity/entity.rb +0 -110
  58. data/integration_tests/minitest/grape_app/app/versioned_entity/service.rb +0 -47
  59. data/integration_tests/minitest/grape_app/app/versioned_entity/validator.rb +0 -61
  60. data/integration_tests/minitest/grape_app/app/versioned_entity_snapshot/entity.rb +0 -32
  61. data/integration_tests/minitest/grape_app/config/boot.rb +0 -9
  62. data/integration_tests/minitest/grape_app/config.ru +0 -3
  63. data/integration_tests/minitest/grape_app/db/proto/change_request.rb +0 -10
  64. data/integration_tests/minitest/grape_app/db/proto/common/doc.rb +0 -86
  65. data/integration_tests/minitest/grape_app/db/proto/event.rb +0 -10
  66. data/integration_tests/minitest/grape_app/db/proto/event_snapshot.rb +0 -10
  67. data/integration_tests/minitest/grape_app/db/proto/project.rb +0 -21
  68. data/integration_tests/minitest/grape_app/db/proto/property.rb +0 -10
  69. data/integration_tests/minitest/grape_app/db/proto/property_snapshot.rb +0 -10
  70. data/integration_tests/minitest/grape_app/db/proto/trackable_object.rb +0 -10
  71. data/integration_tests/minitest/grape_app/db/proto/trackable_object_snapshot.rb +0 -10
  72. data/integration_tests/minitest/grape_app/db/proto/tracking_spec.rb +0 -10
  73. data/integration_tests/minitest/grape_app/test/api/functional/event_test.rb +0 -186
  74. data/integration_tests/minitest/grape_app/test/api/functional/property_test.rb +0 -197
  75. data/integration_tests/minitest/grape_app/test/api/functional/trackable_object_test.rb +0 -134
  76. data/integration_tests/minitest/grape_app/test/api/functional/tracking_spec_test.rb +0 -56
  77. data/integration_tests/minitest/grape_app/test/api/functional/user_test.rb +0 -24
  78. data/integration_tests/minitest/grape_app/test/api/functional/versioned_entity_helper.rb +0 -157
  79. data/integration_tests/minitest/grape_app/test/fixtures/change_requests.rb +0 -83
  80. data/integration_tests/minitest/grape_app/test/fixtures/event_snapshots.rb +0 -105
  81. data/integration_tests/minitest/grape_app/test/fixtures/events.rb +0 -58
  82. data/integration_tests/minitest/grape_app/test/fixtures/properties.rb +0 -66
  83. data/integration_tests/minitest/grape_app/test/fixtures/property_snapshots.rb +0 -124
  84. data/integration_tests/minitest/grape_app/test/fixtures/sample_tracking_spec.json +0 -125
  85. data/integration_tests/minitest/grape_app/test/fixtures/trackable_objects.rb +0 -58
  86. data/integration_tests/minitest/grape_app/test/fixtures/trackable_objects_snapshots.rb +0 -61
  87. data/integration_tests/minitest/grape_app/test/fixtures/tracking_specs.rb +0 -22
  88. data/integration_tests/minitest/grape_app/test/test_helper.rb +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c941483caa54c2444a2cfb3f264f9cea9cf79dbbeaac3e70803f83929d5ca542
4
- data.tar.gz: 942bf3843ba7de39bd80c32b69f045126cc08a8b1a3a7b7d139d992a2642438a
3
+ metadata.gz: 2ca2f5eb98359bf0cbac8abbdedcbe6ca27ec7bf205625f44cf0c4150313eeb4
4
+ data.tar.gz: 6620320c3c54d63117ddc95550c0d4223411bfe9b6b469336fe9f60ffcddcfe3
5
5
  SHA512:
6
- metadata.gz: a2a7862c51c5ecc07bc8ac6a16f99ab5e324a76a8905eab1382369c3e8f0230b1e062e774ad7934f526351c630ad4428478b66759ca68b9d5037d050df07b521
7
- data.tar.gz: 43ea7ed0cbfcbbc354f1a1d7816d4bae213b73acdf9bb9585eeafbab545396f45d7eeb6adaa9aa8f8c085fcb7717f15c579d2f0a60804aed356df2e244c64ab9
6
+ metadata.gz: 6f97e5720ea6fd890e4c6283a1193750eea3c1ffa64de2de550793cecb14f8636a205774a08811a3f4aea35ed5a764023603fa91a33bd0b4d00496925f4ab35c
7
+ data.tar.gz: 7f890be9219df6f89fde38e9bc34479a65c9a3d8d910ec1495732a2a32ac65d33291c507b8e7b6950facca0d02b6a47c726c864ae2abde9a4416ae983096b29c
data/CHANGELOG.md CHANGED
@@ -1,6 +1,30 @@
1
- ## [Unreleased]
2
- ## [0.1.0.pre.2]
3
- - Initial release
4
- - Minitest integration
5
- - RSpec integration
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## Unreleased
8
+ ## 0.2.0
9
+ ### Added
10
+ - CI Recipe in README
11
+ - CI integration for incremental tests in branches/pull requests
12
+ - Release workflow tasks & github actions integration
13
+
14
+ ### Fixed
15
+ - Ensure all methods (including inherited ones) are considered when calculating signatures for a class or module.
16
+ - Fix calculation of total / filtered examples for rspec integration
17
+
18
+ ## 0.1.0.pre.3
19
+ ### Added
20
+ - Improved documentation for Persistence classes
21
+ - Improved documentation for Tracker
22
+
23
+ ### Changed
24
+ - Pinned dependencies to known working minimum versions
25
+
26
+ ## 0.1.0.pre.2
27
+ ### Added
28
+ - Minitest integration
29
+ - RSpec integration
6
30
 
data/Gemfile CHANGED
@@ -11,6 +11,7 @@ group :test, :development do
11
11
  gem "overcommit", "~> 0.59.1"
12
12
  gem "gem-release", "~> 2.2"
13
13
  gem "guard", "~> 2.18"
14
+ gem "keepachangelog", "~> 0.6.1"
14
15
  gem "rubocop", "~> 1.21"
15
16
  gem "rubocop-shopify", require: false
16
17
  gem "rubocop-minitest", "~> 0.22.1"
data/README.md CHANGED
@@ -107,7 +107,55 @@ guard :rspec, cmd: "rspec" do
107
107
  end
108
108
  ```
109
109
  ## Configuring flutter in continuous integration
110
- **TODO**
110
+
111
+ Flutter can be used in continuous integration environments to speed up the turn
112
+ around time from running tests by only running tests affected by the changes
113
+ in a pull request.
114
+
115
+ ### Github Actions
116
+ The following example workflow with github actions does the following:
117
+ - Always run all tests on the `main` branch
118
+ - Only run tests affected by the "current" commit for CI workflows triggered by a `push` event on other branches
119
+ - If the CI workflow is triggered due to a `pull_request` event, run all tests affected by all commits in the branch
120
+ (by comparing against the branch point of the pull request)
121
+
122
+ ```yaml
123
+ # Get the commit where this branch diverges from origin/main
124
+ - name: Retrieve branch point
125
+ if: github.event_name == 'pull_request'
126
+ run: |
127
+ echo "::set-output name=KEY::$(diff -u <(git rev-list --first-parent origin/main) <(git rev-list --first-parent HEAD) | sed -ne 's/^ //p' | head -1)"
128
+ id: cache_keys
129
+ # Use the always-upload-cache action to:
130
+ # - Restore the flutter state from cache from either the branch point (if it was set in the previous step)
131
+ # or the last run in the current branch
132
+ # - After the run cache the flutter state using the current commit hash as the hash key
133
+ - name: Setup flutter state
134
+ id: flutter-state
135
+ uses: pat-s/always-upload-cache@v2.1.5
136
+ env:
137
+ cache-name: cache-flutter-state
138
+ with:
139
+ path: .flutter
140
+ key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.ruby-version }}-${{ github.sha }}
141
+ restore-keys: |
142
+ ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.ruby-version }}-${{ steps.cache_keys.outputs.KEY }}
143
+ # If this is a push event on the main branch, clear the flutter state
144
+ # so that all tests are run and a full state is cached on the main branch
145
+ - name: Clear flutter state
146
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads/main')
147
+ run: rm -rf .flutter
148
+ ```
149
+ > **Note**
150
+ > The exact CI configuration would ofcourse depend on your workflow and confidence in selectively
151
+ > running tests for pull requests.
152
+
153
+ > **Warning**
154
+ > Selectively running tests in a pull request would show a drop in coverage if you are collecting
155
+ > and/or using code coverage as a "Check". One way to make Flutter work hand in hand with code
156
+ > coverage checks is to only validate that the diff in the pull request has a 100% coverage. For
157
+ > example with [codecov](https://docs.codecov.com/docs/commit-status#section-project-status) this can be
158
+ > achieved by only enabling the `project` status for the main branch and `patch` status otherwise.
111
159
 
112
160
  ## Related work
113
161
 
@@ -119,7 +167,19 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
119
167
 
120
168
  This project uses [overcommit](https://github.com/sds/overcommit) to enforce standards. Enable the precommit hooks in your local checkout by running: `overcommit --sign`
121
169
 
122
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
170
+ To install this gem onto your local machine, run `bundle exec rake install`.
171
+
172
+ ### Releasing a new version
173
+ - Ensure that the [Unreleased](./CHANGELOG.md#Unreleased) section of the changelog is up to date
174
+ and contains useful details.
175
+ - Create a new release using the `release` rake task as follows (for more details about specifying the version change
176
+ run `gem bump --help` which is the command used by the task):
177
+ - Patch release `bundle exec rake release["-v patch"]`
178
+ - Minor release `bundle exec rake release["-v minor"]`
179
+ - Major release `bundle exec rake release["-v major"]`
180
+ > **Note**
181
+ > The `release` rake task automates updating the changelog & version, committing the changes & creating a new tag
182
+ - Push the tag. The CI workflow for tag pushes will take care of publishing the gem & creating a github release.
123
183
 
124
184
  ## Contributing
125
185
 
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
3
  require "rake/testtask"
4
+ require "keepachangelog"
5
5
 
6
6
  Rake::TestTask.new(:unit) do |t|
7
7
  t.libs << "test"
@@ -22,3 +22,37 @@ RSpec::Core::RakeTask.new(:spec)
22
22
 
23
23
  task test: [:unit, :spec]
24
24
  task default: [:test, :spec, "rubocop:autocorrect_all"]
25
+
26
+ desc "Increment the version, update changelog and create a tag for the release"
27
+ task :release, [:version] do |_t, args|
28
+ parser = Keepachangelog::MarkdownParser.load("CHANGELOG.md")
29
+ log = parser.parsed_content["versions"].delete("Unreleased")
30
+ sh("gem bump --pretend #{args[:version]}") do |ok, _|
31
+ if ok
32
+ new_version = %x(gem bump --no-commit #{args[:version]} | awk '{print $4}' | uniq).chomp
33
+ parser.parsed_content["versions"]["Unreleased"] = { "url" => nil, "date" => nil, "changes" => {} }
34
+ parser.parsed_content["versions"][new_version] = log
35
+ File.open("CHANGELOG.md", "w") do |file|
36
+ file.write(parser.to_md)
37
+ end
38
+ %x(git add CHANGELOG.md lib/flutter/version.rb)
39
+ %x(git commit -m "Bump flutter to #{new_version}")
40
+ %x(gem tag -s)
41
+ Rake::Task["release_notes"].execute({ version: new_version })
42
+ end
43
+ end
44
+ end
45
+
46
+ desc "Get release notes for the current or specific version"
47
+ task :release_notes, [:version] do |_t, args|
48
+ version = (args[:version] || Flutter::VERSION)
49
+ parser = Keepachangelog::MarkdownParser.load("CHANGELOG.md")
50
+ parser.parsed_content.delete("intro")
51
+ parser.parsed_content.delete("title")
52
+ parser.parsed_content["versions"] = parser.parsed_content["versions"].select { |k, _v| k == version }
53
+ lines = parser.to_md.split("\n")
54
+ chunk = ["## #{version}"] + (
55
+ lines.slice_after { |line| line.include?("## #{version}") }.to_a[1] || []
56
+ )
57
+ puts chunk.join("\n")
58
+ end
data/codecov.yml ADDED
@@ -0,0 +1,13 @@
1
+ coverage:
2
+ status:
3
+ project:
4
+ default:
5
+ paths:
6
+ - "lib"
7
+ branches:
8
+ - main
9
+ patch:
10
+ default:
11
+ paths:
12
+ - "lib"
13
+ informational: false
@@ -10,7 +10,7 @@ end
10
10
  module Flutter
11
11
  module Minitest
12
12
  class << self
13
- attr_accessor :filtered
13
+ attr_accessor :filtered, :total
14
14
 
15
15
  def flutter_tracker
16
16
  @tracker ||= Flutter::Tracker.new(
@@ -31,7 +31,7 @@ module Flutter
31
31
  return unless ::Flutter.enabled
32
32
 
33
33
  Flutter::Minitest.flutter_tracker.persist!
34
- $stdout.puts "Flutter filtered out #{Flutter::Minitest.filtered} tests"
34
+ $stdout.puts "Flutter filtered #{Flutter::Minitest.filtered} / #{Flutter::Minitest.total} tests"
35
35
  if @verbose
36
36
  $stdout.puts "Persisted flutter #{Flutter::Minitest.flutter_tracker}"
37
37
  end
@@ -43,17 +43,15 @@ module Flutter
43
43
  module ClassMethods
44
44
  def runnable_methods
45
45
  Flutter::Minitest.filtered ||= 0
46
+ Flutter::Minitest.total ||= 0
46
47
  default = super()
48
+ Flutter::Minitest.total += default.length
47
49
  default.select do |test|
48
- skip = Minitest.flutter_tracker.skip?(
50
+ !Minitest.flutter_tracker.skip?(
49
51
  "#{name}##{test}",
50
52
  File.absolute_path(instance_method(test).source_location[0]),
51
53
  instance_method(test).source,
52
- )
53
- if skip
54
- Flutter::Minitest.filtered += 1
55
- end
56
- !skip
54
+ ).tap { |skip| Flutter::Minitest.filtered += 1 if skip }
57
55
  end
58
56
  end
59
57
  end
@@ -8,6 +8,11 @@ module Flutter
8
8
  class Parser
9
9
  attr_reader :signatures
10
10
 
11
+ @method_cache = {}
12
+ class << self
13
+ attr_reader :method_cache
14
+ end
15
+
11
16
  def initialize(file)
12
17
  @signatures = {}
13
18
  @targets = Set.new
@@ -49,23 +54,17 @@ module Flutter
49
54
  require_relative @file
50
55
  @targets.each do |container|
51
56
  instance = Kernel.const_get(container)
52
- class_methods = (
53
- instance.methods - Object.methods
54
- ) + (
55
- instance.private_methods - Object.private_methods
56
- )
57
- instance_methods = (
58
- instance.instance_methods - Object.instance_methods
59
- ) + (
60
- instance.private_instance_methods - Object.private_instance_methods
61
- )
57
+ class_methods = instance.methods + instance.private_methods
58
+ instance_methods = instance.instance_methods + instance.private_instance_methods
62
59
 
63
60
  @signatures.merge!(class_methods.map do |method|
64
- ["#{container}::#{method}", source_hash(instance.method(method))]
65
- end.to_h)
61
+ hash = source_hash(instance.method(method))
62
+ ["#{container}::#{method}", hash] if hash
63
+ end.compact.to_h)
66
64
  @signatures.merge!(instance_methods.map do |method|
67
- ["#{container}:#{method}", source_hash(instance.instance_method(method))]
68
- end.to_h)
65
+ hash = source_hash(instance.instance_method(method))
66
+ ["#{container}:#{method}", hash] if hash
67
+ end.compact.to_h)
69
68
  rescue NameError
70
69
  $stderr.puts "failed to load #{container} in #{@file}"
71
70
  end
@@ -74,9 +73,12 @@ module Flutter
74
73
  end
75
74
 
76
75
  def source_hash(callable)
77
- Digest::SHA1.hexdigest(callable.source)
76
+ return unless callable.source_location
77
+
78
+ sep = callable.is_a?(UnboundMethod) ? ":" : "::"
79
+ Parser.method_cache["#{callable.owner}#{sep}#{callable.name}"] ||= Digest::SHA1.hexdigest(callable.source)
78
80
  rescue MethodSource::SourceNotFoundError
79
- "<no-source>"
81
+ nil
80
82
  end
81
83
  end
82
84
  end
@@ -4,40 +4,68 @@ require "fileutils"
4
4
  require "set"
5
5
  module Flutter
6
6
  module Persistence
7
+ # The abstract base storage.
8
+ #
9
+ # To implement a custom storage, override the following methods:
10
+ # * {#test_mapping}
11
+ # * {#source_mapping}
12
+ # * {#update_test_mapping!}
13
+ # * {#update_source_mapping!}
14
+ # * {#load!}
15
+ # * {#persist!}
16
+ # * {#clear!}
17
+ #
18
+ # @abstract Override this class to implement a custom storage
7
19
  class AbstractStorage
8
20
  def initialize
9
21
  load!
10
22
  end
11
23
 
12
24
  # :nocov:
25
+
26
+ # Mapping of +test_id -> source file -> callable_id+
27
+ # @return [Hash<String, Hash<String, Set<String>>>]
13
28
  def test_mapping
14
29
  raise NotImplementedError
15
30
  end
16
31
 
32
+ # Mapping of +source file -> callable_id -> signature+
33
+ # @return [Hash<String, Hash<String, String>>] mapping
17
34
  def source_mapping
18
35
  raise NotImplementedError
19
36
  end
20
37
 
38
+ ##
39
+ # Update {#test_mapping}
40
+ #
41
+ # @param [Hash<String, Hash<String, String>>] mapping
21
42
  def update_test_mapping!(mapping)
22
43
  raise NotImplementedError
23
44
  end
24
45
 
46
+ ##
47
+ # Update {#source_mapping}
48
+ #
49
+ # @param [Hash<String, Hash<String, String>>] mapping
25
50
  def update_source_mapping!(mapping)
26
51
  raise NotImplementedError
27
52
  end
28
53
 
29
- def persist!(updates)
30
- raise NotImplementedError
31
- end
32
-
33
- def to_s
54
+ # Save the state of test & source mapping to the underlying
55
+ # storage
56
+ # @return [void]
57
+ def persist!
34
58
  raise NotImplementedError
35
59
  end
36
60
 
61
+ # Clear any saved state in the underlying storage
62
+ # @return [void]
37
63
  def clear!
38
64
  raise NotImplementedError
39
65
  end
40
66
 
67
+ # If the storage was already persisted load the current state
68
+ # @return [void]
41
69
  def load!
42
70
  raise NotImplementedError
43
71
  end
@@ -48,6 +76,8 @@ module Flutter
48
76
  require "yaml"
49
77
  # ruby >= 3.1 requires this
50
78
  YAML_LOAD_OPTS = RUBY_VERSION > "3.1" ? { permitted_classes: [Hash, Set, Symbol] } : {}
79
+
80
+ # @param [String] path The directory to store the +state.yml+ file
51
81
  def initialize(path:)
52
82
  @path = File.absolute_path(path)
53
83
  @full_path = File.join(@path, "state.yml")
@@ -55,6 +85,7 @@ module Flutter
55
85
  super()
56
86
  end
57
87
 
88
+ # (see AbstractStorage#load!)
58
89
  def load!
59
90
  if File.exist?(@full_path)
60
91
  persisted = YAML.load(File.read(@full_path), **YAML_LOAD_OPTS)
@@ -62,27 +93,33 @@ module Flutter
62
93
  end
63
94
  end
64
95
 
96
+ # (see AbstractStorage#test_mapping)
65
97
  def test_mapping
66
98
  @state.fetch(:test_mapping) { @state[:test_mapping] = {} }
67
99
  end
68
100
 
101
+ # (see AbstractStorage#source_mapping)
69
102
  def source_mapping
70
103
  @state.fetch(:source_mapping) { @state[:source_mapping] = {} }
71
104
  end
72
105
 
106
+ # (see AbstractStorage#update_test_mapping!)
73
107
  def update_test_mapping!(mapping)
74
108
  test_mapping.merge!(mapping)
75
109
  end
76
110
 
111
+ # (see AbstractStorage#update_source_mapping!)
77
112
  def update_source_mapping!(mapping)
78
113
  source_mapping.merge!(mapping)
79
114
  end
80
115
 
116
+ # (see AbstractStorage#clear!)
81
117
  def clear!
82
118
  FileUtils.rm(@full_path) if File.exist?(@full_path)
83
119
  @state.clear
84
120
  end
85
121
 
122
+ # (see AbstractStorage#persist!)
86
123
  def persist!
87
124
  FileUtils.mkdir_p(@path) unless File.exist?(@path)
88
125
  File.open(@full_path, "w") { |file| file.write(@state.to_yaml) }
@@ -95,6 +132,7 @@ module Flutter
95
132
 
96
133
  class Marshal < AbstractStorage
97
134
  require "pstore"
135
+ # @param [String] path The directory to store the marshaled state file +state.pstore+
98
136
  def initialize(path:)
99
137
  @path = File.absolute_path(path)
100
138
  FileUtils.mkdir_p(@path) unless File.exist?(@path)
@@ -103,6 +141,7 @@ module Flutter
103
141
  super()
104
142
  end
105
143
 
144
+ # (see AbstractStorage#load!)
106
145
  def load!
107
146
  @state = PStore.new(@full_path)
108
147
  @state.transaction do
@@ -111,18 +150,21 @@ module Flutter
111
150
  end
112
151
  end
113
152
 
153
+ # (see AbstractStorage#test_mapping)
114
154
  def test_mapping
115
155
  @state.transaction do
116
156
  return @state[:test_mapping]
117
157
  end
118
158
  end
119
159
 
160
+ # (see AbstractStorage#source_mapping)
120
161
  def source_mapping
121
162
  @state.transaction do
122
163
  return @state[:source_mapping]
123
164
  end
124
165
  end
125
166
 
167
+ # (see AbstractStorage#update_test_mapping!)
126
168
  def update_test_mapping!(mapping)
127
169
  @state.transaction do
128
170
  @state[:test_mapping] ||= {}
@@ -130,6 +172,7 @@ module Flutter
130
172
  end
131
173
  end
132
174
 
175
+ # (see AbstractStorage#update_source_mapping!)
133
176
  def update_source_mapping!(mapping)
134
177
  @state.transaction do
135
178
  @state[:source_mapping] ||= {}
@@ -137,13 +180,16 @@ module Flutter
137
180
  end
138
181
  end
139
182
 
183
+ # (see AbstractStorage#clear!)
140
184
  def clear!
141
185
  FileUtils.rm(@full_path) if File.exist?(@full_path)
142
186
  end
143
187
 
188
+ # (see AbstractStorage#persist!)
144
189
  def persist!
145
190
  end
146
191
 
192
+ # @return [String]
147
193
  def to_s
148
194
  "state: #{@full_path}"
149
195
  end
data/lib/flutter/rspec.rb CHANGED
@@ -5,7 +5,7 @@ require_relative "tracker"
5
5
  module Flutter
6
6
  module RSpec
7
7
  class << self
8
- attr_accessor :filtered
8
+ attr_accessor :filtered, :total
9
9
 
10
10
  def tracker
11
11
  @tracker ||= Flutter::Tracker.new(
@@ -17,20 +17,20 @@ module Flutter
17
17
 
18
18
  module ClassMethods
19
19
  def filtered_examples
20
- Flutter::RSpec.filtered ||= 0
20
+ Flutter::RSpec.filtered ||= Set.new
21
+ Flutter::RSpec.total ||= Set.new
21
22
  Flutter::RSpec.tracker.reset! if Flutter.enabled && Flutter.config.reset_storage
22
23
 
23
24
  original = super
25
+ Flutter::RSpec.total.merge(original)
24
26
  return original unless Flutter.enabled
25
27
 
26
28
  original.select do |example|
27
- skip = example.metadata[:block] && Flutter::RSpec.tracker.skip?(
29
+ !(example.metadata[:block] && Flutter::RSpec.tracker.skip?(
28
30
  example.full_description,
29
31
  example.metadata[:absolute_file_path],
30
32
  example.metadata[:block].source,
31
- )
32
- Flutter::RSpec.filtered += 1 if skip
33
- !skip
33
+ ).tap { |skip| Flutter::RSpec.filtered << example if skip })
34
34
  end
35
35
  end
36
36
  end
@@ -58,7 +58,7 @@ if defined?(RSpec.configure)
58
58
  config.after(:suite) do
59
59
  if Flutter.enabled
60
60
  $stdout.puts
61
- $stdout.puts "Flutter filtered out #{Flutter::RSpec.filtered} examples"
61
+ $stdout.puts "Flutter filtered #{Flutter::RSpec.filtered.length} / #{Flutter::RSpec.total.length} examples"
62
62
  Flutter::RSpec.tracker.persist!
63
63
  end
64
64
  end
@@ -7,9 +7,20 @@ require_relative "parser"
7
7
  require "pry"
8
8
 
9
9
  module Flutter
10
+ # @attr [Hash<String, Hash<String, Set<String>>>] test_mapping Mapping of tests to
11
+ # files -> callable_ids
12
+ # @attr [Hash<String, Hash<String, String>>] source_mapping Mapping of
13
+ # source files -> callable_id -> signature
14
+ # @attr [Persistence::AbstractStorage] storage the storage instance used for
15
+ # persisting the state of the tracker
10
16
  class Tracker
11
- attr_reader :test_mapping, :source_mapping
17
+ attr_reader :test_mapping, :source_mapping, :storage
12
18
 
19
+ # @param [Array<String>] sources
20
+ # @param [Array<String>] exclusions
21
+ # @param [Class<Flutter::Persistence::AbstractStorage>] storage_class
22
+ # @param [Hash] storage_options Additionally options that should be passed
23
+ # to the +storage_class+ constructor
13
24
  def initialize(sources, exclusions, storage_class, storage_options)
14
25
  @sources = sources.map { |s| File.absolute_path(s) }
15
26
  @exclusions = exclusions.map { |s| File.absolute_path(s) }
@@ -23,8 +34,8 @@ module Flutter
23
34
  @tracked_files = {}
24
35
  end
25
36
 
26
- # Resets the in-memory test_mapping for each test, and stores the methods that
27
- # the test calls in the in-memory test_mapping
37
+ # Starts tracking calls made by +test+
38
+ # @param [String] test A unique identifier for the test
28
39
  def start(test)
29
40
  # Delete test from the in-memory mapping to allow each new test run
30
41
  # to store all the functions that the test calls into
@@ -35,10 +46,24 @@ module Flutter
35
46
  @current_tracepoint&.enable
36
47
  end
37
48
 
49
+ # End tracking (should be called after a call to {#start})
38
50
  def stop
39
51
  @current_tracepoint&.disable
40
52
  end
41
53
 
54
+ ##
55
+ # Decides if a test should be skipped based on *all* of the following
56
+ # criteria being met:
57
+ #
58
+ # 1. Test was seen before
59
+ # 2. Test sources have not changed since the last time it was executed
60
+ # 3. All the callables triggered within the scope of this test have no
61
+ # changes in their source since the last time this test was executed
62
+ #
63
+ # @param [String] test A unique identifier for the test
64
+ # @param [String] test_location The absolute path to the source file containing the test
65
+ # @param [String] test_source The source code of the test itself
66
+ # @return [TrueClass, FalseClass] If the test should be skipped
42
67
  def skip?(test, test_location, test_source)
43
68
  test_location_rel = relative_path(test_location)
44
69
  @test_source_mapping.fetch(test_location_rel) do
@@ -58,13 +83,22 @@ module Flutter
58
83
  end.all?
59
84
  end
60
85
 
86
+ ##
87
+ # Persist the state of the tracker to the storage
88
+ # specified by {#storage}
89
+ # @return [void]
61
90
  def persist!
62
91
  @storage.update_test_mapping!(@test_mapping)
63
92
  @storage.update_source_mapping!(generate_source_mapping)
64
93
  @storage.persist!
65
94
  end
66
95
 
96
+ ##
97
+ # Reset the state of the tracker and the storage
98
+ # that it was configured with
99
+ # @return [void]
67
100
  def reset!
101
+ $stdout.puts "Resetting flutter: #{@storage}"
68
102
  @storage.clear!
69
103
  @source_mapping.clear
70
104
  @test_mapping.clear
@@ -74,8 +108,6 @@ module Flutter
74
108
  @storage.to_s
75
109
  end
76
110
 
77
- attr_reader :mapping
78
-
79
111
  private
80
112
 
81
113
  def hit!(test, tracepoint)
@@ -116,6 +148,13 @@ module Flutter
116
148
  end
117
149
  end
118
150
 
151
+ ##
152
+ # Check if a file pair should be tracked or not based on the
153
+ # +sources+ and +exclusions+ lists provided when initializing
154
+ # the instance
155
+ #
156
+ # @param [String] file
157
+ # @param [Symbol] _method
119
158
  def tracked?(file, _method)
120
159
  @tracked_files.fetch(file) do
121
160
  @sources.any?(->(source) { File.fnmatch?(source, file) }) && !@exclusions.any?(->(exclusion) {
@@ -124,6 +163,10 @@ module Flutter
124
163
  end
125
164
  end
126
165
 
166
+ ##
167
+ # Generates a mapping of
168
+ #
169
+ # @return [Hash<String, Hash<String, Hash<String, String>>>]
127
170
  def generate_source_mapping
128
171
  @test_mapping.map { |_k, v| v.keys }.flatten.uniq.map do |file|
129
172
  [file, @current_source_mapping.fetch(file) { Flutter::Parser.new(file).signatures }]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Flutter
4
- VERSION = "0.1.0.pre.2"
4
+ VERSION = "0.2.0"
5
5
  end