flutter 0.1.0.pre.2 → 0.2.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 (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